指针进阶(深入理解:函数指针&回调函数)

写在前面

本篇文章主要讲解函数指针和回调函数的理解和使用,属于较难的指针知识。如果想要学习指针简单和数组的相关知识,可以进入下面的传送门。

指针初阶知识传送门

☀️数组知识传送门☀️

下来就是文章内容了,端上小板凳开始阅读吧

文章目录

    • 字符指针
    • 函数指针
    • 函数指针数组
    • 回调函数
    • 指针笔试题(夯实基础)

字符指针

在指针的类型中有一种指针类型为字符指针char*

一般使用方法:
使用字符指针指向字符变量进行读写操作

int main()
{
	char c = 'w';
	char* pc = &c;
	*pc = 'w';
	return 0;
}

另外一种使用方式:

int main()
{
	char* pstr = "hello world";//这里是把一个字符串的首地址放到pstr指针变量里
	printf("%s\n", pstr);
	return 0;
}

代码char* pstr = "hello world";特别容易理解为是把字符串hello world放到字符指针pstr
了,但是其本质是把字符串hello world的首字符h的地址放到了pstr中。

指针进阶(深入理解:函数指针&回调函数)_第1张图片

一道相应考点的面试题

#include 
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	char* str3 = "hello bit.";
	char* str4 = "hello bit.";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");
	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");
	return 0;
}

输出结果:

指针进阶(深入理解:函数指针&回调函数)_第2张图片

这里str3str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域。当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1str2不同,str3str4不同。

函数指针

首先看一段代码:

#include 
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

输出结果:

指针进阶(深入理解:函数指针&回调函数)_第3张图片

输出的是两个地址,这两个地址是test 函数的地址。 那我们的函数的地址要想保存起来使用的话,怎么保存?这时候就需要函数指针保存函数地址了。

定义函数指针

观察下面哪个有能力保存函数的地址。
void (*pfun1)();
void *pfun2();

这里pfun1先与*结合,所以pfun1是一个指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void
pfun2先与()结合,表明这是一个函数,返回值为void*,又因为它没有函数体,所以它是函数的声明。

使用函数指针

使用函数指针的时候,虽然可以写成(*p)(),但是也同样等价于p(),并且为了方便使用,硬性规定函数指针使用方法为p()

有趣代码加深理解

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

先说明代码1:

  1. 根据优先级,可看出最先执行的是*,说明这是指针,但却没有定义变量,说明是个类型。
  2. 往后执行的()表明这是个函数指针类型。
  3. 然后对0进行强制类型转换,说明0是一个函数指针。
  4. 最后解引用0,表示通过函数指针调用函数,只是这里认为函数的地址为0

代码2:

  1. signal先与()结合,说明signal是一个函数,第一个参数为int,第二个参数是一个函数指针。
  2. 该函数的返回值为参数为int,返回值为void的函数指针。
  3. 但是该函数没有函数体,说明这里是一个函数声明。

代码2太复杂,可以进行简化:

typedef void(*pfun_t)(int);//函数指针类型重命名为pfun_t
pfun_t signal(int, pfun_t);//函数声明

函数指针数组

函数指针数组的定义

例:
int(*parr[10])()

函数指针数组的用途:转移表

例子:(计算器)

#include

int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div1(int a, int b)
{
	return a / b;
}

int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf("    1:add        2:sub   \n");
		printf("    3:mul        4:div   \n");
		printf("*************************\n");
		printf("请选择:");
		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 = div1(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

使用函数指针数组的实现:

int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div1 }; //转移表
	while (input)
	{
		printf("*************************\n");
		printf("    1:add        2:sub   \n");
		printf("    3:mul        4:div   \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = p[input](x, y);
			printf("ret = %d\n", ret);
		}
		else
			printf("输入有误\n");
	}

	system("pause");
	return 0;
}

指向函数指针数组的指针

void test(const char* str)
{
	printf("%s\n", str);
}

int main()
{
	//函数指针ptest
	void(*ptest)(const char*) = test;
	//函数指针的数组ptestarr
	void(*ptestarr[10])(const char*) = { ptest };
	//指向函数指针数组首元素的指针pptest
	void(*(*pptest))(const char*) = ptestarr;
	//指向整个函数指针数组的指针pptest1
	void(*(*pptest1)[10])(const char*) = &ptestarr;
	return 0;
}

回调函数

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

通过qsort讲解回调函数的使用

库函数qsort就是回调函数,看一下它的函数声明:
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));

  • 其中base是无类型的指针,可以接受任何的指针类型,但是void*不能解引用,需要根据传入的内容强转成其他的类型进行操作。
  • num是传入数组的元素个数。其类型是无符号整型。
  • size是传入数组的元素大小(字节个数)。
  • compar就是函数指针,用于调用其他函数。

代码使用qsort

#include
#include
#include

//回调函数比较整型调用的函数
int CompInt(const void* xp, const void* yp)
{
	assert(xp);
	assert(yp);

	return (*(int*)xp - *(int*)yp);
}

int main(void)
{
	int arr1[] = { 123,74,724,654,85,946,24325,4363574,-23,-534,24,235,43 };
	int n1 = sizeof(arr1) / sizeof(arr1[0]);
	qsort(arr1, n1, sizeof(arr1[0]), CompInt);
	for (int i = 0;i < n1;i++)
	{
		printf("%d ", arr1[i]);
	}
	printf("\n");
	return 0;
}

运行结果:

指针进阶(深入理解:函数指针&回调函数)_第4张图片

用冒泡排序实现回调函数qsort

#include
#include
#include
#include

//按字节交换位置
void Swap(char* xp, char* yp, size_t size)
{
	while (size--)
	{
		char temp = 0;
		temp = *xp;
		*xp = *yp;
		*yp = temp;
		++xp;
		++yp;
	}
}

//回调函数比较整型调用的函数
int CompInt(const void* xp, const void* yp)
{
	assert(xp);
	assert(yp);

	return (*(int*)xp - *(int*)yp);
}

//回调函数比较浮点型调用的函数
int CompDouble(const void* xp, const void* yp)
{
	assert(xp);
	assert(yp);

	const double* x = (const double*)xp;
	const double* y = (const double*)yp;
	if (*x - *y > 0)
	{
		return 1;
	}
	else if (*x - *y < 0)
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

//回调函数比较字符串调用的函数
int CompString(const void* xp, const void* yp)
{
	assert(xp);
	assert(yp);

	//用char*保存字符串时
	const char* x = *(const char**)xp;
	const char* y = *(const char**)yp;
	while (*x || *y)
	{
		if (*x > * y)
		{
			return  1;
		}
		else
		{
			return -1;
		}
		x++;
		y++;
	}
	return 0;
	return strcmp(x, y);
}

//模仿qsort的功能实现一个通用的冒泡排序
void MyQsort(void* a, size_t n, size_t size, int(*comp)(const void* xp, const void* yp))
{
	assert(a);
	assert(comp);

	char* p = (char*)a;
	int s = 1;
	for (size_t i = 0;i < n - 1; i++)
	{
		for (size_t j = 0;j < n - i - 1;j++)
		{
			if (comp(p + j * size, p + (j + 1) * size) > 0)//这里是字节操作
			{
				Swap(p + j * size, p + (j + 1) * size, size);
				s = 0;
			}
		}
		if (s)
			break;
	}
}

int main(void)
{
	int arr1[] = { 123,74,724,654,85,946,24325,4363574,-23,-534,24,235,43 };
	int n1 = sizeof(arr1) / sizeof(arr1[0]);
	//qsort(arr1, n1, sizeof(arr1[0]), CompInt);
	MyQsort(arr1, n1, sizeof(arr1[0]), CompInt);
	for (int i = 0;i < n1;i++)
	{
		printf("%d ", arr1[i]);
	}
	printf("\n");

	double arr2[] = { 12.3,7.4,72.4,6.54,0.85,94.6,24.325,4363.574,-2.3,-53.4,24.567,235.555,4.3 };
	int n2 = sizeof(arr2) / sizeof(arr2[0]);
	//qsort(arr2, n2, sizeof(arr2[0]), CompDouble);
	MyQsort(arr2, n2, sizeof(arr2[0]), CompDouble);
	for (int i = 0;i < n2;i++)
	{
		printf("%.3f ", arr2[i]);
	}
	printf("\n");

	char* arr3[] = {
		"fxakssxb!",
		"aadcasvaf",
		"acvsvffd=",
		"dasn2r32",
		"3254376fsda",
		"543654bsggs"
	};
	int n3 = sizeof(arr3) / sizeof(arr3[0]);

	//qsort(arr3, n3, sizeof(arr3[0]), CompString);
	MyQsort(arr3, n3, sizeof(arr3[0]), CompString);

	for (int i = 0;i < n3;i++)
	{
		printf("%s\n", arr3[i]);
	}
	printf("\n");

	system("pause");
	return 0;
}

运行结果:

指针进阶(深入理解:函数指针&回调函数)_第5张图片
字符串的大小比较是从第一个元素开始往后依次比较字符的ascii码值大进行排序,只要其中有一个字符大,那么这个字符串就比另一个大。

指针笔试题(夯实基础)

笔试题1

int main()
{
	int a[5] = { 1, 2, 3, 4, 5 };
	int *ptr = (int *)(&a + 1);
	printf( "%d,%d", *(a + 1), *(ptr - 1));
	return 0;
}
//程序的结果是什么?

结果为:
2 ,5

结果分析:
&a的类型是int[5] *,加一后指向整个数组后跟数组大小相同的空间的首字节地址,并把地址传给了ptr。由于ptrint型,只向前移动四个字节,所以指向数组的最后的元素。

笔试题2

//结构体的大小是20个字节
struct Test
{
	int Num;
	char *pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
int main()
{
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

结果为:
100014
100001
100004

结果分析:

  • p的类型是struct Test*,指针加一加上其所指向类型的大小,故加上结构体的大小20,转换为地址的16进制输出即为结果100014
  • p的类型强转为unsigned long,加一即为数字之间的加法,故直接加上一,结果为100001
  • p的类型强转为unsigned int*,指针加一加上其所指向类型的大小,故加上int的大小4,所以结果为100004

笔试题3

int main()
{
	int a[4] = { 1, 2, 3, 4 };
	int *ptr1 = (int *)(&a + 1);
	int *ptr2 = (int *)((int)a + 1);
	printf( "%x,%x", ptr1[-1], *ptr2);
	return 0;
}

结果为:
4,2000000

结果分析:
内存结构图:

指针进阶(深入理解:函数指针&回调函数)_第6张图片
  • &a的类型是int[4] *,加一后指向整个数组后跟数组大小相同的空间的首字节地址,并把地址传给了ptr。由于ptrint型,只向前移动四个字节,所以指向数组的最后的元素。
  • a强转为int类型加一就是值加一,所以就相当于地址值加了一,再强转为int*类型传给ptr2解引用后,小端取出的数值为02 00 00 00,若为大端,取出的数值为00 00 01 00

笔试题4

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

结果为:
1

结果分析:
a数组的元素是类型为int[2]的一维数组,p保存的是a首元素的地址,p[0]访问的是a[0][0]。通过逗号表达式可知,数组初始化的内容是a[0][0] = 1,a[0][1] = 3,a[1][0] = 5,所以打印出的结果是1。

笔试题5

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;
}

结果为:
ff ff ff fc,-4

结果分析:
a数组的内部元素是int[5],即有5个int型的一维数组,一共有5 * 5 = 25个int型的数据。p指针的类型是int[4] *p指针保存了数组的首字节地址。元素p[4][2]前面有4 * 4 + 2 = 18个int的数据,元素a[4][2]前面有4 * 5 + 2 = 22个int的数据。&p[4][2] - &a[4][2]相减时,是两个相同的指针类型相减,等于跨越的数据个数为18 - 22 = -4。-4%p打印出来就是-4的补码ff ff ff fc,以%d打印出来就是-4

笔试题6

int main()
{
	int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int *ptr1 = (int *)(&aa + 1);
	int *ptr2 = (int *)(*(aa + 1));
	printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}

结果为:
10,5

结果分析:

  • &aa的类型是int[2][5] *,加一加上其指向类型的大小,所以ptr1指向整个数组后跟数组大小相同的空间的首字节地址,由于ptr1的类型是int,所以往前移动了4个字节,解引用后就是10。
  • 数组aa的元素类型是int[5],即具有5个int数据的一维数组。加一指向后一个元素,把该地址传给ptr2,由于ptr2int型,往前移动四个字节解引用就是5。

笔试题7

#include 
int main()
{
	char *a[] = {"work","at","alibaba"};
	char**pa = a;
	pa++;
	printf("%s\n", *pa);
	return 0;
}

结果为:
at

结果分析:

指针进阶(深入理解:函数指针&回调函数)_第7张图片

数组a元素类型是char*,保存的是三个字符串的首地址。pa指向数组首元素,pa++后指向数组的第二个元素,解引用后打印出来就是字符串at

笔试题8

int main()
{
	char *c[] = {"ENTER","NEW","POINT","FIRST"};
	char**cp[] = {c+3,c+2,c+1,c};
	char***cpp = cp;
	printf("%s\n", **++cpp);
	printf("%s\n", *--*++cpp+3);
	printf("%s\n", *cpp[-2]+3);
	printf("%s\n", cpp[-1][-1]+1);
	return 0;
}

结果为:
POINT
ER
ST
EW

结果分析:

指针进阶(深入理解:函数指针&回调函数)_第8张图片
  • 数组c中保存的是三个字符串的首元素地址,数组中cp保存的是数组c元素的地址,注意初始化的时候重新赋了值,cpp中保存了cp的首元素地址。
  • ++cpp,改变了cpp里保存的地址,指向了cp[1]cp[1]指向c[2],解引用的时候就打印了"POINT"字符串。
  • ++cpp,又改变了cpp里保存的地址,指向了cp[2]*++cpp访问cp[2]--*++cpp使cp[2]指向c[0],*--*++cpp访问c[0]空间,c[0]空间里保存的"ENTER"字符串首元素地址+3后打印就是"ER"。
  • cpp[-2]访问了cp[0]*cpp[-2]通过cp[0]里保存的地址访问c[3]c[3]空间里保存的"FIRST"字符串首元素地址+3后打印就是"ST"。
  • cpp[-1]访问cp[1]cpp[-1][-1]通过cp[1]保存的地址访问c[1]c[1]空间里保存的"NEW"字符串首元素地址+1后打印就是"NW"。

你可能感兴趣的:(c语言,c语言,指针,c++)