1.函数的分类
C语言程序是由函数组成的,我们写的代码都是从主函数 main()开始执行的。
函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。
从函数定义的角度看,函数可分为系统函数和用户定义函数两种:
-
系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
-
用户定义函数:用以解决用户的专门需要。
2.函数的基本结构
函数的定义就是函数体的实现,函数体就是一个代码块,它在函数调用时执行。
语法
函数返回类型 函数名(形式参数列表){
函数体
执行任务
return 返回值;//如果返回类型不是void就有返回值
}
-
函数返回类型:指定函数执行后返回的值的类型。它可以是基本数据类型,如 int、float、char 等,或者是 void,表示函数不返回任何值。
-
函数名:标识函数的名称,必须遵循标识符的命名规则。
-
参数列表:函数接受的输入参数,函数内部通过这些参数来执行任务。参数列表由一对圆括号
()
包围,可以为空,也可以包含多个参数,参数类型之间用逗号分隔。 -
函数体:函数的实际执行代码块,位于一对花括号
{}
之间。函数体包含执行任务的语句。
3.函数的声明、定义和调用
函数声明(或叫做原型):告诉编译器函数的返回类型、函数名以及参数类型。通常,函数声明放在文件的顶部,或者在头文件中进行声明。
返回类型 函数名(参数类型1, 参数类型2, ...);
例如:
int add(int, int);
函数定义:函数的实际实现,它包含了函数的功能代码。
int add(int a, int b) {
return a + b;
}
函数调用:通过函数名和传递给函数的参数来执行该函数。
int result = add(5, 3);
整体来举个例子
#include<stdio.h>
// 函数的声明
int sum(int, int);
// 函数的使用可以省去重复代码的编写,降低代码重复率
// 求两个数之和
// 函数定义
int sum(int a, int b) {
return a + b;
}
int main() {
int x = 10, y = 20;
// 函数调用
int result = sum(x,y);
printf("两数之和为:%d\n",result);
return 0;
}
4.全局变量和局部变量
全局变量
在函数外部定义的变量叫做全局变量
特点:
-
作用于---在本文件中的任何函数中都可以使用
-
声明周期---在定义变量的时候,系统会申请内存空间,当main()函数结束的时候,系统会回收全局变量对应的内存空间
-
全局变量未初始化的时候值默认为0
局部变量
在函数内部定义的变量叫做局部变量
特点:
-
作用域---只能在定义它的那个函数内部中使用,其他位置不能使用
-
生命周期---在定义变量的时候,系统会申请内存空间,当定义该变量的函数调用结束之后,系统会回收局部变量对应的内存空间
-
局部变量未初始化的时候值默认为随机值
#include<stdio.h>
// 全局变量
int global = 20;
void do_global() {
global++;
printf("gloabl = %d\n",global);
//printf("local = %d\n",local);// 未定义标识符,因为local是局部变量,只能在定义它的那个函数内部中使用
return;
}
void do_local() {
// 局部变量
int local = 10;
local++;
printf("local = %d\n",local);
return;
}
int main() {
do_global();
do_global();
do_global();
printf("=========================================\n");
do_local();
do_local();
do_local();
return 0;
}
静态局部变量
格式:
static 数据类型 变量名;
如:
static int c;
特点:
-
作用域---只能在定义它的函数内部使用,其他位置不能使用
-
生命周期---静态局部变量,可以改变原来变量的生命周期,当定义该变量的函数调用结束后,系统不会回收对应的内存空间,当main()函数结束后,内存空间才会被回收
-
静态局部变量未初始化的时候值默认为0
#include<stdio.h>
void do_stalocal() {
// 静态局部变量
static int sl = 10;
sl++;
printf("sl = %d\n",sl);
return;
}
int main() {
do_stalocal();
do_stalocal();
do_stalocal();
return 0;
}
5.形参和实参
形参:
形参是函数定义时的参数,它们仅在函数内部有效。
形参是函数用来接收传入数据的变量,定义时只是一个占位符,不包含实际值。
例如
int add(int a, int b) { // a 和 b 是形参
return a + b;
}
实参:
实参是函数调用时传递给函数的实际值或变量。它们在调用函数时被传递给形参,函数根据这些值来执行操作。
例如
int result = add(3, 5); // 3 和 5 是实参
int result = add(3, 5); // 3 和 5 是实参
传递方式:
-
值传递:实参的值被传递到形参,函数内的修改不会影响外部的实参。
-
引用传递:通过指针传递实参的地址,这样函数内的修改会直接影响外部的实参。
下面来介绍参数的传递方式
6.函数的参数传递方式
参数传递的本质就是变量之间的赋值操作。
6.1值传递
传递给函数的参数是实参的副本,函数内部的修改不会影响到外部变量。
#include<stdio.h>
void func(int a) {
a = 20;
printf("函数中的形参a的值为:%d\n",a);
}
int main() {
int a = 10;
printf("实参a为:%d\n",a);
func(a);
printf("调用函数之后a的值为:%d\n",a);
return 0;
}
经典例子:交换两个变量的值
使用值传递看看啥情况,要和下面的地址传递来做个对比
#include<stdio.h>
void swap_value(int a,int b) {
// 先定义一个临时变量temp来保存a的值
int temp = a;
// 然后将b的值赋给变量a
a = b;
// 然后将temp的值(也就是刚才a的值给b实现交换)
b = temp;
}
int main() {
int x = 10, y = 20;
swap_value(x,y);
// 看一下有没有将两个变量的值进行交换
printf("x = %d,y = %d\n",x,y);
return 0;
}
6.2地址传递
地址传递(通过指针,指针在上一个笔记中已经介绍了,特别重要的知识点)通过传递参数的地址来允许函数修改外部变量的值。
#include<stdio.h>
void func1(int *a) {
*a = 20;
printf("函数中的形参a的值为:%d\n", *a);
}
int main() {
int a = 10;
printf("实参a为:%d\n", a);
func1(&a);//&是取地址符,取变量a的所在地址的值
printf("调用函数之后a的值为:%d\n", a);
return 0;
}
经典例子:交换两个变量的值
使用地址传递看看啥情况,要和上面的值传递来做个对比
#include<stdio.h>
void swap_addr(int *a, int *b) {
// 先定义一个临时变量temp来保存a的值
int temp = *a;
// 然后将b的值赋给变量a
*a = *b;
// 然后将temp的值(也就是刚才a的值给b实现交换)
*b = temp;
}
int main() {
int x = 10, y = 20;
swap_addr(&x, &y);
// 看一下有没有将两个变量的值进行交换
printf("x = %d,y = %d\n", x, y);
return 0;
}
6.3区别
-
值传递是将实参的值传递给函数的形参。函数接收到的是实参的副本,也就是说,函数操作的是形参的副本,而不是实参本身。就意味着函数内部对形参的修改不会影响外部实参。
-
引用传递是通过指针传递实参的地址给函数。通过传递地址,函数可以直接修改实参的值,因为函数操作的是实参的直接内存地址,而不是它的副本。
结合前面学习的指针应该会比较好理解。
7.指针函数
本质:是一个函数,只不过返回值是一个地址
如何定义:
返回值类型 * 函数名称(类型1 参数1,类型2 参数2){
}例如:
int* fun(int a,int b){
}
也就是说,指针函数其实和普通的函数一样,只不过返回值是一个指针类型的,并且它必须用同类型的指针变量来接收
举个例子
#include<stdio.h>
// 设计一个数组,返回数组首地址
int* des_array(int *plen) {
// 定义局部数组
int a[] = { 1,2,3,4,5 };
int len = sizeof(a) / sizeof(a[0]);
// 通过指针传参的方式,把数组长度传回去
*plen = len;
// 返回数组首地址(注意这里有问题,a是局部变量,函数结束就释放了,看下面解决方案)
return a;
}
// 输出数组
void output_array(int *p,int plen) {
for (int i = 0;i < plen;i++) {
printf("%d ", p[i]);
}
printf("\n");
}
int main() {
int num = 0, i = 0;
int* p = NULL;
p = des_array(&num);
output_array(p,num);
return 0;
}
运行结果如下
#include<stdio.h>
// 设计一个数组,返回数 组首地址
int* des_array(int *plen) {
// 定义静态局部数组
static int a[] = { 1,2,3,4,5 };
int len = sizeof(a) / sizeof(a[0]);
// 通过指针传参的方式,把数组长度传回去
*plen = len;
// 返回数组首地址
return a;
}
// 输出数组
void output_array(int *p,int plen) {
for (int i = 0;i < plen;i++) {
printf("%d ", p[i]);
}
printf("\n");
}
int main() {
int num = 0, i = 0;
int* p = NULL;
p = des_array(&num);
output_array(p,num);
return 0;
}
8.函数指针
本质:函数指针本质是一个指针,只是用来保存函数的地址的,通过函数指针来调用我们需要的函数。
定义方法:
数据类型 (*指针变量名)(类型1 参数1,类型2 参数2){
}
注意:
函数名代表函数的首地址
#include<stdio.h>
// 函数名就代表了函数的地址
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int main() {
int ret = 0;
int a = 10, b = 20;
ret = add(a, b);
printf("a + b = %d\n",ret);
printf("add = %p\n",add);
printf("==============================\n");
int (*funp)(int, int) = add;
printf("funp = %p\n", funp);
ret = funp(a, b);
printf("a + b = %d\n",ret);
funp = sub;
ret = funp(a, b);
printf("a - b = %d\n",ret);
return 0;
}
上面的例子演示的是函数指针来存储函数的地址的,下面来介绍一下使用函数指针来调用函数的功能,也就是回调函数的概念。
回调函数
回调函数是一个通过函数指针调用的函数。如果将某个函数的地址作为参数传递给另一个函数,在另一个函数中用指针来接收,通过指针来调用其函数,我们就说这个是回调函数。回调函数通常用于在特定事件或条件发生时,由另一个函数“回调”执行。
概念可能不是很好理解,下面来举个例子理解一下
在上面的例子中,使用的是先使用函数指针来保存函数的地址然后调用函数,不是很方便,这时候使用回调函数就挺好的,通过一个回调函数将所有的函数都调用了,下面的代码如下
#include<stdio.h>
// 函数名就代表了函数的地址
int add1(int a, int b) {
return a + b;
}
int sub1(int a, int b) {
return a - b;
}
// int a = 10;
// int b = 20;
// int (*funp)(int,int) = add1;
// 或者
// int (*funp)(int,int) = sub1;
//
// 将一个函数的地址(函数指针)作为参数传递给另一个函数
int calc(int x, int y, int(*funp)(int,int)) {
int ret = 0;
// 接收函数指针的函数在内部通过函数指针调用传入的函数
ret = funp(x,y);
return ret;
}
int main() {
int ret = 0;
int a = 10, b = 20;
// 把add函数的地址给calc函数,在calc函数中调用add1函数来实现两个数的相加
ret = calc(a, b, add1);
printf("a + b = %d\n",ret);
// 把sub函数的地址给calc函数,在calc函数中调用sub1函数来实现两个数的相减
ret = calc(a, b, sub1);
printf("a - b = %d\n", ret);
return 0;
}
9.递归函数
概念:在函数体内,自己不断地调用自己,直到某个条件满足才结束调用,这样的函数称为递归函数。
递归的一般结构包括:
void recursion() {
// 基本条件(结束条件)
if (终止条件) {
return;
} else {
// 递归调用
recursion();
}
}
两个要素:
-
(1) 基本条件 确保递归有退出的条件,否则会导致 无限递归,最终程序崩溃(栈溢出)。
-
(2) 递归关系(递推公式) 每次递归 缩小问题规模,最终趋近基本情况。
例子:求n的阶乘
涉及到数学知识了,阶乘的定义为:n!=n×(n−1)×(n−2)×⋯×1
比如说5的阶乘,
5! = 5 * 4! = 5 * 4 * 3! = 5 * 4 * 3 * 2! = 5 * 4 * 3 * 2 * 1! = 5*4*3*2*1
可以发现一个规律就是:n!=n×(n−1)!
代码如下
#include<stdio.h>
int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
else {
return n * factorial(n - 1);
}
}
int main() {
int num = 0;
printf("请输入要求几的阶乘:");
scanf("%d",&num);
int sum = factorial(num);
printf("%d的阶乘为:%d\n",num,sum);
return 0;
}
经典例子:求斐波那契数列
斐波那契数列的规律如下:
-
第 0 项:F(0)=0F(0)=0
-
第 1 项:F(1)=1F(1)=1
-
从第 2 项开始,每一项都等于前两项之和:
F(n)=F(n−1)+F(n−2)F(n)=F(n−1)+F(n−2)
根据定义,斐波那契数列的前几项为:
0,1,1,2,3,5,8,13,21,34,55,89,144,…
#include<stdio.h>
// 求斐波那契数列
int fibonacci(int n) {
if (n == 0) {
return 0;
}
else if (n == 1) {
return 1;
}else{
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
int main() {
int num = 0;
printf("请输入要计算的斐波那契数列的项:");
scanf("%d", &num);
printf("斐波那契数列的第%d项是:%d\n",num,fibonacci(num));
return 0;
}
10.函数的执行流程
先来看一个函数实例来学习执行过程
#include <stdio.h>
void print_test()
{
printf("测试函数的执行过程\n");
}
int main()
{
print_test(); // print_test函数的调用
return 0;
}
-
进入main函数
-
调用print_test()函数
a. 它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;
b. 如果找到,接着检查函数的参数,这里调用函数时没有传参,函数定义也没有形参,参数类型匹配;
c. 开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。
-
print_test()函数执行完( 这里打印一句话 ),main()才会继续往下执行,执行到return 0, 程序执行完毕。