C++ 运算符重载{加号、左移、递增、赋值、关系、函数调用运算符重载} 两种重载方式:利用成员函数重载、利用全局函数重载【重载方式视情况而定】

目录

0、答案速递:什么是运算符重载? 

一、加号(+)运算符重载

二、左移(<<)运算符重载【不能使用成员函数进行重载】

2.1. 基本数据类型使用左移运算符

2.2 左移运算符的调用格式 

2.2. 自定义数据类型使用左移运算符 

三、递增(++)运算符重载【前置递增支持链式调用、后置递增不支持链式调用】 

四、赋值(=)运算符重载

4.1. 问题: 默认赋值运算符(浅拷贝)

4.2 重载赋值运算符,使用深拷贝解决4.1小节的问题

4.3 重载之后的赋值运算符的链式调用

五、关系运算符(==、!=、<、>)重载

 5.1. 内置数据类型使用关系运算符

5.2. 自定义数据类型使用关系运算符 

六、函数调用()运算符重载


0、答案速递:什么是运算符重载? 

运算符重载对已有的运算符重新定义,使其能够作用于用户自定义类型,并赋予其适应特定需求的功能,同时保持与内置数据类型相同的语法习惯。

举个例子来说明上面的红字部分: 

对于内置数据类型,编译器知道如何进行运算,例如:

#include<iostream>
int main(){
    int a=10;
    int b=2;
    int c=a+b;
    std::cout<<"c = "<<c<<std::endl; // c = 12

    return 0;
}

 对于用户自定义数据类型,编译器不知道如何进行运算,例如:

#include<iostream>
class Person{
    public:
    int m_A;
    int m_B;

};

int main(){
    Person p1;
    p1.m_A=10;
    p1.m_B=5;

    Person p2;
    p2.m_A=10;
    p2.m_B=5;

    Person p3 = p1+p2; // 再实例化一个p3对象,想要完成的操作是:p3.m_A=p1.m_A+p2.m_A、p3.m_B=p1.m_B+p2.m_B,但这是错误的❌,因为编译器不认识这种自定义数据类型的加法

    return 0;
}

报错:
那我现在想要实现自定义数据类型的加法操作,该怎么办呢?
        那么可以在类内写一个成员函数,这个函数的功能实现两个对象相加属性后返回新的对象的属性。代码如下所示:

#include<iostream>
class Person{
    public:
    int m_A;
    int m_B;

    Person PersonAdd(Person &p){
        Person temp;
        temp.m_A=this->m_A+p.m_A;
        temp.m_B=this->m_B+p.m_B;
        return temp;
        
    }

};

int main(){
    Person p1;
    p1.m_A=10;
    p1.m_B=5;

    Person p2;
    p2.m_A=10;
    p2.m_B=5;

    Person p3 = p1.PersonAdd(p2); 
    std::cout<<"p3对象的 m_A, m_B 成员变量为:"<<p3.m_A<<","<<p3.m_B<<std::endl; // p3对象的 m_A, m_B 成员变量为:20,10

    return 0;
}

        另一种方法就是运算符重载,它可以让自定义类型像内置类型一样使用运算符。接下来,我们正式进入运算符重载的学习。

        在 C++ 中,operator 关键字用于重载运算符,使得用户自定义类型可以像内置类型一样使用运算符。

一、加号(+)运算符重载

📌 作用:运算符重载允许我们自定义数据类型的加法运算,使其具有类似于基本数据类型的行为。例如,我们可以重载 + 使 MyClass + MyClass 返回一个新的 MyClass 对象。

成员函数重载 + 示例:

#include<iostream>
// + 运算符重载 :
class Person{
    public:
    int m_A;
    int m_B;

    // 成员函数重载 +
    Person operator+(Person &p){
        Person temp; // 创建一个临时对象
        temp.m_A=this->m_A+p.m_A;
        temp.m_B=this->m_B+p.m_B;
        return temp;
    }
};


void test01(){
    Person p1;
    p1.m_A=10;
    p1.m_B=10;

    Person p2;
    p2.m_A=10;
    p2.m_B=10;
    Person p3 = p1+p2; // 如果+运算符不重载,那么将会抛出 error:no operator "+" matches these operandsC/C++(349)
    std::cout<<"p3.m_A="<<p3.m_A<<std::endl; // p3.m_A=20
    std::cout<<"p3.m_B="<<p3.m_B<<std::endl; // p3.m_B=20

}

int main(){
    test01();
    return 0;
}

成员函数重载 + 的本质是: 

Person p3=p1.operator+(p2);

全局函数重载 + 示例代码:

#include<iostream>
// + 运算符重载 :
class Person{
    public:
    int m_A;
    int m_B;
};

// 全局函数重载 + 
Person operator+(Person& p1,Person& p2){
    Person temp; 
    temp.m_A=p1.m_A+p2.m_A;
    temp.m_B=p1.m_B+p2.m_B;
    return temp;
}
void test01(){
    Person p1;
    p1.m_A=10;
    p1.m_B=10;

    Person p2;
    p2.m_A=10;
    p2.m_B=10;
    Person p3 = p1+p2; // 如果+运算符不重载,那么将会抛出 error:no operator "+" matches these operandsC/C++(349)
    std::cout<<"p3.m_A="<<p3.m_A<<std::endl; // p3.m_A=20
    std::cout<<"p3.m_B="<<p3.m_B<<std::endl; // p3.m_B=20

}

int main(){
    test01();
    return 0;
}

全局函数重载 + 的本质是: 

Person p3=operator+(p1,p2);

总结:

  • 运算符重载只能用于用户自定义类型(如类和结构体),不能改变内置数据类型(如 intdouble)的运算规则。C++ 规定不能重载已有运算符的行为,如 int+,即使我们想让 10 + 20 变成 50 也是不可能的。运算符重载仅适用于用户自定义类型
  • 不要滥用运算符重载。尽管 C++ 支持运算符重载,但它应该用于提高代码的可读性,而不是制造混乱。因此希望只在逻辑上合理的情况下重载运算符,例如 + 应该表示“加法”。避免让运算符的语义变得反直觉,例如 + 不能表示“比较大小”。<<>> 一般用于输入输出操作,不要滥用成别的功能。

二、左移(<<)运算符重载【不能使用成员函数进行重载】

        左移(<<)运算符重载的主要作用是支持自定义类型的输出,通常用于 std::cout 这样的流操作,使得我们可以方便地打印类对象的信息。

2.1. 基本数据类型使用左移运算符

#include<iostream>
int main(){
    int a=10;
    std::cout<<a<<std::endl;
}

上述代码能够输出整型数据(内置数据类型)。

分析:

  • std::coutstd::ostream 类型的对象。
  • << 被重载为 operator<<,它将数据插入到流中。
  • 例如 std::cout << a; 其实是 std::cout.operator<<(a); 的简写。

operator<< 重载示例 C++ 标准库为基本类型(intdoublechar* 等)提供了 operator<< 的重载。例如:

std::ostream& operator<<(std::ostream& os, int value);
std::ostream& operator<<(std::ostream& os, double value);
std::ostream& operator<<(std::ostream& os, const char* value);

2.2 左移运算符的调用格式 

std::cout << a; // 等价于 operator<<(std::cout, a);

特点

  • 左操作数是 std::ostream&(通常是 std::cout)。
  • 右操作数是类的实例
  • 必须定义为 非成员函数,并接受 std::ostream& 作为第一个参数。
  • 通常返回 std::ostream& 以支持链式调用:
    std::cout << a << std::endl; // 允许连续 << 操作

2.2. 自定义数据类型使用左移运算符 

那么我现在要直接输出自定义类型的实例,如下所示:

#include <iostream>

class Person {
public:
    std::string name;
    int age;

    Person(std::string n, int a) : name(n), age(a) {}

};

int main() {
    Person p("Alice", 25);
    std::cout << p;  // ❌ 编译出错
}

报错: 那如何直接输出自定义类型的实例?这就需要左移(<<)运算符重载来完成。

✅ 正确的做法:将 << 重载为全局友元函数

#include <iostream>

class Person {
public:
    std::string name;
    int age;

    Person(std::string n, int a) : name(n), age(a) {}

    // ✅ 友元函数重载 << 运算符
    friend std::ostream& operator<<(std::ostream& os, const Person& p) {
        os << "Name: " << p.name << ", Age: " << p.age;
        return os; // 返回 os,支持链式调用
    }
};

int main() {
    Person p("Alice", 25);
    std::cout << p << std::endl;  // ✅ 正确输出:Name: Alice, Age: 25
}

✅ 为什么这样可行?

  • operator<< 是一个全局函数,而不是 Person 的成员函数,若operator<<是Person类成员函数则会有错误,具体下面会分析。
  • 调用时 std::cout 作为第一个参数 os 传入,符合 operator<< 需要的参数顺序。
  • friend 关键字让 operator<< 访问 Person 的私有或保护成员(如果需要)。

注意:运算符 << 不能作为成员函数重载?
原因:operator<< 作为 成员函数 时,它的左操作数是 this(即 Person),但 C++ 期望 std::ostream 在左侧,所以编译器会报错。


📌 为什么 << 不能作为成员函数重载?

错误示例1:自定义数据类型实例之间的 <<。

错误示例1: 

 // ❌ 作为成员函数的错误写法
        void operator<<(Person &p) {

        }

上述 operator<< 作为成员函数的调用方式是

Person p1("Alice", 25);
Person p2("Bob", 30);

p1 << p2;  // 实际调用的是 p1.operator<<(p2);

它的最终调用效果是:

p1.operator<<(p2);

实际上意味着只能用于 Person 对象之间的 << 操作 


错误示例2:自定义数据类型实例<<std::cout,而正确的应该是:std::cout<<自定义数据类型实例。

错误示例2:

#include<iostream>
#include<string>

class Person {
    public:
        std::string name;
        int age;
        // 构造函数
        Person(std::string str,int a):name(str),age(a){} 
    
        // ❌ 作为成员函数的错误写法
        std::ostream& operator<<(std::ostream& os) {
            os << "Name: " << name << ", Age: " << age;
            return os;
        }
    };
    
int main() {
    Person p("Alice", 25);
    std::cout << p; // ❌ 编译报错
}

代码会 编译错误operator<< 被定义为 Person成员函数。成员函数的调用方式 实际上是:

p.operator<<(std::cout);  // ❌ 这里 p 变成了调用对象,而 std::cout 变成了参数

通常 << 的调用方式是 std::ostream 在左侧,而 Person 在右侧。这样的调用方式p.operator<<(std::cout); 会导致Person 在左侧,std::ostream 在右侧。这样编译会出错。


📌 总结

不能std::ostream 不是 Person 的成员,不能作为 this 指针的对象,成员函数 operator<< 无法匹配 std::cout << p 的调用方式。
必须:定义 operator<<全局友元函数,让 std::cout 作为第一个参数传入,符合运算符的调用规则。
operator<< 必须是一个全局函数,因为它的调用方式是 std::cout << p;,第一个参数是 std::ostream,而 std::ostream 不是 Person 的成员。
friend 关键字不会改变 operator<< 的归属,它仍然是全局函数,友元只是让它能访问 Person 的私有成员。
不能把 operator<< 作为成员函数,否则 this 会指向 Person 的实例,导致 std::cout << p; 这样的代码无法解析。

💡 全局函数重载 operator<< 的本质

全局函数重载 operator<< 的本质是自定义类型到输出流 (std::ostream) 的插入操作,使得 std::cout << obj; 这样的代码可以正确执行。

📌 关键点

  1. 不能是成员函数,因为 << 的左操作数是 std::ostream,而 std::ostream 并不是用户定义的类,不能作为 this 指针的对象。
  2. 通过全局函数std::ostream 作为第一个参数,使其支持 std::cout << obj; 这样的调用方式。
  3. 返回 std::ostream&,支持连续输出,如 std::cout << obj1 << obj2;

🔍 本质解析

std::cout << obj; 本质上是调用:

operator<<(std::cout, obj);

其核心作用就是std::ostream 添加 obj 的自定义数据格式,并继续返回 std::ostream,以支持链式调用


📌 总结

本质:让 std::ostream 支持自定义类的格式化输出。
不能是成员函数,因为 std::ostream 不是 Person 的成员,无法用 this 访问。
必须返回 std::ostream& 以支持链式调用(cout << obj1 << obj2;)。
通常声明为友元函数,以访问 Person 的私有成员(如果有)。

三、递增(++)运算符重载【前置递增支持链式调用、后置递增不支持链式调用】 

🔍 作用:重载递增运算符,实现自己的整型数据

在 C++ 中,我们可以 重载 ++ 运算符,让自定义的类对象像整数一样支持递增操作。


📌 递增运算符 ++ 的两种形式

在 C++ 中,递增运算符有 前置递增 (++obj)后置递增 (obj++)

  1. 前置递增 (++obj):先自增,再返回自身。
  2. 后置递增 (obj++):先返回原值的副本,再自增。


📌 代码实现

#include <iostream>

class MyInteger {
private:
    int value;

public:
    MyInteger(int v) : value(v) {}

    // **前置递增运算符重载(返回引用)**
    MyInteger& operator++() {  
        ++value;
        return *this;  // 返回自身引用,支持链式调用
    }

    // **后置递增运算符重载(返回值)**
    MyInteger operator++(int) {  
        MyInteger temp = *this;  // 先保存当前值
        ++value;                 // 自增
        return temp;              // 返回旧值
    }

    void show() const {
        std::cout << "Value: " << value << std::endl;
    }
};

int main() {
    MyInteger num(10);

    std::cout << "原始值:";
    num.show();

    std::cout << "前置递增 (++num):";
    (++num).show();  // 先加1,再返回

    std::cout << "后置递增 (num++):";
    (num++).show();  // 先返回旧值,再加1

    std::cout << "最终值:";
    num.show();

    return 0;
}

📌 运行结果

原始值:Value: 10
前置递增 (++num):Value: 11
后置递增 (num++):Value: 11
最终值:Value: 12

📌 代码解析

前置递增 (++obj)

  • 直接修改 value,返回 *this 的引用。
  • 支持链式调用(++num).show();

后置递增 (obj++)

  • 先保存当前值(创建临时对象 temp)。
  • 修改 value
  • 返回旧值(返回 temp)。
  • 无法链式调用,因为返回的是值,不是引用。

📌 什么时候用前置 ++,什么时候用后置 ++

  • 前置 ++ (++obj) 性能更高,因为它不需要创建临时对象,直接修改原对象并返回引用。
  • 后置 ++ (obj++) 会创建临时对象,适用于需要原值的情况(如 for (int i = 0; i < n; i++))。

📌 总结

  1. ++ 运算符可以被重载,让自定义类像整数一样递增。
  2. 前置递增 (++obj) 返回引用,更高效,支持链式调用。
  3. 后置递增 (obj++) 返回旧值的副本,消耗更大,但符合原生 int++ 语义。
  4. 在需要高效代码的情况下,尽量使用前置递增 (++obj)。

🔹 理解这两种 ++ 的区别,有助于写出更高效、更直观的 C++ 代码! 🚀

📌 为什么前置递增 ++obj 要返回引用 (MyInteger&)?

在 C++ 中,前置递增 (++obj) 应该返回引用,这样可以提高性能,并支持链式调用。让我们详细分析一下:


🚀 原因 1:避免不必要的对象拷贝,提高性能

假设 operator++ 返回值 而不是 引用

MyInteger operator++() {  
    ++value;
    return *this;  // 返回对象本身的拷贝
}
  • 这里 return *this; 会调用拷贝构造函数,创建一个新的 MyInteger 对象,并返回这个副本。
  • 但在大多数情况下,我们只是希望修改当前对象本身,而不需要创建新的对象,这会浪费性能。

💡 优化方式:使用引用返回,避免拷贝:

MyInteger& operator++() {  
    ++value;
    return *this;  // 返回自身的引用,而不是副本
}

这样,函数返回的是当前对象的引用,而不是临时对象,提高了效率!


🚀 原因 2:支持链式调用

如果 operator++ 返回值,则不能进行链式调用:

MyInteger num(10);
(++num)++;  // ❌ 错误!因为返回的是值,不能修改
  • ++num 返回的是一个 临时对象,而 临时对象是不可修改的,所以不能再进行 ++ 递增。

如果 operator++ 返回引用,则支持链式调用:

MyInteger num(10);
++(++num);  // ✅ 正确!因为返回的是引用,可以继续调用
  • ++num 返回 num 的引用,所以可以继续进行 ++ 操作。

返回引用可以让多个 ++ 操作连在一起,符合预期行为!


🚀 总结

返回类型是否修改自身是否避免拷贝是否支持链式调用
MyInteger operator++()✅ 是❌ 否(创建副本)❌ 否
MyInteger& operator++()✅ 是✅ 是✅ 是

结论:

  • 前置递增 (++obj) 应该返回 MyInteger&,以提高性能并支持链式调用!
  • 后置递增 (obj++) 需要返回值,因为要返回递增前的副本!

牢记:

  • 前置递增 (++obj) 应返回引用 (MyInteger&),以支持链式调用,并避免不必要的对象拷贝! 🚀

 📌 为什么后置递增 operator++(int) 需要一个 int 形参?

在 C++ 中,后置递增 (obj++) 与前置递增 (++obj) 需要区分
为了让编译器区分 前置递增后置递增C++ 规定
前置递增: operator++() 无参数
后置递增: operator++(int) 带一个 int 形参(但不使用)


🚀 形参 int 的作用

  • int 只是一个占位符,不用于计算。
  • 这样 编译器就能区分 ++obj(前置) 和 obj++(后置)。
int i=1;
    std::cout<<(i++)++<<std::endl; //❌ error: expression must be a modifiable lvalueC/C++(137)

 🚀 为什么后置递增要返回值?

  • 前置递增 (++obj) 返回 引用,可以继续操作当前对象。
  • 后置递增 (obj++) 需要 返回原始值,所以返回 而不是 引用

总结:

  • operator++() 不带参数,用于 前置递增
  • operator++(int) int 占位参数,用于 后置递增,让编译器区分它们。
  • 后置递增返回旧值(副本),以符合预期行为。 🚀

四、赋值(=)运算符重载

 C++编译器至少给一个类添加4个特殊成员函数:
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
4.赋值运算符operator=,对属性进行值拷贝
赋值运算符重载的作用:如果类中有指向堆区的指针,默认的赋值运算符(operator=)是浅拷贝,会导致多个对象共享同一块内存,进而引发悬挂指针、堆区内存二次释放等问题。因此,需要手写赋值运算符重载,实现深拷贝

4.1. 问题: 默认赋值运算符(浅拷贝)

典型错误代码示例: 

#include<iostream>

class Person{
    public:
    Person(int age){ // 定义有参构造函数
        m_Age = new int(age);
    }

    // 析构函数
    ~Person(){
        if(m_Age!=nullptr){
            delete m_Age; // 释放掉 m_Age(指针) 所指向的内存空间
            m_Age=nullptr; // 让 m_Age 指向 nullptr, 防止野指针
        }
    }
    int *m_Age;
};

void test01(){
    Person p1(18);
    Person p2(20);
    p2 = p1; // 默认赋值操作
    std::cout<<"p1的年龄为多少:"<<*p1.m_Age<<std::endl;
    std::cout<<"p1的年龄为多少:"<<*p2.m_Age<<std::endl;
}

int main(){
    test01();
    return 0;
}

 代码运行不成功,会崩溃,原因是:堆区内存二次释放。关于崩溃的具体原因及分析请参考博主的另一篇博文:C++构造函数详解:初始化、重载与默认行为,-优快云博客中的5.1小节。

4.2 重载赋值运算符,使用深拷贝解决4.1小节的问题

在类内重载赋值运算符,添加的内容如下: 

   // 重载赋值运算符
    void operator=(Person &p){
        // 编译器提供的浅拷贝
        // m_Age=p.m_Age;
        
        // 实现深拷贝
        m_Age=new int(*p.m_Age);
    }

上面的代码可以优化: 

优化前的代码: 

void operator=(Person &p) { 
    // ❌ 这里直接 new 一个新的 int,没有释放旧的 m_Age,导致内存泄漏
    m_Age = new int(*p.m_Age);
}
  • m_Age 指向的是 new 分配的内存。
  • 如果 p2 = p1;,那么 p2.m_Age 重新 new 了一块内存,但原来的 p2.m_Age 指向的旧内存没释放,这就造成了内存泄漏

 ✅ 正确的写法

🚀 解决方案:先 delete 旧的 m_Agenew

Person& operator=(const Person &p) {
    if (this == &p) return *this; // 处理自赋值,防止释放自身

    delete m_Age; // ✅ 释放旧内存,防止内存泄漏
    m_Age = new int(*p.m_Age); // 重新分配新内存并复制值

    return *this; // ✅ 返回 *this,支持 p3 = p2 = p1;
}
  • delete 旧的 m_Age,释放掉旧的 new int(...) 分配的内存。
  • 如果不 delete,每次赋值都会 new,导致旧的 new 申请的内存丢失,造成内存泄漏!
  • return *this; 使得赋值运算支持 p3 = p2 = p1;

 📌 内存泄漏(Memory Leak)是什么?

内存泄漏指的是程序在运行过程中申请了堆内存,但没有释放,导致这部分内存不可用,直到程序结束
长期积累的内存泄漏会导致程序占用越来越多的内存,最终可能导致系统崩溃

4.3 重载之后的赋值运算符的链式调用

我们先来看一个示例代码,如下:

int a=10;
int b=2;
int c=5;
c=a=b;
std::cout<<a<<std::endl; //输出:2
std::cout<<b<<std::endl; //输出:2
std::cout<<c<<std::endl; //输出:2

那么我们如果结合4.2小节的重载赋值运算符对对象这么赋值呢?可以这样操作吗?请看下面的代码:

#include<iostream>

class Person{
    public:
    Person(int age){ // 定义有参构造函数
        m_Age = new int(age);
    }

    // 析构函数
    ~Person(){
        if(m_Age!=nullptr){
            delete m_Age; // 释放掉 m_Age(指针) 所指向的内存空间
            m_Age=nullptr; // 让 m_Age 指向 nullptr, 防止野指针
        }
    }
    int *m_Age;

    // 重载赋值运算符
    void operator=(Person &p){
        // 编译器提供的浅拷贝
        // m_Age=p.m_Age;

        // 实现深拷贝
        m_Age=new int(*p.m_Age);
    }
};

int main(){

    
    Person p1(18);
    Person p2(2);
    Person p3(10);
    p3=p2=p1; // 第一个 = 处报错:no operator "=" matches these operandsC/C++(349)

    std::cout<<*p1.m_Age<<std::endl; 
    std::cout<<*p2.m_Age<<std::endl; 
    std::cout<<*p3.m_Age<<std::endl; 

    return 0;
}

下面的这行代码中第一个 = 处出现问题:

p3=p2=p1;

p3 = p2 = p1; 这条语句报错的根本原因是:在 Person 类中重载的赋值运算符 operator= 的返回值是 void,所以 p2 = p1 这个操作没有返回 Person&,导致 p3 = (p2 = p1) 无法继续赋值,从而报错:

void operator=(Person &p) { // ❌ 这里返回 void,导致无法支持 p3 = p2 = p1;
    m_Age = new int(*p.m_Age);
}

✅ 解决方法

只需要让 operator= 返回 Person&,这样 p2 = p1 会返回 p2,然后 p3 = p2 才能继续执行。

// ✅ 赋值运算符重载,返回 *this,支持链式赋值
Person& operator=(const Person &p) {
    if (this == &p) return *this; // 处理自赋值,避免释放自身

    delete m_Age; // 释放旧内存,防止内存泄漏
    m_Age = new int(*p.m_Age); // 重新分配内存并拷贝数据

    return *this; // ✅ 返回 *this,支持 p3 = p2 = p1;
}

赋值运算符必须返回 Person&,否则无法支持 p3 = p2 = p1; 这样的链式赋值!

五、关系运算符(==、!=、<、>)重载

作用:在 C++ 中,默认情况下类对象之间不能直接使用关系运算符(如 ==!=<> 等)。
我们可以重载这些运算符,使它们支持对象之间的比较

 5.1. 内置数据类型使用关系运算符

#include<iostream>
int main(){
    int a=1;
    int b=2;
    // == 关系运算符
    if(a==b){
        std::cout<<"a 与 b 相等!"<<std::endl;
    }
    else{
        std::cout<<"a 与 b 不相等!"<<std::endl;
    }
    // != 关系运算符
    if(a!=b){
        std::cout<<"a 与 b 不相等!"<<std::endl;
    }
    else{
        std::cout<<"a 与 b 相等!"<<std::endl;
    }

      // < 关系运算符
      if(a<b){
        std::cout<<"a < b!"<<std::endl;
    }
    else{
        std::cout<<"a > b!"<<std::endl;
    }

    return 0;
}

// 输出:
// a 与 b 不相等!
// a 与 b 不相等!
// a < b!

5.2. 自定义数据类型使用关系运算符 

#include<iostream>
class Person{
    public:
    int value;
    Person(int a):value(a){} // 构造函数
};

int main(){
    Person p1(10);
    Person p2(20);
    if(p1==p2){ // ❌报错:no operator "==" matches these operandsC/C++(349)
        std::cout<<"p1 与 p2 相等"<<std::endl;
    }
    else{
        std::cout<<"p1 与 p2 不相等"<<std::endl;
    }


    return 0;
}

上述程序会报错,原因是C++ 不能默认比较自定义类型的对象。

那么如何像基本数据类型使用关系运算符那样让自定义数据类型也能够使用关系运算符。 这就涉及到关系运算符的重载问题。

== 号和 != 号的重载示例代码:

#include<iostream>
#include<string>
typedef std::string STRING;

class Person{
    public:
    int age;
    STRING name;

    Person(STRING str, int a):name(str), age(a){} // 构造函数

    // 重载 == 号
    bool operator==(Person &p){
        if(this->name == p.name && this->age == p.age){ // 如果当前对象的name与age(this->)和传入对象的name和age(p.)相等
            return true;
        }
        return false;
    }

    // 重载 != 号
    bool operator!=(Person &p){
        if(this->name!=p.name || this->age != p.age){
            return true;
        }
        return false;
    }
};

int main(){
    Person p1("Tom", 18);
    Person p2("Tom", 18);
    // == 测试
    if(p1==p2){ // p1==p2相当于p1对象调用 bool operator==(Person &p);这个函数,p2对象作为这个函数的参数传入该函数
        std::cout<<"p1 与 p2 相等"<<std::endl;
    }
    else{
        std::cout<<"p1 与 p2 不相等"<<std::endl;
    }

    Person p3("Jerry", 18);
    // != 测试
    if(p1!=p3){ // p1!=p3相当于p1对象调用 bool operator!=(Person &p);这个函数,p3对象作为这个函数的参数传入该函数
        std::cout<<"p1 与 p3 不相等"<<std::endl;
    }
    else{
        std::cout<<"p1 与 p3 相等"<<std::endl;
    }
    return 0;
}

// 输出:
// p1 与 p2 相等
// p1 与 p3 不相等

六、函数调用()运算符重载

函数调用运算符 () 也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数
仿函数没有固定写法,非常灵活

示例代码: 

#include<iostream>
#include<string>
typedef std::string STRING;

// 打印输出类
class MyPrint{
    public:
    // 重载函数调用运算符
    void operator()(STRING test){
        std::cout<<test<<std::endl;
    }

};

// 正常的函数
void myprint02(STRING test){
    std::cout<<test<<std::endl; 
}

int main(){
    MyPrint myprint;
    myprint02("Hello World!"); // 正常的函数调用
    myprint("Hello World!"); // 仿函数调用,相当于 myprint 对象调用 void operator()(STRING test); , 字符串"Hello World"作为函数的参数传入函数。
    
    return 0;
}
// 输出:Hello World!
// 输出:Hello World!

示例二:

#include<iostream>
#include<string>
typedef std::string STRING;

// 打印输出类
class MyPrint{
    public:
    // 重载函数调用运算符
    int operator()(int num1, int num2){
        return num1+num2;
    }
};

// 正常的函数
auto add(int num1,int num2){
    return num1+num2;
}

int main(){
    int a=1;
    int b=2;
    // 正常函数调用
    int result = add(a,b);
    std::cout<<"result = "<<result<<std::endl; // result = 3

    // 仿函数调用
    MyPrint myprint;
    int ret = myprint(a,b);
    std::cout<<"ret = "<<ret<<std::endl;// ret = 3

    // 匿名函数对象调用
    std::cout<<MyPrint()(a,b)<<std::endl; // 3

    return 0;
}

 匿名函数对象调用的解释

std::cout << MyPrint()(a, b) << std::endl; // 3

这一行代码的本质是创建一个 MyPrint 类的匿名对象,并立刻使用 () 调用它


📌 代码解析

MyPrint()(a, b);

这相当于:

MyPrint temp;  // 创建一个 MyPrint 类的对象 temp
int result = temp(a, b);  // 调用重载的 operator()

只不过这里 没有显式声明 temp,而是直接用 MyPrint() 创建匿名对象,然后立即调用它的 operator() 方法。


📌 详细分解

  1. MyPrint()

    • 创建一个匿名对象,它的类型是 MyPrint
    • 这个对象 不会被命名,所以创建后只能在当前这一行使用。
  2. MyPrint()(a, b)

    • 调用匿名对象的 operator() 方法,相当于 MyPrint::operator()(a, b)
    • 由于 MyPrint 内部重载了 operator(),所以 MyPrint()(a, b) 等价于 a + b
  3. std::cout << MyPrint()(a, b) << std::endl;

    • 计算 MyPrint()(a, b) 的值,结果是 3
    • 3 传递给 std::cout 进行输出。

📌 为什么 MyPrint() 叫做匿名函数对象?

  • 匿名对象:因为 MyPrint() 只是在这一行被创建,没有变量名,不会再其他地方使用。
  • 仿函数(函数对象):因为 MyPrint 通过 operator() 让对象像函数一样被调用。
  • 一次性使用:这个匿名对象在这一行代码执行完毕后就会被销毁。

📌 代码等价转换

std::cout << MyPrint()(a, b) << std::endl;

等价于:

MyPrint temp;              // 创建 MyPrint 对象
int result = temp(a, b);   // 调用 operator() 方法
std::cout << result << std::endl;  // 输出结果

📌 为什么这样写?

  1. 简洁:避免创建临时变量 MyPrint temp,直接使用匿名对象。
  2. 一次性使用:如果 MyPrint 只用于这一行,没必要给它命名,创建匿名对象更高效。
  3. 仿函数(函数对象):C++ STL(标准模板库)中很多算法都使用这种方式,如 std::sort() 传入比较函数对象。

📌 现实应用

匿名仿函数的思想在 C++ 标准库(如 std::sort)中被广泛应用:

#include <iostream>
#include <vector>
#include <algorithm>

struct Compare {
    bool operator()(int a, int b) {
        return a > b; // 降序排序
    }
};

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5};

    // 使用匿名函数对象 Compare() 作为排序规则
    std::sort(v.begin(), v.end(), Compare());

    for (int num : v) {
        std::cout << num << " ";  // 5 4 3 1 1
    }
}
  • Compare()std::sort() 里被创建为匿名对象,立即使用,然后销毁。
  • 这避免了不必要的变量存储,提高代码的简洁性效率

🔹 总结

  • MyPrint()(a, b) 创建匿名对象,然后立即调用 operator()
  • 这种写法适用于一次性使用的对象,避免定义变量,提高代码简洁性
  • STL(如 std::sort)大量使用类似的匿名仿函数对象来实现高效排序和自定义比较。
C++中,可以通过重载后置递增运算符++)来义自义类型的行为。后置递增运算符用于在变量的值被使用之后递增它的值。 下面是一个示例,展示如何重载后置递增运算符: ```cpp #include <iostream> class Number { private: int value; public: Number(int v) : value(v) {} // 重载后置递增运算符 Number operator++(int) { Number temp = *this; // 创建一个临时对象来保存当前值 value++; // 递增当前值 return temp; // 返回保存的临时对象 } // 打印当前值 void printValue() { std::cout << "Value: " << value << std::endl; } }; int main() { Number num(5); num.printValue(); // 输出:Value: 5 Number result = num++; num.printValue(); // 输出:Value: 6 result.printValue(); // 输出:Value: 5 return 0; } ``` 在上面的例子中,我们义了一个名为`Number`的类,它包含一个私有成员变量`value`和一个公共成员函数`printValue()`。我们重载了后置递增运算符`++`,并返回一个临时对象来保存递增前的值。在`main()`函数中,我们创建了一个`Number`对象`num`并打印其初始值。然后,我们使用后置递增运算符对`num`进行递增,并将递增前的值保存到`result`对象中。最后,我们打印`num`和`result`的值,验证了后置递增运算符重载。 请注意,重载后置递增运算符时需要使用一个额外的(但无实际用途的)整数参数`int`,以便将其与前置递增运算符进行区分。这是C++语言的要求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值