【C++ 基础入门】C++多态(下):单/多继承虚表、多态补充与 21 道高频题

👋 欢迎阅读

🔥愿旖旎 · 个人主页

📘学习专栏: 《算法专栏》《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

结语

如果本文对你有帮助,欢迎:

👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

相关推荐
叼烟扛炮5 小时前
[LangChain] 学习笔记(07):大模型长出手脚——工具(Tools)定义与调用闭环
笔记·学习·langchain
Wang's Blog10 小时前
Java 项目实战: 交友项目 - FastDFS:入门案例
java·开发语言·交友
叼烟扛炮10 小时前
[LangChain] 学习笔记(06):突破大模型限制——结构化输出(Structured Output)
windows·笔记·学习·langchain
apcipot_rain10 小时前
C++转Java日记(03)
java·开发语言·c++
布莱克60511 小时前
C++ 中 map 的 insert 与 emplace:key 已存在时会覆盖吗?
开发语言·数据结构·c++·算法·map
且从容.11 小时前
VUE3学习笔记(根据黑马教程总结)
笔记·学习
xys_67811 小时前
深挖C++:类和对象(中)
开发语言·c++
灿飞屋11 小时前
Rust 编程其实很简单
开发语言·rust
蒸蒸yyyyzwd12 小时前
关于 AI 开发相关的一些面经。
笔记·学习