文章目录
一、泛型编程
1.泛型编程的定义
泛型编程就是编写与类型无关的通用代码,是方便高效的代码复用的一种手段,而想要泛型编程就要了解模板,模板又分为函数模板和类模板。
2.泛型编程可以解决的问题
例如实现一个类型通用的swap交换函数,实现int、double等等多种类型的交换。
2.1常规写法就是利用函数重载
void swap(int& left,int& right)
{
int temp=left;
left=right;
right=temp;
}
void swap(double& left,double& right)
{
double temp=left;
left=right;
right=temp;
}
......
......
......
这样写代码的弊端就是只要增加一种类型就需要写一份同样的代码,一个代码出错多有的代码都需要修改,代码复用率比较低。
2.2用泛型编程就可以完全规避这些问题,下面是用模板写的代码。
template<class T>
void swap(T& left,T& right)
{
T temp=left;
left=right;
right=temp;
}
只需要写一个模板函数就可以替代所有类型的函数,下面介绍用法。
二、函数模板
1.函数模板定义
函数模板代表就了一个函数家族,这个函数没有具体的类型,在使用时才被具体的参数化,根据实参类型产生函数的特定类型版本。
2.函数模板原理
函数模板就是相当于一个印刷工具,它本身并不是函数,是编译器根据具体使用类型产生的具体类型函数的工具,本质就是本来该我们自己完成代码重复编写不同类型函数,现在交个了编译器。
编译器根据函数调用传入实参类型来推演生成对应类型的函数以供调用。
3.函数模板格式用法
定义模板参数T1、T2的关键字还可以是typename,不过class常用,建议使用T1,T2代表类型,返回值类型,参数类型都可以使用如果先要在函数中使用多种不同的类型就可以定义多个模板参数template <class T1,class T2,…>返回值类型 类名(形参列表){}
示例:
template <class T>
void swap(T& left,T& right)
{
T& temp=left;
left=right;
right=temp;
}
int main()
{
int a1=1,a2=2;
double d1=1.1,d2=2,2;
swap(a1,a2);
swap(d1,d2);
cout<<a1<<a2<<d1<<d2;
return 0;
}
4.函数模板的实例化
4.1隐式实例化
就是让编译器根据实参推演模板参数的实际类型
template <class T>
T add(const T& a,const T& b)
{
return a+b;
}
int main()
{
int a1=1,a2=2;
double d1=1.1,d2=2.2;
//add就是隐式实例化
int sum=add(a1,a2);
double _sum=add(d1,d2);
return 0;
}
4.3显示实例化
当实参存在歧义,编译器不知道将参数实例化成什么类型时,就要用到显示实例化,显示实例化就是告诉编译器将实参实例化成什么类型
#include<iostream>
using namespace std;
template <class T>
T add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a1 = 1, a2 = 2;
double d1 = 1.1, d2 = 2.2;
//因为实参存在歧义,编译器不知道将参数实例化成什么类型
//所以显示实例化就是告诉编译器将实参实例化成什么类型
int sum = add<int>(d1, a2);
double _sum = add<double>(a1, d2);
return 0;
}
三、类模板
1.类模板的定义
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
2.类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
// Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
四、模板的匹配
一个有具体类型的函数或者类可以和一个同名的泛型函数或者类同时存在,编译器会优先匹配具体类型的函数或者类.
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
数
}
五、非类型模板参数
1.定义
模板参数分类类型形参与非类型形参。
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用,它一但定义不可修改.;
浮点数、类对象以及字符串是不允许作为非类型模板参数的。非类型的模板参数必须在编译期就能确认结果
2.使用场景
在定义一个结构体时,希望在声明的时候就固定结构体的大小就可以用非类型模板参数,比如说位图结构.
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template <size_t N>
class bitSet
{
public:
bitSet()
{
_bit.resize(N / 8 + 1);
}
//位图映射位置设置1
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bit[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bit[i] &= ~(1 << j);
}
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bit[i] & (1 << j);
}
private:
vector<char> _bit;
};
浮点数、类对象以及字符串是不允许作为非类型模板参数的。
非类型的模板参数必须在编译期就能确认结果
六、模板特化
格式:
- 必须要先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
1.全特化
在参数模板类型的基础上对某一具体类型单独进行特殊处理.
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;
};
void TestVector()
{
Data<int, int> d1;
Data<int, char> d2;
}
2.偏特化
2.1部分类型参数特化
有两个或者两个以上的模板参数,只特化一个类型的参数
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data<T1, T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data() {cout<<"Data<T1, int>" <<endl;}
private:
T1 _d1;
int _d2;
};
2.2对参数进一步限制
//两个参数偏特化为指针类型
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); // 调用特化的指针版本
}
七、模板声明定义分离
模板的声明定义不在同一个文件时,在头文件对模板类或者函数进行声明后进行显示实例化,但是这种方法未被了模板的初心用意,没有简明化编程.
一般模板的定义和声明都在同一个文件,可以分离.类的成员函数如果短小可以在类体中直接定义,会被编译器择优优化成联想函数,比较长的可以在类体外定义.
2.位图分离示例
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template <size_t N>
class bitSet
{
public:
bitSet();
void set(size_t x);
void reset(size_t x);
bool test(size_t x);
private:
vector<char> _bit;
};
template <size_t N>
bitSet<N>::bitSet()
{
_bit.resize(N / 8 + 1);
}
template <size_t N>
void bitSet<N>::set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bit[i] |= (1 << j);
}
template <size_t N>
void bitSet<N>::reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bit[i] &= ~(1 << j);
}
template <size_t N>
bool bitSet<N>::test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bit[i] & (1 << j);
}
2.报链接错误的原理
从代码到可执行文件会经历四个阶段:
预处理:函数定义在头文件展开,宏替换,条件编译,去掉注释
编译:检查语法,生成汇编代码
汇编:汇编转二进制
链接:链接库合并源文件.
多个源文件在链接之前没有交互,非模板函数在编译时会直接生成汇编指令,而模板函数因为没有办法实例化函数而无法生成汇编指令.在链接的时候找不到模板函数的地址.