针对在校大学生的C语言入门学习——函数
为什么要使用函数
- 如果你没有接受过系统的训练就能主动想要封装函数,说明你在编程上是一个天才,千万不要辜负自己的天赋!函数在语法上并不难理解,但是什么时候使用函数才合理呢?
- 我们不妨来做一个猜谜游戏——“手持筷子将食物放入嘴中,然后用牙齿将嘴中的食物咬碎,咬碎到可下咽的程度后将食物咽下。期间可以用舌头感知食物的味道。”猜一猜这是在干什么?
- 虽然我描述的还不是很到位,但是我想还不是难猜的,答案就是“吃饭”。我们可以认为“吃饭”这个词就是对上述一系列行为的“封装”。在我们日常说话交流的时候,我们会选择说“吃饭”还是每次都将上述一大段行为赘述一遍呢?答案是显然意见的。在人类的语言中有很多很多这样的词用来对一系列行为做“封装”,目的就是为了使我们的语言交流更加简洁。
- 在编程语言中,我们使用“函数”来对一系列行为做封装。目的就是为了让程序的逻辑表达更简洁清晰。熟练的时候函数后你会发现,不仅逻辑表达更加简洁清晰,对于代码的可维护性、开发的便捷性都有很大帮助。
形参和实参
- 同学们还记得初中时学的三角函数吗?也许你们上学的时候已经不是初中学习三角函数了……sin(θ)这个正弦函数应该还有印象吧,对于这个函数它是封装了求θ角度正弦值的算法。θ是多少呢?不知道,在这里θ出于逻辑表达的一种需求而存在的,所以θ我们叫形参。sin(30)这样写的话就是确定求30度角的正弦值,所以我们30我们叫实参。顺便说一下,sin(30)的结果0.5就是函数的返回值。
- 下面是我对sin函数的一个定义
float sin(float a)
{
//函数体
}
在这里float a就是形参,下面代码是调用这个函数
int main()
{
sin(30);
return 0;
}
在这里30就是函数sin的实参。
- 在这里我想针对形参强调一个问题,就是形参它也是一个变量,它的生命周期以及作用域与在函数中定义的局部变量是一样的。它和局部变量的区别就是形参必须在调用函数的时候使用实参初始化。
函数示例
- 让我们以一个排序的例子来感受一下使用函数的妙处。下面代码我要分别对两个整数类型的数组做升序排序,使用冒泡排序法。
- 先看不使用函数的写法
int main()
{
int arr1[5] = {12,34,6,67,9};
int i, j;
for(i = 0;i < 5-1;i++)//对数组arr1排序,冒泡排序法
{
for(j = 0;j < 5-1-i;j++)
{
if(arr1[j] > arr1[j+1])
{
int temp = arr1[j];
arr1[j] = arr1[j+1];
arr1[j+1] = temp;
}
}
}
for(i = 0;i < 5;i++)//打印数组arr1的排序结果
{
printf("%d ",arr1[i]);
}
printf("\n");
int arr2[10] = {54,6,789,5,3,78,4,2,34,8};
for(i = 0;i < 10-1;i++)//对数组arr2排序,冒泡排序法
{
for(j = 0;j < 10-1-i;j++)
{
if(arr2[j] > arr2[j+1])
{
int temp = arr2[j];
arr2[j] = arr2[j+1];
arr2[j+1] = temp;
}
}
}
for(i = 0;i < 10;i++)//打印数组arr2的排序结果
{
printf("%d ",arr2[i]);
}
printf("\n");
return 0;
}
- 如果你觉得这样写挺好的呀!那只能说明我代码写的可读性太强了……,下面请看封装函数的写法。
#include <stdio.h>
void sortArr(int* arr, int len);
void printArr(int* arr, int len);
int main()
{
int arr1[5] = {12,34,6,67,9};
sortArr(arr1, 5);//数组arr1排序
printArr(arr1, 5);//打印数组arr1的排序结果
int arr2[10] = {54,6,789,5,3,78,4,2,34,8};
sortArr(arr2, 10);//数组arr2排序
printArr(arr2, 10);//打印数组arr2的排序结果
return 0;
}
void sortArr(int* arr, int len)
{
int i, j;
for(i = 0;i < len-1;i++)
{
for(j = 0;j < len-1-i;j++)
{
if(arr[j] > arr[j+1])
{
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
void printArr(int* arr, int len)
{
int i;
for(i = 0;i < len;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
- 使用函数的写法,main函数中的逻辑是不是看起来更加简洁清晰呢。我把排序和打印数组各自封装成函数,在需要排序个打印的地方分别调用函数就可以了,省去了相同的逻辑反复写的麻烦。请大家仔细对比上面两段代码,一定会发现使用函数真香!
- 到这里可能有些同学对上面代码的大概意思懂了,但是对我的函数封装还是看不懂,因为我在参数中使用了指针变量。关于指针变量我想在下一次和大家分享。这一次就只是想分享使用函数的意义。
变量的生命周期
- 变量的生命周期就是变量创建和删除的时机。创建变量我想大家都明白,不创建怎么使用呢。为什么还要删除呢?因为变量是占内存的,如果变量只创建不删除,那么要不了多久内存就爆满了!
- 但是好像我们之前从来没有提过删除变量的事,难道之前写的代码中的变量都没有被删除吗?事实上已经被删除了。下面带大家来分析变量的生命周期。提到变量的生命周期,必须得知道内存是分区的,其中堆、栈、静态区是可以定义变量的分区。天呐!仿佛一大波枯燥的理论正在路上!莫慌,真的很简单。
栈:定义在函数中的变量都在栈空间,栈空间的变量在作用域开始执行的自动创建,在作用域执行完毕后自动删除。我们可以暂时把作用域理解为函数。
堆:使用函数malloc创建的变量在堆空间,使用函数free将其释放。堆空间的变量生命周期自定义。
静态区:全局变量或者使用static修饰的变量在静态区,静态区的变量在程序开始执行的时候自动创建,在程序执行完毕后自动删除。 - 对比堆、栈、静态区不难发现它们的区别是对变量生命周期的管理方式不一样。因为我们之前写的代码都是在栈空间创建的变量,所以不用为它们什么时候删除操心。
变量的作用域
- 变量的作用域就是变量的访问范围。这里我不想赘述书上的概念。当然如果把一个函数当成一个作用域行不行呢?其实没什么大问题,但是不够精确!在C语言中每一对花括号就是一个作用域,这句话大家可以自行尝试,相信会给你带来一些收获。
- 个人的编程经验是尽量把变量定义在小的作用域中,这样就可以尽快的释放内存。比如本来能局部变量非要定义成全局变量,有句话叫“占着茅坑不拉屎”就是说这个。
总结
- 函数的封装是没有固定的套路的,函数的封装很见一个人的编程功力。就像同样是说中国话,有的人能写诗,有的人作文不及格。建议大家多阅读优秀的代码来提升自己的功力,切忌盲目自满。