正文内容
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:动态绑定,运行时看指针指向的真实对象类型。
✅ 重点:多态生效的两个必要条件
- 基类函数标记
virtual,子类重写函数签名完全一致(函数名、参数、const、返回值协变)- 使用基类指针 / 引用指向子类对象;如果是值对象(切片),多态失效。
二、虚析构函数(高频坑)
基类析构一定要 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)
- 有虚函数的类,对象开头会多出一个虚指针
vptr,指向该类的虚函数表 vtable - vtable 是全局数组,存放虚函数的地址
- 子类继承后,会复制一份基类vtable;如果重写虚函数,就把表内对应函数地址替换成自己版本;新增virtual函数追加到表后面。
- 调用虚函数时:对象.vptr → 找到vtable → 取出函数地址,运行期调用。
代价:
- 对象内存增加一个指针大小(8字节 64位)
- 间接寻址,有微小运行开销
- 不能内联(
inline和 virtual 冲突,虚函数无法编译期确定调用目标)
五、override / final(C++11,配合virtual)
override
显式声明这个函数是重写基类虚函数,编译器会检查签名是否匹配,写错直接编译报错,防止手滑。
void func() override;
final
- 修饰虚函数:禁止子类继续重写
virtual void func() final;
- 修饰类:禁止类被继承
class A final {};
六、virtual 继承(virtual 修饰继承,和虚函数不是一回事!)
注意区分两个完全不同的
virtual:
virtual 函数:虚函数,实现多态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;
十、使用场景总结
- 接口抽象:基类写纯虚函数,定义接口规范,子类实现(策略模式、插件框架)
- 扩展基类行为:基类预留虚函数钩子,子类自定义行为
- 多态容器:
vector<Base*>存放不同子类,统一调用接口 - 基类析构用virtual,防止delete泄漏
正文内容已启用复制保护,代码块与行内代码仍支持复制。
读者留言
还没有评论,来发表第一条评论吧。