1
函数的概念可以参照数学中的y = kx + b,给一个x的值,让它计算y的值,同理,函数就是给一个执行指令,让它完成后续操作。
C语言也是由各种各样的函数组成的,函数利用的好,程序执行效率不免会提高。
2
C语言中的函数分为两种函数,一种是库函数,一种是自定义函数。
在学习这两种函数之前,我们不妨学习一下如何学习一个尚未谋面的函数,现在有许多网站可以提供相应的知识点,比如cplusplus,那么假设现在我们查阅strlen怎么用,应该了解什么,就可以去这个网站上学习。
这里面就包括了函数的原型,返回类型和返回值,函数的参数,引用的头文件,当然,,慢慢看吧,全是英文哈哈哈,介绍比较费时间,就交给读者了。
我要表达的是,学习一个函数,应该从 函数原型,函数的返回类型,返回值,头文件,功能实现,参数这几个点进行了解。
好了现在介绍库函数。
1)库函数
C语言并不提供库函数,C语言的国际标准ANSI C规定了常用的函数的标准,不同的编译器厂商提供的C语言标准就给出了一系列的函数的实现,这些实现的函数被称为库函数,比如 printf scanf qsort都是库函数,库函数只要在引用了相应的头文件,直接进行使用就可以。
2)自定义函数
库函数常用的就那么几个,但是这些库函数可不能解决所有的问题,于是有了自定义函数的出现,自定义函数的创建离不开 返回类型,返回值,参数。当然,有时候会用到自己创建的头文件的。
比如,现在我要创建一个能实现三个数相加的函数,库函数没有吧?
来看看
int Add(int a, int b, int c)
{
return a + b + c;
}
int main()
{
int a = 0, b = 0, c = 0;
scanf("%d%d%d", &a, &b, &c);
int ret = Add(a, b, c);
printf("%d", ret);
return 0;
}
运行结果
对的吧?
按照学习陌生函数来看一下这个函数,int 是Add函数的返回类型,所以我们应该用int 类型的ret来接收它,int a,int b,int c是这个函数的参数,这里这三个是形参,下面的是实参,一会儿再看,return a + b + c,是这个函数的返回值,不过是把函数的功能实现和返回值放在一起的而已,神奇吧。
细心的人会发现scanf那里有警告,那么这个警告是因为scanf的返回值没有int类型的变量接受,给它一个int类型的接收就行了。
3
形参,实参,比较好理解,形式参数和实际参数。这里为了好区分,我们把实参换成m,n,z
int Add(int a, int b, int c)
{
return a + b + c;
}
int main()
{
int m = 0, n = 0, z = 0;
scanf("%d%d%d", &m, &n, &z);
int ret = Add(m, n, z);
printf("%d", ret);
return 0;
}
m,n,z是函数的实际参数,那么函数的形式参数就是用来接受实际参数的地址或者是值的
由上上面的代码可以发现,形式参数和实际参数的名字是可以一样的,这是因为内存给它们的函数栈帧不同,即空间不同,名字一样无所谓啦。
那么
为什么a,b,c是形式参数呢?因为如果你不调用这个函数的话,这几个数实际是不存在的,没有向内存申请空间。不信?看看
你看,在还没执行到这一步的时候,a,b,c是没有申请空间的。
执行了之后,a,b,c,有了自己的空间,只不过是还没有接收到实参的值而已。
这是已经实现的这个函数,又重新进入到主函数里面了,但是可以发现a,b,c的值变灰了。
这是因为形参用完了,然后像内存申请的这块空间就被释放了,所以它们不存在了。
那形参和实参是同一个数吗?辨别的方法很容易,看看地址就行了。
看吧,地址也不一样,所以我们常说,形参是实参的一份临时拷贝。这是传值的情况下,
传址的时候,形参就可以操控实参咯。
当然,形参和实参的个数一定要相等。
4
return语句,没错,就是刚才那个实现了函数功能和返回值一起的东西。
关于return记住这几点就行
1) return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执行表达式,再返回表达式 的结果。
2)return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
3)return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
4)return语句执⾏后,函数就彻底返回,后边的代码不再执行。
5)如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
第一个,比如刚才实现的Add函数,我们不是要三个数相加的结果吗,那么我们让return帮我们实现计算了,就不过多的写多余代码了。同理,代码实现到最后我们要实现加减乘除,也可以直接用return语句了。
第二个,因为返回类型是void的话,就代表没有返回值,那么直接return;也是可以的。
第三个,比如返回类型是int,你返回一个a,那么返回的值就是97,上图
字符a的ASCII的值是97,所以ret接收到的就是97咯。
第四个,return代表的就是这个函数执行完毕了,后续当然不会执行了,来看看
第五个,一个程序要结束,就要保证每个情况都有返回值,话不多说,直接上图
vs里面如果没有返回值的话就默认返回1了,在其他的编译器就会报错了。
5
先看一个对数组概念混淆的例子
这串代码是想把数组的所有元素改成1 ,然后进行打印,可是为什么只打印了两个数?
你又再看,sizeof(array)hi不是有警告,且听我一一道来,这是因为数组传参不同于刚才的Add函数,Add函数传的是值,数组传参传的是地址,也就是说,修改XY函数里面的数组会让主函数数组的值得到改变,那么为什么sz等于2呢?
因为传的是地址,地址是指针,在64位环境下,指针的值是8,8 / 2 肯定就是4咯,那个警告其实就是说咱们用另一个值除指针的大小值。
那么我们该怎么在自定义函数里面求得数组的大小呢?既然传的是地址,就免不了使用指针,所以这里不进行介绍,最好的办法就是在主函数里面求完了,在传过去。
如图
看,好了。谨记数组传参传的是首元素地址。
所以数组传参的时候是不会创建一个新的数组的,要是创建一个新数组,对内存的空间浪费多大呀。
6
1)嵌套调用
嵌套嘛,无非就是一个函数里面包含其他函数,嵌套实在是太常见了。
void XY(int array[],int sz)
{
for (int i = 0; i < sz; i++)
{
array[i] = 1;
}
for (int i = 0; i < sz; i++)
{
printf("%d ", array[i]);
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
XY(arr,sz);
return 0;
}
就比如这个,XY函数里面调用了printf函数,这也是一种调用,所以不做过多介绍。
当然,函数是可以嵌套调用的,但是不能嵌套定义,就像一个人不能同时拥有两个名字一样。
2)链式访问
链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数 的链式访问。
比如
strlen的返回值作为printf的参数,就是一种链式访问。
那么来看一段有意思的代码。
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
来看看
这是因为printf的返回值是成功写入的字符数。最后一个printf有两个字符,4 3,所以返回值是2,第二个有一个字符,1,所以返回值是1,那么第一个printf打印出来的就是1,所以最后的结果是4321。
7
红色的部分就是函数的定义,绿色的部分是函数的调用,没错,函数的定义就是整个函数的执行过程。
欸?那函数的声明呢?
因为我把自定义函数放在main函数的前面了,这也算是函数的声明了,虽然程序是从main函数开始的,但是编译器会从第一行开始“扫视”,在函数调用前看到了这个函数,都算声明了。
当我把自定义函数放在下面,就需要先声明了,只要声明的位置在函数调用之前就行,
蓝色是函数的声明,红色的是函数的调用,绿色就是函数的定义。
函数的声明其实就是写清楚函数的返回类型,参数就可以了。
当然,实际的多文件运用中,函数的声明一般放在头文件,函数的实现一般放在源文件里面,最后引用一下自己创建的头文件就可以了。
比如你可以试一下能实现加减乘除的代码呢?在使用多个文件的情况下。
8
1)extern
extern也是关键字,是声明外部符号的,比如我在A文件里想要使用B文件的内容,就可以用到extern。
这样程序就可以执行了,最后打印2024。
在介绍static之前,需要解释一下生命周期和作用域。
作用域是 某个变量只能在某一块代码块使用的那块代码块,局部变量的作用域是该局部变量所在的局部范围,全局变量的作用域就是整个程序。
生命周期是指变量从创建到销毁所消耗的时间,局部变量的声明周期是创建到销毁,全局变量的生命周期是整个程序的生命周期。
2)static
i)static修饰局部变量
void test()
{
static int i = 1;
printf("%d ", i);
i++;
}
int main()
{
for (int i = 0; i < 10; i++)
{
test();
}
return 0;
}
不难发现,有了static之后,局部变量i的生命周期,让它从栈区转到了静态区,静态区的变量的生命周期和全局变量是一样的,所以每次进入函数test之后,i不会随着函数使用结束而销毁,所以i会自增下去。
ii) static修饰全局变量
打印!
然后就报错啦。
同理,static改变了val的生命周期,变成了只能在该源文件使用的变量,本质是因为全局变量是有外部链接属性了,使用extern就可以在其他文件使用了,可是static使外部链接属性变成了内部链接属性,所以就报错咯。
iii) static修饰函数
这个!对!和全局变量是一样的,偷懒了,不写了哈哈哈。
同样只能在当前的源文件使用,没有外部链接属性了,没了。
感谢阅读。