Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
欢迎点赞,关注!
我们之前已经介绍并使用过模板了,现在我们说一下模板的更深层次的用法。当然了这一部分内容很简单,再加上我们不是第一次接触模板了,所以很快就能理解。
1、非类型模板参数
这个很好理解,就是我们之前模板参数都是用class和typename=后面的参数类型,但是这个非类型形参就是我们可以直接用一个常量(注意得是常量)作为模板的参数。
#include<iostream>
using namespace std;
template<class T,size_t N=100>
class A
{
public:
A()
:_a = N;
{}
private:
T _a;
};
需要注意的是,这个非类型模板参数不能是浮点数,类对象和字符串。非类型模板参数在编译期就能确定结果。
2、模板的特化
2.1函数模板特化
其实就是模板的特殊化处理。比如说我们想写一个比较函数,我们给出以下这样的函数模板:
template<class T>
bool cmp(T x, T y)
{
return x > y;
}
但这个时候问题来了,我们传两个int类型没问题,两个double类型没问题,两个指针类型呢?他就会按照两个地址来比较,可是我们不想比地址啊,我们想比地址存储的值,所以说我们直接特殊化处理,当传入两个对象为指定类型时就调用我们特化的函数模板。
#include<iostream>
using namespace std;
template<class T>
bool cmp(T x, T y)
{
return x > y;
}
template<>//这个地方不能填东西
bool cmp<int*>(int* a, int* b)
{
return *a > *b;
}
int main()
{
int a = 1;int b = 2;
double c = 1.6;double d = 1.2;
int* e = &a;
int* f = &b;
cout<<cmp(a, b)<<" ";
cout << cmp(c, d) << " ";
cout << cmp(e, f) << " ";//走特化
return 0;
}
输出结果:
当然其实这里我们直接把这个函数写出来就行(因为有现成先用现成),没必要弄个特化出来。
2.2、类模板特化
类模板特化分全特化和偏特化。全特化就是指定所有的模板参数,偏特化就是指定一部分模板参数,或者说进一步限制了参数
#include<iostream>
using namespace std;
template<class T,class V>
class A
{
public:
A()
{
_a = 100;
_b = 200;
}
private:
T _a;
V _b;
};
//全特化
template<>
class A<int,double>
{
public:
A()
{
_a = 100;
_b = 200;
}
private:
int _a;
double _b;
};
//偏特化
template<class T>
class A<T,int>
{
public:
A()
{
_a = 100;
_b = 200;
}
private:
T _a;
int _b;
};
当然他也符合有现成先用现成。
下面这种限制类型也属于偏特化:
template<class T,class V>
class A<T*, V*>
{
public:
A()
{
_a = 100;
_b = 200;
}
private:
T _a;
V _b;
};
如果我们实例化A<int*,char*>这样一个对象就会调用我们上面偏特化的代码。
接下来我们介绍一个typename特有的应用场景。我们实现这样的一个类模板:
template<class Container>
void Print(const Container& con)
{
typename Container::iterator if = con.begin();
while ()
{
...
}
...
}
我们想取适配器的迭代器,但是问题是我们的编译器不知道iterator是个什么,他可能是个变量也可能是个类(我们之前模拟实现list的时候iterator就是个类),那我们就可以在前面加上typename告诉编译器这是个类。
3、模板的分离编译
首先我们程序运行需要经过几个主要步骤:
(1)预处理
这个阶段主要是编译器来做的。编译器展开头文件,替换宏操作,去掉注释,输出一个纯文本文件(.i或.ii)。
(2)编译
使用编译工具(g++
、clang++等
)将预处理阶段输出的纯文本文件转汇编,并且检查语法。如果语法有问题就会编译报错。如果编译没问题就输出一个.s文件。
(3)汇编
使用汇编工具将我们.s文件转换为二进制机械码(让电脑能读懂的语言),然后输出一个.o或.obj文件。常用的windows系统输出的是.obj文件,大家可能也注意到过。
(4)链接
合并多个目标文件,并输出a.out或.exe文件(windows)。今天我们只要看的就是这个链接。
首先我们说什么是分离编译。我们之前模拟实现stl中的容器是非常习惯写一个.h头文件放所有函数声明,然后一个.cpp文件写所有函数,然后再写一个.cpp文件放测试函数。如果我们把模板函数的声明和定义分开放到不同文件那就没办法正常运行。
test.h
#pragma once
#include<iostream>
//光声明没定义
template<class T>//模板函数
int add(T a, T b);
int mul(int a, int b);//普通函数
testt.cpp(存放函数定义)
#define _CRT_SECURE_NO_WARNINGS 1
#include"test.h"
//光定义没声明
template<class T>
int add(T a, T b)
{
return a + b;
}
int mul(int a, int b)
{
return a * b;
}
main.cpp(测试函数)
#include"test.h"
using namespace std;
int main()
{
int x = add(1, 2);//编译报错:无法解析的外部符号
int y = mul(1, 2);//普通函数支持分离编译
cout << x << endl;
cout << y << endl;
return 0;
}
为什么会编译报错呢?我们简单理解一下:在.h文件中只有声明没有定义。在写定义的cpp文件中有定义,但是我不知道实例化成什么,没有办法正常实例化。因为编译器不确定T到底是什么东西。那为什么我们直接把定义放到.h文件中是可以运行的呢?
我们这样理解,模板函数的定义是既有定义,又有声明。但是我们如果再.h里面写了声明,把定义放到别的文件中,我们走的函数要先找声明,他找的是我们.h中的声明,而我们的定义自带的声明就失效了。
那么怎么解决分离编译的问题呢?
第一,我们显示实例化出一份来。
#define _CRT_SECURE_NO_WARNINGS 1
#include"test.h"
//光定义没声明
template<class T>
int add(T a, T b)
{
return a + b;
}
template<>
int add(int a, int b)
{
return a + b;
}
int mul(int a, int b)
{
return a * b;
}
程序可以运行,但是第一,太麻烦了。第二,我们如果传入的不是int,int呢?如果传int, double或者int char呢?我们需要特化出很多份来。大可不必。
第二,就是你别分离编译(就是这么直接)。一份优秀的代码模板函数不会分离编译。
4、模板总结
使用模板的优点:
1、模板复用了代码,节省资源,更快地迭代器开发,C++的标准模板库(STL)因此而产生。
2、增强了代码的灵活性。
缺陷:
1、会导致代码膨胀问题,导致编译时间变长。
2、出现模板编译错误时,错误信息非常凌乱,不易定位错误。
好了,今天的内容就分享到这,我们下期再见!