【C++】多态的常见习题剖析,看看有没有你不会的


一、总结


静态多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为
动态多态:调用虚函数,不同的对象去调用,就有不同的行为/形态

多态的条件:
a.子类中重写父类的虚函数。
b.必须是由父类的指针或者引用去调用重写的虚函数。

虚函数的重写:
a、父类和子类都必须是虚函数
b、函数名、参数、返回值都必须相同

例外:
a、协变(父子类的指针或引用)
b、析构函数要特殊处理,因为父类指针可能指向子类的对象,就要调用子类对象的析构函数
c、子类中的重写虚函数可以不加virtual

对象的虚表指针是在构造函数的初始化列表才生成,但是虚函数表是在编译期间就生成的。原因:运行期间要动态生成通常需要空间,需要在堆上申请。



二、习题


下面的代码为什么跑不过呢?

class A
{
    
    
public:
	virtual void func1()
	{
    
    }

	int a = 0x10;
};
class B:virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	int b = 0x20;
};
class C :virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	int c = 0x30;
};
class D:public B,public C
{
    
    
public:
	/*virtual void func1()
	{}*/
	int d = 0x40;
};

int main()
{
    
    
	D d;
	return 0;
}

分析它的对象模型,由于B,C虚继承了A,则在d的对象模型当中就会只有一份A,但是B和C都对A进行了重写,导致在d看来func1有两个不同的实现版本,所以在D中不重写func1就会导致编译错误。但是如果B,C中没有对func1进行重写,在D处虽然会有两份func1,但是他们都是一样的,这个时候就不会报编译错误了。
在这里插入图片描述


虚基表指针内容的前四个字节的作用:
是用来算虚表指针到虚基表指针的一个距离的。
下面代码我们来分析d的对象模型。

扫描二维码关注公众号,回复: 13829230 查看本文章
class A
{
    
    
public:
	virtual void func1()
	{
    
    }

	int a = 0x10;
};
class B:virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	virtual void func2()
	{
    
    }
	int b = 0x20;
};
class C :virtual public A
{
    
    
public:
	virtual void func1()
	{
    
    }
	virtual void func2()
	{
    
    }
	int c = 0x30;
};
class D:public B,public C
{
    
    
public:
	virtual void func1()
	{
    
    }
	virtual void func2()
	{
    
    }
	int d = 0x40;
};

int main()
{
    
    
	D d;
	
	return 0;
}

通过内存窗口和监视窗口,我们可以得知,d对象当中的B部分的第一个地址是指向虚函数表的指针,而第二个字段则指向的虚基表,在先前的博客中虚基表的第一个字段一直都是0x00 00 00 00,而现在是0xff ff ff fc,也就是-4,从虚基表指针的地址往上走4字节正好是虚函数表指针。所以,虚基表中第一个字段也就是用来得到虚函数表的位置。
在这里插入图片描述



选择题

  1. 下面哪种面向对象的方法可以让你变得富有( )
    A: 继承 B: 封装 C: 多态 D: 抽象

A.继承,是一种复用

  1. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

D.父类的指针或者引用可以指向子类的虚函数表

  1. 面向对象设计中的继承和组合,下面说法错误的是?()
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为
    白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也
    称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

C.优先使用组合,继承是父类的静态复用,组合是动态复用。

  1. 以下关于纯虚函数的说法,正确的是( ) A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类 C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

A,C中子类可以不实现纯虚函数,不过他就无法实例化出对象。

  1. 关于虚函数的描述正确的是( ) A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数 C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数

B.内联对编译器是一种建议,声明了virtual后就会忽略内联的属性,因为内联函数是没有地址的,他都在调用的地方展开了。而虚函数都是要放到虚函数表当中的。注:vs2013和liunx下都可以跑得过。

  1. 关于虚表说法正确的是( ) A:一个类只能有一张虚表 B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表 C:虚表是在运行期间动态生成的 D:一个类的不同对象共享该类的虚表

D,C中运行期间要动态生成通常需要空间,需要在堆上申请。所以我们可以猜测他是在编译的时候生成的。

  1. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
    A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址 B:A类对象和B类对象前4个字节存储的都是虚基表的地址
    C:A类对象和B类对象前4个字节存储的虚表地址相同 D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

D,虚函数表是多态的,虚基表是虚继承的找超类当中的成员变量


  1. 下面程序输出结果是什么? ()
#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) :C(s1, s3), B(s1, s2), A(s1)
	{
    
    
		cout << s4 << endl;
	}
};
int main() {
    
    
	D *p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

答案:A
首先,排除法,我们声明继承的顺序就是初始化的顺序,注意这个不是初始化列表的顺序,那么我们可以排除CD,因为B要在C的前面。然后我们要先初始化父类,则A就在就前面了。并且编译器会进行优化,每个对象都只会初始化一次。


9.以下的结果

class A
{
    
    
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{
    
    }

	void Print() {
    
    
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
    
    
	A aa(1);
	aa.Print();
	return 0;
}

答案:输出1 随机值
因为成员变量声明的顺序就是初始化列表的顺序,所以他会先走_a2(_a1)然后再走_a1(a)。


  1. 多继承中指针偏移问题?下面说法正确的是( )
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


  1. 以下程序输出结果是什么()
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: 以上都不正确

选B
因为B中没有对test进行重写,那么调用父类的test(),test中调用了func,这里的this指针是B类型的,B并且对func进行了重写,重写是一种接口继承,重写只要满足参数,函数名,返回值相同就可以,而缺省参数会用父类的,所以最终打印出来就是B->1。
普通函数是实现继承。
虚函数的继承,是一种接口继承。
在这里插入图片描述

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

	void test()
	{
    
     
	}
};


// A 编译报错  B运行崩溃  C 正常运行
int main()
{
    
    
	// 1、 
	A* p1 = nullptr;
	p1->func();

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

	return 0;
}

1B 2C,原因,普通的成员函数是放在代码段当中的,我们不需要解引用行为去调用,所以第二个不会出错,而第一个由于多态的两个条件都满足,所以他就会访问p1指向的对象头上四字节,因此会报错。


问答题:

静态成员可以是虚函数吗?
不能,静态成员函数没有this指针,适用类型::成员函数的调用方式无法访问虚函数表,

构造函数可以是虚函数吗?
构造函数需要实现多态吗?没有意义,因为我们实例化子类对象的时候也要调用父类的构造函数。对象的虚函数表是在编译时初始化,虚表指针是在运行时经过构造函数的初始化列表之后才生成的。
对象的虚表指针在构造函数初始化列表才初始化,将构造函数弄成虚函数,那么对象要有虚表指针才能调用构造函数,就自相矛盾了,所以当把父类的构造函数设置为虚函数会直接报编译错误。
在这里插入图片描述

析构函数需要虚函数吗?
需要,因为 当我们delete(指针),我们并不知道这个指针指向的对象是父类的对象还是子类的对象,通过多态我们就可以进行动态的一个检测再释放。

对象访问普通函数快还是虚函数更快??
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
对象调用是不可能实现多态的


小知识点

1.模板属于编译时多态,我们通过反汇编可以看到它运行时call一下就跳转到对应的函数了。
在这里插入图片描述
2.使用父类对象调用的永远都是父类的方法!

3.接口继承的体现在这里插入图片描述
4.静态成员函数不能设置成虚函数的原因,是因为静态成员函数没有this指针。没有this指针无法拿到虚表,没法实现多态。

5.假设重写成功,通过指针或引用就一定能实现多态吗?
错!一定要是父类的指针或引用。

6.在多态和组合都能用的时候,推荐用组合,因为多态的调用是有额外的开销,有虚函数表指针,虚函数表,运行时决议等等。

7.友元函数不能设置成虚函数,因为友元函数不属于成员函数。

总结

多态一节到此为止。

猜你喜欢

转载自blog.csdn.net/weixin_52344401/article/details/122812549