C++中virtual关键字用法总结

文章字数:969

virtual 是 C++ 实现运行时多态的核心机制。默认情况下,通过基类指针调用的函数由静态类型决定(编译期绑定);声明为 virtual 后,调用由动态类型决定(运行时绑定)。

cpp
1
2
3
4
5
6
7
8
9
class Animal {
public:
    void speak() { }              // 非虚 —— 静态绑定
    virtual void move() { }       // 虚函数 —— 动态绑定
};

Animal* ptr = new Dog();
ptr->speak();  // 永远调 Animal::speak()
ptr->move();   // 调 Dog::move(),由实际对象类型决定

虚函数表(vtable)与虚函数指针(vptr)

编译器为每个含虚函数的类生成一张虚函数表,存储虚函数地址。每个实例有一个隐藏的 vptr 指向该表:

1
2
3
4
5
6
对象内存布局:
┌───────────┐
│  vptr     │ ──→ vtable: [ &Base::f, &Base::g, &Base::h ]
├───────────┤
│  成员变量  │
└───────────┘

派生类覆写虚函数时,其 vtable 中对应槽位替换为派生类版本。调用路径:对象 → vptr → vtable → 函数地址

虚析构函数

基类析构函数必须声明为 virtual,否则通过基类指针 delete 派生类对象时,派生类的析构函数不会被调用,造成资源泄漏:

cpp
1
class Base { public: virtual ~Base() {} };  // 正确做法

经验法则:只要类中有虚函数(说明它被设计为基类),析构函数就应该是虚的。

纯虚函数与抽象类

纯虚函数只声明不实现,语法 = 0。包含纯虚函数的类是抽象类,不能实例化,派生类必须实现所有纯虚函数:

cpp
1
2
3
4
5
class Shape {
public:
    virtual double area() const = 0;  // 纯虚函数 = 接口契约
    virtual ~Shape() {}
};

菱形继承与虚继承

菱形继承(D → B,C → A)会让 D 包含两份 A 的数据,访问时歧义报错:

1
2
3
4
5
   A
  / \
 B   C
  \ /
   D      // D 中有两份 A!

virtual 继承确保共同祖先只有一份,放在对象内存末尾,通过 vbptr 间接寻址访问。

Q&A

Q1: 请详细解释 C++ 虚函数表的实现原理

包含虚函数的类在编译时生成虚函数表(vtable),每个实例有一个虚函数指针(vptr)指向它。运行时通过 vptr 查到虚函数信息,从而用基类指针也能调到子类重写的版本。

Q2: 多重继承下的虚表布局是怎样的?

多重继承会为每一个基类各存一个 vptr,放在对应基类数据段的前面:

1
vptr1 | base1 | vptr2 | base2 | child

Q3: 虚继承又引入了什么额外开销?

解决的问题: 菱形继承会导致最终类包含两份共同祖先的数据,访问时编译器因歧义报错。虚继承把共同祖先放在内存最后,子类存一个指针(vbptr)指向到基类的偏移,通过间接寻址访问。

代价:

  • 内存增长(多了 vbptr 和虚基类表)
  • 性能变差(多一次间接寻址)
  • 代码更复杂(最终派生类需手动调用祖先构造函数)

实践建议: 优先组合替代继承;需要多重继承时,至少一方应为纯虚接口类。

本文包含一些 AIGC 辅助生成内容,由作者人工校对整理后发布
本文采用 CC BY-NC-SA 4.0协议,如果对您有帮助或存在意见建议,欢迎在下方评论交流。
本页面浏览次数 加载中...
本页面访客数 加载中...

加载中...