C++ 深入理解模板实现多态思想
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
前言
对C/C++学习感兴趣的可以看看这篇文章噢C/C++教程
最近有时间便用WTL
写了一个兼具群聊、单聊以及传输文件的聊天软件过几天应该就能更新到 C/C++教程系列 中了
所以在这里提前讲解一下WTL
中的一个非常重要的概念模板实现多态
一、模板与多态基础
再进一步了解如何用模板来实现多态前我们还是来看一看这两个概念的基础理解
1.模板
首先是模板其主要用途在于让我们程序员少写代码
比如像下面两个函数类似的一系列函数
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
就可以用模板简写为
template<class T>
T add(T a, T b) {
return a + b;
}
使用的方式如下
add<int>(1,3);
add<double>(1.1, 3.4);
add<char>('s','a');
但由于C++编译器可以自动推断参数类型所以中间的<int>
是可以省略的
这里要注意一个非常重要的问题虽然我们只写了一个模板函数但实际上并不止有一个函数
比如这里我们用了三种类型的add
函数那么编译器就会为我们分别生成三个函数
也就是说这是编译器根据我们写的模板。帮我们自动生成的函数
进一步来说模板是完全给编译器看的并不会参与到最终的可执行文件中
上面这一点便是模板的精髓
为了更加直观的理解我们来看一下最终生成的三个函数的内存地址
#include<iostream>
template<class T>
T add(T a, T b) {
return a + b;
}
int main() {
printf("%p\n", add<int>);
printf("%p\n", add<double>);
printf("%p\n", add<char>);
}
这样我们就能实际的看到最终确实是生成了三个函数因为三个函数的地址完全不同分别就代表着三个版本的add
函数
总结来说就是模板并没有减少最终的代码量它仅仅只是减少了我们程序员需要写的代码量
并且这个过程是在编译期间就完成了的这一点很重要
之所以要用模板来实现多态就是看重了它是在编译期间就完成的而不会去影响最终的可执行文件的执行时间、大小
2.多态
然后便是多态了多态是类中一个很重要的概念其主要用途就是使得函数接口统一化
比如下面这段代码
#include <iostream>
using namespace std;
class A {
public:
virtual void area() {
cout << "这是基类A" << endl;
}
};
class B : public A {
public:
void area()
{
cout << "这是子类B" << endl;
}
};
class C : public A {
public:
void area()
{
cout << "这是子类C" << endl;
}
};
// 程序的主函数
int main()
{
A* a;
a = new B();
a->area();
a = new C();
a->area();
return 0;
}
逻辑并不复杂就是B
,C
两个类都继承于A
并且在基类中我们用到了关键字virtual
定义area
为虚函数还在两个子类里面都分别重写了这个函数
因为B
C
类都继承于A
类所以我们可以用A
类指针来接收B
C
对象
从占用内存上考虑子类是继承父类的所以子类所占用的内存量肯定大于或等于父类占用内存那么子类申请一块内存赋值给父类的指针父类就不可能会内存访问越界而反过来如果用子类指针存储父类对象由于子类访问的内存大于等于父类就可能造成内存访问越界因此一般禁止这样使用
此时我们发现我们只用了一个A
调用同一个函数area
却可以完成两个类的调用
所以很多时候当我们使用别人的提供给我们的类时只要知道了它的父类有哪些函数那么其子类就必然有对应的函数
这可以极大方便类的管理、升级以及使用
虽然它的好处很多但同样也有坏处那就是它是动态绑定函数的依靠了一个叫做虚函数表的东西导致其内存占用更大运行时间更长
比如上面的代码我们就可以在调试窗口中看到其虚函数表
就是这个名为 _vfptr
的变量名称他就是指向虚函数表的函数指针而虚函数表中就存有我们的虚函数
父类指针想要正确使用子类重写的函数就必须要在这个虚函数表中进行遍历查询对应的函数地址
所以一旦你的类中有虚函数那么你的类就肯定会多出一个指针大小的内存用于存储虚函数表的地址并且最终生成的可执行文件也会变大很多字节
这取决于你的虚函数个数每多一个虚函数那么虚函数表就需要多一个指针大小的内存来存储
如果依旧不太懂的可以自行在浏览器中搜索一下有很多优秀的文章对此有解释
总结来说就是使用传统类的多态特性会导致程序效率变低最终生成的可执行文件体积变大
原因就是它生成了虚函数表、虚函数指针在程序运行过程中执行查询函数的操作
MFC就是因为大量使用的这种多态公共控件都继承于基本窗口类一般都有数十上百个虚函数所以这就导致即使你什么都没干一个MFC程序都至少有数兆大小并且运行效率还较低
二、模板实现多态
了解了上面所说的两个基本概念的优缺点之后现在我们就可以来到如何使用模板来实现多态了
因为模板就是编译期间就完成的操作如果让模板来实现多态那么就不存在运行期间去遍历虚函数表来找对应的函数也不需要开辟一个虚函数表来存储虚函数地址
既能节约内存又能提高程序运行效率是不是非常的完美
下面我们就来看一看模板实现多态的基本流程
#include <iostream>
using namespace std;
template<class T>
class A {
public:
void Show() {
T* p=static_cast<T*>(this);
p->area();
}
void area() {
cout << "这是基类A" << endl;
}
};
class B : public A<B> {
public:
void area()
{
cout << "这是子类B" << endl;
}
};
class C : public A<C> {
public:
void area()
{
cout << "这是子类C" << endl;
}
};
// 程序的主函数
int main()
{
B b;
b.Show();
C c;
c.Show();
return 0;
}
这里同样是BC两个类都继承自A类但不同点就在于A类带了一个模板变量
所以BC类在继承A的时候就需要将自己这个类型传递进去
此时三个类都写了area
函数但只有基类写了show
方法对吧
但由于BC类都是继承自A类所以它们其实也已经含有了show
方法
然后便是最重要的一步在基类的show
方法中我将this
指针转化为T
类型指针
static_cast
与强制转化基本等价唯一很大一点的区别就是强制转换可以任意使用比如B没有继承自A类强制转换仍然可以将两者指针进行转换而static_cast
无法转换两个毫不相干的东西这样就保证了传入的类型是继承自基类的否则编译会直接报错
此时这里的p指针
T* p=static_cast<T*>(this);
实际就转化为了调用者的指针以B举例子
B b;
b.Show(); //调用Show方法后完成了指针的转换指代的B那么B调用area函数也就是调用自己重写的area函数
如果现在再多出一个子类D继承于A但里面什么都没有
class D : A<D>{
}
那么当你使用D时
D d;
d.Show(); //将指针转换为D类型由于D类型没有重写area方法所以将调用继承下来的基类area方法
同样是一个show
函数能够却能根据情况选择出不同的函数调用而且还是在编译期间就完成了的
这便是模板实现多态的基本原理
三、实际应用
由于上面说的都是实现原理例子比较奇怪下面我们来直接看一看ATL中的代码
WTL是基于ATL之上开发的而ATL库则是vs开发环境中自带WTL库需要自己去下载
可以输入以下代码
#include<atlwin.h>
class MyWindow :public CWindowImpl<MyWindow>
{
};
然后右键速览CWindowImpl
类接着在跳出的文件中搜索static_cast
:
就能看到很多像上图这样的调用
- 先将
this
指针还原为子类 - 然后再调用对应的函数
- 如果子类重写了这个函数那么就调用子类的函数否则就调用父类的函数
当然这并不完全如此比如上图中的那一出是将其转化为子类后传给某个函数进行处理
不过总体逻辑是一致的在父类中操作子类以实现静态多态的目的