练习1.1
某些C编译器允许嵌套注释。请写一个程序,要求:无论是对允许嵌套注释的编译器还是对不允许的嵌套注释的编译器,该程序都能正常通过编译(无错误消息出现),但是这两种情况下程序执行的结果却不同。
/* /* */
对于一个允许嵌套注释的C编译器,无论上面的符号序列后面跟什么,都属于注释的一部分;而对于不允许嵌套注释的C编译器,后面跟的就是实实在在的代码内容。也许有人因此想到可以在后面跟一个用一对引号引起的注释结束符:
/* /* */ " */ "
如果允许嵌套注释,上面的符号序列就等于一个引号;如果不允许嵌套注释,那么就等于一个字符串"*/"。因此可以接着在后面跟一个注释开始符以及一个引号:
/* /* */ " */ " /*"
如果允许嵌套注释,上面就等于用一对引号引起的注释开始符"/*";如果不允许嵌套注释那么就等于一个用引号括起来的注释结束符,后跟一段未结束的注释。我们可以简单让最后的注释结束:
/* /* */ " */ " /*" /* */
这样,如果允许嵌套注释,上面的表达式就等效于"*/";如果不允许那么就等效于"/*"。
下面这个拍案叫绝的解法:
/* /* /0 */ * */ 1
这样,如果允许嵌套注释上面符号序列中两个/*符号与两个*/符号正好匹配,所以上式的值是1。如果不允许嵌套注释,注释中的/*将被忽略。因此,即使是/出现在注释中也没有特殊的含义,上面的表达式因此将被这样解释:
/* / */ 0* /* */ 1
它的值就是 0 * 1,也就是0。
练习 1.3
为什么 n-->0 的含义是 n-- >0而不是n- ->0?
根据“大嘴法”规则,在编译器读入 > 之前,就已经将 -- 作为单个符号了。
大嘴发(贪心法)规则定义:每一个符号应该包含尽可能多的字符。也就是说,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已经不再可能组成一个有意义的符号。
注意:除了字符串与字符常量,符号的中间不能嵌有空白(空白符,制表符,换行符)。
练习1.4
a+++++b的含义是什么?
上式惟一有意义的解析方式是: a ++ + ++ b
可是,我们注意到,根据“大嘴法”规则,上式应该被分解为: a ++ ++ +b
这个式子从语法上来说是不正确的,它等价于:((a++)++)+b
但是,a++的结果不能作为左值,因此编译器不会接受a++作为后面的++运算符的操作数。这样,如果我们遵循解析词法二义性问题的规则,上列的解析从语法上来说又没有意义。当然,在编程实践中,谨慎的做法就是尽量避免使用类似的结构,除非编程者非常清楚这些结构的含义。
练习2.1
C语言允许初始化列表中出现多余的逗号,例如:
int days[] = { 31,28,31,30,31,30,31,31,30,31,30,31, };
为什么这种特性是有用的?
我们可以把上列的缩排格式稍作改动如下:
int days[] = { 31,28,31,30,31,30, 31,31,30,31,30,31, };
现在我们可以很容易看出,初始化列表的每一行都是以逗号结尾的。正因为每一行在语法上的这种相似性,自动化程序设计工具才能更方便地处理很大的初始化列表。
练习4.1
假定一个程序在一个源文件中包括了声明:
long foo;
而在另一个源文件中包括了: extern short foo;
又进一步假定,如果给long类型的foo赋一个较小的值,例如37,那么short类型的foo就同时获得一个值37.我们能够对运行该程序的硬件做出什么样的推断?如果short类型的foo得到的值不是37而是0,我们又能做出什么样的推断?
如果把值37赋给long型的foo,相当于同时把值37也赋给了short类型的foo,那么这意味着short型的foo与long型的foo中包含了值37的有效位部分,两者在内存中占用同一区域。这有可能是因为Long型和short型被实现为同一类型,但很少有C语言实现会这样做。更有可能的是,Long型foo的低位部分与short型的foo共享相同的内存空间,一般情况下,这个部分所处的内存地址较低;因此我们的一个可能推论就是,运行该程序的硬件是一个低位优先(little-endian)的机器。同理,如果在long型的foo中存储了值37而在short型的foo的值却是0,我们所用的硬件可能是一个高位优先(big-endian)的机器。
big-endian(大端模式):最低地址存放高位字节,称为高位优先,内存从最低地址开始按顺序存放。big-endian存放方式正是我们的书写方式,高位数数字先写(比如,总是按照千、百、十、个位来书写数字)。而且所有的处理器都是按照这个顺序存放数据的。
little-endian(小端模式):最低地址存放低位字节,称为低位优先,内存从最低地址开始按顺序存放。little endian处理器是通过硬件将内存中的little endian排列顺序转换到寄存器的big endian排列顺序的,没有数据加载/存储的开销,不用担心。
练习4.2在某些系统中,下列程序打印的结果是%g,为什么?
void main() { printf("%g ", sqrt(2)); }
在某些C语言实现中,存在着两种不同版本的printf函数:其中一种实现了用于浮点格式的项,如%e、%f、%g等;而在另一种却没有实现这些浮点格式。库文件中同时提供了printf函数的两种版本,这样的话,那些没有用到的浮点运算的程序就可以使用不提供浮点格式支持的版本,从而节省程序空间、减少程序大小。
在某些系统上,编程者必须显示地通知连接器是否用到浮点运算。而另一些系统,则是通过编译器来告知连接器在程序中是否出现了浮点运算,以自动的做出决定。
上面的程序没有进行任何浮点运算!它既没有包含math.h头文件,也没有声明sqrt函数,因此编译器无从得知sqrt是一个浮点函数。这个程序甚至都没有传送一个浮点参数给sqrt函数。所以编译器“自认合理”地通知连接器,该程序没有进行浮点运算。
那么sqrt函数又怎么解释呢?难道sqrt函数是从库文件中取出的这个事实,还不足以证明该程序用到了浮点运算?当然,sqrt函数从库文件中取出的这一点没错;但是,连接器可能在从库文件中取出sqrt函数之前,就已经做出了使用何种版本的printf函数的决定。
练习5.2
下面程序的作用是把它的输入复制到输出:
#include <stdio.h> void main() { register int c; while ((c = getchar()) != EOF) putchar(c); }
从这个程序中移除#include语句,将导致程序不能通过编译,因为这时EOF是未定义的。假定我们手工定义了EOF:
#define EOF -1 void main() { register int c; while ((c = getchar()) != EOF) putchar(c); }
这个程序在许多系统中仍然能运行,但是在某些系统允许起来却慢很多。这是为什么?
函数调用需要花费较长的程序执行时间,因此getchar经常被实现为宏。这个宏在stdio.h头文件中定义,因此如果一个程序没有包含stdio.h头文件,编译器对getchar的定义就一无所知。在这种情况下,编译器会假定getchar是一个返回类型为整型的函数。因此,程序中忘记包含stdio.h头文件的效果就是,在所有getchar宏出现的地方都用getchar函数调用来替换getchar宏。这个程序之所以运行变慢,就是因为函数调用所导致的开销增多。同样的依据也完全适用于putchar。