首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++基础:(七)模版初阶:从泛型编程到类模板

C++基础:(七)模版初阶:从泛型编程到类模板

作者头像
_OP_CHEN
发布2026-01-14 09:47:10
发布2026-01-14 09:47:10
1190
举报
文章被收录于专栏:C++C++

前言

在 C++ 编程中,我们经常会遇到这样的场景:需要实现功能完全相同,但处理数据类型不同的函数或类。例如,交换两个整数、交换两个浮点数、交换两个字符;或者实现一个存储整数的栈、存储字符串的栈、存储自定义对象的栈。如果为每种数据类型都重复编写几乎相同的代码,不仅会导致代码冗余,还会降低可维护性 —— 一旦需要修改逻辑,所有重复的代码都要同步更新。 为了解决这个问题,C++ 引入了模板(Template) 机制,它是泛型编程(Generic Programming)的核心。本文将从泛型编程的基本概念入手,逐步深入讲解函数模板的定义、原理、实例化和匹配原则,最后扩展到类模板的使用,帮助初学者全面掌握 C++ 模板的初阶知识。下面就让我们正式开始吧!


一、 泛型编程:编写与类型无关的通用代码

1.1 通用交换函数面临的问题

首先,我们从一个简单的需求出发:实现一个 “交换两个变量值” 的函数。根据不同的数据类型,我们可能会写出如下代码:

代码语言:javascript
复制
// 交换两个int类型变量
void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

// 交换两个double类型变量
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

// 交换两个char类型变量
void Swap(char& left, char& right)
{
    char temp = left;
    left = right;
    right = temp;
}

// 如果需要交换其他类型(如string、自定义结构体),还需继续添加重载...

虽然通过函数重载可以实现需求,但这种方式存在两个明显的缺陷:

  1. 代码复用率低:所有重载函数的逻辑完全相同,仅数据类型不同。只要有新的类型需要支持,就必须手动添加对应的重载函数,无法做到 “一次编写,多类型复用”。
  2. 代码可维护性差:如果交换逻辑需要修改,则所有重载函数都要同步修改,一旦遗漏某个重载,就会导致逻辑不一致。

1.2 泛型编程:用 “模子” 生成代码

既然问题的核心是 “类型不同但逻辑相同”,我们能否告诉编译器一个 “通用模子”,让编译器根据不同的类型自动生成对应的代码呢?

比如,我们定义一个 “交换模子”,其中数据类型用一个占位符(如T)表示,当需要交换int时,编译器就用int替换T生成int版本的交换函数;当需要交换double时,就用double替换T生成double版本的交换函数。

这种 “编写与类型无关的通用代码,通过编译器自动适配不同类型” 的编程范式,就是泛型编程。而模板,正是实现泛型编程的基础工具。

二、 函数模板:通用函数的 “生产模具”

2.1 函数模板的概念

函数模板(Function Template)代表了一个函数家族,它与具体的数据类型无关,在使用时会被 “参数化”—— 即根据传入的实参类型,由编译器生成该类型对应的具体函数版本。

简单来说,函数模板不是一个真正的函数,而是编译器生成特定函数的 “蓝图” 或 “模具”。

2.2 函数模板的定义格式

函数模板的定义需要使用template关键字声明模板参数,具体格式如下:

代码语言:javascript
复制
template <typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表)
{
    // 函数逻辑(与类型无关)
}
说明:
  • template:声明这是一个模板定义,必须放在函数定义的最前面。
  • <typename T1, ..., Tn>:模板参数列表,其中typename是定义模板参数的关键字,T1T2等是模板参数(通常用大写字母表示,如TUV),代表 “待确定的类型”。
  • 模板参数列表中可以有多个参数,例如<typename T1, typename T2>表示需要两个待确定的类型。
  • 关键字typename也可以用class代替(二者在函数模板中完全等价),但不能用struct代替classstruct在 C++ 中用于定义结构体,不具备模板参数声明的语义)。

基于上述格式,我们就可以将之前的交换函数重写为函数模板:

代码语言:javascript
复制
// 函数模板:通用交换函数
template <typename T>  // 声明模板参数T(T代表任意类型)
void Swap(T& left, T& right)  // 参数类型为T&,返回值类型为void
{
    T temp = left;  // 用T定义临时变量
    left = right;
    right = temp;
}

此时,无论需要交换intdoublechar还是自定义类型(如string),都只需这一段代码,编译器会根据实际使用的类型自动生成对应的交换函数。

2.3 函数模板的原理:编译器如何生成代码?

很多初学者会误以为 “函数模板会在运行时动态适配类型”,但实际上,函数模板的 “适配” 发生在编译阶段。其核心原理是:

  1. 编译器在编译代码时,会扫描函数模板的使用场景。
  2. 当遇到函数模板的调用时,编译器会根据传入的实参类型,推演模板参数(如T)的具体类型。
  3. 编译器根据推演得到的具体类型,生成一份该类型专属的函数代码(即 “模板实例化”)。
  4. 最终生成的可执行文件中,不存在函数模板本身,只存在编译器为不同类型生成的具体函数。

下面我给大家提供了一个编译器生成intdouble版本的 Swap 函数的示例:

代码语言:javascript
复制
int main()
{
    int a = 10, b = 20;
    double c = 3.14, d = 6.28;
    
    Swap(a, b);  // 调用Swap<int>
    Swap(c, d);  // 调用Swap<double>
    
    return 0;
}

编译器在编译时会执行以下步骤:

1. 处理Swap(a, b)

(1)实参ab的类型是int,因此编译器推演模板参数Tint

(2)生成int版本的Swap函数:

代码语言:javascript
复制
void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

2. 处理Swap(c, d)

(1)实参cd的类型是double,因此编译器推演模板参数Tdouble

(2)生成double版本的Swap函数:

代码语言:javascript
复制
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

(3)最终的可执行文件中,包含的是上述两个具体的Swap函数,而非原始的函数模板。

本质上,函数模板是将 “手动编写重复代码” 的工作交给了编译器,既避免了代码冗余,又保证了逻辑的一致性。

2.4 函数模板的实例化

当我们用不同类型的参数调用函数模板时,编译器会生成该类型对应的具体函数,这个过程称为函数模板的实例化。根据模板参数的确定方式,实例化分为两种:隐式实例化显式实例化

2.4.1 隐式实例化:编译器自动推演类型

隐式实例化是最常用的方式:编译器根据传入的实参类型,自动推演模板参数的具体类型,无需用户手动指定。

下面我给大家提供一个隐式实例化的通用加法函数模版示例:

代码语言:javascript
复制
// 函数模板:通用加法函数
template <typename T>
T Add(const T& left, const T& right)  // 两个参数类型均为T
{
    return left + right;
}

int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.5, d2 = 20.5;
    
    // 隐式实例化:编译器根据实参类型推演T为int
    int sum1 = Add(a1, a2);  // 等价于Add<int>(a1, a2)
    cout << "sum1 = " << sum1 << endl;  // 输出:sum1 = 30
    
    // 隐式实例化:编译器根据实参类型推演T为double
    double sum2 = Add(d1, d2);  // 等价于Add<double>(d1, d2)
    cout << "sum2 = " << sum2 << endl;  // 输出:sum2 = 31.0
    
    return 0;
}

但我们还要注意隐式实例化的类型匹配问题。隐式实例化要求编译器能够唯一确定模板参数的类型。如果实参类型不统一,编译器无法推演,则会报错。

例如,以下代码会编译失败:

代码语言:javascript
复制
int main()
{
    int a = 10;
    double d = 20.5;
    
    // 错误:实参类型分别为int和double,编译器无法确定T是int还是double
    Add(a, d);  // 编译报错:no matching function for call to 'Add(int&, double&)'
    
    return 0;
}

解决这个问题有下面两种方式:

1. 手动强制类型转换:将其中一个实参的类型转换为与另一个一致。

代码语言:javascript
复制
Add(a, (int)d);  // 将d强制转换为int,T推演为int
// 或
Add((double)a, d);  // 将a强制转换为double,T推演为double

2. 显式实例化:手动指定模板参数的类型,跳过编译器的推演过程。

2.4.2 显式实例化:用户手动指定类型

显式实例化的格式是:在函数名后加上<具体类型>,直接告诉编译器模板参数的类型,无需编译器推演。格式如下:

代码语言:javascript
复制
函数名<具体类型>(实参列表);

针对上述隐式实例化失败的场景,我们可以通过显式实例化来解决类型不匹配的问题:

代码语言:javascript
复制
int main()
{
    int a = 10;
    double d = 20.5;
    
    // 显式实例化:指定T为int,编译器会将d隐式转换为int(20)
    int sum3 = Add<int>(a, d);  
    cout << "sum3 = " << sum3 << endl;  // 输出:sum3 = 30
    
    // 显式实例化:指定T为double,编译器会将a隐式转换为double(10.0)
    double sum4 = Add<double>(a, d);  
    cout << "sum4 = " << sum4 << endl;  // 输出:sum4 = 30.5
    
    return 0;
}

进行显式实例化时,需要注意如下的类型转换规则:

显式实例化时,如果实参类型与指定的模板类型不匹配,编译器会尝试进行隐式类型转换(如doubleintintdouble)。如果转换失败(如stringint),则编译报错。

比如,下面的代码在编译时会失败,因为string无法隐式转换为int

代码语言:javascript
复制
int main()
{
    string s = "123";
    int a = 10;
    
    // 错误:string无法隐式转换为int
    Add<int>(a, s);  // 编译报错:invalid conversion from 'std::string' to 'int'
    
    return 0;
}

2.5 函数模板的匹配原则

在我们实际的编程过程中,可能会出现 “非模板函数与同名函数模板同时存在” 的场景。此时,编译器会根据一定的规则选择调用哪个函数,这就是模板匹配原则

原则 1:非模板函数与模板函数可同名共存,模板可实例化为非模板函数

如果一个非模板函数与一个同名的函数模板同时存在,且模板可以实例化为与非模板函数完全相同的版本,那么两者可以共存。

代码语言:javascript
复制
// 非模板函数:专门处理int类型的加法
int Add(int left, int right)
{
    cout << "非模板函数 Add(int, int) 被调用" << endl;
    return left + right;
}

// 函数模板:通用加法函数
template <typename T>
T Add(T left, T right)
{
    cout << "函数模板 Add<T>(T, T) 被调用" << endl;
    return left + right;
}

int main()
{
    int a = 10, b = 20;
    
    // 调用非模板函数(与实参类型完全匹配)
    Add(a, b);  // 输出:非模板函数 Add(int, int) 被调用
    
    // 显式实例化:指定T为int,调用模板生成的int版本
    Add<int>(a, b);  // 输出:函数模板 Add<T>(T, T) 被调用
    
    return 0;
}
原则 2:优先调用非模板函数,模板仅在 “更匹配” 时被选择

如果非模板函数与模板实例化后的函数都能匹配实参,编译器会优先选择非模板函数(因为非模板函数是 “现成的”,无需编译器生成)。但如果模板能生成更匹配的函数版本,则会选择模板。

代码语言:javascript
复制
// 非模板函数:处理两个int类型
int Add(int left, int right)
{
    cout << "非模板函数 Add(int, int) 被调用" << endl;
    return left + right;
}

// 函数模板:处理两个不同类型(T1和T2)
template <typename T1, typename T2>
T1 Add(T1 left, T2 right)
{
    cout << "函数模板 Add<T1, T2>(T1, T2) 被调用" << endl;
    return left + right;
}

int main()
{
    int a = 10;
    double d = 20.5;
    
    // 场景1:实参为两个int,非模板函数完全匹配,优先调用非模板
    Add(10, 20);  // 输出:非模板函数 Add(int, int) 被调用
    
    // 场景2:实参为int和double,非模板函数需要转换(double转int),而模板无需转换
    // 模板生成的版本(T1=int, T2=double)更匹配,因此调用模板
    Add(a, d);  // 输出:函数模板 Add<T1, T2>(T1, T2) 被调用
    
    return 0;
}
原则 3:模板不支持自动类型转换,非模板函数支持

在隐式实例化时,编译器不会为模板进行自动类型转换(因为模板的类型需要唯一确定,转换可能导致歧义);而非模板函数支持 C++ 标准的自动类型转换(如intdoublecharint)。

代码语言:javascript
复制
// 非模板函数:int类型加法
int Add(int left, int right)
{
    cout << "非模板函数 Add(int, int) 被调用" << endl;
    return left + right;
}

// 函数模板:通用加法
template <typename T>
T Add(T left, T right)
{
    cout << "函数模板 Add<T>(T, T) 被调用" << endl;
    return left + right;
}

int main()
{
    char c1 = 'A', c2 = 'B';  // 'A'的ASCII码是65,'B'是66
    
    // 非模板函数:char会自动转换为int,调用成功
    Add(c1, c2);  // 输出:非模板函数 Add(int, int) 被调用,返回 65+66=131
    
    // 模板:隐式实例化时,编译器会推演T为char,但如果传入char和int,会报错
    int a = 10;
    // Add(c1, a);  // 错误:无法确定T是char还是int
    
    return 0;
}

三、 类模板:通用数据结构的实现工具

在 C++ 中,不仅常常需要实现与类型无关的通用类,例如栈(Stack)、链表(LinkedList)、队列(Queue)等数据结构。这些数据结构的逻辑完全相同,只是存储的数据类型不同。如果为每种数据类型都实现一个类,会导致大量重复代码。类模板(Class Template)正是为解决这一问题而设计的

3.1 类模板的定义格式

类模板的定义需要在类声明前用template声明模板参数,具体格式如下:

代码语言:javascript
复制
template <class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义(成员变量和成员函数可以使用模板参数作为类型)
};
说明:
  • template <class T1, ..., Tn>:声明模板参数列表,class关键字与typename等价,用于定义模板参数(代表待确定的类型)。
  • 类模板中可以定义成员变量和成员函数,这些成员的类型可以是模板参数(如T)。
  • 类模板的成员函数可以在类内定义,也可以在类外定义(但类外定义时需要特殊的语法)。

下面我提供一个栈(Stack)的类模板实现:

代码语言:javascript
复制
#include <iostream>
using namespace std;

// 类模板:通用栈
template <typename T>  // 声明模板参数T(T代表栈中元素的类型)
class Stack
{
public:
    // 构造函数:初始化栈的容量(默认4)
    Stack(size_t capacity = 4)
        : _capacity(capacity)
        , _size(0)
    {
        _array = new T[capacity];  // 动态开辟T类型的数组
    }

    // 析构函数:释放动态开辟的空间
    ~Stack()
    {
        delete[] _array;
        _array = nullptr;
        _capacity = 0;
        _size = 0;
    }

    // 入栈操作:在类内定义
    void Push(const T& data)
    {
        // 简化处理:实际中应先检查是否需要扩容
        _array[_size] = data;
        _size++;
    }

    // 出栈操作:声明(在类外定义)
    void Pop();

    // 获取栈顶元素:声明(在类外定义)
    T& Top();

    // 判断栈是否为空
    bool Empty() const
    {
        return _size == 0;
    }

    // 获取栈的大小
    size_t Size() const
    {
        return _size;
    }

private:
    T* _array;       // 存储元素的数组(类型为T*)
    size_t _capacity;// 栈的容量
    size_t _size;    // 栈中当前元素个数
};

类模板的成员函数可以在类外定义,但需要遵循特殊的语法:必须再次声明模板参数,并在函数名前加上类模板名<T>作为作用域限定符。

例如,我们可以为上述Stack类模板在类外定义PopTop函数:

代码语言:javascript
复制
// 类外定义Pop函数
template <typename T>  // 必须再次声明模板参数
void Stack<T>::Pop()  // 作用域限定符为Stack<T>
{
    if (Empty())
    {
        cerr << "Stack is empty, cannot pop!" << endl;
        return;
    }
    _size--;  // 简化处理:实际中可根据需要释放元素
}

// 类外定义Top函数
template <typename T>  // 必须再次声明模板参数
T& Stack<T>::Top()  // 作用域限定符为Stack<T>
{
    if (Empty())
    {
        cerr << "Stack is empty, no top element!" << endl;
        // 实际中可抛出异常,此处简化处理
        static T default_val;
        return default_val;
    }
    return _array[_size - 1];
}

需要注意的是:类模板的声明与定义分离问题 与函数模板不同,类模板的声明和定义通常不建议分离到.h(头文件)和.cpp(源文件)中,否则可能会导致链接错误。 原因是:编译器在实例化类模板时,需要看到模板的完整定义(包括成员函数的实现)。如果声明在.h中,定义在.cpp中,当其他文件包含.h并实例化模板时,编译器无法找到成员函数的实现代码,从而导致链接失败。 因此,类模板的声明和定义通常都放在头文件中(或同一文件内)。

3.2 类模板的实例化

类模板的实例化与函数模板的实例化是有显著区别的:

  • 函数模板可以通过实参隐式推演模板参数类型(隐式实例化);
  • 类模板必须显式指定模板参数的类型,无法通过构造函数的参数隐式推演。

类模板实例化的格式是:在类模板名后加上<具体类型>,实例化的结果才是真正的类(称为 “模板类”)。如下:

代码语言:javascript
复制
类模板名<具体类型> 对象名;

下面为大家提供一个实例化不同类型的Stack的示例:

代码语言:javascript
复制
int main()
{
    // 实例化存储int类型的栈
    Stack<int> int_stack;  // Stack<int>是一个具体的类(模板类)
    int_stack.Push(10);
    int_stack.Push(20);
    cout << "int_stack top: " << int_stack.Top() << endl;  // 输出:20
    int_stack.Pop();
    cout << "int_stack top after pop: " << int_stack.Top() << endl;  // 输出:10

    // 实例化存储double类型的栈
    Stack<double> double_stack;  // Stack<double>是另一个具体的类
    double_stack.Push(3.14);
    double_stack.Push(6.28);
    cout << "double_stack top: " << double_stack.Top() << endl;  // 输出:6.28

    // 实例化存储string类型的栈
    Stack<string> string_stack;  // Stack<string>是第三个具体的类
    string_stack.Push("hello");
    string_stack.Push("world");
    cout << "string_stack top: " << string_stack.Top() << endl;  // 输出:world

    return 0;
}

类模板名 vs 模板类:

  • 类模板名(如Stack):是模板本身的名称,不是一个具体的类,不能直接用来定义对象。
  • 模板类(如Stack<int>Stack<double>):是类模板实例化后的结果,是一个具体的类,可以用来定义对象。

例如,Stack是类模板名,而Stack<int>Stack<double>是两个完全独立的类,它们的成员函数由编译器根据模板生成,彼此之间没有继承或关联关系。

类模版在实例化时可以有多个模版参数,需要为每个参数指定具体的类型。

下面为大家举一个支持键值对的Pair类模板为例:

代码语言:javascript
复制
// 类模板:键值对(多参数)
template <typename K, typename V>  // 两个模板参数:键类型K,值类型V
class Pair
{
public:
    Pair(const K& key, const V& value)
        : _key(key)
        , _value(value)
    {}

    void Print() const
    {
        cout << "(" << _key << ", " << _value << ")" << endl;
    }

private:
    K _key;   // 键(类型为K)
    V _value; // 值(类型为V)
};

int main()
{
    // 实例化Pair<int, string>
    Pair<int, string> p1(1, "one");
    p1.Print();  // 输出:(1, one)

    // 实例化Pair<string, double>
    Pair<string, double> p2("pi", 3.14159);
    p2.Print();  // 输出:(pi, 3.14159)

    return 0;
}

总结

模板是 C++ 中实现代码复用的强大工具,掌握模板的使用是编写高效、通用代码的基础。后续进阶内容(如模板特化、模板参数、可变参数模板等)将在此基础上展开,建议大家先熟练掌握本文介绍的初阶知识。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、 泛型编程:编写与类型无关的通用代码
    • 1.1 通用交换函数面临的问题
    • 1.2 泛型编程:用 “模子” 生成代码
  • 二、 函数模板:通用函数的 “生产模具”
    • 2.1 函数模板的概念
    • 2.2 函数模板的定义格式
    • 2.3 函数模板的原理:编译器如何生成代码?
    • 2.4 函数模板的实例化
      • 2.4.1 隐式实例化:编译器自动推演类型
      • 2.4.2 显式实例化:用户手动指定类型
    • 2.5 函数模板的匹配原则
      • 原则 1:非模板函数与模板函数可同名共存,模板可实例化为非模板函数
      • 原则 2:优先调用非模板函数,模板仅在 “更匹配” 时被选择
      • 原则 3:模板不支持自动类型转换,非模板函数支持
  • 三、 类模板:通用数据结构的实现工具
    • 3.1 类模板的定义格式
    • 3.2 类模板的实例化
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档