从今天开始,进入STL的世界。STL是C++标准库的核心,面试必考,机器人开发天天在用。

第一篇讲vector,因为它可能是整个STL里用得最多的容器。你随便打开一个机器人项目的代码文件,大概率第一眼就能看到vector。

面试官问vector,通常从"说说vector的底层实现"开始。这个问题看似简单,但能区分出候选人对C++的理解深度。

vector的底层结构

vector的底层就是一段连续的内存空间。三个指针就能描述它:

start // 指向已用空间的起始位置 finish // 指向已用空间的末尾(最后一个元素的下一个位置) end_of_storage // 指向分配空间的末尾

size就是finish - start,capacity就是end_of_storage - start。

连续内存意味着什么?意味着你可以用下标随机访问,O(1)时间复杂度。也意味着对CPU缓存友好——连续的数据在缓存里挨着放,读取效率高。

这就是为什么机器人代码里到处都是vector。处理激光雷达点云、存储路径规划结果、缓存传感器读数……这些场景都需要连续存储和快速随机访问。

扩容机制:面试必考

vector最经典的面试题就是扩容机制。

当size等于capacity时,再push_back就会触发扩容。扩容的过程是:分配一块更大的新内存,把旧数据拷贝过去,释放旧内存。

关键是:每次扩多大?

大多数编译器实现是2倍扩容(GCC),MSVC是1.5倍

为什么不是每次只多分配一个元素?因为那样每次扩容都要拷贝所有数据,N次push_back的总代价是O(N²)。而用倍增策略,N次push_back的均摊代价是O(N),每次均摊O(1)。

这个均摊分析面试偶尔会考。记住结论就行:虽然单次扩容是O(N),但均摊到每次插入是O(1)。

还有个实用技巧:如果你提前知道要存多少元素,用reserve()预分配空间,避免扩容带来的内存拷贝。

// 处理激光雷达点云,已知有60000个点
std::vector<PointXYZ> cloud;
cloud.reserve(60000);  // 预分配,避免反复扩容
for (int i = 0; i < 60000; i++) {
    cloud.push_back(readPoint(i));
}

在机器人开发里,传感器数据的大小通常是已知的,养成reserve的习惯能避免很多不必要的性能开销。

vector的迭代器失效问题

这个面试考得非常多。

std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;  // 指向3

v.push_back(6);  // 可能触发扩容
// 此时it可能已经失效了!

扩容后,旧内存被释放,迭代器指向的内存已经无效了。继续使用就是未定义行为,轻则数据错误,重则段错误。

不只是push_back,insert和erase也会导致迭代器失效。erase会让被删元素之后的所有迭代器都失效。

面试里有个经典题目:删除vector里所有等于某个值的元素。

// 错误写法!迭代器失效
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == target) {
        v.erase(it);  // it失效了,++it是未定义行为
    }
}

// 正确写法
v.erase(std::remove(v.begin(), v.end(), target), v.end());

这就是著名的erase-remove惯用法,面试几乎必考。erase把要删除的元素移到末尾,返回新的逻辑末尾迭代器,外层的erase再真正删除尾部多余的元素。

C++20还有个更简洁的写法:std::erase(v, target),一行搞定。

vector vs 数组 vs deque

面试常问:什么时候用vector,什么时候用数组,什么时候用deque?

C风格数组:固定大小,栈上分配,快但不灵活。嵌入式机器人开发里,内存受限的场景可能还会用。

vector:动态大小,堆上分配,连续内存。绝大多数场景的首选。

deque:双端队列,分段连续内存。支持两端高效插入,但不像vector那样保证完全连续的内存。

如果你需要频繁在头部插入元素,deque比vector好。但说实话,在机器人开发里,大部分场景vector就够了。需要头插的场景,通常是队列,那就直接用queue(底层默认deque)。

补充一个实际场景。我之前做路径规划的时候,A*算法的open list一开始用了vector,每次取最小代价的节点要排序。后来换成priority_queue(底层是vector实现的堆),性能提升很明显。但如果你的场景是两端都要操作,比如滑动窗口处理传感器数据,deque就是更自然的选择。

还有个细节:vector的内存是连续的,所以&v[0]可以得到指向首元素的指针,这在和C接口交互时特别方便。比如ROS的消息和PCL的点云数据,经常需要把vector的数据直接传给底层C接口:

std::vector<float> scan_data = getLidarScan();
// 直接传给C接口,因为内存连续
sendToDriver(scan_data.data(), scan_data.size());

deque做不到这一点,因为它的内存不是完全连续的。

vector的内存优化小技巧

在机器人开发里,内存和性能都很敏感。几个vector的实用技巧:

shrink_to_fit():释放多余容量。当你删除了大量元素后,capacity可能远大于size,用这个函数让capacity收缩到size。

std::vector<double> data(10000);
// 用了其中100个
data.resize(100);
data.shrink_to_fit();  // 释放多余内存

emplace_back vs push_back:emplace_back直接在容器内部构造对象,省一次拷贝或移动。对于复杂对象,性能差异明显。

// push_back:先构造临时对象,再移动进容器
v.push_back(PointXYZ(1.0, 2.0, 3.0));

// emplace_back:直接在容器内存里构造,零拷贝
v.emplace_back(1.0, 2.0, 3.0);

养成用emplace_back的习惯,性能只有更好没有更差。

顺便提一个vector的"坑":vector<bool>不是真正的容器。它是STL的特化实现,内部用位来存储,省内存但行为和其他vector不一致,连引用返回的都不是真正的bool引用。面试时如果你能主动提到这个特例,面试官会对你印象更深。

再聊一个面试中容易拉开差距的话题:vector的内存连续性对CPU缓存的影响。现代CPU读取内存时不是按字节读的,而是一次读一整条缓存行(通常64字节)。如果数据在内存中连续存放,CPU预取器能提前把后面的数据加载到缓存里,命中率极高。vector正好是连续内存,所以遍历vector的速度远远快于遍历list或者map,后者的节点分散在堆上,缓存命中率很低。在机器人开发里,点云数据、传感器采样队列这些需要高频遍历的场景,用vector存储比用list快几倍甚至十几倍。面试时如果你能从CPU缓存的角度解释为什么vector是默认首选容器,而不只是说"因为连续内存",面试官会觉得你对性能优化有深层次的理解。

给正在准备面试的你一点建议

vector是面试必考的容器,没有之一。

必须掌握的:底层连续内存结构、扩容机制(2倍增长、均摊O(1))、迭代器失效问题、erase-remove惯用法。

加分项:知道reserve和shrink_to_fit的区别、emplace_back的优势、vector和deque的选型。

如果你能聊到"为什么STL选择2倍扩容而不是1.5倍",面试官会觉得你研究得比较深。答案是2倍扩容可以复用之前释放的内存块(在特定条件下),1.5倍则不能。不过这个比较偏,知道就好。

下篇聊list和deque——不同场景下的容器选型。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列 

上一篇:第29篇 命名空间——大型项目中避免冲突的规范

下一篇预告:第31篇 STL之list与deque——不同场景下的容器选型

有任何问题欢迎评论区留言,我会尽量回复。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐