在 C++ 面向对象编程中,对象的复制操作无处不在。无论是函数传参、返回值传递,还是对象间的赋值,都需要精确控制数据的复制行为。C++ 通过拷贝构造函数和赋值运算符重载两套机制,为开发者提供了对象复制的完整解决方案。
一、拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且其他所有参数都有默认值(如果有),就叫做拷贝构造,它是特殊的构造函数。
1.1 解析:拷贝构造特点
拷贝构造函数遵循一些核心规则,理解这些规则能避免很多陷阱:
- 参数必须是引用:拷贝构造函数的第一个参数必须是自身类类型的引用(
类名&或const 类名&)。如果使用传值的方式,逻辑上会引发无穷递归调用——因为要创建形参对象本身就需要调用拷贝构造。 - 默认生成规则:若未显式定义拷贝构造,编译器会生成默认的拷贝构造函数。默认生成的拷贝构造对内置类型成员变量完成值拷贝(浅拷贝),对自定义类型成员变量则调用其拷贝构造。
- 深拷贝的必要性:如果类中包含指向资源的指针成员(如动态分配的内存),编译器默认的浅拷贝会导致多个对象共享同一块资源。析构时,同一块空间会被释放两次,引发程序崩溃。因此,一旦类显式实现了析构并释放资源,就必须显式定义深拷贝。
- 返回值的处理:传值返回会产生临时对象来调用拷贝构造;而传引用返回虽然避免了拷贝,但如果返回的是局部对象的引用,函数结束后该对象销毁,返回引用就会变成野指针。
为了直观理解浅拷贝的危害,我们可以看一个栈(Stack)的例子。如果只进行浅拷贝,两个对象的 _a 指针将指向同一块堆内存。当其中一个对象析构释放了内存,另一个对象再析构时就会发生重复释放错误。
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(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;
};
正确的做法是实现深拷贝,为新对象重新分配内存并复制数据:
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;
}
1.2 关键:拷贝构造的调用场景
拷贝构造函数主要在以下情况被隐式调用:
- 用一个对象初始化另一个同类的对象:例如
Example b(a);或Example c = a;。注意Example c = a;虽然语法像赋值,但在 C++ 中这实际上是拷贝构造,而非赋值运算符重载。 - 函数参数按值传递:当函数参数是该类的对象时,实参会先通过拷贝构造传递给形参。
- 函数按值返回:函数返回对象时,会创建一个临时对象并通过拷贝构造返回给调用者。
二、赋值运算符重载
赋值运算符重载用于完成两个已存在对象之间的拷贝复制,这与拷贝构造函数有本质区别:拷贝构造发生在对象创建时,而赋值发生在对象已创建后。
2.1 铺垫:运算符重载特点
C++ 允许通过运算符重载指定类类型对象使用运算符时的新含义。重载运算符本质上是一个特殊名字的函数,名称由 operator 和后面的运算符构成。
- 成员函数 vs 全局函数:如果是成员函数,左侧运算对象是
this指针,参数少一个。对于<<和>>这种流运算符,通常重载为全局函数,以便让ostream/istream作为第一个参数。 - 限制:不能改变内置类型对象的含义,也不能重载
.*、::、sizeof、? :、.等五个运算符。
2.2 核心:理解赋值运算符重载
赋值运算符重载必须为成员函数。实现时有几个关键点需要注意:
- 自引用检查:赋值前需判断
this != &d,防止自我赋值导致不必要的开销或逻辑错误。 - 返回引用:建议返回当前类类型的引用(
Date&),这样可以支持连续赋值,如d1 = d2 = d3;。 - 深拷贝一致性:如果类需要深拷贝,赋值运算符同样需要执行深拷贝逻辑,确保资源独立。
#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;
}
// 赋值重载
Date& operator=(const Date& d) {
if (this != &d) {
_day = d._day;
_month = d._month;
_year = d._year;
}
return *this;
}
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;
d4 = d1; // 调用赋值重载
d4.Print();
return 0;
}
总结
拷贝构造函数与赋值运算符重载构成了 C++ 对象复制机制的核心支柱。它们分别负责对象初始化和对象赋值两种不同场景的复制需求。掌握这些复制控制机制,不仅能写出更安全的代码,更能深入理解 C++ 对象生命周期的管理哲学。这是从 C++ 使用者迈向 C++ 设计者的重要一步。

