412 lines
13 KiB
Markdown
412 lines
13 KiB
Markdown
# 一、概述
|
||
|
||
在C语言中,函数指的是功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体,因此C语言也称模块化语言。
|
||
|
||
对于函数的使用者,可以简单的将函数视为一个黑箱子,使用者只管按照规定给黑箱一些输入,就会得到一些输出,使用者不需要关注黑箱内部的结构细节。
|
||
|
||

|
||
|
||
函数分为两种形式
|
||
|
||
- 系统自带函数(库函数):只需了解如何使用和结果是什么,如:购买了一台电视机,我们只需要了解电视机对外的接口和使用的方法及最后的结果,不需要知道电视机内部构造----怎么用。
|
||
|
||
> 函数的头文件
|
||
> 函数的功能
|
||
> 函数的参数
|
||
> 函数的返回值
|
||
|
||
|
||
- 用户自定义函数:要明确最终实现的功能,如:自己设计电视机 ----怎么写。
|
||
|
||
> 函数的声明式
|
||
> 函数的实现
|
||
> 函数的调用
|
||
|
||
|
||
# 二、函数入门
|
||
|
||

|
||
|
||
- 函数头:函数对外的接口及运行的结果都在这里体现
|
||
|
||
- 组成
|
||
|
||
- 函数的类型:是函数的运行结果数据类型即黑箱的输出数据类型(函数返回值类型),不是必须的,没有返回值是使用void。
|
||
|
||
- 函数的名字:函数在内存中的地址,代表这个黑箱的名称(必须满足用户标识符定义规则),一个函数必须要有名字。
|
||
|
||
- 函数的参数列表:函数的输入,即黑箱的输入数据列表,不是必须的没有参数输入也不能省略括号(),应当在参数列表中使用void进行修饰
|
||
|
||
- 函数体:函数的功能实现,即黑箱子的内部构造。
|
||
|
||

|
||
|
||
```c
|
||
/* 给定两个数,得到最大值 */
|
||
int Maxfun(int x, int y) // 这是一个函数的定义,该函数接收两个int类型的参数,返回一个int类型数据
|
||
{
|
||
return x>y?x:y;
|
||
}
|
||
|
||
/* 交换两个浮点数 */
|
||
void swap(double *p1, double *p2) // 该函数接收两个double *类型的参数,无返回值
|
||
{
|
||
if (p1 == NULL || p2 == NULL)
|
||
return; // 退出当前函数
|
||
|
||
double tmp = *p1;
|
||
*p1 = *p2;
|
||
*p2 = tmp;
|
||
}
|
||
|
||
/* 液晶屏初始化函数 */
|
||
char *initLCD(void) // 该函数不接收参数,返一个char *类型的数据
|
||
{
|
||
int lcd = open("/dev/fb0", O_RDWR);
|
||
|
||
struct fb_var_screeninfo vinfo;
|
||
ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo);
|
||
|
||
int bpp = vinfo.bits_per_pixel;
|
||
int size = vinfo.xers*vinfo.yers*bpp/8;
|
||
|
||
char *fbmmem = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHSRED, lcd, 0);
|
||
|
||
return fbmmem;
|
||
}
|
||
```
|
||
|
||
总结:
|
||
|
||
- 当函数的类型为void时,表示函数不返回任何数据。
|
||
|
||
- 当函数的参数列表为void时,表示函数不需要任何参数。
|
||
|
||
- 关键字return表示退出函数。
|
||
|
||
①若函数头中规定有函数的类型,则return需要携带一个类型与之匹配的数据;
|
||
|
||
②若函数头中规定函数的类型为void,则return不需要携带数据;
|
||
|
||
# 三、自定义函数
|
||
|
||
- **函数的定义**
|
||
|
||
- 表示函数的功能实现及函数的确立。
|
||
|
||
```c
|
||
返回值类型 函数名字(参数1, 参数2, ...) // 没有返回值则写void 没有参数也写void
|
||
{
|
||
功能语句;
|
||
}
|
||
```
|
||
|
||
- **函数的声明**
|
||
|
||
- 表示告诉编译器函数将会被使用,位于函数被调用前的函数外部。
|
||
|
||
```c
|
||
函数的类型 函数名称(参数类型及个数);
|
||
```
|
||
|
||
注意
|
||
|
||
- 当函数的调用出现在函数的定义位置前,则需要在调用前进行声明。
|
||
|
||
- 函数的声明一般放在头文件中,调用头文件则携带函数声明式。
|
||
|
||
- **函数的调用**
|
||
|
||
- 表示使用函数实现对应的功能,位于一个函数的内部
|
||
|
||
```c
|
||
函数的名字(参数所对应的数据);
|
||
```
|
||
|
||
注意
|
||
|
||
- 当函数被main直接调用或间接调用时函数才会被执行
|
||
|
||
```c
|
||
#include <stdio.h>
|
||
|
||
//int Maxfun(int x, int y); // 函数的声明:当函数的调用发生在函数的定义前时需要书写
|
||
int Maxfun(int , int ); // 函数的声明:当函数的调用发生在函数的定义前时需要书写
|
||
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
|
||
printf("%d与%d的最大值是%d\n", 80, 800, Maxfun(80,800)); // 将Maxfun函数的执行结果作为printf函数的一个参数
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* 交换两个浮点数 */
|
||
void swap(double *p1, double *p2) // 函数的定义 该函数接收两个double *类型的参数,无返回值
|
||
{
|
||
printf("%s is running\n", __FUNCTION__);
|
||
if (p1 == NULL || p2 == NULL)
|
||
return;
|
||
|
||
double tmp = *p1;
|
||
*p1 = *p2;
|
||
*p2 = tmp;
|
||
}
|
||
|
||
/* 给定两个数,得到最大值 */
|
||
int Maxfun(int x, int y) // 这是一个函数的定义,该函数接收两个int类型的参数,返回一个int类型数据
|
||
{
|
||
printf("%s is running\n", __FUNCTION__);
|
||
double a = 3.14, b=5.26;
|
||
printf("交换前:a = %f b = %f\n", a,b);
|
||
swap( &a, &b); // 调用一个函数:定义在调用前可,免去声明
|
||
printf("交换后:a = %f b = %f\n", a,b);
|
||
return x>y?x:y;
|
||
}
|
||
```
|
||
|
||
练习:编写一个函数实现从键盘获取三个数,从小到大输出
|
||
|
||
```c
|
||
/* 输入三个数从小到大输出 */
|
||
#include <stdio.h>
|
||
|
||
void outputNum(void);
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
outputNum();
|
||
return 0;
|
||
}
|
||
|
||
|
||
void swap(int *p1, int *p2) // 函数的定义
|
||
{
|
||
|
||
if (p1 == NULL || p2 == NULL)
|
||
return;
|
||
|
||
*p1 ^= *p2;
|
||
*p2 ^= *p1;
|
||
*p1 ^= *p2;
|
||
|
||
}
|
||
|
||
void outputNum(void)
|
||
{
|
||
int a,b,c;
|
||
printf("请输入三个数:");
|
||
scanf("%d%d%d", &a,&b,&c);
|
||
|
||
if(a>b)
|
||
{
|
||
swap(&a,&b);
|
||
}
|
||
if(a>c)
|
||
{
|
||
swap(&a,&c);
|
||
}
|
||
if(b>c)
|
||
{
|
||
swap(&b, &c);
|
||
}
|
||
printf("%d < %d < %d\n",a,b,c);
|
||
}
|
||
```
|
||
|
||
## 函数参数的传递
|
||
|
||
- 形参与实参
|
||
|
||
- 形参:函数声明式定义中参数列表中的参数,属于函数的局部变量,在函数定义中只是一个形式参数没有实际的值。
|
||
|
||
- 实参:函数调用中参数列表中的参数,具有实际的数值或表示对象,在函数执行期间这个数值或对象将作用与函数内部,实参会在函数执行时初始化函数的形参。
|
||
|
||
- 值传递:表示实参的类型时一个数值,形参的类型是基本数据类型;
|
||
|
||
- 址传递:表示实参是一个地址,形参的类型是一个指针;
|
||
|
||
- 当需要将函数内部的形参的改变作用与实参时则需要使用址传递(传递地址),否则使用值传递(传递数值)。
|
||
|
||
## 总结
|
||
|
||
- 函数的优点
|
||
|
||
- 提高代码的重用性。 试想一下, 一个函数也许会在 N 多地方被使用, 如果没有将该功能封装起来, 而在每一个用到这个功能的地方都写一遍代码, 将会浪费很多资源。就像一个企业给每一个员工都配备一台打印机, 虽然每个人独占资源用起来很便捷, 但却浪费了大量的成本。
|
||
|
||
- 方便维护和升级源代码。 假设需要对一个的算法修正或者修改, 那只要不改变函数接口和功能的情况下, 可以方便地进行, 不需要知道该函数在何处被调用。 调用者也感觉不到代码的改变, 因为函数的封装性使得他们让调用者感觉起来是透明的。
|
||
|
||
- 有利于结构化代码。 将一个个功能封装在相对独立的函数里, 再将函数组装成程序,那么整个逻辑就很清晰, 出错了也较容易排查。 否则, 在一个没有结构化的代码中, 所有的功能杂乱地挤作一团, 逻辑复杂, 也极易出错。
|
||
|
||
- 函数封装的要求
|
||
|
||
- 高内聚:一个功能集中在一个函数的内部。
|
||
|
||
- 低耦合:功能函数与功能函数间的影响要低。
|
||
|
||
# 四、主函数传参
|
||
|
||
- 函数参数列表不为void则表示函数可以接收数据,主函数"int main(int argc, char *argv[])"意味着主函数也支持接收数据。
|
||
|
||

|
||
|
||

|
||
|
||
注意:
|
||
|
||
- 每个命令行参数间使用空格(一个或多个皆可),若参数本身就带有空格则需要使用单引号或双引号将整个内容包含
|
||
|
||

|
||
|
||
- 一般情况下若需要接收命令行参数,则需要在主函数中判定参数是否符号要求,以保障程序的正确执行
|
||
|
||

|
||
|
||
## 练习
|
||
|
||
接收命令行参数三个,输出这个三个数的和
|
||
|
||
# 五、变参函数
|
||
|
||
- 概念:调用函数时可根据实际需求来决定函数参数的个数
|
||
|
||
```c
|
||
int printf(const char *restrict format, ...);
|
||
int scanf(const char *restrict format, ...);
|
||
|
||
printf("%d", a);
|
||
printf("%d, %d", a, b);
|
||
printf("%d, %d, %d", a, b, c);
|
||
```
|
||
|
||
- 定义变参函数
|
||
|
||
- 添加头文件"#inclded stdarg.h"
|
||
|
||
- 定义函数时末尾参数使用省略号(...)表示可以更具需求来确认,省略号前可以自由设置参数类型(至少一个,强制参数)
|
||
|
||
- 在函数定义中创建va_list类型变量,用于存放可变参数
|
||
|
||
- 使用va_start(),用于初始化va_list类型变量,初始化内存。
|
||
|
||
- 使用va_arg() ,来访问可变参数列表中的每个项。
|
||
|
||
- 使用va_end(),来清理va_list变量的内存,释放内存。
|
||
|
||
```c
|
||
#include <stdio.h>
|
||
#include <stdarg.h>
|
||
|
||
int fun(int n,...);
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
fun(2, 1, 2);
|
||
fun(5, 1, 2);
|
||
|
||
return 0;
|
||
}
|
||
|
||
int fun(int n,...)
|
||
{
|
||
// 在函数定义中创建一个va_list变量list,将来调用时传递的参数存储在list中
|
||
va_list list;
|
||
|
||
// 使用强制参数初始化变量list
|
||
va_start(list, n);
|
||
|
||
// 使用va_arg()访问list中的每个项
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
printf("%d\n", va_arg(list, int));
|
||
}
|
||
|
||
// 清除list中的缓存
|
||
va_end(list);
|
||
}
|
||
```
|
||
|
||
## 练习
|
||
|
||
编写一个变参函数,用于计算多个数的平均值(double)保留2位精度。
|
||
|
||
# 六、递归函数
|
||
|
||
- 概念:如果一个函数在内部调用自身,那么这个函数就是递归函数
|
||
|
||
- 组成:必须要有一个结束条件(简单条件),否则会导致程序栈溢出。
|
||
|
||
```c
|
||
void fun(void)
|
||
{
|
||
fun();
|
||
}
|
||
```
|
||
|
||

|
||
|
||
```c
|
||
#include <stdio.h>
|
||
/* 计算阶乘 */
|
||
int func(int n)
|
||
{
|
||
if(n==1)
|
||
return n;
|
||
return n*func(n-1);
|
||
}
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
int n = 0;
|
||
scanf("%d", &n);
|
||
printf("%d", func(n));
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
注意:递归虽好但不是所有情况都适合,比如数据规模太大会导致"栈溢出"
|
||
|
||
递归函数的特点:代码精简但效率低。
|
||
|
||
## 练习
|
||
|
||
- 使用递归打印N的斐波拉契数列
|
||
|
||
1 1 2 3 5 8 13 ......
|
||
|
||
- 有5个人坐在一起,问第五个人多少岁?他说比第四个人大两岁。问第四个人岁数,他说比第三个人大两岁。问第三个人,又说比第二个大两岁。问第二个人,说比第一个人大两岁。最后问第一个,他说是10岁。编写程序,当输入第几个人时求出其对应年龄。
|
||
|
||
# 七、回调函数
|
||
|
||
- 回调(callback) 是一种非常重要的机制, 主要可以用来实现软件的分层设计, 使得不同软件模块的开发者的工作进度可以独立出来, 不受时空的限制, 需要的时候通过约定好的接口(或者标准) 相互契合在一起, 也就是 C++或者 JAVA 等现代编程语言声称的所谓面向接口编程。 同时回调也是定制化软件的基石, 通过回调机制将软件的前端和后端分离, 前端提供逻辑策略, 后端提供逻辑实现。
|
||
|
||
- 作用:统一操作接口,开放功能定制。
|
||
|
||
# 八、内联函数
|
||
|
||
- 使用关键字inline关键字修饰的函数称为内联函数
|
||
|
||
```c
|
||
inline void func(void);
|
||
```
|
||
|
||
- 节省函数间切换所需的时间,提高函数的运行效率
|
||
|
||
- 原理:一个普通函数在调用过程中,会在调用这个函数的函数中形成保护现场和恢复现场的过程,这个需要花费时间,降低程序的运行效率,这时可以将这个函数设计内联函数,在编译器编译过程中,编译器会将符合标准的内联函数直接展开(使用函数的功能代码替换函数的调用),这样就会节省保护现场和恢复现场的时间,同时又做到了模块化编程。
|
||
|
||

|
||
|
||
- 内联函数的使用场景
|
||
|
||
- 代码精简,功能语句简短不具有循环、switch等语句。
|
||
|
||
- 调用频繁
|
||
|
||
**注意:内联函数不是写了inline声明的函数就是内联函数,能否构成内联函数是由编译器决定。若添加inline关键字但不符合编译器的标准,则编译器会将其视为普通函数。**
|
||
|
||
[函数练习题.docx](attachments/WEBRESOURCE94606765850c0696e801236ba49014a2函数练习题.docx)
|
||
|
||
# 作业
|
||
|
||
[函数作业题.docx](attachments/WEBRESOURCEa2d88b6292b52c177b4bc6323a5bdd07函数作业题.docx) |