翻译《有关编程、重构及其他的终极问题?》——23.自动获取字符串的长度

翻译《有关编程、重构及其他的终极问题?》——23.自动获取字符串的长度

标签(空格分隔): 翻译 技术 C/C++
作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
校验者:W1U02
最后更新:2017年04月14日


23.自动获取字符串的长度

下面这段代码摘自OpenSSL库。PVS-Studio诊断的错误说明为: V666 Consider inspecting the third argument of the function ‘strncmp’. It is possible that the value does not correspond with the length of a string which was passed with the second argument(译者注:大意是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))

但在“HEX”被“BITLIST”代替的时候,程序员忘了把3改成7,结果变成了让字符串和“BIT”而非“BITLIST”进行比较。这个错误也许不会引起太大的问题,但依旧是一个错误。

使用复制黏贴生成的代码真的不太好,更坏的是其中的字符串长度还使用的是神奇常量数字。一次又一次的,我们经常碰到这些错误——因为粗心的程序员导致字符串实际长度和设置的数字不符。下面让我们更进一步的看看如何避免这些错误。

正确的代码
一开始看上去似乎使用strcmp()替换strncmp()就好了,然后那些不和琢磨的常量数字就消失了:

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

其实糟糕的是——我们已经改变了代码的原来逻辑。这里我们用strncmp()检查字符串是否以“HEX”开头,但strcmp()检查的实际是两个字符串是否相等。这显而易见是两种不同的检查。

最简单的修正是改变常量数字为正确的值:

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

上面这个代码是正确的,非常遗憾的是神奇的7还在哪里,这也是我建议另外其他方法的原因。

扫描二维码关注公众号,回复: 3311612 查看本文章

建议
类似的错误可以通过在代码中明确的计算字符串长度来避免。最简单的选择就是使用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宏不会返回有意义的东西,实际上根据实际指针的长度(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语言上,或者用在buffer的尺寸不能提前获知这种个情况下。如果我们使用在栈上创建的buffer,那么我们能在C++中使用第二种变体:

char str[BUF_SIZE];
strcpy_s(str, "foo");

这样就没有神奇数字了,也根本不用去计算buffer的大小,短而且甜蜜的。

猜你喜欢

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