目录
泛型编程技术
泛型编程是一种编程方式,通过编写与数据类型无关的算法和数据结构,从而来提高代码重用性和可维护性。泛型编程的核心思想是参数化类型。即编写代码的时候不指明具体使用的数据类型,而是在使用的时候在具体化。
类似于提前做好模具,不管向上面使用什么材料,最后做出的产品形状和功能都是一样的。
函数模板
概述
- template<typename T> :模板头,用于告知编译器使用了模板,同时T是一个类型参数
- typename 等价于class ,用于声明一个类型参数
- T:占位符类型,标识函数参数或者返回值的实际类型将在函数调用的时候指定
计算两个最大值函数模板
多个类型参数,函数模板中有多个类型参数 (根据传入的数据推断数据类型)
函数模板的类型参数具有默认值
函数模板原理
主要实现原理,是基于编译时的模板实例化机制。具体来说,模板实例化是指在编译过程中,编译器会根据模板的使用情况生成对应的具体函数或者类的定义。依据这种机制,使得模板可以在编译的时候根据实际使用的类型生成具体代码。
实现步骤分析
- 模板定义:首先制定自己锁需要的模板类型
- 模板实例化
- 模板函数调用时,编译器根据传递给函数的参数类型来进行模板实例化
- 编译器生成一个与传递参数类型匹配的具体函数定义
- 如果同一个模板被多个不同的类型的参数使用,那么编译器会为每一种类型都生成一个对应的函数定义
- 代码生成
- 模板实例化后,编译成机器代码,其运行与普通函数一致
模板的优缺点
- 优点
- 代码复用:通过模板,相同功能不同类型的函数可以借助一个模板函数就解决了
- 类型安全:模板在编译期的时候会进行类型检查,可以发现并避免许多类型的错误
- 性能优化:模板实例化是在编译期间完成的,生成的具体函数没有运行时的开销
- 缺点
- 编译时间:模板实例化会增加编译时间
- 代码膨胀:每种不同的类型都会有其对应的具体函数
函数模板实例化
函数模板的实例化则是编译过程中,编译器根据具体类型参数生成相应的具体函数,其具体过程上文函数模板原理已经叙述。
显式实例化:目的在于避免编译器在多次调用重复实例化,也就是提前生成好针对哪些类型的实例,而不用等到调用的时候再去实例化。
下面的代码是告诉编译器在此处生成int 和 double两个类型函数的具体实现,不用等到调用的时候再去实现。
// 显式实例化模板
template int getMax<int>(int, int);
template double getMax<double>(double, double);
模板参数匹配原则分析
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以实例化成为这个非模板函数
- 如果已经有针对该类型的函数实现,则不调用模板特化
- 如果指定了类型版本,或者没有具体的函数实现,则调用实例化
模板函数是不支持自动类型转换,普通函数支持自动类型转化
类模板
概述
- tempate<typename T>:模板头
- class 类名:定义类模板,其内部参数使用T
- 注意点:类模板中的函数都必须在类外定义,而且需要加上模板参数列表
#include <iostream>
#include <vector>
// 定义栈的类模板
template <typename T>
class Stack {
public:
void push(T const& elem); // 入栈
void pop(); // 出栈
T top() const; // 获取栈顶元素
bool empty() const { return elems.empty(); } // 检查栈是否为空
private:
std::vector<T> elems; // 存储元素的容器
};
// 成员函数的定义
template <typename T>
void Stack<T>::push(T const& elem) {
elems.push_back(elem);
}
template <typename T>
void Stack<T>::pop() {
if (elems.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back();
}
template <typename T>
T Stack<T>::top() const {
if (elems.empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems.back();
}
int main() {
Stack<int> intStack; // int 类型的栈
Stack<std::string> stringStack; // string 类型的栈
intStack.push(7);
std::cout << "intStack top: " << intStack.top() << std::endl;
stringStack.push("hello");
std::cout << "stringStack top: " << stringStack.top() << std::endl;
return 0;
}
类模板实例化
类模板实例化过程分析
- 模板定义: 根据类模板定义格式,定义一个类模板,着重注意其函数的实现是放在了类外
- 模板实例化:编译器根据具体的类型参数实例化模板,生成对应的具体类定义
- 代码生成:编译器根据实例化后的具体类定义生成机器码
显式实例化,避免编译器在每次调用的时候都使用模板生成实例化内容,可以显式的告知编译器需要生成哪些模板实例类型,从而减少代码体积。
类模板的派生
类模版的派生,主要是用于继承。通过类模版和继承结合使用,从而建立更加灵活的代码使用结构。
类模板从模版类派生:一个类模板从另一个类模板中派生,此时可以为基类和派生类都提供模板参数。从而使得派生类能够从基类中继承模板参数并进行拓展。
- obj 是继承自Base的实例化对象,两个类的参数类型都使用模板参数
- obj 可以调用两个类中的所有函数
类模板从普通类中派生:派生类模板可以在继承普通类的基础上引入模板参数
非类型模板参数
非类型模板参数指的是模板参数中除了模板类型参数之外,还可以使用常量表达式作为模板参数。
非类型模板参数类型
- 整形常量:存储的数字必须在整形范围内
- 指针或者引用:所指向的对象或者函数必须具有静态存储或者拥有外部链接
- std::nullptr_t
- 常量表达式:必须是编译时已知的常量表达式
#include <iostream>
// 定义一个类模板,包含一个非类型模板参数
template <typename T, int N>
class Array {
public:
// 返回数组大小
int size() const { return N; }
// 访问数组元素
T& operator[](int index) {
if (index >= 0 && index < N) {
return data[index];
}
throw std::out_of_range("Index out of range");
}
private:
T data[N]; // 固定大小的数组
};
int main() {
Array<int, 5> intArray; // 创建一个包含5个整型元素的数组
for (int i = 0; i < intArray.size(); ++i) {
intArray[i] = i * 2;
}
for (int i = 0; i < intArray.size(); ++i) {
std::cout << intArray[i] << " ";
}
std::cout << std::endl;
Array<double, 3> doubleArray; // 创建一个包含3个双精度浮点数元素的数组
doubleArray[0] = 1.1;
doubleArray[1] = 2.2;
doubleArray[2] = 3.3;
for (int i = 0; i < doubleArray.size(); ++i) {
std::cout << doubleArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
模板特化
模板特化的作用在于当默认的模版实现无法满足某些特定类型的需求时,通过特化来实现其具体需求。主要类型有两种,一种是完全特化,即将模板中的所有参数提供具体类型的实现(不用模板类型,类或者函数中使用的类型都声明好),一种是部分特化,主要是为部分模板参数提供具体类型的实现(只指定部分参数的类型)。
模版特化的实现
- 定义通用模板的,定义模板的实现的以及其类型
- 定义特化版本,为特定的类型或者部分特定类型提供特化实现
- 实例化模版,使用模板的时候,编译器会根据传递的类型参数选择合适的模板版本。优先使用特化版本。
实例化模板时,编译器的选择规则
- 完全匹配的特化版本
- 部分匹配的特化版本
- 通用模板版本
模板的编译
模板分离编译指的是将模板定义和实现进行分离,将它们放在不同的源文件中进行编写和变异。从而使得代码模块化的同时提高编译速度。
模版分离编译实现
- 模板声明:将模板类的声明和接口放在头文件中
- 模板定义: 将模板类的实现放在实现文件中
- 显式实例化声明:需要使用模板的翻译单元中进行显式实例化声明