关于 vector

1. vector 的底层本质:连续的动态数组

std::vector的核心是一块连续的、固定大小的内存块(可以理解为 “一整块连续的货架”),它有两个关键属性:

  • size:当前实际存储的元素个数(货架上已放的物品数)
  • capacity:内存块的总容量(货架的总格子数),capacity ≥ size

2. 扩容的触发条件

当你执行push_back()/insert()时,如果新元素会导致size > capacity(货架满了,放不下新物品),就必须扩容—— 而连续内存的特性决定了 “扩容不能在原地址直接扩展”(因为原内存块后面的空间可能已经被其他程序占用)。

扩容时,vector会做 3 件事:

  1. 申请新内存:找一块更大的连续内存块
  2. 拷贝 / 移动元素:把原内存块里的所有元素都拷贝(或移动)到新内存块;
  3. 释放原内存:销毁原内存块里的元素,释放原内存空间。

这就是为什么vector插入可能 “重新分配内存 + 拷贝所有原有元素”—— 本质是连续内存无法原地扩容,只能整体搬迁。

std::vector<int> vec; // 可塞入一些值观察输出不同
std::cout << "size=" << vec.size() << ", capacity=" << vec.capacity() << std::endl;

3. 避免频繁扩容:给vector初始化/指定合理的尺寸

方式1,初始化时指定合适的尺寸(慎重)

vector<int> vec(n);      // 初始化size=n的vector, 元素为默认值(如0), 此时 capacity>=n
vector<int> vec(n, val); // 初始化size=n, 元素都为val的vector, 此时 capacity>=n
  • n为元素个数

  • vec的size已被设置为n,但并未锁死,因此允许通过push_back继续添加新元素,且size和capacity会相应变化

  • 需要注意,和通常的习惯不同,vec以此种方式初始化后,赋值应通过中括号[]基于元素位置索引进行“覆盖式赋值” (除非有多于n个的元素,则通过push_back来添加)

方式2,reserve函数重新分配内存块(推荐)

std::vector<int> vec;
vec.reserve(n);  // vec的size不变,但capacity被设置为n,因此只要vec后续添加元素的数量<=n,都不会引发扩容
  • n为元素个数
  • 只改capacity,不改size,目的就是避免频繁扩容,因此只要vec内添加元素的数量<=n,都不会引发扩容
  • 预留的内存不能直接访问(比如用[]
  • 不能缩容:当reserve函数的n值小于vec当前的capacity时, 该reserve代码是无效的(不会缩容,也不会修改任何数据)

方式3,resize函数指定元素数量(慎重)

std::vector<int> vec;
vec.resize(n);       // 将vec的size强制为n, 若vec的原size大于n则多出的元素被销毁; 若vec的原size小于n则新增的size空间内被填充默认值
vec.resize(n, val);  // 同上,但若vec的原size小于n则新增的size空间内被填充指定的val值
  • n为元素个数
  • 将vec的size强制设为n,且也会改变capacity, 但size并未锁死,因此允许后续通过push_back继续添加新元素,且size和capacity会相应变化
  • 若n<vec原size, 则n之后的多余元素会被销毁
  • 若n>vec原size, 则新增 (n - 原size) 个元素,且以默认值或val值初始化
  • 赋值应通过中括号[]基于元素位置索引进行“覆盖式赋值”(除非有多于n个的元素,则通过push_back来添加)

4. 说明

问:

我之前有个疑惑,无论是初始化时/resize时指定尺寸还是reserve重新分配内存,能输入的只有元素个数n,但对于诸如 vector<cv::Mat> 或 vector<vector<cv::Point>> 甚至 vector<map<int,cv::Mat>> 这种嵌套的复杂的容器,将要塞入vector的元素究竟实际有多大还是不确定的,那么重新分配内存的字节数是如何确定的呢?

答:

  • cv::Mat大小是不确定的,但 sizeof(cv::Mat) 是确定的,即矩阵信息头的大小,其中包含了数据指针、矩阵尺寸、矩阵类型等信息,而矩阵内的实际数据并不在其中;
  • vector<cv::Point>内不管存了多少cv::Point,其vector的本质都是“容器头”,即sizeof (vector<cv::Point>)是确定的,只包含了指向数据区的指针、size、capacity等信息;
  • map<int,cv::Mat>也是类似的, sizeof(map<int,cv::Mat>) 的大小是完全固定的,只包含了指针以及描述map的红黑树结构尺寸等信息,实际数据并不在其中

关于 map

1. 情景一

对于一个比较复杂的map,诸如

map<int, map<int, cv::Mat>> dataMapMap;

如果对该数据的某个value,即map<int, cv::Mat> data,进行修改,比如再insert一个新的键值对,则:

  • 该操作是完全可行的
  • 不会引发类似于vector的扩容问题,因为map是基于红黑树的,数据本身就不在连续内存上,因此不存在扩容问题

此外,当我想取出dataMapMap的某个key对应的某个value时,注意 

map<int, cv::Mat> newDataMap = dataMapMap.at(key);

此时这种基于等于号的赋值在cv::Mat层面是浅拷贝,即数据还在同一块内存上。

这种cv::Mat的浅拷贝在并行加速时,如果是只读则没有问题,但若涉及矩阵值修改,则会引发错误。

2. 情景二

问:

对于数据 map<int, map<int, cv::Mat>> dataMapMap;

若用等于号将其赋值给另一个容器:

map<int, map<int, cv::Mat>> newDataMapMap = dataMapMap;

则dataMapMap内的value,即map<int, cv::Mat>是深拷贝还是浅拷贝

答:

  • 对于map<int, cv::Mat>这个子 map 容器本身:是深拷贝(两个子 map 是独立的容器,修改子 map 的结构如插入 / 删除键值对,不会互相影响);
  • 对于子 map 里的cv::Mat元素:是浅拷贝(两个子 map 中相同 key 对应的 cv::Mat 共享像素数据区,修改 Mat 的像素值会互相影响)

进一步地说:

newDataMapMap = dataMapMap的赋值行为分为两层,需逐层理解:

1. 外层 map 的赋值:逐元素深拷贝子 map 容器

std::map的赋值运算符(=)本质是容器级的深拷贝—— 它会为newDataMapMap创建一个全新的红黑树,然后:

  • 拷贝dataMapMap的所有外层 key(比如 1、2、3);
  • 对每个外层 key 对应的map<int, cv::Mat>对象,执行子 map 的赋值(即拷贝整个子 map 的红黑树结构尺寸等信息);
  • 最终newDataMapMapdataMapMap是两个完全独立的外层 map 容器,修改其中一个的外层 key(如插入 / 删除外层 key),不会影响另一个。
2. 子 map 的赋值:逐元素浅拷贝 cv::Mat

子 map:map<int, cv::Mat>的赋值同样是std::map的容器级深拷贝(拷贝子 map 的红黑树结构、所有内层 key),但对每个内层 key 对应的cv::Mat,执行的是 Mat 的浅拷贝(仅拷贝矩阵头,共享像素数据区)。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐