(c语言)-- 深度剖析指针和数组(上)

文章目录

一、前言 二、指针 (一)何为指针? (二)为什么要有指针? (三)指针的内存布局 (四)指针解引用 (五)指针运算 1、指针+-整数 2、指针-指针 3、指针的关系运算 (六)野指针 1、野指针的成因 2、如何规避野指针 三、数组 (一)数组的概念 (二)一维数组 1、一维数组的创建和初始化 2、一维数组的使用 3、一维数组的内存布局 4、理解&a[0]和&a的区别 5、数组名a做为左值和右值的区别 (三)二维数组 1、二维数组的创建和初始化 2、二维数组的使用 3、二维数组的内存布局 4、理解&a[0]和&a的区别 5、数组名a做为左值和右值的区别 四、指针与数组的关系 (一)以指针的形式访问和以数组的形式访问

一、前言

  本篇文章3万5千字+,也是本人撰写博客的第二篇。
  这篇文章主要针对指针和数组的深入理解,对有简单了解过指针和数组的读者阅读起来体验效果更好。虽不敢说全面阐述了指针和数组,但是也是比较详细的阐述了指针和数组一些相关的底层性问题,这些问题可能是我们平时在学习c语言这块内容时不容易关注到的,我都较详细的叙述出来,希望各位读者能够耐心读完。
  我也希望通过这篇文章,会让读者对指针和数组产生更深一层的体验,不仅仅是停留在简单指针的定义和使用,数组的定义和使用层次上,会让读者您对指针和数组的内存布局,一些概念的深入理解的一个讲解
  最后,还请您能够多支持,多评论,互关互赞。谢谢!

二、指针

(一)何为指针?

带着几个问题来了解何为指针?

有没有指针变量这一概念? 指针和指针变量是一个概念吗? 指针和指针变量又有何不同?我们口语中的"定义一个指针"究竟是什么意思?我们该如何理解这种说法?

  针对上述问题,我们先从究竟如何理解编址开始讲起!
  首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据通信传递。
  但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。
  在c语言中,我们程序的执行是先将代码加载进内存中,然后通过CPU的读取和写入来实现程序的功能。在这过程中,CPU和内存之间是需要进行大量数据的交互,所以,两者必须也用线连起来。
如下图所示:
在这里插入图片描述
  不过,我们今天关心一组线,叫做地址总线。
  CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址。(就如同宿舍很多,需要给宿舍编号一样)
  计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
  就像吉他,它并没有花上任何空间来标识”do、re、mi、fa、so“这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识!
  硬件编址也是如此。我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。
  地址信息被下达给内存,在内存内部,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器,实现数据的交互。
因此,内存当中的编址并不需要开辟空间为其存储,而是通过硬件电路的方式对内存进行编制的,也就是说,我们内存的那些地址值,高地址到低地址的排序等这些都是硬件中规定好的,我们只需要遵守它的规定,正确使用规定就好。而我们所说的地址,又称之为"指针"。

注意:指针就是地址!
地址的本质是数据,且地址具有指向性的作用。
因此,指针又可以理解成“具有指向性的数字”。
在这里插入图片描述
  在VS2017集成开发环境中,通过按下键盘F10进入程序调试阶段,打开内存,就可看到地址数据,从内存中可看到,地址(内存单元)从上往下依次递增;地址的字节数为4个字节。
在liunx平台下,地址(内存单元)从下往上依次递减。

  指针的本质是数据,而数据可以被保存在变量空间里面。因此,保存指针(地址)数据的变量就称之为指针变量

  严格意义上来讲,指针和指针变量是两个不同的概念。
  指针就是地址值,而指针变量是c语言规定变量形式的变量,即要在特定区域(栈)开辟空间。该变量可以用来保存地址数据,即保存指针,还可以被用来取地址。
  但是,我们经常在口语化表达的时候,又常将这两个概念混合,并且在一些书籍中,指针和指针变量不进行区别的,这是为什么?
  要解答这个问题,我们先来看一段代码:
在这里插入图片描述
如何看待上面这段代码中a的变量?两者的a一样吗?

答案是不一样。

  我们知道,定义一个变量,本质就是在栈中根据类型开辟空间。对于开辟的这个空间,我们现在从不同的两个层面来理解它。
  第一,有了空间,就必须具有地址来标识这块空间,以方便CPU进行寻址;
  第二,有了空间,就可以把数据保存进来。
  所以,我们先讨论变量的空间和内容两个概念。
  第一个a做为变量来存放数据10,此时a做为空间来使用,代表变量属性;一般情况下,把使用空间的概念称之为左值
  第二个a做为数据保存进变量b中,此时的a取变量的内容来使用,代表数据属性;一般情况下,把使用内容的概念称之为右值

结论:任何一个变量名,在不同的场合中,代表不同的含义!

    作为变量属性时,表示空间(可以存放数据的),一般格式表示为在赋值符号“=”的左边,称左值; 作为数据属性时,表示内容,格式一般表示为在赋值符号“=”的右边,称右值。

在这里插入图片描述
  再来看这段代码,两个p的含义一样吗?
  显示,经过上面的探讨,这里的p使我们意识到,指针变量p既可以充当左值,又可以充当右值;一旦充当右值,就取变量里面的内容来使用,表示地址,而地址就是指针,故容易混淆。

结论:指针就是地址,指针变量是用来存放指针的,指针是唯一标识一块地址空间的。

(二)为什么要有指针?

结论:为了提高CPU在内存中寻址的效率。

举一个例子来理解上述结论。
  假设有一所学校,该学校宿舍楼为 5层,每层20间,每间住8人。其中有一栋宿舍楼年久失修。假设我是一个包工头,负责了这栋宿舍楼的重新装修工作,经过两个月的动工,完成了装修工作。但是,由于我的疏忽,没有给这栋宿舍楼没有装上楼牌号,每间宿舍没有装上门牌号。
  到了第二天,我带着张三和李四来到这栋宿舍楼下,这时我蒙上两人的眼睛并分别带到不同楼层的两间宿舍,然后让张三带电话给李四,让李四过来找他。这时候的李四因为这栋宿舍楼没有楼牌号和门牌号,所以只能通过一间一间的找,费时又费力。
  接下来,我立即装好楼牌号和门牌号,又进行同样的实验,这个时候的李四通过楼牌号和门牌号找到了张三。
  故事到此结束。提问:为什么现实生活中要给我们的楼层装上楼牌号和门牌号,装上的意义何在?
  主要是为了提高查找效率!!!!一间一间查找相当于以遍历的方式去找,很费时间,效率又低。

  刚举的例子中,提到了宿舍楼,每间宿舍以及每间宿舍的床位数。类比一下,宿舍楼相当于计算中的内存访问内存的基本单元是字节,而一个字节,由8个比特位组成,所以一个字节相当于一间宿舍,宿舍中的每一张床位相当于一个比特位。
我作为包工头叫李四去宿舍楼找到张三的具体位置,需要这栋宿舍楼的楼牌号和门牌号;对应的,CPU要在内存中寻址时,如果没有给内存单元进行标记,那么CPU要找到内存单元位置,效率是很低的。因此,为了提高效率,我们给内存单元进行了编号,这些编号被称为内存单元的地址,地址就叫做指针。

注意几个问题:
1、 什么是内存?
  内存就是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存上进行的。所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为内存单元的地址。

2、 内存如何编址?
  经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。
  对于32位的机器,我们简单认为有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或者0),那么32根地址线产生的地址就会是:
在这里插入图片描述
  这里就是2^32次个地址。
  每个地址标识一个字节(内存单元),那么我们就可以给出(2^32 Byte <–> 2^32 / 1024 KB <–>2^32 / 1024 / 1024 MB <–> 2^32 / 1024 / 1024 / 1024 GB <–> 4GB)4G的空间进行编址。
  同样的方法,64位机器,如果给出64根地址线,这里就有2^64次个地址。
  每个地址标识一个字节(内存单元),那么我们就可以给出(2^64 Byte <–> 2^64 / 1024 KB <–>2^64 / 1024 / 1024 MB <–> 2^64 / 1024 / 1024 / 1024 GB <–> 8GB)8G的空间进行编址。
在这里插入图片描述
  在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应该是4个字节。
  那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

3、内存的最小单元是多大?
  根据前面的分析,可知内存的最小单元为一个字节。

结论:指针的大小在32位平台是4个字节,在64位平台是8个字节。
补充一个额外知识:我们一般在挑选电脑的时候,会看重电脑的一个性能指标,就是电脑的内存,一般有4G或者8G;内存越大,对电脑本身有什么好处?大多数人都只知道选越大内存越好,就是不知道为什么。我们电脑要运行各种软件(程序),首先要先把程序加载进内存中才能运行,此时电脑的内存越大,你同一时间开启的软件数量就可以更多,从而不会觉得卡顿。

(三)指针的内存布局

如何画出正确的指针指向图?
在这里插入图片描述

1、这里定义了几个变量?在哪里定义的?

上述代码,定义了两个变量,分别是整型变量a和整形指针变量p;在栈上定义的。
我们知道,所谓的定义变量,其本质就是在内存中开辟空间,开辟大小为变量类型的字节数;内存的分布的情况在前面有详谈过,就是将内存分割为2^32块内存单元,每个内存单元占1个字节;CPU访问内存时需要依靠内存单元编址好后的地址进行访问;整型变量和指针变量占4个字节,也就是定义整型变量时,需要在内存中开辟4个字节数的空间,定义整型指针变量时也是在内存中开辟4个字节数的空间。

2、一个整形,有4个字节,那么应该有4个地址!那么&a取了哪一个地址?那么如何全部访问这4个字节呢?
在这里插入图片描述
调试后可知,&a的地址值是变量a在内存中开辟4个字节数的最低地址(起始地址)。如何访问这4个字节,是通过找到该字节的起始地址,然后根据类型连续访问4个字节。
结论:对变量取地址,不用考虑该变量的类型,该地址是变量在内存中开辟众多字节数的最低地址。

3、如何正确的画出指针指向图
在这里插入图片描述
结论: 指针永远指向变量在内存中开辟众多字节数的最低地址。

(四)指针解引用

#include <stdio.h>
#include <windows.h>

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

上述两种解引用,该怎么理解?

  解引用一句话概括就是把指针变量里面的内容所代表的地址所对应的变量a给拿出来,即*p最终访问的就是变量a,而此时第一个*p在表达式的右侧,取得是*p的右值,也就是变量a的右值,*p就等价于10;第二个*p解引用,访问p变量,找到p变量里面对应的内容,即变量a的地址,该地址指向a,接着访问变量a,访问的是*p指向的目标所对应的空间,代表的是左值。

*p完整理解是,取出p中的地址,访问该地址指向的内存单元。(空间或者内容)(其实通过指针变量访问,本质是一种间接寻址的方式)
*结论:在同类型情况下,对指针解引用,代表指针所指向的目标。

我们再来深入理解一下:

int a = 10;
int *p = &a;

  *是一个操作符,那么*p本质上可以理解成表达式;当我们进行解引用(*p)的时候,使用的是p的左值还是右值?
在这里插入图片描述
  将整型数字0强制类型转换成double指针,此时0当作指针看待,对其解引用表示访问0x00000000地址,数字10.0写入0x00000000地址发生访问冲突。
在这里插入图片描述
  结合上述代码可知,最后编译结束后看到的都是“写入位置0x00000000时发生访问冲突”,第一种是直接访问0x00000000地址,将10.0写入0x00000000地址,最终因地址解引用指向的目标找不到,直接报错;第二种指针变量里面保存的是0x00000000;解引用时也是将10.0写0x00000000地址发生访问冲突,也就是两种情况是等价的,即对指针变量p解引用,使用的是指针变量p里面的内容,即右值

注意:int *p = NULL和*p = NULL的区别

//指针变量p和解引用p(*p)两者的含义有何不同?
int main(){
    int a = 10;
	int *p = &a;
	*p = NULL;
	system("pause");
	return 0;
}

我们先来分析这条代码:

	int *p = NULL;

  我们通过VS2017集成开发环境的调试功能可以看到p的值为0x00000000。这行代码的意思是:定义一个指针变量p,其指向的内存里面保存的是int类型的数据;在定义变量p的同时把p的值设置为0x00000000,而不是把*p的值设置为0x00000000。这个过程叫初始化,是在编译的时候进行的。
  明白了什么是初始化之后,再看下面的代码:

	int a = 10;
	int *p = &a;
	*p = NULL;

  同样,我们在VS2017集成开发环境上调试这三行代码。
  第一行:定义一个变量a并初始化,值为10;
  第二行:定义一个指针变量p,其指向的内存里面保存的是int类型的数据;在定义变量p的同时把p的值设置为a的地址;
  第三行:给*p赋值为NULL,即给p指向的内存赋值为NULL;而p本身的值,即保存变量a的地址并没有改变。

#define NULL (void*)0

注意:NULL是一个宏,被宏定义为0

//最后,我们看看这份有意思的代码
#include<stdio.h>
#include <windows.h>
int main()
{
int *p = NULL;
p = (int*)&p;//这里的强制类型转换主要是确定左右两边的类型一致,
             //字节一样,不代表类型一致;
             //强制类型转换的本质是改变数据的看待方式,其结构内容是完全一致的。
*p = 10;
p = 20;
system("pause");
return 0;
}

  如何理解上述代码?上述代码的本质是让我们深刻的不断强化指针的指向和指针本身两个概念。

  第一行:定义一个指针变量p,其指向的内存保存的是int类型的数据,在定义变量p的同时把p的值设置为NULL(0x00000000);
在这里插入图片描述
  第二行:p是一个变量,既然是变量,就得在内存中开辟空间,只要有空间,那么p就一定有地址,其地址值就是&p,其类型为二级指针int**,通过强制类型转换成一级指针int*,保存进指针变量p中,其不再指向NULL,而是指向&p;换句话说,就是定义一个指针,自己指向自己;
在这里插入图片描述
  第三行:对p解引用,包含两层意思,第一种理解,解引用的操作本质是访问p的右值,相当于p的内容,而p的内容放的就是p的地址,换言之,就是把10直接放到变量p里面;第二种理解,对指针解引用,代表指针所指向的目标,当前的p指向的就是变量p(即自身),所以解引用后就是变量p;
在这里插入图片描述
  第四行:p是一个单独的变量,此时它有左值和右值,这里充当左值,相当于把20放到变量p的空间里面,此时表示指针变量p指向0x00000014地址对应的内存空间。
在这里插入图片描述

(五)指针运算

1、指针±整数

讲解指针±整数运算前,我们先来谈谈指针变量类型?
我们知道,只要是变量,就会有对应的类型;

int main() {
	char a = 0;          //字符型
	short b = 0;         //短整型 
	int c = 0;           //整型
	long d = 0;          //长整型
	float e = 0.0;       //单精度浮点型
	double f = 0.0;      //双精度浮点型
	system("pause");
	return 0;
}

  那么指针变量也是变量,也得有对应得类型,怎样定义指针变量相对应的类型?
  指针变量的类型主要取决于该指针变量里面保存的指针对其解引用指向的目标是什么类型,该指针变量就是那种类型。

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

  定义整型变量a,取出变量a的地址,将其存放进指针变量p中,由于该指针变量p存放的地址解引用后是一个整型变量,故指针变量p的类型为整型,叫整型指针变量

int main() {
	char *pc = NULL;
	short *ps = NULL;
	int *pi = NULL;
	long *pl = NULL;
	float *pf = NULL;
	double *pd = NULL;
	system("pause");
	return 0;
}

  这里可以看到,指针变量的定义方式是: type + * 。 其实: char* 类型的指针变量是为了存放 char 类型变量的地址。 short* 类型的指针变量是为了存放 short 类型变量的地址。 int* 类型的指针变量是为了存放int 类型变量的地址。
  那指针类型的意义是什么?
在这里插入图片描述
  上述代码中,我们取出整型变量a的地址存放进整型指针变量pa中,打印整型变量a的地址和整形指针变量pa的内容,可看到两者的内容是一致的,都是整型变量a的地址,为0x00D3F95C,这时将整型指针变量(做为右值,取其内容)+1操作并打印,打印的内容为0x00D3F960,变成了在原来的内容基础上加4;
  我们取出整型变量a的地址,为整型指针,将其强制转换成字符指针,并存放进字符指针变量pc中,此时打印字符指针变量pc的内容为0x00D3F95C,这时将字符指针变量(做为右值,取其内容)+1操作并打印,打印的内容为0x00D3F95D,变成了在原来的内容基础上加1。
  我们发现,整型指针变量pa和字符指针变量pc保存的内容都是变量a取地址后的整型指针0x00D3F95C,然后分别对两个指针变量(做为右值。取其内容)+1操作,得到了不一样的结果?原来,我们对指针变量进行+1操作时,其本质就是将指针变量保存的指针往高地址进行移动,移动的距离(步长)取决于该指针变量是什么类型,如果该指针变量是整型指针变量,+1操作时,往高地址移动整型个字节数,即移动4个字节。
同理,我们对指针变量进行-1操作时,其本质就是将指针变量保存的指针往低地址进行移动,移动的距离(步长)取决于该指针变量是什么类型,如果该指针变量是整型指针变量,-1操作时,往低地址移动整型个字节数,即移动4个字节。

总结:指针变量的类型决定了指针变量(做为右值,取其内容)±n(整数)时,向低地址或者向高地址走n步有多大(距离)。

//理解以下程序
int main()
{
#define N_VALUES 5		//定义宏,宏的本质是在编译期间进行文本替换;
	float values[N_VALUES] = {0.0};		//定义单精度浮点型数组,元素个数为5;
	float *pl = NULL;		//定义单精度浮点型指针变量;
	//指针+-整数;指针的关系运算
	for (pl = &values[0]; pl < &values[N_VALUES];)
	{
		*pl++ = 0;
	}
	system("pause");
	return  0;
}

上述代码是通过指针的形式对其数组进行初始化;
在这里插入图片描述

2、指针-指针

结论:存在两个指针指向同一数组,两指针进行相减操作,结果为两指针包含的数组元素的个数。
在这里插入图片描述
在这里插入图片描述
注意:低指针 - 高指针,结果是一个负值;高地址 - 低地址,结果为一个正值。

int main()
{
	//库函数strlen()求解数组的长度。
	char arr[] = "this is a array";
	int len = strlen(arr);
	printf("%d\n",len);//15
	system("pause");
	return  0;
}

自定义求解数组长度的函数。

#include <stdio.h>
#include <windows.h>

int my_strlen(char *s)
{
	char *p = s;
	while (*p != '\0')
		p++;
	return p - s;
}
int main()
{
	char arr[] = "this is a array";
	int len = my_strlen(arr);
	printf("%d\n",len);
	system("pause");
	return  0;
}

3、指针的关系运算

标准规定:
  允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

(六)野指针

1、野指针的成因

  所谓的野指针,就是指针指向的位置是不可知的。(随机的、不正确的、没有明确限制的)

野指针的成因有以下三种:

//指针未初始化
#include <stdio.h>
#include <windows.h>

int main(){
	int *p;		//局部变量指针未初始化,默认为随机值
	//int *p = NULL;
	*p = 20;
	system("pause");
	return 0;
}

在这里插入图片描述

//指针越界访问
#include <stdio.h>
#include <windows.h>

int main(){
	int arr[10] = {0};
	int *p = arr;
	int i = 0;
	for(; i <= 10; i++){
		*p++ = i;	//当指针指向的范围超出数组arr的范围时,p就是野指针
	}
	system("pause");
	return 0;
}
#include <stdio.h>
#include <windows.h>

int main()
{
	int *ptr = NULL;
	ptr = (int*)malloc(10 * sizeof(int));	//动态开辟40个字节的内存空间;
	if (NULL != ptr)	//判断ptr指针是否为空;
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(ptr + i) = 0;
		}
	}
	free(ptr);	//释放ptr所指向的动态内存
	//ptr = NULL;	//将指针置为空指针;
	return 0;
}

2、如何规避野指针

 野指针的出现,会使我们我的程序发生错误,规避野指针成为我们不可忽略的问题。规避野指针的方式有以下几点:
1、指针初始化;
 定义指针变量时,如果没有给其附上对应的地址,应该先给指针变量附上空指针NULL。
2、小心指针越界;
3、指针指向的空间释放后立即置为NULL;
 动态开辟内存空间,使用完毕释放空间后,要将指针置为空指针NULL,否则,这个指针就变成了野指针。
4、指针使用前检查有效性。
 使用函数时,如果传入的参数有指针时,应该要在函数的语句块开头检查指针的有效性,如果指针为一个空指针,应当立即退出函数。

三、数组

(一)数组的概念

数组:相同类型的元素的集合。
在这里插入图片描述

代码格式:type array_name[const] = {date,date,date,… };

type 是指数组的元素类型; const 是一个常量表达式,用来指定数组的大小;这个const可以是字面值,也可以是宏定义的常量; date:是数组保存的元素。

含义:数组名与数组下标引用操作符[ ]结合,我们表示为数组,即“数组名[ ]”,在数组名前面加类型,类型取决数组中保存的元素的类型,数组下标引用操作符[ ]中添加常数表达式,表示数组包含元素的个数。

  我们口头话如何描述一个数组,例如:int array[10] = {0};
我们可以这样说,array是一个数组,该数组有10个元素,每个元素的类型是int,初始值都为0。

(二)一维数组

1、一维数组的创建和初始化

一维数组的创建
  根据数组元素个数的表示方式不同,数组的创建,有以下两个格式:

//格式一,数组常量表达式直接赋字面值:
char array1[10];//创建一个数组,里面包含10个char类型的元素;
short array2[10]; //创建一个数组,里面包含10个short类型的元素;
int array3[10]; //创建一个数组,里面包含10个int类型的元素;
long array4[10]; //创建一个数组,里面包含10个long类型的元素;
float array[10]; //创建一个数组,里面包含10个float类型的元素;
double array[10]; //创建一个数组,里面包含10个double类型的元素;
//格式二,数组常量表达式用宏定义:
#define CONST 10
char array1[ CONST];//创建一个数组,里面包含10个char类型的元素;
short array2[ CONST]; //创建一个数组,里面包含10个short类型的元素;
int array3[ CONST]; //创建一个数组,里面包含10个int类型的元素;
long array4[ CONST]; //创建一个数组,里面包含10个long类型的元素;
float array[ CONST]; //创建一个数组,里面包含10个float类型的元素;
double array[ CONST]; //创建一个数组,里面包含10个double类型的元素;

  上述两种方式都是创建一个包含10个元素的数组;但是,用宏定义作为常量表达式来创建数组较格式一灵活且实用;特别是在大型项目中,创建数组用格式二在后期维护起来特别方便。

注意:

    数组创建,[ ]中要给一个常量才可以,不能使用变量。 数组可创建包含任意类型的元素,以上创建的数组的元素类型都是基本数据类型,数组还可以创建元素类型为指针,结构体,数组…等。

一维数组的初始化

一维数组的初始化:在创建数组的同时给数组的内容一些合理初始值(初始化)。

//我们以上面创建数组的一种格式来对其数组进行初始化,代码如下:
short array2[10] = {0,1,2,3,4,5,6,7,8,9}; //创建一个数组,里面包含10个short类型的元素;
int array3[10] = {0,1,2,3,4,5,6,7,8,9}; //创建一个数组,里面包含10个int类型的元素;
long array4[10] = {0,1,2,3,4,5,6,7,8,9}; //创建一个数组,里面包含10个long类型的元素;
float array[10] = {0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0}; //创建一个数组,里面包含10个float类型的元素;
double array[10] = {0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0}; ; //创建一个数组,里面包含10个double类型的元素;

  给数组进行初始化,就是根据数组的类型和常量表达式的大小,在赋号"="右边,在花括号{ }里面附上相对应的值,值与值之间用逗号隔开。
  这里有几个小细节需要注意:

  第一,在函数中创建数组的同时必须进行初始化,不能只创建而不初始化。因为在函数中,任何一个变量的定义不进行初始化,系统会默认附上一个随机值,并且在VS运行环境编译下发出警告!
在这里插入图片描述

  第二,数组创建并初始化时,可以不完全初始化,在花括号里面附上一个0即可;但是此种情况要求创建含有不少于1个元素的数组时,常量表达式不能省略;最终创建出一个包含CONST个元素,并且每个元素的初始值为0的数组。代码如下:

#include <stdio.h>
#include <windows.h>
define CONST 10		//宏定义一维数组的元素个数;
int main(){
	char array[CONST] = {0}; //创建数组时不完全初始化;
	system("pause");
	return 0;
}

在这里插入图片描述

  第三,数组创建时,如果省略常量表达式,那么就得进行完全初始化。数组的元素个数根据初始化的内容来确定。代码如下:

#include <stdio.h>
#include <windows.h>

int main(){
	int array[] = {0,1,2,3,4,5,6,7,8,9};//创建了包含10个元素的数组;
	system("pause");
	return 0;
}

在这里插入图片描述
注意:但是对于下面的代码要区分,内存中如何分配。

#include <stdio.h>
#include <windows.h>
//对数组进行以下两种形式的初始化,数组分别包含了多少个元素?
int main(){
	char arr1[] = "abc";
	char arr2[3] = {'a','b','c'};
	system("psuse");
	return 0;
}

  对于数组arr2,我们知道,arr2没有省略常量表达式,故数组在内存中开辟空间的数量(元素的个数)由常量表达式决定,因此该数组包含了3个元素;
在这里插入图片描述
  对于数组arr1,由于数组省略常量表达式,故数组在内存中开辟空间的数量(元素的个数)由初始化的内容决定。初始化的内容是一个字符串,而字符串是保存在字符串常量池中,只由操作系统保护,真正意义上不可被修改的,以’\0’作为结束标志,在内存空间中需要开辟空间来保存’\0’字符,因此,该数组包含4个元素。
在这里插入图片描述

2、一维数组的使用

  我们创建好了一个数组,接下来就是如何去使用它;数组里面包含多个元素,使用它就是如何获取到数组里面的每个元素的值。
对于数组的使用我们引用一个操作符:[ ],数组下标引用操作符。它其实就是数组访问的操作符。代码如下:

#include <stdio.h>
#include <windows.h>

int main()
{
	int arr[10] = {0};	//数组的不完全初始化 
	int sz = sizeof(arr)/sizeof(arr[0]);	//计算数组的元素个数;
	//对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
	int i = 0;//做下标;
	for(; i < sz; i++)
	{
		arr[i] = i;
	}
	//遍历数组的内容;
	for(; i < sz; ++i)
	{
		printf("%d ", arr[i]);
	}
	system("pause");
	return 0;
}

注意:

    数组中元素的访问格式:数组名[ 下标 ]。其中,数组的下标从0开始+1递增; 数组中每个元素都有属于自己对应的下标,首元素对应的数组下标为0,然后依次递增,直到最后一个元素。 通过for循环的方式依次打印出数组中每个元素的值,我们称这种方式为数组的遍历。

总结:

    数组是使用下标来访问的,下标是从0开始。 数组的大小可以通过计算得到。

3、一维数组的内存布局

//我们先来看以下代码:
#include <stdio.h>
#include <windows.h>

int main(){
	int a = 10;
	int b = 20;
	int c = 30;
	printf("%p\n", &a);
	printf("%p\n", &b);
	printf("%p\n", &c);
	system("pause");
	return 0;
}

在这里插入图片描述
  从运行结果,我们发现,先定义的变量,地址是比较大的,后续依次减小,这是为什么呢?
  因为a,b,c都在main函数中定义,也就是在栈上开辟的临时变量。而a先定义意味着,a先开辟空间,那么a就先入栈,所以a的地址最高;接着b开辟空间,那么b就后入栈,所以b的地址较a的地址小;接着c开辟空间,那么c就入栈。地址也就相对a和b都小。
在这里插入图片描述

//接下来,我们看以下代码
#include<stdio.h>
#include <windows.h>

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

在这里插入图片描述
  上述代码中,创建的数组由10个元素构成。其中,arr是在main函数中定义的数组,所以arr中的10个元素也是在栈上开辟空间的。我们发现,数组的地址排布是:&a[0] < &a[1] < &a[2] < … < &a[9];如果同上述代码相比,那么肯定是a[0]先被开辟空间,即a[0]先入栈,那么肯定&a[0]地址最大啊,可是事实上并非如此!我们从运行结果看到,往后元素依次入栈,对应的地址依次增大,与我们前面的结论正好相反,这是为什么?
  因为数组排布具有线性、连续、递增!,在开辟空间的时候,不应该将数组认为成一个个独立的元素,应该整体给数组开辟开辟空间,整体被释放。
  数组是整体申请空间的,然后将数组当中地址最低的空间,作为a[0]元素,依次类推!
在这里插入图片描述

注意:任何一个c语言当中的变量取地址时,取出来的地址值一定是此变量所开辟的众多字节当中的最小地址。

4、理解&a[0]和&a的区别

#include<stdio.h>
#include <windows.h>

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

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

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

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

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

	system("pause");
	return 0;
}

在这里插入图片描述
结论:对指针+1,本质加上其所指向类型的大小。

#include<stdio.h>
#include <windows.h>

#define N 10

int main()
{
	int a[N] = { 0 };
	printf("%p\n", &a[0]);	//首元素地址
	printf("%p\n", &a[0] + 1);	//第二个元素的地址
	printf("%p\n", &a);	//数组的地址;
	printf("%p\n", &a + 1);	//下一个数组的地址;
	system("pause");
	return 0;
}

在这里插入图片描述
  &a[0]:数组名a分别和运算符&和运算符[]结合,我们知道,运算符[]的运算优先级比运算符&高,所以数组名a先和运算符[]结合,此时表示首元素,在与运算符&结合表示首元素地址,就是数组内第一个元素的地址;&a:数组名与运算符&结合表示数组的地址,就是数组整体的地址。两者在数字层面上的值是一样的,主要差别是体现在类型上。我们知道两者的数据都是地址,而地址就是指针,对此进行指针+1,即&a[0]+1和&a+1时;我们发现指针移动的步长不一样,对&a[0]+1,指针移动的步长为该元素保存的数据的类型大小;对&a+1,指针移动的步长为该数组中元素保存的数据类型大小乘上元素个数。
注意:为什么首元素地址和数组的地址的值一样?因为首元素的地址和数组的地址在在内存开辟总多字节中对应的最低字节是重叠的!所以,地址数据值相等。
  因为类型不同,所以指针+1所对应的步长不一样。

结论:数组名使用的时候代表整个数组只有两种情况:

    &a:数组的地址; sizeof(a):单独使用使用数组名,sizeof内部没有出现任何的表达式。

5、数组名a做为左值和右值的区别

  在前面中,我们从变量的开辟空间来理解左值和右值,现在我们简单的来理解左值和右值。
  出现在赋值符“=”右边的就是右值,出现在赋值符“=”左边的就是左值,比如x=y;
  左值:编译器认为x的含义是x所代表的地址,就是说我们平时所说的变量名,在编译器看来,是一个地址,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。
  右值:编译器认为y的含义是y所代表的地址里面的内容。这个内容是什么,只有到运行才知道。

//数组名充当右值时;
//数组名可以做右值,代表数组首元素的地址
//数组名做右值,本质等价于&arr[0]
//demo1
#include<stdio.h>
#include <windows.h>
#define N 10
int main()
{
	char a[N] = { 0 };
	printf("%p\n",&a[0]);
	char *p = a;
	printf("%p\n", p);
	system("pause");
	return 0;
}

在这里插入图片描述
  当数组名a做为右值时其意义与&a[0]是一样的,代表的是数组的首元素地址,而不是数组的首地址。证明方案是调试代码时时编译器没有任何报错,就证明“char p = a”这条语句中左侧的类型和右侧的类型是兼容的,也就是此时的数组名a并非其他类型,而是char类型。但是要注意,这仅仅是代表,并没有一个地方来存储这个地址,也就是说编译器并没有为数组a分配一块内存来存储其地址。

结论:数组名可以充当右值,代表首元素地址!

//数组名a不可以做左值!
//能够充当左值的,必须是有空间且可被修改的,a不可以整体使用,只能按照元素为单位进行使用。
#include<stdio.h>
#include <windows.h>
#define N 10
int main()
{
	int a[N] = { 0 };
	a = { 1, 2, 3, 4, 5 };//只能够进行整体初始化,不能整体赋值,不能作为左值;
	system("pause");
	return 0;
}

在这里插入图片描述
  a不能做为左值!编译器认为数组名做为左值代表的意思是a的首元素的首地址,而一个能够充当左值的东西,一定必须有对应的空间,那按理说数组名在内存中开辟一块内存是一个总体,可是c语言语法不让数组进行一个整体的赋值操作,所以数组名有对应的空间,
但是这个地址指向的一块内存是一个总体,我们只能访问数组的某个元素,而无法把数组当一个总体进行访问。所以我们可以把a[i]当左值,而无法把a当左值。

结论:数组名不可以充当左值!

(三)二维数组

1、二维数组的创建和初始化

二维数组的创建

代码格式:type array_name[const1][const2];

type 是指数组的元素的类型; const1:常量表达式1,用来指定二维数组的大小。const1可以是字面值,也可以是宏定义的常量; const2:常量表达式2,用来表示二维数组的每个元素(即每个一维数组)的大小。这个const1可以是字面值,也可以是宏定义的常量; date:是数组保存的元素。

  我们前面讨论过,数组里面可以存放任何数据,除了函数。所谓二维数组,就是数组里面存放的元素内容为一维数组,方格本,我相信大家都见过,我们平时就可以把二维数组假象成一个方格本,比如:

char array[3][4];

假象中的二维数组的布局如下:
在这里插入图片描述
  因此,创建二维数组时,我们一般将常量表达式1称之为二维数据的行,常量表达式2称之为数组的列;

  我们口头话如何描述一个二维数组,例如:char array[3][4];
我们可以这样说,array是一个二维数组,该二维数组有3个元素,每个元素的类型是char [4](数组),该数组包含4个元素,每个元素的类型是char。

二维数组的创建,同样有两种格式:

//格式一,常量表达式1和常量表达式2直接附字面值:
int array[5][4];

//格式二:常量表达式1和常量表达式2用宏定义:
#define ROW 5	//ROW表示二维数组的行数;
#define COL 4		//COL表示二维数组的列数;
int array[ROW][COL];

  上述两种格式都是创建一个二维数组,里面有5个元素,每个元素的类型是数组,该数组里面有4个元素;同样推荐格式二来创建二维数组;

二维数组的初始化

//宏定义数组的行数和列数
#define ROW 5
#define COL 4
//格式一,不完全初始化,即二维数组的每个元素都是0;
int array[ROW][COL] = {0};

//格式二:不完全初始化,不够初始化的元素值用0进行初始化;
int array[ROW][COL] = {1,2,3,4};
//等价于:
int array[ROW][COL] = {{1,2,3,4},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}};

//格式三:不完全初始化,不够初始化的元素值用0进行初始化;
int array[ROW][COL] = {{1,2},{3,4},{5,6}};
//等价于:
int array[ROW][COL] = {{1,2,0,0},{3,4,0,0},{5,6,0,0},{0,0,0,0},{0,0,0,0}};

//格式四:完全初始化
int array[ROW][COL] = {{1,2,3,4},{1,2,3,4},{1,2,3,4},{1,2,3,4},{1,2,3,4}};

在这里插入图片描述

注意:
  1.二维数组创建并初始化时,可以省略常量表达式1,不可省略常量表达式2;
在这里插入图片描述
在这里插入图片描述
结论:创建n维数组,就会有n个常量表达式。其中,只有第一个常量表达式可以省略,其余常量表达式不可省略。
原因我将放到(c语言)-- 深度剖析指针和数组(下)进行解释。

2、二维数组的使用

二维数组的使用也是通过下标的方式。看代码:

#include <stdio.h>
#include <windows.h>
//宏定义二维数组的行和列
#define ROW 5	//行数
#define COL 4		//列数
int main(){
	int array[ROW][COL] = {0};		//创建二维数组并初始化
	int i = 0;
	//元素重新赋值;
	for(;i < ROW; i++){
		int j = 0;
		for(; j < COL; j++){
			array[i][j] = i * 4 + j;
		}
	}
	//遍历数组;
	for(;i < ROW; i++){
		int j = 0;
		for(; j < COL; j++){
			printf("%d ",array[i][j]);
		}
		printf("\n");
	}
	system("pause");
	return 0;
}

3、二维数组的内存布局

  我们前面在表示二维数组的布局时,都是采用矩阵形式,这也是大部分书中所画的形式;但是,现在我们要深入讨论二维数组的内存布局,之前的只能称之为示意图,并非真的内存布局。可以想象一些问题,如果按照书中矩阵样子画二维数组的话,那么三维数组,四维数组又该如何画呢?
  我们看如下代码,深入理解二维数组在内存中的正确布局:

#include <stdio.h>
#include <windows.h>
int main()
{
	char a[3][4] = { 0 };	//创建二维数组;
	for (int i = 0; i < 3; i++){	//遍历二维数组;
		for (int j = 0; j < 4; j++){
			printf("&a[%d][%d] : %p\n", i, j, &a[i][j]);
		}
	}
	system("pause");
	return 0;	
}

运行结果:
在这里插入图片描述
结论:二维数组在内存地址空间排布上,也是线性连续且递增的。

  如何正确画出二维数组的内存布局图:

//创建一个二维数组并初始化
char a[3][4] = {0};

在这里插入图片描述
  扩展一下,如果将二维数组做为某一个数组的元素,那么这个数组就是一个三维数组!
在这里插入图片描述
  我们看到,将两个二维数组做为元素拼接在一起,就成为一个三维数组char x[2][3][4]。其中,三维数组有两个元素,每个元素的类型位为char[3][4]。
  同样的,如果将三维数组做为元素拼接在一起,就成为一个四维数组。

技巧:如何看待多维数组中元素的类型?
数组中元素的类型,就是去掉数组名和第一个方括号[ ]及其里面的常量表达式,剩余的就是数组中元素类型;数组中第一个方括号代表的是该数组包含的元素的个数,里面的常量表达式可以省略不写,如果不写,数组的元素个数由初始化决定。
例如:
char x[2][3][4] = {0};
  是一个三维数组,由 于第一个方括号中常量表达式为2,所以该数组有2个元素,每个元素的类型就是去掉x[2],剩余的char [3][4]就是元素的类型;
char a[3][4] = {0};
  是一个二维数组,由于第一个方括号中常量表达式为3,所以该数组有3个元素,每个元素的类型就是去掉a[3],剩余的char [4]就是元素的类型;
char arr[4] = {0};
  是一个一维数组,由于第一个方括号中常量表达式为4,所以该数组有4个元素,每个元素的类型就是去掉arr[4],剩余的char就是元素的类型;

结论:所有的数组,都可以看成”一维数组“。

  我们通过编译上述代码知道,二维数组打印出来的地址都是连续的。所以我们可以得到,所有维度的数组,在空间排布都是线性、连续且递增!
解释:
  我们知道数组具有相同数据元素类型的集合。其特征是,数组中可以保存任意类型,而不单单只是保存基本数据类型。既然可以保存任意类型,那么数组中可以保存数组吗?答案是可以!在理解上,我们甚至可以理解所有的数组都可以当成"一维数组"!
  就二维数组来说,我们认为二维数组,可以被看做“一维数组”,只不过内部“元素”也是一维数组。那么内部一维数组在内存中布局是“线性连续且递增”的,多个该一维数组构成另一个“一维数组”,那么整体便也是线性连续且递增的
  这也就解释了,上述地址为何是连续的。
  在强调一遍,我们认为:二维数组可以被看做内部元素是一维数组的一维数组。

  由于二维数组是线性连续且递增的,故我们在遍历二维数组时还可以用指针的形式进行遍历。

#include <stdio.h>
#include <windows.h>
int main()
{
	char a[3][4] = { 0 };
	char *p = (char*)a;		//这里的a代表首元素(是一个数组!!!char [4])的地址。
	for (int i = 0; i < 3 * 4; i++) {
		//printf("%p\n", &p[i]);
		printf("下标[i]对应的地址:%p\n",i, p + i);
	}
	//用来对比
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 4; j++) {
			printf("&a[%d][%d] : %p\n", i, j, &a[i][j]);
		}
	}
	system("pause");
	return 0;
}

在这里插入图片描述

注意:上述定义二维数组a时,如果a充当右值,取其内容,表示首元素地址,这里的首元素地址类型是char [4],是一个数组。

4、理解&a[0]和&a的区别

  我们在剖析一维数组的时候,对于这两种表示,我们已经给出结论,同样的,对于二维数组,甚至多维数组,其结论都是一样的。但是对于二维数组,我们应该怎样去理解透它,我们看如下代码:

#include <stdio.h>
#include <windows.h>

int main() {
	char a[3][4] = { 0 };
	char (*p1)[3][4] = &a;
	char (*p2)[4] = &a[0];
	char (*p3)[4] = a;
	char *p4 = &a[0][0];

	printf("二维数组的地址:\t\t%p\n", p1);
	printf("二维数组的首元素地址:\t\t%p\n", p2);
	printf("二维数组的首元素地址:\t\t%p\n", p3);
	printf("二维数组的第一个元素地址:\t%p\n", p4);
	printf("===========================================\n");
	printf("二维数组的地址+1:\t\t%p\n", p1 + 1);
	printf("二维数组的首元素地址+1:\t%p\n", p2 + 1);
	printf("二维数组首元素地址+1:\t\t%p\n", p3 + 1);
	printf("二维数组第一个元素地址+1:\t%p\n", p4 + 1);

	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
  对于任何一个多维数组来说,数组名使用的时候代表整个数组只有两种情况,一种是对数组取地址,另一种是用sizeof关键字内部单独使用数组名求数组的类型。其余情况,数组名一律代表首元素地址。这个首元素,我们现在就要特别关注了,在一维数组中,由于一维数组内部包含的元素都是基本数据类型,所以首元素其实就是数组的第一个元素;但是到了多维,我们以二维数组为例,二维数组的首元素是一个一维数组,而二维数组的第一个元素是二维数组的首元素中的首元素,这是两个不同的概念,这点要区分好。这点区分好了,我们来理解上述代码就很容易了。
  &a:对数组名取地址,代表二维数组的地址,+1操作,表示指针加上其所指向的类型大小
  a:单独使用数组名,代表首元素地址,二维数组中首元素是一个一维数组,+1指向二维数组的下一个元素;
  &a[0]:首先,a[0]表示二维数组的首元素,该元素是一个一维数组,对其取地址,代表是的就是首元素地址,与a本质一样;
&a[0][0]:首先,a[0][0]表示二维数组的第一个元素,对其取地址,代表二维数组的第一个元素地址,+1操作,表示指针加上其所指向的类型的大小。
  根据代码运行结果可知,四种表示的地址是一样,这是因为指针永远指向开辟众多字节的最低字节处;
  +1操作后,各自表示的地址出现变化,这是因为对指针进行+1操作,不是单单看字面意思进行简单的+1操作,而是加上指向的类型的大小,由于指针指向的类型各不相同,所以+1操作后,呈现出来的地址也就不一样。
在这里插入图片描述

5、数组名a做为左值和右值的区别

  二维数组的数组名a做右值时,取值内容,代表首元素地址。代码如下:

//二维数组数组名充当右值时;
//二维数组数组名可以做右值,代表数组首元素的地址
//二维数组数组名做右值,本质等价于&a[0]
#include <stdio.h>
#include <windows.h>

int main() {
	char a[3][4] = { 0 };
	char (*p)[4] = a;
	printf("二维数组的首元素地址&a[0]:%p\n",&a[0]);
	printf("数组名做为右值时(内容)时:%p\n",p);
	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
结论:二维数组数组名可以充当右值,代表首元素地址!

//二维数组数组名不可以做左值!
//能够充当左值的,必须是有空间且可被修改的,a不可以整体使用,只能按照元素为单位进行使用。
#include <stdio.h>
#include <windows.h>

int main() {
	char a[3][4] = { 0 };
	a = { {1,2,3,4},{1,2,3,4},{1,2,3,4} };
	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
结论:二维数组数组名不可以充当左值!

四、指针与数组的关系

结论:指针和数组没有关系!

(一)以指针的形式访问和以数组的形式访问

  我们给出的结论是指针和数组没有关系,但是有时候在操作数组元素的时候,发现既可以用数组方式操作,也可以用指针方式操作,而且两者操作的形式特别相似,那为什么两者会没有关系呢?(先给一段代码大家感受一下)

const char *str = "china";
char arr[] = "china;"

对于这两行代码,我提出几个问题,大家自行思考一下;

    代码中两个字符串保存的位置一样的吗?保存在哪里? 为什么要加关键字const?

  我们先从感性来区别一下。这就好比我跟你,我们的性格,一些生活习惯甚至个人行为方式都有很强的相似性,但是你能说我们两个是相同的嘛?答案是不能。同样是,指针和数组虽然在一些方式上具有很强的相似性,但是两者没有半毛钱关系。现在有些人很懵,你说两者没关系,但又有相似性,那么这个相似性体现在哪里,没有关系又体现在哪里?
  学过的同学应该知道,如果要遍历以上两个字符串,用数组的方式和用指针的方式进行遍历,两者的写法是一摸一样的,这就是两者的相似性,但是,虽然遍历元素的方式相似,但是两者的底层含义却是各有不同,这个就证明两者没有关系。接下来我们就来谈谈两者的相似性以及证明两者没有关系,看如下代码:

#include <stdio.h>
#include <string.h>
#include <windows.h>

int main()
{
	const char *str = "china";	//str指针变量在栈上保存,“china”在字符常量区,真正意义上属于操作系统级别的保护,不可修改,const修改的只是编译器级别的保护;
	char arr[] = "china";	//整个数组都在栈上保存,可以被修改,相当于你定义的,空间属于用户,有资格进行修改,这部分可以局部测试一下
	
	//1. 以指针的形式访问指针和以下标的形式访问指针
	printf("以指针的形式访问指针和以下标的形式访问指针\n");
	int len = strlen(str);
	for (int i = 0; i < len; i++) {
		printf("%c\t", *(str + i));
		printf("%c \n", str[i]);
	}
	printf("\n");

	//2. 以指针的形式访问数组和以下标的形式访问数组
	printf("以指针的形式访问数组和以下标的形式访问数组\n");
	len = strlen(arr);
	for (int i = 0; i < len; i++) {
		printf("%c\t", *(arr + i));
		printf("%c \n", arr[i]);
	}
	printf("\n");

	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
  我们发现,指针与数组,在访问多个连续元素的时候,可以指针解引用方案,也可以[ ]方案!
理解链:
  我们的计算机并没有为数组名(数组的起始地址)单独开辟一段内存,去保存数组名,它的数组名就代表着它所对应的整个数组的起始地址,也就是数组名直接等价于数组的首元素地址。如果要访问’c’元素时,只要找到数组名,加上下标0,然后解引用就找到元素’c’;如果要访问’a’元素时,只要找到数组名,加上下标4,然后解引用就能找到元素’a’;数组名大部分情况下,代表首元素地址,换句话说,数组名代表的首元素地址其实就是个字面常量,因此,我们在访问某个元素时,是直接根据这个地址找到起始位置,然后具体想访问哪个元素,就加上对应的索引解引用即可,这是数组进行寻址的一种方案。
在这里插入图片描述
  指针变量str是在栈上开辟的一个变量,用指针访问元素’a’时,同样可以用指针str加上对应的索引,然后解引用即可;虽然跟数组访问方式一样,但是,str是一个变量,而数组名最终是个字面常量,常量可以直接寻址,而str要进行寻址时,需要先找到str,然后把里面的内容(即字符串的起始地址)拿出来,在根据这个内容去加上对应的索引,找到之后解引用就得到元素’a’。
在这里插入图片描述
  换言之,虽然两者对应的写法都是一样的,但是深入一点了解,两者的寻址方案完全不同!!两者仅仅是在c语言的语法范畴内访问元素的方案相似,但是,在底层找数据的时候,两者的找法完全不同,既然找法不一样,就直接证明了两者不是同样的关系!

结论:指针和数组指向(表示)一块空间的时候,访问方式是可以互通的,具有相似性。但是具有相似性,不代表具有相关性。

最后,留个问题。
  为什么c语言在设计的时候非得把指针和数组在访问元素的时候,访问方式设计的这么像,且都是通用?
  关于这块内容以及深度理解指针数组和数组指针、数组传参和指针传参、函数指针等内容,我将放到(c语言)-- 深度剖析指针和数组(下)的博客中,到时欢迎大家互访,谢谢大家!
在这里插入图片描述

来源url
栏目