C语言中函数的基本知识

2022-09-14 08:24:00 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

接着上次的数组,这次我们来简单的讲讲C语言里面的函数。 函数和指针这两大块,在C语言中占据着重要的位置,是C语言中的主体和核心,所以它们的重要性也就不言而喻了。

那什么是函数呢? 1:函数是C语言的模块,一块块的,有较强的独立性,可以相互调用,也就是说,你可以在函数A中调用函数B,又可在函数B中调用函数C,不仅如此,你还可以调用函数自身(递归)。 2:函数是完成一个个特定任务的语句集合,它能完成你所想要的某种特定任务,当你要用时,只需要调用它即可,在后续的修改或是维护过程中,只需要针对这一个进行修改即可。 打个比方来理解函数:

在一个饭店里面,顾客点了一盘菜后,服务员把该菜的名字给后勤,他负责洗菜这项功能,完毕后把菜交给配菜的师傅,待菜切好后,把菜交给厨师,厨师负责炒菜,完毕后,服务员把菜端给顾客。

在上面那个比方内,有四个不同的,独立的角色:服务员,后勤,配菜师傅,厨师。 他们每一个只是负责给他们指定的任务: 服务员相当于是 main() 主函数,他是整个过程的开始,主函数也被称作 程序的接口,我们写的代码都是首先从这里执行下去的。 后勤,配菜师傅,厨师, 相当于三个不同的函数,他们执行各自的功能,互不影响,并且哪里出了问题,很快就可以确定问题的位置。

假如顾客吃到的菜有一只青虫,那我们基本可以确定出问题(bug)的是在后勤部分。

那为什么需要函数呢?

1:在我们写代码的时候,有时候会重复写一段代码,而这段代码所执行的功能,操作是一样的,只是针对的数据不一样,这个时候,将这段功能写成一个函数模块,在需要用到的时候调用即可,进而避免了写重复的代码,避免了重复性操作。 2:方便代码的维护,当我们知道哪个部分出问题后,或者需要修改某个功能,那就只需要修改那部分的代码即可。 在上面饭店的比方中,我们一下子便可知道吃到青虫时,是哪里出了问题。

这里,我将上面那个例子,写成了一个点单的小程序,将洗菜配菜做菜分别写成了三个不同的函数来演示一下:

在主函数体外面:

这三个分别是独立出来的,完成他们各自的功能。互不影响,相互独立。 若是不写成函数,那么直接在 case语句 后面分别写三条 printf ,那将会显得冗长,且如果有部分除了差错,那修改的将是三个,麻烦。


库函数和自定义函数 1: C语言为我们提供了上百个可调用的库函数,例如与字符串有关的 strlen, strcat, strlwr . 或是我们刚接触C语言时候用到的 printf, scanf, 这些都是c语言为我们提供的。在我们使用某一库函数的时候,需要在程序中嵌入(#include<>) 该函数所需要的头文件。 这也就是为啥我们在代码开头都需要写上#include <stdio.h>,因为 printf, scanf,getchar,gets,putchar()这些函数 (也称作标准I/O函数),都是在stdio头文件中。 2:自定义函数就是我们自己写的,例如我上面那个 chef(), coord()


函数的定义 这里记住一点即可:当你函数定义写在主函数之前的时候,那么就无需在主函数里面声明这个函数,只需要把函数体写在主函数前即可:

而若我将exp()写在下面:

那编译器将提示错误:

如果要写在主函数下面,我们则需要告诉编译器,有这个函数的存在:


函数按照是否有参数分为有参类型和无参类型 , 按照是否有返回值也分为有返回值和无返回值两种类型。

有参函数 指的是在该函数被调用的时候,主调函数通过参数向里面传递了数据。 无参函数 很好理解就是不传递数据,因为没有参数,无法获得值。 这里着重介绍下 有参函数 它的定义一般是:

代码语言:javascript复制
函数数据类型 函数名字(参数1, 参数2, 参数3,... , 参数n)
{ 
   
   语句块;
}
//函数数据类型可以是 void (空类型,不带返回值的), int, float, double, 指针类型(int *, char *s)
//参数,根据自己的需要来,你如果想传递浮点类型,那么就需要定义成浮点类型的数据类型(flaot, double)

举例:
//返回两个数字最大的一个
int max(int n1, int n2)
{ 
   
   return ((n1 > n2) ? n1 : n2);
}

//计算三个数字之和
int sum(int n1, int n2, int n3)
{ 
   
    return n1   n2   n3;
}

//计算字符串长度
int _strlen(char *str)
{ 
   
    int l = 0;
    while(*(str l) != '' &&   l) ;
    return l;
}

在我们自定义的函数里面,括号里面的参数叫做形式参数(形参),而在主调函数里面,括号后面的参数叫做实际参数(实参),它可以是常量,变量,表达式 例如你在主调函数调用max函数的时候可以:

代码语言:javascript复制
_max = max(12, 43);        //二者都是常量
_max = max(n1   n2, 4 * 5);  //n1 n2为变量,进行加法操作也是一个表达式

在调用函数的时候,需要知道以下几点: 1:在调用的时候,主调函数向参数传递的是值,在调用结束后,该值不改变。 2:函数只有被调用,系统才会给它分配内存单元。 3:调用结束后,系统给他分配的内存单元立即被释放,而主调函数中的值不会改变 4:主调函数传递给被调用函数值后,被调用的函数里面可以直接用该值,但是要记住,值的数据类型要和参数的数据类型一一对应。

第一点: 函数再调用的时候,发生的是 值传递,也就是把值拷贝一份给形参,在形参里面改变其值并不影响实参的值<这里的值是单向传递的:实参 -> 形参 >:

在 exch() 函数中,我试图改变n的值,但是却没有改变,就是因为我仅仅只是把 n = 46 这个值给拷贝一份给了 exch() 函数,在此时的exch()函数中,它里面的n就有了 46这个值 :

对于第四点: 在数据传递的时候,若是数据类型不同,有可能会发生数据丢失等错误,例如:

这里的数据发生了截断。


函数的传递不仅有传值,也有传址的操作。

传值就是直接用一个变量去存储那个值 传地址就是用一个变量(指针变量)去记住那个地址

这里需要了解一个知识点: 1:数组名的值,就是一个一个指针常量。也就是第一个元素的地址,它的类型取决于数组元素的类型:如果它们是int类型,那么数组名的类型就是 “指向int的常量指针”; 为啥说是常量,因为它在定义的时候系统已经给它分配好了内存地址,也就是说,已经是无法更改了。常量是不能被更改的量。 2:如果你要在函数中,要改变主调函数中某变量的值,那么就要把地址传递给形参, 如果不需要改变,直接传递值的拷贝即可。

结合1,2两点可以理解为啥我们把数组名字作为实参传递进函数的时候可以改变原数组的值。 数组的传递的方式:

代码语言:javascript复制
int sort(int arr[], int n);           //一维数组
sotr(arr, 10);   					    //调用

int sort(int *arr, int n);            //跟一维数组一样,指针的方式定义的
sort(arr, 10);

int sort(int arr[][4], int l, int r); //二维数组,需要告诉每个元素的数组长度
sort(arr, 4, 4);

有同学可能有疑问了,值传递和址传递有区别嘛? 其实是没的,两者可以说是一样的,因为址传递,传递的是地址,地址这个值,由形参里面的指针变量存放着。

看下面一个易错的例子:

代码语言:javascript复制
void swap(int a, int b)
{ 
   
	int t;
	t = a;
	a = b;
	b = t;
}

我若调用

代码语言:javascript复制
swap(a, b);  //a = 2, b = 3;

在主函数中,调用swap函数后,输出的结果是啥? 答案依然是2,3 为啥呢? 因为我仅仅只是把 a = 2, b = 3 这个值给拷贝进了swap的形参中去,不影响主调函数中的变量的值。 根据要注意的点中,第二个要点,我想在被动用的函数中就改变主调函数中参数的值,那么我就需要把地址传递进去,也就是进行址传递,那么我函数的定义就需要借助指针:

为什么会如此呢? 因为我把变量的地址给传递进去了后,形参中的指针变量对地址进行的操作,就会影响该内存空间的值,进而反映到主调函数中去,也就是发生了改变。

本次涉及到了指针的知识,指针又是一个比较难理解的地方,要理解指针,就需要的内存地址有一个深刻的理解,在学习指针的时候,可以用纸和笔在纸上勾画下 数据方框和地址方框,用箭头来表示指针指向地址方框来理解指针。 函数后面还有更加复杂的递归函数,一句话来概括递归就是:递归是函数调用自身的过程。 这里就不多做解释,因为我也并不是对递归有一个深刻的理解,所以也无法讲清楚来。

<如有错误请指出> Gakki

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/148800.html原文链接:https://javaforall.cn

0 人点赞