一、函数的概述
一个源程序文件可由一个或多个函数(非主函数)组成。从main()开始,调用其它函数后回到main()结束。
分类:
(1)标准库函数和用户自定义函数---用户使用角度
(2)无参函数和有参函数---函数参数角度
(3)有返回值函数和无返回值函数---函数返回值角度
二、函数的定义
定义形式:返回值类型 函数名(参数列表){函数体}
举例:如下。但是切记没有多返回值类型函数,因为在函数栈帧问题上我们会了解到:我们只有一个eax寄存器,所以c语言的函数只能有一个返回值。
//无参无返回值类型
void printStar()
{
printf("********************\n");
}
//有参无返回值类型
void printChar(char ch)
{
for(int i=0;i<10;i++)
printf("%c",ch);
printf("\n");
}
//无参有返回值类型
int getNumber()
{
int number=1;
return number;
}
//有参有返回值类型
int getTail(int number)
{
int tail=number%10;
return tail;
}
//多参数类型
int max(int a,int b)
{
int m=(a>b? a:b);
return m;
}
Tips:
(1)参数列表是形式参数,我们只是将实际参数的值赋予了形式参数,形式参数在函数被调用时在栈区开辟空间,调用结束后,函数出栈,形参被销毁。即,形参改变不能影响实参的值。
(2)参数列表是一参一类型,不能定义为int x,y,z;且在参数列表中的变量相当于局部变量,无法在函数体内再次定义。
(3)函数返回值:通过return 语句返回主调函数,return语句可以有0-n个(在分支语句中需要返回的值不同,return的个数也不同),但每一次调用只能有一个起作用,并且return返回的数据的数据类型与函数类型必须一致或能够隐形转换。函数无返回值,函数类型为void。
三、函数的调用
调用的一般形式:函数名(实参列表);
例:max(a,b) getchar() ......
int max(int a,int b)
{
return (a>b?a:b);
}
void main()
{
int a,b,m;
scanf("%d%d",&a,&b);
m=max(a,b);
printf("%d\n",c);
}
1.对被调函数的声明
运行函数的条件
(1)被调函数必须存在
(2)被调函数为库函数,用#include 命令包含库函数所在头文件
(3)被调函数为用户自定义函数,且被调函数必须放在主调函数后面时:在主调函数中必须包含对被调函数的声明。
(4)被调函数在其它.c文件中,需要使用extern关键字声明。但前提是被调函数在其他文件定义/声明时时未使用static关键字。
(5)函数不能嵌套声明,只能嵌套调用
省略声明情况
(1)在所有函数调用之前,对本文件所调函数进行了类型声明。
(2)被调函数在主调函数前定义
(3)被调函数类型为int---不提倡省略
2.调用方式
函数语句
printf(......);
函数表达式
int m = max(a,b);
int m = max(a,b)+1;
函数作参数
printf("%d\n",max(a,b));
int m = max(max(a,b),c);
四、函数的嵌套调用(链式调用)
像循环一样,函数可以嵌套使用。嵌套调用的意义在于被调函数需要多次使用同种功能,那么这时候这个功能就可以提取出一个函数,被调函数在调用这个函数时成为了主调函数,依次类推,这就像一条链子一样,层层调用的情况就是链式调用。如果被调函数调用多个函数,被调用的函数也调用了一个或多个的函数,那么就是这就是嵌套调用。但不可互相调用,否则将会出现循环调用,无穷无尽,导致内存溢出的情况。
举一个简单的例子就是三个数求最值,调用两个数求最值:
int Max2(int a,int b)
{
return (a>b?a:b);
}
int Max3(int a,int b,int c)
{
int m = Max2(a,b);
return (m>b?m:b);
}
五、函数的递归调用
递归的思想:为了解决当前问题F(n),就要解决F(n-1),而F(n-1)的解决依赖于F(n-2)的解决...就这样逐层分解,分解成很多相似的小事件,当最小的事件解决完后,就能解决高层次的事件。这种“逐层分解,逐层合并”的方式就构成了递归的思想。
函数的递归调用是一种及其特殊的链式调用,不过主调函数和被调函数都是自身。注意一点就是递归调用需要有递归公式和终止标志,不能无限调用。
来吧,看一个例子:求斐波那契数列的第n项
long Fib(int n)
{
if (n <= 2)return 1;
return Fib(n - 1) + Fib(n - 2);
}
//测试
int main()
{
int n;
scanf_s("%d", &n);
long f = Fib(n);
printf("%ld\n", f);
return 0;
}
六、函数涉及的变量问题
1.数组/指针作为函数的参数
2.变量的存储类别
(1).作用域角度:局部变量和全局变量
局部变量:在函数内部或者块内定义的变量,局部变量只在它的函数内部或者块内有效。每个函数中定义的变量,只在定义它的函数中有效。不同函数可以使用相同名字的变量,互不干扰且意义不同。形式参数就是局部变量。Tips:可以在复合语句中定义变量,但只在本复合语句内有效。
全局变量:又称外部变量。在函数之外定义的变量,作用范围是从变量定义的位置开始到程序结束。(本文件定义的非静态全局变量,可以被同一个文件夹的另一个文件通过extern声明使用)
“就近”原则:外部变量与局部变量名称相同时,屏蔽外部变量,使用内部变量,但外部变量依旧存在。
(2).生存周期角度:动态存储变量和静态存储变量
静态存储变量:程序运行期间分配的固定的存储空间
动态存储变量:根据需要动态分配的存储空间
*动态局部变量:(自动变量)[auto]函数调用后,变量值不予保留,自动销毁,释放存储空间,再次调用时,原值不能引用。此变量在定义时未赋初值,则随机赋值,极其不稳定。一般对这种变量,建议初始化。
*静态局部变量:[static] 程序编译初期就已经开辟了空间。函数调用后保留原值,存储空间在程序结束时释放,再次调用时,仍是原值。此变量在定义时未赋初值,则自动置零((int)0,(double)0.0,(char)'\0'......);
*静态全局变量:[static] 被static修饰的全局变量,将无法被本文件外的文件使用。
(*).内部函数和外部函数
根据函数能否被其它源文件调用,将函数分为内部函数和外部函数。
内部函数:static 返回值类型 函数名(形参列表){函数体};
外部函数:[extern] 返回值类型 函数名(形参列表){函数体};
*七、函数栈帧
函数栈帧小节内容转自Raizeroko-CSDN博客;
从表面来看函数调用的过程就是写出一个函数后,只需要在调用时中通过函数名将实参传给形参就实现了整个过程,但实际上调用的过程远比你想的复杂,这其中函数栈帧起着关键作用。
在C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。为此,我们先来看一下计算机的内存分区:
内存主要分为栈区、堆区、静态区和其他区域。
栈区:由高地址往低地址增长,主要用来存放局部变量,函数调用开辟的空间,与堆共享一段空间。
堆区:由低地址向高地址增长,动态开辟的空间就在这里(malloc,realloc,calloc,free),与栈有一段共享空间。
静态区:主要存放全局变量和静态变量。
栈:一种先进后出的数据结构。涉及的两个过程分为压栈和出栈。
esp,ebp,eax寄存器( esp和ebp是两个指针,ebp指向当前栈帧栈底,esp指向函数栈栈顶)
ebp | ebp是基址指针,保存调用者函数的地址,总是指向当前栈帧栈底 |
esp | esp是被调函数指针,总指向函数栈栈顶 |
eax | 累加器,用来乘除法,与函数返回值(本篇主要关注第二个功能) |
ebp并不是指向整个函数栈的栈底,而是指向当前栈帧的栈底,而由于esp总是指向栈顶,且栈只允许一个方向的操作,因此esp指向其实也是当前栈帧的栈顶,不过当前栈帧的栈顶始终与栈顶相同,因此说esp指向的是栈顶。
函数执行的全过程略(...),有需要的读者可以看本小节标注转文章。
感谢观看!