正确理解函数名

只有正确理解函数名,才能正确使用函数指针。

函数指针是C语言里一个比较重要的特性。它实现了某种意义上的高阶函数,使得我们可以把函数作为参数和返回值。

我们知道,函数指针也就是一个变量,里面存储着指向的函数的地址。一个比较奇怪的问题是,以下两种写法在绝大多数编译器的眼里都是正确的:

typedef void(*p)();

void func(){}

p pointer = func; //写法1
p pointer = &func; //写法2

有些人会这样解释:对一个函数取地址仍然得到函数本身。

用这个解释,我们可以解决&&func、&&&func的问题,它们仍然会得到func本身。但我们很快就会觉得前一种写法本身与其他指针不太相容,比如说,你永远无法写出这样的代码:

int a = 1;
int p = a;

这么想来,我们不得不怀疑对一个函数取地址仍然得到函数本身这个说法的合理性。其实,问题的关键在于函数名究竟被处理为了什么。

答案很简单,函数名被处理为一个地址字面量值

无论你对此有何评价,Clang、MSVC、GCC都是这样实现的。基于此,我们应该理解

p pointer = func;

的真正意思是,把func的地址赋给pointer。

所以,我们不能这样去写程序:


//main.c
typedef void(*p)();

extern p func;

func2(p func){
  ...
}

int main(){
  func2(func);
}

//func.c

void func(){
  ...
}

如果这么写,你会惊喜地发现func2函数传入的是func函数编译后的二进制代码的前四位。这是因为编译器认为main.c中的func是一个变量,而对其取地址。但实际上func是一个字面量,本身就是要取的地址中的值了。

你可能会问了,这种不正确的写法为什么还可以正常编译链接呢?

这就要问问链接器了。

正确理解链接过程

链接器是石器时代的产物。由于C实际上是被编译为汇编后,再生成目标文件,最后被链接,实际上链接器是给汇编语言用的。

而这就带来了一大问题:链接的接口,即符号表(记录所有符号)与重定向表(记录要被重定向的符号),是没有C语言的类型信息的。

也就是说,每当你使用extern关键字声明外部引用时,你实际上是放弃了对该引用的静态类型检查

而C++则不会有这个问题。你可以将上面的代码改成C++文件名或用C++编译器编译,会发现链接器错误。因为C++有名称修饰,它会将符号做处理,实际上附加了类型信息。但是,名称修饰不蕴含在C++的标准中,各家编译器厂商有自己的实现,所谓的C++ ABI不兼容,这个问题也占了一席之地。