c++:虚函数也可以有默认实现吗?
在 C++ 里,虚函数()是实现运行时多态的关键。在基类里定义函数,并声明为virtual。在派生类中可以重写这个函数。当通过基类指针或引用调用时,实际执行的是派生类的版本(动态绑定)。举个最经典的例子:代码语言:javascriptAI代码解释public:public:// 输出:Hello from Deriveddelete ptr;这里sayHello就是一个虚函数,它让调用发生在运行时
一、什么是虚函数?
在 C++ 里,虚函数(virtual function)是实现 运行时多态 的关键。它的本质就是:
- 在基类里定义函数,并声明为
virtual。 - 在派生类中可以重写这个函数。
- 当通过基类指针或引用调用时,实际执行的是派生类的版本(动态绑定)。
举个最经典的例子:
代码语言:javascript
AI代码解释
#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)的动态分派机制。
所以我们完全可以在基类里提供一个默认实现:
代码语言:javascript
AI代码解释
class Base {
public:
virtual void foo() {
cout << "Base default foo()" << endl;
}
};
如果派生类不重写,那么它就会直接继承这个默认逻辑。
2. 纯虚函数
更有意思的是: 纯虚函数(=0)也可以有默认实现!
代码语言:javascript
AI代码解释
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;
}
};
输出结果:
代码语言:javascript
AI代码解释
Base::foo() default implementation
Derived::foo()
这说明即便是纯虚函数,编译器也允许你在基类里提供一个实现。区别只是:
- 有
=0标记后,基类变成抽象类,不能实例化。 - 派生类必须显式重写,除非它自己也想保持抽象。
三、为什么要给虚函数一个默认实现?
面试的时候如果只停留在语法层面,答案未免太“干巴巴”。面试官更想听到的是你的设计思维。
1. 避免重复代码
很多情况下,子类可能大多数逻辑都一样,只在少数地方有差异。 这时基类提供一个默认实现,派生类只需要覆盖差异部分即可,避免重复造轮子。
2. 提供兜底逻辑
有些接口必须实现,但又希望在特殊情况下给出一个“保底”逻辑。 例如:
- 如果派生类没有实现日志系统,那至少打印到
stdout。 - 如果派生类没有实现错误处理,那至少抛个异常。
3. 便于扩展
假如将来要增加新的派生类,它可以直接继承基类的默认实现,快速使用,而不用立刻写一堆重复代码。
四、普通虚函数 vs 纯虚函数
很多同学容易混淆两者,我整理了一张表:
|
特性 |
普通虚函数 |
纯虚函数 |
|---|---|---|
|
定义方式 |
virtual void f(); |
virtual void f() = 0; |
|
是否必须实现 |
否,可以直接用基类实现 |
是,派生类必须重写 |
|
是否能有实现体 |
可以 |
可以(少见但合法) |
|
是否使类抽象化 |
否 |
是,类不可实例化 |
|
适用场景 |
框架基类、可选重写 |
接口定义、强制重写 |
关键点:
=0并不是说“这个函数没有实现”,而是说“这个类是抽象类”。- 实现体写不写,是两回事。
五、虚函数表(vtable)机制简析
面试官有时会追问:“为什么虚函数能有默认实现?它底层是怎么实现的?”
这就涉及到 虚函数表(vtable)。
- 每个包含虚函数的类,编译器会生成一张虚函数表,里面存放函数指针。
- 对象里有一个指向这张表的指针(vptr)。
- 调用虚函数时,会通过 vptr 找到实际函数地址执行。
举个例子:
代码语言:javascript
AI代码解释
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)
通常我们会写一个纯虚函数接口类:
代码语言:javascript
AI代码解释
class IShape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual ~IShape() {}
};
这样强制子类必须实现。 但如果我们想给一个默认逻辑(比如 perimeter 里先调用 area),那也可以给它加实现。
2. 框架基类
比如写一个 Logger:
代码语言:javascript
AI代码解释
class Logger {
public:
virtual void log(const string& msg) {
cout << "[Default log] " << msg << endl;
}
};
子类如果不关心日志,就直接用默认实现; 如果要写文件,就 override。
3. 混合设计
有时一个类既有普通虚函数,也有纯虚函数:
代码语言:javascript
AI代码解释
class AbstractHandler {
public:
virtual void init() { cout << "Default init" << endl; }
virtual void handle() = 0; // 强制实现
};
这种混搭的设计在框架里很常见:
init给一个默认逻辑,但允许子类定制;handle强制子类必须实现。
七、常见面试陷阱
面试官可能会绕几个弯子:
- 虚函数能是构造函数吗? → 不行,构造函数不能是虚函数。
- 析构函数要不要写成虚的? → 如果类可能被继承,必须写虚析构,否则 delete 基类指针会内存泄漏。
- 纯虚函数能否有实现? → 可以,但类依然是抽象类,不能直接实例化。
- 虚函数影响性能吗? → 有一点点开销(一次指针间接寻址),但大多数情况下可以忽略。
八、最佳实践总结
- 如果希望可选重写,就用普通虚函数并给一个默认实现。
- 如果希望必须重写,就用纯虚函数,但仍可提供兜底实现。
- 如果基类会被继承,一定要有虚析构函数。
- 避免在构造函数和析构函数中调用虚函数(因为此时 vtable 可能不完整)。
更多推荐



所有评论(0)