共计 1446 个字符,预计需要花费 4 分钟才能阅读完成。
核心概念
在 C ++ 中,拷贝构造函数和拷贝赋值函数是控制对象拷贝行为的两个关键成员函数。它们的定义和使用看似简单,但理解其调用时机对于写出高效、正确的代码至关重要。

- 拷贝构造函数:用于用一个已存在的对象初始化一个新对象。其典型签名是
ClassName(const ClassName& other)。 - 拷贝赋值函数:用于将一个已存在对象的值赋给另一个已存在的对象。其典型签名是
ClassName& operator=(const ClassName& other)。
这两者的区别在于:拷贝构造函数是在创建新对象时调用,而拷贝赋值函数是在两个已经存在的对象之间赋值时调用。
调用时机分析
理解这些函数何时被调用,可以帮助我们避免不必要的拷贝,提升程序性能。以下是常见的调用场景:
-
对象初始化:
MyClass obj1; MyClass obj2 = obj1; // 调用拷贝构造函数 MyClass obj3(obj1); // 调用拷贝构造函数 -
函数参数传递:
void func(MyClass obj); MyClass obj; func(obj); // 调用拷贝构造函数 -
函数返回值(在没有返回值优化的情况下):
MyClass createObject() { MyClass obj; return obj; // 可能调用拷贝构造函数 } -
赋值操作:
MyClass obj1, obj2; obj1 = obj2; // 调用拷贝赋值函数
隐式生成规则
如果你没有显式定义这些函数,编译器会自动为你生成。这些隐式生成的函数会执行成员逐个拷贝(浅拷贝)。对于简单的类,这可能足够了,但对于包含指针成员的类,这通常会导致问题。
- 编译器生成的拷贝构造函数会拷贝每个非静态成员变量。
- 编译器生成的拷贝赋值函数也会拷贝每个非静态成员变量。
- 如果你定义了析构函数,通常也需要定义这两个函数(Rule of Three)。
移动语义的影响
C++11 引入了移动语义,这改变了我们对拷贝行为的看法。移动构造函数和移动赋值函数允许我们 ” 窃取 ” 临时对象的资源,而不是进行昂贵的拷贝。
- 移动构造函数签名:
ClassName(ClassName&& other) - 移动赋值函数签名:
ClassName& operator=(ClassName&& other)
当源对象是临时对象(右值)时,编译器会优先调用移动操作而不是拷贝操作,这可以显著提高性能。
避坑指南
- 浅拷贝问题:
- 问题:默认的拷贝操作是浅拷贝,对于包含指针的类会导致多个对象共享同一资源。
-
解决:实现深拷贝,为指针成员分配新内存并复制内容。
-
自赋值问题:
- 问题:
obj = obj这样的自赋值可能导致资源泄漏。 -
解决:在拷贝赋值函数中添加自赋值检查。
-
异常安全问题:
- 问题:拷贝操作中如果抛出异常,对象可能处于不一致状态。
-
解决:使用拷贝并交换 (copy-and-swap) 惯用法。
-
忘记 Rule of Three/Five:
- 问题:定义了析构函数但忘记定义拷贝操作。
-
解决:如果需要自定义析构函数,通常也需要定义拷贝操作(或禁用它们)。
-
不必要的拷贝:
- 问题:在 C ++11 之前,某些情况下无法避免拷贝。
- 解决:使用移动语义和右值引用来优化。
总结与思考题
理解拷贝构造函数和拷贝赋值函数的调用时机是掌握 C ++ 对象生命周期管理的关键。通过合理实现这些函数,并利用 C ++11 的移动语义,我们可以写出更高效、更安全的代码。
思考题:
1. 在现代 C ++ 中,什么时候应该禁用拷贝操作?
2. 移动语义如何改变了我们设计资源管理类的方式?
3. 为什么说拷贝并交换 (copy-and-swap) 惯用法能同时解决异常安全和自赋值问题?
