虚函数调用取决于对象实际类型而非指针类型,通过虚表实现动态绑定;析构函数必须为虚以防止资源泄漏;返回类型可协变但仅限指针/引用,参数类型不可协变;虚函数不可内联且不能用于模板推导。

虚函数调用依赖对象实际类型,不是指针类型
基类指针能调用虚函数,前提是该函数在基类中声明为 virtual,且派生类有对应重写(哪怕没加 override)。C++ 运行时通过对象的虚表(vtable)决定最终调哪个函数,跟指针类型无关——只看它指向的 实际对象 是什么类型。
常见错误是:基类函数没加 virtual,结果调用永远绑定到基类实现;或者派生类函数签名不一致(比如参数 const 修饰不同、返回类型协变没满足),导致“重写失败”,变成隐藏而非多态。
- 确保基类函数声明含
virtual,例如:virtual void draw() const; - 派生类重写时建议显式加
override,编译器会检查签名是否真正匹配 - 不要试图用
static_cast强转基类指针来“切换”虚调用目标——这不会改变虚表查找行为
析构函数必须是虚函数,否则 delete 基类指针会漏掉派生类清理
如果基类指针指向派生类对象,而基类析构函数不是 virtual,执行 delete ptr; 时只会调用基类析构函数,派生类部分的资源(如动态分配内存、文件句柄)不会被释放——这是典型的未定义行为,且往往无编译警告。
只要类设计为被继承、且可能通过基类指针销毁对象,就必须把析构函数设为虚函数。即使函数体为空,也要写 virtual ~Base() = default; 或 virtual ~Base() {}。
- 纯虚析构函数也必须提供定义(哪怕空实现),否则链接失败:
virtual ~Base() = 0;→ 还需在 .cpp 中写Base::~Base() = default; - 普通成员虚函数可以是纯虚,但析构函数不能只声明不定义
虚函数调用开销小,但禁止内联,且不能用于模板特化推导
现代编译器对虚调用优化已很成熟,单次调用成本通常只是一次指针解引用 + 一次间接跳转,远低于函数调用本身开销。但虚函数天然无法被内联(因为编译期不知道具体目标),所以高频小函数(如 get_x())若不需要多态,别为了“统一接口”硬加 virtual。
另一个常被忽略的限制:虚函数不能参与 SFINAE 或模板参数推导。比如你写 std::function<void> f = &obj.virtual_func;</void> 会失败,因为取虚函数地址不合法;也不能靠虚函数签名做 decltype 推导模板实参。
- 需要“可获取地址”的回调接口,改用普通函数指针 + 静态 dispatch,或 std::function + lambda 捕获
- 性能敏感路径(如 inner loop)中,确认虚调用是否真被频繁执行;可用 profiler 验证,别凭直觉
基类指针调用时传参/返回值要注意协变与切片风险
虚函数支持返回类型协变(covariant return),比如基类返回 Base*,派生类可返回 Derived*;但参数类型不协变——所有重写版本参数必须完全一致(const/volatile 修饰、引用/值传递都算不同签名)。
更隐蔽的问题是:如果虚函数返回对象(非引用/指针),会发生切片(slicing)——派生类扩展部分被截断。例如 virtual Data get_data(); 在派生类里返回 DerivedData,但调用方拿到的只是其基类部分。
- 返回值协变只适用于指针和引用类型:
virtual Base& foo();→Derived& foo() override; - 避免按值返回多态对象;优先返回
std::unique_ptr<base>或Base& - 参数一律用 const 引用传递,既避免切片又免拷贝
virtual 就自动解决所有类型适配问题。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











