C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念

C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 《C语言基础》 《数据结构》 《机器学习导论》 《前端基础》 《python基础》 ✨ 数据即知识,压缩即智能


目录

  • [C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念](#C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念)
    • 一、为什么需要泛型编程?
      • [1.1 从交换函数开始](#1.1 从交换函数开始)
      • [1.2 什么是泛型编程?](#1.2 什么是泛型编程?)
    • 二、函数模板
      • [2.1 什么是函数模板?](#2.1 什么是函数模板?)
      • [2.2 函数模板的基本格式](#2.2 函数模板的基本格式)
      • [2.3 typename 和 class 有什么区别?](#2.3 typename 和 class 有什么区别?)
    • 三、函数模板的原理
      • [3.1 函数模板不是函数](#3.1 函数模板不是函数)
      • [3.2 模板会不会让代码只保留一份?](#3.2 模板会不会让代码只保留一份?)
    • C++ 模板属于编译期机制。 !在这里插入图片描述(https://i-blog.csdnimg.cn/direct/0e6a34d1c3244155976b9d51fa9f94ad.png#pic_center))
    • 四、函数模板的实例化
      • [4.1 什么是模板实例化?](#4.1 什么是模板实例化?)
      • [4.2 隐式实例化](#4.2 隐式实例化)
      • [4.3 一个常见错误:两个参数类型不同](#4.3 一个常见错误:两个参数类型不同)
      • [4.4 解决方式一:手动类型转换](#4.4 解决方式一:手动类型转换)
      • [4.5 解决方式二:显式实例化](#4.5 解决方式二:显式实例化)
      • [4.6 解决方式三:使用多个模板参数](#4.6 解决方式三:使用多个模板参数)
    • 五、模板参数匹配原则
      • [5.1 普通函数和函数模板可以同时存在](#5.1 普通函数和函数模板可以同时存在)
      • [5.2 匹配完全相同时,优先调用普通函数](#5.2 匹配完全相同时,优先调用普通函数)
      • [5.3 显式指定模板参数时,会调用模板版本](#5.3 显式指定模板参数时,会调用模板版本)
      • [5.4 如果模板能提供更好的匹配,会选模板](#5.4 如果模板能提供更好的匹配,会选模板)
      • [5.5 模板一般不主动帮你做类型转换](#5.5 模板一般不主动帮你做类型转换)
    • 六、函数模板使用时的常见坑
      • [6.1 模板不是万能替代重载](#6.1 模板不是万能替代重载)
      • [6.2 模板要求类型支持相关操作](#6.2 模板要求类型支持相关操作)
    • 七、类模板
      • [7.1 为什么需要类模板?](#7.1 为什么需要类模板?)
      • [7.2 类模板的基本格式](#7.2 类模板的基本格式)
      • [7.3 类模板不是类](#7.3 类模板不是类)
    • 八、类模板成员函数的定义
      • [8.1 类内定义成员函数](#8.1 类内定义成员函数)
      • [8.2 类外定义成员函数](#8.2 类外定义成员函数)
      • [8.3 为什么模板不建议声明和定义分离到 .h 和 .cpp?](#8.3 为什么模板不建议声明和定义分离到 .h 和 .cpp?)
    • 九、函数模板和类模板的区别
      • [9.1 实例化方式不同](#9.1 实例化方式不同)
      • [9.2 使用场景不同](#9.2 使用场景不同)
    • [十、用类模板改造 Stack](#十、用类模板改造 Stack)
      • [10.1 基础版本](#10.1 基础版本)
      • [10.2 这个 Stack 还不完美](#10.2 这个 Stack 还不完美)
    • 十一、模板初阶常见错误总结
      • [11.1 忘记写 template 声明](#11.1 忘记写 template 声明)
      • [11.2 把类模板当成类使用](#11.2 把类模板当成类使用)
      • [11.3 类外定义成员函数时忘记 Stack<T>](#11.3 类外定义成员函数时忘记 Stack)
      • [11.4 误以为模板会自动处理所有类型转换](#11.4 误以为模板会自动处理所有类型转换)
      • [11.5 模板声明和定义分离导致链接错误](#11.5 模板声明和定义分离导致链接错误)
    • 十二、本文总结

一、为什么需要泛型编程?

1.1 从交换函数开始

假设我们要写一个交换函数。

交换两个 int

cpp 复制代码
void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

交换两个 double

cpp 复制代码
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

交换两个 char

cpp 复制代码
void Swap(char& left, char& right)
{
    char temp = left;
    left = right;
    right = temp;
}

这几段代码的逻辑完全一致:

  1. 创建临时变量;
  2. 保存左值;
  3. 左值接收右值;
  4. 右值接收临时变量。

唯一不同的是类型。

如果每遇到一种新类型就写一个新函数,会带来几个问题:

  • 代码重复度高;
  • 维护成本高;
  • 一个地方写错,多个重载版本都可能要改;
  • 新类型出现时,还要继续补函数;
  • 代码越来越臃肿。

这时候我们自然会想:

能不能写一个"模子",让编译器根据不同类型自动生成对应函数?

这就是模板要解决的问题。


1.2 什么是泛型编程?

泛型编程可以简单理解为:

编写与具体类型无关的通用代码。

所谓"泛型",就是不提前绑定某一个具体类型。

比如交换两个变量这件事,本质上并不关心它们是 intdouble,还是 string

只要这个类型支持:

cpp 复制代码
T temp = left;
left = right;
right = temp;

那么这套交换逻辑就能成立。

这就是泛型编程的价值:

把"类型变化"交给编译器,把"通用逻辑"保留下来。

在 C++ 中,模板就是泛型编程最基础、最核心的工具。


二、函数模板

2.1 什么是函数模板?

函数模板不是一个普通函数。

它更像是一个"函数生成规则"。

它描述了一类函数的共同逻辑,具体类型在使用时再由编译器确定。

例如:

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

这里的 T 不是一个真实类型。

它是一个模板参数,可以在调用时被替换成:

  • int
  • double
  • char
  • string
  • 其他自定义类型

比如:

cpp 复制代码
int a = 1, b = 2;
Swap(a, b);

编译器会根据实参类型推导出:

cpp 复制代码
T = int

于是生成一个处理 intSwap 函数。

再比如:

cpp 复制代码
double x = 1.1, y = 2.2;
Swap(x, y);

编译器会推导出:

cpp 复制代码
T = double

然后生成一个处理 doubleSwap 函数。

这就是函数模板的基本思想。


2.2 函数模板的基本格式

函数模板的一般格式是:

cpp 复制代码
template<typename T1, typename T2, ..., typename Tn>
返回值类型 函数名(参数列表)
{
    // 函数体
}

最常见的写法是单个模板参数:

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

如果有多个模板参数,也可以这样写:

cpp 复制代码
template<typename T1, typename T2>
void Func(T1 x, T2 y)
{
    // ...
}

其中:

cpp 复制代码
template<typename T>

表示声明一个模板参数 T

后面的函数体里就可以把 T 当成一个类型来使用。


2.3 typename 和 class 有什么区别?

模板参数中,下面两种写法都可以:

cpp 复制代码
template<typename T>

也可以写成:

cpp 复制代码
template<class T>

在这里,typenameclass 都表示:

T 是一个类型参数。

所以这两个函数模板是等价的:

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}
cpp 复制代码
template<class T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

但是注意:

cpp 复制代码
template<struct T>

这种写法不可以。

在模板参数声明里,可以用 typenameclass,不能用 struct 代替。

实际写代码时,很多人更喜欢用 typename,因为它语义更直接:这里需要的是一个类型。


三、函数模板的原理

3.1 函数模板不是函数

函数模板本身不是函数,它是编译器生成函数的模具。

比如:

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

这段代码本身并不会直接生成某一个具体函数。

只有当你真正调用它时:

cpp 复制代码
int a = 1, b = 2;
Swap(a, b);

编译器才会根据 ab 的类型推导出:

cpp 复制代码
T = int

然后生成类似下面这样的函数:

cpp 复制代码
void Swap(int& left, int& right)
{
    int temp = left;
    left = right;
    right = temp;
}

当你写:

cpp 复制代码
double x = 1.1, y = 2.2;
Swap(x, y);

编译器又会生成一个 double 版本:

cpp 复制代码
void Swap(double& left, double& right)
{
    double temp = left;
    left = right;
    right = temp;
}

所以模板真正做的事情是:

把重复写代码的工作交给编译器。


3.2 模板会不会让代码只保留一份?

从源代码角度看,我们只写了一份模板。

但从编译生成的代码角度看,不同类型可能会生成不同版本。

例如:

cpp 复制代码
Swap(a, b);   // int 版本
Swap(x, y);   // double 版本

编译器可能会生成:

cpp 复制代码
Swap<int>
Swap<double>

也就是说,模板不是运行时才判断类型,而是编译期根据类型生成代码。

这点和很多脚本语言里的"动态类型"不一样。

C++ 模板属于编译期机制。

四、函数模板的实例化

4.1 什么是模板实例化?

使用不同类型调用函数模板时,编译器根据类型生成对应函数版本。

这个过程叫:

函数模板实例化。

例如:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int main()
{
    int a1 = 10, a2 = 20;
    double d1 = 10.0, d2 = 20.0;

    Add(a1, a2);
    Add(d1, d2);

    return 0;
}

这里会发生两次实例化:

cpp 复制代码
Add<int>
Add<double>

也就是编译器会生成两个具体版本。


4.2 隐式实例化

隐式实例化就是:

让编译器根据实参类型自动推导模板参数。

例如:

cpp 复制代码
Add(a1, a2);

a1a2 都是 int,所以编译器推导:

cpp 复制代码
T = int

再看:

cpp 复制代码
Add(d1, d2);

d1d2 都是 double,所以编译器推导:

cpp 复制代码
T = double

这就是最常见的模板使用方式。


4.3 一个常见错误:两个参数类型不同

看下面代码:

cpp 复制代码
template<class T>
T Add(const T& left, const T& right)
{
    return left + right;
}

int main()
{
    int a = 10;
    double d = 20.0;

    Add(a, d);

    return 0;
}

这段代码通常不能通过编译。

原因是模板参数列表里只有一个 T

但是调用时:

cpp 复制代码
Add(a, d);

第一个参数让编译器推导:

cpp 复制代码
T = int

第二个参数又让编译器推导:

cpp 复制代码
T = double

一个 T 不能同时既是 int 又是 double

所以编译器会报错。


4.4 解决方式一:手动类型转换

可以把其中一个参数转成另一个类型:

cpp 复制代码
Add(a, (int)d);

这样两个参数都是 int,编译器就能推导:

cpp 复制代码
T = int

或者:

cpp 复制代码
Add((double)a, d);

这样两个参数都是 double,编译器就能推导:

cpp 复制代码
T = double

这种方式简单直接,但需要程序员自己决定转换方向。


4.5 解决方式二:显式实例化

显式实例化就是:

在函数名后面的尖括号中明确指定模板参数类型。

例如:

cpp 复制代码
Add<int>(a, d);

这表示:

cpp 复制代码
T = int

然后 d 会尝试转换成 int

也可以写:

cpp 复制代码
Add<double>(a, d);

这表示:

cpp 复制代码
T = double

然后 a 会尝试转换成 double

显式实例化的好处是:

你明确告诉编译器这次模板参数到底是什么。


4.6 解决方式三:使用多个模板参数

如果你希望两个参数本来就可以是不同类型,可以把函数模板写成两个模板参数:

cpp 复制代码
template<class T1, class T2>
auto Add(T1 left, T2 right)
{
    return left + right;
}

这样:

cpp 复制代码
int a = 10;
double d = 20.0;

Add(a, d);

编译器可以推导:

cpp 复制代码
T1 = int
T2 = double

这种写法更加灵活。

不过这里的返回值类型要注意。

如果返回值写成 T1

cpp 复制代码
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
    return left + right;
}

那么 int + double 的结果会被转换成 int 返回,可能丢失小数部分。

现代 C++ 中可以使用 auto 让编译器推导返回类型。


五、模板参数匹配原则

5.1 普通函数和函数模板可以同时存在

C++ 允许普通函数和同名函数模板同时存在。

例如:

cpp 复制代码
int Add(int left, int right)
{
    return left + right;
}

template<class T>
T Add(T left, T right)
{
    return left + right;
}

这里既有普通函数:

cpp 复制代码
int Add(int, int)

也有函数模板:

cpp 复制代码
template<class T>
T Add(T, T)

它们可以共存。


5.2 匹配完全相同时,优先调用普通函数

如果调用:

cpp 复制代码
Add(1, 2);

两个实参都是 int

普通函数:

cpp 复制代码
int Add(int, int)

可以直接匹配。

模板函数也可以实例化出:

cpp 复制代码
Add<int>(int, int)

这时编译器通常会优先选择普通函数。

原因也很好理解:

既然已经有现成的精确匹配函数,就没必要再从模板生成一个。


5.3 显式指定模板参数时,会调用模板版本

如果你写:

cpp 复制代码
Add<int>(1, 2);

这就明确告诉编译器:

我要调用模板实例化出来的 Add<int> 版本。

这时就不会调用普通函数。

所以:

cpp 复制代码
Add(1, 2);

可能调用普通函数。

而:

cpp 复制代码
Add<int>(1, 2);

会调用模板生成的版本。


5.4 如果模板能提供更好的匹配,会选模板

看下面代码:

cpp 复制代码
int Add(int left, int right)
{
    return left + right;
}

template<class T1, class T2>
auto Add(T1 left, T2 right)
{
    return left + right;
}

void Test()
{
    Add(1, 2);
    Add(1, 2.0);
}

对于:

cpp 复制代码
Add(1, 2);

普通函数完全匹配,所以调用普通函数。

对于:

cpp 复制代码
Add(1, 2.0);

普通函数需要把 2.0double 转成 int

而函数模板可以生成:

cpp 复制代码
Add<int, double>

两个参数都能更准确匹配。

所以这时模板版本反而可能更合适。

简单记:

普通函数优先,但不是永远优先。谁匹配得更好,最终就更可能被选择。


5.5 模板一般不主动帮你做类型转换

在普通函数调用中,编译器经常会做隐式类型转换。

比如:

cpp 复制代码
void Func(double x);

Func(10);

这里 10 可以从 int 转成 double

但在模板参数推导过程中,编译器通常不会为了推导模板参数而随意做这种转换。

例如:

cpp 复制代码
template<class T>
T Add(T left, T right);

int a = 10;
double d = 20.0;

Add(a, d);

它不会直接把 a 转成 double,也不会直接把 d 转成 int 来帮你推导。

而是发现一个 T 推导出了两个类型,于是报错。

可以记一句:

普通函数调用更愿意做类型转换,模板参数推导更强调类型匹配。


六、函数模板使用时的常见坑

6.1 模板不是万能替代重载

函数模板可以减少重复代码,但它不是说所有函数重载都不需要了。

比如:

cpp 复制代码
template<class T>
T Add(T left, T right)
{
    return left + right;
}

如果大部分类型都可以用这套逻辑,那么模板很好。

但如果某个类型需要特殊处理,就可能仍然需要重载或特化。

例如字符串拼接、指针比较、自定义对象比较,有时并不适合简单套用通用逻辑。


6.2 模板要求类型支持相关操作

比如:

cpp 复制代码
template<class T>
T Add(T left, T right)
{
    return left + right;
}

这个模板能不能用于某个类型,取决于这个类型是否支持:

cpp 复制代码
left + right

如果类型不支持 operator+,编译就会失败。

再比如:

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
    T temp = left;
    left = right;
    right = temp;
}

这个模板要求类型支持:

  • 拷贝构造
  • 赋值操作

所以模板代码看起来"通用",但并不是对任何类型都无条件成立。

它依赖类型本身具备相应能力。


七、类模板

7.1 为什么需要类模板?

函数模板解决的是函数逻辑复用问题。

类模板解决的是类结构复用问题。

比如我们写一个栈。

如果只支持 int

cpp 复制代码
class StackInt
{
private:
    int* _array;
    size_t _capacity;
    size_t _size;
};

如果再支持 double,可能要写:

cpp 复制代码
class StackDouble
{
private:
    double* _array;
    size_t _capacity;
    size_t _size;
};

如果还要支持 charstring、自定义类型呢?

显然不能每个类型都写一个栈。

因为这些栈的逻辑几乎一样:

  • 入栈
  • 出栈
  • 扩容
  • 判空
  • 获取栈顶

真正变化的只是元素类型。

这时候就可以使用类模板。


7.2 类模板的基本格式

类模板的一般格式是:

cpp 复制代码
template<class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义
};

最常见的是单模板参数:

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }

private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

这里的 T 表示栈中元素的类型。

如果以后写:

cpp 复制代码
Stack<int> st1;

那么 T 就是 int

如果写:

cpp 复制代码
Stack<double> st2;

那么 T 就是 double


7.3 类模板不是类

类模板不是具体的类,类模板实例化之后才是真正的类。

例如:

cpp 复制代码
template<typename T>
class Stack
{
    // ...
};

这里的 Stack 只是类模板名。

它不是一个完整类型。

不能直接写:

cpp 复制代码
Stack st;

必须指定模板参数:

cpp 复制代码
Stack<int> st1;
Stack<double> st2;

其中:

cpp 复制代码
Stack<int>

才是真正的类型。

cpp 复制代码
Stack<double>

也是一个真正的类型。

它们是由同一个类模板生成的两个不同类型。


八、类模板成员函数的定义

8.1 类内定义成员函数

最简单的写法是直接在类模板内部定义成员函数:

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }

    void Push(const T& data)
    {
        _array[_size] = data;
        ++_size;
    }

private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

这种写法适合初学阶段理解。


8.2 类外定义成员函数

如果成员函数在类外定义,写法要完整一些。

先在类中声明:

cpp 复制代码
template<typename T>
class Stack
{
public:
    Stack(size_t capacity = 4);

    void Push(const T& data);

private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

类外定义构造函数:

cpp 复制代码
template<typename T>
Stack<T>::Stack(size_t capacity)
{
    _array = new T[capacity];
    _capacity = capacity;
    _size = 0;
}

类外定义 Push

cpp 复制代码
template<typename T>
void Stack<T>::Push(const T& data)
{
    _array[_size] = data;
    ++_size;
}

注意这里有两个关键点。

第一,类外定义时前面还要写:

cpp 复制代码
template<typename T>

第二,类名位置要写:

cpp 复制代码
Stack<T>::Push

不能只写:

cpp 复制代码
Stack::Push

因为 Stack 不是具体类型,Stack<T> 才表示模板参数为 T 的类模板实例。


8.3 为什么模板不建议声明和定义分离到 .h 和 .cpp?

普通类通常可以这样组织:

cpp 复制代码
Stack.h      // 声明
Stack.cpp    // 定义

但是类模板不建议简单这样拆。

原因是:

模板只有在实例化时,编译器才会根据具体类型生成代码。

如果模板定义放在 .cpp 文件里,而另一个 .cpp 文件只看到了 .h 里的声明,编译器在实例化时可能看不到模板函数的完整定义。

结果就可能出现链接错误。

所以模板代码通常有两种处理方式:

第一,把模板声明和定义都放在头文件中。

这是最常见、最简单的方式。

第二,使用显式实例化。

比如你明确只支持:

cpp 复制代码
Stack<int>
Stack<double>

可以在 .cpp 中显式实例化对应版本。

不过初学阶段更推荐第一种:

模板声明和定义放在同一个头文件中。


九、函数模板和类模板的区别

9.1 实例化方式不同

函数模板通常可以依靠实参自动推导类型:

cpp 复制代码
Swap(a, b);
Add(1, 2);

编译器能根据函数实参推导模板参数。

但类模板通常需要显式写出类型:

cpp 复制代码
Stack<int> st1;
Stack<double> st2;

因为创建对象时,编译器不能总是从上下文中准确推导出你想要的模板参数。

所以初学阶段可以记:

函数模板常常可以自动推导,类模板通常要在类名后面写 <类型>


9.2 使用场景不同

函数模板主要用于:

  • 通用函数逻辑
  • 交换
  • 比较
  • 查找
  • 加法
  • 打印
  • 工具函数

类模板主要用于:

  • 通用数据结构
  • 队列
  • 顺序表
  • 链表
  • 容器
  • 智能指针
  • 迭代器

比如标准库中的很多容器,本质上就是类模板:

cpp 复制代码
vector<int>
vector<double>
list<string>
map<string, int>

这些写法的本质都是:

用同一个类模板,实例化出不同类型的容器。


十、用类模板改造 Stack

10.1 基础版本

下面写一个简单的类模板版本 Stack

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

template<typename T>
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _array = new T[capacity];
        _capacity = capacity;
        _size = 0;
    }

    ~Stack()
    {
        delete[] _array;
        _array = nullptr;
        _capacity = 0;
        _size = 0;
    }

    void Push(const T& data)
    {
        if (_size == _capacity)
        {
            size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
            T* tmp = new T[newCapacity];

            for (size_t i = 0; i < _size; ++i)
            {
                tmp[i] = _array[i];
            }

            delete[] _array;
            _array = tmp;
            _capacity = newCapacity;
        }

        _array[_size] = data;
        ++_size;
    }

    void Pop()
    {
        if (_size > 0)
        {
            --_size;
        }
    }

    const T& Top() const
    {
        return _array[_size - 1];
    }

    bool Empty() const
    {
        return _size == 0;
    }

    size_t Size() const
    {
        return _size;
    }

private:
    T* _array;
    size_t _capacity;
    size_t _size;
};

使用时:

cpp 复制代码
int main()
{
    Stack<int> st1;
    st1.Push(1);
    st1.Push(2);
    st1.Push(3);

    while (!st1.Empty())
    {
        cout << st1.Top() << endl;
        st1.Pop();
    }

    Stack<double> st2;
    st2.Push(1.1);
    st2.Push(2.2);

    while (!st2.Empty())
    {
        cout << st2.Top() << endl;
        st2.Pop();
    }

    return 0;
}

这就是类模板的价值。

我们只写了一份 Stack,但它可以支持:

cpp 复制代码
Stack<int>
Stack<double>
Stack<char>
Stack<string>

甚至支持自定义类型。


10.2 这个 Stack 还不完美

上面的版本适合理解类模板,但它还是有一些问题的。

例如:

  • Top() 没有处理空栈情况;
  • 没有写拷贝构造;
  • 没有写赋值运算符重载;
  • 扩容时使用赋值,要求 T 支持赋值;
  • 对异常安全考虑不够;
  • 更推荐实际使用标准库 std::vectorstd::stack

但作为模板初阶案例,它已经能说明核心思想:

数据结构的逻辑不变,元素类型交给模板参数控制。


十一、模板初阶常见错误总结

11.1 忘记写 template 声明

错误:

cpp 复制代码
typename T>
void Swap(T& left, T& right)
{
}

正确:

cpp 复制代码
template<typename T>
void Swap(T& left, T& right)
{
}

11.2 把类模板当成类使用

错误:

cpp 复制代码
Stack st;

正确:

cpp 复制代码
Stack<int> st;

因为 Stack 是类模板名,不是具体类型。


11.3 类外定义成员函数时忘记 Stack

错误:

cpp 复制代码
template<typename T>
void Stack::Push(const T& data)
{
}

正确:

cpp 复制代码
template<typename T>
void Stack<T>::Push(const T& data)
{
}

11.4 误以为模板会自动处理所有类型转换

错误理解:

cpp 复制代码
Add(a, d);

其中 aintddouble,编译器会自动统一类型。

实际情况是:

如果模板只有一个 T,编译器可能无法推导出唯一类型。

可以改成:

cpp 复制代码
Add<int>(a, d);

或者:

cpp 复制代码
Add<double>(a, d);

或者把模板改成:

cpp 复制代码
template<class T1, class T2>
auto Add(T1 left, T2 right)
{
    return left + right;
}

11.5 模板声明和定义分离导致链接错误

错误做法:

cpp 复制代码
// Stack.h
template<typename T>
class Stack
{
public:
    void Push(const T& data);
};
cpp 复制代码
// Stack.cpp
template<typename T>
void Stack<T>::Push(const T& data)
{
}

然后在其他文件里使用:

cpp 复制代码
Stack<int> st;
st.Push(1);

可能出现链接错误。

初学阶段建议直接把模板定义写在头文件里。


十二、本文总结

本文主要讲了 C++ 模板初阶中的几个核心知识点。

泛型编程:

  • 编写与具体类型无关的通用代码;
  • 是代码复用的重要方式;
  • 模板是 C++ 泛型编程的基础。

函数模板:

  • 表示一族函数;
  • 本身不是具体函数;
  • 编译器根据实参类型生成对应函数版本;
  • 可以隐式实例化,也可以显式实例化。

模板参数推导:

  • 编译器根据实参推导模板参数;
  • 一个模板参数不能同时推导成两个不同类型;
  • 普通函数可以进行自动类型转换,模板推导更强调类型匹配。

模板匹配原则:

  • 普通函数和函数模板可以同名共存;
  • 精确匹配时通常优先普通函数;
  • 如果模板能生成更好匹配的版本,也可能选择模板;
  • 显式写 <类型> 可以指定调用模板版本。

类模板:

  • 用于生成一族类;
  • 类模板本身不是具体类型;
  • Stack<int>Stack<double> 才是真正类型;
  • 类外定义成员函数时要写 Stack<T>::函数名
  • 模板声明和定义通常放在头文件中。

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
2401_841495642 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
fqbqrr2 小时前
2607C++,soui与安卓
c++·soui
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
克里斯蒂亚诺更新2 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117765 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao5 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest