C++ 模板入门:从函数模板到类模板

作为一名 C++ 开发者,你是否曾遇到过这样的场景:想实现一个 "交换变量" 的功能,却要为intdouble、甚至自定义的Person类分别写一遍Swap函数?或者想封装一个栈结构,既要支持int栈,又要支持string栈,只能靠复制粘贴改类型?

其实,C++ 的模板(Template) 早就为我们解决了这个问题 ------ 它能将 "类型" 变成 "参数",用一套通用代码适配多种数据类型,既减少重复代码,又保证类型安全。今天就带大家从 0 到 1 理解 C++ 模板,结合代码和示意图,让你看完就能用!

一、什么是 C++ 模板?核心思想:"类型参数化"

简单来说,模板是一种通用代码框架 :它不直接指定变量或函数的具体类型,而是用一个 "占位符"(比如T)代替,等到编译时再根据传入的实际类型,生成对应版本的代码。

举个通俗的例子:模板就像 "饼干模具",T是模具的 "空位",你往空位里塞 "巧克力"(int类型),就做出巧克力饼干(int版本的代码);塞 "草莓"(double类型),就做出草莓饼干(double版本的代码)------ 模具只需要一个,却能做出多种饼干。

模板主要分为两类:函数模板 (适配函数)和类模板(适配类 / 数据结构),我们逐个拆解。

二、函数模板:让函数 "一键适配" 多种类型

先从最简单的Swap函数说起。如果没有模板,我们要写这样的重复代码:

cpp

复制代码
// 交换int
void Swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// 交换double
void Swap(double& a, double& b) {
    double temp = a;
    a = b;
    b = temp;
}

// 交换自定义类(比如Person)
class Person {
public:
    string name;
    int age;
};
void Swap(Person& a, Person& b) {
    Person temp = a;
    a = b;
    b = temp;
}

而有了函数模板,只需 1 段代码就能搞定所有类型!

1. 函数模板的定义格式

函数模板的核心是template <typename T>(或template <class T>,两者在函数模板中完全等价),其中T就是 "类型占位符"(可以换成任意合法名称,比如Type)。

下面是通用Swap函数的模板实现:

cpp

复制代码
// 函数模板:T是类型参数,代表任意类型
template <typename T>  // 也可以写 template <class T>
void Swap(T& t1, T& t2) {  // 用T代替具体类型,引用传递提高效率
    T temp = t1;  // 临时变量类型也为T
    t1 = t2;
    t2 = temp;
}

2. 如何使用函数模板?编译时自动 "生成代码"

使用函数模板时,我们不需要手动指定T的类型 ------ 编译器会根据传入的参数自动推导 T的实际类型,这个过程叫 "隐式实例化"。

代码示例:调用 Swap 模板

cpp

复制代码
#include <iostream>
#include <string>
using namespace std;

// 上面的Swap函数模板在这里...

class Person {
public:
    string name;
    int age;
    // 重载<<方便打印
    friend ostream& operator<<(ostream& os, const Person& p) {
        os << "(" << p.name << ", " << p.age << ")";
        return os;
    }
};

int main() {
    // 1. 交换int
    int a = 10, b = 20;
    Swap(a, b);
    cout << "交换后a=" << a << ", b=" << b << endl;  // 输出:a=20, b=10

    // 2. 交换double
    double c = 3.14, d = 6.28;
    Swap(c, d);
    cout << "交换后c=" << c << ", d=" << d << endl;  // 输出:c=6.28, d=3.14

    // 3. 交换Person对象
    Person p1 = {"Alice", 25}, p2 = {"Bob", 30};
    Swap(p1, p2);
    cout << "交换后p1=" << p1 << ", p2=" << p2 << endl;  // 输出:p1=(Bob,30), p2=(Alice,25)

    return 0;
}
关键:编译器做了什么?

你以为代码是 "运行时判断类型"?其实不是!编译器在编译阶段,会根据传入的参数类型,自动生成 3 个不同版本的 Swap 函数

  • 当传入int时,生成void Swap(int& t1, int& t2)
  • 当传入double时,生成void Swap(double& t1, double& t2)
  • 当传入Person时,生成void Swap(Person& t1, Person& t2)

这个 "编译时生成代码" 的过程,就是模板的核心优势 ------ 没有运行时类型判断的开销,效率和手写专用函数一样!

示意图 1:函数模板编译时实例化过程

建议用工具(如 DrawIO、Figma)画一张流程示意图,直观展示 "模板→实例化" 的过程:

plaintext

复制代码
[模板代码]                [编译器生成的代码(编译后)]
template <typename T>      void Swap(int& t1, int& t2) { ... }  // 处理int
void Swap(T& t1, T& t2) {  → void Swap(double& t1, double& t2) { ... }  // 处理double
    T temp = t1;           void Swap(Person& t1, Person& t2) { ... }  // 处理Person
    ... 
}

三、类模板:打造通用的数据结构(以栈为例)

函数模板解决了 "函数复用" 问题,而类模板则解决了 "数据结构复用" 问题 ------ 比如栈、链表、哈希表这些结构,核心逻辑(如入栈、出栈)和类型无关,用类模板就能封装成通用组件。

C++ 的 STL(标准模板库)之所以强大,就是因为几乎所有容器(vectorlistmap等)都是用类模板实现的!

1. 类模板的定义格式

类模板和函数模板类似,需要在类声明前加template <class T>(这里classtypename也可互换),然后在类内部用T表示 "元素类型"。

下面我们用类模板实现一个通用栈(Stack),支持动态扩容(容量不够时翻倍)。

2. 实战:通用 Stack 类模板的完整实现

cpp

复制代码
#include <iostream>
#include <cstring>  // 用于memcpy
using namespace std;

// 类模板:T是栈中元素的类型
template <class T>
class Stack {
private:
    T* _array;     // 动态数组,存储栈元素
    size_t _size;  // 当前栈中元素个数
    size_t _capacity;  // 栈的最大容量(可扩容)

public:
    // 构造函数:默认容量为4
    Stack(int n = 4) : _array(nullptr), _size(0), _capacity(n) {
        _array = new T[_capacity];  // 分配容量为n的T类型数组
        cout << "Stack构造:容量=" << _capacity << endl;
    }

    // 析构函数:释放动态内存,避免内存泄漏
    ~Stack() {
        if (_array != nullptr) {
            delete[] _array;  // 注意是delete[](数组),不是delete
            _array = nullptr;
            _size = 0;
            _capacity = 0;
        }
        cout << "Stack析构:内存已释放" << endl;
    }

    // 入栈操作:将元素x压入栈顶
    void Push(const T& x) {  // 传引用,避免拷贝,提高效率
        // 1. 扩容判断:如果元素个数等于容量,将容量翻倍
        if (_size == _capacity) {
            size_t new_cap = _capacity * 2;  // 新容量
            T* tmp = new T[new_cap];         // 分配新数组

            // 2. 拷贝旧数组元素到新数组(注意:若T是复杂类型,memcpy可能不安全,后续会讲)
            memcpy(tmp, _array, sizeof(T) * _size);

            // 3. 释放旧数组,指向新数组
            delete[] _array;
            _array = tmp;
            _capacity = new_cap;

            cout << "Stack扩容:旧容量=" << _capacity/2 << " → 新容量=" << _capacity << endl;
        }

        // 4. 元素入栈:栈顶指针后移
        _array[_size++] = x;
        cout << "元素" << x << "入栈,当前元素数=" << _size << endl;
    }

    // (可选)出栈操作:弹出栈顶元素(读者可自行实现)
    void Pop() {
        if (_size > 0) {
            _size--;  // 逻辑出栈,不需要真的删除元素
            cout << "元素出栈,当前元素数=" << _size << endl;
        } else {
            cout << "栈为空,无法出栈!" << endl;
        }
    }

    // (可选)获取栈顶元素(读者可自行实现)
    T& Top() {
        if (_size == 0) {
            throw out_of_range("栈为空,无栈顶元素!");  // 抛出异常
        }
        return _array[_size - 1];
    }

    // 获取当前栈元素个数
    size_t Size() const { return _size; }
};

3. 类模板必须 "显式实例化"!

和函数模板不同,类模板在创建对象时,必须手动指定T的类型------ 因为编译器无法从 "空对象" 中推导出类型,这个过程叫 "显式实例化"。

代码示例:使用 Stack 类模板

cpp

复制代码
int main() {
    // 1. 创建int类型的栈:显式指定T=int
    Stack<int> int_stack;  // 调用构造函数,默认容量4
    int_stack.Push(10);
    int_stack.Push(20);
    int_stack.Push(30);
    int_stack.Push(40);  // 此时_size=4,等于_capacity=4
    int_stack.Push(50);  // 触发扩容:容量4→8
    cout << "int栈当前元素数:" << int_stack.Size() << endl;  // 输出5
    int_stack.Pop();
    cout << "int栈顶元素:" << int_stack.Top() << endl;  // 输出40

    cout << "------------------------" << endl;

    // 2. 创建double类型的栈:显式指定T=double
    Stack<double> double_stack(2);  // 自定义初始容量2
    double_stack.Push(3.14);
    double_stack.Push(6.28);
    double_stack.Push(9.42);  // 触发扩容:2→4
    cout << "double栈当前元素数:" << double_stack.Size() << endl;  // 输出3

    return 0;
}
运行结果(供参考):

plaintext

复制代码
Stack构造:容量=4
元素10入栈,当前元素数=1
元素20入栈,当前元素数=2
元素30入栈,当前元素数=3
元素40入栈,当前元素数=4
Stack扩容:旧容量=4 → 新容量=8
元素50入栈,当前元素数=5
int栈当前元素数:5
元素出栈,当前元素数=4
int栈顶元素:40
------------------------
Stack构造:容量=2
元素3.14入栈,当前元素数=1
元素6.28入栈,当前元素数=2
Stack扩容:旧容量=2 → 新容量=4
元素9.42入栈,当前元素数=3
double栈当前元素数:3
Stack析构:内存已释放
Stack析构:内存已释放
示意图 2:类模板显式实例化对比

建议画一张 "错误写法 vs 正确写法" 的示意图,帮助读者避免踩坑:

错误写法(编译报错) 正确写法(显式实例化) 原因
Stack st; Stack<int> st; 编译器无法推导 T 的类型,必须显式指定
Stack<double> st(); Stack<double> st; 避免函数声明歧义(括号会被当作函数声明)
示意图 3:Stack 扩容过程

画一张栈的内存变化图,展示Push(50)时的扩容逻辑:

plaintext

复制代码
扩容前(容量4):_array[0]=10, _array[1]=20, _array[2]=30, _array[3]=40 → _size=4
↓
分配新数组(容量8):tmp[0]~tmp[3]拷贝旧元素
↓
释放旧数组,_array指向新数组 → _capacity=8
↓
元素50入栈:_array[4]=50 → _size=5

4. 为什么 STL 容器都用类模板实现?

比如vector(动态数组),它的用法是vector<int>vector<string>------ 本质就是vector是一个类模板,int/string是传入的类型参数T

正是因为类模板,STL 才能做到 "一套代码,适配所有类型",我们不用自己写IntVectorStringVector,直接用vector<T>即可。

示意图 4:STL 容器与类模板的关系

画一张思维导图,展示 STL 核心容器的模板本质:

plaintext

复制代码
C++类模板
├─ STL容器
│  ├─ vector<T>:动态数组(如vector<int>、vector<Person>)
│  ├─ list<T>:双向链表
│  ├─ stack<T>:栈(适配器,基于deque<T>)
│  ├─ map<K, V>:键值对映射(双参数模板)
│  └─ ...
└─ 自定义类模板
   ├─ Stack<T>:通用栈
   ├─ Queue<T>:通用队列
   └─ ...

四、类模板的 "坑":声明和实现不能随便分文件!

在 C 语言中,我们习惯把函数声明写在.h头文件,实现写在.cpp源文件。但类模板不能这么做,否则会导致链接错误!

1. 错误示例:分文件编写类模板

假设我们这样拆分代码:

  • Stack.h(声明):

    cpp

    复制代码
    #ifndef STACK_H
    #define STACK_H
    template <class T>
    class Stack {
    private:
        T* _array;
        size_t _size;
        size_t _capacity;
    public:
        Stack(int n = 4);
        ~Stack();
        void Push(const T& x);
        size_t Size() const;
    };
    #endif
  • Stack.cpp(实现):

    cpp

    复制代码
    #include "Stack.h"
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    template <class T>
    Stack<T>::Stack(int n) : _array(nullptr), _size(0), _capacity(n) {
        _array = new T[_capacity];
    }
    
    template <class T>
    Stack<T>::~Stack() {
        delete[] _array;
    }
    
    template <class T>
    void Stack<T>::Push(const T& x) {
        // 扩容逻辑...
    }
  • main.cpp(调用):

    cpp

    复制代码
    #include "Stack.h"
    int main() {
        Stack<int> st;
        st.Push(10);  // 编译通过,但链接报错:undefined reference to `Stack<int>::Push(int const&)'
        return 0;
    }

2. 为什么会报错?模板需要 "完整代码" 实例化

编译器在编译Stack.cpp时,不知道用户会用T=int还是T=double,所以不会生成任何具体版本的Push函数;而编译main.cpp时,虽然知道T=int,但Stack.h里只有声明,没有实现,无法生成Stack<int>::Push的代码 ------ 最终链接时找不到函数实现,报错!

示意图 5:分文件导致的链接错误

建议截图展示编译器的错误信息(比如 GCC 的报错):

plaintext

复制代码
/usr/bin/ld: /tmp/ccXXXXXX.o: in function `main':
main.cpp:(.text+0x20): undefined reference to `Stack<int>::Stack(int)'
main.cpp:(.text+0x32): undefined reference to `Stack<int>::Push(int const&)'
main.cpp:(.text+0x3e): undefined reference to `Stack<int>::~Stack()'
collect2: error: ld returned 1 exit status

3. 解决方案:两种正确的拆分方式

方式 1:将实现写在头文件中(推荐)

直接在.h文件中实现类模板的成员函数,让编译器在包含头文件时能看到完整代码:

cpp

复制代码
// Stack.h(声明+实现)
#ifndef STACK_H
#define STACK_H
#include <cstring>
#include <iostream>
using namespace std;

template <class T>
class Stack {
private:
    T* _array;
    size_t _size;
    size_t _capacity;
public:
    Stack(int n = 4) : _array(nullptr), _size(0), _capacity(n) {
        _array = new T[_capacity];
    }

    ~Stack() {
        delete[] _array;
    }

    void Push(const T& x) {
        // 完整的扩容和入栈逻辑...
    }
};
#endif
方式 2:显式实例化指定类型(适合固定类型场景)

如果只需要支持少数几种类型(比如只需要intdouble),可以在Stack.cpp末尾显式实例化指定类型:

cpp

复制代码
// Stack.cpp
#include "Stack.h"
// ... 成员函数实现 ...

// 显式实例化T=int和T=double的版本
template class Stack<int>;
template class Stack<double>;

五、小细节:模板参数名可以 "不一致" 吗?

文档中提到一个有趣的点:类模板的声明和实现中,T的名字可以不一样(比如声明用X,实现用T)。

代码示例:参数名不一致的情况

cpp

复制代码
// 声明时用X作为参数名
template <class X>
class Stack {
public:
    void Push(const X& x);  // 这里用X
};

// 实现时用T作为参数名(语法可行,但不推荐)
template <class T>
void Stack<T>::Push(const T& x) {  // 这里用T
    // 逻辑和之前一致...
}

编译器会通过 "模板类名(Stack)" 匹配,而不是参数名,所以语法上没问题。但强烈不推荐这么做 ------ 参数名不一致会让代码可读性大幅下降,别人看代码时会疑惑 "X 和 T 是什么关系?"。

六、总结:模板的优势与注意点

1. 模板的核心优势

  • 代码复用:一套代码适配多种类型,不用重复写 "类型专属版" 代码;
  • 类型安全 :编译时检查类型,避免像void*那样的类型隐患;
  • 无运行时开销:编译时生成具体类型代码,和手写专用代码效率一致。

2. 需要注意的 "坑"

  • 类模板必须显式实例化(创建对象时指定T);
  • 类模板的声明和实现不能随意分文件(推荐写在头文件);
  • memcpy拷贝模板类型时,若T是复杂类型(如带指针的类),会导致 "浅拷贝" 问题(后续可学习 "深拷贝" 和 "移动语义" 解决)。

3. 实践建议

看完这篇文章,你可以尝试:

  1. 给我们的Stack类模板添加Pop(出栈)、Top(获取栈顶)、Empty(判断空栈)方法;
  2. 实现一个Pair<T1, T2>类模板,存储键值对(比如Pair<string, int>存储 "姓名 - 年龄");
  3. 尝试用vector<T>实现一个通用的链表。

模板是 C++ 的核心特性之一,也是 STL 的基石。掌握模板,能让你的代码更简洁、更灵活 ------ 赶紧动手实践吧!

如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎在评论区分享你的学习心得~

相关推荐
浅川.257 小时前
STL专项:vector 变长数组
c++·stl·vector
qq_310658517 小时前
webrtc源码走读(八)系统接口层
服务器·c++·音视频·webrtc
嵌入式进阶行者8 小时前
【算法】回溯算法的基本原理与实例:华为OD机考双机位A卷 - 乘坐保密电梯
c++·算法
六bring个六8 小时前
自实现线程池
c++·线程池
老王熬夜敲代码8 小时前
C++新特性:string_view
开发语言·c++·笔记
ouliten8 小时前
石子合并模型
c++·算法
weixin_461769408 小时前
5. 最长回文子串
数据结构·c++·算法·动态规划
散峰而望8 小时前
【算法竞赛】C++入门(三)、C++输入输出初级 -- 习题篇
c语言·开发语言·数据结构·c++·算法·github
不会c嘎嘎8 小时前
数据结构 -- 常见的八大排序算法
数据结构·c++·算法·排序算法·面试题·快速排序