目录
1.函数的概念
数学中有函数的概念,在C语言中也引入了函数的概念,有些翻译为:子程序,子程序这种翻译更加准确一些。
C语言中的函数就是一个完成某项特定的任务的一小段代码。这段代码是有特殊的写法和调用方法的。C语言的程序就是由无数个大小的函数组合而成,这些函数也是可以重复使用的。
C语言中常见的两类函数:1.库函数 2.自定义函数
2.库函数
C语言规定了C语言中的语法规则,但并不提供库函数。不同编译器厂商根据ANsi提供的C预压标准给出了一系列函数的实现,被称为库函数。
· 之前的 printf、scanf都是库函数,也是现成的函数。库函数根据功能的划分,在不同的头文件中进行了声明,当我们使用这些库函数时,都需要调用这些声明。
库函数头文件:C 标准库头文件 - cppreference.com
3.自定义函数
自定义函数为程序员写代码创造了更多的可能性。
3.1 函数的语法
自定义函数和库函数是一样的,形式如下:
函数返回类型 函数名(形式参数)
{
函数体
}
- 函数返回类型 表示函数计算结果的类型,有时候返回类型可以使void,表示什么都不返回。
- 函数名 是为了方便使用函数;有了函数名后就方便调用,所以要函数名要尽量有意义。
- 形式参数 函数的参数可以是void,表示函数没有参数。如果有参数,就需要交代清楚参数的类型、名字以及参数个数。
- { } 括起来的部分被称为函数体,就是完成计算的过程。
3.2 函数的举例
举个栗子。写一个加法函数,完成两个整型变量的加法操作。代码如下:
int ADD(int x ,int y)
{
int sum = x + y;
return sum;
// 当然为了简化函数,可以直接写
// return x+y; 返回的值就会返回到这个函数上
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b);
// 使用 两个变量代替函数中的两个形式参数
int r = ADD(a, b); //可以将函数返回值放入 r 中
printf("两数相加之和是:%d\n", r);
//也可以直接打印函数返回值
printf("两数相加之和是:%d\n", ADD(a, b));
//但是不可以打印 sum,sum是只在 ADD函数中生命的变量
return 0;
}
使用效果如下:
4.形参和实参。
在函数使用的过程中,我们把函数分为,实参和形参。
让我们以之前的代码为例:
4.1实参
在上面的代码中。第45-51行的代码是对 ADD函数的定义,有了函数,才能在59行和62行调用函数。
在调用函数时,将参数 a 和 b 传递给函数,a 和 b 就是实际参数,简称实参。实参就是真实传递给函数的参数。
4.2形参
在上面的代码中,第45行定义函数时,函数 ADD 后面括号里的 x 和 y ,被称为形式参数,简称形参。
如果只是定义了 ADD 函数,而不去调用的话。x 和 y只是形式上存在的,不会向内存申请空间,不会真是存在,所以叫形式参数。形式参数只有在函数被调用的过程中,为了存放实参传递过来的值,才会向内存申请空间,这个过程就是形式的实例化。
4.3 实参和形参的关系
虽然实参是传递给形参的,但是1实参和形参各自是存放在独立的内存空间的。但是形参可以用与实参相同的变量名字。
x 和 y 的确的到了 a 和 b 的值,但是它的地址是不一样的。所以可以理解为 形参是实参的一份临时拷贝。
5.return语句
函数中经常会出现 return 语句,这里说一下 return 语句使用的注意事项。
- return 后边可以是一个数值,也可以是一个表达式,如果是表达式,则新执行表达式,再返回表达式的结果。
- return 后面也可以什么都没有,直接写 return 。这种写法适合函数返回类型是 void 的情况。
- return 返回的值和函数返回类型不一致,系统会自动将返回值隐式转换为函数的返回类型。
- return 语句执行后,函数就彻底返回,后边的代码不再执行。
- 如果函数中存在 if 等分子语句,则要保证每种情况下都有 return 返回,否则会出现编译错误。
6.数组做函数参数
在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。
例如:写一个函数对将一个整型数组的内容,全部置换为1,再写一个函数打印数组的内容。
在阅读了题目之后,就会想到要写一个,将数组内容全部置换为-1的函数,和一个打印数组的函数。这就是一个大概的框架。
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
set_arr(); //将数组内容全部置换为-1
printf_arr(); //打印数组内容
return 0;
}
想要将数组内容全部置换为-1,可以使用循环遍历数组,再通过求数组元素的个数,来得知元素的下标。这样就可以通过下标,来将每个元素进行置换。将需要使用的数组和元素个数带入到函数中。
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); //将数组内容全部置换为-1
printf_arr(arr,sz); //打印数组内容
return 0;
}
使用 for 循环来遍历数组,将每个元素置换为 -1。
void set_arr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
arr[i] = -1;
}
}
这样就完成了将数组元素置换为 -1。接下来再使用 for 循环,将置换后的数组打印出来。
void printf_arr(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("下标为%d的元素为:%d\n",i,arr[i]);
}
}
最后执行出来的效果如下图所示:
7.嵌套调用和链式访问
7.1 嵌套调用
嵌套调用就是函数之间相互调用,就犹如一个积木零件,哪里需要哪里搬。
假设我们需要计算某年某月有多少天?
如果要使用函数实现,可以设计两个函数:1.is_leap_year() :判断是否为闰年 2.get_day_of_month() :在调用了 is_leap_year 函数判断是否为闰年之后,再使用这个函数来计算这个月的天数。
is_leap_year 函数在 get_day_of_month函数中调用,所以在主函数中只需要调用 get_day_of_month 函数就可以了。这样我们就可以写出这个程序的框架。
之后在 get_day_of_month 设置一个数组存放非闰年的天数,将月份与下标相对应。之后进行分支,判断是否为闰年并且是2月,是则在天数上+1,并且返回天数,否则直接返回天数。
代码如下:
int is_leap_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
return 1;
else
return 0;
}
int get_day_of_month(int y, int m)
{
int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = days[m];
if (is_leap_year&& m == 2)
day += 1;
return day;
}
int main()
{
int y = 0;
int m = 0;
printf("请输入年份和月份:");
scanf("%d %d",&y,&m);
int d = get_day_of_month(y, m);
printf("%d年%d月的天数是:%d\n",y,m,d);
return 0;
}
7.2 链式访问
链式访问就是将一个函数的返回值作为另一个函数的参数。就是像链条一样将函数串一起来就是函数的链式访问。
以下面这个为例:
int main()
{
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
最里层的 printf 打印了 43,次外层的 printf 打印了最内层打印的 34 的数量,也就是2;最外层的 printf 打印次外层 printf 打印的数量,也是就是1。
8.函数的声明和定义
8.1 单个文件
一般我们在使用函数的时候,直接将函数写出来就调用了。
比如:我们要写一个判断一年是否为闰年
代码第130行到136行为函数的定义,第143行是函数的调用。
但是当我们把函数的定义,放到函数调用的后面,就需要在函数的定义前面加上函数的声明,否则会报错。
声明函数只要交代清楚:函数名、函数的返回类型和函数的参数。
函数的调用一定要满足:先定义后使用。 函数的定义也是一种特殊的声明,所以如果函数定义放在调用之前也是可以的。
8.2 多个文件
当写的代码比较多时,不会将所有的代码都放在一个文件中;我们往往会根据程序的功能,将代码拆分放在多个文件中。
一般情况下,函数的声明、类型的声明放在头文件中(.h)中,函数的实现是放在源文件(.c)中
当需要调用头文件中函数和类型的声明时,要使用 #include “XXX.h” 。这里的XXX.h就是要是用的文件的名称。
8.3 static和extern
static和extern都是C语言中的关键字。
static是 静态的 的意思,可以用来:
- 修饰局部变量
- 修饰全局变量
- 修饰函数
extern 是用来声明外部符号的。如果一个全局的符号在A文件中定义的,想在B文件中使用,就可以使用 extern 进行声明,然后使用。
作用域 限定这个名字的可用性的代码范围,就是这个名字的作用域。
- 局部变量的作用域是变量所在的剧局部范围
- 全局变量的作用域是整个工程
生命周期 指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。
- 局部变量的生命周期:进入作用域,生命周期开始;出作用域,生命周期结束。
- 全局变量的生命周期:整个程序的声明周期。
8.3.1 static 修饰局部变量
以下面这段代码为例,这样运行出来的结果是11111.
但是当在 test 函数中的 int i 前面加上static 之后,输出的结果是12345。
static修饰局部变量改变了变量的生命周期,声明周期改变的本质是改变了变量的存储类型。本来一个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。
存储在静态区的变量和全局变量是一样的,生命周期和程序的生命周期一样了。只有程序结束,变量才销毁,内存才回收。但是作用域不变。
使用建议:未来一个变量除了函数后,我们还想保留值,等下次进入函数继续使用,就可以使用 static 修饰。
8.3.2 static修饰全局变量
一个全局变量被 static 修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
全局变量默认是具有外部链接属性的。在使用 static 修饰之后,就变成了内部链接属性。
使用建议:如果一个全局变量,只想在所在源文件内部使用,不想被其他文件使用,就可以使用 static 修饰。
8.3.3 static修饰函数
static 修饰函数和修饰全局变量的效果是一模一样的。
函数默认是具有外部链接属性的。在使用 static 修饰之后,就变成了内部链接属性。
使用建议:一个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用 static 修饰。