C语言 #函数 #库函数 #自定义函数 #形参和实参 #嵌套调用和链式访问 #函数的声明和定义 #函数指针 #函数指针数组#函数递归 #回调函数 #库函数qsort #利用qsort 思想实现冒泡排序

文章目录

前言

一、库函数

1、标准库与头文件

2、库函数的使用

二、自定义函数

三、实参和形参

1、什么是实参、形参?

2、实参与形参的关系:

3、数组传参

四、return 语句

五、嵌套调用与链式访问

1、嵌套调用

2、链式访问

六、函数的声明和定义

七、函数递归

1、什么是函数递归

2、递归的思想:

3、递归的限制条件

4、递归实例:

八、函数指针

1、概念

2、函数指针怎么书写呢?

3、函数名与函数指针

4、函数指针的用处

  5、代码一:

6、代码二:

 九、函数指针数组

1、如何定义函数指针数组

2、函数指针数组的使用

3、转移表:

十、指向函数指针数组的指针

十一、回调函数

1、回调函数是什么?

2、例:利用库函数 qsort实现快速排序

利用库函数qsort 对整型数组进行快速顺序排序:

利用库函数qsort 对结构体变量--成员 name 进行快速顺序排序:

利用库函数qsort 对结构体变量--成员 high 进行快速顺序排序:

3、冒泡排序

4、利用冒泡排序模拟实现库函数qsort:

总结


前言

路漫漫其修远兮,吾将上下而求索。


  • 在正文开始之前,我们先来了解一下什么是函数;在数学中,我们接触过一次函数、二次函数……这些函数都含有一些过程并计算得到一个值;在C语言中函数的概念与上述概念有点类似,可以理解为完成某项特定任务的一段代码。
  • main 函数也是一个函数,所有的代码均是为了实现main 函数
  • 函数大体可分为库函数和自定义函数

一、库函数

1、标准库与头文件

  • C语言标准中规定了使用C语言语法的 的各种规则,但是库函数并不是C语言语法所提供的
  • C语言的国际标准ANSIC 规定了一些常用的函数的标准,被称之为标准库;
  • 而不同的厂商根据这些常用函数的标准库做出了一系列的函数实现,这些函数就称为库函数;
  • 库函数也是函数且库函数的质量与执行效率上都有保证
  • 各种厂商做出来的编译器的标准库中提供了一系列的库函数,按照库函数的功能进行划分,需要在不同的头文件中进行声明;

2、库函数的使用

以 sqrt 为例子:

  • sqrt 的功能是 Calculates the square root. 即计算一个数的平方根

它的使用: double sqrt ( double x )

  • double sqrt 代表着库函数 sqrt 的返回类型为 double ; 
  • ( )中的为库函数sqrt 的参数
  • sqrt  的参数类型为 double 类型

在使用库函数sqrt 记得引头文件 <math.h>

 使用库函数sqrt 实现1-1000素数的打印:

代码如下:

#include<stdio.h>
#include<math.h>

int main()
{
	//先产生1-1000的数
	int i = 0;
	int count = 0;
	for (i = 2; i < 1000; i++)//此处可以优化,因为除了2,其他偶数不为素数
	{
		//再判断此数是不是素数;素数:只能被1以及本身整除
		int j = 0;
		int flag = 1;
		for (j = 2; j <= sqrt((double)i); j++)
		{
			if (i % j == 0)
				flag = 0;
		}
		if (flag)
		{
			printf("%d ", i);
			count++;
		}
	}
	printf("\n%d", count);

	return 0;
}

代码运行结果如下:

分析:

  • 素数的条件:只能被1 和本身整除
  • 思考一下:当一个数不是素数,例如16--> \textbf{}\sqrt{16} = 4 ;16 = 1 * 16 ; 16 = 2* 8; 16 = 4* 4;整数的乘法相乘得到16,只有这三种,不难发现 1,2,4均满足<= \sqrt{16}; 如果你觉得难以相信的话,我们再看一个例子;例如24 --> \sqrt{24} = 2\sqrt{6} ; 24 = 1* 24 ;    24 = 2*12;    24 = 3*  8 ;  24 = 4 * 6 ; 其中,1、2、3、4 均满足<= 2\sqrt{6}  ;
  • 所以在判断素数的循环中,只要产生的数字<= sqrt(i) 便可以进行判断了; 

以上,在使用库函数sqrt 前,引用头文件 <math.h>,后再使用sqrt;

3、库函数文档的一般格式

(还是以sqrt 为例)

二、自定义函数

1、自定义函数的语法形式

  • ret_type 为函数的返回类型;以表示函数计算结果的类型;当此函数什么也没有返回的时候,就用 void ;
  • fun_name 为函数名;funcation 本身就是函数的意思,故而此处的 fun_name 为函数名;函数名最好根据其功能起得有意义;
  • () 中放的是形式参数;有些函数不需要参数,那么此括号中便可以不写参数;为了强调此函数无参数可以在( ) 中写一个 void; 
  • { } 中放的是函数体 ,函数体是实现此函数功能的地方;

三、实参和形参

1、什么是实参、形参?

以加法函数为例子:

 代码如下:

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	int ret = Add(a, b);
	printf("%d\n", ret);

	return 0;
}

代码运行结果如下:

分析:

在主函数中调用函数Add 时,传给函数的参数a 、b 为实际参数,简称实参;顾名思义,实际参数就是实际意义上传给函数的参数

在定义函数Add 的部分,x、y 为形式参数,简称形参;用来接收函数调用时传过来的实参;为什么叫做形式参数呢?因为如果只是单单声明一个函数而未去调用它,那么形式参数就仅仅是一个存在的形式,并未向内存申请空间;只有当调用此函数时,形参为接收实参传递过来的值,才会向内存申请空间,这个过程称为形参的实例化;

2、实参与形参的关系:

  • 传值调用,实参传过去的是其值;形参是实参的一份临时拷贝;即形参也向内存申请一块空间来存放实参的值,故而传值调用时,对于形参的改变并不会影响实参;
  • 传址调用,实参传传过去的是其地址;故而形参接收的是形参的地址,形参回向内存申请空间来存放实参的地址,此时在函数中对形参操作实际上就是在对实参进行操作

注:1、当形参与实参的作用域不同的时候,形参名可以与实参名相同;

      2、形参的个数要与实参的个数保持一致;

3、数组传参

数组名为首元素地址,数组在内存空间中是连续存放的,形参只要知道了数组首元素的地址,以及数组元素的个数,那么便可以访问此数组;

注:形参部分用来接收数组时,可以不用写数组元素的个数;一是因为传参数组名实际上传的是其首元素地址;二是因为数组元素个数是需要另外传递的;三是因为当形参接收实参传递过来的数组的时候,并不会在内存空间中申请内存空间来存放此数组的数据,故而不需要数组的大小(实际上就是传址调用);

四、return 语句

在函数的设计中,若该函数会有返回值,常用到return ;

  • return 后面可以是数值,也可以是一个表达式,而若是一个表达式,则会先执行表达式然后再返回表达式的结果;(如下两图,左边的可以写成右边的形式)

  • return 后面也可以什么都没有,直接写作 return;  这种写法适合函数返回类型为 void 的情况;当然,当此函数类型为 void 的时候,便就可以不用写 return ;(如下两图所示)
  • return 语句执行后,函数就会彻底地返回,后面的代码不再执行

也可以让函数提前结束而不执行后面的代码:

  • return 返回的值和函数的返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型;(效果如下图所示)

  • 如果在函数部分存在 if 等分支地语句,要保证每种情况下都要有return 返回,否则会出现编译错误

分析:test 函数地返回类型为 int ,显然是有返回值,但是在函数体中,n = 10 不满足条件 n<0 以及 n = 0 ;所以对于 test 函数来说此时自己要返回 int 类型的数据,但是不知道返回什么,于是最后返回的是随机值,如上图所示;

上图出现的编程错误就是因为分支语句 if ; else if  ; else 没有写全;

五、嵌套调用与链式访问

1、嵌套调用

嵌套调用就是函数之间的相互调用;

举例来体会嵌套调用(计算某年某月的天数):

代码如下:


#include<stdio.h>
//#include<stdbool.h>

int is_leap_year(int y)
{
	return ((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)); //判断此年是不是闰年
}//逻辑操作符的返回值:真为1;假为0;

//bool is_leap_year(int y)
//{
//	return ((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0));
//}	
//此处也可以用bool ,记得包含头文件 <stdbool.h>

int get_date_of_month(int y, int m)
{
	int d[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//非常巧妙,将月份以数组下标的形式去访问得到此月的天数
	int date = d[m];
	if (is_leap_year(y) && m == 2)//将闰年的二月份单独拎出来
		date++;

	return date;
}

int main()
{
	int year = 0;
	int month = 0;
	scanf("%d%d", &year, &month);
	int date=get_date_of_month(year,month);
	printf("%d年%d月有%d天\n",year, month, date);
	return 0;
}

运行结果如下:

分析:为了得到某一年某一月有多少天数,可以将此功能分离出来,分装成一个函数——get_date_of_month;但是闰年的二月又与平年的二月天数不同,所以需要再写个函数——is_leap_year 判断输入的年是不是闰年;

故而在函数get_date_of_month 中调用 函数 is_leap_year,在 main函数中调用了库函数 scanf、printf 、函数get_date_of_month ,这便是函数的嵌套调用

注:函数可以嵌套调用但是不能嵌套定义

2、链式访问

链式访问就是将一个函数的返回值作为另外一个函数的参数(链式访问:像链条一样将函数串起来)

非链式访问代码如下:

#include<stdio.h>
#include<string.h>

int main()
{
	char ch[] = "i love chongqing";
	size_t len = strlen(ch);
	printf("%zd\n", len);

	return 0;
}

链式访问代码如下:

#include<stdio.h>
#include<string.h>

int main()
{
	printf("%zd\n", strlen("i love chongqing"));
	return 0;
}

代码运行结果如下:

分析:在非链式访问的代码中,我们可以明显地感知到,将库函数strlen 地返回值放到了变量 len 中,然后再利用库函数peintf 将len 中地值打印出来;这个len 就有点像是一个链条将strlen 与 printf 链接了起来,实际上可以写作:printf("%zd\n", strlen("i love chongqing")); 库函数 strlen 的返回值作为了库函数 printf 的参数,这便是链式访问;

六、函数的声明和定义

单个文件

  • 函数的使用要做到先声明后使用;
  • 函数的定义相当于一次特殊的函数声明;函数的定义若是在函数的使用之前,便相当于一次函数调用;
  • 函数的声明需要交代清楚 函数名、函数的返回类型和函数的参数(参数类型+参数名(可省略))

多个文件:

  • 一般一个工程会被拆分并放在多个文件之中,所以专门用头文件(.h)来存放函数的声明、类型的声明等;而函数的实现是放在源文件(.c )中的;
  • 去看本主扫雷游戏的实现就可以很清楚地看到多文件的分工,戳此链接:http://t.csdnimg.cn/i6DDe

七、函数递归

1、什么是函数递归

在C语言中,一个函数自己调用自己就是函数递归

2、递归的思想:

  • 将大事化小;将一个大型较复杂的问题层层转换成一个与原问题相似、但规模较小的子问题来求解;直到子问题不能再被分解,递归便结束了;
  • 递归,递就是向前推进,归就是回归;

3、递归的限制条件

(若没有这两个条件此递归一定有问题,有不保证没有问题)

  • 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续了;
  • 每一次递归之后,会越来越接近于此限制条件

4、递归实例:

例1:(求n 的阶乘)

代码如下:(注:不考虑栈溢出的情况)

//当n 为0时,其阶乘值为1;当n为2时,其阶乘为 2*(2-1);当n为3时,其阶乘为 3*(3-1)*(3-1-1)……
#include<stdio.h>

int Fact(int n)
{
	if (n == 0)
		return 1;
	else
		return n*Fact(n - 1);
	//限制条件为 n == 0; n- 1 确实也让每次递归越来越接近限制条件
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fact(n);
	printf("n的阶乘为%d\n", ret);

	return 0;
}

代码运行结果如下:

图解如下:

此处还可以利用函数栈帧的创建与销毁的知识来理解递归:

如图示:

注:在C语言中,每一次函数的调用都会为这次函数的调用在内存中申请一块内存空间,用来为此次函数的调用存放信息(这样信息才不会杂乱,有专属的空间来维护自己的数据);这便叫做运行时堆栈或者函数栈帧空间;

函数栈帧的空间是如何维护的呢?

如上图所示,随着递归的层次越来越深,向前推进的过程便会创建函数栈帧,而当函数Fact开始返回值的时候(回归),此空间便没有存在的意义了,便会将此函数对应的栈帧销毁即还给操作系统; 

如果一个递归的层次太深,那么它便会向内存中栈区申请的空间会越来越多,故而会出现栈溢出的现象;如果此递归是一个死递归,程序便会崩溃,此时会将所死递归占用的内存空间还给操作系统;

例2:(顺序打印整数的每一位)

思考:在没学习递归之前你可能回想这样处理:利用%10 得到该整数的每一位,然后利用/10 来处理每一位--> 循环下去,直到得到了这个整数的每一位,将这些数存放到数组中然后倒序打印出来即可;

而递归的思想是将一件事一层一层地剖开,然后先执行层次较深的,后执行层次浅的;给我的感受就是剥洋葱,从外面剥到里面,完全剥开之后,先吃里面的再吃外面的;

此处利用递归刚好就可以实现顺序打印一个整数的每一位;用为%10 的这一操作是从低位开始的,利用递归求得此数高位便是层次深的,而打印层次深,就实现了将一个整数每一位的正序打印;

代码如下:

#include<stdio.h>
//限制条件:当n /10 为0,便代表n没有十位了,此时就可以直接打印,不用继续递归;
void Print(int n)
{
	if (n / 10 != 0)
	{
		Print(n / 10);
		printf("%d ", n % 10);
	}
	else
		printf("%d ", n);
}

int main()
{
	int n =0;
	scanf("%d", &n);
	Print(n);
	return 0;
}

代码运行结果如下:

函数递归初见可能会有点难以理解,但是你可以将“洋葱思维”融入其中,会发现点点有趣的意味~去写写递归代码体会体会吧~

八、函数指针

1、概念

数组指针就是存放数组地址的一个指针;

函数指针顾名思义就是存放函数地址的一个指针;

那么函数的地址是怎么得到的呢?是否可以和数组一样,&函数名就可以得到函数的地址?

经过实践(自己动手)发现,&函数名确实可以得到函数的地址;同理,数组名是其首元素地址,那么函数名是什么呢?--> 函数名也是函数的地址

显然,&函数名和函数名都代表着函数的地址;

将函数的地址存放起来--> 函数指针

2、函数指针怎么书写呢?

eg.  int arr[4]= {0}; 如果想要将数组arr 的地址存到指针变量 parr中,是这样写的:  int (* parr )[4] = & arr ;  --> 其中, parr 与 * 结合,代表着变量parr 为指针变量; 指针变量parr 的类型为 int (*) [ 4 ] ,其指向对象的类型为 int [ 4 ] --> 即有4个元素为int 类型的数组;

函数呢?例如我们创建一个加法函数Add:

int Add ( int x , int y)

  return x + y ;   

}

函数Add 的返回类型为 int , ( ) 中放的是参数,参数的类型为 int,int ; 将函数Add 的地址存放到指针 pf 之中 -->   int (* pf ) (int , int ) = &Add ; 或者 int (* pf) ( int x ,int y) = Add ;

注:

  • 要说清参数的类型,参数名可省略;
  • &Add 与 Add 均代表着函数的地址

3、函数名与函数指针

存在即合理,那么函数指针究竟有什么用处呢?

eg. int * pa = &a ; 为什么要将a的地址存放到指针pa 之中?当我们将a 的地址存放到pa 之中时,那么有朝一日我便可以利用这个地址找到变量a 存放再内存空间的数据;可以对这个数据进行读取或者修改的操作;

同理,将函数的地址存放到函数的指针中,那么有朝一日我们可以利用这个指针找到函数;

我们平时调用函数就是直接函数名+ 函数参数,如果这个函数有返回值,便拿一个变量来接收其返回值;

那么对存放函数地址的指针进行解引用操作就可以得到此函数,即对此指针解引用就可以调用此函数;如下图:

思考:函数名也为函数地址;既然这样的话,存放函数地址的指针是否也可以和函数名具有同样的功能呢?如果是这样的话,那对此函数指针是不是就不需要解引用?但是上面 有 * 也可以运行,难道有没有* 会被编译器“忽略”,那么如果有多个*呢?接下来我们带着疑问实践见真知~

代码如下:

#include<stdio.h>

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    int a = 2;
    int b = 3;
    int(*pf)(int, int) = &Add;

    printf("%d\n", (*pf)(a, b));
    printf("%d\n", pf(a, b));
    printf("%d\n", (**************pf)(a, b));

    return 0;
}

代码运行结果如下:

显然我们的猜测是对的;即存放函数地址的指针其实相当于函数名的另外一个“化身”,此指针可以像函数名一样使用既然如此,对函数指针进行解引用操作其实是装装样子,你可以写*,也可以不写*,当然你写************也是可以的(*要保证在括号中与指针结合);

同理,对函数名也可以这样操作:

函数指针就是函数名的“化身”!!!!

4、函数指针的用处

在一个函数中,将另外一个函数的功能作为自己的参数;

例:(写一个具有加、减、乘、除的计算器)

代码如下:

#include<stdio.h>

void menu()
{
    printf("**********************************\n");
    printf("****1、add  2、sub  ***************\n");
    printf("****3、mul  4、div  ***************\n");
    printf("****0、 exit  *********************\n");
    printf("**********************************\n");

}
int Add(int x, int y)
{
    return x + y;
}
int Sub(int x, int y)
{
    return x - y;
}
int Mul(int x, int y)
{
    return x * y;
}
int Div(int x, int y)
{
    return x / y;
}

void calc(int(*pf)(int, int))
{
    int x = 0;
    int y = 0;
    printf("请输入两个操作数:>");
    scanf("%d %d", &x, &y);

    int ret = pf(x, y);
    printf("计算的结果为:%d\n", ret);
}

int main()
{
    int input = 0;
    do
    {
        menu();
        printf("请输入选择:>");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            calc(Add);
            break;
        case 2:
            calc(Sub);
            break;
        case 3:
            calc(Mul);
            break;
        case 4:
            calc(Div);
            break;
        default:
            printf("输入错误,请重新输入\n");
            break;
       }

    } while (input);
    return 0;
}

代码运行结果如下:

分析:

玩家根据菜单选择了对应的计算方式,然后再将此法计算的函数作为参数传到进行数据处理的函数calc 之中;既然传参传的是函数名,函数名又为函数的地址,显然calc 函数中的参数应该用函数指针来接收;即 int (* pf )(int ,int ) ; 其中,* 与pf 结合代表着变量pf 为指针,指针pf 的类型为 int (*) (int ,int ),即指针变量pf 所指向的对象的是一个返回类型为 int ,有两个为int 类型的参数的函数;

  5、代码一:

(* ( void (*) ( ) ) 0 ) ( ) ; 

是不是很迷糊?是不是很迷糊???  下面的认真看好了哈~

我们先把握整体,(xxx)() ,是没有参数的函数调用,即*(void (*)( ) ) 0 为一个函数名;显然0 是一个整型,那么前面的(void (*) () ) 是一个函数指针的类型,这个函数无参数且返回类型为 void;将一个类型装到括号之中并且放到一个数据前,聪明的你一定会想到是强制类型转换;再联想到函数名就为函数的地址,那么想像函数调用一样使用函数地址 (void (*) ( ))0 时,解引用的操作可有可无,即调用函数: (*(void (*)( ))0)( ) 或者 ((void (*)( ))0)( ) ; 即,调用的是 0 作为地址的函数,将整型0 强制类型转换成返回类型为void 并且无需传参的函数的地址

6、代码二:

void (* signal( int , void (*) (int)))( int );

同理,我们先把握整体,即 void (xx)( int ) ; 是函数的声明,那么中间一坨 :  (* signal( int , void (*) (int)))为函数名;整体方向把握之后,我们再逐步分析:* 的优先级比( )  低,所以signal 先与 (int , void (*) (int)) 结合, 即 signal 是一个函数名,并且signal 的参数一个为int ,一个为 void (*)(int)在这里我们便推翻了先前的假设;那么 signal 是一个函数名的话,那剩余的 void (*)(int) 又是什么呢?显然这是函数 signal 的返回类型;

综上思考,我们可知 void (* signal( int , void (*) (int)))( int ); 是 一个类型为 void (*)(int),有两个参数分别为 int 和 void (*)(int ) 的函数 signal 的声明;

当然此代码可以简化:

typedef void(* pf_t )(int) ;

-->  简化为:pf_t signal (int , pf_t) ;

注:typedef 关键字

如果你认认真真地看到这里,必须得夸夸你~太棒啦~~

 九、函数指针数组

函数指针数组顾名思义就是存放函数指针的数组;即一个数的元素为函数指针;

1、如何定义函数指针数组

我们先来看一看指针数组的定义,eg. int * arr[5] ; 由于 [ ] 的优先级比* 高,所以 arr先与 [ ] 结合,除去数组名剩下的为数组的类型,即数组元素的类型为 int *, 即数组arr 中存放了5个为 int* 类型的元素;

同理,对于函数指针数组理应如此; 数组名先与 [ ] ,代表着为数组,然后除去数组名剩下的为数组元素的类型;而函数的类型指针的类型中 要包含函数的返回类型、* 、函数参数的个数与类型;

那么:

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x,int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	//函数地址的获取方法:1、&函数名 2、函数名本身就是地址 
	int(*padd)(int, int) = &Add;
	int(*psub)(int, int) = &Sub;
	int(*pmul)(int, int) = &Mul;
	int(*pdiv)(int, int) = &Div;
	//方式一:&函数名
	int(*arr1[4])(int, int) = { padd,psub,pmul,pdiv };
	//方式二:函数名本身就是函数的地址
	int(*arr2[4])(int, int) = { Add,Sub,Mul,Div };

	return 0;
}

众所周知,数组为一组相同类型元素的集合,所以在数组中存放的函数的地址类型要相同,而函数地址的类型与函数的返回类型,函数参数的个数以及函数参数的类型有关;所以函数指针数组中的函数的返回类型、参数个数、参数类型要相同;

而获得函数的地址有两种形式,一是&函数名、二是函数名(函数名本身就为函数的地址);所以在函数指针数组的写法就有两种;其实本质上就是一种,即函数指针数组的元素为函数名;将函数地址取出来放入函数指针中,再将函数指针存放到函数指针数组中就有点脱裤子放屁的感觉(但确实也为一种形式);

经以上分析,总结:(以上述代码为例)

  • 函数指针数组中存放的函数其返回类型、参数个数、参数类型要相同;
  • * 的优先级比[ ] 低,所以函数名 arr1\ arr2,先与[ ] 结合,代表arr1 、 arr2 为数组;除去数组名剩下的为数组的类型,即数组的类型为 int (*)(int,int) ,显然这是函数指针变量的类型;* 代表着此变量为 指针变量,且指向对象的类型为一个返回类型为 int ,有两个均为 int类型的参数的函数;

2、函数指针数组的使用

代码如下:

#include<stdio.h>

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x,int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	//函数地址的获取方法:1、&函数名 2、函数名本身就是地址 
	int(*padd)(int, int) = &Add;
	int(*psub)(int, int) = &Sub;
	int(*pmul)(int, int) = &Mul;
	int(*pdiv)(int, int) = &Div;
	//方式一:&函数名
	int(*arr1[4])(int, int) = { padd,psub,pmul,pdiv };
	//方式二:函数名本身就是函数的地址
	int(*arr2[4])(int, int) = { Add,Sub,Mul,Div };
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int ret = arr1[i](12, 2);
		printf("%d ", ret);
	}
	printf("\n----------------\n");
	for (i = 0; i < 4; i++)
	{
		int ret = arr2[i](12, 2);
		printf("%d ",ret);
	}
	return 0;
}

代码运行结果如下:

分析:利用下标访问到数组的元素便就可以调用相对应的函数;当然,访问数组中的元素不仅可以利用下标进行访问,还可以利用地址;(想尝试可以去试一下,用地址访问函数指针数组的元素,也有点脱裤子放屁

3、转移表:

利用数组的下标,访问数组便可以转移到某个函数中;

代码如下:

//转移表
#include<stdio.h>

void menu()
{
	printf("*************************\n");
	printf("****1、add  2、sub   *****\n");
	printf("****3、mil  4、div   *****\n");
	printf("****0、exit **************\n");
	printf("*************************\n");
}
int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请输入选择:>");
		scanf("%d", &input);
		int(*parr[5])(int, int) = { 0,Add,Sub,Mul,Div };
		if (input >= 1 && input <= 4)
		{
			int x = 0;
			int y = 0;
			printf("请输入两个所要进行计算的数据:>");
			scanf("%d %d", &x, &y);
			int ret = parr[input](x, y);
			printf("%d\n", ret);
		}
		else if (input == 0)
	    	printf("退出计算器成功\n");
		else
			printf("输入错误请重新输入\n");
	} while (input);
	return 0;
}

代码运行结果如下:

十、指向函数指针数组的指针

存放函数指针的函数指针数组也会在内存中开辟空间,所以此数组也有属于自己的地址,既然是地址,那么也可以存入指针变量中;

若以上述代码中的数组arr2为例;即int(*arr2[4])(int, int) = { Add,Sub,Mul,Div }; 

int (*(*parr2) [4] ) ( int , int )  =  &arr2;

分析: * 先与 parr 结合说明变量parr 为指针变量,去除指针变量剩余的便是指针变量parr 的类型,即 parr 的类型为 int(*(*) [4] )(int , int)  ;即parr 指向的对象的类型为 int (* [4 ])(int ,int) ,其中 [4]代表它为数组,这个数组有 4 个元素,其元素的类型为 int(*)(int,int) ,即此数组的元素为函数指针;所以 指针变量parr 中存放的是函数指针数组的地址; 

十一、回调函数

其实在前文的代码中就用到了回调函数--> 模拟计算器

1、回调函数是什么?

回调函数就是通过函数指针调用的函数;

如果你将一个函数的地址作为一个参数传给另外一个函数,当此指针被用来调用所指向的函数的时候,被调用的函数就是回调函数;回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外一方函数调用的,用于对此事件进行响应;

2、例:利用库函数 qsort实现快速排序

  在写代码之前,我们先来了解一下 库函数 qsort;

库函数qsort 可以实现对任意类型数据的排序;

为什么可以面对任意类型的数据呢?因为qsort 中利用了回调函数;

void qsort (void * base , size_t num , size_t width , int (_cdecl * compare)(const void * elem 1,const void * elem 2)); 

库函数 qsort 的返回类型为 void , 第一个参数 void * base, 可以接收任何类型的地址,即传过来的数据的可以是任意类型; 第二个参数 size_t num, 为所要排序的数据的个数,size_t --> unsigned int 无符号整型;第三个参数,size_t width ,为一个待排序的数据的元素大小(所占内存空间的大小); 第四个参数为 int(_cdecl * compare)(const void * elem 1, const void * elem 2); 其中 ——cdecl* 为函数调用约定, compare 为函数名,函数compare 的返回类型为 int ,有两个参数,第一个为要比较元素1的地址,第二个参数为要比较元素2的地址;

  • 利用库函数qsort 对整型数组进行快速顺序排序:

代码如下:

#include<stdio.h>
#include<stdlib.h>

int cmpare_int(const void* e1, const void* e2)
{
	return *((int*)e1) - *((int*)e2);
}
int main()
{
	int arr[] = { 9,7,10,8,6,4,5,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmpare_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

代码运行结果如下:

分析:

首先,比较函数是需要我们自己根据所比较数据地类型自己写的;因为库函数参数中对于比较函数的要求是:返回类型为int, 有两个参数,且类型均为 const void*(最好这样写,编译器才不会报错,如果函数参数类型根据数据的类型来写,编译器会报错,不过也可以编译成功);

函数compare_int中参数e1、 e2 的类型为 void* 类型;空类型没有指向不能对其直接进行解引用操作,这时我们就需要根据排序数据的类型来确定e1、e2强制类型转换的类型,以及比较的方法 ;此处比较的元素的类型为int 类型,整型的数据直接相减进行大小的比较即可;

在库函数qsort --比较函数返回值的规定中,当e1 - e2 > 0 ;时,return 的值大于0; 当 e1-e2 =0 ;时,return 的值等于0 ; 当 e1-e2 < 0;时,return 的值小于 0;所以此处直接 return *((int*)e1) - *((int*)e2); 便可以了;

注:void* 类型的指针

int a = 10;

char * p1 = &a ; //编译器会报错,因为 &a 的类型为 int* ,而指针变量p1 的类型为 char * 

但是用空类型的指针接收便不会报错-->  void * p2 = &a ;

viod* 指针像个垃圾桶一样可以接收任何类型的指针;由于void* 没有具体类型,所以就不能对void* 类型的指针进行解引用操作或者+、- 整数的操作;因为指针变量的类型决定了解引用此指针变量时访问内存空间的权限是多大以及+、-整数时会跨越多少字节的空间;

  • 利用库函数qsort 对结构体变量--成员 name 进行快速顺序排序:

代码如下:

#include<stdio.h>
#include<stdlib.h>
struct Peo
{
	char name[10];
	int age;
	int high;
};

int compare_struct_of_name(const void* e1, const void* e2)
{
	return strcmp(((struct Peo*)e1)->name, ((struct Peo*)e2)->name);
}
int main()
{
	struct Peo s[] = { {"zhangsan",19,165},{"lisi",25,187},{"wangwu",21,170} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), compare_struct_of_name);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d %d\n", s[i].name, s[i].age, s[i].high);
	}
	return 0;
}

代码运行结果如下:

分析:

首先比较函数是需要我们自己根据所比较数据地类型自己写的;因为库函数参数中对于比较函数的要求是:返回类型为int, 有两个参数,且类型均为 const void*(最好这样写,编译器才不会报错,如果函数参数类型根据数据的类型来写,编译器会报错,不过也可以编译成功);

函数compare_struct_of_name中参数e1、 e2 的类型为 void* 类型;空类型没有指向不能对其直接进行解引用操作,这时我们就需要根据排序数据的类型来确定e1、e2强制类型转换的类型,以及比较的方法 ;此处比较的元素的为结构体变量中的成员name,所以需要将 e1、e2 强制转换成 struct Peo* 类型,然后通过 -> 操作符访问结构体变量中的成员name; 而比较字符串的长度应该用库函数strcmp; 

 

恰好,库函数strcmp 中比较两字符串的返回值与 库函数qsort 中比较函数的返回值同理;所以函数compare_struct_of_name 的返回值便写作  strcmp(((struct Peo*)e1)->name, ((struct Peo*)e2)->name) ;

  • 利用库函数qsort 对结构体变量--成员 high 进行快速顺序排序:

代码如下:

#include<stdio.h>
#include<stdlib.h>
struct Peo
{
	char name[10];
	int age;
	int high;
};

int compare_struct_of_name(const void* e1, const void* e2)
{
	return ((struct Peo*)e1)->high - ((struct Peo*)e2)->high;
}
int main()
{
	struct Peo s[] = { {"zhangsan",19,165},{"lisi",25,187},{"wangwu",21,170} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), compare_struct_of_name);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s %d %d\n", s[i].name, s[i].age, s[i].high);
	}
	return 0;
}

代码运行结果如下:

在上述使用 库函数qsort的案例中,就可以体现回调函数的用法;因为在qsort 自己内部某个合适的时间点上调用了compare 函数

3、冒泡排序

冒泡排序的核心思想:两两相邻的元素进行比较

代码如下:

#include<stdio.h>

void bubble_sort(int* arr, int sz)
{
	int i = 0;
	int j = 0;
	//趟数 :元素个数-1
	for (i = 0; i < sz - 1; i++)
	{
		//比较之后不满足顺序的要求,便两两交换:每走一趟确定一个元素的位置,下一趟比较的元素减少
		for (j = 0; j < sz - 1 - i; j++)
		{
			//条件判断
			if (arr[j]>arr[j+1])
			{
				//交换
				//由于不知所排序元素的类型吗,所以要依靠base 与 width 一个字节一个字节地交换数据信息
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
void Print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);

	}
}
int main()
{
	int arr[] = { 9,10,7,6,8,3,5,4,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	Print(arr, sz);

	return 0;
}

代码运行结果如下:

4、利用冒泡排序模拟实现库函数qsort:

代码如下:

#include<stdio.h>

int compare_int(const void* e1, const void* e2)
{
	return*(int*)e1 - *(int*)e2;
}
void Swap(char* e1, char* e2, int width)
{
	//利用width 来控制循环地次数;每交换完一字节便 width--;
	while (width)
	{
		//交换
		char tmp = *e1;
		*e1 = *e2;
		*e2 = tmp;
		//调整
		e1++;
		e2++;
		width--;
	}
}
void bubble_sort(void* base, size_t sz, size_t width, int(*cmp_int)(const void*, const void*))
{
	int i = 0;
	int j = 0;
	//趟数 :元素个数-1
	for (i = 0; i < sz - 1; i++)
	{
		//比较之后不满足顺序的要求,便两两交换:每走一趟确定一个元素的位置,下一趟比较的元素减少
		for (j = 0; j < sz - 1 - i; j++)
		{
			//条件判断
			if (cmp_int((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				//交换
				//由于不知所排序元素的类型吗,所以要依靠base 与 width 一个字节一个字节地交换数据信息
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
void Print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", p[i]);
		//数组名为首元素地址,而指针变量 p中存放的是数组arr的首元素地址;
		//编译器处理数组的本质是利用其首元素地址往后访问的原理,所以指针变量 p 可以当作数组名一样使用;
		//再者,访问数组元素的实质是利用其首元素地址与偏移值进行解引用操作;
		//即,p[1] 在编译器看来就是*(p+1),同理数组arr[1]--> *(arr+1) 
		//所以指针变量p 可以当作数组名来使用;
	}
}
int main()
{
	int arr[] = { 9,10,7,6,8,3,5,4,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), compare_int);
	Print(arr, sz);
	return 0;
}

代码运行结果如下:

分析:

在利用库函数qsort 模拟实现冒泡排序的时候,只有比较函数需要程序员自己根据所要比较的元素的类型进行调整,而整体的bubble_sort 是可以面对所有类型的数据的;所以在Swap 函数中,就得利用此一个数据的大小来实现数据一个字节一个字节地交换,利用 width 来控制交换字节的次数(实际上就是控制根据此数据在内存中所占内存空间的字节大小来控制交换的长度),以及调整交换指针指向的对象;

传参中所要比较的两元素的指针写作 (char*)base + j * width   、  (char*)base + (j + 1) * width) , 本质上也是对于内存空间字节的掌握;


总结

写于此,看目录回顾便是最好的总结;

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值