1、三之法则
如果一个类需要显式定义以下三个特殊成员函数中的任意一个,通常需要同时定义全部三个:
- 析构函数 (Destructor):释放资源(如
delete
动态内存)。 - 拷贝构造函数(Copy Constructor):定义深拷贝逻辑,避免多个对象共享同一资源。
- 拷贝赋值运算符(Copy Assignment Operator):处理赋值时的资源释放和深拷贝。
1.1 典型问题
若仅定义析构函数而未定义拷贝操作,默认的浅拷贝会导致两个对象共享同一资源。例如:
cpp
class Bad {
public:
Bad() : data(new int(0)) {}
~Bad() { delete data; } // 析构函数释放内存
// 未定义拷贝构造函数和赋值运算符
};
Bad a, b = a; // 默认浅拷贝,a.data 和 b.data 指向同一内存
// 析构时两次 delete 同一地址,导致未定义行为
1.2 解决方案
显式定义三个函数,确保资源深拷贝和正确释放:
cpp
class Good {
public:
Good() : data(new int(0)) {}
~Good() { delete data; }
Good(const Good& other) : data(new int(*other.data)) {} // 深拷贝
Good& operator=(const Good& other) {
if (this != &other) {
delete data;
data = new int(*other.data); // 深拷贝
}
return *this;
}
};
2、五之法则
C++11 及更高版本,引入移动语义后。
核心规则:如果一个类需要显式定义以下五个特殊成员函数中的任意一个,通常需要同时定义全部五个:
- 析构函数(同三法则)。
- 拷贝构造函数(同三法则)。
- 拷贝赋值运算符(同三法则)。
- 移动构造函数(Move Constructor):通过 "窃取" 资源(如转移指针所有权)避免深拷贝。
- 移动赋值运算符(Move Assignment Operator):高效转移资源而非复制。
典型优化:移动语义允许将临时对象的资源直接转移,避免不必要的深拷贝
cpp
class Efficient {
public:
Efficient() : data(new int(0)) {}
~Efficient() { delete data; }
// 拷贝操作(深拷贝)
Efficient(const Efficient& other) : data(new int(*other.data)) {}
Efficient& operator=(const Efficient& other) { /* 同三法则 */ }
// 移动操作(资源转移)
Efficient(Efficient&& other) noexcept : data(other.data) {
other.data = nullptr; // 置空源对象,防止重复释放
}
Efficient& operator=(Efficient&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
编译器行为:
- 若用户定义拷贝操作,编译器不会自动生成移动操作。
- 若用户定义移动操作,编译器会删除拷贝操作(标记为
=delete
)。 - 若用户定义析构函数,编译器不会自动生成移动操作,可能导致意外的深拷贝。
3、零之法则
现代 C++(推荐优先使用)。
核心规则 :尽量不手动定义任何特殊成员函数,而是通过 RAII(资源获取即初始化) 和标准库组件(如智能指针、容器)自动管理资源。
0 之法则的本质是 "资源管理与类的分离":
- 类的职责应聚焦于 "业务逻辑"(如数据聚合、行为封装),而非 "资源管理"。
- 若类需要使用资源(如动态内存),应通过资源管理类 (如
std::vector
、std::string
、std::unique_ptr
)间接持有资源,而非自己管理。 - 由于资源管理类已正确实现了三 / 五法则,外层类无需干预,编译器生成的默认特殊成员函数会自动调用成员的对应函数("逐成员操作"),行为正确。
3.1 适用场景与示例
适用场景
当类的所有成员都是 "自管理资源" 的类型(如内置类型、标准库容器、智能指针等),且类本身不直接持有需要手动释放的资源(如裸指针指向的动态内存、文件描述符)时,适用 0 之法则。
实现方式:将资源封装在具有完整语义的成员对象中,利用其自动生成的特殊成员函数。
cpp
#include <iostream>
#include <string>
#include <vector>
// 遵循0之法则:不声明任何特殊成员函数
class Student {
public:
// 仅包含自管理资源的成员
std::string name; // string管理动态内存
int age; // 内置类型(无资源)
std::vector<int> scores; // vector管理动态数组
};
int main() {
Student s1{"Alice", 18, {90, 85, 95}};
// 1. 拷贝初始化(调用编译器生成的拷贝构造函数)
Student s2 = s1; // s2.name、s2.scores均为s1的深拷贝(string和vector的拷贝是深拷贝)
std::cout << "s2.name: " << s2.name << ", s2.scores[0]: " << s2.scores[0] << std::endl;
// 2. 移动初始化(调用编译器生成的移动构造函数)
Student s3 = std::move(s1); // 资源从s1转移到s3,s1.name和scores变为空
std::cout << "s3.name: " << s3.name << ", s1.name: " << s1.name << std::endl;
// 3. 析构时:编译器生成的析构函数自动调用成员的析构函数(string和vector释放资源)
return 0;
}
Student
类未声明任何特殊成员函数,但编译器自动生成的版本完全正确:
- 拷贝时,
std::string
和std::vector
的深拷贝确保资源不冲突; - 移动时,资源高效转移,避免冗余拷贝;
- 析构时,成员的析构函数自动释放资源,无内存泄漏。
3.2 与 "资源管理类" 的配合
关键是 "将资源管理委托给专用类 "。例如,若需要管理动态内存,应使用std::unique_ptr
或std::shared_ptr
,而非裸指针:
cpp
#include <memory>
// 正确:通过智能指针管理资源,遵循0之法则
class ResourceUser {
public:
// 使用unique_ptr管理动态内存(资源管理委托给智能指针)
std::unique_ptr<int> data = std::make_unique<int>(42);
};
int main() {
ResourceUser u1;
ResourceUser u2 = std::move(u1); // 调用编译器生成的移动构造,unique_ptr转移所有权
// u1.data变为nullptr,u2.data持有资源,析构时自动释放
return 0;
}
分析 :ResourceUser
无需定义任何特殊成员函数:
std::unique_ptr
已实现正确的移动语义(禁止拷贝,支持移动);- 编译器生成的移动构造函数会调用
unique_ptr
的移动构造,实现资源安全转移; - 析构时,
unique_ptr
的析构函数自动释放内存,无泄漏。
4、构造函数生成规则
4.1 默认构造函数
默认构造函数是无参数 或 "所有参数均有默认值 " 的构造函数,用于无初始化器的对象创建(如 A a;
)。编译器仅在特定条件下生成,且行为受成员类型影响。
4.1.1 编译器生成默认构造函数的条件
当类没有任何用户定义的构造函数时,编译器自动生成 "合成默认构造函数"。
示例
cpp
#include <iostream>
#include <string>
using namespace std;
// 类中无任何用户定义的构造函数 → 编译器生成合成默认构造函数
class Person {
public:
// 成员变量(无初始化列表)
string name; // 类类型成员(string有默认构造函数)
int age; // 内置类型成员
};
int main() {
// 1. 局部对象:合成默认构造函数的行为
Person p1; // 调用编译器生成的默认构造函数
cout << "p1.name: " << p1.name << endl; // string默认初始化为空字符串 → 输出空
cout << "p1.age: " << p1.age << endl; // 内置类型局部对象未初始化 → 输出随机值(未定义行为)
// 2. 全局对象:内置类型会默认初始化(区别于局部对象)
static Person p2; // 全局/静态对象,内置类型成员会初始化为0
cout << "p2.name: " << p2.name << endl; // 空字符串
cout << "p2.age: " << p2.age << endl; // 0(全局对象特性)
return 0;
}
输出结果(局部对象 age 为随机值,全局对象 age 为 0):
p1.name:
p1.age: 32767 // 随机值(不同环境可能不同)
p2.name:
p2.age: 0
关键说明:合成默认构造函数对成员的初始化规则:
- 类类型成员(如
string
):调用其自身的默认构造函数; - 内置类型成员(如
int
):局部对象中不初始化 (值随机),全局 / 静态对象中初始化为 0; - 数组成员:对每个元素按上述规则初始化。
4.1.2 编译器不生成默认构造函数的情况
用户定义了任何构造函数(哪怕是带参数的)
cpp
#include <iostream>
using namespace std;
class Person {
public:
// 用户定义了带参数的构造函数 → 编译器不再生成默认构造函数
Person(string name, int age) : name(name), age(age) {}
void print() {
cout << "name: " << name << ", age: " << age << endl;
}
private:
string name;
int age;
};
int main() {
Person p1("Alice", 20); // 正确:调用用户定义的带参构造函数
p1.print(); // 输出:name: Alice, age: 20
// Person p2; // 错误:编译器未生成默认构造函数,无匹配的构造函数
return 0;
}
解决方案 :用 =default
显式要求编译器生成默认构造函数:
cpp
class Person {
public:
// 用户定义带参构造函数
Person(string name, int age) : name(name), age(age) {}
// 显式要求编译器生成默认构造函数(C++11起)
Person() = default; // 等价于编译器合成的默认构造函数
void print() {
cout << "name: " << name << ", age: " << age << endl;
}
private:
string name;
int age;
};
int main() {
Person p2; // 正确:调用显式生成的默认构造函数
p2.print(); // 输出:name: , age: 32767(局部对象age随机)
return 0;
}
类成员 / 基类没有默认构造函数
若类的成员或基类是 "无默认构造函数的类型",编译器无法生成合成默认构造函数(因无法初始化该成员 / 基类)。
cpp
#include <iostream>
using namespace std;
// 类B:无默认构造函数(只有带参构造函数)
class B {
public:
B(int x) : val(x) {} // 无默认构造函数
private:
int val;
};
// 类A:包含B类型成员,且无用户定义的构造函数
class A {
private:
B b; // B无默认构造函数 → 编译器无法生成A的合成默认构造函数
};
int main() {
// A a; // 错误:编译器无法初始化成员B(无默认构造函数)
return 0;
}
解决方案:用户定义 A 的构造函数,显式初始化 B 成员:
cpp
class A {
public:
// 用户定义构造函数,显式初始化B成员
A() : b(10) {} // 给B传参,调用B的带参构造函数
private:
B b;
};
int main() {
A a; // 正确:A的构造函数显式初始化B
return 0;
}
4.1.3 显式禁止默认构造函数(=delete)
若希望禁止类的默认初始化(如单例模式),可显式删除默认构造函数:
cpp
class Singleton {
public:
// 显式删除默认构造函数 → 禁止默认初始化
Singleton() = delete;
// 提供静态方法获取唯一实例
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
int main() {
// Singleton s; // 错误:默认构造函数已被删除
Singleton& s = Singleton::getInstance(); // 正确
return 0;
}
4.2 拷贝构造函数
拷贝构造函数用于 "用已有对象初始化新对象"(如 A a = b;
或 A a(b);
),原型通常为 A(const A& other)
。编译器生成的合成拷贝构造函数默认执行浅拷贝(逐成员复制)。
4.2.1 编译器生成拷贝构造函数的条件
当类没有用户定义的拷贝构造函数时,编译器自动生成 "合成拷贝构造函数"。
示例
cpp
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
// 无用户定义的拷贝构造函数 → 编译器生成合成拷贝构造函数
Person(string name, int age) : name(name), age(age) {}
void print() {
cout << "name: " << name << ", age: " << age
<< " (地址: " << this << ")" << endl;
}
private:
string name; // 类类型成员(string的拷贝构造是深拷贝)
int age; // 内置类型成员(直接复制值)
};
int main() {
Person p1("Bob", 25); // 调用带参构造函数
Person p2 = p1; // 调用编译器生成的合成拷贝构造函数(浅拷贝)
Person p3(p1); // 同上,拷贝初始化的另一种形式
p1.print(); // 输出:name: Bob, age: 25 (地址: 0x7ffeefbff4e0)
p2.print(); // 输出:name: Bob, age: 25 (地址: 0x7ffeefbff4f8) → 新对象,值相同
p3.print(); // 输出:name: Bob, age: 25 (地址: 0x7ffeefbff510) → 新对象,值相同
return 0;
}
关键说明:合成拷贝构造函数的 "浅拷贝" 逻辑:
- 对类类型成员(如
string
):调用其自身的拷贝构造函数(string
的拷贝是深拷贝,因此安全); - 对内置类型成员(如
int
):直接复制其值; - 对数组成员:逐元素复制(若数组元素是内置类型,仍是浅拷贝)。
4.2.2 合成拷贝构造函数的隐患
若类包含指针成员 (指向动态内存),合成拷贝构造函数的浅拷贝会导致两个对象的指针指向同一块内存,析构时触发 "双重释放"(未定义行为,通常导致程序崩溃)。
cpp
#include <iostream>
#include <cstring>
using namespace std;
class String {
public:
// 带参构造函数:动态分配内存
String(const char* str) {
len = strlen(str);
buf = new char[len + 1]; // 分配内存(+1存'\0')
strcpy(buf, str);
cout << "构造函数:分配内存 " << (void*)buf << endl;
}
// 析构函数:释放动态内存
~String() {
cout << "析构函数:释放内存 " << (void*)buf << endl;
delete[] buf; // 释放内存
}
void print() {
cout << "buf: " << buf << " (地址: " << (void*)buf << ")" << endl;
}
private:
char* buf; // 指针成员(指向动态内存)
int len;
};
int main() {
String s1("Hello"); // 调用构造函数,分配内存
String s2 = s1; // 调用合成拷贝构造函数(浅拷贝:s2.buf = s1.buf)
s1.print(); // 输出:buf: Hello (地址: 0x55f8d7a7a2a0)
s2.print(); // 输出:buf: Hello (地址: 0x55f8d7a7a2a0) → 与s1.buf指向同一块内存
// 程序结束时:先析构s2,释放0x55f8d7a7a2a0;再析构s1,再次释放同一地址 → 双重释放
return 0;
}
解决方案:
用户定义拷贝构造函数(深拷贝)
通过自定义拷贝构造函数,为新对象重新分配内存,避免指针指向同一块地址:
cpp
class String {
public:
String(const char* str) {
len = strlen(str);
buf = new char[len + 1];
strcpy(buf, str);
cout << "构造函数:分配内存 " << (void*)buf << endl;
}
// 自定义拷贝构造函数(深拷贝)
String(const String& other) {
len = other.len;
buf = new char[len + 1]; // 为新对象分配独立内存
strcpy(buf, other.buf); // 复制内容(而非指针)
cout << "拷贝构造函数:分配内存 " << (void*)buf << endl;
}
~String() {
cout << "析构函数:释放内存 " << (void*)buf << endl;
delete[] buf;
}
private:
char* buf;
int len;
};
int main() {
String s1("Hello"); // 构造:分配内存 0x55e7b9c6a2a0
String s2 = s1; // 拷贝构造:分配内存 0x55e7b9c6a2c0(独立内存)
// 析构时分别释放两块内存,无双重释放
return 0;
}
4.2.3 编译器不生成 / 删除拷贝构造函数的情况
用户定义了拷贝构造函数: 编译器不再生成合成版本,完全依赖用户定义的逻辑:
cpp
class A {
public:
A() {}
// 用户定义拷贝构造函数
A(const A& other) {
cout << "自定义拷贝构造函数" << endl;
}
};
int main() {
A a1;
A a2 = a1; // 调用用户定义的拷贝构造函数 → 输出:自定义拷贝构造函数
return 0;
}
定义了移动构造 / 移动赋值运算符: 为避免浅拷贝与移动语义冲突,编译器会隐式删除合成拷贝构造函数:
cpp
class A {
public:
A() {}
// 用户定义移动构造函数
A(A&& other) noexcept {
cout << "自定义移动构造函数" << endl;
}
};
int main() {
A a1;
// A a2 = a1; // 错误:合成拷贝构造函数被隐式删除(因定义了移动构造)
A a3 = std::move(a1); // 正确:调用移动构造函数
return 0;
}
4.3 移动构造函数
移动构造函数用于 "将源对象的资源转移到新对象"(避免拷贝开销),原型通常为 A(A&& other) noexcept
(右值引用参数)。编译器生成的合成移动构造函数默认执行浅移动(转移资源所有权)。
4.3.1 编译器生成移动构造函数的条件
当类没有用户定义的拷贝构造函数、拷贝赋值运算符、移动赋值运算符或析构函数时,编译器自动生成 "合成移动构造函数"。
cpp
#include <iostream>
#include <string>
using namespace std;
// 类中无用户定义的拷贝构造、拷贝赋值、移动赋值、析构 → 编译器生成合成移动构造函数
class Person {
public:
Person(string name, int age) : name(name), age(age) {
cout << "构造函数:" << name << endl;
}
void print() {
cout << "name: " << name << ", age: " << age << endl;
}
private:
string name; // string有移动构造函数(转移资源)
int age; // 内置类型:移动等价于拷贝(无资源)
};
int main() {
Person p1("Charlie", 30); // 构造:Charlie
// 用std::move将p1转为右值,触发合成移动构造函数
Person p2 = std::move(p1);
cout << "p2: ";
p2.print(); // 输出:name: Charlie, age: 30(资源转移到p2)
cout << "p1: ";
p1.print(); // 输出:name: , age: 30(p1的string被置空,age无资源)
return 0;
}
关键说明:合成移动构造函数的 "浅移动" 逻辑:
- 对类类型成员(如
string
):调用其移动构造函数(转移资源,源对象成员被置空); - 对内置类型成员(如
int
):直接复制值(因无资源所有权,移动与拷贝等价); - 对数组成员:逐元素移动(若元素是类类型且支持移动)。
4.3.2 编译器不生成移动构造函数的情况
用户定义了拷贝构造 / 拷贝赋值 / 移动赋值 / 析构
编译器认为用户可能需要自定义资源管理逻辑,因此不生成合成移动构造函数:
cpp
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person(string name) : name(name) {}
// 用户定义了析构函数 → 编译器不生成移动构造函数
~Person() {
cout << "析构函数:" << name << endl;
}
private:
string name;
};
int main() {
Person p1("David");
// 无移动构造函数,因此调用拷贝构造函数(而非移动)
Person p2 = std::move(p1); // 等价于 Person p2 = p1;
return 0;
}
类成员 / 基类无法移动
若成员或基类是 "无移动构造函数且无拷贝构造函数" 的类型,编译器会隐式删除合成移动构造函数:
cpp
class B {
public:
B() {}
// 删除移动构造和拷贝构造
B(const B&) = delete;
B(B&&) = delete;
};
class A {
private:
B b; // B无法移动 → A的合成移动构造函数被删除
};
int main() {
A a1;
// A a2 = std::move(a1); // 错误:A的移动构造函数被删除
return 0;
}
4.3.3 自定义移动构造函数(解决指针成员的移动)
若类包含指针成员,需自定义移动构造函数,转移资源所有权后将源对象指针置空(避免析构冲突):
cpp
#include <iostream>
#include <cstring>
using namespace std;
class String {
public:
String(const char* str) {
len = strlen(str);
buf = new char[len + 1];
strcpy(buf, str);
cout << "构造:" << (void*)buf << endl;
}
// 自定义移动构造函数
String(String&& other) noexcept : buf(other.buf), len(other.len) {
other.buf = nullptr; // 源对象指针置空,避免析构时重复释放
other.len = 0;
cout << "移动构造:" << (void*)buf << endl;
}
~String() {
if (buf) { // 仅当buf非空时释放
cout << "析构:" << (void*)buf << endl;
delete[] buf;
} else {
cout << "析构:空指针" << endl;
}
}
private:
char* buf;
int len;
};
int main() {
String s1("Hello"); // 构造:0x55d8b7a8a2a0
String s2 = std::move(s1); // 移动构造:0x55d8b7a8a2a0(转移资源)
// 析构s2:释放0x55d8b7a8a2a0;析构s1:buf为空,无释放
return 0;
}
4.4 比较
构造函数类型 | 生成条件(无用户定义以下函数) | 关键行为 | 常见问题与解决方案 |
---|---|---|---|
默认构造函数 | 任何构造函数 | 成员默认初始化(局部内置类型随机) | 需默认初始化时用=default ;成员无默认构造时显式初始化 |
拷贝构造函数 | 拷贝构造函数 | 浅拷贝(逐成员复制) | 指针成员需自定义深拷贝,避免双重释放 |
移动构造函数(C++11) | 拷贝构造、拷贝赋值、移动赋值、析构函数 | 浅移动(转移资源所有权) |