目录
[一、什么是"重载"?Overload 的真正含义](#一、什么是“重载”?Overload 的真正含义)
[1.1 定义](#1.1 定义)
[1.2 函数重载成立的条件](#1.2 函数重载成立的条件)
[1.3 默认参数 + 重载 = 容易翻车](#1.3 默认参数 + 重载 = 容易翻车)
[二、函数重载的底层:为什么 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 前置++ / 后置++ 的正确写法(超高频))
[2)前置++ 重载](#2)前置++ 重载)
[3)后置++ 重载(int 参数只是占位)](#3)后置++ 重载(int 参数只是占位))
[(2)为什么后置++常常写成 const A 返回?](#(2)为什么后置++常常写成 const A 返回?)
[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)满足重载的几种情况:
-
参数类型不同
-
参数个数不同
-
参数顺序不同
**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 运算符重载的基本规则
-
不能创造新的运算符
比如
operator**(幂运算)不行。 -
不能改优先级、结合性、操作数个数
a + b * c的运算顺序永远不会变。 -
重载至少要有一个操作数是用户自定义类型
不允许你修改内置类型之间的行为(比如重载
int + int)。 -
有些运算符不能重载:
.、::、?:、sizeof等。
-
以下运算符必须作为成员函数重载:
=、[]、()、->
四、典型运算符重载案例
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; // 返回旧值(值传递)
}
};
几个关键点:
-
参数列表中的
int没有实际意义,只是为了区分前置 / 后置。 -
返回值是 对象,而不是引用。
(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 友元,怎么选
一个运算符能写成成员或友元,一般遵循这些经验:
-
单目运算符 (如前置++、取反
!等) → 优先写成成员函数 -
双目运算符:
-
如果左操作数必须是这个类(如
=,[]、()、->) → 必须成员 -
如果是对称运算符(如
==、<、+等) → 用友元往往更灵活(两边都可隐式转换)
-
-
输入输出运算符
<</>>→ 必须写成非成员(通常 + 友元) ,因为左边是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;
把父类的重载集合引入当前作用域。
八、总结
你可以把下面这些当成记忆卡:
-
函数重载只看参数列表,不看返回值、不看默认参数。
-
C++ 能重载,是因为函数名被编译器"改名"了(Name Mangling)。
-
运算符重载本质就是函数重载,只是函数名长得像
operator+。 -
前置++ 返回引用,后置++ 返回对象;后置++ 的 int 只是占位。
-
后置++ 通常返回 const,禁止
i++++这种鬼东西。 -
输入输出运算符一定写成非成员(一般友元),返回流的引用以支持链式调用。
-
=、[]、()、->必须是成员函数重载。 -
重载(overload)是同名不同参,重写(override)是子类改父类虚函数。