【C语言】指针进阶剖析


在这之前,我们可以回忆下指针是什么
a.指针就是变量,是用来存放地址的
b.指针的类型决定了访问空间大小的权限
c.指针的大小在32/64位机器上是4/8字节
d.两指针相减表示两个地址在内存中间隔多少个指针类型的字节倍数

接下来我们将进一步的深入讨论指针的高级用法

1. 字符指针

	字符指针就是指向字符的的指针,用char*类型表示

我们可以看看他是如何使用的:

#include<stdio.h>
int main()
{
	char ch = 'a';
	char* p = &ch;
	*p = 'b';
	printf("%c", *p);
	return 0;
}

还有一种使用方法:

int main()
{
	char* p = "abcdef";
	*p = 'w';
	printf("%c\n", p);
	return 0;
}

第一个问题
可以思考一下p里面真的会赋进去“abcdef”吗?

任何表达式都有两个两个属性:

值属性、类型属性

例如以下代码:

int a = 5;
int b = a + 5;//a+5这个表达式的值是10,10就是他的值属性
//而a+5这个表达式中a是int类型,5也是int类型,int+int还是int,所以这个表达式最后的类型还是int
//这就是他的类型属性

所以char* p = “abcdef”; 实际上赋给指针变量p的是他的值属性,它的值属性是什么呢?是字符串首元素的地址,也就是a的地址;其次, 一个字符的大小是1个字节,这里的abcdef加上最后的\0,已经7个字符了,而char*类型只有4个字节的空间大小,是放不下的;所以这里实际上存入的是首元素的地址,也就是说,p里实际上存入的是a的地址
想要全部存进去需要这样写:char arr[] = “abcedf”;

第二个问题
有些编译器在编译char* p =“abcdef”; 这段代码的时候可能会报警告,不安全?
"abcdef"实际是一个常量字符串,也就意味着这个字符串本身不能被改的,而把这个字符串的起始地址放到p里面去,p的权限就变大了,因为p是没有被修饰的,所以p是敢去改这个内容的。
所以就有可能出现以下两种情况:
情况一:

//main函数里只写下这段代码
char* p = "abcdef";

某些编译器有可能报警告,但不会报错,这样去写了,照样该执行的执行,这就是她的脾气,就像你们有些人的女朋友是不?
情况二:

char* p = "abcedf";
*p = 'w';

这样写你就真的越界了,不太恰当的比喻,违反道德底线了是吧?这样做编译器运行后会直接挂掉,调试这段代码他就会告诉你写入访问冲突。所以有些编译器在情况一会报警告也是正确的,诶,就怕你敢出这事,所以警告你。
如何纠正警告问题呢?如下代码

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

这时就有效的保护了后面的字符串,即使想改也改不掉,不恰当的比喻,你想干违反底线的事也有法律在卡着你不是?

可以看看曾经的一道笔试题:

int main()
{
	const char* p1 = "abcdef";
	const char* p2 = "abcdef";

	char arr1[] = "abcdef";
	char arr2[] = "abcdef";

	if (p1 == p2)
		printf("p1==p2\n");
	else
		printf("p1!=p2\n");

	if (arr1 == arr2)
		printf("arr1 == arr2\n");
	else
		printf("arr1 != arr2\n");

	return 0;
}

这里的运行的结果是什么呢?
p1 == p2 arr1 != arr2
为什么呢?
p1和p2是常量字符串,放在只读数据区,也就是只读,不能被修改,既然不能被改,那么显然每必要在内存中分别开辟两块空间去存放这个字符串,那么p1,p2都将指向字符串的起始位置->a,而arr1 arr2是变量,可以被修改,那么他们两就有自己独立的空间,虽然起始字符都是a,但由于空间位置不同,所以地址也不相同。

2. 指针数组

	指针数组是指针还是数组呢?思考这样一个问题,牛奶是牛还是牛奶呢?
	指针数组就是用来存放指针的数组,类型可以是多种,例如int* arr[3]
	可以存放三个数组,数组的每个元素是int*类型的

有什么用呢?观察如下代码:

int main()
{
	int arr1[] = { 1,2,3,4 };
	int arr2[] = { 5,6,7,8 };
	int arr3[] = { 9,8,7,6 };

	int* parr[3] = { arr1,arr2,arr3 };

	return 0;
}

其实这时,我们就已经模拟出了一个二维数组
如何拿到这些元素呢?如下:

int main()
{
	int arr1[] = { 1,2,3,4 };
	int arr2[] = { 5,6,7,8 };
	int arr3[] = { 9,8,7,6 };

	int* parr[3] = { arr1,arr2,arr3 };

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d ", *(*(parr + i) + j));
		}
	}
	return 0;
}

模拟二维数组:parr+i拿到首元素地址(第几行的地址),解引用(parr+i)就拿到了这行的元素,最后解引用+j,就拿到了这行的每个元素。
这时也不难想到:

	*(parr + i) == parr[i];
	*(*(parr + i) + j) == parr[i][j];

3. 数组指针

	数组指针就是指针,是指向数组的指针

分析一个问题,下面哪个是数组指针?

int main()
{
	int* parr1[10];
	int(*parr2)[10];
	return 0;
}

parr1首先与[10]结合,说明parr1是一个数组,然后与 int星 结合,说明数组的每个元素是 int星 类型;
parr2首先与星号结合,说明parr2是一个指针,然后与 [10] 结合,说明parr2指向的是一个数组,最后与int结合,说明数组的每个元素是 int 类型。

这个数组指针该如何使用呢?
需要先讨论以下数组名:
//数组名通常表示的都是首元素的地址
//但有两个例外:
1.sizeof(数组名) ,(注意:这里严格要求格式,如果是sizeof(数组名+0),这就不算。)这里的数组名表示整个数组,计算的是整个数组的大小。2.&数组名,(这里可以测试一下,通过printf(“%p”,数组名), 这里打印出来和首元素地址是一样的,但表示的意义不一样!)这里的数组表示整个数组,所以这里取出来的是整个数组的地址

&数组名 怎么理解呢?
因为数组的首元素地址是从数组的起始位置开始的,而数组的地址也是从数组的起始位置开始,否则怎么得到数组的每一个元素呢,但是请注意,他们的意义却天差地别,看如下代码:

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

	printf("%p\n", arr);
	printf("%p\n", arr + 1);

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

	return 0;
}

观察运行结果:
在这里插入图片描述
可以观察到
数组名+1 跳过4个字节,也就是跳过数组里的一个元素
原因:数组名表示首元素地址,首元素地址是int * 类型,所以+1跳过一个int*类型的大小,故是跳过4个字节
&数组名 + 1 跳过的是整个数组的大小,跳过了40个字节
原因:&数组名表示整个数组的地址,所以+1跳过整个数组大小,也就是40字节

接下来讨论以下如何创建and使用数组指针:
首先可以想到:

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	return 0;
}
//这里数组名表示首元素地址,首元素地址是int*类型,故存放在int*这样类型的变量中

那么如果是&数组名呢?

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int(*p)[10] = &arr;
	return 0;
}
//这是如何写出来的呢?
//首先数组指针就是是用来存放数组的指针
//那么他因该是个指针
//创建一个指针*p
//他因该指向一个数组,所以是(*p)[10],这里由于优先级问题*p要用括号括起来,否则p就会先于[10结合]
//数组每个元素的类型是int,所以最后
//int(*p)[10],这里想必就可以理解了

补充一点:去掉变量名,剩下的就是类型名,比如int(*p)[10],去掉p,剩下的int( * )[10]就是数组指针类型,这个类型也就觉得你+1,-1操作跳过的字节数。

注意不能这样写:

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*p)[] = &arr;
	return 0;
}
//会报警告: warning C4048: “int (*)[0]”和“int (*)[10]”数组的下标不同

如何使用:

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

	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(*p + i));
	}
	return 0;
}
//p表示整个数组的地址,*p表示通过整个数组的地址找到了这个数组
//数组由什么来表示呢?当然是数组名啦~
//所以*p本质上就是首元素的地址,所以通过*p+i就可以遍历数组每个元素的地址

不过以上这种用法大家有没有觉得很怪,实际上,一个正常人都不会这么去写的,数组指针实际上不是这么用的,因为以上我们一般最常写,最舒服,最易理解的写法是以下代码:

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

	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

那么数组指针到底用在哪里合适呢?
至少是二维数组及以上
看以下代码:

//首先是一般的写法
void print1(int arr[3][4], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
	print1(arr, 3, 4);
	return 0;
}
//下面是指针的写法
void print2(int(*parr)[4], int r, int c)//这里传入就只能用数组指针接收
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(parr + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
	print2(arr, 3, 4);//这里传入的arr是首元素地址,在二维数组中也就是数组第一行的地址
	return 0;
}
//注意这里print2函数接收arr时不能用二级指针去接收
//二级指针是用来接收一级指针的地址
//而这里传去的是二维数组第一行的地址

知道了数组指针和指针数组,可以看看下面代码的意思:

int(*parr[10])[5]
//这是什么呢?
//parr先于[10]结合,说明他是一个数组
//那么,去掉数组名和数组元素个数,剩下的int(*)[5]就是数组元素的类型名
//所以parr是一个存放数组指针的数组

最后,可能又有人会问,那这个东西是不是不常用,不重要?
我想说的是,当你将来阅读别人的代码的时候发现有数组指针,这时你就会觉得重要了~

4. 函数指针

	函数指针是一个指针,指向的是函数的地址

上面我们提到了&数组名和数组名的区别,那么函数指针是否也具有相同的性质呢?他们之间可以类比吗?
观察如下代码:

int Sub(int a, int b)
{
	return a - b;
}
int main()
{
	printf("%p\n", Sub);
	printf("%p\n", &Sub);
	return 0;
}

运行结果:
在这里插入图片描述
发现&函数名,和函数名的地址是一样的,那么他们直接有类似数组名的性质吗?
这里要注意,函数名与&函数名的性质和意义毫无差别,都是函数的地址,和数组名与&数组名不能类比。

下面是函数指针的创建and使用:

//创建
int Sub(int x, int y)
{
	return x - y;
}
int main()
{
	int(*pf)(int, int) = &Sub;
	return 0;
}
//使用
int Sub(int x, int y)
{
	return x - y;
}
int main()
{

	int(*pf)(int, int) = &Sub;
	int ret = (*pf)(1, 2);
	printf("%d\n", ret);
	return 0;
}
//做到了间接通过函数指针访问函数
//*pf找到了这个函数
//接下来去调用他(*pf)(1,2);
//实际不写*,这样写也行,如下:
int Sub(int x, int y)
{
	return x - y;
}
int main()
{

	int(*pf)(int, int) = &Sub;
	int ret = pf(1, 2);
	printf("%d\n", ret);
	return 0;
}
//至于为什么要写上*呢?
//其实是便于你去更好的理解指针,知道通过对地址的解引用找到相应的变量,但这里,实际上*只是个摆设。
//这里肯定还会有人觉得不理解,为什么能这样呢?看如下代码:
int Sub(int x, int y)
{
	return x - y;
}
int main()
{

	//int(*pf)(int, int) = &Sub;
	//int(*pf)(int, int) = Sub;
	int ret = Sub(1, 2);
	printf("%d\n", ret);
	return 0;
}
//在不用指针的情况下,我们进行函数调用的时候通常都写成 函数名(实参)
//我们又知道函数名实际上就是函数的地址
//那么我们用的pf不就是函数的地址吗?
//函数的地址难道不就是函数名吗?
//所以sizeof(pf)与sizeof(&pf)是一样的,地址的大小为4/8;
//有没有一种醍醐灌顶的感觉~ 嘻嘻
这里还有一点值得注意的是pf一定要用(*pf),否则,pf会先于后面的(1 , 2)结合,然后函数调用完后返回一个值,
这个值在被 *号解引用,值怎么能被解引用呢,所以一定要注意。

两个有趣的代码,他们分别是什么意思?
以下代码来自于 ——《C陷阱与缺陷》

//代码一
#include<stdio.h>
int main()
{
	( *( void (*)() )0 )();
	return 0;
}
//因该从0看起,这里实际上是对0的地址的一个强制类型转换
//转换成void(*)()类型,也就是无参,返回值是viod的型的函数指针类型
//括号外的*,对函数指针进行解引用,右边又有一个括号表示对此函数进行调用
//以上代码本质上是一次函数调用
//代码二
#include<stdio.h>
int main()
{
	void (*signal(int, void(*)(int)))(int);
	return 0;
}
//先从signal下手,前面有*,后面有(int,void(*)(int))
//首先这里signal不是指针,因为signal会先与后面的()先结合
//即signal是函数名
//这里进行了一次对signal的一次函数声明,为什么不是函数调用呢?因为这里没有实参
//再看外面是void(*)(int)是一个函数指针
//所以,以上代码实际上是一次函数声明
//声明的signal函数的第一个参数是int,第二个参数是函数指针,该函数指针的返回类型的是void,参数是int,signal函数的返回类型是void(*)(int),是一个函数指针,该函数的参数是int,返回类型是void。

5. 函数指针数组

	函数指针数组是一个数组,数组的每个元素是函数指针类型

如何去创造一个函数指针数组呢?

//假设有4个函数,分别是Add,Sub,Mul,Div,参数皆为两个int类型,返回值为int
//那么就有如下代码
#include<stdio.h>
int main()
{
	int(*parr[4])(int, int) = { Add,Sub,Mul,Div };
	return 0;
}
//数组名是parr,parr[4]表示他是一个数组,剩下的就是类型名
//因为数组里面要存放函数指针,故类型为int(*)(int,int);

他有什么用途呢?
接下来,制作一个简易的计算器来说明:

//这里只用函数指针来做
void menu()
{
	printf("*************************\n");
	printf("******1.Add  2.Sub*******\n");
	printf("******3.Mul  4.Div*******\n");
	printf("******0.exit      *******\n");
	printf("*************************\n");
}
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 Cal(int(*pf)(int, int),int x,int y)
{
	printf("请输入:");
	scanf("%d%d", &x, &y);
	printf("%d\n", pf(x, y));
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Cal(Add,x,y);
			break;
		case 2:
			Cal(Sub, x, y);
			break;
		case 3:
			Cal(Mul, x, y);
			break;
		case 4:
			Cal(Div, x, y);
			break;
		default:
			break;
		}
	} while (input);
	return 0;
}

如果用函数指针数组就可以减少代码量,使其更精炼.

//用函数指针数组
void menu()
{
	printf("*************************\n");
	printf("******1.Add  2.Sub*******\n");
	printf("******3.Mul  4.Div*******\n");
	printf("******0.exit      *******\n");
	printf("*************************\n");
}
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 main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int(*pfarr[])(int, int) = { 0,Add,Sub,Mul,Div };
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出程序\n");
			break;
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入数字:");
			scanf("%d%d", &x, &y);
			printf("%d\n",pfarr[input](x, y));
		}//这里通过pfarr[input]找到相应的数组元素,
		//函数指针用法等于函数名用法,所以后面直接用括号调用
	} while (input);
	return 0;
}

这样看是不是要精炼很多呢~

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

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

套娃开始~
长啥样嘞?

//这是函数指针数组
int(*parr[4])(int, int) = { Add,Sub,Mul,Div };
//这是指向函数指针数组的指针
int(*(*pparr)[4])(int, int) = &parr;
//首先pparr与*结合,说明是个指针
//再与[4]结合,说明是个数组
//数组的每个元素是什么类型呢?挖掉中间刚刚分析的部分
//剩下int(*)(int, int),说名数组的每个元素是一个函数指针类型
//指针指向的是两个个参数为int,返回值为int的函数

指向函数指针数组的指针就讲到这里,因为在讲下去,太多啦,就是无限套娃。
为什么这么说呢?
想象一下,指向函数指针数组的指针,实际上是一个指针,那么,是不是可以把这些指针通过数组存起来呢?叫做指向函数指针数组的指针的数组,那么,是不是还可以把这个数组的地址通过只一个指针存起来呢…

7. 回调函数

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

可能细心的小伙伴已经发现啦,在制作简易计算器的时候,通过函数指针的方式用到过回调函数

void menu()
{
	printf("*************************\n");
	printf("******1.Add  2.Sub*******\n");
	printf("******3.Mul  4.Div*******\n");
	printf("******0.exit      *******\n");
	printf("*************************\n");
}
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 Cal(int(*pf)(int, int),int x,int y)
{
	printf("请输入:");
	scanf("%d%d", &x, &y);
	printf("%d\n", pf(x, y));
}
int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Cal(Add,x,y);
			break;
		case 2:
			Cal(Sub, x, y);
			break;
		case 3:
			Cal(Mul, x, y);
			break;
		case 4:
			Cal(Div, x, y);
			break;
		default:
			break;
		}
	} while (input);
	return 0;
}

//这里通过Cal接收函数指针,并在本函数内部调用这个函数指针所指向的函数,这里过程就是回调函数的过程

码字不易~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈亦康

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

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

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

打赏作者

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

抵扣说明:

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

余额充值