初始化 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_n
和 std::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_n
和 std::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::generate
std::generate_n
std::fill
std::fill_n
等)
std::back_inserter
【讨论】:
值得一提的是vector如果您事先知道大小是多少,那么您应该对其进行初始化,以便只分配一次内存。如果您对大小只有一个粗略的了解,那么您可以使用默认构造函数创建向量,然后保留一个近似正确的数量,而不是像上面那样分配存储空间;例如
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++ 向量的大小的主要内容,如果未能解决你的问题,请参考以下文章