这里写目录标题
- 一、泛型编程与模板
- 二、函数模板
- 1.概念
- 2.原理
- 3.函数模板的实例化
- 3. 模板实现复数类的加法
- 4.模板参数的匹配原则
- 三、类模板
- 1.定义格式
- 用类模板实现顺序表
- 2.非类型模板参数
- 四、模板的特化
- 1.函数模板的特化
- 2.类模板的特化
- 全特化
- 偏特化
- 五、分离编译
- 1.模板不支持分离编译
- 2.解决方案:.hpp文件
- 3.模板总结
一、泛型编程与模板
泛型编程:编写与类型无关的代码,是代码复用的一种手段。模板是泛型编程的基础。
二、函数模板
1.概念
- 函数模板是一个通用的函数,该模板与类型无关,其函数类型和形参类型不确定,在使用时被参数化,根据实参类型产生函数的特定类型版本。
- 函数模板的格式:
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){}
利用函数模板写一个通用的交换函数:
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
函数模板适用于函数体相同、函数的参数个数相同而类型不同的情况。
2.原理
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
3.函数模板的实例化
-
隐式实例化
编译器根据实参推演模板参数的实际类型
-
显式实例化:在函数名后的<>中指定模板参数的实际类型
显式实例化时编译器不进行类型推演,如果类型不匹配,编译器会进行隐式类型转换,无法转换成功则报错。
3. 模板实现复数类的加法
上面的加法函数模板也可以完成自定义类型对象的加法,如定义一个复数类Complex,只需要在类内部对’+'进行重载即可
调用结果:
完整代码:
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
cout << typeid(T).name() << endl;
return left + right;
}
class Complex
{
friend ostream& operator<<(ostream& _cout, const Complex & x);
public:
Complex(double real = 0.0, double image = 0.0)
: _real(real)
, _image(image)
{}
Complex operator+(const Complex& c)const
{
return Complex(_real + c._real, _image + c._image);
}
private:
double _real;
double _image;
};
ostream& operator<<(ostream& _cout, const Complex & x)
{
_cout << x._real << "." << x._image;
return _cout;
}
int main()
{
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3;
c3 = Add(c1, c2);
cout << c3 << endl;
system("pause");
return 0;
}
4.模板参数的匹配原则
-
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
-
对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从改模板产生出一个实例。如果模板可以产生一个更好匹配的函数,那么选择模板。
-
模板不允许自动类型转化
三、类模板
1.定义格式
template<tpyemame T1, typename T2, ..., typename Tn>
class Name
{
//
};
用类模板实现顺序表
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;
template <typename T>
class Seqlist
{
public:
Seqlist(size_t InitCapicity = 3)
:_array(new T[InitCapicity])
, _size(0)
, _capacity(InitCapicity)
{
}
~Seqlist()
{
if (_array)
{
delete[]_array;
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
//尾插
void PushBack(const T& data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
//尾删
void PopBack()
{
if (Empty())
{
return;
}
_size--;
}
//头插
void PushFront(const T& data)
{
CheckCapacity();
for (int i = (int)_size; i > 0; i--)
{
_array[i] = _array[i - 1];
}
_array[0] = data;
_size++;
}
size_t Capacity()
{
return _capacity;
}
size_t Size()
{
return _size;
}
//重载[]
T operator[](int position)
{
//assert()内的条件满足则不会触发断言
assert(position < _size);
return _array[position];
}
T& Back();
T& Front();
private:
void CheckCapacity()
{
if (_size == _capacity)
{
//2倍扩容
_capacity *= 2;
T* temp = new T[_capacity];
//将原来顺序表的内容拷贝到新的空间
for (int i = 0; i < _size; i++)
{
temp[i] = _array[i];
}
//清空旧空间内容
delete[] _array;
_array = temp;
}
}
//判空
bool Empty()
{
return 0 == _size;
}
T* _array;
size_t _size;
size_t _capacity;
};
template<typename T>
T& Seqlist<T>:: Back()
{
return _array[_size - 1];
}
template<typename T>
T& Seqlist<T>::Front()
{
return _array[0];
}
class Date
{
//类内声明为友元函数
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
Date(int year=2022, int month=1, int day=1)
:_year(year)
, _month(month)
, _day(day)
{
}
private:
int _year;
int _month;
int _day;
};
//类外定义
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
int main()
{
Seqlist<int> s1;
s1.PushBack(1);
s1.PushBack(2);
s1.PushBack(3);
s1.PushBack(4);
s1.PushBack(5);
s1.PushFront(0);
cout << s1.Back() << endl;
cout << s1.Capacity() << endl;
cout << s1.Front() << endl;
cout << s1.Size() << endl;
cout << s1[3] << endl;
Seqlist<double> s2;
s2.PushBack(1.8);
s2.PushBack(2.2);
s2.PushBack(3.5);
s2.PushBack(4.9);
s2.PushBack(5.2);
s2.PushFront(6.6);
cout << s2.Back() << endl;
cout << s2.Capacity() << endl;
cout << s2.Front() << endl;
cout << s2.Size() << endl;
cout << s2[3] << endl;
//将Date类对象保存在顺序表中
Seqlist<Date> s3;
Date d1(2022, 8, 4);
Date d2(2022, 8, 5);
Date d3(2022, 8, 6);
s3.PushBack(d1);
s3.PushBack(d2);
s3.PushBack(d3);
cout << s3.Back() << endl;
cout << s3.Capacity() << endl;
cout << s3.Front() << endl;
cout << s3.Size() << endl;
cout << s3[2] << endl;
system("pause");
return 0;
}
将顺序表写成类模板后,可以自定义要存放在顺序表中的数据类型,只需要定义对应数据类型的对象即可,这里分别定义了int、double和自定义类型Date类的数据。
要注意的是,这里Seqlist不是具体的类,而是类模板名,是编译器根据被实例化的类型生成具体类的模具。Seqlist<int>,Seqlist<double>,Seqlist<Date>等才是一个类名。
2.非类型模板参数
- 这里N在函数模板中是一个常量,不可修改。可以给定默认值,与缺省参数作用类似,作默认值。
- 这里N只能传整型家族的数据,如:int、char等
- N的值必须在编译阶段就确定
四、模板的特化
1.函数模板的特化
特化:特殊化处理
template<typename T>
const T Max(const T& left, const T& right)
{
if (left > right)
{
return left;
}
return right;
}
对于上面的函数模板,int 类型和double类型都可以比较大小,但是不能能处理字符串
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字template后面接―对空的尖括号<>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪错误
因为函数模板特化较为复杂且不实用,函数模板一般不需要进行特化 ,如果模板哪种类型出错,直接将该类型对应的普通函数写出来。对于上面字符串类型不能直接比较的情况,直接写出字符串类型比较的普通函数即可。
比较char*类型的普通函数:
char* Max(char* left, char* right)
{
if (strcmp(left, right) > 0)
return left;
return right;
}
2.类模板的特化
全特化
所有的参数都具体化,类型化的类模板
模板类
template<typename T1,typename T2>
class Date
{
public:
Date()
{
cout << "Date(T1,T2)" << endl;
}
private:
T1 _d1;
T2 _d2;
};
全特化的模板类,专门用于处理参数为int,char类型
template<>
class Date<int,char>
{
public:
Date()
{
cout << "Date<int,char>" << endl;
}
private:
int _d1;
int _d2;
};
定义Date类对象时,
偏特化
- 第一种:部分特化
//类模板的第二个参数做了特殊化处理
template<typename T1>
class Date<T1,int>
{
public:
Date()
{
cout << "Date(T1,int)" << endl;
}
private:
T1 _d1;
int _d2;
};
对于这个偏特化的类模板,定义对象时,只要第二个参数是int类型,就会调用特化处理的模板
- 第二种:参数更严格的限制。对模板参数特性进行特化,包括将模板参数特化为指针、引用或是另外一个模板类。
//两个参数偏特化为指针类型
class Date<T1*, T2*>
{
public:
Date()
{
cout << "Date(T1*, T2*)" << endl;
}
private:
T1* _d1;
T2* _d2;
};
模板特化的应用:类型萃取
参考文章:
C++之类型萃取
类型萃取
五、分离编译
概念:
每个项目由多个源文件共同实现,而每个源文件单独编译成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译。
注意:头文件不参与编译,在预处理阶段,头文件已经展开,即:将头文件中的内容拷贝一份放到源文件。
1.模板不支持分离编译
如上图所示,在template.cpp源文件中定义了Sub函数,并在template.h头文件中进行了声明。
但是在编译过程中,编译器是对各个源文件进行单独编译的,template.cpp源文件进行编译的过程中,没有检测到Sub函数模板的实例化,所以不会生成对应的代码,在main.cpp源文件中进行调用,链接阶段便会出错。如图:
理解两个概念:
- 导出符号表:编译完成后该源文件中地址(函数定义的位置)已经确定的函数
- 未解决符号表:源文件中地址还没有确定的函数
这里main.cpp源文件编译完成后,没有找到Sub函数的定义,但是由于头文件中进行了声明,在预处理阶段头文件中的声明会拷贝到源文件中,所以并不会立即报错,而是将Sub函数放在未解决符号表中,链接阶段,在template.cpp文件的导出符号表中找Sub函数的入口地址,而如果Sub函数没有生成则会报错。
2.解决方案:.hpp文件
将模板定义在 .hpp 文件中
调用时只需要包含模板定义所在的.hpp文件即可
3.模板总结
- 【优点】
1.模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
2.增强了代码的灵活性 - 【缺陷】
1.模板会导致代码膨胀问题,也会导致编译时间变长,因为类型推演和生成代码都需要耗费时间
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误
更多推荐
C++模板——模板特化、分离编译
发布评论