函数原型以及相关知识

先说结论:ANSI C以及之前的C语言可以不声明(declare)函数原型(prototype)而直接调用(call)函数,但是C99以及之后的语言标准要求先声明函数原型。

但是各大编译器可以有自己的实现,如GCC(GNU C Comlier,Dev-C++所附带的的MinGW的G就是GNU)可以由定义式(definition)自动获得原型声明,完全正确,但必须在调用该函数之前。若函数定义在调用者的后面,则其会隐式声明为int(int),一旦其定义式与它不一样则报错(如图foo函数在GCC下正常,而bar函数报错)。

以下给出自己查阅资料的结果,最后给出参考资料。

C语言的函数经编译后成为目标码,在运行期(Runtime)时在内存中有它的地址(甚至可以被函数指针“指涉”,refer to)。
函数调用(call)时,主调函数(caller)把实参计算后复制一份压入函数栈(经过优化甚至将前几个参数传入寄存器eax、ebx等),然后,被调用的函数按照参数列表(parameter list)取得参数、进行操作并在遇到return或最后一个右花括号时执行ret汇编指令,把返回值(如有需要)转为声明的类型后返回给主调函数。

这就解释了声明原型或者不声明、乃至无参函数传入参数甚至传入可变参数的道理:
声明原型是告诉主调函数它应该把参数转为何类型、压到哪个地址;不声明原型则都为int、按默认顺序复制到对应地址。
这样主调函数不必操心它调用的函数如何实现,它只需按照原型压栈、等待其返回并取得返回值即可。
这也解释了C语言不区分递归和非递归函数的道理:它根本不必区分,二者本质上没有区别。即使该函数没有定义完全而其内部却要调用自己时,该调用语句也只是被编译为压栈、跳转等等寥寥几行汇编码,而不操心被调用函数干了什么——毕竟已经把活交给被调用的函数了,主调函数就不必操心其实现了。
这也解释了我们常把main定义为void,而操作系统总是给它int argc和char *argv[]两“个”参数,但是没有发生问题。
根据这个道理我们甚至可以自己用神奇的方法(比如stdargs.h里面的va_list等“宏”,macro)取得传入的值,这样就实现了可变参数。

这体现了C语言的哲学以及其“底层”的特点,这种哲学也运用在别处。
如编写链表或者二叉树等等数据结构时,代表每个节点的类型的结构体类型(struct-type)尚未声明完全时即可在其内部声明指向该结构体类型的指针。因为指针仅仅指涉(refer to)某事物,而不操心该事物内部结构如何;甚至不同字长的机器上指针变量的长度(用sizeof(void*)即可查看)也不一样。这跟函数调用时caller只压栈和取返回值,而不关心被调用的函数如何实现是一致的。

怎样,每次调用函数的时候居然做了这么多事情,很神奇吧?
还有inline, __cdecl, __stdcall, _Noreturn等等修饰符用于函数定义或声明,其功能更加纷繁复杂,留待想要深刻学习的同学去探索。

附参考,书名“Modern C”,作者Gens Gustedt,中文版有售。

image
image
image

posted @ 2022-11-06 21:59  全球通u1  阅读(61)  评论(0编辑  收藏  举报