【C++】多态(C++重中之重)(学习与复习兼顾)

发布时间:2022-09-04 23:00

大家好,我是 兔7 ,一位努力学习C++的博主~

如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步

如有不懂,可以随时向我提问,我会全力讲解~

如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~!

你们的支持是我创作的动力!

我相信现在的努力的艰辛,都是为以后的美好最好的见证!

人的心态决定姿态!

本文章CSDN首发!

目录

前言

0. 面向对象的三大特性

1. 多态的概念

1.1 概念

2. 多态的定义及实现

2.2 虚函数

虚函数的virtual和虚继承的virtual的区别

2.3虚函数的重写

虚函数重写的三个例外:

(1) 协变(基类与派生类虚函数返回值类型不同)

(2) 析构函数的重写(基类与派生类析构函数的名字不同)

 (3) 子类的虚函数可以不加virtual也算完成了重写

​编辑2.4 C++11 override 和 final

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3. 抽象类

3.1 概念

3.2 接口继承和实现继承

4.多态的原理

4.1虚函数表

4.2多态的原理

4.3 动态绑定与静态绑定

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

5.2 多继承中的虚函数表

​编辑5.3. 菱形继承、菱形虚拟继承

总结

6. 继承和多态常见的面试问题

7. 总结(问答形式)


前言

        多态的重要性和C++中类和对象、继承相同,都是非常非常重要,所以大家认真仔细学习~

        需要声明的,我的代码是在vs2022下的x86程序中,涉及的指针都是4bytes。如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等。

        此博客为博主以后复习的资料,所以大家放心学习,总结的很全面,每段代码都给大家发了出来,大家如果有疑问可以尝试去调试。

        大家一定要认真看图,图里的文字都是精华,好多的细节都在图中展示、写出来了,所以大家一定要仔细哦~

        感谢大家对我的支持,感谢大家的喜欢, 兔7 祝大家在学习的路上一路顺利,生活的路上顺心顺意~!

0. 面向对象的三大特性

封装:数据和方法都封装到一起,通过访问限定符更好的管理数据的方法。

 【C++】类和对象

继承:类抽象级别的复用,公共数据和方法提取到父类,更好实现现实世界的关系。

【C++】继承

多态:完成某个行为,当不同的对象去完成时会产生不同的状态,与现实世界更贴切了。

【C++】多态

1. 多态的概念

1.1 概念

        多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

        举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票

        再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。其实这背后也是 一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额=random()%99;比如你经常使用支付宝支付或者支付宝账户 中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1。

        总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2. 多态的定义及实现

2.1多态的构成条件

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

        比如Student继承了Person。Person对象买票全价,Student对象买票半价。

【C++】多态(C++重中之重)(学习与复习兼顾)_第1张图片

【C++】多态(C++重中之重)(学习与复习兼顾)_第2张图片

那么在继承中要构成多态还有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(后面会讲到)。

【C++】多态(C++重中之重)(学习与复习兼顾)_第3张图片

2.2 虚函数

        虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

【C++】多态(C++重中之重)(学习与复习兼顾)_第4张图片  

虚函数的virtual和虚继承的virtual的区别

静态成员可以是虚函数吗?

         静态成员函数不属于类中的任何一个对象和实例,属于类共有的一个函数。也就是说,它不能用this指针来访问,因为this指针指向的是每一个对象和实例。

        对于virtual虚函数,它的调用恰恰使用this指针。在有虚函数的类实例中,this指针调用vptr指针,指向的是vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数的地址。总体来说虚函数的调用关系是:this指针->vptr(4字节)->vtable ->virtual虚函数。

        所以说,static静态函数没有this指针,也就无法找到虚函数了。所以静态成员函数不能是虚函数。他们的关键区别就是this指针。

        【C++】多态(C++重中之重)(学习与复习兼顾)_第5张图片

2.3虚函数的重写

        虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    /*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);
	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第6张图片

虚函数重写的三个例外:

(1) 协变(基类与派生类虚函数返回值类型不同)

        派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)

class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};

【C++】多态(C++重中之重)(学习与复习兼顾)_第7张图片  

(2) 析构函数的重写(基类与派生类析构函数的名字不同)

        如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
};

int main()
{
	// 在普通场景下,父子类的析构函数是否构成重写,不重要,没什么影响
	//Person p;
	//Student s;

	Person* p1 = new Person;
	Person* p2 = new Student;

	// 多态行为
	delete p1; // p1->析构函数() + operator delete(p1);
	cout << endl;
	delete p2; // p2->析构函数() + operator delete(p2);

	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第8张图片

【C++】多态(C++重中之重)(学习与复习兼顾)_第9张图片

 (3) 子类的虚函数可以不加virtual也算完成了重写

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person
{
public:
	// 子类的虚函数重写了父类的虚函数
	// 子类虚函数,不写virtual,也认为他是虚函数,完成了重写
	void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p){p.BuyTicket();}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第10张图片2.4 C++11 overridefinal

        从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写。

(1) final:修饰虚函数,表示该虚函数不能再被继承

class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

【C++】多态(C++重中之重)(学习与复习兼顾)_第11张图片

(2) override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Driv() override { cout << "Benz-舒适" << endl; }
};

【C++】多态(C++重中之重)(学习与复习兼顾)_第12张图片

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

【C++】多态(C++重中之重)(学习与复习兼顾)_第13张图片

【C++】多态(C++重中之重)(学习与复习兼顾)_第14张图片

3. 抽象类

3.1 概念

        在虚函数的后面写上 "=0" ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;	// 纯虚函数
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	//Car c;
	Car* pBenz = new Benz;
	pBenz->Drive();

	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第15张图片

3.2 接口继承和实现继承

        普通函数的继承是一种实现继承。

        派生类继承了基类函数,可以使用函数,继承的是函数的实现。

        虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

        所以如果不实现多态,不要把函数定义成虚函数。

4.多态的原理

4.1虚函数表

        这里常考一道笔试题:sizeof(Base)是多少?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

	void Func2()
	{
		cout << "Func2()" << endl;
	}

private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;
	Base bs;

	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第16张图片

        我们可以看到,写不写虚函数的结果是不一样的,那么是不是能确定,虚函数对这个大小有影响呢?我们继续向下看:

【C++】多态(C++重中之重)(学习与复习兼顾)_第17张图片

         除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表 virtual,f代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表。

        那么派生类中这个表放了些什么呢?为了更清楚的分析,我多加了一个虚函数。我们接着往下分析:

【C++】多态(C++重中之重)(学习与复习兼顾)_第18张图片

        在说表中放了什么的同时我也对构成多态进行了总结,并且引出了一个问题,就是上面没有说的 为什么多态的条件之一必须是父类的指针或引用去调用虚函数时发生多态,父类的对象却不行。

【C++】多态(C++重中之重)(学习与复习兼顾)_第19张图片

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过对上面代码的观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针存在于虚表的一部分,另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法层给的概念(的叫法),覆盖是原理(实现)层给的概念(的叫法)。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr,标志数组的结束。(下面演示)
  5. 总结一下派生类的虚表生成:
    a. 先将基类中的虚表内容拷贝一份到派生类虚表中。
    b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
    c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 对象中虚表指针是在什么阶段初始化的呢?虚表又是在什么阶段生成的呢?
            答:(1) 构造函数初始化列表。
                   (2) 编译时就生成好了。
  7. 这里还有一个小伙伴很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
            答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多小伙伴都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
            那么虚表存在哪的呢?
            实际我们去验证一下会发现vs下是存在代码段的,如果大家对Linux g++有兴趣,可以去验证一下哦。验证的方法在下面会讲到~

【C++】多态(C++重中之重)(学习与复习兼顾)_第20张图片

        要想验证虚表存在哪里,就要找到一个方法,我这里提供的方法是:栈上、堆上、数据段上、代码段上都设置一个变量,然后分别取到它们的地址,然后再根据虚表的地址进行比较,找到地址相差不多相差不多一个地址,那么大概率就是存在那里,接下来就来验证我的说法:

class Base
{
public:
	Base()
	{
		cout << "Base()" << endl;
	}

	virtual void Func1()
	{
		cout << "virtual Base::Func1()" << endl;
	}

	virtual void Func2()
	{
		cout << "virtual Base::Func2()" << endl;
	}

	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "virtual Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int j = 0;
int main()
{
	// 取虚表地址打印一下
	Base b;
	Base* p = &b;
	// 取头4/8个字节  ->  (int*)p  ->  解引用拿到_vfptr 
	printf("vftptr:%p\n", *((int*)p));

	int i;
	printf("栈上地址:%p\n", &i);
	printf("数据段地址:%p\n", &j);

	int* k = new int;
	printf("堆地址:%p\n", k);
	const char* cp = "Hello CSDN";
	printf("代码段地址:%p\n", cp);

	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第21张图片

4.2多态的原理

        上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);
	return 0;
}
  1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是 Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。 反思一下为什么?
       覆盖:因为只有实现了虚函数的覆盖才可以做到去完成某个行为,当不同的对象去完成时会产生出不同的状态。
    对象的指针或引用调用虚函数这里我在 4.1 的最后一张图顺便讲了,因为那里的很适合讲这个问题,如果还不是很懂的小伙伴可以跳转到那里哦~)
  5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

【C++】多态(C++重中之重)(学习与复习兼顾)_第22张图片

【C++】多态(C++重中之重)(学习与复习兼顾)_第23张图片

4.3 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
  3. 本小节之前(4.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

5.单继承和多继承关系的虚函数表

        需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的。

5.1 单继承中的虚函数表

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a = 7;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b = 5;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第24张图片

        接下来我就要带大家去证实上面的结论,还是老规矩,先给代码~

// 单继承
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	// 两个不是重写虚函数
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

// 写一个程序打印一下虚表,确认虚表中调用的函数
typedef void(*VFunc)();

//void PrintVFT(VFunc ptr[])
void PrintVFT(VFunc* ptr)   // 存函数指针的数组指针
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; ++i)
	{
		printf("VFT[%d]:%p->", i, ptr[i]);
		ptr[i]();
	}
	printf("\n");
}

// 思路:取出b、d对象的头4bytes,就是虚表的指针,
//       前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
//  1.先取b的地址,强转成一个int*的指针
 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
 // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
 // 4.虚表指针传递给PrintVTable进行打印虚表
 // 5.需要说明的是如果vs版本低的话,这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,
//  虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

int main()
{
	Base b;
	PrintVFT((VFunc*)(*(int*)&b));

	Derive d;
	PrintVFT((VFunc*)(*(int*)&d));
	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第25张图片

5.2 多继承中的虚函数表

         我要先说明,先继承的会在虚表的前面,后继承的会在虚表的后面,接下来大家跟着我的箭头认真看吧,这里有些没什么好解释的,都是看地址看出来的,所以大家认真看,当然我下面要给大家解释一下很长的一段代码,大家如果看不懂了就往后看,等看完了我的解释再回来看,绝对是没问题的~!

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 7;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 6;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1 = 5;
};

typedef void(*VFunc)();

//void PrintVFT(VFunc ptr[])
void PrintVFT(VFunc* ptr)   // 存函数指针的数组指针
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; ++i)
	{
		printf("VFT[%d]:%p->", i, ptr[i]);
		ptr[i]();
	}
	printf("\n");
}

int main()
{
	Base1 b1;
	Base2 b2;

	//PrintVFT((VFunc*)(*(int*)&b1));
	//PrintVFT((VFunc*)(*(int*)&b2));

	Derive d;

	PrintVFT((VFunc*)(*(int*)&d));
	// 因为Base1 和 Base2 是挨着放的,先放的Base1再放的Base2
	// 所以我们只要跳过sizeof(Base1)个字节就可以
	// 但是我们要注意的是 +sizeof(Base1) 是跳过的字节,
	// 所以要将*(int*)&d 转换为*(int*)((char*)&d+ sizeof(Base1))
	PrintVFT((VFunc*)(*(int*)((char*)&d + sizeof(Base1))));



	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第26张图片

        这里可能有一个转换大家看起来有些麻木,我接下来给大家来分析一下:

【C++】多态(C++重中之重)(学习与复习兼顾)_第27张图片5.3. 菱形继承、菱形虚拟继承

        实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的宝宝,可以去看下面的两篇链接文章。

C++虚函数表解析 ---> 酷壳 - CoolShell

C++对象的内存分布 ---> 酷壳 - CoolShell

        当然,接下来来一段我在讲继承的时候用过的代码,完结之前遗留的问题:

class A
{
public:
	virtual void func()
	{
		cout << "A::func()" << endl;
	}
public:
	int _a;
};

// class B : public A
class B : virtual public A
{
public:
	virtual void func()
	{
		cout << "B:func()" << endl;
	}

	virtual void func1()
	{
		cout << "B:func1()" << endl;
	}
public:
	int _b;
};

// class C : public A
class C : virtual public A
{
public:
	virtual void func()
	{
		cout << "C::func()" << endl;
	}

	virtual void func1()
	{
		cout << "C::func1()" << endl;
	}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func()
	{
		cout << "D::func()" << endl;
	}

	virtual void func1()
	{
		cout << "D::func1()" << endl;
	}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

【C++】多态(C++重中之重)(学习与复习兼顾)_第28张图片

总结

        1. 首先虚表根虚基表没有关系,大家要搞清楚。

        2. 虚表是用来存虚函数的,如果没有虚函数,那么就没有虚表。

        3. 虚基表是用来存放相对于虚继承成员,这里是A的偏移量的。

6. 继承和多态常见的面试问题

        前面的选择题大家可以当巩固自己的学习成果,最后我会公布答案,所以大家不要担心选后没有答案,大胆的选!后面的我会给大家讲解,所以大家如果都看到这里了,不要走哦,看到就是赚到~!

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

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

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

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

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

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

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

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

#include
using namespace std;
class A {
public:
	A(const char* s)
	{ cout << s << endl; }

	~A() 
	{}
};
class B :virtual public A
{
public:
	B(const char* s1, const char* s2) 
		:A(s1) 
	{ cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2)
		:A(s1) 
	{ cout << s2 << endl; }
};
class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const 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

        这道题很经典,首先必须知道的是:初始列表执行顺序跟初始化列表本身的顺序没有关系,跟声明有关系,继承成员声明顺序,是按继承顺序算的。

        所以因为是 class D :public B, public C 这么声明的,所以肯定在A、B选项里选。

        虽然B和C中都有初始化A,但是都不执行,因为只有一份A只会被初始化一次,因为是菱形虚拟继承,所以编译器会处理这里。

        然后D肯定是最后打印,因为先初始化完A、B、C才打印的D。

        所以这个题选A~

        为了证明我的推论:

【C++】多态(C++重中之重)(学习与复习兼顾)_第29张图片

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

class A
{
public:
 virtual void func(int val = 1){ std::cout<<"A->"<< val <"<< val <test();
 return 0;
}

        其实这个题很坑很BUG,但是面试管就爱问这样的题,准确来说,面试官就喜欢找C++的漏洞来出题,你说面试官可恨不~(手动滑稽)

        这里其实我在前面就说过了,虚函数重写的是函数实现,继承的父亲的接口定义,所以这里其实val = 0就不起作用,准确来说,写不写都无所谓,但既然写不写都没用,那为什么要让写呢,干脆不让写不就好了,还容易误导人。

        所以这个题的答案就是B -> 1。

        为了验证我的推论:

【C++】多态(C++重中之重)(学习与复习兼顾)_第30张图片

7. 总结(问答形式)

1. 什么是多态?

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

3. 多态的实现原理?

4. inline函数可以是虚函数吗?
答:可以,不过编译器就忽略了inline属性,这个函数就不再是inline,因为虚函数要放到虚表中。

 5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。(2.2的开始有详细的讲解)

6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。(定义虚函数的目的是构成多态,多态调用要去对象的虚表中找到虚函数。构造函数阶段,对应的虚表指针都没有初始化,如何去找?)

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容

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

9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的,vs2022和vs2013在代码段。

10. C++菱形继承的问题?虚继承的原理?
答:注意这里不要把虚函数表和虚基表搞混了。

11. 什么是抽象类?抽象类的作用?
答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。(因为如果子类不重写虚函数,子类也是抽象类,无法实例化出对象,其次抽象类可以更好用来表示显示世界中没有具体实例对应抽象类型,比如:植物)

【C++】多态(C++重中之重)(学习与复习兼顾)_第31张图片

        如上就是 多态 的所有知识,如果大家喜欢看此文章并且有收获,可以支持下 兔7 ,给 兔7 三连加关注,你的关注是对我最大的鼓励,也是我的创作动力~!

        再次感谢大家观看,感谢大家支持!

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号