C语言指针-----回调函数、8 道大厂指针笔试题

目录

1、回调函数 

1.1 qsort 函数的使用

1.2 使用回调函数,模拟实现 qsort (采用冒泡排序)

 2、指针笔试题


1、回调函数 

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

 例.看一个简单的回调函数例子。

#include<stdio.h>

/* 回调函数简单例子 */

void print(char* str)//一级字符指针 str 接收一级字符指针 pstr 中存储的地址
{
	printf("%s\n", str);//从 str 地址开始打印这个字符串,直到遇到 \0 才停止打印。
}

void test(void (*pprint)(char*))//test 函数拿一个函数指针来接收这个 print 函数的地址
{
	char* pstr = "fine,and you?";//使用指针创建字符串,并不是把这个字符串放到指针变量 pstr 中,而是把这个字符串的首个字符的地址存放到 pstr 中
	printf("How are you?\n");
	(*pprint)(pstr);//(*pprint)解引用拿到这个函数,去调用它
}
int main()
{
	test(print);//把函数 print 的地址传递给 test 函数
	return 0;
}

1.1 qsort 函数的使用

qsort() 函数包含在<stdlib.h>头文件中,此函数根据给出的比较条件进行快速排序,通过指针移动实现排序。排序之后的结果仍然放在原数组中。使用 qsort 函数排序必须给自己写一个比较函数。

qsort() 函数原型为: 

void qsort (void* base, size_t num, size_t size,int (*compar)(const void* e1,const void* e2));

size_t 是什么呢?size_t是标准C库中定义的,在64位系统中为long long unsigned int,非64位系统中为long unsigned int。int (*compar)(const void* e1,const void* e2) 是一个函数指针,用来存储函数的地址,也就是存储 比较函数 的地址。函数分析图如下:

 例1.qsort() 函数排序整型数组的数据

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

int cmp_int(const void* e1, const void* e2)// qsort 比较函数
{
	//比较两个整型
	return *(int*)e1 - *(int*)e2;//将 void* 强制转换成 int*
}

void test()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
	int i = 0;
	//arr --> 目标数组的起始地址; sz --> 目标数组中的元素个数; 
	//sizeof(arr[0]) --> 目标数组的每个元素的大小(单位字节); cmp_int --> 比较函数的地址
	qsort(arr, sz, sizeof(arr[0]), cmp_int);//qsort 函数对数组元素进行排序
	for (i = 0; i < sz; i++)//打印排序后的数组元素
	{
		printf("%d ", arr[i]);
	}
}

int main()
{
	test();
	return 0;
}

例2.qsort() 函数排序浮点型数组的数据

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

int cmp_float(const void* e1, const void* e2)//比较函数
{
	//比较两个浮点型的数
	if ((*(float*)e1) > (*(float*)e2)) return 1;
	else if ((*(float*)e1) == (*(float*)e2)) return 0;
	else return -1;
    //强制类型转换的话,如果两个浮点数的差值为 0.5 的话,就会被强制转换成 0,比较函数的返值就为 0;
	//return (int)(*(float*)e1 - *(float*)e2);//建议不要强制类型转换
}

void test()
{
	float f[] = { 9.0,8.0,7.0,6.0,5.0,4.0,3.0,2.0,1.0 };//浮点型数组
	int sz = sizeof(f) / sizeof(f[0]);
	int i = 0;
	qsort(f, sz, sizeof(f[0]), cmp_float);
	for (i = 0; i < sz; i++)
	{
		printf("%f ", f[i]);
	}
}

int main()
{
	test();
	return 0;
}

例3.qsort() 函数排序结构体数组的数据

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

struct stu    //声明一个结构体
{
	char name[30];
	int age;
};

int cmp_stu_by_age(const void* e1, const void* e2)
{
	//比较两个人的年龄
	return (((struct stu*)e1)->age - ((struct stu*)e2)->age);//将 void* 强制转换成结构体指针
}

int cmp_stu_by_name(const void* e1, const void* e2)
{
	//比较名字就是比较字符串
	//字符串比较不能直接用 < > = 号来比较,应该用srtcmp函数
	return strcmp(((struct stu*)e1)->name ,((struct stu*)e2)->name);
}

void test1()//比较年龄
{
	struct stu s[] = { {"张三",20},{"李四",30},{"王五",10},{"曹操",50} };//定义一个结构体数组
	int sz = sizeof(s) / sizeof(s[0]);//计算结构体数组的元素个数
	int i = 0;
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", s[i].age);
	}
	printf("\n");
}

void test2()//比较名字
{
	struct stu s[] = { {"张三",20},{"李四",30},{"王五",10},{"曹操",50}};//定义一个结构体数组
	int sz = sizeof(s) / sizeof(s[0]);//计算结构体数组的元素个数
	int i = 0;
	qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);//曹操( c )、李四( l )、王五( w )、张三( z ) --> 排序名字是按字母的顺序进行排序
	for (i = 0; i < sz; i++)
	{
		printf("%s ", s[i].name);
	}
}

int main()
{
	test1();
	test2();
	return 0;
}

1.2 使用回调函数,模拟实现 qsort (采用冒泡排序)

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

/********************  修改冒泡排序,使它可以排序任何类型的数据  ********************/

struct stu	//声明结构体
{
	char name[30];
	int age;
};

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

int cmp_stu_by_age(const void* e1, const void* e2)
{
	return (((struct stu*)e1)->age - ((struct stu*)e2)->age);
}

int cmp_stu_by_name(const void* e1, const void* e2)
{
	//比较名字就是比较字符串
	//字符串比较不能直接用 < > = 号来比较,应该用srtcmp函数
	return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}

//交换函数,对两个元素交换,交换方式是: 一个字节一个字节的交换
//假设元素是一个整型数据,整型数据字节大小为: width = 4; 数据交换就要交换 width 次。
void sawp(char* e1, char* e2,int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char temp = *e1;
		*e1 = *e2;
		*e2 = temp;
		e1++;
		e2++;
	}
}

void bubble_sort(void *base,int num,int width,int (*cmp)(const void *e1,const void *e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < num - 1; i++)//趟数
	{
		for (j = 0; j < num - i - 1; j++)//一趟比较的次数
		{
			//两个元素的比较
			if ((*cmp)((char*)base + j * width, (char*)base + (j + 1) * width)>0)
			{
				//交换函数,把两个元素的地址传过去,和元素字节大小
				sawp((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}

void test()//排序整型数组
{
	int i = 0;
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void test1()//排序结构体数组
{
	struct stu s[] = { {"张三",20},{"李四",30},{"王五",10},{"曹操",50}};
	int sz = sizeof(s) / sizeof(s[0]);
	int i = 0;
	bubble_sort(s, sz, sizeof(s[0]), cmp_stu_by_name);
	for (i = 0; i < sz; i++)
	{
		printf("%s ", s[i].name);
	}
}

int main()
{
	test();
	test1();
	return 0;
}

分析:

(1)void bubble_sort(void *base,int num,int width,int (*cmp)(const void *e1,const void *e2)) 该函数的参数是 void *base、const void *e1、const void *e2 ,为什么都是 void* 类型的指针呢?原因:因为我们不知道要排序的数组是什么类型的,数组的元素是什么类型的。void* 类型的指针,可以接收任意类型的地址。

(2)(*cmp)((char*)base + j * width, (char*)base + (j + 1) * width) 是什么意思呢?解释:(*cmp)(解引用)函数地址拿到比较函数,把 (char*)base + j * width 和 (char*)base + (j + 1) * width 作为参数传递给该函数。由于 void* 不能进行指针运算,所以将 void* base 强制类型转换 char* ;现在假设我们要排序一个整型数组,整型数组的每个元素的字节大小为:width = 4 个字节,当 j = 0 时,(char*)base 是整型数组第一个元素的地址,(char*)base + 4 是整型数组第二个元素的地址。从 (char*)base(char*)base + 4,char* 指针跳过四个字节的地址,也就是跳过整型数组的一个元素。当 j = 1 时,(char*)base + 4 是整型数组第二个元素的地址,(char*)base + 8 是整型数组第三个元素的地址,从 (char*)base + 4(char*)base + 8,char* 指针跳过四个字节的地址,也是跳过整型数组的一个元素,都是两两相邻的地址。依次类推,比较函数就能拿到两个相邻元素的地址进行解引用操作,就能比较两个元素的大小,返回相应的数值。

 2、指针笔试题

注意:以下所有题目都是在 VS2022 X86 环境(32 位平台)下运行的。

 第一题:

#include<stdio.h>

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* parr = (int*)(&arr + 1);
	printf("%d %d", *(arr + 1), *(parr - 1));
	return 0;
}

分析:int* parr = (int*)(&arr + 1):(&arr)取数组名,取出数组的地址,(&arr + 1) 跳过一个数组,如下图。

(&arr + 1) 还是数组的地址,该地址的类型是一个数组指针类型,想要放到 int* 类型的指针,就要强制类型转换成 int* 类型,才能存放到 int* 的指针里。

*(arr + 1):数组名就是首元素地址,首元素地址 + 1,跳过一个元素,指向第二个元素的地址, 解引用,取出该地址所对应空间的内容,也就是 2 。

*(parr - 1):parr是一个整型指针,parr - 1向后走四个字节,指向的是数组 arr[4] 的地址,解引用,取出该地址所对应空间的内容,也就是 5 。如下图。

 运行结果:*(arr + 1) --> 2、*(parr - 1) --> 5

 第二题:

#include<stdio.h>

//已知该结构体的大小是 20 个字节
struct Test
{
	int Num;
	char* pcName;
	short sDate;
	char cha[2];
	short sBa[4];
}*p; //结构体指针

int main()
{
	p = (struct Test*)0x100000;//初始化p
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int*)p + 0x1);
	return 0;
}

分析:指针运算,指针的类型决定了指针向前或者向后走一步有多大(距离)。指针 +1,能跳过多少个字节取决于指针类型,类型是多少个字节,就跳过多少个字节。

 p = (struct Test*)0x100000:初始化 p,0x100000 虽然是十六进制的形式,不过它仍然是一个整数,整数赋值给 struct Test* 是有问题的,所以需要强制类型转换成 struct Test* 。

p + 0x1: 十六进制 0x1 等于 十进制 1,结构体指针的类型是结构体,结构体的字节大小是 20 ,+1 就跳过 20 个字节,以十六进制的格式打印,结果是:00100014,14 的十进制是 20。

(unsigned long) p + 0x1: 将结构体指针 p 强制类型转换成一个长整型 unsigned long ,整数 + 1 ,结果是:00100001。

(unsigned int*)p + 0x1:将结构体指针 p 强制类型转换成 int*,int 的字节大小是 4,+1跳过四个字节,结果为:00100004。

运行结果: p + 0x1 --> 00100014、(unsigned long)p + 0x1 --> 00100001、(unsigned int*)p + 0x1 --> 00100004

 第三题:

#include<stdio.h>

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

分析:int* ptr1 = (int*)(&arr + 1):和第一题一模一样,这里就不在重复了。ptr1[-1]:ptr1[-1] 等效于 *(ptr1+(-1)) 等效于 *(ptr1-1),整型指针 ptr1 - 1 指向的是数组 arr[3] 的地址,解引用,取出该地址所对应空间的内容。

int* ptr2 = (int*)((int)arr + 1): 数组名首元素地址,(int)arr +1 将首元素的地址强制类型转换成 int,再 + 1。arr 里面存放的地址就是单纯意义上的数字, (int)arr 把这个数字强制类型转换成 int,得到一个数字 +1 ,然后再强制类型转换成整形指针并存放在 ptr2 中。假设首元素的地址为:0x00 00 00 05,强制类型转换成 int 之后为:5 ,5 + 1 = 6 ;再强制类型转换成 int* 类型存放到 ptr2 中,也就是:0x00 00 00 06,那么此时 ptr2 指向的是数组第一个元素中第二个字节的地址(一个 int 类型数组,元素类型都是 int ,都是四个字节,每个字节都有自己的地址)。*ptr2 解引用访问四个字节,也就是下图红色圆圈那块空间的内容。如下图。

运行结果:ptr1[-1] --> 4、因为是小端存储,输出的结果是:*ptr2 --> 2000000 

 第四题:

#include<stdio.h>

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

分析:二维数组 a 里面存放的是三个逗号表达式, 去掉逗号表达式后,那么二维数组的内容应该是:int a[3][2] = { 1,3,5,0,0,0};

 p = a[0];a[0] 等价  *(a+0) ,拿到的是二维数组第一行(第一个元素)的数组名,数组名,首元素地址,也就是第一行第一个元素的地址,指的是 a[0][0] 元素的地址。p[0] 等效于 *(p+0), p 存储的是第一行首元素地址,*(P+0),也就是第一行第一个元素地址 + 0,跳过 0 元素的地址,解引用,取出该地址所对应空间的内容。如下图。

 运行结果:p[0] --> 1

第五题:

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

分析:int a[5][5]:二维数组,该数组的数组指针类型为:int (*)[5]。int(*p)[4]:数组指针,该指针的类型是:int (*)[4]。把一个数组指针类型 int (*)[5] 赋值给一个指针类型为 int (*)[4]。这里有一个误区,很多人以为把 [5] 赋值给 [4] 是不是会导致数组越界呢?其实这样理解是不对的,int (*)[5]、 int (*)[4] 只是指针的类型而已,不代表什么元素个数,它们代表的是指向的数组的元素个数。如:数组指针 int(*p)[4] 存储的是一个数组的地址,该数组有四个元素。就像我们把 int* 类型的指针赋值给 char* 类型的指针一样,所以是没有问题的。

&a[4][2] 这个很好理解,就是取出 a[4][2] 元素的地址,如下图。

 &p[4][2]:  p[4][2] 等效于  *(*(p+4)+2) 。p 指向的是一个整型数组的指针,该数组有四个元素,那么 p + 1 跳过四个元素,也就是下图的 p[1][0] 的位置。依此类推 :p + 2 --> p[2][0]、p + 3 --> p[3][0]、p + 4 --> p[4][0]。*(*(p+4)+2) 拿到的是一个元素,再取地址(相当于:&p[4][2]),就是取得该元素的地址,&p[4][2]的指向如下图。

取地址减取地址,地址的运算,就是指针的运算,指针减指针,得到的是指针与指针之间的元素个数。&p[4][2] - &a[4][2] ,两个指针之间相差了 4 个元素,但因为 &p[4][2] 的地址位置比较低,所以两个相减是一个负数,结果是 -4。要以 %p 的形式打印,%p 是一个无符号的数,就是把 -4 放在内存中的补码的值以十六进制打印出来,所以我们便得知结果应该是一个非常大的十六进制数。- 4 的原反补如下图。

 所以第一个打印的结果是:FFFFFFFC。%d 是一个有符号的打印格式,所以输出的结果就是 -4。

运行结果:以%p的格式打印为:&p[4][2] - &a[4][2] --> FFFFFFFC。以%d的格式打印为:&p[4][2] - &a[4][2] --> -4

 第六题:

#include<stdio.h>

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

 分析:int* ptr1 = (int*)(&aa + 1):取地址数组名+1,跳过一个二维数组,取出的是一个二维数组的地址,该地址的类型是一个数组指针,想要存放到一个整型指针里面去,就要强制类型转换成 int* ,强制类型转换之后存放到 ptr1 中。如下图。

整型指针 ptr1 指向如下图。

*(ptr1 - 1):整型指针 ptr1 - 1 解引用,整型指针 - 1 向后跳过四个字节,此时 ptr1 - 1 的指向如下图。

也就是二维数组 aa[1][4] 的地址,解引用,结果为 10 。

 int* ptr2 = (int*)(*(aa + 1)):aa+1 指向的是二维数组下标为 1 那一行的首地址(数组名),解引用( *(aa + 1) ),拿到数组名,也就是 aa[1][0] 的地址,存放到 ptr2 中。其实,这里的强制类型转换没什么作用,因为 aa[1][0] 本身的地址就是 int* 类型的。ptr2 的指向如下图。

*(ptr2 - 1):ptr2 本身就是一个整型指针类型的,ptr2 - 1向后走四个字节,指向的是 aa[0][4] 的地址,如下图。

解引用,结果为 5 。

运行结果: *(ptr1 - 1) --> 10、*(ptr2 - 1) --> 5

 第七题:

#include<stdio.h>

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

分析:指针数组 a 里存储的是常量字符串首个字符的地址,也就是 "work" w 的地址、"at" a 的地址、"alibaba" a 的地址。需要注意的是,这里并不是把着三个常量字符串存储到该数组中。

 char** pa = a:a 是数组名,首元素地址, 所以 pa 储存的是常量字符串 "work" w 的地址。如图。

pa++:pa++ 指向第二个元素的地址,如下图。

*pa:解引用,拿到常量字符串 "at" a 的地址,以 a 的地址开始打印该常量字符串,直到遇到 \0 方停止打印。打印结果为:at

运行结果: *pa --> at

 第八题:

#include<stdio.h>

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

分析:指针数组 c 里存储的是常量字符串首个字符的地址,也就是 "ENTER" E 的地址、"NEW" E 的地址、"POINT" P 的地址、"FIRST" F 的地址。需要注意的是,这里并不是把着四个常量字符串存储到该数组中,和上题一样。

看到这样一个题时,首先,第一件事是,画出程序的内存布局,可以清楚的观察到它们之间的关系,如下图。 

**++cpp:++cpp 后,cpp 指向的是 c + 2,如下图。

*++cpp 拿到 c + 2 的地址,也就是 c[2] 的地址,再解引用(**++cpp)拿到 c[2] 存储的地址,c[2] 存储着常量字符串 "POINT" 首个字符的地址,以该地址开始打印该常量字符串。 结果是:POINT

 *--*++cpp + 3:第一个 printf 的 ++cpp 使指针指向了 c + 2 ( c[2] )的地址,这里再 ++cpp 使指针指向了 c + 1 (c[1])的地址,如下图。 

解引用(*++cpp)拿到的是 c[1] 的地址。--*++cpp,也就是 c[1]--(减减) 得到的是 c[0] 的地址,再解引用(*--*++cpp)拿到常量字符串 "ENTER" 首个字符的地址( E 的地址 ),首个字符的地址 + 3,指向该常量字符串倒数第二个字符的地址,以该地址开始打印常量字符串。结果为:ER。

*cpp[-2] + 3:*cpp[-2] 等效于 *(*(cpp-2))。cpp[-2] 等效于 *(cpp-2),cpp - 2 的指向如下图。

*(cpp-2)拿到的是 c+3(c[3])的地址,再解引用(*cpp[-2]),拿到 c[3] 存储的地址,也就是常量字符串 " FIRST " 首个字符的地址,再 + 3,也就是常量字符串" FIRST "中 S 的地址,以 S 的地址开始打印字符串,结果是:ST。需要注意的是,虽然 cpp - 2,不过对于 cpp 的本身值没有发生改变,因为 cpp 本身没有进行运算。如:a = 2,a + 1 =3,此时 a 本身的值还是 2,而不是变成 3 了。

cpp[-1][-1] + 1: cpp[-1][-1] 等效于 *(*(cpp-1)-1)。cpp - 1 的指向如下图。

 *(cpp-1) 拿到的是 c+2(c[2])的地址,(*(cpp-1)-1 等效于 c[2]-1 ,也就是 c[1] 的地址,再解引用( *(*(cpp-1)-1 ) 等效于 *c[1]),拿到 c[1] 存储的地址,也就是常量字符串 " NEW " 首个字符的地址,再 + 1,也就是字符 " E " 的地址,以该地址开始打印字符串,结果是:EW。

运行结果是:**++cpp -->POINT、*--*++cpp + 3 -->ER、*cpp[-2] + 3 --> ST、cpp[-1][-1] + 1 -->EW

 最后看一个简单的例子,加深对 p[1] 等效于 *(p+1) 的理解。

#include<stdio.h> 
//p[1] 为什么等效于 *(p+1),例
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* p = arr;
	printf("%d\n", *(p + 1));// *(p+1) == *(arr+1) == arr[1] == p[1]
	//其实我们写的 arr[1] 编译器也会转换成 *(arr+1)
	return 0;
}
  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值