文章目录
- 一、模板的概念与作用
- 二、函数模板
- 模板的非类型参数
- 调用顺序
- 三、类模板
- 四、模板的编译模型
一、模板的概念与作用
C++模板是一种强大的代码复用机制,它允许程序员编写通用的代码,能够处理不同类型的数据,而无需为每种类型都重复编写相似的代码。通过模板,可以在编译时根据具体的类型参数生成对应的代码,极大地提高了代码的灵活性和可复用性。
例如,不用模板的情况下,如果要编写一个交换两个整数的函数和交换两个浮点数的函数,代码可能如下:
// 交换两个整数的函数
void swapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 交换两个浮点数的函数
void swapFloat(float& a, float& b) {
float temp = a;
a = b;
b = temp;
}
而使用模板,就可以编写一个通用的交换函数,能适用于多种类型:
// 模板函数
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
二、函数模板
- 定义
函数模板以template
关键字开头,后面跟着用尖括号括起来的模板参数列表,通常是类型参数(用typename
或class
关键字来声明,表示此处可以是任意类型)。例如上面的swap
函数模板,template <typename T>
就是定义部分,T
就是类型参数,之后定义的函数体中使用了这个T
类型来处理参数,使得该函数可以针对不同类型进行实例化。
函数模板不进行编译,在调用点必须要能看见模板定义,否则将产生链接错误,模板调用点根据类型会实例化一份函数进行编译
- 实例化
函数模板本身并不会生成实际的代码,只有在被调用时,编译器根据传入的实际参数类型来实例化模板,生成对应的具体函数版本。例如:
int num1 = 10, num2 = 20;
swap(num1, num2); // 此时编译器会根据int类型实例化出一个专门用于交换两个整数的swap函数版本
float num3 = 3.14f, num4 = 2.71f;
swap(num3, num4); // 同样,会根据float类型实例化出交换两个浮点数的swap函数版本
编译器会在后台为每一次不同类型的调用生成相应的函数代码,这个过程对程序员来说基本是透明的,但需要注意的是,如果实例化过程中出现类型相关的错误(比如类型不支持某些操作等),编译器会在编译阶段报错。
#include<iostream>
using namespace std;
//函数模板 T E 模板的类型参数
template<typename T , typename E>
void compare(T a, E b)
{
cout << a << endl;
}
//显示实例化
template void compare<int, double>(int ,double);
int main()
{
//隐式实例化
compare(1,1);//模板的实参推演
return 0;
}
模板参数不是一个简单的宏替换,而是重定义的过程 ,不是# define 而是typedef 的过程
老的编译器只检查函数头部是否符合语法规则,不检查函数体是否符合语法,但是新的编译器开始全部检查
函数模板不允许部分特例化
模板的非类型参数
#include<iostream>
using namespace std;
//函数模板 SIZE:模板的非类型参数,是常量
template<typename T , int SIZE>
int findval(T *arr, T val)
{
for (int i = 0; i < SIZE; ++i)
{
if (val == arr[i])
{
cout << "find it" << endl;
return i;
}
}
return -1;
}
int main()
{
int arr[] = {1,2,3,4};
findval<int, 4>(arr, 2);
//
int lenth = sizeof(arr) / sizeof(arr[0]);
findval<int, lenth>(arr, 2);//报错,模板的非类型参数必须是常量
return 0;
}
//以上代码改成 const int lenth = sizeof(arr) / sizeof(arr[0]);即可
- 模板参数推断
在很多情况下,编译器可以根据函数调用时传入的实际参数自动推断出模板参数的类型,不需要显式指定。例如上面的swap
函数调用,传入int
类型参数时,编译器就能推断出T
为int
类型。但也有一些情况需要显式指定模板参数类型,比如:
template <typename T>
T add(T a, T b) {
return a + b;
}
int result = add(10, 20); // 编译器可推断出T为int类型
double result2 = add<double>(10.0, 20.0); // 这里显式指定模板参数为double类型,因为编译器可能无法准确从整数10和20推断出要用于double类型的加法运算
- 重载与特化
函数模板可以重载,就像普通函数一样,只要函数签名(包括模板参数等)不同即可。例如:
template <typename T>
void print(T value) {
std::cout << value << std=""};
template <typename T>
void print(T* value) {
std::cout << "Pointer: " << value << std::endl;
}
上述代码定义了两个 print
函数模板,一个用于打印普通类型的值,一个用于打印指针类型的值,根据传入参数的类型不同,编译器会选择合适的重载版本进行实例化。
此外,还可以对函数模板进行特化,即针对特定的类型提供专门的实现,当使用该特定类型调用模板函数时,就会采用特化的版本。例如:
template <typename T>
void compare(T a, T b) {
std::cout << (a == b? "Equal" : "Not equal") << std::endl;
}
// 针对char*类型的特化
template <>
void compare<char*>(char* a, char* b) {
std::cout << (strcmp(a, b) == 0? "Equal" : "Not equal") << std::endl;
}
当调用 compare
函数模板时,如果传入的参数是 char*
类型,就会使用特化后的版本进行比较,而不是通用的版本。
调用顺序
先调用普通函数
在调用特例化
从函数模板实例化一个
三、类模板
- 定义
类模板的定义方式与函数模板类似,也是以template
关键字开头,后跟模板参数列表。例如:
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(T element) {
elements.push_back(element);
}
T pop() {
T last = elements.back();
elements.pop_back();
return last;
}
};
上述代码定义了一个简单的 Stack
类模板,它可以用来创建存储不同类型元素的栈结构,其中 T
就是类型参数,类中的成员变量和成员函数都基于这个 T
类型进行定义。
- 实例化
类模板同样需要实例化才能生成实际的类对象。实例化可以通过在使用类模板时指定类型参数来实现,例如:
Stack<int> intStack; // 实例化出一个存储int类型元素的栈
Stack<double> doubleStack; // 实例化出一个存储double类型元素的栈
intStack.push(10);
doubleStack.push(3.14);
和函数模板一样,编译器会根据指定的类型参数为每个实例化生成对应的类代码,包括类的成员变量的内存布局以及成员函数的具体实现等都会根据具体类型来确定。
- 模板参数
类模板的模板参数除了常见的类型参数(用typename
或class
声明)外,还可以有非类型参数,非类型参数通常是一些常量表达式,比如整数、枚举值、指针类型等。例如:
template <typename T, int size>
class Array {
private:
T elements[size];
public:
T& operator[](int index) {
return elements[index];
}
};
在上述 Array
类模板中,T
是类型参数,用于指定数组中元素的类型,而 int size
是一个非类型参数,用于指定数组的大小,这样就可以创建不同大小、不同元素类型的数组类对象,例如:
Array<int, 5> intArray; // 创建一个存储5个int元素的数组类对象
Array<double, 10> doubleArray; // 创建一个存储10个double元素的数组类对象
- 成员函数的定义
类模板的成员函数可以在类内定义,也可以在类外定义。如果在类外定义,需要使用模板声明,并且要带上模板参数,例如:
template <typename T>
class MyClass {
public:
void func();
};
template <typename T>
void MyClass<T>::func() {
// 函数体具体实现
}
注意,类模板的成员函数只有在被调用且对应的类模板实例化后才会被实例化生成实际的代码。
- 继承与模板
在涉及继承关系时,模板类可以作为基类或者派生类。例如:
template <typename T>
class Base {
// 基类的定义
};
template <typename T>
class Derived : public Base<T> {
// 派生类的定义,继承自Base<T>,保证类型的一致性
};
派生类在继承模板类时,需要正确指定模板参数,使得继承关系在类型上是匹配的,并且派生类可以根据自身需求扩展或修改基类的功能。
四、模板的编译模型
C++模板采用的是分离编译模型,也就是模板的定义和使用通常可以在不同的源文件中进行。但这也带来了一些问题,因为模板在编译时需要根据具体类型进行实例化,所以编译器在处理模板时,需要能看到模板的完整定义才能正确进行实例化生成代码。
例如,在一个源文件中定义了函数模板:
// file1.cpp
template <typename T>
void myTemplateFunction(T a) {
// 函数体内容
}
在另一个源文件中使用这个模板函数:
// file2.cpp
#include <iostream>
void func() {
int num = 10;
myTemplateFunction(num); // 此处使用模板函数,但编译器可能无法正确实例化,因为看不到模板函数的完整定义
}
为了解决这个问题,常见的做法有将模板的定义和声明都放在头文件中(尽管违背了通常头文件放声明、源文件放定义的原则),或者使用 export
关键字(不过在现代C++中这个关键字已经很少使用且很多编译器支持有限)来告诉编译器模板的定义可以在其他地方找到,以便正确进行实例化。
总之,C++模板是一种非常强大且灵活的编程工具,通过合理运用它,可以写出高效、复用性强的代码,但同时也需要深入理解其语法、实例化机制以及编译相关的特点等内容,才能更好地驾驭它。