C++ 函数重写

在 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 且格式是否完全一致,全部满足就是函数重写,否则都属于函数隐藏。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号