C++笔记(面向对象)六(4+2C++11)个缺省函数详解
c++11 move 新的两个默认成员函数详解
移动构造函数
移动构造函数是一种特殊的构造函数,它通过"窃取"另一个对象(通常是临时对象)的资源来构造新对象,而不是进行昂贵的深拷贝。
语法
ClassName(ClassName&& other) noexcept;
- 参数是 ClassName&&(右值引用)
- 通常标记为 noexcept(不抛出异常)
- 参数不是 const(因为我们要修改它)
为什么要移动构造函数?
先看一个没有移动构造的"痛苦"例子:
#include <iostream>
#include <cstring>
class String {
private:char* data;size_t length;
public:// 普通构造函数String(const char* str = "") {length = strlen(str);data = new char[length + 1];strcpy(data, str);std::cout << "构造: " << data << std::endl;}// 拷贝构造函数(深拷贝)String(const String& other) {length = other.length;data = new char[length + 1];strcpy(data, other.data);std::cout << "拷贝构造: " << data << std::endl;}~String() {delete[] data;std::cout << "析构: " << data << std::endl;} const char* c_str() const { return data; }
};
String createString() {String temp("Hello World"); // 构造临时对象return temp; // 这里可能触发拷贝!
}
int main() {String s = createString(); // 可能有多余的拷贝std::cout << "结果: " << s.c_str() << std::endl;return 0;
}
问题:临时对象 temp 在返回时可能被拷贝,然后立即被销毁,这种拷贝是浪费的!
移动构造函数来解决!
class String {
private:char* data;size_t length;
public:// ... 之前的构造函数和析构函数保持不变 ...// 移动构造函数!String(String&& other) noexcept : data(other.data), length(other.length) { // 窃取资源// 将源对象置于有效但空的状态other.data = nullptr;other.length = 0;std::cout << "移动构造: " << data << std::endl;}
};
String createString() {String temp("Hello World");return temp; // 现在这里会调用移动构造!
}
int main() {String s = createString(); // 调用移动构造函数,没有深拷贝!std::cout << "结果: " << s.c_str() << std::endl;return 0;
}
移动构造的工作原理
关键思想:"资源窃取"
String(String&& other) noexcept : data(other.data) // 直接接管指针, length(other.length) // 直接接管长度
{other.data = nullptr; // 重要!让源对象不管理资源other.length = 0; // 源对象现在为空
}
移动 vs 拷贝的对比
String original("Hello");
// 拷贝构造:昂贵的操作
String copy = original; // 分配新内存 + 复制数据
// original 和 copy 都有完整的数据副本
// 移动构造:高效的操作
String moved = std::move(original); // 只是指针转移
// moved 接管了数据,original 变为空状态
实际示例:观察移动构造的触发
#include <iostream>
#include <utility> // for std::move
class String {// ... 同上,包含移动构造函数 ...
};
int main() {std::cout << "=== 场景1:从函数返回 ===" << std::endl;String s1 = createString(); // 移动构造std::cout << "\n=== 场景2:显式使用 std::move ===" << std::endl;String s2("Temp String");String s3 = std::move(s2); // 移动构造// s2 现在不能再使用了! std::cout << "\n=== 场景3:临时对象 ===" << std::endl;String s4 = String("Direct Temp"); // 可能直接构造,也可能构造+移动std::cout << "\n=== 场景4:标准库容器的好处 ===" << std::endl;std::vector<String> vec;vec.push_back(String("Vector Element")); // 移动构造,不是拷贝!return 0;
}
移动构造函数的重点规则
1. 自动生成条件
编译器会在以下情况下自动生成移动构造函数:
- 没有用户声明的拷贝操作(拷贝构造、拷贝赋值)
- 没有用户声明的移动操作
- 没有用户声明的析构函数
2. 需要自定义移动构造的情况
class ResourceHolder {int* resource;
public:// 当类管理资源时,需要自定义移动构造ResourceHolder(ResourceHolder&& other) noexcept : resource(other.resource) {other.resource = nullptr; // 重要!}
};
3. 移动后的源对象状态
- 移动后,源对象应该处于有效但未定义的状态
- 通常设置为"空状态"(如 nullptr、0)
- 移动后的对象仍然可以安全析构
4. 标记为 noexcept 的重要性
// 好的做法
String(String&& other) noexcept;
// 为什么重要?因为标准库容器(如vector)在重新分配内存时
// 如果移动构造可能抛出异常,它会选择更安全的拷贝构造
总结
移动构造函数的核心价值:
- 性能优化:避免不必要的深拷贝
- 资源转移:高效管理动态内存、文件句柄等
- 与现代C++特性配合:完美转发、智能指针等
- 零开销抽象:在需要时提供最优性能
编译器会自动优化,通常不需要你显式调用
情况1:返回值优化(RVO) - 编译器自动消除拷贝
String createString() {String temp("Hello"); // 在函数内部构造return temp; // 返回局部对象
}
int main() {String s = createString(); // 这里可能根本没有调用移动构造!
}
在现代编译器中,这种情况会通过返回值优化(RVO) 直接在被调用的地方(main 函数中 s 的位置)构造对象,连移动构造都不需要调用。
情况2:命名返回值优化(NRVO)
String createString(bool flag) {String result;if (flag) {result = "Hello";} else {result = "World";}return result; // 命名返回值优化
}
编译器也会尝试优化,可能直接在被调用处构造 result。
那么什么时候真的会调用移动构造函数?
场景1:编译器无法进行 RVO/NRVO 时
String createString(int choice) {String a("Hello");String b("World"); if (choice == 1) {return a; // 无法确定返回哪个,可能调用移动构造} else {return b; // 无法确定返回哪个,可能调用移动构造}
}
场景2:显式使用 std::move
String createString() {String temp("Hello");return std::move(temp); // 强制调用移动构造(但通常不推荐!)
}
注意:实际上,在返回语句中显式使用 std::move 反而可能阻止 RVO,所以通常不推荐这样做。
场景3:存入容器时
std::vector<String> vec;
// 这里会调用移动构造函数
vec.push_back(String("Temporary")); // 临时对象 → 移动构造
String s("Existing");
vec.push_back(std::move(s)); // 显式移动 → 移动构造
场景4:函数参数需要移动
void process(String&& str) { // 接收右值引用String local = std::move(str); // 调用移动构造
}
int main() {process(String("Temp")); // 移动构造可能在这里发生
}
正确的理解方式
移动构造函数是自动被调用的,当:
- 源对象是右值(临时对象、std::move 的结果)
- 编译器认为移动比拷贝更高效
实际开发中的建议
// 好的做法:让编译器决定
String createString() {String result("Hello");// ... 一些操作 ...return result; // 让编译器决定:RVO 或 移动构造
}
// 不要这样做(可能阻止优化)
String createString() {String result("Hello");return std::move(result); // 不推荐!可能阻止 RVO
}
总结
核心思想:你作为类作者,只需要提供移动构造函数。至于什么时候调用它,交给编译器和 C++ 标准来决定。
- 你的责任:为资源管理类正确实现移动构造函数
- 编译器的责任:在合适的时机自动调用移动构造函数
- 标准库的责任:在容器、算法等地方充分利用移动语义
当成员函数返回临时对象时,可能需要调用移动构造,但更可能的是编译器直接优化掉(RVO),你不需要显式去调用它。
移动赋值运算符
作用:给已存在的对象高效赋值(移动语义版)
ClassName& operator=(ClassName&& other) noexcept;
核心实现(三步骤)
String& operator=(String&& other) noexcept {if (this != &other) { // 1. 自赋值检查delete[] data; // 2. 释放当前资源data = other.data; // 3. 窃取资源 + 置空源对象other.data = nullptr;}return *this;
}
使用场景
String s1("Hello");
String s2("World");
s1 = std::move(s2); // 移动赋值:s2资源转移给s1
s1 = createString(); // 临时对象自动移动赋值
与移动构造的区别
核心价值:避免已存在对象赋值时的深拷贝开销。