13 KiB
一、概述
在C语言中,函数指的是功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体,因此C语言也称模块化语言。
对于函数的使用者,可以简单的将函数视为一个黑箱子,使用者只管按照规定给黑箱一些输入,就会得到一些输出,使用者不需要关注黑箱内部的结构细节。
函数分为两种形式
- 系统自带函数(库函数):只需了解如何使用和结果是什么,如:购买了一台电视机,我们只需要了解电视机对外的接口和使用的方法及最后的结果,不需要知道电视机内部构造----怎么用。
函数的头文件 函数的功能 函数的参数 函数的返回值
- 用户自定义函数:要明确最终实现的功能,如:自己设计电视机 ----怎么写。
函数的声明式 函数的实现 函数的调用
二、函数入门
-
函数头:函数对外的接口及运行的结果都在这里体现
-
组成
-
函数的类型:是函数的运行结果数据类型即黑箱的输出数据类型(函数返回值类型),不是必须的,没有返回值是使用void。
-
函数的名字:函数在内存中的地址,代表这个黑箱的名称(必须满足用户标识符定义规则),一个函数必须要有名字。
-
函数的参数列表:函数的输入,即黑箱的输入数据列表,不是必须的没有参数输入也不能省略括号(),应当在参数列表中使用void进行修饰
-
函数体:函数的功能实现,即黑箱子的内部构造。
/* 给定两个数,得到最大值 */
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不需要携带数据;
三、自定义函数
-
函数的定义
-
表示函数的功能实现及函数的确立。
返回值类型 函数名字(参数1, 参数2, ...) // 没有返回值则写void 没有参数也写void
{
功能语句;
}
-
函数的声明
-
表示告诉编译器函数将会被使用,位于函数被调用前的函数外部。
函数的类型 函数名称(参数类型及个数);
注意
-
当函数的调用出现在函数的定义位置前,则需要在调用前进行声明。
-
函数的声明一般放在头文件中,调用头文件则携带函数声明式。
-
函数的调用
-
表示使用函数实现对应的功能,位于一个函数的内部
函数的名字(参数所对应的数据);
注意
- 当函数被main直接调用或间接调用时函数才会被执行
#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;
}
练习:编写一个函数实现从键盘获取三个数,从小到大输出
/* 输入三个数从小到大输出 */
#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[])"意味着主函数也支持接收数据。
注意:
- 每个命令行参数间使用空格(一个或多个皆可),若参数本身就带有空格则需要使用单引号或双引号将整个内容包含
- 一般情况下若需要接收命令行参数,则需要在主函数中判定参数是否符号要求,以保障程序的正确执行
练习
接收命令行参数三个,输出这个三个数的和
五、变参函数
- 概念:调用函数时可根据实际需求来决定函数参数的个数
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变量的内存,释放内存。
#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位精度。
六、递归函数
-
概念:如果一个函数在内部调用自身,那么这个函数就是递归函数
-
组成:必须要有一个结束条件(简单条件),否则会导致程序栈溢出。
void fun(void)
{
fun();
}
#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关键字修饰的函数称为内联函数
inline void func(void);
-
节省函数间切换所需的时间,提高函数的运行效率
-
原理:一个普通函数在调用过程中,会在调用这个函数的函数中形成保护现场和恢复现场的过程,这个需要花费时间,降低程序的运行效率,这时可以将这个函数设计内联函数,在编译器编译过程中,编译器会将符合标准的内联函数直接展开(使用函数的功能代码替换函数的调用),这样就会节省保护现场和恢复现场的时间,同时又做到了模块化编程。
-
内联函数的使用场景
-
代码精简,功能语句简短不具有循环、switch等语句。
-
调用频繁
注意:内联函数不是写了inline声明的函数就是内联函数,能否构成内联函数是由编译器决定。若添加inline关键字但不符合编译器的标准,则编译器会将其视为普通函数。








