如何在 STL 向量中实现 push_back?

Posted

技术标签:

【中文标题】如何在 STL 向量中实现 push_back?【英文标题】:how is push_back implemented in STL vector? 【发布时间】:2010-04-12 20:04:08 【问题描述】:

我在一次采访中被问到这个问题。

我回答的点是这样的

1) 指向当前位置的索引;

2) 必要时调整大小。

谁能详细说明一下?

【问题讨论】:

问题是关于具体的实现?所有实现都允许不同。 答案是“这是定义的实现”。严肃地说,愚蠢的面试问题应该受到线索蝙蝠的严厉殴打...... @DevSolar,我认为不会写向量的程序员应该受到惩罚。我们都有一个观点(和一个球棒)。 @DevSolar,为什么公司要因为展示他们对 C++ 和 STL 的了解程度而受到惩罚?似乎有这些信息很好。 看起来那个雇主侥幸逃脱了。 【参考方案1】:

一个 STL vector 有一个size(当前存储元素的数量)和一个capacity(当前分配的存储空间)。

如果size < capacitypush_back 只需将新元素放在末尾并将size 加1。 如果size == capacitypush_back 之前,分配了一个新的更大的数组(通常是两倍大小,但这是依赖于实现的afaik),所有当前数据都被复制过来(包括新元素) ,并释放旧分配的空间。如果分配失败,这可能会引发异常。

操作的复杂性摊销 O(1),这意味着在导致调整大小的push_back 期间,它不会是一个恒定时间操作(但通常在许多操作,它是)。

【讨论】:

我还想评论一下,如果新容量比旧容量大k 倍,那么摊销成本 O(1) 就是这种情况。同样值得注意的是,对于给定的k,每个元素平均被复制1/(k-1) 次(对于k=2,它只是一个额外的副本)。 另外,2 不是最常见的因素。大多数实现已经转向黄金比例,因为它与内存的关系更好......这已经在 SO 上讨论过,并且引用了一个 comp.lang.c++.moderated 线程。 啊,不知道。谢谢(你的)信息;你能提供你提到的讨论的链接吗?【参考方案2】:
template< typename T >
void std::vector<T>::push_back(const T& obj)

    this->insert(this->end(),obj);

【讨论】:

你的意思是void std::vector&lt;T&gt;::push_back 完美回答一个糟糕的问题。技术上是正确的,但没有回答他们的意思要问什么。 @Graphics:也许我很密集,但我不确定还有什么预期。我想问“std::vector 中的 push_back 是如何实现的?”我会在采访中给出这个答案。 @sbi 我假设他们正在寻找涉及内存管理的答案(如 tzaman 的答案)。 @Graphics:我接受过这样的采访。他们问了一个问题,我指出了其中的错误。没醒。【参考方案3】:

当然,这本质上是实现定义的。假设这是一个如何实现动态数组的问题,一般来说,它会是这样的:

    push_back 检查capacity() 并确保它至少比size() 大一。 如果没有新元素的容量,向量会重新分配它的整个底层存储,将旧存储的内容复制到新存储。旧存储已被释放。 新元素被复制到动态数组的末尾。

一些 STL 实现会通过使用交换(即用于容器的容器)来删除一些副本,但在大多数情况下,这正是它的工作原理。

【讨论】:

【参考方案4】:

他们可能正在寻找push_back 制作被推送到vector 的对象的副本(使用其复制构造函数)。

关于调整大小:标准说a.push_back(x) 等同于a.insert(a.end(),x)insert 的定义部分表示:“如果新容量大于旧容量,则会导致重新分配。”

标准说明了函数应该做什么。但在大多数情况下,它们的实现方式是特定于实现的。

【讨论】:

【参考方案5】:

像这样:


void push_back(T const& param)

  vector temp(rbegin(), rend());
  temp.push_front(param);
  *this = vector(temp.rbegin(), temp.rend());

【讨论】:

这似乎是一个相当昂贵的实现。您首先反向复制向量的内容,这需要分配 + O(n) 迭代来复制向量中的每个元素。然后将参数推送到临时向量的 开始,这需要另一个分配和 O(n) 伴随的元素复制操作。然后,您创建另一个临时副本(分配 + O(n) 反向迭代/元素副本),该副本分配给正在操作的向量。该分配将导致释放 + O(n) 元素销毁操作。有点慢。 :) 迄今为止我所知道的最伟大的实现【参考方案6】:

vector 不使用链表。它使用连续内存。

如果没有足够的保留空间push_back 分配一个新的内存块两倍 是原来的vector。通过这样做,摊销运行时间为 O(1)。

【讨论】:

...一个新的块,它比旧块大一些恒定的因子——但这个因子通常在 1.5 左右而不是 2 左右。 @Jerry,奇怪。对于 1.5 的因子,每个元素平均需要两个副本,而对于 2 的因子,它只需 1 个副本。 @Pavel:Andrew Koenig 多年前写了一篇文章,讲述了支持较小因素的一个原因。使用两倍,您丢弃的块的总和将始终小于您的下一个更大的分配,因此您永远无法将它们重用于同一个容器。只要因子是 @Coffin,仅当底层内存管理器节省分配块时才会出现这种情况。但是,管理器可能会坚持使用 2^n 块,这会使 1.5 方案无用。【参考方案7】:

感谢一些cmets,我完全修改了一个非常不正确的原始答案。

According to the STL spec,你的回答是正确的。向量被实现为一个动态调整大小的数组:

向量容器被实现为动态数组;与常规数组一样,向量容器的元素存储在连续的存储位置,这意味着不仅可以使用迭代器访问它们的元素,还可以使用指向元素的常规指针的偏移量来访问它们的元素。 但与常规数组不同,向量中的存储是自动处理的,可以根据需要进行扩展和收缩。

【讨论】:

没有。绝对不。向量是一个动态数组——标准要求底层存储与内置数组完全相同。该标准只需要 O(1) 摊销 时间。因此,在一般情况下,它只需要 O(1)。 向量的底层缓冲区必须是连续的,这意味着它不能是链表。如果每次调用期间都没有调整大小, push_back() 方法仍然可以在恒定时间内运行(平均而言)。这通常要求缓冲区的大小增加一些因子(例如,当向量满时大小增加一倍)。【参考方案8】:

面试官想要多少细节?例如,他是否在寻找您深入了解较低级别的详细信息?

除了通常的按需调整大小以保留平均 O(1) 语义外,需要考虑的一些事项包括但不限于:

异常安全:如果在添加新元素时抛出异常(例如回滚语义),实现是否保证向量的状态不会被修改?例如,在分配、复制、插入等过程中可能会引发异常。 正确使用分配器:实现是否正确使用vector 的分配器,而不是普通的基于new 的分配器(两者可能相同也可能不同)?理想情况下,这将由vector 的大小调整代码透明地处理,但是实现肯定会有所不同。

【讨论】:

【参考方案9】:

如果 size

很容易解释你必须分配一个比现有数组大两倍的新数组并将所有这些元素复制到新分配的内存中,然后删除旧数组并将新元素插入新数组。但这里有一些我认为面试官希望你提及的关键时刻。

    std::vector 中的元素不保存在一个数组中,因此 std::vector[1000] 的数据成员不是长度为 1000 的 int*。元素保存在内存块中,因此,您在复制时必须考虑到这一点。

    其次,标准的 stl 向量需要“给我一个可复制的对象,我可以插入它”,这意味着对 std::vector 的要求是 sometype 只需要一个 copy_constructer (而不是 operator = )。因此,当您分配 sometype 类型的新内存时,您必须考虑 sometyme 可能没有默认的复制构造函数。在常见的实现中,这是通过放置 new 运算符来完成的,以避免调用复制构造函数。

【讨论】:

老问题,但要么我误解了 1),要么完全错误。 std::vector 保证其所有元素都连续存储在内存中(也称为单个数组)。

以上是关于如何在 STL 向量中实现 push_back?的主要内容,如果未能解决你的问题,请参考以下文章

在指向向量的智能指针上使用 push_back() 时出现运行时错误

使用 C++ 和 STL 的向量元素乘积

如何在 Python 中实现向量自回归?

在 C++ 中的向量中查找() stl

如何在 glsl 中实现任意大小的向量

矢量 push_back 随机发生