ChatGPT解决这个技术问题 Extra ChatGPT

C++中向量的初始容量

使用默认构造函数创建的 std::vectorcapacity() 是什么?我知道 size() 为零。我们可以声明默认构造的向量不调用堆内存分配吗?

这样就可以使用单个分配创建具有任意保留的数组,例如 std::vector<int> iv; iv.reserve(2345);。假设由于某种原因,我不想在 2345 上启动 size()

例如,在 Linux 上(g++ 4.4.5,内核 2.6.32 amd64)

#include <iostream>
#include <vector>

int main()
{
  using namespace std;
  cout << vector<int>().capacity() << "," << vector<int>(10).capacity() << endl;
  return 0;
}

打印 0,10。这是一个规则,还是依赖于 STL 供应商?

标准没有指定向量的初始容量,但大多数实现使用 0 。
无法保证,但我会严重质疑任何分配内存的实现的质量,而无需我请求任何。
@MikeSeymour 不同意。一个真正高性能的实现可能包含一个小的内联缓冲区,在这种情况下,将初始容量()设置为那个是有意义的。
@alastair 使用 swap 时,所有迭代器和引用都保持有效(end() 除外)。这意味着内联缓冲区是不可能的。

M
Mark Ransom

该标准没有指定容器的初始 capacity 应该是什么,因此您依赖于实现。一个常见的实现将从零开始容量,但不能保证。另一方面,没有办法改进您的 std::vector<int> iv; iv.reserve(2345); 策略,所以坚持下去。


我不买你最后的声明。如果您最初不能依赖容量为 0,则可以重组程序以允许向量具有初始大小。这将使堆内存请求的数量减半(从 2 到 1)。
@bitmask:实用:您知道向量在默认构造函数中分配内存的任何实现吗?标准不能保证这一点,但正如 Mike Seymour 指出的那样,在不需要的情况下触发分配对于实施质量来说是一种难闻的气味。
@DavidRodríguez-dribeas:这不是重点。前提是“你不能比你当前的策略做得更好,所以不要担心是否有愚蠢的实现”。如果前提是“没有这样的实现,所以不要打扰”我会买它。结论恰好是正确的,但暗示不起作用。对不起,也许我在挑剔。
@bitmask如果存在在默认构造上分配内存的实现,那么按照您所说的操作将分配数量减半。但 vector::reserve 与指定初始大小不同。采用初始大小值/副本的向量构造函数初始化 n 对象,因此具有线性复杂度。 OTOH,调用reserve 仅意味着复制/移动size() 元素如果 触发了重新分配。在空向量上没有可复制的内容。因此,即使实现为默认构造的向量分配内存,后者也可能是可取的。
@bitmask,如果您担心这种程度的分配,那么您应该查看特定标准库的实现,而不是依赖推测。
Z
Zitrax

std::vector 的存储实现差异很大,但我遇到的所有实现都是从 0 开始的。

以下代码:

#include <iostream>
#include <vector>

int main()
{
  using namespace std;
  
  vector<int> normal;
  cout << normal.capacity() << endl;
  
  for (unsigned int loop = 0; loop != 10; ++loop)
  {
      normal.push_back(1);
      cout << normal.capacity() << endl;
  }
  
  cin.get();
  return 0;
}

给出以下输出:

0
1
2
4
4
8
8
8
8
16
16

在 GCC 5.1、11.2 - Clang 12.0.1 和:

0
1
2
3
4
6
6
9
9
9
13

根据 MSVC 2013。


这被低估了@Andrew
好吧,您几乎在任何地方都发现出于速度目的的建议几乎总是只使用向量,所以如果您正在做任何涉及稀疏数据的事情......
@Andrew 他们应该从什么开始?如果程序员想要保留比默认更多的内存,那么分配任何东西只会浪费时间分配和释放内存。如果您假设它们应该以 1 开头,那么只要有人分配 1,它就会立即分配它。
@Puddle您是在字里行间阅读,而不是从表面上看。这不是讽刺的线索是“聪明”这个词,以及我提到稀疏数据的第二条评论。
@Andrew 哦,太好了,他们从 0 开始就放心了。为什么还要开玩笑地评论它?
D
Don Pedro

据我了解的标准(尽管我实际上无法命名参考),容器实例化和内存分配已被有意解耦,这是有充分理由的。因此,您有不同的、单独的要求

构造函数来创建容器本身

Reserve() 预先分配一个适当大的内存块以容纳至少(!)给定数量的对象

这很有意义。 reserve() 存在的唯一权利是让您有机会在增长向量时围绕可能昂贵的重新分配进行编码。为了有用,您必须知道要存储的对象数量,或者至少需要能够做出有根据的猜测。如果不这样做,您最好远离 reserve(),因为您只会更改浪费内存的重新分配。

所以把它们放在一起:

该标准有意不指定允许您为特定数量的对象预分配内存块的构造函数(这至少比分配实现特定的、固定的“东西”更可取)。

分配不应该是隐含的。因此,要预先分配一个块,您需要单独调用 reserve() 并且这不需要在同一个构造位置(当然可以/应该在以后,在您知道容纳所需的大小之后)

因此,如果一个向量总是预先分配一个实现定义大小的内存块,这将挫败reserve()的预期工作,不是吗?

如果 STL 自然无法知道向量的预期用途和预期大小,那么预分配块的优势是什么?这将是相当荒谬的,如果不是适得其反的话。

相反,正确的解决方案是使用第一个 push_back() 分配和实现特定块 - 如果之前没有被 reserve() 明确分配。

在必要的重新分配的情况下,块大小的增加也是特定于实现的。我所知道的向量实现从大小呈指数增长开始,但会将增量速率限制在某个最大值,以避免浪费大量内存甚至破坏它。

只有不受分配构造函数的干扰,所有这些才能充分发挥作用和优势。对于常见场景,您有合理的默认值,可以由 reserve()(和 shrink_to_fit())按需覆盖。因此,即使标准没有明确说明,我很确定假设新构造的向量不预分配对于所有当前实现来说是一个非常安全的选择。


D
David Woo

作为其他答案的一个小补充,我发现当使用 Visual Studio 在调试条件下运行时,即使容量从零开始,默认构造的向量仍将在堆上分配。

特别是如果 _ITERATOR_DEBUG_LEVEL != 0 那么向量将分配一些空间来帮助迭代器检查。

https://docs.microsoft.com/en-gb/cpp/standard-library/iterator-debug-level

我只是觉得这有点烦人,因为我当时使用的是自定义分配器并且没有期待额外的分配。


有趣的是,他们打破了 noexcept 保证(至少对于 C+17,更早?):en.cppreference.com/w/cpp/container/vector/vector
W
WhiZTiM

这是一个老问题,这里的所有答案都正确地解释了标准的观点以及您可以通过使用 std::vector::reserve 以可移植方式获得初始容量的方式;

但是,我将解释为什么任何 STL 实现在构造 std::vector<T> 对象时分配内存没有意义;

不完整类型的 std::vector;在 C++17 之前,如果在实例化点 T 的定义仍然未知,则构造 std::vector 是未定义的行为。然而,这个限制在 C++17 中被放宽了。为了有效地为对象分配内存,您需要知道它的大小。从 C++17 及更高版本开始,您的客户可能会遇到您的 std::vector 类不知道 T 大小的情况。让内存分配特征依赖于类型完整性是否有意义?不需要的内存分配 有很多很多次,您需要在软件中为图形建模。 (树是图);您最有可能将其建模为: class Node { .... std::vector children; //或 std::vector< *some pointer type* > children; .... };现在想一想,想象一下如果你有很多终端节点。如果您的 STL 实现只是为了在子对象中有对象而分配额外的内存,您会非常生气。这只是一个例子,请随意考虑更多...


A
Archie Yalakki

标准没有指定容量的初始值,但 STL 容器会自动增长以容纳尽可能多的数据,前提是您不超过最大大小(使用 max_size 成员函数知道)。对于向量和字符串,当需要更多空间时,增长由 realloc 处理。假设您想创建一个持有值 1-1000 的向量。在不使用保留的情况下,代码通常会在以下循环期间导致 2 到 18 次重新分配:

vector<int> v;
for ( int i = 1; i <= 1000; i++) v.push_back(i);

修改代码以使用保留可能会导致在循环期间分配 0:

vector<int> v;
v.reserve(1000);

for ( int i = 1; i <= 1000; i++) v.push_back(i);

粗略地说,向量和字符串的容量每次增长 1.5 到 2 倍。