C++模板进阶

好了,之前经过了我们多次模拟实现的分析中,也包含了很多模板的使用,相信对模板的认识也有大致的了解了,现在我们就全部完善一下模板的知识。

非类型模板参数

1.必须是常量

2.必须是整形:char,int

类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

template<class T>
template<typename T>

非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用(可以是整数,枚举,指针或者引用)

template<class T, size_t N = 1>
template<typename T,int N=10>
class Array
{
public:

private:
    T data[N];
};

看上面的代码,我们之前的做法是定义#define宏来制定N的大小。而现在可以直接利用非类型形参(注意:它不是类型,而是一个常量表达式)

可能上面的代码并不能很好展示它的优势,下面再来一下:

使用#define情况:

#define Month 12
#define Day 31
template<typename T>
class GetMonth
{
private:
	T _a[Month];
};

template<typename T>
class GetDay
{
private:
	T _a[Day];
};

int main()
{
	GetDay<int> d;
	GetMonth<int> m;

	return 0;
}

使用非类型形参:

template<typename T, int N>
class Get
{
private:
	T _a[N];
};
int main()
{
	

	Get<int, 12> month;
	Get<int, 30> day;
	return 0;
}

我们可以看出来,如果使用宏的话,你需要写两个,而使用到模板后,只需要写一个,到后面具体实例化后再根据具体的需求来实例化既可,也就是调用了才实例化,不调用就不能(在检查语法过程就报错)。

上面我们说非类型形参是一个常量,那么我们该怎么去证明它就是一个常量呢?

我们知道常量是不可以改变的,那么我们就去看看它能不能改变即可。

 

对比使用宏和非类型形参的区别:

非类型形参
语法和作用域是模板定义的一部分,只在模板内部可见(需要遵循C++:需要实例化)预处理指令(在预处理阶段进行简单的文本替换)
类型检查在实例化时严格检查是否符合其定义的类型要求不检查
代码生成复用性强,可维护性强可能导致代码膨胀
调试难度实例化过程可能复杂,错误信息难以理解,需要借助工具来调试直接是替换掉的代码,难以调试,不清晰

注意:

1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。

2. 非类型的模板参数必须在编译期就能确认结果

即:非类型形参必须是编译时常量表达式,不能使用普通的变量作为非类型实参,因为编译器在编译时才知道其确切值。

对于指针或者引用类型的非类型形参,实参必须是具有静态存储期,eg:全局变量,静态局部变量。

 

写类模板时,写下面的代码,会出现错误,为什么呢?需要加typename

因为编译器搞不清楚它是属于那里的类型,要求加上typename+类型名称,如果不加typename,因为它没有实例化,编译器只有实例化后才敢取,但是const_iterator是对象还是变量类型?不确定,因为静态/内嵌类型也可以玩这个,比如:

class A
{
public:
	int begin()
	{
		return 0;
	}

	static int const_iterator;
};

int A::const_iterator = 1;

 因此正确的应该是:

 

模板的特化

1.使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,

比如下面的情况:

 Less绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果。上述示例中,&b指向的a显然小于&b指向的b对象,但是Less内部并没有比较&b和&a指向的对象内容,而比较的是b和a指针的地址,这就无法达到预期而错误
此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化

函数模板特化


 函数模板的特化步骤:

1. 必须要先有一个基础的函数模板

2. 关键字template后面接一对空的尖括号<>

3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型

4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误

 

我们经过加上了特化后,发现,当程序走到取地址的地方,它会自己去特化的的地方调用,而不是走模板函数。 而且此时的结果是正确的了。

 注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出

也就是直接:(这种简单易懂)

bool Less(int* left,int* right)
{
    return *left<*right;
}

上面是函数模板的特化,那么类模板的特化有哪些呢?

类模板特化:

全特化

1.假设针对int double要进行特殊处理

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
private:
	T1 _d1;
	T2 _d2;
};


// 全特化
template<>
class Data<int,double>
{
public:
	Data() { cout << "Data<int,double>" << endl; }
private:
};

偏特化:一部分参数偏特化

// 偏特化 : 可能是对某些类型的进一步限制
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
private:
};

template<class T1, class T2>
class Data<T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }
private:
};

总说:严格上全特化和偏特化不属于新的类,因为类的基础是单独的。但是它提供出了不同(新)的类型。而模板是什么?是根据不同类型实例化出来,即本来需要我们自己写的东西交给了编译器来写。特化:不需要类里面的成员全部写一遍,而是根据需求来写。(一般是极小的类)

模板分离编译

 什么是分离编译

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式

 二:易错问题

程序运行步骤

预处理---编译---汇编---链接

类模板声明和定义分离会出现链接错误,具体来说,编译阶段不会报错,这是因为编译检查声明的函数参数和返回值等语法内容,只要声明的函数参数和返回值等方面能匹配,编译就能通过。
 
比如有个类模板声明了一些成员函数,在编译时,编译器只看声明的函数签名(参数类型、返回类型等)是否正确,所以有声明即使没有定义,编译也能通过。
 
而链接阶段是要找函数的实际地址来完成链接的。类模板声明相当于做了一个承诺,告诉编译器会有对应的函数实现。但因为没有对类模板进行实例化(也就是没有生成实际的对象和函数实例),所以没有函数定义也就没有函数地址,符号表无法生成完整的信息。在链接时拿着修饰后的函数(比如加上类名、模板参数等修饰)去其他文件的符号表找实际地址,由于没有实例化,就找不到对应的地址,从而产生链接错误。

为了解决这种问题:

1.将声明和定义放到一个文件 "xxx.cpp" 里面或者xxx.h其实也是可以的。推荐使用这种。

2. 模板定义的位置显式实例化。这种方法不实用,不推荐使用。

这也是我们在之前模拟的过程中,为什么不进行声明和定义分离的原因。

总结模板的优缺点: 

1.优点:复用代码,节省资源,更快的迭代开发,因此STL的产生。增强了代码 的灵活性。

2.缺点:容易造成代码的膨胀,导致代码的编译时间变长。出现模板错误时,信息提示变得非常混乱,不易定位查找。

好了,关于模板进阶的知识点分享到这里就结束了,希望你我都共进步!

最后,到了本次鸡汤环节:

 图片文字与大家共勉!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值