· CSDN的uu们,大家好。这里是C++入门的第十四讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题
目录
1. 知识引入
2. 模板的使用
2.1 函数模板
2.2 类模板
3. 模板声明和定义分离
3.1 同一文件中的声明与定义分离
3.2 分文件的声明与定义分离
4. 非类型模板参数
5. 模版的特化
5.1 模板的全特化
5.2 模板的偏特化
6. 模板总结
1. 知识引入
有一天,我们在写C语言程序的时候,想要交换两个数的值,于是我们很快就写了一个交换两个整形变量的函数:
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
但是写了一会代码,你发现你又要交换两个double的值,你又要重新写一个Swap double的函数。假如后来你还需要交换其他类型的变量,那么你就需要写更多的Swap函数。是不是偷一下子就变大了。不过别慌,C++带着新的语法走来了!
2. 模板的使用
听到模板这个名词,我们就想到了显示生活中的模具,通过一个模具我们就能制作出很多产品。同理通过一个模板,我们就能实现很多功能,满足你的各种需求!C++的模板是泛型编程的基础,所谓泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。
我们来看看模板的语法:
template <typaname T1, typename T2, ··· , typaname TN>
函数/类
模板通过关键字 template 来定义,template 后面紧跟一个 尖括号 , 尖括号中加上关键字 typename (typename 换成class也行) 然后跟上模板参数,其中尖括号中的写法与函数的形参列表极为相似。其中的T1,T2 ··· 叫做模板参数,名字可以随意更改。根据template下方定义的类型,模板可以分为函数模板与类模板。
2.1 函数模板
我们先来看看函数模板的用法吧:我们就拿上面的Swap函数来试试吧,看看有了函数模板能省事多少!
//定义函数模板
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
模板起始就是将类型参数化,在上面的代码中我们将Swap函数的参数类型用模板参数代替,当我们调用函数时,编译器会根据传入参数的类型,自动为T实例化出对应的类型!
通过调试发现,即使我们传入不同类型的参数,也能够做到交换两个变量的值。
你可能会好奇,编译时怎么做到的呢?其实是这样的:当你使用 int 类型调用 Swap 函数那么编译器就会根据函数模板生成一个参数类型为 int 的函数,当你使用 double 类型调用 Swap 函数那么编译器就会根据函数模板生成一个参数类型为 double 的函数。你可能会说,这和直接些两个函数没有什么区别啊!但事实时,我们只写了一个函数模板,多余的事儿我们都交给了编译器,这不香吗?
通过观察汇编代码,可以看到确实是生成了连个不同参数类型的函数:
在C++中,通过函数模板生成函数的过程我们叫做模板的实例化。
上面我们使用函数模板的方式叫做隐式实例化 ,即不指定模板参数的类型,编译器根据参数的类型自动推导模板参数的类型。
但是并不是所有的情况都能通过隐式实例化来完成,那个时候就必须显示实例化啦:
template<typename T>
T* alloc(size_t n)
{
return new T[n];
}
int main()
{
int* a1 = alloc(10);
//显示实例化
int* a2 = alloc<int>(10);
return 0;
}
隐式实例化是会报错的,因为他无法通过你传入的参数推导出模板参数T的实际类型。
2.2 类模板
类模板和函数模板差不多,只不过定义函数的地方改成定义类。
在下面的代码中,我们定义了一个名为Stack的模板类,根据模板实例化时传入的模板参数的类型不同,我们就能实例化出来栈内元素类型不同的栈。在C语言中我们只能通过 typedef 来实现变换栈内元素的类型,但是确做不到在一个工程中同时使用 一个数据元素是 int 的栈, 一个数据元素是 double 的栈(除非你不嫌麻烦,赋值一份栈的代码)。但是有了类模板就能轻松做到。
template<typename T>
class Stack
{
public:
private:
T* _a;
int _size;
int _capacity;
};
int main()
{
Stack<int> st1;
Stack<double> st2;
}
我们可以看到:类模板在实例化的时候就只能显示实例化了!没法隐式实例化呢!即使你能够通过类中的构造函数推导出模板参数的类型 ,也不能隐式实例化呢!这是为什么呢?第 3 点会给你答案。
3. 模板声明和定义分离
3.1 同一文件中的声明与定义分离
我们来看全局的函数模板的声明与定义分离该怎么书写:
//函数模板的声明
template<typename T>
void Swap(T& x, T& y);
//函数模板的定义
template<typename T>
void Swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
在声明的时候需要有 template<typename T> 在定义的时候也需要有 template<typename T> 因为模板参数只能在它下面的第一个函数或者类中使用。
相比全局函数实现 声明与定义分离,我们更喜欢用的是类的成员函数的声明与定义分离:
我们在类中声明了一个push函数,虽然我们在外面实现了push函数,但是编译器依然报错,说明我们实现的方式有问题,你可能会说加一个类域?很棒,但是还是不完全正确!在解决这个问题之前还需要铺垫一个知识:普通类的类名即是一个类型!但是模板类的类名还是一个类型吗?我都这样说了,你肯定知道不是啦!的确不是,模板类的类型需要显示指定模板参数才是该类的类型。
像这样:Stack<int> 这就是一个Stack类型。
ok,我们现在大概能猜出类成员函数的声明与定义分离应该怎么写了吧:使得还需要在函数名的前面加上这个类的类型才行 。
template<typename T>
class Stack
{
public:
int size()
{
return _size;
}
void push(const T& val);
private:
T* _a;
int _size;
int _capacity;
};
template<typename T>
void Stack<T>::push(const T & val)
{
_a[size++] = val;
}
像上面这样我们就实现了类成员函数的声明与定义分离了呢!在实际的编程中,我们习惯将那些短小的函数直接在类内定义(默认就是内联函数了),那些比较长的函数实现声明与定义分离。
现在我们就知道为什么模板类不可能隐式实例化了嘛,因为模板类的类名不是类型,必须指定模板参数后才是类型,只有用类型才能定义变量!
3.2 分文件的声明与定义分离
我们在写C语言的时候就喜欢将函数的定义与声明分文件编写嘛!现在我们来看看模板类的成员函数如果声明与定义分文件编写会发生什么:
//
test.h /
//
#pragma once
template<typename T>
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new T[capacity];
_size = 0;
_capacity = capacity;
}
int size()
{
return _size;
}
void push(const T& val);
void pop();
private:
T* _a;
int _size;
int _capacity;
};
///
/// test.cpp /
///
#include"test.h"
template<typename T>
void Stack<T>::push(const T & val)
{
_a[_size++] = val;
}
template<typename T>
void Stack<T>::pop()
{
_size--;
}
///
/// main.cpp /
///
#include<iostream>
#include"test.h"
using namespace std;
int main()
{
Stack<int> st;
st.size(); // 不会出问题
st.pop();
st.push(1);
}
还有一个奇怪的事儿就是当你注释掉 push 和 pop 函数的调用就不会报错了!这是因为 模板 函数会按需实例化 当你没有调用这个函数时 编译器是不会实例化出来对应的函数的!
我们发现调用 push 和 pop 函数会报链接错误。这是为啥呢?链接时错误一般都是在函数有声明,没有定义的时候出现的,但是我在 test.cpp 确实是定义了 push 和 pop 函数的啊!
我们慢慢来分析,size函数没有报错是因为,size函数在声明的时候直接就定义了,编译的时候就能直接确定函数的地址。但是对于 push 和 pop 函数,因为他们的定义在另一个文件,只有在链接的时候才能确定函数的地址,当链接的时候去找 push 和 pop 函数的地址没找到,因此报了链接错误。
为什么就没找到呢?
是因为我们的 push 与 pop 的实现在另一个文件,在模板函数所在的cpp文件,不知道模板参数的具体类型,编译器不知道该实例化什么模板参数是什么类型的函数,从而无法为这两个函数确定函数地址。链接的时候自然就找不到这两个函数的地址了!
该怎么解决这个问题呢?
我们可以在 push 与 pop 所在的文件中,显示实例化模板参数,告诉编译器模板参数的类型:
告诉编译器帮我实例化模板参数为int 的函数,但是如果我们用到了 Stack<double> 那么我们还需要在这个文件中显示实例化模板参数为 double的函数!
因此 在实际的编程中我们更喜欢将模板类的类成员函数的声明与定义放在同一个文件里面!有的人为这样的文件取名为 .hpp 文件用来标识这是一个模板类!
为什么在同一个文件里面实现定义与分离就不会报错呢?我们在cpp文件中使用这个类,都需要包含这个类的头文件,包含这个头文件之后,我们就在一个cpp文件里面同时有了函数的声明与定义。当我们使用这个模板类,肯定会传入模板参数,从而确定了模板参数的类型,编译过程中,那些定义的函数就知道了模板参数的类型,只需要根据模板参数的类型实例化函数即可!
4. 非类型模板参数
模板的参数不仅仅可以通过< typename T> 将类型参数化。还允许使用整形值充当模板参数!(这里的整形值指的是整形家族,例如 int,char,unsigned int 等)
这有什么作用呢?
现在需要你实现一个静态栈,并且要求多个静态栈的大小要不相同!你会怎么做呢?使用#define 栈的大小能解决问题嘛?显然#define 和上面的typename 陷入了一样的困境,当实例化多个时都无法实现我们的需求,那看看非类型模板参数是怎么做的吧:
template<typename T, size_t N>
class Stack
{
public:
private:
T _a[N];
int _size;
int _capacity;
};
int main()
{
Stack<int, 100> st1; // 空间大小为 100 的静态栈
Stack<int, 10> st2; // 空间大小为 10 的静态栈
}
其中那个 N 就是非类型的模板参数, 观察到 N 可以直接用来当作数组的大小。因此这个 N 是一个常量哦!不允许被修改。
下面补充一下 typename 的另一层用途:
我定义了一个类:List,然后 List<T> 中将 ListNode<T> typedef 一下。在类 B 中,我们尝试去取List<T> 中的 Node 来定义一个变量,发现报出了编译错误,这是为什么呢?
原因就在于:向模板类里面取东西,编译器无法确定你取的东西是一个类型还是一个对象(例如:静态成员变量),假设你取的是一个类型,那么 List<T>::Node _node;就不会报错;单如果你取的是一个对象,这条语句就是有问题的!所以为了明确你取的东西,需要加上typename告诉编译器,你取的是一个类型! 这个语法在我后面实现STL容器时会用到!
5. 模版的特化
来看下面的代码:我们实现了一个打印的函数模板,传入什么值就打印什么值,于是我们写出了这样的代码:
template<typename T>
void Print(const T& val)
{
cout << val << endl;
}
打印都没有问题,但是我有这样一个需求,就是当你传入指针的时候,我希望打印的是指针指向的内容而不是打印指针本身,这个时候应该怎么做呢?这就要使用我们的模板特化了!
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板
5.1 模板的全特化
顾名思义全特化就是将模板参数全部都特化成具体的类型。
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
比如上面的例子:我们将模板参数T特化,当传入指针类型的时候,我们打印指针指向的内容。
template<typename T>
void Print(const T& val)
{
cout << val << endl;
}
template<typename T>
void Print(T* val)
{
cout << (*val) << endl;
}
int main()
{
Print(5);
int a = 10;
Print(&a);
double b = 20.5;
Print(&b);
}
注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。 该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化
我们来看看模板类的全特化:
template<class T1, class T2>
class Show
{
public:
Show()
{
cout << "Show(T1, T2)" << endl;
}
};
template<>
class Show<int, int>
{
public:
Show()
{
cout << "Show(int, int)" << endl;
}
};
template<>
class Show<int, double>
{
public:
Show()
{
cout << "Show(int, double)" << endl;
}
};
int main()
{
Show<double, double> s1;
Show<int, int> s2;
Show<int, double> s3;
}
我们看到我们写了特化之后就能针对指定的类型进行特殊处理了:
5.2 模板的偏特化
模板的偏特化,就是值针对一部分模板参数进行特化:
template<class T1, class T2>
class Show
{
public:
Show()
{
cout << "Show(T1, T2)" << endl;
}
};
template<class T1>
class Show<T1, int>
{
public:
Show()
{
cout << "Show(T1, int)" << endl;
}
};
template<class T1>
class Show<T1, double>
{
public:
Show()
{
cout << "Show(T1, double)" << endl;
}
};
int main()
{
Show<double, double> s1;
Show<int, int> s2;
Show<int, double> s3;
}
总之,模板的特化能够使得我们更加方便的处理特殊化的情况。这一点在我们实现STL中的优先级队列会提到!
6. 模板总结
【优点】
1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
2. 增强了代码的灵活性。
【缺陷】
1. 模板会导致代码膨胀问题,也会导致编译时间变长。其实这个问题不可避免,如果没有模板,那么就需要你手写这么多的代码了!
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。(这个是真的令人头大!)