函数是C语言尤为重要的知识点,再嵌入式的学习过程中,对51和32的单片机的学习是重中之重。
一、函数的基本概念
1.介绍
函数是一种可重复使用的代码块,用于执行特定的任务或操作。
函数允许我们将代码逻辑组织成独立的单元,从而提高了代码的可读性、可维护性和重用性。
一个C程序可以由一个或多个源文件构成(C文件扩展名是“.c”),一个源文件是一个编译单位。一个源文件可以由若干个函数构成,函数之间可以相互调用。也就是说,函数是C程序基本的组成单位。
2.作用
- 封装功能,将一个完整的功能封装成函数,提高代码的结构化和复用性。
- 代码模块化,将程序按照功能拆分成若干模块单元,有助于降低复杂度。
- 增强可维护性,如果需要修改某项功能,只需要调整对应的函数代码。
- 隔离细节,通过函数调用可以隐藏实现细节,只关心输入输出。
3.分类
C语言中,从使用的角度,函数可以分类两类。
- 库函数,也称为标准函数,是由C系统提供的,用户不必自己定义,可直接使用它们,使用库函数,必须包含 #include 对应的头文件。
- 自定义函数,解决具体需求而自己定义的函数,需先定义再使用。
二、基本语法
1.结构
返回类型 函数名(参数列表)
{
函数体语句1;
函数体语句2;
… 函数体语句n;
return 返回值;
}
2.代码演示
#include <stdio.h>
// 声明函数
void func()
{
printf("hello func\n");
}
// 实现两个数字相减
int minus(int m, int n)
{
return m - n;
}
// 取两个数字中的最大值
int max(int a, int b)
{
int c;
c = a > b ? a : b;
return c;
}
// 主函数
int main()
{
return 0;
}
3.函数不能嵌套
C程序中的所有函数都是互相独立的,一个函数并不从属于另一个函数,即函数不能嵌套声明。
//错误演示
int func1(int a,int b) //第1个函数的定义
{
...
int func2(int c,int d) //第2个函数的定义
{
...
}
...
}
有些编译器的扩展允许函数嵌套声明,但这不是C标准的一部分,代码的可移植性可能会受到影响,强烈不建议。
三、调用函数
函数名后面加上圆括号即是函数的调用,参数写在小括号中,函数每调用一次,函数体语句都会执行一遍。
如果把参数当成是原汁原味的水果,那函数体就是把水果加工成果汁的榨汁机,返回值就是最终的果汁。
#include <stdio.h>
int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
//调用函数
printf("%d",max(10,19));
return 0;
}
四、函数的返回值
函数调用后数能得到一个确定的值,这就是函数的返回值,返回值常常是一个计算的结果,或是用来作为判断函数执行状态的标记。
函数返回值分为以下三种情况:
- 无返回值类型:针对函数无返回值或明确不需返回值的情况,使用void(即空类型)表示。
- 有返回值类型:指明具体的类型,比如,int、float、char等。需要在函数体内与return语句搭配使用。
- 如果返回值类型不是void,但函数中没有return语句,则函数会返回一个不确定的值。
#include <stdio.h>
// 无返回值
void fun01()
{
printf("fun01\n");
}
// 有明确的返回值
double fun02()
{
return 3.14;
}
// 函数返回一个不确定的值
int fun03()
{
10 + 20;
}
// 返回类型与return的值类型不一致,可能造成精度损失
int fun04()
{
return 20.89;
}
int main()
{
fun01();
printf("%f \n", fun02());
printf("%d \n", fun03()); // 返回一个不确定的值
printf("%d \n", fun04()); // 返回值有精度损失
return 0;
}
输出结果:
fun01
3.140000
10
20
五、函数的参数
函数的参数分为形参与实参:
- 形参:在定义函数时,函数名后面括号()中声明的变量称为形式参数,简称形参。
- 实参: 在调用函数时,函数名后面括号()中的使用的常量、变量、表达式称为实际参数,简称实参。
注意:实参的数量要与形参的数量一致,否则报错。
六、主函数
1.主函数的作用
主函数是程序的入口函数,即所有的程序一定要包含一个主函数,程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。主函数中可以调用其它函数,但其它函数不能反过来调用主函数,主函数也不能调用自己。
2.主函数的返回值
C语言约定,主函数返回值0表示运行成功,如果返回其它非零整数,就表示运行失败。默认情况下,如果主函数里面省略return 0 这一行,编译器会自动加上,即 main() 的默认返回值为0。但是为了保持统一的代码风格,不建议省略。
3.主函数的参数
主函数的声明中可以带有两个参数,格式如下:
int main(int argc, char *argv[])
{
// 函数体
}
其中,形参argc,全称是argument count,表示传给程序的参数个数,其值至少是1;而argv,全称是argument value,argv[]是一个指针数组(12.3.2小节会具体讲解),我们可以暂时将 argv 理解为是一个数组,数组的每个元素都是字符串。
这种方式可以通过命令行的方式执行源代码,并接收指定的字符串传给参数argv。
我们创建一个名为 demo.c 的源文件,代码如下:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("argc = %d\n", argc);
// 函数体
for (int i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
七、函数原型
默认情况下,函数必须先声明,后使用。由于程序总是先运行main() 函数,导致所有其它函数都必须在main() 函数之前声明。
如果想将函数声明写在后面,可以在程序开头处给出函数原型。函数原型,就是提前告诉编译器,每个函数的返回类型和参数类型。其它信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。
#include <stdio.h>
// 使用函数原型进行声明
int fun(int);//可以不能定义确定的变量
// 主函数
int main()
{
printf("%d\n", twice(100));
return 0;
}
// 函数定义
int fun(int num)
{
return 2 * num;
}
结果:200
八、作用域
1.概念
作用域用于确定在代码中某个标识符(如变量、标识符常量、数组等)的可见性和访问范围,它决定了在程序的哪些部分可以引用或访问该标识符。
作用域可以分为全局作用域、局部作用域、块级作用域。
同一个作用域中不能声明同名的标识符。
2.全局作用域
在函数和代码块(分支语句、循环语句等)以外定义的变量、标识符常量、数组等具有全局作用域,在程序的任何地方都可以被访问,通常称它们为全局变量、全局常量、全局数组等。
3.局部作用域
在函数内定义的变量、标识符常量、数组等具有局部作用域,只有在该函数内部才能被访问,通常称它们为局部变量、局部常量、局部数组等。需要注意的是,函数的形参也是局部变量。
4.块级作用域
块级作用域是C99标准引入的概念,在代码块(分支语句、循环语句等)中定义的变量、标识符常量、数组等具有块级作用域,只有在该代码块内部才能被访问,代码块通常具有花括号 {} 结构。
这些被称为块级变量、块级常量、块级数组等,同时也可以被称为局部变量、局部常量、局部数组,且与函数中的局部变量、局部常量、局部数组具有相同的特性。
5.作用域和内存
(1)栈区域
局部变量、局部数组等通常存储在栈(Stack)区,这些局部数据的内存分配和释放是自动管理的,它们的生命周期受限于其定义的函数或块级作用域,当函数返回或块级作用域结束时,这些变量的内存会被自动释放。
函数每调用一次就创建一次局部数据,调用结束就销毁;下次调用再创建新的局部数据。
(2)全局静态区域
全局变量、全局数组等存储在全局静态区,这些全局的数据在程序的整个生命周期内都存在,它们在程序启动时被分配,在程序结束时才被释放。
(3)变量生命周期演示
#include <stdio.h>
// 定义全局变量
int a = 10;
// 定义全局数组
int arr[3] = {10, 22, 34};
// 定义全局函数
void fun(int num)
{
// 定义局部变量
int b = 230;
printf("%d", num + b);
}
int main()
{
fun(20);
// 遍历数组
for (int i = 0; i < 3; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 再次调用函数
fun(60);
return 0;
}
(4)static 和 extern 关键字
static关键字可以声明静态变量和静态函数,以控制变量和函数的作用范围和生存周期。
<1>静态局部变量
使用static关键字修饰的局部变量,称为静态局部变量,静态局部变量与全局变量一样存储在内存中的全局静态区。静态局部变量具有如下特点:
- 静态局部变量只在函数第一次调用时初始化一次并将生命周期延长到整个程序的执行期间
- 静态局部变量如果声明时没有初始赋值,系统会自动初始化为零,同全局变量的初始化规则一致。
<2>多文件编译
C 编译器可以将多个源文件编译成一个可执行文件。创建两个源文件,分别命名为file01.c和 file02.c。
VS Code 默认是无法同时编译多个源文件的,我们使用命令行终端进行编译,打开VS Code 内置终端,运行如下命令:
gcc file01.c file02.c -o main.exe
运行之后,会生产 main.exe 可执行文件,再次通过命令行运行 mian.exe 即可。
./main.exe
运行完会显示“Hello file02”。
<3>extern
如果想要使用外部文件中定义的变量,我们可以使用 extern 关键字声明外部链接。
<4>静态全局变量
使用 static 关键字修饰的全局变量称为静态全局变量。
普通全局变量对整个工程可见,其他文件中,使用extern外部声明后,可以直接使用。静态全局变量仅对当前文件可见,其他文件不可访问,其他文件中可以定义与其同名的变量,两者互不影响。
静态全局变量对于需要编译多个源代码文件的程序,能够有效地降低源文件之间的耦合,避免不同文件同名变量的冲突。
此外static关键字还可以修饰函数(静态函数)、全局数组、全局常量等,功能作用与静态全局变量一致。
九、课后练习题
1.定义函数,实现求两个double数字的最大值,并返回。
#include <stdio.h>
double max(double a,double b)
{
return a>b?a:b;
}
int main()
{
double a1,b2;
//输入浮点数
scanf("%lf",&a1);
scanf("%lf",&b2);
//调用函数
printf("%lf",max(a1,b2));
return 0;
}
2.定义函数,求出三个int 类型数的和,并返回。
#include <stdio.h>
//定义函数
int sum(int a,int b,int c)
{
return a+b+c;
}
int main()
{
int num1,num2,num3;
//输入
scanf("%d",&num1);
scanf("%d",&num2);
scanf("%d",&num3);
//调用函数
printf("%d",sum(num1,num2,num3));
return 0;
}
3.定义函数,判断一个数字是否是质数。
#include <stdio.h>
int fun(int a)
{
int count=2;
for(int i=2;i<a;i++)//输入比这个数小的所有整数,除了1和它本身。
{
if(a%i==0)
{
count++;//只要有多余的整除因数,就可以终止
break;
}
}
if(count==2)//符合条件的返回1
{
return 1;
}
}
int main()
{
int num1;
//输入
scanf("%d",&num1);
//调用函数
if(fun(num1)==1)
{
printf("%d是质数",num1);
}else{
printf("%d不是质数",num1);
}
return 0;
}
4.函数可以没有返回值案例,编写一个函数,从终端输入一个整数打印出对应的金字塔,函数可以传入金字塔层数。
#include<stdio.h>
//函数可以没有返回值案例,编写一个函数,从终端输入一个整数(层)打印出对应的金子塔。
//层数n是通过形参传入
void Star(int n) {
int i,j,k;
for(i = 1; i <= n; i++) {//控制层
//输出空格, 使用 k 控制空格的循环
for (k=1; k <= n - i; k++) {
printf(" ");
}
for(j = 1; j <= 2 * i - 1; j++) {
if(j == 1 || j == 2 * i - 1 || i == n) {
printf("*");
} else {
printf(" ");
}
}
printf("\n");
}
}
int main(){
int a;
printf("请打印金字塔层数:");
scanf("%d",&a);
Star(a);
}