本文所关注的问题是:程序顺利通过编译,没有任何警告或者错误消息,但是程序并没有按照程序员所期待的方式执行。更进一步,即关注c语言程序中可能产生这类错误的方式,从而避免这类隐晦的错误。
程序是有符号(token)序列组成,将程序分解成符号的过程,称为“词法分析”。
词法分析中的“贪心法” 简单地说,每一个符号应该包含尽可能多的字符。
专业点的解释,如果编译器的输入流截止至某个字符之前都已经被分解为一个个符号,那么下一个符号将包括从该字符之后可能组成一个符号的最长字符串。
通俗点结束,编译器将程序分解成符号的方法是:从左到右一次读入每个字符,如果该字符可以组成一个有意义的符号,那么再读入下一个字符,若能与之前符号组成有意义的字符,则继续读入下一个字符,如此反复,知道读入的字符组成的字符串已不能组成一个有意义的符号。这个处理策略就被称为“贪心法”或“大嘴法”。
如果一个整型常量的第一个字符是数字0,那么该常量将被视作八进制数。
int a = 046;
int b = 46;
printf("a = %d\n", a); // a = 38
printf("b = %d\n", b); // b = 46
printf("a = %o\n", a); // a = 46
何时会掉入陷阱: 为了格式的对齐,可能无意中将十进制写成了八进制,例如:
struct {
int num;
char *description;
} p[] = {
046, "xxxxx" ,
047, "xxxxxxxxxxxx" ,
125, "xxxxxxx"
};
用单引号括起的字符实际上代表的是一个整数,值为该字符对应的acii码; 用双引号括起的字符(串)代表一个指针,指向无名数组起始字符的指针,并会额外添加一个'\0'字符最为结尾,这是字符串与其他数据类型本质的区别。
函数或变量的声明都由两部分组成:类型 + 声明符(declarator)。
我们通过一个比较复杂的声明来进行理解:'((void()())0)()'。
先给出结论:对常数0进行类型转化,转换成一个函数指针,然后调用该函数指针所指向的函数,即位置0的函数。
分析: 假定变量fp是一个函数指针,那么如何调用fp所指向的函数呢? 方法如下:
(*fp)();
ANSI C标准允许将上式简写为fp(),但是一定要记住这种写法只是一种简写形式
如果我们想要调用0位置的函数,那么可以这么写:
// error code
(*0)();
上式并不能生效,正确的写法如下:
( * ( void(*) () ) 0 ) ();
当然,使用typedef可以简化上式,使期表述更加清晰:
typedef void (*funcptr) ();
(* (funcptr) 0) ();
当然,上述代码在实际运行中会导致segmentfault,因为位置0内容通常我们是没有访问权限的。 下面给出一个可以正确运行的例子
void func(){
printf("hello\n");
}
int main(){
void (*fun)();
fun = func;
func(); // hello
fun(); // hello
(* (void (*)())&(*fun))(); // hello
(* (void (*)())&(func))(); // hello
return 0;
}
在一些偏底层的系统编程中,可能会用到signal函数,该函数的声明如下: '''c void ( signal( int sig, void ( handler)( int )))( int ); ''' signal函数有两个参数:一个整型的参数,它表示信号;另一个参数是一个函数指针,表示信号处理函数。signal函数的返回值又是一个函数指针,该指针就是指向信号处理函数(可以看出signal返回的函数指针所指向的函数声明和第二个参数所指向的函数声明是一样的,都是一个带有一个int参数并无返回值得函数)这样就可以执行信号处理函数了。
同样可以利用typedef简化上面的函数声明:
typedef void (*HANDLER) (int);
HANDLER signal(int, HANDLER);