21. 泛型编程上

泛型编程
泛型编程(generic programming)与面向对象编程的差异

泛型编程是一种与面向对象编程(object-oriented programming)截然不同的编程模式。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法

STL 通过通用算法(generic algorithm)不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。

复制代码
 // 泛型编程:用模板使算法独立于数据类型
 template <typename T>                    // T 为任意满足要求的类型
 T add(const T& a, const T& b) {          // 算法只依赖类型 T 的行为
     return a + b;                        // 要求 T 支持 operator+
 }
为何使用迭代器(iterator)

模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型------两者都是 STL 通用方法的重要组成部分。为数组和链表分别实现查找函数时,从实现细节上看两个算法不同(一个用数组下标遍历,一个把指针重置为 start->p_next);但从广义上说,两个算法是相同的:将值依次与容器中的每个值进行比较,直到找到匹配为止。

泛型编程旨在使用同一个 find 函数处理数组、链表或任何其他容器类型,即函数不仅独立于存储的数据类型,而且独立于容器本身的数据结构。模板提供了数据类型的通用表示,因此还需要遍历容器中值的通用表示------迭代器正是这样的通用表示。

迭代器应具备的特征

要实现通用的 find 函数,迭代器应具备以下特征:应能对迭代器执行解除引用(dereference)操作,即若 p 是迭代器,应对 *p 进行定义;应能将一个迭代器赋给另一个,即若 p 和 q 都是迭代器,应对表达式 p = q 进行定义;应能将一个迭代器与另一个进行比较看是否相等,即应对 p == q 和 p != q 进行定义;应能使用迭代器遍历容器中的所有元素,这可通过为迭代器 p 定义 ++p 和 p++ 来实现。

注:常规指针就能满足迭代器的要求,因此可以把指针用作迭代器;STL 按功能的强弱定义了多种级别的迭代器。

复制代码
 // 用指针作为迭代器:常规指针满足全部迭代器要求
 typedef double* iterator;                // 指针即迭代器
 ​
 // 用两个区间指针重写查找:begin 指向起始,end 指向超尾
 iterator find_arr(iterator begin, iterator end, const double& val) {
     for (iterator ar = begin; ar != end; ++ar) { // 遍历区间 [begin, end)
         if (*ar == val)                  // 解除引用比较值
             return ar;                   // 找到:返回迭代器
     }
     return end;                          // 未找到:返回超尾迭代器
 }
为链表定义迭代器类
复制代码
 struct Node {                            // 链表节点
     double item;                         // 节点数据
     Node* p_next;                        // 指向下一节点
 };
 ​
 class iterator {                         // 链表迭代器类
     Node* pt;                            // 当前节点指针
 public:
     iterator(Node* pn = nullptr) : pt(pn) {} // 构造函数
     double& operator*() { return pt->item; } // 解除引用:返回节点数据
     iterator& operator++() {             // 前缀 ++
         pt = pt->p_next;                 // 移到下一节点
         return *this;                    // 返回自身
     }
     iterator operator++(int) {           // 后缀 ++(参数 int 不使用)
         iterator tmp = *this;            // 保存旧值
         pt = pt->p_next;                 // 移到下一节点
         return tmp;                      // 返回旧值副本
     }
 };
超尾元素:把要求从迭代器转移到容器类

数组版 find_arr 使用超尾迭代器(past-the-end iterator)检测结尾,链表版 find_ll 使用存储在最后一个节点中的空值检测结尾;除了这种差别外两个函数完全相同。可以让数组和链表都有超尾元素,并在迭代器到达超尾位置时结束搜索------这样两个 find 检测数据尾的方式将相同,从而成为相同的算法。

STL 遵循这一方法:每个容器类(vector、list、deque 等)定义相应的迭代器类型(可能是指针,也可能是对象),每个容器类都有超尾标记、begin() 和 end() 方法;begin() 返回指向第一个元素的迭代器、end() 返回指向超尾位置的迭代器。

复制代码
 // 各容器提供统一的 begin()/end() 接口与迭代器类型
 // std::vector<double> scores;
 std::vector<double>::iterator pr;        // vector 类作用域内的迭代器 typedef
 // for (pr = scores.begin(); pr != scores.end(); ++pr)
 //     // 从第一个元素遍历到超尾位置
 // 改用 std::list<double> 时唯一不同是 pr 的类型(list 的迭代器)
 // C++11 可用 auto pr = scores.begin(); 自动推断
六大组件概述

STL(Standard Template Library)提供六大组件:容器(containers)、算法(algorithms)、迭代器(iterators)、仿函数(functors)、配接器(adapters)、配置器(allocators),彼此可以组合套用。它不是面向对象的,主要依赖模板而非封装、继承和虚函数。

STL 的通用方法总结

STL 的通用方法分两步:

  1. 处理容器的算法应尽可能用通用的术语来表达,使之独立于数据类型和容器类型; 如:同一份 sort 代码,不需要修改就能对 vector(连续内存)生效,也能对 list(链表)生效。

  2. 即基于算法的要求,设计基本迭代器的特征和容器特征。不同的算法对"移动能力"的要求不同。 如:

    1. vector(数组):内存连续,天生支持"随机跳跃",所以它提供的迭代器是随机访问迭代器,满足 sort 的要求。

    2. list(链表):内存不连续,只能一个一个节点找,不能跳跃。所以它提供的迭代器是双向迭代器。 C++ 标准库专门为 list 提供了一个专属的 list::sort(利用链表特性归并排序)

复制代码
 // 避免直接写迭代器循环,优先使用 STL 算法或范围 for
 // std::for_each(scores.begin(), scores.end(), Print);  // 算法处理细节
 // for (auto x : scores)                                // C++11 范围 for
 //     // 依次访问每个元素
配接器(adapter)

配接器是一种用来修饰容器、仿函数或迭代器接口的组件,把一种接口转换成 STL 使用的另一种接口。

配接器不改变被包装组件的内部实现,只改变对外暴露的接口;配接器容器(stack/queue/priority_queue)因此不提供迭代器。

复制代码
 #include <stack>
 std::stack<int> stk;        // 容器适配器:把底层 deque 包装成栈接口
 // stk.push(x);  stk.pop();  // 只提供栈操作,不支持随机访问与遍历
配置器(allocator)

定义(是什么):配置器是负责空间配置与管理的类模板,实现动态空间配置、空间管理和空间释放,是 STL 的六大组件之一。

把内存分配策略从容器中分离出来,使容器代码不直接依赖 new/delete,可替换为内存池等更高效的分配策略。

容器模板的最后一个模板参数是分配器,默认 allocator<T>(内部使用 new 和 delete);一般无需显式指定。

复制代码
 #include <vector>
 std::vector<int> v;                     // 省略分配器参数:默认 allocator<int>
 // template<class T, class Allocator = allocator<T>> class vector;
 // 分配器负责容器的动态内存申请与释放
迭代器的五种类型

STL 定义了 5 种迭代器:输入迭代器(input iterator)、输出迭代器(output iterator)、正向迭代器(forward iterator)、双向迭代器(bidirectional iterator)和随机访问迭代器(random access iterator)

因为不同的算法对迭代器的要求不同------查找算法需要定义 ++ 以便遍历整个容器,要求能读取数据但不要求能写数据;排序算法要求能随机访问以便交换两个不相邻的元素,且要求能读写数据。

复制代码
 // 算法原型用迭代器类型标注需求
 template <class InputIterator, class T>
 InputIterator find(InputIterator first, InputIterator last, const T& value);
 // 需要输入迭代器:++ 遍历、可读、无需随机访问
 ​
 template <class RandomAccessIterator>
 void sort(RandomAccessIterator first, RandomAccessIterator last);
 // 需要随机访问迭代器:可读写、可交换不相邻元素
输入迭代器(input iterator)

输入迭代器的算法不会修改容器中的值。且必须能够访问容器中所有的值,这通过支持 ++ 运算符(前缀和后缀格式)实现。

对于单通行(single-pass)、只读算法,可以使用输入迭代器。

输入迭代器是单向迭代器,可以递增,但不能倒退。

输出迭代器(output iterator)

输出迭代器与输入迭代器相似,只是解除引用让程序能修改容器值,而不能读取。

可以修改发送到显示器的字符流,却不能读取屏幕上的内容;

对于单通行、只写算法,可以使用输出迭代器。输出迭代器也不能倒退。

正向迭代器(forward iterator)

与输入迭代器和输出迭代器相似,正向迭代器只使用 ++ 运算符来遍历容器,每次沿容器向前移动一个元素;但与输入、输出迭代器不同的是,它总是按相同的顺序遍历一系列值。将正向迭代器递增后,仍然可以对前面的迭代器值解除引用(如果保存了它),并可以得到相同的值。正向迭代器既可以使得能够读取和修改数据,也可以使得只能读取数据(如用 int* 表示读写迭代器、用 const int* 表示只读迭代器)

正向迭代器是单向前进的;读写能力由所指类型是否为 const 决定。

注:正向迭代器虽然是单向前进的,但会保存副本。所以仍然可以对前面的迭代器值解除引用。

双向迭代器(bidirectional iterator)

双向迭代器在正向迭代器的全部功能之上增加 --pp--

随机访问迭代器(random access iterator)

随机访问迭代器具有双向迭代器的所有特性,同时添加了支持随机访问的操作(如指针加法运算)和用于对元素进行排序的关系运算符:a + n(指向 a 之后第 n 个元素)、a - n(指向 a 之前第 n 个元素)、r += n、r -= n、an(等价于 *(a + n))、b - a(结果为这样的 n 值:b == a + n)、以及 a < b、a > b、a <= b、a >= b 关系比较。

a + n 这样的表达式仅当 aa+n 都位于容器区间(包括超尾)内时才合法。

迭代器层次结构与算法选用

算法选用的"向下兼容"原则。"正向迭代器"具备"输入和输出迭代器的全部功能(方法)";"双向"具备"正向的全部功能";"随机访问"具备"双向的全部功能"。

注:"可以使用方法" ≠ "可以自动类型转换"。

迭代器类型 拥有的"核心能力"(方法/操作) 相比上一级新增的能力
输入迭代器 ++(前移)、*(读取)、==/!=(比较) ------
输出迭代器 ++(前移)、*(写入) ------
正向迭代器 ++*(读写)、==/!= 多趟通行 (保存旧副本依然有效) + 同时具备读写能力(如果没有 const
双向迭代器 正向的全部能力 --(后退一步)
随机访问迭代器 双向的全部能力 +n / -n(跳跃)[](下标访问)< / >(比较大小)
相关推荐
青瓦梦滋2 小时前
传输层UDP/TCP协议
linux·网络·c++·网络协议·tcp/ip·udp
AI备案指南-满满2 小时前
大模型与算法备案全流程详解:从零到通过的完整指南
人工智能·算法·备案·大模型备案·算法备案
江畔柳前堤2 小时前
LLM 训练核心机制深度解析:Warmup、Cosine Decay 与 Perplexity 的完整知识体系
网络·人工智能·深度学习·算法·机器学习·语音识别
一只旭宝2 小时前
细讲C加加【9】C++ std::function与std::bind详解|仿函数、绑定器、类成员绑定、占位符、成员偏移指针
开发语言·c++·算法
良木林2 小时前
子串 - LeetCode hot 100
算法·leetcode·职场和发展
摇滚侠3 小时前
《SpringBoot 3:入门与应用实战》第 2 章 IOC 思想与实现 阅读笔记 1
spring boot·笔记·后端
陌诺曦.3 小时前
Python扩展小练习
算法
蒸蒸yyyyzwd3 小时前
秋招不熟悉力扣学习笔记1
笔记·学习·leetcode