C语言陷阱解析--I Do Not Know C

博客原文 http://kukuruku.co/hub/programming/i-do-not-know-c
博主只做翻译修正和注释,12个问题后为博主自己补充

写这篇文章的意义在于让大家了解C语言中的暗点,即使那些平常的代码中也包含着未定义的行为。
一般这类容易混淆问题或关于编译器规则 ,或关于溢出等机器运行原理,所以要注意要注意规范编码和了解的运算原理,才能减少出错的概率。
1.

int i;
int i = 10;

Q:这段代码正确吗?是否会因为变量被定义了两次而导致错误的出现?代码源于同一个源码文件,而不是函数体或代码段的一部分。

A:这段代码是正确的。第一行是临时的定义,直到编译器处理了第二行的定义之后才成为正式的“定义”。
2.

extern void bar(void);
void foo(int *x)
{
  int y = *x;  /* (1) */
  if(!x)       /* (2) */
  {
    return;    /* (3) */
  }
  bar();
  return;
}

Q: 这样写的结果是即使 x 是空指针 bar() 函数都会被调用,并且程序不会崩溃。这是否是优化器的错误,或者全部是正确的?

A: 全部是正确的。
如果 x 是空指针,未定义的行为出现在(1) 处,但程序并不会在 (1) 处崩溃, 假如已经成功运行 (1)也不会试图在(2) 返回
让我们来探讨编译器遵循的规则,它都按如下的方式进行。
在对第 (1) 行的分析之后,编译器认为 x 不会是一个空指针
关键在此,于是第 (2) 行和 第 (3) 行就被认定为是没用的代码。变量 y 被当做没用的变量去除。从内存中读取的操作也会被去除,因为 *x 并不符合易变类型(volatile)。

这是无用的变量导致空指针检查失效的例子。

3.
函数如下

#define ZP_COUNT 10
void func_original(int *xp, int *yp, int *zp)
{
  int i;
  for(i = 0; i < ZP_COUNT; i++)
  {
    *zp++ = *xp + *yp;
  }
}

有人试图按以下方法优化

void func_optimized(int *xp, int *yp, int *zp)
{
  int tmp = *xp + *yp;
  int i;
  for(i = 0; i < ZP_COUNT; i++)
  {
    *zp++ = tmp;
  }
}

Q:调用原始的函数和调用优化后的函数,对于变量 zp 是否有可能获得不同的结果?
A:这是可能的,当 yp == zp 时结果就不同。

4.

double f(double x)
{
  assert(x != 0.0);
  return 1.0 / x;
}

Q: 这个函数是否可能返回最大下界(inf) ?假设浮点数运算是按照IEEE 754 标准(大部分机器遵循)执行的, 并且断言语句是可用的(NDEBUG 并没有被定义)

A通过传入一个非规范化的 x 的值,比如 1e-309,可以返回
5.

int my_strlen(const char *x)
{
  int res = 0;
  while(*x)
  {
    res++;
    x++;
  }
  return res;
}

Q: 上面提供的函数应该返回以空终止字符结尾的字符串长度,请找bug 。

A:使用 int 类型来存储对象的大小是错误的,因为无法保证 int 类型能够存下任何对象的大小,应该使用 size_t
6.

#include <stdio.h>
#include <string.h>
int main()
{
  const char *str = "hello";
  size_t length = strlen(str);
  size_t i;
  for(i = length - 1; i >= 0; i--)
  {
    putchar(str[i]);
  }
  putchar('\n');
  return 0;
}

Q: 为什么是死循环?

A: size_t 是无符号类型。 i 可能是无符号类型, 那么 i >= 0 永远都是正确的。
7.

#include <stdio.h>
void f(int *i, long *l)
{
  printf("1. v=%ld\n", *l); /* (1) */
  *i = 11;                  /* (2) */
  printf("2. v=%ld\n", *l); /* (3) */
}
int main()
{
  long a = 10;
  f((int *) &a, &a);
  printf("3. v=%ld\n", a);
  return 0;
}

这个程序分别用两个不同的编译器编译并且在一台小字节序的机器上运行。获得了如下两种不同的结果:

   1. v=10    2. v=11    3. v=11
   2. v=10    2. v=10    3. v=11

Q:你如何解释第二种结果?

A:所给程序存在未定义的行为。程序违反了编译器的强重叠规则(strict aliasing)。
虽然 int 在第 (2) 行被改变了,但是编译器可以假设任何的 long 都没有改变。因此,编译器传递在第一行的执行过程中被读取到的相同 long (第(3)行)。
应注意到,我们不能间接引用那些和其他不兼容类型指针相重名的指针。
8.

#include <stdio.h>
int main()
{
  int array[] = { 0, 1, 2 };
  printf("%d %d %d\n", 10, (5, array[1, 2]), 10);
}

Q: 这个代码是否是正确的?如果不存在未定义行为,那么它会输出什么?

A: 代码正确, 这里使用了逗号运算符。首先,逗号左边的参数被计算后丢弃,然后,右边的参数经过计算后被当做整个运算符的值使用,所以输出是 10 2 10。
注意:在函数调用中的逗号符号(比如 f(a(), b()))并不是逗号运算符,因此也就不会保证运算的顺序,a() 和 b() 会以随机的顺序计算。
9.

unsigned int add(unsigned int a, unsigned int b)
{
  return a + b;
}

Q: 函数 add(UINT_MAX, 1) 的结果是什么?

A:对于无符号数的溢出结果是有定义的,结果是 2^(CHAR_BIT * sizeof(unsigned int)) ,所以函数 add 的结果是 0
补充:所有无符号数运算都是以2的n次方为模(n是结果中的位数)。当它超过范围时,从零开始重新计数!当一个无符号数和有符号数相加的时候,有符号数会自动转化为无符号数参与运算!有符号数运算 是可能发生“溢出”的,而且“溢出”的结果不固定。相关的符号数基础知识可搜索看“编程范式”的公开课

10.

int add(int a, int b)
{
  return a + b;
}

Q:函数 add(INT_MAX, 1) 的结果是什么?

A:有符号整数的溢出结果是未定义的行为,无法知道。
11.

int neg(int a)
{
  return -a;
}

Q:这里是否可能出现未定义的行为?如果是的话,是在输入什么参数时发生的?

A:neg(INT_MIN)。如果 ECM 用附加码(补码)表示负整数, 那么 INT_MIN 的绝对值比 INT_MAX 的绝对值大一。在这种情况下,-INT_MIN 造成了有符号整数的溢出,这是一种未定义的行为。
12.

int div(int a, int b)
{
  assert(b != 0);
  return a / b;
}

Q:这里是否可能出现未定义的行为?如果是的话,是在什么参数上发生的?

A:如果 ECM 用附加码表示负数, 那么 div(INT_MIN, -1) 导致了与上一个例子相同的问题。

—English version Written by Dmitri Gribenko Abovegribozavr@gmail.com

13.
Q:

char *p,a='5';
p=&a;                     //(1)正确
p="abcd";              //(2)
char *p = “hello”;      //(3)
char a[10];
a = “hello”   //(4)为什么不正确?
const char* ptr = "abc"//(5)不用const行吗

A:第2行中,双引号表示将返回的地址 赋值给了 p
第3行中,正确的定义行为。可以认为是编译器规则。
第4行中,可以看到编译器会提示,左边不是可以修改的左值。这就是数组的指针所指向的值跟普通指针指向的值不同的地方。由于数组指针指向的值是常量,所以这样是不正确的。
由此可想到,在使用指针的时候,指针可以自增,而数组不能自增。
第5行中,这里的”abc”当成常量并把它放到程序的常量区是编译器
最合适的选择。;这样如果后面写ptr[0] = ‘x’的 话编译器就不会让它编译通过,避免了运行异常。
14.
Q:

#define ABC(x) x*x
#include "stdio.h"
int main()
{
    int a=3,b;
    b=ABC(a+1);
    printf("b=%d", b);
}

输出是b=a+1*a+1=7,原因是宏只是简单替换,不包括括号
如果改为b=ABC((a+1));,则输出结果为16

  1. 其他陷阱总结
    (1). int a = 0101;//代表8进制数
    (2). int i = ‘ab’;//i=97*256+98=24930
    (3). int *g(),(*h)();//g是一个返回值类型为指向整形的指针的函数,h是一个函数指针,其指向的函数的返回值是int型
    (4). buf++ = c;//先++后
    (5). a[i]==(a+i)==(i+a)==i[a]
    i[4][7]==(i[4]+7)==(*(i+4)+7)
    (6). y[i++] = x[i]的行为不确定,根据编译器而异
    (7).为字符串malloc内存时,要比原句子多申请一个字节(因为字符串以’\0’结尾)
    (8).strcpy(buf, “1234567890ab ”“cdefg “);
    等同于:
    strcpy(buf, “1234567890abcdefg “);
    (9)余数的符号是和被除数的符号相同,即(-10)%3==-1; 10%(-3)==1;
    (10)%o以八进制数形式输出整数
    %x以十六进制数形式输出整数
    %e以指数形式输出实数,
    %g根据大小自动选f格式或e格式,且不输出无意义的零。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值