【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、下面程序输出结果是什么? ( ) 

#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

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

这里 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 )

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

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


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

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: 以上都不正确

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

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


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

class A
{
public:
	virtual void func(int val = 1)
	{}
	void test()
	{}
};

int main()
{
	//1、
	A* p1 = nullptr;
	p1->func();

	//2、
	A* p2 = nullptr;
	p2->test();
	
	return 0;
}

 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、什么是抽象类?抽象类的作用?

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

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

猜你喜欢

转载自blog.csdn.net/weixin_74531333/article/details/133710695