C ++中向量的初始容量


90

什么是capacity()std::vector这是使用默认constuctor产生的?我知道那size()是零。我们可以声明默认构造的向量不调用堆内存分配吗?

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

例如,在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供应商相关?


7
Standard没有指定向量的初始容量,但是大多数实现使用0。
阿努比斯先生2012年

11
没有保证,但是我会严重质疑在没有我请求的情况下分配内存的任何实现的质量。
Mike Seymour 2012年

2
@MikeSeymour不同意。真正高性能的实现可能包含一个较小的内联缓冲区,在这种情况下,将初始Capacity()设置为该值是有意义的。
alastair

6
@alastair使用swap所有迭代器时,引用保持有效(除外end())。这意味着无法使用内联缓冲区。
Notinlist,2016年

Answers:


73

该标准未指定capacity容器的初始名称,因此您依赖于实现。常见的实现将使容量从零开始,但是并不能保证。另一方面,没有办法更好地std::vector<int> iv; iv.reserve(2345);坚持这一策略。


1
我不买你的最后一句话。如果最初不能依赖于容量为0,则可以重组程序以允许向量具有初始大小。这将是堆内存请求数量的一半(从2到1)。
bitmask 2012年

4
@bitmask:实用:您是否知道在默认构造函数中矢量分配内存的任何实现?该标准并不能保证这一点,但是正如Mike Seymour指出的那样,触发分配而不需要分配会给实现质量带来不良影响
DavidRodríguez-dribeas 2012年

3
@DavidRodríguez-dribeas:这不是重点。前提是“你不能这样做比你目前的策略,所以也懒得想知道是否有可能是愚蠢的实现”。如果前提是“没有这样的实现,那么就不用理会”我会买。结论恰好是正确的,但其含义不起作用。抱歉,也许我正在采摘。
bitmask 2012年

3
@bitmask如果存在在默认构造上分配内存的实现,则按照您说的做将分配数量减半。但是vector::reserve与指定初始大小不同。采用初始大小值/副本的向量构造函数初始化n对象,因此具有线性复杂度。OTOH,主叫储备只意味着复制/移动的size()元件,如果重新分配被触发。在空向量上,没有要复制的内容。因此,即使实现为默认构造的向量分配内存,也可能需要后者。
Praetorian 2012年

4
@bitmask,如果您担心这种程度的分配,则应查看特定标准库的实现,而不要依赖推测。
Mark Ransom 2012年

36

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和:

0
1
2
3
4
6
6
9
9
9
13

根据MSVC 2013。


3
@Andrew
Valentin

好吧,您几乎在所有地方都发现,出于速度目的的建议几乎总是只使用向量,因此,如果您要进行涉及稀疏数据的任何事情……
Andrew

@Andrew应该从什么开始?如果程序员想要保留比默认更多的内存,分配任何东西只会浪费时间分配和释放该内存。如果您假设它们应该以1开头,那么无论如何有人分配1时,它将立即分配它。
水坑

@Puddle您正在阅读两行之间的内容,而不是看重表面价值。这不是讽刺的线索是“聪明”一词,还有我第二个提到稀疏数据的评论。
安德鲁

@Andrew哦,好,您从0开始就对此感到宽慰。为什么还要开玩笑地评论它?
水坑

7

据我了解的标准(尽管我实际上不能命名参考),容器实例化和内存分配出于有充分的理由被有意地分离了。因此,您有不同的,单独的要求

  • constructor 创建容器本身
  • reserve() 预分配一个适当的大内存块以容纳至少(!)个给定数量的对象

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

所以放在一起:

  • 标准故意不指定一个构造函数,允许你预先分配的存储块的对象的特定数目(这将是至少比分配的实现特定的,固定的“东西”罩下更理想)。
  • 分配不应是隐式的。因此,要预分配块,您需要单独调用,reserve()并且不必在同一构造位置(可以/当然应该稍后,在您知道要容纳的大小之后)
  • 因此,如果向量总是总是预分配实现定义大小的存储块,这将挫败的预期工作reserve(),不是吗?
  • 如果STL自然不知道向量的预期目的和预期大小,则预分配块有什么好处?如果不是适得其反,那将是荒谬的。
  • 正确的解决方案是使用第一个分配和实现特定的块push_back()-如果尚未显式分配by reserve()
  • 在需要重新分配的情况下,块大小的增加也是特定于实现的。我所知道的向量实现以大小的指数增加开始,但是将增加速率限制为一定的最大值,以避免浪费大量的内存,甚至避免浪费内存。

所有这些都只有在不被分配构造函数干扰的情况下才能发挥全部作用和优势。对于常见情况,您有合理的默认值,可以根据需要将其替换为reserve()(和shrink_to_fit())。因此,即使标准没有明确声明,对于所有当前的实现,我都可以肯定地假设一个新构造的向量没有预先分配是一个相当安全的选择。


4

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

具体来说,如果_ITERATOR_DEBUG_LEVEL!= 0,则vector将分配一些空间来帮助进行迭代器检查。

https://docs.microsoft.com/zh-CN/cpp/standard-library/iterator-debug-level

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


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

4

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

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

  1. std::vector<T> 类型不完整;

    在C ++ 17之前,std::vector<T>如果在T实例化点仍不确定的构造,则构造a的行为是不确定的。但是,该约束在C ++ 17中得到了放松

    为了有效地为对象分配内存,您需要知道其大小。从C ++ 17及更高版本开始,您的客户端可能会遇到您的std::vector<T>类不知道的大小的情况T。具有依赖于类型完整性的内存分配特征是否有意义?

  2. Unwanted Memory allocations

    有很多很多次您需要在软件中为图形建模。(树是图);您很可能将其建模为:

    class Node {
        ....
        std::vector<Node> children; //or std::vector< *some pointer type* > children;
        ....
     };
    

    现在考虑一下,想象一下您是否有很多终端节点。如果您的STL实现仅在预期中包含对象时就分配了额外的内存,您将非常生气children

    这只是一个例子,您可以随意考虑更多...


2

Standard没有指定容量的初始值,但是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之间。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.