C++ 拷贝构造函数与赋值运算符重载详解
引言
在 C++ 面向对象编程中,对象的复制操作无处不在。无论是函数传参、返回值传递,还是对象间的赋值,都需要精确控制数据的复制行为。
C++ 通过拷贝构造函数和赋值运算符重载两套机制,为开发者提供了对象复制的完整解决方案。本文将从基础概念出发,深入解析这两种复制机制的实现细节与应用技巧。
C++ 拷贝构造函数用于对象初始化,首个参数需为自身类型引用。编译器默认生成浅拷贝,含资源指针时需自定义深拷贝防止重复释放。赋值运算符重载用于已存在对象间复制,需检查自赋值并返回引用支持链式调用。两者均为特殊成员函数,分别对应初始化与赋值场景。掌握深浅拷贝机制是确保 C++ 对象生命周期管理与内存安全的关键。

在 C++ 面向对象编程中,对象的复制操作无处不在。无论是函数传参、返回值传递,还是对象间的赋值,都需要精确控制数据的复制行为。
C++ 通过拷贝构造函数和赋值运算符重载两套机制,为开发者提供了对象复制的完整解决方案。本文将从基础概念出发,深入解析这两种复制机制的实现细节与应用技巧。
如果一个构造函数的第一个参数是自身类型的引用,且 其他所有参数都有默认值(如果有) ,就叫做 拷贝构造,是特殊的构造函数。
#include <iostream>
using namespace std;
// 基本形式
class Example {
public:
Example(Example& d) {
// ...
}
};
(部分规则与构造函数相同)
const)。 如果使用传值的方式,在逻辑上会引发无穷递归调用;Date 类,成员变量全是内置类型且不指向资源,编译器默认生成的拷贝构造就够了。类似 Stack 类,虽然也都是内置类型,但是指针指向资源,那么编译器默认生成的浅拷贝/值拷贝就不太够,需要显式定义深拷贝。再对于 MyQueue 类,自定义类型 Stack 变量成员就直接调用它的拷贝构造。【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。
解释特点第 2 条:
当拷贝构造函数传值传参时,函数的形参是实参拷贝出来的新对象,要调用拷贝构造,但是拷贝构造函数也是传值传参就又要调用拷贝构造,这样无限循环下去……
其次,在引用传参最好加上 const,因为将对象传过来,也不会将对象进行改变操作,那么 const 就方便了传参(权限缩小)。当然,这时候传 const 对象也是可以的(权限平移)。
#include <iostream>
using namespace std;
// 传指针
class Date {
public:
// 构造函数:全缺省
Date(int day = 8, int month = 1, int year = 2026) {
_day = day;
_month = month;
_year = year;
}
// 指针传参
Date(Date* d) {
_day = d->_day;
_month = d->_month;
_year = d->_year;
}
void Print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main() {
// 调用构造函数初始化 d1
Date d1;
d1.Print();
// 传地址
Date d2(&d1);
d2.Print();
return 0;
}
拷贝构造函数就和构造、析构有点不同。它会对内置类型的成员变量进行处理。
类似 Date 类这样全是内置类型的变量,编译器默认生成的就够用;对于复杂结构的类 Stack,就要自定义深拷贝;对于 MyQueue 这样的类,不显式定义拷贝构造,编译器就会调用成员变量对应类的拷贝构造。
typedef int STDataType;
class Stack {
public:
Stack(int n = 4) {
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a) {
perror("malloc 申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
// Stack st2(st1);
Stack(const Stack& s) {
_a = s._a;
_capacity = s._capacity;
_top = s._top;
}
void Push(STDataType x) {
if (_top == _capacity) {
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (tmp == NULL) {
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack() {
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main() {
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
(会产生错误,通常是内存相关的问题。)
这样浅拷贝会使两个对象的指针变量都指向同一块空间,最后的两次析构就导致第二次析构对已经释放完的空间再次释放,发生错误。
Stack(const Stack& s) {
_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
if (_a == NULL) {
perror("realloc fail");
return;
}
memcpy(_a, s._a, s._top * sizeof(STDataType));
_capacity = s._capacity;
_top = s._top;
}
调试程序发现:不指向同一空间。
int& func1() {
int ret = 1;
return ret; // 返回的是 ret 的别名
}
Stack& func2() {
Stack st;
return st;
}
int main() {
int ret1 = func1(); // ret 在函数结束时就销毁了,所以这里存在错误
cout << ret1 << '\n'; // 可能是 1 或者随机值
Stack ret2 = func2(); // 调拷贝构造,但是 st 不存在
return 0;
}
根据特点 7,函数传值返回是会调用拷贝构造的,但是传引用返回不会。对于 st 这里,函数就是进行析构(成员函数),那么在通过返回的别名来访问 st 肯定是错的。
【所以,在传引用返回是一定要注意返回对象是否还存在!】
基本形式:
Example a;
Example b(a); // 调用拷贝构造函数
Example c = a; // 调用拷贝构造函数
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数:全缺省
Date(int day = 8, int month = 1, int year = 2026) {
_day = day;
_month = month;
_year = year;
}
// 拷贝构造函数
Date(Date& d) {
_day = d._day;
_month = d._month;
_year = d._year;
}
void Print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main() {
// 调用构造函数初始化 d1
Date d1;
// 不要写成 Date d1();
d1.Print();
// 创建对象的同时,调用拷贝构造进行初始化
Date d2(d1);
d2.Print();
return 0;
}
基本形式:
void func(Example obj) { ... }
Example a;
func(a); // 调用拷贝构造函数
注意: 调用函数,形参是用实参拷贝构造出来的新对象,将实参传递就符合调用拷贝构造的规则。(函数形参也是一个需要被创建的对象。)
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数:全缺省
Date(int day = 8, int month = 1, int year = 2026) {
_day = day;
_month = month;
_year = year;
}
// 拷贝构造函数
Date(Date& d) {
_day = d._day;
_month = d._month;
_year = d._year;
}
void Print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
void func(Date d) {
d.Print();
}
int main() {
// 调用构造函数初始化 d1
Date d1;
d1.Print();
func(d1); // 调用拷贝函数
return 0;
}
operator 和后面的运算符构成,与普通函数一样,具有返回值、返回类型、参数、函数体等;this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个;.*、::、sizeof、? :、. 以上五个运算符不能重载;operator+(int x, int y);Date 类的 operator- 有意义,operator* 没有意义;operator++,无法很好的区分。C++ 规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分;<< 和 >> 时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象 <<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。【搭配举例】:
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int day = 8, int month = 1, int year = 2026) {
_day = day;
_month = month;
_year = year;
}
// 如果运算符重载载在类外实现,解决变量私有化一个方法:
// int Getyear(){return _year;}
// 或者在类内定义
// 比较 Date 类对象是否相等
bool operator==(const Date& d) {
// this 占第一个,显式第一个参数为左侧运算对象
// 第 4 条:默认第一个参数为 this 指针
return _day == d._day && _month == d._month && _year == d._year;
}
private:
int _day;
int _month;
int _year;
};
int main() {
Date d1(1, 1, 1);
Date d2;
cout << (d1 == d2) << endl; // 第 3 条
}
【额外注意】:
int Getyear(); 之类函数,获取成员变量;<< / >> 优先级较高,所以 ... == ... 要加括号。【介绍 .* 运算符】:C++ 不常用,了解
#include <iostream>
using namespace std;
void func1() {
cout << "void func()" << endl;
}
class A {
public:
void func2() {
cout << "A::func()" << endl;
}
};
int main() {
// 普通函数指针
void (*pf1)() = func1;
(*pf1)();
// A 类型成员函数的指针
void (A::* pf2)() = &A::func2;
A aa;
(aa.*pf2)(); // 这里就是使用的 .*
// (aa.*pf2)(&aa); 错误,this 指针不能显式出现参数。
return 0;
}
赋值运算符重载是一个默认成员函数,用于完成两个已存在的对象直接的拷贝复制,要和拷贝构造区分开。
const 当前类类型引用传参,当然传值传参会调用拷贝构造;Date 类,为内置类型成员且不指向任何资源,编译器默认生成的浅拷贝就够了。但是类似 Stack 类,有指向的资源,就需要自定义深拷贝。(这里和拷贝构造类似)
【技巧】:如果一个类显式实现了析构并释放资源,那么他就需要显式定义深拷贝,否则就不需要。
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int day = 8, int month = 1, int year = 2026) {
_day = day;
_month = month;
_year = year;
}
// 拷贝构造
Date(const Date& d) {
_day = d._day;
_month = d._month;
_year = d._year;
}
// 赋值重载
// d1 = d2
Date& operator=(const Date& d) {
if (this != &d) {
_day = d._day;
_month = d._month;
_year = d._year;
}
return *this; // 返回 d1 别名,不拷贝
}
void Print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _day;
int _month;
int _year;
};
int main() {
Date d1(1, 1, 1);
Date d2(d1); // 拷贝构造
Date d3 = d1; // 拷贝构造
d2.Print();
d3.Print();
Date d4;
d5 = d4 = d1; // 赋值重载
d4.Print();
return 0;
}
Date& operator=(const Date& d);为什么可以传引用返回?
在拷贝构造部分,有过说明'传值返回会发生拷贝',但是 this 不是这个函数的局部对象,不会销毁,额外的拷贝就很麻烦,没必要。拷贝构造函数与赋值运算符重载构成了 C++ 对象复制机制的核心支柱。它们分别负责对象初始化和对象赋值两种不同场景的复制需求。
掌握这些复制控制机制,不仅能写出更安全的代码,更能深入理解 C++ 对象生命周期的管理哲学。这是从 C++ 使用者迈向 C++ 设计者的重要一步。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online