云计算百科
云计算领域专业知识百科平台

深入理解C++系列(06)——模版初阶与STL

文章目录

    • 上期回顾
  • 一、C++ 模板初阶
    • 一、本节核心主线
    • 二、泛型编程
      • 1. 为什么需要泛型编程?
      • 2. 泛型编程的思想
    • 三、函数模板
      • 1. 函数模板概念
      • 2. 函数模板格式
      • 3. 通用 Swap 函数模板
    • 四、函数模板的原理⭐️⭐️⭐️
      • 1. 函数模板不是函数
      • 2. 模板把重复工作交给编译器
    • 五、函数模板实例化⭐️
    • 1. 隐式实例化(常用)
    • 2. 隐式实例化的类型冲突
    • 3. 解决方式一:强制类型转换
    • 4. 解决方式二:显式实例化
    • 六、模板参数匹配原则
      • 1. 普通函数和函数模板可以同名共存
      • 2. 普通函数和模板都匹配时,优先普通函数
      • 3. 如果模板能生成更匹配的版本,会选择模板
      • 4. 模板一般不主动做类型转换
    • 七、类模板
    • 1. 类模板定义格式
    • 2. 类模板成员函数在类外定义
    • 3. 模板不建议声明和定义分离到 `.h` 和 `.cpp`⭐️⭐️⭐️⭐️
    • 八、类模板实例化
    • 九、易错点汇总
      • 易错点 1:把函数模板当成函数
      • 易错点 2:模板参数中用 `struct`
      • 易错点 3:同一个 `T` 被推导成不同类型
      • 易错点 4:以为模板会自动类型转换
      • 易错点 5:类模板实例化忘记写 `<类型>`
      • 易错点 6:类模板成员函数类外定义忘记 `Stack<T>::`
    • 十、本节总结
  • 二、STL 简介复习总结
    • 一、本讲核心主线
    • 二、什么是 STL
    • 三、STL 的版本
      • 1. 原始版本 / HP 版本
      • 2. P. J. 版本
      • 3. RW 版本
      • 4. SGI 版本
    • 四、STL 的六大组件
      • 1. 容器
      • 2. 算法
      • 3. 迭代器
      • 4. 仿函数
      • 5. 适配器
      • 6. 空间配置器
    • 五、本节总结⭐️⭐️
    • 下期预告
    • string
    • 结语

◆ 博主: @此生决int

分享编程知识!深挖底层原理!持续原创更新!

热门专栏: 深入理解 C++系列|算法系列 快速复习系列 |Java 速通系列

📌 本系列知识点前后关联较强,建议按照专栏顺序阅读哦

本系列主要面向的阅读人群

1,学完 C 语言,准备系统学习 C++ 的同学

2,已经学习过 C++,希望重新梳理知识体系的同学

3,想深入理解 C++ 设计思想与底层原理,而不仅仅停留在语法层面的同学

不止教你会写代码,更带你理解代码为什么这样设计!

上期回顾

  hello,大家好,这里是 此生决int,上一期呢我们学习了C++中数据的存储,新学习了new delete等等C++申请和释放内存的方式,那么今天我们再来学习一个C++面向对象的一个厉害的地方,模版和STL,这可以算的上是C++区别于C语言的最大的一个特点,甚至有人说,C语言+STL就是C++!!!那么今天我们就来学习学习这个模版和STL!

一、C++ 模板初阶


一、本节核心主线

模板初阶主要解决一个问题:

如何写一份与类型无关的通用代码,让编译器根据不同类型自动生成对应代码?


二、泛型编程

1. 为什么需要泛型编程?

如果要写交换函数,普通做法可能是函数重载:

void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}

void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}

void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}

问题:太麻烦了!

  • 代码逻辑完全一样,只是类型不同。
  • 每出现一个新类型,就要新增一个函数。
  • 代码复用率低。
  • 维护成本高,一个逻辑错误可能每个重载版本都要改。

  • 2. 泛型编程的思想

    泛型编程:深刻理解与类型无关的通用代码!!!

    编写与类型无关的通用代码,是代码复用的一种重要手段。

    模板就是泛型编程的基础。可以把模板理解成一个“模具”:

    模板 = 模具
    类型 = 材料
    实例化后的函数/类 = 具体成品


    三、函数模板

    1. 函数模板概念

    函数模板代表一个函数家族。就是众多只是类型不同的函数里面的类型参数被抽象成了一个T!


    2. 函数模板格式

    template<class T>//class或者typename都可以,但是不能用 `struct`
    template<typename T>
    返回值类型 函数名(参数列表)
    {
    }


    3. 通用 Swap 函数模板

    template<typename T>
    void Swap(T& left, T& right)
    {
    T temp = left;
    left = right;
    right = temp;
    }

    调用:

    int a = 1, b = 2;
    Swap(a, b);

    double x = 1.1, y = 2.2;
    Swap(x, y);

    编译器会根据实参类型生成:

    void Swap(int& left, int& right);
    void Swap(double& left, double& right);

    注意哦,编译器会生成对应的函数,根据模版,但是是你要什么生成什么而不是全部生成!


    四、函数模板的原理⭐️⭐️⭐️

    1. 函数模板不是函数

    函数模板本身不是函数,而是编译器生成具体函数的蓝图。编译期间,编译器会根据模板使用情况生成具体类型的函数。

    例如:

    Swap(a, b); // a、b 是 int

    编译器推导出:

    T = int

    于是生成一份处理 int 的 Swap 函数。


    2. 模板把重复工作交给编译器

    原来需要程序员手写:

    Swap(int&)
    Swap(double&)
    Swap(char&)

    使用模板后:

    程序员写一份模板,编译器按需生成多份具体函数。


    五、函数模板实例化⭐️

    用不同类型的参数使用函数模板,称为函数模板实例化。

    分为:

  • 隐式实例化
  • 显式实例化

  • 1. 隐式实例化(常用)

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

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

    int a1 = 10, a2 = 20;
    double d1 = 10.0, d2 = 20.0;

    Add(a1, a2); // T 推导为 int
    Add(d1, d2); // T 推导为 double


    2. 隐式实例化的类型冲突

    Add(a1, d1);

    如果模板是:

    template<class T>
    T Add(const T& left, const T& right)

    编译器推导:

    • 根据 a1 推导 T = int
    • 根据 d1 推导 T = double

    同一个 T 推导出两个类型,编译器无法决定,因此报错。


    3. 解决方式一:强制类型转换

    Add(a1, (int)d1);

    此时两个实参都可以推导为 int。


    4. 解决方式二:显式实例化

    显式指定模板参数类型:

    Add<int>(a1, d1);

    表示强制实例化出 T = int 的版本。

    如果类型不匹配,编译器会尝试隐式类型转换。


    六、模板参数匹配原则

    1. 普通函数和函数模板可以同名共存

    int Add(int left, int right)
    {
    return left + right;
    }

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


    2. 普通函数和模板都匹配时,优先普通函数

    Add(1, 2);

    如果普通函数完全匹配,编译器优先调用普通函数,而不是从模板生成一个实例。


    3. 如果模板能生成更匹配的版本,会选择模板

    例如:

    int Add(int left, int right);

    template<class T1, class T2>
    T1 Add(T1 left, T2 right);

    Add(1, 2.0);

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

    模板可以生成:

    Add(int, double)

    更匹配,所以会选择模板。


    4. 模板一般不主动做类型转换

    重点:

    在模板中,编译器一般不会进行类型转换操作,因为一旦转换出问题,编译器就需要背锅。

    例如:

    template<class T>
    T Add(T left, T right);

    Add(1, 2.0); // T 推导冲突,通常报错

    除非你显式指定模板参数:

    Add<int>(1, 2.0);


    七、类模板

    1. 类模板定义格式

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

    示例:

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

    void Push(const T& data);

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


    2. 类模板成员函数在类外定义

    类外定义成员函数时要写模板参数:

    template<class T>
    void Stack<T>::Push(const T& data)
    {
    _array[_size] = data;
    ++_size;
    }

    注意两点:

    1. 前面要写:

    template<class T>

    2. 类名位置要写:

    Stack<T>::


    3. 模板不建议声明和定义分离到 .h 和 .cpp⭐️⭐️⭐️⭐️

    在此强调:

    模板不建议像普通函数那样声明放 .h、定义放 .cpp,否则容易出现链接错误。

    当前阶段先记结论:

    模板通常把声明和定义都放在头文件中。

    后面深入模板编译原理时再理解原因。


    八、类模板实例化

    类模板实例化和函数模板不同。

    函数模板可以根据实参自动推导:

    Swap(a, b);

    但类模板使用时通常需要显式写类型:

    Stack<int> st1;
    Stack<double> st2;

    重点:

    Stack 是类模板名,不是真正的类型;Stack<int> 才是真正的类型。


    九、易错点汇总

    易错点 1:把函数模板当成函数

    函数模板不是函数,而是生成函数的模具。


    易错点 2:模板参数中用 struct

    正确:

    template<typename T>
    template<class T>

    错误:

    template<struct T>


    易错点 3:同一个 T 被推导成不同类型

    Add(1, 2.0);

    如果模板只有一个 T,编译器无法同时让 T 是 int 和 double。


    易错点 4:以为模板会自动类型转换

    模板推导阶段通常不会主动做类型转换。

    要么手动强转:

    Add(1, (int)2.0);

    要么显式实例化:

    Add<int>(1, 2.0);


    易错点 5:类模板实例化忘记写 <类型>

    错误:

    Stack st;

    正确:

    Stack<int> st;


    易错点 6:类模板成员函数类外定义忘记 Stack<T>::

    正确:

    template<class T>
    void Stack<T>::Push(const T& data)
    {
    }


    十、本节总结

    • 泛型编程:写与类型无关的通用代码。
    • 模板是泛型编程的基础。
    • 函数模板不是函数,是生成函数的蓝图。
    • 模板实例化:编译器根据类型生成具体函数或类。
    • 隐式实例化:编译器根据实参推导类型。
    • 显式实例化:程序员用 <> 指定类型。
    • 同一个模板参数不能同时推导成多个类型。
    • 普通函数和模板都匹配时,普通函数优先。
    • 模板能生成更匹配版本时,会选择模板。
    • 模板一般不会在推导阶段主动做类型转换。
    • Stack 是类模板名,Stack<int> 才是真正类型。
    • 模板定义通常放头文件,不建议声明定义分离到 .h/.cpp。

    二、STL 简介复习总结

    本节内容偏了解,可直接跳转到总结部分!


    一、本讲核心主线

    这一讲主要是 STL 的入门介绍,重点不是具体使用细节,而是建立整体框架:

  • STL 是什么
  • STL 有哪些版本
  • STL 的六大组件
  • STL 为什么重要
  • STL 应该怎么学
  • 一句话概括:

    STL 是 C++ 标准库的重要组成部分,它把常用数据结构和算法做成了可复用的软件框架,让我们不用重复造轮子。


    二、什么是 STL

    STL 全称:

    Standard Template Library
    标准模板库

    它是 C++ 标准库的重要组成部分。

    STL 不只是一个简单的组件库,而是:

    包含数据结构与算法的软件框架。

    它提供了大量通用组件,例如:

    • 顺序表 / 动态数组:vector
    • 链表:list
    • 栈:stack
    • 队列:queue
    • 优先级队列:priority_queue
    • 集合:set
    • 映射:map
    • 哈希表相关容器:unordered_map、unordered_set
    • 通用算法:排序、查找、遍历等

    这些内容也就是我们接下来要学习的内容!!


    三、STL 的版本

    本节我们介绍几个 STL 版本,复习时不需要死背细节,但要知道 SGI 版本比较重要。


    1. 原始版本 / HP 版本

    由:

    • Alexander Stepanov
    • Meng Lee

    在惠普实验室完成。

    特点:

    • STL 的原始版本。
    • 具有开源精神。
    • 是所有 STL 实现版本的始祖。

    2. P. J. 版本

    由 P. J. Plauger 开发。

    特点:

    • 继承自 HP 版本。
    • 被 Windows Visual C++ 采用。
    • 不能公开或修改。
    • 可读性较低,符号命名较怪。

    3. RW 版本

    由 Rouge Wage 公司开发。

    特点:

    • 继承自 HP 版本。
    • 被 C++ Builder 采用。
    • 不能公开或修改。
    • 可读性一般。

    4. SGI 版本

    由 Silicon Graphics Computer Systems 公司开发。

    特点:

    • 继承自 HP 版本。
    • 被 GCC / Linux 采用。
    • 可移植性好。
    • 可以公开、修改甚至贩卖。
    • 命名风格和编程风格可读性较高。

    重点:

    后续学习 STL 源码时,主要参考 SGI 版本。


    四、STL 的六大组件

    STL 通常分为六大组件:目前只做了解,后面都会详细讲

    组件作用
    容器 存储数据的数据结构
    算法 操作数据的通用算法
    迭代器 连接容器和算法,类似泛化指针
    仿函数 像函数一样使用的对象
    适配器 对已有组件进行包装转换
    空间配置器 负责内存申请和释放

    1. 容器

    容器用于存储数据。

    常见容器:

    vector
    list
    deque
    stack
    queue
    priority_queue
    set
    map
    unordered_set
    unordered_map

    可以把容器理解为:

    STL 中封装好的常用数据结构。


    2. 算法

    算法用于操作容器中的数据。例如:

    sort
    find
    count
    reverse
    lower_bound
    upper_bound

    算法通常不直接依赖某一种容器,而是通过迭代器操作数据。


    3. 迭代器

    迭代器是 STL 中非常核心的概念。

    作用:

    连接容器和算法。

    可以把迭代器先粗略理解为:

    类似指针的东西,用来访问容器中的元素。

    例如:

    vector<int> v;
    vector<int>::iterator it = v.begin();

    后面学习 STL 时,迭代器是重点。


    4. 仿函数

    仿函数是像函数一样使用的对象。

    本质通常是重载了 operator() 的类对象。

    例如后续可能看到:

    greater<int>()
    less<int>()

    当前阶段了解即可。


    5. 适配器

    适配器是对已有组件做包装,让它表现出另一种使用形式。

    典型例子:

    stack
    queue
    priority_queue

    它们底层通常依赖已有容器实现,但对外提供特定接口。

    例如:

    • stack 提供栈接口。
    • queue 提供队列接口。
    • priority_queue 提供堆 / 优先级队列接口。

    6. 空间配置器

    空间配置器负责内存管理。

    它是 STL 底层组件,主要负责:

    • 空间申请
    • 空间释放
    • 对象构造
    • 对象析构

    当前阶段知道它是 STL 的底层内存管理组件即可。


    五、本节总结⭐️⭐️

    • STL = Standard Template Library,标准模板库。
    • STL 是 C++ 标准库的重要组成部分。
    • STL 是数据结构和算法的软件框架。
    • STL 六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器。
    • 容器负责存数据。
    • 算法负责处理数据。
    • 迭代器负责连接容器和算法。
    • 适配器是对已有组件的包装转换。
    • 学习 STL 的三层境界:能用、明理、能扩展。
    • 后续源码学习主要参考 SGI 版本。
    • STL 不只是容器,还包括算法、迭代器、仿函数、适配器、空间配置器。
    • stack、queue、priority_queue 更准确地说是容器适配器。
    • 学 STL 不能只停留在会调接口,面试还要理解底层原理。
    • 迭代器是连接容器和算法的核心,后续要重点掌握。
    • SGI STL 版本可读性较高,后续源码学习主要参考它。

    下期预告

    string

    结语

      本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。   也欢迎订阅我的 深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++ 算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线 快速复习系列:知识梳理、查漏补缺,考前冲刺必备 Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


      愿每一次敲下键盘,都比昨天更进一步!

      愿每一行代码落下,都让未来多一种可能!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 深入理解C++系列(06)——模版初阶与STL
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!