C++虚函数详解

作为一个写了几年C++的程序员,虚函数这个东西算是又爱又恨。爱的是它让代码能灵活应对各种复杂场景,恨的是初学那会踩过的坑,至今想起来都脑壳疼。今天就尽量用大白话,把虚函数的来龙去脉掰扯清楚。

一、为什么需要虚函数?先从一个反例说起

假设我们要写一个图形类的程序,先定义一个基类Shape,里面有个计算面积的函数calcArea(),然后派生出Circle(圆形)和Rectangle(矩形)两个子类,各自实现自己的面积计算逻辑:

cpp 复制代码
#include <iostream>
using namespace std;

class Shape {
public:
    void calcArea() {
        cout << "这是基类的面积计算" << endl;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    void calcArea() {
        cout << "圆形面积:" << 3.14 * radius * radius << endl;
    }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    void calcArea() {
        cout << "矩形面积:" << width * height << endl;
    }
};

看起来没问题对吧?那我们写个测试函数试试:

cpp 复制代码
void printArea(Shape* shape) {
    shape->calcArea();
}

int main() {
    Circle c(5);
    Rectangle r(4, 6);
    printArea(&c);
    printArea(&r);
    return 0;
}

你猜输出是什么?不是我们期望的圆形和矩形面积,而是两行"这是基类的面积计算"。这就是C++默认的"静态绑定"------编译器在编译的时候,只看指针的类型是Shape*,就直接绑定了基类的calcArea(),不管实际指向的是哪个子类对象。

这显然不符合我们的需求:明明传的是圆形对象,却调用了基类的方法。这时候就需要虚函数登场了,它的核心作用就是实现"动态绑定"------程序运行时,根据指针实际指向的对象类型,调用对应的方法。

二、虚函数的基本用法:加个virtual就行

只需要在基类的函数前面加个virtual关键字,问题就解决了:

cpp 复制代码
class Shape {
public:
    virtual void calcArea() {  // 加virtual变成虚函数
        cout << "这是基类的面积计算" << endl;
    }
};

再运行刚才的测试代码,输出就变成了:

复制代码
圆形面积:78.5
矩形面积:24

完美!这时候编译器就不会在编译时绑定函数了,而是在运行时根据对象的实际类型来调用对应的calcArea()

这里要注意几个点:

  1. 子类重写虚函数时,virtual关键字可以省略(编译器会自动识别),但建议加上,代码可读性更好。
  2. 重写的函数签名必须和基类完全一致(返回值、函数名、参数列表都要一样),不然就变成"重载"而不是"重写"了。
  3. 如果基类的虚函数是纯虚函数(后面会讲),子类必须重写它,否则子类也会变成抽象类,不能实例化对象。

三、虚函数的底层原理:虚表和虚指针

很多人用了好几年虚函数,却不知道它是怎么实现的。其实原理说穿了也简单,就是靠"虚表"(vtable)和"虚指针"(vptr)。

当一个类包含虚函数时,编译器会为这个类生成一个虚表,里面存着该类所有虚函数的地址。同时,每个对象的内存布局最前面,会多一个虚指针,指向这个类的虚表。

比如刚才的Shape类,它的虚表里存着Shape::calcArea()的地址;Circle类继承后,会重写calcArea(),所以它的虚表里存的是Circle::calcArea()的地址;Rectangle类同理。

当我们用Shape*指针指向Circle对象时,指针访问的是对象开头的虚指针,通过虚指针找到Circle类的虚表,再从虚表里取出Circle::calcArea()的地址,最后调用这个函数。这就是动态绑定的过程。

这里有个小细节:如果子类没有重写某个虚函数,那它的虚表里对应的位置,会继承基类的虚函数地址。比如如果Shape还有个draw()虚函数,Circle没重写,那Circle的虚表里draw()的地址就是Shape::draw()的地址。

四、纯虚函数和抽象类

有时候我们的基类只是个"模板",本身不需要实例化对象,比如Shape,现实中不存在一个"形状"对象,只有具体的圆形、矩形。这时候就可以把基类的虚函数定义为"纯虚函数":

cpp 复制代码
class Shape {
public:
    virtual void calcArea() = 0;  // 纯虚函数,=0表示没有实现
};

包含纯虚函数的类叫做"抽象类",抽象类不能实例化对象,比如Shape s;这样写编译器会报错。但可以定义抽象类的指针或引用,用来指向子类对象。

子类必须重写所有纯虚函数,否则子类也会变成抽象类。比如Circle必须实现calcArea(),不然Circle c(5);也会报错。

纯虚函数的作用就是强制子类实现特定的方法,相当于定义了一个"接口",保证所有子类都有统一的行为。

五、虚析构函数:最容易踩的坑

虚函数的另一个重要应用是虚析构函数。假设我们有这样的代码:

cpp 复制代码
class Base {
public:
    ~Base() {
        cout << "Base析构函数" << endl;
    }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() {
        data = new int[10];
    }
    ~Derived() {
        delete[] data;
        cout << "Derived析构函数" << endl;
    }
};

int main() {
    Base* p = new Derived();
    delete p;
    return 0;
}

运行后输出只有"Base析构函数",Derived的析构函数没被调用!这就导致data指向的内存泄漏了,因为delete p的时候,编译器只看到pBase*,就只调用了基类的析构函数,子类的析构函数没执行。

解决方法就是把基类的析构函数设为虚函数:

cpp 复制代码
class Base {
public:
    virtual ~Base() {  // 虚析构函数
        cout << "Base析构函数" << endl;
    }
};

这时候再运行,输出就是:

复制代码
Derived析构函数
Base析构函数

先调用子类的析构函数,再调用基类的,内存就不会泄漏了。所以只要你的类可能被继承,并且子类有动态分配的内存,就一定要把基类的析构函数设为虚函数。

六、虚函数的常见误区

  1. 构造函数不能是虚函数:因为对象构造时,虚指针还没初始化,无法进行动态绑定。而且构造函数是用来创建对象的,虚函数的意义是运行时多态,这时候对象都还没创建,显然没必要。
  2. 静态成员函数不能是虚函数:静态成员函数属于类,不属于对象,没有this指针,而虚函数的动态绑定依赖于对象的虚指针,所以静态函数不能是虚函数。
  3. 内联函数可以是虚函数,但运行时多态时不会内联:如果直接调用对象的虚函数,编译器可能会内联;但如果通过指针或引用调用,因为要动态绑定,编译器无法确定调用哪个函数,所以不会内联。
  4. 虚函数的开销:虚函数会增加对象的内存(多了个虚指针),调用时也会多一次虚表查找的开销。但现在的编译器优化得很好,这个开销几乎可以忽略不计,除非你写的是对性能要求极高的代码(比如实时系统),否则不用太在意。

七、总结

虚函数是C++实现多态的核心机制,它通过动态绑定让程序能根据对象的实际类型调用对应的方法,大大提高了代码的灵活性和可扩展性。

记住几个关键点:

  • 基类中加virtual的函数就是虚函数,子类可以重写它。
  • 纯虚函数(=0)用来定义抽象类,强制子类实现方法。
  • 基类析构函数一定要设为虚函数,避免内存泄漏。
  • 底层靠虚表和虚指针实现动态绑定。
相关推荐
Dxy12393102162 小时前
Python使用XPath定位元素:动态计算与函数调用
开发语言·python
小柯博客2 小时前
STM32MP2安全启动技术深度解析
c语言·c++·stm32·嵌入式硬件·安全·开源·github
cpp_25012 小时前
P1832 A+B Problem(再升级)
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
Evand J2 小时前
【MATLAB代码介绍】三种CT模型的IMM(交互式多模型)对目标高精度定位
开发语言·matlab·ct·imm·交互式多模型·多模型·转弯
AC赳赳老秦2 小时前
OpenClaw权限管理实操:团队共享Agent,设置操作权限,保障数据安全
服务器·开发语言·前端·javascript·excel·deepseek·openclaw
geovindu2 小时前
go: Proxy Pattern
开发语言·后端·设计模式·golang·代理模式
langsiming2 小时前
【无标题】
java·开发语言·数据库
꧁细听勿语情꧂2 小时前
合并两个有序表、判断链表的回文结构、相交链表、环的链表一和二
c语言·开发语言·数据结构·算法
Rust语言中文社区3 小时前
【Rust日报】2026-04-24 Vizia 0.4 发布——纯 Rust 声明式响应式 GUI 框架
开发语言·后端·rust