系列文章目录

构造函数超详解(特性+类型+初始化方式+易错避坑)

析构函数超详解(误区、语法、调用时机、析构顺序)

拷贝构造函数超详解(浅拷贝、深拷贝、拷贝构造 vs 赋值运算符重载、易错点)

赋值运算符重载 operator=超详解(深浅拷贝、自赋值判断、连续赋值原理、三法则、完整示例代码)

【C++初阶】const 成员函数 & 取地址运算符重载超详解(六大默认成员函数收官篇)



前言

很多新手分不清"拷贝构造"和"赋值重载",写带动态内存的类(Stack栈)时,直接使用编译器默认赋值会导致程序崩溃。

本篇文章将为大家详细讲解赋值重载的知识内容。


一、什么是赋值运算符重载

1. 核心定义

operator=赋值运算符针对自定义类对象时,编译器会调用赋值重载成员函数完成对象之间的数据拷贝。
语法:类名& operator=(const 类名& 源对象)

2. 核心区分:拷贝构造 VS 赋值重载(重点)

二者区分:

  • 拷贝构造:新对象创建阶段,用已有对象初始化(如:Date d2 = d1; / Date d2(d1);),此时d2还不存在;
  • 赋值重载:两个都已经存在的对象,把右边对象数据覆盖给左边对象(如:d2 = d1;),d1d2提前都定义好了。

代码:

// Date日期类已提前创建好
Date d1(2025,1,1);
Date d2 = d1;  // 拷贝构造:d2刚创建
Date d3(2000,1,1);
d3 = d1;       // 赋值重载:d3、d1均已存在,覆盖赋值

关键规则:

  1. operator=只能重载为类成员函数,不能写成全局函数;
  2. 若不显式写赋值重载,编译器会自动生成默认赋值重载
  3. 默认赋值重载行为:内置类型逐字节浅拷贝,自定义成员自动调用自身赋值重载;
  4. 类持有堆内存、文件句柄等资源时,默认浅拷贝会崩溃,必须手写深拷贝赋值

二、标准赋值重载完整规范写法

标准格式:

类名& operator=(const 类名& src)
{
    // 1. 判断自赋值:防止自己赋值自己
    if (this != &src)
    {
        // 2 释放当前对象旧资源(指针类必须写)
        // 3 开辟新资源,拷贝src数据
        // 4 拷贝普通内置成员
    }
    // 5 返回*this,支持连续赋值
    return *this;
}

解释 4 个强制规范:

  1. 返回值是类引用 类名&
    目的:支持连续赋值 d3 = d2 = d1;,如果返回值是void无法链式赋值;返回值不加引用会产生临时对象,效率低。
  2. 参数为const 类名&
    引用避免传值触发拷贝构造;const保证不会修改源对象,安全通用。
  3. 必须判断this != &src(自赋值检测)
    防止d1 = d1这种场景下,提前释放自身资源,后续无数据可拷贝。
  4. 结尾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)判断:

  1. 先执行free(_a)把自己的堆内存释放;
  2. 再去拷贝st的数据,此时st._a已经是空指针;
  3. memcpy访问空指针,直接会程序崩溃(如果你的程序没有直接崩溃,那只是侥幸,代码还是存在严重未定义行为)。

加上判断后,识别是自身赋值,直接return *this,跳过释放、拷贝逻辑,更加安全。

六、连续赋值原理:为什么返回引用?

内置类型支持链式赋值:

int a,b,c;
c = b = 10;

自定义类同样依靠return *this实现:

Date d1(2024,1,1),d2,d3;
d3 = d2 = d1;

执行顺序:

  1. 先执行d2 = d1operator=返回d2自身引用;
  2. 等价于d3 = (d2 = d1),把返回的d2赋值给d3

如果返回值是Date(值而非引用),每次赋值都会创建临时对象,大量拷贝,效率低。

七、类设计黄金三法则

总结:如果一个类显式实现析构释放资源,那么必须同时手写拷贝构造 + 赋值重载

  • Stackfree析构 → 必须深拷贝拷贝构造 + 深拷贝赋值重载;
  • 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 成员函数与取地址运算符重载,欢迎点赞收藏关注!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐