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>::函数名
  • 模板声明和定义通常放在头文件中。

相关推荐
郝学胜-神的一滴1 小时前
Qt 高级开发 031:QListWidget图标布局实战
开发语言·c++·qt·程序人生·软件构建·用户界面
踏着七彩祥云的小丑1 小时前
嵌入式测试学习第35 天:蓝牙、WiFi嵌入式设备测试基础概念
单片机·嵌入式硬件·学习
caimouse1 小时前
Reactos 第 8 章 结构化异常处理 — 8.4 软异常
服务器·开发语言·windows
艾莉丝努力练剑1 小时前
【Qt】界面优化:绘图API
linux·运维·开发语言·网络·qt·tcp/ip·udp
QiLinkOS1 小时前
极客精神与商业思维的融合实践(3)
c语言·c++·人工智能·算法·开源协议
牛油果子哥q1 小时前
队列(Queue)深度精讲,先进先出原理、顺序/链式/循环队列、STL queue底层、栈队列互模拟与面试考点全解
开发语言·c++·面试
暖阳华笺2 小时前
【数据结构与算法】哈希专题
数据结构·c++·算法·leetcode·哈希算法
聆风吟º2 小时前
【Python编程日志】Python基础数据类型完整梳理
开发语言·python·数据类型
LuminousCPP2 小时前
数据结构 - 单链表第二篇:单链表进阶操作
c语言·数据结构·笔记·链表