C++ 多态详解:概念、虚函数重写与多态原理
读完《02.多态》讲义后整理的入门博客,是《C++ 继承》的下一篇。术语照样顺手解释,代码照抄能跑,放心往下读。
什么是多态?
买票这件事,不同的人有不同的形态:普通人全价,学生打折,军人优先。动物叫一声,猫是"(>ω<)喵",狗是"汪汪"。同一个行为,不同的对象做出不同的反应------这就是多态(polymorphism,"多种形态")。
多态其实分两种,一张表先分清:
| 类型 | 别名 | 实现手段 | 什么时候确定行为 |
|---|---|---|---|
| 编译时多态 | 静态多态 | 函数重载、函数模板 | 编译时按参数匹配确定 |
| 运行时多态 | 动态多态 | 虚函数 + 重写 | 运行时看对象是谁 |
本文重点讲运行时多态------这才是 C++ 面试和工程里说的"多态"本尊。
一、多态的两个必须条件
多态发生在继承关系下:基类指针或引用调用同一个函数,不同对象产生不同行为。讲义给的条件非常硬性,缺一不可:
实现多态必须满足两个条件:
- 必须是基类的指针或者引用调用虚函数;
- 被调用的函数必须是虚函数 ,并且派生类完成了重写/覆盖。
为什么必须是基类的指针或引用?因为只有它们才能"既指向基类对象,又指向派生类对象"------上一章的切片规则在这里正好反过来用:基类指针/引用能绑住任何子类对象。为什么必须重写?因为重写了,基类和派生类才有"不同的函数"可供切换。
cpp
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; } // ① 虚函数
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; } // ② 重写
};
// ③ 基类指针调用
void Func(Person* ptr) {
ptr->BuyTicket(); // 和 ptr 的类型无关,由 ptr 指向的对象决定
}
int main() {
Person ps;
Student st;
Func(&ps); // 买票-全价
Func(&st); // 买票-打折 ← 同一行代码,两种形态
return 0;
}
!note 什么是"重写/覆盖"?
派生类里有一个和基类完全相同 的虚函数(返回值类型、函数名、参数列表三者都相同),就叫重写。派生类的虚函数不加
virtual也能构成重写(继承后虚属性还在),但不规范,不建议这么写------考试选择题最爱拿这个挖坑。
1.1 一道经典选择题:默认值跟谁走?
先看代码,猜输出:
cpp
class A {
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A {
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main() {
B* p = new B;
p->test(); // 输出什么?
return 0;
}
答案是 B->1 。这里 test() 里的 func() 走的是多态调用:函数体 看运行时对象(所以执行 B 的版本,打印 "B->"),但缺省参数 在编译期按调用者的静态类型(A)确定,所以 val 取 A 的默认值 1。
一句话记忆:虚函数重写只重写"实现",不重写"默认参数"------默认值永远跟静态类型走。
二、重写路上的几个坑
2.1 协变:返回值也能"各自返回各自的"(了解即可)
严格说重写要求返回值类型相同,但有一个例外:基类虚函数返回基类对象的指针/引用 ,派生类返回派生类对象的指针/引用,这叫协变。
cpp
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person {
public:
virtual B* BuyTicket() { cout << "买票-打折" << endl; return nullptr; } // 协变,合法
};
讲义原话:"协变的实际意义并不大,了解一下即可。"
2.2 析构函数为什么要加 virtual?(高频面试题)
基类和派生类的析构函数名字明明不一样(~A() vs ~B()),看起来不符合重写规则?实际上编译器把析构函数统一处理成 destructor ,所以只要基类析构加了 virtual,派生类析构一定义就构成重写。
不加会怎样?看这个内存泄漏现场:
cpp
class A {
public:
virtual ~A() { cout << "~A()" << endl; } // 基类析构必须是虚函数
};
class B : public A {
public:
~B() {
cout << "~B()->delete:" << _p << endl;
delete _p; // 释放资源
}
protected:
int* _p = new int[10];
};
int main() {
A* p1 = new A;
A* p2 = new B;
delete p1; // 正确调 ~A()
delete p2; // 若 ~A() 不加 virtual:只调 ~A(),~B() 不执行,_p 泄漏!
return 0;
}
delete p2 时指针类型是 A*------不构成多态的话,编译器只会调 A 的析构,B 里 new 出来的内存就永远没人管了。只要一个类会被当作基类用(也就是会被基类指针 delete),析构函数就该是虚函数。
2.3 override 和 final:让编译器替你把关
重写的要求太严格,手一抖(函数名拼错、参数写错)就不会构成重写------而且编译器不报错,要等运行时行为不对了才来 debug,得不偿失。C++11 给了两把钥匙:
cpp
// override:声明"我就是来重写的",写错了编译器立刻报警
// error C3668: "Benz::Drive": 包含重写说明符"override"的方法没有重写任何基类方法
class Car {
public:
virtual void Dirve() {} // 注意:基类手滑拼成了 Dirve
};
class Benz : public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; } // 编译报错,当场抓包
};
// final:声明"这个函数不许再被重写"
// error C3248: "Car::Drive": 声明为"final"的函数无法被"Benz::Drive"重写
class Car2 {
public:
virtual void Drive() final {}
};
class Benz2 : public Car2 {
public:
virtual void Drive() { } // 编译报错
};
实用建议 :派生类重写虚函数时一律加上
override,成本一个词,收益是把运行时 Bug 提前到编译期。
2.4 重载 / 重写 / 隐藏:三兄弟对比
这三个概念经常一起考,一张表钉死:
| 对比项 | 重载(overload) | 重写(override) | 隐藏(hiding) |
|---|---|---|---|
| 位置 | 同一作用域(同一个类) | 基类和派生类之间 | 基类和派生类之间 |
| 函数名 | 相同 | 相同 | 相同 |
| 参数 | 必须不同 | 必须相同 | 相同不同都行 |
| 返回值 | 可以不同 | 必须相同(协变除外) | 可以不同 |
| 是否要求虚函数 | 不要求 | 基类必须是虚函数 | 不要求 |
| 绑定时机 | 编译时 | 运行时(多态) | 编译时 |
记忆口诀:重载看参数、重写看虚函数、隐藏是"同名就遮"。参数不同的"重写"其实都是隐藏------上一章的坑在这里又出现了。
三、纯虚函数和抽象类:只定规矩,不给实现
在虚函数后面写上 = 0,它就成了纯虚函数 ------只声明,不实现(因为反正要被派生类重写)。包含纯虚函数的类叫抽象类 ,抽象类不能实例化出对象;派生类继承后不重写纯虚函数,派生类也还是抽象类。
cpp
class Car {
public:
virtual void Drive() = 0; // 纯虚函数:只会开车的"规矩",没有实现
};
class Benz : public Car {
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
class BMW : public Car {
public:
virtual void Drive() { cout << "BMW-操控" << endl; }
};
int main() {
// Car car; // 编译报错:error C2259 "Car" 无法实例化抽象类
Car* pBenz = new Benz;
pBenz->Drive(); // Benz-舒适
Car* pBMW = new BMW;
pBMW->Drive(); // BMW-操控
return 0;
}
纯虚函数的价值在于强制契约:派生类不重写就实例化不出对象,相当于语法层面逼着每个子类把接口落实。想表达"这个基类只是个概念,不该被创建"时,就用纯虚函数。
#mermaid-svg-TuJCCRrUVe7JiyEG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TuJCCRrUVe7JiyEG .error-icon{fill:#552222;}#mermaid-svg-TuJCCRrUVe7JiyEG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TuJCCRrUVe7JiyEG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TuJCCRrUVe7JiyEG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TuJCCRrUVe7JiyEG .marker.cross{stroke:#333333;}#mermaid-svg-TuJCCRrUVe7JiyEG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TuJCCRrUVe7JiyEG p{margin:0;}#mermaid-svg-TuJCCRrUVe7JiyEG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG .cluster-label text{fill:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG .cluster-label span{color:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG .cluster-label span p{background-color:transparent;}#mermaid-svg-TuJCCRrUVe7JiyEG .label text,#mermaid-svg-TuJCCRrUVe7JiyEG span{fill:#333;color:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG .node rect,#mermaid-svg-TuJCCRrUVe7JiyEG .node circle,#mermaid-svg-TuJCCRrUVe7JiyEG .node ellipse,#mermaid-svg-TuJCCRrUVe7JiyEG .node polygon,#mermaid-svg-TuJCCRrUVe7JiyEG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TuJCCRrUVe7JiyEG .rough-node .label text,#mermaid-svg-TuJCCRrUVe7JiyEG .node .label text,#mermaid-svg-TuJCCRrUVe7JiyEG .image-shape .label,#mermaid-svg-TuJCCRrUVe7JiyEG .icon-shape .label{text-anchor:middle;}#mermaid-svg-TuJCCRrUVe7JiyEG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TuJCCRrUVe7JiyEG .rough-node .label,#mermaid-svg-TuJCCRrUVe7JiyEG .node .label,#mermaid-svg-TuJCCRrUVe7JiyEG .image-shape .label,#mermaid-svg-TuJCCRrUVe7JiyEG .icon-shape .label{text-align:center;}#mermaid-svg-TuJCCRrUVe7JiyEG .node.clickable{cursor:pointer;}#mermaid-svg-TuJCCRrUVe7JiyEG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TuJCCRrUVe7JiyEG .arrowheadPath{fill:#333333;}#mermaid-svg-TuJCCRrUVe7JiyEG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TuJCCRrUVe7JiyEG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TuJCCRrUVe7JiyEG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TuJCCRrUVe7JiyEG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TuJCCRrUVe7JiyEG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TuJCCRrUVe7JiyEG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TuJCCRrUVe7JiyEG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TuJCCRrUVe7JiyEG .cluster text{fill:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG .cluster span{color:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TuJCCRrUVe7JiyEG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TuJCCRrUVe7JiyEG rect.text{fill:none;stroke-width:0;}#mermaid-svg-TuJCCRrUVe7JiyEG .icon-shape,#mermaid-svg-TuJCCRrUVe7JiyEG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TuJCCRrUVe7JiyEG .icon-shape p,#mermaid-svg-TuJCCRrUVe7JiyEG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TuJCCRrUVe7JiyEG .icon-shape .label rect,#mermaid-svg-TuJCCRrUVe7JiyEG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TuJCCRrUVe7JiyEG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TuJCCRrUVe7JiyEG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TuJCCRrUVe7JiyEG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不能实例化
可实例化
可实例化
必须重写才能落地
必须重写才能落地
抽象类 Car
virtual void Drive() = 0
Car car ✗
Benz
重写 Drive()
Benz b ✓
BMW
重写 Drive()
BMW w ✓
四、多态的原理:虚函数表
4.1 对象里多出来的那个指针
先来一道经典的 sizeof 题(32 位程序):
cpp
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
protected:
int _b = 1; // 4 字节
char _ch = 'x'; // 1 字节(对齐补 3)
};
int main() {
Base b;
cout << sizeof(b) << endl; // 输出 12,而不是 8!
return 0;
}
答案是 12 。多出来的 4 字节是一个指针------虚函数表指针(__vfptr) ,放在对象的最前面(某些平台也可能放最后)。一个含虚函数的类,其所有虚函数的地址都被收集进一张虚函数表(虚表),而每个对象里都存着指向这张表的指针。
text
Base 对象(32 位,12 字节)
+------------------+
| __vfptr (4字节) | --> 虚表: [&Base::Func1, ...]
+------------------+
| _b (4字节) |
+------------------+
| _ch + 填充(4字节) |
+------------------+
4.2 动态绑定的底层真相
ptr->BuyTicket() 到底怎么做到"指向谁就调谁的"?看编译成汇编后两条路径的对比:
text
// ptr 是指针 + BuyTicket 是虚函数 → 动态绑定
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr] ; 取对象前 4 字节 → 虚表指针
00EF2004 mov edx,dword ptr [eax] ; 取虚表地址
00EF2008 mov ecx,dword ptr [ptr] ; this 指针
00EF200B mov eax,dword ptr [edx] ; 从虚表里取出函数地址
00EF200D call eax ; 调用的是"查表查出来的"地址
// 不满足多态条件 → 静态绑定,编译期写死
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call BuyTicket (0EA153Ch) ; 地址直接写死在指令里(讲义原文此处写的是构造函数符号,此处按语义修正)
三个"取地址"动作连起来,就是多态的全部秘密:运行时到指向对象的虚表里查函数地址。指向 Person 就查 Person 的虚表,指向 Student 就查 Student 的虚表------同一行代码,两种形态,这就对上了开头那两个"必须条件"。
#mermaid-svg-gJsfA9b9RYP3yImD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gJsfA9b9RYP3yImD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gJsfA9b9RYP3yImD .error-icon{fill:#552222;}#mermaid-svg-gJsfA9b9RYP3yImD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gJsfA9b9RYP3yImD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gJsfA9b9RYP3yImD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gJsfA9b9RYP3yImD .marker.cross{stroke:#333333;}#mermaid-svg-gJsfA9b9RYP3yImD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gJsfA9b9RYP3yImD p{margin:0;}#mermaid-svg-gJsfA9b9RYP3yImD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gJsfA9b9RYP3yImD .cluster-label text{fill:#333;}#mermaid-svg-gJsfA9b9RYP3yImD .cluster-label span{color:#333;}#mermaid-svg-gJsfA9b9RYP3yImD .cluster-label span p{background-color:transparent;}#mermaid-svg-gJsfA9b9RYP3yImD .label text,#mermaid-svg-gJsfA9b9RYP3yImD span{fill:#333;color:#333;}#mermaid-svg-gJsfA9b9RYP3yImD .node rect,#mermaid-svg-gJsfA9b9RYP3yImD .node circle,#mermaid-svg-gJsfA9b9RYP3yImD .node ellipse,#mermaid-svg-gJsfA9b9RYP3yImD .node polygon,#mermaid-svg-gJsfA9b9RYP3yImD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gJsfA9b9RYP3yImD .rough-node .label text,#mermaid-svg-gJsfA9b9RYP3yImD .node .label text,#mermaid-svg-gJsfA9b9RYP3yImD .image-shape .label,#mermaid-svg-gJsfA9b9RYP3yImD .icon-shape .label{text-anchor:middle;}#mermaid-svg-gJsfA9b9RYP3yImD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gJsfA9b9RYP3yImD .rough-node .label,#mermaid-svg-gJsfA9b9RYP3yImD .node .label,#mermaid-svg-gJsfA9b9RYP3yImD .image-shape .label,#mermaid-svg-gJsfA9b9RYP3yImD .icon-shape .label{text-align:center;}#mermaid-svg-gJsfA9b9RYP3yImD .node.clickable{cursor:pointer;}#mermaid-svg-gJsfA9b9RYP3yImD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gJsfA9b9RYP3yImD .arrowheadPath{fill:#333333;}#mermaid-svg-gJsfA9b9RYP3yImD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gJsfA9b9RYP3yImD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gJsfA9b9RYP3yImD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gJsfA9b9RYP3yImD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gJsfA9b9RYP3yImD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gJsfA9b9RYP3yImD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gJsfA9b9RYP3yImD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gJsfA9b9RYP3yImD .cluster text{fill:#333;}#mermaid-svg-gJsfA9b9RYP3yImD .cluster span{color:#333;}#mermaid-svg-gJsfA9b9RYP3yImD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gJsfA9b9RYP3yImD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gJsfA9b9RYP3yImD rect.text{fill:none;stroke-width:0;}#mermaid-svg-gJsfA9b9RYP3yImD .icon-shape,#mermaid-svg-gJsfA9b9RYP3yImD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gJsfA9b9RYP3yImD .icon-shape p,#mermaid-svg-gJsfA9b9RYP3yImD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gJsfA9b9RYP3yImD .icon-shape .label rect,#mermaid-svg-gJsfA9b9RYP3yImD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gJsfA9b9RYP3yImD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gJsfA9b9RYP3yImD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gJsfA9b9RYP3yImD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Student 对象
Person 对象
指向 Person
指向 Student
Person* ptr
__vfptr
__vfptr(继承自基类部分)
Person 虚表
BuyTicket → 全价版
Student 虚表
BuyTicket → 打折版(重写已覆盖)
4.3 虚表的几条关键规则
讲义把虚表的行为总结得很全,逐条过:
- 同类型对象共用一张虚表,不同类型各有各的虚表------基类和派生类的虚表是独立的;
- 派生类对象的虚表指针继承自基类部分,自己不再生成新的(但和基类对象的那份不是同一个);
- 重写就是"覆盖虚表条目":派生类重写了基类虚函数,派生类虚表里对应位置就换成派生类版本的地址------"覆盖"这个词在虚表层面是字面意思;
- 派生类虚表的内容 = 基类的虚函数地址(没被重写的部分)+ 重写后覆盖的地址 + 派生类自己新增的虚函数地址;
- 虚表本质是一个存虚函数指针的指针数组 ,VS 系列编译器会在末尾放一个
0x00000000作标记(g++ 不放,这是编译器自己的约定)。
两个"存在哪"的问题:
!question 虚函数存在哪?虚表又存在哪?
- 虚函数 和普通函数一样,编译好后就是一段指令,存在代码段,只是它的地址又存进了虚表;
- 虚表 的位置 C++ 标准没规定。讲义用打印地址对比验证:VS 下虚表地址和常量区字符串紧挨着------存在代码段(常量区)。
验证代码也给你,自己跑一遍看看地址:
cpp
// 配合前文的 Base / Derive 类定义使用
int main() {
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Base虚表地址:%p\n", *(int*)p3);
printf("Derive虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
总结与行动建议
多态 = 继承 + 虚函数重写 + 基类指针/引用调用,底层靠虚表在运行时查地址;静态多态(重载、模板)则在编译时就安排好了一切。
建议
- 基类析构函数一律加
virtual,防止delete 基类指针时泄漏派生类资源。 - 派生类重写一律加
override,把拼写、签名错误挡在编译期。 - 概念性基类用纯虚函数 (
func() = 0)定义接口契约,强制派生类实现。 - 别在虚函数重写里改默认参数------默认值跟静态类型走,改了也不会生效,只会埋雷。
多态的本质,是把"调用哪个函数"这个决定,从编译期推迟到了运行期------程序因此获得了根据对象"随机应变"的能力。