vector起步与底层-三个指针迭代器失效与为模板升级内置类型

vector 起步与底层:三个指针、迭代器失效的现场,与"为了模板升级内置类型"

我的github:(https://github.com/xcx55/ubuntu-linux-project)

感谢各位大佬参观我的github!!

一、vector 第一印象:和 string 高度相似,但少了两个东西

上手 vector 的第一个感受:用法和 string 高度相似 。毕竟 string 底层也是数组、也是动态扩容的顺序表,vector 只是把"字符"换成了任意类型 T。

但有两个东西 string 有、vector 没有:流插入和流提取。

想想其实合理------string 的每个元素是字符,整串打出来有天然的可读语义;vector 的元素类型是模板参数 T,编译器根本不知道你的自定义类型该怎么"流出去"。所以遍历 vector 只能走那三条老路:下标、迭代器、范围 for。

cpp 复制代码
std::vector<std::string> v;
v.push_back("东神");
v.push_back("小东东");
for (auto e : v) {
    std::cout << e << std::endl;
}

还有一个和 string 的细节差异:string 底层是数组但会多开一块空间存 \0 ,vector 不需要------它没有"以什么结尾"的约定,_finish 指到哪就是哪。

二、构造与 push_back 里的临时对象

cpp 复制代码
std::vector<std::string> v;
std::vector<int> c1{ 1, 2, 3 };

push_back 的签名是 void push_back(const value_type& val),所以传 "张三" 时,走的是类型转换生成临时对象 的路:const char* → 临时 string → 挂到 const 引用上。同理 v2.push_back(A(2,2)) 是显式构造匿名对象,v2.push_back({3,3}) 是多参数的隐式转换------本质都是先生成临时对象,再进容器。

而匿名对象的本质就是生成一个临时对象,出了这一行代码就会死亡。

范围 for 那条伏笔在这里也对上了:范围 for 的本质就是将 *迭代器 赋值给 e ------auto 识别类型,再用拷贝构造出 e。一个是构造,一个是临时后拷贝,两码事。

三、底层结构:string 是三个指针,vector 也是三个指针

一般顺序表(C 语言版)的底层是 _size + _capacity 加一个定长/动态数组,两个成员。但 string 和 vector 的底层是三个指针:

复制代码
_start        --------- 指向数据头
_finish       --------- 指向有效数据的尾(size 的位置)
_endofstorage --------- 指向容量尾(capacity 的位置)

所以 size() == _finish - _start、capacity() == _endofstorage - _start,全靠指针相减算出来。这个实现"有点像队列"------头、尾、capacity 三个游标的样子。

这也顺便解释了为什么拷贝构造要 size_t i = a._finish - a._start 这样先算长度、_start = new T[j] 再逐个搬------三个指针都要重新锚定到新空间。

四、迭代器失效:扩容那一刻,pos 变野指针

这是本篇的重点。场景:你在 erase/insert 里拿到了一个指向中间元素的迭代器 pos,然后一次扩容发生了------新开一块空间,数据整体搬家,旧空间释放。

复制代码
扩容前:_start ──────────── 旧空间,pos 指向其中
扩容后:_start ────────► 新空间,旧空间已还给操作系统

pos 还指着旧空间,它成了野指针------这就是"迭代器失效"。

把条件说全,使传入指针失效有两个前置条件:

  1. 传入指针参数------函数拿到了你的迭代器;
  2. 要扩容------扩容后整体位置改变。

两个凑齐,传入的迭代器就废了。find 返回的指针也可能是失效的,因为空间可能在你 find 之后被重置了。所以规矩是:传入迭代器参数、函数内部又可能扩容的,就要返回其修正值 ------调用方拿返回值更新自己的迭代器,it = v.erase(it) 这个写法的由来就在这。

这就是"改变量内部数组的结构后,旧迭代器全部不可信"的本质:vector 的迭代器就是裸指针,指针指着的空间被换掉了。

五、size_t h(100); ------为了模板,C++ 给内置类型补了构造函数

底层1 笔记里那个 size_t h(100); 引出一段很有意思的推理:

模板之后,对于缺省参数和默认值,C++ 就不得不考虑内置类型的构造函数。所以:

cpp 复制代码
int a = 110;   // 赋值初始化
int a(110);    // 构造式初始化------C++ 对内置类型进行了升级!

以前 C 语言里 int 哪来的构造函数?但模板泛型化之后,代码要写成对 T 一视同仁:T x; 得保证无论 T 是自定义类型还是 int 都能有一个"默认出来的值"。这样做就是为了适应模板------调默认构造,一般就是往 0 去靠近。内置类型在 C++ 里被补上了构造/析构的语法外衣,这是语言层面为了泛型做的让步。

六、写 vector 拷贝构造的三个提醒

笔记里给手写 vector 立了几条规矩:

  1. 有资源,就要写深拷贝,否则默认生成的拷贝函数是浅拷贝------一个函数结束时释放了这个空间,后面二次析构直接崩。
  2. 建议给成员定义缺省值 (C++11 的声明缺省值),针对于初始化列表------三个指针出生时就是 nullptr/0,忘了初始化也不至于带随机值上路。
  3. 构造、拷贝构造、赋值重载是三个完全不同的函数 :T a = 10 和 a = 10 调用的函数截然不同------前者是构造(含隐式转换),后者是赋值重载。

还有一句容易混的:"数组自定义 和嵌套类 完全不是一个东西"------数组元素是自定义类型,只是逐个调用构造即可;嵌套类是成员 private 里声明了自定义类类型的那种,两者在拷贝、析构上的行为完全不同层级的深浅拷贝问题,下一篇展开。

收个尾

vector 这一章的开局三件事:

  • 底层三指针,size/capacity 全是指针差值;
  • 迭代器失效 = 扩容换空间 + 裸指针指着旧空间,所以可能扩容的接口都要返回修正值;
  • 内置类型的"构造函数"是给模板铺路的 ------int a(110) 不是语法糖,是泛型时代的必需品。

下一篇进 memcpy 的坑:为什么 vector<string> 的扩容拷贝用 memcpy 会出大事。

相关推荐
誰能久伴不乏3 小时前
桌面客户端的运行日志系统:SQLite 分块 + 独立写线程 + 文本降级
数据库·c++·qt·架构·sqlite
choumin13 小时前
C++中万能引用与std::forward的作用和用法
c++·万能引用·std forward
下一站春风里14 小时前
位运算完整学习笔记:按位与(&)+ 异或(^)
开发语言·c++·算法
努力努力再努力wz16 小时前
【C++高阶系列】C++20 协程底层原理:函数为什么能暂停?从状态机到 Task,彻底串起挂起、恢复与协程调度
开发语言·c++·c++20·1024程序员节
划破黑暗的第一缕曙光17 小时前
[C++]:4.模板
c++·template·模板·函数模版·类模版
十五年专注C++开发17 小时前
pybind11:一个跨C++和Python平台的编程库
开发语言·c++·python·pybind11
CodeYp201917 小时前
第11讲:stack 和 queue
开发语言·c++·queue·stack
沫璃染墨18 小时前
《Qt从零入门系列(十五):Qt线程安全与锁机制详解——从QMutex到RAII与读写锁实战》
开发语言·c++·qt·安全·交互·线程·qt6.3
hetao173383718 小时前
2026-10-25 hetao1733837 的刷题记录
c++·算法