目录
1.函数的概念
在数学领域中我们便接触过函数,例如一次函数y=kx+b。我们可以将函数比作一个工厂,你输入原材料,它经过加工输出产品。
在C语言中同样有着函数(function)的概念,也称作:子程序。在C语言中函数就是一个完成某项特定的任务的一小段代码。事实上,C语言的程序就是由无数个小的函数组合而成的。
在C语言当中我们一般会见到两类函数:
- 库函数
- 自定义函数
2.库函数
2.1 标准库和头文件
C语⾔标准中规定了C语⾔的各种语法规则,但并不提供库函数;C语⾔的国际标准ANSI规定了⼀些常⽤的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。库函数也是函数,只不过这些函数是现成的,我们可以直接拿来用,能够极大的提高效率。库函数根据功能的划分,在不同的头文件中进行了声明。
可在库函数相关头文件中找到需要的头文件。
3.自定义函数
与库函数不同,自定义函数是为了满足某种需求自己创造的函数,所以它能带来更大的灵活性。
3.1 函数的语法形式
其语法格式如下:
ret_type fun_name(形式参数)
{
}
其中:
- ret_type是函数的返回值类型,即函数计算结果的类型,返回类型可以是void,表示什么都不返回。
- fun_name是函数名,可根据实际需求设置
- 括号中是形式参数,要交代清楚参数的类型和名字,以及参数的个数。函数的参数也可以是void。
- {}中是函数体,即完成计算的过程。
例如:
完成一个加法函数,输入两个整型,输出它们的和。
#include <stdio.h>
//加法函数
int Add(int x,int y)
{
int z = x + y;
return z;//输出x,y的和
}
int main()
{
int a = 0, b = 0;
//输入
scanf("%d%d", &a, &b);
int c = Add(a,b);
printf("%d\n", c);
return 0;
}
运行结果:
其中Add函数也可以简化为:
int Add(int x, int y)
{
return x + y;
}
4.形参和实参
4.1 实参
在函数使用过程中,函数的参数分为实参和形参。
我们先看看前面的代码:
#include <stdio.h>
//加法函数的定义
int Add(int x,int y)
{
int z = x + y;
return z;//输出x,y的和
}
int main()
{
int a = 0, b = 0;
//输入
scanf("%d%d", &a, &b);
int c = Add(a,b);//函数的调用
printf("%d\n", c);
return 0;
}
在上述代码中,3~7行是Add函数的定义,第13行是函数的调用,传递给函数的参数a和b,称为实际参数,简称实参。实际参数就是真实传递给函数的参数。
4.2 形参
在上述代码中,第三行定义Add函数时,在函数名后的括号中写的x和y,称为形式参数,简称形参。通过字面我们可以看出,它只是形式上存在,不会向内存申请空间,只有在函数被调用的过程中来存放实参穿过来的值,才向内存申请空间,并且在函数调用完毕后,形参就会在内存中销毁。
4.3 实参和形参的关系
我们可以通过如下代码来研究实参和形参的关系:
#include <stdio.h>
void Test(int x, int y)
{
printf("x的地址为:%p\n", &x);
printf("x的值:%d\n", x);
printf("y的地址为:%p\n", &y);
printf("y的值:%d\n", y);
}
int main()
{
int a = 3;
int b = 5;
printf("a的地址为:%p\n", &a);
printf("a的值:%d\n", a);
printf("b的地址为:%p\n", &b);
printf("b的值:%d\n", b);
Test(a,b);
return 0;
}
运行结果:
由上述代码及运行结果可以看出,虽然实参和形参的值相等,但实参与形参的地址并不相同,所以可以理解为形参是实参的一份临时拷贝。
5.return 语句
在函数使用中,经常会用到return语句。
- return后边可以是一个数值,也可以是一个表达式,如果是表达式,则返回表达式的结果。
- return后边也可以什么都没有,这种写法适合函数返回类型为void的情况。
- return返回的值和函数返回类型不一致时,系统会自动将返回的值隐式转换为函数的放回类型。
- return语句执行后,函数就彻底返回,后面的代码不再执行。
- 如果函数中存在if等分支语句,则要保证每种情况下都有return返回,否则会出现编译错误。
6.嵌套调用和链式访问
6.1 嵌套调用
嵌套调用就是函数之间相互调用。每个函数之间相互调用从而实现了一个大的程序。
例如:输入某年某月,输出这一月有多少天。
#include <stdio.h>
//判断是否为闰年
int LeapYear(int year)
{
if((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
return 1;
else
return 0;
}
//判断天数
int Day(int year, int month)
{
int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = days[month];
if(LeapYear(year) && month == 2)//嵌套调用:在Day函数中调用了LeapYear函数
day++;
return day;
}
int main()
{
int year = 0;
int month = 0;
scanf("%d%d", &year, &month);
int d = Day(year,month);
printf("%d", d);
return 0;
}
上述代码中:
- Day函数中嵌套调用了LeapYear函数
- main函数中调用了scanf、Day、printf函数。
6.2 链式访问
链式访问就是将一个函数的返回值作为另外一个函数的参数,像链条一样将函数串起来就是函数的链式访问。
例如:
#include <stdio.h>
int main()
{
printf("%d\n", strlen("abcdefgh"));
return 0;
}
上述代码中,就是将strlen的返回值直接作为printf函数的参数,实现了链式访问。
7.函数的声明和定义
7.1 单个文件
让我们来看以下代码:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 9;
int b = 4;
int num = Add(a,b);
printf("%d", num);
return 0;
}
其中2~4行是函数的定义,第10行代码是函数的调用。这种情况下,函数的定义在函数的调用前面。
那如果函数的调用在函数的定义前面呢?
#include <stdio.h>
int main()
{
int a = 9;
int b = 4;
int num = Add(a,b);
printf("%d", num);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
上述代码在编译运行时会报错,因为C语言编译器对源代码编译时自上而下执行,当调用Add函数时会发现前面并没有定义Add函数。
那该如何解决上述问题呢?那就是在使用函数之前声明一下函数,需要交代清楚:函数名、函数的返回类型和函数的参数。
#include <stdio.h>
int Add(int x, int y);//函数的声明
int main()
{
int a = 9;
int b = 4;
int num = Add(a,b);
printf("%d", num);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
函数的定义也是一种特殊的声明,如果函数定义放在调用之前也是可以的。
7.2 多个文件
在实际应用中,代码量可能会很大,我们往往会根据功能的不同,将代码拆分在多个文件中。一般情况下,将函数的声明、类型的声明放在头文件中,函数的实现放在源文件中。
例如:
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 = 4;
int b = 5;
//函数的调用
int c = Add(a,b);
printf("%d", c);
return 0;
}
7.3 static
static和extern都是C语言中的关键字。
static的中文是“静态的”,它可以用来:
- 修饰局部变量
- 修饰全局变量
- 修饰函数
首先我们来了解一下作用域和生命周期:
作用域(scope):限定某个名字的可用性的代码范围就是这个名字的作用域。简单来说,作用域就是某个东西能起作用的区域。
- 局部变量的作用域是变量所在的局部范围,即它所在的{}内。
- 全局变量的作用域是整个工程。
生命周期:指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。
- 局部变量的生命周期是:进入作用域,变量创建,生命周期开始;出作用域,生命周期结束。
- 全局变量的生命周期是整个程序的生命周期。
7.3.1 static 修饰局部变量
static修饰局部变量会改变变量的生命周期。生命周期改变的本质是改变了变量的存储类型,由存储在内存的栈区变为存储在静态区。存储在静态区的变量和全局变量一样,生命周期和程序的生命周期一样,但作用域仍然不变。一个变量出了函数后,若我们仍想保留值,就可以使用static修饰
例如:
static int i = 0;
static char b = "abc";
7.3.2 static 修饰全局变量
一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他文件中使用。本质原因是全局变量默认是具有外部链接属性的,在外部的文件中想使用,只要适当的声明就可以使用;但是全局变量被static修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明了,也是无法正常使用的。
7.3.3 static 修饰函数
static修饰函数和static修饰全局变量十分相似,都会使其只能在本源文件内使用,其他文件无法正常的链接使用。本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个工程中只要适当的声明就可以被使用。但是被static修饰后变成了内部链接属性,使得函数只能在自己所在源文件内部使用。
7.4 extern
extern用来声明外部符号。如果一个全局的符号在A文件中定义,在B文件中想使用,就可以使用extern进行声明。
例如:
add.c
int a = 0;
test.c
#include <stdio.h>
extern int a;
int main()
{
printf("%d", a);
return 0;
}
当然,如果全局变量a被static修饰,即使用extern声明,也不能在其他源文件中使用。