十四、指针(三)

9 字符指针变量

用法一:用来指向字符。

int main()
{
	char ch = 'w';
	char* pc = &ch;
    //pc 就是字符指针变量
	*pc = 'w';
	return 0;
}

用法二:用来指向字符串。

int main()
{
	const char* p = "abcdefg";
	printf("%s\n", p);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

事实上,p 里面存的还是首字符 a 的地址,只是因为字符串是连续存放的,所以给人一种指向的是整个字符串的错觉。

例:

#include <stdio.h>
int main()
{
	const char* p = "abcdefg";
	//不是将"abcdefg"字符串存放到p中,而是将首字符a的地址存在p中
	printf("%c\n", *p);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

笔试题:下面代码输出的结果是什么?

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	const char* str3 = "hello bit.";
	const 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;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

10 二级指针

指针变量也是变量,是变量就有地址,二级指针就用来存放指针变量的地址。

int a = 10;
int* pa = &a;
int** ppa = &pa;

对于二级指针的运算有:

• *ppa 通过对 ppa 中的地址进行解引用,这样找到的是 pa ,*ppa 其实访问的就是 pa。

• **ppa 先通过 *pa 找到 pa,然后对 pa 进行解引用操作从而找到 a。

例:

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	**ppa = 30;
	printf("%d", **ppa);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11 指针数组

指针数组就是用来存放指针的数组。

指针数组指向的元素是地址,这个地址又指向一个区域。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11.1 指针数组模拟二维数组

例:

#include <stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int arr4[] = { 4,5,6,7,8 };
	int arr5[] = { 5,6,7,8,9 };
	//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
	int* parr[5] = { arr1, arr2, arr3,arr4,arr5 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 5; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
            //parr[i]    == *(parr+i)
			//parr[i][j] == *(*(parr+i)+j)
		}
		printf("\n");
	}
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

parr[i] 就是访问 parr 数组中的元素,parr[i] 指向的数组元素指向了整型一维数组, parr[i][j]就是整型一维数组中的元素。

上述代码只是模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行不是连续的。

12 数组指针变量

刚才我们知道指针数组是一种数组,数组中存放的是地址(指针)。

//语法
int arr[6];
int (*p1)[6] = &arr;

类比:

• 整形指针变量:存放的是整型变量的地址,能够指向整型数据。

• 浮点型指针变量:存放浮点型变量的地址,能够指向浮点型数据。

所以数组指针变量存放的应该是数组的地址,能够指向数组。

//指针数组和数组指针的定义方式
int *p1[10];//指针数组
int (*p2)[10];//数组指针

p2 先和 * 结合,说明 p2 是一个指针变量,然后指向的是一个大小为10的整型数组,所以 p2 是一个数组指针,指向一个数组。

注意:由于 [ ] 的优先级要高于 * ,所以必须加上 ( ) 来保证 p2 先和 * 结合。

数组指针类型解析:

int (*p) [10] = &arr;
 |    |    |
 |    |    |
 |    |    p指向数组的元素个数
 |    p是数组指针变量名
 p指向的数组的元素类型

例:

char* ch[8];//指针数组
char* (*p2)[8] = &ch;//*p2 是一个数组指针,指向一个大小为8,类型为 char* 的数组。

注意:在定义上面的数组指针时,容易把 char 后面的 * 忽略掉,如果忽略掉之后,指向的就是大小为8,类型为 char 的数组。

一个比较简单的判断指针类型的方法是,把指针名去掉,比如 char* (*p2)[8] ,去掉指针名 p2 后就是 char* (*)[8],就是它的类型。

12.1 数组指针的初始化

用 &数组名 可以获得数组的地址,如果要存放这个数组的地址,就需要存放在数组指针变量中,如:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;
	return 0;
}

调试:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从调试中也可以看到,&arr 和 p 的类型完全一致。

例:

#include <stdio.h>
void print1()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int i = 0;
	printf("用整型指针打印数组每一个元素:");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	printf("\n");
}
void print2()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr;
	int i = 0;
	printf("用数组指针打印数组每一个元素:");
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);
	}
	printf("\n");
}
int main()
{
	print1();
	print2();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虽然数组指针也可以打印数组的元素,但在使用的时候不建议,容易出错。

12.2 二维数组传参的本质

要理解二维数组传参的本质,首先我们再来理解一下二维数组。二维数组本质上可以看作是每个元素是一维数组的数组,也就是说二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是一个一维数组。

所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据图中的例子,第一行一维数组的类型就是 int [5],所以第一行的地址类型就是数组指针类型 int (*)[5]。这就意味着二维数组传参本质上也是传递了地址,传递的是第一行一维数组的地址。

以往一个二维数组需要传参给一个函数时,我们是这样写的:

#include <stdio.h>
void test(int a[3][5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", a[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };
	test(arr, 3, 5);
	return 0;
}

知道了二维数组传参的本质后,就可以这样写了:

#include <stdio.h>
void test(int(*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + j));
            //      甚至写成(*(p + i))[j]也可以
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };
	test(arr, 3, 5);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论:二维数组传参,形参的部分可以写成数组的形式,也可以写成指针形式。

13 函数指针变量

13.1 函数指针变量的创建

函数其实也有地址,有了地址,就可以创建指针来存储这个地址。

例:

//打印函数的地址
#include <stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("test:  %p\n", test);
	printf("&test: %p\n", &test);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从输出结果可以看到,函数确实有地址,而函数名就是函数的地址,当然也可以通过 &函数名 的方式来获得函数的地址。这和数组有所区别,数组名是数组首元素的地址,而 &数组名 才是数组的地址。

如果要将函数的地址存放起来,就需要创建函数指针变量,创建函数指针后,后续就可以通过地址来调用函数。

例1:

void test()
{
 	printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;

例2:

int Add(int x, int y)
{
 	return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都可以

函数指针类型解析:

int (*pf3) (int x, int y)
 |    |     ------------ 
 |    |           |
 |    |           pf3指向函数的参数类型和个数的交代
 |    函数指针变量名
 pf3指向函数的返回类型
 
int (*) (int x, int y) //pf3函数指针变量的类型

13.2 函数指针变量的使用

通过函数指针调用指针指向的函数:

#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int(*pf3)(int, int) = Add;

	printf("%d\n", (*pf3)(2, 3));//事实上,不用解引用也可以
	printf("%d\n", pf3(2, 3));
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

特例1:下面这段代码是什么意思?

(*(void (*)())0)();

上述代码中,0是一个数字,类型为 int,而它前面的 void (*)() 是一个函数指针类型,而把一个类型放在一对括号里又是强制类型转换,所以 (void (*)())0 的意思就是把0强制转换为 void (*)() 类型,也就是把0当成 void (*)() 类型的地址。(*(void (*)())0)() 就是在调用0地址处的函数,这个函数没有参数,返回类型是 void。

特例2:下面这段代码是什么意思?

void (*signal(int , void(*)(int)))(int);

上述代码中,其实是一次函数声明,声明的 signal 函数有2个参数,第一个参数是 int 类型,第二个参数是函数指针类型,该函数指针指向的函数参数是 int 类型,返回类型是 void,signal 的返回类型也是一个函数指针,该函数指针指向的函数参数是 int,返回类型是void。

13.3 typedef关键字

typedef 用来给类型重命名,可以将复杂的类型简单化。

例:

typedef unsigned int uint;
//将 unsigned int 类型重命名为 uint
typedef int* ptr_t;
//将 int* 类型重命名为 ptr_t

给数组指针和函数指针重命名时,语法稍微有些区别。

例:

typedef int(*parr_t)[5]; 
//将数组指针类型 int(*)[5] 重命名为 parr_t
typedef void(*pfun_t)(int);
//将 void(*)(int) 类型重命名为 pf_t

如果要对刚才的特例2进行简化,那就可以这样写:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

14 函数指针数组

把函数的地址存到一个数组中,这个数组就叫函数指针数组。

//函数指针数组的定义语法。
int (*parr1[3])();
//parr1 先和 [] 结合,说明 parr1 是数组,数组的内容是 int (*)() 类型的函数指针。
int *parr2[3]();
int (*)() parr3[3];

14.1 转移表

转移表就是一个函数指针数组。

要理解转移表的用途,我们最好用个例子来解释。

例:写一个程序,用于实现计算器的功能。

//一般实现
#include <stdio.h>
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 div(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(" 0:exit \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 = div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

当计算器只有几个操作符时,使用 switch 语句尚可应付得过来,可如果是一个具有上百个操作符的计算器,那么这条 switch 语句将会非常长。而我们还发现,为了使用 switch 语句,表示操作符的代码必须是整数并且是连续的,这个时候就可以用转移表来实现相同的任务。

//用转移表实现
#include <stdio.h>
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 div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int (*p[5])(int x, int y) = { 0,add,sub,mul,div };//转移表
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf(" 0:exit \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("选择错误,重新选择\n");
		}
	} while (input);
	return 0;
}

上面的代码中,函数指针数组就像是一个跳板,用表示操作符的代码作为下标去函数指针数组里面找一个地址,然后通过这个地址去调用这个函数,所以函数指针数组又叫做转移表。

不过转移表也有自己的局限性,由于转移表的实现依赖于函数指针数组,而放在这个数组的元素的类型必须是相同的,也就是说放在函数指针数组中的函数的参数和返回类型必须是一样的。比如在上面的代码中,我们就只能用整型数来进行计算。

15 回调函数

如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。

例:写一个程序,用于实现计算器的功能。

//一般实现
#include <stdio.h>
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 div(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(" 0:exit \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 = div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到,上面的代码是重复出现的,虽然其中执行计算的逻辑有区别,但是输入输出操作是冗余的,如果想要简化一些,就需要使用回调函数。

//用回调函数实现
#include <stdio.h>
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 div(int a, int b)
{
	return a / b;
}

void cal(int(*pf)(int, int))
{
	int x, y;
	int ret = 0;
	printf("输入操作数:");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("ret = %d\n", ret);
}

int main()
{
	
	int input = 1;
	
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf(" 0:exit \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			cal(add);
			break;
		case 2:
			cal(sub);
			break;
		case 3:
			cal(mul);
			break;
		case 4:
			cal(div);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

15.1 回调函数举例:qsort

qsort 函数是一个包含在头文件 <stdlib.h> 中的库函数,它可以对任意类型的数组进行排序,内部使用的是快速排序的方法。

//qsort函数的声明
void qsort(void* base, size_t nitems, size_t size, int (*compar)(const void* a, const void* b));

qsort 函数的声明中:

  1. 参数 base 指向数组的起始地址,即数组名。
  2. 参数 nitems 表示该数组的元素个数。
  3. 参数 size 表示该数组中每个元素的大小(单位:字节)。
  4. 参数 int (*compar)(const void*, const void*)) 为指向比较函数的函数指针,决定了排序的顺序。

其中 void* 是一种指针类型,也被称为通用指针类型,它可以接收任意数据类型的地址,但要注意的是,这种类型只能用于存放地址,不能参与运算也不能解引用。

由于 qsort 函数是一个库函数,而实现 qsort 函数的设计者事先并不知道这个函数将被用来给什么样类型的数据进行排序,所以设计者把参数中的 base 和 *compar 中的参数定义为 void* 类型。此时,由于 void* 类型可以接收任意数据类型的地址,而不同类型的数据所占字节的大小又各不相同,所以需要把表示每个元素大小的参数也输入给函数,这就是参数 size 的用途。

另外,compar 参数是 qsort 函数排序的核心内容,它指向一个比较两个元素的函数,这个函数需要使用 qsort 函数的人自己去实现,因为只有使用者才知道待排序的数据是什么类型。注意两个形参必须是 const void* 类型,同时在调用 compar 函数(compar 实质为函数指针,这里称它所指向的函数也为 compar)时,传入的实参也必须转换成 const void* 类型。在 compar 函数内部才会将 const void* 类型转换成实际类型。

int compar(const void* a, const void* b);

compar 函数虽然可以自己实现,但也需要满足一些规则。一个通用的 compar 函数应该满足:

• 如果 compar 返回值小于0,那么 a 所指向元素会被排在 b 所指向元素的前面。

• 如果 compar 返回值等于0,那么 a 所指向元素与 b 所指向元素的顺序不确定。

• 如果 compar 返回值大于0,那么 a 所指向元素会被排在 b 所指向元素的后面。

因此,如果想让 qsort 函数进行从小到大(升序)排序,那么 compar 参数可以这样写:

 //方法一
int compare (const void* a, const void* b)
 {
   if ( *(MyType*)a <  *(MyType*)b ) return -1;
   if ( *(MyType*)a == *(MyType*)b ) return 0;
   if ( *(MyType*)a >  *(MyType*)b ) return 1;
 }
//方法二
int compare(const void* a, const void* b)
{
	return (*(MyType*)a - *(MyType*)b);
}

注意:实际使用时,需要将 MyType 转换成实际数组元素的类型。

例:使用 qsort 函数对 int 数组进行升序排序。

#include <stdio.h>
#include <stdlib.h>
int IntCmp(const void* a, const void* b)
{
	return (*(int*)a - *(int*)b);
}
int main()
{
	int arr[] = { 9,1,2,8,6,4,3,5,7,0 };
	int i = 0;
	int num = sizeof(arr) / sizeof(arr[0]);
	qsort(arr,num,sizeof(arr[0]),IntCmp);
	for (i = 0; i < 10; i++)
		printf("%d ", arr[i]);
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

15.2 qsort函数模拟实现

知道了 qsort 函数的原理,下面我们用冒泡排序来模拟实现 qsort 函数。

//冒泡排序核心代码
void bubble_sort(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)//确定趟数
	{
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)//一趟冒泡排序的过程
		{
			if (arr[j] > arr[j + 1])//比较
			{
                //交换
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

由于 qsort 函数可以对任意类型的数组进行排序,而上面的代码显然只能对整型数组进行排序,要利用冒泡排序的思想对任意类型的数组进行排序,我们就需要对上面的代码进行改造。

首先,我们要对函数接收的参数进行改造,让这个函数能够接收任意类型的数组。其次,由于不同类型的数据的比较方法存在差异(比如整型数据和结构体数据的比较方法并不相同),所以我们需要对比较的方法进行改造,让函数在排序时能够比较不同类型的数据。同理,交换的代码也需要修改。

函数参数的改造:

//由于我们是在模拟实现qsort函数,因此函数的参数部分直接仿写即可。
//改造前
void bubble_sort(int arr[], int sz);
//改造后
void Bubble_sort(void* base, size_t nitems, size_t size, int(*compar)(const void* a, const void* b));

比较方法的改造:

由于我们不知道要比较的两个数据有多大,这就造成我们不能像用整型指针那样直接对指针进行加法运算就能得到后边元素的地址。而我们发现 char 类型的数据只占1个字节,恰好参数 size 又表示的是待排序数组中每个元素所占字节的大小,我们通过把 base 强制转换为 char* 类型后,这样一来如果我们想通过 base 得到下一个元素的地址,只需要在 base 的基础上加上 size 个字节即可。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上图分别以短整型和整型数组为例展示了将 base 强制转换为 char* 类型后 base 访问后续元素的方法。

//改造前
if (arr[j] > arr[j + 1])
//改造后
int Int_cmp(const void* a, const void* b)
{
	return (*(int*)a - *(int*)b);
}

if (Int_cmp(((char*)base + j * size), ((char*)base + (j + 1) * size)) > 0)

交换方法的改造:

和前面遇到的情况一样,由于我们不知道要交换的两个数据有多大,所以在 Swap 函数中我们需要输入一个参数 size 用来表示元素的大小。

由于我们想要实现一个不管是什么类型的数据都能通过这个函数进行交换的功能,所以我们不能像以往交换整型数据那样直接把它的四个字节都同时交换掉,我们需要把不同类型的数据统一强制转化为只有1个字节的 char 类型后,再通过逐个字节进行交换的办法,实现整体交换。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上图以两个整型数据3和9为例展示了 Swap 函数的交换过程。

//改造前
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
//改造后
void Swap(char* a, char* b,int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *a;
		*a = *b;
		*b = tmp;
		a++;
		b++;
	}
}

Swap(((char*)base + j * size), ((char*)base + (j + 1) * size),size);

完整代码及测试内容:

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

void Swap(char* a, char* b, int size)
{
	int i = 0;
	for (i = 0; i < size; i++)
	{
		char tmp = *a;
		*a = *b;
		*b = tmp;
		a++;
		b++;
	}
}

void Bubble_sort(void* base, size_t nitems, size_t size, int(*compar)(const void* a, const void* b))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < nitems - 1; i++)
	{
		for (j = 0; j < nitems - 1 - i; j++)
		{
			if (compar(((char*)base + j * size), ((char*)base + (j + 1) * size)) > 0)
			{
				Swap(((char*)base + j * size), ((char*)base + (j + 1) * size), size);
			}
		}
	}
}

//测试Bubble_sort函数排序整型数据
int Int_cmp(const void* a, const void* b)
{
	return (*(int*)a - *(int*)b);
}

void test1()
{
	int arr1[] = { 9,1,3,7,4,6,8,2,5,0 };
	int i = 0;
	size_t num = sizeof(arr1) / sizeof(arr1[0]);
	printf("测试Bubble_sort函数排序整型数据:\n");
	printf("排序前:");
	for (i = 0; i < num; i++)
	{
		printf("%d ", arr1[i]);
	}
	printf("\n");
	Bubble_sort(arr1, num, sizeof(arr1[0]), Int_cmp);
	printf("排序后:");
	for (i = 0; i < num; i++)
	{
		printf("%d ", arr1[i]);
	}
	printf("\n");
}

//测试Bubble_sort函数排序结构体数据
struct Stu
{
	char name[20];
	int age;
};
int Stu_cmp_by_name(const void* a, const void* b)
{
	return strcmp(((struct Stu*)a)->name, ((struct Stu*)b)->name);
}
int Stu_cmp_by_age(const void* a, const void* b)
{
	return (((struct Stu*)a)->age) - (((struct Stu*)b)->age);
}
void test2()
{
	struct Stu arr2[] = { {"zhangsan",15},{"lisi",12},{"wangwu",18} };
	struct Stu* p = arr2;
	int i = 0;
	size_t num = sizeof(arr2) / sizeof(arr2[0]);
	printf("测试Bubble_sort函数排序结构体数据:\n");
	printf("排序前:\n");
	for (p = arr2; p < &arr2[3]; p++)
	{
		printf("%s,%d\n", (*p).name, (*p).age);
	}
	printf("\n");
	Bubble_sort(arr2, num, sizeof(arr2[0]), Stu_cmp_by_age);
	printf("排序后(根据年龄排):\n");
	for (p = arr2; p < &arr2[3]; p++)
	{
		printf("%s,%d\n", (*p).name, (*p).age);
	}
	printf("\n");
	Bubble_sort(arr2, num, sizeof(arr2[0]), Stu_cmp_by_name);
	printf("排序后(根据姓名排):\n");
	for (p = arr2; p < &arr2[3]; p++)
	{
		printf("%s,%d\n", (*p).name, (*p).age);
	}
	printf("\n");
}
int main()
{
	test1();
	test2();
	return 0;
}

输出结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HackerKevn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值