c++ 拷贝构造函数、移动构造函数
在 C++ 中,如果不为类提供拷贝构造函数和移动构造函数,编译器会生成默认的版本。但这些默认行为可能引发严重的资源管理问题和性能问题,尤其是当类管理动态资源(如堆内存、文件句柄)时。以下是具体问题和场景分析:
一、不提供拷贝构造函数的问题
1. 浅拷贝导致双重释放(Double Free)
- 问题:若类中有指针成员指向动态分配的资源,默认的拷贝构造函数只会进行浅拷贝(复制指针值,而非资源本身)。多个对象将共享同一块内存,析构时会多次释放同一内存,导致程序崩溃。
- 示例:
class ShallowString { char* data; public: ShallowString(const char* str) { data = new char[strlen(str) + 1]; strcpy(data, str); } ~ShallowString() { delete[] data; } // 析构函数释放内存 }; int main() { ShallowString s1("Hello"); ShallowString s2 = s1; // 默认拷贝构造函数(浅拷贝) // s1 和 s2 的 data 指向同一内存 return 0; } // 程序崩溃:s1 和 s2 析构时重复 delete[] 同一内存!
2. 悬空指针(Dangling Pointer)
- 问题:若一个对象被拷贝后,原对象被销毁,新对象的指针成员将指向已释放的内存,访问它会导致未定义行为。
- 示例:
ShallowString s1("Hello"); { ShallowString s2 = s1; // 浅拷贝 } // s2 析构,释放 data 指向的内存 std::cout << s1.data; // s1.data 已是悬空指针,访问可能崩溃!
二、不提供移动构造函数的问题
1. 性能损失(退化为深拷贝)
- 问题:若未定义移动构造函数,当操作临时对象(右值)或显式调用
std::move
时,编译器会尝试调用拷贝构造函数。对于大型资源(如数组、容器),深拷贝会带来不必要的性能开销。 - 示例:
class HeavyResource { int* large_data; // 假设是大型数据 public: // 未定义移动构造函数 HeavyResource(const HeavyResource& other) { /* 深拷贝 */ } }; HeavyResource createResource() { HeavyResource res; // 假设初始化大型数据 return res; // 本应是移动构造,但退化为深拷贝 } HeavyResource r = createResource(); // 深拷贝大型数据,性能低下!
2. 无法利用右值优化
- 问题:标准库容器(如
std::vector
)在扩容时,会尝试移动元素而非拷贝。若类无移动构造函数,会强制深拷贝,导致性能下降。 - 示例:
std::vector<HeavyResource> vec; vec.push_back(HeavyResource()); // 临时对象本应移动,但退化为拷贝
三、默认生成的构造函数的规则
1. 拷贝构造函数的生成
- 如果未声明任何拷贝构造函数、移动构造函数、移动赋值运算符或析构函数,编译器会生成默认的拷贝构造函数(浅拷贝)。
- 如果声明了移动构造函数或移动赋值运算符,默认的拷贝构造函数会被删除。
2. 移动构造函数的生成
- 如果未声明任何拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符或析构函数,编译器会生成默认的移动构造函数(逐成员移动)。
- 如果声明了拷贝构造函数、拷贝赋值运算符或析构函数,编译器不会生成默认的移动构造函数。
四、解决方案:规则三/五法则
1. 规则三(拷贝语义)
- 如果类需要手动管理资源(如定义析构函数),则必须定义:
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
2. 规则五(扩展为移动语义)
- 若需要优化性能,可额外定义:
- 移动构造函数
- 移动赋值运算符
示例代码(安全实现):
class SafeString {
char* data;
public:
SafeString(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 拷贝构造函数(深拷贝)
SafeString(const SafeString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 移动构造函数(转移资源)
SafeString(SafeString&& other) noexcept : data(other.data) {
other.data = nullptr; // 置空源对象
}
// 析构函数
~SafeString() {
delete[] data;
}
};
五、拷贝构造函数、移动构造函数
1. 拷贝构造函数
定义
拷贝构造函数用于创建一个新对象,并将其初始化为另一个对象的副本。它的典型形式如下:
ClassName(const ClassName& other);
- 参数是一个常量引用(
const ClassName&
),表示源对象。 - 通常用于深拷贝,即复制源对象的所有资源。
使用场景
- 当对象通过值传递时(如函数参数或返回值)。
- 当对象被显式拷贝时(如
ClassName obj2 = obj1;
)。
示例
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
public:
// 普通构造函数
MyString(const char* str = "") {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 拷贝构造函数
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
std::cout << "Copy Constructor Called!" << std::endl;
}
// 析构函数
~MyString() {
delete[] data;
}
void print() const {
std::cout << data << std::endl;
}
};
int main() {
MyString str1("Hello");
MyString str2 = str1; // 调用拷贝构造函数
str1.print(); // 输出: Hello
str2.print(); // 输出: Hello
return 0;
}
输出
Copy Constructor Called!
Hello
Hello
解析
- 当
str2
被初始化为str1
的副本时,拷贝构造函数被调用。 - 拷贝构造函数执行深拷贝,确保
str2
拥有自己的资源副本。
2. 移动构造函数
定义
移动构造函数用于将资源从一个对象“移动”到另一个对象,而不是复制资源。它的典型形式如下:
ClassName(ClassName&& other) noexcept;
- 参数是一个右值引用(
ClassName&&
),表示源对象。 - 通常用于优化性能,避免不必要的深拷贝。
使用场景
- 当对象是临时对象(右值)时。
- 当使用
std::move
显式移动对象时。
示例
#include <iostream>
#include <cstring>
class MyString {
private:
char* data;
public:
// 普通构造函数
MyString(const char* str = "") {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 拷贝构造函数
MyString(const MyString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
std::cout << "Copy Constructor Called!" << std::endl;
}
// 移动构造函数
MyString(MyString&& other) noexcept {
data = other.data; // 转移资源
other.data = nullptr; // 置空源对象的资源
std::cout << "Move Constructor Called!" << std::endl;
}
// 析构函数
~MyString() {
delete[] data;
}
void print() const {
if (data) {
std::cout << data << std::endl;
} else {
std::cout << "Empty" << std::endl;
}
}
};
int main() {
MyString str1("Hello");
MyString str2 = std::move(str1); // 调用移动构造函数
str1.print(); // 输出: Empty
str2.print(); // 输出: Hello
return 0;
}
输出
Move Constructor Called!
Empty
Hello
解析
- 当
str2
被初始化为str1
的移动副本时,移动构造函数被调用。 - 移动构造函数将资源从
str1
转移到str2
,并置空str1
的资源。
3. 拷贝构造函数 vs 移动构造函数
特性 | 拷贝构造函数 | 移动构造函数 |
---|---|---|
参数类型 | const ClassName& | ClassName&& |
资源管理 | 深拷贝(复制资源) | 移动资源(转移资源) |
性能 | 较低(需要复制资源) | 较高(避免复制资源) |
使用场景 | 对象需要独立副本时 | 对象是临时对象或显式移动时 |
示例 | ClassName obj2 = obj1; | ClassName obj2 = std::move(obj1); |
4. 使用场景总结
-
拷贝构造函数:
- 当需要创建对象的独立副本时。
- 当对象通过值传递或返回时。
-
移动构造函数:
- 当对象是临时对象(如函数返回值)时。
- 当显式使用
std::move
移动对象时。 - 当需要优化性能,避免不必要的深拷贝时。
5.注意事项
-
深拷贝与浅拷贝:
- 如果类管理动态资源(如指针),必须实现深拷贝的拷贝构造函数。
- 如果没有实现深拷贝,可能会导致资源重复释放或内存泄漏。
-
移动语义:
- 移动构造函数通常标记为
noexcept
,以确保在标准库容器中使用时不会抛出异常。 - 移动后,源对象应处于有效但未定义的状态(通常置空资源)。
- 移动构造函数通常标记为
-
规则三/五法则:
- 如果类需要自定义拷贝构造函数、拷贝赋值运算符或析构函数,通常也需要自定义移动构造函数和移动赋值运算符(规则五法则)。
六、总结
问题场景 | 不提供拷贝构造函数 | 不提供移动构造函数 |
---|---|---|
资源管理 | 浅拷贝导致双重释放、悬空指针 | 无直接资源问题,但性能低下 |
性能影响 | 无额外性能损失(但逻辑错误更严重) | 右值操作退化为深拷贝,性能差 |
典型错误 | 程序崩溃、未定义行为 | 容器操作、临时对象处理效率低 |
解决方式 | 实现深拷贝的拷贝构造函数 | 实现移动语义并标记 noexcept |
核心原则:
- 管理动态资源的类必须实现拷贝构造函数和析构函数(规则三)。
- 需要高性能操作右值时,应实现移动构造函数(规则五)。
在 C++ 中,拷贝构造函数和移动构造函数是两种特殊的构造函数,用于控制对象的拷贝和移动行为。它们的主要区别在于:
- 拷贝构造函数:用于创建一个新对象,并将其初始化为另一个对象的副本(深拷贝)。
- 移动构造函数:用于将资源从一个对象“移动”到另一个对象,通常用于优化性能(避免不必要的深拷贝),适用于临时对象或显式移动的场景。