C++多态

1.多态的概念

简单来说就是多种状态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同状态。

2.多态的定义及实现

2.1多态的构成条件

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

继承构成多态还有俩个条件:

1.必须通过基类的指针或者引用调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2虚函数

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

class Person

{

public:

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

};

2.3虚函数的重写

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

代码示例:

cpp 复制代码
#include<iostream>

using namespace std;

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

//再重写基类虚函数时,派生类的虚函数不加virtual也可以,因为继承基类的虚函数也被继承下来了。在派生类也保持了虚函数的属性
class Student :public Person
{
public:
	virtual void BuyTick() { cout << "买票-半价" << endl; }
};

void Fun(Person* ptr)
{
	ptr->BuyTick();
}
int main()
{
	Person p;
	Student s;
	Fun(&p);
	Fun(&s);
	return 0;
}

例题:

2继承基类并不是真的把基类的东西移到派生类,会先去派生类找,没有就到基类找了,所以继承的成员类型还是基类的类型,不会是派生类的类型.

首先创建B,指针p指向B,调用继承的test,test是类A的成员函数,有隐藏参数this,所以有基类的指针(A* this),且里面调用了func(),满足了多态条件,所以会以为是D,但是重写只是把派生类的函数实现覆盖基类的重写的函数实现,参数还是被重写的,重写的缺省值是1,所以为B。

虚函数重写的俩个例外:

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

派生类重写基类虚函数时,与基类函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象指针或者引用,称为协变。(只要满足基类返回基类,派生类返回派生类就行,任何一个继承体系就行)

cpp 复制代码
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; }
};

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

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,虽然函数名字不相同,看起来违背了重写的规则,但是不然,可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一成destructor。(因为多态想通过不同对象走对于的析构在继承情况下(根),所以析构都会改名字为destruct,所以才有之前析构函数会隐藏)

cpp 复制代码
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;
}

3.C++11 override 和 final

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

1.final:修饰虚函数,表示虚函数不能被重写

cpp 复制代码
class Car {
public:
	virtual void Drive() final{}
};

class Benz :public Car {

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

2.override:检查派生类虚函数是否重写某个虚函数,如果没有重写编译错误

cpp 复制代码
class Car {
public:
	virtual void Drive() {}
};

class Benz :public Car {

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

重载。覆盖(重写),隐藏(重定义)的对比

4.抽象类

概念:

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

cpp 复制代码
class Car {
public:
	virtual void Drive() = 0;
};

class Benz :public Car {
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};
void test() {
	Car* a = new Car;//会报错
	Car* p = new Benz;
}

接口继承和实现继承

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

5.多态的原理

虚函数表

cpp 复制代码
class Base {
public:
	virtual void Fun1() {
		cout << "Fun1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
}

//通过测试对象大小是8bytes,除了_b成员,还有多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后),对象中的这个指针我们叫虚函数表指针,一个含有虚函数的类至少都有一个虚函数表指针,因为虚函数的地址要放到虚函数表中,虚函数表也简称虚表

代码示例2:

cpp 复制代码
class Base {
public:
	virtual void Fun1() {
		cout << "Base::Fun1()" << endl;
	}

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

	void Fun3()
	{
		cout << "Base::Fun3()" << endl;
	}
private:
	int _b = 1;
};

class Drive :public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base::Fun2()" << endl;
	}
	void Fun3()
	{
		cout << "Base::Fun3()" << endl;
	}
private:
	int _b = 1;
};
class Derive :public Base
{
public:
	virtual void Fun1()
	{
		cout << "Derive::Fun1()" << endl;
	}
private:
	int _d = 1;
};

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

通过观察和测试,我们发现以下几点问题:

1.派生类对象d中也虚表指针,d对象由俩部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员

2.基类b对象和派生类d对象虚表是不一样,这里我们发现Fun1完成重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫覆盖,覆盖就是虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func2继承下来也是虚函数,所以放进虚表,Func3也继承下来,但是不是虚函数,不会放进虚表。

4.虚函数本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr,总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中,b.如果派生类重写了基类中的某个虚函数;c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚函数存在那里:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段,只是它的指针又存到了虚表中。另外对象中存的不是虚表,而是虚表的指针。虚表在vs是存在代码段的,不同环境可能不一样。

图解

1.观察红色箭头可知,p是指向mike对象时,p->BuyTicket在johnson的虚表中找到的函数是Person::BuyTicket。

2.观察下图的蓝色箭头可知,p是指向johnson时,p->BuyTicket在johson的虚表中找到的虚函数

是Student::BuyTicket

满足多态以后的函数调用,不是在编译时确定的,是运行起来后到对象中去找的。不满足多态是函数调用时确定好的。

cpp 复制代码
void Func(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person mike;
	Func(&mike);
	mike.BuyTicket();

	return 0;
}
// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
	...
		p->BuyTicket();
		// p中存的是mike对象的指针,将p移动到eax中
		001940DE  mov         eax, dword ptr[p]
		// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
		001940E1  mov         edx, dword ptr[eax]
		// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
		00B823EE  mov         eax, dword ptr[edx]
		// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
		以后到对象的中取找的。
		001940EA  call        eax //先找到才call
		00头1940EC  cmp       esi, esp
}
int main()
{
	...
		// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
		用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
		mike.BuyTicket();
		00195182  lea         ecx, [mike]
		00195185  call        Person::BuyTicket(01914F6h)
		...
}

6.动态绑定和静态绑定

1.静态绑定也叫前期绑定,在程序编译期间确定了程序的行为,也叫静态多态,比如函数重载

动态绑定也叫后期绑定,是程序在运行时,根据具体得到的类型确定程序的行为,调用具体的函数,动态多态。

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

单继承中的虚函数表

观察监视窗口看不见func3和func4,被隐藏了。可以通过代码来看到它们地址。

cpp 复制代码
class Base {
public:
	virtual void fun1() { cout << "Base::func1" << endl; }
	virtual void fun2() { cout << "Base::func2" << endl; }
private:
	int _a;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func2() { cout << "Derive::func2" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int _b;
};




typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

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

多继承的虚函数表

cpp 复制代码
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 printvtable(vfptr vtable[])
{
	cout << " 虚表地址>" << vtable << endl;
	for (int i = 0; vtable[i] != nullptr; ++i)
	{
		if (vtable[i] == nullptr)
			break;
		printf(" 第%d个虚函数地址 :0x%x,->", i, vtable[i]);
		vfptr f = vtable[i];
		f();
	}
	cout << endl;
}
int main()
{
	derive d;
	vfptr* vtableb1 = (vfptr*)(*(int*)&d);
	printvtable(vtableb1);
	vfptr* vtableb2 = (vfptr*)(*(int*)((char*)&d + sizeof(base1)));
	printvtable(vtableb2);
	return 0;
}

观察下图:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

相关推荐
努力学习编程的伍大侠2 分钟前
基础排序算法
数据结构·c++·算法
数据小爬虫@17 分钟前
如何高效利用Python爬虫按关键字搜索苏宁商品
开发语言·爬虫·python
ZJ_.19 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
Narutolxy24 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader31 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默42 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
yuyanjingtao1 小时前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
Code哈哈笑1 小时前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_433618441 小时前
shell 编程(二)
开发语言·bash·shell