对进阶指针的追忆

目录

思维导图

指针前言

一:字符指针

二:指针数组

三:数组指针

四:数组参数 && 指针参数

五:函数指针

六:函数指针数组

七:函数指针数组的指针

八:回调函数


 思维导图:

指针前言:

学过C语言的各位“铁汁们”,想必都知道,C语言的“灵魂”所在就是指针啦!

指针其实就是用来存放某个东西的“地址”(也可以理解为:指针是打开一个房间门的一把钥匙)!注意这是形象化的理解哈,站在计算机的角度理解就是:一串一串的数字。计算机的内存被划分无数个的小单元,每一个小单元(可以理解为一个房间)都对应一个唯一的地址标识,换言之就是指针(地址),通过指针就可以进入此房间,进行合理的一些操作。

1)关于地址的由来:(对于32为平台而言)地址是由32个 0 或者 1组成的一串二进制序列

2)指针大小是固定的:4个字节(32为平台下)或者是8个字节(64为平台下)

3)指针的类型决定了指针在 加上或者减去一个整数的时候,他解引用时候的权限

4)不管是一级指针还是二级指针亦或是高级指针,他们都是指针(注意哈:咱可不要站在门缝里看指针,把指针看扁了),是指针就是一个变量,都是用来存放地址的。

5)指针的运算:指针-指针的绝对值得到的是元素的个数

1.字符指针

可以借助类比的思想,对于整形指针想必我们应不陌生吧。

整形指针:用来存放整形数据的指针

那么顾名思义,字符指针就是用来存放字符的数据类型的指针呗

那接下来,话不多说,举几个栗子就可以 get 到了 。

1)

2)

显然是不可能滴!

指针p的大小是4个字节(32位平台上)而字符串“hello world”所占大小是11个字节。

此时只是把首字符串h 的地址放在p指针里面了,通过记住此时字符h 的地址,以便后续的操作。

那接下来小试牛刀一把,以加强对此知识点的理解。

对于当前的程序,输出的结果是啥???

分析:

数组名表示数组首元素的地址,数组名 arr1和 数组名 arr2 是2个完全不一样的数组,所以 输出: arr1 and  arr2  are not same

虽然这两个数组的首元素都是字符 h ,但是他们所表示的含义是不一样的。

对于常量字符串 hello ,是存放在代码区域的(只能指向读的操作),其中字符指针str1  ,str2 都是指向这个常量字符串的,站在计算机内存角度,相同的代码没有必要存放2份,所以输出:str1 and str2 are same

运行结果如下:

2.指针数组
2.1 定义

依然借助类比思想,通过对整形指针数组的理解,开头知道

指针数组:是存放指针的数组

数组:存储一组相同类型数据的集合

2.2 使用

3.数组指针
3.1定义

数组指针:首先他是指针,是指向数组的指针

3.2使用

1)

在这里,想必有不少的初学者对于数组指针  和  指针数组 这两个分不清吧!

其实只要把主语定位准确就好

数组指针旨在强调指针,只不过是指向数组的指针罢了

指针数组旨在强调数组,只不过是用来存放指针的数组罢了

对于以下2种写法,各位看看哪一个是数组指针呢

 第46行:这是指针数组;去掉p之后就是数组的类型,这个数组有3个元素。每个元素是int* 类型

第47行:是一个数组指针;去掉p之后,就是指针p的类型,指向一个数组的地址,这个数组有3个元素,每个元素都是int

2)数组指针具体场景的使用:二维数组

想必大家对这个二维数组的访问,应该感到并不陌生吧,甚至说再熟悉不过了。双层for循环,通过控制二维数组的行和列来便利访问当前数组罢了。 

接下来看看借助数组指针如何来遍历

 

二维数组名的理解:

1)他依然表示数组首元素的地址;只不过是表示第一行元素的地址(换言之就是:以数组指针的形式表示的)

2)可以把二维数组看作是由一个个一维数组组成的元素

3.3 数组名 与 &数组名的不同

数组名:代表数组首元素的地址;但是有2 个特殊情况。

第一个:sizeof(数组名):求的是整个数组的地大小(单位是字节)

第二个:&数组名,取出的是整个数组的地址(注意只不过在打印的时候是从当前首元素地址开始打印,但二者含义是不一样是)。

结合下面的栗子,有助于更好的理解!

4.数组传参和指针传参
4.1一维数组传参

思考:实参传递  arr,  对应的形参应该以啥形式来接收???

第85行:形参以数组的形式接收,支持(注意:数组可以不指定大小

第87行:形参以数组的形式接收只不过相比较上面指定了数组的大小,支持

第89行:形参以指针的形式接收,支持;数组名就是表示数组首元素的地址嘛

第91行:形参以指针数组的形式接收,不支持

 注意:

当实参以数组名的形式传参的时候,形参要是写成数组的形式,可以指定数组大小也可以不指定数组的大小;因为此时虽然是以数组的形式传参,但是其实底层是以指针的方式来接收的

4.2二维数组传参

此时arr:是一个二维数组的数组名

第99行:当实参传递数组名,和对应的列数时候,形参也是以同样方式接受的,是支持的

第101行:未指定二维数组的列,不支持

第103行:支持

第105:不支持,形参此时是一个一级整形指针而实参是一个数组指针

第107:不支持,形参是一个二级指针,而实参是一个数组指针

第109行:支持,形参实参类型都是:数组指针

 注意:

对于二维数组而言:在传参的时候 ,行可以省,但是二维数组的列不能省二维数组名表示数组首元素的地址(也就是第一行元素的地址)

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

 分析:实参传递的是一个整形指针;

当形参的参数是指针的时候,对应的实参可以是什么类型???

1)指针

2)一个普通的变量

3)一维数组名

4)常量字符串

4.4二级指针传参

当形参是二级指针的时候,对应的实参可以传那些类型???

1)二级指针

2)二维数组名

3)存放一级指针的指针的地址

5.函数指针

还是老问题,借助类比的思想:

数组指针:是存放数组的指针

函数指针:就是存放函数的指针

5.1 对&函数名与函数名的理解

通过对结果的分析可知:&函数名和 函数名所代表的含义一样都表示当前函数的地址

注意这里是不同于 &数组名 和 数组名的含义

 有了对函数名的理解以及,接下来看一下下面2行代码所表示的含义吧!!!

 分析:

第一行代码:调用0地址处的函数

第二行代码:是signal 函数的声明

5.2 typedef 关键字的使用 

1)typedef 作用:对类型进行重新命名

比如说:unsigned char 这个数据类型名字过长,使用typedef 可以对unsigned char 进行重新命名为 :un_c(也就是 说,给unsigned char 起一个别名,只不过还是同一个数据类型,只不过称呼不一样啦)

2)举例

但是若是对函数指针进行重命名的话,稍许有点不太一样

6.函数指针数组

要想弄清楚函数指针数组,那就必须对函数 以及函数指针掌握透彻。

函数指针是啥???

函数名 和 

 &函数名 又是啥 ???

顾名思义:函数指针数组:是用来存放函数指针的一个数组,数组的元素是一个个函数指针

6.1函数指针数组的使用

 

 写一个计算器的程序:

普通版本:

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()
{
	//实现一个计算器的功能:一般写法
	int input = 0;
	int x, y, ret = 0;
	do
	{
		Menu();
		printf("输入您的选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("输入2个操作数:>");
			scanf("%d %d", &x, &y);
			switch (input)
			{
			case 1:
				ret = add(x, y);
				printf("ret = %d\n", ret);

				break;
			case 2:
				ret = sub(x, y);
				printf("ret = %d\n", ret);

				break;
			case 3:
				ret = mul(x, y);
				printf("ret = %d\n", ret);

				break;
			case 4:
				ret = div(x, y);
				printf("ret = %d\n", ret);

				break;
			}
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("输入错误,重新输入\n");
		}
	} while (input);
	return 0;
}

分析:随着用户需求的不断更改,对计算器的更能也需要不断完善,比如实现 2个数据的 按位与,按位或等等。这将会致使case 语句越来越冗余,而且复用率极低,不是一个很好的代码。 

借助函数指针的版本:

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()
{
	int input = 0,ret = 0;
	int x, y;
	do
	{
		Menu();
		int (*p[5]) (int, int) = { NULL,add,sub,mul,div };
		printf("输入您的选择:>");
		scanf("%d", &input);

		if (input >= 1 && input <= 4)
		{
			printf("输入2个操作数:>");
			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;
}

1)之所以把函数指针数组大小设置为5而不是4:就是为了保持与case 语句的一致。

2)函数名:表示函数的地址 

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

还是老问题:函数指针数组的指针 是 指向函数指针数组的 指针

8.回调函数
8.1回调函数定义

首先回调函数是借助一个函数指针来调用的函数。当把函数的地址作为参数传递给一个指针,来间接调用要想调用的函数,此时这个被调用的函数就是回调函数

8.2回调函数应用

1)还是借助计算器实现的那个程序进行解释:一个计算器的功能不止一个(可以进行加法运算,减法运算,乘法运算,除法运算,按位与……)。那有没有一个函数可以实现多种功能:不仅可以进行加法运算,还可以进行减法运算,乘法运算…… 这就是回调函数该发挥作用啦!

初始版本的计算器代码:

其实不难发现:对于圈出部分,过于冗余了,可复用性并不高

 借助回调函数实现的计算器代码:

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;
}
int Calc(int (*p)(int, int ))
{
	int x, y;
	printf("请输入2个操作数:>");
	scanf("%d %d", &x, &y);

	return p(x, y);
}
int main()
{
	int input;
	int ret = 0;
	
	do
	{
		Menu();
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			
			ret =Calc(Add);//此时被间接调用的Add函数就成为回调函数
			printf("ret = %d\n", ret);
			break;
		case 2:
			ret = Calc(Sub);

			printf("ret = %d\n", ret);
			break;
		case 3:
			ret = Calc(Mul);

			printf("ret = %d\n", ret);
			break;
		case 4:
			ret = Calc(Div);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("输入错误,重新输入\n");
			break;
		}
	} while (input);
	return 0;
}

使用回调函数模拟实现qsort()(注意是基于冒泡思想实现的qsort函数)

1)冒泡核心思想:两两相邻元素进行交换(满足一定条件的)

2)qsort( )函数的初步使用

使用此函数对int 类型数据进行排序(注意qsort函数默认是以升序来排列的)

对于compare()这个函数,需要注意以下几点:

第一:此函数返回类型是int  类型

第二:此函数的参数类型必须是const void *

第三:对于void*类型的指针是不能直接进行解引用的,需要先进行类型强转,再进行解引用 

qsort()可以用来排序任意类型的数据

对int 类型数据排序

int cmp(void* p1, void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}

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

对结构体类型数据排序:

typedef struct student
{
	int age;
	char name[20];
	char sex;
}stu;int Cmp_name(const void* p1, const void* p2)//注意对于void*类型指针不能直接解引用
{
	return strcmp(((stu*)p1)->name, ((stu*)p2)->name);//注意字符串比较大小只能实用strcmp()函数
}
int main()
{
	stu arr[] = { {10,"lisi",'m'},{5,"bai",'w'},{15,"duan",'w'}};
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(stu), Cmp_name);
}
8.3 基于冒泡函数思想模拟qsort() 实现

完整代码:

int cmp(const void* p1, const void* p2)
{
	return (*(int*)p1 - *(int*)p2);
}
typedef struct student
{
	int age;
	char name[20];
	char sex;
}stu;
int Cmp_age(const void* p1,const void* p2)
{
	return (((stu*)p1)->age -( (stu*)p2)->age);// 注意 (stu*)p2->age错误的
}
int Cmp_name(const void* p1, const void* p2)//注意对于void*类型指针不能直接解引用
{
	return strcmp(((stu*)p1)->name, ((stu*)p2)->name);//注意字符串比较大小只能实用strcmp()函数
}
void Swap(char* p1, char* p2, int size)
{
	//注意是一个字节一个字节交换
	for (int i = 0; i < size; ++i)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		++p1;
		++p2;
	}
}
void Qsort(void* base, int num, int size, int(*cmp)(const void* p1, const void* p2))
{
	for (int i = 0; i < num - 1; ++i)
	{
		for (int j = 0; j < num - i - 1; ++j)
		{
			//相邻2元素进行比较
			//注意这里必须强转成char*类型指针
			if (cmp(((char*)base + j * size) ,((char*)base + (j + 1) * size) )  > 0) //注意这里条件必须注明是>0 还是<0
			{
				//交换
				Swap(((char*)base + j * size), ((char*)base + (j + 1) * size), size);
			}
		}
	}
}
int main()
{
	int arr[10] = { 7,4,1,2,5,8,9,6,3,10 };
	Qsort(arr, 10, sizeof(arr[0]), cmp);
	stu arr1[] = { {10,"lisi",'m'},{5,"bai",'w'},{15,"duan",'w'} };
	//Qsort(arr1, 3, sizeof(stu), Cmp_name);
	Qsort(arr1, 3, sizeof(stu), Cmp_age);//注意最后一个函数指针对应的函数需要使用者自己实现
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

 结语:

首先非常感谢各位老铁可以看到最后,想必收获满满吧!(有疑惑也是正常的,毕竟知识量太大了,俺也是整了不止一时半会才写完这篇博客的)对于指针这部分的学习,是真的及其重要!

对于后期数据结构以及C嘎嘎的学习,起着至关重要的作用,为了检验大家对指针这一知识点的学习,大家可以看看关于指针和数组相关面试的题型,链接在此,各位自取哈~~~

 https://blog.csdn.net/X_do_myself/article/details/134366577

今日的share到此就结束了,希望各位友友们可以支持一下~~~

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值