C++模板

时间:2021-07-05 00:36:02

写在前面

现在我们来开启C++不同于C语言的地方.大家都知道C语言没有标准的数据结构相关的库,而C++存在STL,原因就是C++支持泛型编程,这是我们今天需要知道重点,先来简单的认识一下.


泛型编程

**所谓的泛型就是不再是针对一种特定的类型进行分析,而是关注于更加广泛的类型.**大家可能不太理解这句话,我用一个简单的例子来和大家解释,在C语言中我们要写一个简单的两个数简单的交换,我们需要根据这两个数的类型,写出不同的函数.由于C语言中不支持函数重载,我们的函数名还不能相同,非常的不便利.

C++的重载是可以解决一部分问题的,但是函数重载仅仅是类型不同,代码复用率比较低,只要有新类型出现时,我们就需要自己增加对应的函数,而且一般而言,重载函数代码的可维护性比较低,一个出错可能所有的重载均出错 ,毕竟我们都是按照一个思路写的.

泛型就不一样了,我们可以用一个简单的泛型函数就可以解决上面的问题.大家可能现在还看不懂下面的代码,我们先知道有这个东西就可以了,后面一个一个和大家解释.

template<class T>
void swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段.模板是泛型编程的基础.我们都知道活字印刷术,泛型函数就相当于一个模板,我们需要什么类型,就把什么类型传给编译器,让编译器通过模板和我们给的类型自动的把这个函数给写出来.

模板分类

模板主要分为两类,本质都是一样的,

  • 函数模板
  • 类模板

泛型关键字

C++提供两个关键字来帮助我们学习模板,下面的代码就是.

下面代码的意思是声明一个函数和一个类是一个模板,其中T1,T2,T3...代表是数据类型,我们使用的时候需要告诉编译器,下面就是我们告诉编译器的.

template<typename T1,typename T2 ...,typename T3>

这里我要声明一下,一些旧的语法当中,我们也可以使用class替换typename,混用也是可以的,现在我们认为它们作用一样,后期再说区别.**切记不能使用struct代替class **

# 函数模板

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本.

template<typename T1, typename T2,......,typename Tn>
返回值 函数名(参数列表)
{
}

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器 .我们先来看看函数模板,依旧先用用交换函数来举例,后面有很多例子.

#include <iostream>
using std::cout;
using std::endl;

template <class T>
void Swap(T &left, T &right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1;
int b = 2;
Swap(a, b);
cout << "a: " << a << " b: " << b << endl;

double d1 = 1.1111;
double d2 = 2.2222;
Swap(d1, d2);
cout << "d1: " << d1 << " d2: " << d2 << endl;
return 0;
}

C++模板

函数模板实例化

什么是模板的实例化呢?用不同类型的参数使用函数模板时,称为函数模板的实例化 .就是我们在使用泛型函数的时候,我们传递给编译器具体的类型,由编译器自动推导出的具体函数的过程.

template <class T>
void Swap(T &left, T &right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1;
int b = 2;
Swap(a, b);
cout << "a: " << a << " b: " << b << endl;
return 0;
}

C++模板

这就是模板的实例化,编译器通过我们给的数据类型(或者自动推导)自动生成相应的函数.函数模板就相当于一个模子,它本身没有地址,当我们传入参数后,编译器自动通过模板自动推出一个与参数匹配的函数,这时这个函数才会有地址,或者有一个确定的函数.但是模子本身没有改变.

我们先来解决一个不太困难的问题,提供不同的参数类型进行模板的实例化,是调用同一个函数吗?这个肯定不是的.大家先来看看现象.

int main()
{
int a = 1;
int b = 2;
double c = 1.0;
double d = 2.0;
Swap(a, b);
cout << "=========" << endl;
Swap(c, d);
return 0;
}

C++模板

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此

C++模板

这里和大家说一下,以后再C++中,我们不需要再写交换函数了,C++标准库里面写了,以后我们可以直接使用了.

C++模板

隐式实例化

一般而言,函数模板的模板参数一般是函数参数的,也就是我们使用模板函数的时候,我们给其传入参数,编译器会自动推导出我们参数的类型,这个时候不需要我们显示的告诉编译器我们参数是什么类型,可以自动推导出来.

template<class T>
T add(T& a, T& b)
{
return a + b;
}

这是我们提供给编译器的一个模板,一旦我们决定调用add函数,这里就会发生一件事.我们传入了参数,编译器会通过参数来自动推导数据类型,然后自动生成一个相应的函数.

int main()
{
int a = 1;
int b = 2;
int ret = add(a,b);
cout << ret << endl;
return 0;
}

C++模板

这还有一个问题,我们在对函数的模板传入常数的时候,这里面会存在下面的问题.

template<class T>
T add(T& a, T& b)
{
return a + b;
}

int main()
{

int ret = add(1,2);
cout << ret << endl;
return 0;
}

C++模板

注意,这是我们模板函数的实现的问题,我们要是使用常数传参,参数是引用的话,是需要加上const的,所以说我们一定要好好的设计参数.

template<class T>
T add(const T& a, const T& b)
{
return a + b;
}

int main()
{

int ret = add(1,2);
cout << ret << endl;
return 0;
}

C++模板

显式实例化

现在我们存在了一个问题,难道编译器一定会通过参数推导出类型吗?看看下面的函数.我们传入的参数的类型已经确定了,那么你告诉我返回值的类型该怎么确定?

template<class T>
T func(int n)
{
return n;
}

这里就需要显示的调用这个函数,编译器不知道,但是我们可以告诉它啊,看看下面的用法.

template<class T>
T func(int n)
{
return n;
}

int main()
{
int ret = func<int>(10);
cout << ret << endl;
return 0;
}

C++模板

既然存在了两种实例化方式,那么我想知道如果同时存在,哪个起决定作用?答案很明显,显示起决定作用,编译器有限选择显示的,这一点要记好.

template<class T>
T func(T n)
{
return n;
}

C++模板

模板参数缺省

模板参数是可以有缺省值的,也就是说,我们可以通过提供给模板的缺省值,不过这个缺省值是一个类型.看看用法,你就会发现和函数参数的用法是很像的.

template<class K,class V>
void func()
{
cout << sizeof(K) << endl;
cout << sizeof(V) << endl;
}

int main()
{
func<int,double>();
return 0;
}

C++模板

模板参数是支持缺省的,这里先看全缺省.

template<class K = int,class V = char>
void func()
{
cout << sizeof(K) << endl;
cout << sizeof(V) << endl;
}

int main()
{
//func<int,double>();
func();
return 0;
}

C++模板

看一下部分缺省,后面我还要说.

template<class K,class V = char>
void func()
{
cout << sizeof(K) << endl;
cout << sizeof(V) << endl;
}

int main()
{
func<int>();
return 0;
}

C++模板

我们想模板参数的缺省要求是不是要和缺省函数必须从右向左,这里也和大家演示一下.我们发现是不用从右开始缺省的.

template<class T = int,typename K>
T func(int n)
{
return n;
}

int main()
{
int ret = func<int,double>(10);
cout << ret << endl;
return 0;
}

C++模板

但是这里我们调用的时候是有一点问题的,我们注意一下就可以了.

template<class T = int,typename K>
T func(int n)
{
return n;
}

int main()
{
int ret = func<int>(10); // 这让编译器产生一种错觉
cout << ret << endl;
return 0;
}

C++模板

匹配规则

我们存在一个问题,先来分析一下,下面的两个是否可以同时存在.这是可以的,模板又不是函数,不会形成重复.

template<class T>
T add(T& left, T& right)
{
cout << "T add" << endl;
return left + right;
}

int add(int& left, int& right)
{
cout << "int add" << endl;
return left + right;
}

既然可以存在了,那么这个地方又有一个问题,编译器优先调用哪个?

int main()
{
int x = 1;
int y = 2;
add(x,y);
return 0;
}

C++模板

我们直接看现象就可以发现,优先调用函数,而不是函数模板.这个我们还是可以理解的,既然有现成的,还费力气去自己生成干嘛.

那么我们如何让编译器调用模板?这里也有一个方法,使用显式实例化直接调用.

C++模板

类模板

我们先来认识一下类模板.我们发现它们和普通的类没有什么区别,就是把具体数据类型改成泛型了.

template<class T>
class Stack
{
public:
Stack(int cap = 4);
~Stack();
void push();

private:
T* elme;
size_t size;
int cap;
};

类模板格式

我们先来看一下类模板的格式.

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};

类包含类的定义和里面的成员函数定义.如果我们想类里面的成员函数的声明和定义不分离,这样就可以按照下面的方法做就可以了.

class Student
{
public:
Student()
{
_cap = 4;
_elem = new T[_cap];
_size = 10;
}
~Student();
private:
T* elem;
szie_t _size;
szie_t _cap;
};

但是如果成员函数的声明和定义分离,我们就不能这么做了.我们需要对类模板的成员函数进行一定的修饰和突破类域可以这么说,每个成员函数都会带着template\和Student\::这是编译器要求的,我们按照这样作就可以了.

template<class T>
class Student
{
public:
Student();
~Student();
private:
T* elem;
szie_t _size;
szie_t _cap;
};

template<class T>
Student<T>::Student()
{
_cap = 4;
_elem = new T[_cap];
_size = 10;
}

template<class T>
Student<T>::~Student()
{
if (elem)
{
delete[] elem;
_size = 0;
_cap = 0;

}
}

类模板实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类 .

我们在使用的时候,记住显示实例化就可以了,我们看看用法.

int main()
{
Student<char> student;
return 0;
}

C++模板

模板参数

一般而言,模板参数分类型形参非类型形参

  • 类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
  • 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

类型形参

上面我们说的模板参数都是类型形参,这里说点不一样的.

在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅.可以这么说模板函数不允许自动类型转换,但普通函数可以进行自动类型转换.

template<class T>
T add(const T& left, const T& right)
{
cout << "T add" << endl;
return left + right;
}

int main()
{
add(1, 2.2);
return 0;
}

C++模板

该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错.我们可以通过下面的方法来解决问题

C++模板

非类型模板参数

在谈着这个知识之前,如果我们想定义一个固定的大小的栈,也就是静态栈,看看下面的做法怎么样?

#define N 100
template<class T>
class stack
{
private:
T _elem[N];
size_t top;
};

我们觉得可以啊,这不就是一个模板的静态栈吗?没问题,我们实例化模板类存储想要的类型栈就可以了,皆大欢喜.

int main()
{
stack<int> s1;
stack<double> s2;
return 0;
}

是的,我们已经完成要求了,但是现在我又提了一个要求,我看那double类型的栈有点大,可能有点浪费了,你去修改一下,你过去了,把宏的大小修改成了50,可以了.但是int类型的栈的不愿意了,我本来是够的,你改了我怎么办.这就是现在的问题.非类型模板参数可以很完美的解决掉这个问题,你想要多少就是开辟多少.

template<class T, size_t N>
class stack
{
private:
T _elem[N];
size_t top;
};

int main()
{
stack<int,100> s1;
stack<double,50> s2;
return 0;
}

非类型模板参数限制

注意,非类型模板参数是一个常量,是不能够修改的.浮点数、类对象以及字符串是不允许作为非类型模板参数的.并且非类型的模板参数必须在编译期就能确认结果.

template<class T, size_t N>
class stack
{
public:
void f()
{
N = 100;
}
private:
T _elem[N];
size_t top;
};

int main()
{
stack<int, 50> s;
s.f();
return 0;
}

C++模板

这里还有一个要注意的,类型必须是一个整型.

template<class T, float N>
class stack
{
private:
T _elem[N];
size_t top;
};

int main()
{
stack<int, 50.0> s;
return 0;
}

C++模板

模板特化

所谓的特化就是对一些类型进行特殊化处理,分为函数模板特化和类模板特化,其中,主要看看类模板特化。

函数模板特化

通常情况下使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板 我们用一下之前的Date类的代码,比较一下日期类的大小,大家看看情况.

template<class T>
bool Less(T left, T right)
{
return left < right;
}

int main()
{
Date d1(2022, 7, 18);
Date d2(2022, 7, 17);
cout << Less(d1, d2) << endl;
return 0;
}

C++模板

说实话,这样的话我们还是可以接受的,但是如果我们的日期类是new出来的的呢,这个代码就不太适合了.

int main()
{

Date* p1 = new Date(2022, 7, 18);
Date* p2 = new Date(2022, 7, 19);
cout << Less(p1, p2) << endl;

delete p1;
delete p2;
return 0;
}

C++模板

这里我们可以思考一下,我们传入的是指针,该指针指向一个日期类,编译器是会自动推导出一个指针类型,然后去比较,此时比较的是地址,所以这个结果是不确定的.我也是运行的几次才得到上面的这个结果,我们不害怕结果出错,就害怕结果不确定.

那我们是否可以通过一个方式解决这个问题呢,现在的函数模板特化就可以解决这个问题,我们让编译器碰到指针的时候去比较指针解引用的东西就可以了.

函数模板的特化步骤:

  1. 必须要先有一个基础的函数模板
  2. 关键字template后面接一对空的尖括号<>
  3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 特化
template<>
bool Less<int*>(int* left, int* right)
{
cout << "Less<int*>" << endl;
return *left < *right;
}

int main()
{
int a = 0;
int b = 1;
cout << Less(&a,&b) <<endl;
return 0;
}

C++模板

其中,步骤三不一定非要做的.

template<>         
bool Less(Date* left, Date* right)
{
return *left < *right;
}

C++模板

我们看看编译器到底是用了哪一个模板,通过调用我们发现,使用的是我们特殊化处理的.

template<>
bool Less(Date* left, Date* right)
{
cout << "bool Less(Date* left, Date* right)" << endl;
return *left < *right;
}

C++模板

有的人就会感到疑惑了,函数模板是可以和一摸一样的函数一起存在的,而且还是优先调用自己实现的具体函数.我看这个函数模板特化没有什么用处,是的,我们很少对函数模板进行特化,这就像编译器吃饭一样,有外卖最好,我们直接调用函数,吃泡面也行,这就是特化,实在不行,我自己去做饭吃,这就是编译器自己实例化模板.

类模板特化

可以这么说,模板特化主要是为了类模板特化,这里面也是存在一些问题的.下面的模板特化我们看着没有什么用处,实际上我们我们现在还没有遇到合适的例子,大家先记住,后面遇到话有点印象就行.

全特化

全特化即是将模板参数列表中所有的参数都确定了

template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Data<int, char>
{
public:
Data() { cout << "Data<int, char>" << endl; }
private:
int _d1;
char _d2;
};

int main()
{
Data<int,char> d1;
return 0;
}

C++模板

偏特化

上面的特化是全特化,这里还有一个类似缺省函数额半特化,也叫作偏特化.有大概两个特性.其中之一将模板参数类表中的一部分参数特化 ,等下注意一下格式

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

半特化的话,只要是已经显式类型的模板参数和我们要实例化的对象的参数完全对应上,优先调用该半特化的模板进行实例化,

int main()
{
Data<int, int> d1;
Data<char, int> d2;
Data<double, int> d3;
Data<float, int> d4;
return 0;
}

C++模板

我们测试一下偏特化是不是可以具体的类型在前面.

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

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

int main()
{
Data<int, int> d1;
Data<char, int> d2;
return 0;
}

C++模板

下面我说另外一个特性,偏特化是针对模板参数更进一步的条件限制所设计出来的一个特化版本.例如下面的例子,凡是指针就去偏特化.

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

private:
T1 _d1;
T2 _d2;
};

// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data<T1 *, T2 *>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }

private:
T1 _d1;
T2 _d2;
};

int main()
{
Data<int *, int *> d3; // 调用特化的指针版本
return 0;
}

C++模板

下面我们做一个总结,看看下面的代码.

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

private:
T1 _d1;
T2 _d2;
};

template <>
class Data<int, int>
{
public:
Data() { cout << "Data<int, int>" << endl; }

private:
int _d1;
int _d2;
};

// 两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data<T1 *, T2 *>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }

private:
T1 _d1;
T2 _d2;
};
// 两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data<T1 &, T2 &>
{
public:
Data(const T1 &d1, const T2 &d2)
: _d1(d1), _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}

private:
const T1 &_d1;
const T2 &_d2;
};
void test2()
{
Data<double, int> d1; // 调用特化的int版本
Data<int, double> d2; // 调用基础的模板
Data<int *, int *> d3; // 调用特化的指针版本
Data<int &, int &> d4(1, 2); // 调用特化的指针版本
}

int main()
{
test2();
return 0;
}

C++模板

模板编译分离

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

上面我们已经在C语言里面就说过了,如果我想让模板的生命和定义分离呢?这里我们要分为两个情况进行考虑.

这个问题需要分开来看.同一文件中,既然模板可以认为是一个函数,是可以声明和定义分离的,不过我们都需要加上模板关键字.

C++模板

在不同文件中,注意,我们要知道一件事情,模板不支持声明和定义在两个文件中的,会出现链接错误.很多时候,我们都是把模板声明和定义放在同一个文件中,有的把这文件的后缀默认写为**.hpp**,这样可以告诉我们这是一个存在模板的头文件,但不是必须的,一种习惯而已,看个人爱好.

C++模板

这里简单的说一下,为什么报错.我们知道所有的链接错误都是符号表里面找不到函数的地址,这我们在C语言编译链接的时候就已经说过了.我们知道,所谓的函数实现是在源文件中的,那就是这个源文件会进行预处理,编译,汇编等最终形成目标文件,此时每一个目标文件都会形成一个符号表来保存函数的地址以供后面的链接.可是我们知道模板函数可不是一个函数,他是一个模板,此时符号表里面是不可能存在所谓的地址的,这里一定会报连接错误.

那么我们应该如何解决呢?很简单,这里我提供两个方法.

  • 模板函数实现和定义分离,但是需要在一个文件中,这个文件应该是头文件.
  • 在不同的文件中,我们需要将我们需要的类型在模板函数的显示的告知

第一个方式我们不谈了,就是在一个头文件中使用就可以了,这里说一下第二种.

template<class T>
void Swap(const T& x,const T& y)
{
std::cout << "Swap(const T& x,const T& y)"<<std::endl;
}

template
void Swap<int>(const int& x,const int& y);

如果我们想要调用,很简单.

#include <iostream>
#include "test.h"
using namespace std;


int main()
{
Swap<int>(1,2);
return 0;
}

C++模板

但是我们这样作就不可以了.

int main()
{
Swap<int>(1,2);
Swap<double>(1,2);
return 0;
}

C++模板

这我们可以理解,如果我们定义和申明进行分离,我们每一次使用需要检测在是否告知编译器模板参数的具体的类型.类模板也是有这个问题的.

// test.h文件
template <class T>
class A
{
public:
A() {}
~A() {}

void func();
};

// file.cc 文件
template<class T>
void A<T>::func()
{
std::cout << "void A<T>::func()"<<std::endl;
}

//test.cc文件
#include "test.h"
using namespace std;
int main()
{
A<int>a;
a.func();
return 0;
}

C++模板

我们也是可以按照上面的方法尽心解决的,不过不建议分离.

template<class T>
void A<T>::func()
{
std::cout << "void A<T>::func()"<<std::endl;
}

template
void A<int>::func();

C++模板