浅学【C语言】之回调函数qsort的模拟实现及指针数组相关习题讲解

 

目录

回调函数

用冒泡排序模拟实现qsort函数

qsort函数

回忆冒泡排序

给冒泡排序函数增加新成员

比较函数传参的注意事项

 比较函数

完整代码

指针和数组相关习题讲解

指针笔试题


回调函数

官方定义:

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

代码解释:

#include<stdio.h>
void test()
{
	printf("HaHaHa");
}

void print(void (*pr)())//创建一个函数指针接受test函数地址
{
	pr();//间接调用test函数打印 因此 test函数便被称为回调函数
}
int main()
{
	print(test);//不直接调用test函数打印 而是将test函数的地址作为参数传给print函数
	return 0;
}

用冒泡排序模拟实现qsort函数

qsort函数

在模拟实现之前 ,我们要先了解一下qsort函数可以实现什么功能,以及相关参数是什么?

qsort函数实现了一个快速排序算法,其可以对任何数据进行升序排序,但需要传入相关参数。

void qsort( 
void *base,   //待排序数据的起始位置
size_t num,   //数组的元素个数
size_t width, //一个元素的字节大小
int (__cdecl *compare )(const void *elem1, const void *elem2 ) 
//比较函数的函数指针 用来调用比较函数
);//elem1和elem2代表待比较的两个数据的地址  

其中比较函数需要自行创建,返回值为:

< 0elem1 less than elem2
0elem1 equivalent to elem2
> 0elem1 greater than elem2

数组按照比较函数定义的递增顺序排序。要按降序对数组排序,请颠倒比较函数中“大于”和“小于”的含义。

了解完qsort函数的相关参数,那么让我们着手用冒泡排序来实现一样的功能吧!

回忆冒泡排序

#include<stdio.h>
void bubble_sort(int* arr, int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)//sz-1趟 一趟排一个数字 最后一个数字不用排 因此为sz-1
	{
		for (j = 0; j < sz - 1 - i; j++) //比较的对数
		{
			if (arr[j] > arr[j + 1]) //若大于就交换 排序完为升序
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}
int main()
{
	int arr[] = { 1,3,5,2,4,7,11,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

给冒泡排序函数增加新成员

我们可以看到,qsort函数定义时,是用 void*类型的指针来接收地址的,其意义是什么?

因为qsort函数的作者不知道我们即将要排序的数据是什么类型的,int?字符串?又或者是结构体?,他无从而知,所以他将用void*这种具有无限包容性的指针类型来接收,也因此,qsort函数可以对任何数据进行排序。所以,我们在更改冒泡函数时,接收地址的指针类型也要为void*类型.

void bubble_sort(void* base, int num, int width, int (*comp)(void* e1, void* e2))

比较函数传参的注意事项

 我们比较函数需要传入待比较数据的地址,但是我们只知道数据的起始地址base,且他还是void*类型的,那么我们如何用这个参数得到待比较数据的地址呢???又如何对其进行交换呢??

我们都知道内存中是以字节为单位存放数据的,而char*类型的指针每+1,可前进一个字节,正好访问到下一个内存单元,这样,不管任何类型的数据,都可精准访问到,而我们接下来要知道的,就是控制访问的宽度,即 我们拿到的数据存放在几个单元格内,比如int类型的是四个字节,即四个单元格,那么我们如何知道呢?

对咯,我们传入的参数中有个width,即数据的字节大小,在交换的时候,我们一个单元格一个单元格的交换

交换的问题我们解决了,那么如何传参呢?

其实和交换的道理类似,将首地址转换为char*类型后,再加上width就可以得到第二个数据的地址了。

void bubble_sort(void* base, int num, int width, int (*comp)(void* e1, void* e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - 1 - i; j++)
		{
			if (comp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
				
		}
	}
}
//swap为交换函数,width是告诉其交换几次,有几个字节(我们所说的单元格)交换几次

 比较函数

我们在写比较函数的代码时,注意形参类型也要为void*,因为我们的函数指针中形参的类型为void*要一一对应,在使用时,将其强制类型转为换对应的类型便可,比如比较的是整型,将其转换为int*型,再解引用进行比较。

完整代码

//test1为整型数组的比较
//test2为结构体数据的比较

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

int compare(void* e1, void* e2)
{
	return (*((int*)e1) - *((int*)e2));
}
struct stu
{
	char name[10];
	int age;
};

int cmp1(void* e1, void* e2)
{
	strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}

void swap(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 (*comp)(void* e1, void* e2))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - 1 - i; j++)
		{
			if (comp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
				
		}
	}
}


void test2()
{
	struct stu arr[3] = { {"ziu",20},{"wang",23},{"ihang",25} };
	bubble_sort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr[0]), cmp1);
}

void test1()
{
	int arr[] = { 10,1,6,7,3,4,5,2,9,8 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), compare);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	test2();
	return 0;
}

指针和数组相关习题讲解

好,那么先让我们复习一下知识点

数组名一般情况下代表首元素的地址,但是有两个例外

1.sizeof(数组名) - 这里的数组名代表整个数组

2.&数组名  这里取出的是整个数组的地址 要用数组指针来接收

(编译器平台为32位)

//一维数组
1.int a[] = {1,2,3,4};
2.printf("%d\n",sizeof(a));
3.printf("%d\n",sizeof(a+0));
4.printf("%d\n",sizeof(*a));
5.printf("%d\n",sizeof(a+1));
6.printf("%d\n",sizeof(a[1]));
7.printf("%d\n",sizeof(&a));
8.printf("%d\n",sizeof(*&a));
9.printf("%d\n",sizeof(&a+1));
10.printf("%d\n",sizeof(&a[0]));
11.printf("%d\n",sizeof(&a[0]+1));

1.数组名单独放在了sizeof内部,这里代表整个数组,即求整个数组的字节大小,又因为元素为int类型,有四个元素,即 16

2.

2、3、5、10、11为一个类型,以第二题为例讲解

这里数组名没有单独放在sizeof内部,所以代表首元素地址,+0后依然是首元素地址,即&a[0]

,因为是地址,地址的大小为 4/8

4.这里的数组名依然表示数组首元素地址,解引用后得到第一个元素,类型为int,即 4

6. *(a+i)相当于a[ i ],即拿到的是元素本身   元素类型为int,所以这里为4

7.&a拿到的是整个数组的地址,但是它依然是地址,是地址大小就为4/8

8.我们从后往前看,首先&a拿到整个数组的地址,再解引用得到整个数组,所以结果是16

   还可以理解为 *与&抵消了,剩下了数组名,而数组名单独放在sizeof内部表示整个数组

9.&a表示拿到整个数组的地址,+1表示跳过了整个数组,但还是地址   4/8

运行结果: 

 接下来我将对必要的进行讲解

//字符数组
char arr[] = {'a','b','c','d','e','f'};
1.printf("%d\n", sizeof(arr));
2.printf("%d\n", sizeof(arr+0));
3.printf("%d\n", sizeof(*arr));
4.printf("%d\n", sizeof(arr[1]));
5.printf("%d\n", sizeof(&arr));
6.printf("%d\n", sizeof(&arr+1));
7.printf("%d\n", sizeof(&arr[0]+1));

1.arr单独放在sizeof内部表示整个数组,此数组用字符abcdef来初始化,大小为6

2.数组名没有单独放在sizeof内部,表示地址   4/8

3.*arr -> 字符a  char型  大小为1

4. 1  // 其表示字符b 

5.4/8

6.4/8

7.4/8

运行结果:

strlen是传入一个地址,从该地址向后计算\0之前的字符数目

//字符数组
	char arr[] = { 'a','b','c','d','e','f' };
1.	printf("%d\n", strlen(arr));
2.	printf("%d\n", strlen(arr + 0));
3.	printf("%d\n", strlen(*arr));
4.	printf("%d\n", strlen(arr[1]));
5.	printf("%d\n", strlen(&arr));
6.	printf("%d\n", strlen(&arr + 1));
7.	printf("%d\n", strlen(&arr[0] + 1));

1.因arr数组初始化时没有放入\0,所以在 ' f ' 后,我们不知道什么时候遇到\0,所以是随机值

2. 与1同理 随机值

3.*arr表示字符a,字符a为97,只要传入strlen,他就会认为这是个地址,但是这里的地址是非法的,可以理解为我们无法访问,因此这是个错误代码(运行时将此代码屏蔽掉)

4.与3同理

5.随机值   

&arr得到的是整个数组的地址,从数值方面来说,整个数组地址与首元素地址一样,所以在strlen这里,它就是从首元素的地址向后计算字符个数

6.此时&arr+1跳到了字符f后面的地址,但是我们依然不知道什么时候遇到\0    所以为随机值

7.从第二个元素后开始计算 结果依然为随机值,但是与1相比结果差1,因为跳过了一个元素

运行结果:

char arr[] = "abcdef"; //这种初始化方式 f后隐藏了 \0

1	printf("%d\n", sizeof(arr));
2	printf("%d\n", sizeof(arr + 0));
3	printf("%d\n", sizeof(*arr));
4	printf("%d\n", sizeof(arr[1]));
5	printf("%d\n", sizeof(&arr));
6	printf("%d\n", sizeof(&arr + 1));
7	printf("%d\n", sizeof(&arr[0] + 1));

1.7   &arr为整个数组,因为隐藏了\0,所以为7

2.4/8

3.1   *arr得到字符a 

4.1    arr[1] -> 字符b

5.4/8 

6.4/8  地址的大小

7.4/8

运行结果:

	char arr[] = "abcdef";
1	printf("%d\n", strlen(arr));
2	printf("%d\n", strlen(arr + 0));
3	printf("%d\n", strlen(*arr));
4	printf("%d\n", strlen(arr[1]));
5	printf("%d\n", strlen(&arr));
6	printf("%d\n", strlen(&arr + 1));
7	printf("%d\n", strlen(&arr[0] + 1));

1.6          计算的是 \0 之前的字符个数

2.7

3.错误代码   *arr -> 'a' -> 97

4.错误代码

5.6         

&arr是整个数组的地址 但依然从首元素地址处开始计算,从数值方面来说,整个数组地址与首元素地址一样

6.随机值          f后的空间中存的是什么我们无从得知,不知道什么时候遇到\0,所以为随机值

        

7.5          从b处开始计算

运行结果:

	char* p = "abcdef";
1	printf("%d\n", sizeof(p));
2	printf("%d\n", sizeof(p + 1));
3	printf("%d\n", sizeof(*p));
4	printf("%d\n", sizeof(p[0]));
5	printf("%d\n", sizeof(&p));
6	printf("%d\n", sizeof(&p + 1));
7	printf("%d\n", sizeof(&p[0] + 1));

1.4/8          p为指针,存放首元素地址

2.4/8          p+1为第二个元素的地址

3. 1            *p->'a'->char型->大小为1

4. 1             p[0] 等价于 *(p+0) 为'a'

5.4/8           &p表示地址

6.4/8

7.4/8             &p[0]+1 -> &(*(p+0) ) +1  ->表示字符b的地址

运行结果:

	char* p = "abcdef";
1	printf("%d\n", strlen(p));
2	printf("%d\n", strlen(p + 1));
3	printf("%d\n", strlen(*p));
4	printf("%d\n", strlen(p[0]));
5	printf("%d\n", strlen(&p));
6	printf("%d\n", strlen(&p + 1));
7	printf("%d\n", strlen(&p[0] + 1));

 1.6

2.5     从字符b开始计算

3.错误代码    *p为字符a   (运行时记得屏蔽)

4.错误代码

5.随机值

6.随机值

7.5     从b处开始计算

运行结果: 

二维数组薄弱的同学这道题建议大家反复食用!!!!(当然也包括作者本人!!!)

	//二维数组
	int a[3][4] = { 0 };
1	printf("%d\n", sizeof(a));
2	printf("%d\n", sizeof(a[0][0]));
3	printf("%d\n", sizeof(a[0]));
4	printf("%d\n", sizeof(a[0] + 1));
5	printf("%d\n", sizeof(*(a[0] + 1)));
6	printf("%d\n", sizeof(a + 1));
7	printf("%d\n", sizeof(*(a + 1)));
8	printf("%d\n", sizeof(&a[0] + 1));
9	printf("%d\n", sizeof(*(&a[0] + 1)));
10	printf("%d\n", sizeof(*a));
11	printf("%d\n", sizeof(a[3]));

注:二维数组数组名表示首元素地址,为第一行的地址。

1.48   数组名单独放在sizeof内部,表示整个数组

2.4     a[0][0]表示第一个元素,又因为类型为int

3.16   a[0]表示第一行的数组名,数组名单独放在sizeof内部,代表这一整行

4.4/8  数组名没有单独放在sizeof内部,所以这里代表数组首元素地址,

5.4     a[0]为第一行的数组名没有单独放在sizeof内部,所以这里就代表首元素地址,+1后为第二个元素的地址,解引用访问,拿到第一行第二个元素,类型为int,因而为4

6.4/8  a为二维数组数组名,没有单独放在sizeof内部,所以表示数组首元素地址,因为是二维数组,首元素的地址为第一行的地址,+1后为第二行的地址,是地址就是4/8

7.16 (a+1)为第二行的地址,解引用拿到第二行;或者可以这么理解:*(a+1) -> a[1] -> 第二行的数组名,单独放在sizeof内部,求的是第二行的大小

8.4/8   a[0]是第一行的数组名,&数组名就是求整个第一行的地址,+1跳过一整行,表示第二行的地址

9.16    对第二行的地址解引用拿到第二行

10.16  a没有单独存在,所以表示数组首元素地址,对其解引用拿到第一行

           还可理解为:*a -> *(a+0) -> a[0]

11.16  sizeof不会访问数组内部,只看类型,a[3]我们知道是不存在的,但是a[2],a[1],a[0]是存在的,其类型一样为int [4] ,所以大小为16

运行结果:

我们总结一下:我们还是要多多注意数组名是否&和单独放在sizeof内部。

指针笔试题

1. 2,5

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

 解析:

&a+1指向5后面的地址,此时指针类型为int (*) [5],所以需要强制类型转换成int*。

ptr-1,就指向了5的地址,解引用拿到数字5

*(a+1) -> a[1] -> 2

2.

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

我们注意:p为结构体指针,0x表示十六进制数字

p + 0x1加的是一个结构体,而结构体大小为20个字节,所以为:100014

将p转换为整数类型,此时+0x1就是加1,所以为100001

将p转换为unsigned int*型,此时再+1就是跳过一个int,即+4,所以为100004

3. %x是以十六进制进行打印

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

&a+1指向4后面的地址, 转换成int*类型赋给ptr,ptr[-1]表示ptr的地址向前走1然后访问,即数字4

然后将其转换成十六进制,进行打印

结果为:4   2000000

4. 

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

这里是逗号表达式,结果为最后一个表达式 

 

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

 

两地址相减表示地址之间的元素个数,又因为这道题中是低地址减高地址所以为-4,%d打印出来肯定是-4,但是%p就不一定了,我们首先要看补码,

原码:10000000000000000000000000000100
反码:11111111111111111111111111111011
补码:11111111111111111111111111111100

对于%p来说,认为内存中存的是地址,直接把地址打印出来,所以打印的是补码,转换为十六进制为:FFFFFFFC

6.  10,5

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

 

7.  at

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

 

 

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

 

 

 

 

运行结果: 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值