鱼C论坛

 找回密码
 立即注册
查看: 5569|回复: 13

[技术交流] 你敢说自己懂 C 语言么?

[复制链接]
发表于 2016-7-22 15:44:38 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
这篇文章的目的是让每个程序员(特别是 C 程序员)说:我真的不懂 C。我想要让大家看到 C 语言的那些阴暗角落比我们想象中更近,甚至那些平常的代码中就包含着未定义的行为。

这篇文章设置了一系列的问题和答案。所有的例子都是从源代码中单独分离出来的。


1.



  1. extern void bar(void);
  2. void foo(int *x)
  3. {
  4.   int y = *x;  /* (1) */
  5.   if(!x)       /* (2) */
  6.   {
  7.     return;    /* (3) */
  8.   }
  9.   bar();
  10.   return;
  11. }
复制代码

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

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

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


2.



有这样一个函数:

  1. #define ZP_COUNT 10
  2. void func_original(int *xp, int *yp, int *zp)
  3. {
  4.   int i;
  5.   for(i = 0; i < ZP_COUNT; i++)
  6.   {
  7.     *zp++ = *xp + *yp;
  8.   }
  9. }
复制代码

有人想要按如下方式来优化它:

  1. void func_optimized(int *xp, int *yp, int *zp)
  2. {
  3.   int tmp = *xp + *yp;
  4.   int i;
  5.   for(i = 0; i < ZP_COUNT; i++)
  6.   {
  7.     *zp++ = tmp;
  8.   }
  9. }
复制代码

Q:调用原始的函数和调用优化后的函数,对于变量 zp 是否有可能获得不同的结果?

A:这是可能的,当 yp == zp 时结果就不同。


3.



  1. double f(double x)
  2. {
  3.   assert(x != 0.);
  4.   return 1. / x;
  5. }
复制代码

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

A:是的,这是可以的。通过传入一个非规范化的 x 的值,比如 1e-309.


4.



  1. int my_strlen(const char *x)
  2. {
  3.   int res = 0;
  4.   while(*x)
  5.   {
  6.     res++;
  7.     x++;
  8.   }
  9.   return res;
  10. }
复制代码

Q:上面提供的函数应该返回以空终止字符结尾的字符串长度,找出其中存在的一个 bug 。

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


5.



  1. #include <stdio.h>
  2. #include <string.h>
  3. int main()
  4. {
  5.   const char *str = "hello";
  6.   size_t length = strlen(str);
  7.   size_t i;
  8.   for(i = length - 1; i >= 0; i--)
  9.   {
  10.     putchar(str[i]);
  11.   }
  12.   putchar('n');
  13.   return 0;
  14. }
复制代码

Q:这个循环是死循环。这是为什么?

A:size_t 是无符号类型。 如果 i 是无符号类型, 那么 i >= 0 永远都是正确的。


6.



  1. #include <stdio.h>
  2. void f(int *i, long *l)
  3. {
  4.   printf("1. v=%ldn", *l); /* (1) */
  5.   *i = 11;                  /* (2) */
  6.   printf("2. v=%ldn", *l); /* (3) */
  7. }
  8. int main()
  9. {
  10.   long a = 10;
  11.   f((int *) &a, &a);
  12.   printf("3. v=%ldn", a);
  13.   return 0;
  14. }
复制代码

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

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

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

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


7.



  1. #include <stdio.h>
  2. int main()
  3. {
  4.   int array[] = { 0, 1, 2 };
  5.   printf("%d %d %dn", 10, (5, array[1, 2]), 10);
  6. }
复制代码

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

A:是的, 这里使用了逗号运算符。首先,逗号左边的参数被计算后丢弃,然后,右边的参数经过计算后被当做整个运算符的值使用,所以输出是 10 2 10。

注意在函数调用中的逗号符号(比如 f(a(), b()))并不是逗号运算符,因此也就不会保证运算的顺序,a() 和 b() 会以随机的顺序计算。

8.



  1. unsigned int add(unsigned int a, unsigned int b)
  2. {
  3.   return a + b;
  4. }
复制代码

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

A:对于无符号数的溢出结果是有定义的,结果是 2^(CHAR_BIT * sizeof(unsigned int)) ,所以函数 add 的结果是 0 。


9.



  1. int add(int a, int b)
  2. {
  3.   return a + b;
  4. }
复制代码

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

A:有符号整数的溢出结果是未定义的行为。


10.



  1. int neg(int a)
  2. {
  3.   return -a;
  4. }
复制代码

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

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


11.



  1. int div(int a, int b)
  2. {
  3.   assert(b != 0);
  4.   return a / b;
  5. }
复制代码

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

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

评分

参与人数 4荣誉 +23 鱼币 +23 贡献 +20 收起 理由
zltzlt + 5 + 5 + 5
灭·世 + 5 + 5 + 3 第一个例子就看的很纠结,水平不到家,唉。
拈花小仙 + 5 + 5 + 4 支持楼主!
小甲鱼 + 8 + 8 + 8 ~好文~

查看全部评分

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2016-7-22 15:46:59 | 显示全部楼层
c语言就是有一些奇怪的语法,纠结这个东西没什么用。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2016-8-13 10:48:46 | 显示全部楼层
学学
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2016-8-14 18:10:33 | 显示全部楼层
好文
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2016-8-17 22:55:15 | 显示全部楼层
长见识了。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2016-8-18 18:57:39 | 显示全部楼层
这种代码不必研究,真有程序猿写这种代码, 估计会被公司开除
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 0 反对 1

使用道具 举报

发表于 2016-10-13 10:48:41 | 显示全部楼层
楼主好给力
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2016-11-1 17:05:11 | 显示全部楼层
楼主是个有心人
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2016-11-2 13:58:36 | 显示全部楼层
看懂一点点,好尴尬
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2016-12-7 11:13:53 | 显示全部楼层
我就厉害了,我本来就不懂   
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2016-12-7 11:43:35 | 显示全部楼层
本帖最后由 hyj57555 于 2016-12-7 11:53 编辑
hyj57555 发表于 2016-12-7 11:13
我就厉害了,我本来就不懂


试了下第一题,定义了一个空指针,打印出空指针里面存储的数据是0000000000000000,所以!x是true,会return,但加上int y=*p;这句后,报出了停止工作的错误,之后的内容也没有执行。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2017-1-22 20:37:44 | 显示全部楼层
说实话  C要想学精通  不是简单的事   面向过程写个项目   结构有时候很乱   而且指针的奥秘有时候又是那么琢磨不透    好好加油吧
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 1 反对 0

使用道具 举报

发表于 2017-3-11 17:41:19 | 显示全部楼层
tttb 发表于 2017-1-22 20:37
说实话  C要想学精通  不是简单的事   面向过程写个项目   结构有时候很乱   而且指针的奥秘有时候又是那么 ...

不动
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2017-5-1 11:15:18 | 显示全部楼层
好文
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-24 02:06

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表