前言

  • 在上一节的博客中,我们深入理解了类的四大默认成员函数
  • 这些成员函数堪称编译器自动生成的 “基础设施”,它们在幕后默默发挥着关键作用,妥善处理对象的初始化、清理和拷贝等重要操作

具体来说

  • 构造函数负责完成对象的初始化工作;
  • 析构函数则承担着释放对象所占用资源的重任;
  • 拷贝构造函数的存在有效避免了浅拷贝可能带来的陷阱;
  • 而赋值运算符重载函数能够巧妙解决自赋值问题。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

  • 在这一节中,我们将延续之前的讨论,继续深入探讨运算符重载相关的重要内容,带你一步一步理清C++运算符重载,赋值运算符重载和取地址运算符重载

一,运算符重载

1.1 什么是运算符重载

  • 在 C++ 里,运算符重载是一项很有用的特性。
  • 简单来说,就是让已有的运算符(像 +、-、== 这类)对自定义的类对象也能发挥作用。通过运算符重载,
  • 我们能让代码更简洁、更易读。

1.2 为什么需要运算符重载

  • 当你定义了一个新的类,比如 Date 类来表示日期。
  • 要是想比较两个日期是否一样,或者把两个日期相加,用常规方法可能会比较麻烦。
  • 运算符重载就能让你直接用 == 或者 + 这样的运算符去操作这些自定义类的对象,就跟操作内置类型(像 int、double)一样方便。
  • 不使用运算符重载的代码
不使用运算符重载
#include <iostream>

// 定义 Date 类
class Date {

public:
    int _year;
    int _month;
    int _day;
    // 构造函数
    Date(int y, int m, int d)  {
        _year = y;
        _month = m;
        _day = d;
    }
};
bool isEqual(const Date& d1, const Date& d2)
{
    return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main() 
{
    Date date1(2024, 7, 5);
    Date date2(2024, 7, 5);
    bool result = isEqual(date1, date2);
    if (result) {
        std::cout << "两个日期相等" << std::endl;
    }
    else {
        std::cout << "两个日期不相等" << std::endl;
    }
    return 0;
}
  • 使用运算符重载的代码

#include <iostream>

 //定义 Date 类
class Date {

public:
    int _year;
    int _month;
    int _day;
    // 构造函数
    Date(int y, int m, int d)  
    {
        _year = y;
        _month = m;
        _day = d;
    }

    bool operator == (const Date& other)
    {
        return _year ==other._year&& _month == other._month&& _day ==other._day;
    }
};
int main() {
    Date date1(2024, 7, 5);
    Date date2(2024, 7, 5);

    // 直接使用 == 运算符进行比较
    if (date1 == date2) {
        std::cout << "两个日期相等" << std::endl;
    }
    else {
        std::cout << "两个日期不相等" << std::endl;
    }
    return 0;
}

1.3具体对比区别

//不使用运算符重载
bool isEqual(const Date& d1, const Date& d2)
{
    return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main() 
{
    Date date1(2024, 7, 5);
    Date date2(2024, 7, 5);
    bool result = isEqual(date1, date2);
    if (result) {
        std::cout << "两个日期相等" << std::endl;
    }
    else {
        std::cout << "两个日期不相等" << std::endl;
    }
}
//使用运算符重载
bool operator == (const Date& other)
{
    return _year ==other._year&& _month == other._month&& _day ==other._day;
}
int main() {
    Date date1(2024, 7, 5);
    Date date2(2024, 7, 5);

    // 直接使用 == 运算符进行比较
    if (date1 == date2) {
        std::cout << "两个日期相等" << std::endl;
    }
    else {
        std::cout << "两个日期不相等" << std::endl;
    }
}

大家发现什么了吗?

使用运算符重载我们能让代码更简洁、更易读

1.4 运算符重载的具体调用过程(如何调用 operator== 函数)

date1 == date2 的调用过程

// 直接使用 == 运算符进行比较
    if (date1 == date2) {
        std::cout << "两个日期相等" << std::endl;
    }
    else {
        std::cout << "两个日期不相等" << std::endl;
    }
  • 当我们写 date1 == date2 时,C++ 会自动调用 Date 类中定义的 operator== 函数。
  • date1 是调用对象:
  • 在表达式 date1 == date2 中, date1 是调用 operator== 函数的对象。在函数内部, date1 对应的是 this 指针指向的对象
  • 因此, _year 、 _month 和 _day 是 date1 的成员变量。
  • date2 是参数对象:
  • date2 是传递给 operator== 函数的参数。在函数内部, date2 对应的是 other 参数。
  • 因此, other._year 、 other._month 和 other._day 是 date2 的成员变量

operator== 函数的内部逻辑 - 在 operator== 函数中

bool operator==(const Date& other) const {
    return _year == other._year && _month == other._month && _day == other._day;
}

  • _year 、 _month 和 _day 是当前对象( date1 )的成员变量
  • other._year 、 other._month 和 other._day 是传入对象( date2 )的成员变量。
  • 函数通过比较两个对象的成员变量来判断它们是否相等

总体调用过程

1. 创建对象

  • date1 和 date2 是两个 Date 类型的对象,它们的成员变量 _year 、 _month 和 _day 分别被初始化为 (2024, 7, 5)

2. 调用 operator==

  • 当执行 date1 == date2 时,C++ 会调用 Date 类中的 operator== 函数。
  • 在函数内部
  • date1 是调用对象,对应 this 指针指向的对象。
  • date2 是传入的参数对象,对应 other 参数。

3. 比较逻辑

  • 函数内部的比较逻辑是
return _year == other._year && _month == other._month && _day == other._day;

  • 因此,比较的结果是 true ,因为所有成员变量都相等
    在这里插入图片描述

1.5 运算符重载函数的参数

  • 重载运算符函数的参数数量和该运算符所作用的运算对象数量是一致的。
  • 一元运算符有一个参数,二元运算符有两个参数。
  • 对于二元运算符,左侧运算对象会被传递给第一个参数,右侧运算对象则传递给第二个参数。

什么意思呢?

意思是,当你重载一个运算符时,函数的参数数量必须与该运算符所作用的操作数数量一致
具体来说:

  • 一元运算符(如 ++ 、 – 、 ! 等):只有一个操作数,因此重载的函数只有一个参数
  • 二元运算符(如 + 、 - 、 == 、 < 等):有两个操作数,因此重载的函数有两个参数。

一元运算符的重载

  • 一元运算符只有一个操作数,因此重载的函数只有一个参数。这个参数通常是类的引用( const 或非 const )
class Counter {
public:
    int value;

    // 构造函数
    Counter(int v) {
        value = v;
    }

    // 前置递增运算符
    Counter& operator++() {
        ++value;
        return *this;
    }

    // 后置递增运算符
    Counter operator++(int) {
        Counter temp = *this; // 保存当前值
        ++(*this);           // 调用前置递增
        return temp;         // 返回原始值
    }
};

  • 二元运算符的重载

二元运算符有两个操作数,因此重载的函数有两个参数。通常,第一个参数是调用对象本身(通过 this 指针隐式传递),第二个参数是显式传递的对象。

class Date {
public:
    int year, month, day;

    Date(int y, int m, int d) {
        year = y;
        month = m;
        day = d;
    }

    // 重载 + 运算符
    Date operator+(const Date& other) const {
        return Date(year + other.year, month + other.month, day + other.day);
    }
};

1.6运算符重载的规则

优先级和结合性

  • 运算符重载之后,它的优先级和结合性与对应的内置类型运算符保持一致

不能创建新运算符

  • 不可以通过连接语法里不存在的符号来创建新的操作符,例如 operator@ 是不被允许的。

不能重载的运算符

  • .*、::、sizeof、?:、. 这 5 个运算符不能进行重载,这在选择题中经常出现,需要牢记。

类类型参数要求

  • 重载操作符至少要有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,像 operator+(int x, int y) 这种定义是不被允许的。
#include<iostream>
using namespace std;
// 编译报错:“operator +” 必须至少有一个类类型的形参
int operator+(int x, int y)
{
    return x - y;
}
  • 修改后代码
#include <iostream>
using namespace std;

class Number {
public:
    int value;

    // 构造函数
    Number(int v) {
        value = v;
    }

    // 重载 + 运算符
    Number operator+(const Number& other) const {
        return Number(value + other.value);
    }
};

二,赋值运算符重载

  • 在 C++ 里,赋值运算符重载属于默认成员函数,其用途是实现两个已存在对象之间的拷贝赋值.
  • 在C++中,赋值运算符 = 用于将一个对象的值赋给另一个对象
  • 注意将它和拷贝构造区分开来,拷贝构造是把一个对象拷贝初始化给另一个正在创建的对象。

咱们可以打个比方,拷贝构造就像是在建造一座新房子时,完全依照另一座已有的房子来建造而赋值运算符重载则是在两座已经建好的房子之间,把一座房子里的东西原封不动地搬到另一座房子里

2.1 为什么需要重载赋值运算符?

  • 默认的赋值运算符只是简单地将对象的成员变量逐个复制,这在某些情况下可能会导致问题。
  • 比如,当对象中包含指针成员时,如果只是简单地复制指针,会导致两个对象指向同一个内存地址,这可能会引发内存泄漏、重复释放等问题
  • 举个例子,我们对比一下

不重载赋值运算符的情况

class Box {
public:
    int value;

    Box(int v) { // 构造函数
        value = v;
    }
};

int main() {
    Box box1(10); // 创建box1,value为10
    Box box2(20); // 创建box2,value为20

    box2 = box1; // 使用默认的赋值运算符
    // 现在box2的value也被赋值为10
    return 0;
}

  • 在这个例子中, box2 = box1 会直接将 box1.value 的值复制给 box2.value ,这是简单的整数赋值。

重载赋值运算符的情况

class Box {
public:
    int value;

    Box(int v) { // 构造函数
        value = v;
    }

    Box& operator=(int v){
        value = v;// 将传入的整数赋值给成员变量
        return *this; // 返回当前对象的引用
    }

};
int main()
{
    Box box1(10); // 创建box1,value为10
    Box box2(20); // 创建box2,value为20
    box2 = 30; // 调用重载的赋值运算符,将30赋值给box2的value
    // 现在box2的value变为30
    return 0;
}


2.2 赋值运算符重载的好处和特点

赋值运算符重载是对赋值运算符 “=” 进行重新定义,以满足自定义类型对象之间赋值的特殊需求。它有以下好处和特点:

好处

  • 避免浅拷贝问题:对于包含动态分配资源(如动态数组、指针指向的内存等)的自定义类,默认的赋值操作是浅拷贝,即只复制指针而不复制指针指向的内容,这会导致多个对象指向同一块内存,在对象析构时可能造成多次释放内存或内存泄漏等错误。通过重载赋值运算符,可以实现深拷贝,为新对象分配独立的资源,从而避免这些问题 。
  • 实现特定的赋值逻辑:根据类的具体功能和业务需求,可以在重载的赋值运算符中定义特殊的赋值规则。比如,一个表示时间的类,在赋值时可能需要进行时间格式的统一转换等操作。
  • 增强代码的可读性和易用性:使自定义类对象的赋值操作像内置类型一样自然直观,方便程序员使用,也让代码更易理解。例如,在操作自定义的矩阵类时,重载赋值运算符后可以直接使用 “=” 进行矩阵间的数据传递,符合常规的编程习惯。

特点

  • 必须重载为成员函数:赋值运算符只能作为类的成员函数进行重载,这样可以直接访问类的私有和保护成员。同时,由于成员函数有一个隐含的 this 指针,代表当前对象,使得赋值操作针对当前对象进行。
  • 建议参数为 const 当前类类型引用:通常将参数写成 const 当前类类型引用(如 const MyClass& other),这样既可以避免传值传递带来的对象拷贝开销,提高效率,又能确保在函数内部不会意外修改传入的对象。
  • 有返回值且建议为当前类类型引用:返回值类型一般为当前类类型的引用(如 MyClass&),目的是支持连续赋值操作,如 a = b = c,并且引用返回可以避免返回对象时的拷贝构造,提高效率。
  • 编译器会自动生成默认版本:如果没有显式地实现赋值运算符重载,编译器会自动生成一个默认的赋值运算符重载函数。对于成员变量都是内置类型的简单类,默认的赋值运算符重载能完成值拷贝;但对于包含动态资源或有特殊需求的类,默认的赋值行为可能不符合要求,此时就需要自定义重载函数 。
  • 需进行自赋值检查:在重载函数中,通常需要检查是否是对象给自己赋值(this != &other)。因为自我赋值时,如果不进行特殊处理,可能会导致资源提前释放等问题,例如先释放了自己的资源再去拷贝自己的资源就会出错。

三,取地址运算符重载

3.1 const 成员函数

  • 在 C++ 里,要是你想让一个成员函数不修改类的任何成员变量,就可以用 const 来修饰这个成员函数。把 const 关键字放在成员函数参数列表的后面,就像这样:
void Print() const;

在上述代码里,Print 就是一个 const 成员函数。

3.2 const 对 this 指针的修饰

  • 在每个成员函数里,都有一个隐含的 this 指针,它指向调用该成员函数的对象

  • 对于普通的成员函数,this 指针的类型是 Date* const,这意味着 this 指针自身不能被修改,但它所指向的对象可以被修改

  • 而对于 const 成员函数,this 指针的类型就变成了 const Date* const,这表示 this 指针自身不能被修改,同时它所指向的对象也不能被修改

#include<iostream>
using namespace std;

class Date
{
public:
    // 构造函数,用于初始化日期对象
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // const 成员函数,用于打印日期信息
    void Print() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    // 创建一个非 const 对象
    Date d1(2024, 7, 5);
    // 非 const 对象调用 const 成员函数
    d1.Print();

    // 创建一个 const 对象
    const Date d2(2024, 8, 5);
    // const 对象调用 const 成员函数
    d2.Print();

    return 0;
}

在这里插入图片描述

  • 在这个示例中,Print 函数是一个 const 成员函数,这表明在这个函数里不能修改 Date 类的任何成员变量
  • 非 const 对象 d1 和 const 对象 d2 都能够调用 Print 函数,这是因为非 const 对象调用 const 成员函数属于权限的缩小,是被允许的

3.2 取地址运算符重载

3.2.1 取地址运算符重载的概念

  • 取地址运算符 & 也能够被重载,这可以让你自定义对象取地址时的行为。
    取地址运算符重载一般有两种
  • 普通取地址运算符重载和 const 取地址运算符重载
普通取地址运算符重载
  • 普通取地址运算符重载针对的是普通对象,它的返回类型是对象的指针。
Date* operator&()
{
    return this;
}

在上述代码中,operator&() 是普通取地址运算符重载函数,它返回 this 指针,也就是对象自身的地址

const 取地址运算符重载
  • const 取地址运算符重载针对的是 const 对象,它的返回类型是 const 对象的指针。下面是示例代码
const Date* operator&() const
{
    return this;
}

在上述代码中,operator&() const 是 const 取地址运算符重载函数,它返回 this 指针,也就是 const 对象自身的地址

3.2.2 完整示例代码

#include<iostream>
using namespace std;

class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    // 构造函数,用于初始化日期对象
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date* operator&()
    {
        return this;
    }
    const Date* operator&()const
    {
        return this;
    }
};

int main()
{
    // 创建一个非 const 对象
    Date d1(2024, 7, 5);
    // 取非 const 对象的地址
    Date* ptr1 = &d1;

    // 创建一个 const 对象
    const Date d2(2024, 8, 5);
    // 取 const 对象的地址
    const Date* ptr2 = &d2;



}

在这个示例中,我们重载了取地址运算符,普通对象 d1 调用普通取地址运算符重载函数,const 对象 d2 调用 const 取地址运算符重载函数
总结

  • const 成员函数可以保证在函数内部不会修改类的成员变量,const 对象只能调用 const 成员函数。
  • 取地址运算符重载能够自定义对象取地址时的行为,一般情况下使用编译器自动生成的就可以了,除非有特殊需求。

总结核心概念速记

运算符重载 = 重载规则 + 特殊运算符(赋值/取地址) + 实现逻辑

  • 重载本质
    • 自定义类型赋予内置运算符的新含义(如 Date==Date 比较日期)。
    • 不能重载.::sizeof?:.*5 大禁区)。
  • 赋值运算符
    • 深拷贝:避免浅拷贝导致的内存泄漏(如指针成员需重新分配空间)。
    • 三要素const 引用参数、返回自身引用、自赋值检查if (this==&other) return *this;)。
  • 取地址运算符
    • 普通版T* operator&())和const版const T* operator&() const),通常用默认实现,除非需隐藏地址。

运算符重载规则对比

规则项具体要求示例代码
参数数量一元运算符 1 个,二元运算符 2 个Date& operator++();(前置++,一元)
Date operator+(Date& d);(二元)
参数类型至少含一个类类型参数int operator+(int, int);Date operator+(Date, Date);
重载形式可作成员函数友元函数(赋值运算符必须是成员函数成员:bool operator==(Date&);
友元:friend bool operator<(Date&, Date&);
返回值通常为引用(提高效率)或自定义类型Date& operator=();(赋值)
bool operator==();(比较)

知识图谱

C++ 运算符重载(八)  
├─ **运算符重载基础**  
│  ├─ 定义:`operator+` 形式函数  
│  ├─ 作用:让自定义类支持运算符  
│  ├─ 规则:5 大不能重载运算符 + 类类型参数要求  
│  └─ 调用:`a+b` → `a.operator+(b)` 或友元函数  
├─ **赋值运算符重载**  
│  ├─ 场景:`obj1 = obj2`(非初始化赋值)  
│  ├─ 问题:默认浅拷贝 → 指针悬挂  
│  ├─ 实现:  
│  │   1. 自赋值检查  
│  │   2. 释放旧资源  
│  │   3. 深拷贝新资源  
│  │   4. 返回`*this`  
│  └─ 对比:**拷贝构造**(初始化) vs **赋值重载**(已初始化对象)  
├─ **取地址运算符重载**  
│  ├─ 普通版:`T* operator&() { return this; }`  
│  ├─ const版:`const T* operator&() const { return this; }`  
│  └─ 用途:自定义对象取地址行为(通常用默认)  
└─ **关键代码**  
   ├─ 赋值重载示例:  
   │   ```cpp  
   │   String& operator=(const String& s) {  
   │       if (this != &s) {  
   │           delete[] str;  
   │           str = new char[strlen(s.str)+1];  
   │           strcpy(str, s.str);  
   │       }  
   │       return *this;  
   │   }  
   │   ```
   └─ 取地址重载:  
       ```cpp  
       Point* operator&() { return this; }  
       const Point* operator&() const { return this; }  
       ```

重点提炼

  1. 运算符重载本质

    • 函数重载的一种形式,不改变运算符优先级和结合性
  2. 赋值运算符必须关注

    • 深拷贝:对含动态资源(如指针)的类,必须重载赋值运算符,避免多个对象共享同一内存。
    • 自赋值风险:先释放自身资源可能导致后续拷贝无数据可用(如 a = a 时)。
  3. 取地址重载场景

    • 通常无需自定义,除非需隐藏对象地址(如智能指针禁止直接取地址)。
  4. const 成员函数

    • const 修饰成员函数,确保不修改对象内容,const 对象只能调用 const 成员函数

赋值运算符 vs 拷贝构造函数

特征赋值运算符重载(operator=拷贝构造函数(ClassName(const ClassName&)
调用时机已存在对象的赋值(如 a = b;新对象的初始化(如 Class a(b);
参数const ClassName& (非初始化)const ClassName& (初始化)
返回值ClassName& (支持链式赋值)无返回值(构造函数特性)
默认实现编译器生成(浅拷贝)编译器生成(浅拷贝)
深拷贝需求含动态资源时必须自定义含动态资源时必须自定义

常见错误与解决方案

错误场景原因分析解决方案
浅拷贝导致内存重复释放默认赋值/拷贝构造复制指针值自定义深拷贝逻辑
忘记自赋值检查释放自身资源后无法拷贝自身在赋值重载中添加 if (this == &other)
const 对象无法调用非const函数非const成员函数修改对象状态将函数声明为 const 成员函数
重载不存在的运算符operator@仅重载 C++ 预定义运算符

关键代码模板

赋值运算符重载(深拷贝)

class MyClass {  
private:  
    int* data;  
public:  
    MyClass& operator=(const MyClass& other) {  
        if (this != &other) { // 自赋值检查  
            delete[] data;    // 释放旧资源  
            data = new int[other.size]; // 分配新资源  
            memcpy(data, other.data, sizeof(int)*size); // 深拷贝  
        }  
        return *this; // 支持链式赋值  
    }  
};  

取地址运算符重载(const 版本)

class ConstClass {  
public:  
    const int* operator&() const { return &value; } // const 对象取地址  
    int* operator&() { return &value; } // 普通对象取地址  
private:  
    int value;  
};  

面试高频问题

  1. 为什么赋值运算符必须定义为成员函数?

    答:因为赋值运算符需要修改当前对象的状态,通过 this 指针直接访问私有成员,友元函数无法保证对 this 的直接操作。

  2. 拷贝构造函数和赋值运算符的区别?

    答:拷贝构造用于初始化新对象,赋值运算符用于已存在对象的赋值;拷贝构造调用时对象尚未创建,赋值运算符调用时对象已存在。

  3. 什么时候需要自定义取地址运算符?

    答:当需要隐藏对象真实地址(如智能指针),或返回特殊指针(如代理对象)时。通常使用编译器默认实现即可。


以上就是这篇博客的全部内容,下一篇我们将继续探索更多精彩内容。

我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482

非常感谢您的阅读,喜欢的话记得三连哦

在这里插入图片描述

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐