五.函数
1.函数的基本信息
https://beryl-licorice-3a8.notion.site/8dbf5aa24aa543108a5587685f076b2a?pvs=4原文地址 观看效果更佳
C语言中的函数(function)就是一个完成某特定的任务的一小段代码,也可也翻译为子程序。这段代码是有特殊的写法和调用的方法的。
C语言的程序就是由无数小函数构造的。
函数的好处是什么? 提升开发效率
- 拆分任务
- 可以复用
在C语言中有两类函数:库函数和自定义函数
- 库函数就是现成的,可以直接使用的函数
- 自定义函数就是自定义创建使用的
2.库函数
2.1标准库和头文件
C语言标准中规定了各种语法规则,但C语言不提供库函数。但是C语言规定了一些库函数的各种信息
scanf - 名字 - 参数 - 返回类型 - 函数功能……—让C语言编译器的厂商根据规定来实现这些函数。
这导致函数的实现细节可能有差异,但对程序员来说基本无感。
各种编辑器都提供了一系列的库函数,这些库函数根据功能的不同,都在不同的头文件中进行了声明
这种分布式处理的好处在于模块化组织 减少编译时间 便于维护更新
库函数相关头文件:
2.1.2库函数文档的一般格式:
- 头文件 函数原型
- 函数功能介绍
- 参数和返回类型介绍
- 代码举例说明 代码输出说明 错误处理说明 相类似函数介绍
3.自定义函数
自定义函数其实更加重要,给程序更多的创造性
3.1自定义函数的语法形式
函数返回值类型 函数名(函数输入参数值) 函数体
int a (int nt){
}
{}函数体描述函数完成任务的过程
3.2函数的注意事项
- 函数名是自定义的,根据实际情况起名。
- 函数的个数也是根据实际情况来确定,可以有0个参数,也可也有多个参数。要交带清楚每个参数的类型是啥,形参的名字是啥。
- 函数的返回值根据实际情况,可以有返回值也可以没有返回值,void类型不返回值。
4.形式参数和实际参数
函数的使用过程中,把函数参数分为实参和形参
4.1实参
实际参数就是在函数调用时真实传递给函数的参数,实参可以是变量 常量 表达式等等
4.2形参
定义函数时,函数内部存在的参数,形式参数不能是常量
int Add(int a int b){
return a + b;
}
参数a b 就是形式上存在的参数,在函数声明的时候甚至可以省略!写成:
int Add(int int);
形式参数只有在函数被调用的过程中为了存放实际参数传递过来的值,向内存申请空间。函数调用完成后,向系统返还空间。形参获得实参值的时候,称为形参的实例化
**形参和实参的空间不相同,传递值时,形参是实参的一份临时拷贝。**对形参的修改不会影响实参。
形参和实参的参数名可以相同
5.Return语句
return后面可以是表达式也可以是常量,也可以什么都不写-适合void类型的情况。也可以不写return;
需要设定某种条件时,可以设置return;
void Printer(int n) {
if (n < 1)
return;// 函数提前返回,提前结束
int i = 0;
for (i = 0; i < n; i++) {
printf("%d", i);}
- rerurn后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
- return后面也可以什么都没有,直接写
return;
这种写法适合函数返回类型是void的情况。 - rreturn返回的值和函数返回的类型不一致,系统会自动讲返回的值隐式转换为函数的返回类型。
- return语句执行后,函数彻底返回,函数代码块结束,函数内部后面的代码不再执行。
- 如果函数存在if等分支语句,需要保证每种情况下都有return返回,否则出现编译错误。
函数的返回类型:
1.可以是char int short double….这些内置类型也可以是void,甚至可以是自定义类型
2.void的意思就是函数不需要返回值,也不返回值
3.函数需不需要返回值是根据实际的情况来设计的
函数参数:
- 可以有0个也可以有多个
- 参数的类型只要是合法的类型都行,char int double size_t……也可以是自定义类型
- 函数的参数如果是0个,也就是没有参数,函数的参数类型可以写成void,在这里void表示函数没有参数,也不需要参数
void menu() {
printf("我");
printf("\n爱");
printf("\n你");
}
int main()
{
void menu();//函数声明
menu();//函数调用
return 0;}
void menu() {
printf("我");
printf("\n爱");
printf("\n你");
{
void menu();//函数声明
menu();//函数调用
menu(1);
menu(18,8);}
进一步改进:防止声明menu(1);这种错误的传参
void menu(void) {//防止往函数里传参
printf("我");//传参会报错
printf("\n爱");
printf("\n你");
}
int main()
{
void menu();//函数声明
menu();//函数调用
return 0;}
menu() {
printf("我");
printf("\n爱");
printf("\n你");
}
int main()
{int ret = menu();
printf("%d",ret);
return 0;}
这就是上面右图打印出的结果
再说一点:不写函数类型,默认为int,且会返回一个值
并且返回的是printf中的参数数量
6.数组做函数参数
数组传参,给数组名;形参写的是数组的形式,并且在形参书写的时候可以省略元素个数。(一维数组)二维数组是行可以省略,列不能省略。数组传参进去时,形参和实参的数组共享同一个地址。
简单思考一下我想让一个函数把数组改值再打印:
#include<stdio.h>
int main(){
int arr[10] = { 0 };
set arr();//设置数组的内容的值全为-1
print_arr();//打印数组的内容
return 0;}
这里的set_arr()
函数需要对数组的内容进行设置,记得把数组作为参数传递给函数,同时函数内部也应该遍历数组。因此我们需要想函数传递两个参数:
-
数组本身
-
数组个数的数量
我们用sizeof
计算数组元素个数,用数组名传递数组本身。
int sz = sizeof(arr) / sizeof(arr[0]);
//设置传参的数组元素是数量
void set_arr(int arr[],int sz)
{ int i = o;
for(i;i<sz;i++){
arr[i] = -1;}}
print_arr(int arr[],int sz){
int i = 0;
for(i=0;i<sz;i++)
printf("%d ",i);
}
//完成对数组的打印
注意事项:
- 数组(实参)传参写的是数组名,形参对应书写数组的形式,并且可以省略元素数量(一维)
- 数组传参后,形参的数组和实参的数组是同一块空间,改变形参的数组就是在修改实参的数组
- 数组传参,形参是不会创建新的数组的
7.嵌套调用和链式访问
7.1嵌套调用
嵌套调用就是函数之间的互相调用。
举个例子:计算某年某月有多少天?我们可以设计两个函数来解决这个问题
/function_1
int get_days_of_month(int year, int month) {
int days[] = {0, 31,28,31,31,30,31,30,31 };//前面放0使得对应起来更符合实际情况
//我们发现2月需要判断闰年
int day = days[month];//这里不要和数组名混淆
if (is_leapyear(year) && month == 2) {
day++;
}
return day;}
/function_2
int is_leapyear(int year) {
if ((year % 4 == 0) && (year % 100 != 0) || y % 400 == 0)
return 1;
else return 0;}
函数一调用函数二,主函数再调用函数一构成嵌套调用的逻辑
7.2链式访问
所谓链式访问就是将一个函数的返回值作为另一个函数的参数,像链条一样将函数串起来。
7.2.1链式访问初探
//链式访问:
ize_t len = strlen("abcdef");
printf("%zd ", len);
//进行链式访问:
printf("%zd", strlen("abcdef"));
7.2.2链式访问拔高
int main(){
printf("%d", printf("%d", printf("%d", 43)));
printf("%d ", printf("%d ", printf("%d ", 43)));
return 0;
}
第一个打印结果是:4321
第二个打印结果是:43 3 2
注意空格也算一个元素,并且会把空格本身打印出来
首先我们要知道printf函数会返回一个int类型的值
该值是printf所打印的元素数量
8.函数的声明
8.1单个文件
由于函数是顺序结构的,如果函数定义在后面,那么我们在前面使用函数的时候就必须进行函数声明
我们要知道,函数定义的作用囊括了函数声明,这就是为什么函数定义在前可以不声明的原因。
8.2多个文件
好处自然是:逻辑清晰 方便协作 便于隐藏代码
8.3static和extern
static
和extern
是C语言的关键字,static是静态的意思,exern是用来声明外部符号的。
讲解之前,我们先了解作用域和生命周期。
**作用域(scope)**是程序设计概念,通常来说,⼀段程序代码中所⽤到的名字并不总是有效(可用)
的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
-
局部变量的作用域是变量所在的局部范围。
-
全局变量的作用域是整个工程(项目)。
⽣命周期指的是变量的创建(申请内存)到变量的销毁(回收内存)之间的⼀个时间段。
-
局部变量的生命周期是:进⼊作用域变量创建,生命周期开始,出作用域生命周期结束。
-
全局变量的⽣命周期是:整个程序的生命周期。
#include<stdio.h>
int a = 10;//全局变量
int main()
{
{//创建变量,建立内存
int b = 10;//局部变量
int b = a + 1;
}//生命周期结束,变量销毁,回收内存
//printf("%d", b); 系统找不到b
printf("%d", a); //打印出a,全局变量的生命周期是直到程序结束
return 0;}
8.4**static
是静态的意思,可以用来:**
- 修饰局部变量
- 修饰全局变量
- 修饰函数
我们知道内存有三个区:栈区,堆区,静态区
栈区:局部变量 形式参数 临时变量 特点:都是临时的,进作用域创建,出作用域销毁 |
---|
堆区:动态内存分配(malloc calloc realloc free等函数) |
静态区:全局变量 静态变量(static修饰) 特点:静态区的变量生命周期为整个程序的生命周期 |
8.4.1 static 修饰局部变量
本质上影响了变量的存储类型,一个局部变量是存放在栈区的,但是被static修饰后就存放在静态区了,因为存储类型的变化,生命周期跟着变化,变长了。但我要提醒:作用域不变
void test()
{ int i= 0;
i++; printf("%d",i);
int main()
{ for(int i=1;i<5;i++){
text();
}
return 0;
}
使用static:
void test(){
static int i = 0;
i++;
printf("%d", i);}
int main(){
int i = 0;
for(i=0;i<5;i++){
test();}
return 0;}
**解释:**第一个的test函数中的局部变量i是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。
第二个的test函数中,我们从输出结果来看,i的值有累加的效果,其实test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
**使用建议**:未来⼀个变量出了函数后,我们还想保留值,等下次进⼊函数继续使用,就可以使用`static`修饰。
8.4.2 static修饰全局变量- extern
是用来声明外部符号的。
- 首先我们要知道全局变量是具有外部链接属性的
- 全局变量被static修饰后,外部链接属性就变成了内部链接属性,就是这个全局变量只能在自己所在的**.c文件中使用,其他文件不能使用。相当于作用域变小了**
- extern是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern 进⾏声明,然后使⽤。
extern int add(int,int); //声明外部函数,前提是外部函数没有被static
static修饰函数:
- 函数也是具有外部链接属性的
- 但是被static修饰后,外部连接属性变内部链接属性,这个函数只能在自己所在的.c文件中使用,即使是被声明了也不能在其它文件中使用
使用建议:如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使用static修饰。