本系列暴死,不会更新了。
该系列为本人的学习笔记,主要由本人整理书写而成。部分内容来自教材、视频课程等,不能保证完全原创性。
萌新的学习笔记,写错了恳请斧正。
# 泛式编程
在 C 语言笔记 #22 中,我们提到了泛式编程可以减少对特定数据类型的依赖,并且使用泛型指针 void* 在 C 语言中实现了一个 “万能” 的冒泡排序。而在 C++ 中,泛式编程更加容易,因为 C++ 提供了模版这么一个概念。
# 函数模板
一个函数模板就代表了一个函数家族,函数模板与类型无关,只在使用时才被参数化,根据参数的类型来产生函数的特定版本。
如果我们想在函数中使用未定的类型,我们只要在函数的声明和定义前各加上一行 template 语句。
template 语句的格式为:
template <typename T1, typename T2, ..., typename Tn> | |
// 下面直接接模版函数的本体即可,本体中就可以利用我们设置的模版类型 T1,T2…… | |
// 这里 typename 也可以替换为 class,但是这样不符合自然语言逻辑: | |
template <class T1, class T2, ..., class Tn> |
比方说如果我们想写一个任意类型的加法函数,只需要这样:
template <typename T> | |
T add(T a, T b) | |
{ | |
return a + b; | |
} |
但是这里要求参数 a 和参数 b 是同一个类型的变量,如果我们的函数涉及到多个互不关联的未定的类型,就需要在 template 的形参列表中加上一些东西,比方说写一个比较两个类型变量的内存大小的函数:
template <typename T1, typename T2> | |
bool TypeSizeBigger(T1 x, T2 y) | |
{ | |
return (sizeof(x) > sizeof(y)) ? true : false; | |
} |
# 函数模板的实例化
在编译阶段,编译器根据传入模板的实参类型来推演生成对应类型的函数以供调用。
比方说对于上面的 add 函数在程序中实际有三种类型传入进来调用。那么编译器实际上在编译时就根据模板生成了三个仅类型不同的函数,每一个用于处理不同的类型的调用。用不同类型的参数使用函数模板是,也称为函数模板的实例化。
实例化分为隐式实例化和显示实例化:
# 隐式实例化
隐式实例化就是让编译器根据实参自己推演模板参数的实际类型。
template <typename T> | |
T Add(const T& a, const T& b) | |
{ | |
return a + b; | |
} | |
int main() | |
{ | |
int a = 5, b = 6; | |
double c = 7.1, d = 8.2; | |
Add(a, b); | |
Add(c, d); | |
return 0; | |
} |
# 显式实例化
显式实例化就是在调用时于函数名后加一个 <> 来指定模板参数的实际类型:
template <typename T> | |
T Add(const T& a, const T& b) | |
{ | |
return a + b; | |
} | |
int main() | |
{ | |
int a = 5, b = 6; | |
double c = 7.1, d = 8.2; | |
Add<int>(a, b); | |
Add<double>(c, d); | |
return 0; | |
} |
如果类型不匹配,编译器将会尝试进行隐式类型转换,如果无法转换成功就会报错。
# 模板函数的匹配原则
可以有非模板函数与模板函数同名,而且该函数模板还可以被实例化为这个非模板函数。
// 专门处理 int 类型int f(int a, int b)
{return a + b + 1;
}template <typename T>
T f(T a, T b)
{return a + b;
}int main()
{cout << f(1, 2) << endl;
cout << f<int>(1, 2) << endl;
return 0;
}此时这个函数输出是先 4 后 3,为什么呢?因为在能够与非模板函数匹配时,编译器不会再特化生成一个对应的函数,而是直接调用。而在指定模板实例化时,编译器依旧会按照模板生成并调用。
注意这里并不会冲突报错,哪怕这非模板函数和生成的函数的函数名(包括返回值与参数列表)都一样。因为在编译过程中,函数的名称会经过修饰以确保函数的唯一性。虽然模板函数和普通函数在表面上看起来参数和返回类型相同,但由于模板的存在,编译器在生成模板函数时会应用不同的名称修饰规则。
在 linux gcc 环境下,我们测试到非模板函数 f 的原型和修饰后的函数符号如下:
int f(int a, int b)
_Z1fii
其中
_Z代表这是经过修饰的 C++ 符号,1f代表名字长度为 1 且名字为 f,ii代表参数类型为int, int。而模板生成函数的原型与函数符号为:
template <typename T>
T f<int>(T a, T b)
_Z1fIiEii
其中
_Z代表这是经过修饰的 C++ 符号,1f代表名字长度为 1 且名字为 f,IiE代表这是一个模板实例化,其中i代表模板参数类型为 int,最后的ii代表参数类型为int, int。如果模板能够匹配产生一个更好的函数,则优先调用模板。
// 专门处理 int 类型int f(int a, int b)
{return a + b + 1;
}template <typename T1, typename T2>
T1 f(T1 a, T2 b)
{return a + b;
}int main()
{cout << f(1.0, 2) << endl;
return 0;
}此时优先调用的是模板生成的函数。
注意:模板函数不允许自动类型转换,但是普通函数可以自动类型转换。
# 类模板
与模板函数类似,我们可以定义模板类。模板类前同样需要 template 语句:
template <class T1, class T2, ..., class Tn> | |
// 下面直接接模板类即可 |
比方说写一个动态顺序表模板类:
template <class T> | |
class SeqList | |
{ | |
public: | |
SeqList(size_t capacity = 10) | |
: m_data(new T[capacity]) | |
, m_capacity(capacity) | |
, m_size(0) | |
{} | |
~SeqList() | |
{ | |
if (m_data) | |
delete[] m_data; | |
m_size = m_capacity = 0; | |
} | |
void push_back(const T& value) | |
{ | |
if (m_size == m_capacity) | |
{ | |
size_t new_capacity = m_capacity * 2; | |
T* new_data = new T[new_capacity]; | |
for (size_t i = 0; i < m_size; ++i) | |
new_data[i] = m_data[i]; | |
delete[] m_data; | |
m_data = new_data; | |
m_capacity = new_capacity; | |
} | |
m_data[m_size++] = value; | |
} | |
void pop_back() | |
{ | |
if (m_size > 0) | |
--m_size; | |
} | |
size_t size() const | |
{ | |
return m_size; | |
} | |
T& operator[](size_t index) | |
{ | |
if (index >= m_size) | |
throw out_of_range("Index out of range"); | |
return m_data[index]; | |
} | |
private: | |
T* m_data; | |
size_t m_capacity; | |
size_t m_size; | |
}; |
# 类模板声明与定义分离
如果想要类模板声明与定义分离,我们以上面的动态顺序表的析构为例:
template <class T> | |
class SeqList | |
{ | |
public: | |
//... | |
~SeqList(); | |
//... | |
}; | |
template <class T> | |
SeqList<T>::~SeqList() | |
{ | |
if (m_data) | |
delete[] m_data; | |
m_size = m_capacity = 0; | |
} |
我们看到如果类模板的函数在类外定义时,需要加模板参数列表。
# 类模板的实例化
类模板的实例化与函数模板类似,在类名称之后加上 <> 以限定类型。(类模板不能隐式实例化)
SeqList<int> s1; | |
SeqList<double> s2; |