13.【CPP】详解多态

0x00.What is 多态

多态(polymorphism)是面向对象三大特征之一。同一行为,通过不同的子类,可以体现出来的不同的形态。举个最经典的例子就是不同的人去买票,对于这个相同的行为,学生买学生票,军人免票,成人买成人票,不同的类别执行不同的动作而呈现出来的形态叫多态。

0.1.示例代码

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

	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	virtual B* BuyTicket()
	{ 
		cout << "Student买票-半价" << endl;
		return nullptr;
	}

	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/

	~Student()
	{
		cout << "~Student()" << endl;
	}
};

0.2.多态的要求(重要)

a.被调用的函数必须是虚函数,子类要实现对父类虚函数的重写(三同:参数,返回值,函数名相同)

b.必须得用父类指针或引用去调用,传值不行。

编译器类型指的是‘=’左边的类型,运行期类型指的是‘=’右边的类型。当有继承关系时,可能发生编译期类型和运行期类型不同的情况,即编译期类型是父类类型,运行期类型是子类类型。即:父类引用指向子类对象

多态的要点

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

0.3.例外

a.三同之中返回值可以不同,但必须是父子关系的指针或引用(协变)

b.父类用virtue函数后,子类的重写可以不加virtue(接口继承)

ps:普通函数的继承是一种实现继承,而虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态

0.4区分重载,重写,重定义

重载:两函数在同一个作用域,函数名相同,参数不同,返回值无要求。

重写(覆盖):两个函数分别在派生类和基类的作用域,函数名、参数、返回类型相同(协变除外),两个函数必须是虚函数。

重定义(隐藏):两个基类和派生类的同名函数不构成重写就是重定义。

0x01.抽象类

1.1概念

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

1.2示例代码

class Car
{
public:
    virtual void Drive() = 0;
};
class Benz :public Car
{
public:
    virtual void Drive()
{
    cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
    cout << "BMW-操控" << endl;
}
};
void Test()
{
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
}

1.3意义

抽象类不能实例化出对象,一般一个类型在现实中没有对应的实体,我们就可以将它定义为抽象类。主要目的就是让子类去重写,更体现出接口继承

1.4接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。多态时接口继承会继承基类的缺省值。

例题

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

上述代码执行结果

 

调用test,test函数中传入的是基类A的指针A*this,满足多态的条件,用子类B的指针去调用func执行时符合多态条件执行的是B类的func函数。又因为多态实现时是接口继承,包含基类缺省值,所以val等于1。

目录

0x00.What is 多态

0.1.示例代码

0.2.多态的要求(重要)

0.3.例外

0.4区分重载,重写,重定义

0x01.抽象类

1.1概念

1.2示例代码

1.3意义

1.4接口继承和实现继承

例题

0x02.多态的原理

2.1虚函数表

2.2虚函数的调用

2.3打印虚函数

2.4多继承

0x03.面试概念题

3.1虚函数使用规则:

3.2其他


 

0x02.多态的原理

2.1虚函数表

1.如果父类和子类有虚函数的实现,父类和子类对象中都有一个虚函数表指针指向虚表,当构成多态时,便会去虚表中找到虚函数的地址。

2.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址

3.派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

4.注意
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存放在代码段的,只是
他的指针又存到了虚表中。另外实例出的对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段(常量区)的

5.虚表什么时候产生? --编译阶段生成的

6.虚表指针是在构造函数的初始化列表时初始化的

总结:每个有虚函数的类有虚表,类的实例对象有虚表指针(一般是前4个字节),虚函数存在于代码段

2.2虚函数的调用

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,
比如:函数重载,模板
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态。

3.在运行期间,通过传递不同类的对象,编译器选择调用不同类的虚函数:编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用哪个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。

总结:普通调用是编译时确定函数地址,而多态调用是在运行时通过查虚函数表确定函数地址

2.3打印虚函数

由于在vs下虚表中最后以nullptr结尾(g++下不是),我们可以通过for循环遍历拿到虚表指针并打印出来

typedef void(*VF_PTR)();

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; }
	void func5() { cout << "Derive::func5" << endl; }
private:
	int b;
};

void PrintVFTable(VF_PTR table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("%p\n", table[i]);
		VF_PTR f = table[i];
		f();
	}
}
int main()
{
	Base b;
	Derive d;
	/*PrintVFTable((VF_PTR*)(*(int*)&b));
	PrintVFTable((VF_PTR*)(*(int*)&d));*/
	PrintVFTable((*(VF_PTR**)&b));
	PrintVFTable((*(VF_PTR**)&d));
	return 0;
}

运行结果:

自己可以理解下这里的参数传递,非常的细!

2.4多继承

1.多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

2.为何地址不同

第一个先call寄存器找到jmp的地址,jmp到func1的地址执行func1

第二个先jmp到伪func1(ptr2中的func1),再修正ecx(存的是this指针,减的是一个base1的大小),指向ptr1的func1,再jmp到func1执行。

换句话说因为先继承的base1所以base2要修正,如果先继承base2,则base1需要修正。

0x03.面试概念题

3.1虚函数使用规则:

(1)虚函数在类中声明和类外定义的时候,virtual关键字只在声明时加上,而不能加在在类外实现上

(2)静态成员不可以是虚函数。因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

(3)友元函数不属于成员函数,不能成为虚函数

(4)静态成员函数就不能设置为虚函数(原因:静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数)

(5)析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数(尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态)

3.2其他

1.inline可以是虚函数吗?

可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

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

0x04.总结

在一个类的实例中,只会存放非静态的成员变量。 如果该类中存在虚函数的话,再多加一个指向虚函数列表指针—vptr。

1. 类对象中,只有成员变量与vptr.

2. 普通成员函数在内存的某一位置放着。它们与c语言中定义的普通函数没有区别。 当我们通过对象或对象指针调用普通成员函数时, 编译器会拿到它。怎么拿到呢?当然是通过名字了,编译器都会对我们写的函数的名字进行修饰映射,让它们变成内存中唯一的函数名。

3. 无论基类还是子类,每一种类类型的虚函数表只有一份,它里面存放了基类的类型信息和指向基类中的虚函数的指针。 某一类类型的所有对象都指向了相同的虚函数表。

 

 为何可以通过指针和引用实现多态,对象不能?

 

 

以上就是本文的全部内容,点个赞再走吧

  • 34
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值