校长讲堂第九讲
- 2020 年 4 月 10 日
- 笔记
语义“陷阱”
一个句子可以是精确拼写的并且没有语法错误,但仍然没有意义。在这一节中,我们将会看到一些程序的写法会使得它们看起来是一个意思,但实际上是另一种完全不同的意思。 我们还要讨论一些表面上看起来合理但实际上会产生未定义结果的环境。我们这里讨论的东西并不保证能够在所有的 C 实现中工作。我们暂且忘记这些能够在一些实现中工作但可能不能在另一些实现中工作的东西,直到以后讨论可以执行问题为止。
3.3 C 并不总是转换实参
下面的程序段由于两个原因会失败: double s; s = sqrt(2); printf("%gn", s); 第一个原因是 sqrt()需要一个 double 值作为它的参数,但没有得到。第二个原因是它返回一个double 值但没有这样声名。改正的方法只有一个: double s, sqrt(); s = sqrt(2.0); printf("%gn", 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'和'