在 C++ 中,函数重写(Overriding)平时也常叫做 “函数覆盖”,指的是派生类按照完全相同的格式,重新实现基类中已有的虚函数。
当基类的虚函数只提供了通用的默认行为,而某个派生类需要改用自己专属的处理逻辑时,我们就可以通过函数重写来替换掉基类的默认实现。
C++ 函数重写的基本用法
在类的继承关系中,如果希望派生类改写基类虚函数的行为,我们只需要在派生类里定义一个格式完全相同的成员函数即可。
我们以商城订单结算为例:普通订单 Order 按原价结算,会员订单 VipOrder 继承普通订单后,重写结算函数按 8 折计算实付金额。下面我们来看一个例子。
示例 1:重写基类的虚函数
#include <iostream>
class Order {
public:
virtual void checkout(double amount) {
std::cout << "普通订单,实付金额:" << amount << " 元\n";
}
};
class VipOrder : public Order {
public:
// 重写基类虚函数
void checkout(double amount) {
std::cout << "会员订单(8 折),实付金额:" << amount * 0.8 << " 元\n";
}
};
int main() {
VipOrder vip;
Order& order = vip;
order.checkout(100.0);
return 0;
}运行结果如下。
会员订单(8 折),实付金额:80 元分析:
在这个例子中,基类 Order 将 checkout(double amount) 声明为了虚函数。派生类 VipOrder 中的同名函数在名称、参数类型 double 和返回值类型 void 上都与基类完全一致,因此构成了函数重写。
一旦重写关系成立,当我们通过基类引用 order 调用 order.checkout(100.0) 时,程序就会在运行时自动执行 VipOrder 重写后的版本,按 8 折输出 80 元。
从上面的例子可以看出,函数重写写起来非常直观。不过小伙伴们要注意,派生类想要成功重写基类的成员函数,并不是函数名一样就可以,必须同时满足以下 3 个条件:
- 基类中的原函数必须在声明前面加上
virtual关键字。 - 派生类函数的名称、参数个数、参数类型以及返回值类型,都必须与基类虚函数完全一致。
- 如果基类虚函数是 const 成员函数,派生类函数末尾也必须加上
const。如果基类没有const,派生类也不能多加。
在重写函数中调用基类函数
在实际开发中,派生类重写基类的虚函数,往往不是为了把基类原有的代码全部推翻,而是希望 “先保留基类已完成的基础操作,再补充派生类新增的操作”。
遇到这种需求时,我们不需要把基类的代码复制粘贴一遍,直接在派生类的重写函数内部通过 基类名::函数名(实参) 调用基类的版本就可以了。
语法:
返回值类型 函数名(参数列表) {
基类名::函数名(实参);
// 派生类新增的逻辑
}说明:
函数名前面的 基类名:: 是作用域限定符,用来明确告诉编译器:这里要调用的是基类里的那个版本,而不是当前派生类自己的版本。
示例 2:重写时复用基类逻辑
#include <iostream>
#include <string>
class Order {
public:
std::string orderId;
Order(std::string id) : orderId(id) {}
virtual void showInfo() {
std::cout << "订单编号:" << orderId << '\n';
}
};
class VipOrder : public Order {
public:
int points;
VipOrder(std::string id, int p) : Order(id), points(p) {}
void showInfo() {
Order::showInfo(); // 先调用基类的版本
std::cout << "赠送积分:" << points << '\n';
}
};
int main() {
VipOrder vip("NO202601", 200);
Order& order = vip;
order.showInfo();
return 0;
}运行结果如下。
订单编号:NO202601
赠送积分:200分析:
Order::showInfo(); 这句代码表示在 VipOrder 的重写函数内部,先调用基类 Order 的 showInfo() 打印订单编号,紧接着再执行下一行代码打印新增的赠送积分。
这里小伙伴们要特别注意:在重写函数内部调用基类同名函数时,前面的 Order:: 千万不能省略。如果写成下面这样,就会变成函数自己无限调用自己,直接导致程序崩溃:
void showInfo() {
showInfo(); // 漏写 Order::,导致无限递归
std::cout << "赠送积分:" << points << '\n';
}C++ 函数隐藏
如果派生类定义了一个与基类同名的成员函数,却没有完全满足前面的 3 个重写条件,此时并不会构成函数重写,而是会触发 “函数隐藏”。比如基类漏加了 virtual、两边参数列表不同,或者末尾的 const 不一致,都属于这种情况。
所谓的函数隐藏,说白了就是派生类的同名函数在子类内部把基类的同名函数给 “遮住” 了。哪怕两个函数的参数列表完全不同,只要函数名一样,基类的同名函数也会被直接隐藏。请看下面例子。
示例 3:同名不同参触发函数隐藏
#include <iostream>
class Order {
public:
virtual void checkout(double amount) {
std::cout << "普通订单结算:" << amount << " 元\n";
}
};
class VipOrder : public Order {
public:
// 参数列表不同,隐藏基类同名函数
void checkout() {
std::cout << "会员免密快捷结算\n";
}
};
int main() {
VipOrder vip;
Order& order = vip;
vip.checkout();
vip.Order::checkout(100.0);
order.checkout(100.0);
return 0;
}运行结果如下。
会员免密快捷结算
普通订单结算:100 元
普通订单结算:100 元分析:
这里基类 Order 定义的是带参数的 checkout(double amount),而派生类 VipOrder 定义的是无参的 checkout()。由于参数列表不一致,两者并没有构成重写,通过基类引用执行 order.checkout(100.0) 时调用的依然是基类版本。
与此同时,无参的 checkout() 还在子类内部把基类的同名函数遮住了。如果我们直接通过子类对象带参调用,就会编译报错:
VipOrder vip;
vip.checkout(100.0); // 编译报错:基类同名函数已被隐藏可能你会觉得很奇怪:为什么 vip.checkout(100.0) 没有像函数重载那样,自动匹配到基类的 checkout(double) 呢?
这是因为函数重载只发生在同一个类的内部,而基类和派生类属于两个不同的作用域。编译器在 VipOrder 里找到名叫 checkout 的函数后,就会停止往基类查找,必须像 vip.Order::checkout(100.0) 这样加上作用域限定符才能调用基类版本。
除了参数列表不一致之外,漏写函数末尾的 const 也是初学者容易踩坑的地方。如果基类虚函数带有 const,而派生类漏写了 const,同样会变成函数隐藏。我们来看下面这个例子。
示例 4:漏写 const 导致重写失败
#include <iostream>
class Order {
public:
virtual void showStatus() const {
std::cout << "普通订单状态\n";
}
};
class VipOrder : public Order {
public:
// 漏写 const,未构成重写
void showStatus() {
std::cout << "会员订单状态\n";
}
};
int main() {
VipOrder vip;
Order& order = vip;
vip.showStatus();
order.showStatus();
return 0;
}运行结果如下。
会员订单状态
普通订单状态分析:
从运行结果可以看出,直接通过子类对象执行 vip.showStatus() 时输出的是 “会员订单状态”,但通过基类引用执行 order.showStatus() 时,输出的却是基类的 “普通订单状态”。
原因就在于基类的 showStatus() const 末尾带有 const,而派生类的 showStatus() 漏写了 const。编译器会把它们看成两个格式不同的函数,因此派生类并没有成功重写基类的虚函数。
提示 写错参数或漏写 const 时,编译器默认不会报错,而是静默变成函数隐藏,排查起来比较费劲。在实际开发中,我们推荐配合后面学习的 override 说明符一起使用,一旦格式不匹配编译器就会直接报错提醒。
重载、重写与隐藏的区别
学到这里,我们已经接触了函数重载、函数重写和函数隐藏这三个名字相近的概念。它们在所在范围、格式要求和调用表现上有着明显的区别,如下表所示。
| 概念 | 所在范围 | 格式要求 | 调用表现 |
|---|---|---|---|
| 函数重载 | 同一个类中 | 函数名相同,参数列表不同 | 编译时按实参匹配函数 |
| 函数重写 | 基类与派生类 | 基类加 virtual,函数格式完全一致 | 基类引用或指针调用子类版本 |
| 函数隐藏 | 基类与派生类 | 函数名相同,但不满足重写条件 | 子类对象调用时遮住基类函数 |
平时区分这三者时,小伙伴们只需要分两步来判断。第一步先看两个同名函数是不是在同一个类里,在同一个类里就是函数重载。如果分别在基类和派生类中,第二步再看基类有没有加 virtual 且格式是否完全一致,全部满足就是函数重写,否则都属于函数隐藏。
