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


    一、概念考查

    1、下面哪种面向对象的方法可以让你变得富有( A )

    A. 继承                B. 封装                C. 多态                D. 抽象

    继承机制是面向对象程序设计使代码可以复用的最重要手段,继承是类设计层次的复用。


    2、( D )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。

    A. 继承                B. 模板                C. 对象的自身引用                D. 动态绑定

    动态绑定又称后期绑定或晚绑定,就是多态。


    3、面向对象设计中的继承和组合,下面说法错误的是?( C

    A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用。

    B. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用。

    C. 优先使用继承,而不是组合,是面向对象设计的第二原则。

    D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现。

    优先使用组合,而不是继承。


    4、以下关于纯虚函数的说法,正确的是( A )

    A. 声明纯虚函数的类不能实例化对象                B. 声明纯虚函数的类是虚基类

    C. 子类必须实现基类的纯虚函数                       D. 纯虚函数必须是空函数

    虽然声明纯虚函数的类不能实例化对象,但声明纯虚函数的类可以定义指针。


    5、关于虚函数的描述正确的是( B )

    A. 派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B. 内联函数不能是虚函数  

    C. 派生类必须重新定义基类的虚函数                                   D. 虚函数可以是一个 static 型的函数

    首先排除 A 和 C 选项,其次虚函数的地址是放在对象的虚表中,如果要形成多态,就必需要用对象的指针或引用来调用,而 static 就意味着是静态的,你连 this 指针都没有,那就不合理了。

    内联函数不能是虚函数其实是一个存疑的选项,己验证。在 VS2019 下,内联函数加上虚函数后依然能编译通过,但是我们得知道内联函数对编译器而言只是一个建议,实际上一个函数真的成为内联函数,它就不可能是虚函数,因为内联函数是没有地址的,它直接在调用的地方展开,而虚函数是要把地址放到虚函数表中,所以这里一定会把 inline 给忽略掉。


    6、关于虚表说法正确的是( D

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

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

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

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

    上面的多继承中就有两张虚表,且严格来说虚表不是在类,而是在对象,所以 A 选项错误;

    不管是否完成重写,父子类的对象都是有独立的虚表,所以排除 B 选项;

    虚表如果是运行时动态生成,虚表是需要空间的,且运行起来只能在堆上申请,而虚表是在常量区或代码段,所以虚表是在在编译阶段生成的, C 选项也错。

    正确答案为 D。


    7、假设A类中有虚函数,B 继承自 A,B 重写 A 中的虚函数,也没有定义任何虚函数,则( D 

    A. A 类对象的前 4 个字节存储虚表地址,B 类对象前 4 个字节不是虚表地址

    B. A 类对象和 B 类对象前 4 个字节存储的都是虚基表的地址

    C. A 类对象和 B 类对象前 4 个字节存储的虚表地址相同

    D. A 类和 B 类虚表中虚函数个数相同,但 A 类和 B 类使用的不是同一张虚表

    A 类有虚函数,A 类对象的前 4 个字节当然是存储虚表地址,只要 B 类继承了 A 类,B 类的前 4 个字节也当然是存储虚表地址,只不过是不同的虚表地址,所以排除 A 选项;

    虚基表是用来解决菱形继承问题的,与虚函数表是两个概念。注意区分解决菱形继承的虚继承的虚基表,所以排除 B 选项;

    不管是否重写,父子类的对象都是有独立的虚表,所以排除 C 选项;


    8、下面程序输出结果是什么? ( ) 

    1. #include
    2. using namespace std;
    3. class A{
    4. public:
    5. A(char *s)
    6. {
    7. cout << s << endl;
    8. }
    9. ~A(){}
    10. };
    11. class B:virtual public A
    12. {
    13. public:
    14. B(char *s1,char*s2):A(s1)
    15. {
    16. cout << s2 << endl;
    17. }
    18. };
    19. class C:virtual public A
    20. {
    21. public:
    22. C(char *s1,char*s2):A(s1)
    23. {
    24. cout << s2 << endl;
    25. }
    26. };
    27. class D:public B,public C
    28. {
    29. public:
    30. D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
    31. {
    32. cout << s4 << endl;
    33. }
    34. };
    35. int main() {
    36. D *p=new D("class A","class B","class C","class D");
    37. delete p;
    38. return 0;
    39. }

    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

    注意这里的初始化顺序和初始化列表中的顺序无关,这里是与继承的顺序,也就是声明的顺序有关。

    这里 D 继承了 B、C,要去调用父类的构造函数,谁先继承谁就先调,按理说先由 D 调用 B 的构造函数,再由 B 调用 A 的构造函数,再由 D 调用 C 的构造函数,再由 C 调用 A 的构造函数 (A ➡ B ➡ A ➡ C ➡ D)。

    但是因为 virtual 后,编译器做了处理,不可能让 B 对 A 初始化一次,C 对 A 再初始化一次,所以应该是 (A ➡ B ➡ C ➡ D)。


    9、多继承中指针偏移问题?下面说法正确的是( C )

    1. class Base1{
    2. public:
    3. int _b1;
    4. };
    5. class Base2{
    6. public:
    7. int _b2;
    8. };
    9. class Derive : public Base1, public Base2{
    10. public:
    11. int _d;
    12. };
    13. int main(){
    14. Derive d;
    15. Base1* p1 = &d;
    16. Base2* p2 = &d;
    17. Derive* p3 = &d;
    18. return 0;
    19. }

    A. p1 == p2 == p3        B. p1 < p2 < p3        C. p1 == p3 != p2        D. p1 != p2 != p3

    如下图,所以选择 C 选项。注意 p1 和 p3 虽然都指向同一地址,但是它们的类型不一样,p1 是 Base1 的大小,p3 是 Derive 的大小。


    10、以下程序输出结果是什么( 

    1. class A
    2. {
    3. public:
    4. virtual void func(int val = 1)
    5. {
    6. std::cout << "A->" << val << std::endl;
    7. }
    8. virtual void test()
    9. {
    10. func();
    11. }
    12. };
    13.  
    14. class B : public A
    15. {
    16. public:
    17. void func(int val=0)
    18. {
    19. std::cout << "B->" << val << std::endl;
    20. }
    21. };
    22.  
    23. int main(int argc ,char* argv[])
    24. {
    25. B*p = new B;
    26.    p->test();
    27.    return 0;
    28. }

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

    首先,这里不涉及多态,因为 p 的类型是子类的指针,p 再去调用父类继承下来的 test,但是这里父类中 test 函数的参数中有一个 A* this 的指针,所以调用时就是一个父类的指针指向子类对象,满足多态的条件之一,其次子类重写可以不写 virtual,我们需要重写虚函数,并满足三同或三个例外,但是没有说缺省参数也要相同,标准也基本不会提,我们就认为它构成重写。所以这里 this 调用 func 时符合多态,调用的是子类的 func,所以这里就从 B 选项 和 C 选项中选择。

    我们又说了普通函数的继承是实现继承,而虚函数的继承是接口继承,接口继承指的是函数的声明,包括函数名、参数、返回值,所以这里把函数的缺省参数也继承下来,而这里重写的是它的实现,跟参数这些无关,所以选择 B 选项。


    11、以下两段程序输出结果是什么 ( B C )

    1. class A
    2. {
    3. public:
    4. virtual void func(int val = 1)
    5. {}
    6. void test()
    7. {}
    8. };
    9. int main()
    10. {
    11. //1、
    12. A* p1 = nullptr;
    13. p1->func();
    14. //2、
    15. A* p2 = nullptr;
    16. p2->test();
    17. return 0;
    18. }

     A. 编译报错                        B. 运行崩溃                        C. 正常运行

    成员函数的地址不在对象中存储,而存在于公共代码段。这里调用成员函数,不会去访问 p1 和 p2 指向的空间,也就不存在空指针解引用了,这里把 p1 和 p2 传递给隐含的 this 指针,但是 p1 是一个父类的指针,而 func 是 virtual,这里转换必然要去虚表中找,因为从语法识别的角度,编译器看到 p1->func() 时也不知道指向的是哪个对象,所以这里依然对 p1 进行解引用了,所以选择 B 和 C 选项。


    二、问答题

    1、什么是多态?

    多态是指不同继承关系的类和对象去调用同一函数,产生了不同的行为。

    多态又分为静态多态和动态多态。


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

    • 重载是指在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。
    • 重写(覆盖)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名、参数、返回值都必须相同(协变例外),且这两个函数都是虚函数。
    • 重定义(隐藏)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名相同即可。

    3、多态的实现原理?

    构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。

    因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。


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

    可以,内联函数是会在调用的地方展开的,是没有地址的,但是 inline 只是一个建议,可以定义成虚函数的,当我们把内联函数定义成虚函数后,在多态调用中,编译器就忽略了该函数的内联属性,这个函数就不再是 inline 了,因为虚函数的地址被放到虚表中去。


    5、静态成员(static 函数)可以是虚函数吗?

    不能,因为静态成员函数是存在整个类域中,没有 this 指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

    虚函数是为了实现多态,多态都是运行时去虚表找决议,而静态成员函数都是在编译时决议,它是virtual 没有价值。


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

    不可以,因为对象中的虚函数表指针是在构造函数初始化列表阶段(运行时)才初始化的,如果构造函数是虚函数,那么调用构造函数时对象中的虚表指针都没有初始化。构造函数时虚函数没有意义。


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

    可以,并且最好把基类的析构函数定义成虚函数。当我们 new 一个父类对象和一个子类对象,并均用父类指针指向它们,在我们使用 delete 调用析构函数并释放对象空间时,只有当父类的析构函数是虚函数的情况下,才能正确调用父类和子类的析构函数,否则当我们使用父类指针 delete 对象时,只能调用到父类的析构函数。


    8、拷贝构造和 operator= 可以是虚函数吗?

    不可以,拷贝构造也是构造函数,答案参考上面的构造函数。

    operator= 可以,但是没有实际价值。


    9、对象访问普通函数快还是虚函数更快?

    如果虚函数不构成多态,是普通对象,二者是一样快的。

    如果虚函数构成多态的调用,是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,在运行时调用虚函数需要到虚函数表中去查找虚函数的地址。


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

    构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针。虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。


    11、C++ 菱形继承的问题?虚继承的原理?

    注意这里不要把虚函数表和虚基表搞混了。

    菱形继承子类对象当中有两份父类的成员,会导致数据冗余和二义性的问题。
    虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。


    12、什么是抽象类?抽象类的作用?

    抽象类体现了虚函数的继承是一种接口继承,强制子类去抽象纯虚函数,如果子类不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。

    其次,抽象类可以很好的去表示现实世界中没有示例对象对应的抽象类型,比如:植物、人、动物等。

  • 相关阅读:
    RabbitMQ高可用集群部署
    java数据结构与算法刷题-----LeetCode1094:拼车
    Java策略模式在我司应用
    canaladapter 同步mysql(一对多关系)到es,es 添加逗号分词查询
    中国驱虫湿巾行业竞争状况与销售前景预测报告2022-2028年
    一文讲通嵌入式现状
    9.4JavaEE——声明式事务管理(一)基于XML方式的声明式事务
    NC191 二叉搜索树的最近公共祖先
    探讨苹果商店那些“变身包”究竟是怎么上架的
    324. 摆动排序 II
  • 原文地址:https://blog.csdn.net/weixin_74531333/article/details/133710695