翻译《有关编程、重构及其他的终极问题?》——34.容易被你忽视的未定义行为

翻译《有关编程、重构及其他的终极问题?》——34.容易被你忽视的未定义行为

标签(空格分隔):翻译 技术 C/C++
作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
最后更新:2018年07月18日


34.容易被你忽视的未定义行为

这次我很难从实际应用程序中给出一个例子。不过,我还是很频繁的看到那些疑似会导致以下问题的代码。这次这里要说的错误很可能在使用大尺寸数组时发生,所以我无法确切知道那些项目会使用这么大尺寸的数组(译者注:很多项目在使用大数组的时候,其实是在内存堆上动态分配的,所以只静态的看代码,是无法直接知道其具体大小的)。我们不怎么聊到64位系统的错误(译者注:之前的错误大多数是32位的),所以这次的例子是人为设计的。

让我们来看看下面这个合成的代码示例:

size_t Count = 1024*1024*1024; // 1 Gb
if (is64bit)
  Count *= 5; // 5 Gb
char *array = (char *)malloc(Count);
memset(array, 0, Count);

int index = 0;
for (size_t i = 0; i != Count; i++)
  array[index++] = char(i) | 1;

if (array[Count - 1] == 0)
  printf("The last array element contains 0.\n");

free(array);

解释

上面的代码在32位版本的程序上可以正常工作,但如果我们在64位版本上编译它,情况就变得更复杂了。

这个代码的64位版本的程序分配了5G内存,并且给它初始化为0。随后用一个循环修改它,给它赋予非零值(使用“|1”来保证)。

如果你运行这个程序的可调式版本,因为index越界了,所以会导致程序崩溃。好像可以解释为:是index变量溢出了,它的值将会变为-2147483648(INT_MIN)。

听上去符合逻辑,真的吗?根本不是!这其实是一种未定义行为,并且可能发生任何事。

要挖掘更多相关信息,我建议大家打开下面这些链接阅读:
- 整型溢出
- 理解C/C++中的整型溢出
- 在C++中,带符号整型溢出依旧是未定义行为吗?

有趣的是,每当我或者别的什么人提到这个例子的未定义行为时,总有人开始啰里啰唆的反驳(作者注:从前一篇和这篇看,作者估计被很多人怼过,所以这么在意,感觉还是比较生气的)。我不知道这是为什么,但这感觉好像他们假设自己知道了C++的一切,而且包括编译器如何工作。

但其实他们不是真的了解这一切。如果他们真的了解,他么就不会说出这些话(一群人的观点):

这是一种没有意义的理论,嗯,是的,整型溢出的确会导致未定义行为,但这个行为除了有些模糊外没什么大不了的。实际上,我们一直都能知道我们将会得到什么。如果你从1加到INT_MAX,再继续你会得到INT_MIN。也许在这个宇宙里有些未知体系架构编译器,但至少在我的Visual C++/GCC编译体系中我会得到一个不正确的结果。

然而现在没有任何魔法,我提供了使用简单代码的例子来演示这个未定义行为,也没有使用什么稀有的架构体系,仅仅是一个64位程序。

上面的代码足够编译一个Release版本,并且能够运行。这个程序在运行时会停止并且崩溃,而那个警告“the last array element contains 0”将不会打印。

对应的未定义行为是通过如下方式被揭开。这个数组被彻底的填充了,无论整型的index变量是否足够大到便利这个数组的所有元素。那些还是不相信我的人,可以看下下面的会变代码:

  int index = 0;
  for (size_t i = 0; i != Count; i++)
000000013F6D102D  xor         ecx,ecx  
000000013F6D102F  nop  
    array[index++] = char(i) | 1;
000000013F6D1030  movzx       edx,cl  
000000013F6D1033  or          dl,1  
000000013F6D1036  mov         byte ptr [rcx+rbx],dl  
000000013F6D1039  inc         rcx  
000000013F6D103C  cmp         rcx,rdi  
000000013F6D103F  jne         main+30h (013F6D1030h)

看到未定义的行为了吧!并且这里没有什么非典型编译器被使用,就是用的VS2015。

如果你使用无符号整型代替有符号振兴,那么这个未定义行为将会消失。这个数组仅仅大部分被填充,最后我们会得到一个消息——“the last array element contains 0”。

下面是使用无符号整型后的汇编代码:

  unsigned index = 0;
000000013F07102D  xor         r9d,r9d  
  for (size_t i = 0; i != Count; i++)
000000013F071030  mov         ecx,r9d  
000000013F071033  nop         dword ptr [rax]  
000000013F071037  nop         word ptr [rax+rax]  
    array[index++] = char(i) | 1;
000000013F071040  movzx       r8d,cl  
000000013F071044  mov         edx,r9d  
000000013F071047  or          r8b,1  
000000013F07104B  inc         r9d  
000000013F07104E  inc         rcx  
000000013F071051  mov         byte ptr [rdx+rbx],r8b  
000000013F071055  cmp         rcx,rdi  
000000013F071058  jne         main+40h (013F071040h)

正确的代码

为了让程序运行正常你必须使用合适的数据类型。如果你要使用一个大尺寸的数组,请忘掉有符号和无符号整型。合适的类型应该是:ptrdiff_tintptr_tsize_tDWORD_PTRstd::vector::size_type等。在这个例子中,我们使用size_t

size_t index = 0;
for (size_t i = 0; i != Count; i++)
  array[index++] = char(i) | 1;

建议

如果你碰到了C/C++语言的未定义行为,不要去做什么争论,或去预测它们将会怎么行为。正确的方式应该是避免写导致如此的危险代码。

有很多顽固的程序员选择忽视任何可疑的负数转换、比较this和null或者有符号类型的溢出。

不要那么做,事实上,程序现在工作完好不意味着真的没有问题。前面展示的那种未定义行为是不可预知的。预期的程序行为只是未定义行为的一个变体。

猜你喜欢

转载自blog.csdn.net/headman/article/details/81093910
今日推荐