首页
社区
课程
招聘
[翻译]关于C++编程的42条建议(三)
发表于: 2017-3-4 17:38 3802

[翻译]关于C++编程的42条建议(三)

2017-3-4 17:38
3802


21.检查文件终止符(EOF)有正确到达终点

让我们继续讨论和文件有关的话题,再来看EOF。但这次我们要讨论的是完全不同的bug类型。它通常只出现在本地化软件版本里。

下面的代码片段是选自Computational Network Toolkit错误被PVS-Studio诊断为:V739 EOF不应该和char类型比较。‘c’应该是int类型。

string fgetstring(FILE* f)
{ 
  string res;  
  for (;;)  
 {     
    char c = (char) fgetc(f); 
    if (c == EOF)      
     RuntimeError("error reading .... 0: %s", strerror(errno));
    if (c == 0)    
       break;    
       res.push_back(c); 
  }  
   return res; 
}

解释

让我们来看EOF是怎样声明的:

#define EOF (-1)


如你所见,EOF就是一个值为‘-1’int类型。Fgetc()返回一个int类型的值。也就是说,它可以返回0255的一个数,或者是-1EOF)。

使用扩展字符集(Extended ASCII Codes)的用户在程序中某一个字母处理不当的时候可能会出错。

比如说,在Windows1251编码表中,俄文的最后一个字母是0xFF ,而在程序中就会被编译为文件终止符。

正确代码

for (;;)
{
int c = fgetc(f);
if (c == EOF)
RuntimeError("error reading .... 0: %s", strerror(errno));
if (c == 0)
break;
res.push_back(static_cast<char>(c));
}

建议

在这里并没有什么特殊的建议,但既然我们谈到了EOF,我想展示一些人没有注意到的比较有趣的错误。

只要记住,如果一个函数的返回值是int类型,就不要急于把它转化为char类型。停下来检查看有没有错。顺便提一句,我们在第二个技巧“大于0并不意味着是1”那里讨论memcmp()函数的时候用到了相似的代码。(关于MySQL漏洞那一块)。

22.不要用#pragma warning(default:X)

下面的代码来自TortoiseGIT项目。该错误被PVS-Studio诊断为:V665 也许,在此处,‘#pragma warning(default: X)'用得不对。应该用'#pragma warning(push/pop)'

#pragma warning(disable:4996) 
LONG result = regKey.QueryValue(buf, _T(""), &buf_size); 
#pragma warning(default:4996)


解释

程序员经常认为,早期让警告实现的"pragma warning(disable: X)"指令在使用"pragma warning(default : X)"后就能继续工作。但并非如此。'pragma warning(default : X)'这条指令是把‘X’警告设置为DEFAULT状态,这两个不是同一件事。

假设一个文件在编译时用了-wall,那么就会出现C4061警告。如果你增加了"#pragma warning(default : 4061)" 指令,那么这条警告就不会出现,因为它已经被默认关掉了。

正确代码

#pragma warning(push) 
#pragma warning(disable:4996) 
LONG result = regKey.QueryValue(buf, _T(""), &buf_size)
#pragma warning(pop)


建议

返回警告的之前状态的正确方法是使用"#pragma warning(push[ ,n ])" "#pragma warning(pop)"这两条指令。可以看Visual C++11关于这些指令描述的文档:: Pragma Directives. Warnings.

库开发者应该要特别注意V665警告。忽视定制警告会在库使用者这边引起巨大的灾难。

关于这个话题的,值得一看的好文章:So, You Want to Suppress This Warning in Visual C++

23.自动计算字符串长度

下面的代码来自OpenSSL库。错误被PVS-Studio诊断为:V666 检查‘strncmp’的第三个参数。它可能和字符串的长度不一致,字符串长度取决于第二个参数。

if (!strncmp(vstart, "ASCII", 5))  
 arg->format = ASN1_GEN_FORMAT_ASCII; 
else if (!strncmp(vstart, "UTF8", 4))  
 arg->format = ASN1_GEN_FORMAT_UTF8; 
else if (!strncmp(vstart, "HEX", 3))   
  arg->format = ASN1_GEN_FORMAT_HEX; 
else if (!strncmp(vstart, "BITLIST", 3)) 
  arg->format = ASN1_GEN_FORMAT_BITLIST; 
else   
 ....


解释

很难停止使用魔数。而且没有理由不使用类似0, 1, -1, 10这些常数。更难的是,给这些常数命名,而且,它们会使阅读代码变得更复杂。

然而,减少使用魔数的个数是有用的。比如说,避免使用用来定义字符串长度的魔数就很有用。

让我们来看一下上面给出的代码。代码看上去很像是复制粘贴的。程序员复制了下面这一行:

else if (!strncmp(vstart, "HEX", 3))

之后,用"BITLIST"代替"HEX" ,但是程序员忘了把3改为7. 结果就是,字符串不是和"BITLIST"做比较,而是仅仅比较了"BIT"。这个错误似乎不太严重,但终究是个错误。

用复制粘贴来写代码真的很糟糕。更严重的是,字符串长度由魔数来定义。一直以来,我们遇到这样的错误,就是字符串长度和确定的数字不一致的错误,都是因为拼写错误或者程序员的疏忽。所以,这是一个典型的错误,我们需要做点什么来避免它。让我们来看看应该如何避免。

正确代码

乍一看,只要把strncmp()换成strcmp()就好了。那样魔数就消失了。

else if (!strcmp(vstart, "HEX"))

不好——我们改变了代码运作的逻辑。strncmp()的作用是检查一个字符串是否是以‘HEX’开始的,而strcmp()是检查两个字符串是否相等的。它们做的是不同的检查。

要解决这个问题最简单的方法就是改变常数:

else if (!strncmp(vstart, "BITLIST", 7))   arg->format = ASN1_GEN_FORMAT_BITLIST;

这个代码是正确的,但是魔数7还在那里,这就有点不好了。这也是为什么我要建议另一种方法。

建议

如果我们能够正确估计字符串的长度,这样的错误就能够避免。最简单的方法就是用strlen()函数。

else if (!strncmp(vstart, "BITLIST", strlen("BITLIST")))

在下面例子中,如果你忘记更改字符串,你能更快地发现它们的不匹配。

else if (!strncmp(vstart, "BITLIST", strlen("HEX")))

但是这个建议有两个缺点:

  1. 无法保证编译器会不会优化strlen()调用:用一个常数来代替它。

  2. 你要逐字复制字符串。看上去不好看,而且也会出错。

第一个问题可以在编译阶段用专门计算字符串长度的结构来解决。比如说,你可以用这样的宏:

#define StrLiteralLen(arg) ((sizeof(arg) / sizeof(arg[0])) - 1) ....
 else if (!strncmp(vstart, "BITLIST", StrLiteralLen("BITLIST")))

但这个宏有点危险。在重构的时候会出现下面的代码:

const char *StringA = "BITLIST"; 
if (!strncmp(vstart, StringA, StrLiteralLen(StringA)))

在这个例子中,StrLiteralLen宏会返回一些没意义的东西。取决于指针的大小(48字节),我们会得到37 。但是在C++中我们可以避免这种尴尬的局面,通过使用更复杂的技巧:

template <typename T, size_t N> 
char (&ArraySizeHelper(T (&array)[N]))[N];
 #define StrLiteralLen(str) (sizeof(ArraySizeHelper(str)) - 1)

现在,如果StrLiteralLen宏的参数是一个简单的指针,我们可能无法编译上面的代码。

让我们来看第二个问难题(复制字符串)。我不知道要对C程序员说什么。你可以为它写一个特别的宏,但是我个人不喜欢这样。我不热衷于宏。所以我知道要建议什么。

C++中,一切都很好。而且,我们可以用更聪明的法子来解决第一个问题。模板函数会帮我们很大的忙。你可以用不同的方法写,但是一般是长这样的:

template<typename T, size_t N> 
int mystrncmp(const T *a, const T (&b)[N])
{   
 return _tcsnccmp(a, b, N - 1); 
}

现在,字符串只用一次。在编译阶段就能计算字符串的长度。你不会遇到简单指针也不会错误的计算字符串的长度。完美!

总结:在处理字符串的时候避免使用魔数。使用宏或模板函数。这样函数不仅会变得更安全,而且会更漂亮更简短。

作为例子,你可以看strcpy_s ()的声明:

errno_t strcpy_s(  
  char *strDestination, 
  size_t numberOfElements, 
  const char *strSource  
  );
template <size_t size> 
errno_t strcpy_s(  
  char (&strDestination)[size],  
  const char *strSource 
 ); // C++ only

第一个是针对于C语言的,或者在不是提前知道缓冲区大小的情况的。如果我们要处理缓冲区,创建一个栈,这样我们就可以在C++中用第二个了。


原文链接:https://software.intel.com/en-us/articles/the-ultimate-question-of-programming-refactoring-and-everything
本文由 看雪翻译小组 lumou 编辑


第一部分:http://bbs.pediy.com/thread-215956.htm

第二部分:



原本是打算每十个发一次的,但是当比较多的时候调整结构我又比较烦,所以还是翻译得多少就传多少吧。。。

因为论文看不进,只好又翻译了(捂脸)。。。



[课程]FART 脱壳王!加量不加价!FART作者讲授!

收藏
免费 1
支持
分享
最新回复 (2)
雪    币: 44229
活跃值: (19955)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
2
辛苦了
2017-3-4 17:55
0
雪    币: 175
活跃值: (2331)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
写的很好,学习。
2017-3-4 18:00
0
游客
登录 | 注册 方可回帖
返回
//