C++ 基础入门篇(六)类和对象-第三篇:运算符重载

文章目录

  • [1. 运算符重载](#1. 运算符重载)
    • [1.1. 运算符重载的意义](#1.1. 运算符重载的意义)
    • [1.2 运算符重载应当有意义](#1.2 运算符重载应当有意义)
    • [1.3 运算符重载的定义](#1.3 运算符重载的定义)
    • [1.4 运算符重载的使用注意事项](#1.4 运算符重载的使用注意事项)
    • [1.5 运算符重载的创建](#1.5 运算符重载的创建)
      • [1.5.1 创建重载运算符" == "](#1.5.1 创建重载运算符“ == ”)
    • [1.6 运算符重载的调用](#1.6 运算符重载的调用)
    • [1.7 重载为成员函数的调用](#1.7 重载为成员函数的调用)
      • [1.7.1 隐式调用](#1.7.1 隐式调用)
      • [1.7.2 显式调用](#1.7.2 显式调用)
      • [1.7.3 成员函数重载的关键点](#1.7.3 成员函数重载的关键点)
    • [1.8 调用常见问题:访问限制](#1.8 调用常见问题:访问限制)

1. 运算符重载

1.1. 运算符重载的意义

内置运算符主要作用于内置类型和指针等。对于类类型对象,C++ 允许通过运算符重载为运算符指定新的含义,使自定义类型也能像内置类型一样参与运算。当运算符作用于类类型对象时,编译器会进行重载解析:候选包括成员 operator@、非成员 operator@ 以及内置运算符。若找到最佳匹配,则调用对应函数或内置运算符;若找不到可行候选,则编译报错。内置类型之间的运算符不可重载,且运算符的优先级、结合性和操作数个数不能改变。

所有运算符重载不是"修改运算符本身的含义",而是为特定类类型或枚举类型提供运算符函数,让自定义类型能像内置类型一样参与运算,并融入 C++ 的重载解析和泛型体系

1.2 运算符重载应当有意义

运算符重载应当有意义。一个类需要重载哪些运算符,不是看哪些运算符"能"重载,而是看哪些运算符作用于该类对象时具有自然、直观、符合领域惯例的语义。判断时要同时考虑操作数类型和返回类型,不能只看运算符符号。

以 Date 类为例:

  • Date - Date 返回两个日期相差的天数,有意义;

  • Date + int 返回新日期,表示日期向后推移若干天,有意义;

  • Date - int 返回新日期,表示日期向前推移若干天,有意义;

  • Date + Date 没有自然含义,无意义。

因此,operator+ 并非一律不能重载;真正无意义的是 Date + Date。若某个运算没有自然语义,就不应重载该运算符,而应使用普通函数,如 addDays()、daysBetween() 等。

1.3 运算符重载的定义

  • 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。

  • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

1.4 运算符重载的使用注意事项

1. 重载运算符时,operator 后面必须跟一个 C++ 中已经存在的、允许重载的运算符符号。不能自己发明一个不存在的运算符符号。不要把 operator 和任意符号拼在一起,假装它是个新运算符。C++ 不允许创造新运算符。

另外要注意:我们写的 operator@,这里的 @ 只是一个占位符,表示"某个运算符",并不是说真的可以写 operator@。例如书上说"运算符重载函数形式为 operator@",实际写代码时要把它替换成具体符号,比如 operator+、operator-、operator==。

运算符重载只能"给已有运算符赋予新含义",不能"创造新运算符"。operator@ 中的 @ 只是书本里的占位符,不是真的能写进代码。

2. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致:不能改变优先级、结合性、操作数个数a + b * c 仍然先算 * ,不会因为你重载了 + 就改变。

3. 至少一个操作数必须是类类型或枚举类型,不能重载 int operator+(int, int) 这种纯内置类型之间的运算。

4. 有些运算符不能重载如:.、.*、::、?:、sizeof、typeid、alignof、noexcept、各种 cast 等。

1.5 运算符重载的创建

以Date类为例:

cpp 复制代码
class Date{
public:
Date(int year,int month,int day)
{
     _year = year;
     _month = month;
     _day = day;
}
private:
   int _year;
   int _month;
   int _day;
};

1.5.1 创建重载运算符" == "

要让d1 == d2,就让d1的所有成员变量 == d2所有成员变量。返回值设置为bool类型。传递参数两个Date类类型。

cpp 复制代码
#include <iostream>
class Date
{
public:
Date(int year,int month,int day)
{
     _year = year;
     _month = month;
     _day = day;
}
//private:
   int _year;
   int _month;
   int _day;
};
bool  operator==(const Date& x1 , const Date& x2)
{
	return x1._year == x2._year
			&&x1._month == x2._month
			&&x1._day == x2._day;
}

注意:运算符重载函数的参数中,至少有一个必须是类类型或枚举类型。不能为两个内置类型重新定义运算符。不能改变 int、double 等内置类型之间运算符的原有含义。

所以下面这个函数是非法的:

cpp 复制代码
int operator+(int x, int y)
{
    return x - y;
}

原因:两个参数 x、y 都是 int,没有类类型或枚举类型参数。它试图改变内置类型 int 的 + 的含义,把加法变成减法。C++ 编译器会直接报错,不允许这种重载。

1.6 运算符重载的调用

1. 显式调用

因为运算符重载本质上就是函数,所以可以像普通函数一样直接写函数名并传参。

直接把重载后的运算符看作是一个函数,传入参数,如下:

cpp 复制代码
int main()
{
    Date d1(2025,2,23);
    Date d2(2025,3,26);
    operator==(d1,d2);
    return 0;
}

但要区分两种情况:operator== 是非成员函数还是成员函数。

非成员函数版:可以这样写

如果 operator== 定义为非成员函数那么下面这种显式调用是合法的 :operator==(d1, d2); // 显式调用非成员 operator==

成员函数版:不能写成 operator==(d1, d2)

如果 operator== 是成员函数:

cpp 复制代码
class Date {
public:
    bool operator==(const Date& x) const {
        return _year == x._year
            && _month == x._month
            && _day == x._day;
    }
private:
    int _year, _month, _day;
};

那么显式调用形式是: d1.operator==(d2);而不是: operator==(d1, d2); // 错误:没有这个非成员函数,因为成员函数的左操作数是调用对象 d1,右操作数是参数 d2。

总结:运算符重载可以显式调用:非成员函数写 operator==(d1, d2),成员函数写 d1.operator==(d2)。它的本质就是普通函数调用,但通常直接写 d1 == d2 更自然、更可读。

2. 隐式调用

隐式调用就是:不写 operator==(d1, d2),而是直接写运算符表达式 d1 == d2,由编译器自动把它翻译成对应的 operator@ 函数调用。这是运算符重载最正常、最常用的使用方式。

非成员函数版

cpp 复制代码
bool operator==(const Date& x1, const Date& x2)
{
    return x1._year == x2._year
        && x1._month == x2._month
        && x1._day == x2._day;
}

int main()
{
    Date d1(2025, 2, 23);
    Date d2(2025, 3, 26);

    bool b = d1 == d2;   // 隐式调用
    // 等价于:bool b = operator==(d1, d2);
}

成员函数版

cpp 复制代码
class Date {
public:
    bool operator==(const Date& x) const {
        return _year == x._year
            && _month == x._month
            && _day == x._day;
    }
private:
    int _year, _month, _day;
};

int main()
{
    Date d1(2025, 2, 23);
    Date d2(2025, 3, 26);

    bool b = d1 == d2;   // 隐式调用
    // 等价于:bool b = d1.operator==(d2);
}

总结:隐式调用就是直接写 d1 == d2 这样的运算符表达式,编译器自动把它翻译成 operator==(d1, d2) 或 d1.operator == (d2)。它是运算符重载最自然、最常用的调用方式。

1.7 重载为成员函数的调用

当运算符重载为成员函数时,它本质上是类的一个成员函数,函数名是 operator@。调用时,左操作数自动成为调用对象(this),右操作数作为函数参数。

cpp 复制代码
class T
{
public:
    返回类型 operator@(参数列表) 
    {
        // ...
    }
};

对于二元运算符 :a @ b; // 等价于 a.operator@(b)

a 是左操作数,也是调用对象。b 是右操作数,作为参数传入。函数内部通过 this 访问 a 的成员。

对于一元运算符 :@a; // 等价于 a.operator@()

a 是操作数,也是调用对象。没有显式参数(后置 ++/-- 除外)。

1.7.1 隐式调用

直接写运算符表达式,编译器自动翻译为成员函数调用。

cpp 复制代码
class Date {
public:
Date(int year,int month,int day)
{
     _year = year;
     _month = month;
     _day = day;
}

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

private:
    int _year, _month, _day;
};

int main() {
    Date d1(2025, 2, 23);
    Date d2(2025, 3, 26);

    bool b = d1 == d2;   // 隐式调用:d1.operator==(d2)
}

自动转换成显示调用,d1传递给this指针,d2传递给x

1.7.2 显式调用

直接写成员函数调用形式:d1.operator==(d2); // 显式调用成员 operator==

也可以用于其他运算符:

cpp 复制代码
a.operator+(b);      // a + b
a.operator+=(b);     // a += b
a.operator++();      // ++a
a.operator++(0);     // a++
a.operator[](i);     // a[i]
a.operator()(x);     // a(x)

1.7.3 成员函数重载的关键点

1. 左操作数必须是本类对象
成员函数重载要求左操作数是当前类类型(或可隐式转换为当前类类型)。例如 d1 == d2 中,d1 必须是 Date 对象。

2. 右操作数作为参数
参数类型可以是 const T&、T、其他类型,取决于语义。

3. this 指针
在成员函数内部,this 指向左操作数。如果函数不修改对象,应加 const。

cpp 复制代码
bool operator==(const Date& x) const;

4. 返回值

  • 复合赋值 +=、-=:返回 * this 的引用,支持链式。

  • 前置 ++/--:返回 * this 的引用。

  • 后置 ++/--:返回旧值,通常返回值。

  • 比较运算符:返回 bool。

  • 算术运算符:返回新对象。

5. 必须是成员的运算符

=、\[\]、()、->、类型转换运算符只能重载为成员函数。

6. 不能是成员的运算符

<<、>> 通常必须是非成员,因为左操作数是流对象,不是本类对象。

总结:重载为成员函数时,左操作数自动成为调用对象 this,右操作数作为参数;隐式调用写 a @ b,显式调用写 a.operator@(b)。必须为成员的运算符有 =、\[\]、()、-> 等。

1.8 调用常见问题:访问限制

在运算符重载的调用中,访问限制(public / protected / private)同样起作用。无论是隐式调用 d1 == d2,还是显式调用 d1.operator == (d2),编译器都会检查对应的 operator@ 函数是否可访问。若不可访问,直接编译报错。

解决办法:

1. 最挫的方法:成员变量公有化

2. 仿JAVA常用方法:由类提供get()函数

3. 后面会讲:友元函数

4. 最好的方法:重载为成员函数

1. 成员变量公有化:最差

cpp 复制代码
class Date {
public:
    int _year, _month, _day;
};

优点:非成员 operator== 能直接访问。

缺点:彻底破坏封装,外部可随意修改,对象可能变成非法状态。

结论:不推荐,几乎不应使用。

2. 提供 get() 函数:在对象外调用getyear就可以通过返回值间接得到_year。

cpp 复制代码
int Getyear()
{
    return _year;
}

优点:保持封装,外部通过接口访问。

缺点:成员多时要写很多 get,运算符实现也变啰嗦。

结论:比公有化好,但不是运算符重载的首选。

3. 友元函数:C++ 常用手段

cpp 复制代码
class Date {
    friend bool operator==(const Date&, const Date&);
public:
    // ...
private:
    int _year, _month, _day;
};

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

优点:非成员运算符也能访问私有成员,写法直接。

缺点:增加类与函数的耦合,友元过多会削弱封装。

结论:需要非成员运算符时,友元是合理选择。

4. 重载为成员函数:很多场景下最好

cpp 复制代码
class Date {
public:
Date(int year,int month,int day)
{
     _year = year;
     _month = month;
     _day = day;
}

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

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

优点:直接访问私有成员,写法简洁,左操作数就是本类对象。

缺点:左操作数必须是本类对象,不能处理 2 + a、cout << a 这类情况。

结论:左操作数是本类对象时,成员函数通常最好。

总结:优先成员函数;必须非成员时用友元;尽量别用 get() 堆接口;绝对不要公有化成员变量。

相关推荐
CRMEB系统商城1 小时前
CRMEB开源商城系统(PHP)v8.0,打造全新AI二开商城底座!
开发语言·人工智能·小程序·开源·php
宇卿.1 小时前
C#字段、属性、索引器、常量
开发语言·计算机视觉·c#
菜鸟~noob2331 小时前
【电子战】第31篇:3D AOA 定位——方位+俯仰联合
开发语言·matlab·3d
wuyk5551 小时前
28 搜索引擎索引更新:从 LSM Tree 到布隆过滤器的高效实现
stm32·单片机·嵌入式硬件·算法
小七在进步1 小时前
初识C++中的模板
java·开发语言·c++
程序员Linc1 小时前
ONNXOCRSharp 开源手记 02|v0.4.0 PDF识别
开发语言·pdf·c#·ocr·onnxocrsharp
我命由我123451 小时前
LRU 缓存淘汰策略
数据结构·redis·学习·mysql·算法·缓存·学习方法
回眸&啤酒鸭2 小时前
【回眸】HireKey 智能招聘全流程应用场景
开发语言·人工智能·python
云川之下2 小时前
【存储】搭建nfs 存储示例
开发语言·php