4.4.1 函数指示符—指针转换
再回头来看main函数,通过语句
swap_ab(& m, & n);
可以看出,要交换的变量是m和n。尽管我们已经非常熟悉函数调用,但实际上你可能并不是真的懂它,因为函数调用运算符( )的左操作数实际上必须是一个指针,而且是指向函数的指针。
问题是,在上述语句中,swap_ab是一个函数指示符。不过没关系,C语言规定,除非作为一元&运算符的操作数,否则,函数指示符将自动转换为指向函数的指针,这称为函数指示符-指针转换。
调用函数swap_ab时,传递的实际参数是表达式& m和& n的值。这是两个指针,分别指向变量m和变量n。
再回到函数swap_ab,参数a、b在该函数开始执行时被创建为两个变量以接受传递给它们的指针。如图4-5所示,一旦实际参数被传递给变量a和b,则它们的值现在各自指向main函数内的变量m和n。
图4-5 函数参数的值指向main函数内的变量
要交换两个变量的值,需要使用第三个变量,这是很容易理解的。为此,我们在函数swap_ab里声明了一个变量temp,并初始化为表达式* b的值:
int temp = * b;
在这里,左值b要先执行左值转换,转换为变量b的存储值,这是一个指针。然后,一元*运算符作用于这个指针,得到一个(代表变量n的)左值。该左值继续执行左值转换,得到变量n的存储值,然后用这个值初始化变量temp。
你可能觉得我很啰唆,因为一眼就能看出表达式* b的结果就是变量n的存储值。但我这样做的目的是让你学会并习惯如何解析涉及指针的表达式。很多人在分析简单表达式的时候觉得自己很明白,但表达式一复杂就晕头转向、一筹莫展,就是因为只凭感觉而缺乏正确的、科学的分析方法。同样的道理,在第二条语句
* b = * a;
中,表达式* b = * a的两个子表达式* a和* b的结果都是左值,实际上分别代表变量m和n。但是,因为左值* b位于赋值运算符的左侧,不执行左值转换,左值* a位于赋值运算符的右侧,执行左值转换,然后赋给左值* b。即,读取变量m的值并把它保存到变量n。
在函数swap_ab的最后一条语句
* a = temp;
里,表达式* a = temp用于将变量temp的值保存到左值* a所代表的变量(实际上是变量m)。至此,我们完成了两个变量的值的互换。
函数swap_ab内没有return语句,但这不影响它的返回。对于没有返回值(返回类型为void)的函数来说,不通过return语句返回没有任何问题,当函数的执行到达组成函数体的右花括号“}”时,相当于执行了一条不带表达式的return语句。
但是,如果函数的返回类型不是void,而且函数的返回是因为执行到组成函数体的右花括号“}”,则调用者不得使用函数的返回值,否则程序的行为是未定义的。唯一的例外是从C99开始,宿主式环境下的main函数通过右花括号“}”返回时,则默认返回0。不过,要是main函数的返回类型不是或者不和int等价,则返回值不确定。
再回到main函数,调用函数swap_ab之后,我们又声明了一个变量pf,其类型为指向函数的指针:
void(* pf)(int *, int *)= swap_ab;
如图4-6所示,要解读这个声明,依然是从标识符开始。我们说过,C语言的一个特点是变量和函数的声明与它们在程序中的使用在形式上一致。因此,声明中的非字母符号虽然不是运算符,但却继承了它们的优先级规则。如果不是用圆括号将“* pf”括起来,那么,标识符pf将优先与它右边的(int *, int *)进行语法关联。
图4-6 声明一个变量,其类型为指向函数的指针
但是,因为圆括号的存在,标识符pf被认为是与它左边的星号“*”进行关联的,因此,是需要先向左读,即,“pf的类型是指针(*)”或者“pf是一个指针”。
既然是一个指针,那么它必须指向另一个类型。到底指向谁呢?如果(* pf)的右边没有东西,则它可以继续往左读,但是它右边是(int *, int *),那就意味着该指针指向一个函数。因此,我们进一步往右读做“指向一个函数”。
对函数来说,重要的是它的参数类型和返回类型。因此,必须在声明里提供参数类型和返回类型。于是,我们可以继续读做“第1个参数的类型是int *,第2个参数的类型是int *”。和不带函数体的声明一样,在这里,形参的名字不是必须的。
函数的返回类型一定是在左边,于是我们回过头来往左看,那里是一个关键字“void”,于是我们读作“返回类型为void”。如果一个函数的返回类型是void,则意味着它不返回任何值,或者说它返回空值。
到此,整个声明的左边和右边再没有其他东西,不再继续往下读,标识符pf的类型已经完全确定。笼统地说,pf是一个变量,其类型为指向函数的指针;再具体一点,pf是一个变量,其类型为指向void(int *, int *)类型的指针;如果要用类型名来描述的话,则pf是一个变量,其类型是void(*)(int *, int *);如果还要更具体的话,就是“pf是一个变量,其类型为指向函数的指针,被指向的函数有两个参数,其类型都是指向int的指针,函数的返回类型为void。
在变量pf的声明里带有一个初始化器swap_ab,在这里它是一个函数指示符,必须执行函数指示符-指针转换。因为swap_ab的类型是void(int *, int *),自动转换为指向这种函数类型的指针,即void(*)(int *, int *),和变量pf的类型一致,可用于初始化操作。
接下来,语句
pf(& m, & n);
又一次发起函数调用,不过这一次属于本色调用,因为函数调用运算符的左操作数本来就是指针。表达式pf是一个指针类型的左值,故先进行左值转换,转换为该变量的存储值,这是一个指向函数的指针,实际上指向函数swap_ab。因为函数调用需要一个指向函数的指针,相比之下,这是C语言比较喜欢的写法,毕竟不需要做函数指示符—指针转换。当然,如果你非要这么写也是可以的:
(* pf)(& m, & n);
函数调用运算符( )的优先级比一元*运算符高,故这里必须用括号来形成一个基本表达式以阻止不恰当的结合。因为pf是一个指针类型的左值,左值转换后得到一个指针,而一元*运算符作用于这个指针,得到一个函数指示符。然后,函数指示符又反过来继续转换为一个指针函数的指针。显然,这是在转圈圈,既然是这样,下面的写法也没问题:
(& * pf)(& a, & b); (* & * pf)(& a, & b); (& * & * pf)(& a, & b);
练习4.3
1.为什么上面三种函数调用的写法都没问题?请分析它们的工作原理。
2.若变量pf的类型是指向函数的指针,被指向的函数有两个char类型的参数,且返回类型是int,请写出pf的声明,以及它的类型名。