模板,代码变得简洁!本节将介绍泛型编程中模板的用法。
对于一组功能相同单参数类型不同的函数,在C语言中只能写多个不同名的函数来实现;
void Swapc(char& a, char& b) {
char tmp = a;
a = b;
b = tmp;
}
void Swapi(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
void Swapf(float& a, float& b) {
float tmp = a;
a = b;
b = tmp;
}
在C++中我们学习了函数重载,可以写多个同名参数类型不同的函数来实现; C++函数重载解决了函数同名的问题,但是我们还是要写多个函数,而它们仅仅只有类型不同;
void Swap(char& a, char& b) {
char tmp = a;
a = b;
b = tmp;
}
void Swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
void Swap(float& a, float& b) {
float tmp = a;
a = b;
b = tmp;
}
这种方法缺点明显:
需要根据参数类型来手动增加接受该类型的函数,这对于我们来说很麻烦; 这一组函数代码的可维护性差,要改就需要更改一组函数,也很麻烦;
基于类似这样的原因,C++提出了泛型编程的概念,我们只需要写出一个函数模板而不是具体的函数,我们直接使用这个函数模板,具体的函数由编译器自动生成;
编写与类型无关的通用代码,是代码复用的方法之一。 模板是泛型编程中的基本组成部分,分为函数模板和类模板。
函数模板代表了一个函数家族,与具体类型无关,在使用时被参数化,编译器会根据实参类型产生函数的特定类型版本
C++模板引入了新关键字template
表示模板;
对于函数模板参数类型并不是具体的类型,而是class/typename
来表示通用类型;
typename也是一个C++关键字;
template<typename T1,typename T2,...,typename Tn>
返回值类型 模板函数名(函数参数列表){
//模板函数体
}
例子 交换函数模板
//函数模板
template<class T>
void Swap(T& t1, T& t2) {
T tmp = t1;
t1 = t2;
t2 = tmp;
}
或
//函数模板
template<typename T>
void Swap(T& t1, T& t2) {
T tmp = t1;
t1 = t2;
t2 = tmp;
}
int main() {
int a = 1, b = 2;
cout << "前: " << a << " " << b << endl;
Swap(a, b);
cout << "后: " << a << " " << b << endl;
float c = 3.14, d = 9.99;
cout << "前: " << c << " " << d << endl;
Swap(c, d);
cout << "后: " << c << " " << d << endl;
return 0;
}
我们写了一个函数模板并使用它时,编译器到底做了什么呢? 函数模板只是一个模板,一张图纸,不是一个具体的函数 编译器在编译时根据实参类型顺序推导模板参数的通用类型为某一特定类型,然后根据推倒的类型生成具体的特定类型的函数(函数实例化)
//函数模板
template<typename T>
void Swap(T& t1, T& t2) {
T tmp = t1;
t1 = t2;
t2 = tmp;
}
int main() {
int a = 1, b = 2;
Swap(a, b);
float c = 3.14, d = 9.99;
Swap(c, d);
return 0;
}
不同类型的参数使用函数模板时,生成不同类型的函数称为函数模板的实例化; 分为隐式实例化和显式实例化;
由编译器在编译阶段根据我们所传实参推导函数模板参数实际类型然后生成某一具体类型的函数;
//函数模板
template<typename T>
T Add(const T& t1, const T& t2) {
return t1 + t2;
}
int main() {
int a = 1, b = 2;
double c = 3.14, d = 9.99;
cout << Add(a, b) << endl;
cout << Add(c, d) << endl;
return 0;
}
我们在使用函数模板时在模板函数名后额外加上<具体类型>
可以指定模板函数参数的实际类型,这样,编译器不在根据参数类型进行推导,而是直接根据指定类型生成对应的函数;
template<typename T>
T Add(const T& t1, const T& t2) {
return t1 + t2;
}
int main() {
int a = 1, b = 2;
double c = 3.14, d = 9.99;
Add<int>(a, b);
Add<double>(c, d);
cout << Add<int>(a, b) << endl;
cout << Add<double>(c, d) << endl;
return 0;
}
当遇到实参与模板参数类型不完全匹配时,编译器会报错,因为模板函数不允许自动类型转换;
对于
Add()
函数模板来说,我们传入两个实参类型不同,而模板函数只有一个通用类型,也只能推导出一个具体的类型,这样就总会有一个实参类型匹配不上; 这里的报错是编译器无法根据实参类型明确推导出一个具体的函数了,不涉及类型转换(发生在具体的函数传参时);
//函数模板
template<typename T>
T Add(const T& t1, const T& t2) {
return t1 + t2;
}
int main() {
int a = 1;
double b = 3.14;
Add(a, b);
return 0;
}
解决方法1:具体函数由我们指定而不是由编译器推导,不过这样会发生隐式类型转换
int main() {
int a = 1;
double b = 3.14;
Add<int>(a, b);
Add<double>(a, b);
return 0;
}
解决方法2:手动强制类型转换,确保编译器类型推导正确
int main() {
int a = 1;
double b = 3.14;
Add(a, (int)b);
Add((double)a, b);
return 0;
}
解决方法3:类模板增加参数
//多参函数模板
template<typename T1, typename T2>
T1 Add(const T1& t1, const T2& t2) {
return t1 + t2;
}
int main() {
int a = 1;
double b = 2;
Add(a, b);
Add(b, a);
return 0;
}
我们在模板那里写的函数名是模板的函数名,不能称之为实际的函数名;
实际的函数名需要在模板函数名后面<>内
顺序加上对应的实参类型;
template<typename T>
T Add(const T& t1, const T& t2) {
return t1 + t2;
}
模板函数名
Add
实际函数名可以是:
Add<char>
Add<int>
Add<float>
//......
template<typename T1, typename T2>
T1 Add(const T1& t1, const T2& t2) {
return t1 + t2;
}
模板函数名
Add
实际函数名可以是:
Add<int, int>
Add<double, double>
Add<int, double>
Add<double, int>
编译器将会优先选择我们写好的匹配的可用函数,其次才是编译器通过函数模板自动生成;
int Add(const int& t1, const int& t2) {
return t1 + t2;
}
template<typename T>
T Add(const T& t1, const T& t2) {
return t1 + t2;
}
优先调用自己实现的Add函数
int main() {
int a = 1, b = 2;
//调用自己实现的Add函数
cout << Add(a, b) << endl;
return 0;
}
指定指定使用函数模板推导生成Add函数
int main() {
int a = 1, b = 2;
//指定使用函数模板推导生成的Add函数
Add<int>(a, b);
cout << Add<int>(a, b) << endl;
return 0;
}
相同功能的实际函数可以与其函数模板同时存在; 这并不冲突,函数模板不是函数,不会与实际函数冲突; 就算模板函数实例化出具体的函数也不会和已经存在的实际函数冲突,因为我们写的函数和函数模板生成的函数虽然完成相同的功能,但是二者是完全不同的函数,函数地址也不相同;
int main() {
int a = 1, b = 2;
Add(a, b);
Add<int>(a, b);
return 0;
}
如果模板可以产生一个具有更好匹配的函数, 编译器将会选择模板实例化出的函数; **也就是说,编译器选择优先考虑是匹配问题; **
int Add(const int& t1, const int& t2) {
return t1 + t2;
}
template<typename T1, typename T2>
T1 Add(const T1& t1, const T2& t2) {
return t1 + t2;
}
int main() {
int a = 1;
double b = 3.14;
Add(a, b);
cout << Add(a, b) << endl;
return 0;
}
具体函数和函数模板都存在时,优先调用具体函数而不是函数模板; 如果我们显式使用函数模板生成的具体函数也可以正常运行得到结果; 这说明我们实现的具体函数和函数模板推导生成的具体函数是不同的函数,函数地址不同; 即我们写的具体函数与函数模板推导生成的具体函数的函数名修饰规则是不同的,否则会报错 所以编译器的原则是在最满足匹配时,优先调用显式实现的;
接下来介绍类模板; 相比函数模板,类模板使用更加广泛
类模板的出现是为了解决一些问题,与函数模板相似,解放我们,辛苦辛苦编译器; 对于一个写好的具体的类来说,其成员变量的类型是确定的某一类型,这是理所当然的;
class A {
public:
A(int a = 1)
:_a(a) {}
void Print() {
cout << _a << endl;
}
private:
int _a;
};
如果有这样的需求,保持类的结构大体不变,只改变成员变量的类型; 没有类模板的话我们只能在写一个相似的类出来,这样效率不高,还造成代码冗余,但是没有类模板也只能这样做;
class A1 {
public:
A1(int a = 1)
:_a(a) {}
void Print() {
cout << _a << endl;
}
private:
int _a;
};
class A2 {
public:
A2(float a = 1.1)
:_a(a) {}
void Print() {
cout << _a << endl;
}
private:
float _a;
};
#define定义宏
和typedef重命名类型
可以用一个通用类型代替具体的类型,更换时只需要在一处修改类型;
但是有什么问题呢?
#define TypeDate int
class A {
public:
A(TypeDate a = 1)
:_a(a) {}
void Print() {
cout << _a << endl;
}
private:
TypeDate _a;
};
typedef int TypeDate;
class A {
public:
A(TypeDate a = 1)
:_a(a) {}
void Print() {
cout << _a << endl;
}
private:
TypeDate _a;
};
通用类型是比较方便的,但是没有解决不同类型成员变量同时存在的问题,比如既需要int型有需要float型时
而typedef
只能满足其中一种类型,而不是多种;
类模板随之而来,利落的解决了这个问题,达到了我们想创建哪个类型的类都可以的目的。
template<typename T1, typename T2,...,typename Tn>
class className{
//...
}
例子
template<typename T>
class A {
public:
A(T a = 1)
:_a(a){}
void Print() {
cout << _a << endl;
}
private:
T _a;
};
int main() {
A<char> a1('a');
A<int> a2(10);
A<double> a3(3.14);
a1.Print();
a2.Print();
a3.Print();
}
类模板实例化与函数模板实例化有些差别,类模板实例化必须在类模板名字后跟<>
,<>
中写实例化的类型
,注意类模板名字不是真正的类,而实例化的结果才是真正的类(也就是类模板名加上具体的类型是真正的类名);
这里有个问题,类模板实例化为什么必须在其后加上<具体类型>
呢?
或者说为什么我们需要指定类模板实例化的类型而不是像函数模板实例化那样由编译器推导类型再实例化呢?
编译器对于类模板类型一般没有推导时机,而是需要我们对类模板显式实例化
类模板函数定义在类模板外时相比普通函数需要更多的处理:
完整地类名是类模板名+<类型>
;
指定类外函数作用域时也要使用完整的类名,在函数定义开头还需要模板参数出现;
template<typename T>
class A {
public:
A(T a = 1)
:_a(a){}
void Print();
private:
T _a;
};
template<typename T>//类模板参数
void A<T>::Print() {//完整类名
cout << _a << endl;
}
类模板分离编译会报链接错误 一般建议类模板在同一个文件中声明和定义分离,这是最好的方式了,达到了类中简洁只有函数声明,同时没有各种错误; 来看看类声明和定义分离且不在一个文件会遇到的问题:
程序运行报错 - 链接错误
test.o
文件找不到要调用的由类模板实例化的成员函数,那么为什么找不到呢?
这牵扯到了多个源文件的编译链接过程
链接错误,说明不是语法问题,而是链接时,test.o
在class.o
中找不到要调用的类模板实例化出来的函数,即类模板没有实例化处具体的函数,class.o
符号表中也就没有相应函数的地址;
为什么在类模板没有实例化出具体的函数呢?
因为类模板成员函数定义与类模板分离,
test.cpp
和class.cpp
各自的预处理/编译/汇编都是独立进行的;test.c
中有类模板的实例化(我们显式实例化的A),class.cpp
中没有类模板的实例化A,类模板函数无法实例化成具体类型的函数,class.o
符号表中也就没有类具体函数的地址; 而test.o
中虽有实例化A,但是头文件class.h
展开后,test.cpp
只有类模板函数的声明,只实例化了类的函数的声明,而函数的声明没有实际有效地址,故test.o
会在链接期间到class.o
中寻找函数有效地址(类函数实例化后才有); 但class.o
符号表中是没有具体函数的地址的,结果是test.o
哪里都找不到待调用函数有效地址,而这又发生在链接阶段,导致链接错误;
这是一个不太好(实用)的方法
既然链接错误是因为,类模板成员函数只有声明显式实例化了,那么我们也在类模板成员函数定义文件内显式实例化即可;
本例中即是在class.cpp源文件中额外加上我们所写的类模板显式实例化
template
class A<int>;
或
template class A<int>;
程序便可以正常运行;
class.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cassert>
using namespace std;
template<typename T>
class A {
public:
//构造
A(T a = 1);
//拷贝构造
A(A& a);
//赋值运算符重载
A& operator=(const A& a);
//析构
~A();
void Print();
private:
T _a;
};
class.cpp
#include "class.h"
template class A<int>;
template<typename T>
A<T>::A(T a)
:_a(a) {}
template<typename T>
A<T>::A(A& a) {
_a = a._a;
}
//说模板实参列表要与形参列表匹配
template<typename T>
A<T>& A<T>::operator=(const A<T>& a) {
_a = a._a;
return *this;
}
template<typename T>
A<T>::~A() {
_a = 0;
}
template<typename T>
void A<T>::Print() {
cout << _a << endl;
}
test.cpp
#include "class.h"
int main() {
A<int> a1(10);
A<int> a2(20);
a1.Print();
a1 = a2;
a1.Print();
return 0;
}
本例即是
class.h
或class.hpp
类模板成员函数声明和定义分离但在同一个文件,这样就不会报错了;
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cassert>
using namespace std;
template<typename T>
class A {
public:
//构造
A(T a = 1);
//拷贝构造
A(A& a);
//赋值运算符重载
A& operator=(const A& a);
//析构
~A();
void Print();
private:
T _a;
};
template<typename T>
A<T>::A(T a)
:_a(a) {}
template<typename T>
A<T>::A(A& a) {
_a = a._a;
}
template<typename T>
A<T>& A<T>::operator=(const A<T>& a) {
_a = a._a;
return *this;
}
template<typename T>
A<T>::~A() {
_a = 0;
}
template<typename T>
void A<T>::Print() {
cout << _a << endl;
}
//定义在命名空间中,防止和库里面的类名冲突
namespace weihe {
template<typename T>
class Array {
public:
//[]运算符重载
T& operator[](size_t i) {
assert(i < 10);
return _a[i];
}
private:
T _a[N];
};
}
int main() {
weihe::Array<int> a;
for (int i = 0; i < 10; ++i) {
a[i] = i;
}
for (int i = 0; i < 10; ++i) {
cout << a[i] << " ";
}
cout << endl;
for (int i = 0; i < 10; ++i) {
a[i] *= 10;
}
for (int i = 0; i < 10; ++i) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
[]运算符重载,返回类成员数组的下标为i的元素; 防止类名
Array
和标准库std
中的名字(本例中命名空间std被完全展开了)冲突,建立一个命名空间域weihe
;
assert断言
用于检查任何数组越界的情况,比编译器检查的抽查形式更加严格;
编译器对数组下标越界的检查是抽查,即在数组边界写容易检查出来,远离数组边界的越界写不容易检查出来;在数组边界读和远离数组边界读基本不被检查出来
而我们的assert断言
形式的检查绝对不放过任何可能的越界读和写,统统报错;
本节主要介绍了泛型编程基础概念 – 模板。模板的存在帮助我们减轻了负担,其中类模板需要重点关注。 下次再见!