一、什么是虚函数?

在 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 强制子类必须实现。

七、常见面试陷阱

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

  1. 虚函数能是构造函数吗? → 不行,构造函数不能是虚函数。
  2. 析构函数要不要写成虚的? → 如果类可能被继承,必须写虚析构,否则 delete 基类指针会内存泄漏。
  3. 纯虚函数能否有实现? → 可以,但类依然是抽象类,不能直接实例化。
  4. 虚函数影响性能吗? → 有一点点开销(一次指针间接寻址),但大多数情况下可以忽略。

八、最佳实践总结
  • 如果希望可选重写,就用普通虚函数并给一个默认实现。
  • 如果希望必须重写,就用纯虚函数,但仍可提供兜底实现。
  • 如果基类会被继承,一定要有虚析构函数。
  • 避免在构造函数和析构函数中调用虚函数(因为此时 vtable 可能不完整)。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐