文章目录
- 函数(function)
- **函数的概念**
- **函数的作用**
- 在本阶段一般会涉及到两类函数:库函数和自定义函数
- 自定义函数
- **函数的语法形式**
- **形参和实参**
- **实参和形参的关系**
- 函数返回值
- **函数返回值类型说明**
- **return 语句**
- 数组做函数参数
- **函数嵌套调用和链式访问**
- 函数嵌套调用
- **函数链式访问**
- **小插入:有趣的代码**
- **内部函数和外部函数**
- **函数声明和定义**
- **单文件**
- 多个文件(涉及到外部函数)
- **局部变量和全局变量**
- **作用域和生命周期**
- **关键字static、extern**
函数(function)
函数的概念
函数:是指程序中的实现某项特定需求的一小段代码(容易跟数学上函数混淆),程序中函数翻译称为子程序。通常也称为接口(接口是内外连续的窗口,实现不同的功能和效果)
函数的作用
程序其实是由无数个小的函数组成,比如:我们编写 int main() 也是属于函数。函数就是运用"大事化小"的思想,将一个大问题分为若干个小问题,一个大功能分为几个小功能实现。遇到需求可以调用对应的函数解决问题并且函数可以复用,遇到相同需求直接调用该函数,不需要重新CV或者敲一遍一样的程序,减少了代码的冗长,提高了开发的效率,程序的可读性。
在本阶段一般会涉及到两类函数:库函数和自定义函数
C语言标准规定许多语法法则,但是C语言不提供库函数,但是可以使用库函数中的函数。C语言的国际标准ANSI C规定了部分常用的函数的标准,被称为标准库,对于不同编译器厂商根据ANSI C给出标准库给出了常用函数的实现称为库函数。
- 标准库:调用函数某种标准规范
- 库函数:部分常用的函数集合
对于一些常见的功能可以直接调用对应的库函数,比如打印函数,内存函数等,提高了开发的效率。
各种编译器的标准库中提供了⼀系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。
库函数相关头文件:https://zh.cppreference.com/w/c/header (有兴趣可以自己学习)
函数调用:在不同的文件中实现函数调用需要进行函数声明,库函数是在标准库中对应的头文件声明,所以在使用库函数时,需要所对应的头文件。
自定义函数
"自定义"意味更多的创造性,自定义函数更为重要,在不同实际需求中,灵活设计程序满足需求。
函数的语法形式
库函数和自定义函数是一样的(只不过是库函数已经有大佬敲出来,我们只负责调用)
//函数定义格式
ret_type(返回值类型) fun_name(函数名)(形式参数/形参)
{
函数体语句
}
//函数调用格式
int main()
{
ret_type(实参);
}
使用注意:
-
ret _type:表示返回函数计算结果的类型,当返回类型为void,表示没有返回值
-
fun_name:函数名为了方便调用对应的函数,所以函数名尽量根据函数功能取名更有意义(不要用拼音表示函数名,显得很俗,尽量使用英文)
-
函数参数可以为void,明确表示函数没有参数,保留空号,不需要特殊声明
-
函数有多个参数的话,需要用逗号分隔开,表示独立的参数,要对应好参数的类型、名字、个数。
-
形参的书写需完善,不能只写参数类型而省略参数名字
#include <stdio.h>
int Add(int x,int y)
{
return x+y;
}
int main()
{
int a=0,b=0;
scanf("%d %d",&a,&b);
printf("%d",Add(a,b));//Add函数会返回数值,这里可以直接嵌套在printf中
}
形参和实参
在调用函数的过程中,函数的参数为实参和形参
-
实际参数: 简称为实参,是函数调用时的实际参数值
-
形式参数:简称为形参,是函数声明和定义时指定的参数名(参数名尽量取得有意义)
如果只是定义这个函数,而不去调用的话,函数的参数部分只是形式上存在,不会向内存申请空间(不是真实存在的)。只有当函数被调用的过程中为存放(拷贝)实参传递过来的值,才向内存申请空间的,这个过程称为:形参的实例化
实参和形参的关系
既然只当函数调用时,向内存申请空间,则实参和形参都有各自独立的内存空间,那么实参和形参地址可能是不同的
void Swap(int x,int y)
{
}
int main()
{
int a=1,b=6;
Swap(a,b);
return 0;
}
结论:从图中可以看出,虽然x和y确实得到了a和b的值,但是x和y跟a和b的地址是不同的。因为形参是实参的一份临时拷贝,将实参的数值拷贝到形参中,而不是连同地址拷贝给形参,那么意味着形参的改变不会影响到实参
如果我想要通过形参去影响实参,那么可以提前看会指针的知识,通过地址对进行修改–链接
函数返回值
函数返回值类型说明
- 函数默认类型是int,可以省去前面的类型说明符(建议写上,提高代码的可读性)
- 类型说明符如果是void,表示函数没有返回值
return 语句
return语句的两种用途
当函数处理函数体中的数据,需要返回数值,通过返回语句来传送返回值到函数调用点(递归时常见)
表示程序结束,从函数返回调用点,不返回函数的值,可以不用使用return语句
使用return语句的注意事项:
-
return后边可以是一个数值,也是可以是一个表达式。如果后边是一个表达式,那么会先执行表达式,再将表达式的结果返回
-
return 后边不带东西,比如return ;表示返回类型是void,跳出本次函数
-
执行return语句,直接跳出本次函数,return后边代码不再执行
-
当return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型
-
OJ常见的问题,如果函数中存在if等分支语句,则要保证每种情况下都有返回值,否则会出现编译报错
返回值规则
- 任何C/C++函数都必须有类型,如果函数不需要返回数据(不要省略返回值类型),应该声明void类型。不然的话,不加类型说明的函数,一律自动按照整形处理,但是这样子容易误解为void类型
- 不要将正常值和错误标记混在一起返回
数组做函数参数
设计函数时,有些情况需要调用外部数组,在函数中对数组元素进行修改
样例:
int f(int nums[],int sz)
{
for(int i=0;i<sz;i++)
{
nums[i]=1;
printf("%d ",nums[i]);
}
}
int main()
{
int arr[5]={1,2,3,4,5};
int sz=sizeof(arr)/sizeof(arr[0)];
f(arr,sz);
return 0;
}
数组传参注意事项:
-
函数形参和函数实参个数匹配
-
函数实参是数组,形参也可以写成数组和指针类型(传递数组会被转化为指针)
-
形参如果是一维数组,数组大小可以省略不写
-
形参如果是二维数组,行可以省略,但是列不能省略
-
数组传参,形参是不会创建新的数组,形参修改还是实参数组元素
-
形参以数组的形式接收,不能省略[],如果是int arr,类型上实参和形参无法对应
问题:如果将外部数组传参到函数中,在函数中计算数组的大小,是否可行
答:不可行。数组名是数组首元素的地址,那么传参是传递指针,可以使用数组和指针类型接受(传递数组会被转化为指针),对此int sz=sizeof(arr)/sizeof(arr[0)];
这里arr不再是数组名,而是一个指针变量,计算结果会有误差。
函数嵌套调用和链式访问
函数嵌套调用
嵌套调用:指一个函数的函数体中嵌套一个函数,函数之间有效的互相调用,大一些代码都是函数之间的嵌套调用,但是函数是不能嵌套定义的
函数链式访问
链式访问:指将一个函数的返回值作为另外一个函数的参数,像链条一样将函数串起来。
比如:printf("%d",Add(x,y));
对于上面两个知识点,通过一道题目加深理解
知识铺垫:假设我们需要计算某年某月又多少天,但是由于闰年的关系,在闰年这里一年中,一年变为366天,其中二月份的天数会多一天
如果需要函数实现的,可以设计两个函数
-
get_days_of_month():得到天数,在其函数中,需要对是否为闰年进行判断,再进行修改
-
is_leap_year(): 根据年份判断是否是闰年
打印闰年
is_leap_year(int y)
{
if(((year%4==0) && (year%100!=0)) || (year%400==0))
return 1;
else
return 0;
}
int get_days_of_month(int y, int m)
{
int days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//首元素为0的目的,是方便输入月份得到相对的天数
int day = days[m];
if ((is_leap_year(y)) && m == 2)//1为真,0为假
day++;
return day;
}
int main()
{
int y = 0; int m = 0;
scanf("%d %d", &y, &m);
printf("%d",get_days_of_month(y, m));
//那么函数参数部分能不能是函数呢?当然这些到后面才慢慢理解下了
return 0;
}
小插入:有趣的代码
printf("%d", printf("%d", printf("%d", 43)));
这段函数会打印出什么结果,首先我们需要知道,printf函数的返回值是什么,如果想要了解某个库函数细节printf函数
printf函数返回的是打印屏幕上字符的个数,printf函数执行的方向是从右到左,当然在不同编译器有所差异
程序执行顺序:
1. 第一个printf打印第二个printf的返回值,而第二个返回值打印的是第三个printf的返回值
2. 先第三个printf打印43,在屏幕上打印2个字符,再返回2。其次第二个printf打印2,再屏幕上打印1个字符,再放回1,最后printf打印1
3. 最后屏幕上显示:4321
内部函数和外部函数
根据函数是否被其他源文件调用,将函数区分为内部函数和外部函数。主要是解决不同源文件中函数之间调用问题
-
内部函数:一个函数只能被本文件的其他函数所调用
-
外部函数:默认情况下,函数的定义是外部可见的,无需使用
extern
关键字来指定。在定义函数时,在函数最左端加关键字extern(外部),这样子的函数称为外部函数,如果当定义函数时省略extern,则默认为外部函数,外部函数可供其他文件调用
函数声明和定义
单文件
//函数声明
void f(void);
//函数定义
void f(void)
{
函数体;
};
通过判断闰年函数对了解下函数定义和声明使用
int leap_year(int year)
{
if(((year%4==0) && (year%100!=0)) || (year%400==0))
return 1;
else
return 0;
}//这里属于函数的定义
int main()
{
int year=0;
scanf("%d",&year);
int ret=leap_year(year);
if(ret==1)
printf("%d是闰年",year)
else
printf("%d不是闰年",year);
}
问题:如果是在函数调用之前实现函数定义,是没有任何的报错的,如果是将函数的定义放在函数的调用后边,会不会出现报错呢?
int main()
{
int year=0;
scanf("%d",&year);
int ret=leap_year(year);
if(ret==1)
printf("%d是闰年",year)
else
printf("%d不是闰年",year);
}
int leap_year(int year)
{
if(((year%4==0) && (year%100!=0)) || (year%400==0))
return 1;
else
return 0;
}
输入栏这里有提示调用函数前,没有定义该函数,检查不够严格,没有直接报错,这个代码VS2022上编译是会报错的
是因为编译器对源文件进行编译时,是从第一行往下扫描的,当遇到函数调用时,并没有发现前面有该函数的定义
如果使用该函数时,可以在调用函数的后边,但是需要提前声明,也可以在函数调用之前对该函数声明,声明函数需要交代:函数名、函数返回值类型和函数参数(当然一般也是将函数的定义放在上边,并且函数的声明一般在头文件中进行声明)
函数声明中参数可以只保留类型,省略掉名字也是可以的
多个文件(涉及到外部函数)
在企业中写代码时,当代码量比较多,不会将所有代码都放在一个文件中,往往会根程序的功能,将代码拆分在多个文件中
⼀般情况下:函数的声明、类型的声明放在头文件(.h)中,函数的实现是放在原文件(.c)文件中
比如:
局部变量和全局变量
- 局部变量:定义在某一函数或某一部程序内部的变量,称为局部变量。局部变量只能在其所定义的局部范围(作用域)内起作用,离开该范围,它们将会自动销毁(结束生命周期),因此,又称为局部自动变量
- 全局变量:定义在所有函数之外,可供所有函数访问的变量,称为全程变量或全局变量
作用域和生命周期
作用域(scope):是程序设计概念,通常来说,一段程序代码中所用到的名字并不是总是有效的,而限定这个名字的可用性的范围就是这个名字的作用域,简而言之:
- 局部变量的作用域:变量所在的局部范围
- 全局变量的作用域:整个工程
生命周期:指变量创建(申请空间)到变量销毁(收回空间)之间的一个时间段
- 局部变量的生命周期:进入作用域生命周期开始,出作用域生命周期结束
- 全局变量的生命周期:整个程序的生命周期(int main->return结束后)
小知识点:就近原则,了解即可,一般不会这样子设计变量名
- 当局部变量和全局变量同名时采取–>就近原则(一般这样子会出现重命名的问题,但是这里有两个域,不同的域取相同的名字,不会有命名冲突)
- 优先使用离使用地方近的变量
通过下列代码方便理解下上面的知识点:
**特别说明下:**这里的b是未定义的,因为局部变量b的作用域在if语句中,出了if语句(出了作用域)局部变量b就被销毁(生命周期结束)了。
关键字static、extern
static(静态)
作用:1.修饰局部 2.全局变量 3.修饰函数
extern
作用:声明外部符号
通过局部、全局变量、函数来深入了解这两个关键字
static修饰局部变量–静态局部变量
分析上面代码,对于上面知识稳固和理解static修饰局部变量的意义
代码1:在test函数中,创建局部变量i(生命周期开始)并且赋值为0,再++,打印,退出函数(生命周期结束)。
代码2:从输出结果上来看,变量i的值有累加的效果,因为在test函数中创建局部变量i之后,出函数是不会销毁的,重新进入函数也不会重新创建变量,被static修饰只能定义一次,直接累加的数值参与计算中
结论:static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,本来一个局部变量是存储在内存的栈区中,但是被static修饰后存储到了静态区中。全局变量是存储在静态区中,那么存储在静态区的变量和全局变量,生命周期就和程序的生命周期一样的,只有当程序结束,变量才被销毁,内存才能被回收,但是作用域是不变的
静态局部变量的特点
- 只能被定义一次,需要在定义同时初始化
- 生命周期是全局的,作用域是局部的,出作用域不会销毁
static修饰全局变量
代码1正常,代码2在编译的时候会出现连接性错误。
extern是用来声明外部符号,如果一个全局变量的符号在A文件中定义,想在B文件使用,可以使用extern进行声明,之后可以被使用
本质原因:全局变量默认具有外部链接属性,在外部文件中使用,只需要声明下就可以使用
结论:
一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能被其他源文件内使用。因为全局变量被static修饰之后外部链接属性就变成内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明,也不能正常使用的
使用建议:一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用static修饰
static修饰函数
】
代码1正常,代码2在编译的时候会出现连接性错误。
static修饰函数和static修饰全局变量是一样的,一个函数可以在整个工程中使用,被static修饰后,只能在本文件内部使用,其他文件无法正常链接使用
本质:函数默认是具有外部链接属性,使得在整个工程中,只需要适度的声明就可以使用。
但是被static修饰后变成了内部链接属性,使得函数只能在自己所在源文件内部使用
使用建议:一个函数,只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用static修饰
当然针对上面指针相关的问题,我们留到后边来讲,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。
谢谢大家的观看,这里是个人笔记,希望对你学习C有帮助。