C++ 里有一组概念,几乎每次面试都会问,每次项目重构都会踩坑:函数重载、隐藏、覆盖,以及基类指针和派生类指针之间的类型转换。很多人把这些概念背得很熟,但一碰到实际代码就分不清——尤其是当函数名、参数表长得一模一样,或者用了不合适的类型转换把程序直接搞崩的时候。我写这篇内容不打算复述教科书,而是想把这几个概念放回真实的工程场景里,从继承、多态、类型转换三条线串起来,说清楚它们各自解决什么问题,又分别会在哪些地方坑人。
这篇文章适合刚学完C++语法、正在啃面向对象这部分的新手,也适合写了两年业务代码、想彻底把继承和多态理清楚的朋友。我会尽量把重载、隐藏、覆盖的判定流程、虚函数表的底层逻辑、以及四种类型转换运算符的适用场景都拆开讲,每一个结论都配上可以直接上机的代码片段。
1. 函数重载、隐藏、覆盖:最容易混淆的三兄弟
1.1 先下定义:同名函数到底分几种
C++ 里同名函数看似简单,实则可以分为三种完全不同的关系,判断依据是“作用域是否相同、是否虚函数、参数列表是否一致”三个维度。
- 重载(Overload):同一个作用域内,函数名相同,参数列表不同。典型场景是同一个类里写多个构造函数,或者提供
print(int)和print(string)。 - 隐藏(Hide):派生类中存在与基类同名的函数,无论参数是否相同、无论基类版本是不是虚函数,基类的同名函数都会被隐藏。隐藏的核心是“名字遮蔽”,编译器一旦在派生类作用域找到了这个名字,就不会继续到基类作用域去找。
- 覆盖(Override):基类函数是虚拟函数,派生类用完全相同的函数签名重新实现它。覆盖是多态的基础,只有覆盖了虚函数,才能通过基类指针或引用调用到派生类版本。
我见过不少人把隐藏和覆盖混为一谈,实际上它们没有必然联系。覆盖要求参数、函数名、返回类型(或协变返回类型)完全匹配,并且基类函数必须是虚函数;隐藏则只要派生类里出现一个同名函数就行,哪怕参数列表完全不同。
下面这段代码把三种情况放在一起,一眼就能看出区别:
cpp复制#include <iostream>
using namespace std;
class Base {
public:
// 重载:同一个类里的两个 func
void func(int x) {
cout << "Base::func(int) " << x << endl;
}
void func(double x) {
cout << "Base::func(double) " << x << endl;
}
// 虚函数,准备被派生类覆盖
virtual void draw() {
cout << "Base::draw" << endl;
}
void print() {
cout << "Base::print" << endl;
}
};
class Derived : public Base {
public:
// 隐藏:派生类的 func 把基类两个 func 都藏起来了
void func(int x, int y) {
cout << "Derived::func(int,int) " << x << "," << y << endl;
}
// 覆盖:签名与基类虚函数完全一致
void draw() override {
cout << "Derived::draw" << endl;
}
// 隐藏:非虚函数,无 override 关键字
void print() {
cout << "Derived::print" << endl;
}
};
用 Derived 对象调用时,d.func(1) 会直接编译报错,因为 Derived 作用域里有 func(int, int),编译器找到了名字后在当前作用域匹配参数,发现没有 func(int),并不会回头去基类里找。这就是隐藏带来的第一个坑。
1.2 隐藏这个“坑”,我建议你用两招避开
第一招是显式调用基类版本,d.Base::func(1);第二招是在 Derived 类里加 using Base::func; 声明,把基类所有同名重载引入派生类作用域。两种方式按场景选择,我更推荐 using,因为它语义清晰,不会在代码里散布一堆 Base:: 前缀。
实际工程里我喜欢加一行注释说明为什么这样做,防止后人“好心”把这行删掉:
cpp复制class Derived : public Base {
public:
// 让基类的 func(int) 和 func(double) 参与到重载决议中
using Base::func;
void func(int x, int y) {
cout << "Derived::func(int,int) " << x << "," << y << endl;
}
};
加了 using 之后,d.func(1) 会调用 Base::func(int),d.func(1, 2) 调用 Derived::func(int, int),两者共存,互不干扰。
关于隐藏,还有一个容易被忽略的细节:构造函数、拷贝构造函数等特殊的成员函数不存在“隐藏”的概念,因为它们的函数名是类名,派生类和基类名字不同,不可能同名。但析构函数是特例,虽然名字不同,delete 一个 Base* 指向派生类对象时,如果基类析构函数不是虚函数,就会只调用 Base::~Base()
