c++:虚函数也可以有默认实现吗?

前言

有人前不久去面试的时候,面试官突然抛出了一个问题:
“虚函数也可以有默认函数吗?”

这个问题乍一看很简单,但背后其实能延伸出很多关于 C++ 语言设计和软件工程实践的内容。

这个回答当然是:

可以。虚函数完全可以有默认实现,甚至纯虚函数也能写函数体。

这个问题这其实是个不错的切入点,可以展开聊聊虚函数的方方面面,于是希望这篇文章能帮到准备面试或者想深入理解 C++ 的同学。


一、什么是虚函数?

在 C++ 里,虚函数(virtual function)是实现 运行时多态 的关键。它的本质就是:

  • 在基类里定义函数,并声明为 virtual

  • 在派生类中可以重写这个函数。

  • 当通过基类指针或引用调用时,实际执行的是派生类的版本(动态绑定)。

举个最经典的例子:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void sayHello() {
        cout << "Hello from Base" << endl;
    }
};

class Derived : public Base {
public:
    void sayHello() override {
        cout << "Hello from Derived" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->sayHello(); // 输出:Hello from Derived
    delete ptr;
}

这里 sayHello 就是一个虚函数,它让调用发生在运行时决定,而不是编译时决定。


二、虚函数是否可以有默认实现?

1. 普通虚函数

当然可以。
虚函数并不是等于“必须要子类实现”。 它只是告诉编译器:这个函数要走虚函数表(vtable)的动态分派机制。

所以我们完全可以在基类里提供一个默认实现:

class Base {
public:
    virtual void foo() {
        cout << "Base default foo()" << endl;
    }
};

如果派生类不重写,那么它就会直接继承这个默认逻辑。


2. 纯虚函数

更有意思的是:
纯虚函数(=0)也可以有默认实现!

class Base {
public:
    virtual void foo() = 0; // 纯虚函数
};

void Base::foo() {
    cout << "Base::foo() default implementation" << endl;
}

class Derived : public Base {
public:
    void foo() override {
        Base::foo(); // 先用基类逻辑
        cout << "Derived::foo()" << endl;
    }
};

输出结果:

Base::foo() default implementation
Derived::foo()

这说明即便是纯虚函数,编译器也允许你在基类里提供一个实现。区别只是:

  • =0 标记后,基类变成抽象类,不能实例化。

  • 派生类必须显式重写,除非它自己也想保持抽象。


三、为什么要给虚函数一个默认实现?

面试的时候如果只停留在语法层面,答案未免太“干巴巴”。面试官更想听到的是你的设计思维

1. 避免重复代码

很多情况下,子类可能大多数逻辑都一样,只在少数地方有差异。
这时基类提供一个默认实现,派生类只需要覆盖差异部分即可,避免重复造轮子。

2. 提供兜底逻辑

有些接口必须实现,但又希望在特殊情况下给出一个“保底”逻辑。
例如:

  • 如果派生类没有实现日志系统,那至少打印到 stdout

  • 如果派生类没有实现错误处理,那至少抛个异常。

3. 便于扩展

假如将来要增加新的派生类,它可以直接继承基类的默认实现,快速使用,而不用立刻写一堆重复代码。


四、普通虚函数 vs 纯虚函数

很多同学容易混淆两者,我整理了一张表:

特性普通虚函数纯虚函数
定义方式virtual void f();virtual void f() = 0;
是否必须实现否,可以直接用基类实现是,派生类必须重写
是否能有实现体可以可以(少见但合法)
是否使类抽象化是,类不可实例化
适用场景框架基类、可选重写接口定义、强制重写

关键点

  • =0 并不是说“这个函数没有实现”,而是说“这个类是抽象类”。

  • 实现体写不写,是两回事。


五、虚函数表(vtable)机制简析

面试官有时会追问:“为什么虚函数能有默认实现?它底层是怎么实现的?”

这就涉及到 虚函数表(vtable)

  • 每个包含虚函数的类,编译器会生成一张虚函数表,里面存放函数指针。

  • 对象里有一个指向这张表的指针(vptr)。

  • 调用虚函数时,会通过 vptr 找到实际函数地址执行。

举个例子:

class Base {
public:
    virtual void foo() { cout << "Base foo()" << endl; }
};

class Derived : public Base {
public:
    void foo() override { cout << "Derived foo()" << endl; }
};

在运行时,Derived 的 vtable 会把 foo 指向 Derived::foo
而如果没有 override,就会沿用 Base::foo

所以说,虚函数能否有默认实现,完全取决于 vtable 是否有指针指向它。答案显然是肯定的。


六、实际开发中的用法

1. 接口类(Interface)

通常我们会写一个纯虚函数接口类:

class IShape {
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual ~IShape() {}
};

这样强制子类必须实现。
但如果我们想给一个默认逻辑(比如 perimeter 里先调用 area),那也可以给它加实现。


2. 框架基类

比如写一个 Logger

class Logger {
public:
    virtual void log(const string& msg) {
        cout << "[Default log] " << msg << endl;
    }
};

 

子类如果不关心日志,就直接用默认实现;
如果要写文件,就 override。


3. 混合设计

有时一个类既有普通虚函数,也有纯虚函数:

class AbstractHandler {
public:
    virtual void init() { cout << "Default init" << endl; }
    virtual void handle() = 0; // 强制实现
};

 

这种混搭的设计在框架里很常见:

  • init 给一个默认逻辑,但允许子类定制;

  • handle 强制子类必须实现。


七、常见面试陷阱

面试官可能会绕几个弯子:

  1. 虚函数能是构造函数吗?
    → 不行,构造函数不能是虚函数。

  2. 析构函数要不要写成虚的?
    → 如果类可能被继承,必须写虚析构,否则 delete 基类指针会内存泄漏。

  3. 纯虚函数能否有实现?
    → 可以,但类依然是抽象类,不能直接实例化。

  4. 虚函数影响性能吗?
    → 有一点点开销(一次指针间接寻址),但大多数情况下可以忽略。


八、最佳实践总结

  • 如果希望可选重写,就用普通虚函数并给一个默认实现。

  • 如果希望必须重写,就用纯虚函数,但仍可提供兜底实现。

  • 如果基类会被继承,一定要有虚析构函数。

  • 避免在构造函数和析构函数中调用虚函数(因为此时 vtable 可能不完整)。


九、结语

回到最初的问题:
虚函数可以有默认实现吗?


答案是:可以,甚至纯虚函数也能写函数体。

理解了这一点,就不会被 =0 迷惑。虚函数机制的精髓在于 运行时多态,而默认实现只是语法和设计上的灵活性。

从设计角度看,虚函数的默认实现其实是 “接口与抽象”“复用与扩展” 的平衡点。
这也是为什么很多优秀的 C++ 框架源码里,你会看到“纯虚函数带实现”的写法——它并不是奇技淫巧,而是一种深思熟虑的工程实践。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渡我白衣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值