有人前不久去面试的时候,面试官突然抛出了一个问题: “虚函数也可以有默认函数吗?”
这个问题乍一看很简单,但背后其实能延伸出很多关于 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 就是一个虚函数,它让调用发生在运行时决定,而不是编译时决定。
当然可以。 虚函数并不是等于“必须要子类实现”。 它只是告诉编译器:这个函数要走虚函数表(vtable)的动态分派机制。
所以我们完全可以在基类里提供一个默认实现:
class Base {
public:
virtual void foo() {
cout << "Base default foo()" << endl;
}
};如果派生类不重写,那么它就会直接继承这个默认逻辑。
更有意思的是:
纯虚函数(=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 标记后,基类变成抽象类,不能实例化。
面试的时候如果只停留在语法层面,答案未免太“干巴巴”。面试官更想听到的是你的设计思维。
很多情况下,子类可能大多数逻辑都一样,只在少数地方有差异。 这时基类提供一个默认实现,派生类只需要覆盖差异部分即可,避免重复造轮子。
有些接口必须实现,但又希望在特殊情况下给出一个“保底”逻辑。 例如:
stdout。
假如将来要增加新的派生类,它可以直接继承基类的默认实现,快速使用,而不用立刻写一堆重复代码。
很多同学容易混淆两者,我整理了一张表:
特性 | 普通虚函数 | 纯虚函数 |
|---|---|---|
定义方式 | virtual void f(); | virtual void f() = 0; |
是否必须实现 | 否,可以直接用基类实现 | 是,派生类必须重写 |
是否能有实现体 | 可以 | 可以(少见但合法) |
是否使类抽象化 | 否 | 是,类不可实例化 |
适用场景 | 框架基类、可选重写 | 接口定义、强制重写 |
关键点:
=0 并不是说“这个函数没有实现”,而是说“这个类是抽象类”。
面试官有时会追问:“为什么虚函数能有默认实现?它底层是怎么实现的?”
这就涉及到 虚函数表(vtable)。
举个例子:
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 是否有指针指向它。答案显然是肯定的。
通常我们会写一个纯虚函数接口类:
class IShape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~IShape() {}
};这样强制子类必须实现。 但如果我们想给一个默认逻辑(比如 perimeter 里先调用 area),那也可以给它加实现。
比如写一个 Logger:
class Logger {
public:
virtual void log(const string& msg) {
cout << "[Default log] " << msg << endl;
}
};子类如果不关心日志,就直接用默认实现; 如果要写文件,就 override。
有时一个类既有普通虚函数,也有纯虚函数:
class AbstractHandler {
public:
virtual void init() { cout << "Default init" << endl; }
virtual void handle() = 0; // 强制实现
};这种混搭的设计在框架里很常见:
init 给一个默认逻辑,但允许子类定制;
handle 强制子类必须实现。
面试官可能会绕几个弯子:
回到最初的问题: 虚函数可以有默认实现吗?
答案是:可以,甚至纯虚函数也能写函数体。
理解了这一点,就不会被 =0 迷惑。虚函数机制的精髓在于 运行时多态,而默认实现只是语法和设计上的灵活性。
从设计角度看,虚函数的默认实现其实是 “接口与抽象” 与 “复用与扩展” 的平衡点。 这也是为什么很多优秀的 C++ 框架源码里,你会看到“纯虚函数带实现”的写法——它并不是奇技淫巧,而是一种深思熟虑的工程实践。