0
点赞
收藏
分享

微信扫一扫

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

Just_Esme 2022-02-11 阅读 86

多态总结


一、总结


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

多态的条件:
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的对象模型。

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字节正好是虚函数表指针。所以,虚基表中第一个字段也就是用来得到虚函数表的位置。
在这里插入图片描述



选择题

A.继承,是一种复用

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

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

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

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

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

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.友元函数不能设置成虚函数,因为友元函数不属于成员函数。

总结

多态一节到此为止。

举报

相关推荐

C++多态练习题

0 条评论