首先,什么是函数?函数(function)是完成特定任务的独立程序代码单元。在现实生活中,为了解决某一个问题,我们需要把解决这个问题的大任务分解成单个单个的小任务。因为可能在这个任务里面有很多事重复类似的任务,对这种重复的任务我们只需要共用一个方法去解决即可。
这样的方法同样适应计算机里的任务,在C语言里面就提供了这样的方式,就是用函数来代替处理重复任务的方法。
函数的目的也就解决一个小任务,可以说函数算是C语言处理的最小单位了。
用一个一个的函数组合起来就能解决这个大的任务。可以说函数就是积木。我们为了搭建出下面这个房子,用了不同形状的小积木块,这些小积木块 如:、 构成这么大的积木。
当然我们还可以利用小的积木,拼成的二级小积木当成一个模块使用,就比如
。
为什么要使用函数?首先,使用函数可以省去编写重复代码的苦差。如果程序要多次完成某项任务(复用性),那么只需编写一个合适的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序中使用putchar()一样。其次,即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高了程序代码的可读性(扩展性),更方便后期修改、完善。
像下面的代码就是较为恰当的定义和使用函数,就像 show_n_char() 函数是放在主函数之前的一个声明,也叫函数原型,这个能告诉编译器怎么去使用这个函数,函数原型提供了函数外部的所有接口信息,函数的实现其实就可以放在其他地方就行了。
对于函数原型可以有两种形式
就行下面的代码里面说的
// 函数原型方式1
void show_n_char(char ch, int num);
// 函数原型方式2
// void show_n_char(char, int);
//主函数
int main()
{
show_n_char('d', 5);
return 0;
}
//函数定义
void show_n_char(char ch, int num)
{
printf("ch: %c, num: %d", ch, num);
}
一个完整的函数总是由 这几部分构成:
返回值 函数名(函数参数)
{
函数体
}
以下有几个指的注意的事项:
一个函数可以理解为一个小的模块,一个黑盒子,函数也许需要外部输入、也可能需要输出东西,或者不输出不输入调用即可更改某些属性。这种表现形式完全是由开发人员决定的。
比如需要输入输出的一个函数
int MultipleNum(int num1, int num2)
{
return (num1+num2)*(num1+num2);
}
只有输出的函数
int MultipleNum()
{
return (12+12)*(14+14);
}
或者无输入输入
void MultipleNum()
{
printf(num: %d", 12+12)*(14+14));
}
int MultipleNum(int num1, int num2)
{
return (num1+num2)*(num1+num2);
}
以上面的例子说,函数参数其实就是指的 int MultipleNum (int num1, int num2) 里面的 (int num1, int num2)
函数参数是不定的,括号里面可以没有东西的话可以写成 像这种 ()、(void)
多个参数就需要用 ‘,’ 逗号分隔,每个参数都是有类型名和变量名,这个变量名也叫形参,这个变量是后面能参与到函数体里面去使用的。当然了,也不是说形参变量定义了就必须被使用哈,这些都是不确定的啦。
这个没啥特殊的,命名反正要体现出你需要的那种格式,而且命名不能过长。
函数的返回值也是可有可无的,这个也是由使用者确定的。
在不需要返回值的时候就要写 void ,同时在函数体中可有不写return 语句,或者写 return;
像下面两种的写法都是对的。
void MultipleNum(int num1, int num2)
{
//....
return ;
}
void MultipleNum(int num1, int num2)
{
//....
}
程序栈是支持函数执行的内存区域,通常和堆共享。也就是说,它们共享同一块内存区域。程序栈通常占据这块区域的下部,而堆用的则是上部。
下图就简单说明程序栈堆的模型
调用函数时,函数的栈帧被推到栈上,栈向上“长出”一个栈帧。当函数终止时,其栈帧从程序栈上弹出。可以理解为直到main函数从程序栈里弹出后,程序就结束了。
- 栈帧所使用的内存不会被清理,但最终可能会被推到程序栈上的另一个栈帧覆盖。
有时候我们听到的一个术语:栈溢出
可以理解为程序栈一直压栈,导致程序的的内存需要超过了真实物理内存,然后程序奔溃;我了解的情况就是有几个:
示意图:
栈帧由以下几种元素组成。
返回地址
函数完成后要返回的程序内部地址。
局部数据存储
为局部变量分配的内存。
参数存储
为函数参数分配的内存。
栈指针和基指针
运行时系统用来管理栈的指针。(我们写代码不用管,系统帮我们来完成)栈指针通常指向栈顶部。基指针(帧指针)通常存在并指向栈帧内部的地址,比如返回地址,用来协助访问栈帧内部的元素。这两个指针都不是C指针,它们是运行时系统管理程序栈的地址
注意要点:
float average(int*arr,int size){
int sum = 0;
printf("arr:%p\n",&arr);
printf("size:%p\n",&size);
printf("sum:%p\n",&sum);
for(int i=0;i<size;i++)
{
sum+=arr[i];
}
return(sum*1.0f)/size;
}
这个代码中,在函数栈里分配空间顺序是: size–>arr–>返回计算值地址–>sum.
在实际程序里面的层层调用也是按照下面这些形式,比如在 main函数中引入了 a(),a() 引用了 b()。
实际的执行情况就按照下面的 1 2 3 4 5 6 7 8 9 顺序运行的。
C允许函数调用它自己,这种调用过程称为递归(recursion)。
递归有时难以捉摸,有时却很方便实用。
结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。无限递归的归属就是程序栈溢出崩溃。
可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁,但效率却没有循环高。
因为函数指针和指针部分是有重叠的,函数指针与内存的一些分布有关系,感兴趣可以参考一下 我之前的笔记
指针与函数