C++移动构造函数什么时候会被调用
移动构造函数是 C++11 引入的重要特性,主要用于提升性能,避免不必要的深拷贝。当对象持有堆内存、文件句柄等资源时,使用移动构造函数可以直接“窃取”临时对象的资源,而非复制一份。
以下通过具体代码实例和场景分析,详细说明移动构造函数的触发时机。
1. 准备测试类
为了直观地观察调用情况,首先定义一个包含打印信息的 MyString 类。该类手动实现了拷贝构造函数和移动构造函数,并在控制台输出特定标识。
编写如下代码:
#include <iostream>
#include <vector>
#include <string>
class MyString {
public:
// 普通构造函数
MyString(const char* str = "") {
data = new char[strlen(str) + 1];
strcpy(data, str);
std::cout << "构造函数被调用: " << data << std::endl;
}
// 拷贝构造函数 (深拷贝)
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
std::cout << "拷贝构造函数被调用: " << data << std::endl;
}
// 移动构造函数 (窃取资源)
MyString(MyString&& other) noexcept {
data = other.data;
other.data = nullptr; // 置空,防止析构时释放
std::cout << "移动构造函数被调用" << std::endl;
}
~MyString() {
if (data) {
std::cout << "析构函数被调用: " << data << std::endl;
delete[] data;
} else {
std::cout << "析构函数被调用: [空指针]" << std::endl;
}
}
private:
char* data;
};
2. 场景一:使用 std::move 显式转换
当对象是左值(有名字的变量)时,编译器默认调用拷贝构造函数。如果想调用移动构造函数,必须使用 std::move 将左值强制转换为右值引用。
执行以下步骤:
- 创建一个
MyString对象a。 - 使用
std::move(a)初始化新对象b。
void test_move() {
MyString a("Hello"); // 调用构造函数
MyString b = std::move(a); // 调用移动构造函数
// 此时 a.data 已为 nullptr,b.data 指向原内存
}
观察输出结果,屏幕上会显示“移动构造函数被调用”。这表明 b 窃取了 a 的资源,没有进行内存分配和数据复制。
3. 场景二:初始化临时对象(右值)
当使用一个临时对象(右值)去初始化一个新对象时,编译器会优先选择移动构造函数,因为临时对象即将销毁,其资源可以被复用。
执行以下代码:
void test_temporary() {
// "World" 创建临时对象,然后移动给 c
// 或者编译器直接优化为直接构造 c (RVO),省略移动
MyString c = MyString("World");
}
注意:这里有一个特殊情况。现代编译器通常具有返回值优化(RVO/NRVO)。在开启优化的情况下,编译器可能直接在 c 的内存上构造 "World",从而完全省略了移动构造函数的调用。若要强制观察到移动行为,需关闭编译器优化或构造更复杂的场景。
4. 场景三:函数返回值
当一个函数返回一个局部对象时,理论上会调用移动构造函数将返回值赋给接收者。但同样受限于 RVO 优化,移动构造函数经常被省略。
编写如下函数并测试:
MyString createString() {
MyString temp("FuncReturn");
return temp; // 这里可能发生移动,也可能被 NRVO 优化掉
}
void test_return() {
MyString d = createString();
}
若编译器未执行 NRVO 优化,temp 会被移动给 d。若执行了优化,则只会有一次构造函数调用。
5. 场景四:STL 容器扩容或插入
这是移动构造函数最有用的场景。当 std::vector 等容器发生扩容(Reallocation)或插入元素时,如果元素类型实现了移动构造函数且声明为 noexcept,容器会优先移动而非复制现有元素。
执行以下步骤:
- 创建一个
std::vector<MyString>。 - 循环
push_back10 个对象。
void test_vector() {
std::vector<MyString> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(MyString("Element")); // 将临时对象插入容器
}
}
分析过程:
- 当向
vec插入临时对象MyString("Element")时,会调用移动构造函数将临时对象“搬”进容器。 - 当
vec容量不足触发扩容时,容器会分配更大的内存,并将旧内存中的元素移动到新内存中。如果没有移动构造函数,这里将发生昂贵的深拷贝。
6. 调用条件总结表
下表总结了不同情况下构造函数的调用逻辑。
| 操作场景 | 源对象类型 | 调用的函数 | 备注 |
|---|---|---|---|
A a; A b = a; |
左值 | 拷贝构造函数 | 深拷贝,保留源对象 |
A a; A b = std::move(a); |
将亡值 (xvalue) | 移动构造函数 | 窃取资源,源对象置空 |
A a = A(); |
纯右值 (prvalue) | 移动构造函数 | 通常被 RVO 优化为直接构造 |
vector.push_back(A()) |
临时对象 | 移动构造函数 | 高效插入 |
vector 扩容时 |
容器内现有对象 | 移动构造函数 | 需函数标记为 noexcept |
7. 触发流程图
为了更清晰地理解编译器选择移动构造函数的决策过程,请参考以下流程图。该图展示了从对象初始化请求到最终调用函数的判断逻辑。
被 std::move 转换?} B -- 否 --> C[调用拷贝构造函数] B -- 是 --> D{类定义了
移动构造函数?} D -- 否 --> E[调用拷贝构造函数
如果没有则报错] D -- 是 --> F{是否为 STL 容器扩容场景?} F -- 否 --> G[调用移动构造函数] F -- 是 --> H{移动构造函数
是否标记为 noexcept?} H -- 否 --> I[若拷贝构造函数存在则调用拷贝
(保证强异常安全)] H -- 是 --> G G --> J[完成资源转移] C --> K[完成深拷贝] I --> K
解析图中的关键节点:
- 节点 B:判断是否具备移动的前提条件(必须是右值)。
- 节点 H:这是一个极其重要的细节。在
std::vector扩容时,如果移动构造函数没有声明为noexcept,C++ 标准库为了保证异常安全(万一移动过程中抛出异常,原数据不能丢),会退而求其次调用拷贝构造函数。因此,务必在移动构造函数后加上noexcept关键字。
// 正确的移动构造函数声明
MyString(MyString&& other) noexcept {
// ... 实现代码
}
确保在编写高性能 C++ 代码时,始终检查移动构造函数是否被正确声明为 noexcept,以确保 STL 容器能够优先使用它。

暂无评论,快来抢沙发吧!