深刻理解C++STL库常见容器功能和底层

vector

底层机制

vector本质是一个动态分配连续内存的数组

维护了三个指针(迭代器)

  • _start:指向连续内存的起始物理地址
  • _finish:指向当前最后一个有效元素下一个位置
  • _end_of_storage:指向整块内存的末尾

有了三个指针,所有方法都是指针运算:

  1. size()(当前元素个数): 就是 finish - start

  2. capacity()(当前总容量): 就是 end_of_storage - start

  3. empty()(是否为空): 就是判断 start == finish

这里需要注意:

C++STL设计是左闭右开:[start, end)

动态扩容机制

什么时候会扩容,怎么扩容?

  • 触发场景:_finish == _end_of_storage时,没有空间塞数据了
  • 做法:一定不是原地扩容,OS很难在原内存后面找到一块合适的空间!
    • 去堆上找一块全新的、更大的内存(GCC通常2倍,MSVC通常1.5倍)
    • 把老内存里的数据,逐一**拷贝(或移动)**到新内存
    • 调用老内存中所有对象的析构函数
    • 释放老内存空间

为什么不能原地扩容?

涉及到缓存命中的问题:

  • 内存碎片:堆上的内存分配是动态、无序的,new了一个100字节内存,随着程序运行,这100字节前后的内存大概率都被其它对象占用了
  • 缓存命中:CPU从内存中读数据非常慢(本质是拷贝),CPU内部有L1/L2/L3三级缓存(SRAM),CPU每次去内存拿数据,会把目标地址附近的一整块连续内存(通常是64字节,一个Cache Line)一次性取完

如果 vector 容量满了,你想在原地直接把这块内存拉长,现实中极大概率是做不到的,因为屁股后面紧挨着的那块物理内存,已经被别人占了!

深拷贝、浅拷贝、移动辨析

  • 浅拷贝:纯粹按位赋值,对于内置类型没问题,但是对于自定义类型,浅拷贝只会拷贝指针的地址 ,这导致了新老vector里的指针指向同一块内存,任意一方修改数据,会影响到另一方,而且会造成Double Free问题
  • 深拷贝:开辟一块和源vector一样的内存空间,并拷贝源vector的数据到新的内存中
  • 移动语义:老内存若是快要销毁,直接把老对象的资源指针过来,把老对象的指针置空即可

注意:只有当类明确使用 noexcept 标记了移动构造函数 时,vector 扩容才会大胆地使用移动语义!vector是极其保守的!

迭代器机制

迭代器本质是封装了指针的类对象,,并重载了 ++--\*-> 等运算符

对于使用者来说,无论底层是连续数组还是复杂的红黑树,都可以统一使用 it++ 来走向下一个元素,用 *it 来获取数据。它完美屏蔽了底层数据结构内存布局的差异

这就是C++封装的思想:屏蔽底层实现细节,暴露给用户一套标准的操作接口,用户不用关心底层到底是怎么实现的

但是,迭代器很容易失效!

我们先讲连续内存派:vectorstring为例

  • 失效场景1:触发动态扩容

    • 原因:vector 扩容是去堆上找一块新内存,然后把老内存释放掉。如果在扩容前保存了一个迭代器,扩容后,这个迭代器里面包着的指针还指着那块已经被系统回收的老内存!
    • 解决: 扩容后,绝对不要使用 任何之前保存的迭代器、指针或引用。如果需要记录位置,记录索引而不是迭代器
  • 失效场景2:中间插入或删除

    • 原因: 当调用 v.erase(it) 删除中间某个元素时,为了保持物理连续性,vector 会把该位置之后的所有元素统统往前挪一步。此时,手里的 it 虽然物理地址没变,但它指向的数据已经变成了原本在它后面的那个元素!如果你再执行一次 it++,你就会漏掉一个元素

    • 解决:利用 erase 的返回值。erase 会自动返回一个指向被删除元素下一个位置的有效迭代器

      cpp 复制代码
      // 错误写法(会漏删元素,或者越界崩溃)
      while(it != v.end()) {
          if(*it == 2) v.earse(it);
          it++;
      }
      
      // 正确写法
      while(it != v.end()) {
          if(*it == 2) it = v.earse(it);	// 删除了,erase已经帮你走了一步,接管新迭代器就好
          else it++;	// 只有在没有删除的情况下,才会手动往后走
      }

高频API

push_back()和emplace_back()

  • push_back 的本质:接收一个已经构造好的对象(或者右值)。在底层会调用拷贝构造函数移动构造函数 ,将数据放进 vector 的内存中

  • emplace_back 的本质:它的底层使用了 C++11 的可变参数模板和完美转发 。它不接收对象,而直接接收"构造对象所需的参数",然后在 vector _finish 指针指向的那块裸内存上,直接调用定位 new(Placement new) 原位构造对象

emplace_back伪代码:

cpp 复制代码
template <typename... Args>  // 1. 可变参数模板:接收任意数量参数
void emplace_back(Args&&... args) { // 万能引用:接住所有左值/右值
    
    // 检查容量,不够就扩容...
    if (_finish == _end_of_storage) {
        // ... 扩容逻辑
    }
    
    // 核心动作
    // 2. 完美转发:std::forward<Args>(args)... 原封不动地把参数状态传下去
    // 3. 定位 new:在 _finish 裸指针指向的内存上,直接原地构造对象!
    new ((void*)_finish) T(std::forward<Args>(args)...); 
    
    // 指针后移
    ++_finish;
}

为什么emplace_backpush_back快?

两者执行v.push_back(User("张三", 18))v.emplace_back("张三", 18) 时的动作:

push_back 路线:

  1. 在外面的栈上构造一个临时的 User 对象
  2. 把临时对象作为参数传进 push_back
  3. push_back 内部调用移动构造函数 (或拷贝构造),把数据挪到 _finish 的内存上
  4. 析构外面栈上的那个临时 User 对象

emplace_back 路线:

  1. 把字符串 "张三" 和整数 18 打包
  2. 完美转发到底层
  3. _finish 内存上直接调用带参构造函数,一次性成型!

零次临时对象创建,零次拷贝/移动构造,零次临时对象析构

总结:只要是往容器尾部追加元素,无脑优先使用 emplace_back

clear()

clear的本质是逻辑清空 ,循环调用所有有效元素的析构函数,然后把_finish指针拉回到_start的位置,size归零,但是capacity还占着呢,内存并没有释放,把这个释放时机交给用户处理

那怎么释放这块大内存呢?

cpp 复制代码
std::vector<int> v;
// ... 假设 v 经历了大量插入,capacity 暴涨到了 100000,但现在 size 只有 10
// v.clear(); // 没用,capacity 还是 100000

// 利用匿名临时对象交换指针
std::vector<int>(v).swap(v);
  1. std::vector<int>(v) 触发拷贝构造,创建了一个匿名临时对象只申请刚好能装下 10 个元素的内存 (它的 capacity 就是 10)

  2. .swap(v) 将临时对象内部的三个指针,与原来 v 内部的三个指针进行互换。

  3. 互换后,v 的容量成功收缩到了 10。而那个匿名临时对象手里现在攥着原来那 100000 的庞大内存

  4. 这行代码一结束,匿名临时对象生命周期到期,自动调用析构函数,顺理成章地带着那块庞大的内存同归于尽

resize()和reserve()

  • reserve(n)预留空间,操作的是 _end_of_storage 指针
    • 动作:和OS交涉:"给我准备能装n个元素的连续空间!"
    • 结果:只改变capacity,绝对不改变size_finish指针保持不动,新开辟的空间未初始化,绝对不能v[index]去访问
  • resize(n)调整大小,操作_finish指针
  • 动作:它不仅要求物理空间到位,还会在这些空间上真正去构造对象
  • 结果:它同时改变 capacitysize 。如果 n 大于当前的 size,它会调用对象的默认构造函数把多出来的坑填满。填满后,可以直接用 v[index] 去访问修改

未完待续~

相关推荐
wjm0410062 小时前
ios学习路线 -- Swift基础(1)
开发语言·ios·swift
夏玉林的学习之路2 小时前
委托构造和using关键字
开发语言·c++·算法
jiang_changsheng2 小时前
VMware 虚拟机无法上网排查解决教程
开发语言·网络·php
历程里程碑2 小时前
Linux 46 HTTPS(协议原理)安全通信全流程解析
linux·网络·c++·网络协议·http·https·排序算法
阿成学长_Cain2 小时前
Windows IP 配置查看器 ipconfig 详解
开发语言·php
Yupureki2 小时前
《C++实战项目-高并发内存池》2.ObjectPool构造
linux·服务器·c语言·开发语言·jvm·c++
XiYang-DING2 小时前
【Java SE】Java中的static关键字总结
java·开发语言
格林威2 小时前
工业相机图像高速存储(C++版):内存映射文件(MMF)零拷贝方案,附堡盟 (Baumer) 相机实战代码!
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
xiaoliuliu123452 小时前
CentOS 7 安装 gcc-c++-4.8.5-44.el7.x86_64.rpm 详细步骤(含依赖解决)
linux·c++·centos