函数
1.函数的概念
C语言中的函数的概念就是子程序,C语言中的函数就是完成某特定任务的小段程序。这小段程序有特殊写法和调用方法。
【C语言的程序就是由数个小函数组合而成】
C语言中的两类函数:
-
库函数
-
自定义函数
2.库函数
标准库和头文件
C语言本身并不提供库函数,编译器根据C语言标准给出的一系列可以实现的函数是库函数。
库函数相关头文件网址:C 标准库头文件 - cppreference.com
C/C++官方链接:C 标准库头文件 - cppreference.com
cplusplus.com:C library - C++ Reference
库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件。
3.自定义函数
自定义函数形式:
ret_type name(形式参数) { }
-
ret_type是函数返回类型;
-
name是函数名;
-
小括号中是形式参数;
-
大括号{}括起来的是函数体。
【ret_type有时的返回类型是void,表示什么都不返回】
//instance:加法函数,实现两数相加操作 #include<stdio.h> int add(int x,int b) { int z = 0; z = x + y; return z; } /*也可以这么实现 int add(int x,int y) { return x+y; } */ int main() { int a = 0; int b = 0; scanf("%d %d",&a,&b); int r = add(a,b); printf("%d\n",r); return 0; }
注:函数的参数部分需要交代清楚参数个数、参数类型、形参名字。
4.形参和实参
-
实参:真正传递给自定义函数的参数(上面代码中的a、b);
-
形参:只是形式上存在的参数(上面代码中的x、y),不会向内存申请空间,只有在被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。
【x、y和a、b的地址是不同的,但是x、y在调用过程中得到a、b的值,可以将形参理解为实参的一份临时拷贝。】
5.return语句
-
return 后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果;
-
return后边可以什么都没有,直接写“return;”,这种写法适合函数返回类型是void的情况;
-
return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型;
-
return语句执行后,函数就彻底返回,后边的代码不再执行;
-
如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
6.数组做函数参数
//写一个函数将一个整型数组的内容全部置为-1,再写一个函数打印数组的内容。基本形式如下: #include<stdio.h> int main() { int arr[] = {1,2,3,4,5,6,7,8,9,10}; set_arr(); print_arr(); return 0; }
set_arr函数要能够对数组内容进行设置,就得把数组作为参数传递给函数,同时函数内部在设置元素时需遍历数组,需要知道元素个数,因此要传递2个参数(数组+数组的元素个数)。print_arr在执行时也同样需要两个参数。
将上面形式丰富一下(补全参数)
#include<stdio.h> int main() { int arr[] = {1,2,3,4,5,6,7,8,9,10}; int sz = sizeof(arr)/sizeof(arr[0]); set_arr(arr,sz); print_arr(arr,sz); return 0; }
数组传参的重要知识:
-
函数的形式参数要和实际参数个数匹配;
-
函数的实参是数组,形参也可以写成数组形式;
-
形参如果是一位数组,数组大小可以忽略不写;
-
形参如果是二维数组,行数可以忽略,列不能忽略;
-
数组传参,形参不会创建新数组;
-
形参操作的数组和实参的数组是同一个数组。
实现上述函数:
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"); }
7.嵌套调用和链式访问
嵌套调用就是函数之间的互相调用。
//计算某年某月有多少天 #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 y = 0; int m = 0; scanf("%d %d",&y,&m); int d = get_days_of_month(y,m); printf("%d\n",d); return 0; }
在上面的例子中出现的函数调用:
-
main函数调用了scanf、printf、get_days_of_month函数;
-
get_days_of_month函数调用了is_leap_year函数。
链式访问:将一个函数的返回值作为另外一个函数的参数,像链条一样地将函数串起来。
//简单举例 #include<stdio.h> int main() { printf("%d\n",strlen("abcdef")); return 0; }
//第二个例子^.^ #include<stdio.h> int main() { printf("%d\n",printf("%d",printf("%d",43))); return 0; }
第二个例子输出结果是:4321
【printf函数的返回值是打印在屏幕的字符个数】
首先最里面的printf函数输出“43”,两个字符使第二层的printf输出“2”,最外层printf输出“1”.
8.函数的声明和定义
在单个函数使用时,直接将函数写出来就可以使用
例如闰年判断:
#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 y =0; scanf("%d",&y); int r = is_leap_year(y); if(r == 1) printf("闰年\n"); else printf("不是闰年\n"); return 0; }
这样是可以正常运行该操作的.
#include<stdio.h> int main() { int y =0; scanf("%d",&y); int r = is_leap_year(y); if(r == 1) printf("闰年\n"); else printf("不是闰年\n"); return 0; } int is_leap_year(int y)//闰年判断函数 { if(((y%4==0)&&(y%100!=0))||(y%400==0)) return 1; else return 0; }
但是,如果顺序相反,编译器会报错。因为编译器在对源代码进行编译时是从第一行向下扫描,在扫描到函数调用时没有发现该调用函数的定义,就会报错。
解决方法:在函数调用前声明函数,交代清楚函数名、函数的返回类型和函数参数。
#include<stdio.h> int is_leap_year(int y);//函数声明 int main() { int y =0; scanf("%d",&y); int r = is_leap_year(y); if(r == 1) printf("闰年\n"); else printf("不是闰年\n"); return 0; } int is_leap_year(int y)//闰年判断函数 { if(((y%4==0)&&(y%100!=0))||(y%400==0)) return 1; else return 0; }
在使用的代码在多个文件中时,将函数的声明、类型声明放在头文件(.h)中,函数实现放在源文件中(.c)。
//add.c,函数定义 int add(int x,int y) { return x+y; }
//add.h,函数声明 int add(int x,int y)
//test.c #include<stdio.h> #include"add.h" int main() { int a = 10; int b = 20; int c = add(a,b); printf("%d\n",c); return 0; }
static和extern
static和extern是C语言中的关键字。
作用域的概念:一段程序代码所用到的名字并非持续有效,而限定这个名字可用性的代码范围就是这个名字的作用域。
-
局部变量的作用域就是变量所在的局部范围;
-
全局变量的作用域是整个工程。
生命周期指的是变量创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。
-
局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域生命周期结束;
-
全局变量的生命周期是:整个程序的生命周期。
static是静态的意思,用来:
-
修饰局部变量
-
修饰全局变量
-
修饰函数
extern是用来声明外部符号的。
static修饰局部变量:
//代码1 #include<stdio.h> void test() { int i = 0; i++; printf("%d ",i); } int main() { int i = 0; for(i=0; i<5; i++) { test(); } return 0; } //代码2 #include<stdio.h> void test() { static int i = 0; i++; printf("%d ",i); } int main() { int i = 0; for(i=0; i<5; i++) { test(); } return 0; }
代码1中test函数中的局部变量i是每次进入test函数先创建变量(此时生命周期开始)并赋值0,然后++,再打印,出函数的时候生命周期将要结束(释放内存)。
代码2中i值有累加的效果,因为test函数中的i创建好后,出函数时不会销毁,重新进入函数也不会重新创建变量,直接用上次累积的数值继续计算。
【总结:static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,存储位置由栈区变为静态区,生命周期和程序的生命周期一样。作用域不变。】
static修饰全局变量:
代码1:
add.c
int g_val = 2024;
test.c
#include<stdio.h> extern int g_val; int main() { printf("%d\n",g_val); return 0; }
代码2:
add.c
static int g_val = 2024;
test.c
#include<stdio.h> extern int g_val; int main() { printf("%d\n",g_val); return 0; }
代码1正常,代码2在编译时出现链接性错误。
extern用来声明外部符号,如果一个全局符号在A文件中定义,在B文件中想使用,就可以用extern进行声明再进行使用。
【结论:⼀个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
本质原因是全局变量默认是具有外部链接属性的,在外部的文件中想使用,只要适当的声明就可以使用;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明了,也是无法正常使用的】
static修饰函数:
static 修饰函数和 static 修饰全局变量是⼀模⼀样的,⼀个函数在整个工程都可以使用,被static修饰后,只能在本文件内部使用,其他文件无法正常的链接使用了。
本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个工程中只要适当的声明就可以被使用。但是被 static 修饰后变成了内部链接属性,使得函数只能在自己所在源文件内部使用