拷贝构造函数语义与移动构造函数语义
一、拷贝构造函数语义
拷贝构造函数用于复制对象。当一个对象被复制时,编译器会根据特定条件生成默认的拷贝构造函数。
1. 编译器生成拷贝构造函数的条件
-
类中有类类型成员且没有自定义拷贝构造函数
-
示例:
class CTB { public: CTB() {} CTB(const CTB& other) { /* 复制逻辑 */ } }; class A { public: CTB member; // CTB类型成员 }; // A的拷贝构造函数会自动生成 A a1; A a2 = a1; // 调用A的拷贝构造函数
-
-
子类没有自定义拷贝构造函数且父类有拷贝构造函数
-
示例:
class CTB { public: CTB() {} CTB(const CTB& other) { /* 复制逻辑 */ } }; class CTBSon : public CTB { public: // 没有自定义拷贝构造函数 }; // CTBSon的拷贝构造函数会自动生成 CTBSon son1; CTBSon son2 = son1; // 调用CTBSon的拷贝构造函数
-
-
子类定义了虚函数
-
示例:
class Base { public: virtual void func() {} }; class CTBSon : public Base { public: // 没有自定义拷贝构造函数 }; // CTBSon的拷贝构造函数会自动生成 CTBSon son1; CTBSon son2 = son1; // 调用CTBSon的拷贝构造函数
-
-
类中含有虚基类
-
示例:
class VirtualBase { public: VirtualBase() {} VirtualBase(const VirtualBase& other) { /* 复制逻辑 */ } }; class A : public VirtualBase { public: // 没有自定义拷贝构造函数 }; // A的拷贝构造函数会自动生成 A a1; A a2 = a1; // 调用A的拷贝构造函数
-
2. 自定义拷贝构造函数
用户可以自定义拷贝构造函数以实现特定的复制逻辑。例如,可以选择深拷贝或浅拷贝。
class MyClass {
public:
int* data;
MyClass(int value) : data(new int(value)) {}
// 自定义拷贝构造函数
MyClass(const MyClass& other) {
data = new int(*other.data); // 深拷贝
}
~MyClass() {
delete data;
}
};
3. 深拷贝与浅拷贝
-
浅拷贝:仅复制指针的值,两个对象指向同一内存地址,可能导致悬空指针或双重释放的问题。
-
示例:
class ShallowCopy { public: int* data; ShallowCopy(int value) : data(new int(value)) {} // 浅拷贝的构造函数 ShallowCopy(const ShallowCopy& other) : data(other.data) {} ~ShallowCopy() { delete data; // 可能导致双重释放 } };
-
-
深拷贝:复制指针所指向的数据,确保每个对象都有自己的独立副本。
4. 拷贝构造函数的使用场景
- 对象传递:在函数参数中传递对象时,通常会调用拷贝构造函数。
- 返回对象:当函数返回一个对象时,拷贝构造函数会被调用。
5. 拷贝赋值运算符
拷贝构造函数和拷贝赋值运算符的逻辑通常相似,但它们的使用场景不同。拷贝赋值运算符用于将一个对象的值赋给另一个已存在的对象。
6. 规则五(Rule of Five)
如果自定义了拷贝构造函数,通常也需要自定义移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数,以确保资源管理的正确性。确保它们的实现不冲突,并且正确管理资源。
二、移动构造函数语义
移动构造函数用于转移对象的资源。编译器生成移动构造函数的条件更加严格。
1. 禁止生成移动构造函数
-
条件:如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数(任意一个),则编译器不会为该类生成移动构造函数和移动赋值运算符。
-
示例:
class MyClass { public: MyClass() {} MyClass(const MyClass& other) { /* 复制逻辑 */ } // 自定义拷贝构造函数 }; // MyClass不会生成移动构造函数 MyClass obj1; MyClass obj2 = std::move(obj1); // 错误:没有移动构造函数
2. 生成移动构造函数的条件
-
条件:只有在类未定义任何自定义版本的拷贝构造函数、拷贝赋值运算符和析构函数时,且类的每个非静态成员都可以移动,编译器才会为该类合成移动构造函数和移动赋值运算符。
-
示例:
class Movable { public: Movable() {} Movable(Movable&& other) noexcept { /* 移动逻辑 */ } }; class MyClass { public: Movable member; // 可移动 // 没有自定义拷贝构造函数、拷贝赋值运算符和析构函数 }; // MyClass会生成移动构造函数 MyClass obj1; MyClass obj2 = std::move(obj1); // 正常调用移动构造函数
3. 成员可以移动的定义
-
内置类型:如整型、浮点型等的成员变量可以移动。
-
类类型:如果成员变量是一个类类型,并且该类有对应的移动构造函数或移动赋值运算符,则该成员变量可以移动。
-
示例:
class Movable { public: Movable() {} Movable(Movable&& other) noexcept { /* 移动逻辑 */ } }; class NonMovable { public: NonMovable() {} NonMovable(const NonMovable& other) { /* 复制逻辑 */ } }; class MyClass { public: Movable movableMember; // 可移动 NonMovable nonMovableMember; // 不可移动 // 由于存在不可移动的成员,MyClass不会生成移动构造函数 }; // MyClass不会生成移动构造函数 MyClass obj1; MyClass obj2 = std::move(obj1); // 错误:没有移动构造函数
三、总结
- 拷贝构造函数主要用于复制对象,编译器会在特定条件下自动生成,确保对象的成员正确复制。
- 移动构造函数用于转移对象的资源,编译器的生成条件更为严格,主要依赖于类是否有自定义的拷贝相关函数和成员的可移动性。
四、补充内容
1. 拷贝与移动的区别
- 拷贝:创建一个对象的副本,所有成员都被复制,原对象和副本相互独立。
- 移动:转移资源的所有权,原对象的资源被转移到新对象,原对象处于有效但未定义的状态。
2. 使用场景
- 拷贝构造函数适用于需要保留原对象状态的情况,例如在需要多个相同对象的场景。
- 移动构造函数适用于临时对象或资源管理,能够减少不必要的内存开销,提高性能。
3. 实践建议
- 尽量使用移动语义以提高性能,尤其是在处理大型对象或动态分配资源时。
- 在自定义类时,如果定义了拷贝构造函数,考虑是否也需要定义移动构造函数,以便在需要时能够高效地转移资源。