【C++初阶】赋值运算符重载 operator=超详解(深浅拷贝、自赋值判断、连续赋值原理、三法则、完整示例代码)
系列文章目录
拷贝构造函数超详解(浅拷贝、深拷贝、拷贝构造 vs 赋值运算符重载、易错点)
赋值运算符重载 operator=超详解(深浅拷贝、自赋值判断、连续赋值原理、三法则、完整示例代码)
【C++初阶】const 成员函数 & 取地址运算符重载超详解(六大默认成员函数收官篇)
文章目录
前言
很多新手分不清"拷贝构造"和"赋值重载",写带动态内存的类(Stack栈)时,直接使用编译器默认赋值会导致程序崩溃。
本篇文章将为大家详细讲解赋值重载的知识内容。
一、什么是赋值运算符重载
1. 核心定义
operator=赋值运算符针对自定义类对象时,编译器会调用赋值重载成员函数完成对象之间的数据拷贝。
语法:类名& operator=(const 类名& 源对象)
2. 核心区分:拷贝构造 VS 赋值重载(重点)
二者区分:
- 拷贝构造:新对象创建阶段,用已有对象初始化(如:
Date d2 = d1;/Date d2(d1);),此时d2还不存在; - 赋值重载:两个都已经存在的对象,把右边对象数据覆盖给左边对象(如:
d2 = d1;),d1、d2提前都定义好了。
代码:
// Date日期类已提前创建好
Date d1(2025,1,1);
Date d2 = d1; // 拷贝构造:d2刚创建
Date d3(2000,1,1);
d3 = d1; // 赋值重载:d3、d1均已存在,覆盖赋值
关键规则:
operator=只能重载为类成员函数,不能写成全局函数;- 若不显式写赋值重载,编译器会自动生成默认赋值重载;
- 默认赋值重载行为:内置类型逐字节浅拷贝,自定义成员自动调用自身赋值重载;
- 类持有堆内存、文件句柄等资源时,默认浅拷贝会崩溃,必须手写深拷贝赋值。
二、标准赋值重载完整规范写法
标准格式:
类名& operator=(const 类名& src)
{
// 1. 判断自赋值:防止自己赋值自己
if (this != &src)
{
// 2 释放当前对象旧资源(指针类必须写)
// 3 开辟新资源,拷贝src数据
// 4 拷贝普通内置成员
}
// 5 返回*this,支持连续赋值
return *this;
}
解释 4 个强制规范:
- 返回值是类引用
类名&
目的:支持连续赋值d3 = d2 = d1;,如果返回值是void无法链式赋值;返回值不加引用会产生临时对象,效率低。 - 参数为
const 类名&
引用避免传值触发拷贝构造;const保证不会修改源对象,安全通用。 - 必须判断
this != &src(自赋值检测)
防止d1 = d1这种场景下,提前释放自身资源,后续无数据可拷贝。 - 结尾
return *this
返回当前对象本体,满足连续赋值左操作数返回规则。
无资源类示例:Date 日期类Date只有int _year/_month/_day内置成员,没有指针、堆资源,编译器默认赋值即可:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int y=1900,int m=1,int d=1)
{
_year=y;_month=m;_day=d;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025,5,1);
Date d2(1999,12,12);
d2 = d1; // 调用编译器默认赋值重载
d2.Print(); // 输出2025-5-1
return 0;
}

此时编译器生成的默认赋值,会逐字节拷贝三个 int,就不需要手写operator=了。
三、带资源类默认赋值需要深拷贝(Stack 案例)
1. 浅拷贝带来的问题
Stack类中有_a指针指向堆开辟的数组,只用编译器默认赋值(浅拷贝)会程序崩溃:
#include <iostream>
typedef int STDataType;
class Stack
{
public:
Stack(int n=4)
{
_a = (STDataType*)malloc(sizeof(STDataType)*n);
_capacity = n;
_top = 0;
}
~Stack()
{
free(_a); // 析构释放堆内存
_a = nullptr;
}
void Push(int x){_a[_top++]=x;}
private:
STDataType* _a;
size_t _capacity,_top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
Stack st2;
st2 = st1; // 默认浅拷贝!
return 0;
}

崩溃原理:
默认赋值只拷贝指针变量的值,也就是st2._a = st1._a,两个对象指针指向同一块堆内存。(如下图监视窗口所示)
函数结束析构顺序:先析构st2,执行free(_a)释放内存;
再析构st1,再次free同一块已释放内存,导致内存重复释放,程序崩溃。
2. 解决方案:手写深拷贝赋值重载
Stack& operator=(const Stack& st)
{
// 自赋值判断
if (this != &st)
{
// 1 释放自身旧内存
free(_a);
// 2 开辟和源对象同等大小新空间
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(1); // 分配失败直接退出,防止后续memcpy空指针
}
// 3 拷贝所有数据
memcpy(_a, st._a, sizeof(STDataType) * st._top);
// 4 拷贝普通成员
_capacity = st._capacity;
_top = st._top;
}
return *this;
}

st2 = st1会创建独立堆数组,两个对象析构互不冲突,不会崩溃。
四、为什么赋值重载不能写成全局函数?
operator=只能是成员函数,禁止全局重载
原因:
编译器会为所有类默认生成成员版赋值重载,如果写全局operator=(Stack& left, const Stack& right),会出现两套同名赋值函数,重载冲突,编译报错。
像+、==、<<等运算符可以全局,唯独=、()、[]、&只能成员重载。
错误示范:
// 非法!赋值不能全局
Stack& operator=(Stack& left, const Stack& right)
{
// ...
}

五、自赋值判断到底有什么用?
很多新手觉得d1=d1不会有人写,没必要判断,实际存在间接自赋值场景:
Stack* p = &st1;
st1 = *p; // 等价于 st1 = st1,属于自赋值
如果没有if(this != &st)判断:
- 先执行
free(_a)把自己的堆内存释放; - 再去拷贝
st的数据,此时st._a已经是空指针; memcpy访问空指针,直接会程序崩溃(如果你的程序没有直接崩溃,那只是侥幸,代码还是存在严重未定义行为)。
加上判断后,识别是自身赋值,直接return *this,跳过释放、拷贝逻辑,更加安全。
六、连续赋值原理:为什么返回引用?
内置类型支持链式赋值:
int a,b,c;
c = b = 10;
自定义类同样依靠return *this实现:
Date d1(2024,1,1),d2,d3;
d3 = d2 = d1;
执行顺序:
- 先执行
d2 = d1,operator=返回d2自身引用; - 等价于
d3 = (d2 = d1),把返回的d2赋值给d3。
如果返回值是Date(值而非引用),每次赋值都会创建临时对象,大量拷贝,效率低。
七、类设计黄金三法则
总结:如果一个类显式实现析构释放资源,那么必须同时手写拷贝构造 + 赋值重载。
Stack有free析构 → 必须深拷贝拷贝构造 + 深拷贝赋值重载;Date无动态资源,编译器默认析构 → 编译器默认拷贝、默认赋值;MyQueue(两个栈实现队列)内部是Stack成员,默认赋值会调用Stack自定义赋值,析构也是调用Stack的析构,也不需要手写拷贝构造和赋值重载。
八、完整的Date + Stack示例
1. Date(无资源,使用默认赋值)
#include <iostream>
using namespace std;
class Date
{
public:
Date(int y = 1, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year, _month, _day;
};
int main()
{
Date d1(2026,7,5);
Date d2;
d2 = d1; // 使用默认赋值重载
d2.Print();
Date d3 = d2; // 拷贝构造
d3.Print();
return 0;
}
2. Stack(动态内存,手写深拷贝赋值)
#include <iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
_capacity = n;
_top = 0;
}
// 拷贝构造(深拷贝)
Stack(const Stack& st)
{
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(1); // 分配失败直接退出,防止后续memcpy空指针
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_capacity = st._top;
_top = st._top;
}
// 赋值重载(深拷贝)
Stack& operator=(const Stack& st)
{
if (this != &st)
{
free(_a);
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (_a == nullptr)
{
perror("malloc fail");
exit(1); // 分配失败直接退出,防止后续memcpy空指针
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_capacity = st._capacity;
_top = st._top;
}
return *this;
}
~Stack()
{
free(_a);
_a = nullptr;
}
void Push(int x)
{
_a[_top++] = x;
}
void print() {
for (size_t i = 0; i < _top; i++) {
cout << _a[i] << " ";
}
cout << endl;
}
private:
STDataType* _a;
size_t _capacity, _top;
};
int main()
{
Stack st1;
st1.Push(10);
st1.Push(20);
Stack st2 = st1;// 拷贝构造(深拷贝)
st2.print();
Stack st3;
st3 = st2; // 赋值重载(深拷贝)
st3.print();
return 0;
}
总结
下一篇博客讲解 const 成员函数与取地址运算符重载,欢迎点赞收藏关注!
更多推荐



所有评论(0)