再探指针(指针类型详解)


前言

指针是C语言中较难的一部分,需要大家反复学习,思考,再学习,再思考。学好指针基本就能掌握C语言数据结构层面上的精髓。

一、字符指针

int main()
{
    
    
	char* p = "abcde";//"abcde"是一个常量字符串
	printf("%c\n", *p);//p存放的是常量字符串的首地址
	printf("%s\n", p);
	return 0;
}

在这里插入图片描述

int main()
{
    
    
	char* p = "abcde";
	*p = 'q';//常量字符串是不可以修改的
	printf("%c\n", *p);
	printf("%s\n", p);
	return 0;
}

在这里插入图片描述
常量字符串是不可以修改的。虽然程序没有报错且正常生成,但是运行时程序崩溃。,我们通常再常量字符串前加 const修饰

int main()
{
    
    
	const char* p = "abcde";
	*p = 'q';//常量字符串是不可以修改的
	printf("%c\n", *p);
	printf("%s\n", p);
	return 0;
}

在这里插入图片描述
const修饰会防止我们对常量字符串进行有误操作,避免程序崩溃。
我们通过一个案例再深刻了解一下字符指针:

int main()
{
    
    
	char ch1[] = "abcde";
	char ch2[] = "abcde";
	const char* p1 = "abcde";
	const char* p2 = "abcde";
	if (ch1 == ch2)
	{
    
    
		printf("比较的是数组的元素!\n");
	}
	else
	{
    
    
		printf("比较的是数组的地址!\n");
	}
	if (p1 == p2)
	{
    
    
		printf("比较的是指针的地址!\n");
	}
	else
	{
    
    
		printf("比较的是指针的元素!\n");
	}
	if (!strcmp(ch1, ch2))
	{
    
    
		printf("比较的是数组的元素!\n");
	}
	else
	{
    
    
		printf("比较的是数组的地址!\n");
	}
	return 0;
}

在这里插入图片描述
对比结果,我们可以看出当直接用数组名来比较时比较的是地址,数组名代表的是数组首元素的地址,所以打印的结果为比较地址,但直接比较指针发现,两个指针地址相同,因为两个指针指向的是一样的常量字符串,所以两个指针指向同一开辟的空间。要比较两个数组的元素,就要用到strcmp()函数,如果两个数组结果相同返回0,结果不同返回非0

二、数组指针

数组指针顾名思义是数组,数组指针是用来存放指针的

int main()
{
    
    
	int arr[5] = {
    
     0 };//整形数组,里面含有5个整形元素
	char ch[5] = {
    
     0 };//字符数组,里面含有5个字符元素
	int* parr[5];//整形数组指针,里面含有5个整形指针
	char* pch[5];//字符数组指针,里面含有5个字符指针
	return 0;
}

一般用法如下:

int main()
{
    
    
	int arr1[] = {
    
     1,2,3 };
	int arr2[] = {
    
     4,5,6 };
	int arr3[] = {
    
     7,8,9 };
	int* parr[] = {
    
     arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
    
    
		for (int j = 0; j < 3; j++)
		{
    
    
			printf("%d ", *(parr[i] + j));//j表示整形数组的元素
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

三、指针数组

指针数组顾名思义是指针,指针数组是指向数组的指针

int main()
{
    
    
	int* pi = NULL;//pi是整形指针,用来存放整形的地址,指向整形元素
	char* pc = NULL;//pc是字符指针,用来存放字符的地址,指向字符元素
	int arr[5] = {
    
     1,2,3,4,5 };
	int(*parr)[5] = &arr;//&arr是数组的地址,[]的优先级高与*,*和parr结合,代表parr是一个指针
	return 0;
}

[ ]的优先级高与*,*和parr结合,代表parr是一个指针,再和[ ]结合,表示指向的是数组。

int main()
{
    
    
	int arr[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//p指向arr数组的首元素
	for (int i = 0; i < 10; i++)
	{
    
    
		printf("%d ", *(p + i));//指针偏移i个位置
		printf("%d ", p[i]);
		printf("%d ", *(arr + i));//arr代表首元素地址,偏移i个位置
		printf("%d ", arr[i]);
		printf("\n");
	}
	return 0;
}

在这里插入图片描述
我们看以看出,数组可以当成指针使用,指针也可以通过下标访问数组元素。
我们通过一个案例再深刻了解一下指针数组:

void print1(int arr[3][3])
{
    
    
	for (int i = 0; i < 3; i++)
	{
    
    
		for (int j = 0; j < 3; j++)
		{
    
    
			printf("%d ", arr[i][j]);//j表示整形数组的元素
			//printf("%d ", *(arr[i] + j));//数组当指针用
			//printf("%d ", *(*(arr + i) + j));//数组当指针用
		}
		printf("\n");
	}
}
void print2(int(*p)[3])
{
    
    
	for (int i = 0; i < 3; i++)
	{
    
    
		for (int j = 0; j < 3; j++)
		{
    
    
			printf("%d ", *(*(p + i) + j));
			//p+i表示第几行元素,通过解引用获得这一行元素下标。
			//+j表示找到这一行的第j个元素,通过解引用获得这一个元素。
			//printf("%d ", *(p[i] + j));//指针当数组用
			//printf("%d ", p[i][j]);//指针当数组用
		}
		printf("\n");
	}
}
int main()
{
    
    
	int arr[3][3] = {
    
     {
    
    1,2,3},{
    
    4,5,6},{
    
    7,8,9} };
	int(*p)[3] = &arr;
	print1(arr);//数组首元素地址,数组形式
	printf("\n");
	print2(arr);//指针数组形式
	printf("\n");
	return 0;
}

在这里插入图片描述
下面的这个代表什么含义呢?

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

首先p先和 [ ] 结合,代表p是数组,把p[ ]拿出,结构变为int (* ) [10]。
上面代码的含义为:该数组有5个元素,每个元素为指针数组,每个指针数组指向含有10个整形元素的数组。

四、函数指针

函数指针是指向函数的指针
了解函数指针之前我们先看一下函数的地址:

int ADD(int x, int y)
{
    
    
	return x + y;
}
int main()
{
    
    
	printf("%p\n", &ADD);//对ADD取地址
	printf("%p\n", ADD);//函数名
	return 0;
}

在这里插入图片描述

我们可以发现取函数地址和函数名都代表函数地址。所以两种形式的函数调用起到相同结果。
下面我们正式的了解一下函数指针:

int ADD(int x, int y)
{
    
    
	return x + y;
}
int main()
{
    
    
	int* p1;//整形指针
	int* p2[5];//数组指针
	int(*p3)[5];//指针数组
	int(*p4)(int ,int);//函数指针
	p4 = &ADD;
	printf("%d\n", (*p4)(2, 5));//通过解引用调用
	printf("%d\n", p4(2, 5));//直接通过函数地址调用,和函数名调用类似
	return 0;
}

在这里插入图片描述

对比我们所了解过的指针,我们可以发现:

int main()
{
    
    
	int* p1;//整形指针
	//p1和*结合,代表是一个指针,类型为int
	int* p2[5];//数组指针
	//[ ]的优先级高于*,所以p2先和[ ]结合,代表p2是一个数组,剩下int *,代表存储的类型,为整形指针。
	int(*p3)[5];//指针数组
	//p3先和*结合,代表一个指针,剩下的为int [5],代表一个含有5个整形元素的数组,p3指向这个数组
	int(*p4)(int, int)=&ADD;//函数指针
	//p4先和*结合,代表一个指针,剩下的为int (int,int),代表两个形参都为int类型,返回值也为int类型
}

单一使用函数指针来进行函数的调用过于麻烦,用法会在回调函数中提到。

五、函数指针数组

函数指针数组是指针数组,数组中用来存放函数的地址。
如何确定一个函数指针数组呢?
根据前面的结合来看我们只需要再指针名后面加[ ]

int(*p5[5])(int, int);//函数指针数组
//p5先和[ ]结合,代表p5是一个数组,剩下的为int(*)(int, int),这个是不是很熟悉,剩下这个就是函数指针

我们通过一个简单的计算器来看函数指针数组的使用:

void test()
{
    
    
	printf("1,相加,2,相减\n");
	printf("3,相乘,4,相除\n");
	printf(" 0,退出程序\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 num = 0;
	int(*p5[5])(int, int) = {
    
    0, ADD,SUB,MUL,DIV };//数组下标从零开始,我们可以把数组下标为0的元素直接初始化为0
	test();
	do
	{
    
    
		int x, y;
		printf("请输入要进行的操作:");
		scanf("%d", &num);
		switch (num)
		{
    
    
		case 0:
			printf("程序即将退出\n");
			break;
		case 1:
			printf("\n请输入要进行的操作的两个数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", p5[num](x, y));
			break;
		case 2:
			printf("\n请输入要进行的操作的两个数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", p5[num](x, y));
			break;
		case 3:
			printf("\n请输入要进行的操作的两个数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", p5[num](x, y));
			break;
		case 4:
			printf("\n请输入要进行的操作的两个数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", p5[num](x, y));
			break;
		default:
			printf("输入错误,请重新输入!\n");
			break;
		}
	} while (num);
	return 0;
}

在这里插入图片描述
我们可以用输入的数直接调用相应的函数,但我们发现这个代码中有部分代码出现冗余现象,我们可以把冗余的代码封装为一个函数。

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

指向函数指针数组的指针是一个指针,指针指向一个数组,数组的元素都是函数指针。
经过上面几个指针的推导,我们可以轻松的写出一个指向函数指针数组的指针:

int(*(*p6)[5])(int, int);//指向函数指针数组的指针
//p6先和*结合,代表p5是一个指针,剩下的为int(* [ ])(int, int)就是函数指针数组
//p6是一个数组指针,指向的的数组有5个元素。
//每个元素的类型是一个函数指针类型int(*)(int,int)

七、回调函数

回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这个是回调函数。回调函数不是由该函数的实现方法直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件的响应。
下面我们用自己的排序来模仿库函数中的qsort函数

void Swap(char* p1, char* p2,int width)//交换两个元素的值
{
    
    
	for (int i = 0; i < width; i++)
	{
    
    
		char ch = *p1;
		*p1 = *p2;
		*p2 = ch;
		p1++;
		p2++;
	}
}
int cmp_int(void* p1,void* p2)//整形比较方法
{
    
    
	return *((int*)p1) - *((int*)p2);
}
int cmp_float(void* p1, void* p2)//浮点型比较方法
{
    
    
	if (*(float*)p1 - *(float*)p2 > 0)
	{
    
    
		return 1;
	}
	else if (*(float*)p1 - *(float*)p2 == 0)
	{
    
    
		return 0;
	}
	else
	{
    
    
		return -1;
	}
}
void Bubble_sort(void *arr,int size,int width,int (*p)(const void *p1, const void* p2))//排序函数
{
    
    
	for (int i = 0; i < size - 1; i++)
	{
    
    
		for (int j = 0; j < size - i - 1; j++)
		{
    
    
			if (p((char*)arr + j * width, (char*)arr + (j + 1) * width) > 0)
			//把void*强制转化为char*,通过width来增加地址。
			{
    
    
				Swap((char*)arr + j * width, (char*)arr + (j + 1) * width, width);
				//对要交换元素一个字节一个字节进行交换
			}
		}
	}
}
void test1()
{
    
    
	int arr[10] = {
    
     1,5,9,7,3,8,4,2,6,0 };
	int size = sizeof(arr) / sizeof(arr[0]);
	Bubble_sort(arr,size,sizeof(int), cmp_int);
	//第一个参数:待排序数组的首元素地址
	//第二个参数:待排序的元素个数
	//第三个参数:待排序数组中每个元素的大小
	//第四个参数:函数指针,用来比较两个元素的所用函数的地址-需要我们自己实现。
	//函数指针的两个参数是待比较的两个元素的地址
	for (int i = 0; i < size; i++)
	{
    
    
		printf("%d\t", arr[i]);
	}
	printf("\n");
}
void test2()
{
    
    
	float farr[10] = {
    
     1.1,5.5,9.9,7.7,3.3,8.8,4.4,2.2,6.6,0.5 };
	int size = sizeof(farr) / sizeof(farr[0]);
	Bubble_sort(farr, size, sizeof(float), cmp_float);
	for (int i = 0; i < size; i++)
	{
    
    
		printf("%.1f\t", farr[i]);
	}
}
int main()
{
    
    
	test1();//整形排序
	test2();//浮点形排序
	return 0;
}

在这里插入图片描述
我们通过一个我们自己的Bubble_sort函数实现了浮点形和整形排序。因为Bubble_sort函数不知道要排序的是何种类型的数组,所以我们排序的函数用void * 来接收(void * 可以接收任何类型地址,但void * 不可以直接解引用。所以在排序时要强制类型转化)。当我们使用我们的排序函数进行排序时,我们知道要比较的方法,所以我们要建立一个自己的比较函数cmp_int和cmp_float,如果我们想要排序字符型数组,我们可以写一个字符比较函数,通过Bubble_sort进行调用调用比较函数时把void * 强制转化为char * ,通过width来增加地址,因为排序函数未知我们要排序的数据类型,我们可以通过char为字节为1,加上我们穿的要比较字符的大小,来确定每个元素的位置。
从上面的例子可以看出我们没有直接调用我们所写的比较函数,而是通过排序函数进行函数指针调用,这就是所谓的回调函数。

总结

下面看一个例子,更加深刻的了解指针的内容吧。

void (*test(int, void (*)(int)))(int);
//test是一个函数声明
//test函数的参数有2个,第一个是int,第二个是函数指针,该函数指针指向的函数的参数是int,返回值为void
//test函数的返回值也是一个函数指针,该函数指针指向的函数的参数为int,返回值为void
typedef void(*fun)(int);//对void(*)(int)重命名,fun要跟着*,不可以写在最后。
fun test(int, fun);//简化版本

日拱一卒终有尽,功不唐捐终入海。指针需要我们反复学习和思考。

猜你喜欢

转载自blog.csdn.net/2301_76986069/article/details/130446613
今日推荐