指针——下篇

目录

一.二级指针

二.数组指针

1.数组指针的定义

2.数组指针的使用

三.指针数组

四.函数指针

1.函数指针的定义

2.回调函数

五.函数指针数组

1.函数指针数组的定义

2.函数指针数组的应用(转移表)

六.指向函数指针数组的指针

七.经典题解析

1.第一道题

2.第二道题


一.二级指针

指针即地址,地址即指针

假设存在下面这段程序

 a变量在栈区有相应自己的空间进行存储,而空间肯定就有其对应的地址,而p变量则开辟了相应的空间,恰好存储了a的地址,所以&a,p实际上是同一个东西.

既然p变量也开辟了相应的空间存储(指针上篇已经讨论过这个问题,4个字节或8个字节大小),有空间肯定就有对应地址,而我们把这个地址再次存起来,就是我们的二级指针.

就像有个名叫a的人,在酒店定了四间连续编号的房间,而有个仰慕a的名叫p(一级指针)的人,偷偷记下来第一间房间的房间号,所以通过p,我们可以知道a住的四个房间.

但是p不知道,他定的四间连续编号的房间,早已经被一个名叫pp(二级指针)的人也偷偷记住,通过pp,我们可以知道p住的四个房间.

二.数组指针

1.数组指针的定义

数组指针中的数组是定语,是用来修饰指针的,所以数组指针,本质上是一个指针.

指向数组的指针,我们便称之为数组指针.

那如何定义呢?我们先尝试类比一下

 我们知道存储浮点数的地址,就在float/double后面加一个*,存储整型的地址,就在int,long,char后面加一个*,于是假设一个存在一个数组int arr[10](存储了10个整型)

然后指针需要加入*说明它是一个指针,那我们就可以得到

int *p [10]

但是这样就可以吗?我们还知道[]的优先级要高于*号的,所以我们还需要加上()来保证p先和*结合

于是就可以得到  int (*p) [10],这个指针指向一个数组,总共10个元素,每个元素类型为int.

2.数组指针的使用

指针中篇中我们其实也谈到过,数组名代表的是首元素地址,而&数组名,取出的是整个数组的地址,表面上两者的数值是相同的,但是比如说+1等步长,两者跳过的空间大小是不同的.

现在我们便知道它们两者本质区别在哪里了

前者的类型是int *,加步长1跳过的只是int

后者的类型是int (*) [num],加步长1跳过的是一个数组的大小.

那我们具体如何使用呢?

#include <stdio.h>
int main ()
{
    int arr [ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 };
    int ( * p )[ 10 ] = & arr ;   // 把数组 arr 的地址赋值给数组指针变量 p
    return 0 ;
}
像上面那段程序,我们刚刚解释了是可行的,但我们一般不是这么用,因为没有什么用处,我们一般使用指针,是想访问相应的元素.
数组指针真正的作用在于二维数组.
我们知道二维数组实际是一行一行作为单元进行存储,也就是二维数组数组名,作为首元素的地址,实际上指向第一行数组的地址.
在函数传参中,假设我们传一个二维数组进入函数,我们形参设置上除设置完全一样的二维数组
(比如说int arr[num1][num2], 注意形参中可以省略行数,但绝对不能省略列数,对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素),同时, 设置一个数组指针接收二维数组也是完全可以的.

三.指针数组

和数组指针相反,现在的定语变为指针,是用来修饰数组的,所以数组指针,本质上是一个数组.

存储指针(元素是指针)的数组,我们便称之为指针数组.

通过得到数组指针的推理,我们可以轻松得到指针数组的表现形式,比如说:

int * p[10];(1)

char * arr2 [ 4 ];(2)
char ** arr3 [ 5 ];(3)
都是指针数组,因为没有加括号,优先和[ ]结合,说明这是一个数组,那里面的元素类型就由前面所决定,比如(1)中元素类型就是int *,(2)中元素类型是char*,(3)中元素类型是char**,一个二级指针.

四.函数指针

1.函数指针的定义

既然存在数组指针,那我们稍微改变一下定语,将数组改为函数,便得到函数指针.

指向函数的指针,我们便称为函数指针.

函数也是有地址的,因为C语言调用一个函数,也同时需要开辟一个空间,具体可以看函数栈帧的开辟一节,而空间就对应有地址.

&函数名,函数名都是代表函数的地址,两者没有区别.

void test ()
{
    printf ( "hello\n" );
}
假设存在这样一个函数,类似数组指针,我们可以按照同样思路设计出函数指针.
void *pfun ()

同样,我们可以知道函数调用运算符优先级最高,为了说明它是一个指针,需要加一个括号

void (*pfun) (),这样pfun和*优先结合,说明是一个指针,剩下的部分就是指针指向的内容,一个不需要参数,返回也是void的函数.

再看上面这个例子,我设计了一个pf的函数指针,存放Add函数的地址,然后通过解引用pf,就可以调用Add这个函数. 

值得注意的是,因为我们在函数调用的时候,Add(3,5)即可,而此时pf存放了Add这个函数的地址,因此pf无论是否加*使用,都是可以的.

接下来,我们再看两个具体的例子

先看代码1

第一个突破口就在0这,0是一个int类型的变量,我们注意到最前面还有一个*,只有指针也就是地址,才可以解引用.

第二个突破口是void (*)(),这是一个函数指针类型,指向一个不需要返回值,参数的函数.

结合两个突破口,我们便知道,这段代码的实际含义

将0强制类型转化为一个函数指针类型,然后解引用,访问0地址处的函数.

再看代码2

先看第一部分,signal是一个函数名,signal第一个参数是Int,第二个参数是一个函数指针. 

再看第二个部分,void (*) (int)是一个函数指针类型,括号里面应该放一个函数指针.

结合两个部分,我们便知道,这是一个函数声明,声明的函数叫signal

signal返回的类型也是一个函数指针,第一个参数是int,返回类型是void.

但本身这段代码是不易阅读的,而我们还注意到void (*) (int),实际上重复出现了两次,所以我们可以考虑用typedef对它进行简化.

 首先将函数指针类型,重命名为pf_t

然后根据我们上面分析的思路,可以将代码大大化简. 

2.回调函数

实际上,函数指针的概念我们并不陌生,在qsort函数模拟实现中,我们曾经就运用过这个概念,qsort函数其中一个参数,就需要我们传递一个比较两个元素方法的函数指针.

无独有偶,在实现顺序线性表L中查找第1个值与我们给定元素满足compare()的元素的位序的函数时,我们也运用到函数指针作为形参.

int LocateElem_Sq(Sqlist L,ElemType e,int (* compare) (ElemType,ElemType));

像这样

如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数
时,我们就说这是回调函数.
像上面的compar,compare都是回调函数
一般它是 在特定的事件或条件发生时由另外的一方调用的,而不是函数实现方直接调用.

五.函数指针数组

1.函数指针数组的定义

我们第三部分曾经提到过指针数组,也就是每一个元素是一个指针,而函数指针本质就是一个指针.

所以自然存在函数指针数组这样的概念.

首先我们设定一个数组   parr1 [ 10 ]
假定数组中每一个元素都是一个int (*) (int x,int y)类型的函数指针
结合两者,我们类比指针数组,就可以得到指针数组的形式
int (*parr1[10])(int x, int y)

2.函数指针数组的应用(转移表)

假设设计一个简单的计算器,代码如下

void menu()
{
	printf("*********************************\n");
	printf("******* 1.Add    2.sub     ******\n");
	printf("******* 3.mul    4.div     ******\n");
	printf("******* 0.exit             ******\n");
	printf("*********************************\n");
	printf("*********************************\n");
}
int main()
{
	int input = 0;
	int ret = 0, x = 0, y = 0;
	do
	{
		menu();
		printf("请输入数字: \n");
		scanf("%d", &input);
		switch (input)
		{
		   case 1:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = add(x, y);
			printf("ret = %d\n", ret);
			break;
		   case 2:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = sub(x, y);
			printf("ret = %d\n", ret);
			break;
		   case 3:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = mul(x, y);
			printf("ret = %d\n", ret);
			break;
		  case 4:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = div(x, y);
			printf("ret = %d\n", ret);
			break;
		  case 0:
			printf("退出程序\n");
			break;
		  default:
			printf("选择错误\n");
			break;
		}
	}while (input);
	return 0;
}
但我们可以发现,这段代码实际上很冗余,假设我未来要给该计算器再加上新的功能,那就需要加更多的switch case语句,那代码阅读体验感就很差.
我们可以通过函数指针数组的概念进行优化.
int main()
{
    int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	do{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出程序\n");
		}
		else if ((input <= 4 && input > 0))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else
			printf("输入有误\n");
	} while (input);
	return 0;
}

于是,假如存在新的元素,我们只需要改变函数指针数组的大小,和menu菜单函数即可,大大优化了代码的可读性.

六.指向函数指针数组的指针

函数指针数组是一个数组,那同样可以通过一个指针指向该数组.

这个指针,就被称为指向函数指针数组的指针.

假设存在这样一个函数指针数组

void ( * pfunArr [ 5 ])( const char* str );
那为了表示它是一个指针,就需要加一个*,而*优先级我们知道低于[ ],因此,我们需要加上括号.
void ( *     (*ppfunArr )     [ 5 ])( const char* );

七.经典题解析

1.第一道题

求输出的值为多少?

解析:

 a为首元素的地址,则a作为二维数组的数组名,实际上它的类型是int (*)[5],而p的类型仅仅只能指向4个int类型的数组.因此会先发生强制类型转化.

p[4][2]代表的仅是*(*(p + 4) + 2),p作为int (*)[4]仅仅只能访问含4个int的数组,因此p+4,最后移动到图示位置,然后解引用可以访问一个4个int的数组,加2则访问这个数组的第二个元素.

因此&p[4][2] - &a[4][2]得到的是-4,地址打印即是-4的补码,因为数在电脑中以补码形式存储,地址无正负号区别,因此,%p打印的是-4的补码.

最后答案:

2.第二道题

 

求最后输出的答案是什么?

解析: 

 根据题目,我们可以画出如上图的表示图

cpp++后,两次解引用,我们可以得到POINT中P的首元素地址,%S打印出来即是POINT

 

 接下来,cpp++,由于*,++,--优先级都比+号高,所以会先对cpp++,进行解引用,得到c+1,但此时又对它进行--,变成了c

此时对c又解引用,得到E的首元素地址,+3得到E的地址,然后打印,结果是ER

注意:此时cpp没有发生移动

cpp[-2]意味着又移动cpp回到初始位置,解引用得到c+3 

前面又加了颗*,则F首元素的地址,+3得到S的地址,最后打印ST

最后cpp[-1][-1] = *(*(cpp - 1)- 1) + 1

*(cpp - 1)得到c + 2,-1得到c+1

然后解引用得到N的地址,+1得到E的地址,最后打印EW

答案:

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值