【C语言】函数(涉及生命周期与作用域)

是店小二呀 2024-08-13 12:35:01 阅读 97

Alt

🌈个人主页:是店小二呀

🌈C语言笔记专栏:C语言笔记

🌈C++笔记专栏: C++笔记

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅

请添加图片描述

文章目录

函数(function)**函数的概念****函数的作用**在本阶段一般会涉及到两类函数:库函数和自定义函数自定义函数**函数的语法形式**

**形参和实参****实参和形参的关系**

函数返回值**函数返回值类型说明****return 语句**

数组做函数参数**函数嵌套调用和链式访问**函数嵌套调用**函数链式访问****小插入:有趣的代码**

**内部函数和外部函数****函数声明和定义****单文件**多个文件(涉及到外部函数)

**局部变量和全局变量****作用域和生命周期****关键字static、extern**

函数(function)

请添加图片描述

函数的概念

函数:是指程序中的实现某项特定需求的一小段代码(容易跟数学上函数混淆),程序中函数翻译称为子程序。通常也称为接口(接口是内外连续的窗口,实现不同的功能和效果)

函数的作用

程序其实是由无数个小的函数组成,比如:我们编写 int main() 也是属于函数。函数就是运用"大事化小"的思想,将一个大问题分为若干个小问题一个大功能分为几个小功能实现。遇到需求可以调用对应的函数解决问题并且函数可以复用,遇到相同需求直接调用该函数,不需要重新CV或者敲一遍一样的程序,减少了代码的冗长,提高了开发的效率,程序的可读性

在本阶段一般会涉及到两类函数:库函数和自定义函数

C语言标准规定许多语法法则,但是C语言不提供库函数,但是可以使用库函数中的函数。C语言的国际标准ANSI C规定了部分常用的函数的标准,被称为标准库,对于不同编译器厂商根据ANSI C给出标准库给出了常用函数的实现称为库函数。

标准库:调用函数某种标准规范库函数:部分常用的函数集合

对于一些常见的功能可以直接调用对应的库函数,比如打印函数,内存函数等,提高了开发的效率。

各种编译器的标准库中提供了⼀系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明

库函数相关头文件:https://zh.cppreference.com/w/c/header (有兴趣可以自己学习)

函数调用:在不同的文件中实现函数调用需要进行函数声明,库函数是在标准库中对应的头文件声明所以在使用库函数时,需要所对应的头文件。

自定义函数

"自定义"意味更多的创造性,自定义函数更为重要,在不同实际需求中,灵活设计程序满足需求。

函数的语法形式

库函数和自定义函数是一样的(只不过是库函数已经有大佬敲出来,我们只负责调用)

<code>//函数定义格式

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类型不要将正常值和错误标记混在一起返回

数组做函数参数

设计函数时,有些情况需要调用外部数组,在函数中对数组元素进行修改

样例:

<code>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(): 根据年份判断是否是闰年

打印闰年

<code>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函数执行的方向是从右到左,当然在不同编译器有所差异

<code>程序执行顺序:

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有帮助。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。