Skip to content

Latest commit

 

History

History
944 lines (736 loc) · 34.1 KB

File metadata and controls

944 lines (736 loc) · 34.1 KB

九、调试技术

在本章中,我们将涵盖以下主题:

  • 有效调试
  • 调试策略
  • 调试工具
  • 使用 GDB 调试应用
  • 用 Valgrind 调试内存泄漏
  • 记录

有效调试

调试是一门艺术而不是科学,本身就是一个非常大的课题。强大的调试技能是优秀开发人员的优势。所有的专家开发人员都有一些共同的特点,其中强大的解决问题和调试技能是最重要的。修复 bug 的第一步是重现问题。非常有效地捕捉复制错误所涉及的步骤是至关重要的。经验丰富的质量保证工程师将知道捕获详细的重现步骤的重要性,因为如果开发人员不能重现问题,他们将发现很难修复它。

在我看来,一个无法复制的 bug 是无法修复的。人们可以猜测和拐弯抹角,但不能确定这个问题是否真的得到了解决,首先不能重现这个 bug。

以下细节将帮助开发人员更快地重现和调试问题:

  • 重现问题的详细步骤
  • bug 的截图图像
  • 优先级和严重性
  • 重现问题的输入和场景
  • 预期和实际产出
  • 错误日志
  • 应用日志和跟踪
  • 转储文件以防应用崩溃
  • 环境详细信息
  • 操作系统详细信息
  • 软件版本

一些常用的调试技术如下:

  • 使用cout / cerr打印报表非常方便
  • 核心转储、迷你转储和完整转储有助于远程分析错误
  • 通过检查变量、参数、中间值等,使用调试工具逐步执行代码
  • 测试框架首先有助于防止这个问题
  • 性能分析工具对于发现性能问题非常有帮助
  • 扣除内存泄漏、资源泄漏、死锁等的工具

The log4cpp open source C++ library is an elegant and useful log utility which helps add debug messages that support debugging, which can be disabled in the release mode or in the production environment.

调试策略

调试策略在快速复制、调试、检测和有效修复问题方面有很大帮助。下面的列表解释了一些高级调试策略:

  • 使用缺陷跟踪系统,如 JIRA、布奇拉、TFS、优酷、团队合作等
  • 应用崩溃或冻结必须包括核心转储、小型转储或完全转储
  • 应用跟踪日志在所有情况下都是很好的帮助
  • 启用多级错误日志
  • 在调试和发布模式下捕获应用跟踪日志

调试工具

调试工具通过使用断点、变量检查等逐步执行来帮助缩小问题的范围。虽然一步一步地调试问题可能是一项耗时的任务,但它绝对是确定问题的可靠方法,而且我可以说它几乎总是有效的。

下面是 C++ 调试工具的列表:

  • GDB :这是一个开源的 CLI 调试器
  • Valgrind :这是一个开源的 CLI,对内存泄漏、死锁、竞速检测等都有好处
  • 亲和调试器:这是 GDB 的商业 GUI 工具
  • GNU DDD :这是一个面向 GDB、DBX、JDB、XDB 等的开源图形调试器
  • GNU Emacs GDB 模式:这是一个开源工具,支持最少的图形调试器
  • KDevelop :这是一个支持图形调试器的开源工具
  • nemirver:这是一个开源工具,在 GNOME 桌面环境下运行良好
  • SlickEdit :这对于调试多线程和多处理器代码很有好处

In C++, there are quite a lot of open source and commercially licensed debugging tools. However, in this book, we will explore the GDB and Valgrind open source command-line interface tools.

使用 GDB 调试应用

传统的 C++ 开发人员使用打印语句来调试代码。但是,使用打印跟踪消息进行调试是一项耗时的任务,因为您需要花费大量精力在多个地方编写打印语句、重新编译和执行应用。

旧式的调试方法需要多次这样的迭代,通常,每次迭代都需要添加更多的打印语句来缩小问题的范围。一旦问题得到解决,我们需要清理代码并删除打印语句,因为过多的打印语句往往会降低应用的性能。此外,调试打印消息会分散注意力,并且对于在生产环境中使用您的产品的最终客户来说是不相关的。

带有<cassert>头的 C++ 调试assert()宏语句可以用于调试。C++ assert()宏可以在发布模式下禁用,只能在调试模式下启用。

调试工具可以把你从这些乏味的工作中解救出来。GDB 调试器是一个开源的命令行界面工具,它是 Unix/Linux 世界中 C++ 的调试器。对于 Windows 平台,Visual Studio 是最受欢迎的一站式 IDE,内置了调试功能。

让我们举一个简单的例子:

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std; //Use this judiciously - this is applicable throughout the book

class MyInteger {
      private:
           int number;

      public:
           MyInteger( int value ) {
                this->number = value;
           }

           MyInteger(const MyInteger & rhsObject ) {
                this->number = rhsObject.number;
           }

           MyInteger& operator = (const MyInteger & rhsObject ) {

                if ( this != &rhsObject )
                     this->number = rhsObject.number;

                return *this;
           }

           bool operator < (const MyInteger &rhsObject) {
                return this->number > rhsObject.number;
           }

           bool operator > (const MyInteger &rhsObject) {
                return this->number > rhsObject.number;
           }

           friend ostream & operator << ( ostream &output, const MyInteger &object );
};

ostream & operator << (ostream &o, const MyInteger& object) {
    o << object.number;
}

int main ( ) {

    vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };

    cout << "\nVectors entries before sorting are ..." << endl;
    copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, "\t" ) );
    cout << endl;

    sort ( v.begin(), v.end() );

    cout << "\nVectors entries after sorting are ..." << endl;
    copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, "\t" ) );
    cout << endl;

    return 0;
}

程序的输出如下:

Vectors entries before sorting are ...
10 100 40 20 80 70 50 30 60 90

Vectors entries after sorting are ...
100 90 80 70 60 50 40 30 20 10

但是,我们的预期输出如下:

Vectors entries before sorting are ...
10 100 40 20 80 70 50 30 60 90

Vectors entries after sorting are ...
10 20 30 40 50 60 70 80 90 100

bug 很明显;让我们放松对 GDB 的学习。让我们首先在调试模式下编译程序,即启用调试元数据和符号表,如下所示:

g++ main.cpp -std=c++ 17 -g

GDB 需要快速参考

以下 GDB 快速提示图表将帮助您找到调试应用的 GDB 命令:

| 命令 | 短命令 | 描述 | | gdb yourappln.exe | - | 在 GDB 打开应用进行调试 | | break main | b main | 将断点设置到main功能 | | run | r | 执行程序,直到到达断点,以便逐步执行 | | next | n | 一步一步地执行程序 | | step | s | 逐步进入函数以逐步执行函数 | | continue | c | 继续执行程序,直到下一个断点;如果没有设置断点,它将继续正常执行应用 | | backtrace | bt | 打印整个调用堆栈 | | quit | qCtrl + d | GDB 出口 | | -help | -h | 显示可用选项并简要显示其用途 |

有了前面的基本 GDB 快速参考,让我们开始调试我们的错误应用来检测错误。让我们首先用以下命令启动 GDB:

gdb ./a.out

然后,让我们在main()处添加一个断点来执行分步执行:

jegan@ubuntu:~/MasteringC++ Programming/Debugging/Ex1$ g++ main.cpp -g
jegan@ubuntu:~/MasteringC++ Programming/Debugging/Ex1$ ls
a.out main.cpp
jegan@ubuntu:~/MasteringC++ Programming/Debugging/Ex1$ gdb ./a.out

GNU gdb (Ubuntu 7.12.50.20170314-0ubuntu1.1) 7.12.50.20170314-git
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...done.
(gdb) b main
Breakpoint 1 at 0xba4: file main.cpp, line 46.
(gdb) l
32 
33 bool operator > (const MyInteger &rhsObject) {
34 return this->number < rhsObject.number;
35 }
36 
37 friend ostream& operator << ( ostream &output, const MyInteger &object );
38 
39 };
40 
41 ostream& operator << (ostream &o, const MyInteger& object) {
(gdb)

gdb启动我们的应用后,b main命令将在main()函数的第一行添加一个断点。现在让我们尝试执行应用:

(gdb) run
Starting program: /home/jegan/MasteringC++ Programming/Debugging/Ex1/a.out 

Breakpoint 1, main () at main.cpp:46
46 int main ( ) {
(gdb) 

正如您可能已经观察到的,在我们的main()函数中,程序执行在行号46处暂停,因为我们在main()函数中添加了一个断点。

现在,让我们一步一步地执行应用,如下所示:

(gdb) run
Starting program: /home/jegan/MasteringC++ Programming/Debugging/Ex1/a.out 

Breakpoint 1, main () at main.cpp:46
46 int main ( ) {
(gdb) next
48   vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };
(gdb) next
50   cout << "\nVectors entries before sorting are ..." << endl;
(gdb) n
Vectors entries before sorting are ...51   copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, "\t" ) );
(gdb) n
52   cout << endl;
(gdb) n
10 100 40 20 80 70 50 30 60 90 
54   sort ( v.begin(), v.end() );
(gdb) 

现在,让我们在行号2933处再添加两个断点,如下所示:

Breakpoint 1 at 0xba4: file main.cpp, line 46.Breakpoint 1 at 0xba4: file main.cpp, line 46.(gdb) run
Starting program: /home/jegan/Downloads/MasteringC++ Programming/Debugging/Ex1/a.out 
Breakpoint 1, main () at main.cpp:46
46 int main ( ) {
(gdb) l
41 ostream& operator << (ostream &o, const MyInteger& object) {
42    o << object.number;
43 }
44 
45 
46 
int main ( ) {
47 
48   vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };
49    
50   cout << "\nVectors entries before sorting are ..." << endl;
(gdb) n
48   vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };
(gdb) n
50   cout << "\nVectors entries before sorting are ..." << endl;
(gdb) n
Vectors entries before sorting are ...
51   copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, "\t" ) );
(gdb) break 29
Breakpoint 2 at 0x555555554f88: file main.cpp, line 29.
(gdb) break 33
Breakpoint 3 at 0x555555554b80: file main.cpp, line 33.
(gdb) 

由此,您将理解断点可以通过函数名或行号来添加。现在让程序继续执行,直到到达我们设置的一个断点:

(gdb) break 29
Breakpoint 2 at 0x555555554f88: file main.cpp, line 29.
(gdb) break 33
Breakpoint 3 at 0x555555554b80: file main.cpp, line 33.
(gdb) continue Continuing.
Breakpoint 2, MyInteger::operator< (this=0x55555576bc24, rhsObject=...) at main.cpp:30 30 return this->number > rhsObject.number; (gdb) 

如您所见,程序执行在行号29处暂停,因为每当sort函数需要决定在按升序排序向量条目的过程中是否必须交换这两个项目时,它都会被调用。

让我们探索如何检查或打印变量,this->numberrhsObject.number:

(gdb) break 29
Breakpoint 2 at 0x400ec6: file main.cpp, line 29.
(gdb) break 33
Breakpoint 3 at 0x400af6: file main.cpp, line 33.
(gdb) continue
Continuing.
Breakpoint 2, MyInteger::operator< (this=0x617c24, rhsObject=...) at main.cpp:30
30 return this->number > rhsObject.number;
(gdb) print this->number $1 = 100 (gdb) print rhsObject.number $2 = 10 (gdb) 

你看到<>运算符的实现方式了吗?操作员检查小于操作,而实际执行检查大于操作,并且在>操作员过载方法中也观察到类似的错误。请检查以下代码:

bool operator < ( const MyInteger &rhsObject ) {
        return this->number > rhsObject.number;
}

bool operator > ( const MyInteger &rhsObject ) {
        return this->number < rhsObject.number;
}

虽然sort()函数应该是以升序对vector条目进行排序,但输出显示它是以降序对它们进行排序,而前面的代码是问题的根本原因。因此,让我们解决这个问题,如下所示:

bool operator < ( const MyInteger &rhsObject ) {
        return this->number < rhsObject.number;
}

bool operator > ( const MyInteger &rhsObject ) {
        return this->number > rhsObject.number;
}

有了这些变化,让我们编译并运行程序:

g++ main.cpp -std=c++ 17 -g

./a.out

这是您将获得的输出:

Vectors entries before sorting are ...
10   100   40   20   80   70   50   30   60   90

Vectors entries after sorting are ...
10   20   30   40   50   60   70   80   90   100

酷,我们修好了窃听器!不用说,您将认识到 GDB 调试工具有多有用。虽然我们只是触及了 GDB 工具功能的表面,但它提供了许多强大的调试功能。然而,在本章中,涵盖 GDB 工具支持的每一个特性是不切实际的;因此,我强烈建议您探索 GDB 文档,以便在https://sourceware.org/gdb/documentation/进一步学习。

用 Valgrind 调试内存泄漏

Valgrind 是一个开源的 C/C++ 调试和分析工具的集合,适用于 Unix 和 Linux 平台。Valgrind 支持的工具集合如下:

  • Cachegrind :这是缓存剖析器
  • Callgrind :这与缓存分析器的工作方式类似,但是支持调用者-被调用者序列
  • Helgrind :这有助于检测线程同步问题
  • DRD :这是螺纹误差检测仪
  • 地块:这是堆剖面仪
  • 拉克:这提供了关于应用的基本性能相关统计和测量
  • exp-sgcheck :这检测堆栈溢出;它通常有助于发现 Memcheck 找不到的问题
  • exp-bbv :这对于计算机架构 R & D 相关的工作很有用
  • exp-dhat :这是另一个堆剖析器
  • 内存检查:这有助于检测与内存问题相关的内存泄漏和崩溃

在这一章中,我们将只探索 Memcheck,因为演示每个 Valgrind 工具都不在本书的范围内。

记忆检查工具

Valgrind 使用的默认工具是 Memcheck。Memcheck 工具可以检测到相当详尽的问题列表,其中一些如下:

  • 访问数组、堆栈或堆溢出的边界之外
  • 使用未初始化的内存
  • 访问已经释放的内存
  • 内存泄漏
  • newfreemallocdelete使用不匹配

让我们在接下来的小节中看看一些这样的问题。

检测数组边界之外的内存访问

下面的示例演示了数组边界之外的内存访问:

#include <iostream>
using namespace std;

int main ( ) {
    int a[10];

    a[10] = 100;
    cout << a[10] << endl;

    return 0;
}

以下输出显示了 valgrind 调试会话,该会话精确指向数组边界之外的内存访问:

g++ arrayboundsoverrun.cpp -g -std=c++ 17 

jegan@ubuntu  ~/MasteringC++/Debugging  valgrind --track-origins=yes --read-var-info=yes ./a.out
==28576== Memcheck, a memory error detector
==28576== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==28576== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==28576== Command: ./a.out
==28576== 
100
*** stack smashing detected ***: ./a.out terminated
==28576== 
==28576== Process terminating with default action of signal 6 (SIGABRT)
==28576== at 0x51F1428: raise (raise.c:54)
==28576== by 0x51F3029: abort (abort.c:89)
==28576== by 0x52337E9: __libc_message (libc_fatal.c:175)
==28576== by 0x52D511B: __fortify_fail (fortify_fail.c:37)
==28576== by 0x52D50BF: __stack_chk_fail (stack_chk_fail.c:28)
==28576== by 0x4008D8: main (arrayboundsoverrun.cpp:11)
==28576== 
==28576== HEAP SUMMARY:
==28576== in use at exit: 72,704 bytes in 1 blocks
==28576== total heap usage: 2 allocs, 1 frees, 73,728 bytes allocated
==28576== 
==28576== LEAK SUMMARY:
==28576== definitely lost: 0 bytes in 0 blocks
==28576== indirectly lost: 0 bytes in 0 blocks
==28576== possibly lost: 0 bytes in 0 blocks
==28576== still reachable: 72,704 bytes in 1 blocks
==28576== suppressed: 0 bytes in 0 blocks
==28576== Rerun with --leak-check=full to see details of leaked memory
==28576== 
==28576== For counts of detected and suppressed errors, rerun with: -v
==28576== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[1] 28576 abort (core dumped) valgrind --track-origins=yes --read-var-info=yes ./a.out

正如您将注意到的,由于非法内存访问,应用因核心转储而崩溃。在前面的输出中,Valgrind 工具准确地指向导致崩溃的那条线。

检测对已经释放的存储器位置的存储器访问

下面的示例代码演示了对已经释放的内存位置的内存访问:

#include <iostream>
using namespace std;

int main( ) {

    int *ptr = new int();

    *ptr = 100;

    cout << "\nValue stored at pointer location is " << *ptr << endl;

    delete ptr;

    *ptr = 200;
    return 0;
}

让我们编译前面的程序,了解 Valgrind 如何报告试图访问已经释放的内存位置的非法内存访问:

==118316== Memcheck, a memory error detector
==118316== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==118316== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==118316== Command: ./a.out
==118316== 

Value stored at pointer location is 100
==118316== Invalid write of size 4
==118316== at 0x400989: main (illegalaccess_to_released_memory.cpp:14)
==118316== Address 0x5ab6c80 is 0 bytes inside a block of size 4 free'd
==118316== at 0x4C2F24B: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==118316== by 0x400984: main (illegalaccess_to_released_memory.cpp:12)
==118316== Block was alloc'd at
==118316== at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==118316== by 0x400938: main (illegalaccess_to_released_memory.cpp:6)
==118316== 
==118316== 
==118316== HEAP SUMMARY:
==118316== in use at exit: 72,704 bytes in 1 blocks
==118316== total heap usage: 3 allocs, 2 frees, 73,732 bytes allocated
==118316== 
==118316== LEAK SUMMARY:
==118316== definitely lost: 0 bytes in 0 blocks
==118316== indirectly lost: 0 bytes in 0 blocks
==118316== possibly lost: 0 bytes in 0 blocks
==118316== still reachable: 72,704 bytes in 1 blocks
==118316== suppressed: 0 bytes in 0 blocks
==118316== Rerun with --leak-check=full to see details of leaked memory
==118316== 
==118316== For counts of detected and suppressed errors, rerun with: -v
==118316== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Valgrind 精确指向试图访问在行号12处释放的存储位置的行号(14)。

检测未初始化的内存访问

下面的示例代码演示了未初始化的内存访问的使用,以及如何使用 Memcheck 检测相同的情况:

#include <iostream>
using namespace std;

class MyClass {
    private:
       int x;
    public:
      MyClass( );
  void print( );
}; 

MyClass::MyClass() {
    cout << "\nMyClass constructor ..." << endl;
}

void MyClass::print( ) {
     cout << "\nValue of x is " << x << endl;
}

int main ( ) {

    MyClass obj;
    obj.print();
    return 0;

}

现在,让我们使用 Memcheck 编译并检测未初始化的内存访问问题:

g++ main.cpp -g

valgrind ./a.out --track-origins=yes

==51504== Memcheck, a memory error detector
==51504== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==51504== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==51504== Command: ./a.out --track-origins=yes
==51504== 

MyClass constructor ...

==51504== Conditional jump or move depends on uninitialised value(s)
==51504== at 0x4F3CCAE: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)
==51504== by 0x4009A1: main (uninitialized.cpp:26)
==51504== 
==51504== Use of uninitialised value of size 8
==51504== at 0x4F3BB13: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F3CCD9: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)
==51504== by 0x4009A1: main (uninitialized.cpp:26)
==51504== 
==51504== Conditional jump or move depends on uninitialised value(s)
==51504== at 0x4F3BB1F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F3CCD9: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)
==51504== by 0x4009A1: main (uninitialized.cpp:26)
==51504== 
==51504== Conditional jump or move depends on uninitialised value(s)
==51504== at 0x4F3CD0C: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)
==51504== by 0x4009A1: main (uninitialized.cpp:26)
==51504== 
Value of x is -16778960
==51504== 
==51504== HEAP SUMMARY:
==51504== in use at exit: 72,704 bytes in 1 blocks
==51504== total heap usage: 2 allocs, 1 frees, 73,728 bytes allocated
==51504== 
==51504== LEAK SUMMARY:
==51504== definitely lost: 0 bytes in 0 blocks
==51504== indirectly lost: 0 bytes in 0 blocks
==51504== possibly lost: 0 bytes in 0 blocks
==51504== still reachable: 72,704 bytes in 1 blocks
==51504== suppressed: 0 bytes in 0 blocks
==51504== Rerun with --leak-check=full to see details of leaked memory
==51504== 
==51504== For counts of detected and suppressed errors, rerun with: -v
==51504== Use --track-origins=yes to see where uninitialised values come from
==51504== ERROR SUMMARY: 18 errors from 4 contexts (suppressed: 0 from 0)

前面输出中以粗体突出显示的行清楚地指向访问未初始化变量的确切行:

==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)
==51504== by 0x4009A1: main (uninitialized.cpp:26)

 18 void MyClass::print() {
 19 cout << "\nValue of x is " << x << endl;
 20 } 

显示前面的代码片段供您参考;但是,Valgrind 不会显示代码细节。底线是 Valgrind 精确地指向访问未初始化变量的行,这通常很难用其他方法检测到。

检测内存泄漏

让我们以一个有一些内存泄漏的简单程序为例,探索 Valgrind 工具如何在 Memcheck 的帮助下帮助我们检测内存泄漏。由于 Memcheck 是 Valgrind 使用的默认工具,因此在发出 Valgrind 命令时没有必要显式调用 Memcheck 工具:

valgrind application_debugged.exe --tool=memcheck

下面的代码实现了一个单链表:

#include <iostream>
using namespace std;

struct Node {
  int data;
  Node *next;
};

class List {
private:
  Node *pNewNode;
  Node *pHead;
  Node *pTail;
  int __size;
  void createNewNode( int );
public:
  List();
  ~List();
  int size();
  void append ( int data );
  void print( );
};

正如您可能已经观察到的,前面的类声明有方法来append()一个新节点,print()列表,和一个size()方法返回列表中的节点数。

让我们探索实现append()方法、print()方法、构造函数和析构函数的list.cpp源文件:

#include "list.h"

List::List( ) {
  pNewNode = NULL;
  pHead = NULL;
  pTail = NULL;
  __size = 0;
}

List::~List() {}

void List::createNewNode( int data ) {
  pNewNode = new Node();
  pNewNode->next = NULL;
  pNewNode->data = data;
}

void List::append( int data ) {
  createNewNode( data );
  if ( pHead == NULL ) {
    pHead = pNewNode;
    pTail = pNewNode;
    __size = 1;
  }
  else {
    Node *pCurrentNode = pHead;
    while ( pCurrentNode != NULL ) {
      if ( pCurrentNode->next == NULL ) break;
      pCurrentNode = pCurrentNode->next;
    }

    pCurrentNode->next = pNewNode;
    ++ __size;
  }
}

void List::print( ) {
  cout << "\nList entries are ..." << endl;
  Node *pCurrentNode = pHead;
  while ( pCurrentNode != NULL ) {
    cout << pCurrentNode->data << "\t";
    pCurrentNode = pCurrentNode->next;
  }
  cout << endl;
}

以下代码演示了main()功能:

#include "list.h"

int main ( ) {
  List l;

  for (int count = 0; count < 5; ++ count )
    l.append ( (count+1) * 10 );
  l.print();

  return 0;
}

让我们编译程序,并尝试检测前面程序中的内存泄漏:

g++ main.cpp list.cpp -std=c++ 17 -g

valgrind ./a.out --leak-check=full 

==99789== Memcheck, a memory error detector
==99789== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==99789== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==99789== Command: ./a.out --leak-check=full
==99789== 

List constructor invoked ...

List entries are ...
10 20 30 40 50 
==99789== 
==99789== HEAP SUMMARY:
==99789== in use at exit: 72,784 bytes in 6 blocks
==99789== total heap usage: 7 allocs, 1 frees, 73,808 bytes allocated
==99789== 
==99789== LEAK SUMMARY:
==99789== definitely lost: 16 bytes in 1 blocks
==99789== indirectly lost: 64 bytes in 4 blocks
==99789== possibly lost: 0 bytes in 0 blocks
==99789== still reachable: 72,704 bytes in 1 blocks
==99789== suppressed: 0 bytes in 0 blocks
==99789== Rerun with --leak-check=full to see details of leaked memory
==99789== 
==99789== For counts of detected and suppressed errors, rerun with: -v
==99789== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

从前面的输出来看,很明显我们的应用泄漏了 80 个字节。虽然definitely lostindirectly lost表示我们的应用泄漏的内存,still reachable不一定表示我们的应用,也可能是第三方库或者 C++ 运行时库泄漏的。它们可能不是真正的内存泄漏,因为 C++ 运行时库可能使用内存池。

修复内存泄漏

让我们尝试通过在List::~List()析构函数中添加以下代码来修复内存泄漏问题:

List::~List( ) {

        cout << "\nList destructor invoked ..." << endl;
        Node *pTemp = NULL;

        while ( pHead != NULL ) {

                pTemp = pHead;
                pHead = pHead->next;

                delete pTemp;
        }

        pNewNode = pHead = pTail = pTemp = NULL;
        __size = 0;

}

从以下输出中,您将观察到内存泄漏已经修复:

g++ main.cpp list.cpp -std=c++ 17 -g

valgrind ./a.out --leak-check=full

==44813== Memcheck, a memory error detector
==44813== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==44813== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==44813== Command: ./a.out --leak-check=full
==44813== 

List constructor invoked ...

List entries are ...
10 20 30 40 50 
Memory utilised by the list is 80

List destructor invoked ...
==44813== 
==44813== HEAP SUMMARY:
==44813== in use at exit: 72,704 bytes in 1 blocks
==44813== total heap usage: 7 allocs, 6 frees, 73,808 bytes allocated
==44813== 
==44813== LEAK SUMMARY:
==44813== definitely lost: 0 bytes in 0 blocks
==44813== indirectly lost: 0 bytes in 0 blocks
==44813== possibly lost: 0 bytes in 0 blocks
==44813== still reachable: 72,704 bytes in 1 blocks
==44813== suppressed: 0 bytes in 0 blocks
==44813== Rerun with --leak-check=full to see details of leaked memory
==44813== 
==44813== For counts of detected and suppressed errors, rerun with: -v
==44813== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

如果您仍然不相信前面输出中报告的still reachable问题,让我们在simple.cpp中尝试以下代码,以了解这是否在我们的控制范围内:

#include <iostream>
using namespace std;

int main ( ) {

    return 0;

} 

执行以下命令:

g++ simple.cpp -std=c++ 17 -g

valgrind ./a.out --leak-check=full

==62474== Memcheck, a memory error detector
==62474== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==62474== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==62474== Command: ./a.out --leak-check=full
==62474== 
==62474== 
==62474== HEAP SUMMARY:
==62474== in use at exit: 72,704 bytes in 1 blocks
==62474== total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==62474== 
==62474== LEAK SUMMARY:
==62474== definitely lost: 0 bytes in 0 blocks
==62474== indirectly lost: 0 bytes in 0 blocks
==62474== possibly lost: 0 bytes in 0 blocks
==62474== still reachable: 72,704 bytes in 1 blocks
==62474== suppressed: 0 bytes in 0 blocks
==62474== Rerun with --leak-check=full to see details of leaked memory
==62474== 
==62474== For counts of detected and suppressed errors, rerun with: -v
==62474== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

如你所见,main()函数除了返回0什么也不做,Valgrind 报告这个程序也有相同的部分:still reachable": 72, 704 bytes in 1 blocks。因此,Valgrind泄漏总结中真正重要的是以下任何或所有部分是否有泄漏报告:definitely lostindirectly lostpossibly lost

不匹配地使用 new 和 free 或 malloc 和 delete

这类问题很少,但也不能排除发生的可能性。当一个遗留的基于 C 的工具移植到 C++ 时,一些内存分配被错误地分配,但是使用delete关键字被释放,反之亦然。

以下示例演示了如何使用 Valgrind 检测问题:

#include <stdlib.h>

int main ( ) {

        int *ptr = new int();

        free (ptr); // The correct approach is delete ptr

        char *c = (char*)malloc ( sizeof(char) );

        delete c; // The correct approach is free ( c )

        return 0;
}

以下输出演示了一个 Valgrind 会话,该会话检测到freedelete的不匹配使用:

g++ mismatchingnewandfree.cpp -g

valgrind ./a.out 
==76087== Memcheck, a memory error detector
==76087== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==76087== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==76087== Command: ./a.out
==76087== 
==76087== Mismatched free() / delete / delete []
==76087== at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==76087== by 0x4006FD: main (mismatchingnewandfree.cpp:7)
==76087== Address 0x5ab6c80 is 0 bytes inside a block of size 4 alloc'd
==76087== at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==76087== by 0x4006E7: main (mismatchingnewandfree.cpp:5)
==76087== 
==76087== Mismatched free() / delete / delete []
==76087== at 0x4C2F24B: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==76087== by 0x400717: main (mismatchingnewandfree.cpp:11)
==76087== Address 0x5ab6cd0 is 0 bytes inside a block of size 1 alloc'd
==76087== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==76087== by 0x400707: main (mismatchingnewandfree.cpp:9)
==76087== 
==76087== 
==76087== HEAP SUMMARY:
==76087== in use at exit: 72,704 bytes in 1 blocks
==76087== total heap usage: 3 allocs, 2 frees, 72,709 bytes allocated
==76087== 
==76087== LEAK SUMMARY:
==76087== definitely lost: 0 bytes in 0 blocks
==76087== indirectly lost: 0 bytes in 0 blocks
==76087== possibly lost: 0 bytes in 0 blocks
==76087== still reachable: 72,704 bytes in 1 blocks
==76087== suppressed: 0 bytes in 0 blocks
==76087== Rerun with --leak-check=full to see details of leaked memory
==76087== 
==76087== For counts of detected and suppressed errors, rerun with: -v
==76087== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

摘要

在本章中,您学习了各种 C++ 调试工具和 Valgrind 工具的应用,例如检测未初始化的变量访问和检测内存泄漏。您还了解了 GDB 工具,以及检测由于非法访问已释放的内存位置而出现的问题。

在下一章中,您将学习代码异味和整洁的代码实践。