第四章
4.1 空语句与表达式语句
C语言中最简单的语句是空语句,只包含一个分号,在要求出现一条完整语句但不需要执行任何任务的情况下出现。C语言不存在专门的赋值语句,赋值是在表达式语句中的一种操作,只要在表达式后加上一个分号,就能将表达式转换为语句:x=y+3; ch=getchar();
y+3; getchar();
也是表达式语句,只不过没有进行赋值操作而已,表达式被求值但没有任何地方保存它们,第一条语句没有任何效果,第二条语句则是读取输入中的下一个字符,然后将其丢弃。没有任何效果的语句还有printf
和i++;
等操作,它们只是表达式的值被忽略,但还存在副作用。
4.2 代码块和if语句
代码块是位于一对花括号之间的声明和语句列表。C语言if
语句中不存在布尔类型,而是用整型代替,零值为假,非零值为真。if语句嵌套悬空出现时,else子句从属于最靠近它的不完整if语句。如果想让下图的else子句从属于第一条if语句,可以给第二条if语句加上空的else子句或者用一对花括号将该if语句包含在第一条if语句之内。
4.3 while 语句 与 for语句
while语句循环的条件测试在循环体开始执行之前进行,如果一开始就是假,循环体不会执行。while循环中可以使用break语句,用于永久终止循环,continue语句则用于终止当前的那次循环,在执行完continue之后,重新执行条件测试,决定是否继续执行循环。这两条语句如果出现于嵌套循环内部,只对最内层的循环起作用。还可以把条件测试移到if语句中,让它来控制整个循环的流程。for循环则分为初始化部分,条件部分和调整部分。for语句和while语句执行过程的区别在于出现continue语句时,continue跳过循环体的剩余部分,直接跳到调整部分执行,而while语句中,调整部分是循环体的一部分,也会将它跳过。
4.4 do语句和switch语句
do语句很像while语句,只是它的条件测试在循环体执行之后才进行,而不是先于循环体执行,所以这种循环的循环体至少执行一次。C语言的switch语句类似于其他语言,与其它语言不同的是,它的条件测试的结果必须是整型值。case标签并不把语句列表划分为几个部分,而是确定语句列表的进入点。执行流将贯穿各个case标签,而不是停留在单个标签,只有配合使用break语句才能停留在单个标签,如下面这段代码。
switch (num) {
case 0 :
case 1:
case 2:
case 3:
}
在这个语句中,如果num=0。那么四个case语句都将执行。如果direction=1。将执行剩下三个语句。原因是switch语句原理是跳转到case位置执行剩下的语句,直到最后或者遇见break为止。
在switch case 语句中不能使用continue 关键字。continue语句的作用是跳出本次循环,转入执行下一次循环。故而,continue语句只能用于循环语句中,而switch case语句为多分支选择语句,不是循环语句,所以在switch case 语句中是不能使用continue 关键字的(除非把continue关键字放在循环语句中)。如果遇到了break语句,执行流会立即跳到语句列表的末尾,其实际效果就是把语句列表划分为不同的部分。
4.5 goto语句
要使用goto语句,必须在希望跳转的语句前面加上语句标签,语句标签就是在标识符后面加上冒号。goto语句适合用于跳出多层嵌套的循环,由于break语句只影响包围它的最内层循环,要想立即从深层嵌套的循环中退出只有使用goto语句较为合适。要想不使用goto语句就退出来,第一种方案是当你希望退出所有循环时设置一个状态标志位,但这个标志位在每个循环中都必须进行条件判断。另一种方案是把所有的循环都放到一个单独的函数中,当最内层循环满足退出条件时直接使用return语句离开这个函数。
第五章 操作符和表达式
5.1 操作符
5.1.1 算术操作符和移位操作符
首先介绍的是算术操作符,C语言中常用的算术操作符有:+ - * / %
。除了%操作符,其他几个操作符都是既适用于浮点类型又适用于整数类型。当/
操作符的两个操作数都是整数时,它执行整除运算,在其他情况下执行浮点运算。
其次是移位操作符,两个操作数必须是整型类型。在左移位操作中,值最左边的几位被丢弃,右边多出来的几个空位则由0补齐,如图所示:
右位移操作存在一个左位移所不存在的问题:就是左边多出来的几个空位可以选择两种方案:一是逻辑移位,左边移入的位用0填充,二是算数移位,左边的空位由原先该值的符号位决定,这样能保持原先的正负形式不变。如果值10010110右移两位,逻辑位结果是00100101,算术移位的结果是11100101.,左移则相同,只是操作数为负值时不同。
需要注意的是,无符号值时执行的所有移位操作都是逻辑移位,但对于有符号值,执行哪种移位取决于编译器。a<<-5这种结果可能是没有意义的,与编译器有关。
5.1.2 位操作符与赋值
位操作符对它们的操作数的各个位执行and,or和xor等逻辑操作,它们都要求操作数位整数类型,赋值是表达式的一种,而不是某种类型的语句,所以只要允许出现表达式的地方,都允许进行赋值。x = y + 3;表面=的操作数是x和表达式y+3的值。赋值也是个表达式,表达式具有一个值,赋值表达式的值就是左操作数的新值,赋值也可以作为其他赋值操作符的操作数,例如:a=x=y+3;赋值操作符的结核性是从右到左,所以这个表达式相当于:a=(x=y+3);
还有一种复合赋值符, 有以下几种形式:+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=。
5.1.3 单目操作符和关系操作符
C具有一些单目操作符,也就是只接受一个操作数的操作符,有!,++,-,&,sizeof,~,–,+和 *。(类型)操作符被称为强制类型转换,它用于显式地把表达式的值转换为另外的类型。增值操作符++和–操作符只能作用于可以位于赋值符号左边的表达式。前缀形式的++操作符出现在操作数的前面,操作数的值增加,表达式的值就是操作数增加后的值。后缀形式的++操作符出现在操作数的后面,操作数的值仍然增加,但表达式的值是操作数之前的值。
关系操作符包含>,>=,<,<=,!=和==,这些操作符产生的结果都是一个整型值,而不是布尔值,如果两端的操作数符合操作符指定的关系,表达式的结果是1,否则表达式的结果为0。关系操作符的结果是整型值,所以它可以赋值给整型变量,但通常它们在if条件语句中,作为测试表达式。
5.1.4 逻辑,条件和逗号操作符
逻辑操作符有&&和||,这两个操作符看上去有点像位操作符,但它们的具体操作却大相径庭,用于对表达式求值,测试它们的值是真还是假。它们的优先级较低,&&操作符的左操作数总是首先进行求值,如果它的值为真,才紧接着对右操作数进行求值。如果左操作数的值为假,那么右操作数便不再进行求值,因为整个表达式的值肯定是假的。||操作符也具有相同的特点,它首先对左操作数进行求值,如果它的值为真,右操作数便不再求值,因为真个表达式的值此时已经确定,这个行为常被称为短路求值。
条件操作符接受三个操作数,它也会控制子表达式的求值顺序,是a>5?b-6:c/2
的形式,与if else语句效果相同,但可以简化它。逗号操作符将两个或多个表达式分隔开来,这些表达式自左向右逐个进行求值,整个逗号表达式的值就是最后那个表达式的值。例如if(b+1, c/2, d>0)
,如果d的值大于0,那么整个表达式的值为真。但是没人这么写代码,因为前两个表达式的求值毫无意义,只是被简单丢弃。
还有几种操作符是下标引用、函数调用和结构成员,其中下标引用操作符是一对方括号,接受两个操作数:一个数组名和一个索引值。函数调用操作符接受一个或多个操作数,第一个操作数是希望调用的函数名,剩下的操作数就是传递给函数的参数。.
和->
操作符用于访问一个结构体的成员,如果s是个结构体变量,那么s.a就是访问s中名叫a的成员,而如果拥有一个指向结构体s的指针而不是结构体本身,要访问它的成员时,需要使用到->
操作符而不是.
操作符。
5.2 布尔值与左值和右值与表达式求值
C不具备显示的布尔类型,用整数来代替,零是假,非零值为真。左值就是出现在赋值符号左边的东西,例如a = b + 25;
a是个左值,因为它表示了一个可以存储结果的地点,b+25是一个右值,因为它指定了一个值。互换后,b + 25 = a;
a可以当作右值,但是b+25不能当一个左值,因为它没有标识一个特定的位置。计算机计算b+25时,它的结果必然保存于某个地方,但程序员没办法预测该结果保存在哪里,也无法保证下次还会存储于那个地方,因此这个表达式不是一个左值,基于同样的利用,字面值常量也都不是左值。a[b + 10] = 0;
下标引用实际上是一个操作符,所以表达式的左边是个表达式,但它却是一个合法的左值,因为它标识了一个特定的位置,以后在程序中还可以引用它。
5.2.1 隐式类型转换
C的整型算术运算总是至少以缺省整型类型的精度来进行的,为了获取这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。例如char a,b,c; a=b+c;
b和c的值被提升为普通整型,再执行加法运算,加法运算的结果被截短,然后再存储于a中。
5.4.2 算术转换与操作符的属性
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行,下面的层次体系称为寻常算数转换。复杂表达式的求值顺序是由3个因素决定的:操作符的优先级,操作符的结合性以及操作符是否控制执行的顺序。两个相邻的操作符哪个先执行取决于它们的优先级,如果两者优先级相同,那么它们的执行顺序由它们的结合性决定。
第六章 指针
6.1 内存和地址
计算机的内存由数以万计的bit组成,通常8个bit组成一个字节,内存中每个存储单元拥有一个十六进制地址,每个地址以一个字节编址,即每个存储单元大小是一个字节。为了存储更大的值,可以把两个或更多字节组合在一起作为一个更大的内存单位,例如许多机器以字为单位存储整数,每个字由2个或4个字节组成。注意,尽管一个字包含了4个字节,但它仍然只有一个地址。
程序员需要关注的是两件事:1.内存中的每个位置由一个独一无二的地址标识,2.内存中的每个位置都包含一个值。下图显示了5个整数,每个整数存储在一个字也就是4个字节中。如果记住了一个值的存储地址就可以根据这个地址得到这个值。但是记得这些地址过于笨拙,高级语言提供的特性就是通过名字来访问内存的位置,如下图所示,采用a,b,c,d,e等变量来代替地址,需要注意的是变量名称与内存位置之间的关联并不是硬件所提供的,而是由编译器为我们实现的,这些变量给我们一种更方便的方法记住地址,但硬件仍然通过地址访问内存位置。
6.2 值和类型
上图中的 第三个位置所存储的一个非常大的整数,可以解释为整数也可以解释为浮点数,这取决于它们被使用的方式。这引出了一个重要的结论:不能简单地通过检查一个值的位来判断它的类型,为了判断值的类型以及它的值,必须观察程序中这个值的使用方式。例如以二进制形式表示的32位值:01100111011011000110111101100010,可以解释为以下5种不同类型的值:
6.3 指针变量的内容与解引用操作符
已知声明 int a = 112, b = -1; float c = 3.14; int *d = &a; float *e = &c;
指针变量d存储的内容和整型变量a的地址一致,e的内容和c的地址一致。d的地址是112,而它的内容是100,100这个数值也是用于标识其他位置。a的值是212,b的值是-1,c的值是3.14,d的值是100,e的值是108,需要注意的是:d和e的值分别是a和c的地址,而不是该地址所对应的值,一个变量的值是分配给这个变量的内存位置所存储的值。所以不能简单地认为由于d和e是指针,所以它们就能自动获得存储于它们里面的地址所对应的值。
通过一个指针访问它所指向的地址的过程称为间接访问或解引用指针,用于执行这个操作的操作符是单目操作符*。d的值是100,当对d使用解引用操作符时,它标识访问内存位置100并查看那里的值。因此*d的右值是112,它的左值是位置100本身。
6.4 未初始化和非法的指针
int *a; *a=12;
这个声明创建了一个指针变量a,并将12存储在a指向的内存位置。但是由于没有初始化a,我们不知道a指向了哪里。指针变量和其他变量一样,如果变量是静态的,会被初始化为0,但如果变量是自动的,它不会被初始化。无论是哪种情况,声明一个指向整型的指针都不会创建用于存储整型值的内存空间。所以如果只声明了指针变量就进行赋值操作,可能会出现a的初始值是个非法地址,这样赋值语句就会出错,在Linux系统上,这个错误被称为段错误或内存错误,它提示程序试图访问一个并未分配的内存位置。更严重的情况则是如果这个未初始化的指针包含了一个合法的地址,它可能会在你意识不到的情况下去修改该地址的值从而导致难以发现的错误。
6.5 NULL指针
NULL指针是一个特殊的指针变量,表示不指向任何位置。要是一个指针变量变为NULL,可以给它赋为零值,为了测试一个指针变量是否为NULL,可以将它与零值进行比较。对指针进行解引用操作可以获得它指向的值,但是NULL指针并未指向任何东西,因此对一个NULL指针进行解引用操作是非法的。 如果对一个NULL指针进行间接访问,可能会访问内存位置零。编译器能够确保内存位置零没有存储任何变量,但机器并未妨碍你访问或修改这个位置。这会导致程序包含了这个错误,但机器却隐匿了这个错误,这就使得这个错误很难查找。在其他机器上对NULL指针进行间接访问将引发一个错误,并终止程序,宣布这个错误比隐匿这个错误要好得多,因为程序员可以容易修正它。如果所有指针变量能够自动初始化为NULL,那是件好事,但事实并非如此。如果你已经知道指针将被初始化为什么地址,就把它初始化为该地址,否则就把它初始化为NULL。
6.6 指针、间接访问和左值以及变量
指针变量可以作为左值,并不是因为它们是指针,而是因为它们是变量。对指针变量进行间接访问表示我们应该访问指针所指向的位置,间接访问指定了一个特定的内存位置,这样可以把间接访问表达式的结果作为左值使用。*d = 10 - *d;
这条语句包含了两个间接访问操作,右边的间接访问作为右值使用,它的值是d所指向的值。左边间接访问作为左值使用,所以d所指向的位置把赋值符右侧的表达式结果作为它的新值。而d = 10 - *d;
这条语句是错误的,因为它表示把一个整型数存储于一个指针变量中。
*&a = 25;
的含义是将值25赋值给变量a,与a = 25;
的效果是一样的。它表示取a的地址,它是一个指针常量,接着解引用访问该地址内存中的数据,并赋予25。
6.7 指针常量和指针的指针
假定变量a存储于位置100,那么*100 = 25;
看上去像是把25赋值给a,因为a是位置100所存储的变量,但是这是错误的,因为字面值100的类型是整型,而间接访问操作符只能作用域指针类型表达式,如果确实想把25存储于位置100,必须使用强制类型转换*(int *)100 = 25;
强制类型转换将100从整型转换为指向整型的指针,这样进行间接访问就是合法的。但是你需要使用这种技巧的机会很少,因为程序员通常无法预测编译器会把某个特定的变量存在内存中的某个位置,无法预知它的地址。用&操作符得到地址很容易,但表达式只有在程序执行时才会进行求值,此时已经来不及把它的结果作为字面值常量复制到源代码。这个技巧的有用之处在于偶尔需要通过地址访问内存中某个特定的位置,它并不是用于访问某个变量,而是访问硬件本身。
int a = 12; int *b = &a; c = &b;
已知上述代码片段,可知c的类型是一个指针,它指向的也是一个指针,变量b是一个指向整型的指针,所以c是一个指向指针的指针,c的声明一般为int **c;
即多重指针。指针操作符*
具有从右向左的结合性,所以**c
必须从里向外逐层求值。
6.8 指针表达式
char ch = 'a'; char *cp = &*ch;
上述声明对应下图:
6.9 实例与指针运算