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类型的值。也就是说,它可以返回0到255的一个数,或者是-1(EOF)。
使用扩展字符集(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")))
但是这个建议有两个缺点:
无法保证编译器会不会优化strlen()调用:用一个常数来代替它。
你要逐字复制字符串。看上去不好看,而且也会出错。
第一个问题可以在编译阶段用专门计算字符串长度的结构来解决。比如说,你可以用这样的宏:
#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宏会返回一些没意义的东西。取决于指针的大小(4或8字节),我们会得到3或7 。但是在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
第二部分:
原本是打算每十个发一次的,但是当比较多的时候调整结构我又比较烦,所以还是翻译得多少就传多少吧。。。
因为论文看不进,只好又翻译了(捂脸)。。。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法