c语言 - 函数

c语言 - 函数,函数声明,函数定义,形式参数和实际参数

函数定义

如果函数不需要参数,应该将其参数设为void,这样如果在别处调用该函数时传入了参数,在编译期间会报错,这是一个编程好习惯,虽然这并不是必须这么做,你也可以不加void,通常情况下也没什么不妥。函数可以有多个return语句,但是只有第一个return语句会被执行。

函数名会在汇编阶段、链接阶段、运行阶段(动态链接)被替换成函数的入口地址:

形参和实参

函数定义中出现的参数称之为形式参数,可以看作是一个占位符
在函数调用时出现的参数称之为实际参数,此时,将实参的值赋给形参,也就是内存的拷贝
形参只有在函数被调用时分配内存,在定义函数时,不分配内存
注意,在调用函数过程中,形参实参的值互不影响,因为它们拥有不同的内存

函数声明

在使用某个函数之前,我们应该先定义该函数
但是某些情况,我们想让函数在后面定义,这时候就需要函数声明
函数声明也称为函数原型,函数声明很简单,只是去掉大括号就行

关于函数

函数不能嵌套定义,但是可以嵌套调用,所谓嵌套调用就是可以在一个函数的定义或调用过程中出现对另一函数的调用。如果自己调用自己,那么就属于递归调用,C 语言允许递归调用,但是递归调用逻辑结构不清晰,容易造成代码混乱,常被改写为循环。

函数体外,除了预处理指令变量的定义类型的定义,不允许出现任何具有运算、逻辑处理能力的语句。
main()函数是 C 程序的入口函数,是最先入栈的函数,也是最后出栈的函数,可以调用其它函数(包括 main 函数自己)。

main()函数

我们之前的main()函数,都是没有参数的,但是实际上,main()函数是可以接收命令行参数的,它有两种标准的原型:

第一个参数是字符串的数目,第二个参数是一个指针数组,指针数组的第0个元素总是当前被执行的程序的文件名,所以,argc最小也为1。

回调函数

函数指针可以作为某个函数的参数来使用,回调函数就是一个通过函数指针调用的函数。先看下面的例子:

被传入的函数,即getNextRand(),叫做回调函数;传入回调函数这个动作,即setArrayElem(array, len, getNextRand),叫做登记回调函数;函数setArrayElem()叫做中间函数;中间函数的调用者,即main()函数,叫做起始函数

可变参数

不知道你发现没有,我们一直用的 printf()、scanf() 其实是参数个数可变的函数,所谓可变参数就是,能根据具体的需求接受可变数量的参数。

printf() 原型为:int printf(const char *fmt, ...);
scanf() 原型为:int scanf(const char *fmt, ...);

除了第一个固定位置的参数fmt,就是...省略号了,这个省略号就表示可变参数

变参函数实现原理
C 调用约定下可使用 va_list 系列变参宏实现变参函数,此处 va 意为 variable-argument(可变参数)

先来认识一下 va_list 系列变参宏

这实际上也不是真正的变参函数,我们实际上内定了参数为 int、double、char *

变参宏根据堆栈生长方向和参数入栈特点,从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。变参宏的定义和实现因操作系统、硬件平台及编译器而异(但原理相似)。

函数参数入栈顺序
我们知道,栈是由高地址向低地址延伸的,栈顶在内存的低地址,先入栈的变量,内存地址高于后入栈的变量
一般来说,c语言的函数参数入栈顺序是从右往左的,正好符合我们的思维习惯,参数也是从低到高的地址依次排列

但是实际上还是因操作系统、硬件平台、编译器而异,比如我的树莓派3b(armv7)上使用gcc编译就是从左往右入栈的

而且奇怪的是,我在CentOS7.3上使用gcc也是这种情况,而在VS2017下就相反了

注意:va_arg(vl, data_type) 的 data_type 不能为下面这些数据类型:

  • charsigned charunsigned char
  • shortsigned shortunsigned short
  • float

因为在C语言中,调用者会对每个参数执行默认实际参数提升(default argument promotions)

  • float类型提升为double
  • charshort以及对应的signedunsigned提升为int
  • 如果int不能存储原值,则提升为unsigned int

简单应用:利用变参函数,求一组数字的平均数

仔细想想,在使用printf()、scanf()及其家族函数时,我们明明没有像上面一样传入一个count这样存储参数个数的参数,而它们却能够准确无误的输出我们给的各种数据类型?

其实它是通过解析const char *fmt中的格式化字符串来判断的,有多少个%那就有多少个变参,然后再根据它们后面的数据类型,就可以知道它们的长度,进而取得它们的值了!