C++中令人疑惑的内存问题

C++的内存问题时常令人十分困惑。总结起来C++的内存问题可以分为以下几类

  1. 内存泄露
    当程序员使用new(或malloc)关键字分配内存而忘记使用 delete (或free)函数或 delete[] 运算符释放内存时,C++ 中就会发生内存泄漏。在 C++ 中使用错误的删除运算符会发生内存泄漏最多的情况之一。delete 运算符应用于释放单个分配的内存空间,而 delete [] 运算符应用于释放数据值数组。内存泄漏对于很多不能停机的程序是致命的。它会导致内存使用量就不断增加,直至程序宕机。
  2. 内存越界
    内存越界的背后其实是访问异常的内存位置。内存越界导致的问题往往让C++程序员十分困惑的问题,因为由内存越界导致程序出现crash的位置往往不是真正导致程序crash的位置,这给排查内存越界带来很大的困难。
  3. 访问未初始化对象
    与内存越界类似,访问未初始化的对象往往也会导致程序在其他位置crash。而产生crash的位置往往并不是导致crash原因。

对于一个严谨的程序员是绝不能让内存访问问题带到正式发布版本的。因此需要一些工具来帮助我们排查内存相关问题。本文的示例代码为内存泄漏

静态检查工具---Cppcheck

CppCheck是一个C/C++代码缺陷静态检查工具。不同于C/C++编译器及其它分析工具,CppCheck只检查编译器检查不出来的bug,不检查语法错误。所谓静态代码检查就是使用一个工具检查我们写的代码是否安全和健壮,是否有隐藏的问题。

Cppcheck的安装

在Ubuntu下只需要运行sudo apt install cppcheck即可安装。安装成功后,通过cppcheck --version命令,可以检查是否安装成功,以及查看版本。作者安装的版本是1.82。

Cppcheck的使用

cppcheck的使用也十分简单对于项检查的文件只需要输入cppcheck ${filename}即可。
例如在内存泄漏示例中,对main.cpp进行静态代码检查,会发现如下问题:

~/Code/leak_example$ cppcheck main.cpp 
cppcheck main.cpp 
Checking main.cpp ...
[main.cpp:13]: (error) Array 'b[10]' accessed at index 99, which is out of bounds.
[main.cpp:35]: (error) Memory leak: a

会发现在main.cpp第13行,出现了数组越界的问题,同时会发现代码中没有回收在开始时分配的内存。对于源代码分散在多个目录下的项目,可以直接把根目录作为cppcheck命令的参数,这样cppcheck就可以递归的检查该目录下所有的文件。

但是仅仅是静态代码检测是不足够的,细心地读者应该发现了其他的内存使用问题。事实上确实有很多问题不运行是难以发现的,因此需要valgrind进行更加完备的内存访问检测。

动态检查工具---valgrind

Valgrind 是一个用于构建动态分析工具的仪器框架。 Valgrind 工具可以自动检测许多内存管理和线程错误,并详细分析您的程序。您还可以使用 Valgrind 构建新工具。

Valgrind 发行版目前包括七个生产质量工具:一个内存错误检测器、两个线程错误检测器、一个缓存和分支预测分析器、一个调用图生成缓存和分支预测分析器,以及两个不同的堆分析器。可见valgrind是一个非常强大的CPP分析工具,在本文中主要介绍如何用valgrind进行内存问题的排查。

valgrind安装

在Ubuntu系统中只需运行sudo apt install valgrind即可开启内存检测之旅。同样可以使用valgrind --version来检查是否安装成功并检查版本。作者安装的版本是3.13.0。

valgrind使用和报告说明

在使用valgrind进行内存泄漏检测时一定要用debug模式编译项目,否则valgrind无法获取问题出现的文件行数。编译完之后运行valgrind ./${executablefile}就可以对生成的可执行文件进行检测了。valgrind将会执行该文件,并记录下内存使用问题的位置。在我们检测示例得到的可执行文件,可以得到以下报告(为了节省篇幅,只截取报告问题的部分)。

==12288== Invalid write of size 4
==12288==    at 0x1091B1: main (main.cpp:9)
==12288==  Address 0x5b7fc84 is 0 bytes after a block of size 4 alloc'd
==12288==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12288==    by 0x109176: main (main.cpp:7)


==12288== Invalid read of size 8
==12288==    at 0x109C0A: __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >::__normal_iterator(int* const&) (stl_iterator.h:783)
==12288==    by 0x109819: std::vector<int, std::allocator<int> >::begin() (stl_vector.h:564)
==12288==    by 0x10C522: Printer<int>::print() (printer.cpp:7)
==12288==    by 0x109203: main (main.cpp:17)
==12288==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

==12288== Process terminating with default action of signal 11 (SIGSEGV)
==12288==  Access not within mapped region at address 0x0
==12288==    at 0x109C0A: __gnu_cxx::__normal_iterator<int*, std::vector<int, std::allocator<int> > >::__normal_iterator(int* const&) (stl_iterator.h:783)
==12288==    by 0x109819: std::vector<int, std::allocator<int> >::begin() (stl_vector.h:564)
==12288==    by 0x10C522: Printer<int>::print() (printer.cpp:7)
==12288==    by 0x109203: main (main.cpp:17)

报告中主要发现了两个问题,,一个是位于main.cpp:9的内存访问越界,另一个是位于main.cpp:17的对空指针的成员函数调用。其中第一个错误是因为在分配内存时,错误的使用sizeof(),只分配了4字节的容量,却将其误用为大小为100个int大小的内存区域。第二个错误是因为使用了空指针的成员变量(member,printer.cpp:7)。修正这两个错误,继续检测该程序。

==13652== HEAP SUMMARY:
==13652==     in use at exit: 400 bytes in 1 blocks
==13652==   total heap usage: 16 allocs, 15 frees, 74,500 bytes allocated

依然检测到异常信息,但是并没有定位到有效位置,只定位到了程序的最后一行。这时需要增加选项--leak-check=full --show-leak-kinds=all,展示更多信息。最终得到更多的信息:

==12568== 400 bytes in 1 blocks are still reachable in loss record 1 of 1
==12568==    at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12568==    by 0x109176: main (main.cpp:7)

原来是程序开始时(main.cpp:7)分配的内存,没有被回收,与cppcheck检测结果一致。

valgrind的局限性

修正错误之后依然会发现下面的错误,这是因为访问数组b时出现了越界。然而valgrind没有定位到具体位置,只是报了一个异常退出的错误。这里就不得不提到valgrind自身的局限性:不能检测在栈上分配内存的使用正确性。因此对于这种情况,可以结合cppcheck来共同检查,确保内存使用的正确性。

*** stack smashing detected ***: <unknown> terminated
==13774== 
==13774== Process terminating with default action of signal 6 (SIGABRT)
==13774==    at 0x541DFB7: raise (raise.c:51)
==13774==    by 0x541F920: abort (abort.c:79)
==13774==    by 0x5468966: __libc_message (libc_fatal.c:181)
==13774==    by 0x5513B60: __fortify_fail_abort (fortify_fail.c:33)
==13774==    by 0x5513B21: __stack_chk_fail (stack_chk_fail.c:29)
==13774==    by 0x10943F: main (main.cpp:28)

侯磊
13 声望5 粉丝