目录
- 前言
- 一、stack的模拟实现
-
- [1.1 适配器Container的封装概念](#1.1 适配器Container的封装概念)
- 二、queue的模拟实现
- 三、deque的介绍
-
- [3.1 deque的使用](#3.1 deque的使用)
- [3.2 CPU高速缓存访问命中率与数据访问效率](#3.2 CPU高速缓存访问命中率与数据访问效率)
-
- [3.2.1 数据访问效率](#3.2.1 数据访问效率)
- [3.2.2 CPU高速缓存访问命中率](#3.2.2 CPU高速缓存访问命中率)
- 结语


🎬 云泽Q :个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、stack的模拟实现
对于一般人来说模拟实现的栈的底层就是一个数组,让数组尾部做栈顶。无论是数组栈还是链式栈,很多的东西都是和顺序表和链表是类似的
但是STL中的栈不是这样实现的,STL在设计初的考量就是相似的一些东西直接复用,像实现一个数组栈,直接如图封装一个vector实现,就不用再实现顺序表和链表的内部的一些逻辑了。

1.1 适配器Container的封装概念
且C++设计STL的这些大佬的思路还更开阔一些,他们还加入了一种叫做容器适配器 (Container,转换的意思)的东西


这样封装适配器的思路是非常牛的
"自己从头实现栈" 的写法(比如注释里的T* _a+_top+_capacity)------ 需要自己管理动态数组的扩容、释放;而现在这个stack是 "复用现有容器(比如vector/list)的功能,包装成栈的接口",底层存储完全交给Container(比如vector),自己只封装 "栈需要的操作"(push/pop/top 等)。
这种适配结构的核心优点
- 【少写重复代码】复用现有容器,不用自己实现底层存储
原来注释里的 "数组栈" 需要自己写:
-
- 动态数组的初始化、扩容(比如_capacity满了要realloc);
-
- 内存的释放(防止内存泄漏);
-
- 下标访问、边界检查等。
而现在的stack直接用vector/list作为底层容器 ------ 这些容器本身已经实现了动态扩容、内存管理、尾插 / 尾删 等功能,你只需要调用_con.push_back()/_con.pop_back(),相当于 "站在现有容器的肩膀上",不用重复造轮子。
- 下标访问、边界检查等。
- 【灵活切换场景】换底层容器只改模板参数,不用动栈的逻辑
代码里用的是vector< int >作为底层(yunze::stack<int, vector< int >>),但如果场景变了:
- 比如需要 "频繁在栈顶插入 / 删除,且不想承担vector扩容的开销"------ 只需要把模板参数换成list< int >,代码改成:
cpp
yunze::stack<int, list<int>> st; // 底层变成链表,其他代码完全不用改
st.push(1); // 还是用同样的push接口
st.top(); // 还是用同样的top接口
底层容器换了,但栈的调用代码完全不用改------ 因为stack封装了统一的接口,不管底层是vector还是list,对外都是 "栈的操作"。
-
【统一接口】不用关心底层实现,只用记 "栈的用法"
不管底层是vector(数组)还是list(链表),你用这个stack时,只需要调用
push/pop/top------ 不用关心底层是 "数组尾插" 还是 "链表尾插",也不用记vector的push_back或list的push_back细节,只需要记 "栈的标准操作" 即可,降低了学习和使用的成本。 -
【稳定可靠】借现有容器的成熟性,减少 bug
STL 里的
vector/list是经过大量测试、优化的容器,比自己写的 "数组栈"(比如注释里的T* _a)更稳定:
- 比如vector的扩容逻辑是经过性能优化的(通常是 2 倍扩容,减少频繁申请内存的开销);
- list的尾插 / 尾删是 O (1) 且不会有内存碎片问题;
- 这些容器本身也做了边界检查(比如empty()时调用back()会报错)。
用它们作为底层,比自己手写的存储结构更不容易出 bug。
- 【适配多类型】底层容器支持的类型,栈都能直接用
比如你想存string类型的栈,只需要写:
cpp
yunze::stack<string, vector<string>> st;
st.push("hello");
cout << st.top() << endl; // 输出hello
因为vector< string >本身支持string类型,你的stack不需要额外写任何代码,直接适配所有Container支持的类型。
看到这里可能看着该全新的写法还有一些疑问,下面再看一下具体的模板参数实例化和函数调用过程






库中的STL还支持使用缺省参数deque(一个容器),deque既不是一个数组栈,也不是一个链式栈,而是一个双端队列适配出来的栈,双端队列不要求先进先出,其在功能上是list和vector的融合体

二、queue的模拟实现
队列是队尾入数据,队头出数据


也可以在queue的类中用erase代替pop_front();vector不支持直接头删,但是erase是支持头删的,但是这就让效率强行降低了,STL中的vector设计之初没有直接支持头删接口的原因就是希望少用(功能上还是支持的)
cpp
void pop()
{
_con.erase();
}
三、deque的介绍
deque(双端队列) :是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),也可以在中间任意位置插入删除,与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高,其在功能上是list和vector的融合体,

接口和list和vector的接口都基本是类似的
3.1 deque的使用

仅管从功能上看deque好像可以替代vector和list,但是在实际情况下是不可以的,如果可以的话STL就没有vector和list两个容器了
deque更像是一个有着很好的设计初衷,为了解决vector和list的问题,但实际成品没有达到预期的一个容器,但是其也有很多可取之处
下图是vector和list的优缺点

3.2 CPU高速缓存访问命中率与数据访问效率
3.2.1 数据访问效率
CPU高速缓存访问命中率高间接的也就是说数据访问效率高,反之。该优缺点在归并排序和快速排序的性能测试尤其体现,这里的性能测试该篇文章有写C++ List 容器详解:迭代器失效、排序与高效操作

这里上面打错了,是100w个数据的数组。
排序的性能差异就是由于数据的访问效率低,排序的过程就要对数据结构中的数据进行大量的访问和交换,数据越多,这个差异就体现出来了
3.2.2 CPU高速缓存访问命中率
下面说一下CPU高速缓存访问命中率这个概念
一、计算机存储介质的层级特性
计算机的存储体系核心是 "速度越快→容量越小→成本越高" 的层级设计,从慢到快、从大到小分为:
- 硬盘:永久存储介质(断电数据不丢失),容量极大(几百 GB~ 数 TB),但访问速度最慢(毫秒级)。程序 / 文件的持久化存储依赖硬盘,读取硬盘数据时,需先将数据加载到内存中才能被 CPU 访问;
- 内存:临时存储介质(断电数据丢失),容量适中(8GB/16GB/32GB 为主),访问速度远快于硬盘(几十纳秒级),但仍慢于 CPU 运算速度。所有运行中的程序、数据结构(如 vector/list)的实际数据都存储在内存中;
- CPU 缓存(L1/L2/L3):介于内存和寄存器之间的高速存储,容量远小于内存(L1 几十 KB、L2 几百 KB、L3 几 MB~ 几十 MB),访问速度是内存的 10~100 倍(纳秒级);
- 寄存器 :CPU 内部的超高速存储(几字节~几十字节),速度最快(亚纳秒级),但容量极小,仅能存储 CPU 运算时的临时数据(如 ++ 操作的中间值)。


CPU 执行指令的核心逻辑:为什么缓存是刚需
以代码for(auto& e : con) { e++; }为例,CPU 执行 "e++" 的本质是:
- 从存储介质中读取 e 的数值;
- 把数值加载到寄存器中,通过加法器完成 ++ 运算;
- 把运算后的数值写回存储介质。
如果直接从内存读取 / 写入e,会因为内存速度远慢于 CPU 运算速度,导致 CPU 大部分时间处于 "等待数据" 的空闲状态(即 "CPU stall")。因此 CPU 设计了缓存层级:
- 优先从缓存 / 寄存器读取数据,只有缓存中没有目标数据时,才从内存加载;
- 加载数据时,不是 "要多少读多少",而是基于 "局部性原理" 批量加载,最大化缓存的利用率。

CPU 三级缓存的结构与访问规则
如四核 CPU 缓存架构图所示:
- 缓存的层级归属:
-
- L1 缓存:每个 CPU 核心独占,且分为 "数据缓存(Data Cache)" 和 "指令缓存(Instruction Cache)"------ 程序的执行指令(如 for 循环的跳转、++ 运算指令)会加载到指令缓存,数据(如 vector/list 的元素)会加载到数据缓存;
-
- L2 缓存:每个 CPU 核心独占,不区分数据 / 指令,容量比 L1 大,速度略慢;
-
- L3 缓存:所有 CPU 核心共享,容量最大,速度比 L2 慢,但仍远快于内存。
- 缓存行(Cache Line):数据加载的最小单位 :
CPU 从内存向缓存加载数据时**,必须按 "缓存行" 加载**(主流 CPU 的缓存行大小为 64 字节)------ 哪怕只需要 1 个 4 字节的int变量,CPU 也会把该变量所在的 "64 字节连续内存块" 全部加载到缓存中。
原因:加载数据的硬件成本(如内存总线的 "探测线" 读取 0/1 比特位)与加载量无关,类似 "开公交车接人,接 1 人和接 30 人(64 字节)的发车成本一致",不如一次性加载更多数据。 - 缓存命中 / 未命中的访问流程 :
CPU 访问数据时,按 "L1→L2→L3→内存" 的顺序检查:
-
- 命中(Hit):目标数据在某一级缓存中,直接读取,效率最高;
-
- 未命中(Miss):目标数据不在缓存中,需先从内存加载 "目标数据所在的 64 字节缓存行" 到 L3→L2→L1 缓存,再从缓存读取(此过程会产生 "缓存缺失延迟",是性能瓶颈的核心)。
局部性原理:缓存设计的核心依据
CPU 缓存的批量加载逻辑,完全基于局部性原理(计算机程序的核心运行规律):
- 时间局部性:访问过的某个内存地址数据,短期内大概率会再次访问(如 for 循环的迭代变量i会被多次读取 / 修改);
- 空间局部性 :访问过的某个内存地址数据,其相邻的连续内存地址数据短期内大概率会访问(如数组的第 i 个元素后,大概率会访问第 i+1 个元素)。
基于这一原理,CPU 加载 "当前数据 + 后续连续数据" 到缓存,能最大化 "缓存命中" 的概率 ------ 这也是vector和list缓存命中率差异的核心根源。
五、缓存命中率的定义与核心影响
缓存命中率 = 缓存命中的访问次数 / 总数据访问次数 × 100%。
- 命中率越高:CPU 越少访问慢内存,数据访问效率越高,程序执行速度越快;
- 命中率越低:CPU 频繁触发 "缓存未命中→内存加载",大量时间浪费在等待数据上,效率越低。
缓存污染
缓存容量是有限的,若加载了大量 "后续不会被访问" 的数据到缓存,会挤占 "有用数据" 的缓存空间,导致有用数据被挤出缓存→后续访问有用数据时触发 "未命中",这种现象称为缓存污染。
- 例如:遍历一个分散存储的 list 时,每次加载的缓存行只包含 1 个有效节点,其余 60 + 字节都是无用数据,这些无用数据会挤占缓存空间,导致其他有用数据被污染,进一步降低命中率。

- vector:极致利用缓存→命中率接近 100%
以vector<int> v = {1,2,3,4,5,...,20}为例(int 占 4 字节,20 个元素占 80 字节):
- 当 CPU 访问第一个元素v[0]=1(地址
0x100)时,触发 "缓存未命中",CPU 从内存加载0x100~0x13F(64 字节,包含前 16 个元素:1~16)到 L1 缓存; - 访问
v[1]=2~v[15]=16时,这些元素已在 L1 缓存中,100% 命中,直接读取; - 访问v[16]=17时,触发第二次 "未命中",加载
0x140~0x17F(64 字节,包含 17~20)到缓存,后续访问17~20仍为命中。
整个遍历过程中,仅触发 2 次缓存未命中,其余 18 次均为命中,命中率≈90%;且 vector 无缓存污染(加载的缓存行全是有效元素)。
- list:完全违背缓存设计→命中率接近 0
以list<int> l = {1,2,3,4}为例:
- 访问第一个节点1(地址
0x100):未命中,加载0x100~0x13F到缓存(但该缓存行中只有0x100是有效节点 1,其余 60 字节均为无用数据,缓存污染); - 访问第二个节点2(地址
0x200):该地址不在上一个缓存行中,触发未命中,加载0x200~0x23F到缓存(仅0x200是有效节点 2); - 访问节点 3、4 时,重复上述过程,每访问一个节点都触发未命中,命中率≈0%。
且每次加载的缓存行中,有效数据仅占 4/64=6.25%,其余均为无用数据,严重污染缓存→挤占其他有用数据的缓存空间,进一步降低整体效率。

还有专门写该部分原理的博客程序员相关的CPU缓存知识
结语
