目录
-
-
- [13.1 概述(Overview)](#13.1 概述(Overview))
- [13.2 拷贝构造与拷贝赋值(Copy and Copy Assignment)](#13.2 拷贝构造与拷贝赋值(Copy and Copy Assignment))
- [13.3 `delete` 关键字](#13.3
delete关键字) - [13.4 移动构造与移动赋值(Move and Move Assignment)](#13.4 移动构造与移动赋值(Move and Move Assignment))
-
在 C++ 中, 特殊成员函数(Special Member Functions) 是编译器会在特定条件下自动生成的类成员函数,用于处理对象的核心操作(如创建、复制、移动、销毁等)。
13.1 概述(Overview)
特殊成员函数共 6 个,均与对象的 "生命周期管理" 和 "值传递语义" 直接相关,编译器默认生成的前提是:类中未显式定义该函数,且程序中需要用到它(即 "按需生成")。
我们不必写出其中任何一个!它们都有自动生成的默认版本! 6 个特殊成员函数包括:
| 构造函数 | 表达 |
|---|---|
| 默认构造函数(Default constructor) | T() |
| 析构函数(Destructor) | ~T() |
| 拷贝构造函数(Copy constructor) | T(const T&) |
| 拷贝赋值运算符(Copy assignment operator) | T& operator=(const T&) |
| 移动构造函数(Move constructor) | T(T&&) |
| 移动赋值运算符(Move assignment operator) | T& operator=(T&&) |

默认构造函数:不接收参数创建一个新对象。
拷贝构造:创建一个新对象,作为另一个对象的逐成员副本。
cpp
Widget widgetOne;
Widget widgetTwo = widgetOne; // 此时复制构造函数被调用
拷贝赋值运算符:将一个已存在的对象分配给另一个对象。
cpp
Widget widgetOne;
Widget widgetTwo;
widgetOne = widgetTwo
// 请注意,这里两个对象都是在使用 = 运算符之前构建的。
析构函数:当对象超出作用域时调用。
13.2 拷贝构造与拷贝赋值(Copy and Copy Assignment)
二者均实现 "深拷贝" 语义(编译器默认是 "浅拷贝",需手动优化),核心是 "用一个已存在的对象,创建 / 赋值给另一个对象"。
| 类型 | 功能 | 函数原型示例(假设类名为MyClass) |
|---|---|---|
| 拷贝构造函数 | 用已有对象初始化新对象(如MyClass a = b;) | MyClass(const MyClass& other); |
| 拷贝赋值运算符 | 用已有对象给已存在的对象赋值(如a = b;) | MyClass& operator=(const MyClass& other); |
cpp
template <typename T>
Vector<T>::Vector()
{
_size = 0;
_capacity = 4;
_data = new T[_capacity];
}
这里发生了两个步骤:首先是_size、_capacity 和_data 可能已经被默认初始化了。然后是对变量的赋值,这实际上使工作量加倍了。
初始化成默认值,再重新赋值,是低效的!
我们可以使用初始化列表一次性声明它们并为其赋予所需的值:
cpp
template <typename T>
Vector<T>::Vector() : _size(0), _capacity(4), _data(newT[_capacity]) { }
直接用预期值构造成员变量更快、更高效。
它适用于任何构造函数,甚至是带参数的非默认构造函数!
要是变量是不可赋值类型呢?
cpp
template <typename T>
class MyClass {
const int _constant;
int& _reference;
public:
// Only way to initialize const and reference members
MyClass(int value, int& ref) : _constant(value), _reference(ref) { }
};
当类成员变量是不可赋值类型(如const常量或引用)时,必须使用初始化列表来初始化,而不能在构造函数体内赋值,原因如下:
- 常量 (
const) 的特性 :const变量一旦创建就必须初始化,且之后不能被修改。如果在构造函数体内尝试为const int _constant赋值,这本质上是修改操作,违反了const的只读属性,编译器会报错。 - 引用 (
&) 的特性 :引用必须在创建时就绑定到一个对象,且之后不能再绑定到其他对象。如果在构造函数体内为int& _reference赋值,实际是给引用所指向的对象赋值,而不是完成引用的初始化绑定,这不符合引用的语法规则。 - 初始化与赋值的区别:初始化列表在对象成员创建时直接完成初始化,而构造函数体中的操作是在成员已经创建后的赋值。对于不可赋值类型,必须在创建的同时完成初始化,因此只能通过初始化列表实现。
- 编译器默认行为 :逐成员复制(浅拷贝 )------ 若类中包含指针、动态内存(如
new分配的空间),会导致 "double free"(重复释放内存)等错误,此时必须显式重写这两个函数,实现深拷贝。

很多时候,你会希望创建一个副本,它所做的不仅仅是复制成员变量。
深拷贝
浅拷贝:
a和b的指针指向了同一块内存,就是浅拷贝,只是数据的简单赋值;
深拷贝:
在拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源
是原始对象完整且独立的副本的对象
在这些情况下,你会希望用自己的实现来覆盖默认的特殊成员函数!
像任何函数一样,在头文件中声明它们,并在.cpp 文件中编写它们的实现!

这段代码是C++中Vector模板类的拷贝构造函数实现,用于实现对象的"深拷贝"(deep copy),解析如下:
- 这是一个模板类
Vector<T>的拷贝构造函数,参数为const Vector<T>& other,表示以常量引用的方式接收另一个Vector对象 - 初始化列表部分:
_size(other._size):将当前对象的大小设置为与被拷贝对象相同_capacity(other._capacity):将当前对象的容量设置为与被拷贝对象相同_data(new T[other._capacity]):为当前对象分配新的内存空间,大小与被拷贝对象的容量一致
- 循环部分:
- 通过for循环遍历被拷贝对象的数据
- 将
other._data[i]的值逐个复制到当前对象的_data[i]中
- 深拷贝的意义:
- 这种方式会创建新的内存空间并复制数据内容,而不是简单地共享指针
- 这样两个对象(当前对象和被拷贝的other对象)会拥有各自独立的数据副本
- 避免了浅拷贝可能导致的双重释放(double free)问题,以及一个对象修改数据影响另一个对象的问题
图示中两个对象的data指针指向不同的内存地址,但存储的内容相同,直观展示了深拷贝的特点。
13.3 delete 关键字
delete 用于禁止编译器生成特定的特殊成员函数,本质是 "主动关闭某种语义",避免程序中出现不安全的对象操作。
使用场景 :当类不应该支持复制 / 移动时(如单例类、管理独占资源的类),显式用delete禁用对应的函数。
示例:
cpp
class NoCopy {
public:
// 禁止复制构造和复制赋值
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
// 允许默认构造
NoCopy() = default;
};
- 注意:
delete也可用于普通函数(如禁止特定参数类型的重载),但核心用途是控制特殊成员函数。
将一个特殊成员函数设为删除状态会移除其功能!

cpp
vector<int> func(vector<int> vec0) { // 拷贝构造函数
vector<int> vec1; // 默认构造函数
vector<int> vec2(3); // 自定义构造函数,不是SMF
vector<int> vec3{3}; // 统一初始化,不是SMF
vector<int> vec4(); // 函数定义
vector<int> vec5(vec2); // 拷贝构造函数
vector<int> vec6{}; // 统一初始化,初始化列表是空的
vector<int> vec7{static_cast<int>(vec2.size() + vec6.size())}; // 统一初始化
vector<int> vec8 = vec2; // 拷贝构造函数
vec8 = vec2; // 拷贝赋值函数
return vec8; // 拷贝构造函数
}
13.4 移动构造与移动赋值(Move and Move Assignment)
13.4.1 左值引用和右值引用
cpp
void foo(Photo pic) {
Photo beReal = pic; // pic是左值
Photo insta = takePhoto(); // takePhoto()是右值
}
void foo(Photo pic) {
Photo* p1 = &pic; // pic是左值,有栈上地址,&pic能获取有效地址
Photo* p2 = &takePhoto(); // ❌ Doesn't work!右值不能用&引用,报错:takePhoto()是右值,无持久地址,&操作符无效
}
判断等号右边的是左值or右值:
cpp
int a = 4; // 右
int& b = a; // 左
vector<int> c = {1, 2, 3}; // 右
int d = c[1]; // 左
int* e = &c[2]; // 右
size_t f = c.size(); // 右
左值的生命周期持续到作用域结束,是持久的。
右值的生命周期持续到行尾,是临时的。
如果我们有一个左值,如何避免复制它的内存?左值引用
cpp
void upload(Photo pic);
int main() {
Photo selfie =takePhoto(); // selfie is lvalue
upload(selfie); // 🤦 Unnecessary copy is made here
}
// 通过按引用传递!
void upload(Photo& pic);
int main() {
Photo selfie = takePhoto(); // selfie is lvalue
upload(selfie); // ✅ No copy is made here
}
如果我们有一个右值,如何避免复制它的内存?
cpp
void upload(Photo& pic);
int main() {
upload(takePhoto()); // Does this work?
}
// ❌ candidate function not viable: expects lvalue as 1st argument
// 用&&实现右值引用!
void upload(Photo&& pic);
int main() {
upload(takePhoto());
}
左值引用
- 语法:Type&
- 持久存在,函数终止后必须保持对象处于有效状态
右值引用
- 语法:Type&&
- 临时的,我们可以窃取(移动)其资源
- 对象可能会处于无效状态,但没关系!因为它是临时的!

13.4.2 移动语义
我们之所以有移动语义(move semantics),是因为在某些情况下,我们将要获取的资源,其原所有者(original owner)已不再需要该资源。
cpp
Photo selfie = pic;
// copy persistent objects (e.g. variables)
// that might get used in the future
Photo selfie = takePhoto();
// move temporary objects (e.g return values)
// since we no longer need to use them
C++11 引入,用于实现 **"移动语义"**------ 核心是 "转移对象的资源所有权",而非复制资源,避免不必要的内存拷贝(提升性能)。
| 类型 | 功能 | 函数原型示例 |
|---|---|---|
| 移动构造函数 | 用 "即将销毁的临时对象" 初始化新对象(如函数返回值) | MyClass(MyClass&& other);(&&为右值引用) |
| 移动赋值运算符 | 用 "临时对象" 给已存在对象赋值 | MyClass& operator=(MyClass&& other); |
- 核心逻辑 :移动时,只 "窃取" 源对象的资源(如指针指向的内存),并将源对象的资源指针设为
nullptr(避免源对象析构时释放资源); - 编译器默认行为:若类中未显式定义移动函数,且未显式定义复制函数 / 析构函数,编译器会自动生成 "逐成员移动" 的版本;若显式定义了复制函数或析构函数,移动函数默认不会生成。
代码练习:


Photo selfie = takePhoto();
在这节课中,我们认为它发生两个行为:
1、调用拷贝构造函数,拷贝给selfie
2、调用析构函数,析构takePhoto()
事实上,不会真正调用拷贝构造函数和析构函数,原因是编译器会有返回值优化Return-value optimization (RVO)
函数的返回值是临时的,会在下一行代码执行前被销毁。
拷贝语义:

移动语义(避免了复制):assigment7/unique_ptr.h的移动构造函数处体现


比喻:

13.4.3 强制移动语义std::move
使用 std::move(x) 可将左值(l-value)x 转换为右值(r-value),以便你能直接获取它所拥有的资源。
以下代码使用拷贝语义,然而在这个场景下,原位置的元素 (elems[i - 1]) 在被拷贝后就不再被使用了(因为后续会被其他元素覆盖)。
因此,使用拷贝语义会造成不必要的性能开销。
cpp
void PhotoCollection::insert(const Photo& pic, int pos) {
for (int i = size(); i > pos; i--)
elems[i] = elems[i -- 1]; // Shuffle elements down
elems[i] = pic;
}
通过强制使用移动语义(例如使用std::move),可以避免不必要的拷贝操作,提高代码效率,尤其是在处理大型对象或进行频繁插入操作时。
cpp
void PhotoCollection::insert(const Photo& pic, int pos) {
for (int i = size(); i > pos; i--)
elems[i] = std::move(elems[i -- 1]);
elems[i] = pic;
}
注意,不要用std::move移动左值,否则会导致未知状态!
另一个例子:
cpp
class Photo {
public:
Photo::Photo(Photo&& other) {
keywords = other.keywords; // other是临时的,我们真的需要拷贝吗?
}
private:
std::vector<string> keywords;
};
// 使用std::move
class Photo {
public:
Photo::Photo(Photo&& other) {
keywords = std::move(other.keywords);
}
private:
std::vector<string> keywords;
};
std::move的本质:它本身并不执行任何 "移动" 操作,只是将左值(lvalue)强制转换为右值引用(rvalue reference),其内部实现类似:
static_cast<typename std::remove_reference<T>::type&&>(t)
风险提示 :和const_cast一样,使用std::move是一种 "主动选择" 可能有风险的行为。因为被移动后的对象可能处于未定义状态,如果继续使用会导致不可预期的错误。
使用建议 :除非有充分理由(如确实需要优化性能,且明确知道被移动的对象不会再被使用),否则应避免显式使用std::move。
13.4.4 完美转发std::forward()
在 C++11 之前,泛型函数在传递参数时无法保持参数的原始类型 (左值或右值), 导致额外的拷贝或移动操作。完美转发 (Perfect Forwarding) 是一种高效传递参数的技术,能够保持参数的原始特性,避免额外的性能开销。
完美转发是指在泛型模板函数中,以参数的原始形式 (左值或右值) 传递给目标函数,从而避免不必要的拷贝或移动操作。
cpp
#include <iostream>
using namespace std;
void process (int& x) { cout << "Lvalue reference:"<< x << endl;}
void process (int&&x) ( cout << "Rvalue referendCe:"<< x <<endl;
// 泛型函数,使用完美转发
template <typename T>
void forwardExample(T&& arg) { // 万能引用 = 右值引用 + 模板
process(std::forward<T>(arg));// 关键:std::forrward 保持原始类型
}
int main (){
int a = 10;
forwardExample (a); // 传递左值
forwardExample (20); // 传递右值
return 0;
}
std::forward<T>(arg)通过引用折叠 和类型推导决定参数是否应该保留右值特性。
T类型 |
T&&推导后 |
std::forward(arg)结果 |
|---|---|---|
| int | int&& | 右值int&& |
| int&左值 | int& && -> int& | 左值int& |
| int&&右值 | int&& &&-> int&& | 右值int&& |
应用
cpp
// 传递构造函数参数
class MyClass {
public:
template <typename T>
MyClass (T&& arg) : data(std::forward<T>(arg)) {}
private:
int data;
};
// std::forward<T>(arg) 确保 arg 以最佳方式传递给 data, 避免不必要的拷贝
///////////////////////////////////////////////////////////////////////////
// 传递函数参数
#include <utility>
void print (const std::string& s){
std::cout << "Lvalue:"<< s << std::endl;
void print (std::string&& s){
std::cout << "Rvalue:"<< s << std::endl;
// 通过完美转发调用 print
template <typename T>
void callPrint(T&& arg){
print(std::forward<T>(arg));
}
std::move和std::forward区别
1、应用场景(语义)
std::move 用于移动语义,从而实现资源转移
std::forward 用于完美转发,使目标函数接收到的实参与被传递给转发函数的实参保持一致 (值类别)
2、转换右值
std::move无条件的将变量 (const 变量除外) 转换为右值引用
std::forward有条件的 (变量被右值初始化时)将变量转换为右值引用
零原则
"零规则"(Rule of Zero)是 C++ 中的一个重要设计原则,核心思想是:当类不需要手动管理资源时,应避免定义任何特殊成员函数(SMF),包括构造函数、析构函数、拷贝构造函数和拷贝赋值运算符等。
cpp
class a_string_with_an_id() {
public:
/// getter and setter methods for our private variables
private:
int id;
std::string str;
}
a_string_with_an_id object;
在你提供的示例中,a_string_with_an_id类包含两个成员变量:int id和std::string str。这两个类型都是 C++ 标准库中的基础类型或已正确实现资源管理的类型:
int是基本数据类型,无需特殊资源管理std::string内部已经妥善实现了所有必要的特殊成员函数,能够自动管理字符串内存
因此,根据零规则,a_string_with_an_id类不需要显式定义任何特殊成员函数。编译器会自动生成默认版本,这些默认版本会正确调用成员变量的相应函数(如std::string的拷贝构造函数),从而实现正确的资源管理。
这样做的好处是:
- 减少代码冗余,避免重复实现已有功能
- 降低出错风险,避免手动管理资源时可能出现的错误
- 使代码更简洁、更专注于类的核心功能
三原则
要理解C++中的"三法则(Rule of Three)",核心是抓住编译器默认函数的局限性 与手动内存管理的冲突,具体可拆解为以下逻辑:
1. 三法则的核心定义
"三法则"是C++类设计的基础准则:当一个类需要自定义析构函数(destructor) 时,它几乎必然也需要自定义拷贝构造函数(copy constructor)和**拷贝赋值运算符(copy assignment operator)**。这三个函数共同负责类的"资源管理",缺一不可。
2. 为什么需要自定义析构函数?
类需要自定义析构函数的核心场景是:类内部手动管理了动态资源 (比如用new分配的内存、打开的文件句柄、网络连接等)。
编译器生成的默认析构函数 只会做浅清理------它只会销毁类的成员变量本身(如指针变量),但不会释放指针指向的动态内存(或关闭资源)。若不自定义析构函数,会导致内存泄漏(动态内存永远无法回收)或资源泄漏(文件/连接一直占用)。
举个例子:
cpp
class MyString {
private:
char* data; // 手动管理动态内存的指针
public:
// 构造函数:用new分配内存
MyString(const char* s) {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
// 必须自定义析构函数:释放new的内存,否则内存泄漏
~MyString() { delete[] data; }
};
3. 为什么缺了拷贝构造/赋值会出问题?
当类有自定义析构函数(即手动管理资源)时,编译器生成的默认拷贝构造函数 和默认拷贝赋值运算符会执行"浅拷贝"------只复制成员变量的"值",而不复制其指向的动态资源。
这会导致两个严重问题:
**双重释放(double free)**:两个对象的指针指向同一块动态内存。当它们生命周期结束时,析构函数会被调用两次,试图释放同一块内存,触发程序崩溃。
**悬垂指针(dangling pointer)**:若一个对象释放了内存,另一个对象的指针会变成"悬垂指针"(指向已释放的内存),后续访问会导致未定义行为(程序崩溃、乱码等)。
用上面的MyString举例(未自定义拷贝函数的错误情况):
cpp
MyString s1("hello");
MyString s2 = s1; // 调用默认拷贝构造:s2.data = s1.data(浅拷贝,指向同一块内存)
// 生命周期结束时:s2先析构,释放data指向的内存;s1再析构,试图释放已释放的内存→崩溃
4. 总结:三法则的本质
"三法则"的本质是"资源管理的一致性":
自定义析构函数:负责"释放资源",避免泄漏;
自定义拷贝构造/赋值:负责"复制资源"(深拷贝),避免多个对象共享同一份资源导致的双重释放或悬垂指针。
编译器无法自动生成能处理"手动资源管理"的拷贝函数------它只能做浅拷贝,而浅拷贝在有动态资源的场景下必然出错。因此,只要需要自定义析构函数,就必须配套自定义另外两个拷贝相关的函数。
五原则
Rule of Five 是 C++ 中关于类资源管理的核心设计准则,本质是为了避免"资源泄漏""浅拷贝错误",并优化性能,其核心逻辑围绕"类是否管理了堆内存、文件句柄等非自动释放的资源"展开。
1. 核心背景:为何需要"五法则"?
C++ 编译器会为每个类默认生成 5 个特殊成员函数(默认构造、析构、拷贝构造、拷贝赋值、移动构造、移动赋值------实际是 6 个,默认构造单独归类)。但这些默认函数仅适用于"无资源管理"的简单类 (如仅包含 int、double 等基础类型的类)。
一旦类需要管理"资源"(如 new 分配的堆内存、打开的文件、网络连接等),默认函数的"浅拷贝""无资源释放"特性会导致严重问题:
- 默认析构函数不会释放堆内存 → 资源泄漏;
- 默认拷贝构造/赋值会直接复制指针(浅拷贝)→ 多个对象指向同一块内存,析构时重复释放 → 程序崩溃。
此时就需要手动定义部分特殊成员函数,而"五法则"则规定了:手动定义其中一个,就该考虑定义全部,尤其要补全移动语义相关函数以优化性能。
2. "五法则"包含的 5 个特殊成员函数
这 5 个函数均与"对象的创建、复制、移动、销毁"及"资源处理"直接相关,具体功能如下:
| 函数类型 | 核心作用 | 关键场景 |
|---|---|---|
| 析构函数(Destructor) | 释放类管理的资源(如delete堆内存、关闭文件),避免资源泄漏 |
对象生命周期结束时自动调用 |
| 拷贝构造函数(Copy Constructor) | 用一个对象"深拷贝"初始化新对象(复制资源本身,而非仅复制指针) | A a = b;或func(A a) |
| 拷贝赋值运算符(Copy Assignment Operator) | 用一个对象"深拷贝"赋值给另一个已存在的对象(先释放目标对象旧资源,再复制) | a = b;(a 已初始化) |
| 移动构造函数(Move Constructor) | "窃取"一个临时对象的资源(不复制,直接接管指针),避免无用拷贝 | 临时对象初始化新对象(如函数返回值) |
| 移动赋值运算符(Move Assignment Operator) | "窃取"一个临时对象的资源,赋值给已存在的对象 | a = func();(func 返回临时对象) |
3. 核心准则:"要一个,就尽量要全部"
- 强制逻辑 :若手动定义了"析构函数""拷贝构造""拷贝赋值"中的任意一个,说明类需要管理资源------此时编译器会取消默认的移动构造/移动赋值 。若不手动定义这两个移动函数,当代码中需要"移动对象"时(如临时对象传递),会退而使用"拷贝函数",导致不必要的深拷贝,性能下降。
- Optional的含义:移动构造/移动赋值的"可选",并非指"可以不定义",而是指"编译器不会强制报错"------但从性能和资源效率角度,只要类需要管理资源,就应该定义它们,避免冗余拷贝。
4. 一句话总结
当你的 C++ 类需要管理堆内存、文件等资源(不得不手动写析构/拷贝函数)时,务必补全移动构造和移动赋值函数------否则代码能跑,但会做无用功,变慢。