C语言:指针的进阶

在之前已经学习了指针初阶相关知识,知道了指针的概念

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间.
  2. 指针的大小固定是4/8个字节(32位平台/64位平台)
  3. 指针是有类型的,指针的类型决定了指针±整数的步长,指针解引用操作的权限.
  4. 指针的运算
    具体可以见我之前写的指针初阶

下面是对指针高阶知识的简单描述

1. 字符指针

用来存放char字符类型地址的指针类型是char*字符指针类型

  • 一般是这样使用的
#include <stdio.h>

int main(void)
{
	char ch = 'w';
	char* pc = &ch;

	*pc = 'r';
	printf("%c\n", ch);

	return 0;
}

可以使用指针存放变量的地址,通过对指针的解引用对变量本身存放的内容进行修改.

  • 还有一种使用方法
#include <stdio.h>

int main(void)
{
	char arr[] = "abcdef";
	const char* p = "abcdef";

	printf("%s\n", p);

	return 0;
}
  1. 之前一直使用字符数组用来存放字符串的,在栈区开辟(字符串长度+1)的空间来存放.
  2. 同样可以用字符指针来存放常量字符串的首元素地址,与字符数组所存放的字符串不同的是:*p是常变量即不可以直接修改,不可以通过对指针p的解引用进行修改.
  3. p是指针,并不是它存放了这个字符串的所有内容,指针p所存放的是该字符串首元素的地址.
  4. 常量字符串也不是在栈区存储的,它是存放在静态区中的只读区(loader section),在源文件编译阶段就已经存放在该区域了.具体在下图的.text段
    在这里插入图片描述
#include <stdio.h>

int main(void)
{
	char* p = "abcdef";

	printf("%p\n", p);
	printf("%c\n", *p);
	printf("%p\n", &(*p));

	return 0;
}

通过上述代码,更加证实了指针p存放的是字符串首元素的地址.
p进行解引用得到的是字符串第一个元素'a',将'a'的地址和字符串的地址都打印出来,发现两地址值是一样的.
在这里插入图片描述

在这里插入图片描述

这个字符串的地址偏低,也可以证实它确实是被存放在低地址上的只读区.

  • 下面有一道题,问打印出什么?
#include <stdio.h>

int main(void)
{
	char str1[] = "abcdef";
	char str2[] = "abcdef";
	const char* str3 = "abcdef";
	const char* str4 = "abcdef";
	
	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;
}
  1. str1str2都是在栈区临时开辟一块空间用来存放这个字符串,在内存中这是两块地方.数组名表示的是数组首元素地址,明显两者是不相等的.

  2. str3str4同时存放只读区中的常量字符串"abcdef"的首元素地址,显然两者相等.
    在这里插入图片描述

  3. 程序运行结果如下 [外链图片转存失败,源站可能有防盗链机制,
    在这里插入图片描述

2. 指针数组

指针初阶同样已经学习过指针数组了,具体请点链接.

int* arr1[10];  //整型指针的数组
char* arr2[10]; //一级字符指针的数组
char** arr3[10];//二级字符指针的数组,每个元素存放一级指针的地址

通常使用指针数组模拟二维数组,但并不是真正的二维数组.

#include <stdio.h>

int main(void)
{
	int arr1[] = { 1,2,3,4,5 };			//整型数组
	int arr2[] = { 2,3,4,5,6 };			//整型数组
	int arr3[] = { 3,4,5,6,7 };			//整型数组


	int* arr[] = { arr1, arr2, arr3 };	//指针数组
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

3. 数组指针

3.1 数组指针的定义

数组指针是指针

整型指针int* pi是指向整型数据的指针.
浮点型指针float* pf是指向浮点型数据的指针.
同理,数组指针:指向数组类型的指针.

  • 那么,下面哪个是数组指针呢?
int* p1[10];
int (*p2)[10];
  1. p1是指针数组,能存放十个元素,每个元素是int*类型的.
  2. p2是数组指针,p2先与*结合,表示这是一个指针类型,然后指向一个int [10]类型的数组,数组是构造类型.
  3. []的优先级是要高于*的,所以需要使用()来保证p先与*结合.

3.2 &数组名 VS 数组名

对于下面的数组:

int arr[10];
  • arr&arr分别是啥呢?

我们都知道arr是数组名,数组名表示数组首元素的地址,是一个常量.
但是又两个例外,在sizeof操作符后,得到的是整个数组所占字节的大小,还有一个例外是在&取地址操作符后,那么&arr到底是什么意思呢?

首先,我们打印arr&arr的值

#include <stdio.h>

int main(void)
{
	int arr[10] = { 0, };

	printf("arr = %p\n", arr);
	printf("&arr = %p\n", &arr);

	return 0;
}

程序运行如下:
在这里插入图片描述

可见arr&arr打印的地址是一样的,但两者真的是一样的吗?

  • 下面还有一段代码
#include <stdio.h>

int main(void)
{
	int arr[10] = { 0, };

	printf("arr         = %p\n", arr);
	printf("arr + 1     = %p\n", arr + 1);
	printf("&arr[0]     = %p\n", &arr[0]);
	printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
	printf("&arr        = %p\n", &arr);
	printf("&arr + 1    = %p\n", &arr + 1);

	return 0;
}

发现结果有点不一样了:
在这里插入图片描述

  1. arr1,步长为4个字节,正好是一个int类型元素所占据的内存空间.与&arr[0]1等效.
  2. &arr1,步长却是0x28 = 40个字节,这正好是整个数组所占据的内存空间的大小.
  3. 通过调试,编译器也给我们了答案,arrint*类型的,这是整型指针类型;而&arrint (*)[10]类型的,这是数组指针类型. 在这里插入图片描述

编译器写的int[10] *,只是为了好分辨理解,正确的数组指针写法应该是int (*p)[10].

3.3 数组指针的使用

那么数组指针又是如何使用的呢?

传递数组给函数:当数组作为参数传递给函数时,实际上会将数组的首地址传递给函数。函数可以使用数组指针来操作整个数组,而不需要传递数组的长度。

动态内存分配:使用动态内存分配函数(如malloc、calloc等)分配内存时,会返回一个指向分配内存的指针。可以通过将该指针视为数组指针来操作动态分配的内存块。

多维数组:对于多维数组,可以使用数组指针来简化访问。例如,二维数组可以被视为一个指向一维数组的指针数组,可以使用指针运算来遍历或访问元素。

字符串操作:C语言中的字符串实际上是以字符数组的形式存储的。可以使用字符数组指针来进行字符串操作,如拷贝、连接、比较等。

数组的动态管理:通过数组指针,可以方便地动态管理数组的大小和内容。可以使用realloc函数重新调整已分配数组的大小,从而实现动态数组的功能。

本章先重点讲一下使用数组指针对于二维数组中使用,包括作为形参传递.

  • 当然使用数组指针是可以在一维数组中使用的:
#include <stdio.h>

int main(void)
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

	//使用数组指针来遍历一维数组
	int(*p1)[10] = &arr;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(*p1 + i));
		//printf("%d ", (*pi)[i]);
	}

	printf("\n");

	//使用整型指针来遍历一维数组
	int* p2 = arr;

	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2 + i));
	}

	return 0;
}
  1. printf("%d ", *(*p1 + i));首先*p1,对p1解引用得到指针指向的整个数组的首元素的地址,再通过对数组首元素地址偏移解引用遍历到每个元素.*&arr -> arr.

  2. 可以发现,这样做的代码易读性明显不如直接使用整型指针来进行遍历

  3. 程序运行结果如下:
    在这里插入图片描述

  4. 所以一般,二维数组以上使用数组指针.

  • 下面是一个数组指针的使用例子:
#include <stdio.h>

void print_arr1(int arr[3][5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

void print_arr2(int(*arr)[5], int row, int col)
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}

int main(void)
{
	int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };

	print_arr1(arr, 3, 5);

	printf("\n");

	print_arr2(arr, 3, 5);

	return 0;
}
  1. 定义了一个二维数组,3行5列.我们可以将它看成如下图所示,实际上二维数组在内存中和一维数组一样,是连续存储的.
    在这里插入图片描述

  2. 数组名表示数组首元素的地址,那么二维数组的数组名就表示"第一行"的地址,即"第一个一维数组"的地址,而一维数组的地址也是首元素的地址.
    &arr = &arr[0] = &arr[0][0].

  3. 既然传递的arr表示的是一行一维数组,那么就可以用数组指针来接收,即int (*)[5]数组指针类型来接收.

  4. 那么arr虽然实际上是int [3][5]类型的,但也可以把它看作int (*)[5]类型的数组指针的数组,每个指针指向一个int [5]类型的一维数组.arr[0]表示{1,2,3,4,5}这个数组,等等.
    在这里插入图片描述

  5. print_arr1直接创建了一个二维数组来接收形参.

  6. print_arr2而是创建了一个数组指针来接收形参.

  7. 本质上都是传递指针

  8. 程序运行结果如下:
    在这里插入图片描述

  • 这样下面的代码我们就能很快知道是什么意思了
int arr[5];             //整型数组, 能存放5个元素
int *parr1[10];         //整型指针数组, 能存放10个元素
int (*parr2)[10];       //数组指针, 指向一个能存放10个元素的数组
int (*parr3[10])[5];    //数组指针数组, int (*)[10]类型, 等同于二维数组, 5行10列

4.数组参数,指针参数

在写代码的时候难免要把[数组]或者[指针]传给函数,那么函数的参数应该如何设计呢?

4.1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok 编译器会忽视[]中的10
{}
void test(int *arr)//ok
{}
void test2(int *arr[20])//ok
{}
void test2(int **arr)//ok
{}
int main()
{
    int arr[10] = {0};
    int *arr2[20] = {0};
    test(arr);
    test2(arr2);
}

4.2 二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//err    传递的是一维数组
{}
void test(int* arr[5])//err     传递的是一维指针数组
{}
void test(int (*arr)[5])//ok
{}
void test(int **arr)//err       传递的是一维指针数组
{}
int main()
{
    int arr[3][5] = {0};
    test(arr);
}

二维数组在内存中连续存放的,必须要知道一行有多少元素,这样编译器才好分配空间.

4.3 一级指针传参

#include <stdio.h>

void print(int *p, int sz)
{
    int i = 0;
    for(i=0; i<sz; i++)
    {
        printf("%d\n", *(p+i));
    }
}

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int *p = arr;
    int sz = sizeof(arr)/sizeof(arr[0]);
    //一级指针p,传给函数
    print(p, sz);
    return 0;
}

形参是一级指针,实参可以是一级指针变量和普通变量地址.

4.4 二级指针传参

#include <stdio.h>

void test(int** ptr)
{
    printf("num = %d\n", **ptr);
}

int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
}

形参是二级指针,实参可以是二级指针变量和一级指针地址.

5. 函数指针

函数指针是指向函数的指针

数组指针是指向数组的指针,那么函数指针就是指向函数的指针.

  • 首先看一段代码:
#include <stdio.h>

void test()
{
	printf("hehe\n");
}

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

程序运行结果如下:
在这里插入图片描述

可以看到函数也是有地址的,&testtest的地址都是一样的.

  • 那么,怎么保存函数地址呢?这就需要用到函数指针,来保存函数地址.
void test()
{
    printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
  1. pfun1 可以存放, pfun1先和*结合,表明这是一个指针;剩下的void ...(),表示指针指向一个函数,这个函数没有参数,返回值是void.
  2. void *pfun2();这只是声明一个函数,函数名叫pfun2,没有参数,返回值是void*.
  • 怎么使用函数指针呢?
#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}

int main(void)
{
	int a = 3;
	int b = 5;
	int (*p)(int, int) = Add;	//p是函数指针,指向Add函数
	
	int ret1 = Add(a, b);
	int ret2 = (*p)(a, b);
	int ret3 = p(a, b);

	printf("%d\n", ret1);
	printf("%d\n", ret2);
	printf("%d\n", ret3);

	return 0;
}
  1. 定义了函数指针p指向了Add函数
  2. p = &Add = Add -> (*p) = (*&Add) = Add = p
  • 下面有两行代码,取自《C陷阱与缺陷》,可以加深对函数指针的理解
//代码1
(*(void (*)())0)();

从内向外一步一步理解

  1. void (*)()表示一个函数指针类型,指向一个没有参数,返回值是void的函数.
  2. (void (*)())0表示将0强制类型转换为函数指针类型.
  3. (*(void (*)())0)()表示对函数指针变量0解引用得到0地址处的函数,同时后面跟上参数(),总结就是调用了0地址处的函数.
  4. 但运行这段代码会崩溃,0地址处的代码需经操作系统转换为内核态才可以访问.
//代码2
void (*signal(int , void(*)(int)))(int);
  1. void(*)(int)表示一个函数指针类型,指向一个有一个int类型参数,返回值是void的函数.
  2. signal(int , void(*)(int))signal没有和*被括号括上,说明signal是一个函数,这个函数有两个参数,一个是int类型,一个是void (*) (int)函数指针类型
  3. 观察整体结构,大概是这样void(* p )(int),这是一个函数指针类型,同时也是signal函数的返回值
  4. 总结:signal函数的声明,有两个参数:int类型和void(*)(int)函数指针类型,返回值也是void(*)(int)类型的

代码2太复杂了,可以使用typedef进行优化

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

这样代码可读性就大大提高了.

6. 函数指针数组

函数指针数组是一个数组,存储了多个函数指针的地址

  • 声明如下:
int (*parr[10])()
返回类型 (*函数指针数组名[数组大小])(参数列表)

parr先和[]结合,说明parr是一个数组,数组的类型就是剩余的int (*)()
,说明这个数组类型是函数指针类型.

通过使用函数指针数组,可以根据索引调用不同的函数.

  • 例如(计算器),实现对整数的加减乘除
    • 一般方法实现
#include <stdio.h>

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

void menu()
{
	printf("##########################\n");
	printf("###### 1.add  2.sub ######\n");
	printf("###### 3.mul  4.div ######\n");
	printf("###### 0.exit       ######\n");
	printf("##########################\n");
}

int main(void)
{
	int x = 0;
	int y = 0;
	int input = 0;

	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", add(x, y));
			break;
		case 2:
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", sub(x, y));
			break;
		case 3:
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", mul(x, y));
			break;
		case 4:
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", div(x, y));
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("输入有误,请重新输入\n");
			break;
		}
	} while (input);

	return 0;
}
  1. 上面的程序使用了switch-case语句用来判断输入,明显代码有冗余

  2. 若后续还需添加模运算,逻辑运算,则需要更多的case分支,这样代码就成了"屎山"…

  3. 引入函数指针数组可以有效减少冗余,让代码更加简洁.

    • 函数指针数组方式实现
#include <stdio.h>

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

void menu()
{
	printf("##########################\n");
	printf("###### 1.add  2.sub ######\n");
	printf("###### 3.mul  4.div ######\n");
	printf("###### 0.exit       ######\n");
	printf("##########################\n");
}

int main(void)
{
	int x = 0;
	int y = 0;
	int input = 1;
	int (*pArr[5])(int, int) = { NULL, add, sub, mul, div };

	while (input)
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);

		if ((input >= 1) && (input <= 4))
		{
			printf("请输入操作数:");
			scanf("%d %d", &x, &y);
			printf("%d\n", (*pArr[input])(x, y));
		}
		else
		{
			printf("输入有误,请重新输入\n");
		}
	} 

	return 0;
}
  1. 函数指针数组提供了一种灵活的方式来管理和调用多个函数

7. 指向函数指针数组的指针

指向函数指针数组的指针,是一个指针指向一个数组,数组的元素都是函数指针

#include <stdio.h>

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

int main(void)
{
	//函数指针
	void (*pFun)(const char*) = test;
	//函数指针数组
	void (*pFunArr[5])(const char*);
	pFunArr[0] = test;
	//指向函数指针数组的指针
	void (*(*ppFunArr)[5])(const char*) = &pFunArr;

	return 0;
}
  1. 写复杂指针,从简单写到复杂.
  2. 首先它是一个指针,(*p)
  3. 这个指针指向了一个数组,(*p)[5]
  4. 数组类型是函数指针类型,void (*(*p)[5])(const char*)

8.回调函数

回调函数是指作为参数传递给另一个函数的函数,当满足特定条件时,另一个函数会调用该回调函数来执行特定的操作.

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

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时,由另外一方调用的,用于对该事件或条件进行响应.

8.1 使用回调函数实现计算器程序

  • 上述的计算器,也可以用回调函数,实现更小的冗余量
#include <stdio.h>

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

void calculate(int (*p)(int, int))
{
	int x = 0;
	int y = 0;

	printf("请输入操作数:");
	scanf("%d %d", &x, &y);

	printf("%d\n", p(x, y));
}

void menu()
{
	printf("##########################\n");
	printf("###### 1.add  2.sub ######\n");
	printf("###### 3.mul  4.div ######\n");
	printf("###### 0.exit       ######\n");
	printf("##########################\n");
}

int main(void)
{
	int input = 1;

	while (input)
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			calculate(add);
			break;
		case 2:
			calculate(sub);
			break;
		case 3:
			calculate(mul);
			break;
		case 4:
			calculate(div);
			break;
		case 0:
			printf("退出计算器.\n");
			break;
		default:
			printf("你的输入有误,请重新输入.\n");
			break;
		}
	} 

	return 0;
}
  1. calculate函数中,传入了函数指针类型的参数,根据用户的不同输入来调用不同的回调函数进行计算
  2. 在用户输入操作数后,调用传递过来的函数指针,进行计算,并输出结果
  3. 计算器程序通过回调函数的机制,使得计算逻辑与菜单显示解耦,同时也提高了代码的可扩展性和灵活性.可以根据需求添加更多的运算函数,并在菜单中相应地增加选项
  4. 回调函数只有在被调用后,才会运行

8.2 库函数qsort

之前已经写过对数组的内容进行冒泡排序,但是只能对整型数据进行排序.

那么如何写一个能对通用(适用于所有类型的)的排序呢?

  • 其实,C语言库提供了一个基于快速排序的函数qsort
#include <stdlib.h>

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

qsort函数是对数组的元素进行排序

  • 对基于base开始的连续numsize字节的元素,使用compar回调函数来进行排序.
  • 排序算法,是通过调用传入qsort函数的函数指针所指向的函数compar来实现的.
  • 函数没有返回值,直接对数组的内容进行修改,使用compar回调函数进行排序.
  • 该函数对于两个等价的元素不进行排序.

参数: 在这里插入图片描述

  • void*无类型指针
    在传入的函数指针参数中,该函数类型中的参数内容是const void*,const是为了不让指针所指向的内容被修改,那void*究竟是什么呢?

void*是一种特殊的C语言数据类型,称为"无类型指针".它可以被强制类型转换为任意类型的指针.

可用于

  1. 函数传参: 当一个函数需要接受不同类型的指针参数时,可以使用void* 参数,然后在函数内部根据实际需要进行类型转换。
void foo(void *ptr) 
{
    int *p = (int *)ptr; // 将 void* 转换为 int*
    // 对 p 进行操作
}

int main() 
{
    int num = 10;
    foo(&num); // 传递 int* 类型指针的地址给 foo 函数
    return 0;
}

  1. 动态内存分配:在使用 malloccalloc 动态分配内存时,返回的指针类型为 void *.需要根据实际情况进行类型转换.
int *ptr = (int *)malloc(sizeof(int)); // 将 void* 转换为 int*
  1. 通用数据结构:可以使用 `void *`` 指针来在数据结构中存储不同类型的元素,然后根据需要进行类型转换.
struct Node 
{
    void *data;
    struct Node *next;
};

需要注意的是:void*类型的指针,不能直接进行解引用,进行指针运算.需要先将该指针强制类型转换为具体指针类型,才能进行相关指针操作.

  • 下面使用qsort测试实现对不同类型数据的排序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//学生类型结构体
struct Stu
{
	char name[20];
	int age;
};

//打印学生结构体数组内容
void print_stu(struct Stu* stuArr, size_t num)
{
	int i = 0;
	
	for (i = 0; i < num; i++)
	{
		printf("%s %d\n", stuArr[i].name, stuArr[i].age);
	}
}

//对整型数据进行排序 test1
int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;	//先将指针强制转换类型为int*, 再使用*操作符对指针进行解引用得到数据
}

void test1()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	//调用qsort函数
	qsort(arr, num, size, cmp_int);

	int i = 0;
	for (i = 0; i < num; i++)
	{
		printf("%d ", arr[i]);
	}

	printf("\n");
}

//对结构体类型进行排序, 基于整型数据 test2
int cmp_stu_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age; //先将指针强制转换类型为struct Stu*, 再使用->操作符访问到指针成员变量age
}

void test2()
{
	//创建结构体数组
	struct Stu arr[3] = {
		{"zhangsan", 20},
		{"lisi", 21},
		{"wangwu", 19}
	};

	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	//使用qsort进行排序
	qsort(arr, num, size, cmp_stu_age);

	print_stu(arr, num);

	printf("\n");
}

//对结构体类型进行排序, 基于字符类型数据 test3
int cmp_stu_name(const char* p1, const char* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);	//使用strcmp 比较字符串
}

void test3()
{
	//创建结构体数组
	struct Stu arr[3] = {
		{"zhangsan", 20},
		{"lisi", 21},
		{"wangwu", 19}
	};

	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	//使用qsort进行排序
	qsort(arr, num, size, cmp_stu_name);

	print_stu(arr, num);

	printf("\n");
}

//对排序字符类型进行排序 test4
int cmp_char(const char* p1, const char* p2)
{
	return *(char*)p1 - *(char*)p2;
}

void test4()
{
	char arr[] = { 'b','e','c','a','f','d' };

	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	qsort(arr, num, size, cmp_char);

	int i = 0;
	for (i = 0; i < num; i++)
	{
		printf("%c ", arr[i]);
	}

	printf("\n");
}

int main(void)
{
	//测试排序整型数组的数据
	test1();

	//测试排序结构体数组的数据, 基于整型数据排序 
	test2();

	//测试排序结构体数组的数据, 基于字符类型数据排序 
	test3();

	//测试排序字符类型数组的数据
	test4();

	return 0;
}

主要是为了熟悉cmp函数的写法
程序运行结果如下:
在这里插入图片描述

  • 熟悉了qsort的用法,模拟qsort的功能,实现通用的冒泡排序
#include <stdio.h>

//交换函数
void swap(void* p1, void* p2, size_t size)
{
	int i = 0;

	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)p1 + i);		
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}

//通用冒泡排序
void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const char*, const char*))
{
	int i = 0;
	int j = 0;

	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - i - 1; j++)
		{
			if (cmp((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size) > 0)	//arr[j] > arr[j+1]
			{
				swap((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size, size);
			}
		}
	}
}
  1. 写出冒泡排序的框架:一共num - 1轮, 每轮比较num - 1 - i
  2. 使用比较函数,两个参数相当于arr[j]arr[j+1].
    • 首先要将void *类型的数组首地址base,强制类型转换为unsigned char *,一则能对base进行指针运算,二则将base看成一连串的字符数组;通过对base地址的加减,得到对应元素的地址.

    • j * size是元素相对于数组首元素地址的偏移量, 将数组首元素地址加上偏移量,就能得到对应元素的地址
      在这里插入图片描述

    • cmp函数的两个参数就是const char* ,直接将偏移后的元素地址传入即可

    • cmp函数返回值:

      • 若第一个元素所指向的值>第二个元素所指向的值, 返回1
      • 若第一个元素所指向的值==第二个元素所指向的值, 返回0
      • 若第一个元素所指向的值<第二个元素所指向的值, 返回-1
        若要求升序排序数组,则如果前一个元素大于后一个元素的值,则判断为真,两元素进行交换.所以需要cmp(....) > 0
  3. 将两元素进行交换,构造了swap函数,参数有三个,前两个为被交换的两元素地址,第三个为两元素所占字节大小
    • 本质是将两元素按字节进行交换, 假设元素占据size = 4个字节,循环则需要重复4次,每次将一个字节的内容进行交换
      在这里插入图片描述

    • (char*)p1p1强制转换类型为char*,方便对指针运算,按字节访问p1所指向的数据

    • (char*)p1i,依次按字节访问该地址所指向空间的数据

    • *((char*)p1 + i),对指针*解引用,访问到该地址上占据一个字节的数据

    • 交换两个数据,简单设置个中间量tmp,将两个数据进行交换

对自建的bubble_sort函数进行测试

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

//交换函数
void swap(void* p1, void* p2, size_t size)
{
	int i = 0;

	for (i = 0; i < size; i++)
	{
		char tmp = *((char*)p1 + i);		
		*((char*)p1 + i) = *((char*)p2 + i);
		*((char*)p2 + i) = tmp;
	}
}

//通用冒泡排序
void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const char*, const char*))
{
	int i = 0;
	int j = 0;

	for (i = 0; i < num - 1; i++)
	{
		for (j = 0; j < num - i - 1; j++)
		{
			if (cmp((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size) > 0)	//arr[j] > arr[j+1]
			{
				swap((unsigned char*)base + j * size, (unsigned char*)base + (j + 1) * size, size);
			}
		}
	}
}

//学生类型结构体
struct Stu
{
	char name[20];
	int age;
};

//打印学生结构体数组内容
void print_stu(struct Stu* stuArr, size_t num)
{
	int i = 0;

	for (i = 0; i < num; i++)
	{
		printf("%s %d\n", stuArr[i].name, stuArr[i].age);
	}
}

//对整型数据进行排序 test1
int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;	//先将指针强制转换类型为int*, 再使用*操作符对指针进行解引用得到数据
}

void test1()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	bubble_sort(arr, num, size, cmp_int);

	int i = 0;
	for (i = 0; i < num; i++)
	{
		printf("%d ", arr[i]);
	}

	printf("\n");
}

//对结构体类型进行排序, 基于整型数据 test2
int cmp_stu_age(const void* p1, const void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age; //先将指针强制转换类型为struct Stu*, 再使用->操作符访问到指针成员变量age
}

void test2()
{
	//创建结构体数组
	struct Stu arr[3] = {
		{"zhangsan", 20},
		{"lisi", 21},
		{"wangwu", 19}
	};

	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	bubble_sort(arr, num, size, cmp_stu_age);

	print_stu(arr, num);

	printf("\n");
}

//对结构体类型进行排序, 基于字符类型数据 test3
int cmp_stu_name(const char* p1, const char* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);	//使用strcmp 比较字符串
}

void test3()
{
	//创建结构体数组
	struct Stu arr[3] = {
		{"zhangsan", 20},
		{"lisi", 21},
		{"wangwu", 19}
	};

	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	bubble_sort(arr, num, size, cmp_stu_name);

	print_stu(arr, num);

	printf("\n");
}

//对排序字符类型进行排序 test4
int cmp_char(const char* p1, const char* p2)
{
	return *(char*)p1 - *(char*)p2;
}

void test4()
{
	char arr[] = { 'b','e','c','a','f','d' };

	size_t num = sizeof(arr) / sizeof(arr[0]);
	size_t size = sizeof(arr[0]);

	bubble_sort(arr, num, size, cmp_char);

	int i = 0;
	for (i = 0; i < num; i++)
	{
		printf("%c ", arr[i]);
	}

	printf("\n");
}

int main(void)
{
	//测试排序整型数组的数据
	test1();

	//测试排序结构体数组的数据, 基于整型数据排序
	test2();

	//测试排序结构体数组的数据, 基于字符类型数据排序
	test3();

	//测试排序字符类型数组的数据
	test4();

	return 0;
}

程序运行结果符合预期:
在这里插入图片描述

本章完.

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值