初始化 C++ 向量的大小

Posted

技术标签:

【中文标题】初始化 C++ 向量的大小【英文标题】:Initializing the size of a C++ vector 【发布时间】:2014-08-03 20:36:29 【问题描述】:

初始化 C++ 向量以及其他容器的大小有哪些优点(如果有)?有什么理由不只使用默认的无参数构造函数吗?

基本上,两者之间是否存在显着的性能差异

vector<Entry> phone_book;

vector<Entry> phone_book(1000);

这些示例来自 Bjarne Stroustrup 的 The C++ Programming Language Third Edition。如果这些容器应该总是用一个大小来初始化,有没有一种好的方法来确定一个好的开始大小是多少?

【问题讨论】:

它们不需要用大小初始化,但如果你知道(或多或少)你会在性能上有很大的提升。它从大小 N 开始,添加 N + 1 个元素,它必须分配 2N 缓冲区并复制旧数据。这又是 2N + 1 等等...... 是否需要构造1000个Entry对象?如果是,那么第二个版本就是这样做的。如果没有,那么你就不需要它。 @juanchopanza 绝对不是。这是书中的一个例子,这就是为什么它让我感到困惑,为什么他需要将它初始化为 1000。 是的,肯定有原因,如果你知道向量的初始状态将是默认初始化,最好这样,例如你需要初始化一个包含 1000 个计数器的向量(初始化为 0),你可以使用这个构造函数。 【参考方案1】:

有几种方法可以使用n 元素创建vector,当您事先不知道元素数量时,我什至会展示一些填充向量的方法。

但首先

不该做的事

std::vector<Entry> phone_book;
for (std::size_t i = 0; i < n; ++i)

    phone_book[i] = entry; // <-- !! Undefined Behaviour !!

如上例所示,默认构造向量会创建一个空向量。访问向量范围之外的元素是未定义的行为。不要期望得到一个很好的例外。未定义的行为意味着任何事情都可能发生:程序可能会崩溃,或者似乎可以工作,或者可能以一种不稳定的方式工作。请注意,使用reserve 不会改变向量的实际大小,即您无法访问超出向量大小的元素,即使您为它们保留。

现在分析了一些选项

默认 ctor + push_back (次优)

std::vector<Entry> phone_book;
for (std::size_t i = 0; i < n; ++i)

    phone_book.push_back(entry);

这样做的缺点是当你推回元素时会发生重新分配。这意味着内存分配、元素移动(或复制,如果它们是不可移动的,或者对于 pre c++11)和内存释放(带有对象销毁)。对于一个相当大的n,这很可能会发生不止一次。值得注意的是,push_back 保证了“摊销常数”,这意味着它不会在每个push_back 之后进行重新分配。每次重新分配都会以几何方式增加大小。进一步阅读:std::vector and std::string reallocation strategy

当您事先不知道尺寸,甚至没有估计尺寸时,请使用此选项。

"count default-inserted instances of T" ctor with later assignment(不推荐)

std::vector<Entry> phone_book(n);
for (auto& elem : phone_book)

    elem = entry;

这不会导致任何重新分配,但所有n 元素将最初默认构造,然后为每次推送复制。这是一个很大的缺点,并且对性能的影响很可能是可以衡量的。 (这对于基本类型不太明显)。

不要使用它,因为几乎每种情况都有更好的选择。

“计算元素的副本数”ctor (推荐)

std::vector<Entry> phone_book(n, entry);

这是最好的使用方法。当您在构造函数中提供所需的所有信息时,它将进行最有效的分配+分配。如果Entry 有一个简单的复制构造函数,这有可能导致无分支代码,以及用于赋值的矢量化指令。

默认 ctor + reserve + push_back (根据情况推荐)

vector<Entry> phone_book;
phone_book.reserve(m);

while (some_condition)

     phone_book.push_back(entry);


// optional
phone_book.shrink_to_fit();

不会发生重新分配,对象只会被构造一次,直到您超过预留容量。 push_back 的更好选择可以是 emplace_back

如果你有一个粗略的大小,请使用这个。

储备价值没有神奇的公式。针对您的特定场景使用不同的值进行测试,以获得应用程序的最佳性能。最后你可以使用shrink_to_fit

默认 ctor + std::fill_nstd::back_inserter (根据情况推荐)

#include <algorithm>
#include <iterator>

std::vector<Entry> phone_book;

// at a later time
// phone_book could be non-empty at this time
std::fill_n(std::back_inserter(phone_book), n, entry);

如果您需要在创建矢量后向矢量填充或添加元素,请使用此选项。

默认 ctor + std::generate_nstd::back_inserter (用于不同的 entry 对象)

Entry entry_generator();

std::vector<Entry> phone_book;
std::generate_n(std::back_inserter(phone_book), n, []  return entry_generator(); );

如果每个entry 都不同并且从生成器获得,则可以使用它

初始化器列表(奖励)

既然这已经成为一个很大的答案,超出了问题的范围,如果我没有提到初始化列表构造函数,我将被淘汰:

std::vector<Entry> phone_bookentry0, entry1, entry2, entry3;

在大多数情况下,当您有一小部分初始值用于填充向量时,这应该是默认的首选构造函数。


一些资源:

std::vector::vector (constructor)

std::vector::insert

standard algorithm library(与std::generatestd::generate_nstd::fillstd::fill_n等)

std::back_inserter

【讨论】:

值得一提的是vector phone_book(n);在 c++11 和 c++11 之后具有不同的行为。在 c++11 之前,将构造一个 Entry 实例,然后复制 n 次。在c++11之后,会创建n个Entry实例。这是使用指针类时的重要信息,它还初始化对象(可能使用新对象),vector phone_book(n);直到 c++11 实际复制地址 n 次,本质上指向同一个对象。【参考方案2】:

如果您事先知道大小是多少,那么您应该对其进行初始化,以便只分配一次内存。如果您对大小只有一个粗略的了解,那么您可以使用默认构造函数创建向量,然后保留一个近似正确的数量,而不是像上面那样分配存储空间;例如

vector<Entry> phone_book();
phone_book.reserve(1000);

// add entries dynamically at another point

phone_book.push_back(an_entry);

编辑:

@juanchopanza 提出了一个很好的观点——如果你想避免默认构造对象,那么如果你有一个移动构造函数,请保留并使用push_back,或者直接在原地构造使用emplace_back

【讨论】:

如果您提前知道大小并且您需要 1000 个默认构造的 Entry 对象。否则,您必须考虑是否真的要使用 size 构造函数。 @juanchopanza 请注意,在 C++11 之前,这 1000 个条目实际上是一个构造条目的副本。 c++11之后,实际会构造1000个条目。【参考方案3】:

当您对需要存储在向量中的元素数量有一个很好的了解时,您可以初始化大小。如果您正在从数据库或其他来源检索数据,例如您知道其中有 1000 个元素,那么继续为向量分配一个内部数组来保存这么多数据是有意义的。如果您事先不知道所需的大小,那么可以让向量随着时间的推移按需增长。

正确答案取决于您的应用程序及其特定用例。您可以根据需要测试性能并调整大小。通常最好让事情正常工作,然后再回去测试这些变化的影响。很多时候你会发现默认值工作得很好。

【讨论】:

【参考方案4】:

这是 Bjarne Stroustrup 的一个坏例子。 而不是第二个定义a

vector<Entry> phone_book(1000);

这样写会更好

vector<Entry> phone_book;
phone_book.reserve( 1000 );

没有通用的“好方法”来确定开始时的合适尺寸。这取决于您拥有的有关该任务的信息。但无论如何,如果您确定将新元素添加到向量中,您可以使用一些初始分配。

【讨论】:

为什么第一种方式不好? @svenoaks 看到我的回答 据我所知,第一种方法在初始化向量时构造了 1000 个入口对象,而第二种方法只是为它保留空间。 @svenoaks 第一种方法很糟糕,因为您构造了 1000 个 Entry 类型的对象,尽管尚不清楚 1)您是否将使用所有 1000 个对象和 2)您是否确实需要调用默认构造函数给定的条目,或者您需要使用参数调用构造函数。我确信当您拥有有关添加条目(例如姓名、电话号码等)的信息时,最好使用参数调用构造函数。

以上是关于初始化 C++ 向量的大小的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中的 main() 之前初始化向量

C++ 向量初始容量

运行时在构造函数中初始化向量类成员——C++

C++:为向量中的非连续索引赋值?

向量 C++ 上的 Memset

小白学习C++ 教程五C++数据结构向量和数组