搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第14章 运算符重载

第14章 运算符重载

入门 👤 已掌握类与对象基础的开发者 🔧 让自定义类型像内置类型一样直观运算
本章你将学到
  • 运算符重载的基本语法与规则
  • 如何重载常用运算符(+, -, ==, <<)
  • 成员函数重载与友元函数重载的区别
核心概念运算符函数友元函数

想象一下,你设计了一个Vector2D类来表示二维向量。如果想让两个向量相加,你只能写v1.add(v2),这远不如直接写v1 + v2直观。运算符重载就是赋予C++运算符操作自定义类型的能力,让代码更简洁、更符合直觉。

运算符重载:语法与规则

运算符重载的本质是定义一个特殊的成员函数或全局函数,函数名由operator关键字后接要重载的运算符符号组成。

class Complex {
public:
    double real, imag;
    // 成员函数形式重载‘+’运算符
    Complex operator+(const Complex& other) const {
        return Complex{real + other.real, imag + other.imag};
    }
};

// 使用
Complex c1{1.0, 2.0};
Complex c2{3.0, 4.0};
Complex c3 = c1 + c2; // 等价于 c1.operator+(c2)

关键点: 重载不能改变运算符的优先级和结合性,也不能创造新的运算符(如`**`)。它的操作数中至少有一个是用户自定义的类型。

并非所有运算符都能被重载。下表列出了可重载与不可重载的运算符,这是你必须遵守的语法边界。

可重载的运算符不可重载的运算符
算术运算符:+ - * / %
关系运算符:== != < > <= >=
逻辑运算符:! && ||
位运算符:& | ^ ~ << >>
赋值运算符:= += -= 等
自增自减:++ --
下标与函数调用:[] ()
成员访问:-> ->*
内存管理:new new[] delete delete[]
逗号:,
作用域解析运算符:::
成员访问运算符:.
成员指针访问运算符:.*
条件运算符:? :
sizeof 运算符
typeid 运算符

两种实现方式:成员函数 vs. 友元函数

这是运算符重载的核心决策点。选择哪种方式,取决于运算符的语义和第一个操作数的类型。

成员函数重载
⇄
友元函数重载
↓
隐含this指针作为左操作数
→
对称性操作,如`a + b`与`b + a`

成员函数重载: 运算符函数作为类的成员。此时,左操作数必须是该类的对象,它通过this指针隐式传递。这非常适合修改自身状态的运算符,如+=、++。

class MyArray {
public:
    int& operator[](int index) { return data[index]; } // 下标运算符必须是成员函数
    MyArray& operator+=(const MyArray& other); // 复合赋值通常为成员函数
private:
    int data[100];
};

友元函数重载: 运算符函数作为全局函数,并在类内声明为friend。当左操作数不是类的对象(比如是内置类型),或者你需要运算符具有完全的对称性时,必须使用友元函数。

生活类比: 就像给一个定制工具箱(自定义类)增加“连接”功能。成员函数重载像是工具箱自带一个连接另一个工具箱的卡扣(`工具箱A.连接(工具箱B)`)。友元函数重载则像是一个外部的通用连接器,它不专属于任何一个工具箱,但被授权可以操作它们的内部结构,因此能实现`连接器(工具箱A, 工具箱B)`甚至`连接器(标准箱子, 工具箱A)`。

技术定义: 友元函数重载是在类外部定义的全局函数,通过`friend`关键字获得访问该类私有成员的权限,常用于实现运算符左、右操作数可互换的对称操作。

class Complex {
public:
    // 声明友元函数
    friend Complex operator+(const Complex& lhs, const Complex& rhs);
private:
    double real, imag;
};

// 定义友元函数(注意,没有Complex::限定)
Complex operator+(const Complex& lhs, const Complex& rhs) {
    return Complex{lhs.real + rhs.real, lhs.imag + rhs.imag};
}
// 现在可以写 2.5 + c1 (如果实现了`operator+(double, const Complex&)`)

常见误区

  • 过度使用友元: 只在必要时(如实现对称运算符`<<`、`+`)才使用友元。滥用友元会破坏封装性。
  • 返回临时对象的引用: 像`+`、`-`这类运算符应该返回一个新对象(值),而不是引用。返回局部变量的引用会导致未定义行为。
  • 忽略const正确性: 对于不修改操作数的运算符(如`==`、`+`),应将运算符函数和参数声明为const。

实战:重载输入输出运算符(<< 和 >>)

这是最经典的友元函数重载案例。因为左操作数是std::ostream&或std::istream&,而不是你的自定义类对象。

#include <iostream>
class Point {
public:
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
    friend std::istream& operator>>(std::istream& is, Point& p);
private:
    int x, y;
};

// 输出运算符
std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x << ", " << p.y << ")";
    return os; // 必须返回流引用以支持链式调用:cout << p1 << p2;
}

// 输入运算符
std::istream& operator>>(std::istream& is, Point& p) {
    is >> p.x >> p.y;
    return is; // 同样返回流引用
}

// 使用
Point pt;
std::cin >> pt;   // 等价于 operator>>(std::cin, pt)
std::cout << "Point is: " << pt << std::endl;

我在项目中踩过一个坑:忘记返回流引用。这导致std::cout << a << b;这样的链式调用编译失败,错误信息通常是“没有找到匹配的`operator<<`...”,排查起来很费时间。记住,流插入和提取运算符必须返回流引用。

编者提示: 运算符`<<`和`>>`的重载依赖于输入输出流(`iostream`)的知识。如果你对`std::cout`、`std::cin`以及`std::ostream`类型感到陌生,建议回顾第21章《文件输入输出》中关于标准流的基础部分。

特殊运算符的重载要点

有些运算符的重载有固定范式,偏离这些范式很容易出错。

自增/自减 (++/--)

  • 区分前置(++obj)与后置(obj++)。
  • 后置版本接受一个额外的int形参作为区分,该参数无实际意义。
  • 前置返回引用,后置返回值。

赋值运算符 (=)

  • 必须是成员函数。
  • 注意处理自赋值(`a = a`)的情况。
  • 通常返回`*this`的引用以支持链式赋值(`a = b = c`)。

函数调用运算符 (())

  • 让对象像函数一样被调用,称为“函数对象”或“仿函数”。
  • 这是STL算法(第19章)广泛使用的技术。

这里给出一个完整的自增运算符重载示例,实际开发中,对于包含动态内存的类,你还需要重载赋值运算符并遵循“三法则”。

class Counter {
public:
    // 前置++:先加,后返回自身
    Counter& operator++() {
        ++value;
        return *this;
    }
    // 后置++:int形参仅为区分,返回加之前的值
    Counter operator++(int) {
        Counter temp = *this; // 保存原值
        ++(*this);         // 调用前置++实现自增
        return temp;        // 返回原值
    }
private:
    int value = 0;
};

动手试一试

创建一个Money类,表示人民币(元、角、分)。

  1. 实现成员函数重载的加法运算符+,使得Money m3 = m1 + m2;可以正确工作(注意:1元=10角=100分,运算后需进位)。
  2. 实现友元函数重载的相等运算符==,用于比较两个Money对象是否相等。
  3. (挑战)实现友元函数重载的流输出运算符<<,使其能按“¥元.角分”的格式输出,例如Money{12, 3, 4}输出为“¥12.34”。

数据成员建议:int yuan, jiao, fen;

检验你的理解

  1. 判断:重载运算符`*`时,可以将其功能改为除法运算。这种做法是否被允许?为什么?
  2. 如果你想实现`double d = 5.0 + myComplexObj;`(`myComplexObj`是你的复数类对象),你应该将`operator+`重载为成员函数还是友元函数?为什么?
  3. 下面的代码片段试图重载`<<`运算符,但有一处关键错误导致编译失败。请找出错误并修正。
    class MyClass {
        int data;
    public:
        friend void operator<<(std::ostream& os, MyClass obj);
    };
    void operator<<(std::ostream& os, MyClass obj) {
        os << obj.data;
    }

本章小结

  • 运算符重载通过定义operator@函数,让自定义类型支持C++内置运算符,提升代码可读性。
  • 实现方式有两种:成员函数(左操作数为当前对象)和友元函数(常用于需要对称性或左操作数为非类类型的情况)。
  • 流运算符<<和>>必须重载为友元函数,且必须返回流引用以支持链式调用。
  • 重载自增/自减运算符时,需用额外int参数区分后置版本,并注意返回值类型(前置返引用,后置返回值)。
  • 遵循运算符的常规语义,保持代码直觉,避免滥用和反直觉的实现。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注