C++语言是桌面系统,尤其是系统软件、大型应用软件的主流开发语言。C++语言以其灵活性著称,同时也更复杂。利用C++编写健壮的代码,更具有挑战性。C++允许动态内存管理,同时也容易导致更多和内存相关的问题。
为能够有效地避免开发中潜在的危险代码,应遵循C++相关的编码规范和惯用法:
1,基类或者带有虚函数的类应该将其析构函数声明为虚函数。
2,在构造函数中防止内存泄漏,在析构函数中不要抛出异常。
3,使用对应形式的new和delete。即:用delete来释放new申请的内存,delete[ ]释放new[ ]申请的内存。
4,指针在使用前必须初始化,指向动态内存的指针在释放后应立即置为空。
5,如果类构造函数中分配了资源,那么需要显式提供拷贝构造函数和赋值操作符,并且在析构函数中释放资源。
关于C++中的惯用法RAII。
RAII英文意思:Resource acquisition is initialization,资源获取即初始化。该原则或技术用于对资源的管理。其核心思想,是利用对象来管理资源,在对象的构造函数中获取资源,在其析构函数中释放资源。为了保证动态申请的内存能在即使出现异常的情况下仍能释放,比较理想的方法是使用局部变量来管理动态内存的所有权(ownership),就是所谓的智能指针。STL中的auto_ptr就是为解决资源所有权问题设计的,但是缺少对引用数和数组的支持并且不能用在STL容器中。Boost 库提供的智能指针相对成熟,实用价值高。其中,shared_ptr线程安全并且可以用在STL容器中。
BUG解决与调试:
发现问题是解决问题的前提。相对于修复内存缺陷,发现内存缺陷并准确定位导致缺陷的代码更为费时费力。及早准确地发现内存缺陷,对于提高开发效率非常重要。
一,利用断言及早暴露内存缺陷
断言是布尔调试语句,用来检测在程序运行的时候某一条件的值是否总为真。断言经常用来确认函数的输入、输出,检查对象的当前状态是否合法等。
在以下的场景使用断言可以帮助发现和内存非法访问相关的错误:
(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)。
二,利用C运行时刻库检查内存泄漏
VC++的C运行库(CRT)提供了广泛的功能,帮助用户检测内存泄漏。CRT提供了_CrtMemCheckPoint、_CrtDump MemoryLeaks、_CrtSetDbgFlag等函数来帮助调试内存泄漏。对于非MFC的工程, 要开启有效的内存泄漏报告功能, 需要进行如下设置:
(1)在StdAfx.h的头部添加如下代码并开启编译器/Yu 选项:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#define DEBUG_NEW new(_NORMAL_BLOCK, THIS_FILE, __LINE__)
(2)确保在每个.cpp文件的头部包含以下内容:
#include "stdafx.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#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继承来的类,MFC 已经按照上述方法做了基础工作
(参考MFC源代码 afxmem.cpp、dumpinit.cpp)。要有效转储从CObject继承的对象,需要:(1)对每个从CObject继承的类重载虚函数Dump。(2)在程序的初始化部分加入代码 afxDump.SetDepth(1)来开启深度转储。
三,利用VC++环境的调试和诊断功能,检查和发现常见内存缺陷
理解常见的内存缺陷问题以及在VC++环境下的症状,能辅助我们减少问题的发生和及时修改问题。
从错误的表现形式上看, 和堆栈有关的错误主要分为两大类:堆栈溢出和函数返回信息被破坏
(1)堆栈溢出(overflow)
此类错误主要有两种情形:
1)过大的局部变量。缺省情况下Windows为每个线程保留1M堆栈空间。在菜单
Project->Properties->Configuration Properties -> Linker->System中可以看到
Stack Reserve Size选项可以调整保留的堆栈空间大小。
2)递归调用层数过深。
在调试过程中,调用堆栈(call stack)窗口中可以发现函数递归调用的模式。
(2)函数返回信息被破坏此类错误主要有两种情形:
1)对局部变量的写操作超出了范围(上溢)。在调试过程中,函数堆栈被破坏掉的明显标志是无法显示调用堆栈,并且错误发生在被调用函数即将返回的位置。
2)在调用函数和被调用函数之间如果出现了函数参数的不匹配或者调用规范的不一致。
为了检查此类错误,应该在代码编译时打开/GS、/RTCs开关
(在菜单Project->Properties->Configuration Properties-> C/C++->Code Generation下设置)。
另外一类错误是动态内存错误。典型的情况如下:
(1)内存写越界。在调试版本中,如果是写上溢,就会收到“Damage:after block...”的跟踪
消息,如果是写下溢出就会收到“Damage: before block...”的跟踪消息。
(2)删除不合法指针。在调试版本中,删除未初始化的指针或者非堆指针时,会收到
_CrtIsValidHeapPointer断言错误。
(3)多次释放。在调试版本中,如果多次删除同一指针,会收到_BLOCK_TYPE_IS_VALID断言错误。要防止此类错误,应在delete某个指向动态内存的指针后立即将其置为空。
四,利用Windows结构化异常处理机制处理发布版本软件的内存崩溃
在程序的发布阶段,应尽量减少程序错误尤其是内存崩溃。如果崩溃了,应该“优雅”地退出,尽量收集程序崩溃时的运行信息以帮助程序供应商后续的调试。要捕捉内存非法访问并获知非法访问的指令地址、寄存器内容等信息,需要用到Windows的结构化异常处理(Structured Exception Handling,SEH)机制[6]。MiniDumpWriteDump是dbghelp.dll提供的一个 API函数(参考MSDN),用于转储用户模式程序的一些信息(比如堆栈情况等)并存为一个文件(比如.dmp文件),此文件可以被微软的调试器(VC++或者WinDBG)利用进行事后调试。使用此函数需要dbghelp.h、dbghelp.lib和dbghelp.dll(这些文件可以在Windows Platform SDK中找到)。要事后根据.dmp文件调试代码,需要为发布版本软件产生debug symbols (pdb)文件(打开编译器/DEBUG选项)。在拿到.dmp文件以后,用VC++打开.dmp文件,然后调试执行(按F5键)。这样,崩溃现场就会重现。
常见报错信息汇总:
一,Unresolved external symbol (不确定的外部“符号”)
如果连接程序不能在所在的库和目标文件内找到所引用的函数、变量或标签,将产生此错误消息。一般说来,发生错误的原因有两个:一是所引用的函数、变量不存在,拼写不正确或者使用错误;其次可能使用了不同的版本。
二,可能产生LNK2001错误的原因
一.由于编码错误导致的LNK2001
1.不相匹配的程序代码或模块定义(.DEF)文件能导致LNK2001。例如, 如果在C++ 源文件内声明了一变量“var1”,却试图在另一文件内以变量“VAR1”访问该变量,将发生该错误。
2.如果使用的内联函数是在.CPP文件内定义的,而不是在头文件内定
义将导致LNK2001错误。
3.调用函数时如果所用的参数类型同函数声明时的类型不符将会产生LNK2001。
4.试图从基类的构造函数或析构函数中调用虚拟函数时将会导致LNK2001。
5.要注意函数和变量的可公用性,只有全局变量、函数是可公用的。静态函数和静态变量具有相同的使用范围限制。当试图从文件外部访问任何没有在该文件内声明的静态变量时将导致编译错误或LNK2001。函数内声明的变量(局部变量) 只能在该函数的范围内使用。C++ 的全局常量只有静态连接性能。这不同于C,如果试图在C++的多个文件内使用全局变量也会产生LNK2001错误。一种解决的方法是需要时在头文件中加入该常量的初始化代码,并在.CPP文件中包含该头文件;另一种方法是使用时给该变量赋以常数。
二.由于编译和链接的设置而造成的LNK2001
1.如果编译时使用的是/NOD(/NODEFAULTLIB)选项,程序所需要的运行库和MFC库在连接时由编译器写入目标文件模块, 但除非在文件中明确包含这些库名,否则这些库不会被链接进工程文件。在这种情况下使用/NOD将导致错误LNK2001。
2.如果没有为wWinMainCRTStartup设定程序入口,在使用Unicode和MFC时将得到“unresolved external on [email protected]”的LNK2001错误信息。
3.使用/MD选项编译时,既然所有的运行库都被保留在动态链接库之内,源文件中对“func”的
引用,在目标文件里即对“__imp__func” 的引用。如果试图使用静态库LIBC.LIB或LIBCMT.LIB进行连接,将在__imp__func上发生LNK2001;如果不使用/MD选项编译,在使用MSVCxx.LIB连接时也会发生LNK2001。
4.使用/ML选项编译时,如用LIBCMT.LIB链接会在_errno上发生LNK2001。
5.当编译调试版的应用程序时,如果采用发行版模态库进行连接也会产生LNK2001;同样,使用调试版模态库连接发行版应用程序时也会产生相同的问题。
6.不同版本的库和编译器的混合使用也能产生问题,因为新版的库里可能包含早先版本没有的
符号和说明。
7.在不同的模块使用内联和非内联的编译选项能够导致LNK2001。如果创建C++库时打开了函数内联(/Ob1或/Ob2),但是在描述该函数的相应头文件里却关闭了函数内联(没有inline关键字),这时将得到该错误信息。为避免该问题的发生,应该在相应的头文件中用inline关键字标志内联函数。
8.不正确的/SUBSYSTEM或/ENTRY设置也能导致LNK2001。