文章目录
- 前言
- 一、模板与多态基础
- 1.模板
- 2.多态
- 二、模板实现多态
- 三、实际应用
前言
对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;
}
这里同样是B,C两个类都继承自A类,但不同点就在于A类带了一个模板变量
所以B,C类在继承A的时候,就需要将自己这个类型传递进去
此时三个类都写了area
函数,但只有基类写了show
方法对吧
但由于B,C类都是继承自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
指针还原为子类 - 然后再调用对应的函数
- 如果子类重写了这个函数,那么就调用子类的函数,否则就调用父类的函数
当然这并不完全如此,比如上图中的那一出,是将其转化为子类后,传给某个函数进行处理
不过总体逻辑是一致的:在父类中操作子类,以实现静态多态的目的