在这里讲讲所谓的模版在c++的应用。
通过字面就可以直到大致上的模版的作用以及诞生的原因了。
"模板" 是 C++ 中一种 "通用代码" 的工具,目的是让你写一份代码,能适配多种不同的数据类型,不用为每种类型重复写相似的代码。
打个比方:你想做一个 "交换两个东西" 的工具,既能交换两个数字,也能交换两个字符串,还能交换两个自定义的 "学生" 对象。如果没有模板,你可能需要写 3 个不同的函数;但有了模板,你只需要写 1 份代码,就能自动适配所有类型。
模板就是为了解决这个问题:用一个 "通用模板" 代替所有重复的类型相关代码。
模板的核心思想:"类型作为参数"
普通函数的参数是 "值"(比如 int a 中的 a 是值参数),而模板的参数是 "类型"(比如用 T 代表任意类型)。
比如交换函数的模板可以写成这样:
cpp
// 模板声明:告诉编译器"T是一个类型参数"
template <typename T> // 这里的T就代表"任意类型"
void swap(T &a, T &b) // 用T作为参数类型
{
T temp = a; // T也可以作为变量类型
a = b;
b = temp;
}
这里的 T 就像一个 "类型占位符",你可以理解为:"我现在不确定用什么类型,等实际调用的时候再说,反正逻辑都是交换"。
模板怎么用?
使用模板时,编译器会根据你传入的参数类型,自动生成对应类型的函数(这个过程叫 "实例化")。
cpp
int x = 1, y = 2;
swap(x, y); // 传入int类型,编译器自动生成"交换int"的函数
double a = 1.5, b = 2.5;
swap(a, b); // 传入double类型,编译器自动生成"交换double"的函数
string s1 = "hello", s2 = "world";
swap(s1, s2); // 传入string类型,编译器自动生成"交换string"的函数
你不需要手动写 swapInt、swapDouble,编译器会帮你搞定。
简单说:函数模板的核心,就是把函数中 "固定的变量类型" 变成 "可替换的模板参数",从而让同一个函数逻辑能适配多种类型。
需要注意几个点:
1可以为类的成员函数创建模板,但不能是虚函数和析构函数。
(在我的理解下就是,因为虚函数在继承的时候是需要实现的,可以理解为虚函数相当于子类生成的模版。所以不能将虚函数定义为模版,因为这样会生成的子类不确定。)
2使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。
原因:
函数模板本身不是可执行代码 ,它需要编译器根据 "具体类型" 生成实际的函数(实例化)。如果类型不明确(比如实参类型混乱,编译器无法推导T),就无法生成具体函数,会导致编译错误。
cpp
template <typename T>
void func(T a, T b) {}
func(1, 2.5); // 错误:int和double无法推导出统一的T
编译器无法确定T是int还是double,必须通过显式指定(如func<double>(1, 2.5))或调整实参类型(如都用int或都用double)来明确类型。
3使用函数模板时,推导的数据类型必须适应函数模板中的代码。
函数模板中的代码隐含了对类型的 "操作要求",如果推导的类型不支持这些操作,会编译失败。
例如,下面的模板要求T支持+操作:
cpp
template <typename T>
T add(T a, T b) { return a + b; }
- 若传入
int/double(支持+),正常工作; - 若传入自定义的
Student类(未重载+),编译会报错(Student不支持+操作)。
4使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换。
-
编译器在自动推导
T时,会要求所有实参的类型 "完全一致"(或能被编译器统一为同一类型),否则无法确定T。如果允许隐式转换,可能导致歧义(比如int和short都能转换为int或long,编译器无法抉择)。cpptemplate <typename T> void func(T a, T b) {} int a = 1; short b = 2; func(a, b); // 错误:int和short无法自动推导为同一T(不隐式转换) -
显式指定类型时,转换是 "人为可控的" :当手动指定
T后,编译器会将实参隐式转换为指定的T类型(只要转换合法),因为此时类型已明确,不会有歧义。cppfunc<int>(a, b); // 正确:short b会隐式转换为int
5函数模板支持多个通用数据类型的参数。
实际场景中,函数可能需要处理多种不同类型的参数。例如 "比较两个不同类型的值"(如int和double),单参数模板无法满足,因此模板支持多类型参数(用逗号分隔)。
6函数模板支持重载,可以有非通用数据类型的参数。
假设我们有一个比较两个值是否相等的模板:
cpp
// 通用模板:比较两个T类型的值是否相等
template <typename T>
bool isEqual(T a, T b) {
return a == b;
}
- 对于
int、double等基本类型,这个模板没问题(==可以直接比较)。 - 但如果是
char*(C 风格字符串),a == b比较的是指针地址 ,而我们实际想比较的是字符串内容 (需要用strcmp)。
这时候就需要为char*类型定制具体化版本:
cpp
// 为char*类型具体化(特化)
template<> bool isEqual<char*>(char* a, char* b) {
return strcmp(a, b) == 0; // 比较字符串内容
}
具体化的语法细节
cpp
// 语法1:显式指定类型(推荐)
template<> void Swap<int>(int &a, int &b) { ... }
// 语法2:省略类型(编译器可从参数推导,仅限函数模板)
template<> void Swap(int &a, int &b) { ... }
注意:两种语法效果完全一致,都是为int类型定制Swap的实现。
当同时存在 "普通函数""通用模板""具体化模板" 时,编译器的优先级如下(按顺序匹配):
普通函数 > 具体化模板 > 通用模板
cpp
// 1. 普通函数
void Swap(int &a, int &b) {
cout << "普通函数 Swap(int)\n";
// 特殊实现(如位运算)
}
// 2. 通用模板
template <typename T>
void Swap(T &a, T &b) {
cout << "通用模板 Swap(T)\n";
// 通用逻辑
}
// 3. 具体化模板(int类型)
template<> void Swap<int>(int &a, int &b) {
cout << "具体化模板 Swap(int)\n";
// 定制逻辑
}
// 调用时:
int x = 1, y = 2;
Swap(x, y); // 优先匹配"普通函数"(规则1)
函数模版同样是可以和函数一样分成头文件以及源文件,只不过具体化可以分成头文件+源文件,但是通用模版只能放在头文件
假设你需要一个 "栈" 数据结构,既能存int,又能存double,还能存string。如果没有类模板,你可能需要写 3 个几乎一样的类,同样的你也需要一个模版类。
用一个 "通用类模板" 描述逻辑,通过指定类型生成具体的类。
类模板的核心是用一个 "类型占位符"(通常用T)代替具体类型,语法如下:
cpp
template <class T> // 声明模板参数T(T代表任意类型)
class Stack { // 类模板名:Stack
private:
T* data; // 用T作为成员变量的类型
int top;
int capacity;
public:
// 构造函数(用T作为参数类型)
Stack(int cap) : capacity(cap), top(-1) {
data = new T[capacity]; // 动态分配T类型的数组
}
// 压栈函数(参数是T类型)
void push(T value) {
if (top < capacity - 1) {
data[++top] = value;
}
}
// 出栈函数(返回值是T类型)
T pop() {
if (top >= 0) {
return data[top--];
}
return T(); // 返回T类型的默认值(如int返回0,string返回空)
}
};
使用类模板时,必须明确指定具体类型(编译器无法像函数模板那样自动推导),
语法是:类模板名<具体类型> 对象名(参数)。
cpp
// 创建存int的栈(容量为5)
Stack<int> intStack(5);
intStack.push(10);
intStack.push(20);
int x = intStack.pop(); // x是20(int类型)
// 创建存string的栈(容量为3)
Stack<std::string> strStack(3);
strStack.push("hello");
strStack.push("world");
std::string s = strStack.pop(); // s是"world"(string类型)
编译器会根据你指定的类型(int或string),自动生成对应的类(如Stack<int>、Stack<string>),这个过程也叫 "实例化"。
创建对象时必须指明具体类型
函数模板可以自动推导类型(如Swap(a, b)不用写Swap<int>),但类模板不行。因为类的构造函数参数可能和存储类型无关,编译器无法推导。
cpp
Stack s(5); // 错误!必须指明类型,如Stack<int> s(5)
类型必须适应类模板中的代码
类模板中的代码对类型有 "隐含要求",如果指定的类型不支持这些操作,会编译报错。
可以指定缺省类型(C++11 及以上)
可以给模板参数设置 "默认类型",如果创建对象时不指定类型,就用默认类型。
成员函数可以在类外实现
类模板的成员函数可以在类外定义,但需要加上模板声明,并且用类名<T>::函数名的形式。
cpp
template <class T> // 类外实现必须带模板声明
class Stack {
public:
void push(T value); // 类内声明
T pop();
};
// 类外实现push
template <class T> // 重复模板声明
void Stack<T>::push(T value) { // 必须加<T>
if (top < capacity - 1) {
data[++top] = value;
}
}
// 类外实现pop
template <class T>
T Stack<T>::pop() {
if (top >= 0) {
return data[top--];
}
return T();
}
可以用new创建模板类对象
和普通类一样,模板类对象也可以用new在堆上创建,返回对应的指针类型。
cpp
Stack<int>* pStack = new Stack<int>(5); // 堆上创建int栈
pStack->push(100);
delete pStack; // 记得释放
成员函数 "用了才会创建"
编译器对模板类的成员函数采用 "按需实例化" 策略:只有当你调用某个成员函数时,编译器才会生成该函数的具体代码。
我这么理解哈,类同样可以有模板,同样可以进行具体化,部分具体化和完全具体化。
- 完全具体化的类模板(如
template<> class Stack<string>):是针对特定类型(string)定制的具体类型,功能上和 "手写一个StringStack类" 完全等价,只是复用了模板的通用逻辑。- 部分具体化的类模板(如
template<class T2> class Pair<int, T2>):是 "半通用半定制" 的类型,固定部分参数,剩余参数仍可变化(如Pair<int, double>、Pair<int, string>都是它生成的具体类型)。在这里,单纯的通用类模版是很难与其他进行互动的。
类模板的 "互动能力":依赖于具体类型
- 通用类模板(如
Stack<T>)因为T未知,无法直接用来定义变量(编译器不知道T是什么,无法分配内存),也无法直接作为函数参数(函数不知道要接收什么类型的Stack)。- 但一旦指定具体类型(如
Stack<int>),它就具备了和普通类完全一样的 "互动能力":
- 可以定义对象、作为函数参数 / 返回值;
- 可以继承其他类,或被其他类继承;
- 可以作为容器的元素类型(如
vector<Stack<int>>)。但是具体化的类模版就相当于一个已知类,好比和int一样已知以及使用。
- 通用类模板 :就像一个 "类型生成器",本身不是类型,而是生成具体类型的 "模具"。例如
template <class T> class Stack,单独说 "Stack是一个类型" 是错的,因为它缺少具体类型参数。类模板名<具体类型>:才是真正的类型,相当于int、double这些内置类型。例如:
Stack<int>是一个 "存储int的栈类型"Stack<string>是一个 "存储string的栈类型"- 这两种都是具体类型,就像
int和double是不同的具体类型一样。类模板名<具体类型> 对象名(参数)。
在这里,类模板名<具体类型> 可以当成一个类型,相当于int,而类模板名<具体类型> 对象名(参数),其实就是相当于int a=5;
以下是具体实现的分类讨论
一、模板类的具体化(特化)
模板类的具体化是为特定类型定制类模板的实现,分为 "完全具体化" 和 "部分具体化" 两种,核心是 "为特殊类型改写通用逻辑"。
1. 完全具体化(针对所有模板参数指定具体类型)
当模板类有多个参数时,为所有参数都指定具体类型,称为完全具体化。
示例 :为Stack模板类完全具体化string类型(因为字符串需要特殊处理,比如深拷贝):
cpp
// 通用模板类
template <class T>
class Stack {
private:
T* data;
int top;
int capacity;
public:
Stack(int cap) : capacity(cap), top(-1) {
data = new T[capacity];
}
void push(T value) { /* 通用实现 */ }
T pop() { /* 通用实现 */ }
};
// 完全具体化:针对T=string
template<> // 空模板参数列表,表示完全具体化
class Stack<std::string> { // 明确指定T为string
private:
std::string* data; // 存储string的数组
int top;
int capacity;
public:
Stack(int cap) : capacity(cap), top(-1) {
data = new std::string[capacity];
}
// 字符串压栈可能需要特殊处理(如避免浅拷贝,但string本身支持深拷贝)
void push(const std::string& value) {
if (top < capacity - 1) {
data[++top] = value; // string的赋值是深拷贝,安全
}
}
std::string pop() {
if (top >= 0) {
return data[top--];
}
return ""; // 空字符串
}
};
使用时:
cpp
Stack<int> intStack(5); // 使用通用模板
Stack<std::string> strStack(3); // 使用完全具体化的版本
2. 部分具体化(只指定部分模板参数,剩余参数仍为通用类型)
当模板类有多个参数时,为部分参数指定具体类型,剩余参数保留为模板参数,称为部分具体化。
示例 :有一个双参数模板类Pair,部分具体化 "第一个参数为int" 的情况:
cpp
// 通用双参数模板类
template <class T1, class T2>
class Pair {
public:
T1 first;
T2 second;
Pair(T1 f, T2 s) : first(f), second(s) {}
};
// 部分具体化:第一个参数T1固定为int,第二个参数T2仍为通用类型
template <class T2> // 只保留T2为模板参数
class Pair<int, T2> { // T1明确为int
public:
int first;
T2 second;
Pair(int f, T2 s) : first(f), second(s) {
// 针对int的特殊处理,比如范围检查
if (f < 0) first = 0;
}
};
使用时:
cpp
Pair<double, std::string> p1(3.14, "pi"); // 通用模板(T1=double, T2=string)
Pair<int, std::string> p2(-5, "count"); // 部分具体化版本(T1=int, T2=string)
// p2.first会被修正为0(因为构造函数有范围检查)
3. 具体化的优先级
- 完全具体化 > 部分具体化 > 通用模板
- 例如,若同时存在 "完全具体化
Pair<int, string>" 和 "部分具体化Pair<int, T2>",则Pair<int, string>会优先匹配完全具体化版本。
二、模板类与继承
模板类和继承结合时,基类或派生类可以是模板类或普通类,常见有 5 种形式:
1. 模板类继承普通类(最常见)
模板类作为派生类,继承自一个普通类(非模板类)。示例 :Stack模板类继承普通类Log(日志功能):
cpp
// 普通基类:提供日志功能
class Log {
public:
void log(const std::string& msg) {
std::cout << "[Log] " << msg << std::endl;
}
};
// 模板类继承普通类
template <class T>
class Stack : public Log { // 继承自Log
private:
T* data;
int top;
int capacity;
public:
void push(T value) {
log("push value"); // 调用基类的log方法
// ...压栈逻辑
}
};
2. 普通类继承模板类的实例化版本
普通类作为派生类,继承自 "模板类的某个具体实例"(必须指定模板参数)。示例 :普通类IntStack继承Stack<int>(Stack的int实例):
cpp
// 模板类
template <class T>
class Stack {
public:
void push(T value) { /* 通用实现 */ }
};
// 普通类继承模板类的实例化版本(Stack<int>)
class IntStack : public Stack<int> {
public:
// 可以新增int栈特有的功能
void pushMultiple(int n, ...) { /* 一次压入多个int */ }
};
3. 普通类继承模板类(需指定模板参数)
普通类继承模板类时,必须在继承列表中明确模板参数 (可以是具体类型或默认参数)。示例 :普通类StringStack继承Stack模板类,并指定T=string:
cpp
template <class T>
class Stack { /* ... */ };
// 普通类继承模板类(指定T=string)
class StringStack : public Stack<std::string> {
public:
void pushString(const char* cstr) {
push(std::string(cstr)); // 调用基类Stack<string>的push
}
};
4. 模板类继承模板类
派生类和基类都是模板类,派生类可以复用基类的模板参数或新增自己的参数。示例 :AdvancedStack模板类继承Stack模板类:
cpp
template <class T>
class Stack { /* 基础栈功能 */ };
// 模板类继承模板类(复用T作为参数)
template <class T>
class AdvancedStack : public Stack<T> { // 基类参数与派生类一致
public:
T peek() { // 新增"查看栈顶"功能
// 调用基类的成员(需用this->或Stack<T>::限定)
return this->data[this->top];
}
};
5. 模板类继承模板参数给出的基类
派生类的基类由模板参数指定(基类必须是具体类,不能是模板类)。示例:
cpp
// 普通类A和B
class A { public: void funcA() {} };
class B { public: void funcB() {} };
// 模板类,基类由模板参数T指定
template <class T>
class Derived : public T { // 基类是T(必须是具体类,如A或B)
public:
void callBase() {
// 调用基类的方法(依赖T有对应的方法)
if constexpr (std::is_same_v<T, A>) {
this->funcA();
} else if constexpr (std::is_same_v<T, B>) {
this->funcB();
}
}
};
// 使用:基类分别为A和B
Derived<A> d1; d1.callBase(); // 调用A::funcA()
Derived<B> d2; d2.callBase(); // 调用B::funcB()
三、模板类与函数
模板类可以作为函数的参数或返回值,有 3 种常见形式:
1. 普通函数:参数 / 返回值是模板类的实例化版本
函数参数或返回值是 "模板类的具体实例"(指定了类型参数)。示例:
cpp
template <class T>
class Stack { /* ... */ };
// 普通函数:参数是Stack<int>
void printIntStack(Stack<int>& s) {
while (!s.isEmpty()) {
std::cout << s.pop() << " ";
}
}
// 普通函数:返回值是Stack<string>
Stack<std::string> createStringStack() {
Stack<std::string> s(3);
s.push("a");
s.push("b");
return s;
}
2. 函数模板:参数 / 返回值是特定模板类
函数模板的参数或返回值是 "某个模板类",但类型参数通用。示例:
cpp
template <class T>
class Stack { /* ... */ };
// 函数模板:参数是Stack<T>(任意类型的Stack)
template <class T>
void clearStack(Stack<T>& s) {
while (!s.isEmpty()) {
s.pop();
}
}
// 使用:对任意Stack实例生效
Stack<int> intStack;
clearStack(intStack); // T=int
Stack<double> dblStack;
clearStack(dblStack); // T=double
3. 函数模板:参数 / 返回值是任意类型(支持模板类和普通类)
函数模板的参数是通用类型T,T可以是普通类、模板类的实例等。示例:
cpp
// 函数模板:打印任意类型的对象(包括模板类实例)
template <class T>
void print(T obj) {
std::cout << obj << std::endl; // 要求T支持<<运算符
}
// 使用:
int a = 10;
print(a); // T=int
Stack<int> s(2);
s.push(5);
print(s); // T=Stack<int>(需Stack<int>重载<<)