【C++】继续和多态常见的面试标题
继续笔试面试题
[*]什么是菱形继续?菱形继续的标题是什么?
[*]什么是菱形假造继续?怎样解决数据冗余和二义性的
[*]继续和组合的区别?什么时间用继续?什么时间用组合?
1. 什么是菱形继续?菱形继续的标题是什么?
菱形继续指的是一个类通过两个子类继续了同一个基类,这两个子类再被另一个派生类继续,形成菱形结构。比方:
class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};
在这个结构中,D 通过 B 和 C 间接继续了两次 A,这就形成了菱形继续。
菱形继续的标题紧张有两个:
[*]数据冗余:由于 D 通过 B 和 C 继续了两份 A,导致存在两份类似的 A 成员。这在内存中会引起冗余,浪费空间。
[*]二义性标题:当在 D 中试图访问 A 的成员时,编译器无法确定该访问来自 B 继续的 A 还是 C 继续的 A,从而导致二义性。比方:
D d;
d._a;// 编译器不确定是访问 B::_a 还是 C::_a
2. 什么是菱形假造继续?怎样解决数据冗余和二义性?
菱形假造继续是为相识决菱形继续中的数据冗余和二义性标题的一种机制。通过在继续时使用 virtual 关键字,编译器确保只会有一个 A 的实例被共享,而不会有两个冗余的 A 实例。比方:
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 的成员时,不会再出现二义性。比方:
D d;
d._a;// 正常访问唯一的 A 实例
假造继续通过虚基表指针实现,B 和 C 各自保存一个指针,指向一个共享的 A 实例,这样就避免了冗余的 A。
3. 继续和组合的区别?什么时间用继续?什么时间用组合?
继续和组合都是用来复用代码和实现类之间关系的两种本领,但它们的适用场景和概念有明显的区别:
[*]继续(Inheritance):
[*]形貌的是“is-a”关系:如果类 B 继续类 A,意味着 B 是 A 的一种特别范例(好比 Cat 是一种 Animal)。
[*]特点:
[*]子类自动继续父类的全部属性和行为。
[*]继续是一种强耦合,子类的行为和父类行为紧密关联。
[*]一旦父类发生变化,子类也会受到影响。
[*]适用场景:
[*]当子类是父类的一种范例时,使用继续是合适的。比方:Car 继续 Vehicle。
[*]得当复用父类的方法和属性,同时答应子类对父类的行为进行扩展或重写。
[*]组合(Composition):
[*]形貌的是“has-a”关系:如果类 B 包罗了类 A 的对象,意味着 B 拥有 A 的功能或行为,但 B 并不是 A 的一种范例。比方,Car 包罗了一个 Engine,但 Car 不是一种 Engine。
[*]特点:
[*]组合是弱耦合,每个类保持独立性。
[*]可以动态地替换或改变组合类的行为,不需要修改组合类本身的代码。
[*]适用场景:
[*]当一个类的功能可以通过另一类来实现,但它们之间不是范例的关系时,使用组合。
[*]当需要在运行时组合对象行为或功能,而不希望由于继续导致复杂的耦合关系。
总结:
[*]继续用于表达“B 是 A”的关系(is-a)。
[*]组合用于表达“B 拥有 A”的关系(has-a)。
[*]当两个类之间有明显的条理关系时,继续是得当的;当一个类需要复用另一个类的功能时,但没有“范例”关系时,使用组合。
选择题
[*]
https://img-blog.csdnimg.cn/img_convert/21488aca799b9823dca97bb35214ea7a.png
A.可以存在,如函数重载
B.基类与子类函数名字类似,参数差别,形成的是隐蔽
C.可以共存
D.成员函数在同一个类内里同名,此时构成了重载,但变量肯定不能同名,故正确
[*]
https://img-blog.csdnimg.cn/img_convert/60c1e85b1816921d7a61c0100122b8dd.png
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 是最准确的选择。
[*]
https://img-blog.csdnimg.cn/img_convert/009f1f7420dec31621e28b0a4ae3c79c.png
A. 错误
[*]解释:如果基类有默认构造函数,派生类的构造函数并不需要显式调用基类的构造函数。只有在基类没有默认构造函数,或需要传递参数给基类构造函数时,派生类才需要在初始化列表中显式调用基类的构造函数。因此,A 是不正确的。
B. 错误
[*]解释:派生类的构造函数首先会调用 基类 的构造函数来初始化基类部分成员,之后才会初始化派生类的成员。因此,初始化的顺序是 先基类,再子类,B 的表述是反的。
C. 错误
[*]解释:派生类的析构函数会自动调用基类的析构函数,并按照 构造顺序的逆序 进行析构,即先析构派生类,再析构基类。因此,C 是不正确的。
D. 正确
[*]解释:在界说派生类的构造函数时,确实有时间需要参考基类的构造函数,特别是当基类没有默认构造函数,或者需要特定参数来构造基类时。在这种情况下,派生类的构造函数必须通过初始化列表显式调用基类的构造函数。因此,D 是正确的。
综上所述,答案 D 是正确的。
[*]
https://img-blog.csdnimg.cn/img_convert/2abe924cdaee68a199358242b4463bff.png
子类实例化对象,由于继续的有父类。所以会先构造父类,然后在构造子类,析构顺序完全按照构造的相反顺序进行析构,故答案为 C
[*]
https://img-blog.csdnimg.cn/img_convert/6270ca916903bf55cb97f18e2a11e0cc.png
A.先构造父类,在构造子类 故正确
B.不肯定,如果父类有默认构造函数就不需要
C.刚好相反,先调用子类,在调用父类
D.派生类的析构函数每每还需要连同父类析构函数一起调用,同时清除父类的资源
[*]
https://img-blog.csdnimg.cn/img_convert/93f309cba92c121882d8d754b8346b2b.png
A.静态成员函数也可以被继续
B.成员变量全部的都会被继续,无论公有私有
C.友元函数不能被继续,相当于你爹的朋侪不肯定是你的朋侪
D.静态成员属于整个类,不属于任何对象,所以在整体体系中只有一份
[*]
https://img-blog.csdnimg.cn/img_convert/3291e27c14debc31977cfb88cd68602d.png
A.静态变量就不被包罗
B.同理,静态变量就不被包罗
C.父类全部成员都要被继续,因此包罗了
D.静态成员肯定是不被包罗在对象中的
E.很显然,以上说法都不正确
区分静态成员和静态变量
[*]
https://img-blog.csdnimg.cn/img_convert/0c9f3979263cc2986e7aeaa78e88c5e9.png
答案是 B,原因如下:
A. 正确
[*]解释:基类指针可以直接指向子类对象,这叫做 向上转型(upcasting)。由于子类是基类的扩展,它包罗了基类的全部部分,所以基类指针可以指向子类对象。这种操纵是合法的且常见。
B. 错误
[*]解释:基类对象不能直接**赋值**给子类对象。由于基类对象没有子类特有的成员和方法,直接赋值会丢失子类中的额外信息或导致范例不匹配,因此不答应这种操纵。这叫做 向下转型(downcasting),需要显式的范例转换,而且在某些情况下需要范例查抄(如在C++中使用 dynamic_cast)。
C. 正确
[*]解释:子类对象的引用不能引用基类的对象。这是由于基类对象不包罗子类的特有成员或行为,因此无法用子类的引用来指向基类对象。这种操纵黑白法的。
D. 正确
[*]解释:子类对象可以直接赋值给基类对象,这也是 向上转型。这种情况下,赋值操纵会切割掉子类对象中特有的部分,只保留基类部分。这在赋值时会发生“对象切割”标题。
[*]
https://img-blog.csdnimg.cn/img_convert/9d77b3dda365ad415b401aa5423be36a.png
分析:p1和p2虽然都是其父类,但在子类内存模子中,其位置差别,所以p1和p2所指子类的位置也不类似,因此p1!=p2,
由于p1对象是第一个被继续的父类范例,全部其地址与子类对象的地址p3所指位置都为子类对象的起始位置,因此p1==p3,所以C正确
[*]
https://img-blog.csdnimg.cn/img_convert/dbc6b84bb75ce9aa4295a630f9db2a8f.png
答案是 C,详细剖析如下:
A. 正确
[*]解释:在这段代码中,class D 继续了两个 B,分别从 C1 和 C2 继续。因此,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. 正确
[*]解释:由于 C1 和 C2 都继续了 B,而且没有使用假造继续,所以 D 中实际上有两个 B 的实例,分别存在于 C1 和 C2 中。因此,B 是正确的。
C. 错误
[*]解释:由于 D 类中存在两份 B,分别继续自 C1 和 C2,因此 D 对象不能直接访问 b 成员。直接访问 b 会产生二义性:b 是来自 C1 继续的 B 还是 C2 继续的 B?为了消除二义性,必须通过指定路径来访问,比方 d.C1::b 或 d.C2::b。
因此,C 是不正确的。
D. 正确
[*]解释:菱形继续的确会带来二义性标题,由于子类会从两个路径继续同一个基类。在这种情况下,可以使用 假造继续 来避免多个基类实例的标题。因此,D 是正确的。
假如 C1,C2都假造继续了B 那么本题的每个选项解释如下:
如果 C1 和 C2 都假造继续了 B,那么菱形继续的结构会发生变化。通过假造继续,**B**** 类只会在 **D** 类中存在一份实例**,解决了二义性标题。基于这个前提,我们可以重新分析每个选项。
先来看类的界说,假设 C1 和 C2 现在假造继续了 B:
A. D 统共占了 20 个字节
[*] 解释:假造继续改变了继续的结构。假造继续确保 B 类只存在一份实例,这样可以避免多次继续 B 的标题。
由于假造继续可能会带来额外的指针(用来指向假造基类实例),详细的内存结构依靠于编译器实现,因此在某些编译器中假造继续可能会导致额外的字节开销,终极结果未必是 20 字节。不外,假设编译器管理良好且没有其他额外开销,该选项可能是正确的,但在实际编译器中每每会比 20 字节大。A 可能正确,但不肯定是完全准确的。
[*]B 类的大小为 4 字节(假设 int 为 4 字节)。
[*]C1 和 C2 类通过假造继续继续了 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 对象中存储了两份
[*]解释:由于 C1 和 C2 都采用了假造继续,B 类在 D 类中只存在一份实例。因此,B 是错误的。
C. D 对象可以直接访问从基类继续的 b 成员
[*]解释:在使用假造继续的情况下,D 类中 B 的实例只存在一份,因此不再存在二义性标题。D 对象可以直接访问从 B 继续的 b 成员,无需显式地指定路径。因此 C 是正确的。
D. 菱形继续存在二义性标题,尽量避免设计菱形继续
[*]解释:通过假造继续,菱形继续的二义性标题得到了很好的解决。虽然菱形继续仍然可能引发复杂的设计标题,但在这种情况下,二义性不再存在。因此 D 是错误的,由于假造继续已经解决了二义性。
[*]
多态
概念考察
[*]下面哪种面向对象的方法可以让你变得富有( A )
A: 继续 B: 封装 C: 多态 D: 抽象
[*]解释:这是一道幽默的选择题,实际上并没有哪种面向对象方法可以直接让你变得“富有”。不外这里的“富有”是双关语,隐喻了“继续”在实际编程中可以让你复用父类的属性和方法,就像生存中的“继续财富”一样。通过继续,你可以得到父类的功能,而不需要重新实现,因此继续“让你变得富有”是一个双关的比喻。
[*](D)是面向对象程序设计语言中的一种机制。这种机制实现了方法的界说与详细的对象无关,
而对方法的调用则可以关联于详细的对象。
A: 继续 B: 模板 C: 对象的自身引用 D: 动态绑定
解释:动态绑定(Dynamic Binding)是一种多态机制,它答应在程序运行时根据实际对象的范例来调用相应的方法。在编译时,程序并不确定将调用哪个方法,而是在运行时决定。因此,这使得代码更加灵活和可扩展。动态绑定是实现多态的核心机制。
其他选项的解释:
[*]A: 继续:继续答应子类从父类继续属性和方法,不能直接实现动态绑定。
[*]B: 模板:模板是C++中的一种泛型编程机制,答应在编译时生成代码,并不涉及动态绑定。
[*]C: 对象的自身引用:这是对象通过this指针访问自身成员的机制,与动态绑定无关。
[*]面向对象设计中的继续和组合,下面说法错误的是?(C)
A:继续答应我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时间才确定的,是一种动
态复用,也称为黑盒复用
C:优先使用继续,而不是组合,是面向对象设计的第二原则
D:继续可以使子类能自动继续父类的接口,但在设计模式中以为这是一种破坏了父类的封
装性的表现
解释:这道题考察的是继续与组合的区别,错误的选项是C。实际上,组合优先于继续 是面向对象设计中的一条紧张原则,特别是在设计模式中被称为“组合优于继续原则”(Favor composition over inheritance)。太过使用继续会导致代码耦合性增强、灵活性降低。
其他选项的解释:
[*]A:继续答应我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用:正确。继续中的复用被称为“白盒复用”,由于子类能够直接看到父类的实现细节。
[*]B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时间才确定的,是一种动态复用,也称为黑盒复用:正确。组合是一种“黑盒复用”,对象间的关系可以在运行时创建,而不需要相识彼此的内部实现。
[*]D:继续可以使子类能自动继续父类的接口,但在设计模式中以为这是一种破坏了父类的封装性的表现:正确。继续虽然提供了接口复用,但有时会导致父类的封装性被破坏,由于子类可以依靠父类的内部实现。
[*]以下关于纯虚函数的说法,正确的是(A )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
解释:声明白纯虚函数的类被称为抽象类,不能直接实例化对象。抽象类的目的是提供一个接口,并由派生类实现该接口的纯虚函数。
其他选项的解释:
[*]B:声明纯虚函数的类是虚基类:错误。纯虚函数与虚基类无关,虚基类用于解决多重继续标题。
[*]C:子类必须实现基类的纯虚函数:不完全正确。子类可以选择不实现纯虚函数,但这样它本身也会变成抽象类。
[*]D:纯虚函数必须是空函数:错误。纯虚函数只是声明没有实现,但可以有实现,只不外声明部分是= 0。
[*]关于虚函数的形貌正确的是(D—>B )
A:派生类的虚函数与基类的虚函数具有差别的参数个数和范例 B:内联函数不能是虚函数
C:派生类必须重新界说基类的虚函数 D:虚函数可以是一个static型的函数
解释:虚函数的多态机制是在运行时实现的,而内联函数是在编译时展开的。这两者是相互辩论的,因此虚函数不能是内联函数。
其他选项的解释:
- **A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型**:错误。虚函数的重写要求派生类中的虚函数与基类的虚函数有相同的参数列表。
- **C:派生类必须重新定义基类的虚函数**:错误。派生类可以选择重写基类的虚函数,<u>但不一定必须重新定义。</u>
- **D:虚函数可以是一个static型的函数**:错误。<u>虚函数是与对象实例相关的,而静态函数与类相关,虚函数不能是静态函数。</u>
[*]关于虚表说法正确的是( C—>D )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的差别对象共享该类的虚表
解释:
[*]D:一个类的差别对象共享该类的虚表:正确。虚表(vtable)是针对类生成的,而不是针对每个对象生成的,因此同一个类的全部对象共享同一张虚表。对象中的虚表指针(vptr)指向这张虚表。
其他选项的解释:
[*]A:一个类只能有一张虚表:错误。如果一个类继续了多个具有虚函数的类,或者自身界说了多个虚函数,可能会有多张虚表存在。
[*]B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表:错误。即使子类没有重写虚函数,它依然有本身的虚表,虽然可能会指向类似的虚函数。
[*]C:虚表是在运行期间动态生成的:错误。虚表是在编译期生成的,而不是在运行时生成。虚表指针会在运行时动态调解,以指向正确的虚表
[*]假设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类有本身独立的虚表,因此它们的虚表地址差别。
[*]下面程序输出结果是什么? (A)
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
解释:
[*]虚继续会确保在派生类中共享一个唯一的基类实例。因此,当 B 和 C 虚继续了 A 时,在派生类 D 中,A 类的构造函数只会被调用一次。
[*]其他就是按顺序来就行了
[*]多继续中指针偏移标题?下面说法正确的是( C )
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
解释:
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。
[*]以下程序输出结果是什么(B)
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(); }
[*]test 是 A 类的虚函数,而且 B 没有重写它。因此,当你调用 p->test() 时,它实际上实验的是 A::test()。
[*]**A::test()**** 中调用了 **func()**,而 **func()** 是虚函数**。虚函数的行为是动态绑定的,意味着根据对象的实际范例来调用合适的函数。在这种情况下,p 是 B* 范例,所以 func() 会调用 B::func()。
[*]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()。
调用流程:
[*]p->test() 实际调用的是 A::test()。
[*]A::test() 中调用了虚函数 func()。由于 p 指向 B 类对象,动态绑定会让 B::func() 被调用。
[*]虽然调用的是 B::func(),但 A::test() 使用的是 A 的默认参数 val = 1,由于默认参数是在编译时绑定的。
输出结果:
[*]虽然 B::func(int val = 0) 具有默认参数 0,但由于 A::test() 调用了 func() 并使用了 A 类的默认参数 1,因此程序输出的是:B->1
问答题
作为面试者,我将依次回答上列面试题:
1. 什么是多态?
多态是面向对象编程中的一种特性,它答应同一个函数或方法在差别对象上具有差别的行为。在 C++ 中,多态紧张有两种情势:
[*]编译时多态(静态多态):通过函数重载和运算符重载实现。
[*]运行时多态(动态多态):通过继续和虚函数机制实现。当基类的指针或引用指向派生类对象时,调用虚函数会根据实际对象的范例实验差别的实现。
多态的核心目的是进步代码的可扩展性和复用性。
2. 什么是重载、重写(覆盖)、重界说(隐蔽)?
[*]重载(Overloading):是同一个作用域内,答应函数同名但参数范例或参数个数差别的征象。编译器通过参数列表的差别进行区分,这是静态多态的一种情势。
[*]例子:
void func(int a);
void func(double a);
[*]重写(Overriding):也称为覆盖,是派生类重新实现基类中的虚函数,必须保持函数签名完全类似。通过这种方式,基类的指针或引用在运行时调用派生类的实现,这是动态多态的实现方式。
[*]例子:
class Base {
public:
virtual void func() { cout << "Base"; }
};
class Derived : public Base {
public:
void func() override { cout << "Derived"; }
};
[*]重界说(Hiding):是派生类中的同名函数隐蔽了基类中的非虚函数或静态成员函数。尽管函数签名差别,基类函数不再可见。可以通过作用域分辨符 Base::func() 访问基类函数。
[*]例子:
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(虚函数表)来实现动态绑定,而静态成员函数无法访问虚函数表。因此,静态成员函数不能是虚函数。
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. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,而且在多态场景下,基类的析构函数最好界说为虚函数。
[*]如果一个类可能作为基类被继续,而且会通过基类指针或引用指向派生类对象,在这种情况下,基类的析构函数需要是虚函数。否则,当通过基类指针删除派生类对象时,只有基类的析构函数会被调用,而派生类的析构函数不会被实验,从而导致资源泄漏。
场景:
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;
}
输出:
Derived destructor called
Base destructor called
如果基类的析构函数不是虚函数,Derived 类的析构函数将不会被调用,造成内存泄漏或其他资源开释标题。
8. 对象访问普通函数快还是虚函数更快?
答:普通函数更快。
[*]如果是通过普通对象调用函数,普通函数和虚函数的访问速度是一样的。
[*]但在通过指针或引用调用时,普通函数更快。调用虚函数需要动态绑定,这意味着编译时不能确定详细要调用哪个函数,必须通过查找虚函数表(vtable)来找到对应的函数指针,因此虚函数调用会略慢一些。普通函数则在编译时就能确定,不需要查表,直接调用。
例子:
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++ 中的菱形继续是一种特别的多继续结构,它是指一个类从两个基类继续,而这两个基类又继续自同一个父类。菱形继续引发了两个紧张标题:
[*]数据冗余标题:
当派生类通过多条路径继续同一个基类时,基类的成员变量会在派生类中出现多份副本。这意味着,派生类对象中会有多份类似的基类成员,导致内存浪费和逻辑混乱。
[*]二义性标题:
由于派生类通过多个路径继续基类,编译器可能无法确定调用哪个基类的成员函数,尤其是当基类有同名成员时。比方,编译器会碰到二义性:调用 A::func() 时是从 B 继续的 A 版本,还是从 C 继续的 A 版本。
例子:
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 关键字,表现对该基类进行虚继续。
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
[*]虚基表:虚继续的实现依靠于虚基表(Virtual Base Table,简称 VBT)。虚基表存储了虚基类成员在派生类中的偏移量。当派生类访问虚基类的成员时,会通过虚基表找到正确的偏移,从而解决了菱形继续中的二义性标题。虚基表的工作原理是:
[*]当类进行虚继续时,派生类会为虚基类保留一份特别的偏移量表,即虚基表。每个虚继续的类都会有指向虚基表的指针。
[*]在访问虚基类的成员时,编译器会通过该指针找到虚基表,进而盘算虚基类成员的实际地址,避免产生冗余的副本。
虚继续的利益:
[*]只保留一份基类成员,节省内存,避免数据冗余。
[*]消除了访问基类成员的二义性,解决了编译时的歧义。
11. 什么是抽象类?抽象类的作用?
抽象类界说:
抽象类是包罗纯虚函数的类。纯虚函数是一个没有实现的虚函数,其界说如下:
virtual void func() = 0;
任何包罗至少一个纯虚函数的类都称为抽象类。抽象类不能被实例化,必须通过派生类来实现此中的纯虚函数。
抽象类的作用:
[*]逼迫子类实现特定功能:
抽象类通过界说纯虚函数,逼迫派生类必须提供这些函数的详细实现。这种设计确保了某些行为在派生类中肯定会被实现。
[*]提供接口继续:
抽象类体现了接口继续的概念,即抽象类界说了一组功能的接口,而派生类实现详细功能。通过这种方式,可以实现面向接口编程,淘汰对详细实现的依靠,从而进步代码的扩展性和可维护性。
[*]实现多态:
抽象类是实现多态的紧张本领。使用抽象类的指针或引用,可以在运行时通过动态绑定调用派生类的详细实现。
例子:
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;
}
抽象类通过纯虚函数为派生类提供了同一的接口,同时也使得派生类必须实现这些接口,从而包管了多态的实现。
选择题
[*]
https://img-blog.csdnimg.cn/img_convert/f6e8f6e43c3a3a3b2c80268e41bb16a6.png
选项 A. 被 virtual 修饰的函数称为虚函数
[*]解释:此说法基本正确,但需要更精确地表述。被 virtual 关键字修饰的**成员函数才称为虚函数,用于支持动态绑定和多态。详细来说,virtual 关键字用于标记基类中的成员函数,使得在派生类中可以通过基类指针或引用调用派生类的重写版本(动态绑定)。所以,准确的表述应为“被 virtual 修饰的成员函数**称为虚函数”。因此,A 是不完全准确的。
选项 B. 虚函数的作用是用来实现多态
[*]解释:这句话是正确的。虚函数的紧张作用就是支持 运行时多态。通过虚函数,C++ 实现了动态绑定,使得程序在运行时能够根据实际对象范例来调用正确的函数版本,而不是仅仅根据编译时的范例。这样可以实现“一个接口,多种实现”的设计模式。
因此,虚函数是实现运行时多态的关键机制。B 是正确的。
- **静态绑定**:编译时决定调用哪个函数,适用于非虚函数。
- **动态绑定**:运行时根据对象的实际类型决定调用哪个函数,适用于虚函数。
选项 C. 虚函数在类中声明和类外界说时,都必须加 virtual 关键字
[*]解释:这是 错误的。virtual 关键字只需要在函数声明时添加,在类外界说时不需要重复添加 virtual 关键字。编译器通过函数声明中的 virtual 关键字来知道该函数是虚函数,后续的界说部分不需要再次声明。比方:
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`。虚函数必须通过对象来调用,因为虚函数的行为依赖于对象的动态类型,而静态成员函数与类绑定,不依赖于对象,所以这两个关键字是互相矛盾的。
[*]
https://img-blog.csdnimg.cn/img_convert/e2d4cedf321f4c8ed9fc736cf39e5209.png
A.友元函数不属于成员函数,不能成为虚函数
B.静态成员函数就不能设置为虚函数
C.静态成员函数与详细对象无关,属于整个类,核心关键是没有隐蔽的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
D.尤其是父类的析构函数强力发起设置为虚函数,这样动态开释父类指针所指的子类对象时,能够达到析构的多态
[*]
https://img-blog.csdnimg.cn/img_convert/2d1707e7e5c4933ca87678cd8c167a6a.png
A.多态分为编译时多态和运行时多态,也叫早期绑定和晚期绑定
B.编译时多态是早期绑定,紧张通过重载实现
C.模板属于编译时多态,故错误
D.运行时多态是动态绑定,也叫晚期绑定
[*]
https://img-blog.csdnimg.cn/img_convert/4d309a686e435c6f0c84c47b5bc5b4d1.png
A.必须是父类的函数设置为虚函数
B.必须通过父类的指针或者引用才可以,子类的不行
C.不是在编译期,而应该在运行期间,编译期间,编译器紧张检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道详细指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
D.正确,实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了
[*]
https://img-blog.csdnimg.cn/img_convert/704f9ffa9e6628ca12cc2da21a3e0344.png
A.重写即覆盖,针对多态, 重界说即隐蔽, 两者都发生在继续体系中
B.重载只能在一个范围内,不能在差别的类里
C.只有重写要求原型类似
D.重写和重界说是两码事,重写即覆盖,针对多态, 重界说即隐蔽
E.重写和重界说是两码事,重写即覆盖,针对多态, 重界说即隐蔽
F.重写要求函数完全类似,重界说只需函数名类似即可
G.很明显有说法正确的答案
[*]
https://img-blog.csdnimg.cn/img_convert/51d2c54bb5095136fe1d92cb1b0a2218.png
选项 B 是正确的,由于其他选项的形貌都有误
A. 如果父类和子类都有类似的方法,参数个数差别,将子类对象赋给父类对象后,采用父类对象调用该同名方法时,实际调用的是子类的方法。
[*]错误:这个形貌肴杂了重载和覆盖。此处说的“类似的方法”实际上应该是“重载的方法”,由于它们参数个数差别。在这种情况下,父类的同名方法(如果不被重写)不会调用子类的方法,而是调用父类的方法。
B. 选项全部都不正确。
[*]正确:由于 A、C、D 选项都有错误,因此 B 是正确的。
C. 重载和多态在 C++ 面向对象编程中常常用到的方法, 都只在实现子类的方法时才会使用。
[*]错误:重载是在同一作用域内对同一函数名的差别参数组合的实现,与子类无关。多态是通过虚函数实现的,通常用于基类和派生类之间。
D.
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.1 是 double 范例,编译器会发生范例转换以匹配 float。终极输出是 1.1,而不是 1。
[*]
https://img-blog.csdnimg.cn/img_convert/8c311507461a9325882f1413adfaf87f.png
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 中的实现是重界说,而不是重写。
[*]
https://img-blog.csdnimg.cn/img_convert/76d465833350e4291831543f5c58b1a9.png
在这道题中,我们需要分析关于抽象类和纯虚函数的形貌,确定哪个选项是错误的。下面是每个选项的详细剖析:
选项分析
A. 纯虚函数的声明以“=0;”结束
[*]正确:这是纯虚函数的尺度语法。在 C++ 中,纯虚函数通过在声明后加上 = 0 来界说,表现该函数没有实现而且在派生类中必须被重写。
B. 有纯虚函数的类叫抽象类,它不能用来界说对象
[*]正确:这个形貌是正确的。只要一个类中有一个或多个纯虚函数,这个类就被称为抽象类,抽象类不能直接实例化对象。
C. 抽象类的派生类如果不实现纯虚函数,它也是抽象类
[*]正确:这个说法也是正确的。如果派生类未实现基类中的纯虚函数,则派生类也成为抽象类,不能被实例化。
D. 纯虚函数不能有函数体
[*]错误:这个说法是错误的。虽然纯虚函数通常在基类中不需要实现,但是它可以有函数体。这种情况下,纯虚函数可以在基类中提供默认的实现,派生类可以选择重写这个实现。如下所示:
class Base {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
virtual void implementedFunction() {
// 有函数体的虚函数
}
};
[*]
https://img-blog.csdnimg.cn/img_convert/54594000b4570d9a21a257e2429009cb.png
A.抽象类不能实例化对象,所以以对象返回是错误
B.抽象类可以界说指针,而且常常这样做,其目的就是用父类指针指向子类从而实现多态
C.参数为对象,所以错误
D.直接实例化对象,这是不答应的
[*]
https://img-blog.csdnimg.cn/img_convert/1236d7caedf8e19e703703eee16650f0.png
A. 一个类只能有一张虚表
[*]错误:当使用多重继续时,可能会为每个基类生成差别的虚表。因此,多个基类可能会导致同一派生类对象有多张虚表。
[*]好比菱形继续:B,C继续A。D继续了B,C。 这就导致了 D 类拥有多张虚表。
B. 基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
[*]错误:虽然子类可以使用基类的虚函数,但每个类都有本身的虚表。在这种情况下,子类会有本身的虚表,即使此中的虚函数没有重写。基类和子类的虚表是差别的。
C. 虚表是在运行期间动态生成的
[*]错误:虚表通常是在编译期间生成的,而不是在运行期间。虚表的结构是静态的,它在编译时确定。
D. 一个类的差别对象共享该类的虚表
[*]正确:全部该类的对象共享同一张虚表。这意味着同一类的差别对象在调用虚函数时,都会使用同一张虚表来进行查找。这可以通过代码验证,创建一个类的多个对象并调用虚函数,确保它们都共享同一虚表。
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;
}
[*]
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(),原因如下:
多态的工作原理
[*]虚函数:类 A 中的 f() 是一个虚函数,意味着可以通过基类指针调用派生类的实现。即使派生类 B 的 f() 函数是私有的,编译器在处理虚函数调用时,仍然会查找对象的虚表,找到实际指向的 B 类的 f()。
[*]逼迫范例转换:A* pa = (A*)new B; 这行代码将 B 类的对象逼迫转换为 A 类的指针。虽然 B 的 f() 是私有的,但由于 pa 实际上指向的是一个 B 范例的对象,因此在调用 pa->f() 时,程序会根据对象的实际范例(<font style="color:#DF2A3F;">B</font>)来决定调用哪个版本的 f()。
[*]访问权限与多态:虽然 B 的 f() 是私有的,但这并不妨碍通过 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. 编译错误,私有的成员函数不能在类外调用:错误,由于访问权限不影响多态的运行时行为。
[*]
https://img-blog.csdnimg.cn/img_convert/6e469b680f72ba33431f10344a376c6d.png
正确答案是 B. B::x()。在调用 b.x(); 时,由于 B 类重写了 A 类的虚函数 x(),终极调用的是 B 中的 x() 方法。
[*]
以下程序输出结果是( )
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. 以上都不对
详细实验步调
[*]对象创建 A* p = new B;:
[*]创建 B 对象时,会先调用 A 的构造函数。
[*]实验 A 的构造函数 A(): m_iVal(0) { test(); }:
[*]将 m_iVal 初始化为 0。
[*]调用 test(),此时仍在 A 的构造阶段。
[*]注意:在此阶段,B 的 func() 尚未被调用,由于虚表尚未完成构造。调用的是 A 的 func()。
[*]**实验 **A::test():
[*]在 A::test() 中调用 func()。由于此时在 A 的构造过程中,虚函数机制并未见效,因此调用的是 A 中的 func()。
[*]输出 0(当前 m_iVal 值)。
[*]返回到 B 的构造函数:
[*]A 的构造函数完成后,接下来实验 B 的构造函数。
[*]在 B 的构造函数中实验 test()。
[*]实验 B 的构造函数 B() { test(); }:
[*]再次调用 test(),此时 B 的虚表已经构建完成,可以正常调用 B 的虚函数。
当在 B 的构造函数中调用 test() 时,实际上是调用了 A 的 test() 方法。由于:
继续:B 继续了 A 的全部公共和保护成员,包括方法 test()。因此,B 类的对象可以直接调用 A 的成员函数。
可见性:由于 test() 是 A 中的公共成员函数,它在 B 中是可见的。
[*]**实验 **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_iVal 从 0 变为 1,并输出 1。
[*]**返回到 main() 中的 **p->test();:
[*]再次调用 p->test()(由于 p 是指向 B 的 A 指针,这里也会调用 B 的 test())。
[*]调用 func(),此时会调用 B 中的 func(),使 m_iVal 从 1 变为 2,并输出 2。
终极输出
因此,输出结果依次为:
[*]第一次输出 0(来自 A::func())
[*]第二次输出 1(来自 B::func(),当 B 构造时)
[*]第三次输出 2(来自 B::func(),在 main() 中调用时)
终极的输出是 0 1 2,因此选择 C 作为答案。
[*]
https://img-blog.csdnimg.cn/img_convert/ce065f6578f474bbe011e963f078b5cc.png
A.父类对象和子类对象的前4字节都是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚表的地址,只是各自指向各自的虚表。选B.
C.不类似,各自有各自的虚表
D.A类和B类不是同一类内容差别
[*]
https://img-blog.csdnimg.cn/img_convert/581e15a78ebce29f5f6aa8c7b0de5dee.png
标题背景
假设有以下类的继续结构:
[*]B1 和 B2 是基类,都包罗虚函数。
[*]D 类继续 B1 和 B2,而且对 B1 和 B2 的虚函数进行了重写,同时还增加了新的虚函数。
虚表的概念
[*]虚表是由类的虚函数组成的数据结构,每个包罗虚函数的类都有本身的虚表。
[*]虚表指针是指向虚表的指针,通常保存在每个对象的内部。每个对象的虚表指针指向它的虚表。
选项剖析
A. D类对象模子中包罗了3个虚表指针
[*]错误:在多重继续中,子类(这里是 D)只有本身的虚表和继续自每个父类的虚表指针。由于 D 只继续了两个基类(B1 和 B2),因此只会有两个虚表指针。即使 D 重写了 B1 和 B2 的虚函数,也不会增加虚表的数量。因此,D 不会包罗三个虚表指针。
B. D类对象有两个虚表,D类新增加的虚函数放在第一张****虚表最后
[*]正确:D 类对象确实只包罗两个虚表,一个是来自 B1,一个是来自 B2。D 本身的新虚函数会被添加到第一个父类(B1)的虚表中,由于 D 在调用这些函数时只会通过其第一个父类的虚表进行剖析。其新添加的虚函数被放置在 B1 的虚表的最后一项。
C. D类对象有两个虚表,D类新增加的虚函数放在第二张虚表最后
[*]错误:新增加的虚函数只会放在 B1 的虚表中,而不是放在 B2 的虚表中。虽然 D 对两个基类的虚函数进行了重写,但新的虚函数不需要被放入第二个虚表中,因此这一选项是错误的。
D. 以上全部错误
[*]错误:如上所述,选项 B 是正确的,因此此选项也不创建。
https://img-blog.csdnimg.cn/img_convert/a6c9b3878136cd37f0638eab0443e417.png
[*]
页:
[1]