文章目录
- 函数概述
- 函数声明(function declaration)和函数原型(function prototype)
- 函数定义(function definition)
- 函数之间的通信
- 函数的递归(recursion)
函数概述
函数的作用:
- 执行某些动作
- 计算得到一个值
一个函数可以同时或者单独具有上述两个作用。
函数声明(function declaration)和函数原型(function prototype)
ANSI C之前的函数声明要求指明函数返回值的类型,不要求指明形参的类型,即:
函数返回值类型 函数名();
括号中为空即可。
ANSI C新增了函数原型,可以直接复制函数定义中的函数头,但需要用分号结尾,即:
函数返回值类型 函数名(形参类型 形参名1, 形参类型 形参名2, ... , 形参类型 形参名n);
函数原型中的形参名是假名,可以省略,也可以随意命名而与函数定义的函数头中的函数名不相同也是可以的,即:
函数返回值类型 函数名(形参类型, 形参类型 , ... , 形参类型);
或:
函数返回值类型 函数名(形参类型 形参名1', 形参类型 形参名2', ... , 形参类型 形参名n');
函数原型指明的信息有:函数返回值的类型,形参数量,每一个形参的类型。
在函数原型中使用变量名并没有创建变量。
函数原型提供了函数返回值的类型和函数形参变量的类型,这些信息被称为函数的签名(signature)。
使用函数原型的目的就是让编译器在第一次使用函数之前就知道怎么使用这个函数。如果将函数定义放在函数调用之前,就不需要函数原型,此时函数定义就相当于函数原型。
一般都把main()函数放在所有函数最前面,因为main()提供了程序的框架。C程序的编译是自上向下的,如果没有函数原型作为函数的声明,那么编译到函数名时将无法识别这个标识符。如果将函数定义放在函数调用(function call)前面,当然也可以不写函数声明,但这是不符合编程规范的。
函数原型也可以放在主调函数的里面而不一定非要是在主调函数的外面,只要函数原型在函数调用之前就行。
main()被称为主函数,其他函数称为子函数,子函数除了可以被主函数调用外,子函数之间也可以互相调用。主函数只能被操作系统调用。
函数传参时,函数原型会覆盖C语言数据类型的自动数据类型转换。比如,char和short类型实参传递给形参变量时会自动转换为int类型,但形参变量定义的类型会覆盖这一自动类型转换。如:
函数原型:
void fun(float a , float b);
调用函数fun:
char ch = 'A';
short sh = 8;
fun(ch , sh);
这时ch不会被转为int而是转为float,sh不会转为int而是转为float。
函数声明告知编译器函数的类型,函数定义则提供函数的代码。
函数定义(function definition)
函数分为函数头和函数体。
函数头:
函数返回值类型 函数名(形参类型 形参名1, 形参类型 形参名2, ... , 形参类型 形参名n)
函数体:
{
// 语句
}
函数返回值类型即函数类型。
函数头中,函数没有返回值时,函数返回值类型处应为void。函数没有形参时圆括号内应为void。圆括号内定义了一个或多个形参,如果有同类型的,也必须在每一个形参变量名前加类型名并用逗号隔开,不能多个同类型的形参变量共用一个类型名称(就行普通变量的定义那样:int a , b , c
)。函数头后面不带分号。
定义带形参的函数时,并没有创建形参变量,直到调用这个函数时,才创建了形参变量,同时初始化,即将实参赋值给形参变量。
void fun(int a , int b , int c) // 正确的函数头
void fun(int a , b , c) // 错误的函数头
函数如果有返回值,那么类型以函数头中定义的类型为准。如:
int fun(void);
int main(void)
{
printf("%d\n" , fun());
return 0;
}
int fun(void)
{
float a = 1.0f;
int b = 2;
float c;
c = a / b;
return c;
}
结果:
0
这里c求出来被应该是0.5,但被赋值给int类型,截断之后为0。
函数之间的通信
带形参的函数要和主调函数建立通信,没有形参的函数不需要和主调函数之间建立通信。
信息从主调函数到被调函数:传参
信息从被调函数到主调函数:return 语句
形参:formal parameter
实参:actual argument
形参是局部变量(local variable),属于该函数私有。函数传参就是给形参赋值。
实参可以是常量、变量或表达式等等,首先要对实参求值,再将该值拷贝到形参中,形参中的值是实参的副本。所有无论被调函数对拷贝的数据做何种操作都不会影响主调函数中的原始数据。
如果函数没有返回值即函数头是void,函数体可以没有return语句或者带这样的return语句:return;
。这种return语句只有在void函数中才会用到。
参数传递的机制根据系统的不同而不同。对于PC,主调函数将实参拷贝一份,放进栈(stack)中,被调函数从栈中读取数据。栈是一个临时存储区。实参放进栈和形参从栈中读取数据这两个过程要匹配,否则出错。
实参根据自己的数据类型决定放进栈中的数据是什么类型。被调函数以形参的数据类型从栈中读取数据。
使用ANSI C之前的函数声明时,实参只会进行自动类型转换,如short、char变为int,float变为double等。这样实参拷贝之后保存在栈中的备份的类型未必和形参的类型一致,如果不一致,形参读取数据时将发生错误。也不会检查实参数量是否和形参相等。
函数原型提供的信息有:函数返回值的类型,形参数量,每一个形参的类型。使用函数原型时,实参将根据函数原型提供的信息检查实参数量和形参是否相等,并将实参的类型转换为形参的类型,于是栈中存储的实参的副本的类型未必和实参本身类型相同,但一定和形参的类型相同,于是形参从栈中读取数据时格式一定是匹配的。这里的数据类型转换属于自动类型转换。
对支持ANSI C的编译器来说,
int fun();
被视为没有使用函数原型,将不会检查参数。为明确表明确实函数没有参数,应该使用void,即:
int fun(void);
此时编译器认为使用了函数原型,此函数没有参数,函数调用时会进行检查,以确保没有参数。
对于形参数目可变的函数,如printf()和scanf()等,ANSI C允许使用部分原型,C库通过stdarg.h头文件提供了一个定义形参数量不固定的函数的标准方法。
比如printf()函数的函数原型是:
int printf(const char* , ...);
此函数原型表明,第一个参数是字符串,还可能有其他未指定的参数。
函数的递归(recursion)
函数调用自己称为递归。递归是函数调用的特殊情况。
递归的代码中要有可以终止递归的条件测试部分,否则将会无限制的递归下去。
可以使用循环的地方通常都可以使用递归。
示例:
// 递归示例
#include<stdio.h>
void up_and_down(int n);
int main(void)
{
up_and_down(1);
return 0;
}
void up_and_down(int n)
{
printf("level %d location : %p\n" , n , &n);
if (n<4)
up_and_down(n+1);
printf("LEVEL %d location : %p\n" , n , &n);
}
结果:
level 1 location : 0061FF10
level 2 location : 0061FEF0
level 3 location : 0061FED0
level 4 location : 0061FEB0
LEVEL 4 location : 0061FEB0
LEVEL 3 location : 0061FED0
LEVEL 2 location : 0061FEF0
LEVEL 1 location : 0061FF10
程序解释:main()调用up_and_down()称为第一级递归,up_and_down()调用up_and_down()自己称为第二级递归,接着第二级递归调用第三级递归,第三级递归调用第四级递归,等等。于是,第四级递归的主函数是第三级递归,第三级递归的主函数是第二级递归,第二级递归的主函数是第一级递归,即主调函数调用被调函数,此处被调函数即递归函数。
第四级递归调用结束时,控制权转移到主函数即第三级递归调用,第三级递归调用继续执行递归代码部分后面的代码。程序必须按顺序逐级返回递归。
每级递归调用都创建变量n(包括第一级递归调用),每级递归调用的变量n都是这一级私有,名称相同但值不同。每级递归创建的变量都存放在栈中。
递归函数中,位于递归调用之前的语句按照被调函数的顺序执行,位于递归调用之后的语句按照被调函数的相反顺序执行。
尾递归(tail recursion)指:递归调用在递归函数的末尾,即刚好在return语句之前。
即第三级递归调用,第三级递归调用继续执行递归代码部分后面的代码。程序必须按顺序逐级返回递归。
每级递归调用都创建变量n(包括第一级递归调用),每级递归调用的变量n都是这一级私有,名称相同但值不同。每级递归创建的变量都存放在栈中。
递归函数中,位于递归调用之前的语句按照被调函数的顺序执行,位于递归调用之后的语句按照被调函数的相反顺序执行。
尾递归(tail recursion)指:递归调用在递归函数的末尾,即刚好在return语句之前。