模版 c++

在这里讲讲所谓的模版在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"的函数

你不需要手动写 swapIntswapDouble,编译器会帮你搞定。

简单说:函数模板的核心,就是把函数中 "固定的变量类型" 变成 "可替换的模板参数",从而让同一个函数逻辑能适配多种类型

需要注意几个点:

1可以为类的成员函数创建模板,但不能是虚函数和析构函数。

(在我的理解下就是,因为虚函数在继承的时候是需要实现的,可以理解为虚函数相当于子类生成的模版。所以不能将虚函数定义为模版,因为这样会生成的子类不确定。)

2使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。

原因:

函数模板本身不是可执行代码 ,它需要编译器根据 "具体类型" 生成实际的函数(实例化)。如果类型不明确(比如实参类型混乱,编译器无法推导T),就无法生成具体函数,会导致编译错误。

cpp 复制代码
template <typename T>
void func(T a, T b) {}

func(1, 2.5);  // 错误:int和double无法推导出统一的T

编译器无法确定Tint还是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。如果允许隐式转换,可能导致歧义(比如intshort都能转换为intlong,编译器无法抉择)。

    cpp 复制代码
    template <typename T>
    void func(T a, T b) {}
    
    int a = 1;
    short b = 2;
    func(a, b);  // 错误:int和short无法自动推导为同一T(不隐式转换)
  • 显式指定类型时,转换是 "人为可控的" :当手动指定T后,编译器会将实参隐式转换为指定的T类型(只要转换合法),因为此时类型已明确,不会有歧义。

    cpp 复制代码
    func<int>(a, b);  // 正确:short b会隐式转换为int

5函数模板支持多个通用数据类型的参数。

实际场景中,函数可能需要处理多种不同类型的参数。例如 "比较两个不同类型的值"(如intdouble),单参数模板无法满足,因此模板支持多类型参数(用逗号分隔)。

6函数模板支持重载,可以有非通用数据类型的参数。


假设我们有一个比较两个值是否相等的模板:

cpp 复制代码
// 通用模板:比较两个T类型的值是否相等
template <typename T>
bool isEqual(T a, T b) {
    return a == b;
}
  • 对于intdouble等基本类型,这个模板没问题(==可以直接比较)。
  • 但如果是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类型)

编译器会根据你指定的类型(intstring),自动生成对应的类(如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是一个类型" 是错的,因为它缺少具体类型参数。
  • 类模板名<具体类型> :才是真正的类型,相当于intdouble这些内置类型。例如:
    • Stack<int> 是一个 "存储int的栈类型"
    • Stack<string> 是一个 "存储string的栈类型"
    • 这两种都是具体类型,就像intdouble是不同的具体类型一样。

类模板名<具体类型> 对象名(参数)。

在这里,类模板名<具体类型> 可以当成一个类型,相当于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>Stackint实例):

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. 函数模板:参数 / 返回值是任意类型(支持模板类和普通类)

函数模板的参数是通用类型TT可以是普通类、模板类的实例等。示例

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>重载<<)
相关推荐
oioihoii2 小时前
C++中的线程同步机制浅析
开发语言·c++
不知几秋2 小时前
配置JDK和MAVEN
java·开发语言·maven
没有bug.的程序员2 小时前
Spring Cloud Gateway 路由与过滤器机制
java·开发语言·spring boot·spring·gateway
枫叶丹42 小时前
【Qt开发】布局管理器(五)-> QSpacerItem 控件
开发语言·数据库·c++·qt
月下倩影时3 小时前
ROS1基础入门:从零搭建机器人通信系统(Python/C++)
c++·python·机器人
_OP_CHEN3 小时前
C++进阶:(八)基于红黑树泛型封装实现 map 与 set 容器
开发语言·c++·stl·set·map·红黑树·泛型编程
C116113 小时前
Jupyter中选择不同的python 虚拟环境
开发语言·人工智能·python
无敌最俊朗@3 小时前
C++线程中detach和join的注意点
c++
努力努力再努力wz3 小时前
【Linux进阶系列】:线程(下)
linux·运维·服务器·c语言·数据结构·c++·算法