虚函数draw()用派生类对象直接调用时执行派生类版本,因静态类型与动态类型一致,编译期静态绑定到circle::draw;若发生对象切片(如值传递基类参数),则只调用基类版本。

Draw() 这类虚函数,直接用派生类对象调用时,执行的是派生类重写的版本——前提是该对象是完整、未被切片的派生类实例,且调用方式是通过对象本身(非指针/引用)或通过派生类类型的引用/指针。
这个行为看似简单,但背后有几个关键点容易被忽略,直接影响你能否写出可预测的代码。
用 Circle c(5.0); c.Draw(); 为什么调用的是 Circle::Draw
因为静态类型和动态类型一致:变量 c 的类型是 Circle,它既不是基类指针也不是基类引用,编译器在编译期就能确定调用目标。此时虚函数机制其实没被“激活”,不走虚表查表流程,而是直接绑定到 Circle::Draw ——这叫**静态绑定**,但结果和动态绑定一致。
换句话说:能确定具体类型的场合,C++ 优先做静态绑定;只有当类型不确定(如 Shape& s = c;)时,才依赖虚表做运行时分发。
- 即使函数是
virtual,只要调用表达式左侧是具体派生类对象(或派生类引用),就一定调用该类的实现 - 如果派生类没重写,会自动向上查找基类中最近的虚函数实现(包括普通虚函数的默认实现)
- 这种调用不涉及虚表跳转,性能上等同于普通函数调用
什么情况下看似“直接调用”,实际却执行了基类版本
最常见的是**对象切片(object slicing)**:当你把派生类对象传给一个以基类对象为参数的函数时,派生部分被丢弃,只剩基类子对象。
void render_by_value(Shape s) { // 注意:s 是 Shape 对象,不是引用或指针
s.Draw(); // 这里永远调用 Shape::Draw,哪怕你传进来的是 Circle
}
这是因为 s 在函数体内是一个全新的、独立的 Shape 对象,内存里根本没有 Circle 的成员(比如 radius_),自然也不可能调用 Circle::Draw。
- 切片后,所有派生类特有数据丢失,虚函数表指针也被替换为基类的虚表指针
- 参数按值传递、返回局部对象、赋值给基类对象,都可能触发切片
- 解决办法:改用
const Shape&或Shape*作为参数类型
用派生类指针调用时,virtual 关键字还重要吗
重要,而且必须存在。如果你在基类中没写 virtual,哪怕派生类写了 override,调用时也只看指针/引用的静态类型:
class Shape {
void Draw() const { cout Draw(); // 输出 "Shape",不是 "Circle"
没有 virtual,就没有虚表项,也就没有运行时多态能力。此时 p->Draw() 完全按 Shape* 类型解析,和 Circle 无关。
-
virtual必须出现在基类声明中,派生类用override只是可选的语法检查手段 - 一旦基类函数是虚的,所有派生类中同签名函数自动成为虚函数,无需重复写
virtual - 省略
override不影响功能,但建议加上,防止签名不一致导致意外的隐藏而非重写
虚函数的“虚”只在类型不确定时才起作用;而直接操作派生类对象这件事本身,天然就确定了类型。真正容易出错的,不是“该调哪个”,而是你以为在操作派生类对象,实际上已经变成了基类对象——切片无声无息,调试时很难察觉。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











