C语言函数全解析
函数的概念C语言中的函数就是一个完成某项特定的任务的一小段代码C语言的程序其实是由无数个小的函数组合而成的函数是有特殊的写法和调用方法的函数有库函数和自定义函数两种库函数标准库和头文件C语言的国际标准ANSI C规定了⼀ 些常用的函数的标准被称为标准库那不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。这些函数就被称为库函数。我们前面内容中学到的printf、scanf都是库函数库函数也是函数不过这些函数已经是现成的我们只要学会就能直接使用了。有了库函数⼀些常见的功能就不需要程序员自己实现了一定程度提升了效率同时库函数的质量和执行效率上都更有保证。库函数根据功能的划分都在不同的头文件中进行了声明库函数相关头文件C 标准库标头 - cppreference.comhttps://zh.cppreference.com/c/header库函数的使用方法以下给出几个库函数的学习和查看工具C library - C Referencehttps://legacy.cplusplus.com/reference/clibrary/C 标准库标头 - cppreference.comhttps://zh.cppreference.com/c/header举例我们用sqrt计算平方根举例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include math.h int main() { double ret sqrt(100); printf(%lf,ret); return 0; }自定义函数语法ret_typefun_name(形式参数){函数体}ret_type是函数返回类型有时候返回类型可以是void表示什么都不返回fun_name是函数名括号中放的是形式参数。函数的参数也可以是void明确表示函数没有参数。如果有参数要交代清楚参数的类型和名字以及参数个数{}括起来的是函数体函数体就是完成计算的过程。举例写一个加法函数完成2个整型变量的加法操作。#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h //函数的定义 int Add(int x,int y) { int z 0; z x y; return z; } //函数的调用 int main() { int a 0; int b 0; scanf(%d%d,a,b); int c Add(a, b); printf(%d\n, c); return 0; }形参和实参在函数使用的过程中把函数的参数分为实参和形参在我们上一段代码中x、y属于形参而a、b属于实参实参实际参数就是真实传递给函数的参数形参如果只是定义了Add函数而不去调用的话Add函数的参数x和y只是形式上存在的不会向内存申请空间不会真实存在的所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值才向内存申请空间这个过程就是形式的实例化。实参与形参的关系虽然我们提到了实参是传递给形参的他们之间是有联系的但是形参和实参各自是有独立的内存空间。x和y确实可以得到a和b的值但是x和y的地址和a和b的地址是不⼀样的所以我们可以理解为形参是实参的一份临时拷贝。return语句在函数的设计中函数中经常会出现return语句我们讲一下return语句使用的注意事项。return后边可以是一个数值也可以是一个表达式如果是表达式则先执行表达式再返回表达式的结果。return后边也可以什么都没有直接写return; 这种写法适合函数返回类型是void的情况。return返回的值和函数返回类型不一致系统会自动将返回的值隐式转换为函数的返回类型。return语句执行后函数就彻底返回后边的代码不再执行。如果函数中存在if等分支的语句则要保证每种情况下都有return返回否则会出现编译错误。数组做函数参数在使用函数解决问题的时候难免会将数组作为参数传递给函数在函数内部对数组进行操作。 比如写一个函数将一个整型数组的内容全部置为-1再写一个函数打印数组的内容#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h void set_arr(int arr[], int sz) { int i 0; for (i 0; i sz; i) { arr[i] -1; } } void print_arr(int arr[], int sz) { int i 0; for (i 0; i sz; i) { printf(%d ,arr[i]); } printf(\n); } int main() { int arr[10] { 0 }; int sz sizeof(arr) / sizeof(arr[0]); print_arr(arr, sz); set_arr(arr, sz); print_arr(arr, sz); return 0; }注意函数的形式参数要和函数的实参个数匹配函数的实参是数组形参也是可以写成数组形式形参如果是一维数组数组大小可以省略不写形参如果是二维数组行可以省略但是列不能省略数组传参形参是不会创建新的数组的形参操作的数组和实参的数组是同⼀个数组嵌套调用和链式访问嵌套调用假设我们计算某年某月有多少天如果要函数实现可以设计2个函数is_leap_year()根据年份确定是否是闰年get_days_of_month()调用is_leap_year确定是否是闰年后再根据月计算这个月的天数#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int is_leap_year(int y) { if (((y % 4 0) (y % 100 ! 0)) || (y % 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 }; int day days[m]; if (is_leap_year(y) m 2) day 1; return day; } int main() { int year 0; int month 0; scanf(%d %d,year,month); int days get_days_of_month(year, month);//函数的调用 printf(%d\n,days); return 0; }main函数调用scanf、printf、get_days_of_monthget_days_of_month函数调用is_leap_year注意函数不能嵌套定义链式访问所谓链式访问就是将一个函数的返回值作为另外一个函数的参数像链条一样将函数串起来就是函数的链式访问举例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h #include string.h int main() { printf(%d\n,strlen(abcde));//链式访问 return 0; }我们再看一段有趣的代码#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { printf(%d,printf(%d,printf(%d,43))); return 0; }结果如图所示printf返回的是打印在屏幕上字符的个数第三个printf打印43在屏幕上打印2个字符再返回2第二个printf打印2在屏幕上打印1个字符再放回1第一个printf打印1所以屏幕上最终打印4321函数的声明和定义单个文件我们用判断是否为闰年举例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int is_leap_year(int y) { if (((y % 4 0) (y % 100 ! 0)) || (y % 400 0)) return 1; else return 0; } int main() { int year 0; scanf(%d,year); int r is_leap_year(year);//函数的调用 if (r 1) printf(是闰年\n); else printf(不是闰年\n); }如果我们将函数的调用放在函数的定义之前会出现什么结果呢编译器发出警告这是因为C语言编译器对源代码进行编译的时候从第一行往下扫描的当遇到第7行的函数调用的时候并没有发现前面有is_leap_year的定义就报出了上述的警告。那么我们如何解决这个问题呢就是函数调用之前先声明一下is_leap_year这个函数声明函数只要交代清楚函数名函数的返回类型和函数的参数。比如int is_leap_year(int y)这就是函数声明函数声明中参数只保留类型省略掉名字也是可以的。#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int is_leap_year(int y);//函数声明 int main() { int year 0; scanf(%d,year); int r is_leap_year(year); if (r 1) printf(是闰年\n); else printf(不是闰年\n); } int is_leap_year(int y) { if (((y % 4 0) (y % 100 ! 0)) || (y % 400 0)) return 1; else return 0; }这样编译器就不会报警告函数的调用一定要满意先声明后使用函数的定义也是一种特殊的声明所以如果函数定义放在调用之前也是可以的多个文件一般在企业中我们写代码时候代码可能比较多不会将所有的代码都放在一个文件中我们往往会根据程序的功能将代码拆分放在多个文件中。一般情况下函数的声明、类型的声明放在头文件.h中函数的实现是放在源文件.c文件中。有了函数声明和函数定义的理解我们写代码就更加方便了static和externstatic和extern都是C语言的关键字static的意思静态的可以用来修饰局部变量修饰全局变量修饰函数extern用来声明外部符号作用域与生命周期作用域限定这个名字的可用性的代码范围就是这个名字的作用域局部变量的作用域是变量所在的局部范围全局变量的作用域是整个工程项目生命周期是变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段局部变量的生命周期是进入作用域变量创建生命周期开始出作用域生命周期结束全局变量的生命周期是整个程序的生命周期main函数static修饰局部变量static修饰局部变量改变了变量的生命周期生命周期改变的本质是改变了变量的存储类型本来一个局部变量是存储在内存的栈区的但是被static修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的生命周期就和程序的生命周期一样了只有程序结束变量才销毁内存才 回收。但是作用域不变。我们通过以下代码来理解#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h void test() { int n 0; n; printf(%d ,n); } int main() { int i 0; for (i 0; i 5; i) { test(); } return 0; }结果为#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h void test() { static int n 0; n; printf(%d ,n); } int main() { int i 0; for (i 0; i 5; i) { test(); } return 0; }结果为我们用一张图来直观表达static修饰全局变量extern是用来声明外部符号的如果一个全局的符号在A文件中定义的在B文件中想使用就可以使用extern进行声明然后使用一个全局变量被static修饰使得这个全局变量只能在本源文件内使用不能在其他源文件内使用。 本质原因是全局变量默认是具有外部链接属性的在外部的文件中想使用只要适当的声明就可以使用但是全局变量被static修饰之后外部链接属性就变成了内部链接属性只能在自己所在的源文件内部使用了其他源文件即使声明了也是无法正常使用的。所以如果一个全局变量只想在所在的源文件内部使用不想被其他文件发现就可以使用static修饰static修饰函数static修饰函数和static修饰全局变量是一模一样的一个函数在整个工程都可以使用被static修饰后只能在本文件内部使用其他文件无法正常的链接使用了。本质是因为函数默认是具有外部链接属性具有外部链接属性使得函数在整个工程中只要适当的声明就可以被使用。但是被static 修饰后变成了内部链接属性使得函数只能在自己所在源文件内部使用。所以一个函数只想在所在的源文件内部使用不想被其他源文件使用就可以使用static修饰