文章详情 学习记录

C++学习(十九)

发布时间:2026-10-08 14:23 浏览次数:32 最后更新:2026-10-08 14:23

C++ virtual 用法详解

正文内容

C++ virtual 关键字详解

virtual 用来实现运行期多态(动态多态),核心机制:虚函数表 vtable + 虚指针 vptr。

一句话总结:基类用 virtual 修饰成员函数,允许子类重写(override),通过基类指针/引用调用时,实际执行子类版本,而不是基类版本。

一、基础语法

#include <iostream>
using namespace std;

class Base {
public:
    // 虚函数
    virtual void func() {
        cout << "Base::func" << endl;
    }
};

class Derived : public Base {
public:
    // 重写虚函数,C++11推荐加 override
    void func() override {
        cout << "Derived::func" << endl;
    }
};

int main() {
    Base* p = new Derived();
    p->func(); // 输出 Derived::func,动态绑定
    delete p;
    return 0;
}
  • 不加 virtual:静态绑定,p->func() 永远调用 Base::func,编译期就确定。
  • 加 virtual:动态绑定,运行时看指针指向的真实对象类型。

✅ 重点:多态生效的两个必要条件

  1. 基类函数标记 virtual,子类重写函数签名完全一致(函数名、参数、const、返回值协变)
  2. 使用基类指针 / 引用指向子类对象;如果是值对象(切片),多态失效。

二、虚析构函数(高频坑)

基类析构一定要 virtual,否则用基类指针delete子类对象时,只会调用基类析构,子类析构不执行,内存泄漏!

class Base {
public:
    virtual ~Base() { cout << "Base dtor" << endl; }
};
class Derived : public Base {
public:
    ~Derived() override { cout << "Derived dtor" << endl; }
};

int main() {
    Base* p = new Derived();
    delete p; // 先调用Derived析构,再Base析构
    return 0;
}

规则:只要类打算被继承,并且会用基类指针 delete 子类实例,析构必须 virtual。 如果不打算被继承,或者不会通过基类指针释放对象,可以不用虚析构(节省vtable开销)。

三、纯虚函数 & 抽象类

virtual void func() = 0; // 纯虚函数
  • 包含至少一个纯虚函数的类叫抽象类,不能实例化对象,只能用来做基类。
  • 子类必须实现所有纯虚函数,否则子类依然是抽象类,不能创建对象。
class AbstractBase {
public:
    virtual void show() = 0; // 纯虚函数
    virtual ~AbstractBase() = default;
};

class Son : public AbstractBase {
public:
    void show() override {
        cout << "Son show" << endl;
    }
};

四、虚函数底层原理(vtable / vptr)

  1. 有虚函数的类,对象开头会多出一个虚指针 vptr,指向该类的虚函数表 vtable
  2. vtable 是全局数组,存放虚函数的地址
  3. 子类继承后,会复制一份基类vtable;如果重写虚函数,就把表内对应函数地址替换成自己版本;新增virtual函数追加到表后面。
  4. 调用虚函数时:对象.vptr → 找到vtable → 取出函数地址,运行期调用。

代价:

  • 对象内存增加一个指针大小(8字节 64位)
  • 间接寻址,有微小运行开销
  • 不能内联(inline 和 virtual 冲突,虚函数无法编译期确定调用目标)

五、override / final(C++11,配合virtual)

override

显式声明这个函数是重写基类虚函数,编译器会检查签名是否匹配,写错直接编译报错,防止手滑。

void func() override;

final

  1. 修饰虚函数:禁止子类继续重写
virtual void func() final;
  1. 修饰类:禁止类被继承
class A final {};

六、virtual 继承(virtual 修饰继承,和虚函数不是一回事!)

注意区分两个完全不同的 virtual:

  1. virtual 函数:虚函数,实现多态
  2. virtual 继承:虚继承,解决菱形继承二义性问题

菱形继承:B、C都继承A,D同时继承B、C → D里面有两份A成员,产生二义性。 virtual public 让B、C虚继承A,D只会保留一份A。

class A { public: int a; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

虚继承底层:虚基类指针,指向虚基类表,会带来额外内存开销。

七、常见易错点

1. 函数签名不一致,不算重写,变成隐藏(hide)

class Base{
public:
    virtual void func(int x){}
};
class Son:public Base{
public:
    void func(double x){} // 不是重写!只是隐藏,不会触发多态
};

建议永远加 override,编译器帮你检查。

2. 构造函数不能是virtual

构造的时候,vptr还没初始化,对象还没完整构造,无法动态绑定。

❌ virtual Base(); 语法报错。 构造函数里调用虚函数:只会调用当前类版本,不会触发多态。

3. 静态成员函数不能virtual

static不属于对象,没有this指针,不存在vptr,不能动态绑定。

4. 值传递会发生对象切片,多态失效

void test(Base b) { b.func(); } // 值拷贝,子类对象被切成Base
test(Derived()); // 调用Base::func,没有多态

✅ 要用引用或者指针:void test(Base& b)

5. 返回值协变

重写虚函数时,返回值可以是基类指针/引用的子类版本,允许协变,也算合法重写。

class Base{
public:
    virtual Base* get() { return this; }
};
class Son:public Base{
public:
    Son* get() override { return this; } // 协变,合法
};

八、virtual 与 const

const 属于函数签名一部分,virtual void foo() const,子类重写也必须带const,否则不是重写。

class Base {
public:
    virtual void foo() const {}
};
class Son : Base {
public:
    void foo() override {} // ❌ 签名不一致,编译报错,加override直接发现
};

九、C++17 补充:virtual 与默认函数

可以把虚函数设为 =default,在类内生成默认实现:

virtual ~Base() = default;

十、使用场景总结

  1. 接口抽象:基类写纯虚函数,定义接口规范,子类实现(策略模式、插件框架)
  2. 扩展基类行为:基类预留虚函数钩子,子类自定义行为
  3. 多态容器:vector<Base*> 存放不同子类,统一调用接口
  4. 基类析构用virtual,防止delete泄漏

正文内容已启用复制保护,代码块与行内代码仍支持复制。

评论区

欢迎参与讨论,评论提交与展示状态会在下方同步更新。

当前站点暂未开放评论提交功能。

读者留言

还没有评论,来发表第一条评论吧。

共 0 条评论 已展示 0 / 0 条评论
当前还没有评论,来发表第一条评论吧。若评论需要审核,通过后会展示在这里。