C++ -- 多态

1. 多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2. 多态的定义和实现

2.1 满足条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2 虚函数

学习多态之前,必须了解一个东西就是虚函数,被virtual修饰的类成员函数称为虚函数。

class A {
public:
	virtual void fun() { cout << "A::fun()" << endl; }
};

2.3 见见多态

#include <iostream>
using namespace std;

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;
}
//运行结果:
//买票-全价
//买票-半价

是否满足多态:满足继承前提,Func()函数中通过基类的引用p调用函数,被调用的函数BuyTicket()是虚函数,且派生类对基类的虚函数进行了重写。

2.4 虚函数的重写(覆盖)

什么是虚函数的重写(覆盖)?派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。上述例子中派生类中的BuyTicket()函数就是一个虚函数重写了基类的BuyTicket()虚函数。上述例子中派生类不带virtual修饰成员函数是否构成虚函数重写?构成重写。

#include <iostream>
using namespace std;

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

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写,这里叫做接口继承(因为继承后基类的虚函数被继承下来了(继承了参数,也就是拷贝了一份基类的虚函数的参数)在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。这里如果只是把基类中的virtual修饰关键字去掉能否构成虚函数重写?不能,因为不满足派生类对基类的虚函数进行重写了,看下面:

#include <iostream>
using namespace std;

class Person {
public:
	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;
}
//运行结果
//买票-全价
//买票-全价

为什么不满足多态会出现上面的情况呢?不满足多态了,就是看调用对象的类型了,这里p.BuyTicket()中的p对象是person类型的,所以两次调用都是调用的基类的BuyTicket()函数。小结:满足多态看调用对象,不满足多态看调用对象类型。如果两个函数都不加virtual修饰呢?

#include <iostream>
using namespace std;
class Person {
public:
	void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
	Student s;
	s.BuyTicket();
	return 0;
}
//运行结果:买票-半价

首先不构成多态,不满足多态满足条件的任何一条,但是这里是继承,观察得到这里就构成了继承中的重定义。再回过头来看上一个例子中为啥运行结果是:买票-全价 买票-全价;原因就是不满足多态,但是构成了继承的重定义(隐藏),但是这里是基类对象去调用的基类成员函数,这里虽然构成隐藏,但是这里没有用子类对象去调用子类的成员函数BuyTicket(),也就不构成隐藏关系,只有子类调用才会体现隐藏关系。

2.5 虚函数重写的两个例外

2.5.1 析构函数重写(虚函数名不同)

#include <iostream>
using namespace std;
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}
//运行结果:
//~Person()
//~Student()
//~Person()

上述中满足多态两个条件:通过基类Person的指针调用虚函数,同时也Person()构成了虚函数,并且子类的虚函数Studen()对父类进行了重写,这里的虚函数重写不是应该满足派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同吗,但是这里的函数名是不同的,这是因为可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这里如果把基类的析构函数中的virtual关键字去掉呢?

#include <iostream>
using namespace std;
class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}
//运行结果:
//~Person()
//~Person() (程序崩溃)

这里的原因就是没有构成多态,导致看对象类型调用函数,因为p1和p2都是Person类型,所以都是调用的~Person()函数。

2.5.2 协变(返回值类型不同)

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

#include <iostream>
using namespace std;
class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { 
		cout << "Person() -> f()" << endl;
		return new A; }
};
class Student : public Person {
public:
	virtual B* f() { 
		cout << "Student() -> f()" << endl;
		return new B; }
};
void fun(Person& p)
{
	p.f();
}
int main()
{
	Person p;
	fun(p);
	Student s;
	fun(s);
	return 0;
}

2.6 例题

2.6.1 无变更

#include <iostream>
using namespace std;
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: 以上都不正确

解析:

2.6.2 变更1

#include <iostream>
using namespace std;
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[])
{
	A* p = new B; //B* --> A* 对象赋值转换
	p->test(); //最终和无变更例子一样输出 B->1
	return 0;
}

2.6.3 变更2

#include <iostream>
using namespace std;
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
	virtual void test() { func(); }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

解析:并不满足多态

2.7 C++11 override 和 final

2.7.1 override

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

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

2.7.2 final

修饰虚函数,表示该虚函数不能再被重写

#include <iostream>
using namespace std;
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; } //声明为final函数无法被Benz:Drive重写
};
int main()
{
	Car* p = new Benz;
	p->Drive();
	return 0;
} 

2.8 重载&&重写&&重定义

3. 抽象类

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

#include <iostream>
using namespace std;
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;
	}
};
int main()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}
//输出结果:
//Benz-舒适
//BMW-操控

4. 多态的原理

4.1 引出

// 这里常考一道笔试题:sizeof(Base)是多少?
#include <iostream> 
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _a = 0;
	int _b = 1;
	char _c;
};
int main()
{
	Base B;
	cout << sizeof(B) << endl;
	return 0;
}

考虑到内存对齐,这里不应该是12字节大小吗,为什么是16字节大小呢?通过监视窗口观察:

这里我们发现多了一个_vfptr的void**类型的指针。对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function,ptr代表pointer)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

4.2 多态的原理

看下面代码思考一个问题:多态满足条件中有个条件是:必须通过基类的指针或者引用调用虚函数。那么这里怎么来实现的用引用或者指针就可以调用不同类的成员函数?为什么用普通类型就不能实现调用不用类的成员函数呢?

//构成多态
#include <iostream> 
using namespace std;
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;
}

通过观察上述问题就可以得以解决,其实就是_vfptr指针来实现调用不同类的成员函数的。

4.3 多态和非多态调用区别

4.3.1 非多态

直接按照对应对象的类型进行调用成员函数。

4.3.2 多态

上述观察到,进行执行p.BuyTicket()语句时,编译器是不知道调用哪个类的成员函数的,当完成p.BuyTicket()语句后, 编译器做出了处理工作,直接跳转到Person::BuyTicket()成员函数。怎么证明进行执行p.BuyTicket()语句时编译器是不知道调用哪个类的成员函数的?

通过调试观察到,的确执行p.BuyTicket()语句时编译器是不知道调用哪个类的成员函数的。

4.4 虚函数表

可以看出来_vfptr是一个指向虚函数表的指针,这个虚函数表里面就有很多对应的成员函数指针。虚函数表本质就是一个函数指针数组。前面我们把虚函数重写也称为覆盖,这里介绍完虚函数表再来看为什么叫做覆盖?

上述我们观察到BuyTicket()成员函数进行了重写,但是travel()和ClaimCoupon()成员函数并没有重写,此时观察到Person类维护的有一个虚函数表,Student维护的也有一个虚函数表,BuyTicket()成员函数重写了也就覆盖了,可以这么理解,原本的子类是继承父类的成员函数的,那么当前子类的虚函数表也就是父类的虚函数表,但是当子类对父类的虚函数重写后,原本的父类的虚函数就被覆盖为新的重写的虚函数,这个例子的体现就在_vfptr[0]位置上的BuyTicket()虚函数指针。 _vfptr[1]和 _vfptr[2]两个虚函数并没有被重写,所以就是继承的父类的虚函数。

4.5 回望满足条件

4.5.1 虚函数重写

我们描述了虚函数表,那么如果父类中没有虚函数的话就不能构成多态,再来看这句话不难理解了,因为没有虚函数,就没有虚函数表,当使用父类的引用和指针来调用对应的函数时,就不会在虚函数表中查找,而是直接依据类型来进行调用。

4.5.2 父类指针或引用调用

为什么指针或者引用可以调用相对应的成员函数,但是对象不行呢?对象也能够完成切片啊?原因还是在虚函数表上,对象的话就要拷贝,这里也就要拷贝虚函数表,如果拷贝就会有很大的问题:就比如子类拷贝给了父类,此时就分不清了,父类的虚函数表也是子类的了。

4.6 打印虚函数表

4.6.1 引出

#include <iostream>
using namespace std;

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;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

上述观察到虚函数表中并没有对应的子类Derive中的Func4()函数,这里Func4()不是virtual修饰的虚函数吗,为什么子类中的虚函数表没有呢?通过内存窗口看(怀疑状态):

image-20230405193522234

这里类中的函数地址怎么打印出来呢?下面的工作就是打印虚函数表。

4.6.2 打印虚函数表

虚函数表是一个函数指针数组,所以打印这里的函数指针数组即可。虚函数表是以nullptr为结束标记的。虚函数表指针是对象前四个或者八个字节。

#include <iostream>
using namespace std;
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;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};

typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
	for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
	{
		printf("[%d]:%p\n", i, table[i]);
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	//只是支持32位机器下
	//&b -> Base* -> (int*)&b -> 强制转换为int* -> (*(int*)&b) -> 拿到b对象的前四个字节 -> (_vfptr*)(*(int*)&b) -> 强制转换为_vfptr*(函数指针的指针)
	print_virtual_function((_vfptr*)(*(int*)&b));  
	print_virtual_function((_vfptr*)(*(int*)&d)); 
	//支持32和64位机器下
	print_virtual_function((_vfptr*)(*(long long*)&b));
	print_virtual_function((_vfptr*)(*(long long*)&d));
	//支持32和64位机器下
	//&b -> Base* -> (_vfptr**)&b -> Base*强制转换为_vfptr**(函数指针的指针) -> (*(_vfptr**)&b) -> _vfptr*
	print_virtual_function((*(_vfptr**)&b)); 
	print_virtual_function((*(_vfptr**)&d));
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37TauRBN-1681121198967)(https://jinyinhan.oss-cn-beijing.aliyuncs.com/QQ截图20230405195810.png)]

通过观察可以发现Func4()这个函数是在Derive这个子类的虚函数表中的,只不过这个监视窗口进行了修饰而已,其实是有的。对打印窗口优化:

typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
	for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
	{
		printf("[%d]:%p->", i, table[i]);
		_vfptr f = table[i]; //拿到指针
		f(); //调用类中的虚函数
	}
	cout << endl;
}

4.7 其他问题

  1. 虚函数表是什么阶段生成的? 编译期间已经完成,因为编译就有函数的地址了。
  2. 对象中的虚表指针什么时候初始化?构造函数的初始化列表阶段。
  3. 虚表存在虚拟内存地址的什么区域?visual studio 2019编译器是常量区(代码段)。

4.8 多继承中的虚函数表

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(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
	for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
	{
		printf("[%d]:%p->", i, table[i]);
		_vfptr f = table[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	print_virtual_function((*(_vfptr**)&d)); //打印Base1虚表
	//&d -> Derive* -> (char*)&d -> 强制转化为char* -> ((char*)&d + sizeof(Base1)) -> 通过sizeof(Base1)大小偏移量按照一个字节偏移到Base2虚表位置
	//(_vfptr**)((char*)&d + sizeof(Base1)) -> char*强制转换为_vfptr**(指针的指针) -> *(_vfptr**)((char*)&d + sizeof(Base1)) -> 解引用后_vfptr*
	print_virtual_function(*(_vfptr**)((char*)&d + sizeof(Base1))); //打印Base2虚表
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEajyGbk-1681121198968)(https://jinyinhan.oss-cn-beijing.aliyuncs.com/QQ截图20230405204128.png)]

Derive子类有两个父类所以有两个虚函数表,这样就很清晰的看出来Derive子类中的Func3()函数是放在Base1这个父类的虚表中的。另外再次观察Base1和Base2中的func1()都玩成了重写但是这里的Base1和Base2父类中的func1()虚函数地址并不一样这是什么原因呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trd514U5-1681121198968)(https://jinyinhan.oss-cn-beijing.aliyuncs.com/QQ截图20230405210905.png)]

这里根据汇编可以看的出来调用p1->func1()和p2->func1()最终都是调用Derive::func1()。需要注意的是p2->func1()函数中第一次jmp跳转到sub指令,这里的ecx寄存器就是存的this指针,通过减少8个字节找到Base1类的。

4.9 多继承例题

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

根据上面4.8的多继承虚函数表的说明可以看得出选择C,Base1和Base2是Derive父类,这里Base1先声明,先继承Base1。

4.10 动态绑定与静态绑定

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

脚踏车(crush)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值