欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

让你的C++代码变的更加健壮

程序员文章站 2022-06-10 17:50:03
介绍 在实际的项目中,当项目的代码量不断增加的时候,你会发现越来越难管理和跟踪其各个组件,如其不善,很容易就引入BUG。因此,我们应该掌握一些能让我们程序更加健壮的方法。 这篇文章提出了一些建议,能有引导我们写出更加健壮的代码,以避免产生灾难性的错误。即使、因为其复杂性和项目团队结构,你的程序目前不 ......

介绍

在实际的项目中,当项目的代码量不断增加的时候,你会发现越来越难管理和跟踪其各个组件,如其不善,很容易就引入bug。因此,我们应该掌握一些能让我们程序更加健壮的方法。

这篇文章提出了一些建议,能有引导我们写出更加健壮的代码,以避免产生灾难性的错误。即使、因为其复杂性和项目团队结构,你的程序目前不遵循任何编码规则,按照下面列出的简单的规则可以帮助您避免大多数的崩溃情况。

背景

先来介绍下作者开发一些软件(crashrpt),你可以网站上下载源代码。crashrpt 顾名思义软件崩溃记录软件(库),它能够自动提交你电脑上安装的软件错误记录。它通过以太网直接将这些错误记录发送给你,这样方便你跟踪软件问题,并及时修改,使得用户感觉到每次发布的软件都有很大的提高,这样他们自然很高兴。

在分析接收的错误记录的时候,我们发现采用下文介绍的方法能够避免大部分程序崩溃的错误。例如:局部变量未初始化导致数组访问越界,指针使用前未进行检测(null)导致访问访问非法区域等。

我已经总结了几条代码设计的方法和规则,在下文一一列出,希望能够帮助你避免犯一些错误,使得你的程序更加健壮。

initializing local variables (局部变量初始化)

使用未初始化的局部变量是引起程序崩溃的一个比较普遍的原因,例如、来看下面这段程序片段:

// define local variables

bool bexitresult; // this will be true if the function exits successfully

file* f; // handle to file

tchar szbuffer[_max_path];   // string buffer

 

// do something with variables above...

上面的这段代码存在着一个潜在的错误,因为没有一个局部变量初始化了。当你的代码运行的时候,这些变量将被默认负一些错误的数值。例如bexitresult 数值将被负为-135913245 ,szbuffer?必须以“”结尾,结果不会。因此、局部变量初始化时非常重要的,如下正确代码:

// define local variables

// initialize function exit code with false to indicate failure assumption

bool bexitresult = false;

// this will be true if the function exits successfully

// initialize file handle with null

file* f = null; // handle to file

// initialize string buffer with empty string

tchar szbuffer[_max_path] = _t("");   // string buffer

// do something with variables above...

 

注意:有人说变量初始化会引起程序效率降低,是的,确实如此,如果你确实非常在乎程序的执行效率,去除局部变量初始化,你得想好其后果。

initializing winapi structures

许多windows api都接受或则返回一些结构体参数,结构体如果没有正确的初始化,也很有可能引起程序崩溃。大家可能会想起用zeromemory宏或者memset()函数去用0填充这个结构体(对结构体对应的元素设置默认值)。但是大部分windows api 结构体都必须有一个cbsize参数,这个参数必须设置为这个结构体的大小。

看看下面代码,如何初始化windows api结构体参数:

notifyicondata nf; // winapi structure

memset(&nf,0,sizeof(notifyicondata)); // zero memory

nf.cbsize = sizeof(notifyicondata); // set structure size!

// initialize other structure members

nf.hwnd = hwndparent;

nf.uid = 0;  

nf.uflags = nif_icon | nif_tip;

nf.hicon = ::loadicon(null, idi_application);

_tcscpy_s(nf.sztip, 128, _t("popup tip text"));

// add a tray icon

shell_notifyicon(nim_add, &nf);

 

注意:千万不要用zeromemory和memset去初始化那些包括结构体对象的结构体,这样很容易破坏其内部结构体,从而导致程序崩溃.

// declare a c++ structure

struct iteminfo

{

  // the structure has std::string object inside

  std::string sitemname;

  int nitemvalue;

};

 

// init the structure

iteminfo item;

// do not use memset()! it can corrupt the structure

// memset(&item, 0, sizeof(iteminfo));

// instead use the following

item.sitemname = "item1";

item.nitemvalue = 0;

这里最好是用结构体的构造函数对其成员进行初始化.

// declare a c++ structure

struct iteminfo

{

    // use structure constructor to set members with default values

    iteminfo()

    {

      sitemname = _t("unknown");

      nitemvalue = -1;

    }

 

    std::string sitemname; // the structure has std::string object inside

    int nitemvalue;

};

// init the structure

iteminfo item;

// do not use memset()! it can corrupt the structure

// memset(&item, 0, sizeof(iteminfo));

// instead use the following

item.sitemname = "item1";

item.nitemvalue = 0;

validating function input

在函数设计的时候,对传入的参数进行检测是一直都推荐的。例如、如果你设计的函数是公共api的一部分,它可能被外部客户端调用,这样很难保证客户端传进入的参数就是正确的。

例如,让我们来看看这个hypotethical drawvehicle()?函数,它可以根据不同的质量来绘制一辆跑车,这个质量数值(ndrawingqaulity )是0~100。prcdraw?定义这辆跑车的轮廓区域。

看看下面代码,注意观察我们是如何在使用函数参数之前进行参数检测:

bool drawvehicle(hwnd hwnd, lprect prcdraw, int ndrawingquality)

{

    // check that window is valid

    if(!iswindow(hwnd))

      return false;

 

    // check that drawing rect is valid

    if(prcdraw==null)

      return false;

 

    // check drawing quality is valid

    if(ndrawingquality<0 || ndrawingquality>100)

      return false;

 

    // now it's safe to draw the vehicle

 

    // ...

 

    return true;

}

在指针使用之前,不检测是非常普遍的,这个可以说是我们引起软件崩溃最有可能的原因。如果你用一个指针,这个指针刚好是null,那么你的程序在运行时,将报出异常。

cvehicle* pvehicle = getcurrentvehicle();

 

// validate pointer

if(pvehicle==null)

{

    // invalid pointer, do not use it!

    return false;

}

 

// use the pointer

initializing function output

如果你的函数创建了一个对象,并要将它作为函数的返回参数。那么记得在使用之前把他复制为null。如不然,这个函数的调用者将使用这个无效的指针,进而一起程序错误。如下错误代码:

int createvehicle(cvehicle** ppvehicle)

{

    if(cancreatevehicle())

    {

      *ppvehicle = new cvehicle();

      return 1;

    }    

 

    // if cancreatevehicle() returns false,

    // the pointer to *ppvehcile would never be set!

    return 0;

}

正确的代码如下;

int createvehicle(cvehicle** ppvehicle)

{

    // first initialize the output parameter with null

    *ppvehicle = null;

 

    if(cancreatevehicle())

    {

      *ppvehicle = new cvehicle();

      return 1;

    }    

 

    return 0;

}

cleaning up pointers to deleted objects

在内存释放之后,无比将指针复制为null。这样可以确保程序的没有那个地方会再使用无效指针。其实就是,访问一个已经被删除的对象地址,将引起程序异常。如下代码展示如何清除一个指针指向的对象:

// create object

cvehicle* pvehicle = new cvehicle();

delete pvehicle; // free pointer

pvehicle = null; // set pointer with nul

cleaning up released handles

在释放一个句柄之前,务必将这个句柄复制伪null (0或则其他默认值)。这样能够保证程序其他地方不会重复使用无效句柄。看看如下代码,如何清除一个windows api的文件句柄:

handle hfile = invalid_handle_value;

// open file

hfile = createfile(_t("example.dat"), file_read|file_write, file_open_existing);

if(hfile==invalid_handle_value)

{

    return false; // error opening file

}

// do something with file

// finally, close the handle

if(hfile!=invalid_handle_value)

{

    closehandle(hfile);   // close handle to file

    hfile = invalid_handle_value;   // clean up handle

}

下面代码展示如何清除file *句柄:

// first init file handle pointer with null

file* f = null;

// open handle to file

errno_t err = _tfopen_s(_t("example.dat"), _t("rb"));

if(err!=0 || f==null)

return false; // error opening file

// do something with file

// when finished, close the handle

if(f!=null) // check that handle is valid

{

    fclose(f);

    f = null; // clean up pointer to handle


using delete [] operator for arrays

如果你分配一个单独的对象,可以直接使用new?,同样你释放单个对象的时候,可以直接使用delete . 然而,申请一个对象数组对象的时候可以使用new,但是释放的时候就不能使用delete ,而必须使用delete[]:

// create an array of objects

cvehicle* pavehicles = new cvehicle[10];

delete [] pavehicles; // free pointer to array

pavehicles = null; // set pointer with null

或者:

// create a buffer of bytes

lpbyte pbuffer = new byte[255];

delete [] pbuffer; // free pointer to array

pbuffer = null; // set pointer with null

allocating memory carefully

有时候,程序需要动态分配一段缓冲区,这个缓冲区是在程序运行的时候决定的。例如、你需要读取一个文件的内容,那么你就需要申请该文件大小的缓冲区来保存该文件的内容。在申请这段内存之前,请注意,malloc() or new是不能申请0字节的内存,如不然,将导致malloc() or new函数调用失败。传递错误的参数给malloc() 函数将导致c运行时错误。如下代码展示如何动态申请内存:

// determine what buffer to allocate.

uint ubuffersize = getbuffersize();

lpbyte* pbuffer = null; // init pointer to buffer

// allocate a buffer only if buffer size > 0

if(ubuffersize>0)

    pbuffer = new byte[ubuffersize];

 

为了进一步了解如何正确的分配内存,你可以读下secure coding best practices for memory allocation in c and c++这篇文章。

using asserts carefully

asserts用语调试模式检测先决条件和后置条件。但当我们编译器处于release模式的时候,asserts在预编阶段被移除。因此,用asserts是不能够检测我们的程序状态,错误代码如下:

#include <assert.h>

// this function reads a sports car's model from a file

cvehicle* readvehiclemodelfromfile(lpctstr szfilename)

{

    cvehicle* pvehicle = null; // pointer to vehicle object

    // check preconditions

    assert(szfilename!=null); // this will be removed by preprocessor in release mode!

    assert(_tcslen(szfilename)!=0); // this will be removed in release mode!

    // open the file

    file* f = _tfopen(szfilename, _t("rt"));

    // create new cvehicle object

    pvehicle = new cvehicle();

    // read vehicle model from file

    // check postcondition

    assert(pvehicle->getwheelcount()==4); // this will be removed in release mode!

    // return pointer to the vehicle object

    return pvehicle;

}

看看上述的代码,asserts能够在debug模式下检测我们的程序,在release 模式下却不能。所以我们还是不得不用if()来这步检测操作。正确的代码如下:

#include <assert.h>

cvehicle* readvehiclemodelfromfile(lpctstr szfilename, )

{

    cvehicle* pvehicle = null; // pointer to vehicle object

    // check preconditions

    assert(szfilename!=null);

    // this will be removed by preprocessor in release mode!

    assert(_tcslen(szfilename)!=0);

    // this will be removed in release mode!

    if(szfilename==null || _tcslen(szfilename)==0)

      return null; // invalid input parameter

    // open the file

    file* f = _tfopen(szfilename, _t("rt"));

    // create new cvehicle object

    pvehicle = new cvehicle();

    // read vehicle model from file

    // check postcondition

    assert(pvehicle->getwheelcount()==4); // this will be removed in release mode!

    if(pvehicle->getwheelcount()!=4)

    {

      // oops... an invalid wheel count was encountered!  

      delete pvehicle;

      pvehicle = null;

    }

    // return pointer to the vehicle object

    return pvehicle;

}

checking return code of a function

断定一个函数执行一定成功是一种常见的错误。当你调用一个函数的时候,建议检查下返回代码和返回参数的值。如下代码持续调用windows api ,程序是否继续执行下去依赖于该函数的返回结果和返回参数值.

hresult hres = e_fail;

iwbemservices *psvc = null;

iwbemlocator *ploc = null;

 

hres =  coinitializesecurity(

    null,

    -1,                          // com authentication

    null,                        // authentication services

    null,                        // reserved

    rpc_c_authn_level_default,   // default authentication

    rpc_c_imp_level_impersonate, // default impersonation  

    null,                        // authentication info

    eoac_none,                   // additional capabilities

    null                         // reserved

    );

 

if (failed(hres))

{

    // failed to initialize security

    if(hres!=rpc_e_too_late)

       return false;

}

 

hres = cocreateinstance(

    clsid_wbemlocator,

    0,

    clsctx_inproc_server,

    iid_iwbemlocator, (lpvoid *) &ploc);

 

if (failed(hres) || !ploc)

{

    // failed to create iwbemlocator object.

    return false;

}

 

hres = ploc->connectserver(

     _bstr_t(l"root\\cimv2"), // object path of wmi namespace

     null,                    // user name. null = current user

     null,                    // user password. null = current

     0,                       // locale. null indicates current

     null,                    // security flags.

     0,                       // authority (e.g. kerberos)

     0,                       // context object

     &psvc                    // pointer to iwbemservices proxy

     );

 

if (failed(hres) || !psvc)

{

    // couldn't conect server

    if(ploc) ploc->release();    

    return false;  

}

hres = cosetproxyblanket(

   psvc,                        // indicates the proxy to set

   rpc_c_authn_winnt,           // rpc_c_authn_xxx

   rpc_c_authz_none,            // rpc_c_authz_xxx

   null,                        // server principal name

   rpc_c_authn_level_call,      // rpc_c_authn_level_xxx

   rpc_c_imp_level_impersonate, // rpc_c_imp_level_xxx

   null,                        // client identity

   eoac_none                    // proxy capabilities

);

if (failed(hres))

{

    // could not set proxy blanket.

    if(psvc) psvc->release();

    if(ploc) ploc->release();    

    return false;              

}

using smart pointers

如果你经常使用用享对象指针,如com 接口等,那么建议使用智能指针来处理。智能指针会自动帮助你维护对象引用记数,并且保证你不会访问到被删除的对象。这样,不需要关心和控制接口的生命周期。关于智能指针的进一步知识可以看看smart pointers – what, why, which??和 implementing a simple smart pointer in c++这两篇文章。

如面是一个展示使用atl’s ccomptr template 智能指针的代码,该部分代码来至于msdn。

#include <windows.h>

#include <shobjidl.h>

#include <atlbase.h> // contains the declaration of ccomptr.

int winapi wwinmain(hinstance hinstance, hinstance, pwstr pcmdline, int ncmdshow)

{

    hresult hr = coinitializeex(null, coinit_apartmentthreaded |

        coinit_disable_ole1dde);

    if (succeeded(hr))

    {

        ccomptr<ifileopendialog> pfileopen;

        // create the fileopendialog object.

        hr = pfileopen.cocreateinstance(__uuidof(fileopendialog));

        if (succeeded(hr))

        {

            // show the open dialog box.

            hr = pfileopen->show(null);

            // get the file name from the dialog box.

            if (succeeded(hr))

            {

                ccomptr<ishellitem> pitem;

                hr = pfileopen->getresult(&pitem);

                if (succeeded(hr))

                {

                    pwstr pszfilepath;

                    hr = pitem->getdisplayname(sigdn_filesyspath, &pszfilepath);

                    // display the file name to the user.

                    if (succeeded(hr))

                    {

                        messagebox(null, pszfilepath, l"file path", mb_ok);

                        cotaskmemfree(pszfilepath);

                    }

                }

                // pitem goes out of scope.

            }

            // pfileopen goes out of scope.

        }

        couninitialize();

    }

    return 0;

}

using == operator carefully

先来看看如下代码;

cvehicle* pvehicle = getcurrentvehicle();

// validate pointer

if(pvehicle==null) // using == operator to compare pointer with null

   return false;

// do something with the pointer

pvehicle->run();

上面的代码是正确的,用语指针检测。但是如果不小心用“=”替换了“==”,如下代码;

cvehicle* pvehicle = getcurrentvehicle();

// validate pointer

if(pvehicle=null) // oops! a mistyping here!

return false;

// do something with the pointer

pvehicle->run(); // crash!!!

看看上面的代码,这个的一个失误将导致程序崩溃。

这样的错误是可以避免的,只需要将等号左右两边交换一下就可以了。如果在修改代码的时候,你不小心产生这种失误,这个错误在程序编译的时候将被检测出来。

// validate pointer

if(null==pvehicle)

// exchange left side and right side of the equality operator

    return false;

// validate pointer

if(null=pvehicle)

// oops! a mistyping here! but the compiler returns an error message.

    return false;

  博主是一个有着7年工作经验的架构师,对于c++,自己有做资料的整合,一个完整学习c语言c++的路线,学习资料和工具。可以进我的q群7418,18652领取,免费送给大家。希望你也能凭自己的努力,成为下一个优秀的程序员!另外博主的微信公众号是:c语言编程基地,欢迎关注!