C++ 运算符重载

运算符重载核心知识点讲解

1 什么是运算符重载?

运算符重载是 C++ 的核心特性,本质是函数重载的特殊形式—— 它允许你为自定义类型(类 / 结构体)重新定义运算符的行为,让自定义类型能像intfloat等内置类型一样使用+-<<=等运算符。

举个例子:默认情况下MyClass a, b; a + b; 是非法的(编译器不知道如何 “加” 两个自定义对象),但通过运算符重载,你可以定义a + b的逻辑(比如把两个对象的成员变量相加)。

2 运算符重载的核心规则(不可违反)

这是重载的 “红线”,违反会导致编译错误或逻辑混乱:

  1. 只能重载已有运算符:不能发明新运算符(如@#),支持重载的运算符包括+-*<<=[]等。
  2. 不可改变运算符的基本特性
    • 优先级不变(如*始终比+优先);
    • 结合性不变(如+是左结合,a+b+c等价于(a+b)+c);
    • 操作数个数不变(如+是双目运算符,重载后仍需两个操作数)。
  3. 部分运算符绝对不能重载.(成员访问)、.*(成员指针访问)、::(作用域解析)、?:(三目运算符)、sizeoftypeid
  4. 语义一致性:重载后的运算符功能应符合直觉(比如+重载为 “加法”,而非 “减法”),否则代码可读性极差。

实例 + 调用

1. 基础:双目运算符重载(以+为例)

双目运算符(需要两个操作数)有两种重载方式:成员函数(左操作数是this)和全局 / 友元函数(两个操作数都是参数)。

方式 1:成员函数重载+

#include <iostream>
using namespace std;

// 自定义复数类
class Complex {
private:
    double real;  // 实部
    double imag;  // 虚部
public:
    // 构造函数
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 成员函数重载+:左操作数是this(当前对象),右操作数是other
    // const:保证不修改当前对象;返回值:返回新对象(避免修改原对象)
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // 辅助打印
    void print() const {
        cout << real << " + " << imag << "i" << endl;
    }
};

int main() {
    // 1. 创建对象
    Complex c1(1.5, 2.5), c2(3.5, 4.5);

    // 2. 调用方式1:运算符形式(推荐,直观)
    Complex c3 = c1 + c2;
    cout << "c1 + c2 = ";
    c3.print();  // 输出:5 + 7i

    // 3. 调用方式2:等价的函数形式(底层本质)
    Complex c4 = c1.operator+(c2);
    cout << "c1.operator+(c2) = ";
    c4.print();  // 输出:5 + 7i

    return 0;
}

编译器逆向破译的过程:
1.发现c1+c2;
2.解读为c1.operator+(c2);
3.调用c1的+运算符重载函数

方式 2:全局 / 友元函数重载+

适合左操作数不是类对象的场景(如10 + c1):

#include <iostream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 声明友元函数(突破私有成员访问限制)
    friend Complex operator+(const Complex& c1, const Complex& c2);
    void print() const { cout << real << " + " << imag << "i" << endl; }
};

// 全局函数实现+重载:两个操作数都是参数
Complex operator+(const Complex& c1, const Complex& c2) {
    return Complex(c1.real + c2.real, c1.imag + c2.imag);
}

int main() {
    Complex c1(1, 2), c2(3, 4);
    
    // 调用方式1:运算符形式
    Complex c3 = c1 + c2;
    c3.print();  // 5 + 7i

    // 调用方式2:函数形式
    Complex c4 = operator+(c1, c2);
    c4.print();  // 5 + 7i

    // 支持交换操作数(如果重载了int和Complex的+,还能支持10 + c1)
    return 0;
}

2. 输入输出运算符(<</>>

注意: 类的成员函数会隐含一个this指针(指向当前类对象),且this强制绑定到运算符的左操作数;同时,成员函数的参数个数 = 运算符操作数总数 - 1(因为左操作数被this占用)。

因此对于左移操作符<<重载: 我们逻辑上是想让类对象向外输出(ostream<<类对象), 但是如果作为成员函数,this指针会强行绑定左操作数(调用时必须:类对象<<ostream), 所以只能写成: 写在类外面, 又由于对象的成员变量大多是私有的, 所以需要写成友元方便访问;

必须用全局 / 友元函数重载(因为左操作数是cout/cin,不是自定义类对象),且返回流对象以支持链式调用。

#include <iostream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 声明友元:重载<<(输出)和>>(输入)
    friend ostream& operator<<(ostream& os, const Complex& c);
    friend istream& operator>>(istream& is, Complex& c);
};

// 重载<<:输出自定义对象
ostream& operator<<(ostream& os, const Complex& c) {
    os << c.real << " + " << c.imag << "i";
    return os;  // 返回os,支持链式输出(cout << c1 << c2)
}

// 重载>>:输入自定义对象
istream& operator>>(istream& is, Complex& c) {
    cout << "请输入实部:";
    is >> c.real;
    cout << "请输入虚部:";
    is >> c.imag;
    return is;  // 返回is,支持链式输入(cin >> c1 >> c2)
}

int main() {
    Complex c;
    
    // 1. 调用>>:输入对象
    cin >> c;  // 等价于 operator>>(cin, c)

    // 2. 调用<<:输出对象(链式调用)
    cout << "你输入的复数是:" << c << endl;  // 等价于 operator<<(cout, c)

    return 0;
}

调用示例输入输出

请输入实部:2
请输入虚部:3
你输入的复数是:2 + 3i

3. 赋值运算符(=

必须是成员函数,编译器会生成默认版本,但涉及动态内存时需手动实现深拷贝(避免内存泄漏 / 野指针)。

#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
    char* str;  // 动态分配内存
    int len;
public:
    // 构造函数
    MyString(const char* s = "") {
        len = strlen(s);
        str = new char[len + 1];  // 分配内存(+1存'\0')
        strcpy(str, s);
    }

    // 重载=:深拷贝(必须成员函数)
    MyString& operator=(const MyString& other) {
        // 1. 防止自赋值(a = a)
        if (this == &other) return *this;

        // 2. 释放当前对象的内存
        delete[] str;

        // 3. 深拷贝
        len = other.len;
        str = new char[len + 1];
        strcpy(str, other.str);

        // 4. 返回自身,支持链式赋值(a = b = c)
        return *this;
    }

    // 析构函数:释放内存
    ~MyString() { delete[] str; }

    // 辅助打印
    void print() const { cout << str << endl; }
};

int main() {
    MyString s1("Hello"), s2("World");
    
    // 1. 调用=:运算符形式
    s1 = s2;  // 等价于 s1.operator=(s2)
    s1.print();  // 输出:World

    // 2. 链式调用
    MyString s3;
    s3 = s1 = s2;
    s3.print();  // 输出:World

    return 0;
}

4. 自增 / 自减运算符(++/--

区分前置++a)和后置a++):

  • 前置:无参数,返回引用(支持连续++);
  • 后置:加int形参(仅作标记),返回值(返回自增前的状态)。
#include <iostream>
using namespace std;

class Counter {
private:
    int count;
public:
    Counter(int c = 0) : count(c) {}

    // 1. 前置++:无参数,返回引用
    Counter& operator++() {
        count++;
        return *this;  // 返回自身,支持 ++(++a)
    }

    // 2. 后置++:int是标记(无实际意义),返回值
    Counter operator++(int) {
        Counter temp = *this;  // 保存自增前的状态
        count++;
        return temp;           // 返回旧值
    }

    int getCount() const { return count; }
};

int main() {
    Counter c(5);
    
    // 1. 前置++调用
    ++c;  // 等价于 c.operator++()
    cout << "前置++后:" << c.getCount() << endl;  // 6

    // 2. 后置++调用
    Counter c2 = c++;  // 等价于 c.operator++(0)
    cout << "后置++后c:" << c.getCount() << endl;   // 7
    cout << "后置++返回值c2:" << c2.getCount() << endl;  // 6

    return 0;
}

5. 下标运算符([]

必须是成员函数,通常重载const(只读)和非const(可修改)两个版本,支持数组式访问。

#include <iostream>
using namespace std;

class MyArray {
private:
    int arr[5] = {10, 20, 30, 40, 50};  // 固定数组
public:
    // 1. 非const版:可修改元素
    int& operator[](int index) {
        // 边界检查(避免越界)
        if (index < 0 || index >= 5) {
            cerr << "下标越界!" << endl;
            exit(1);
        }
        return arr[index];  // 返回引用,支持修改
    }

    // 2. const版:只读(const对象调用)
    const int& operator[](int index) const {
        if (index < 0 || index >= 5) {
            cerr << "下标越界!" << endl;
            exit(1);
        }
        return arr[index];
    }
};

int main() {
    MyArray arr;
    
    // 1. 调用[]修改元素(非const版)
    arr[2] = 300;  // 等价于 arr.operator[](2) = 300
    cout << "修改后arr[2]:" << arr[2] << endl;  // 300

    // 2. const对象调用const版
    const MyArray arr2;
    cout << "arr2[0]:" << arr2[0] << endl;  // 10(只读,无法修改)

    return 0;
}

实战练习:

// User.h
class User {
public:
    std::string name = "ZhangSan";
    friend std::ostream& operator<<(std::ostream& os, const User& u);
};

// 任何支持标准流的地方都能用!
inline std::ostream& operator<<(std::ostream& os, const User& u) {
    os << "User: " << u.name;
    return os;
}
//Logger.h
// Logger 里的模板函数
template <typename T>
LogMessage &operator<<(const T &info)
{
    std::stringstream ss;
    ss << info; // 【关键】:这里调用了 User 定义的标准重载
    _loginfo += ss.str();
    return *this;
}

完整的调用链条(Step-by-Step)

假设你在 main.cpp 里写了这样一行代码:

User u;
LOG(INFO) << u;
//LOG(INFO)是一个宏替换,执行一个函数返回一个临时的LogMessage类实例

这里有两个 operator<< 在起作用:

  1. 外层:LogMessage 里的 operator<<(模板函数)。

  2. 内层:User 里的 operator<<(也就是你刚刚贴的这段代码)。

这行代码执行时,发生了以下 4 个步骤的连环调用:

第 1 步:宏展开与对象创建

LOG(INFO) 展开并创建了一个 LogMessage 临时对象。
此时代码变成了:

LogMessage_Object << u;

第 2 步:调用 LogMessage 的模板函数 (入口)

编译器看到左边是 LogMessage 对象,右边是 User 对象。
它去 Logger.hpp 里找匹配的函数,找到了这个泛型模板

// Logger.hpp 中的 LogMessage 类
template <typename T> 
LogMessage &operator<<(const T &info) // 此时 T 被推导为 User
{
    std::stringstream ss;
    
    // 【关键点!】
    // 这里执行了 ss << u; 
    ss << info; 
    
    _loginfo += ss.str();
    return *this;
}

第 3 步:调用 User 的重载函数 (核心)

现在程序执行到了 ss << info 这一行(即 ss << u)。

  • 左边:ss 是 std::stringstream 类型。

  • 右边:info 是 User 类型。

重点来了: std::stringstream 是 std::ostream 的子类(儿子)。
所以编译器会去寻找一个函数,签名满足 (std::ostream&, const User&)。

它正好找到了你在 User.h 里定义的那个函数!

// User.h
inline std::ostream& operator<<(std::ostream& os, const User& u) {
    // 此时 os 就是上面的 ss
    // 此时 u 就是上面的 info
    
    os << "User: " << u.name; // 把字符串写入流中
    return os;
}

注意: 此operator<<函数不是写在User类中,只是在User.h中, 因为类中this强制绑定到运算符的左操作数,强制改变参数规则和操作数顺序;

第 4 步:数据回流

  1. User 的重载函数把 "User: ZhangSan" 写进了 ss 的缓冲区。

  2. 函数返回,回到 LogMessage 的代码中。

  3. LogMessage 调用 ss.str(),把刚刚写入的字符串取出来。

  4. 拼接到 _loginfo 后面。

为什么 stringstream 能传给 ostream?

你可能会问:“我在 User.h 里定义的参数是 std::ostream&,但在 Logger 里用的是 std::stringstream,这能行吗?”

完全没问题!这是 C++ 面向对象的多态特性。

  • 继承关系:std::stringstream 继承自 std::iostream,而 std::iostream 继承自 std::ostream。

  • 里氏替换原则:任何需要父类引用(std::ostream&)的地方,都可以传入子类对象(std::stringstream);你可以理解为子类是父类的加强版, 满足一切父类的要求;

图解

调用方:  LOG(INFO) << u
             |
             v
1. 进入: LogMessage::operator<<(User u)
             |
             |  内部创建 std::stringstream ss
             |  执行 ss << u
             |
             v
2. 跳转: User::operator<<(ostream& os, User u)  <--- 你的代码在这里被调用
             |  (此时 os 引用的是 ss)
             |
             |  执行 os << "User: " << u.name
             |  (数据写入 ss 的缓冲区)
             |
             v
3. 返回: 回到 LogMessage
             |
             |  _loginfo += ss.str()  (从 ss 里取出 "User: ZhangSan")
             |
             v
4. 结束: 返回 LogMessage& (以便继续 << 其他东西)

总结

  1. 调用方式:运算符重载后,既可以用「运算符形式」(如a+bcin>>a),也可以用「函数形式」(如a.operator+(b)operator>>(cin,a)),两者等价。
  2. 实现规则
    • 双目运算符:成员函数版参数少 1 个(左操作数是this),全局 / 友元版参数个数等于操作数个数;
    • 特殊运算符(=/[]/++/--)必须是成员函数,<</>>必须是全局 / 友元函数;
    • 前置++返回引用,后置++int标记且返回值。
  3. 核心目的:让自定义类型像内置类型(int/float)一样使用运算符,提升代码可读性和直观性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值