为什么vector::clear不会从向量中删除元素?

32

当我在std::vector上使用clear()时,它应该销毁vector中的所有元素,但实际上它并没有这样做。

示例代码:

vector<double> temp1(4);
cout << temp1.size() << std::endl;
temp1.clear();
cout << temp1.size() << std::endl;

temp1[2] = 343.5; // I should get segmentation fault here ....

cout << "Printing..... " << temp1[2] << endl;
cout << temp1.size() << std::endl;

现在,当我尝试访问已清除的向量时,我应该得到分段错误(segmentation fault),但它却填充了那里的值(根据我的观察,这非常有问题)。

结果如下:

4
0
Printing..... 343.5
0

这是正常现象吗?这是一个非常难以发现的错误,它基本上让我的代码停滞了数月。


2
如果你想捕获类似的错误,可以使用一个带检查的容器(gcc可以做到这一点,或者使用外部的stl库等)。 - Karoly Horvath
1
分段错误是由内存管理单元产生的,这是C++不需要的硬件组件。如果未能获得分段错误导致程序行为异常,那么您可能有更严重的问题。 - Potatoswatter
7
你可以使用 at 运算符,它会进行边界检查并引发异常。我建议使用 at 而不是 [] - Bill
1
@KarolyHorvath:MSVC的库默认在调试版本中进行检查,而在发布版本中不进行检查。这太棒了。 - Mooing Duck
你可以考虑使用clang静态分析器来分析你的代码:http://clang-analyzer.llvm.org/。我认为它会标记出这个错误。 - Hack Saw
你可能考虑使用其他工具,如Purify、Valgrind等,即使这些内存访问错误不会导致崩溃,它们也会标记出来。 - layman
10个回答

77

您没有获得分段错误的权利。而且,分段错误甚至不是C++的一部分。您的程序正在从向量中删除所有元素,并且您正在非法地访问容器超出边界。这是未定义行为,意味着任何事情都可能发生。确实,发生了某些事情。


再次谈到C++编译器的话题,那个在UB上格式化你的硬盘的东西 :D - Vorac
从C++标准到充满新手编译器的“类” - “你有权保持沉默”... :-) - GuruM

31

当您访问向量的范围之外时,会出现未定义行为。这意味着任何事情都可能发生。

因此,您可能会获得旧值、垃圾或段错误。您不能依赖任何内容。

如果您想进行边界检查,请使用at()成员函数而不是operator []。它将抛出异常而不是调用未定义行为。


28

来自cppreference:

void clear();

清空容器中的所有元素。使任何引用、指针或迭代器无效,这些引用、指针或迭代器是指向容器中包含的元素的。可能会使任何指向超出末尾的迭代器无效。许多实现在调用 clear() 后不会释放已分配的内存,有效地保持向量的容量不变。

因此,没有明显的问题是因为向量仍然在存储器中具有可用的内存。当然这仅取决于具体实现,但这并不是一个 bug。此外,正如其他答案所指出的,您的程序在访问已清除的内容时也存在未定义行为,因此从技术上讲,任何事情都可能发生。


1
这个突出部分是由标准保证的,还是只是典型的行为? - Mark Ransom
1
@sasha.sochka 这有什么关系吗? - Praetorian
1
@MarkRansom 标准只是说它会清除容器中的所有元素。到目前为止,我还没有找到任何保证这种行为的东西。 - David G
2
@0x499602D2 序列容器要求(§23.2.3 / 表100)指出:a.clear() - 销毁 a 中的所有元素。使所有引用、指针和迭代器失效,并可能使过去的终止迭代器失效。 cppreference 在 capacity() 不变以及过去的终止迭代器保持有效方面是错误的。 - Praetorian
1
是的,我认为这会误导人。请参阅https://dev59.com/F2w15IYBdhLWcg3wA3GT我已更新cppreference以反映这是特定于实现的。 - Nate Kohl
显示剩余5条评论

8

假设你很富有(也许你是,也许你不是……无所谓!)

由于你很富有,你在摩尔岛(风暴群岛,法属波利尼西亚)上购买了一块土地。你非常确定这是一块好地产,所以你在那个岛上建了一座别墅并住在那里。你的别墅有一个游泳池、一个网球场、一个大车库,甚至还有更多漂亮的东西。

过了一段时间,你觉得摩尔岛变得非常无聊,因此你卖掉了你的土地和别墅,并决定搬到其他地方去。

如果你过一段时间回来,你可能会遇到很多不同的事情,但你不能确定其中任何一件事。

  • 你的别墅可能已经消失,被俱乐部酒店取代。
  • 你的别墅可能仍然在那里。
  • 该岛可能已经沉没。
  • ......

谁知道呢?即使别墅不再属于你,你可能还能跳进游泳池或者再次打网球。甚至可能有另一个别墅在旁边,你可以在一个更大的游泳池里游泳,没有人会干扰你。

如果你再次回来,你无法保证你会发现什么,这与我查看的实现中包含三个指针的向量是一样的:

  • begin 指向分配的内存位置的开始(即 X)
  • end 指向分配的内存的末尾 +1(即 begin+4)
  • last 指向容器中的最后一个元素 +1(即 begin+4)

通过调用 clear 函数,容器可能会销毁所有元素并重置 last = begin;。函数 size() 很可能会返回 last-begin;,因此你将观察到一个大小为 0 的容器。尽管如此,begin 可能仍然有效,并且可能仍有内存分配(end 可能仍然是 begin+4)。你甚至可以在 clear() 之前设置的值。

std::vector<int> a(4);
a[2] = 12;
cout << "a cap " << a.capacity() << ", ptr is " << a.data() << ", val 2 is " << a[2] << endl;
a.clear();
cout << "a cap " << a.capacity() << ", ptr is " << a.data() << ", val 2 is " << a[2] << endl;

打印:

一个 cap 4,ptr 是 00746570,val 2 是 12
a cap 4,ptr 是 00746570,val 2 是 12

为什么没有观察到任何错误呢?这是因为 std::vector<T>::operator[] 不执行任何越界检查(与 std::vector<T>::at() 相反)。 由于 C++ 中不存在 "segfaults",所以您的程序似乎运行正常。

注意:在 MSVC 2012 上,如果在调试模式下编译,则 operator[] 执行边界检查。

欢迎来到未定义行为的世界!事情可能发生或可能不会发生。您可能连一个单独的情况都不能确定。 您可以冒险并大胆地研究它,但这可能不是生成可靠代码的方法。


6

operator[]很高效,但代价是它不会执行边界检查。

有更安全且高效的方法访问向量,例如迭代器等。

如果您需要随机访问向量(即不总是顺序的),要么非常小心地编写程序,要么使用不那么高效的at(),在相同条件下,后者会抛出异常。


更准确地说,许多实现在清除后不会释放内存--请参见0x499602D2答案中的评论。 - Nate Kohl

2
你可能会出现段错误,但这并不确定,因为在调用clear()之后,使用operator[]访问超出向量范围的元素只是未定义行为。根据你的帖子,看起来你想尝试元素是否被销毁,所以你可以使用at公共函数来实现:

该函数自动检查n是否在向量的有效元素范围内,并在其不在范围内时抛出一个out_of_range异常(即n大于或等于其大小)。这与成员操作符[]相反,后者不检查范围。

此外,在调用clear()之后:

与此容器相关的所有迭代器、指针和引用都无效。

http://www.cplusplus.com/reference/vector/vector/at/

2
尝试访问比构造函数中使用的4更大的元素,可能会导致分段错误。另一个来自cplusplus.com的想法是:清除内容。从向量中删除所有元素(这些元素将被销毁),使容器的大小为0。不能保证重新分配,并且由于调用此函数而导致向量容量更改也不能保证。强制重新分配的典型替代方法是使用交换:vector().swap(x); //清除x并重新分配

2
如果你使用 <\p>
temp1.at(2) = 343.5;

代替

temp1[2] = 343.5;

您会发现问题所在。建议使用at()函数,而operator[]不会检查边界。即使不知道STL vector的实现,也可以避免错误。

顺便说一下,我在我的Ubuntu (12.04)上运行了您的代码,结果与您所说的一样。然而,在Win7上报告“Assertion Failed”。

好的,这让我想起stringstream的类型。如果定义句子

stringstream str;
str << "3456";

如果要重复使用str,我被告知应该这样做。
str.str("");
str.clear();

不要只使用这个句子,

请尝试使用更加生动形象的表达方式。
str.clear();

我在Ubuntu系统中尝试了resize(0)操作,但效果不佳。


我很好奇 - 是否有办法让GCC像MSVC一样工作,并自动检测此类问题?我通常在Windows上编程,我发现这非常有帮助,但我也在Linux下工作,我想使用相同的错误检查机制。 - Andrei Bârsan

1

是的,这是正常的。clear()并不保证重新分配内存。在clear()之后尝试使用resize()


1
“resize”不能保证重新分配内存,但它确保元素将被重置为已知的值。 - Mark Ransom

0

到目前为止,回答中有一个重要补充:如果向量所实例化的类提供了析构函数,那么在清除时(也包括resize(0))将会调用该函数。

试试这个:

struct C
{
    char* data;
    C()           { data = strdup("hello"); }
    C(C const& c) { data = strdup(c.data); }
    ~C()          { delete data; data = 0; };
};
int main(int argc, char** argv)
{
    std::vector<C> v;
    v.push_back(C());
    puts(v[0].data);
    v.clear();
    char* data = v[0].data; // likely to survive
    puts(data);             // likely to crash
    return 0;
}

这个程序很可能会因为分段错误而崩溃 - 但是(很有可能)不是在char* data = v[0].data;这一行,而是在puts(data);这一行(使用调试器查看)。

典型的向量实现会保留内存分配并在调用析构函数后将其保持不变(但没有保证 - 记住,这是未定义的行为!)。最后一件事是将C实例的数据设置为nullptr,虽然在C++/vector的意义上无效,但内存仍然存在,因此可以(非法地)访问它而不会发生分段错误。当在puts中解引用char* data指针时,会出现这种情况,因为它为空...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接