【深入理解指针与数组】

大家好,这里是我时隔许久又一良心巨著,主要是介绍了指针和数组的一些深入的理解,特意整理出来一篇博客供我们一起学习,如果文章中有理解不当的地方,还希望朋友们在评论区指出,我们相互学习,共同进步!

一:指针

1.1什么是指针

重新理解变量:定义一个变量,本质是在内存中根据类型来开辟空间。有了空间,就必须具有地址来标识空间,来方便CPU进行寻址。有了空间,就可以把数据保存起来。

指针就是地址!那么地址的本质是什么呢?地址是数据,数据可以被保存在变量空间中。

1.2为什么要有指针

一句话,为了方便CPU寻址的效率。

1.3指针解引用

int main()
{
    
    
	int a = 10;
	int *p = &a;
	
	*p = 20;
	system("pause");
	return 0;
}

*p的完整理解是,取出p的地址,访问该地址指向的内存单元(空间或内容)(其实通过指针变量访问,本质是一种间接寻址的方式)
口诀:对指针解引用,就是指针指向的目标

二:数组

2.1概念

数组是具有相同类型的集合

#define N 10
int	main()
{
    
    
	int a[N] = {
    
     0 };
	system("pause");
	return 0;
}

我们都知道临时变量是在栈区上开辟空间,且地址由高到低,那么数组是如何开辟空间的呢?

int main()
{
    
    
	int a[10] = {
    
     0 };
	for (int i = 0; i < 10; i++)
	{
    
    
		printf("&a[%d]:%p\n", i, &a[i]);
	}
	system("pause");
	return 0;
}

&a[0]:003CFAB8
&a[1]:003CFABC
&a[2]:003CFAC0
&a[3]:003CFAC4
&a[4]:003CFAC8
&a[5]:003CFACC
&a[6]:003CFAD0
&a[7]:003CFAD4
&a[8]:003CFAD8
&a[9]:003CFADC
请按任意键继续. . .

有代码结果我们可以看出,数组是整体申请空间的,然后将地址最低的空间,作a【0】元素,以此类推。

在这里插入图片描述引用自比特课件

2.2理解指针+1

int main()
{
    
    
	char * c = NULL;
	short * s = NULL;
	int * i = NULL;
	double * d = NULL;

	printf("%d\n", c);//0
	printf("%d\n", c + 1);//1

	printf("%d\n", s);//0
	printf("%d\n", s + 1);//2

	printf("%d\n", i);//0
	printf("%d\n", i + 1);//4

	printf("%d\n", d);//0
	printf("%d\n", d + 1);//8
	system("pause");
	return 0;
}

口诀:对指针+1,本质加上其所指类型大小

2.3数组名a作为左值和右值的区别

数组名可以做右值,代表数组首元素的地址。

int main()
{
    
    
	int a[10] = {
    
     0 };
	char *p = a;
	system("pause");
	return 0;
}

在这里插入图片描述

数组名不可以做左值!能够当左值的,必须是有空间可以被修改的,数组名不可以整体使用,只能按照元素为单位使用。

三:指针和数组的关系

没关系

3.1以指针的形式和以数组的形式访问

int main()
{
    
    
	char * str = "hello world";
	char arr[] = "hello world";

	int len = strlen(str);
	for (int i = 0; i < len; i++)
	{
    
    
		printf("%c\t", *(str + i));
		printf("%c\n", str[i]);
	}

	printf("\n");

	for (int i = 0; i < len; i++)
	{
    
    
		printf("%c\t", *(arr + i));
		printf("%c\n", arr[i]);
	}
	system("pause");
	return 0;
}

str指针变量在栈上保存,“hello world”在字符常量区,不可修改。
arr整个数组在栈上保存,可以被修改。

代码中,虽然*(str + i)与*(arr + i)的写法一样,但是寻址方法是完全不一样的

指针和数组指向或表示一块空间的时候,访问方式是可以互通的,既有相似性。

那么C语言为什么要这么设计?

说白了就是方便程序员编程,如果没有将指针和数组元素访问打通,那么在C语言中(面向过程)如果有大量的函数调用和大量数组传参,会要求程序员进行各种各种访问习惯的变化,只要是要求人做的,就会提升代码出错的概率和调试i的难度。

四:指针数组和数组指针

4.1数组名与&数组名的区别

在这里插入图片描述

int main()
{
    
    
	char arr[5] = {
    
     'a', 'b', 'c', 'd', 'e' };

	char(*p1)[3] = &arr;
	char(*p2)[3]= arr;

	system("pause");
	return 0;
}

在这里插入图片描述
结论:数组元素个数,也是数值指针类型的一部分。

4.2地址的强制转换

struct test
{
    
    
	int NUM;
	char * pcName;
	char ch[2];
}*p = (struct test *)0x100000;

int main()
{
    
    
	printf("%p\n", p + 0x1);
	printf("%p\n", (unsigned long)p + 0x1);
	printf("%p\n", (unsigned int *)p + 0x1);
	system("pause");
	return 0;
}
0010000C
00100001
00100004
请按任意键继续. . .

结论:强制类型转换,改变的是对特定内容的看待方式,对数据本身不发生改变。

五:多维数组和多级指针

5.1 二维数组基本内存布局

int main()
{
    
    
	char arr[3][4] = {
    
     0 };
	for (int i = 0; i < 3; i++)
	{
    
    
		for (int j = 0; j < 4; j++)
		{
    
    
			printf("arr[%d][%d] : %p\n", i, j, &arr[i][j]);
		}
	}
	system("pause");
	return 0;
}

arr[0][0] : 012FFAFC
arr[0][1] : 012FFAFD
arr[0][2] : 012FFAFE
arr[0][3] : 012FFAFF
arr[1][0] : 012FFB00
arr[1][1] : 012FFB01
arr[1][2] : 012FFB02
arr[1][3] : 012FFB03
arr[2][0] : 012FFB04
arr[2][1] : 012FFB05
arr[2][2] : 012FFB06
arr[2][3] : 012FFB07
请按任意键继续.

结论:二维数组在内存地址空间排布上,也是连续递且递增的。

5.2二维数组如何画图

只有正确画出二维数组的布局图,才能算真正深刻理解二维数组的空间布局。
以 char【3】【4】 = { 0 };为例
在这里插入图片描述

理解链:数组的定义是既有相同元素类型的集合,特征是数组中可以保存任意类型。那么可以保存数组吗,答案是可以的!
在理解上,我们甚至可以认为所有的数组都可以当作一维数组,就二维数组而言,可以被当作“一维数组”, 只不过内部“元素”也是一维数组。

那么内部一维数组是在内存中布局是“线性连续且递增”的,多个该一维数组构成另一个“一维数组”,那么整体也是线性且连续递增的。这也就解释了为何上述二维数组的地址是连续递增的。

如果愿意的话,我们也可采用如下代码遍历二维数组:

int main()
{
    
    
	char arr[3][4] = {
    
     0 };
	char *p = (char *)arr;
	for (int i = 0; i < 3 * 4; i++)
	{
    
    
		printf("%p\n", p + i);
	}
	system("pause");
	return 0;
}

00FDFCCC
00FDFCCD
00FDFCCE
00FDFCCF
00FDFCD0
00FDFCD1
00FDFCD2
00FDFCD3
00FDFCD4
00FDFCD5
00FDFCD6
00FDFCD7
请按任意键继续. . .

下面我给出一组题目,看朋友们能否明白答案的由来:

int main()
{
    
    
	int arr[3][4] = {
    
     0 };
	printf("%d\n", sizeof(arr));
	printf("%d\n", sizeof(arr[0][0]));
	printf("%d\n", sizeof(arr[0]));
	printf("%d\n", sizeof(arr[0] + 1));
	printf("%d\n", sizeof(*(arr[0] + 1)));
	printf("%d\n", sizeof(arr + 1));
	printf("%d\n", sizeof(*(arr + 1)));
	printf("%d\n", sizeof(&arr[0] + 1));
	printf("%d\n", sizeof(*(&arr[0] + 1)));
	printf("%d\n", sizeof(*arr));
	system("pause");
	return 0;
}

48
4
16
4
4
4
16
4
16
16
请按任意键继续. . .

欢迎评论区讨论

5.3二级指针

同样的,二级指针是变量,变量有地址,地址是数据,数据可以被保存。

int main()
{
    
    
	int a = 10;
	int * p = &a;
	int *pp = &p;

	p = 100;
	*p = 100;
	pp = 100;
	*pp = 100;
	** pp = 100;
	return 0;
}

六:数组参数和指针参数

6.1一维数组传参

数组传参是要发生降维的。

void show(int * arr)
{
    
    
	printf("main: %d\n", sizeof(arr));
}
int main()
{
    
    
	int arr[10];
	printf("main: %d\n", sizeof(arr));

	show(arr);

	system("pause");
	return 0;
}

main: 40
main: 4
请按任意键继续. . .

为什么要降维?

在C语言中,只要函数调用,必定发生拷贝,形参是实参的一份临时拷贝,所以如果不发生降维,那么会导致成本高,效率低。

降维成什么?

降维成指向其内部元素的指针

6.2一级指针传参

发生函数调用,指针作为参数,要不要发生临时拷贝?

void test(char * p)
{
    
    
	printf("test: &p = %p\n", &p);
}
int main()
{
    
    
	char * p = "hello world";
	printf("main: &p = %p\n", &p);
	test(p);

	system("pause");
	return 0;
}

main: &p = 012FFB50
test: &p = 012FFA7C
请按任意键继续. . .

需要! 因为指针变量,也是变量,在传参上,它也必须符合变量的要求,进行临时拷贝.所以上述代码形参与实参地址不一样!

6.3二维数组参数和二级指针参数

二维数组传参也要发生降维
降维成什么?

指向其内部元素的指针

void show(char arr[][4])
{
    
    
	printf("hello world!");
}
int main()
{
    
    
	char arr[3][4] = {
    
     0 };
	printf("main: %d\n", sizeof(arr));

	show(arr);

	system("pause");
	return 0;
}

数组名arr代表首元素地址,此时首元素是一个一维数组,其地址是一个数组指针,须知所有的数组传参,都会发生降维,这里则是降维成一个一级数组指针来传参

思考为什么在形参接收是行标可以省略,而列标不可以呢?

这里就涉及到前面所说的数组指针的类型与元素个数有关,如上述代码在形参列表也可表示成降维成:
void show(char (*arr)[4])
所以列标不可以省

综上:任何维度的数组,在传参是时候都要发生降维,降维成指向其内部元素类型的指针.
那么二维数组内部元素是"一维数组",就应该降维成指向一维数组的指针.
同理三维数组,四维数组一样的道理!

谢谢大家的支持,还希望看到这里的能够给一个三连支持,谢谢大家!

猜你喜欢

转载自blog.csdn.net/qq_43727529/article/details/122501512