👋 欢迎阅读

🔥愿旖旎 · 个人主页
📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》
🌄 钱塘江上潮信来,今日方知我是我
✨当前学习内容: 《C++学习》
📑 目录
一、前置讲解
在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。
🧠 前置知识一:单继承的虚表
子类复制 父类虚表,重写 的项被覆盖,新增 的虚函数追加到表尾。
🧵 前置知识二:多继承的虚表
继承了几个带虚函数的基类,就有几张虚表 ;未重写的虚函数挂在第一张虚表里。
✂️ 前置知识三:切片不拷贝虚表
子类对象赋给父类对象 时不会拷贝虚表 ,所以必须用指针 / 引用才能多态。
🚫 前置知识四:四类"不能是虚函数"
静态成员函数、构造函数 不能是虚函数;内联函数 可以但会失去 inline;析构函数 可以且最好设为虚。
⚙️ 前置知识五:虚表生成时机
虚表在编译期 生成,运行时只是把对象的 vptr 指向对应虚表。
二、单继承和多继承关系的虚函数表
这里重点看派生类对象的虚表模型------基类的虚表模型前面已经看过了,没什么需要特别研究的。
2.1 单继承中的虚函数表
cpp
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; } // 重写
virtual void func3() { cout << "Derive::func3" << endl; } // 新增
virtual void func4() { cout << "Derive::func4" << endl; } // 新增
private:
int b;
};
监视窗口里会"看不见" func3 和 func4 ------这是编译器的监视窗口故意隐藏 了这两个函数,也可以认为是它的一个小 bug。监视窗口里只能看到和父类有关的虚函数。

真实的虚表应该是下面这样:

| 虚表槽位 | 内容 | 说明 |
|---|---|---|
| 第 1 项 | Derive::func1 |
重写,覆盖 了 Base::func1 |
| 第 2 项 | Base::func2 |
继承来的虚函数,原样保留 |
| 第 3 项 | Derive::func3 |
新增虚函数,按声明次序追加 |
| 第 4 项 | Derive::func4 |
同上 |
| 末尾 | nullptr |
虚表结束标记 |
💡 所以看虚表不能只信监视窗口------它只列"和父类有关"的项。要看全,可以用内存窗口按虚表指针去数。
2.2 多继承中的虚函数表
cpp
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; } // 重写(两个基类都有 func1)
virtual void func3() { cout << "Derive::func3" << endl; } // 新增
private:
int d1;
};
两个关键结论:
| 结论 | 说明 |
|---|---|
| 继承几个虚表,子类就有几个虚表 | Derive 继承了 Base1 和 Base2,所以 Derive 对象里有两个虚表指针、两张虚表 |
| 未重写的虚函数放在第一个基类的虚表中 | Derive 新增的 func3 没有 出现在它自己"专属"的位置,而是追加到第一张虚表(Base1 那张)的末尾 |

Derive 的两张虚表大致长这样:
| 虚表 | 槽位内容 |
|---|---|
第一张(继承自 Base1) |
Derive::func1(重写覆盖)、Base1::func2、Derive::func3(新增,追加) |
第二张(继承自 Base2) |
Derive::func1(重写覆盖)、Base2::func2 |
💡 注意
func1在两张表里都被覆盖 了:Base1和Base2各有一个func1,Derive::func1同时把它们都重写了。这也是多继承下"一个重写可能改多张表"的典型情况。
2.3 菱形继承、菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,所以这里不进行考究。
💡 原因很简单:菱形继承本身就是设计禁忌 (数据冗余 + 二义性),菱形虚拟继承的虚表模型更是复杂到没必要考 。面试问到时能说清"不推荐、所以不深究"就够了------但虚函数表和虚基表千万别搞混(见文末最后一题)。
三、多态知识补充
3.1 为什么不能用父类对象去调用虚函数?
为什么不能是父类对象,而是其指针或引用?
因为子类赋值给父类对象时,不会拷贝虚表,因此不能。
反过来想就明白了:如果子类能把虚表拷贝给父类对象,那么拷贝完之后,父类对象的虚表就变成了子类的虚表 ;此时如果再生成一个父类指针或引用去调用"父类的虚函数",实际调到的却是子类的虚函数------整个类型体系就乱了。
| 传参方式 | 虚表会不会跟着走 | 能否多态 |
|---|---|---|
父类对象 (Person p = s;) |
不拷贝虚表 | 不能 |
父类指针 / 引用 (Person* p = &s; / Person& r = s;) |
指向的仍是子类对象本体,虚表指针没变 | 能 |
那反过来假设一下:如果虚表真的跟着对象走了,会怎样?
cpp
class Person
{
public:
Person(const string& name = "person") : _name(name) {}
virtual void Introduce()
{
cout << "我是 Person:" << _name << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const string& name, const string& school)
: Person(name), _school(school)
{}
void Introduce() override
{
cout << "我是 Student:" << _name << ",来自 " << _school << endl;
}
private:
string _school; // 子类独有的成员
};
int main()
{
Student s("小明", "第一中学");
// 切片时子类的虚表也一起拷了过来
Person p = s; // 于是 p 的 vptr 指向 Student 的虚表
// 拿一个父类指针,指向这个切片后的父类对象
Person* pp = &p;
pp->Introduce(); // 静态类型是 Person*,但虚表是 Student 的
}
运行结果:
我是 Student:小明,来自 第一中学
💡 推到这里矛盾就出来了:
pp是Person*,指向的也是切片后的Person对象,结果却调到了Student::Introduce------而p里根本没有_school,这一读就是越界,程序拿到的是垃圾值或者直接崩溃。能推出这种自相矛盾的结果,说明这条路走不通:编译器不会在切片时拷贝虚表 。父类对象的
vptr只有指向父类虚表这一条路,p.Introduce()才一定调父类版本。多态要"看本体",所以只能用指针/引用指向子类对象本身。
💡 一句话记:对象传的是"切片后的副本",指针/引用传的是"对象本身"。多态要的正是"看本体",所以只能用指针/引用。
3.2 四个小知识
| 问题 | 答案 | 原因 |
|---|---|---|
| 内联函数可以是虚函数吗? | 可以 ,但会舍弃 inline 属性 |
因为虚函数要放到虚表中,调用要经过查表,没法保证就地展开 |
| 静态成员函数可以是虚函数吗? | 不可以 | 静态成员函数没有 this 指针 ,也没有对象,类名::函数() 的调用方式访问不到虚表 |
| 构造函数可以是虚函数吗? | 不可以 | 对象里的虚表指针是在构造函数初始化列表阶段才初始化的,构造时虚表还没准备好 |
| 析构函数可以是虚函数吗? | 可以,而且基类析构最好设为虚 | 因为编译器把基类和派生类的析构函数名统一处理成 destructor ,能构成重写,delete 基类指针时才不会漏掉子类清理 |
四、复盘(附答案)
💡 选择题
1. 下面哪种面向对象的方法可以让你变得富有( )
A:继承 B:封装 C:多态 D:抽象
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A:继承 B:模板 C:对象的自身引用 D:动态绑定
3. 面向对象设计中的继承和组合,下面说法错误的是?( )
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象
B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数
D:纯虚函数必须是空函数
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数
D:虚函数可以是一个 static 型的函数
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
7. 假设 A 类中有虚函数,B 继承自 A,B 重写 A 中的虚函数,也没有定义任何虚函数,则( )
A:A 类对象的前 4 个字节存储虚表地址,B 类对象前 4 个字节不是虚表地址
B:A 类对象和 B 类对象前 4 个字节存储的都是虚基表的地址
C:A 类对象和 B 类对象前 4 个字节存储的虚表地址相同
D:A 类和 B 类虚表中虚函数个数相同,但 A 类和 B 类使用的不是同一张虚表
8. 下面程序输出结果是什么?( )
cpp
#include <iostream>
using namespace std;
class A
{
public:
A(char* s) { cout << s << endl; }
~A() {}
};
class B : virtual public A
{
public:
B(char* s1, char* s2) : A(s1) { cout << s2 << endl; }
};
class C : virtual public A
{
public:
C(char* s1, char* s2) : A(s1) { cout << s2 << endl; }
};
class D : public B, public C
{
public:
D(char* s1, char* s2, char* s3, char* s4)
: B(s1, s2), C(s1, s3), A(s1)
{
cout << s4 << endl;
}
};
int main()
{
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
9. 多继承中指针偏移问题?下面说法正确的是( )
cpp
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
10. 以下程序输出结果是什么( )
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;
}
A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
📝 选择题答案与解析
| 题号 | 答案 | 解析 |
|---|---|---|
| 1 | A | "变得富有"当然是继承(幽默题,考的是继承的字面含义) |
| 2 | D | 只需记住:动态绑定就是多态------方法定义与具体对象无关、调用关联具体对象 |
| 3 | C | 错在"优先使用继承" :正确原则是优先使用组合,因为组合耦合度低、是黑盒复用 |
| 4 | A | 含纯虚函数的类即抽象类,不能实例化对象 (正确)。B 错:虚基类 是虚继承得来的,和纯虚函数无关;C 错:子类不实例化 也可以不实现;D 错:纯虚函数可以有定义 |
| 5 | 无正确选项 | A 错:重写要求参数个数和类型相同 ;B 错:内联可以是虚函数 ,只是会舍弃 inline 属性;C 错:只有要实现多态 才必须重定义;D 错:静态成员函数没有 this,不能是虚函数。 |
| 6 | D | 一个类的不同对象共享该类的虚表 (正确)。A 错:多继承可能有多张虚表 ;B 错:虚表是独立 的,不重写也是各一张;C 错:虚表在编译期 生成,运行时只是把对象的 vptr 指向 虚表地址,不会动态创建 |
| 7 | D | A 错:只要有虚表,前 4/8 字节都是 __vfptr ;B 错:虚基表 和虚继承 有关,和虚函数无关;C 错:虚表是独立的 ,地址不同;D 对:两张表内容项数相同(个数一样),但不是同一张表 |
| 8 | A | 虚继承中最派生类(D)负责构造虚基类(A) :D 的初始化列表里 A(s1) 先执行,输出 class A;随后 B、C 构造函数体分别输出 class B、class C;最后 D 的构造体输出 class D,即 class A class B class C class D。B 和 C 里写的 A(s1) 在 D 实例化时不生效(保留是为了 B/C 能被独立实例化) |
| 9 | C | Derive 第一个继承的是 Base1 ,所以 Base1 子对象位于 Derive 对象的起始地址 ,因此 p1 == p3 ;而 Base2 子对象在后面,地址有偏移,所以 p2 不等 |
| 10 | B | func() 是在 A::test() 里调用的,默认参数按静态类型 A 取 1 ;虽然实际执行的是 B::func(虚函数动态绑定),但它收到的参数已经是 1 ,所以输出 B->1 |
结语
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充
你的每一次互动都是我继续更新的动力,我们下一篇见!🚀