一、移动语义:C++11 最革命性的性能优化
1.1 为什么需要移动语义?
传统拷贝的性能灾难 在 C++11 之前,对象的传递只能通过拷贝实现。对于包含大量动态分配资源的对象(如 std::string、std::vector),深拷贝会带来巨大的性能开销:
cpp
std::string create_large_string() {
std::string s(1000000, 'a'); // 分配1MB内存
return s; // 传统上会调用拷贝构造函数,再销毁原对象
}
int main() {
std::string s = create_large_string();
// 总共:1次构造 + 1次拷贝构造 + 1次析构
// 实际执行了2次内存分配和2次数据拷贝!
}
问题本质:临时对象在拷贝后会立即被销毁,我们实际上在 "复制一个即将被扔掉的东西"。如果能直接 "偷走" 临时对象的资源,就能避免不必要的拷贝。
1.2 左值与右值:移动语义的基石
核心定义
- 左值 (lvalue):可以取地址、有名字的表达式。代表一个持久的对象。
- 右值 (rvalue):不能取地址、没有名字的表达式。代表一个临时对象或即将销毁的对象。
直观判断:能放在赋值运算符左边的是左值,只能放在右边的是右值。
cpp
int a = 10; // a是左值,10是右值
int* p = &a; // 正确,可以取左值地址
// int* p2 = &10; // 错误,不能取右值地址
std::string s1 = "hello"; // s1是左值,"hello"是右值
std::string s2 = s1 + s1; // s1+s1的结果是右值(临时对象)
C++11 右值细分
- 纯右值 (prvalue):字面量、临时对象、lambda 表达式等
- 将亡值 (xvalue):通过 std::move 转换的左值,代表即将被移动的对象
1.3 右值引用:绑定到右值的引用
语法 :T&&
- 左值引用
T&只能绑定到左值 - 右值引用
T&&只能绑定到右值 - const 左值引用
const T&可以绑定到左值和右值(这是 C++11 之前临时对象能被传递的原因)
cpp
int a = 10;
int& lref = a; // 正确,左值引用绑定左值
// int& lref2 = 10; // 错误,左值引用不能绑定右值
int&& rref = 10; // 正确,右值引用绑定右值
// int&& rref2 = a; // 错误,右值引用不能绑定左值
const int& clref = a; // 正确
const int& clref2 = 10; // 正确,const左值引用可以绑定右值
右值引用的意义:让我们能够区分 "普通对象" 和 "临时对象",从而为临时对象提供特殊的处理逻辑(移动)。
1.4 移动构造函数与移动赋值运算符
移动构造函数
cpp
class MyString {
private:
char* data;
size_t size;
public:
// 构造函数
MyString(const char* str) {
size = strlen(str);
data = new char[size + 1];
strcpy(data, str);
std::cout << "构造函数:分配了" << size+1 << "字节\n";
}
// 拷贝构造函数(深拷贝)
MyString(const MyString& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
std::cout << "拷贝构造函数:深拷贝了" << size+1 << "字节\n";
}
// 移动构造函数(转移资源,不分配新内存)
MyString(MyString&& other) noexcept {
// 直接"偷"资源
data = other.data;
size = other.size;
// 将原对象置空,防止析构时释放我们已经拥有的资源
other.data = nullptr;
other.size = 0;
std::cout << "移动构造函数:转移了" << size+1 << "字节\n";
}
// 析构函数
~MyString() {
delete[] data;
std::cout << "析构函数:释放了内存\n";
}
};
移动赋值运算符
cpp
MyString& operator=(MyString&& other) noexcept {
if (this != &other) { // 防止自赋值
// 先释放自己当前的资源
delete[] data;
// 偷取对方的资源
data = other.data;
size = other.size;
// 置空对方
other.data = nullptr;
other.size = 0;
std::cout << "移动赋值运算符:转移了" << size+1 << "字节\n";
}
return *this;
}
关键要点
- noexcept 关键字:必须加上!否则标准库容器(如 std::vector)在重新分配内存时会选择拷贝而不是移动,因为移动可能抛出异常会导致容器处于不一致状态。
- 原对象置空:移动后原对象必须处于 "可析构" 的状态,不能让它持有任何资源。
- 移动后原对象不可用:除了赋值和析构,不要对移动后的对象做任何其他操作。
1.5 std::move:强制转换为右值
std::move 的本质:一个模板函数,将左值无条件转换为右值引用。它本身不移动任何数据,只是触发移动语义。
cpp
MyString s1("hello");
MyString s2 = s1; // 调用拷贝构造函数(s1是左值)
MyString s3 = std::move(s1); // 调用移动构造函数!
// 此时s1已经被移动,内部data为nullptr,不能再使用
// std::cout << s1 << std::endl; // 未定义行为!
使用场景
- 当你确定一个左值不再需要使用时,可以用 std::move 将其资源转移给其他对象
- 标准库容器的插入操作:
vec.push_back(std::move(obj))
1.6 完美转发与 std::forward
问题:如何编写一个函数模板,将参数原封不动地转发给另一个函数,保持其左值 / 右值属性?
引用折叠规则
T& &折叠为T&T& &&折叠为T&T&& &折叠为T&T&& &&折叠为T&&
std::forward 的作用:根据模板参数的类型,有条件地将参数转换为右值引用。如果传入的是左值,转发后还是左值;如果传入的是右值,转发后还是右值。
cpp
template<typename T, typename Arg>
std::unique_ptr<T> create_unique(Arg&& arg) {
// 完美转发arg给T的构造函数
return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}
int main() {
std::string s = "hello";
auto p1 = create_unique<std::string>(s); // s是左值,转发为左值,调用拷贝构造
auto p2 = create_unique<std::string>(std::move(s)); // 转发为右值,调用移动构造
}
1.7 移动语义的常见误区
-
std::move 不是万能的:如果一个类没有定义移动构造函数 / 移动赋值运算符,即使使用 std::move,也会调用拷贝构造函数 / 拷贝赋值运算符。
-
不要返回局部对象的 std::move :
cppMyString func() { MyString s("hello"); return std::move(s); // 错误!会阻止返回值优化(RVO) // 正确:直接return s; 编译器会自动进行RVO,比移动更高效 } -
移动后的对象只能析构或赋值:不要对移动后的对象进行任何其他操作,除非文档明确说明该操作是安全的。
二、智能指针:解决内存管理的终极方案
2.1 原始指针的七大原罪
- 内存泄漏:忘记 delete
- 野指针:指针指向已经释放的内存
- 重复释放:同一个内存被 delete 多次
- 悬空指针:指针指向的对象已经被销毁
- 所有权不明确:谁负责释放内存?
- 异常安全问题:函数抛出异常时,delete 语句可能不会执行
- 多线程安全问题:多个线程同时访问和修改同一个指针
智能指针通过 RAII(资源获取即初始化)机制,自动管理内存,解决了上述所有问题。
2.2 C++11 及以后的三种智能指针
| 智能指针 | 所有权 | 拷贝 / 移动 | 适用场景 | 底层实现 |
|---|---|---|---|---|
std::unique_ptr<T> |
独占所有权 | 不可拷贝,只能移动 | 单个对象的所有权管理 | 裸指针 + 析构函数 |
std::shared_ptr<T> |
共享所有权 | 可拷贝,可移动 | 多个对象共享同一个资源 | 裸指针 + 引用计数 |
std::weak_ptr<T> |
无所有权 | 可拷贝,可移动 | 解决 shared_ptr 的循环引用问题 | 指向 shared_ptr 的控制块 |
2.3 std::unique_ptr:独占所有权的智能指针
基本用法
cpp
#include <memory>
int main() {
// 创建方式1:C++11及以后
std::unique_ptr<int> p1(new int(10));
// 创建方式2:C++14推荐,更安全(异常安全)
std::unique_ptr<int> p2 = std::make_unique<int>(20);
// 访问方式:和原始指针一样
std::cout << *p1 << std::endl; // 10
std::cout << p1.get() << std::endl; // 获取原始指针
// 释放资源:当unique_ptr离开作用域时,自动调用delete
// 不需要手动delete!
}
移动语义与 unique_ptr unique_ptr 不可拷贝,但可以移动:
cpp
std::unique_ptr<int> p1 = std::make_unique<int>(10);
// std::unique_ptr<int> p2 = p1; // 错误!unique_ptr不可拷贝
std::unique_ptr<int> p3 = std::move(p1); // 正确!移动所有权
// 此时p1为空,不再拥有任何资源
自定义删除器 unique_ptr 支持自定义删除器,用于管理非 new 分配的资源:
cpp
// 管理文件指针
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "w"), fclose);
if (file) {
fprintf(file.get(), "Hello, World!\n");
}
// 离开作用域时自动调用fclose关闭文件
适用场景
- 函数返回值:返回一个动态分配的对象
- 类成员变量:管理类的动态资源
- 容器元素:在容器中存储动态分配的对象
2.4 std::shared_ptr:共享所有权的智能指针
基本原理 shared_ptr 内部维护一个引用计数,当有新的 shared_ptr 指向同一个对象时,引用计数加 1;当 shared_ptr 被销毁时,引用计数减 1。当引用计数变为 0 时,自动释放对象。
基本用法
cpp
// 创建方式1:C++11及以后
std::shared_ptr<int> p1(new int(10));
// 创建方式2:推荐,更高效(一次内存分配)
std::shared_ptr<int> p2 = std::make_shared<int>(20);
// 拷贝:引用计数加1
std::shared_ptr<int> p3 = p2;
std::cout << p2.use_count() << std::endl; // 输出2
// 重置:引用计数减1
p2.reset();
std::cout << p3.use_count() << std::endl; // 输出1
// 当p3离开作用域时,引用计数变为0,自动释放内存
引用计数的存储位置 shared_ptr 包含两个指针:
- 指向实际对象的指针
- 指向控制块的指针
控制块中存储:
- 引用计数
- 弱引用计数(用于 weak_ptr)
- 删除器
- 分配器
注意:使用 new 创建 shared_ptr 时,会进行两次内存分配(一次分配对象,一次分配控制块)。而 std::make_shared 会进行一次内存分配,同时分配对象和控制块,效率更高。
适用场景
- 多个对象需要共享同一个资源
- 资源的生命周期不确定
2.5 std::weak_ptr:解决循环引用问题
循环引用的灾难
cpp
class A {
public:
std::shared_ptr<A> next;
~A() { std::cout << "A被销毁\n"; }
};
int main() {
std::shared_ptr<A> p1 = std::make_shared<A>();
std::shared_ptr<A> p2 = std::make_shared<A>();
p1->next = p2;
p2->next = p1;
// 离开作用域时,p1和p2被销毁,但它们的引用计数都从2变为1,永远不会变为0
// 内存泄漏!
}
weak_ptr 的作用 weak_ptr 是一种 "弱引用",它指向一个 shared_ptr 管理的对象,但不增加引用计数。它的存在不会阻止对象被销毁。
解决循环引用
cpp
class A {
public:
std::weak_ptr<A> next; // 使用weak_ptr而不是shared_ptr
~A() { std::cout << "A被销毁\n"; }
};
int main() {
std::shared_ptr<A> p1 = std::make_shared<A>();
std::shared_ptr<A> p2 = std::make_shared<A>();
p1->next = p2;
p2->next = p1;
// 离开作用域时,p1和p2被销毁,引用计数变为0,对象被正确释放
}
weak_ptr 的用法
cpp
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // 不增加引用计数
// 检查对象是否还存在
if (!wp.expired()) {
// 升级为shared_ptr,此时引用计数加1
std::shared_ptr<int> sp2 = wp.lock();
std::cout << *sp2 << std::endl;
}
2.6 智能指针的常见陷阱
-
不要用同一个原始指针初始化多个智能指针
cppint* p = new int(10); std::shared_ptr<int> sp1(p); std::shared_ptr<int> sp2(p); // 错误!两个shared_ptr指向同一个对象,会导致重复释放 -
不要在函数参数中直接使用 new
cppvoid func(std::shared_ptr<int> sp, int x) {} // 可能导致内存泄漏! func(new int(10), some_function_that_may_throw()); // 正确写法 func(std::make_shared<int>(10), some_function_that_may_throw()); -
不要用 shared_ptr 管理数组
cpp// 错误!会调用delete而不是delete[] std::shared_ptr<int> sp(new int[10]); // 正确写法 std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>()); // 或者C++17以后 std::shared_ptr<int[]> sp(new int[10]); -
避免裸指针与智能指针混用 一旦将原始指针交给智能指针管理,就不要再使用原始指针访问或释放对象。
-
不要返回 this 的 shared_ptr
cppclass A { public: std::shared_ptr<A> get_self() { return std::shared_ptr<A>(this); // 错误!会导致重复释放 } }; // 正确写法:继承自std::enable_shared_from_this class A : public std::enable_shared_from_this<A> { public: std::shared_ptr<A> get_self() { return shared_from_this(); } };
三、移动语义与智能指针的结合使用
3.1 unique_ptr 的移动特性
unique_ptr 的实现完全依赖于移动语义。它不可拷贝,但可以移动,这完美体现了 "独占所有权" 的语义:
- 当你移动一个 unique_ptr 时,原 unique_ptr 不再拥有对象的所有权,新的 unique_ptr 成为唯一的所有者。
- 这确保了在任何时刻,只有一个 unique_ptr 指向同一个对象,避免了重复释放和悬空指针的问题。
3.2 shared_ptr 的移动优化
shared_ptr 也支持移动语义。移动一个 shared_ptr 比拷贝一个 shared_ptr 更高效:
- 拷贝 shared_ptr 需要原子操作增加引用计数
- 移动 shared_ptr 不需要修改引用计数,只需要转移两个指针的值
cpp
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = std::move(sp1); // 比拷贝快得多
// 此时sp1为空
3.3 函数返回智能指针
返回 unique_ptr
cpp
std::unique_ptr<int> create_int(int value) {
return std::make_unique<int>(value);
// 编译器会自动进行RVO,不需要std::move
}
int main() {
std::unique_ptr<int> p = create_int(10);
}
返回 shared_ptr
cpp
std::shared_ptr<int> create_int(int value) {
return std::make_shared<int>(value);
}
3.4 在容器中使用智能指针
cpp
#include <vector>
#include <memory>
int main() {
std::vector<std::unique_ptr<int>> vec;
// 插入元素
vec.push_back(std::make_unique<int>(10));
vec.push_back(std::make_unique<int>(20));
// 遍历元素
for (const auto& p : vec) {
std::cout << *p << std::endl;
}
// 移动元素
std::unique_ptr<int> p = std::move(vec[0]);
vec.erase(vec.begin());
}