C++ 虚函数的使用开销以及替代方案

Part1虚函数的核心概念与底层实现

1.1、虚函数的定义与本质

虚函数是 C++ 实现动态多态的核心机制,其本质是通过运行时绑定实现 "基类接口、派生类实现" 的设计思想。在基类中用virtual关键字声明,派生类通过override关键字显式重写(C++11 起),需满足函数签名完全匹配(含参数类型、const/volatile 限定符、返回值类型,协变返回类型除外)。

核心作用:允许通过基类指针 / 引用调用派生类的重写函数,例如:

复制代码
#include <iostream>
using namespace std;
class Base {
public:
    // 虚函数声明
    virtual void func(int x) const { 
        cout << "Base::func(" << x << ")" << endl; 
    }
};
class Derived : public Base {
public:
    // 显式重写,编译器会检查签名匹配性
    void func(int x) const override { 
        cout << "Derived::func(" << x << ")" << endl; 
    }
};
int main() {
    Base* ptr = new Derived(); // 基类指针指向派生类对象
    ptr->func(10); // 运行时绑定,调用Derived::func
    delete ptr;
    return 0;
}

关键特性:

  • 继承传递性:基类虚函数在派生类中默认保持虚特性,即使不写virtual
  • override强制检查重写有效性,避免因签名错误导致的 "隐式隐藏"
  • 析构函数若需多态释放,必须声明为虚函数

1.2、虚函数的底层实现:vtable 与 vptr

C++ 标准未规定虚函数实现方式,但主流编译器均采用虚函数表(vtable)+ 虚表指针(vptr) 机制,具体实现如下:

1.2.1、核心组件

  • vtable:每个包含虚函数的类拥有唯一的 vtable(全局存储),本质是函数指针数组,存储该类所有虚函数的入口地址
  • vptr:每个对象的内存布局中首个成员(通常),指向所属类的 vtable,由编译器在构造函数中自动初始化

1.2.2、内存布局示例(64 位系统)

复制代码
// 单继承场景
class Base {
public:
    virtual void f1() {}
    virtual void f2() {}
private:
    int a; // 4字节
};
class Derived : public Base {
public:
    void f1() override {} // 重写f1
    virtual void f3() {} // 新增虚函数
private:
    int b; // 4字节
};
  • Base 对象布局:[vptr (8 字节)] + [a (4 字节)] → 总 16 字节(内存对齐)
  • Derived 对象布局:[vptr (8 字节)] + [a (4 字节)] + [b (4 字节)] → 总 16 字节
  • Derived 的 vtable:[&Derived::f1, &Base::f2, &Derived::f3]

1.2.3、多继承的 vtable 处理

多继承时派生类会拥有多个 vptr(每个基类对应一个),例如:

复制代码
class Base1 { virtual void f1() {} };
class Base2 { virtual void f2() {} };
class Derived : public Base1, public Base2 {
    void f1() override {}
    void f2() override {}
};
  • Derived 对象布局:[vptr (Base1)] + [vptr (Base2)] + [成员变量]
  • 两个 vtable 分别对应 Base1 和 Base2 的虚函数重写,编译器通过调整this指针实现正确调用

1.3、虚函数表的构造与查找流程

1.3.1、vtable 构造时机

编译期:编译器为每个含虚函数的类生成 vtable,并填入虚函数地址

继承时:

  • 派生类复制基类 vtable 的所有条目
  • 若派生类重写某虚函数,替换 vtable 中对应条目为派生类函数地址
  • 若派生类新增虚函数,在 vtable 末尾添加新条目

1.3.2、函数调用流程(ptr->func ())

  • 取 ptr 指向对象的 vptr(*(void**)ptr)
  • 根据 func 在 vtable 中的索引(编译期确定)获取函数地址
  • 调整this指针(多继承场景)
  • 调用目标函数

示例验证(GDB 调试):

公众呺:Linux教程

分享Linux、Unix、C/C++后端开发、面试题等技术知识讲解

119篇原创内容

公众号

Part2虚函数的正确使用场景

2.1、动态多态的核心场景

当需要 "统一接口、差异化实现" 且类型需在运行时确定时,必须使用虚函数,典型场景包括:

2.1.1、框架扩展接口

例如图形库中的形状绘制:

复制代码
// 框架定义的抽象接口
class Shape {
public:
    virtual double area() const = 0; // 纯虚函数,强制派生类实现
    virtual void draw() const = 0;
    virtual ~Shape() = default; // 虚析构,避免内存泄漏
};
// 用户扩展的具体实现
class Circle : public Shape {
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
    void draw() const override { cout << "Drawing circle" << endl; }
private:
    double radius;
};
// 框架统一处理逻辑
void render_all(const vector<unique_ptr<Shape>>& shapes) {
    for (const auto& s : shapes) {
        s->draw(); // 多态调用
    }
}

2.1.2、回调机制实现

例如网络库中的事件处理器:

复制代码
class EventHandler {
public:
    virtual void on_connect() = 0;
    virtual void on_disconnect() = 0;
};
class TCPClient {
public:
    TCPClient(EventHandler* handler) : handler(handler) {}
    void connect() {
        // 连接逻辑...
        handler->on_connect(); // 回调派生类实现
    }
private:
    EventHandler* handler;
};

2.2、抽象类与接口设计

  • 抽象类:含至少一个纯虚函数的类,无法实例化,用于定义基类接口(如上述Shape)

  • 接口类:仅含纯虚函数和虚析构的类(模拟 Java 接口),例如:

    class Serializable { // 序列化接口
    public:
    virtual string to_string() const = 0;
    virtual void from_string(const string& s) = 0;
    virtual ~Serializable() = default;
    };

设计原则:

  • 接口需稳定,避免频繁修改纯虚函数签名
  • 析构函数必须为虚函数,否则派生类对象通过基类指针释放时会泄漏资源

2.3、动态绑定 vs 静态绑定对比

|------|--------|------------------|
| 特性 | 静态绑定 | 动态绑定 |
| 绑定时机 | 编译时 | 运行时 |
| 实现机制 | 函数名解析 | 虚函数表查找 |
| 性能 | 无额外开销 | 额外指针解引用 |
| 适用场景 | 无多态需求 | 需要多态性 |
| 代码示例 | func() | basePtr->func() |

代码对比:

复制代码
class A {
public:
    void static_func() { cout << "A::static" << endl; }
    virtual void dynamic_func() { cout << "A::dynamic" << endl; }
};
class B : public A {
public:
    void static_func() { cout << "B::static" << endl; }
    void dynamic_func() override { cout << "B::dynamic" << endl; }
};
int main() {
    A* ptr = new B();
    ptr->static_func(); // 静态绑定:A::static
    ptr->dynamic_func(); // 动态绑定:B::dynamic
    delete ptr;
    return 0;
}

Part3虚函数的局限性与替代方案

3.1、虚函数的性能开销分析

虚函数的灵活性伴随可量化的性能成本,主要体现在以下方面:

3.1.1、时间开销

  • 调用延迟:需通过 vptr 查找 vtable,比直接调用多 2-5 个 CPU 时钟周期(x86 架构)
  • 分支预测失效:vtable 查找的间接跳转难以被 CPU 分支预测优化,高频调用时开销显著

3.1.2、空间开销

  • vptr 开销:每个对象增加 4(32 位)/8(64 位)字节
  • vtable 开销:每个类一个 vtable,条目数 = 虚函数个数(通常可忽略,但海量类场景需注意)

3.1.3、不可接受的场景

  • 高频调用函数(如游戏引擎的update()、信号处理的process())
  • 内存受限环境(如嵌入式设备的小型对象)
  • 实时系统(需严格控制延迟)

3.2、非多态类的设计原则

对于无需扩展的类,应禁用虚函数以避免开销,核心原则:

1)、明确不需要继承:用final关键字标记类,编译器可优化

复制代码
class MathUtils final { // 禁止继承
public:
    static double add(double a, double b) { return a + b; }
};

2)、行为固定:如数据结构类(Vector2D、Matrix)、工具类

3)、性能敏感:如高频调用的计算函数

3.3、静态多态:模板与 CRTP

模板通过编译期类型推导实现静态多态,无运行时开销,是虚函数的主要替代方案。

3.3.1、模板泛型编程

适用于类型已知且无需运行时切换的场景,例如排序算法封装:

复制代码
// 排序策略接口
template <typename T>
class Sorter {
public:
    void sort(vector<T>& data) const = 0;
};
// 具体排序实现
class QuickSorter : public Sorter<int> {
public:
    void sort(vector<int>& data) const override {
        // 快速排序实现
    }
};
// 静态多态容器
template <typename T, typename Sorter>
class SortableContainer {
public:
    void add(T val) { data.push_back(val); }
    void sort() { Sorter{}.sort(data); }
private:
    vector<T> data;
};
// 使用:编译期绑定排序策略
SortableContainer<int, QuickSorter> container;

优缺点:

  • 优点:无运行时开销、类型安全
  • 缺点:代码膨胀(每个模板实例生成独立代码)、错误信息复杂

3.3.2、奇异递归模板模式(CRTP)

通过 "派生类作为基类模板参数" 实现编译期多态,例如:

复制代码
// CRTP基类
template <typename Derived>
class BaseCRTP {
public:
    void interface() {
        // 调用派生类的实现
        static_cast<Derived*>(this)->implementation();
    }
};
// 派生类
class DerivedCRTP : public BaseCRTP<DerivedCRTP> {
public:
    void implementation() {
        cout << "DerivedCRTP implementation" << endl;
    }
};
// 使用
DerivedCRTP d;
d.interface(); // 编译期绑定到DerivedCRTP::implementation

典型应用:

  • Boost 库的enable_shared_from_this
  • 高性能计算中的类型特化优化
  • 避免虚函数开销的框架扩展

3.4、行为注入:函数指针与 std::function

适用于简单行为动态切换,无需类继承结构。

3.4.1、函数指针

最轻量的动态行为方案,适用于 C 兼容场景:

复制代码
// 函数指针类型定义
typedef int (*MathOp)(int a, int b);
// 具体实现
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// 使用
int compute(MathOp op, int a, int b) {
    return op(a, b);
}
compute(add, 2, 3); // 5
compute(multiply, 2, 3); // 6

3.4.2、std::function(C++11 起)

支持函数、lambda、成员函数等,灵活性更高:

复制代码
#include <functional>
class Calculator {
public:
    using Operation = function<double(double, double)>;
    Calculator(Operation op) : op(op) {}
    double calculate(double a, double b) { return op(a, b); }
private:
    Operation op;
};
// 使用lambda注入行为
Calculator adder([](double a, double b) { return a + b; });
Calculator power([](double a, double b) { return pow(a, b); });

性能对比(单次调用开销):

|--------------|----------|-----------|
| 方案 | 开销(时钟周期) | 特点 |
| 直接调用 | 1~2 | 最快,无灵活性 |
| 函数指针 | 2~3 | 轻量,仅支持函数 |
| std:function | 5~10 | 灵活,类型擦除开销 |
| 虚函数 | 3~6 | 支持继承多态 |

3.5、策略模式的两种实现对比

策略模式可通过 "虚函数" 或 "模板" 实现,适用于算法动态切换:

|-------|------|--------|----|-----------|
| 实现方式 | 绑定时机 | 切换能力 | 开销 | 适用场景 |
| 虚函数策略 | 运行时 | 支持动态切换 | 中等 | 需运行时更换算法 |
| 模板策略 | 编译期 | 仅静态切换 | 无 | 算法固定,性能敏感 |

模板策略实现

复制代码
// 策略实现
struct QuickSort {
    template <typename T>
    void operator()(vector<T>& data) const { /* 实现 */ }
};
struct BubbleSort {
    template <typename T>
    void operator()(vector<T>& data) const { /* 实现 */ }
};
// 模板策略容器
template <typename T, typename SortStrategy>
class SortedVector {
public:
    void sort() { SortStrategy{}(data); }
private:
    vector<T> data;
};
// 静态绑定策略
SortedVector<int, QuickSort> sv;

Part4C++11及以后的虚函数增强特性

4.1、显式重写与禁止重写(override/final)

4.1.1、override 关键字

作用:显式声明重写基类虚函数,编译器检查签名匹配性

避免错误:防止因参数类型、const 限定等不匹配导致的 "意外隐藏"

复制代码
class Base {
public:
    virtual void func(int) const {}
};
class Derived : public Base {
public:
    // 错误:参数类型不匹配,编译器报错
    void func(double) const override {} 
};

4.1.2、final 关键字

禁止虚函数重写:

复制代码
class Base {
public:
    virtual void func() final {} // 禁止派生类重写
};

禁止类继承:

复制代码
class FinalClass final {}; // 无法被继承
class Derived : public FinalClass {}; // 编译错误

4.2、默认函数控制(=default /=delete)

4.2.1、=default:显式默认实现

适用于需保留默认行为的虚函数(如析构函数):

复制代码
class Base {
public:
    // 虚析构函数,使用编译器默认实现
    virtual ~Base() = default; 
    // 虚函数默认实现
    virtual void func() = default;
};

4.2.2、=delete:禁用函数

防止不当使用,例如禁止拷贝:

复制代码
class NonCopyable {
public:
    NonCopyable(const NonCopyable&) = delete; // 禁用拷贝构造
    NonCopyable& operator=(const NonCopyable&) = delete;
    virtual ~NonCopyable() = default;
};

4.3、虚函数与智能指针的协同

智能指针需与虚析构函数配合使用,避免内存泄漏:

4.3.1、unique_ptr 与多态

复制代码
unique_ptr<Shape> create_shape() {
    return make_unique<Circle>(5.0); // 自动转换为基类指针
}
int main() {
    auto shape = create_shape();
    shape->draw(); // 多态调用
    // 自动释放,调用Circle::~Circle(因Shape::~Shape为虚函数)
}

4.3.2、动态类型转换

复制代码
shared_ptr<Shape> s = make_shared<Circle>(3.0);
// 动态转换为Circle指针,失败返回nullptr
auto c = dynamic_pointer_cast<Circle>(s);
if (c) {
    cout << "Radius: " << c->radius() << endl;
}

Part5Qt框架中的虚函数实践与优化

5.1、事件处理中的虚函数

Qt 的事件机制完全基于虚函数,例如:

复制代码
class MyWidget : public QWidget {
protected:
    // 重写虚函数处理鼠标事件
    void mousePressEvent(QMouseEvent* event) override {
        if (event->button() == Qt::LeftButton) {
            // 左键处理逻辑
        }
    }
    // 重写事件分发函数
    bool event(QEvent* event) override {
        if (event->type() == QEvent::KeyPress) {
            // 拦截键盘事件
            return true;
        }
        return QWidget::event(event); // 传递其他事件
    }
};

事件处理流程:

  • QApplication::notify()分发事件
  • 调用目标对象的event()虚函数
  • event()根据事件类型调用具体虚函数(如mousePressEvent)

5.2、信号槽与虚函数的结合

Qt 中虚函数常用于 "数据提供",信号用于 "状态通知",例如QAbstractItemModel:

复制代码
class MyModel : public QAbstractItemModel {
    Q_OBJECT
public:
    // 虚函数:提供数据(多态实现)
    QVariant data(const QModelIndex& index, int role) const override {
        if (role == Qt::DisplayRole) {
            return "Item";
        }
        return QVariant();
    }
    // 信号:通知数据变化
    void update_data() {
        emit dataChanged(index(0,0), index(0,0));
    }
};

5.3、Qt 中的虚函数优化技巧

1)、使用Q_DECL_OVERRIDE:兼容旧编译器的override关键字

复制代码
void func() Q_DECL_OVERRIDE;

2)、禁用不必要的虚函数:如QObject的eventFilter仅在需要时重写

3)、使用直接连接:信号槽连接时指定Qt::DirectConnection,避免队列开销

复制代码
connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::DirectConnection);

利用final优化:对确定不重写的虚函数加final,编译器可内联调用

Part6顶级C++著作中的虚函数深度解析

6.1、《C++ Primer》中的虚函数

关键观点

  • 虚函数是实现多态的核心机制
  • 基类析构函数必须是虚函数,以避免资源泄漏
  • 虚函数调用涉及运行时查找,有轻微性能开销

注意

"如果基类包含虚函数,那么它的析构函数也应该是虚函数。否则,通过基类指针删除派生类对象时,可能导致派生类的析构函数不会被调用。"

6.2、《Effective C++》中的虚函数

关键条款

  • 条款7:为多态基类声明一个虚析构函数
  • 条款33:避免隐藏继承的名称
  • 条款34:区分接口继承和实现继承

重要观点

"在C++中,虚函数调用的开销是可接受的,除非在性能关键路径上。当需要多态性时,虚函数是正确选择。"

6.3、《C++ Templates: The Complete Guide》中的虚函数

关键观点

  • 虚函数与模板可以结合使用
  • 但虚函数不能是模板函数(因为虚函数需要在编译时确定,而模板需要在实例化时确定)
  • 通常在模板类中使用虚函数来实现类型无关的多态

示例

复制代码
template <typename T>
class Logger {
public:
    virtual void log(const T& message) = 0;
};
template <typename T>
class FileLogger : public Logger<T> {
public:
    void log(const T& message) override {
        // 写入文件
    }
};

Part7虚函数与设计模式的深度结合

7.1、模板方法模式(Template Method)

通过 "基类定义算法骨架,派生类重写步骤" 实现,例如:

复制代码
// 抽象基类(算法骨架)
class DataProcessor {
public:
    // 模板方法:固定算法流程
    void process() {
        load_data();
        validate_data();
        analyze_data();
        save_result();
    }
protected:
    virtual void load_data() = 0; // 纯虚步骤
    virtual void validate_data() { /* 默认实现 */ }
    virtual void analyze_data() = 0;
    virtual void save_result() = 0;
};
// 派生类实现具体步骤
class CSVProcessor : public DataProcessor {
protected:
    void load_data() override { /* 读取CSV */ }
    void analyze_data() override { /* CSV分析 */ }
    void save_result() override { /* 保存CSV */ }
};

7.2、工厂方法模式(Factory Method)

通过虚函数延迟对象创建,例如:

复制代码
// 抽象产品
class Product {
public:
    virtual ~Product() = default;
    virtual void use() = 0;
};
// 具体产品
class ConcreteProductA : public Product {
public:
    void use() override { /* 实现A */ }
};
// 抽象工厂
class Factory {
public:
    virtual ~Factory() = default;
    // 工厂方法(虚函数)
    virtual unique_ptr<Product> create_product() = 0;
};
// 具体工厂
class FactoryA : public Factory {
public:
    unique_ptr<Product> create_product() override {
        return make_unique<ConcreteProductA>();
    }
};

7.3、观察者模式(Observer)

观察者的更新函数通常为虚函数,支持多态通知:

复制代码
// 主题接口
class Subject {
public:
    virtual void attach(class Observer* obs) = 0;
    virtual void notify() = 0;
};
// 观察者接口
class Observer {
public:
    virtual ~Observer() = default;
    // 虚函数:接收通知
    virtual void update(Subject* sub) = 0;
};
// 具体观察者
class ConcreteObserver : public Observer {
public:
    void update(Subject* sub) override {
        // 处理通知
    }
};

Part8虚函数与替代方案的选择准则

|--------------|----------------------|--------------|
| 场景需求 | 推荐方案 | 理由 |
| 需运行时切换实现 | 虚函数 | 动态绑定支持灵活扩展 |
| 性能敏感,类型编译期已知 | 模板 / CRTP | 静态绑定无运行时开销 |
| 简单行为注入 | std::function/lambda | 无需继承,实现简洁 |
| 算法固定,禁止扩展 | 非虚函数 + final | 编译器可优化,避免滥用 |
| 跨平台 / 框架接口 | 抽象类(纯虚函数) | 强制统一接口,支持多实现 |

结语

虚函数是 C++ 动态多态的基石,但其性能开销并非总是可忽略。在实际开发中,需根据 "灵活性需求" 与 "性能要求" 权衡选择:

  • 框架设计、运行时扩展场景优先使用虚函数
  • 高频调用、内存受限场景优先选择模板、CRTP 等静态方案
  • 简单行为切换可采用std::function或函数指针

在设计类层次结构时,先考虑是否真的需要多态性。如果不需要,避免使用虚函数。如果需要,合理使用虚函数,并在必要时考虑现代C++的替代方案。记住,"不要为了多态而多态",每个虚函数都有其代价。

点击下方关注【Linux教程】,获取 大厂技术栈学习路线、项目教程、简历模板、大厂面试题pdf文档、大厂面经、编程交流圈子等等。

相关推荐
feng_blog66886 小时前
环形缓冲区实现共享内存
linux·c++
Larry_Yanan6 小时前
QML学习笔记(四十七)QML与C++交互:上下文对象
c++·笔记·qt·学习·ui
黑菜钟7 小时前
代码随想录第53天 | 图论二三题
c++·图论
西哥写代码7 小时前
基于dcmtk的dicom工具 第十二章 响应鼠标消息实现图像的调窗、缩放、移动
c++·mfc·dicom·dcmtk·vs2017
头发还没掉光光7 小时前
Linux多线程之生产消费模型,日志版线程池
linux·运维·开发语言·数据结构·c++
lkx097887 小时前
笔记C++语言,太焦虑了
前端·c++·笔记
眠りたいです8 小时前
基于脚手架微服务的视频点播系统-脚手架开发部分-FFmpeg,Etcd-SDK的简单使用与二次封装
c++·微服务·云原生·架构·ffmpeg·etcd
星竹晨L20 小时前
C++继承机制:面向对象编程的基石
开发语言·c++
9ilk20 小时前
【仿RabbitMQ的发布订阅式消息队列】--- 模块设计与划分
c++·笔记·分布式·后端·中间件·rabbitmq