正文
nullptr
,
其旨在代替
NULL
宏。
让我提醒你一下,在
C++
中,
NULL
的定义是
0
,没有其他的了。
当然,看起来它好像就是一些语法糖(
syntactic sugar
)。那么当我们写
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
又是整形,所以其实调用的是第二个函数。
然而,如果程序员用的是
nullptr
,就不会出现这样的问题,而且第一个函数也能正确调用。另一种比较常见的使用
NULL
的例子是这样的:
if (unknownError)
throw NULL;
在我看来,传一个指针到异常里面真的很奇怪。尽管如此,还是有人这么做。这样看来,那些程序员应该是这么写代码的。无论如何,关于这样写是好是坏的讨论已经超纲了。
最重要的是,那个程序员打算在处理未知错误的时候抛出一个异常,然后‘发送’一个空指针到外部世界。
事实上,这不是一个指针,而是一个整形。结果就是,异常处理的结果不像程序员所期望的那样。
"throw nullptr;"
这行代码能让我们避免不幸,但是并不意味着我相信这样的代码能被接受。
在一些例子中,如果你用
nullptr
,不正确的代码就不会被编译。
假设有些
WinApi
函数返回一个
HRESULT
类型。
HRESULT
类型没有能用来处理指针的东西。然而还是有可能会写出类似这种无意义的代码:
if
(WinApiFoo(a, b, c) != NULL)
这行代码能够编译,因为
NULL
是
0,
是一个整形,然后
HRESULT
是一个长整形。长整形和整形是可以比较值的。如果你有
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。我们可以清晰的看到这里至少2到3个参数很混乱。这就是为啥分析器给出了两个警告 V575:
V575 'memset' 函数要处理”512“值。检查第二个参数。crashhandler.cpp 499
V575 'memset' 函数要处理”0“元素。检查第三个参数。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
会帮助你避免某些愚蠢的错误,然后就可以稍微加快开发进程。
39.
为什么不正确的代码也能运行
这个
bug
是在
Miranda NG
的项目中发现的。代码中包含的错误被
PVS-Studio
分析器诊断为:
V502
可能
'?:'
操作的结果和预期的有差。
'?:'
的优先级低于
'|'
。
#define MF_BYCOMMAND 0x00000000L
void CMenuBar::updateState(const HMENU hMenu) const
{
....
::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR,
MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED);
....
}
解释
我们有看到过很多例子,即使代码逻辑不对也能运行。这一次,我想提出一个不一样的、发人深思的话题来讨论。有时我们会看到完全不正确的代码很偶然的,即使困难重重,还是运行了。现在,对于有经验的程序员来说,这没什么好惊讶的(这是另一个故事了),但对那些
C/C++
的初学者来说,这就有点令人感到困惑了。所以,今天我们来看看这样的例子。
在上面给出的代码中,我们需要调用带有一定标志的
CheckMenuItem()
;
而且,猛地一看我们会觉得,如果
bShowAvatar
是