一篇搞懂 C++ 重载:函数重载 + 运算符重载,从入门到会用(含 ++、<<、== 实战)

目录

摘要:

[一、什么是"重载"?Overload 的真正含义](#一、什么是“重载”?Overload 的真正含义)

[1.1 定义](#1.1 定义)

[1.2 函数重载成立的条件](#1.2 函数重载成立的条件)

1)要点一句话:

2)满足重载的几种情况:

3)示例:

4)不构成重载的情况:

[1.3 默认参数 + 重载 = 容易翻车](#1.3 默认参数 + 重载 = 容易翻车)

1)无法构成重载的两种极端:

2)经验之谈:

[二、函数重载的底层:为什么 C 不支持,C++ 却支持?](#二、函数重载的底层:为什么 C 不支持,C++ 却支持?)

[2.1 编译器干了啥?](#2.1 编译器干了啥?)

[2.2 C 的做法:函数名就是字面量](#2.2 C 的做法:函数名就是字面量)

[2.3 C++ 的做法:函数名被"改名"了(Name Mangling)](#2.3 C++ 的做法:函数名被“改名”了(Name Mangling))

[三、运算符重载总览:operator 其实就是"函数重载的一种"](#三、运算符重载总览:operator 其实就是“函数重载的一种”)

[3.1 什么是运算符重载?](#3.1 什么是运算符重载?)

[3.2 运算符重载的基本规则](#3.2 运算符重载的基本规则)

四、典型运算符重载案例

[4.1 重载 +:自定义类型的"加法"](#4.1 重载 +:自定义类型的“加法”)

[4.2 前置++ / 后置++ 的正确写法(超高频)](#4.2 前置++ / 后置++ 的正确写法(超高频))

1)内置类型语义回顾

[2)前置++ 重载](#2)前置++ 重载)

[3)后置++ 重载(int 参数只是占位)](#3)后置++ 重载(int 参数只是占位))

(1)为什么后置++不能返回引用?

[(2)为什么后置++常常写成 const A 返回?](#(2)为什么后置++常常写成 const A 返回?)

(3)实践

[4.3 重载关系运算符:==、>、< 等](#4.3 重载关系运算符:==、>、< 等)

[4.4 重载输入输出运算符:<< 和 >>](#4.4 重载输入输出运算符:<< 和 >>)

[4.5 赋值 =、[]、()、->:只能成员函数重载](#4.5 赋值 =、[]、()、->:只能成员函数重载)

[五、类型转换相关:类型转换运算符 & 转换构造函数](#五、类型转换相关:类型转换运算符 & 转换构造函数)

[5.1 类型转换运算符:对象 -> 基本类型](#5.1 类型转换运算符:对象 -> 基本类型)

[5.2 转换构造函数:基本类型 / 其他类 -> 本类对象](#5.2 转换构造函数:基本类型 / 其他类 -> 本类对象)

[六、运算符重载:成员 vs 友元,怎么选](#六、运算符重载:成员 vs 友元,怎么选)

[七、重载 vs 重写 vs 隐藏(高频概念对比)](#七、重载 vs 重写 vs 隐藏(高频概念对比))

[7.1 重载(Overload)](#7.1 重载(Overload))

[7.2 重写(Override)](#7.2 重写(Override))

[7.3 隐藏(Name Hiding)](#7.3 隐藏(Name Hiding))

八、总结


摘要:

很多小白对"重载"这个概念有一种似懂非懂的感觉:

  • 知道函数可以同名

  • 知道 operator+operator<< 之类

  • 但一到自己写,就开始晕:

    • 函数参数怎么改算重载?

    • 返回值不同算不算?

    • 为什么 C 不支持重载而 C++ 行?

    • 前置++ / 后置++ 的重载到底咋写?

    • <<== 这些常用运算符到底该写成成员还是友元?

这篇文章就从 0 基础视角 带你把 C++ 重载吃透:


一、什么是"重载"?Overload 的真正含义

1.1 定义

重载(Overload) 就是:

同一作用域 中,出现了多个 名字相同,但参数列表不同 的函数 / 运算符。
调用时由 编译器 根据实参类型来决定具体调用哪一个。

它是一种 编译期多态(compile-time polymorphism)。

典型例子:同名的 print

cpp 复制代码
void print(int i)       { cout << "int: " << i << endl; }
void print(double d)    { cout << "double: " << d << endl; }
void print(const string& s) { cout << "string: " << s << endl; }

int main() {
    print(10);          // 调用 print(int)
    print(3.14);        // 调用 print(double)
    print("hello");     // 调用 print(string)
}

名字都叫 print,但是参数类型不同,编译器会自动挑一个最合适的。


1.2 函数重载成立的条件

1)要点一句话:

名字相同,参数列表不同;返回值不能单独区分重载。

2)满足重载的几种情况:

  1. 参数类型不同

  2. 参数个数不同

  3. 参数顺序不同

**3)**示例:

cpp 复制代码
int Add(int a, int b);
double Add(double a, double b);   // 参数类型不同

int Add(int a, int b, int c);     // 参数个数不同

void func(char c, int i);
void func(int i, char c);         // 参数顺序不同

4)不构成重载的情况:

仅返回值不同,参数一致

cpp 复制代码
int func(int a);
double func(int a);   // ❌ 不构成重载,仅返回值不同

1.3 默认参数 + 重载 = 容易翻车

1)无法构成重载的两种极端:

cpp 复制代码
void func(int a);
void func(int a = 10);

不能 构成重载,因为参数列表本质上一样。

更离谱的是:

cpp 复制代码
void func();
void func(int a = 10);

func();   // ❌ 二义性:到底是调用 func() 还是 func(10)?

2)经验之谈:

判断是不是重载,

只看:参数个数、参数类型、参数顺序
不看:返回值、默认参数、函数体。


二、函数重载的底层:为什么 C 不支持,C++ 却支持?

这一块很多书喜欢讲得很玄,其实本质就两件事:编译流程 + 名字修饰(name mangling)

2.1 编译器干了啥?

简化版流水线:

源文件 .cpp → 预编译 → 编译(生成汇编) → 汇编(生成 .o/.obj) → 链接(生成可执行文件)

编译阶段 ,编译器会把函数名、全局变量名等整理成 符号表

链接阶段 ,链接器要根据符号表,去给每个"函数名"找到对应的地址。


2.2 C 的做法:函数名就是字面量

在 C 语言中,如果你写:

cpp 复制代码
int Add(int x, int y);
double Add(double x, double y);

编译器看到两个叫 Add 的函数,会直接判定符号重复------因为它在符号表里就是一个名字:_Add(不同平台略有变化),没有参数信息。

所以:

C 无法区分"同名不同参"的函数 → 不支持函数重载。


2.3 C++ 的做法:函数名被"改名"了(Name Mangling)

C++ 会把函数名 + 参数类型等信息,编码成一个新的符号名

比如在某些编译器中:

cpp 复制代码
int Add(int, int);          // 可能变成:?Add@@YAHHH@Z
double Add(double, double); // 可能变成:?Add@@YANNN@Z

在 Linux 的 g++ 下,大致规则类似:

  • _Z 开头

  • 之后是函数名长度 + 函数名 + 参数类型编码

比如:

cpp 复制代码
int func();

可能被修饰成:

bash 复制代码
_Z4funcv   // Z + 名字长度4 + func + v(无参数)

这样一来:

在链接阶段,"int 版 Add"和"double 版 Add"是两个完全不同的符号,互不冲突,就能支持函数重载了。

结论:
C++ 能函数重载,本质上就是:函数名被带上了参数信息,变成独一无二的"长名字"。


三、运算符重载总览:operator 其实就是"函数重载的一种"

3.1 什么是运算符重载?

运算符重载就是:
用函数的形式,为已有运算符赋予"对自定义类型"的新含义。

比如:

  • int 来说,+ 是整数加法

  • 对你自定义的 Vector2D 来说,也可以用 + 做向量加法

语法上:

cpp 复制代码
class A {
public:
    A operator+(const A& other) const {
        A res;
        // ...
        return res;
    }
};

这里的 operator+ 本质上就是一个普通成员函数,只是名字比较特殊而已


3.2 运算符重载的基本规则

  1. 不能创造新的运算符

    比如 operator**(幂运算)不行。

  2. 不能改优先级、结合性、操作数个数
    a + b * c 的运算顺序永远不会变。

  3. 重载至少要有一个操作数是用户自定义类型

    不允许你修改内置类型之间的行为(比如重载 int + int)。

  4. 有些运算符不能重载:

    • .::?:sizeof 等。
  5. 以下运算符必须作为成员函数重载:

    • =[]()->

四、典型运算符重载案例

4.1 重载 +:自定义类型的"加法"

cpp 复制代码
class Vec2 {
public:
    double x, y;

    Vec2(double x = 0, double y = 0) : x(x), y(y) {}

    Vec2 operator+(const Vec2& other) const {
        return Vec2(x + other.x, y + other.y);
    }
};

使用:

cpp 复制代码
Vec2 a(1, 2), b(3, 4);
Vec2 c = a + b;   // 实际调用 a.operator+(b)

小结:

双目运算符作为成员函数时:

  • 左操作数:隐含是 this(即 a

  • 右操作数:函数参数传入(即 b


4.2 前置++ / 后置++ 的正确写法(超高频)

1)内置类型语义回顾

  • i++(后置):

    • 返回旧值

    • 然后执行 i = i + 1

  • ++i(前置):

    • 先执行 i = i + 1

    • 再返回新值

自定义类型也必须保持 同样语义


2)前置++ 重载

cpp 复制代码
class A {
public:
    int x;

    A(int x = 0) : x(x) {}

    // 前置 ++i
    A& operator++() {
        ++x;       // 先自增
        return *this;  // 返回自身引用
    }
};

特点:

  • 修改自身

  • 返回修改后的自身引用

  • 不产生临时对象 → 更高效


3)后置++ 重载(int 参数只是占位)

cpp 复制代码
class A {
public:
    int x;

    A(int x = 0) : x(x) {}

    // 前置
    A& operator++() {
        ++x;
        return *this;
    }

    // 后置 i++
    const A operator++(int) {
        A tmp = *this;   // 保存旧值
        ++(*this);       // 复用前置++
        return tmp;      // 返回旧值(值传递)
    }
};

几个关键点:

  1. 参数列表中的 int 没有实际意义,只是为了区分前置 / 后置。

  2. 返回值是 对象,而不是引用。


(1)为什么后置++不能返回引用?

因为:

后置++ 要返回"旧值"。

旧值是一个 临时对象 tmp ,函数结束就销毁。

如果你返回 A& 引用,引用将指向已经死亡的对象,立刻变成 悬空引用

所以只能返回值,不可返回引用。


(2)为什么后置++常常写成 const A 返回?
cpp 复制代码
const A operator++(int);

目的是:

禁止 i++++ 这种写法。

分析:

  • i++ 返回旧值(一个临时对象)

  • 再对这个临时对象做 ++,根本不是在改原对象

  • 语义混乱且无用

加上 const,让返回的临时对象是只读的,自然不能再次 ++


(3)实践

对于迭代器、复杂自定义类型:

cpp 复制代码
for (Iterator it = v.begin(); it != v.end(); ++it)  // ✅ 推荐
// 而不是 it++

因为前置++ 不创造临时对象,更高效。


4.3 重载关系运算符:==、>、< 等

以一个简单类为例:

cpp 复制代码
class Point {
public:
    int x, y;
    Point(int x = 0, int y = 0) : x(x), y(y) {}
};

// 使用友元方式重载
bool operator==(const Point& a, const Point& b) {
    return a.x == b.x && a.y == b.y;
}

bool operator<(const Point& a, const Point& b) {
    return a.x < b.x;  // 假设只按 x 比
}

建议:

  • 关系运算符一般要"成对重载":

    • < 最好有 >

    • == 最好配 !=(通常实现成 !(a == b)

  • 具有 对称性 的运算符(如 ==<>),比较常用 全局函数 + 友元 写法,方便两边都能进行隐式转换。


4.4 重载输入输出运算符:<< 和 >>

<< / >> 是最常见也最容易写错的运算符。

关键点一句话:
它们的左操作数是 ostream / istream,所以通常需要写成全局函数 + 友元。

示例:

cpp 复制代码
class A {
public:
    int x, y;
    A(int x = 0, int y = 0) : x(x), y(y) {}

    friend ostream& operator<<(ostream& os, const A& a);
    friend istream& operator>>(istream& is, A& a);
};

ostream& operator<<(ostream& os, const A& a) {
    os << "x=" << a.x << ", y=" << a.y;
    return os;   // 支持链式输出:cout << a << b;
}

istream& operator>>(istream& is, A& a) {
    is >> a.x >> a.y;
    return is;   // 支持:cin >> a >> b;
}

使用:

cpp 复制代码
A a;
cin >> a;
cout << a << endl;

注意:

一般不要在 operator<< 里顺便输出换行,留给调用者决定什么时候换行。


4.5 赋值 =、[]、()、->:只能成员函数重载

这几个运算符 只能 以成员函数的形式重载:

  • operator=

  • operator[]

  • operator()

  • operator->

常见场景:

  • operator=:实现深拷贝

  • operator[]:做"数组类"、"字符串类"的下标访问

  • operator():构造"函数对象"(仿函数)

  • operator->:智能指针

这里只先记住规则,有具体类再细讲。


五、类型转换相关:类型转换运算符 & 转换构造函数

这两个经常在运算符重载一章一起讲,顺带说一下。

5.1 类型转换运算符:对象 -> 基本类型

写法:

cpp 复制代码
class A {
    int x;
public:
    A(int x = 0) : x(x) {}

    // 转换为 char
    operator char() const {
        return static_cast<char>(x);
    }
};

使用:

cpp 复制代码
A a(65);
char c = a;  // 自动调用 operator char()

特点:

  • 函数名是 operator 类型名

  • 没有返回类型(语法里不写)、没有参数

  • 通常写成 const 成员函数


5.2 转换构造函数:基本类型 / 其他类 -> 本类对象

如果一个构造函数 只有一个参数(且不是本类的 const 引用),那么它就可以作为"转换构造函数"使用。

cpp 复制代码
class A {
public:
    int x, y;

    A(int x = 0, int y = 0) : x(x), y(y) {}

    // 把 double 转成 A
    A(double d) {
        x = static_cast<int>(d);
        y = 0;
    }
};

A a = 3.14;   // 自动调用 A(double) 构造函数

区别:

  • 类型转换运算符:对象 → 其他类型

  • 转换构造函数:其他类型 → 本类

实际工程中,如果隐式转换太多会比较危险,常会配合 explicit 限制。


六、运算符重载:成员 vs 友元,怎么选

一个运算符能写成成员或友元,一般遵循这些经验:

  1. 单目运算符 (如前置++、取反 ! 等) → 优先写成成员函数

  2. 双目运算符

    • 如果左操作数必须是这个类(如 =[]()->) → 必须成员

    • 如果是对称运算符(如 ==<+ 等) → 用友元往往更灵活(两边都可隐式转换)

  3. 输入输出运算符 << / >>必须写成非成员(通常 + 友元) ,因为左边是 ostream/istream


七、重载 vs 重写 vs 隐藏(高频概念对比)

最后顺手把这几个概念也一起捋一下:

7.1 重载(Overload)

  • 同一作用域

  • 函数名相同

  • 参数列表不同

  • 编译期多态

7.2 重写(Override)

  • 父类 / 子类之间

  • 父类函数必须是 virtual

  • 子类函数参数列表、返回值(或协变)、const 修饰一致

  • 运行期多态(虚函数表)

cpp 复制代码
struct Base {
    virtual void foo(int);
};

struct Derived : Base {
    void foo(int) override;
};

7.3 隐藏(Name Hiding)

子类定义了与父类同名函数,但参数不同 / 非虚,导致父类同名函数在子类中被"盖住"。

cpp 复制代码
struct Base {
    void foo(int);
};

struct Derived : Base {
    void foo(double);  // 隐藏 Base::foo(int)
};

Derived d;
// d.foo(10);   // 只看到 Derived::foo(double)

可以用:

cpp 复制代码
using Base::foo;

把父类的重载集合引入当前作用域。


八、总结

你可以把下面这些当成记忆卡:

  1. 函数重载只看参数列表,不看返回值、不看默认参数。

  2. C++ 能重载,是因为函数名被编译器"改名"了(Name Mangling)。

  3. 运算符重载本质就是函数重载,只是函数名长得像 operator+

  4. 前置++ 返回引用,后置++ 返回对象;后置++ 的 int 只是占位。

  5. 后置++ 通常返回 const,禁止 i++++ 这种鬼东西。

  6. 输入输出运算符一定写成非成员(一般友元),返回流的引用以支持链式调用。

  7. =[]()-> 必须是成员函数重载。

  8. 重载(overload)是同名不同参,重写(override)是子类改父类虚函数。

相关推荐
2501_941144421 小时前
Python + C++ 异构微服务设计与优化
c++·python·微服务
程序猿编码1 小时前
PRINCE算法的密码生成器:原理与设计思路(C/C++代码实现)
c语言·网络·c++·算法·安全·prince
charlie1145141912 小时前
深入理解C/C++的编译链接技术6——A2:动态库设计基础之ABI设计接口
c语言·开发语言·c++·学习·动态库·函数
Cx330❀2 小时前
C++ STL set 完全指南:从基础用法到实战技巧
开发语言·数据结构·c++·算法·leetcode·面试
zmzb01032 小时前
C++课后习题训练记录Day33
开发语言·c++
Want5952 小时前
C/C++贪吃蛇小游戏
c语言·开发语言·c++
草莓熊Lotso3 小时前
《算法闯关指南:动态规划算法--斐波拉契数列模型》--01.第N个泰波拉契数,02.三步问题
开发语言·c++·经验分享·笔记·其他·算法·动态规划
草莓熊Lotso4 小时前
Git 分支管理:从基础操作到协作流程(本地篇)
大数据·服务器·开发语言·c++·人工智能·git·sql
报错小能手4 小时前
C++异常处理 终极及总结
开发语言·c++