世界杯战报
掌握C语言函数的应用:从入门到精通
1、函数的定义
说明:在C语言中,函数意味着功能模块,一个典型的C语言程序(项目),就是由一个个功能模块拼接起来的整体,也因为如此,C语言被称为模块化语言(面向过程的语言),对于函数的使用者,可以简单地理解为一个黑箱,使用者只管按照规定给黑箱输入一些输入,就会得到一些输出,而不必理会黑箱内部的运行情况
黑箱的输入和输出:
举个例子(电视机): 日常使用的电视机可以被理解为一个典型的黑箱,它有一些公开的接口提供给使用者操作,比如:音量、频道等。作为使用者不需要理会其内存电路,更不需要管电视机的工作原理,只需要规定接口,操作接口就可以得到结果
对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节,对于 一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的
2、函数的说明和使用
函数头:函数对外的接口
函数名称:命名规则与变量一致,一般取与函数实际功能相符合的、顾名思义的名称(在这里建议大家采用ST公司的命名规则)参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,甚至可以不要参数返回类型:即黑箱的输出数据类型,一个函数可不返回数据,但最多只能返回一个数据
函数体:函数功能的内部实现(即使用花括号括起来的内容)语法说明:
关键字 返回值类型 函数名(参数列表)
{
代码块;
return 返回值;
/*
说明:
关键字: 用来修饰函数的(一般系统会存在默认关键字,非必要的), 比如: static inline
1、使用 static 修饰的函数称之为静态函数, 仅限于本文件中有效;
2、使用 inline 修饰的函数称之为内联函数,类似与宏替换;
返回值类型: 用来确定返回值是什么类型的数据(return 0; -> int return 'a'; -> char ...)
函数名: C语言标识符,也就是给一块{ }代码起一个名字。(如果要调用此函数,则通过函数名调用即可)
参数列表: 调用函数的时候,想要给这个函数传递什么样的数据。(传参)
*/
}
代码示例:
示例1:有返回值,有传参示例2:没有返回值,有传参示例3:有返回值,没有传参示例4:没有返回值,没有传参
#include
#include
#include
// 函数示例1(有返回值,有传参):比较两个int型数据的大小
int MAX_OfTwoNum(int num1, int num2)
{
int max = 0;
max = num1>num2?num1:num2;
return max;
}
// 函数示例2(没有返回值,有传参):交换两个数据
void SWAP_OfTwoNum(double *p1, double *p2)
{
// 1、判断p1和p2是否为NULL,是的话直接退出即可
if ( (p1 == NULL) || (p2 == NULL) )
return;
// 2、数据交换
double tmp = 0;
tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 函数示例3(有返回值,没有传参):申请一块堆空间(申请资源的时候)
char* MEM_Init(void)
{
// 堆空间申请内存(推荐)
char *p = malloc(100); // 堆空间, 只能由程序员申请和释放(或者整个程序结束了)
bzero(p, 100);
// 栈区申请内存
// char buf[128] = {0}; // 不建议在函数里面这么写,因为函数结束后,其内存空间会被系统释放掉(所以其它工程里面的其它函数用不了这个内存空间)
// char *p = buf;
return p;
}
// 使用函数示例3申请的内存空间
void func(char *p)
{
strcpy(p, "人生没有彩排,只有现场直播,所以每一件事都要努力做到最好\n");
// 只有这个指针p指向的这个内存有合法空间并且可读可写,才可以使用这个strcpy的字符串函数进行赋值操作
}
// 函数示例4(没有返回值,没有传参):消息提示、初始化
void MSG_Tip(void)
{
printf("今天天气真好,花儿都开了\n");
}
// 主函数
int main(int argc, char const *argv[])
{
// - 示例1:有返回值,有传参
int ret = 0;
ret = MAX_OfTwoNum(100, 200);
printf("ret == %d\n", ret);
// - 示例2:没有返回值,有传参
double f1 = 3.14;
double f2 = 6.28;
printf("交换前:f1 == %.1f, f2 == %.1f\n", f1, f2);
SWAP_OfTwoNum(&f1, &f2);
printf("交换后:f1 == %.1f, f2 == %.1f\n", f1, f2);
// - 示例3:有返回值,没有传参
char *p1 = MEM_Init(); // 让指针p1指向一个合法内存空间
func(p1); // 通过指针p1将数据赋值到指针p1指向的内存空间里面
printf("p1 == %s\n", p1); // 通过指针p1打印它指向的那个内存空间的数据
// - 示例4:没有返回值,没有传参
MSG_Tip();
return 0;
}
语法汇总:
当函数的参数列表为void时,表示该函数不需要任何参数当函数的返回类型为void时,表示该函数不返回任何数据关键字return表示退出函数:若函数头中规定由返回数据类型,则return需要携带一个类型与之匹配的数据;若函数头中规定返回的类型为void,则return不需要携带参数(直接return即可)
图解:
3、函数的实参和形参
概念:
函数调用中的参数,被称为实参,即:argument("实际参数": 在函数调用过程中,主调函数传递给被调函数的输入参数值,我们称为实际参数。(具有实际数据))。函数定义中的参数,被称为形参,即:parameter("形式参数": 被调函数在定义时的参数。(只有一个形式/框架,不具有实际数据))。
实参和形参的关系:
实参和形参的类型和个数必须一一对应形参的值由实参初始化形参与实参位于不同的内存区域,彼此独立
示例代码:
#include
// 宏定义 -- 比较两个数据的大小
#define MAX(A, B) (A)>(B)?(A):(B)
// 普通函数
int MAX_OfThreeNum(int num1, int num2 , int num3) // 函数定义的地方:形参(形式参数),实参与形参的类型和个数必须一一对应
{
int max = MAX(num1, num2);
max = MAX(max, num3);
return max;
}
// 主函数
int main(int argc, char const *argv[])
{
int num1 = 100; // 实参和形参位于不同的内存区域,彼此独立(因为它们处于不同的花括号{}中)
int num2 = 200;
int num3 = 300;
int ret = 0;
ret = MAX_OfThreeNum(num1, num2, num3); // 函数调用的地方:实参(实际参数)
printf("ret == %d\n", ret);
return 0;
}
4、函数的局部变量和栈内存
局部变量的概念:凡是被一对{}花括号包含的变量,称为局部变量局部变量特点:
某一函数内部的局部变量,存储在该函数的特定的栈内存中局部变量只能在该函数内存可见,在该函数外不可见当该函数退出后,局部变量所占的内存被系统回收,因此局部变量也称为临时变量函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量
栈内存的特点:
每当一个函数被调用的时候,系统会自动分配一段栈内存给该函数,用于存放其局部变量每当一个函数退出的时候,系统会自动回收其栈内存系统为函数分配栈内存时,遵循从上(高位地址)到下(低位地址)分配的原则
图解:
示例代码:
#include
#include
#include
// 普通函数
int func1(int a, int b) // 局部变量(栈区)3:函数的形参(虽然不被花括号{}括住,但是依然属于函数体的范畴)
{
// int a; // 上面的形参,相当于在函数体里面定义了这两个变量
// int b;
int c = a+b; // 局部变量(栈区)4:
return c; // 在函数执行完前,已经将这个c变量里面的值,传了出去
}
char *func2(void)
{
// 栈内存空间申请
char p[] = "hello world!"; // 局部变量(栈区)5:
return p;
/*
解析:
warning: function returns address of local variable [-Wreturn-local-addr]
翻译:警告,返回的是一个函数的临时变量的地址
说明:
所以这个地址可以获取,但是其指向的内存空间已经被系统释放掉了
其它函数调用这个局部变量的地址的函数后,会出现这个问题
*/
// 推荐:堆空间的申请(但是要注意的事情是,需要及时释放申请的空间,否则会造成内存泄漏)
// char *p = malloc(100);
// bzero(p, 100);
// return p;
}
// 主函数
int main(int argc, char const *argv[])
{
// (1)、局部变量的说明(凡是被一对{}花括号包含的变量,称为局部变量)
// (只要被花括号括起来的就是另一个内存区域,即使和其它区域的名字相同,也是相互独立的)
// 局部变量(栈区)1:
int a = 100;
int b = 200;
{
int a = 100; // 局部变量(栈区)2:
int b = 200;
}
// 函数调用
int ret = func1(100, 200);
/*
返回的是值(储存它的值的内存空间在赋值前才被释放掉,这个值重新
找了现在这个变量ret来存放其数据了(记住一件事情:你看看你现在操作的
内存是不是被释放掉的内存(或者说合不合法))
*/
printf("ret == %d\n", ret);
// (2)、函数运行完后,系统将自动将其内存释放,最好不要再次调用
char *p = func2(); // 因为指针p指向的还是那块内存(已经被释放掉的内存,所以后续指针对其操作是不可以的)
return 0;
}
5、回调函数
概念:回调函数就是一个被作为参数传递的函数。
int func(int a)
{
printf("func a:%d\n", a);
}
int show(int (*pfunc)(int))
{
pfunc(10);
}
// 主函数
int main(int argc, char **argv)
{
show(func); // 此时的func函数就是一个回调函数
/*
头文件: #include
函数原型: void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
参数说明:
base 指向你要排序的数组的开头地址(就是你要排序的数组)
nmemb 代表数组中的元素数量(就是数组有多少个元素)
size 表示每一个元素的大小(元素数据类型大小)
compar 为一个函数指针,这个函数用来判断两个元素间的大小关系的,
若传给compar的第一个参数所指的元素大于第二个参数所指的元素则返回大于0的值,若两值相等,则返回0;
*/
return 0;
}
执行过程;
1 main()识别到show(func),执行show()函数,并将func函数传递到show()中;
2 在show()函数中,识别到pfunc()的形参,则识别形参*pfunc的地址,并将10传递到func函数中,再跳转到func中;
3 在func()函数中,接受形参a的值,并执行其中的代码语句;
*/
6、内联函数
概念:类似于宏替换,是由inline修饰的函数。意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。这样做的好处是省去了调用的过程,加快程序运行速度。(将函数的东西插入到原代码中) ----- (这不是原生C语言的函数,是引用C++里面的)应用场景:代码工程量大时,需要简短代码;函数被频繁调用时;
示例代码:
#include
inline int add(int a, int b)
{
return (a + b);
}
int main(void)
{
int a;
a = add(1, 2);
printf("a+b=%d\n", a);
return 0;
}
使用注意:
内联函数实际上是用空间代价(程序尺寸增大)来换取时间效率(不再需要切换函数);内联函数定义一般放在头文件中;内联函数的代码块尽量不超过5行;内联函数体中,不能有循环语句、if语句或switch语句,否则,函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。内联函数要在函数被调用之前声明。关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。而且内联函数在声明阶段也要加上inline关键字。
内联函数与函数封转区别:
资源开销区别:内联函数是在编译阶段展开,函数封装在代码运行时调用展开的,宏定义是在预处理阶段展开的;内联函数在编译阶段展开,所以可能会增加生成的可执行文件大小; (编译过程产生可执行文件)函数封装在代码运行时调用展开的,所以可执行文件相对较小,但运行效率相对低一点;(运行过程会影响CPU效率)宏定义在预处理阶段展开,且只做宏替换,所以代码编译过程中产生的文件较大。
7、静态函数
概念:由static修饰的函数,称之为静态函数作用:防止一个工程中不同文件因函数名冲突,静态函数仅限于本文件有效。(总结:限制函数作用域,就是防止函数名相同导致函数名冲突)示例代码
#include
// 以下函数通过关键字static修饰,只能在本文件中使用
// 通过static修饰的函数,就叫静态函数
static int static_func()
{
printf("这是%s的static_func函数\n", __FILE__);
}
int func()
{
printf("这是%s的func函数\n", __FILE__);
static_func(); // 这是test.c的static_func函数
}
static int static_func()
{
printf("这是%s的static_func函数\n", __FILE__);
}
int main() {
func(); // 注释了main.c的func函数后,调用时的是test.c中的func函数
static_func(); // 这是main.c的static_func函数
/*
vvc@DESKTOP-CFCSUDI:/mnt/e/share$ gcc main.c test.c -o main && ./main
这是test.c的func函数
这是test.c的static_func函数
这是main.c的static_func函数
*/
}
8、递归函数
概念:函数自己调用自己,称之为递归函数
递归函数设计:
问题模型本身要符合递归模型(递推模型);
问题的解,当递归到一定层次时,答案是显而易见的,且能结束函数。
先明确函数要实现的功能与参数的关系,暂不管功能具体的实现。
呈现第n层与第n-1层的递推关系。
示例代码:
#include
// 幂运算函数
int func(int base, int mi)
{
if (mi == 0) // 1、可退出条件(任何数的0次方,都是1)
{
return 1;
}
if (mi > 1) // 1、可退出条件
{
base = base*func(base, mi-1); // 2、逐渐递进(条件越来越接近)的过程
}
return base; // 3、逐渐回归(答案越来越接近)的过程
}
// 斐波那契数列fibonacci sequence
int f_s(int N) // 6
{
if ( (N == 1) || (N == 2)) // 1、可退出条件
{
return 1;
}
return f_s(N-1)+f_s(N-2); // 2、答案和条件越来越近的过程
}
/*
解析
f_s(6-1)+f_s(6-2)+
f_s(5-1)+f_s(5-2) f_s(4-1)+1
f_s(4-1)+1 1+1 1+1+1
1+1+1 1+1 1+1+1
*/
// 例子1:输出自然数(大于等于0的正整数:0 1 2 3 4 5 6 7 8...)
void func1(int num) // 5 4 3 2 1 0 -1
{
if ( num < 0) // 1、可退出条件
{
return;
}
func1(num-1); // 2、逐渐递进的过程(退出条件越来越接近)
printf("%d ",num); // 3、逐渐回归的过程(答案越来越接近)
}
// 例子2:阶乘(不能乘0,也没有必要乘1:0*1*2*3*4*5)
int func2(int num) // 5*func2(5-1)、5*4*func2(4-1)、5*4*3*func2(3-1)、5*4*3*2func2(2-1)、1
{
if (num>1) // 1、设置可退出条件
{
num = num*func2(num-1); // 2、逐渐递进的过程(退出条件越来越接近)
}
return num; // 3、逐渐回归的过程(答案越来越接近)
}
// 主函数
int main(void)
{
// main(); 这么写,而且没有退出条件,直接撑爆了栈内存(报错误:Segmentation fault (core dumped))
// 例子1:输出自然数
func1(5);
printf("\n");
//练习1: 幂运算。写一个函数,输入参数数据(底数和幂数), 比如,底数为5,幂数为5,返回值(相当于 == 5*5*5*5*5 )
printf("数据 == %d\n", func(5, 5));
//练习2:编写一个程序,用户输入整数 N,程序输出第 N 项斐波那契数。
int ret = f_s(7);
printf("ret == %d\n", ret);
return 0;
}
程序图解:
递归函数注意事项:
自己调用自己的才叫递归函数,自己调用别的函数叫函数嵌套;递归函数一定要有一个终止条件,否则容易出现栈溢出(段错误);递归函数最好不要递归太深(太多次),否则容易出现栈空间奔溃;
递归函数和循环的区别:
递归函数是一种调用自身的方法,通常用于解决可以分解成相似子问题的任务,但注意栈溢出,不能递归深度过大;循环是一种重复执行一段代码的机制,通常用于处理重复性高的任务,但执行代码没递归直观;递归函数每调用函数时,都会在栈上创建一个新的帧,循环只使用一个帧;递归适用于自然分解子问题的任务,如树的遍历、阶乘计算等;循环一般用在数组便利、计数等比较简单的任务;