C语言:指针的进阶

2023-10-26

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

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

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

本章完.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言:指针的进阶 的相关文章

  • 让CHAT介绍下V2ray

    CHAT回复 V2Ray是一个网络工具 主要用于科学上网和保护用户的网络安全 它的名字源自Vmess Ray 光线 通过使用新的网络协议 为用户提供稳定且灵活的代理服务 下面是一些V2Ray的主要特性 1 多协议支持 V2Ray 提供了大量
  • 软件测试|教你如何使用Python绘制出奥运五环旗

    简介 我们之前介绍过使用turtle来绘制正多边形 但是绘制正多边形只是turtle模块最基础的使用 我们可以使用turtle模块绘制出更多不一样的精彩图形 本文就来给大家介绍一个比较简单的turtle绘图实例 绘制奥运五环旗 初始化参数
  • 基于java的学生成绩在线管理系统设计与实现

    基于java的学生成绩在线管理系统设计与实现 I 引言 A 研究背景和动机 基于Java的学生成绩在线管理系统设计与实现的研究背景和动机是设计一个可以方便管理学生成绩的系统 该系统可以方便地记录学生的成绩 并为老师和学生提供查询和统计功能
  • 软件测试|教你使用Python下载图片

    前言 我一直觉得Windows系统默认的桌面背景不好看 但是自己又没有好的资源可以进行替换 突然我一个朋友提醒了我 网络上的图片这么多 你甚至可以每天换很多个好看的背景 但是如果让我手动去设置的话 我觉得太麻烦了 我不如使用技术手段将图片下
  • 基于java的物流信息网系统设计与实现

    基于java的物流信息网系统设计与实现 I 引言 A 研究背景和动机 基于Java的物流信息网系统设计与实现的研究背景和动机 随着互联网的普及和电子商务的快速发展 物流信息网系统已成为现代物流管理的重要组成部分 物流信息网系统能够实现物流信
  • 【计算机毕业设计】电影播放平台

    电影播放平台采用B S架构 数据库是MySQL 网站的搭建与开发采用了先进的java进行编写 使用了springboot框架 该系统从两个对象 由管理员和用户来对系统进行设计构建 主要功能包括 个人信息修改 对用户 电影分类 电影信息等功能
  • 【计算机毕业设计】毕业生就业管理微信小程序_lm9q0

    腾讯公司在2017年1月19日发布了一款不需要下载 不需要卸载 不需要存储的软件叫微信小程序 受到了很多人的喜欢 微信小程序自2017年发布至今 依托微信的社交属性和庞大的用户基数 已经渗透到生活的方方面面 1 微信小程序可以将基于微信平台
  • 【计算机毕业设计】宝鸡文理学院学生成绩动态追踪系统

    研究开发宝鸡文理学院学生成绩动态追踪系统的目的是让使用者可以更方便的将人 设备和场景更立体的连接在一起 能让用户以更科幻的方式使用产品 体验高科技时代带给人们的方便 同时也能让用户体会到与以往常规产品不同的体验风格 与安卓 iOS相比较起来
  • 【C++】__declspec含义

    目录 一 declspec dllexport 如果这篇文章对你有所帮助 渴望获得你的一个点赞 一 declspec dllexport declspec dllexport 是 Microsoft Visual C 编译器提供的一个扩展
  • Android Navigation的四大要点你都知道吗?

    在JetPack中有一个组件是Navigation 顾名思义它是一个页面导航组件 相对于其他的第三方导航 不同的是它是专门为Fragment的页面管理所设计的 它对于单个Activity的App来说非常有用 因为以一个Activity为架构
  • 面试官随便问几个问题就知道你究竟做没做过微信支付宝支付

    面试官随便问几个问题就知道你究竟做没做过微信支付宝支付 你知道直连模式和服务商模式吗 网上的课程一般给你演示的都是直连模式 而企业中有不少是申请成为了服务商 因为里面有佣金提成 我粗俗地解释 直连模式 就是说你是一个会做生意的老板 自己会搞
  • Hutool改变我们的coding方式(二)

    Hutool改变我们的coding方式 Hutool 简介 Hutool如何改变我们的coding方式 文档 安装 Maven
  • (2024最新整理)Java最全八股文及答案!

    Java的特点 Java是一门面向对象的编程语言 面向对象和面向过程的区别参考下一个问题 Java具有平台独立性和移植性 Java有一句口号 Write once run anywhere 一次编写 到处运行 这也是Java的魅力所在 而实
  • 计算机Java项目|基于SSM的篮球系列网上商城设计与实现

    作者简介 Java领域优质创作者 CSDN博客专家 CSDN内容合伙人 掘金特邀作者 阿里云博客专家 51CTO特邀作者 多年架构师设计经验 腾讯课堂常驻讲师 主要内容 Java项目 Python项目 前端项目 人工智能与大数据 简历模板
  • 计算机Java项目|在线图书管理

    作者简介 Java领域优质创作者 CSDN博客专家 CSDN内容合伙人 掘金特邀作者 阿里云博客专家 51CTO特邀作者 多年架构师设计经验 腾讯课堂常驻讲师 主要内容 Java项目 Python项目 前端项目 人工智能与大数据 简历模板
  • 【自适应滤波】一种接近最佳的自适应滤波器,用于突发系统变化研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码及文章
  • 【Linux】文件周边001之系统文件IO

    樊梓慕 个人主页 个人专栏 C语言 数据结构 蓝桥杯试题 LeetCode刷题笔记 实训项目
  • 2024年华为OD机试真题-分割均衡字符串-Python-OD统一考试(C卷)

    题目描述 均衡串定义 字符串只包含两种字符 且两种字符的个数相同 给定一个均衡字符串 请给出可分割成新的均衡子串的最大个数 约定字符串中只包含大写的 X 和 Y 两种字符 输入描述 均衡串 XXYYXY 字符串的长度 2 10000 给定的
  • 【C#】基础巩固

    最近写代码的时候各种灵感勃发 有了灵感 就该实现了 可是 实现起来有些不流畅 总是有这样 那样的卡壳 总结下来发现了几个问题 1 C 基础内容不是特别牢靠 理解的不到位 导致自己想出来了一些内容 但是无法使用正确的C 代码实现 导致灵感无法
  • sychnorized积累

    sychnorized 1 对象锁 包括方法锁 默认锁对象为this 当前实例对象 和同步代码块锁 自己指定锁对象 2 类锁 指synchronize修饰静态的方法或指定锁对象为Class对象 3 加锁和释放锁的原理 现象 时机 内置锁th

随机推荐

  • 微信小程序如何循环控制一行显示几个wx:for

    正如上图所显示的一样 我们改如何控制一行显示几个图形呢 首先第一种方法 数量少的可以自己一行一行的写 但是当数据很多的时候呢 这时候就需要我们区使用循环进行代码的编写 废话不多数 直接写代码 demo item width 40 demo
  • JVM系列(六) JVM 对象终结方法finalize

    我们有几个特别容易混淆的关键字final finally finalize 他们之间的区别是什么 final 是java关键字 finally 是try catch finally finalize 是Object 根类的方法 今天我们着重
  • 「PAT乙级真题解析」Basic Level 1073 多选题常见计分法 (问题分析+完整步骤+伪代码描述+提交通过代码)

    乙级的题目训练主要用来熟悉编程语言的语法和形成良好的编码习惯和编码规范 从小白开始逐步掌握用编程解决问题 PAT乙级BasicLevelPractice 1073 多选题常见计分法 问题分析 题设要求按照老师批改多选题的方法来计算学生的总分
  • Map.entry详解

    Map entrySet 这个方法返回的是一个Set
  • 【软件工程基础复习整理】第一章软件工程基础前言(1)软件、工程和软件工程

    想要把软件缺陷全消灭 要用最锐利的目光去审视每一行代码 用最慎密的心思来制定每一份计划 用最严谨的态度去查看每一项工作 不掌握一定的软件工程知识 不按软件工程的有关原理进行软件开发 不积极学习新的软件开发方法和技术 就不能高效 高质量地开发
  • HCIP第六天

    OSPF的不规则区域 OSPF区域划分的要求 1 区域之间必须存在ABR 2 区域划分必须按照星型拓扑结构划分 1 远离骨干的非骨干区域 2 不连续骨干 1 通过VPN隧道将R4连接到骨干区域中 使其合法化 1 当一个路由器同时连接骨干区域
  • k8s kubectl 日常命令使用记录

    Kubectl 自动补全 设置shell自动补全 要先安装bash completion 并永久添加自动补全 source lt kubectl completion bash echo source lt kubectl completi
  • 如何安装电子水尺?

    垂直安装 吊装 根据现场实际情况 可以由不同单元规格的传感器段组合成需要的长度 然后用U形连接件连接成一支整体传感器 倾斜贴壁安装 与垂直安装方法相同 此时要将传感器测量长度转换为测量高度 故而测量精度将提高 阶梯式安装 各支传感器分别安装
  • 微信小程序从A小程序跳转至B小程序内wx.navigateToMiniProgram

    多个小程序多个主体之间相互跳转wx navigateToMiniProgram 代码在最后 参数说明 属性 类型 必填 说明 appId string 否 要打开的小程序 appId path string 否 打开的页面路径 如果为空则打
  • 深度学习之遥感变化检测数据集整理

    1 The River Data Set 高光谱河流变化检测数据 该数据集包含两幅高光谱影像 分别于2013年5月3日和12月31日采集自中国江苏省的某河流地区 所用传感器为Earth Observing 1 EO 1 Hyperion 光
  • 牛顿迭代法解非线性方程组 c语言,用牛顿迭代法解非线性方程组有两个非线性方程,未知数是x1,x2:(15x1+10x2)/[(40-30x1-10x2)^...

    共回答了24个问题采纳率 91 7 function r n mulNewton x0 eps if nargin 1 eps 1 0e 4 end r x0 myf x0 inv dmyf x0 n 1 tol 1 while tol g
  • QML中鼠标拖动移动ListView中项的位置

    在QML开发中 ListView是我们经常用到的控件 可以用它给用户展示出列表 但是往往都是将项目的显示顺序排好后 直接让ListView显示出来 亦或者是知道要移动到具体的那一位置 然后调整数据在ListView中的顺序来达到要求 现有一
  • 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序!

    题目 给定一个数组 nums 编写一个函数将所有 0 移动到数组的末尾 同时保持非零元素的相对顺序 java 解决此题的思想就是将非零的数移到数组前端非零的后面 最简单最高效的方法其实使用双指针 一个指针就正常遍历数组查看是否为0 另一个指
  • 三十.Python学习笔记.6

    组合数据类型 2 一 字典类型定义 映射是一种键 索引 和值 数据 的对应 字典类型是 映射 的体现 键值对 键是数据索引的扩展 字典是键值对的集合 键值对之间无序 采用大括号 和dict 键值对用冒号 表示 二 字典类型操作函数的方法 函
  • 基于STM32HAL库编写状态机模式

    概述 本篇文章介绍如何使用STM32HAL库 以马达转动的状态示例 来说明 项目中使用的状态模式 参考该文章链接 比较懒 基本都是照搬框架 这种写法确实在项目后续新增功能时 方便不少 还是值得学习 这样的思路 加油 技术同仁们 硬件 STM
  • Python自动化测试实战

    接口自动化测试是指通过编写程序来模拟用户的行为 对接口进行自动化测试 Python是一种流行的编程语言 它在接口自动化测试中得到了广泛应用 下面详细介绍Python接口自动化测试实战 1 接口自动化测试框架 在Python接口自动化测试中
  • 关于IP网络号和主机号的原理

    转自xhamigua QQ496400739的文章 http xhamigua blog 163 com blog static 61786908201191112512850 文章中可能有些小错误 不过不影响学习 关于IP网络号和主机号的
  • 高精度人脸表情识别 开源代码

    人脸表情识别 半监督 Margin Mix Semi Supervised Learning for Face Expression Recognition 作者 Corneliu Florea Mihai Badea Laura Flor
  • 【网络编程】应用层协议——HTTPS协议(数据的加密与解密)

    文章目录 一 HTTP协议的缺陷 二 HTTPS协议的介绍 三 加密与解密 3 1 加密与解密流程 3 2 为什么要加密和解密 3 3 常见的加密方式 3 3 1 对称加密 3 3 2 非对称加密 3 3 3 数据摘要 数据指纹 3 3 4
  • C语言:指针的进阶

    在之前已经学习了指针初阶相关知识 知道了指针的概念 指针就是个变量 用来存放地址 地址唯一标识一块内存空间 指针的大小固定是4 8个字节 32位平台 64位平台 指针是有类型的 指针的类型决定了指针 整数的步长 指针解引用操作的权限 指针的