数学中我们其实就⻅过函数的概念,⽐如:⼀次函数 y=kx+b ,k和b都是常数,给⼀个任意的x,就得到⼀个y值。 其实在C语⾔也引⼊函数(function)的概念,有些翻译为:⼦程序,⼦程序这种翻译更加准确⼀些。 C语⾔中的函数就是⼀个完成某项特定的任务的⼀⼩段代码。这段代码是有特殊的写法和调⽤⽅法的。 C语⾔的程序其实是由⽆数个⼩的函数组合⽽成的,也可以说:⼀个⼤的计算任务可以分解成若⼲个较⼩的函数(对应较⼩的任务)完成。同时⼀个函数如果能完成某项特定任务的话,这个函数也是可以复⽤的,提升了开发软件的效率。 在C语⾔中我们⼀般会⻅到两类函数:
C语⾔标准中规定了C语⾔的各种语法规则,C语⾔并不提供库函数;C语⾔的国际标准ANSI C规定了⼀些常⽤的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数 我们前⾯内容中学到的 printf 、 scanf 都是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要学会就能直接使⽤了。有了库函数,⼀些常⻅的功能就不需要程序员⾃⼰实现了,⼀定程度提升了效率;同时库函数的质量和执⾏效率上都更有保证 各种编译器的标准库中提供了⼀系列的库函数,这些库函数根据功能的划分,都在不同的头⽂件中进⾏了声明 库函数相关头⽂件:https://zh.c·ppreference.com/w/c/header 有数学相关的,有字符串相关的,有⽇期相关的等,每⼀个头⽂件中都包含了,相关的函数和类型等信息,库函数的学习不⽤着急⼀次性全部学会,慢慢学习,各个击破就⾏
库函数的学习和查看⼯具很多,⽐如: C/C++官⽅的链接:https://zh.cppreference.com/w/c/header cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/
了解了库函数,我们的关注度应该聚焦在⾃定义函数上,⾃定义函数其实更加重要,也能给程序员写代码更多的创造性
其实⾃定义函数和库函数是⼀样的,形式如下:
ret type fun_name(形式参数)
{
}
• ret_type 是函数返回类型 • fun_name 是函数名 • 括号中放的是形式参数,简称形参 • {}括起来的是函数体 我们可以把函数想象成⼩型的⼀个加⼯⼚,⼯⼚得输⼊原材料,经过⼯⼚加⼯才能⽣产出产品,那函数也是⼀样的,函数⼀般会输⼊⼀些值(可以是0个,也可以是多个),经过函数内的计算,得出结果
现在我们知道了自定义函数的格式,我们来简单实现一下一个加法函数,要求:给定一个函数Add,其参数为两个整型,返回值为两个整型的和,具体实现如下:
int Add(int x,int y)
{
int c = 0;
int c = x + y;
return c;
}
这样一个简单的自定义加法函数我们就完成了,我们可以试试它能不能正常使用,如图:
可以看到函数成功实现了我们的需求,再次分析一下: (1)函数Add返回值为整型,因为整型相加还是整型 (2)它的形参为两个整型x和y,而在主函数中的a和b则是实参,形参和实参可以使用不同的名字 (3)在函数体中,我们借助另一个参数c存放两个整型的和,最后使用return语句将其返回
我们使用上面的例子来讲解形参和实参:
在上图中,在主函数中使用时传递的两个整型为a和b,称为实际参数,简称为实参,实际参数就是真实传递给函数的参数
在函数Add的定义部分有两个整型x和y,它们称为形式参数,简称形参。为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化
实参和形参是有关系的,当没有使用函数时,形式参数并不会在内存中开辟空间,只有在使用函数时,将实参传递给函数,那么对应的形参就会接收实参的值,此时形参也会在内存中开辟自己的空间,所以它们的关系可以说是:形参是实参在内存中的临时拷贝
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项。
return后面跟了表达式,使得整个函数变得更加简洁了
在使⽤函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进⾏操作。 ⽐如:写⼀个函数set_arr将⼀个整型数组的内容,全部置为-1,再写⼀个函数print_arr打印数组的内容 现在我们将准备工作做好,创建一个整型数组arr,将函数预先写出,方便了解需求,后面再去写函数的代码,如图:
这⾥的set_arr函数要能够对数组内容进⾏设置,就得把数组作为参数传递给函数,同时函数内部在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给set_arr传递2个参数,⼀个是数组,另外⼀个是数组的元素个数。仔细分析print_arr也是⼀样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素 而给函数传递数组,只需要传递它的名字,如图:
数组作为参数传递给了set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢? 这⾥我们需要知道数组传参的⼏个重点知识: (1)函数的形式参数要和函数的实参个数匹配 (2)函数的实参是数组,形参也是可以写成数组形式的 (3)形参如果是⼀维数组,数组⼤⼩可以省略不写 (4)形参如果是⼆维数组,⾏可以省略,但是列不能省略 (5)数组传参,形参是不会创建新的数组的 (6)形参操作的数组和实参的数组是同⼀个数组 现在我们开始着手创建这两个函数,如图:
现在我们来看看代码能不能正常跑起来,如图:
可以看到已经实现了我们的需求
嵌套调⽤就是函数之间的互相调⽤,每个函数就像⼀个乐⾼零件,正是因为多个乐⾼的零件互相⽆缝的配合才能搭建出精美的乐⾼玩具,也正是因为函数之间有效的互相调⽤,最后写出来了相对⼤型的程序,可以简单的理解为在一个函数定义中使用了另一个函数,就叫做嵌套调用 假设我们计算某年某⽉有多少天?如果要函数实现,可以设计2个函数,一个函数leap_year用来判断这一年是否是润年,另一个函数get_days用来算出该月的天数
int arr[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
这样,arr[month]就可以取出对应月份的天数,当然我们要注意一个特殊的地方,润年2月份是29天,我们就可以使用上面的函数leap_year来判断年份是否为润年,如果是润年,那么就让原本的28天加1,如图:
最后我们看看主函数,以及最后执行的效果:
所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问 如以下例子对比:
通过对比可以看出,后面的例子直接将函数strlen作为了函数printf的一个参数,使得代码更加简洁,这就是函数的链式访问
请思考以下代码的输出结果:
为什么会出现这个结果呢?这时我们就要了解printf的返回值了,printf也有返回值,它返回的是它的屏幕上打印的字符个数,比如:
此时第一个printf执行,在屏幕上打印了abc并且换行,然后用sz接收它的返回值,可以看到返回了4,这里换行符也算一个字符
了解清楚这个我们就可以分析上面的题目了: (1)第三个printf打印43,在屏幕上打印2个字符,再返回2 (2)第⼆个printf打印2,在屏幕上打印1个字符,再放回1 (3)第⼀个printf打印1 所以屏幕上最终打印:4321
如:
如图,函数Add定义在main函数之前,编译器不会有警告,可以顺利完成编译
此时程序还是可以正常运行,但是报了一条警告,说函数Add未定义,这是因为C语⾔编译器对源代码进⾏编译的时候,从第⼀⾏往下扫描的,当遇到Add函数调⽤的时候,并没有发现前⾯有Add的定义,就报出了上述的警告
解决方法:在main函数之前声明一下,只需要声明函数名,函数的返回类型和函数的参数,也就是除了大括号那一部分的内容,如:
这样函数就不会报错了
⼀般在企业中我们写代码时候,代码可能⽐较多,不会将所有的代码都放在⼀个⽂件中;我们往往会根据程序的功能,将代码拆分放在多个⽂件中 ⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中,如:
//add.c文件
int Add(int x,int y)
{
return x+y;//
//将函数的定义放在.c文件中
}
//add.h
int Add(int x,int y);
//函数声明放在.h文件中
随后如果我们想在test.c中使用函数Add,那么只需要包含add.h这个头文件即可,如果包含的头文件是我们自己写的,那么要用引号引起,如:
#include "add.h"
这样代码就可以正常运行了
作用域:⼀段程序代码中所⽤到的名字并不总是有效(可⽤)的,⽽限定这个名字的可⽤性的代码范围就是这个名字的作⽤域
⽣命周期:指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的⼀个时间段
extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern 进⾏声明,然后使⽤,使用方法如下:
在上面两段代码中,我们将整型全局变量定义在add.c中,当我们要在另一个文件test.c中使用它时,就要使用extern来声明它,随后可以正常使用,函数也是如此,如图:
我们在另一个文件定义了Add函数,想使用就可以用extern关键字对它进行声明,随后正常使用
static会改变所修饰函数或变量的生命周期,而不改变它的作用域,我们下面详细介绍一下
它们的运行结果有什么不同呢?这个就要将到static修饰局部变量了 代码1的test函数中的局部变量i是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存) 代码2中,test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算 以上两个代码的运行结果如下:
解析:代码1由于是局部变量,每一次调用函数结束就要被销毁,导致每一次进入函数test都从0开始,加1后就是1,循环往复。代码2由于被static修饰,此时变量i的生命周期发生了变化,变成了全局变量的生命周期,但是作用域不变,此时当函数调用结束后i不会被销毁,下一次进入函数就不会被重置为0,也就有了累加的效果 结论:static修饰局部变量改变了变量的⽣命周期,⽣命周期改变的本质是改变了变量的存储类型,本来⼀个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是⼀样的,⽣命周期就和程序的⽣命周期⼀样了,只有程序结束,变量才销毁,内存才回收。但是作⽤域不变的
可以看到变量a可以正常使用,接下来我们在定义全局变量a时加上static:
可以看到编译器直接报错了,原因就是创建在一个文件下的,并且被static修饰的全局变量,只能在该文件使用,而不能在其它文件使用 使⽤建议:如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使⽤static修饰。
可以看出只要声明了就可以使用,下面是使用了static修饰函数Add的情况(修饰函数只需要在函数的返回类型前加一个static即可):
可以看到此时代码出错了,是因为static 修饰函数和 static 修饰全局变量是⼀模⼀样的,⼀个函数在整个⼯程都可以使⽤,被static修饰后,只能在本⽂件内部使⽤,其他⽂件⽆法正常的链接使⽤了 本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个⼯程中只要适当的声明就可以被使⽤。但是被 static 修饰后变成了内部链接属性,使得函数只能在⾃⼰所在源⽂件内部使⽤ 使⽤建议:⼀个函数只想在所在的源⽂件内部使⽤,不想被其他源⽂件使⽤,就可以使⽤ static 修饰
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有