C语言学习笔记----4(指针)

文章目录

  • 指针深入介绍
    • 指针类型
      • 二级指针
      • 字符指针
      • 指针数组
      • 数组指针
      • 函数指针
      • 函数指针数组
      • 函数指针数组指针
    • 回调函数
    • 总结

指针深入介绍

众所周知,亚里士多德1撑起了古希腊科学、哲学的半壁江山。

而指针就像是C语言中的亚里士多德一样,撑起了C语言的半壁江山。
C语言学习笔记----4(指针)_第1张图片
不,与其说,指针撑起了C语言的半壁江山,不如说是指针创造了C语言的一片天地。C语言学习笔记----4(指针)_第2张图片

小明:指针不就是存放了一个地址的变量而已嘛?就这?

小明,数学题你都会做了?还是你终于考上大学了?来这凑啥热闹?

C语言学习笔记----4(指针)_第3张图片

指针类型

首先,要登场的是我们的二级指针
C语言学习笔记----4(指针)_第4张图片

二级指针

小明同学在不经意间,已经说出了一个惊天大秘密!!!

指针就是一个变量
只不过是说 指针 这个变量和其他的变量不同,它存放的是某个数据的位置

变量,就意味着指针也是存放在内存中的某个位置,它也有自己的“门牌号”
也就是只要我有钥匙,我就可以光明正大进入指针的小家

C语言学习笔记----4(指针)_第5张图片
em…当然最关键的还是需要 “&” 老大哥来帮忙配个钥匙
ALT

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	//此时ppa变量中存放的内容就是pa指针的地址
	return 0;
}

此时,我们就通过ppa可以进入pa的小家,“光明正大”地实施改造计划。

C语言学习笔记----4(指针)_第6张图片

首先,趁着 pa 出门打酱油,将 a 进行改变

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	**ppa = 100;
	return 0;
}

在这里插入图片描述

小明:就这???

那来个狠的

int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	**ppa = 100;
	int b = 20;
	*ppa = &b;
	return 0;
}

在这里插入图片描述

新创建一个 b 变量,进行操作 **ppa = &b;
此时再使用 pa 得到的就不是 a 变量,而是 b 变量

指针pa:我媳妇呢???

通过观察,可以知道 二级指针的结构int* *p
其中 int* 表示 p 的数据类型是 int* 类型的
剩下的 **“ * ”**则表示 p 是一个指针
合起来就是一个 指向一个int*类型的变量的指针

可以通过观察二级指针的结构,推导出三级指针,四级指针…

总结

  1. 二级指针存放的是一级指针的地址,而一级指针存放的是数据的地址
  2. 二级指针可以通过两次解引用操作来访问数据
  3. 二级指针进行一次解引用操作就可以改变一级指针存放的内容

小明:感觉也就一般般吧

C语言学习笔记----4(指针)_第7张图片

字符指针

小明(不屑):字符指针?

eg

char ch = 'w';
char* pc = &ch;
printf("%c\n", ch);
printf("%c\n", *pc);

C语言学习笔记----4(指针)_第8张图片

小明(不屑地准备滑走)

eg

char* str1 = "Hello World";
char* str2 = "Hello World";
char strArr1[] = "Hello World";
char strArr2[] = "Hello World";

问:
1、此时 str1str2 相等吗?

答:相等C语言学习笔记----4(指针)_第9张图片

2、此时 strArr1strArr2 相等吗?

答:不相等
C语言学习笔记----4(指针)_第10张图片

小明:为什么 str1str2 相等,而 strArr1strArr2 不相等呢?

"Hello World"是一个字符串常量2,将字符串常量赋值给字符指针的时候是将字符串常量的首元素的地址传给了字符指针,因此 str1str2 指向的都是 字符串常量的首元素的地址

strArr1strArr2 是数组,将字符串赋值给字符数组,相当于将字符一个一个复制到了数组中。

小明:那为啥他俩不还是一模一样?为啥电脑告诉我他们不相等?电脑骗人?

注意,计算机是不会骗人的,他们只是严格的执行一条接一条的指令而已。
之所以 strArr1strArr2 不相等是因为他们确实不相等。

C语言学习笔记----4(指针)_第11张图片

咳咳~strArr1strArr2 是两个数组
只要不是同一个数组,那么他们的首元素地址不可能相等

小明(点头):em。。。我明白了

说到数组,有两个孪生兄弟忍不住想来了C语言学习笔记----4(指针)_第12张图片

指针数组

顾名思义,指针数组就是一个存放指针的数组
网恋需谨慎,能见才靠谱

int main()
{
	int* arr[10];
	char** arr2[10];
	return 0;
}

C语言学习笔记----4(指针)_第13张图片
而 char** arr2[10]; 表示一个有10个数据类型是 char** 的元素的数组

数组指针

顾名思义,数组指针就是一个指向数组指针
同样,先来看看数组指针的结构

int main()
{
	int* p1[10];
	int* (p2[10]);
	int (*p3)[10];
	return 0;
}

小明:这都是数组指针???

我:你猜

小明:C语言学习笔记----4(指针)_第14张图片

C语言学习笔记----4(指针)_第15张图片

所以 p1 不是数组指针

C语言学习笔记----4(指针)_第16张图片

所以 p2 也不是数组指针

C语言学习笔记----4(指针)_第17张图片
所以这里只有 p3数组指针
因此,不难发现,数组指针和指针数组的区别

名字 本质 所占空间
数组指针 指针 4byte / 8byte
指针数组 数组 4 * n byte / 8 * n byte(n为数组元素个数)
名字 数据类型
数组指针 int (*) [](指向的数组中的元素是int类型)
指针数组 int*[](数组中的元素都是int*类型)
名字 作用
数组指针 存放一个数组的首元素的地址,表示整个数组的地址
指针数组 存放n个指针,每个指针指向的元素类型相同

数组指针更多的是用来当二维数组3作为函数实参的形参

#include
void print(int (*pa)[3], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < row; i++)
	{
		for(j = 0; j < col; j++)
		{
			printf("%d ", pa[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[2][3] = {{1, 2}, {3, 4}, {5, 6}};
	print(arr, 2, 3);
	return 0;
}

此时 arr 表示的是首元素的地址,同时也表示 第一行的地址 ,也是一个一维数组的地址
类比可得:
arr[1] 表示第二行的地址;
arr[2] 表示第三行的地址;

因此,pa相当于接收了第一行的地址(arr的每行都是 两个int类型 的数据)
而 pa[ i ] [ j ] 和 *( *( pa + i ) + j)是等价的
所以 pa[ i ] [ j ] 可以读取到第 i 行,第 j 列的元素

介绍到这里,是时候来看看指针Plus版本了
C语言学习笔记----4(指针)_第18张图片
问:解释下列代码的含义

int arr1[5];         //1
int *arr2[5];        //2
int (*arr3)[5];      //3
int (*arr4[10])[5];  //4

小明:
1 是数组
2 是数组指针
3 是指针数组
4 …

你这不行啊,小明

C语言学习笔记----4(指针)_第19张图片C语言学习笔记----4(指针)_第20张图片

当括号将 * 和 字符单独括起来,后面加一个[ n ],前面加一个数据类型
那这就是数组指针

小明:所以第四个是数组?

对,但也不全对

C语言学习笔记----4(指针)_第21张图片
那我们不妨将arr[10]拿掉,看看它到底是何方神圣

C语言学习笔记----4(指针)_第22张图片

小明:天哪?!这不是个数组指针吗?

是的,就是数组指针

由此,4到底是什么已经真相大白了。
它就是
一个有 10 个元素的指针数组
每个指针都是指向的一个有 10 个整形元素的数组

答:
1 是一个有 5 个 int 类型的元素的数组
2 是一个有 5 个 int* 类型的元素的数组
3 是一个指向一个有 5 个 int 类型的元素的数组的指针
4 是一个有 10个 int*[ 5 ] 类型的元素的数组

还有两个小伙伴也是迫不及待了

小明:还有???

C语言学习笔记----4(指针)_第23张图片

函数指针

介绍函数指针之前,先看一段代码

#include
void test()
{
	;
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
}

C语言学习笔记----4(指针)_第24张图片

由此可见, 函数也没有那么神秘
也是在栈4中开辟的一块地址
而且函数和数组很相似,名字都是它们的地址

要能够存储地址就必须是一个指针
判断一下,pfun1和pfun2哪个是指针

void (*pfun1)();
void *pfun2();

小明:pfun1
因为有括号,所以 pfun1 先和 * 结合
后面那个括号应该就是表示这是一个函数指针

有点东西啊,小明
不过,还是没完全对

pfun1 和 * 先结合表示这个是一个指针
后面的括号是指向函数的 参数(此时什么都没写,表示传递的参数为空)
void 表示 函数的返回类型

pfun2就是一个函数,它的返回值的类型是void*,同时传递的参数为空

又到了提问的环节
解释下列代码的含义:

(*(void (*)())0)();                      //1
void (* signal(int, void(*)(int)))(int); //2

小明:
C语言学习笔记----4(指针)_第25张图片

不慌,且听我慢慢道来

首先,先一层一层推进
C语言学习笔记----4(指针)_第26张图片

小明:还是不明白

再推进一层

在这里插入图片描述
显然,这是一个函数指针类型

在这里插入图片描述
而这,就是将 0 进行强制类型转换为void(*)()类型
C语言学习笔记----4(指针)_第27张图片
再对 0 进行解引用
C语言学习笔记----4(指针)_第28张图片
末尾的 ( ) 表示函数的参数(此时也是什么都没有传递)

综上,代码1的含义是:
0 强制类型转换为void(*)(),再进行解引用,再传递参数
从而实现了调用在 0 地址处的函数( 0 地址一般无法这样操作)

小明:代码2呢?

同样,括号这么多,先一层一层向内部推进
C语言学习笔记----4(指针)_第29张图片

小明:有点眼熟

再推进一层
C语言学习笔记----4(指针)_第30张图片

小明:这表示的是两个类型,一个int, 一个void(*)(int)

没错
在这里插入图片描述
这就是函数signal的两个参数类型
字符串已经和括号结合,表示这是一个函数的声明
那么剩下的就是这个函数的返回类型了
在这里插入图片描述
所以,这个函数的返回值类型就是 void(*)(int)

综上所述,
代码2表示的含义是 函数signal 的声明
其中,这个 signal 的参数一个是 int, 另一个是 void(*)(int)
signal 的返回值的类型也是 void(*)(int)

这两个代码均在《C陷阱和缺陷》中提及

代码2的结构太复杂,可以利用 typedef 来简化

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

小明:如果,我需要使用多个函数,而他们的参数,返回值都是一样的,我可以用什么来进行简化呢?

接下来就是函数指针数组来大放光彩了

函数指针数组

函数指针数组和一般的指针数组一样
只不过
函数指针数组中的指针指向的都是函数

在见到函数指针数组之前,先看看正常情况下计算器的实现代码

void menu()
{
	printf("******************************\n");
	printf("0.exit\n");
	printf("1.Add\n");
	printf("2.Subtract\n");
	printf("3.Multiply\n");
	printf("4.Divide\n");
	printf("******************************\n");
}

int Add(int x, int y)
{
	return x + y;
}

int Subtract(int x, int y)
{
	return x - y;
}

int Multiply(int x, int y)
{
	return x * y;
}

int Divide(int x, int y)
{
	return x / y;
}

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	menu();
	do
	{
		scanf("%d", &input);
		switch (input)
		{
		case 0: 
			break;
		case 1: 
			scanf("%d %d", &x, &y);
			printf("%d\n", Add(x, y));
			break;
		case 2:
			scanf("%d %d", &x, &y);
			printf("%d\n", Subtract(x ,y));
			break;
		case 3:
			scanf("%d %d", &x, &y);
			printf("%d\n", Multiply(x, y));
			break;
		case 4:
			scanf("%d %d", &x, &y);
			printf("%d\n", Divide(x, y));
			break;
		default:
			break;
		}

	} while(input);
	return 0;
}

而现在对其进行改造

int main()
{
	int input = 0;
	int(*arr[5])(int, int) = { 0 , Add, Subtract, Multiply, Divide };
	int x = 0;
	int y = 0;
	do
	{
		menu();
		scanf("%d", &input);
		if (input > 0 && input < 5)
		{
			scanf("%d %d", &x, &y);
			printf("%d\n", (*arr[input])(x, y));
		}

	} while (input);
	return 0;
}

其中 int(*arr[5])(int, int) = { 0 , Add, Subtract, Multiply, Divide }; 就是一个函数指针数组

既然有函数指针数组,那么函数指针数组指针自然也是少不了的
C语言学习笔记----4(指针)_第31张图片

函数指针数组指针

将函数指针去掉,就是之前介绍过的数组指针
所以,通过观察数组指针
可以得到
函数指针数组指针
是一个指向一个函数指针数组的指针,里面存放的是数组的地址

函数指针数组指针虽然名字看起来很复杂,但是他的结构也一点不简单

小明:
C语言学习笔记----4(指针)_第32张图片

首先,写出数组中的元素类型

int(*)(int, int)

其次,将指针和数组添加上去

int(*(*parr)[5])(int, int) = arr;

回调函数

简介:回调函数就是一个通过函数指针调用的函数。
也就是说,将A函数的指针,作为参数传递给B函数
当A函数的指针被用来调用它所指向的A函数时,称此为回调函数

qsort就有回调函数的使用
在使用qsort的时候就需要传递一个函数指针,用来自定义如何排序
下面来简单介绍一下 qsort

在这里插入图片描述

qsort 的头文件是
qsort 的参数含义是数组名,元素个数,元素所占字节大小,比较方法的函数

#include
int com(void* q1, void* q2)
{
	return *(void*)q1 - *(void*)q2;
}
int main()
{
	int arr[10] = { 1,3,2,4,6,8,7,10,9,5 };
	qsort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(arr), com);
	return 0;
}

我们自己自定义的函数返回值的类型一定要是int,同时要有两个void*类型的指针作为参数
com 的返回值是小于等于 0 的时候,qosrt不会对数组做出改变
而当 com 的返回值是大于 0 的时候,qsort对数组进行排序
在这里插入图片描述
比较方法的函数可以根据需要排序的数组中的元素类型来灵活调整

总结

以上,便是C中部分的指针的介绍。
C的指针的奥妙绝不是我这三言两语能够说的清楚的,它在我看来,指针就是C中最牛逼的部分,不接受反驳C语言学习笔记----4(指针)_第33张图片

注释:


  1. 亚里士多德(Aristotle公元前384~前322),古代先哲,古希腊人,世界古代史上伟大的哲学家、科学家和教育家之一,堪称希腊哲学的集大成者。他是柏拉图的学生,亚历山大的老师。 ↩︎

  2. 字符串常量是一对双引号括起来的字符序列。 字符常量可以赋值给字符变量,如"char b=‘a’;",但不能把一个字符串常量赋给一个字符变量,同时也不能对字符串常量赋值! ↩︎

  3. 因为二维数组在内存中的使用是连续的,因此,二维数组的本质就是一维数组。只不过,二维数组相当于是多个一维数组叠加在一起 ↩︎

  4. 关于栈的内容,可以移步到这篇文章中 ↩︎

你可能感兴趣的:(c语言,指针)