C++语言是桌面系统,尤其是系统软件、大型应用软件的主流开发语言。C++语言以其灵活性著称,同时也更复杂。利用C++编写健壮的代码,更具有挑战性。C++允许动态内存管理, 同时也容易导致更多和内存相关的问题。一般而言, 除了系统设计上的缺陷, 基于C++的软件的缺陷和错误大部分都和内存缺陷(主要包括内存访问错误和内存泄漏两类)相关。 所以,消除代码中的内存相关缺陷,成为程序员编写、调试、维护代码中的任务,也是保证软件质量的关键。
本文的工作基于“863”计划项目“面向网络海量空间信息的大型GIS”课题。该系统是基于C++/MFC编写,开发环境是Visual Studio .net 2003。本文基于此项目的工程实践,总结了如何使用C++语言机制、开发环境和相关质量保证工具来预防、发现各种编译期、运行期和内存相关的缺陷的方法和工具。
1 遵循C++相关的编码规范和惯用法,预防缺陷
编码规范是语言相关的规则,是经过实践总结出来的经验。良好的编程标准将有效地帮助开发人员避免开发有潜在危险的代码。一般来说,为了减少内存缺陷,应该遵循下列编码规则[1]:
(1)基类或者带有虚函数的类应该将其析构函数声明为虚函数。
(2)在构造函数中防止内存泄漏,在析构函数中不要抛出异常。
(3)使用对应形式的new和delete。即:用delete来释放new申请的内存,delete[]释放new[]申请的内存。
(4)指针在使用前必须初始化,指向动态内存的指针在释放后应立即置为空。
(5)如果类构造函数中分配了资源,那么需要显式提供拷贝构造函数和赋值操作符,并且在析构函数中释放资源。
值得重视的是C++中的惯用法RAII。RAII核心思想是利用对象来管理资源,在对象的构造函数中获取资源,在其析构函数中释放资源[2]。为了保证动态申请的内存能在即使出现异常的情况下仍能释放,比较理想的方法是使用局部变量来管理动态内存的所有权(ownership),就是所谓的智能指针。STL中的auto_ptr就是为解决资源所有权问题设计的,但是缺少对引用数和数组的支持并且不能用在STL容器中。Boost库[3]提供的智能指针相对成熟,实用价值高。其中,shared_ptr线程安全并且可以用在STL容器中。具体示例参考文献[3]。
1.1 编码规范检查工具 CodeWizard
CodeWizard能够对源程序直接进行自动扫描、分析和检查。一旦发现违例,产生信息告知与哪条规则不符并作出解释。以CodeWizard 4.3 为例,其中内置了超过500条编码标准。CodeWizard可以选择对于当前的工程执行哪些编码标准。CodeWizard可以和VC++紧密集成,安装完毕以后,VC++中有CodeWizard工具条。
1.2 代码检查工具 PC-Lint
PC-Lint可检查编译器不易发现的错误。PC-Lint可对100多个C库函数进行检查,可以发现标准C/C++代码中的1 000多个常见错误。要把PC-lint和Visual Studio集成在一起,需要自己配置。Jon Zyzyck提供了一个报告生成器,可以帮助完成这个工作。 。文献[4]说明了如何在VC++环境中集成PC-Lint。
2 利用语言机制、开发环境和相关工具以预防和发现内存缺陷
发现问题是解决问题的前提。相对于修复内存缺陷,发现内存缺陷并准确定位导致缺陷的代码更为费时费力。及早准确地发现内存缺陷,对于提高开发效率非常重要。
2.1 利用断言及早暴露内存缺陷
断言是布尔调试语句,用来检测在程序运行的时候某一条件的值是否总为真。断言经常用来确认函数的输入、输出,检查对的当前状态是否合法等。 在以下的场景使用断言可以帮助发现和内存非法访问相关的错误:
(1)验证指针是否可读/写。在函数的入口处,经常需要验证指针所指向的内容区域是否可读/写。 通常采用assert(p!= NULL)的检测形式。 但是,指针的值不为空并不代表指针指向了合法可读/写内存。Win32 API提供了函数IsBadReadPtr、IsBadWritePtr、IsBadStringPtr、IsBadCodePtr用来检测指针指向的内存区域是否可读/写。C运行时库提供了_CrtIs ValidPointer、_CrtIsValidHeapPointer等函数,MFC库提供了AfxIsValidAddress、AfxIsValidString函数来完成类似功能。
(2)对基于MFC的程序,ASSERT_VALID宏通过调用重载的AssertValid函数来确定指向CObject派生类对象的指针是否有效。ASSERT_VALID宏主要调用了AfxIsValidAddress函数和CObject派生类对象的AssertValid函数(参考MFC源代码afx.h、objcore.cpp)。
2.2 利用C运行时刻库检查内存泄漏
VC++的C运行库(CRT)提供了广泛的功能,帮助用户检测内存泄漏。CRT提供了_CrtMemCheckPoint、_CrtDump MemoryLeaks、_CrtSetDbgFlag等函数来帮助调试内存泄漏。
对于非MFC的工程, 要开启有效的内存泄漏报告功能, 需要进行如下设置:
(1)在StdAfx.h的头部添加如下代码并开启编译器/Yu 选项:
1.#define _CRTDBG_MAP_ALLOC
2.#include
3.#include
4.#define DEBUG_NEW new(_NORMAL_BLOCK, THIS_FILE, __LINE__)
(2)确保在每个.cpp文件的头部包含以下内容:
1.#include "stdafx.h"
2.#ifdef _DEBUG
3.#define new DEBUG_NEW
4.#undef THIS_FILE
5.static char THIS_FILE[] = __FILE__;
6.#endif
(3)在程序的开始处开启报告内存泄漏的开关:
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);
对于MFC工程, MFC已经做了相关的工作, 只需要确认在每个.cpp文件的头部包含上述第(2)点的内容。
在某些情况下,需要知道发生内存泄漏的内存块中的内容,但是标准的内存转储只是内存块头部的十六进制形式。为了得到更多的有用信息,需要以用户块类型(_CLIENT_ BLOCK)申请内存,并利用_CrtSetDumpClient建立用户块型内存的转储函数。具体的说,对于不是从CObject继承的类,需要:
(1)为每个类/结构指定一个用户块子类型(参考crtdbg.h)。
(2)在申请内存时,采用重载的new形式:void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)(参考MFC源代码 afxmem.cpp),其中nType就是用户块的子类型。
(3)创建一个用户块内存转储函数,专门对每种需要转储的子类型进行处理(需要包含dbgint.h)。
(4)利用_CrtSetDumpClient对用户块内存转储函数进行注册(参考MFC源代码dumpinit.cpp)。
对于从CObject继