<<c专家编程>>笔记
C专家编程摘录
-
c操作符的优先级
有时一些c操作符有时并不会像你想象的那样工作。
下方表格将说明这个问题:
优先级问题 | 表达式 | 期望的情况 | 实际情况 |
---|---|---|---|
. 优先级高于* | *p.f | (*p).f | *(p.f) |
[ ]优先级高于* | int *ap[ ] | int (*ap)[ ] | int *(ap[ ]) |
函数()优先级高于* | int *fp() | int (*fp)(), fp是 int型函数的指针 | int *(fp( )),fp是返回int型指针的函数 |
==和!=优先级高于位操作 | (val & mask != 0) | (val &mask) != 0 | val & (mask != 0) |
==和!=优先级高于= | c=getchar() != EOF | (c=getchar()) != EOF | c=(getchar() != EOF) |
算数运算符高于位移运算符 | msb << 4 + lsb | (msb << 4) + lsb | msb << (4 + lsb) |
, 优先级最低 | i = 1,2; | i = (1,2); | (i = 1),2; |
对于上表做出补充: |
1.表中第二条。指向数组的指针(ptr to array of ints)和指针数组(array of ptrs-to-int)的区别.
指向数组的指针可以认为是二维数组,即int (*ap)[ ] 等价于 int ap[ ][ ].ap指针及每个ap(即ap+i)偏移指针指向一个一维数组(一串数)。
而指针数组是由连续多个int指针组成的数组,每个数组元素指向一个int 变量。
对于指针数组要多提一点,虽然声明一个指针数组并不会被编译器报错,但是这是不安全的。 首先考虑这样一种情况,声明一个int 数组
int Na[5];
Na数组中的5个值都是随机值。当数组元素为指针时,情况也是相同的,即申请了一堆野指针。这将会导致运行时错误。
实测图:
可以看到,a[1]是一个空指针(nil),当用户使用这个指针指向的内存时,将会导致程序异常终止。
2. 一个非常妥善的对策是使用小括号将表达式包含起来(小括号优先级很高)。
-
c操作符的结合性
每个操作符都有优先级和“左”或“右”的结合性。当操作符优先级不同时,求值顺序取决于操作符优先级,而当操作符优先级相同时,这时将使用结合性。
1.除“位与”和“位或”外的所有操作符都遵守从右到左的结合性。(前者从左到右)
2.结合性目的在于表达式中操作符优先级相同情况下给出标准的运算顺序。
3.当操作符的优先级和结合性组合情况较为复杂时,可以采用拆分表达式或者使用小括号。
eg:
int a,b=1,c=2;
a = b = c;
上述两行代码a,b,c结果为: a = 2,b = 2,c = 2;
int a,b=1,c=2;
a = b = c;
-
c中的空格
c中的空格并非人们普遍认为的那样不重要,可以随意增加或者减少。有时候空格会在根本上改变程序的原意。
1.反斜杠可以用来跳过一些字符,包括跳过一个新行(跳过回车符)。一个被反斜杠跳过回车的新行和他的上方未结束的行被认为是逻辑上的一行。但是如果上述情况下在反斜杠后面多打了一个空格(space),情况就不一样了。(反斜杠跳过了空格,而不是回车)。
如图:
2.现在这个问题主要由于ANSI 标准c中的“最大一口策略(maximal munch strategy)”。 考虑下述代码:
int z = y+++x;
z = y + ++x;?还是 z = y++ + x;?
根据最大一口策略,答案是前者。
(注:最大一口策略:如果表达式下一个符号(token)有超过一种语义的可能性,编译器将更愿意咬掉更长的字符序列)
考虑下述代码:
int z = y+++++x;
编译器如何解析?根据maximal munch strategy,结果为 :
z = y++ ++ +x;
但不妙的是,编译器不认识“++”,所以程序编译错误。
3.第三种情况发生在类似这样的一行代码中:
int ratio = *x/*y;
编译器如何处理这条语句?
结果如图:
即“/*”被认为是一段注释的开始,与我们期望的大相径庭。而15行在加入空格(space)后则解决了这个问题。
4.其他情况(上述三种情况并不代表全部)。
-
C声明中的优先级规则
c语言的声明表达对于编译器来说并没什么难度去解析,但对于学习C语言的程序员来说这种复杂性会造成一定的困扰。
举个复杂声明的例子:
char* const (next)();
下面介绍几个较为有用的运算符优先级,并提供一个系统的面向于人的解析c复杂声明的方法,最后附加一个c语言小程序,用于实现一个声明的自动解析。
1.较为有用的优先级
声明从名字开始解析,然后按照优先级顺序继续读取下一个词汇单元。
优先级顺序:(从高到低)
1、小括号包含起来的声明。
2、后继操作符“()”表示一个函数,后继操作符“[]”表示一个数组。
3、前驱操作符“*”表示一个“指向...的指针”。
4、如果const或volatile在类型标识符(e.g.int,long,etc)旁,则它与类型标识符匹配。否则与它直接的左边的星号匹配。
2.系统的方法
处理顺序:
对于整个表达式,采取从右向左的顺序逐次擦除掉记号(token)。当所有记号被擦除,声明的解读过程也就完成了。
<--------
cha* const *(*next)();
step1:找到最左端的标识符;
读作:“标识符 是...”;
step2:如果右边下一个记号是方括号“[possible-size]”;
读作:“...的数组”;
step3:如果右边下一个记号是一个开放的小括号“(possible-parameter)”;
处理:一直读到最近的右小括号匹配。
读作:”返回...的函数“;
step4:如果记号是左小括号 “(”;
处理:这个记号是当一个开放的小括号内记号被处理完后向左查询到的。这时左右小括号里的记号已经被擦除,继续读取右到小括号匹配,擦除,返回step2;
step5:如果左记号是"const,volatile,*"三者中的一者
处理:继续向左查询记号,知道不再出现这三者中任何一个记号为止。从step4重新开始。
读作:const:“常...”, volatile: “非常...”,* : “...指针”;
step6:当记号是基础的类型说明符时。
处理:结束
读作:相应类型名。
下面以一个例子帮助理解:
(处理中的记号使用粗体)
待处理声明 | 下一步处理 | 结果 |
---|---|---|
char* const *(*next) ( ) | step1 | 称"next是...” |
char * const *(* __)__ ( ) | step2,step3 | 右小括号不匹配,下一步 |
char *const *(* ) ( ) | step4 | *不匹配,下一步 |
char *const *( * ) ( ) | step5 | ”*“ 匹配,称”...的指针“,应用step4 |
char *const *__(__ ) ( ) | step4 | "(" 和 “)”匹配,应用step2 |
char *const * __(__ ) | step2 | 不匹配,下一步 |
char *const * ( ) | step3 | 称”返回...的函数“ |
char *const * | step4 | 不匹配,下一步 |
char *const * | step5 | 称”...的指针“ |
char *const | step5 | 称”常...“ |
char * | step5 | 称”...的指针“ |
char | step6 | 称“字符类型” |
最后连在一起表达:next是一个指向返回指向常字符指针的函数的指针。:) |
-
c中的指针和数组的关系
首先,如果你认为在c中指针和数组是等价的,那么你很有必要向下看。
数组和指针本质是不同的,数组名代表的是一个地址,可以认为是一个只读的整数(const int),而指针是一个地址的地址(一个保存地址的变量)。但是由于在很多情况下,数组可以转换为指向数组首元素地址的指针,只不过这种转换是隐式的,但正因如此,才造成了我们的困惑。
在下面三种情况下,数组将转换为指针:
1.数组作为函数的参数,将以指针的形式传递。
eg:
void foo(int *arr) { ... }main { int arr[...]; foo(a); }</code>
2.数组作为表达式一部份参与求值。
<code>eg:
int c = a[i];
//隐式转换:
int c = *(a+i); //此时a是一个指针
3.使用下标的数组名将会被转换成一个指针加上偏移量。
eg:
a[i] --->*(a+i) ;
注意:指针不会转换成数组。
附:简单的c语言声明语义解析程序