Lambda 表达式
Lambda 表达式是 C++11 引入的一种强大的功能,它允许你在代码中直接定义匿名函数对象。Lambda 表达式可以捕获上下文中的变量,并在需要时使用它们。它们通常用于简化代码,尤其是那些需要传递函数对象作为参数的场景(如标准库中的算法函数)。
1. Lambda 表达式的语法
Lambda 表达式的基本语法如下:
[capture](parameters) -> return_type { body }
2. Lambda 表达式的用法
2.1 无捕获、无参数的 Lambda
最简单的 Lambda 表达式不捕获任何变量,也不接受任何参数。
auto lambda = []() {std::cout << "Hello, Lambda!" << std::endl;
};lambda(); // 输出:Hello, Lambda!
2.2 带参数的 Lambda
Lambda 表达式可以接受参数,就像普通函数一样。
auto add = [](int a, int b) {return a + b; };std::cout << add(3, 4) << std::endl; // 输出:7
2.3 捕获变量的 Lambda
Lambda 表达式可以通过捕获列表捕获上下文中的变量。捕获方式有以下几种:
值捕获:
[x]
,捕获变量x
的副本。引用捕获:
[&x]
,捕获变量x
的引用。捕获所有变量:
[=]
,捕获所有变量的副本;[&]
,捕获所有变量的引用。
int x = 10;
auto lambda = [x]() {std::cout << "x = " << x << std::endl; // 使用捕获的变量 x
};lambda(); // 输出:x = 10
如果捕获变量后修改了变量的值,捕获方式会影响 Lambda 表达式的行为:
int x = 10;
auto lambda1 = [x]() {x = 20; // 错误:不能修改捕获的变量副本
};auto lambda2 = [&x]() {x = 20; // 正确:修改捕获的变量引用
};lambda2();
std::cout << x << std::endl; // 输出:20
2.4 Lambda 作为函数参数
Lambda 表达式常用于作为函数参数,尤其是标准库中的算法函数。
#include <vector>
#include <algorithm>
#include <iostream>int main() {std::vector<int> vec = {1, 2, 3, 4, 5};// 使用 Lambda 表达式作为参数std::for_each(vec.begin(), vec.end(), [](int x) {std::cout << x << " ";});std::cout << std::endl;// 使用 Lambda 表达式作为谓词auto isEven = [](int x) {return x % 2 == 0;};std::vector<int> evenVec;std::copy_if(vec.begin(), vec.end(), std::back_inserter(evenVec), isEven);for (int x : evenVec) {std::cout << x << " ";}std::cout << std::endl;return 0;
}
2.5 Lambda 作为成员函数
Lambda 表达式也可以绑定到对象上,从而实现类似成员函数的行为。
class MyClass {
public:int value;MyClass(int v) : value(v) {}void print() const {std::cout << "Value = " << value << std::endl;}
};int main() {MyClass obj(42);// 使用 Lambda 表达式绑定成员函数auto printLambda = [&obj]() {obj.print();};printLambda(); // 输出:Value = 42return 0;
}
3. Lambda 表达式的优点
简洁性:Lambda 表达式允许你直接在需要的地方定义函数对象,避免了定义单独的函数或函数对象类。
灵活性:Lambda 表达式可以捕获上下文中的变量,使其在函数体中可用。
性能:Lambda 表达式通常比
std::bind
更高效,因为它们没有额外的类型擦除开销。可读性:Lambda 表达式通常比函数指针或函数对象类更易读。
过度使用:虽然 Lambda 表达式非常强大,但过度使用可能会使代码难以理解。
捕获复杂性:捕获变量时,需要特别注意捕获方式(值捕获或引用捕获)对变量生命周期的影响。
C++14 引入了 Lambda 捕获初始化的功能,允许你在捕获列表中初始化变量。
#include <iostream>int main() {auto lambda = [x = 10, y = 20]() mutable {std::cout << "x = " << x << ", y = " << y << std::endl;x = 30; // 修改捕获的变量};lambda(); // 输出:x = 10, y = 20lambda(); // 输出:x = 30, y = 20
}
[capture]
:捕获列表,用于捕获上下文中的变量。捕获方式可以是值捕获([x]
)或引用捕获([&x]
),也可以捕获所有变量([=]
或[&]
)。(parameters)
:参数列表,与普通函数的参数列表类似。-> return_type
:返回类型(可选)。如果 Lambda 表达式的返回类型可以自动推导,则可以省略。{ body }
:函数体,可以包含任意合法的 C++ 代码。
C++ 右值引用与移动语义详解
右值引用和移动语义是 C++11 引入的最重要特性之一,它们极大地提升了 C++ 程序的性能,特别是在处理临时对象和资源管理方面。
1. 左值 vs 右值
左值 (lvalue)
- 可以取地址的表达式
- 有持久的状态(在内存中有固定位置)
- 通常有名字
- 示例:
int a = 10; // a 是左值
int* p = &a; // 可以取地址
右值 (rvalue)
- 不能取地址的临时表达式
- 通常是即将销毁的临时对象
- 没有名字
- 示例:
10; // 字面量是右值a + b; // 表达式结果是右值std::string("hello"); // 临时对象是右值
总结一句话就是能取地址是左值不能取地址是右值
2. 右值引用 (Rvalue Reference)
基本概念
- 使用 `&&` 语法声明
- 只能绑定到右值
- 主要用途:标识可以被"移动"的资源
int&& rref = 10; // 右值引用
std::string&& sref = std::string("hello");
重要特性
1. **延长临时对象生命周期**:右值引用绑定的临时对象生命周期会延长到引用作用域结束
2. **重载决议**:可以重载函数来区分左值和右值参数
3. 移动语义 (Move Semantics)
核心思想
- 允许"窃取"右值对象的资源(如动态内存),而非深拷贝
- 避免不必要的资源分配和释放
- 显著提升性能,特别是对于管理资源的类(如容器、字符串等)
移动构造函数
class MyClass {
public:// 移动构造函数:参数是右值引用MyClass(MyClass&& other) noexcept {// 转移资源所有权(例如指针)// noexcept 是一个用于声明函数不会抛出异常的关键字this->ptr = other.ptr;other.ptr = nullptr; // 确保原对象不再拥有资源}private:int* ptr;
};
- 核心特征:
- 参数类型为
T&&
(右值引用)。 - 通常会 “窃取” 原对象的资源(如指针、文件句柄),并将原对象置为有效但空的状态。
- 参数类型为
4. std::move
作用
- 将左值显式转换为右值引用
- 表示对象可以被移动(资源可以被窃取)
- 位于 `<utility>` 头文件
std::string str = "Hello";
std::string another = std::move(str);
// str 现在处于有效但未指定的状态
注意事项
1. 被移动的对象处于有效但未指定的状态(通常为空/null)
2. 不应再使用被移动对象的值,但可以重新赋值或销毁
3. 不是所有类型都支持移动语义(基本类型移动等同于拷贝)
5. 完美转发 (Perfect Forwarding)
std::forward
- 保持参数原始值类别(左值/右值)
- 用于模板函数中转发参数
template<typename T>
void wrapper(T&& arg) {// 保持 arg 的原始值类别some_function(std::forward<T>(arg));
}
完美转发 (Perfect Forwarding)
在 C++ 中,完美转发(Perfect Forwarding)是一种将参数原封不动地传递给另一个函数的技术,同时保留参数的值类别(左值或右值)和常量性
1. 核心概念:为什么需要完美转发?
假设有一个包装函数 wrapper
,需要将参数 arg
传递给 func
,同时保留 arg
的原始属性(左值 / 右值):
template<typename T>
void func(T&& arg) {// 处理参数 arg
}template<typename T>
void wrapper(T&& arg)//这里是万能引用而非右值引用
{func(arg); // ❌ 错误:无论arg是左值还是右值,传递给func时都会变成左值
}
- 问题:
arg
作为函数参数,本身是一个左值(即使它被声明为右值引用T&&
)。因此,func(arg)
总是调用func
的左值版本。 - 目标:让
wrapper
能够区分传入的参数是左值还是右值,并将这种属性 “完美” 地传递给func
。
2. 完美转发的实现:std::forward
C++11 引入了 std::forward
(位于 <utility>
头文件),用于在转发参数时保留其原始值类别:
template<typename T>
void wrapper(T&& arg) {func(std::forward<T>(arg)); // ✅ 完美转发:保留arg的原始左值/右值属性
}
- 原理:
std::forward<T>(arg)
根据T
的推导类型,将arg
转换为对应的左值引用或右值引用。- 当
T
被推导为左值引用(如int&
)时,std::forward
返回左值引用; - 当
T
被推导为右值引用(如int&&
)时,std::forward
返回右值引用。
3. 万能引用(Universal Reference)与引用折叠
(1)万能引用(Universal Reference)
T&&
在两种情况下有不同含义:
- 右值引用:明确指定类型时(如
int&&
)。 - 万能引用:用于模板类型推导时(如
template<typename T> void f(T&&)
)。
例如:
template<typename T>
void f(T&& arg); // 这里的 T&& 是万能引用,可接受左值或右值void g(int&& x); // 这里的 int&& 是右值引用,只能接受右值
(2)引用折叠规则
当涉及多重引用时,C++ 遵循以下规则:
T& &
折叠为T&
T& &&
折叠为T&
T&& &
折叠为T&
T&& &&
折叠为T&&
例如:
template<typename T>
void wrapper(T&& arg) {// 当传入左值时,T 被推导为 int&,则 T&& 为 int& &&,折叠为 int&// 当传入右值时,T 被推导为 int, 则 T&& 为 int&&
}
4. 示例代码
(1)基本完美转发
#include <utility>
#include <iostream>void print(int& x) {std::cout << "左值: " << x << std::endl;
}void print(int&& x) {std::cout << "右值: " << x << std::endl;
}template<typename T>
void wrapper(T&& arg) {print(std::forward<T>(arg)); // 完美转发
}int main() {int a = 42;wrapper(a); // 传递左值,调用 print(int&)wrapper(123); // 传递右值,调用 print(int&&)
}
(2)转发多个参数
template<typename F, typename... Args>
decltype(auto) call(F&& f, Args&&... args) {return std::forward<F>(f)(std::forward<Args>(args)...);
}// 使用示例
int add(int a, int b) { return a + b; }int result = call(add, 3, 4); // 等价于 add(3, 4)
5. 常见应用场景
(1)函数包装器
template<typename Func>
class Wrapper {
private:Func func;
public:template<typename F>Wrapper(F&& f) : func(std::forward<F>(f)) {} // 完美转发构造函数template<typename... Args>decltype(auto) operator()(Args&&... args) {return func(std::forward<Args>(args)...); // 完美转发调用}
};
(2)工厂函数
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}// 使用示例
auto ptr = make_unique<MyClass>(arg1, arg2); // 完美转发参数到MyClass构造函数
(3)移动语义与完美转发结合
template<typename T>
void push_back(T&& value) {container.push_back(std::forward<T>(value)); // 保留value的左值/右值属性
}
6. 注意事项
仅对需要转发的参数使用
std::forward
:template<typename T> void f(T&& x) {g(x); // 传递左值g(std::move(x)); // 强制转换为右值(移动语义)g(std::forward<T>(x)); // 完美转发 }
避免在中间变量上使用完美转发:
template<typename T> void wrapper(T&& arg) {auto intermediate = std::forward<T>(arg);// ❌ 错误:intermediate是左值func(intermediate); // 总是传递左值 }
std::forward
需要显式指定模板参数:std::forward<T>(arg); // ✅ 正确 std::forward(arg); // ❌ 错误:必须指定T
可变参数模板(Variadic Templates)
可变参数模板(Variadic Templates)是 C++11 引入的核心特性之一,它允许模板定义接受任意数量、任意类型的参数,极大地增强了模板的灵活性和通用性。标准库中的 std::tuple
、std::make_unique
、std::format
等功能都依赖于可变参数模板实现。
一、核心概念:参数包(Parameter Pack)
可变参数模板的核心是参数包(Parameter Pack),它分为两种:
- 模板参数包(Template Parameter Pack):表示零个或多个模板参数。
- 函数参数包(Function Parameter Pack):表示零个或多个函数参数。
二、基础语法
1. 模板参数包的定义
用 typename... Args
(或 class... Args
)声明模板参数包,其中 Args
是参数包的名称(可自定义):
// 模板参数包:Args 表示零个或多个类型
template<typename... Args>
struct MyStruct {};
2. 函数参数包的定义
用 Args&&... args
声明函数参数包(结合万能引用 &&
可转发参数值类别):
// 函数参数包:args 表示零个或多个函数参数
template<typename... Args>
void my_func(Args&&... args) {}
3. 完整示例:接受任意参数的函数
// 可变参数模板函数:打印任意数量、任意类型的参数
template<typename... Args>
void print(Args&&... args) { // 后续讲解如何“展开”参数包
} // 调用示例:支持任意数量和类型的参数
print(1, "hello", 3.14, std::vector<int>{1,2,3});
三、参数包的展开(Unpacking)
参数包本身是 “打包” 的,无法直接使用,必须通过展开(Unpacking)才能访问其中的单个元素。C++ 提供了多种展开方式,常用的有以下几种:
1. 递归展开(C++11 起)
递归展开是最经典的方式:通过递归函数,每次处理参数包中的第一个元素,再对剩余元素递归调用,直到参数包为空。
// 递归终止条件:处理零个参数(递归出口)
void print_recursive() { std::cout << "递归结束\n";
} // 递归函数:处理第一个参数,剩余参数递归传递
template<typename First, typename... Rest>
void print_recursive(First&& first, Rest&&... rest) { std::cout << "参数:" << first << "(剩余" << sizeof...(rest) << "个)\n"; print_recursive(std::forward<Rest>(rest)...); // 递归展开剩余参数
} // 调用示例
print_recursive(1, "hello", 3.14);
输出:
参数:1(剩余2个)
参数:hello(剩余1个)
参数:3.14(剩余0个)
递归结束
2. 折叠表达式(Fold Expressions,C++17 起)
C++17 引入了折叠表达式,可通过运算符对参数包进行 “批量运算”,无需递归,语法更简洁。
折叠表达式的语法为:(包操作 ... 运算符)
或 (运算符 ... 包操作)
,支持 +
、*
、&&
、||
、,
等运算符。
// 示例1:用折叠表达式求和(支持任意数量的数值类型参数)
template<typename... Args>
auto sum(Args&&... args) { return (args + ...); // 折叠表达式:args1 + args2 + ... + argsN
} // 示例2:用折叠表达式打印参数(借助逗号运算符)
template<typename... Args>
void print_fold(Args&&... args) {
// 逗号运算符:先执行打印,再返回0;折叠后等效于 (print(args1), print(args2), ..., 0) (std::cout << ... << args) << "\n";
} // 调用示例
int total = sum(1, 2, 3, 4.5); // 1+2+3+4.5=10.5
print_fold("sum = ", total); // 输出:sum = 10.5
3. 结合 std::tuple
和 std::apply
(C++17 起)
通过 std::tuple
存储参数包,再用 std::apply
展开调用函数:
#include <tuple>
#include <functional>// 目标函数:接受3个参数
void func(int a, double b, const std::string& c) { std::cout << a << ", " << b << ", " << c << "\n";
} // 可变参数模板:将参数打包为tuple,再调用apply展开
template<typename F, typename... Args>
void call_with_tuple(F&& f, Args&&... args) { auto tuple_args = std::make_tuple(std::forward<Args>(args)...); // 打包参数 std::apply(std::forward<F>(f), tuple_args); // 展开tuple并调用函数
} // 调用示例
call_with_tuple(func, 10, 3.14, "hello"); // 输出:10, 3.14, hello
四、与万能引用和完美转发结合
可变参数模板常与万能引用(&&
)和 std::forward
配合,实现完美转发(Preserve Value Category),即保持参数的原始值类别(左值 / 右值)。
示例:完美转发任意参数
// 可变参数模板:转发参数给目标函数func
template<typename... Args>
void wrapper(Args&&... args) { func(std::forward<Args>(args)...); // 展开参数包并完美转发
} // 目标函数:重载左值和右值版本
void func(int& x) { std::cout << "左值引用:" << x << "\n"; }
void func(int&& x) { std::cout << "右值引用:" << x << "\n"; } // 调用示例
int a = 10;
wrapper(a); // 传递左值:调用func(int&)
wrapper(20); // 传递右值:调用func(int&&)
wrapper(std::move(a)); // 传递右值:调用func(int&&)
五、模板参数包的推导规则
当调用可变参数模板时,编译器会自动推导参数包的类型:
- 若传递左值(如
int a;
),推导为左值引用类型(如int&
); - 若传递右值(如字面量
10
或std::move(a)
),推导为非引用类型(如int
)。
结合万能引用 &&
后,参数包会保持原始值类别的信息,配合 std::forward
即可完美转发。
六、应用场景
- 通用函数封装:如
std::make_unique
、std::thread
的构造函数,接受任意参数转发给对象构造函数。 - 容器 / 数据结构:
std::tuple
存储任意数量、任意类型的元素;std::variant
支持多类型备选。 - 格式化与打印:
std::format
接受任意数量的格式化参数;日志库的打印函数。 - 元编程:在编译期遍历类型列表、计算参数数量等(结合
sizeof...(Args)
获取参数个数)。
类型别名 (using 替代 typedef)
在 C++ 中,类型别名(Type Alias)是为现有类型定义新名称的机制,用于提高代码可读性和可维护性。C++11 引入的 using
语法是传统 typedef
的现代替代品,功能更强大,语法更直观。
1. 基本语法对比
(1)传统 typedef
typedef int MyInt; // 为int定义别名MyInt
typedef void (*FuncPtr)(int); // 为函数指针定义别名
typedef std::vector<int> IntVec; // 为vector<int>定义别名
(2)现代 using
语法
using MyInt = int; // 等价于typedef
using FuncPtr = void (*)(int); // 函数指针别名
using IntVec = std::vector<int>; // 容器别名
关键区别:using
的语法更接近赋值语句,直观易读;而 typedef
的语法更像声明变量。
2. using
的优势
(1)模板别名(Template Aliases)
using
支持定义模板别名(Template Aliases),而 typedef
无法直接实现:
// 传统typedef:无法直接定义模板别名
template<typename T>
struct VecWrapper {typedef std::vector<T> type; // 需通过嵌套类型定义
};
VecWrapper<int>::type vec; // 使用时需写::type// 现代using:直接定义模板别名
template<typename T>
using VecAlias = std::vector<T>; // 直接定义别名
VecAlias<int> vec; // 使用更简洁
(2)函数对象和 lambda 类型
using
更适合处理复杂的类型,如函数对象和 lambda:
// 函数对象类型别名
using Compare = std::function<bool(int, int)>;// lambda类型别名(需用decltype)
auto lambda = [](int x) { return x * 2; };
using LambdaType = decltype(lambda);
(3)可读性更强
对于复杂类型(如嵌套模板、函数指针),using
的语法更清晰:
// 函数指针示例
typedef void (*Callback)(int, char); // 传统typedef
using Callback = void (*)(int, char); // using更直观// 嵌套模板示例
typedef std::map<std::string, std::vector<int>> MapType; // 传统
using MapType = std::map<std::string, std::vector<int>>; // 现代
3. 别名模板(Alias Templates)
using
允许创建参数化的类型别名,称为别名模板:
// 为有特定分配器的vector定义别名模板
template<typename T>
using MyVector = std::vector<T, MyAllocator<T>>;// 使用
MyVector<int> vec; // 等价于std::vector<int, MyAllocator<int>>
对比:typedef
无法直接实现参数化的类型别名,需借助模板和嵌套类型。
4. 作用域和继承中的差异
(1)在类中使用
using
和 typedef
在类中定义类型别名的方式类似,但 using
更灵活:
class MyClass {
public:// 传统typedeftypedef int IntType;// 现代usingusing SizeType = std::size_t;// 使用模板别名template<typename T>using Pair = std::pair<T, T>;
};// 使用类中的别名
MyClass::IntType x = 42;
MyClass::Pair<double> p(3.14, 2.71);
(2)继承中的类型别名
using
可以更方便地在派生类中引入基类的类型别名:
class Base {
public:using ValueType = int;
};class Derived : public Base {
public:using Base::ValueType; // 引入基类的类型别名(可选)
};
5. 与 auto
和 decltype
的结合
using
可与 auto
和 decltype
结合,为复杂类型创建简洁别名:
// 为lambda类型创建别名
auto add = [](int a, int b) { return a + b; };
using AddType = decltype(add); // AddType是lambda的类型// 为函数返回类型创建别名
using ResultType = decltype(someFunction()); // someFunction返回值的类型
std::function
在 C++ 中,std::function
是一个通用的多态函数包装器(位于 <functional>
头文件),它可以存储、复制和调用任何可调用对象(函数、函数指针、成员函数指针、lambda 表达式、仿函数等)。std::function
是类型安全的,常用于回调机制、事件处理和函数对象的存储。
1. 基本语法与用法
(1)定义与初始化
#include <functional>// 定义一个接受两个int并返回int的函数对象
std::function<int(int, int)> add;// 用lambda初始化
add = [](int a, int b) { return a + b; };// 调用
int result = add(3, 4); // 结果为7
(2)存储不同类型的可调用对象
// 1. 普通函数
int subtract(int a, int b) { return a - b; }
std::function<int(int, int)> op = subtract;// 2. lambda表达式
op = [](int a, int b) { return a * b; };// 3. 函数对象(仿函数)
struct Divide {int _a;int operator()(int a, int b) const { return a / b; }
};
op = Divide();
std::function<int(Divide&)> a = &Divide::_a;
std::cout<<a(1)<<std::endl;// 4. 成员函数指针
struct Adder {int add(int a, int b) { return a + b; }
};
Adder adder;
std::function<int(int, int)> member_op = [&adder](int a, int b) {return adder.add(a, b);
};
2. 成员函数的包装
包装成员函数时,需要绑定对象实例(通过 lambda 或 std::bind
):
struct Logger {void log(const std::string& msg) {std::cout << "Log: " << msg << std::endl;}
};
//如果不使用bind或者lambda的话,第一个参数则是类引用,第二个是函数类型
innt main()
{std::function<string(Logger&,string)> lo = &Logger::log
}// 方法1:使用lambda捕获对象
Logger logger;
std::function<void(const std::string&)> log_func = [&logger](const std::string& msg) {logger.log(msg);
};// 方法2:使用std::bind(C++11起)
#include <functional>
log_func = std::bind(&Logger::log, &logger, std::placeholders::_1);// 调用
log_func("Hello, world!"); // 输出:Log: Hello, world!
3. 与函数指针的对比
特性 | std::function | 函数指针 |
---|---|---|
可调用对象类型 | 任意(函数、lambda、仿函数、成员函数等) | 仅普通函数和静态成员函数 |
类型擦除 | 支持(存储任意可调用对象) | 不支持(类型严格匹配) |
状态存储 | 支持(如 lambda 的捕获列表) | 不支持 |
空状态检查 | 可通过 operator bool() 检查 | 直接与 nullptr 比较 |
性能 | 有少量开销(堆分配、虚函数调用) | 无额外开销 |
4. 空状态与错误处理
std::function
可以为空(未初始化或被赋值为 nullptr
),调用空的 std::function
会抛出 std::bad_function_call
异常:
std::function<void()> func; // 默认构造,为空if (!func) { // 检查是否为空std::cout << "Function is empty!" << std::endl;
}try {func(); // 调用空函数,抛出异常
} catch (const std::bad_function_call& e) {std::cerr << "Error: " << e.what() << std::endl;
}// 赋值为nullptr
func = nullptr;
5. 应用场景
(1)回调函数
// 定义一个接受回调函数的函数
void onEvent(std::function<void(int)> callback) {// 事件发生时调用回调callback(42);
}// 使用lambda作为回调
onEvent([](int code) {std::cout << "Event code: " << code << std::endl;
});
(2)函数注册表
#include <unordered_map>// 注册表:字符串映射到函数
std::unordered_map<std::string, std::function<int(int, int)>> op_map;// 注册函数
op_map["add"] = [](int a, int b) { return a + b; };
op_map["sub"] = [](int a, int b) { return a - b; };// 调用
int result = op_map["add"](3, 4); // 结果为7
(3)延迟执行
// 存储一个函数供后续执行
std::function<void()> task;// 初始化任务
task = []() {std::cout << "Task executed!" << std::endl;
};// 稍后执行
task();
6. 性能考虑
- 优势:
std::function
提供了类型安全和灵活性,适合需要存储多种可调用对象的场景。 - 劣势:相比直接调用函数或函数指针,
std::function
有少量开销(堆分配、虚函数调用),因此在性能敏感的代码中需谨慎使用。
智能指针
在 C++ 中,智能指针(Smart Pointer)是一种用于管理动态分配内存的类模板,它能够自动释放不再使用的对象,避免内存泄漏。智能指针通过 RAII(资源获取即初始化)技术,将堆内存的生命周期与对象的生命周期绑定,是现代 C++ 编程的核心工具之一。
一、为什么需要智能指针?
传统的裸指针(Naked Pointer)存在以下问题:
- 内存泄漏:忘记调用
delete
释放内存。 - 悬空指针:对象已被释放,但指针仍在使用。
- 重复释放:多个指针指向同一对象,多次调用
delete
。
智能指针通过自动管理内存生命周期,解决了这些问题。
二、C++ 标准库中的智能指针
C++11 引入了三种智能指针(位于 <memory>
头文件):
std::unique_ptr
:独占所有权的智能指针。std::shared_ptr
:共享所有权的智能指针。std::weak_ptr
:弱引用,配合shared_ptr
使用,避免循环引用。
三、std::unique_ptr
1. 特性
- 独占所有权:同一时间只能有一个
unique_ptr
指向某个对象。 - 自动释放:
unique_ptr
销毁时,其管理的对象会被自动删除。 - 不可复制:禁止拷贝构造和赋值,但支持移动语义。
2. 基本用法
#include <memory>// 创建unique_ptr(推荐使用make_unique,C++14起)
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);// 等价于(C++11写法)
std::unique_ptr<int> ptr2(new int(42));// 访问对象
std::cout << *ptr1 << std::endl; // 输出:42// 转移所有权(通过移动语义)
std::unique_ptr<int> ptr3 = std::move(ptr1); // ptr1变为空
3. 作为函数参数和返回值
// 函数返回unique_ptr
std::unique_ptr<Shape> createCircle() {return std::make_unique<Circle>();
}
std::unique_ptr<Shape> createCircle() {return std::unique_ptr<Circle>();
}// 函数接受unique_ptr(通过移动)
void processShape(std::unique_ptr<Shape> shape) {// ...
}// 使用示例
auto circle = createCircle();
processShape(std::move(circle)); // 转移所有权到函数
四、std::shared_ptr
1. 特性
- 共享所有权:多个
shared_ptr
可以指向同一对象。 - 引用计数:通过引用计数(Reference Counting)记录有多少个
shared_ptr
共享对象。 - 自动释放:当最后一个
shared_ptr
被销毁时,对象才会被释放。
2. 基本用法
// 创建shared_ptr(推荐使用make_shared,效率更高)
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);// 拷贝构造,引用计数+1
std::shared_ptr<int> ptr2 = ptr1;// 当前引用计数
std::cout << ptr1.use_count() << std::endl; // 输出:2// 重置其中一个,引用计数-1
ptr2.reset();
std::cout << ptr1.use_count() << std::endl; // 输出:1
3. 自定义删除器
// 自定义删除器(例如关闭文件)
void fileDeleter(FILE* file) {if (file) fclose(file);
}// 使用自定义删除器
std::shared_ptr<FILE> file(fopen("test.txt", "r"), fileDeleter);
五、std::weak_ptr
1. 特性
- 弱引用:不控制对象的生命周期,仅观测
shared_ptr
管理的对象。 - 解决循环引用:当
shared_ptr
之间形成循环引用时,使用weak_ptr
打破循环。 - 必须转换为
shared_ptr
才能使用对象:通过lock()
方法获取shared_ptr
。
2. 循环引用示例
struct Node {std::shared_ptr<Node> next; // 循环引用:导致内存泄漏~Node() { std::cout << "Node destroyed" << std::endl; }
};// 修复循环引用:使用weak_ptr
struct Node {std::weak_ptr<Node> next; // 弱引用,不增加引用计数~Node() { std::cout << "Node destroyed" << std::endl; }
};// 使用示例
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node2的引用计数为1
node2->next = node1; // 若next为shared_ptr,则形成循环;改为weak_ptr后无循环
3. 使用 weak_ptr
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared; // 弱引用shared// 检查对象是否存在并使用
if (auto locked = weak.lock()) { // 转换为shared_ptrstd::cout << *locked << std::endl; // 输出:42
}
六、智能指针的选择原则
场景 | 推荐智能指针 |
---|---|
独占资源所有权 | std::unique_ptr |
共享资源所有权 | std::shared_ptr |
避免循环引用 | std::weak_ptr |
作为类成员变量 | 优先 unique_ptr |
函数返回动态对象 | unique_ptr 或 shared_ptr |
缓存 / 观察者模式 | weak_ptr |
七、常见注意事项
避免混合使用裸指针和智能指针:
int* raw = new int(42); std::shared_ptr<int> ptr1(raw); // 危险:raw可能被多处管理 std::shared_ptr<int> ptr2(raw); // 错误:重复释放同一内存
优先使用工厂函数创建智能指针:
// 推荐:自动推导类型,异常安全 auto ptr = std::make_unique<MyClass>(args...);// 不推荐:手动new,可能导致内存泄漏 std::unique_ptr<MyClass> ptr(new MyClass(args...));
shared_ptr
不要管理栈上对象:int x = 42; std::shared_ptr<int> ptr(&x); // 错误:ptr会尝试delete栈上对象
八、智能指针与数组
C++11 起,智能指针支持管理数组:
// unique_ptr管理数组(自动调用delete[])
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
arr[0] = 42;// shared_ptr管理数组(需显式指定删除器)
std::shared_ptr<int> arr2(new int[10], [](int* p) { delete[] p; });
bind函数
在 C++ 里,std::bind
是一个很重要的函数模板,其作用是把可调用对象(像函数、函数指针、成员函数指针、函数对象等)和它的参数绑定起来,进而生成一个新的可调用对象。下面为你详细介绍它的功能、使用方法以及相关注意要点。
功能概述
std::bind
主要具备以下功能:
- 能固定可调用对象的部分参数,也就是所谓的 “部分函数应用”。
- 可以对参数的传递顺序进行调整。
- 借助占位符,能灵活地控制参数的传递时机。
基础语法
std::bind
的基本使用格式如下:
auto newCallable = std::bind(可调用对象, 参数1, 参数2, ...);
其中:
可调用对象
指的是要绑定的函数或者其他可调用实体。参数1, 参数2, ...
是传递给可调用对象的参数,这里面可以包含占位符(例如std::placeholders::_1
)。
占位符的运用
在std::bind
中,占位符(如_1
, _2
, _3
, ...)用来表示新可调用对象的参数位置。这些占位符都定义在std::placeholders
命名空间里。下面通过例子来说明:
#include <iostream>
#include <functional>using namespace std::placeholders;void print(int a, int b) {std::cout << "a = " << a << ", b = " << b << std::endl;
}int main() {// 把print函数的第一个参数绑定为10,第二个参数使用占位符_1auto f = std::bind(print, 10, _1);f(20); // 输出:a = 10, b = 20// 交换参数顺序auto g = std::bind(print, _2, _1);g(100, 200); // 输出:a = 200, b = 100return 0;
}
绑定成员函数
在绑定类的成员函数时,需要把对象实例(或者对象指针、引用)作为第一个参数,示例如下:
#include <iostream>
#include <functional>class Calculator {
public:int add(int a, int b) { return a + b; }
};int main() {Calculator calc;auto add = std::bind(&Calculator::add, &calc, _1, _2);std::cout << add(3, 4) << std::endl; // 输出:7return 0;
}
参数传递方式
- 值传递:默认情况下,参数会以值传递的方式被保存。
- 引用传递:如果想以引用的方式传递参数,需要使用
std::ref
或者std::cref
。
void increment(int& x) { ++x; }int main() {int value = 10;auto func = std::bind(increment, std::ref(value));func();std::cout << value << std::endl; // 输出:11return 0;
}
与 lambda 表达式的对比
虽然std::bind
和 lambda 表达式都能用于捕获参数,但它们各有特点:
std::bind
:适合进行简单的参数绑定,不过在处理复杂逻辑时可读性会变差。- lambda 表达式:语法更加简洁直观,而且功能更为强大,所以在现代 C++ 编程中更受推荐。
注意要点
- 要使用
std::bind
,需要包含<functional>
头文件。 - 占位符的命名空间是
std::placeholders
。 - 当绑定的参数涉及动态资源时,要留意对象的生命周期管理问题。