C++多态之四种实现形式

在学习多态之前我们先来了解一下什么是多态,在文章的最后我会给出四种多态实现的示例代码,来方便大家的参考和学习。

学习和理解多态的小技巧:
​1.目的是让同一段代码或函数可以实现不同的功能
2.类的指针可以调用到自己类的方法函数

多态也是面向对象设计中对人类思维方式的一种直接模拟。

什么是多态

什么是多态?表面意思就是多种状态。在一本c++的书上看到一个感觉定义比较好的说法,多态性是指一段程序能够具备处理多种类型对象的能力,这句话的意思就是同一段代码或者同一个函数可以实现不同类型数据的相同或相似功能的实现(比如不同类型数据的打印输出),即过程相同细节不同。

多态性可以通过四种形式来实现,分别是强制多态、重载多态、类型参数化多态、包含多态

其中强制多态和重载多态属于特殊多态性,只是表面的多态性;包含多态和类型参数化多态属于一般多态性,是真正的多态。

重载多态就是利用c++的重载给同名函数赋予不同的函数体来实现多态,重载多态比较简单就不过多介绍了,最后也会给出对应的多态实现代码。

强制多态

强制多态就是将一种数据类型显示或隐式转换成另一种数据类型,即通过强制类型转换来实现的

本人之前对类型转换最多的用法就是某个数据类型与要求数据类型不匹配时,为了解决那个警告或错误就使用类型转换强转一下,让代码可以通过编译,当然要保证代码在自己的可控范围下再进行操作。

这里就用类和强制类型转换来擦出不一样的火花

我们都知道类中的成员函数,虽然是属于类的,但是成员函数并不占用类的大小。为什么会这样呢?大家细琢磨琢磨这个事如果为每个类的变量都去编译出一个函数地址,并且这些变量的相同函数实现的东西都是一模一样的,这是不是就很浪费资源,所以为了解决这个浪费就把类的方法函数拿了出来只编译出一个函数的地址,然后类定义的所有变量想访问类的方法函数时都去访问那一个地址,这样既可以节省资源也可以保证正确的函数调用,这就是类的成员函数不占用类内存的原因。

我们就可以利用类对成员方法的这种特点,来实现我们的强制多态

怎么实现呢?首先要明白一个道理如果一个变量的类型是某种自定义类型,那就可以访问这个类型下它所具备的东西。那就是让编译器认为我们这个变量就是类定义的一个变量,这样就可以去访问这个类的成员函数。那问题又来了如何去让编译器认为这个变量是通过类来声明的呢?那就是把变量转换为类的变量或指针(类型转换)

下面用一段简单的代码来实际实现一下:

class Animal
{
public:
	void cry() {
		printf("a = %d\n", a);
		std::cout << "叫~" << std::endl;
	}
	Animal(int _a) {
		a = _a;
	}
	int a;
};

class duck
{
public:
	void cry() {
		printf("a = %d\n", a);
		std::cout << "ga ga ga" << std::endl;
	}
	duck(int _a = 2)  {
		a = _a;
	}
	int a;
};

int main(int argc, char* argv[])
{
	Animal _tes(5);
	Animal* _tes2 = new Animal(4);
	_tes.cry();
	_tes2->cry();
	std::cout << "===================" << std::endl;
	((duck*)&_tes)->cry();
	((duck*)_tes2)->cry();
	return 0;
}

在这里插入图片描述

我们可以看到上面的代码也是成功的实现了我们想要的结果,两个类之间也是没有任何关系,就可以实现类中函数的调用。这就是强制多态

这种方法也是比较灵活的,下面给出一个比较变态玩法的代码,就需要大佬们私下自行实际操作看一下,用处就需要大家在今后的学习中不断领悟了。

int main(int argc, char* argv[])
{
	int sss = 77;
	((duck)sss).cry();
	((Animal*)&sss)->cry();
}

强制多态就介绍到这里,最后会给出一个比较完整的参考代码。

包含多态

包含多态是通过类的继承和虚函数来实现的,并且该种多态为运行时的多态,即动态绑定

这种方式实现的多态也是最常见的一种实现方式,理解起来也比较容易

在用该方式实现多态之前,先简要讲一下虚函数

虚函数

我们知道虚函数在类中是有一张虚函数表的,调用类的虚函数时,是通过在虚函数表中去找所调用的虚函数的地址来实现类中虚函数的调用

注意:类中只会编程生成一张虚函数表和一套虚函数的函数地址,并不是为类的每一个变量都去编译生成这些东西,只是为类的变量里面增加了一个用于存放虚函数表地址的指针变量,即类中的变量共有一张虚函数表和一套虚函数地址。所以可以看到如果类中有虚函数,则类的所占字节大小只会增加一个指针所占的字节大小(不考虑内存对齐的前提下)

通过反汇编代码可以证实上面的说法,当然也可以通过指针转换来看到类变量中虚函数表的所在地址

查看类的虚函数表地址示例代码:

class dog
{
public:
	virtual void cry() {
		printf("a = %d\n", a);
		std::cout << "wang wang wang" << std::endl;
	}
	dog(int _a = 3) {
		a = _a;
	}
	int a;
};
int main(int argc, char* argv[])
{
	dog* tes = new dog(5);
	dog* tes2 = new dog(4);
	std::cout << tes << "  " << tes2 << std::endl;
	std::cout << *(int**)tes << "  " << *(int**)tes2 << std::endl;
    return 0;
}

在这里插入图片描述

注:类存放虚函数表地址变量的位置在类的最前面,也就是类变量地址下面存放的就是虚函数表的地址,运行结果可以发现同一个类的所有变量里面的虚函数表地址都是同一个内存地址,也验证了上面的说法。接下来就来详细说明一下怎么用虚函数实现多态。

实现原理

利用虚函数实现多态的原理就是,让基类的指针变量去调用指定类虚函数表里的函数,前提是该函数在基类中也是虚函数,为什么要都是虚函数呢?这样做的目的就是让其的反汇编代码一致,调用函数时都是先通过虚函数表来调用函数的,这样就可以实现动态调用。

如果不是虚函数的话,就只能调到变量类型所在类的该函数,无法实现多态。

下面利用反汇编代码来说明一下包含多态是动态绑定:

	tes->cry();
;汇编代码
8B 45 EC             mov         eax,dword ptr [tes]  
8B 10                mov         edx,dword ptr [eax]  
8B F4                mov         esi,esp  
8B 4D EC             mov         ecx,dword ptr [tes]  
8B 02                mov         eax,dword ptr [edx]  
FF D0                call        eax  
3B F4                cmp         esi,esp  
E8 F6 A4 FF FF       call        __RTC_CheckEsp (XXXXXXXh)
	tes2->cry();
;汇编代码
8B 45 E0             mov         eax,dword ptr [tes2]  
8B 10                mov         edx,dword ptr [eax]  
8B F4                mov         esi,esp  
8B 4D E0             mov         ecx,dword ptr [tes2]  
8B 02                mov         eax,dword ptr [edx]  
FF D0                call        eax  
3B F4                cmp         esi,esp  
E8 E1 A4 FF FF       call        __RTC_CheckEsp (XXXXXXXh)

我们可以看到,上面两个类指针变量函数调用所生成的反汇编代码几乎一模一样,但是调用的函数却是不一样的,这也说明了包含多态是动态绑定的,意思就是绑定工作在程序运行阶段完成的

注:类之间存在继承是为了提升类之间的关系性,其实没有派生继承的关系,只是虚函数完全可以实现包含多态。

包含多态就介绍到这里,最后会给出一个比较完整的参考代码。

类型参数化多态

类型参数化多态是通过继承和模板来实现的

该种多态的实现原理就是把类当作参数传给基类,让基类可以知道自己要把函数转换为那个派生类中的同名函数,然后基类就可以通过强制类型转换调用到派生类中的同名函数,所以类型参数化多态是需要有继承派生关系的(可以说该种方式实现多态的思想建立在强制多态的基础之上)。

基类示例代码:

template<class T>
class test
{
public:
	void show() {
		T* p = (T*)this;
		p->Name();
	}
protected:
	void Name() { std::cout << "test" << std::endl; }
};

上面的代码定义,派生类就可以把自己当作参数传给基类,开篇我们就说过自己类的指针可以调用到自己类的方法函数,这样指针p就可以调用到参数T类类型的同名函数。

然后派生类就可以继承上面的类:

class Sub :public test<Sub>//把自己当作参数告诉给基类
{
public:
	void Name() { std::cout << "Sub" << std::endl; }
};

还是我们开篇说的小技巧,类指针可以调用到自己类的方法函数,再来理解这个做法就不会那么懵了。

这种多态,不仅仅可以把自定义类型看成参数,可扩展和挖掘学习的地方也非常多。

类型参数化多态就介绍到这里,最后会给出一个比较完整的参考代码。

示例参考代码

强制多态示例代码:

class Animal
{
public:
	void cry() {
		printf("a = %d\n", a);
		std::cout << "叫~" << std::endl;
	}
	Animal(int _a) {
		a = _a;
	}
	int a;
};

class duck
{
public:
	void cry() {
		printf("a = %d\n", a);
		std::cout << "ga ga ga" << std::endl;
	}
	duck(int _a = 2)  {
		a = _a;
	}
	int a;
};

class dog
{
public:
	void cry() {
		printf("a = %d\n", a);
		std::cout << "wang wang wang" << std::endl;
	}
	dog(int _a = 3) {
		a = _a;
	}
	int a;
};

int main(int argc, char* argv[])
{
	Animal* tes = (Animal*)new dog(5);
	Animal* tes2 = (Animal*)new duck(4);
	((dog*)tes)->cry();//把tes强转为dog类型的指针
	((duck*)tes2)->cry();//把tes2强转为duck类型的指针
    return 0;
}

重载多态示例代码:

void print(char* str)
{
	printf("%s(%d) : ", __FILE__, __LINE__);
	printf("%s\n", str);
}

void print(std::string& str)
{
	printf("%s(%d) : ", __FILE__, __LINE__);
	printf("%s\n", str.c_str());
}

void print(int a)
{
	printf("%s(%d) : ", __FILE__, __LINE__);
	printf("%d\n", a);
}

int main(int argc, char* argv[])
{
	char str[] = "hello char";
	std::string str1 = "hello string";
	int a = 1024;
	print(str);
	print(str1);
	print(a);
	return 0;
}

包含多态示例代码:

class Animal
{
public:
	virtual void cry() {
		printf("a = %d\n", a);
		std::cout << "叫~" << std::endl;
	}
	Animal() {
		a = -1;
	}
	Animal(int _a) {
		a = _a;
	}
	int a;
};

class duck :public Animal
{
public:
	virtual void cry() {
		printf("a = %d\n", a);
		std::cout << "ga ga ga" << std::endl;
	}
	duck(int _a = 2) {
		a = _a;
	}
	int a;
};

class dog :public Animal
{
public:
	virtual void cry() {
		printf("a = %d\n", a);
		std::cout << "wang wang wang" << std::endl;
	}
	dog(int _a = 3) {
		a = _a;
	}
	int a;
};
int main(int argc, char* argv[])
{
	Animal* tes = (Animal*)new dog(5);
	Animal* tes2 = (Animal*)new duck(4);
	tes->cry();//调用申请对象虚函数表里对应虚函数的地址
	tes2->cry();
    return 0;
}

类型参数化多态示例代码:

template<class T>
class test
{
public:
	void show() {
		T* p = (T*)this;
		p->Name();
	}
protected:
	void Name() { std::cout << "test" << std::endl; }
};

class Sub :public test<Sub>
{
public:
	void Name() { std::cout << "Sub" << std::endl; }
};

class Sub2 :public test<Sub2>
{
public:
	void Name() { std::cout << "Sub2" << std::endl; }
};

int main(int argc, char* argv[])
{
	Sub sub;
	Sub2 sub2;
	sub.show();
	sub2.show();
}

总结

因为包含多态需要虚函数来实现,所以用包含多态实现出来相同的功能内存占用相对于类型参数化多态来说比较大。在WTL皮肤库中实现多态大量使用的类型参数化多态而非包含多态,所以WTL生成的exe文件大小相对于MFC就小的多,随之而来的就是更多功能的实现而不会对速度造成较大的影响。

四种实现多态的方法都各有千秋,运用的场景也各不相同,也就不存在着好坏之分,本文章对多态的介绍属于基础,相信通过大家的进一步学习和理解可以实现出更为复杂代码的抽象化,让其可以满足更多的需求。

感谢观看学习,大佬们多多指点,愿明天的自己会感谢当下的努力!!!!

  • 29
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mr.Lu ‍

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

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

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

打赏作者

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

抵扣说明:

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

余额充值