Big five
Cpp中的Big five指的是C++类中的5个重要的成员函数:
构造函数,析构函数,拷贝构造函数,复制赋值运算符,移动构造函数。
其中构造函数和析构函数通常是必须的,其余三个应根据需求实现。
1. 构造函数 (Constructor)
构造函数很简单,就是初始化对象的默认状态,若没有定义任何构造函数,编辑器就会提供一个默认构造函数。
2. 析构函数 (Destructor)
析构函数在对象生命周期结束时会自动调用,作用是释放资源。
3. 拷贝构造函数 (Copy Constructor)
拷贝构造函数是一种特殊的构造函数,其通过现有对象来初始化新对象。即用于初始化一个新对象,以另一个同类型对象的内容作为初始值。拷贝构造函数分为深拷贝和浅拷贝。
浅拷贝: 仅拷贝对象的指针成员,不拷贝指针所指向的资源。
原对象和拷贝对象共享同一块资源。
容易导致资源管理问题,如多次释放同一块内存。
适用于不需要独立资源的场景,如只读数据或资源由外部管理。
class Shallow { public: int* data; Shallow(int value) { data = new int(value); } // 默认的拷贝构造函数实现浅拷贝 Shallow(const Shallow& other) : data(other.data) {} ~Shallow() { delete data; } }; int main() { Shallow obj1(10); Shallow obj2 = obj1; // 浅拷贝 return 0; }
obj1
和obj2
都指向同一个内存地址。因此,修改obj2
的数据会影响到obj1
的数据。
深拷贝: 拷贝对象的指针成员,还拷贝指针所指向的资源。
原对象和拷贝对象拥有独立的资源。
避免了资源管理问题,如内存泄漏和多次释放同一块内存。
适用于需要独立资源的场景,如动态分配内存的类。
class Deep { public: int* data; Deep(int value) { data = new int(value); } // 自定义拷贝构造函数实现深拷贝 Deep(const Deep& other) { data = new int(*other.data); } ~Deep() { delete data; } }; int main() { Deep obj1(10); Deep obj2 = obj1; // 深拷贝 return 0; }
obj1
和obj2
指向不同的内存地址,因此修改obj2
的数据不会影响obj1
的数据。
4. 赋值操作符重载 (Assignment Operator Overloading) 或复制赋值运算符。
在Cpp中,默认的复制操作符 ' = ' 执行的是浅拷贝(即只是简单地复制对象的数据成员。如果对象包含动态分配的资源(如指针),这种浅拷贝可能会导致资源管理问题,如双重释放(double delete)或内存泄漏。),这在处理内存或资源时有可能导致问题,因此我们需要通过重载赋值操作符,可以定制对象赋值时的行为,确保正确管理资源,让对象在赋值时不会共享同一块内存。
赋值操作符重载确保赋值时正确释放旧资源并分配新资源,实现深拷贝:
class Deep { public: int* data; // 构造函数 Deep(int value) { data = new int(value); std::cout << "构造函数调用" << std::endl; } // 拷贝构造函数实现深拷贝 Deep(const Deep& other) { data = new int(*other.data); std::cout << "拷贝构造函数调用" << std::endl; } // 赋值操作符重载实现深拷贝 Deep& operator=(const Deep& other) { std::cout << "赋值操作符调用" << std::endl; if (this != &other) { // 避免自我赋值 delete data; // 释放已有资源 data = new int(*other.data); // 分配新资源并复制值 } return *this; } ~Deep() { delete data; std::cout << "析构函数调用" << std::endl; } }; int main() { Deep obj1(10); // 调用构造函数 Deep obj2(20); // 调用构造函数 obj2 = obj1; // 调用赋值操作符 std::cout << "obj1.data: " << *obj1.data << std::endl; //obj1.data: 10 std::cout << "obj2.data: " << *obj2.data << std::endl; //obj2.data: 10 *obj2.data = 30; // 修改obj2的数据 std::cout << "obj1.data: " << *obj1.data << std::endl; //obj1.data: 10 std::cout << "obj2.data: " << *obj2.data << std::endl; //obj2.data: 30 return 0; }
结果如下: obj1 构造函数调用 obj2 构造函数调用 赋值操作符调用,obj2 = obj1 析构函数调用,释放 obj2 的原始内存 obj1.data: 10 // 显示 obj1 的数据 obj2.data: 10 // 显示 obj2 的数据,应该与 obj1 的数据相同 After modifying obj2.data: obj1.data: 10 // 修改 obj2.data 后,obj1 的数据不变 obj2.data: 30 // 修改 obj2.data 后,显示修改后的值 析构函数调用,释放 obj1 的内存 析构函数调用,释放 obj2 的内存
若没有赋值操作符重载:
结果如下: obj1 构造函数调用 obj2 构造函数调用 浅拷贝obj2 = obj1 析构函数调用,释放 obj2 的原始内存 obj1.data: 10 // 显示 obj1 的数据 obj2.data: 10 // 显示 obj2 的数据,应该与 obj1 的数据相同 After modifying obj2.data: obj1.data: 30 // 修改 obj2.data 后,obj1 也被修改 obj2.data: 30 // 修改 obj2.data 后,显示修改后的值 析构函数调用,释放 obj1 的内存 析构函数调用,释放 obj2 的内存,试图释放已释放的内存
***注意:拷贝构造函数 Deep obj2 = obj1;
而赋值操作 obj2 = obj1;如果没有重载赋值操作符,Cpp会使用默认的浅拷贝。
不要混淆。
5.移动构造函数 (Move Constructor)
移动构造函数数是一种特殊的构造函数,用于“移动”资源,而不是“复制”资源。
使用一个临时对象 (右值) 来初始化/赋值 (而非拷贝) 对象,这避免了不必要的深拷贝操作。可以大大提高性能 (其不用像拷贝,需要new)。
右值引用(Rvalue Reference)是用 '&&' 表示的新引用类型,用于绑定到右值(临时对象)。这使得移动语义成为可能。
class MyClass {
public:
int a;
int b;
// 默认构造函数
MyClass(int value1 = 0, int value2 = 0)
: a(value1), b(value2) {
cout << "构造函数: 初始化值" << endl;
}
// 拷贝构造函数
MyClass(const MyClass& other)
: a(other.a), b(other.b) {
cout << "拷贝构造函数: 复制值" << endl;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept
: a(other.a), b(other.b) {
other.a = 0;
other.b = 0;
cout << "移动构造函数: 移动值" << endl;
}
// 拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) {
a = other.a;
b = other.b;
cout << "拷贝赋值运算符: 复制值" << endl;
}
return *this;
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
a = other.a;
b = other.b;
other.a = 0;
other.b = 0;
cout << "移动赋值运算符: 移动值" << endl;
}
return *this;
}
// 打印函数
void print() const {
cout << "a: " << a << ", b: " << b << endl;
}
};
int main() {
MyClass obj1(10, 20); // 调用构造函数
MyClass obj2 = std::move(obj1); // 调用移动构造函数
obj2.print();
// 检查 obj1 的状态
if (obj1.a == 0 && obj1.b == 0) {
cout << "obj1 已被移动,值已重置" << endl;
}
return 0;
}
std::move 函数用于将 obj1 转换为右值引用,从而触发移动构造函数。
构造函数: 初始化值
移动构造函数: 移动值
a: 10, b: 20
obj1 已被移动,值已重置
对于noexpect:
1. 如果编译器知道一个函数不会抛出异常,它可以进行一些优化,比如省略为处理异常所需的代码。这可以减少代码大小和运行时的开销。
2. 某些标准库容器(如 std::vector
)在内部使用移动构造函数和移动赋值运算符来管理元素。如果这些函数被标记为 noexcept
,容器可以更安全地执行内存管理操作,而不需要担心异常导致的资源泄漏或未定义行为。
3. 明确标记一个函数不会抛出异常,可以提高代码的安全性,帮助开发者更好地理解和维护代码。