看完这些问题后,你还会说自己懂 C 语言么?

本文由 伯乐在线 - xxmen 翻译,Alick 校稿。未经许可,禁止转载!
英文出处:Dmitri Gribenko。欢迎加入翻译组

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

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

1.


1

2

int i;

int i = 10;

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

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

2.


1

2

3

4

5

6

7

8

9

10

11

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) 行崩溃, 也不会试图在第 (2) 行返回假如已经成功运行第 (1) 行。让我们来探讨编译器遵循的规则,它都按如下的方式进行。在对第 (1) 行的分析之后,编译器认为 x 不会是一个空指针,于是第 (2) 行和 第 (3) 行就被认定为是没用的代码。变量 y 被当做没用的变量去除。从内存中读取的操作也会被去除,因为 *x 并不符合易变类型(volatile)。

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

3.有这样一个函数:


1

2

3

4

5

6

7

8

9

#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;

  }

}

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


1

2

3

4

5

6

7

8

9

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.


1

2

3

4

5

double f(double x)

{

  assert(x != 0.);

  return 1. / x;

}

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

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

5.


1

2

3

4

5

6

7

8

9

10

int my_strlen(const char *x)

{

  int res = 0;

  while(*x)

  {

    res++;

    x++;

  }

  return res;

}

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

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

6.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

#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.


1

2

3

4

5

6

7

8

9

10

11

12

13

14

#include <stdio.h>

void f(int *i, long *l)

{

  printf("1. v=%ldn", *l); /* (1) */

  *i = 11;                  /* (2) */

  printf("2. v=%ldn", *l); /* (3) */

}

int main()

{

  long a = 10;

  f((int *) &a, &a);

  printf("3. v=%ldn", a);

  return 0;

}

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


1

2

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)行)的原因。

8.


1

2

3

4

5

6

#include <stdio.h>

int main()

{

  int array[] = { 0, 1, 2 };

  printf("%d %d %dn", 10, (5, array[1, 2]), 10);

}

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

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

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

9.


1

2

3

4

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 。

10.


1

2

3

4

int add(int a, int b)

{

  return a + b;

}

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

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

11.


1

2

3

4

int neg(int a)

{

  return -a;

}

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

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

12.


1

2

3

4

5

int div(int a, int b)

{

  assert(b != 0);

  return a / b;

}

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

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

全能程序员交流QQ群290551701,群内程序员都是来自,百度、阿里、京东、小米、去哪儿、饿了吗、蓝港等高级程序员 ,拥有丰富的经验。加入我们,直线沟通技术大牛,最佳的学习环境,了解业内的一手的资讯。如果你想结实大牛,那 就加入进来,让大牛带你超神!

时间: 2024-11-06 09:28:59

看完这些问题后,你还会说自己懂 C 语言么?的相关文章

ae模板怎么套用?看完这篇ae模板套用教程你就懂了

首先我们要知道,模板,是什么?模板就是别人做好了的工程文件,所以东西都已经完成,不需要任何效果方面的修改了.那么ae模板怎么套用?就是你用的时候,替换.修改或者删去某些东西,变成自己的视频.比如片头LOGO模板,你肯定得要有自己的LOGO,去替换模板里的LOGO,然后渲染成片,才会成你的片头:看完这篇ae模板套用教程你就懂了. https://www.macdown.com 本站提供了海量AE模板,我们选择片头LOGO模板,做你自己的片头. 首先下载完后,打开这个模板. 然后看一眼项目栏 我用的

Sharding JDBC如何分库分表?看完你就会了

Sharding JDBC的操作分为配置使用.读写分离.分库分表以及应用等,今天我们主要来了解一下关于分库分表的操作,如果你对此感兴趣的话,那我们就开始吧. 环境准备 pom.xml <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE

一个优秀团队是怎么散伙的?看完后感受很深!

那天,靠近河边的丛林突起大火,并迅速蔓延到整个绿地.聪明的狮子和猫头鹰见势不妙,不是跑了就是飞了:无情的大火吞噬着蟑螂.蜘蛛,以及飞不远的苍蝇和蝉,而且一群蚂蚁也被大火包围. 来自四面八方的蚂蚁突然迅速聚集,抱成一团向河边滚去.外层的蚂蚁被活活烧死,但直到烧焦它们也不离开自己的岗位,一直坚守到冲向河流.河面泛起一大片被烧焦了的蚂蚁,但大部分蚂蚁却因此获救. 看着蚂蚁如此惨烈的壮举,返回的狮子有些感动.它认为,蚂蚁世界里来自群体成员中精诚合作的"快捷且难以抗拒的"力量,是蚂蚁战胜自然及众

看完这个你还不理解右值引用和移动构造 你就可以来咬我(上)

共分三篇,这是第一篇.另外两篇,看完这个你还不理解右值引用和移动构造 你就可以来咬我(中),看完这个你还不理解右值引用和移动构造 你就可以来咬我(下). C++ 右值引用 & 新特性 C++ 11中引入的一个非常重要的概念就是右值引用.理解右值引用是学习"移动语义"(move semantics)的基础.而要理解右值引用,就必须先区分左值与右值. 对左值和右值的一个最常见的误解是:等号左边的就是左值,等号右边的就是右值.左值和右值都是针对表达式而言的,左值是指表达式结束后依然存

[经验] 电脑维修(看完后就可以开一家自己的电脑维修店!)

[经验] 电脑维修(看完后就可以开一家自己的电脑维修店!) 第一部分 总则 第一章 电脑维修的基本原则和方法 第二章 电脑维修步骤与维修操作注意事项 第二部分 常见故障判断 第一章 加电类故障 第二章 启动与关闭类故障 第三章 磁盘类故障 第四章 显示类故障 第五章 安装类故障 第六章 操作与应用类故障 第七章 局域网类故障 第八章 Internet类故障 第九章 端口与外设故障 第十章 音视频类故障 第十一章 兼容类故障 第三部分 附录 硬盘基本知识 挽救硬盘的几个方法 硬盘逻辑锁巧解 WIN

看完此文,妈妈还会担心你docker入不了门?

??上周对象突然心血来潮说想养个小宠物,我问想养啥她又说随便,你看着办!!!这我真的比较难办啊!但是咱们程序员能有个对象就不错了,还不赶紧宠着,我只能照办咯! ??我去到了一家宠物店,半天也没有找到合适的目标.正在我犹豫彷徨之时,看到了老板门口鱼缸里面的金鱼游来游去还挺顺眼!于是我问老板 ??我:老板,金鱼多少钱? ??老板:加鱼缸一起100块钱不讲价! ??我:这...便宜一点咯! ??老板:小伙子看你骨骼惊奇,定是个养鱼的奇才,2块钱卖给你吧!但是鱼缸可不能给你! ??我:那,你帮我打包一条

看完这篇你还敢说,不懂Spring中的IoC容器?

一. 什么是IoC 什么是耦合和内聚 耦合指的就是模块之间的依赖关系.模块间的依赖越多,则表示耦合度越高,相应的维护成本就越高.内聚指的是模块内功能之间的联系.模块内功能的联系越紧密,则表示内聚度越高,模块的职责也就越单一.所以在程序开发中应该尽量的降低耦合,提高内聚.也就是设计原则中的开闭原则和单一职责原则. 工厂模式 工厂模式就是用来解决程序间耦合的一种设计模式.可以把所有要创建的对象放在工厂的一个集合里,当需要使用这个对象的时候,直接从工厂里面取出来用就行. 工厂模式的优点: 一个调用者想

【转】“数学怪才”陈景润:唯一的儿子今何在?看完后你会佩服他!

“数学怪才”陈景润:唯一的儿子今何在?看完后你会佩服他! 文|历史回炉 有这么一个人,世界数学大师说他所做的每一项工作都像是在喜马拉雅山顶上散步.这很危险,但是一旦他成功了,肯定会影响世界.邓小平曾经说过,如果中国有一千个像他这样的人,那就太好了!这个人就是震撼世界的中国“数学怪才”——陈景润.那么“数学怪才”陈景润:唯一的儿子今何在?看完后你会佩服他! 陈景润出生在福州一个小镇的贫困家庭,他母亲生他的时候没有奶水,还是借邻居的米汤喂他.当他要去上学时,作为邮局一个职员,他父亲的薪水少的可怜,而

原地打方向原来很伤车?看完还敢吗?

原地打方向原来很伤车?看完还敢吗? http://auto.sohu.com/20160818/n465238431.shtml 经常有不少朋友在车辆掉头时,为了省事也好,或者是为了减小车辆的转弯半径,习惯性的原地打方向,长此以往,就养成坏习惯,其实这种的做法是很伤车的. 原地打方向对车辆都有什么样的伤害? 一 影响轮胎寿命 原地打方向时由于车轮没有转动,尤其是前轮在承受着车身重量的同时还要移动转向,这无疑将增加对轮胎表面的磨损,减少轮胎使用寿命.轮胎磨损,受力不均匀,就造成轮胎胎面受损程度不同