HOWTO · C++

C++ 中的函数重载 VS 覆盖

本文演示如何在 C++ 中使用函数重载 VS 覆盖

本页内容

函数重载和函数重写在 C++ 中解决的是不同问题。函数重载是在同一作用域中为参数列表不同的函数使用相同名称;编译器会根据调用时传入的实参选择匹配项。函数重写则允许派生类为继承的 virtual 函数提供匹配实现;通过基类引用或指针调用时,程序会在运行时将调用派发给派生类对象。

C++ 中函数重载与函数重写的含义

函数重载是从参数不同的函数中选择合适的函数;函数重写则是在继承层次中替换函数行为。仅有不同的返回类型不足以构成重载,而函数重写要求基类函数是虚函数。override说明符会让编译器检查派生类中的声明是否确实匹配基类虚函数。

函数重载的工作方式

在同一作用域中声明多个同名函数,并为它们指定不同的参数列表。参数列表可以在参数数量或类型上有所不同。调用时,重载解析会根据实参及候选函数的参数类型进行选择。返回类型不会用于选择重载,因此,仅返回类型不同的两个声明不能组成有效的重载集合。

运行下面的示例:

本节中的代码以及显示的标签或输出仍为英文。

#include <iostream>

class Formatter {
public:
    void show(int value) const {
        std::cout << "integer: " << value << '\n';
    }

    void show(double value) const {
        std::cout << "decimal: " << value << '\n';
    }
};

int main() {
    Formatter formatter;
    formatter.show(7);
    formatter.show(2.5);
}
integer: 7
decimal: 2.5

提供的运行证据确认,此示例已成功执行,并产生了所示输出。整数和小数实参会选择不同的 show 重载。添加重载时,请确保参数列表有实际区别,并且调用不会产生歧义。

函数重写的工作方式

基类将函数声明为 virtual;派生类则提供名称和参数列表相同,且限定符与返回类型兼容的函数。为派生类中的声明标注 override,这样不匹配就会产生编译期诊断。通过基类引用或指针调用函数时,虚函数派发会为该对象选择最终重写函数。

运行下面的示例:

本节中的代码以及显示的标签或输出仍为英文。

#include <iostream>

class Message {
public:
    virtual void print() const {
        std::cout << "message\n";
    }
};

class Alert : public Message {
public:
    void print() const override {
        std::cout << "alert\n";
    }
};

int main() {
    Alert alert;
    const Message& message = alert;
    message.print();
}
alert

提供的运行证据确认,此示例已成功执行,并产生了所示输出。虽然 message 的静态类型是基类,但它引用的是 Alert 对象,因此这次虚函数调用会执行 Alert::print。基类也可以声明纯虚函数,例如使用 = 0;这样的类是抽象类,无法实例化,除非具体派生类实现了它的纯虚函数。

函数重载与函数重写:主要区别

对比方面 函数重载 函数重写
目的 为不同实参提供同名操作 为派生类型专门实现继承的行为
要求 同一作用域中存在不同的参数列表 存在继承关系,并且基类有匹配的 virtual 函数
选择方式 根据调用实参在编译期进行重载解析 通过基类引用或指针在运行时进行虚函数派发
有用的检查 确保调用会选中预期的重载 添加 override,检查声明是否匹配

重载可以不依赖继承或虚函数。函数重写则依赖继承和虚函数派发;它不会因为传入不同的参数列表而被选中。

常见错误及识别方法

派生类中的函数即使与基类函数同名,也不一定构成重写。如果基类函数不是虚函数,派生类声明会隐藏基类中的同名函数,而不会启用虚函数派发。如果参数、const限定符或函数类型的其他部分不匹配,预期的重写就可能失败;写上 override 可以让编译器在编译期指出这种不匹配。

不要只通过更改返回类型来尝试重载。如果需要多态行为,也不要按值传递派生类对象:将其复制到基类对象会发生对象切片,派生部分会被丢弃。使用基类引用或指针来保留动态派发。一般情况下,优先使用自动存储期对象或智能指针;如果要通过基类指针删除派生类对象,基类应具有虚析构函数。