六、 函数
欢迎阅读这篇文章 目录1、函数的概念2、 库函数2.1标准库和头文件2.2库函数的使用2.2.1功能2.2.2头文件包含2.2.3实践2.2.4 库函数⽂档的⼀般格式3、⾃定义函数3.1 函数的语法形式3.2函数练习4、形参和实参4.1实参4.2形参4.3 实参和形参的关系5、return语句6、数组做函数的参数7、嵌套调⽤和链式访问7.1嵌套调用7.2 链式访问8、函数的声明和定义8.1单个文件8.2多个文件8.3static和extern8.3.1 static 修饰局部变量静态局部变量8.3.2 static 修饰全局变量静态全局变量8.3.3 static修饰函数1、函数的概念C语言中引入了函数的概念有些会翻译为子程序C语言中的函数就是一个完成某项特定任务的任务的一小段代码。这段代码有特殊的写法和调用方法C语言的程序其实是由无数个小的函数组成的也就是说一个大的计算任务可以分解成若干个较小的函数完成。同时对于一个能完成特定任务的函数这个函数是可以复用的提升了软件开发效率。在C语言中我们可以见到两种函数库函数自定义函数2、 库函数2.1标准库和头文件C语言标准中规定了C语言的各种语法规则C语言并不提供库函数。C语言的国际标准ANSI C规定了一些常用的函数标准被称为标准库。不同的编译器厂商根据ANSI提供的C语言标准给出了一系列函数的实现。这些函数被称为库函数。前面学到的printf和scanf都是库函数库函数也是函数不过这些函数是现成的学会了就可以使用。有了库函数一些常见功能的实现就不需要程序员自己实现提升了效率。同时库函数的质量和执⾏效率上都更有保证。各种编译器的标准库中提供了一系列的库函数这些库函数根据功能的划分都在不同的头文件中进行了声明。库函数相关头文件https://zh.cppreference.com/c/header2.2库函数的使用库函数学习工具https://legacy.cplusplus.com/reference/clibrary/举例 sqrtdoublesqrt(doublex);//sqrt是函数名//x是函数的参数表示调用sqrt函数时需要传递一个double类型的值//double是返回值的类型-表示函数计算的结果是double类型的2.2.1功能Compute square root-计算平方根Returns the square root of x.-返回平方根2.2.2头文件包含库函数是在标准库中对应的头文件中声明的所以库函数的使用必须包含头文件。2.2.3实践#includestdio.h#includemath.hintmain(){doublei16.0;doublejsqrt(i);printf(%f ,i);return0;}2.2.4 库函数⽂档的⼀般格式函数原型函数功能介绍参数返回类型说明代码举例代码输出相关知识链接3、⾃定义函数了解了库函数也要重点关注一下自定义函数自定义函数能使程序员写代码更具有创造性。3.1 函数的语法形式ret_typefun_name(形式参数){}ret_type是函数返回类型fun_name是函数名括号中是形式参数{}中是函数体ret_typefun_name(形式参数)//函数头包含函数名、参数、返回值类型我们可以把函数想象成⼩型的⼀个加⼯⼚⼯⼚得输⼊原材料经过⼯⼚加⼯才能⽣产出产品那函数也是⼀样的函数⼀般会输⼊⼀些值可以是0个也可以是多个经过函数内的计算得出结果。ret_type是函数返回值类型有时候返回类型可以是void表示什么都不返回。有返回值就要写清楚类型具体是什么。fun_name是为了方便使用函数。函数的参数就相当于工厂送进去的原材料函数的参数也可以是void明确表示函数没有参数。如果有参数要交代清楚参数的类型、名字和参数的个数。{}括起来的部分被称为函数体函数体就是完成计算的过程。3.2函数练习写⼀个加法函数完成2个整型变量的加法操作。我们根据要完成的功能给函数取名Add函数Add需要接受2个整型类型的参数函数计算的结果也是整型#includestdio.hintadd(intx,inty){intrxy;returnr;}intmain(){inta0;intb0;scanf(%d %d,a,b);intcadd(a,b);printf(%d,c);return0;}Add函数也可以简化为intadd(intx,inty){returnxy;}函数的参数部分需要交代清楚参数个数每个参数的类型形参的名字。4、形参和实参在函数使⽤的过程中把函数的参数分为实参和形参。4.1实参#includestdio.hintadd(intx,inty){intrxy;returnr;}intmain(){inta0;intb0;scanf(%d %d,a,b);intcadd(a,b);printf(%d,c);return0;}上面的代码26行是Add函数的定义有了函数后再第13行调用Add函数把第13行调用Add函数时传递给函数的参数a和b称为实际参数简称实参。实际参数就是真实传递给函数的参数。4.2形参上面的代码中第2行定义函数时在函数名Add后的括号中写的x和y称为形式参数简称形参。实际上如果只是定义了Add函数而不去调用Add函数的参数x和y只是形式上的存在不会向内存申请空间不会真实存在的所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值才向内存申请空间这个过程就是形参的实例化。4.3 实参和形参的关系我们提到了实参是传递给形参的他们之间是有联系的但是形参和实参各⾃是独⽴的内存空间。可以在调试中来看我们在调试的时候可以观察到x和y确实得到了a和b的值但是x和y的地址和a和b的地址是不⼀样的所以我们可以理解为形参是实参的⼀份临时拷⻉。5、return语句return语句会经常出现下面有几条注意事项。return后面可以是一个数值也可以是一个表达式如果是表达式则先执行表达式再返回表达式的结果。当函数的返回类型为void时return后面可以什么都没有表示强制返回return也可以没有return。return语句执行后函数就彻底返回后边的代码不再执行。当函数的返回类型和return返回的值的类型不一样时系统会自动将return返回的值转换为函数的返回类型。如果函数中存在if等分⽀的语句则要保证每种情况下都有return返回否则会出现编译错误函数的返回类型如果不写编译器默认为int。函数写了返回类型但是没有使用return返回值那么函数的返回值为未知。6、数组做函数的参数在使⽤函数解决问题的时候难免会将数组作为参数传递给函数在函数内部对数组进⾏操作。我们需要知道数组传参的⼏个重点知识函数的形式参数要和函数的实参个数匹配函数的实参是数组形参也写成数组形式形参如果是一维数组参数个数可以省略不写形参如果是⼆维数组⾏可以省略但是列不能省略数组传参形参是不会创建新的数组的形参操作的数组和实参的数组是同⼀个数组⽐如写⼀个函数将⼀个整型数组的内容全部置为-1再写⼀个函数打印数组的内容。#includestdio.hvoidset_arr(intarr2[10],intsz2){inti0;for(i;isz2;i){arr2[i]-1;}}voidprint_arr(intarr2[10],intsz2){inti0;for(i;isz2;i){printf(%d ,arr2[i]);}printf(\n);}intmain(){intarr[10]{0};intszsizeof(arr)/sizeof(arr[0]);print_arr(arr,sz);//先打印原始数组set_arr(arr,sz);//设置数组内容为-1print_arr(arr,sz);//打印数组return0;}对于数组传参实参中数组部分只写数组名形参中数组部分要写类型数组名二维常量7、嵌套调⽤和链式访问7.1嵌套调用嵌套调用就是函数之间的互相调用正是函数之间有效的互相调⽤最后写出来了相对⼤型的程序。假设我们计算某年某⽉有多少天如果要函数实现可以设计2个函数is_leap_year()根据年份确定是否是闰年get_days_of_month()调⽤is_leap_year确定是否是闰年后再根据⽉计算这个⽉的天数#includestdio.hintis_leap_year(intx){if((x%40)(x%100!0)||(x%4000))return1;elsereturn0;}intget_days_of_month(inta,intb){intdays[]{0,31,28,31,30,31,30,31,31,30,31,30,31};intdaydays[b];if(is_leap_year(a)b2){day1;}returnday;}intmain(){intyear0;intmonth0;scanf(%d %d,year,month);intdget_days_of_month(year,month);printf(%d,d);return0;}这⼀段代码完成了⼀个独⽴的功能。代码中反应了不少的函数调⽤main函数调⽤scanf、printf、get_days_of_monthget_days_of_month函数调⽤is_leap_year未来的稍微⼤⼀些代码都是函数之间的嵌套调⽤但是函数是不能嵌套定义的。7.2 链式访问链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数像链条⼀样将函数串起来就是函数的链式访问。#includestdio.h#includestring.hintmain(){printf(%d\n,strlen(abcdef));return0;}再看的代码下⾯代码执⾏的结果是什么呢#includestdio.hintmain(){printf(%d ,printf(%d ,printf(%d ,32)));return0;}先明⽩ printf 函数的返回什么printf函数返回的是打印在屏幕上的字符的个数。上面的例子第一个printf打印第二个printf的返回值第二个printf打印第三个printf的返回值第三个printf返回2第二个printf打印2返回1第二个printf返回1第一个printf打印18、函数的声明和定义8.1单个文件⼀般我们在使⽤函数的时候直接将函数写出来就使⽤了。⽐如我们要写⼀个函数判断⼀年是否是闰年。#includestido.hintis_leap_year(intx){if(((x%40)(x%100!0))||(x%4000))return1;elsereturn0;}intmain(){inty0;scanf(%d,y);intris_leap_year(y);if(r1)printf(是闰年\n);elseprintf(不是闰年\n);return0;}上方代码中28行是函数的定义14行是函数的调用。这种场景下是函数的定义在函数调⽤之前没啥问题。那如果我们将函数的定义放在函数的调⽤后边如下#includestido.hintmain(){inty0;scanf(%d,y);intris_leap_year(y);if(r1)printf(是闰年\n);elseprintf(不是闰年\n);return0;}intis_leap_year(intx){if(((x%40)(x%100!0))||(x%4000))return1;elsereturn0;}这时编译器会报错这是因为C语⾔编译器对源代码进⾏编译的时候从第⼀⾏往下扫描的当遇到的is_leap_year函数调⽤的时候并没有发现前⾯有is_leap_year的定义就报出了上述的警告。函数调⽤之前先声明⼀下is_leap_year这个函数声明函数只要交代清楚函数名函数的返回类型和函数的参数。#includestido.hintis_leap_year(intx);//函数的声明intmain(){inty0;scanf(%d,y);intris_leap_year(y);if(r1)printf(是闰年\n);elseprintf(不是闰年\n);return0;}intis_leap_year(intx){if(((x%40)(x%100!0))||(x%4000))return1;elsereturn0;}函数的调⽤⼀定要满⾜先声明后使⽤函数的定义也是一种特殊的声明所以如果函数定义放在调⽤之前也是可以的。8.2多个文件写代码的时候代码可能会比较多不会将所有的代码都放在⼀个⽂件中我们往往会根据程序的功能将代码拆分放在多个文件中。一般情况下函数的声明类型的声明是放在.h文件中函数的实现是放在.c文件中。如add.cintadd(intx,inty){returnxy;}add.hintadd(intx,inty);test.c#includestdio.h#includeadd.hintmain(){inta0;intb0;scanf(%d %d,a,b);intcadd(a,b);printf(%d,c);return0;}8.3static和externstatic和extern都是C语言的关键字。static是静态的意思可以用于修饰局部变量修饰全局变量修饰函数extern是用于声明外部符号的。作用域和生命周期作⽤域scope是程序设计概念通常来说一段程序代码中所用到的名字并不总是有效的而限定这个名字的可用性的代码范围就是这个名字的作用域。1.局部变量的作用域是变量所在的局部范围。2.全局变量的作用域是整个工程项目。生命周期指的是变量的创建申请内存到变量的销毁收回内存之间的一个时间段。1.局部变量的生命周期为进入作用域变量创建生命周期开始出作用域生命周期结束。2.全局变量的生命周期为整个程序的生命周期。8.3.1 static 修饰局部变量静态局部变量对比以下两个代码//代码1#includestdio.hvoidtest(){inti0;i;printf(%d ,i);}intmain(){inti0;for(i;i5;i)test();return0;}//代码2#includestdio.hvoidtest(){//static修饰局部变量staticinti0;i;printf(%d ,i);}intmain(){inti0;for(i;i5;i)test();return0;}代码1的test函数中的局部变量i时每次进入test前先创建变量生命周期开始并赋值为0然后再打印出函数的时候变量的生命周期就要结束。代码2我们从输出结果来看i的值有累加的效果其实 test函数中的i创建好后出函数的时候是不会销毁的重新进⼊函数也就不会重新创建变量直接上次累积的数值继续计算。结论static修饰局部变量改变了变量的生命周期生命周期改变的本质是改变了变量的存储类型本来一个局部变量存储在内存的栈区但是被static修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的⽣命周期就和程序的⽣命周期⼀样了只有程序结束生命周期才结束变量才销毁内存才收回但是作用域不变。静态局部变量程序一开始就分配空间函数结束不销毁下次调用还保留上次的值作用域只能在当前函数使用使⽤建议未来⼀个变量出了函数后我们还想保留值等下次进⼊函数继续使⽤就可以使⽤static修饰。8.3.2 static 修饰全局变量静态全局变量代码1add1.cintex_year2026;test.c#includestdio.hexternintex_year;intmain(){printf(%d ,ex_year);return0;}extern是⽤来声明外部符号的如果⼀个全局的符号在A⽂件中定义的在B⽂件中想使⽤就可以使⽤extern进⾏声明然后使⽤。代码2add1.cstaticintex_year2026;test.c#includestdio.hexternintex_year;intmain(){printf(%d ,ex_year);return0;}结论一个全局变量被static修饰使得这个全局变量只能在本源文件内使用不能在其他源⽂件内使⽤。本质原因是全局变量默认是具有外部链接属性的在外部的⽂件中想使⽤只要适当的声明就可以使⽤但是全局变量被static修饰之后外部链接属性就变成了内部链接属性只能在⾃⼰所在的源⽂件内部使⽤了其他源⽂件即使声明了也是⽆法正常使⽤的。静态全局变量只能在当前这个 .c 文件里用别的文件访问不到用来隐藏变量8.3.3 static修饰函数代码1add.hintadd(intx,inty);add.cintadd(intx,inty){returnxy;}test.c#includestdio.h#includeadd.hintmain(){inta0;intb0;scanf(%d %d,a,b);intcadd(a,b);printf(%d ,c);}代码2add.hintadd(intx,inty);add.cintadd(intx,inty){returnxy;}test.c#includestdio.h#includeadd.hintmain(){inta0;intb0;scanf(%d %d,a,b);intcadd(a,b);printf(%d ,c);}其实static修饰函数和static修饰全局变量是⼀模⼀样的⼀个函数在整个⼯程都可以使⽤被static修饰后只能在本⽂件内部使⽤其他⽂件⽆法正常的链接使⽤了。本质是因为函数默认是具有外部链接属性具有外部链接属性使得函数在整个⼯程中只要适当的声明就可以被使⽤。但是被static修饰后变成了内部链接属性使得函数只能在⾃⼰所在源⽂件内部使⽤。使⽤建议⼀个函数只想在所在的源⽂件内部使⽤不想被其他源⽂件使⽤就可以使⽤ static 修饰。