C++八股 —— vector底层

1. 底层原理

底层为动态数组

1.1. 类构成

  • class vector : protected _Vector_base
  • _Vector_base:
    • _M_start:容器元素开始的位置
    • _M_finish:容器元素结束的位置
    • _M_end_of_storage:动态内存最后一个元素的下一个位置

image-20250622160315213

1.2. 构造函数

  • 无参构造

    根据性能优先规则,没有申请动态内存。

    只有三个指针,64为系统下,vector的默认大小为: 8 × 3 = 24 8\times3=24 8×3=24字节。

  • 初始化元素个数的构造

    申请了动态内存,避免多次申请影响性能。其元素存储时在上的

1.3. 扩容机制

size == capacity时,下一次插入会引发扩容。

扩容过程为:

  • 先分配新的空间

    • VS是1.5倍扩容,gcc是2倍扩容

      为什么会有1.5倍扩容,2倍有什么问题?

      • 空间浪费较高

      • 扩容时无法复用已经释放的内存

        image-20250622212608931

      2倍扩容的优点:

      • 扩容次数相较更少

        image-20250622212944965

      • 元素拷贝开销更低

        image-20250622213019311

    • 扩容用的是malloc,分配器对内存的分配和对象的构建进行了分离,对象的构建采用placement new机制(placement new不分配新内存,而是通过调用构造函数在用户指定的已分配内存上初始化对象。这一机制将内存分配与对象构造过程分离。‌‌)

  • 将原有空间的数据移动到新的空间中

    涉及拷贝构造、赋值操作符重载;如果对象支持移动语义,则涉及移动拷贝构造,移动赋值操作符重载。

    • 怎么避免扩容带来的移动代价

      在能预测元素个数的情况下预分配空间

    • vector存放对象好还是指针好,各自有什么优缺点

      • 存储对象:缺点是复制和赋值开销大,优点是对象的生命周期统一由vector管理;
      • 存储指针:优点是复制和赋值开销小,缺点是需要处理指针和对象的什么周期。
  • 最后释放旧空间

1.4. 迭代器失效

迭代器失效指的是在容器被修改后,某些迭代器可能不再指向容器中的有效元素。这可能会导致未定义行为,包括程序崩溃或数据损坏。

  • 扩容会引起迭代器失效

    insertemplacepush_backemplace_back

  • 元素移动会引起迭代器失效

    eraseemplaceinsert

2. 相关操作

插入元素

先检查空间,不够则申请内存翻倍

  • 插入到最后
  • 插入到非最后:待插入位置之后的元素往后移一位,然后插入

删除元素

删除元素不会释放已申请的内存

  • 删除最后:_M_finish往前移动一位
  • 删除非最后:待删除位置之后的元素往前移一位,然后执行“删除最后”操作

读取元素

返回的是具体元素的引用

  • 操作符[]
  • at():比[]多一步越界检查操作

修改元素

  • 不支持直接修改某个位置的元素
  • 通过读取元素,获取引用,然后修改
  • 先删除后插入

释放空间

  • swap一个空容器
  • C++ 11:shrink_to_fit 将容器的空间收缩到容器大小。先clear()将容器清空(不会释放空间),然后收缩空间大小

参考:【C++面试题】vector底层实现原理_哔哩哔哩_bilibili


fill和assign

  1. std::fill(算法库函数)

    • 功能:将指定范围内的元素逐个赋值为给定值

    • 语法

      #include <algorithm>
      std::fill(iterator begin, iterator end, value);
      
    • 行为

      • 遍历 [begin, end) 范围内的元素,将每个元素赋值为 value
      • 不改变容器大小,仅修改已有元素的值。
    • 适用容器:所有支持迭代器的容器(如 vector, array, deque, 原生数组等)。

    • 优点

      • 高效:直接覆盖现有元素,无需重新分配内存。
      • 灵活性:可局部修改(例如只修改某一部分元素)。
    • 缺点:需要确保目标范围有效(例如不越界)。

示例

std::vector<int> vec = {1, 2, 3, 4};
std::fill(vec.begin(), vec.end(), 0); // vec → {0, 0, 0, 0}
std::fill(vec.begin() + 1, vec.end() - 1, 5); // vec → {0, 5, 5, 0}
  1. assign(容器成员函数)

    • 功能替换容器的内容,可以指定新元素的数量和值,或从其他容器/迭代器复制数据。

    • 语法

      // 用 n 个 value 替换容器内容
      container.assign(n, value);
      
      // 用迭代器范围 [begin, end) 替换容器内容
      container.assign(begin, end);
      
    • 行为

      • 销毁原有元素,重新分配内存(可能触发内存重新申请)。
      • 新内容可以是重复值或来自其他容器。
    • 适用容器:支持动态修改的序列容器(如 vector, deque, list, string)。

    • 优点

      • 简洁性:一行代码即可替换整个容器的内容。
      • 灵活性:支持从不同数据源(如另一个容器或初始化列表)赋值。
    • 缺点

      • 潜在性能开销:若新内容大小与原有内容不同,可能触发内存重分配。
      • 数据丢失:原有内容被完全覆盖。

示例

std::vector<int> vec = {1, 2, 3, 4};
vec.assign(3, 10);       // vec → {10, 10, 10}(大小变为 3)
vec.assign({5, 6, 7});   // vec → {5, 6, 7}(用初始化列表替换)
  1. 关键区别
特性std::fillassign
作用范围修改指定范围内的已有元素替换整个容器的内容
内存操作不改变容器大小,无内存分配可能触发内存重新分配
性能高效(直接覆盖)可能低效(若大小变化)
适用场景局部修改或保持容器大小不变时需要完全替换内容或调整大小时
代码简洁性需要显式指定范围一行代码替换全部内容
  1. 如何选择
  • 使用 std::fill
    • 需要保留容器原有结构(如大小不变)。
    • 仅需修改部分或全部元素的值(例如重置为默认值)。
    • 避免内存重分配(对性能敏感的场景)。
  • 使用 assign
    • 需要完全替换容器内容(例如用新数据覆盖旧数据)。
    • 需要调整容器大小(例如从 n 个元素变为 m 个元素)。
    • 从其他容器或初始化列表快速赋值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值