初始化c++ vector的大小

Initializing the size of a C++ vector

本文关键字:vector c++ 初始化      更新时间:2023-10-16

初始化c++ vector和其他容器的大小有什么优点(如果有的话)?有什么理由不使用默认的无参数构造函数吗?

基本上,在

之间是否存在显著的性能差异?
vector<Entry> phone_book;

vector<Entry> phone_book(1000);

这些例子来自Bjarne Stroustrup的《c++编程语言第三版》。如果这些容器应该总是用一个大小初始化,那么是否有一个好的方法来确定一个合适的开始大小?

有几种方法可以用n元素创建vector,我甚至会展示一些在你事先不知道元素数量的情况下填充矢量的方法。

但首先

不要做什么

std::vector<Entry> phone_book;
for (std::size_t i = 0; i < n; ++i)
{
    phone_book[i] = entry; // <-- !! Undefined Behaviour !!
}

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

现在分析了一些选项

default + push_back (次优)

std::vector<Entry> phone_book;
for (std::size_t i = 0; i < n; ++i)
{
    phone_book.push_back(entry);
}

这样做的缺点是,当您推回元素时将发生重新分配。这意味着内存分配,元素移动(或者复制,如果它们不可移动,或者在c++11之前)和内存释放(使用对象销毁)。对于一个相当大的n来说,这种情况很可能不止一次发生。值得注意的是,它是保证的"平摊常数"。对于push_back,这意味着它不会在每个push_back之后进行重新分配。每次重新分配都会以几何方式增加大小。进一步阅读:std::vector和std::string重新分配策略

在你事先不知道尺寸,甚至没有估计尺寸的时候使用

计数默认插入的T"(不推荐)

std::vector<Entry> phone_book(n);
for (auto& elem : phone_book)
{
    elem = entry;
}

这不会引起任何重新分配,但是所有的n元素最初都是默认构造的,然后在每次push时复制。这是一个很大的缺点,对性能的影响很可能是可衡量的。(对于基本类型,这一点不太明显)。

不要使用这个,因为几乎在每种情况下都有更好的替代方案。

count elements"男星(推荐)

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

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

default 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);

如果你需要在vector创建后填充或添加元素,可以使用

默认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都不同,并且从生成器

获得,则可以使用此方法。

初始化列表(Bonus)

由于这已经成为一个很大的答案,超出了所问的问题,如果我没有提到初始化列表构造函数,我将被原谅:

std::vector<Entry> phone_book{entry0, entry1, entry2, entry3};

在大多数情况下,当你有一个小的初始值列表来填充向量时,这应该是默认的构造函数。


一些资源:

std::vector::vector (constructor)

std::vector::insert

标准算法库(含std::generate std::generate_n std::fill std::fill_n等)

std::back_inserter

如果您提前知道大小是多少,那么您应该初始化它,以便只分配一次内存。如果你对vector的大小只有一个粗略的概念,那么你可以使用默认构造函数创建vector,然后保留一个大致正确的大小,而不是像上面那样分配存储空间;例如

vector<Entry> phone_book();
phone_book.reserve(1000);
// add entries dynamically at another point
phone_book.push_back(an_entry);
编辑:

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

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

正确的答案取决于您的应用程序及其特定的用例。您可以测试性能并根据需要调整大小。通常这是一个好主意,只是让事情工作,然后回过头来测试这些变化的影响。很多时候,您会发现默认设置工作得很好。

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

vector<Entry> phone_book(1000);

不如直接写

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

没有通用的"好方法"来确定开始时的合适大小。这取决于你对任务的了解程度。但是在任何情况下,如果您确定将向vector中添加新元素,则可以使用一些初始分配。