首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C语言指针内存管理实战:手写qsort、理解函数指针与二级指针

C语言指针内存管理实战:手写qsort、理解函数指针与二级指针

作者头像
码途随笔
发布2026-01-12 19:51:31
发布2026-01-12 19:51:31
990
举报

一、数组名的理解

  • 数组名是首元素的地址 验证:
代码语言:javascript
复制
int main()
{
	int arr[10] = { 0 };
	printf("arr[0]=%p\n", &arr[0]);
	printf("arr   =%p\n", arr);
	return 0;
}

结果:

sizeof的疑惑?为啥sizeof(arr)是40,不应该是4/8吗?

数组名确实是数组的首地址,但是有两个例外

  • sizeof():sizeof里面单独放一个数组,这里的数组名表示的是整个数组,计算的是整个数组的大小,单位字节
  • &arr:这里的数组名也表示的是整个数组,取出的是整个数组的地址(整个数组的地址与数组首元素的地址有区别) 除此之外,数组名在任何地方都代表数组的首地址

整个数组的地址与数组首元素的地址有啥区别?

虽然数值相同,但是类型不一样

二、指针访问数组

前提:

  • 数组在内存里连续存放
  • 指针的加减运算,方便我们获得每一个元素的地址
代码语言:javascript
复制
#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	for (int i = 0; i < sz; i++)
	{
		scanf("%d", p + i);
	}
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}

更多指针访问数组的写法扩展:

如何理解:

三、数组传参的本质

看下面的代码

代码语言:javascript
复制
#include<stdio.h>
void Print(int arr[])
{
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7 };
	Print(arr);
	return 0;
}

为啥sz放到函数里面,不能把数组完整的打印出来?

由于数组传的是指针,应该用指针变量来接收,形参虽然可以写成数组的形式,但是本质还是指针变量 因为传的是指针变量,所以求的是指针变量的大小,sizeof(arr) / sizeof(arr[0])得不到元素的大小 小结:

  • 数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参的数组是同一个数组的。
  • 形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略掉数组大小的。

四、冒泡排序

需要想清楚以下问题?

  • 每一趟把一个最大的数排在后面,则要多少趟
  • 每一趟需要排多少对?对数是变化的
  • 每一对里面具体是如何变化的

具体代码:

代码语言:javascript
复制
void bubble_arr(int *arr,int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
void Print(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}
int main()
{
	int arr[7] = { 4,9,7,6,3,2,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_arr(arr, sz);
	Print(arr, sz);
	return 0;
}

当数组接近排好序时,可能会多此一举

优化后:

代码语言:javascript
复制
void bubble_arr(int *arr,int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		int flag = 1;
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 0;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}
void Print(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}
int main()
{
	int arr[7] = { 4,9,7,6,3,2,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_arr(arr, sz);
	Print(arr, sz);
	return 0;
}

五、二级指针

5.1 二级指针的理解

二级指针(变量)存放的是一级指针的地址 一级指针和二级指针理解方式是一样的,如图:

代码语言:javascript
复制
int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	return 0;
}

p+1–>跳过4个字节 pp+1–>跳过4/8个字节(取决于环境)

5.2 二级指针的用法

*pp指向的是p,那**pp指向的就是a,有间接访问的效果

六、指针数组

是数组还是指针?

是数组 指针数组的每个元素是用来存放指针

用法:指针数组模拟二维数组管理一维数组

代码语言:javascript
复制
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 6,7,8,9,10 };
	int* arr[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

可以模拟模拟二维数组,但不是真的数组,二维数组是连续存放的,而它只是打印出来 原因:

七、字符指针变量

两种写法:

代码语言:javascript
复制
int main()
{
	char arr1[] = "abcdef";
	char arr2 = "abcdef";
	return 0;
}

常量字符串是不能改的

标准: const char* p2 = arr2;让编译器提前编译出错误 还可以打印出来 printf("%s", p1); printf("%s", p2); 注意:%s打印字符串时,只需提供起始地址 子符串常量放在代码块,不在静态区、栈区和堆区(因为它们可以改的,而字符串常量不能改)

下面结果是啥?

原因:子符串常量放在代码块,不在静态区、栈区和堆区(因为它们可以改的,而字符串常量不能改)

八、数组指针变量

理解方法:类比 int (*p)[10]:把*p去掉,相当于把数组名去掉,剩下类型int[10],所以是指向数组的指针 但是实际类型是int (*)[10],这是数组指针类型

如何理解:

不恰当的用法(非常啰嗦了) 补充知识: *&互为逆运算,并且&arr就是p,因此*parr也就是首元素的地址

代码语言:javascript
复制
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int (*p)[10] = &arr;
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);
	}
	return 0;
}

具体用法在二维数组传参里用到

九、二维数组传参的本质

以前我们二维数组实参传的是数组,形参接收也是数组,那二维数组传参的本质是什么呢?

补充:

  • 不管一维数组还是二维数组,数组名都是首元素的地址(除了&arrsizeof(arr)这两种情况以外) 那二维数组首元素的地址是谁的地址? 把二维数组的一行看成是一维数组组成,把每一个一维数组看成一个元素,二维数组每个元素是一维数组,首元素的地址就是第一行的地址,第一行的地址就是一维数组地址,类型是数组指针类型
  • 假设arr是第一行的地址,(arr+i)是每一行的地址,*(arr+i)是每一行的起始地址,而在二维数组中理解方法中arr[i]可以用来表示每一行起始地址,然后*(*(arr+i)+j)就可以表示arr[i][j] 打印二维数组(指针实现)
代码语言:javascript
复制
void Print(int (*p)[4], int x, int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{
			printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][4] = { {1,2,3,4}, { 5,6,7,8 } ,{ 9,10,11,12 } };
	Print(arr, 3, 4);
	return 0;
}

十、函数指针变量

10.1函数指针解析

函数也有地址吗?可以打印出来吗?

不能类比数组

Add函数为例 &AddAdd是同一个地址,不存在首元素的地址,都是函数的地址,没有区别

怎么存函数的地址呢?

函数指针变量存放函数的地址

去掉名字,int (*)(int,int)为函数指针类型,函数里的参数类型需要保持一致,形参的名字是可以干掉的(不用写,压根就不用)

具体用法: 以Add函数为例

原来调用函数是用函数名调用Add(),如何用指针实现这个功能呢?

Add函数为例(自己用要根据实际情况来看要不要传参,要不要接收返回值)

根据指针可以找到函数并调用函数,但是Add能直接调用,其实指针不用解引用也能够直接调用,*号可以不写或者写多个,这样也不会有问题,*就是个摆设

10.2 有趣的代码

  • 代码1

里面函数指针类型和强制类型转换非常重要,需要识别出来

  • 代码2

解析:

代码二理解起来非常难,有什么办法呢?

具体用typedef进行简化

十一、typedef关键字

11.1 typedef用法

概念:当类型不合适时,可以重新起名

用法:适用于简单的指针类型(int*、char*……)

代码语言:javascript
复制
typedef unsigned int u_int;
int main()
{
  unsigned int a1;
  u_int a2;
}

对于复杂的指针类型

正确方式:

同理函数指针一样

回到第十章中的代码2,可以简化:

11.2 #define的区别

区别:

  • ptr_t就是int *类型
  • PTR_Tint*替代了,是符号不是类型

总结:用define指定类型不够彻底,最好不用,但是也不是没有这种做法,看具体情况

十二、函数指针数组

先类比一下指针数组int *arr[10],说明数组是可以存放指针的,当数组存放的是函数指针时,那就是函数指针数组 应用场景:

使用方法:

有了函数指针数组,就可以进行多次计算(利用for循环遍历数组):

当你写了一个计算器(用switch语句不停的调用case1、case2、case3……不停的调用,并且补充相应的功能),如何用函数指针去优化

操作:

总的操作:

很好的利用到函数指针数组,这样写方便扩展其他的功能

函数指针数组就像是个跳板,可以转到不同的功能,故称为转移表

补充:函数指针的知识

函数指针应用场景: 回调函数就是通过函数指针调用的函数 当在主调函数里面没有直接去调用函数时,传递所需的函数的地址给另一个函数,在另一个函数中用函数指针调用所需的函数,这些所需的函数就叫做回调函数,另一个函数相当于中介,中介与主调函数是拿函数指针去沟通的

十三、qsort

13.1 qsort的用法

qsort——是用来排序的库函数,底层用的是快速排序的方法,而我们之前学过冒泡排序了

补充知识:冒泡排序函数实现,对一组整形数据进行排序,排为升序 思想:两两相邻元素进行比较,不满足顺序就交换

代码语言:javascript
复制
void Bubble_arr(int *p,int sz)
{
	for (int i = 0; i < sz-1; i++)
	{
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (*(p+j) > *(p + j+1))
			{
				int tmp = *(p + j);
				*(p + j) = *(p + j + 1);
				*(p + j + 1) = tmp;
			}
		}
	}
}
void Print(int *arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
	printf("\n");
}
int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Bubble_arr(arr, sz);
	Print(arr, sz);
	return 0;
}

缺点:只能排序整形数据,形参部分已经写死了,再写一个函数排浮点型太麻烦了 qsort可以排序任意类型的数据

如何理解第四个参数: 看一下如何用回调函数改造冒泡排序的案例:

注意:

  • 作为qsort()的使用者,明确知道要排序的是什么数据以及这些数据怎么比较,所以应由我们来提供比较函数
  • void*类型的指针是无具体类型指针,也不能进行解引用(不知道访问几个整型)和指针加减运算 整数比较为例
代码语言:javascript
复制
int com_int(void* p1, void* p2)
{
	return *(int*)p1 - *(int*)p2;
}
void print_arr(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}
int main()
{
	int arr[] = { 4,6,7,1,3,9,0,1,3 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), com_int);
	print_arr(arr, sz);
	return 0;
}

结构体比较,按名字来排序

代码语言:javascript
复制
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[100];
	int age;
};
int Fuc(void* p1, void* p2)
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}
int main()
{
	struct Stu arr[] = { {.name = "zhangsan",.age = 18},{"lishi",19},{"hehe",20} };
	int len=sizeof(arr) / sizeof(arr[0]);
	qsort(arr, len, sizeof(arr[0]), Fuc);
	return 0;
}

结构体比较,按年纪来排序

代码语言:javascript
复制
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[100];
	int age;
};
int Fuc(void* p1, void* p2)
{
	return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int main()
{
	struct Stu arr[] = { {.name = "zhangsan",.age = 19},{"lishi",18},{"hehe",20} };
	int len = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, len, sizeof(arr[0]), Fuc);
	return 0;
}

可以通过调试里的监视来进行验证

13.2 模仿qsort

模仿qsort函数来实现冒泡排序的函数,可以实现排序任何数据 原来bubble函数中形参为int*,为了接收任何数据的地址,改成void*类型的1地址

代码语言:javascript
复制
void Swp(char* p1, char* p2, size_t width)
{
	for (int i = 0; i < width; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}
int cmp(void* p1, void* p2)
{
	return *(int *)p1-*(int *)p2;
}
void Bubble_arr(void* base, size_t sz, size_t width, int(*cmp)(void* p1, void* p2))
{
	for (int i = 0; i < sz - 1; i++)
	{
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + width * j, (char*)base + width * (j + 1)) > 0)
			{
				Swp((char*)base + width * j, (char*)base + width * (j + 1), width);
			}
		}
	}
}
void Print(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
}
int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Bubble_arr(arr, sz, sizeof(arr[0]), cmp);
	Print(arr, sz);
	return 0;
}

分析过程如下:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-10-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、数组名的理解
  • 二、指针访问数组
  • 三、数组传参的本质
  • 四、冒泡排序
  • 五、二级指针
    • 5.1 二级指针的理解
    • 5.2 二级指针的用法
  • 六、指针数组
  • 七、字符指针变量
  • 八、数组指针变量
  • 九、二维数组传参的本质
  • 十、函数指针变量
    • 10.1函数指针解析
    • 10.2 有趣的代码
  • 十一、typedef关键字
    • 11.1 typedef用法
    • 11.2 #define的区别
  • 十二、函数指针数组
  • 十三、qsort
    • 13.1 qsort的用法
    • 13.2 模仿qsort
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档