【C++】继承和多态常见的面试问题

文章目录

  • 继承
    • 笔试面试题
      • [1. 什么是菱形继承?菱形继承的问题是什么?](#1. 什么是菱形继承?菱形继承的问题是什么?)
      • [2. 什么是菱形虚拟继承?如何解决数据冗余和二义性?](#2. 什么是菱形虚拟继承?如何解决数据冗余和二义性?)
      • [3. 继承和组合的区别?什么时候用继承?什么时候用组合?](#3. 继承和组合的区别?什么时候用继承?什么时候用组合?)
    • 选择题
  • 多态
    • 概念考察
    • 问答题
      • [1. 什么是多态?](#1. 什么是多态?)
      • [2. 什么是重载、重写(覆盖)、重定义(隐藏)?](#2. 什么是重载、重写(覆盖)、重定义(隐藏)?)
      • [3. 多态的实现原理?](#3. 多态的实现原理?)
      • [4. `inline` 函数可以是虚函数吗?](#4. inline 函数可以是虚函数吗?)
      • [5. 静态成员可以是虚函数吗?](#5. 静态成员可以是虚函数吗?)
      • [6. 构造函数可以是虚函数吗?](#6. 构造函数可以是虚函数吗?)
      • [7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?](#7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?)
      • [8. 对象访问普通函数快还是虚函数更快?](#8. 对象访问普通函数快还是虚函数更快?)
      • [9. 虚函数表是在什么阶段生成的,存在哪?](#9. 虚函数表是在什么阶段生成的,存在哪?)
      • [10. C++菱形继承的问题?虚继承的原理?](#10. C++菱形继承的问题?虚继承的原理?)
      • [11. 什么是抽象类?抽象类的作用?](#11. 什么是抽象类?抽象类的作用?)
    • 选择题

继承

笔试面试题

  1. 什么是菱形继承?菱形继承的问题是什么?
  2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
  3. 继承和组合的区别?什么时候用继承?什么时候用组合?

1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承指的是一个类通过两个子类继承了同一个基类,这两个子类再被另一个派生类继承,形成菱形结构。例如:

cpp 复制代码
class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};

在这个结构中,D 通过 BC 间接继承了两次 A,这就形成了菱形继承。

菱形继承的问题主要有两个:

  • 数据冗余 :由于 D 通过 BC 继承了两份 A,导致存在两份相同的 A 成员。这在内存中会引起冗余,浪费空间。
  • 二义性问题 :当在 D 中试图访问 A 的成员时,编译器无法确定该访问来自 B 继承的 A 还是 C 继承的 A,从而导致二义性。例如:
cpp 复制代码
D d;
d._a;  // 编译器不确定是访问 B::_a 还是 C::_a

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性?

菱形虚拟继承 是为了解决菱形继承中的数据冗余和二义性问题的一种机制。通过在继承时使用 virtual 关键字,编译器确保只会有一个 A 的实例被共享,而不会有两个冗余的 A 实例。例如:

cpp 复制代码
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

解决方式

  • 数据冗余问题 :通过虚拟继承,D 类中的 A 只会有一份实例,不管是通过 B 还是 C 继承,D 都只包含一份 A。这样消除了冗余。
  • 二义性问题 :由于只有一个 A 实例,编译器在 D 中访问 A 的成员时,不会再出现二义性。例如:
cpp 复制代码
D d;
d._a;  // 正常访问唯一的 A 实例

虚拟继承通过虚基表指针 实现,BC 各自保存一个指针,指向一个共享的 A 实例,这样就避免了冗余的 A

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承组合都是用来复用代码和实现类之间关系的两种手段,但它们的适用场景和概念有明显的区别:

  • 继承 (Inheritance):
    • 描述的是"is-a"关系 :如果类 B 继承类 A,意味着 BA 的一种特殊类型(比如 Cat 是一种 Animal)。
    • 特点
      • 子类自动继承父类的所有属性和行为。
      • 继承是一种强耦合,子类的行为和父类行为紧密关联。
      • 一旦父类发生变化,子类也会受到影响。
    • 适用场景
      • 当子类是父类的一种类型 时,使用继承是合适的。例如:Car 继承 Vehicle
      • 适合复用父类的方法和属性,同时允许子类对父类的行为进行扩展或重写。
  • 组合 (Composition):
    • 描述的是"has-a"关系 :如果类 B 包含了类 A 的对象,意味着 B 拥有 A 的功能或行为,但 B 并不是 A 的一种类型。例如,Car 包含了一个 Engine,但 Car 不是一种 Engine
    • 特点
      • 组合是弱耦合,每个类保持独立性。
      • 可以动态地替换或改变组合类的行为,不需要修改组合类本身的代码。
    • 适用场景
      • 当一个类的功能可以通过另一类来实现,但它们之间不是类型的关系时,使用组合。
      • 当需要在运行时组合对象行为或功能,而不希望因为继承导致复杂的耦合关系。

总结

  • 继承 用于表达"BA"的关系(is-a)。
  • 组合 用于表达"B 拥有 A"的关系(has-a)。
  • 当两个类之间有明显的层次关系时,继承是适合的;当一个类需要复用另一个类的功能时,但没有"类型"关系时,使用组合。

选择题

A.可以存在,如函数重载

B.基类与子类函数名字相同,参数不同,形成的是隐藏

C.可以共存

D.成员函数在同一个类里面同名,此时构成了重载,但变量一定不能同名,故正确

A. 错误

  • 解释 :选项 A 认为会打印 "A::f()",这是不正确的。原因是 B 类中的 f(int) 函数隐藏了基类 A 中的 f(),因此在调用 b.f() 时,编译器无法找到无参数的 f() 函数,导致编译错误。所以不会打印 "A::f()"

B. 错误

  • 解释 :选项 B 认为会打印 "B::f()",但这是不可能的,因为 b.f() 调用的是无参数版本的函数,而 B 类中的 f(int) 是带参数的。因此,编译器也无法匹配到 B::f(int)。最终仍然会导致编译错误。

C. 错误

  • 解释 :选项 C 部分正确,即"不能通过编译是对的",但给出的原因是错的。问题不在于成员变量 a,而是 子类 B 中的 f(int) 函数隐藏了父类 A 中的 f() 函数 ,并且由于没有无参的 f() 函数可用,编译时会报错。因此,C 的理由是不准确的。

D. 正确

  • 解释 :D 是正确的选项,因为前面所有的解释都有误。真正的编译问题来源于函数隐藏,而不是成员变量 a 的名字冲突或者其他原因。

因此,答案选 D 是最准确的选择。

A. 错误

  • 解释:如果基类有默认构造函数,派生类的构造函数并不需要显式调用基类的构造函数。只有在基类没有默认构造函数,或需要传递参数给基类构造函数时,派生类才需要在初始化列表中显式调用基类的构造函数。因此,A 是不正确的。

B. 错误

  • 解释 :派生类的构造函数首先会调用 基类 的构造函数来初始化基类部分成员,之后才会初始化派生类的成员。因此,初始化的顺序是 先基类,再子类,B 的表述是反的。

C. 错误

  • 解释 :派生类的析构函数会自动调用基类的析构函数,并按照 构造顺序的逆序 进行析构,即先析构派生类,再析构基类。因此,C 是不正确的。

D. 正确

  • 解释:在定义派生类的构造函数时,确实有时候需要参考基类的构造函数,特别是当基类没有默认构造函数,或者需要特定参数来构造基类时。在这种情况下,派生类的构造函数必须通过初始化列表显式调用基类的构造函数。因此,D 是正确的。

综上所述,答案 D 是正确的。

子类实例化对象,由于继承的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构,故答案为 C

A.先构造父类,在构造子类 故正确

B.不一定,如果父类有默认构造函数就不需要

C.刚好相反,先调用子类,在调用父类

D.派生类的析构函数往往还需要连同父类析构函数一起调用,同时清除父类的资源

A.静态成员函数也可以被继承

B.成员变量所有的都会被继承,无论公有私有

C.友元函数不能被继承,相当于你爹的朋友不一定是你的朋友

D.静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份

A.静态变量就不被包含

B.同理,静态变量就不被包含

C.父类所有成员都要被继承,因此包含了

D.静态成员一定是不被包含在对象中的

E.很显然,以上说法都不正确
区分静态成员和静态变量

答案是 B,原因如下:

A. 正确

  • 解释 :基类++指针++ 可以++直接指向++ 子类对象,这叫做 向上转型(upcasting)。由于子类是基类的扩展,它包含了基类的所有部分,所以基类指针可以指向子类对象。这种操作是合法的且常见。

B. 错误

  • 解释基类对象不能直接 **++赋值++ **给子类对象 。因为基类对象没有子类特有的成员和方法,直接赋值会丢失子类中的额外信息或导致类型不匹配,因此不允许这种操作。这叫做 向下转型 (downcasting),需要显式的类型转换,而且在某些情况下需要类型检查(如在C++中使用 dynamic_cast)。

C. 正确

  • 解释 :++子类对象的引用++ ++不能++ ++引用基类的对象++。这是因为基类对象不包含子类的特有成员或行为,因此无法用子类的引用来指向基类对象。这种操作是非法的。

D. 正确

  • 解释 :子类对象可以直接++赋值++ 给基类对象,这也是 向上转型 。这种情况下,赋值操作会切割掉子类对象中特有的部分,只保留基类部分。这在赋值时会发生"对象切割"问题。

分析:p1和p2虽然都是其父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2,

由于p1对象是第一个被继承的父类类型,所有其地址与子类对象的地址p3所指位置都为子类对象的起始位置,因此p1==p3,所以C正确

答案是 C,详细解析如下:

A. 正确

  • 解释 :在这段代码中,class D 继承了两个 B,分别从 C1C2 继承。因此,D 中实际上包含了两个 B 对象、一个 C1 对象、一个 C2 对象和一个 D 对象。
    • B 类的大小为 4 字节(假设 int 为 4 字节)。
    • C1 类的大小为 4(B) + 4(c1) = 8 字节。
    • C2 类的大小为 4(B) + 4(c2) = 8 字节。
    • D 类的大小为 8(C1) + 8(C2) + 4(d) = 20 字节。
      因此,D 的总大小为 20 字节,故 A 是正确的

B. 正确

  • 解释 :由于 C1C2 都继承了 B,并且++没有使用虚拟继承++ ,所以 D 中实际上有两个 B 的实例,分别存在于 C1C2 中。因此,B 是正确的

C. 错误

  • 解释 :由于 D 类中存在两份 B,分别继承自 C1C2,因此 D 对象不能直接访问 b 成员 。直接访问 b 会产生二义性:b 是来自 C1 继承的 B 还是 C2 继承的 B?为了消除二义性,必须通过指定路径来访问,例如 d.C1::bd.C2::b
    因此,C 是不正确的

D. 正确

  • 解释 :菱形继承的确会带来二义性问题,因为子类会从两个路径继承同一个基类。在这种情况下,可以使用 虚拟继承 来避免多个基类实例的问题。因此,D 是正确的
    ++假如 C1,C2都虚拟继承了B++ 那么本题的每个选项解释如下:

如果 C1C2 都虚拟继承了 B,那么菱形继承的结构会发生变化。通过虚拟继承,**B**** 类只会在 **D** 类中存在一份实例**,解决了二义性问题。基于这个前提,我们可以重新分析每个选项。

先来看类的定义,假设 C1C2 现在虚拟继承了 B

A. D 总共占了 20 个字节

  • 解释 :虚拟继承改变了继承的布局。虚拟继承确保 B 类只存在一份实例,这样可以避免多次继承 B 的问题。

    由于虚拟继承可能会带来额外的指针(用来指向虚拟基类实例),具体的内存布局依赖于编译器实现,因此在某些编译器中虚拟继承可能会导致额外的字节开销,最终结果未必是 20 字节。不过,假设编译器管理良好且没有其他额外开销,该选项可能是正确的 ,但在实际编译器中往往会比 20 字节大。A 可能正确,但不一定是完全准确的。

    • B 类的大小为 4 字节(假设 int 为 4 字节)。
    • C1C2 类通过虚拟继承继承了 B,所以 B 的实例在 D 中只存在一份。
    • C1 类的大小为 4(虚拟 B) + 4(c1)
    • C2 类的大小为 4(虚拟 B) + 4(c2)
    • D 类的大小为 4(虚拟 B) + 4(c1) + 4(c2) + 4(d)

B. B 中的内容总共在 D 对象中存储了两份

  • 解释 :由于 C1C2 都采用了虚拟继承,B 类在 D 类中只存在一份实例。因此,B 是错误的

C. D 对象可以直接访问从基类继承的 b 成员

  • 解释 :在使用虚拟继承的情况下,D 类中 B 的实例只存在一份,因此不再存在二义性问题。D 对象可以直接访问从 B 继承的 b 成员,无需显式地指定路径。因此 C 是正确的

D. 菱形继承存在二义性问题,尽量避免设计菱形继承

  • 解释 :通过虚拟继承,菱形继承的二义性问题得到了很好的解决。虽然菱形继承仍然可能引发复杂的设计问题,但在这种情况下,二义性不再存在。因此 D 是错误的,因为虚拟继承已经解决了二义性。

多态

概念考察

  1. 下面哪种面向对象的方法可以让你变得富有( A )
    A: 继承 B: 封装 C: 多态 D: 抽象
  • 解释:这是一道幽默的选择题,实际上并没有哪种面向对象方法可以直接让你变得"富有"。不过这里的"富有"是双关语,隐喻了"继承"在实际编程中可以让你复用父类的属性和方法,就像生活中的"继承财富"一样。通过继承,你可以得到父类的功能,而不需要重新实现,因此继承"让你变得富有"是一个双关的比喻。
  1. (D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,
    而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

解释:动态绑定(Dynamic Binding)是一种多态机制,它允许在程序运行时根据实际对象的类型来调用相应的方法。在编译时,程序并不确定将调用哪个方法,而是在运行时决定。因此,这使得代码更加灵活和可扩展。动态绑定是实现多态的核心机制。

其他选项的解释:

  • A: 继承:继承允许子类从父类继承属性和方法,不能直接实现动态绑定。
  • B: 模板:模板是C++中的一种泛型编程机制,允许在编译时生成代码,并不涉及动态绑定。
  • C: 对象的自身引用 :这是对象通过this指针访问自身成员的机制,与动态绑定无关。
  1. 面向对象设计中的继承和组合,下面说法错误的是?(C
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
    用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
    态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
    装性的表现

解释 :这道题考察的是继承与组合的区别,错误的选项是C。实际上,组合优先于继承 是面向对象设计中的一条重要原则,特别是在设计模式中被称为"组合优于继承原则"(Favor composition over inheritance)。过度使用继承会导致代码耦合性增强、灵活性降低。

其他选项的解释:

  • A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用:正确。继承中的复用被称为"白盒复用",因为子类能够直接看到父类的实现细节。
  • B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用:正确。组合是一种"黑盒复用",对象间的关系可以在运行时建立,而不需要了解彼此的内部实现。
  • D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现:正确。继承虽然提供了接口复用,但有时会导致父类的封装性被破坏,因为子类可以依赖父类的内部实现。
  1. 以下关于纯虚函数的说法,正确的是(A )
    A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
    C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

解释:声明了纯虚函数的类被称为抽象类,不能直接实例化对象。抽象类的目的是提供一个接口,并由派生类实现该接口的纯虚函数。

其他选项的解释:

  • B:声明纯虚函数的类是虚基类:错误。纯虚函数与虚基类无关,虚基类用于解决多重继承问题。
  • C:子类必须实现基类的纯虚函数:不完全正确。子类可以选择不实现纯虚函数,但这样它本身也会变成抽象类。
  • D:纯虚函数必须是空函数 :错误。纯虚函数只是声明没有实现,但可以有实现,只不过声明部分是= 0
  1. 关于虚函数的描述正确的是(D--->B )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数

解释 :虚函数的多态机制是在++运行时实现++ 的,而内联函数是在++编译时展开的++。这两者是相互冲突的,因此虚函数不能是内联函数。

其他选项的解释:

- **A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型**:错误。虚函数的重写要求派生类中的虚函数与基类的虚函数有相同的参数列表。
- **C:派生类必须重新定义基类的虚函数**:错误。派生类可以选择重写基类的虚函数,<u>但不一定必须重新定义。</u>
- **D:虚函数可以是一个static型的函数**:错误。<u>虚函数是与对象实例相关的,而静态函数与类相关,虚函数不能是静态函数。</u>
  1. 关于虚表说法正确的是( C--->D
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表

解释

  • D:一个类的不同对象共享该类的虚表 :正确。虚表(vtable)是针对++类++ 生成的,而不是针对每个对象生成的,因此++同一个类的所有对象共享同一张虚表++。对象中的虚表指针(vptr)指向这张虚表。

其他选项的解释:

  • A:一个类只能有一张虚表:错误。如果一个类继承了多个具有虚函数的类,或者自身定义了多个虚函数,可能会有多张虚表存在。
  • B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表 :错误。即使子类没有重写虚函数,它++依然有自己的虚表++,虽然可能会指向相同的虚函数。
  • C:虚表是在运行期间动态生成的 :错误。++虚表是在编译期生成的++,而不是在运行时生成。虚表指针会在运行时动态调整,以指向正确的虚表
  1. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则(B-->D
    A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
    B:A类对象和B类对象前4个字节存储的都是虚基表的地址
    C:A类对象和B类对象前4个字节存储的虚表地址相同
    D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚

解释

  • D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表:正确。A类和B类的虚表中的虚函数数量相同,但由于B类重写了A类的虚函数,B类的虚表会指向B类的实现,而A类的虚表指向A类的实现。因此,虽然虚表中函数的数量相同,虚表本身是不同的。

其他选项的解释:

  • A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址:错误。A类和B类对象的前4个字节都是存储虚表指针(vptr),用于指向各自的虚表。
  • B:A类对象和B类对象前4个字节存储的都是虚基表的地址:错误。这与虚基类无关,A类和B类存储的都是虚表指针。
  • C:A类对象和B类对象前4个字节存储的虚表地址相同:错误。B类重写了A类的虚函数,B类有自己独立的虚表,因此它们的虚表地址不同。
  1. 下面程序输出结果是什么? (A
cpp 复制代码
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

解释:

  • 虚继承会确保在派生类中共享一个唯一的基类实例 。因此,当 BC 虚继承了 A 时,在派生类 D 中,A 类的构造函数只会被调用一次
  • 其他就是按顺序来就行了
  1. 多继承中指针偏移问题?下面说法正确的是( C )
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

解释:

cpp 复制代码
d的内存布局类似:
Base1 部分:存储 Base1::_b1
Base2 部分:存储 Base2::_b2
Derive 部分:存储 Derive::_d


Base1* p1 = &d;  // 指向 d 中的 Base1 部分,p1 指向对象 d 的起始地址。
Base2* p2 = &d;  // 指向 d 中的 Base2 部分,这个地址与 p1 不同
Derive* p3 = &d; // 指向 d 整个 Derive 对象的起始地址。与 p1 相同,
                           //因为 Derive 对象的起始部分就是 Base1。
  1. 以下程序输出结果是什么(B
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(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

解释:

1.virtual void test() { func(); }

  • testA 类的虚函数,并且 B 没有重写它。因此,当你调用 p->test() 时,它实际上执行的是 A::test()
  • **A::test()**** 中调用了 **func()** ,而 **func()** 是虚函数**。虚函数的行为是动态绑定 的,意味着根据对象的实际类型来调用合适的函数。在这种情况下,pB* 类型,所以 func() 会调用 B::func()
  1. virtual void func(int val = 1)
  • 虽然 B::func() 覆盖了 A::func(),但有一个非常重要的点:++默认参数是静态绑定的++ ++,它在编译时绑定++ 。具体来说,A::test() 中的 func()++调用会使用++ <u>A</u>++类中定义的默认参数++ ,而不是 B 类中的默认参数。
  • 因此,当 A::test() 调用 func() 时,它使用的是 A 类中给定的默认参数 val = 1,即使实际调用的是 B::func()

调用流程:

  1. p->test() 实际调用的是 A::test()
  2. A::test() 中调用了虚函数 func()。由于 p 指向 B 类对象,动态绑定会让 B::func() 被调用。
  3. 虽然调用的是 B::func(),但 A::test() 使用的是 A 的默认参数 val = 1,因为默认参数是在编译时绑定的。

输出结果:

  • 虽然 B::func(int val = 0) 具有默认参数 0,但由于 A::test() 调用了 func() 并使用了 A 类的默认参数 1,因此程序输出的是:B->1

问答题

作为面试者,我将依次回答上列面试题:

1. 什么是多态?

多态是面向对象编程中的一种特性,它允许同一个函数或方法在不同对象上具有不同的行为。在 C++ 中,多态主要有两种形式:

  • 编译时多态(静态多态):通过函数重载和运算符重载实现。
  • 运行时多态(动态多态):通过继承和虚函数机制实现。当基类的指针或引用指向派生类对象时,调用虚函数会根据实际对象的类型执行不同的实现。

多态的核心目的是提高代码的可扩展性和复用性。

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

  • 重载 (Overloading):是同一个作用域内,允许函数同名但参数类型或参数个数不同的现象。编译器通过参数列表的不同进行区分,这是静态多态的一种形式。
    • 例子:
cpp 复制代码
void func(int a);
void func(double a);
  • 重写 (Overriding):也称为覆盖,是派生类重新实现基类中的虚函数,必须保持函数签名完全相同。通过这种方式,基类的指针或引用在运行时调用派生类的实现,这是动态多态的实现方式。
    • 例子:
cpp 复制代码
class Base {
public:
    virtual void func() { cout << "Base"; }
};
class Derived : public Base {
public:
    void func() override { cout << "Derived"; }
};
  • 重定义 (Hiding):是派生类中的同名函数隐藏了基类中的非虚函数或静态成员函数。尽管函数签名不同,基类函数不再可见。可以通过作用域分辨符 Base::func() 访问基类函数。
    • 例子:
cpp 复制代码
class Base {
public:
    void func(int a) { cout << "Base"; }
};
class Derived : public Base {
public:
    void func(double a) { cout << "Derived"; }
};

3. 多态的实现原理?

多态的实现基于 虚函数表(vtable)虚函数指针(vptr)

  • 虚函数表:对于包含虚函数的类,编译器会为该类生成一个虚函数表,表中存储该类的虚函数地址。
  • 虚函数指针:每个对象都会有一个虚函数指针(vptr),指向其所属类的虚函数表。当通过基类指针或引用调用虚函数时,编译器通过该指针找到对象对应的虚函数表,从而在运行时调用正确的派生类函数。

这一机制支持运行时根据对象的实际类型执行相应的虚函数。

4. inline 函数可以是虚函数吗?

可以 ,但是编译器通常会忽略虚函数的 inline 特性。当函数被声明为虚函数时,它必须通过虚函数表来调用,而不是内联替换。虚函数调用涉及动态绑定,无法直接替换为内联代码,因此虚函数即使被声明为 inline,在大多数情况下也不会被内联。

5. 静态成员可以是虚函数吗?

不能。原因如下:

  • 静态成员函数 不属于任何对象,它们不依赖于具体的实例,也没有 this 指针。
  • 虚函数 依赖于对象的 this 指针,通过 vtable(虚函数表)来实现动态绑定,而静态成员函数无法访问虚函数表。因此,静态成员函数不能是虚函数。
cpp 复制代码
class Base {
public:
    virtual void func() { // 普通虚函数
        cout << "Base::func() called" << endl;
    }

    static void staticFunc() { // 静态成员函数
        cout << "Base::staticFunc() called" << endl;
    }
};

class Derived : public Base {
public:
    void func() override { // 重写虚函数
        cout << "Derived::func() called" << endl;
    }

    // 重定义静态成员函数
    static void staticFunc() {
        cout << "Derived::staticFunc() called" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    
    //通过虚函数表实现了动态绑定,ptr 实际指向 Derived 对象
    // 调用虚函数,运行时绑定,输出 Derived::func()
    ptr->func();

    // 静态成员函数是基于类名调用的,不能通过指针动态绑定
    // ptr->staticFunc(); // 错误!静态成员函数不能通过对象指针调用

    // 必须用类名调用静态成员函数,静态成员函数没有虚表,不能实现多态
    Base::staticFunc();    // 输出 Base::staticFunc() called
    Derived::staticFunc(); // 输出 Derived::staticFunc() called

    delete ptr;
    return 0;
}

6. 构造函数可以是虚函数吗?

答:不能 ,原因是对象中的虚函数表指针(vptr)是在构造函数初始化列表阶段才初始化的。在对象构造过程中,虚函数表还未完成设置,此时如果调用虚函数,会无法正确绑定到具体的函数实现。因此,构造函数无法是虚函数。

详细解释:

  • 虚函数的作用是在运行时实现动态绑定(多态),但在构造函数执行时,类的虚函数表指针尚未被正确设置。因为在对象创建时,基类部分的构造函数先执行,这时候派生类部分还没被初始化,如果基类构造函数是虚函数,就会产生不一致的行为。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且在多态场景下,基类的析构函数最好定义为虚函数。

  • 如果一个类可能作为基类被继承,并且会通过基类指针或引用指向派生类对象,在这种情况下,基类的析构函数需要是虚函数。否则,++当通过基类指针删除派生类对象时,只有基类的析构函数会被调用,而派生类的析构函数不会被执行,从而导致资源泄漏++。

场景:

cpp 复制代码
class Base {
public:
    virtual ~Base() { cout << "Base destructor called" << endl; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived destructor called" << endl; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 如果Base的析构函数不是虚函数,这里只会调用Base的析构函数,导致派生类的析构函数不执行。
    return 0;
}

输出:

plain 复制代码
Derived destructor called
Base destructor called

如果基类的析构函数不是虚函数,Derived 类的析构函数将不会被调用,造成内存泄漏或其他资源释放问题。

8. 对象访问普通函数快还是虚函数更快?

答:普通函数更快

  • 如果是通过普通对象调用函数,普通函数和虚函数的访问速度是一样的。
  • 但在通过指针或引用 调用时,普通函数 更快。调用虚函数需要动态绑定,这意味着编译时不能确定具体要调用哪个函数,必须通过查找虚函数表(vtable )来找到对应的函数指针,因此虚函数调用会略慢一些。++普通函数则在编译时就能确定++,不需要查表,直接调用。

例子:

cpp 复制代码
class Base {
public:
    virtual void virtualFunc() { cout << "Base::virtualFunc" << endl; }
    void normalFunc() { cout << "Base::normalFunc" << endl; }
};

int main() {
    Base b;
    Base* ptr = &b;

    ptr->normalFunc();  // 普通函数,直接调用
    ptr->virtualFunc(); // 虚函数,查表调用

    return 0;
}

对于普通函数调用,编译器直接生成调用指令;而虚函数则需要通过虚函数表查找,稍微增加了开销。

9. 虚函数表是在什么阶段生成的,存在哪?

答:虚函数表(vtable)是在编译阶段生成的

虚函数表一般存储在代码段(常量区) 。每个带有虚函数的类在编译时会生成一个虚函数表,虚函数表中存储了该类的所有虚函数的地址。每个对象有一个虚表指针(vptr) ,指向虚函数表的位置。编译器在构造对象时,会初始化虚表指针(vptr),指向类对应的虚函数表。

  • 在程序运行时,如果通过指针或引用调用虚函数,编译器会通过对象的 vptr 查找虚函数表中的函数地址,从而实现动态绑定。

总结:

  • 编译阶段生成虚函数表。
  • 虚函数表一般存储在代码段,属于程序的常量区域。

10. C++菱形继承的问题?虚继承的原理?

菱形继承问题:

C++ 中的菱形继承是一种特殊的多继承结构,它是指一个类从两个基类继承,而这两个基类又继承自同一个父类。菱形继承引发了两个主要问题:

  1. 数据冗余问题
    当派生类通过多条路径继承同一个基类时,基类的成员变量会在派生类中出现多份副本。这意味着,派生类对象中会有多份相同的基类成员,导致内存浪费和逻辑混乱。
  2. 二义性问题
    由于派生类通过多个路径继承基类,编译器可能无法确定调用哪个基类的成员函数,尤其是当基类有同名成员时。例如,编译器会遇到二义性:调用 A::func() 时是从 B 继承的 A 版本,还是从 C 继承的 A 版本。

例子:

cpp 复制代码
class A {
public:
    int x;
    void func() { cout << "A's func" << endl; }
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

int main() {
    D obj;
    // obj.x 会导致二义性,编译器不确定是B::A::x还是C::A::x
    // obj.func() 也会导致二义性
}

虚继承的原理:

虚继承是为了解决上述菱形继承中的问题。通过虚继承,基类的成员在继承路径上只保留一份拷贝,从而解决了数据冗余和二义性问题。

  • 虚继承声明 :在继承时,基类前加上 virtual 关键字,表示对该基类进行虚继承。
cpp 复制代码
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
  • 虚基表 :虚继承的实现依赖于虚基表(Virtual Base Table,简称 VBT)。虚基表存储了虚基类成员在派生类中的偏移量。当派生类访问虚基类的成员时,会通过虚基表找到正确的偏移,从而解决了菱形继承中的二义性问题。虚基表的工作原理是:
    • 当类进行虚继承时,派生类会为虚基类保留一份特殊的偏移量表,即虚基表。每个虚继承的类都会有指向虚基表的指针。
    • 在访问虚基类的成员时,编译器会通过该指针找到虚基表,进而计算虚基类成员的实际地址,避免产生冗余的副本。

虚继承的好处:

  • 只保留一份基类成员,节省内存,避免数据冗余。
  • 消除了访问基类成员的二义性,解决了编译时的歧义。

11. 什么是抽象类?抽象类的作用?

抽象类定义:

抽象类是包含纯虚函数的类。纯虚函数是一个没有实现的虚函数,其定义如下:

cpp 复制代码
virtual void func() = 0;

任何包含至少一个纯虚函数的类都称为抽象类。抽象类不能被实例化,必须通过派生类来实现其中的纯虚函数。

抽象类的作用:

  1. 强制子类实现特定功能
    抽象类通过定义纯虚函数,强制派生类必须提供这些函数的具体实现。这种设计确保了某些行为在派生类中一定会被实现。
  2. 提供接口继承
    抽象类体现了接口继承的概念,即抽象类定义了一组功能的接口,而派生类实现具体功能。通过这种方式,可以实现面向接口编程,减少对具体实现的依赖,从而提高代码的扩展性和可维护性。
  3. 实现多态
    抽象类是实现多态的重要手段。使用抽象类的指针或引用,可以在运行时通过动态绑定调用派生类的具体实现。

例子:

cpp 复制代码
class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override { cout << "Drawing a circle" << endl; }
};

class Rectangle : public Shape {
public:
    void draw() override { cout << "Drawing a rectangle" << endl; }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();
    shape1->draw();  // 动态绑定,调用Circle的draw
    shape2->draw();  // 动态绑定,调用Rectangle的draw
    delete shape1;
    delete shape2;
}

抽象类通过纯虚函数为派生类提供了统一的接口,同时也使得派生类必须实现这些接口,从而保证了多态的实现。

选择题

选项 A. virtual 修饰的函数称为虚函数

  • 解释 :此说法基本正确,但需要更精确地表述。被 virtual 关键字修饰的**++成员函数++ 才称为虚函数,用于支持动态绑定和多态。详细来说,virtual 关键字用于标记基类中的成员函数,使得在派生类中可以通过基类指针或引用调用派生类的重写版本(动态绑定)。所以,准确的表述应为"被 virtual 修饰的 成员函数**称为虚函数"。因此,A 是不完全准确的。

选项 B. 虚函数的作用是用来实现多态

  • 解释 :这句话是正确的。虚函数的主要作用就是支持 运行时多态。通过虚函数,C++ 实现了动态绑定,使得程序在运行时能够根据实际对象类型来调用正确的函数版本,而不是仅仅根据编译时的类型。这样可以实现"一个接口,多种实现"的设计模式。

因此,虚函数是实现运行时多态的关键机制。B 是正确的。

- **静态绑定**:编译时决定调用哪个函数,适用于非虚函数。
- **动态绑定**:运行时根据对象的实际类型决定调用哪个函数,适用于虚函数。

选项 C. 虚函数在类中声明和类外定义时,都必须加 virtual 关键字

  • 解释 :这是 错误的virtual 关键字只需要在函数声明 时添加,在类外定义 时不需要重复添加 virtual 关键字。编译器通过函数声明中的 virtual 关键字来知道该函数是虚函数,后续的定义部分不需要再次声明。例如:
cpp 复制代码
class Base {
public:
    virtual void show();  // 声明时加 virtual
};

// 定义时无需加 virtual
void Base::show() {
    cout << "Base show" << endl;
}

因此,C 是错误的

选项 D. 静态虚成员函数没有 this 指针

  • 解释:此说法包含两部分:

因此,D 是错误的

1. **静态成员函数没有 **`this`** 指针**:这是正确的,因为静态成员函数属于类本身,而不属于某个对象,因此它们不依赖于具体的对象,也没有 `this` 指针。
2. **静态虚成员函数**:这是不可能的。C++ 不允许成员函数同时是 `static` 和 `virtual`。虚函数必须通过对象来调用,因为虚函数的行为依赖于对象的动态类型,而静态成员函数与类绑定,不依赖于对象,所以这两个关键字是互相矛盾的。

A.友元函数不属于成员函数,不能成为虚函数

B.静态成员函数就不能设置为虚函数

C.静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数

D.尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态

A.多态分为编译时多态和运行时多态,也叫早期绑定和晚期绑定

B.编译时多态是早期绑定,主要通过重载实现

C.模板属于编译时多态,故错误

D.运行时多态是动态绑定,也叫晚期绑定

A.必须是父类的函数设置为虚函数

B.必须通过父类的指针或者引用才可以,子类的不行

C.不是在编译期,而应该在运行期间,编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

D.正确,实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了

A.重写即覆盖,针对多态, 重定义即隐藏, 两者都发生在继承体系中

B.重载只能在一个范围内,不能在不同的类里

C.只有重写要求原型相同

D.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏

E.重写和重定义是两码事,重写即覆盖,针对多态, 重定义即隐藏

F.重写要求函数完全相同,重定义只需函数名相同即可

G.很明显有说法正确的答案

选项 B 是正确的,因为其他选项的描述都有误

A. 如果父类和子类都有相同的方法,参数个数不同,将子类对象赋给父类对象后,采用父类对象调用该同名方法时,实际调用的是子类的方法。

  • 错误:这个描述混淆了重载和覆盖。此处说的"相同的方法"实际上应该是"重载的方法",因为它们参数个数不同。在这种情况下,父类的同名方法(如果不被重写)不会调用子类的方法,而是调用父类的方法。

B. 选项全部都不正确。

  • 正确:由于 A、C、D 选项都有错误,因此 B 是正确的。

C. 重载和多态在 C++ 面向对象编程中经常用到的方法, 都只在实现子类的方法时才会使用。

  • 错误:重载是在同一作用域内对同一函数名的不同参数组合的实现,与子类无关。多态是通过虚函数实现的,通常用于基类和派生类之间。

D.

cpp 复制代码
class A{ 
public: 
    void test(float a) { cout << a; } 
}; 

class B : public A{ 
public: 
    void test(int b) { cout << b; } 
}; 

void main() { 
    A *a = new A; 
    B *b = new B; 
    a = b; 
    a->test(1.1); 
}
  • 错误 :虽然 a 最后指向了 B 的对象,++但++ **<u>a</u>**++是++ **<u>A</u>**++类型的指针++ ,调用 a->test(1.1) 会查找 A 中的 test(float a) 方法。而因为参数类型不匹配,1.1double 类型,编译器会发生类型转换以匹配 float。最终输出是 1.1,而不是 1

A. 基类和子类的f1函数构成重写

  • 错误 :虽然 B 中的 f1 函数是重定义(因为 f1 在基类 A 中没有被声明为虚函数),它并没有构成重写。重写发生在子类重写父类的虚函数,而 f1 并不是虚函数。

B. 基类和子类的f3函数没有构成重写,因为子类f3前没有增加virtual关键字

  • 错误f3 函数确实构成重写,因为在 C++ 中,当子类中的函数与基类的虚函数具有相同的签名时,即使子类不显式地使用 virtual 关键字,也视为重写。f3 在基类中是虚函数,因此 B 中的 f3 是对 A::f3 的重写。

C. 基类引用引用子类对象后,通过基类对象调用f2时,调用的是子类的f2

  • 错误 :这一说法的后半部分是错误的。虽然基类引用可以引用子类对象,并且在通过基类引用调用虚函数时会执行子类的实现(多态),但题目中提到的是通过"基类对象"调用 f2。这意味着如果我们通过 A 的对象调用 f2,那么无论 f2 是否是虚函数,它都会调用 A 的实现,而不会是 B 的实现。因此,基类对象调用时总是调用基类的版本

D. f2和f3都是重写,f1是重定义

  • 正确 :这一说法是正确的。f2 是虚函数,在 B 中被重写;f3 也是重写,尽管没有加 virtual 关键字。f1 是基类中的非虚函数,因此在 B 中的实现是重定义,而不是重写。

在这道题中,我们需要分析关于抽象类和纯虚函数的描述,确定哪个选项是错误的。下面是每个选项的详细解析:

选项分析

A. 纯虚函数的声明以"=0;"结束

  • 正确 :这是纯虚函数的标准语法。在 C++ 中,纯虚函数通过在声明后加上 = 0 来定义,表示该函数没有实现并且在派生类中必须被重写。

B. 有纯虚函数的类叫抽象类,它不能用来定义对象

  • 正确:这个描述是正确的。只要一个类中有一个或多个纯虚函数,这个类就被称为抽象类,抽象类不能直接实例化对象。

C. 抽象类的派生类如果不实现纯虚函数,它也是抽象类

  • 正确 :这个说法也是正确的。++如果派生类未实现基类中的纯虚函数,则派生类也成为抽象类++,不能被实例化。

D. 纯虚函数不能有函数体

  • 错误:这个说法是错误的。虽然纯虚函数通常在基类中不需要实现,但是它可以有函数体。这种情况下,纯虚函数可以在基类中提供默认的实现,派生类可以选择重写这个实现。如下所示:
cpp 复制代码
class Base {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数
    virtual void implementedFunction() {
        // 有函数体的虚函数
    }
};

A.抽象类不能实例化对象,所以以对象返回是错误

B.抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态

C.参数为对象,所以错误

D.直接实例化对象,这是不允许的

A. 一个类只能有一张虚表

  • 错误:当使用多重继承时,可能会为每个基类生成不同的虚表。因此,多个基类可能会导致同一派生类对象有多张虚表。
  • 比如菱形继承:B,C继承A。D继承了B,C。 这就导致了 D 类拥有多张虚表。

B. 基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表

  • 错误 :虽然子类可以使用基类的虚函数,但每个类都有自己的虚表。在这种情况下,子类会有自己的虚表,即使其中的虚函数没有重写。基类和子类的虚表是不同的。

C. 虚表是在运行期间动态生成的

  • 错误 :虚表通常是在编译期间生成的,而不是在运行期间。虚表的结构是静态的,它在编译时确定。

D. 一个类的不同对象共享该类的虚表

  • 正确所有该类的对象共享同一张虚表。这意味着同一类的不同对象在调用虚函数时,都会使用同一张虚表来进行查找。这可以通过代码验证,创建一个类的多个对象并调用虚函数,确保它们都共享同一虚表。
cpp 复制代码
class Base {
public:
    virtual void func() { std::cout << "Base::func()" << std::endl; }
};

int main() {
    Base obj1, obj2;
    // 检查虚表地址
    std::cout << "obj1 virtual table address: " << *(void**)&obj1 << std::endl;
    std::cout << "obj2 virtual table address: " << *(void**)&obj2 << std::endl;
    // 确认共享同一虚表
    if (*(void**)&obj1 == *(void**)&obj2) {
        std::cout << "Both objects share the same virtual table." << std::endl;
    }
    else {
        std::cout << "Objects do not share the same virtual table." << std::endl;
    }
    return 0;
}
cpp 复制代码
7.下面函数输出结果是( )
class A {
public: 
    virtual void f() {
        cout << "A::f()" << endl;
    }
};

class B : public A {
private://注意是 私有的
    virtual void f() {
        cout << "B::f()" << endl;
    }
};

int main() {
    A* pa = (A*)new B; // 强制类型转换,将 B 的对象指针转为 A 的指针
    pa->f(); // 调用虚函数 f
    return 0;
}

A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用

函数的输出结果是 B::f(),原因如下:

多态的工作原理

  1. 虚函数 :类 A 中的 f() 是一个虚函数,意味着可以通过++基类指针调用派生类++ 的实现。即使派生类 Bf() 函数是私有的,编译器在处理虚函数调用时,仍然会查找对象的虚表,找到实际指向的 B 类的 f()
  2. 强制类型转换A* pa = (A*)new B; 这行代码将 B 类的对象强制转换为 A 类的指针。虽然 Bf() 是私有的,但由于 pa 实际上指向的是一个 B 类型的对象,因此在调用 pa->f() 时,程序会根据对象的实际类型(<font style="color:#DF2A3F;">B</font>)来决定调用哪个版本的 f()
  3. 访问权限与多态 :虽然 Bf() 是私有的,但这并不妨碍通过 A 的指针调用它。C++ 的多态机制依赖于虚表(vtable)来查找方法,确保调用的是对象的真实类型的方法,而不是指针类型的方法。因此,尽管 <font style="color:#DF2A3F;">f()</font> 是私有的,<font style="color:#DF2A3F;">pa->f()</font> 调用的依然是 <font style="color:#DF2A3F;">B</font><font style="color:#DF2A3F;">f()</font>

选项分析

  • A. B::f() :正确,因为实际调用的是 B 类的 f() 方法。
  • B. A::f(),因为子类的f()函数是私有的:错误,因为私有性只影响访问权限,而不影响多态调用。
  • C. A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象 :错误,pa 指向的是 B 类型的对象。
  • D. 编译错误,私有的成员函数不能在类外调用:错误,因为访问权限不影响多态的运行时行为。

正确答案是 B. B::x() 。在调用 b.x(); 时,由于 B 类重写了 A 类的虚函数 x(),最终调用的是 B 中的 x() 方法。

cpp 复制代码
以下程序输出结果是( )
class A
{
public:
  A(): m_iVal(0) { test(); }  // 1
  virtual void func() { std::cout << m_iVal << ' '; } // 2
  void test() { func(); } // 3

public:
  int m_iVal;
};

class B : public A
{
public:
  B() { test(); } // 4
  virtual void func() { ++m_iVal; std::cout << m_iVal << ' '; } // 5
};

int main(int argc, char* argv[])
{
  A* p = new B; // 6
  p->test(); // 7
  return 0;
}
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对

详细执行步骤

  1. 对象创建 A* p = new B;
    • 创建 B 对象时,会先调用 A 的构造函数。
  2. 执行 A 的构造函数 A(): m_iVal(0) { test(); }
    • m_iVal 初始化为 0
    • 调用 test(),此时仍在 A 的构造阶段。
    • 注意 :在此阶段,Bfunc() 尚未被调用,因为虚表尚未完成构造。调用的是 Afunc()
  3. **执行 **A::test()
    • A::test() 中调用 func()。由于此时在 A 的构造过程中,虚函数机制并未生效,因此调用的是 A 中的 func()
    • 输出 0(当前 m_iVal 值)。
  4. 返回到 B 的构造函数
    • A 的构造函数完成后,接下来执行 B 的构造函数。
    • B 的构造函数中执行 test()
  5. 执行 B 的构造函数 B() { test(); }
    • 再次调用 test(),此时 B 的虚表已经构建完成,可以正常调用 B 的虚函数。

当在 B 的构造函数中调用 test() 时,实际上是调用了 Atest() 方法。因为:

继承B 继承了 A 的所有公共和保护成员,包括方法 test()。因此,B 类的对象可以直接调用 A 的成员函数。

可见性 :由于 test()A 中的公共成员函数,它在 B 中是可见的。

  1. **执行 **B::test()
    • B::test() 中调用 func(), 由于此时 <font style="color:#DF2A3F;">this</font> 指针指向的是 <font style="color:#DF2A3F;">B</font> 的对象,所以此时会调用 <font style="color:#DF2A3F;">B</font> 中的 <font style="color:#DF2A3F;">func()</font>,因为已经完成了 <font style="color:#DF2A3F;">B</font> 的构造。
    • ++m_iVal; 使 m_iVal0 变为 1,并输出 1
  2. **返回到 main() 中的 **p->test();
    • 再次调用 p->test()(由于 p 是指向 BA 指针,这里也会调用 Btest())。
    • 调用 func(),此时会调用 B 中的 func(),使 m_iVal1 变为 2,并输出 2

最终输出

因此,输出结果依次为:

  • 第一次输出 0(来自 A::func()
  • 第二次输出 1(来自 B::func(),当 B 构造时)
  • 第三次输出 2(来自 B::func(),在 main() 中调用时)

最终的输出是 0 1 2,因此选择 C 作为答案。

A.父类对象和子类对象的前4字节都是虚表地址

B.A类对象和B类对象前4个字节存储的都是虚表的地址,只是各自指向各自的虚表。选B.

C.不相同,各自有各自的虚表

D.A类和B类不是同一类内容不同

问题背景

假设有以下类的继承结构:

  • B1B2 是基类,都包含虚函数。
  • D 类继承 B1B2,并且对 B1B2 的虚函数进行了重写,同时还增加了新的虚函数。

虚表的概念

  • 虚表是由类的虚函数组成的数据结构,每个包含虚函数的类都有自己的虚表。
  • 虚表指针是指向虚表的指针,通常保存在每个对象的内部。每个对象的虚表指针指向它的虚表。

选项解析

A. D类对象模型中包含了3个虚表指针

  • 错误 :在多重继承中,子类(这里是 D)只有++自己的++ 虚表和++继承++ 自每个父类++的++ 虚表指针 。由于 D 只继承了两个基类(B1B2),因此只会有两个虚表指针。即使 D 重写了 B1B2 的虚函数,也不会增加虚表的数量。因此,D 不会包含三个虚表指针。

B. D类对象有两个虚表,D类新增加的虚函数放在第一张****虚表最后

  • 正确D 类对象确实只包含两个虚表,一个是来自 B1,一个是来自 B2D 自己的新虚函数会被添加到第一个父类(B1)的虚表中,因为 D 在调用这些函数时只会通过其第一个父类的虚表进行解析。其新添加的虚函数被放置在 B1 的虚表的最后一项。

C. D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后

  • 错误 :新增加的虚函数只会放在 B1 的虚表中,而不是放在 B2 的虚表中。虽然 D 对两个基类的虚函数进行了重写,但新的虚函数不需要被放入第二个虚表中,因此这一选项是错误的。

D. 以上全部错误

  • 错误:如上所述,选项 B 是正确的,因此此选项也不成立。

  1. 📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
  2. 本人也很想知道这些错误,恳望读者批评指正!
  3. 我是:勇敢滴勇~感谢大家的支持!
相关推荐
luoluoal1 分钟前
基于Spring Boot的装饰工程管理系统源码(springboot)
java·spring boot·后端
yy_xzz15 分钟前
QLineEdit 控件在设置了 QDoubleValidator 之后无法正确输入小数
c++·qt
J不A秃V头A16 分钟前
IDEA实用小技巧:方法之间的优雅分割线
java·intellij-idea
涛涛6号27 分钟前
PageHelper(springboot,mybatis)
java·spring boot·后端
ya888g30 分钟前
信息学奥赛复赛复习19-CSP-J2023-02公路-贪心算法、向上取整、向下取整
c++·算法
夜雨翦春韭39 分钟前
【代码随想录Day58】图论Part09
java·开发语言·数据结构·算法·leetcode·图论
#Y清墨1 小时前
STL映射
数据结构·c++
豪宇刘1 小时前
Shiro回话管理和加密
java·后端·spring
V+zmm101341 小时前
警务辅助人员管理系统小程序ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·课程设计·1024程序员节
无职转生真好看1 小时前
数据结构:利用队列的基本操作,模拟病人到医院排队看病过程。
数据结构·c++·算法