C语言指针进阶:数组指针和函数指针

1、指针基础知识总结

       指针就是地址(内存中一个最小单元的编号,一个内存单元就是1byte),也是一种数据类型(如int*类型,char*类型),口语中说的指针通常指的是指针变量,存放的是地址。32位机器(地址线32根,有4G的编址空间),一个指针变量的大小就是4字节。

       指针变量的基类型用来指定此指针变量可以指向的变量的类型,指针类型有两个含义:被解引用操作时访问几个字节;指针加减操作的步长。

       野指针:指针指向的位置是不可知的(随机的、不正确的、没有明确限制的):①int *p;p没有初始化,放的随机值,非法地址,*p=10,非法访问内存,p就是野指针。②当指针指向的范围超过数组arr范围时,就是野指针。③函数创建局部变量,函数调用完之后变量被销毁(返回局部变量地址),int *p=test()也是野指针。编程时应避免野指针的出现:明确的给指针变量初始化,如果不知初始化什么可int *p=NULL。

       二级指针类型是两个*,如int**,二级指针变量是用来存放一级指针变量的地址的,例int *p = &a;int **pp = &p,pp就是二级指针。

在指针指向数组元素时,可以对指针进行以下运算:

1)加减一个整数:如p+1,p-1;

2)自加运算,自减运算:p++,p--;

3)两个指针相减:如p1-p2(只有p1和p2都指向同一数组的元素才有意义),结果是p1-p2的值(地址之差)除以数组元素的长度,这个结果是有意义的,表示中间有几个元素。注意不是字节数,例一个int型10个数的数组,最后一个指针减第一个指针的结果是9不是36!两个地址不能相加,如p1+p2是无实际意义的。

       指针的三种运算:指针+-整数,指针减指针,指针的关系运算(比较,如for循环中经常用到的p<p_end),需注意标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。编程中需注意尤其是for循环条件语句。

一个结构体指针运算练习题:

struct Test
{
    int Num;
    char* pcNanme;
    short sDate;
    char cha[2];
    short sBa[4];
}*p;

       假设上面结构体指针p的值为0x100000,已知结构体Test类型的变量大小是20个字节(X86环境),求下列表达式地址的值?

p + 0x1;
(unsigned long)p + 0x1;
(unsigned int*)p + 0x1;

解析:p是结构体指针,指向整个结构体,p+1跳过整个结构体,所以p+1是0x100014;把p指针类型强制转换为unsigned long型后,指针类型变成unsigned long数据类型了,也就是整型,整型+1就是+1,所以是0x100001;把p结构体指针类型强制类型转换为unsigned int*类型,+1就跳过4个字节,所以是0x100004。

2、指针和数组

int *p1[10];                                //指针数组

int (*p2)[10];                           //数组指针

注意:[ ]的优先级要高于*,所以必须加上()来保证p先和*结合。

       存放指针的数组就是指针数组,例int* parr[10]={&a,&b,&c};解引用*(parr[i])就可以访问a,b,c了。指针数组无足轻重,在视频学习时讲到将一维数组的首地址通过指针数组关联起来,可以模拟二维数组,比较好理解此处不做赘述。

2.1 数组指针

       数组指针就是指向数组的指针。数组指针是用来存放数组的地址(这句话需要好好理解,同整型指针是用来存放整型的地址一样),数组指针也是一种类型,int (*)[?],所以加1跳过类型对应的长度即对应数组的长度。

怎么用数组指针存放数组的地址:

int arr[10] = {0};

int *p = arr;                //p存的仅仅是首元素地址,不是数组地址

首先要明白什么是数组地址?(&数组名就是数组地址),所以:

int (*p)[10] = &arr;                //[10]表示指向元素个数为10的数组,10必须确定

int *p = &arr就是错误的写法了,因为根本就是两个不同的类型,整型指针存的是整型地址,怎么能存放数组地址呢。 

通过int (*p)[10] = &arr 定义数组指针,那么怎么解引用数组元素呢?

       p指向数组,*p相当于数组名(首元素地址),所以*(*p+i)就可以访问数组元素,但是这样使用非常别扭,一维数组不这么用,普遍用int *p = arr,*(p+i)访问数组元素。这种用法常见是用在二维数组,下面将介绍,明白了数组指针就可以更好的理解指针引用二维数组了。

思考:char *arr[5] = {0};怎么定义指针存放该指针数组的地址呢? 答案:char *(*p)[5] = &arr。

2.2 通过指针引用数组

       引用数组元素可以用下标法,也可以用指针法,其实C编译系统是将a[i]转化为*(a+i)处理的。因为数组是一组连续的内存空间,数组名代表首元素地址,所以数组的操作大多时候可以转化为指针操作,使用指针法能使目标程序质量高(占内存少,运行速度快)。善用指针引用数组元素,方便灵活,有不少技巧。

2.2.1 通过指针引用一维数组

       *(a+i)或 *(p+i):其中a是数组名,p是指向数组元素的指针变量,其初值p=a,a是指针常量代表首元素地址不能++等运算,p可以

       使用指针变量指向数组元素时一定要注意指针变量的当前值。将++和--运算符用于指针变量十分有效,可以使指针变量自动向前或或向后移动,指向上一个或下一个数组元素,但如果不小心很容易弄错,因此在使用*p++形式的运算时一定要十分小心,弄清楚先取p值还是先使p加1。

       指针变量可以指向数组以后的存储单元,例如定义一个数组指定它包含10个元素,在程序中引用arr10](越界访问),虽然并不存在这个元素,但C编译并不认为此非法。应避免出现这样的情况,在使用指针变量指向数组元素时,应切实保证指向数组中有效的元素。

       因为程序编译时对下标的处理是转换为地址的,指向数组的指针变量p也可以带下标,但必须弄清楚p的当前值是什么,如果p指向arr[3],则p2不代表arr[2],而是arr[5],建议少用这种容易出错的用法。

       注意*p++等价于*(p++),先解引用p再使p自增1,不是p指向的值自增1。

2.2.2 通过指针引用二维数组

      通过指针引用一维数组很简单,二维数组就要复杂一些了,因为二维数组是数组的数组,还引入了数组指针的概念。

二维数组的数组名的理解:

       特别注意:以int arr[3][4]为例:二维数组名也表示二维数组首元素地址,但一定明确不是表示arr[0][0]的地址。二维数组是数组的数组,每一行就是一个元素,arr数组包含3行,即3个行元素:arr[0],arr[1],arr[2]。所以首元素就不是一个简单的整型元素,而是由4个整型元素所组成的一维数组因此arr代表的是首行(行序号为0的行)的首地址,二维数组名就是数组指针,指向数组arr[0]。arr+1代表序号为1的行的首地址,arr[0],arr[1],arr[2]是一维数组名。

二维数组元素的地址:

       那么arr数组0行1列元素的地址怎么表示?当然是arr[0]+1,与arr+1表示的地址含义如下图1所示,假设首行首地址为2000:

图1 二维数组元素的地址

       前已述及:arr[i]和*(arr+i)等价(务请记住!),因此arr[0]+1和*(arr+0)+1都是&arr[0][1],arr[1]+2和*(arr+1)+2都是&arr[1][2],*(arr+3)相当于arr[3]。所以指针访问二维数组元素怎么表示,以访问arr[1][2]为例:*(arr[1]+2),也即*(*(arr+1)+2),它与*(arr[1] + 2)等价,于是又与arr[1][2]等价(利用arr[i]和*(arr+i)等价推导),所以二维数组传参如果传的是数组指针,是可以用形参名[ ][ ]来访问二维数组的。

进一步说明arr[i]的性质:

       arr[i]从形式上看是arr数组中序号为i的元素。如果arr是一维数组名,则arr[i]代表arr数组序号为i的存储单元,arr[i]是有物理地址的,是占存储单元的。但如果arr是二维数组,则arr[i]是一维数组名,它只是一个地址,是数组指针,并不代表某一元素的值(如同一维数组名只是一个指针常量一样)。二维数组中:arr,arr+i,arr[i],*(arr+i),*(arr+i)+j,arr[i]+j都是地址。

表1 二维数组arr的有关指针
表示形式含义地址
arr二维数组名,指向一维数组arr[0],即0行首地址2000
arr[0],*(arr+0),*arr0行0列元素地址2000
arr+1,&arr[1]1行首地址2016
arr[1],*(arr+1)1行0列元素arr[1][0]的地址2016
arr[1]+2,*(arr+1)+2,&arr[1][2]1行2列元素arr[1][2]的地址2024
*(arr[1]+2),*(*(arr+1)+2),arr[1][2]1行2列元素arr[1][2]的值

怎么理解arr+1和*(arr+1)都是地址?

      首先说明,arr+1是二维数组arr中序号为1的行的首地址,而*(arr+1)并不是arr+1单元的值,因为arr+1并不是一个变量的存储单元,也就谈不上指针解引用了。*(arr+1)就是arr[1],是一维数组名,所以也是地址,指向arr[1][0]。arr[1]和*(arr+1)都是二维数组中地址的不同表现形式。

      arr+1与arr[0]+1是不同的,arr+1是序号为1的行的首地址,arr+1指向序号为1的行,而*(arr+1)或arr[1]或arr[1]+0都指向1行0列元素,二者地址虽然相同,但是含义不同。arr和arr[0]的值虽然相同,但是由于指针的类型不同,面向的对象不一样,arr指向一维数组arr[0],是数组指针,而arr[0]指向列元素arr[0][0],是int型变量指针,所以对不同的指针进行加1的运算得到的结果是不同的。

       再次强调:二维数组名是指向行的,一维数组名是指向列元素的,我的大学C语言教科书上有一个形象的比喻,arr相当于排长,而每一行即一维数组arr[0],arr[1],arr[2]相当于班长,每一行中的元素相当于战士,排长指向班,纵向管理,纵向走一步就跳过一个班;而班长指向战士,横向管理,横向走一步只是指向下一个战士。

       arr[0],arr[1],arr[2]的类型为int *类型,指向整型变量,而arr的类型为int *[4]类型,指向含4个元素的一维数组(数组指针)。

怎么用sizeof计算二维数组的行数和列数?

行数计算:sizeof(arr) / sizeof(arr[0]);

列数计算:sizeof(arr[0])。

思考(下列定义分别表示什么含义):

int arr[5];

int *parr1[10];

int (*parr2)[10];

int (*parr3[10])[5];

答案:分别是整型数组,整型指针数组,数组指针,数组指针数组。怎么理解最后一个是存放数组指针的数组?

       将(*parr3[10])拿出来是一个指针数组,int [5]表示指针指向的是含5个整型元素的数组,所以可初始化为:int (*parr3[10])[5] = {&arr1,&arr2,&arr3}。

二维数组指针及二维数组在内存中存储方式笔试题:

#include<stdio.h>

int main()
{
	int a[5][5];
	int(*p)[4];
	p = a;
	printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);

	return 0;
}

       内存指针类分析要善于画图,a是int(*)[5]类型,p是int(*)[4]类型,都是数组指针,它们在内存中的存储示意图如下:

图2 二维数组指针笔试题

考点一:指针相减结果是地址之差除以数组元素长度,所以以%d形式打印时输出是-4而不是-16;

考点二:-4以指针(%p)的形式输出时是输出-4这一地址内容,即-4的补码形式(在内存中存储的形式,地址相当于无符号,没与源码补码之说,%d是将补码转换为源码,输出源码形式),-4就是地址内容,所以输出0xFFFFFFFC(X86环境)。

二维数组初始化及指针笔试题(巨坑):

int main()
{
	int a[3][2] = { (0,1),(2,3),(4,5) };
	int* p;
	p = a[0];
	printf("%d\n", p[0]);
	return 0;
}

       指针考点很简单,p[0]就是a[0][0]的值,但是要小心a[0][0]的值不是0,而是1,上面二维数组初始化实际是int a[3][2] ={1,3,5,0,0,0},不是int a[3][2] ={{0,1},{2,3},{4,5}}。

3、函数指针

3.1 函数指针概念 

       数组指针指向数组,同理,函数也是有地址的,如果在程序中定义了一个函数,在编译时,编译系统为函数代码分配一段存储空间,这段存储空间的其实地址称为这个函数的指针。函数指针指向函数,&函数名和函数名都是函数的地址。函数指针的定义(*必须与名称括起来保证pf先与*结合,第一个int表示指针指向返回值类型为int的函数,后两个int表示指向的函数有两个int型参数):

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

int main()
{
    int (*pf)(int,int) = &add;    //函数指针定义
}

        对pf解引用可以找到函数,对其传参也相当于调用函数,即:

int ret = (*pf)(2,3);        //(*pf)括号必须要有

       *可以不写,即:

int ret = pf(2,3);

        函数地址传参用法:

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

void fun(int (*pf)(int,int))        //函数指针形参接收函数地址
{
    int a = 3;
    int b = 5;
    int ret = pf(a,b);        //函数调用
}

int main()
{
    fun(add);        //函数名作实参

    return 0;
}

       首先看下面两句代码分别表示什么含义?

1、(*(void (*)() )0 )();

       这是一次函数调用,调用的是0作为地址处的函数:首先void(*)()是函数指针类型,指向无参无返回值的函数,( void(*)()0 )把0强制类型转换为无参无返回值的函数的地址;然后解引用地址调用0地址处的这个函数。

2、void (* signal( int,void(*)(int) ) )(int)

       首先signal一定是函数名,int 和void(*)(int)是signal函数的两个参数,一个是int型,另一个是函数指针类型,类似于add(int x,int y);还差返回类型,把signal( int,void(*)(int) )拿掉后就剩下void (*)(int),这是一个函数指针,说明signal函数的返回值是函数指针类型。因此,上面代码是一次函数声明,声明的函数signal函数的第一个参数是int型,第二个参数是函数指针,指向的函数参数是int型,返回类型是void,signal函数的返回类型是函数指针。

       上面代码可以简化写法,利用typedef类型定义将void(*)(int)函数指针类型重定义为一个比较简单的名字,不过要注意不是typedef    void(*)(int)     pf_t;而是

typedef     void(* pf_t)(int),这样上述函数声明就可以写成:pf_t  signal(int,pf_t);

3.2 用函数指针调用函数

       调用函数除了通过函数名调用外,还可以通过指向函数的指针变量。函数名调用函数只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数,下面通过一个简单的例子说明:

       有两个整数x和y,由用户输入1,2,3或4,程序分别给出x和y的加、减、乘、除计算结果,首先如果没有学过函数指针的程序员肯定是通过if语句或switch语句进行判断调用不同的函数实现该功能,少不了如下图3所示代码:

图3 不用函数指针例程

       上面例程代码冗余,不够简洁专业。每次选择时调用不同的函数,这时,用指针变量就比较方便了,定义一个函数cal,其中一个形参设计为可以指向加、减、乘、除函数的函数指针(用函数指针作形参),这样每次选择后调用cal函数只需根据选择向cal函数传入不同函数名作实参即可。改进后的例程如下图4所示:

图4 通过函数指针调用函数

        显然用指针变量使程序更简洁和专业。从本例中可以清楚地看到,不论调用add,sub,mul或div,函数cal都没有改变,只是改变实参函数名而已,在不同的情况下调用了不同的函数,因此结果是不同的,这就增加了函数使用的灵活性。可以编写一个通用的函数来实现各种专用的功能,例如编写一个求定积分的通用函数,用其分别求以下4个函数的定积分:

\int_{a}^{b}\left (1+x \right )dx        \int_{a}^{b}\left ( 2x+3 \right )dx        \int_{a}^{b}\left ( e^{x}+1 \right )dx        \int_{a}^{b}\left ( 1+x \right ) ^{2}dx

       可以编写一个求定积分的通用函数intergal,它有三个形参:下限a,上限b以及指向函数的指针变量pf,intergal函数原型可以写为:

float integral(float a,float b,float(*pf)(float))

3.3 回调函数(qsort函数)

       回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。回调函数通常用于事件处理、异步编程和处理各种操作系统和框架的API

异步编程:指在代码执行时不会阻塞程序运行的方式。

事件驱动:指程序的执行是由外部事件触发而不是顺序执行的方式。

       回调函数是一种非常灵活和强大的编程技术,可以让我们更好地处理各种异步操作和事件,它可以在异步操作完成后调用一个预定义的函数来处理结果。回调函数通常用于处理事件、执行异步操作或响应用户输入等场景。回调函数的作用是将代码逻辑分离出来,使得代码更加模块化和可维护。使用回调函数可以避免阻塞程序的运行,提高程序的性能和效率。另外,回调函数还可以实现代码的复用,因为它们可以被多个地方调用。回调函数的使用场景包括:

事件处理:回调函数可以用于处理各种事件,例如鼠标点击、键盘输入、网络请求等。

异步操作:回调函数可以用于异步操作,例如读取文件、发送邮件、下载文件等。

数据处理:回调函数可以用于处理数据,例如对数组进行排序、过滤、映射等。

插件开发:回调函数可以用于开发插件,例如 WordPress 插件、jQuery 插件等。

回调函数实现数据排序—qsort库函数

       首先看冒泡排序代码:

void bubble_sort(int arr[],int sz)        //冒泡排序算法
{
    int i = 0;
    for(i =0; i < sz -1; i++)
    {
        int flag = 1;    
        int j = 0;
        for(j = 0; j < sz-1-i; j++)
        {
            if(arr[j] > arr[j + 1])
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                flag = 0;
            }
        }
    }
    if(flag == 1)
    {
        break;
    }
}

       冒泡排序算法由于参数是int arr[ ],所以只能排序整型数据,倘若有一组结构体数据类型需要排序,以一个含有人名、年龄、电话等元素的结构体为例,就不是简单的按大小排序了,我们可以定义各种比较方法,例如按年龄大小、名字等。C库函数qsort函数就可以实现任意类型数据的排序,不同类型的数据采用不同比较方法,把比较这一模块由程序员自定义一个函数,qsort函数的一个参数就是函数指针,指向一个可以实现某种比较方法的函数。qsort函数的原型为:

#include<stdlib.h>
void qsort(void* base,								//待排序的数据的起始位置
	size_t num,										//待排序的数据元素的个数						
	size_t width,									//待排序的数据元素的大小(单位是字节)
	int(_cdecl* compare)(const void* elem11, const void* elem12)	//函数指针-比较函数
);    //函数指针两个参数是待比较两个元素的地址

//返回值:返回int型,e1>e2返回大于0的值,e1=e2返回0,e1<e2返回小于0的值

_cdecl:函数调用约定;

void*:因为可以比较任意类型的数据,所以参数用void*无具体类型的指针(即泛型)。void* 是泛型指针,可以接收任意类型指针,不能直接解引用操作,需要先强制类型转换再解引用,也不能+-整数。

       所以要使用qsort函数比较整型数据,提供一个比较两个整型数据大小的函数即可,代码例程为:

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

//自定义一个比较两个整型数据的函数传给qsort用
int cmp_int(const void* e1, const void* e2)//返回类型与参数类型需与qsort函数指针指向的函数一致
{
	//*************复杂写法start***************
	//if (*(int*)e1 > *(int*)e2)		//因为比较的是整型数组里的元素,将void* 泛型强制类型转换为整型
	//{
	//	return 1;		//qsort库函数要求e1>e2返回大于0的值
	//}
	//else if (*(int*)e1 < *(int*)e2)
	//{
	//	return -1;		//qsort库函数要求e1<e2返回小于0的值
	//}
	//else
	//{
	//	return 0;		//qsort库函数要求e1=e2返回0
	//}
	//*************复杂写法end*****************

	//高明写法只需一句
	return ((*(int*)e1) - (*(int*)e2));		//qsort默认是升序

	//降序写法
	//return ((*(int*)e2) - (*(int*)e1));	

}

int main()
{
	int arr[10] = { 1,3,4,5,6,8,2,7,10,9 };		//待比较的整型数组

	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	qsort(arr, sz, sizeof(int), cmp_int);		//调用qsort对数组排序

	printf("排序后:");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	return 0;
}

       可以看到使用qsort函数排序整型数据对程序员十分友好,不需要我们实现像冒泡排序那样复杂的算法,只需提供一个比较两个整型数据大小的函数传给qsort即可。而且因为有个参数是函数指针类型,只要我们提供一个比较函数,就可以排序任意类型数据。以排序一个结构体类型数据为例:

例程:

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

struct Stu			//待比较的数据类型-结构体类型
{
	char name[20];
	int age;
};

int cmp_by_name(const void* e1, const void* e2)		//对结构体数组排序通过比较名字(即字符串比较)进行排序
{
	return strcmp( ( (struct Stu*)e1 )->name , ( (struct Stu*)e2 )->name );	//注意括号和->的使用,->前面的括号必须有
}
int main()
{
	struct Stu s[] = { {"zhangsan", 14}, {"lisi",15}, {"wangwu",16}};		//待排序的结构体数组

	int sz = sizeof(s) / sizeof(s[0]);
	printf("排序前:");
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%s  ", s[i].name );
	}
	printf("\n");
	qsort(s, sz, sizeof(s[0]), cmp_by_name);
	printf("排序后:");
	for (i = 0; i < sz; i++)
	{
		printf("%s  ", s[i].name);
	}
	printf("\n");

	return 0;
}

运行结果:

图5 qsort函数比较结构体数据例程运行结果

strcmp函数比较机制:两个字符串逐个字符比较直至遇到不同字符按ASCII值比较大小,恰好它的返回值和qsort函数的返回值一样。

       知道了qsort函数的参数和功能之后,我们甚至可以改造冒泡排序函数模拟实现qsort函数,使其可以排序各种类型数据,改进例程如下(自行完成,成就感拉满):

#include<stdio.h>

int cmp_int(const void* e1, const void* e2)		//比较整型数据大小
{
	return ((*(int*)e1) - (*(int*)e2));
}

swap(char* base_t, int width_t, int x)
{
	int i = 0;
	char tmp =0;

	for (i = 0; i < width_t; i++)
	{
		tmp = *(base_t + x * width_t + i);
		*(base_t + x * width_t + i) = *(base_t + (x + 1) * width_t + i);
		* (base_t + (x + 1) * width_t + i) = tmp;
	}
}

void bubble_sort(void* base, size_t num, size_t width, int (*compare)(const void* ele1, const void* ele2))	//参考qsort函数实现各种类型数据排序
{
	//用冒泡排序思想
	int i = 0, j = 0;

	for (i = 0; i < num-1; i++)
	{
		for (j = 0; j < num - 1 - i; j++)
		{
			if (compare(((char*)base + j * width), ((char*)base + (j + 1) * width)) > 0)
			{
				swap((char*)base,width,j);
			}
		}
	}

}

int main()
{
	int arr[10] = { 6,8,9,3,1,4,10,7,2,5 };

	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:");
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	
	bubble_sort(arr, sz, sizeof(int), cmp_int);	
	
	printf("排序后:");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	return 0;
}

例程解析:

       冒泡排序的核心思想是实现逐对相邻两个数的比较,首先需要数据类型转换,因为不知道要比较的数据是什么类型,选择转换为单位最小的char*类型;然后根据待排序元素大小参数计算相邻两个数地址,传给compare函数,用户自定义的compare函数会将两个地址强制类型转换为想要比较的数据类型,例如本例程比较int型数据在解引用时就会先强制类型转换为int型;qsort默认升序,因此只有在compare函数(参考qsort函数)返回大于0值的时候才需要交换比较的相邻两个数,交换过程的实现由于也不知道数据类型,所以采用逐个字节一一对应交换的方法,共交换width个字节

3.4 函数指针数组

       函数指针数组就是存放函数指针的数组,C语言中不可定义函数数组,只能通过定义函数指针数组来操作。正确定义函数指针数组的前提:这些函数必须有相同的输入输出值,例如3.2中add,sub,mul和div函数它们都有相同的返回类型和参数类型,就可以把它们的函数名(地址)存放在一个函数指针数组中,定义函数指针数组类型的写法为:

int (*arr[4])(int,int) = {add,sub,mul,div};    //add,sub,mul,div均为函数名

怎么理解?

       arr先和[4]结合,去掉arr[4]就是数组中每个数据类型,即int (*)(int,int)函数指针类型,所以函数指针数组定义写法就是把函数名改为arr[ ?]。arr[i]就分别对应add,sub,mul,div函数地址,也可直接arr[i]调用,例如arr[0](4,8)就等价于add(4,8)。

       函数指针数组是否和函数指针一样也是大有用处呢?答案是肯定的,同样通过3.2计算器例程进行说明:假设现在要新增200个计算机功能(比如异或,左移右移等),图3例程switch语句中后面就要增加200个case语句,代码变得非常长,改动大,并且每一次处理都要做如此多的判断之后才找到正确的处理函数,代码的执行效率也不高。函数指针数组就解决了这个问题,改进后的例程如下图6所示:

图6 通过函数指针数组改进例程

       上述代码通过定义一个函数指针数组存放各个函数的地址,摒弃了switch语句,通过函数指针数组下标调用函数,免去了判断过程并且减少了代码编写工作量,而且如果需要新增函数实现更多功能时代码的改动也方便简洁,函数指针数组的优势显现无疑。

同数组指针一样,当然也有指向函数指针数组的指针,用的不多不作展开。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值