函数重载
定义与作用
函数重载(Function Overloading)允许在同一个作用域内定义多个同名但参数列表不同的函数。编译器根据调用时的实参类型自动选择最匹配的版本,这个过程称为重载决议(Overload Resolution)。
重载的核心价值:
- 接口统一:相同语义的操作使用同一个名字
- 类型安全:编译器在编译期选择正确版本
- 泛型基础:模板函数与重载配合形成强大的泛型机制
核心原理
重载决议三步骤
const 重载
成员函数可以通过 const 修饰实现重载,提供读写和只读两种接口:
class Container {
public:
int& operator[](size_t i); // 非 const 版本:可读写
const int& operator[](size_t i) const; // const 版本:只读
};
ADL(Argument-Dependent Lookup)
ADL 使得 std::cout << "hello" 中的 operator<< 能在 std 命名空间中被找到。
完整示例
示例一:日志系统的重载设计
场景说明:设计一个日志系统,通过重载支持多种数据类型的输出。
#include <iostream>
#include <string>
#include <vector>
#include <iomanip>
// 日志级别枚举
enum class LogLevel { Info, Warning, Error };
// 基础输出函数 — 不同数据类型的重载
void log(LogLevel level, const std::string& msg) {
const char* prefix;
switch (level) {
case LogLevel::Info: prefix = "[INFO] "; break;
case LogLevel::Warning: prefix = "[WARN] "; break;
case LogLevel::Error: prefix = "[ERROR]"; break;
default: prefix = "[????] ";
}
std::cout << prefix << " " << msg << std::endl;
}
// 重载:接收整数
void log(LogLevel level, int value) {
log(level, std::to_string(value));
}
// 重载:接收浮点数
void log(LogLevel level, double value) {
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << value;
log(level, oss.str());
}
// 重载:接收布尔值
void log(LogLevel level, bool value) {
log(level, std::string(value ? "true" : "false"));
}
// 重载:简洁版本(默认 Info 级别)
void log(const std::string& msg) {
log(LogLevel::Info, msg);
}
int main() {
log("系统启动"); // 调用简洁版
log(LogLevel::Info, "数据库连接成功"); // 完整版 string
log(LogLevel::Warning, 404); // 整数版
log(LogLevel::Error, 3.14159); // 浮点版
log(LogLevel::Info, true); // 布尔版
return 0;
}
预期输出:
[INFO] 系统启动
[INFO] 数据库连接成功
[WARN] 404
[ERROR] 3.14
[INFO] true
逐段分析:
log(LogLevel, string)是主实现,其他重载版本转换为 string 后委托给它——DRY 原则- 编译器根据实参类型自动选择:
404匹配int、3.14159匹配double、true匹配bool - 简洁版
log(string)默认使用 Info 级别,通过参数数量不同实现重载 - 如果添加
log(LogLevel, long),log(LogLevel::Info, 404)会产生歧义(int → long 和 int → double 都是标准转换)
示例二:数学向量类的 const 重载与 ADL
场景说明:实现二维向量类,通过 const 重载提供读写/只读访问,并利用 ADL 实现运算符。
#include <iostream>
#include <cmath>
namespace math {
class Vec2 {
public:
Vec2(double x, double y) : x_(x), y_(y) {}
// const 重载:只读访问
double x() const { return x_; }
double y() const { return y_; }
// 非 const 重载:可修改访问
double& x() { return x_; }
double& y() { return y_; }
double length() const {
return std::sqrt(x_ * x_ + y_ * y_);
}
private:
double x_, y_;
};
// 全局运算符:通过 ADL 找到
Vec2 operator+(const Vec2& a, const Vec2& b) {
return Vec2(a.x() + b.x(), a.y() + b.y());
}
Vec2 operator-(const Vec2& a, const Vec2& b) {
return Vec2(a.x() - b.x(), a.y() - b.y());
}
// operator<< 也在 math 命名空间,ADL 可找到
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << "(" << v.x() << ", " << v.y() << ")";
}
} // namespace math
int main() {
using math::Vec2; // 只引入 Vec2,其他通过 ADL
Vec2 pos(3.0, 4.0);
Vec2 offset(1.0, 2.0);
// 无需 using math::operator+,ADL 自动查找
auto result = pos + offset;
auto diff = pos - offset;
// 无需 using math::operator<<
std::cout << pos << " + " << offset << " = " << result << std::endl;
std::cout << pos << " - " << offset << " = " << diff << std::endl;
// const 重载的使用
const Vec2 constant_vec(10.0, 0.0);
std::cout << "常量向量的长度: " << constant_vec.length() << std::endl;
// constant_vec.x() = 5.0; // 编译错误:const 版本返回 const double&
// 非 const 对象可以使用读写版本
Vec2 mutable_vec(1.0, 1.0);
mutable_vec.x() = 100.0; // 调用非 const 重载
std::cout << "修改后: " << mutable_vec << std::endl;
return 0;
}
预期输出:
(3, 4) + (1, 2) = (4, 6)
(3, 4) - (1, 2) = (2, 2)
常量向量的长度: 10
修改后: (100, 1)
逐段分析:
x()有两个重载:非 const 版返回double&(可修改),const 版返回double(只读)const Vec2对象调用x()时,自动选择 const 版本——编译器根据对象的 const 限定符匹配operator+和operator<<定义在math命名空间中,Vec2也在math中——ADL 使得这些函数在不写using的情况下也能被找到- ADL 避免了写
math::operator+(pos, offset)的冗长语法,使得pos + offset可以自然书写
示例三:重载歧义与解决方案
#include <iostream>
void f(int) { std::cout << "f(int)" << std::endl; }
void f(double) { std::cout << "f(double)" << std::endl; }
void f(long) { std::cout << "f(long)" << std::endl; }
int main() {
f(42); // 精确匹配 f(int)
f(3.14); // 精确匹配 f(double)
f('A'); // char → int 提升,匹配 f(int)
f(42L); // 精确匹配 f(long)
// f(3.14f); // 歧义!float → int? float → double? float → long?
// 三者都是标准转换,无以分出最佳
// 解决方案:显式转换
f(static_cast<double>(3.14f)); // 明确调 f(double)
return 0;
}
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 只有返回类型不同的重载 | 编译错误 | 重载必须参数列表不同 |
| 派生类同名函数隐藏基类所有重载 | 基类重载不可见 | 使用 using Base::f; 引入 |
| 默认参数与重载引发歧义 | 意外匹配 | 减少默认参数,使用重载替代 |
| 窄化转换导致意外匹配 | 调用了意料外的重载 | 使用显式转换或调整重载设计 |
| 不同命名空间的函数通过 ADL 被意外找到 | 行为不符合预期 | 注意命名空间设计 |
常见面试问题
重载决议的三步骤是什么?——建立候选函数集 → 筛选可行函数 → 选择最佳可行函数。最佳匹配通过隐式转换序列的等级排名决定。
重载(overload)、覆盖(override)、隐藏(hide)的区别?——重载是同作用域同名不同参;覆盖是派生类虚函数替代基类虚函数;隐藏是派生类同名非虚函数屏蔽基类所有同名函数。
ADL 是什么?为什么需要它?——Argument-Dependent Lookup,在函数实参的关联命名空间中查找函数。它让
cout << "hello"能自动找到std::operator<<。为什么
void f(int)和void f(const int)不构成重载?——顶层 const 被忽略。void f(int)和void f(int&)可以重载。void f(int*)和void f(const int*)能重载吗?——能。底层 const 不被忽略,这两个函数的参数类型不同。
小结
- 重载决议三步骤:候选 → 可行 → 最佳(精确匹配 > 提升 > 标准转换)
- const 重载提供读写/只读双接口,编译器根据对象 const 性自动选择
- ADL 让运算符和自定义类型的函数调用更自然
- 派生类同名函数会隐藏基类所有重载,用
using Base::f;引入 - 避免仅返回类型不同的重载、避免重载与默认参数混用以防歧义