C++彻底搞定面试--多态篇,博主呕心沥血之作,万字解析,弄透多态

面向对象特性–多态


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iQteiD6m-1665569715813)(D:\gitee仓库\博客使用的表情包\请坐.png)]

ps:温馨提示,由于多态的很多知识点是建立在继承的基础上的,所以建议大家先去复习复习或者学习学习继承

可以参考博主的这篇文章:

(190条消息) 彻底搞定面试题–继承篇(C++继承讲解),万字解析,弄透继承!_龟龟不断向前的博客-CSDN博客

🚀1.多态的概念

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KOrX5u7x-1665569715814)(D:\gitee仓库\博客使用的表情包\举个例子.jpg)]

​ 火车站买票,有学生,普通人,军人,三类对象去买票,明明都是去完成买票这种行为,但是他们最终产生的效果是不同的:

学生–票价优惠,普通人–全价买票,军人–优先买票。这就是现实生活中的多态行为。


🚀2.多态的定义与实现

🍉静态多态

​ 静态的多态是在程序编译时就确定的。其中函数重载模板就实现了多态。

#include<iostream>
using namespace std;

int main()
{
	int a;
	double da;
	cin >> a;
	cin >> da;

	cout << a << endl;
	cout << da << endl;

	int i = 0, j = 1;
	double d = 1.1, e = 2.2;
	swap(i, j);
	swap(d, e);

	return 0;
}

​ 上述调用的输入,输出中,a和ch的输入,输出调用的是不同的operator>>operator<<

包括int类型与double类型的交换,调用的也是不一样的swap函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WkBBieEV-1665569715814)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012085950459.png)]


🍉动态多态

首先我们需要介绍两个概念,虚函数以及虚函数的重写

🍇虚函数

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

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

BuyTicket()就是虚函数。

而上一篇文章中,继承时,在继承方式前面加上virtual关键字是虚继承


🍇虚函数的重写(覆盖)

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

ps:这里我们不要把继承中的隐藏和多态中的重写弄混淆了,下方有总结

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


class Student : public Person 
{
public:
 virtual void BuyTicket() //完成了父类虚函数BuyTicket的重写
 { 
     cout << "买票-半价" << endl;                  
 }
};
🍇构成动态多态的条件
  1. 继承下,子类完成父类的虚函数的重写
  2. 使用父类的指针或者引用去调用子类重写的虚函数

ps:如果使用父类对象去调用子类重写的虚函数,是不会构成多态的,最终调用的都是父类的虚函数

代码演示:

#include<iostream>
using namespace std;

class Person {//基类
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }//父类的虚函数
};

class Student : public Person
{
public:

	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

class Soldier : public Person
{
public:
	virtual void BuyTicket() { cout << "优先-买票" << endl; }
};

void f(Person& p)
{
	 //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
	p.BuyTicket();
}

void f(Person* p)
{
	 //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
	p->BuyTicket();
}

int main()
{
	Person p; // 普通人
	Student st; // 学生
	Soldier so; // 军人

	f(p);
	f(st);
	f(so);
	cout << endl;
    
    f(&p);
	f(&st);
	f(&so);
	cout << endl;

	return 0;
}

​ 上述代码就演示了,明明是不同对象(普通人,学生,军人)调用同一个ButTicket()函数,最终达到的是不同的效果。


下面演示一下,如果没有构成多态(使用父类对象去调用子类的虚函数),看看是怎样的效果

#include<iostream>
using namespace std;

class Person {//基类
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }//父类的虚函数
};

class Student : public Person
{
public:

	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

class Soldier : public Person
{
public:
	virtual void BuyTicket() { cout << "优先-买票" << endl; }
};

void f(Person p)//这里我们使用了父类的对象来调用子类的虚函数,不符合构成多态的条件
{
	p.BuyTicket();
}


int main()
{
	Person p; // 普通人
	Student st; // 学生
	Soldier so; // 军人

	f(p);
	f(st);
	f(so);
	cout << endl;

	return 0;
}

​ 最终的效果是调用的都是父类的虚函数


🍇那些同样构成动态多态的例外

1. 协变(了解即可)

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

​ 大家可以看看下面的构不构成多态。

#include<iostream>
using namespace std;

class a{};
class b : public a {};//父子关系--分别做以下的父子关系虚函数的返回值

class person {
public:
	virtual a* f() { 
		cout << "a* person::f()" << endl;
		return new a; 
	}
};

class student : public person {
public:
	virtual b* f() { 
		cout << "b* student::f()" << endl;
		return new b; 
	}
};

void func(person& p)
{
	p.f();
}

void func(person* pp)//函数重载
{
	pp->f();
}

int main()
{
	person p;
	student s;

	func(p);
	func(s);
	cout << endl;

	func(&p);
	func(&s);

	return 0;
}

​ 上述的student的虚函数和person的虚函数的返回类型不一样,但是person虚函数的返回类型是A的指针(父类指针),student虚函数的返回类型是B的指针(子类指针)。符合协变情况,所以也构成多态。

ps:建议不要写协变


2.子类的虚函数重写可以不添加virtual关键字

​ 因为认为,子类从父类继承,将父类的虚属性也继承下来了,就算不写virtual关键字,也认为其是虚函数,从而构成多态。

同学们可以将上述代码中,子类的重写虚函数virtual关键字去掉,同样也构成了多态。


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

​ 如果严格遵守多态的构成条件,那么子类析构函数就算定义成虚函数也无法完成重写了,因为父子类的析构函数是不可能同名的

编译器做了一件事,凡是父子类的析构函数,都将父子类的析构函数名变成destructor()其目的就是为了父子类的析构函数可以构成多态

​ 这也解决了咱们上篇文章的问题:为什么父类与子类的析构函数构成了隐藏,因为他们的析构函数同名了。如果不构成多态,那么就构成了隐藏。


​ 那么为什么要将父类的析构函数与子类的析构函数构成多态呢?因为会出现以下场景

#include<iostream>
using namespace std;

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

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

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1; 
	delete p2;

	return 0;
}

​ 当我们使用new,申请了一块子类空间,而我们想用父类指针去指向这块子类空间,我们像上述代码一样,不重写析构函数,那么下方的delete p2将是错误的,运行结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hAmAlQiI-1665569715815)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012141608358.png)]

​ 而当我们完成子类的析构函数的重写(在父子类的析构函数前加上virtual),结果才是正确的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0EE790A2-1665569715815)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012141943928.png)]

ps:我们是建议子类重写父类的析构函数的,这样不仅可以实现多态,而且是不影响正常的使用的


🚀3.如何让父类的虚函数无法被重写–final

class Car 
{
public:
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};
  1. 当我们在虚函数的后面加上final关键字时,这个虚函数是无法被子类的虚函数重写的

    所以上述代码会报错!

  2. 而上一篇文章继承中,在类的后面加上final关键字时,这个类时无法被继承的

要分清楚这个关键字的不同用法哦


🚀4.如何强制要求完成虚函数的重写–override

​ 如果C语言C++看作是两兄弟,那么C语言更像是一个自由自在的孩子,而C++更像一个有规矩的孩子,例如C++中有封装,不能随便访问类的成员变量

​ 而override就像一个监督你有没有完成虚函数的重写的老师一样,如果在函数后面加了override又没有完成虚函数的重写,程序就会报错

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TSa5quqc-1665569715816)(D:\gitee仓库\博客使用的表情包\查作业.jpg)]

#include<iostream>
using namespace std;

class Car{
public:
	virtual void Drive(char ch){}
};

class Benz :public Car {
public:
	virtual void Drive(int ch) override { cout << "Benz-舒适" << endl; }
};
int main()
{
    
	return 0;
}

​ 虚函数重写的要求是:子类重写的虚函数必须完全跟子类的虚函数一致(函数的返回类型,函数名,函数参数)

而上述代码子类的虚函数与父类的虚函数的函数参数不同,那么程序就会报错,因为你没有完成虚函数的重写

所以咱们在写子类的析构函数时,可以加一个override来帮我们检查,是不是完成了虚函数的重写


🚀5.抽象类

​ 现实生活中:

学生是一个具体类,因为可以让学生定义一个对象 小明 小红 小刚等(小学小人组)

狗是一个具体类,因为可以让狗定义对象 小白 旺财 莱温斯基等

​ 但是植物不是一个具体类,因为不会用它来定义对象,太抽象了,太广泛了,严格来说,人和动物也不能叫做具体类,他们定义的对象无法归结成一类

​ 像这种我们通常将其称为抽象类,而抽象类是只用来继承,不用来定义对象的

在介绍抽象类之间,我们先介绍什么叫纯虚函数


🍉纯虚函数
class car
{
public:
	virtual void drive() = 0
	{
		cout << "class car()";
	}
};

​ 再虚函数的声明时,添加一个赋值0,那么这个虚函数就是纯虚函数

那么类的虚函数是纯虚函数的类,叫做抽象类


🍉抽象类的目的
#include<iostream>
using namespace std;

class car
{
public:
	virtual void drive() = 0;
};

class benz :public car
{
public:
	void drive()
	{
		cout << "benz-舒适" << endl;
	}
};

class bmw :public car
{
public:
	virtual void drive()
	{
		cout << "bmw-操控" << endl;
	}
};

int main()
{
	//car c;//error抽象类car,无法定义对象
	//普通对象
	benz b;
	bmw bm;
	b.drive();
	bm.drive();
	cout << endl;
    
    //多态
    car* pbenz = new benz;
	pbenz->drive();

	car* pbmw = new bmw;
	pbmw->drive();
	cout << endl;

	car& rbanz = b;
	car& rbmw = bm;
	rbanz.drive();
	rbmw.drive();
	cout << endl;

	return 0;
}

​ 就像我们虚函数的时候说过,就算虚函数的重写中,子类虚函数不加virtual,也可以实现虚函数的重写,因为继承使得子类的对象函数有了虚函数!

​ 同样地,上述代码中,由于子类的继承,父类的纯虚函数是会被继承下来的,如果不重写这个纯虚函数,那么这个子类也将成为一个抽象类,会使得子类无法定义对象!

总结:继承抽象类的子类,一定要完成抽象类的纯虚函数的重写,否则无法使用子类创建对象,像这种继承抽象类,我们通常也称为接口继承,可以理解成,如果玩多态就可以用抽象类,不玩多态,就不玩抽象类


🚀6.虚函数的底层原理

🍉笔试题:当类中有虚函数时,计算这个类的内存大小
#include<iostream>
using namespace std;

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

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

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

private:
	int _b = 1;
	char _ch = '\0';
};

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

​ 大家可以自己运行一下,看看答案是多少?


🍉虚函数表指针(虚表指针)

​ 我们通过vs监视窗口可以观察到对象bs的成员跟我们想的不一样

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qs5XtsP6-1665569715817)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012151634611.png)]

​ 成员列表的第一个位置多放了一个虚函数表指针(虚表指针)所以上述的答案才会是12(还进行了内存对齐)


🍉虚函数表(虚表)

​ 根据上述指针的名字–虚表指针也知道,它肯定指向的是虚表,下面我们借助vs的内存窗口来看看效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ajyT9vkC-1665569715818)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012152226544.png)]


​ 也就是说,成员变量中的虚表指针(虚表的首元素地址)指向的是虚表(函数指针数组),而虚表里面存放的是虚函数的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qKcqC1C6-1665569715819)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012152547551.png)]


🍉虚表指针和虚表是在何时生成的

还没进入构造函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6dWeu3C-1665569715819)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012180824005.png)]

进入构造函数(初始化列表):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWqioNUO-1665569715820)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012180902399.png)]

因为虚表指针也算作成员变量,在类与对象中说过,成员变量是在初始化列表中定义的,所以虚表指针是在构造函数中生成的

而虚函数在编译过程中就要确定地址,所以虚表是在编译时确定的


🚀7.多态的底层原理

​ 不知道大家是否有疑问:为什么实现多态非要是父类的引用或者指针去调用子类的虚函数,为什么父类的对象去对用就不可以呢?

🍉虚表的继承与覆盖
🍇父类中只有一个虚函数

咱们继续拿买票的例子来解释

#include<iostream>
using namespace std;

class Person {//基类
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }//父类的虚函数
};

class Student : public Person
{
public:

	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

class Soldier : public Person
{
public:
	virtual void BuyTicket() { cout << "优先-买票" << endl; }
};

void f(Person& p)
{
	 //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
	p.BuyTicket();
}

void f(Person* p)
{
	 //传不同类型的对象,调用的是不同的函数,实现了调用的多种形态
	p->BuyTicket();
}

int main()
{
	Person p; // 普通人
	Student st; // 学生
	Soldier so; // 军人

	f(p);
	f(st);
	f(so);
	cout << endl;
    
    f(&p);
	f(&st);
	f(&so);
	cout << endl;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erjnnhTY-1665569715820)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012154812842.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ariMU5Cd-1665569715821)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012154845026.png)]

​ 这也是为什么完成重写了,就调用各自的函数,没有完成重写就调用父类的函数的原因之一


🍇父类中有多个虚函数
#include<iostream>
using namespace std;

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 main()
{
	Base b;
	Derive d;

	return 0;
}

​ 上述代码中,父类中有两个虚函数,但是子类中只重写了其中的一个虚函数,咱们用vs监视窗口给大家演示以下效果!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yoEl35kH-1665569715821)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012161911692.png)]


🍉只能用父类指针或引用才构成多态的原因
#include<iostream>
using namespace std;

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

	int _p = 1;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }//进行了覆盖(重写)

	int _s = 2;
};

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

void Func(Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);
    Func(Mike);

	Student Johnson;
	Johnson._p = 10;
	Func(Johnson);
	Func(Johnson);	
	return 0;
}
🍇引用调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cGbOLgbA-1665569715822)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012163520773.png)]


🍇指针调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2AyeghWs-1665569715822)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012163841869.png)]


🍇对象调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lp9WcsQC-1665569715823)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012164831100.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIbPePtV-1665569715824)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012164937909.png)]

所以父类对象来调用子类的重写虚函数是无法构成多态的,因为这时父类对象中压根就没有子类对象的虚表


🚀8.虚函数在内存存储位置

虚函数是存在内存中的什么位置?请设计一个程序来测试

  1. 数据段(静态区)
  2. 代码段(常量区)

​ 我们可以将栈,堆,数据段,代码段以及虚函数的地址打印以下,进行一个对比

这里给大家复习以下内存分布:(图解)

图片来源:

(157条消息) C++图解模板_龟龟不断向前的博客-CSDN博客

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9bsd2pz4-1665569715824)(C:\Users\Cherish\Desktop\74067c00aa414b898664c98b5eeb7c2f.png)]


​ 现在主要的问题是如何拿到虚函数的地址,由于虚表指针是放在成员变量中的第一个,而且一个指针的大小是4个字节(32位环境下)

,拿我们可以拿到对象的地址,取出前四个字节的地址内容,即可得到虚表指针的地址,再解引用即可得到第一个虚函数的地址

#include<iostream>
using namespace std;

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;//取出其地址,然后取出前四个字节的地址
	printf("虚函数的地址:%p\n", *(int*)p);//虚表指针再解引用,得到的才是虚函数的地址

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

	int* pi = new int;
	printf("堆上的地址:%p\n", pi);

	char* ptr = "abcdefg";
	printf("代码段的地址:%p\n", ptr);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2wVkI0N-1665569715825)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012170556247.png)]


🚀9.如何把虚表打印在屏幕上–并且得知是哪个虚函数的地址

上述给大家展示多态的底层时我们使用了两种方式

  1. vs监视窗口–但是监视窗口不一定就是真实的
  2. vs的内存窗口

这里介绍第三种方法:打印虚表

​ 通过上面的学习我们知道,虚表是一个函数指针的数组(函数类型可能不同),既然我们可以得到虚表指针–即虚表的首地址

拿我们也可以像数组一样,将函数指针数组里面的内容(虚函数地址)打印出来,开干开干

#include<iostream>
using namespace std;

class Base {
public:
	
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a = 1;
};

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 = 2;
};


//将函数指针类型给他重命名
typedef void(*VFunc)();//由于函数指针名字的复杂性,咱们给他进行一个重命名

void PrintVFT(VFunc* ptr) //存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()
{
	//先拿到函数虚表的地址
	Base b;
	int p = *(int*)&b;//先拿到虚表地址的数值
	PrintVFT((VFunc*)p);//再将虚表地址强转以下即可

	Derive d;
	p = *(int*)&d;
	PrintVFT((VFunc*)p);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ziyaeu6p-1665569715825)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012171750571.png)]

​ 上述代码中Derive继承了Base,重写了func1,没有重写func2

所以func1调用的是自己的,func2调用的是父类的,然后func3func4都是子类自己的

这种方式真的是很牛,非常的直观,斑爷也称你为最强

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vxz1pcqQ-1665569715826)(D:\gitee仓库\博客使用的表情包\最强.jpg)]


🚀10.多继承下虚表的覆盖情况

#include<iostream>
using namespace std;

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

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

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

typedef void(*VFunc)();

void PrintVFT(VFunc* ptr)   // 存函数指针的数组指针
{
	printf("虚表指针:%p\n", &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;

	Derive d;
	int p = *(int*)&d;//取出第一个虚表地址的数值
	PrintVFT((VFunc*)(p));

	char* pc = ((char*)&d + sizeof(Base1));//得到第二个虚表指针的地址
	p = *(int*)pc;//取出第二个虚表地址的数值
	PrintVFT((VFunc*)(p));

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NwI53d69-1665569715826)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012172758157.png)]

​ 上述代码中:Derive继承了BaseBase,重写func1,但是Base1Base2中都有fun1,子类的虚表会怎么覆盖和继承呢?

先继承的Base1,再继承的Base2,所以d成员中有两个虚表指针,Base1的虚表指针在前,Base2的虚表指针在后

大家仔细看上述代码,看是如何得到第二个虚表指针的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyudnGC3-1665569715827)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012173159357.png)]

​ 我们发现,Base1Base2func1都被重写了,而Derive自己的虚函数只放在了第一个虚表里面,没有放在第一个虚表里面


🚀11.菱形继承–虚继承与虚函数结合问题(了解即可)

​ 上一篇文章中,我们还遗留了一个问题,就是为什么虚基表中的初始位置放的是0,开始解决问题

#include<iostream>
using namespace std;

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()//这个是虚继承A过来的,不会生成虚表
	{
		cout << "B:func()" << endl;
	}

	virtual void func1()//这个是B自己的虚函数,会生成虚表
	{
		cout << "B:func1()" << endl;
	}
public:
	int _b;
};

// class C : public A
class C : virtual public A
{
public:
	virtual void func()//这个是虚继承A过来的,不会生成虚表
	{
		cout << "C::func()" << endl;
	}

	virtual void func1()//这个是C自己的虚函数,会生成虚表
	{
		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;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a8ceOvhf-1665569715827)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012174235194.png)]

咱们调用监视窗口和内存窗口来看一下底层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j4JCeEOn-1665569715828)(C:\Users\Cherish\AppData\Roaming\Typora\typora-user-images\image-20221012174449222.png)]

虚继承和虚基表同时存在时,那么成员变量中就既有虚表指针也有虚基表指针

而且我们发现:

虚基表的受位置放的是虚表指针对虚基表指针的偏移量,而我们上一篇文章中的例子没有虚表指针,所以首地址放的是0

🚀12.经典例题

🍉菱形继承构造函数的顺序
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;
}

​ 大家思考一下程序的输出结果,答案运行一下即可

🍉虚函数的重写
class A
{
public:
	A()
	{}

	virtual inline void func(int val = 1){ std::cout << "A->" << val << std::endl; }
	virtual void test(){ func(); }
};

class B : public A
{
public:
	void func(int val = 10000000){ std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	B*p = new B;
	p->test();

	return 0;
}

​ 上述代码中,B继承下来A,重写时,返回值,函数名,函数参数要和A中的保持一致(其中A的缺省值也给了B,B中的缺省值是无效的)

这道题是一个C++设计的Bug

🚀13.总结

函数重载,隐藏(重定义),重写(覆盖)的区别
  1. 函数重载

    条件:函数名相同函数参数不同,并且两个函数要在同一的作用域

  2. 隐藏(重定义)

    条件:继承中成员变量同名,成员函数同名即可

  3. 重写(覆盖)

    条件:子类的虚函数要与父类的虚函数完全一致(函数名,函数参数,函数的返回值)

重写的要求比隐藏严格,继承中函数名相同时,不是重写便是隐藏


🚀14.多态的面试题大全

  1. 什么是多态?
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?
  3. 多态的实现原理?
  4. inline函数可以是虚函数吗?答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
  5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的。
  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义 成虚函数。
  8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是 引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码 段(常量区)的。
  10. C++菱形继承的问题?虚继承的原理?注意这里不要把虚函数表和虚基表搞混了。
  11. 什么是抽象类?抽象类的作用?抽象类强制重写了虚函数,另外抽象类体现出 了接口继承关系。

没具体写清楚答案的,都在文章中


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UeTFS07r-1665569715828)(D:\gitee仓库\博客使用的表情包\给点赞吧.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lj6XyQvi-1665569715829)(D:\gitee仓库\博客使用的表情包\要赞.jpg)]

  • 45
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 46
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 46
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值