前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >校长讲堂第九讲

校长讲堂第九讲

作者头像
一头小山猪
发布2020-04-10 10:30:34
5280
发布2020-04-10 10:30:34
举报
文章被收录于专栏:微光点亮星辰微光点亮星辰

语义“陷阱”

一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。 我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的 C 实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到以后讨论可以执行问题为止。

3.3 C 并不总是转换实参

下面的程序段由于两个原因会失败: double s; s = sqrt(2); printf("%g\n", s); 第一个原因是 sqrt()需要一个 double 值作为它的参数,但没有得到。第二个原因是它返回一个double 值但没有这样声名。改正的方法只有一个: double s, sqrt(); s = sqrt(2.0); printf("%g\n", s); C 中有两个简单的规则控制着函数参数的转换:(1)比 int 短的整型被转换为 int;(2)比 double短的浮点类型被转换为 double。所有的其它值不被转换。确保函数参数类型的正确行使程序员的责任。 因此,一个程序员如果想使用如 sqrt()这样接受一个 double 类型参数的函数,就必须仅传递给它float 或 double 类型的参数。常数 2 是一个 int,因此其类型是错误的。 当一个函数的值被用在表达式中时,其值会被自动地转换为适当的类型。然而,为了完成这个自动转换,编译器必须知道该函数实际返回的类型。没有更进一步声名的函数被假设返回 int,因此声名这样的函数并不是必须的。然而,sqrt()返回 double,因此在成功使用它之前必须要声名。 实际上,C 实现通常允许一个文件包含 include 语句来包含如 sqrt()这些库函数的声名,但是对那 些自己写函数的程序员来说,书写声名也是必要的——或者说,对那些书写非凡的 C 程序的人来说是有必 要的。 这里有一个更加壮观的例子: main() { int i; char c; for(i = 0; i < 5; i++) { scanf("%d", &c); printf("%d", i); } printf("\n"); } 表面上看,这个程序从标准输入中读取五个整数并向标准输出写入 0 1 2 3 4。实际上,它并不总是这么做。譬如在一些编译器中,它的输出为 0 0 0 0 0 1 2 3 4。 为什么?因为 c 的声名是 char 而不是 int。当你令 scanf()去读取一个整数时,它需要一个指向一个整数的指针。但这里它得到的是一个字符的指针。但 scanf()并不知道它没有得到它所需要的:它将输入看作是一个指向整数的指针并将一个整数存贮到那里。由于整数占用比字符更多的内存,这样做会影响到 c 附近的内存。 c 附近确切是什么是编译器的事;在这种情况下这有可能是 i 的低位。因此,每当向 c 中读入一个值,i 就被置零。当程序最后到达文件结尾时,scanf()不再尝试向 c 中放入新值,i 才可以正常地增长,直到循环结束。

3.4 指针不是数组

C 程序通常将一个字符串转换为一个以空字符结尾的字符数组。 假设我们有两个这样的字符串 s 和 t,并且我们想要将它们连接为一个单独的字符串 r。我们通常使用库函数 strcpy()和 strcat()来完成。 下面这种明显的方法并不会工作: char *r; strcpy(r, s); strcat(r, t); 这是因为 r 没有被初始化为指向任何地方。尽管 r 可能潜在地表示某一块内存,但这并不存在,直到你分配它。 让我们再试试,为 r 分配一些内存: char r[100]; strcpy(r, s); strcat(r, t); 这只有在 s 和 t 所指向的字符串不很大的时候才能够工作。不幸的是,C 要求我们为数组指定的大小是一个常数,因此无法确定 r 是否足够大。然而,很多 C 实现带有一个叫做 malloc()的库函数,它接受一个数字并分配这么多的内存。通常还有一个函数成为 strlen(),可以告诉我们一个字符串中有多少个字符: 因此,我们可以写: char *r, *malloc(); r = malloc(strlen(s) + strlen(t)); strcpy(r, s); strcat(r, t); 然而这个例子会因为两个原因而失败。首先,malloc()可能会耗尽内存,而这个事件仅通过静静地返回一个空指针来表示。 其次,更重要的是,malloc()并没有分配足够的内存。一个字符串是以一个空字符结束的。而strlen()函数返回其字符串参数中所包含字符的数量,但不包括结尾的空字符。因此,如果 strlen(s)是 n,则 s 需要 n + 1 个字符来盛放它。因此我们需要为 r 分配额外的一个字符。再加上检查 malloc()是否成功,我们得到: char *r, *malloc(); r = malloc(strlen(s) + strlen(t) + 1); if(!r) { complain(); exit(1); } strcpy(r, s); strcat(r, t);

3.5 避免提喻法

提喻法(Synecdoche, sin-ECK-duh-key)是一种文学手法,有点类似于明喻或暗喻,在牛津英文词典中解释如下:“a more comprehensive term is used for a less comprehensive or vice versa;as whole for part or part for whole, genus for species or species for genus, etc.(将全面的单位用作不全面的单位,或反之;如整体对局部或局部对整体、一般对特殊或特殊对一般,等等。)” 这可以精确地描述 C 中通常将指针误以为是其指向的数据的错误。正将常会在字符串中发生。例如: char *p, *q; p = "xyz"; 尽管认为 p 的值是 xyz 有时是有用的,但这并不是真的,理解这一点非常重要。p 的值是指向一个有四个字符的数组中第 0 个元素的指针,这四个字符是'x'、'y'、'z'和'\0'。因此,如果我们现在执行: q = p; p 和 q 会指向同一块内存。内存中的字符没有因为赋值而被复制。这种情况看起来是这样的:要记住的是,复制一个指针并不能复制它所指向的东西。因此,如果之后我们执行: q[1] = 'Y'; q 所指向的内存包含字符串 xYz。p 也是,因为 p 和 q 指向相同的内存。

3.6 空指针不是空字符串

将一个整数转换为一个指针的结果是实现相关的(implementation-dependent),除了一个例外。这个例外是常数 0,它可以保证被转换为一个与其它任何有效指针都不相等的指针。这个值通常类似这样定义: #define NULL 0 但其效果是相同的。要记住的一个重要的事情是,当用 0 作为指针时它决不能被解除引用。换句话说,当你将 0 赋给一个指针变量后,你就不能访问它所指向的内存。不能这样写: if(p == (char *)0) ... 也不能这样写: if(strcmp(p, (char *)0) == 0) ... 因为 strcmp()总是通过其参数来查看内存地址的。 如果 p 是一个空指针,这样写也是无效的: printf(p); 或 printf("%s", p);

3.7 整数溢出

C 语言关于整数操作的上溢或下溢定义得非常明确。 只要有一次操作数是无符号的,结果就是无符号的,并且以 2n为模,其中 n 为字长。如果两个操作 数都是带符号的,则结果是未定义的。 例如,假设 a 和 b 是两个非负整型变量,你希望测试 a + b 是否溢出。一个明显的办法是这样的: if(a + b < 0) complain(); 通常,这是不会工作的。 一旦 a + b 发生了溢出,对于结果的任何赌注都是没有意义的。例如,在某些机器上,一个加法运算会将一个内部寄存器设置为四种状态:正、负、零或溢出。 在这样的机器上,编译器有权将上面的例子实现为首先将 a 和 b 加在一起,然后检查内部寄存器状态是否为负。如果该运算溢出,内部寄存器将处于溢出状态,这个测试会失败。 使这个特殊的测试能够成功的一个正确的方法是依赖于无符号算术的良好定义,既要在有符号和无符 号之间进行转换: if((int)((unsigned)a + (unsigned)b) < 0) complain();

3.8 移位运算符

两个原因会令使用移位运算符的人感到烦恼: 1. 在右移运算中,空出的位是用 0 填充还是用符号位填充? 2. 移位的数量允许使用哪些数? 第一个问题的答案很简单,但有时是实现相关的。如果要进行移位的操作数是无符号的,会移入 0。如果操作数是带符号的, 则实现有权决定是移入 0 还是移入符号位。如果在一个右移操作中你很关心空位,那么用 unsigned 来声明变量。这样你就有权假设空位被设置为 0。 第二个问题的答案同样简单:如果待移位的数长度为 n,则移位的数量必须大于等于 0 并且严格地小于 n。因此,在一次单独的操作中不可能将所有的位从变量中移出。 例如,如果一个 int 是 32 位,且 n 是一个 int,写 n << 31 和 n << 0 是合法的,但 n << 32 和 n << -1 是不合法的。 注意,即使实现将符号为移入空位,对一个带符号整数的右移运算和除以 2 的某次幂也不是等价的。 为了证明这一点,考虑(-1) >> 1 的值,这是不可能为 0 的。[译注:(-1) / 2 的结果是 0。]

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2016-10-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微光点亮星辰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 语义“陷阱”
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档