在开始文章之前,回顾几个概念。

段错误(segmentation fault)

通常意义上的段错误是指程序访问的内存超出了系统给该程序的内存空间或者程序通过符号访问了不存在的物理内存。就自己的经历而言,常见有两种类型:数组越界使用悬挂指针。当然,根据对段错误的理解,还可能存在其他多种情况,对于一个C++程序员,这是我们的光荣,因为C++赋予了我们操作内存的权利,而同时也是我们的不幸,因为野指针(悬挂指针)满天飞的程序,画面都懂的。

core文件

当程序挂掉后,内核会把程序当前内存映射到core文件中。当然,程序挂掉后,对于可以产生core文件的情况,当我们需要确实产生core文件的时候,通过设置core文件的大小来允许产生。命令如下:

#设置core大小为无限
ulimit -c unlimited

通常来说,当程序接受到UNIX信号的时候可以产生core文件,信号列表如下:

信号名字 信号说明
SIGABRT 异常终止
SIGBUS 硬件故障
SIGEMT 硬件故障
SIGFPE 算术异常
SIGILL 非法硬件指令
SIGIOT 硬件故障
SIGQUIT 终端退出符
SIGSEGV 无效存储访问
SIGSYS 无效系统调用
SIGTRAP 硬件故障
SIGXCPU 超过CPU限制
SIGXFSZ 超过文件长度限制

SIGSEGV就是发生段错误的信号。在使用GDB调试程序的时候,当调试到程序挂掉的地方时,会产生该信号。

在调试挂掉的程序的时候,可以利用朴素的GDB单行调式。但是利用core文件可以快速定位挂掉的代码行,前提是设置core文件大小,见上段文字。具体操作如下,假设测试程序为test.cpp:

g++ -g test.cpp -o test

参数-g告诉编译器要生成调试所需信息(例如一些变量符号信息等)。

gdb test core

载入core文件。

(gdb)bt

查看挂掉程序的函数调用栈。

从调试流程上说就是这样的,目的在于定位crash的地方。但是如果对于原因不够了解,段错误依然是调试困难的问题(写这篇文章的时候,groot项目还有一个段错误问题没有解决)。

从groot中把测试场景拉出来,代码如下:

vector::erase()方法是从指定容器删除指定位置的元素或者某段范围内元素,通常的声明如下(LLVM/clang的实现):

iterator erase(const_iterator position);

如果是删除指定位置的元素时,返回值是一个迭代器,指向删除元素下一个元素。

iterator erase(const_iterator first, const_iterator last);

如果是删除某范围内的元素时,返回值也表示一个迭代器,指向最后一个删除元素的下一个元素。

在不同的平台下测试结果如下:

MAC和Ubuntu编译和运行都OK,Windows下编译OK,运行挂掉。Windows下的错误窗口(由实验室冒冒同学提供)如下所示:

erase-crash

为什么呢?不同平台针对相同的STL标准,给出了不同的STL实现,我抽出了MAC和Ubuntu中的对应函数的源代码,如下:

Mac中的编译器是LLVM/clang,代码如下:

Ubuntu中的编译器是g++,代码的实现是GNU ISO C++库的一部分,代码如下:

从源代码来看,和上述提到的erase的含义保持一致。Windows中 VS 的编译器是cl.exe,如果能够看到对应的STL实现就比较好。

好,现在按照对erase的理解是:返回值指向被删除元素的下一个元素。看我们的测试代码,假设

v.push_back(20);

在这行代码后再添加一行代码:

v.push_back(20);

也就是添加两个连续的值20,此时测试代码能够删除两个20?

不能!很显然,erase(iter)之后,删除了第一个20,此时iter已经指向第二个20了,然后for循环中iter++,此时iter已经指向了30,跳过了第二个20。

解决方法:

当然主流的解决方法还有第二种:

将第一个for循环代码改为:

v.erase(remove(v.begin(),v.end(),20),v.end());

总结:本文回顾了程序crash掉后的调试过程,抛出了一个Windows运行STL代码的结果和Mac及Ubuntu不同的问题,给出了vector::erase()的源代码实现,最后提到了一个erase常见的陷阱。正如恐惧的来源一样,陷阱是由于未知造成的。同时愿C++程序员的世界没有野指针。