翻译《有关编程、重构及其他的终极问题?》——38.从今以后使用nullptr而非NULL

翻译《有关编程、重构及其他的终极问题?》——38.从今以后使用nullptr而非NULL

标签(空格分隔):翻译 技术 C/C++
作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
最后更新:2018年09月17日


38.从今以后使用nullptr而非NULL

新的C++标准带来了很多有用的改变。其中有些我不会马上就直接使用,但还有一些的确有必要马上使用,因为这些会带来立竿见影的好处。

其中有一个现代化的改变就是关键字nullptr,它是用来取代NULL宏定义的。

在这里我还是提醒一下,在C++中NULL的定义其实就是0,而不是任何其他什么值。

当然,这看上去好像仅仅是个语法糖。但如果我们使用nullptr或者NULL,这两者区别是什么呢?使用nullptr可以避免大量的错误。我将会用例子来说明这点。

假设我们有两个重载的函数:

void Foo(int x, int y, const char *name);
void Foo(int x, int y, int ResourceID);

一个程序员也许会这么调用:

Foo(1, 2, NULL);

然后这个程序员也许会确信他的确通过这条语句调用了第一个函数。然后事实却并非如此。因为NULL只是被定义为0,而0是一个int类型,所以真实情况是:第二个函数被调用了,而非第一个函数。

然而,如果这个程序员使用的是nullptr就不会发生这个错误——第一个函数会被调用。

另外一种NULL常被使用的情况如下:

if (unknownError)
  throw NULL;

从我的角度看,用传递指针的方式产生异常是比较有争议的,然后,的确有时大家会这么干。显然,那个程序员(译者注:写上面代码的的程序员)需要这么写代码。然而,讨论这种做法时候合适,已经超出了本文的范围。

这里的核心点在于,那个程序员在遇到未知错误的时候,就通过异常来向外抛出一个空指针。

事实上,他抛出的不是一个指针而是int型。结果,这个异常处理的方式却不是这个长许愿所希望的。

“throw nullptr;”就可以让我们免于不幸,但这不代表我完全接受以上代码的写法(译者注:指有争议的抛出指针这种方式)。

在另外一些例子中,如果你使用nullptr,就可以让不正确的代码编译不能通过(译者注:这表明如果这些不正确的代码使用NULL,就编译通过了)。

想想看,在Win API的部分函数中常返回HRESULT类型,这种HRESULT类型和指针一点关系都没有(译者注:HRSULT其实是32位无符号整型)。然而,却有人很可能写下如此荒唐的代码:

if (WinApiFoo(a, b, c) != NULL)

这段代码而且还能编译通过,因为NULL是0,是int类型,而HRSULT是long类型。对编译器而言,int和long类型是可以比较的。如果你使用nullptr,下面的代码就不会编译通过:

if (WinApiFoo(a, b, c) != nullptr)

因为编译器报错了,这个程序员就会注意,并且修正代码。

我认为你已经知道怎么回事了。其实还有大量的类似例子,但大多都是语义列举出来的例子,所以不是一直很让人信服。有没有真实的例子呢?有的,下面就是。下面只是其中之一,要注意的是,下面这个例子不太很优雅和精简。

下面的代码来自MTASA项目。

有一个RtlFillMemory()函数,可能是个真实的函数或者是一个宏定义,这无关紧要。这个函数和memset()函数类似,只是其第二和第三个参数交换了位置。下面是这个宏的声明方式:

#define RtlFillMemory(Destination,Length,Fill) \
  memset((Destination),(Fill),(Length))

还有一个FillMemory()函数,和RtlFillMemory()函数没有什么区别:

#define FillMemory RtlFillMemory

是的,上面的语句都是又长又复杂,但这至少是一段真实的错误实例。

接下来,有一段使用FillMemory宏的代码:

LPCTSTR __stdcall GetFaultReason ( EXCEPTION_POINTERS * pExPtrs )
{
  ....
  PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&g_stSymbol ;
  FillMemory ( pSym , NULL , SYM_BUFF_SIZE ) ;
  ....
}

这段代码有一些bug。我们可以清楚的看到在这里第二和第三个参数有些让人迷惑,这也就是为什么分析器(译者注:指的是作者的PVS-Studio的分析器)有两个V575警告:

  • V575 The ‘memset’ function processes value ‘512’. Inspect the second argument. crashhandler.cpp 499
  • V575 The ‘memset’ function processes ‘0’ elements. Inspect the third argument. crashhandler.cpp 499

因为NULL就是0,所以代码编译成功了。结果0个元素被填充了,但这里的错误却不是指这个。NULL一般来说在这里是不合适的,因为memset()函数使用的是字节填充,所以使用NULL去填充有些无厘头,有些荒谬,正确的代码如下:

FillMemory(pSym, SYM_BUFF_SIZE, 0);

或者下面的也可以:

ZeroMemory(pSym, SYM_BUFF_SIZE);

但这不是我们想集中关注的,我们要关注的是,以上无意义的代码竟然编译通过了。但如果,这个程序员有了使用nullptr而非NULL的习惯,下了下面的代码:

FillMemory(pSym, nullptr, SYM_BUFF_SIZE);

编译器就会报错,然后这个程序员就会意识到他做了错误的事情,以后说不定会更注意写代码的方式。

注意,我知道在这个例子中,NULL本身没有问题。问题在于使用了NULL后,错误的代码会被编译器编译通过。

建议

立即开始使用nullptr吧。并且可以以此更新你公司的编程规范。

使用nullptr可以避免一些愚蠢的错误,并且也会因此而略微加快团队的研发速度。

猜你喜欢

转载自blog.csdn.net/headman/article/details/82732405