C++ 容器常见问题:emplace_back、push_back、reserve、resize 的区别
C++ vector 常见接口:push_back、emplace_back、reserve、resize
一、前言
在阅读 C++ 代码时,经常会看到下面几个 std::vector 相关接口:
push_back
emplace_back
reserve
resize
它们看起来都和“往容器里放东西”有关,但实际含义并不相同。
本文主要整理几个问题:
push_back 和 emplace_back 有什么区别?
reserve 和 resize 有什么区别?
什么时候该用哪个?
常见错误有哪些?
本文以 std::vector 为例进行说明。
二、push_back 和 emplace_back
2.1 示例结构体
先定义一个简单的结构体:
struct Point {
double x;
double y;
double z;
Point(double x, double y, double z)
: x(x), y(y), z(z) {}
};
定义一个 vector:
std::vector<Point> points;
后面用这个 points 来说明 push_back 和 emplace_back 的区别。
2.2 push_back:把已有对象放进容器
push_back 的含义是:把一个已经构造好的对象添加到容器末尾。
例如:
Point p(1.0, 2.0, 3.0);
points.push_back(p);
这段代码可以理解为:
先在外部创建 Point 对象 p
再把 p 拷贝到 vector 中
也可以直接传入一个临时对象:
points.push_back(Point(1.0, 2.0, 3.0));
这个过程可以理解为:
先构造一个临时 Point 对象
再把这个临时对象移动到 vector 中
最后销毁临时对象
所以 push_back 更强调的是:把一个现成的对象放进容器。
2.3 emplace_back:在容器内部直接构造对象
emplace_back 的含义是:直接在容器末尾构造对象。
例如:
points.emplace_back(1.0, 2.0, 3.0);
这行代码会把参数:
1.0, 2.0, 3.0
直接传给 Point 的构造函数:
Point(double x, double y, double z)
也就是说,它会直接在 vector 内部的内存空间中构造一个 Point 对象。
可以简单理解为:
emplace_back 不需要你提前构造 Point 对象
它会用你传入的参数,在容器内部直接构造对象
2.4 两者的核心区别
push_back 和 emplace_back 的核心区别如下:
push_back:先构造对象,再放入容器
emplace_back:直接在容器内部构造对象
例如:
points.push_back(Point(1.0, 2.0, 3.0));
大致过程是:
构造临时 Point 对象
移动到 vector 中
销毁临时对象
而:
points.emplace_back(1.0, 2.0, 3.0);
大致过程是:
直接在 vector 内部构造 Point 对象
所以在某些情况下,emplace_back 可以减少一次临时对象构造或移动。
2.5 emplace_back 并不总是更好
虽然 emplace_back 有时可以减少临时对象,但并不是任何场景都应该无脑使用 emplace_back。
例如已经有一个对象:
Point p(1.0, 2.0, 3.0);
下面两种写法差别并不大:
points.push_back(p);
points.emplace_back(p);
因为此时传入的已经是一个 Point 对象,而不是构造 Point 所需的参数。
如果语义是“把已有对象放进容器”,使用 push_back 反而更直观:
points.push_back(p);
如果想把已有对象移动进去,可以写:
points.push_back(std::move(p));
或者:
points.emplace_back(std::move(p));
但从可读性上看,已有对象更推荐使用 push_back。
2.6 如何选择 push_back 和 emplace_back
可以按下面的原则选择:
已有对象:使用 push_back 更直观
直接构造对象:使用 emplace_back 更合适
需要转移资源:配合 std::move
例如:
std::vector<Point> points;
Point p(1.0, 2.0, 3.0);
// 已经有对象,放入容器
points.push_back(p);
// 直接在容器内部构造对象
points.emplace_back(4.0, 5.0, 6.0);
// 移动已有对象
points.push_back(std::move(p));
简单记忆:
push_back(obj):把对象放进去
emplace_back(args...):用参数直接在容器里构造对象
三、reserve 和 resize
3.1 先理解 size 和 capacity
在理解 reserve 和 resize 之前,需要先区分 vector 的两个概念:
size:当前实际元素个数
capacity:当前已分配空间最多能容纳的元素个数
例如:
std::vector<int> v;
刚创建时通常是:
v.size() == 0
v.capacity() == 0
当然,capacity 的具体值由标准库实现决定,但可以先这样理解。
3.2 reserve:只预留空间,不创建元素
reserve(n) 的含义是:提前分配能够容纳 n 个元素的空间,但不创建元素。
例如:
std::vector<int> v;
v.reserve(10);
执行后:
v.size() == 0
v.capacity() >= 10
注意:size 仍然是 0。
也就是说,虽然 vector 已经预留了可以容纳 10 个元素的空间,但容器里面还没有真正的元素。
因此下面这种写法是错误的:
std::vector<int> v;
v.reserve(10);
v[0] = 123; // 错误
原因是:
reserve 只是预留内存空间
并没有创建第 0 个元素
正确写法应该是继续使用 push_back 或 emplace_back 添加元素:
std::vector<int> v;
v.reserve(10);
v.push_back(1);
v.push_back(2);
v.push_back(3);
此时:
v.size() == 3
v.capacity() >= 10
reserve 的主要作用是优化性能,减少 vector 频繁扩容。
3.3 resize:改变元素数量
resize(n) 的含义是:把 vector 的元素数量改成 n 个。
例如:
std::vector<int> v;
v.resize(10);
执行后:
v.size() == 10
v.capacity() >= 10
这时 vector 里面真的有 10 个元素。
对于 int 类型来说,这些元素会被初始化为 0:
std::vector<int> v;
v.resize(3);
// v 中现在有 3 个元素:0, 0, 0
所以可以直接使用下标访问:
v[0] = 123;
v[1] = 456;
v[2] = 789;
如果 resize 的大小比原来的 size 更小,多余的元素会被删除:
std::vector<int> v = {1, 2, 3, 4, 5};
v.resize(3);
// v 现在是:1, 2, 3
所以 resize 影响的是容器中真实存在的元素数量。
3.4 reserve 和 resize 的核心区别
reserve 和 resize 的核心区别如下:
| 接口 | 是否改变 size | 是否改变 capacity | 是否创建元素 | 是否可以直接用下标访问新位置 |
|---|---|---|---|---|
reserve(n) |
否 | 可能改变 | 否 | 否 |
resize(n) |
是 | 可能改变 | 是 | 是 |
举例:
std::vector<int> a;
a.reserve(10);
结果是:
a.size() == 0
a.capacity() >= 10
不能这样访问:
a[0] = 1; // 错误
因为 a 中还没有第 0 个元素。
再看:
std::vector<int> b;
b.resize(10);
结果是:
b.size() == 10
b.capacity() >= 10
可以这样访问:
b[0] = 1; // 正确
因为 b 中已经真实存在 10 个元素。
3.5 常见错误:reserve 后直接用下标访问
一个常见错误是:
std::vector<int> v;
v.reserve(10);
for (int i = 0; i < 10; ++i) {
v[i] = i; // 错误
}
这段代码的问题在于:
v.reserve(10);
只是预留了空间,并没有创建 10 个元素。
虽然底层内存可能已经分配好了,但从 vector 的语义上看,v.size() 仍然是 0。因此 v[0]、v[1] 等元素都不存在。
正确写法之一是使用 push_back:
std::vector<int> v;
v.reserve(10);
for (int i = 0; i < 10; ++i) {
v.push_back(i);
}
或者使用 emplace_back:
std::vector<int> v;
v.reserve(10);
for (int i = 0; i < 10; ++i) {
v.emplace_back(i);
}
如果确实需要通过下标访问,可以使用 resize:
std::vector<int> v;
v.resize(10);
for (int i = 0; i < 10; ++i) {
v[i] = i;
}
这时 v 中已经真实存在 10 个元素,所以可以通过下标访问。
3.6 如何选择 reserve 和 resize
可以按下面的原则选择:
后续要 push_back / emplace_back:使用 reserve
后续要通过下标访问:使用 resize
例如,如果是不断追加元素:
std::vector<int> v;
v.reserve(1000);
for (int i = 0; i < 1000; ++i) {
v.push_back(i);
}
适合使用 reserve。
如果是先创建固定数量元素,然后通过下标赋值:
std::vector<int> v;
v.resize(1000);
for (int i = 0; i < 1000; ++i) {
v[i] = i;
}
适合使用 resize。
简单记忆:
reserve:我要预留空间,之后慢慢添加
resize:我要现在就有这么多个元素
四、结合点云代码理解
4.1 vector 存储指针的情况
在一些点云相关代码中,可能会看到类似这样的容器:
std::vector<Generic3dPoint*> points;
这里的 vector 存储的不是点对象本身,而是指向点对象的指针。
也就是说,points 中每个元素的类型是:
Generic3dPoint*
4.2 使用 reserve 的含义
如果代码中写:
points.reserve(n);
它的含义是:
提前给 vector 分配能容纳 n 个指针的空间
但 points 当前还没有 n 个元素
此时:
points.size() == 0
points.capacity() >= n
所以不能直接写:
points[0] = pointPtr; // 错误
因为第 0 个元素还不存在。
后面通常需要配合:
points.push_back(pointPtr);
或者:
points.emplace_back(pointPtr);
完整示例:
std::vector<Generic3dPoint*> points;
points.reserve(n);
for (int i = 0; i < n; ++i) {
Generic3dPoint* pointPtr = getPoint(i);
points.push_back(pointPtr);
}
这里 reserve(n) 的作用只是减少后续 push_back 时的扩容次数。
4.3 使用 resize 的含义
如果代码中写:
points.resize(n);
它的含义是:
points 中现在真的有 n 个指针元素
此时:
points.size() == n
points.capacity() >= n
所以可以通过下标访问:
points[0] = pointPtr;
更安全的写法通常是:
points.resize(n, nullptr);
意思是:
创建 n 个元素,每个元素初始值都是 nullptr
例如:
std::vector<Generic3dPoint*> points;
points.resize(n, nullptr);
for (int i = 0; i < n; ++i) {
points[i] = getPoint(i);
}
这种写法适合已经知道最终元素数量,并且后面要通过下标直接填充的场景。
五、常用搭配
5.1 reserve + emplace_back
比较常见的高效写法是:
std::vector<Point> points;
points.reserve(10000);
for (int i = 0; i < 10000; ++i) {
points.emplace_back(i, i + 1, i + 2);
}
这段代码的含义是:
先预留能容纳 10000 个 Point 的空间
然后不断在 vector 内部直接构造 Point 对象
适合场景:
提前知道大概数量
需要不断追加元素
希望减少 vector 扩容次数
对象可以直接通过构造参数创建
这种写法的优点是:
reserve 减少扩容次数
emplace_back 减少临时对象构造或移动
5.2 resize + 下标赋值
另一种常见写法是:
std::vector<int> values;
values.resize(10);
for (int i = 0; i < 10; ++i) {
values[i] = i;
}
这段代码的含义是:
先创建 10 个元素
然后通过下标逐个赋值
适合场景:
需要提前创建固定数量元素
后续通过下标直接写入
元素数量一开始就确定
对于对象类型,也可以这样使用:
std::vector<Point> points;
points.resize(100);
for (int i = 0; i < 100; ++i) {
points[i] = Point(i, i + 1, i + 2);
}
不过这里需要注意:
resize 会真的构造元素
如果对象构造成本较高,可能会有额外开销
这种情况下,如果只是不断追加对象,通常 reserve + emplace_back 更合适。
六、总结
6.1 四个接口一句话总结
| 接口 | 含义 |
|---|---|
push_back(obj) |
把已有对象加入容器 |
emplace_back(args...) |
用参数在容器内部直接构造对象 |
reserve(n) |
预留空间,不创建元素 |
resize(n) |
改变元素数量,会创建或删除元素 |
6.2 选择原则
可以按下面的规则记忆:
已有对象:push_back
直接构造对象:emplace_back
提前预留空间:reserve
需要真实元素数量:resize
reserve 后不能直接用下标写
resize 后可以直接用下标写
更具体一点:
1. 如果已经有一个对象,要放进 vector,使用 push_back。
2. 如果想直接用构造参数在 vector 里创建对象,使用 emplace_back。
3. 如果知道后面要插入很多元素,但现在还不需要创建它们,使用 reserve。
4. 如果现在就需要 vector 中存在 n 个元素,使用 resize。
5. reserve 只影响 capacity,不影响 size。
6. resize 会影响 size,也可能影响 capacity。
7. reserve 后不能访问 v[0],因为元素还不存在。
8. resize 后可以访问 v[0],因为元素已经存在。
七、完整示例对比
7.1 reserve + push_back
std::vector<int> v;
v.reserve(5);
for (int i = 0; i < 5; ++i) {
v.push_back(i);
}
执行过程:
先预留空间
再逐个添加元素
最终 size 为 5
7.2 resize + 下标赋值
std::vector<int> v;
v.resize(5);
for (int i = 0; i < 5; ++i) {
v[i] = i;
}
执行过程:
先创建 5 个元素
再逐个通过下标赋值
最终 size 为 5
7.3 错误示例
std::vector<int> v;
v.reserve(5);
for (int i = 0; i < 5; ++i) {
v[i] = i; // 错误
}
原因:
reserve 只是预留空间
没有创建元素
v.size() 仍然是 0
不能通过下标访问不存在的元素
八、最终记忆版
最后可以用下面几句话记住:
push_back:对象已经有了,把它放进去。
emplace_back:对象还没有,用参数直接在容器里造一个。
reserve:只是提前占位置,不是真的有人坐。
resize:真的安排了 n 个座位,并且每个座位上都有一个元素。
在实际代码中,最常见的推荐组合是:
std::vector<Point> points;
points.reserve(10000);
for (int i = 0; i < 10000; ++i) {
points.emplace_back(i, i + 1, i + 2);
}
这表示:
提前预留空间,减少扩容
然后直接在容器内部构造对象
这是使用 std::vector 时比较常见、也比较高效的写法。

浙公网安备 33010602011771号