专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
云技术  ·  88万元,数据可视化系统大单:帆软中标 ·  6 小时前  
云技术  ·  88万元,数据可视化系统大单:帆软中标 ·  6 小时前  
终码一生  ·  用好缓存的10条军规 ·  昨天  
终码一生  ·  用好缓存的10条军规 ·  昨天  
计算机与网络安全  ·  200页PPT AI ... ·  昨天  
51好读  ›  专栏  ›  看雪学苑

C++编程的 42 条建议(六)完

看雪学苑  · 公众号  · 互联网安全  · 2017-03-13 18:38

正文

请到「今天看啥」查看全文


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







请到「今天看啥」查看全文