共计 2623 个字符,预计需要花费 7 分钟才能阅读完成。
核心概念
在 C ++ 中,拷贝构造函数和拷贝赋值函数是类的重要成员函数,用于控制对象的拷贝行为。它们的定义如下:

- 拷贝构造函数:用于用一个已存在的对象初始化一个新对象,形式为
ClassName(const ClassName& other) - 拷贝赋值函数:用于将一个已存在对象的值赋给另一个已存在对象,形式为
ClassName& operator=(const ClassName& other)
在 C ++11 中,随着移动语义的引入,这两个函数有了新的变化。编译器会自动生成移动构造函数和移动赋值函数(除非显式声明了拷贝操作、析构函数或移动操作)。
痛点分析
开发者在使用拷贝构造函数和拷贝赋值函数时,常常会遇到以下困惑:
- 对象初始化时,何时调用拷贝构造函数,何时调用移动构造函数
- 函数参数传递时,按值传递会触发多少次拷贝
- 函数返回值优化 (NRVO) 和移动语义如何影响拷贝行为
- 容器操作(如 vector 的 push_back)中对象的拷贝 / 移动行为
- 继承和多态场景下的拷贝行为
技术方案
拷贝构造函数调用时机
-
用一个对象初始化另一个对象时
MyClass a; MyClass b(a); // 调用拷贝构造函数 -
函数参数按值传递时
void func(MyClass obj); MyClass a; func(a); // 调用拷贝构造函数 -
函数返回对象时(在没有 NRVO 优化的情况下)
MyClass func() { MyClass a; return a; // 可能调用拷贝构造函数(没有优化时)}
拷贝赋值函数调用时机
-
一个已存在对象被另一个已存在对象赋值时
MyClass a, b; a = b; // 调用拷贝赋值函数 -
容器中元素被替换时
std::vector<MyClass> vec; MyClass a; vec[0] = a; // 调用拷贝赋值函数
C++11 的移动语义
C++11 引入了右值引用和移动语义,使得在某些情况下可以避免不必要的拷贝:
-
用临时对象初始化新对象时,优先调用移动构造函数
MyClass func(); MyClass a(func()); // 调用移动构造函数 -
用一个临时对象赋值给已存在对象时,优先调用移动赋值函数
MyClass func(); MyClass a; a = func(); // 调用移动赋值函数
代码示例
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass() { std::cout << "Default constructor\n";}
MyClass(const MyClass& other) {std::cout << "Copy constructor\n";}
MyClass& operator=(const MyClass& other) {
std::cout << "Copy assignment\n";
return *this;
}
MyClass(MyClass&& other) noexcept {std::cout << "Move constructor\n";}
MyClass& operator=(MyClass&& other) noexcept {
std::cout << "Move assignment\n";
return *this;
}
};
MyClass createObject() {return MyClass(); // NRVO 优化可能发生
}
void passByValue(MyClass obj) {}
int main() {
std::cout << "=== 1. Initialization ===\n";
MyClass a; // 默认构造
MyClass b(a); // 拷贝构造
MyClass c = a; // 拷贝构造
MyClass d(createObject()); // 可能移动构造或 NRVO
std::cout << "\n=== 2. Assignment ===\n";
a = b; // 拷贝赋值
a = createObject(); // 移动赋值
std::cout << "\n=== 3. Function calls ===\n";
passByValue(a); // 拷贝构造
MyClass e = createObject(); // 可能移动构造或 NRVO
std::cout << "\n=== 4. Containers ===\n";
std::vector<MyClass> vec;
vec.push_back(a); // 拷贝构造(如果 vector 需要扩容)vec.push_back(MyClass()); // 移动构造(如果 vector 需要扩容)return 0;
}
性能考量
不必要的拷贝操作会对性能产生显著影响:
- 大型对象的拷贝代价高昂
- 容器操作中的拷贝会放大性能问题
- 多次拷贝可能导致内存碎片
优化方法:
- 尽量使用移动语义
- 对于不可拷贝但可移动的资源(如 unique_ptr),使用移动
- 在函数参数传递时,考虑使用 const 引用而非传值
- 合理使用 std::move 将左值转为右值
- 实现移动构造函数和移动赋值函数时标记为 noexcept
避坑指南
- Rule of Three/Five/Zero
- 如果定义了拷贝操作、析构函数或移动操作中的一个,通常需要定义全部
-
或者遵循 Rule of Zero,使用编译器生成的默认实现
-
避免自赋值
MyClass& operator=(const MyClass& other) {if (this == &other) return *this; // 自赋值检查 // 赋值操作 return *this; } -
正确处理资源管理
- 在拷贝操作中深度拷贝所有资源
-
在移动操作中将源对象置于有效但可析构的状态
-
谨慎使用 std::move
- 不要对 const 对象使用 std::move
-
移动后不要继续使用源对象(除非明确重置)
-
注意容器行为
- vector 的 push_back 可能导致元素移动或拷贝
- emplace_back 可以避免临时对象的创建
思考题
- 在什么情况下,即使定义了移动构造函数,拷贝构造函数仍会被调用?
- 如何实现一个只可移动不可拷贝的类?
- 当类继承自一个不可拷贝的基类时,派生类的拷贝行为会怎样?
- 在模板编程中,如何根据类型特性选择最佳的拷贝 / 移动策略?
- 为什么移动构造函数和移动赋值函数通常应该标记为 noexcept?
总结
理解 C ++11 中拷贝构造函数和拷贝赋值函数的调用时机对于编写高效、正确的代码至关重要。通过掌握这些核心概念,合理使用移动语义,遵循最佳实践,可以避免常见的性能陷阱和逻辑错误。在实际开发中,建议结合工具(如编译器警告、静态分析)来检测不必要的拷贝操作,并持续优化代码的拷贝 / 移动行为。
正文完
