再论一维数组名,二维数组名及指针联系与区别

一、概念详解:

指针:指针与“int a”,“float b”一样,也是一种变量,只是指针变量中存储的是内存单元的地址,这是与“int a”和“float b”的本质区别,C语言的精华就在于指针、结构体和链表。

一维数组:定义一维数组之后,即在内存中分配一段连续的地址空间,如C语言中,int num[5],在32位系统中,int占据4个字节,现假设num[0]的地址空间为0x00000004,那么num[1]的地址空间即为0x00000008 (0x00000004 + 4),num[2]的地址空间即为0x0000000c,其余几个的地址空间依次类推,加4即可。

二维数组二维数组其实可以看成是一个矩阵,如C语言中,int a[3][4],即可以看成是一个3行4列的矩阵,在内存中每一个位置存储一个数据,用a[i][j]表示。

指针数组:它实际上是一个数组,数组的每个元素存放的是一个指针类型的元素,如C语言中,定义一个指针数组用int  *arr[5],本质是一个数组,只是每个数组中的内容是变量的地址,而不是变量本身。。

int* arr[8];
//优先级问题:[]的优先级比*高
//说明arr是一个数组,而int*是数组里面的内容
//这句话的意思就是:arr是一个含有8和int*的数组

这里写图片描述

数组指针:通常用来指向二维数组的指针叫数组指针,如C语言中,int (*p)[5] , 可以用p来指向二维数组。:它实际上是一个指针,该指针指向一个数组。

int (*arr)[8];
//由于[]的优先级比*高,因此在写数组指针的时候必须将*arr用括号括起来
//arr先和*结合,说明p是一个指针变量
//这句话的意思就是:指针arr指向一个大小为8个整型的数组。

这里写图片描述

 

二、数组与指针的表示法:

在实际应用中数组和指针是相辅相成的,使用数组的地方一定会见到指针,在实际应用中数组一般也是通过指针引用的方式来表示的,一般不直接使用数组,数组下标表示法可以理解成先取地址再指针解引用

一维数组是一个线形表,它被存放在一片连续的内存单元中。C语言对数组的访问是通过数组名(数组的起始地址)加上相对于起始地址的相对量(由下标变量给出),得到要访问的数组元素的单元地址,然后再对计算出的单元地址的内容进行访问。通常把数据类型所占单元的字节个数称为扩大因子。

实际上编译系统将数组元素的形式a[i]转换成*(a+i),然后才进行运算。对于一般数组元素的形式:<数组名>[<下标表达式>],编译程序将其转换成:*(<数组名>+<下标表达式>),其中下标表达式为:下标表达式*扩大因子。整个式子计算结果是一个内存地址,最后的结果为:*<地址>=<地址所对应单元的地址的内容>。由此可见,C语言对数组的处理,实际上是转换成指针地址的运算。

数组与指针暗中结合在一起。因此,任何能由下标完成的操作,都可以用指针来实现,下标的实现实际是由指针加偏移量实现的,一个不带下标的数组名就是一个指向该数组的指针。

编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转化为指针加偏移量的引用。这么说大家可能不理解,首先什么是引用呢?引用其实就是使用,从编译器的角度来看,就是从一个符号找到对应内存并进行读写的过程。如果p指针指向数组b时,b[i]、*(b+i)、p[i]、*(p+i)都是对数组第i个元素的正确引用方式

 

三、难点解析:

1、一维数组名

当一个指针变量被初始化成数组名时,就说该指针变量指向了数组。如:

char str[20], *ptr;
ptr=str;

  ptr被置为数组str的第一个元素的地址,大家都知道数组名表示数组的首地址,数组的首地址实际指的是数组第一个元素的地址&str[0] ,这点一定不要忘记。此时可以认为指针ptr就是数组str(反之不成立),这样对数组的处理都可以用指针来实现。如对数组元素的访问,既可以用下标变量访问,也可以用指针访问。

2、二维数组名

首先定义一个二维数组

int a[2][3]={{1,2,3},{4,5,6}};
#or int a[2][2]={1,2,3,4};

新的理解:我们可以这样认为,a可以看作是一个一维数组,包含2个元素,每个元素恰好是包含3个整型的元素,a这个名字的值成了指向一个包含3个整型元素的数组的指针(你学过数组指针就该知道,他们是一个道理,后面我会讲解数组指针)然后开始讨论二维数组和数组名的关系

a表示的是整个数组的首地址,a[0]表示的是第一行的首地址,这两者在数值上是一样的,但含义不同(或者说类型不同),数组名a是对于整个数组,a[0]是对于第一行。

#打印 a a[0] 的值和 a[0][0] 的地址
cout << a << endl;#0x7ffffffee120
cout << a[0] << endl;#0x7ffffffee120	
cout << &a[0][0] << endl;#0x7ffffffee120

​ 所以a、a[0]的值和 a[0][0]的地址三个表达指向一个地址, 可以看出数组名a,a[0],a[1]这些都代表了一个地址a  = a[0] = *(a+0) = *a = &a[0][0],这要注意*a取到的是a[0]的值,不是a[0]的地址,即a[0][0]的地址,   *(a+1)取到的是a[1][0]的地址, **a取到的是a[0][0]的值,重点!!!

在实际代码中经常使用的是二维数组元素的地址,所以我们要看懂每一行首元素地址代表的含义,比如&a[0][0], &a[1][0]等,在内存中二维数组是按行存储的,可以把二维数组看成一个一维数组,只不过每个一维数组元素又是一个一维数组,a[0],a[1],a[3]是一维数组名(二维数组的元素),即指向一维数组(二维数组的元素)首地址的指针,如a[0]是一维数组a[0][0],a[0][1]的首地址,其指针基类型为int型,所以以下等价成立:

a[0]=&a[0][0]

a[0]+1=&a[0][1]

a[1]=&a[1][0]

a[1]+1=&a[1][1]

 

接着看每一行

cout << a[0] << endl;#0x7ffffffee120
cout << a[1] << endl;#0x7ffffffee12c

输出的结果是每行的首地址,两个地址之间相差12个字节,也就是三个int的大小。

下面我们通过a[0]&a[0][0]这两个地址推导出其他位置的元素。

# 由a[0]推出其他,这里以第1行第1列为例
cout << a[0+1]+1 << endl;			#0x7ffffffee130
cout << *(a[0+1]+1) << endl;		#5
cout << a[0]+4 << endl;				#0x7ffffffee130
cout << *(a[0]+4) << endl;			#5

# 由&a[0][0]推出其他, 这里以第1行第1列为例
cout << &a[0][0]+4<< endl;		#0x7ffffffee130
cout << *(&a[0][0]+4) << endl;	#5

前两行通过加a[0]的索引得到其他行的首地址,然后再加偏移量得到元素地址,解引用获得该地址的值。

之后两行是直接计算a[0]的偏移量得到元素地址,解引用获得该元素地址的值。最后两行与直接加偏移量的情况相同。

具体可参考表格:

 

*( *(a+1) + 1)图解:

a是数组的名字,它的类型是“指向包含三个整型元素数组的指针”,默认指向第一行。

a+1

现在它指向了第二行,可以理解为当一个int*的指针指向一个数组的某一个int时,++他便指向了后一个int,同理,这里的数组指针指向了一个数组,所以++他就要指向下一个数组。

*(a+1)

若是int*解引用后我们拿到了int类型数据,那数组指针呢?实际上我们对一个数组的指针解引用,拿到的是一个数组

*(a+1) == a[1]

*(a+1)就是第二行的数组名,如果你觉得这样表达很奇怪,那么a[1]和他意义相同。

我们现在拿到这个二维数组的第二行,第二行实际上就是一个一维数组,所以或许我们所有的处理都可以使用一维数组的方式。

*(a+1)+ 1

让他指向了这个数组中的第2个元素。

*(*(a+1)+ 1)

最后一步就可以拿到想要的值。

我们来看看一些关于取地址的问题

printf("%d\n",sizeof(a+1));//8//第二行的地址(指针的大小)
printf("%d\n",sizeof(&a[0]+1));//8
printf("%d\n",sizeof(*(&a[0]+1)));//12

a是一个数组指针,加一后指向下一行他还是指针,sizeof中就是指针的大小了。

给a[0]取地址?a[0]是一个一维数组的名字,也就是一个指针,再次取地址后的结果就是一个二级指针。而之前我们对数组指针解引用得到数组名,这次刚好相反,所以&arr[0]你可以理解为,我们现在拿到了一个数组指针,对数组指针+1就是让他指向第二行,sizeof中就是指针的大小。(所以数组指针也叫二级指针)

对数组指针解引用拿到了第二行数组的名字,sizeof他就是第二行大小。

最后进行个小测试

int a[2][3] = {0};
printf("%d\n",sizeof(a));//24//输出这个二维数组的大小
printf("%d\n",sizeof(a[0][0]));//4//第一个元素的大小(int)
printf("%d\n",sizeof(a[0]));//12//第一行的数组的大小
printf("%d\n",sizeof(a[0]+1));//8//第一行第二个元素的地址(指针的大小)
printf("%d\n",sizeof(*(a[0]+1)));//4//第一行第二个元素的大小
printf("%d\n",sizeof(a+1));//8//第二行的地址(指针的大小)
printf("%d\n",sizeof(*(a+1)));//12//第二行的大小
printf("%d\n",sizeof(&a[0]+1));//8//第二行的地址(指针的大小)
printf("%d\n",sizeof(*(&a[0]+1)));//12//第二行的大小
printf("%d\n",sizeof(*a));//12//第一行的大小
printf("%d\n",sizeof(a[2]));//12//虽然越界了但是sizeof只是一个关键字他不关心这块空间是否真的存在

数组指针

 

四、一维数组与二维数组传参

数组传参时,会退化为指针,所以我们先来看看什么是退化! 
(1)退化的意义:C语言只会以值拷贝的方式传递参数,参数传递时,如果只拷贝整个数组,效率会大大降低,并且在参数位于栈上,太大的数组拷贝将会导致栈溢出。 
(2)因此,C语言将数组的传参进行了退化。将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址。

1.一维数组的传参

#include <stdio.h>
//传参方式正确
//用数组的形式传递参数,不需要指定参数的大小,因为在一维数组传参时,形参不会真实的创建数组,传的只是数组首元素的地址。(如果是变量的值传递,那么形参就是实参的一份拷贝)
void test(int arr[])
{}

//传参方式正确
//不传参数可以,传递参数当然也可以
void test(int arr[10])
{}

//传参方式正确
//一维数组传参退化,用指针进行接收,传的是数组首元素的地址
void test(int *arr)
{}

//传参方式正确
//*arr[20]是指针数组,传过去的是数组名
void test2(int *arr[20])
{}

//传参方式正确
//传过去是指针数组的数组名,代表首元素地址,首元素是个指针向数组的指针,再取地址,就表示二级指针,用二级指针接收
void test2(int **arr)
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}

 

2.二维数组的传参

//传参正确
//表明二维数组的大小,三行五列
void test(int arr[3][5])
{}

//传参不正确
//二维数组的两个方括号,不能全部为空,也不能第二个为空,只能第一个为空
void test(int arr[][])
{}

//传参正确
//可以写成如下这样传参形式,但是不能写int arr[3][]
void test(int arr[][5])
{}

//传参不正确
//arr是一级指针,可以传给二维数组,但是不能正确读取
void test(int *arr)
{}

//传参不正确
//这里的形参是指针数组,是一维的,可以传参,但是读取的数据不正确
void test(int* arr[5])
{}

//传参正确
//传过去的是二维数组的数组名,即数组首元素的地址,也就是第一行的地址,第一行也是个数组,用一个数组指针接收
void test(int (*arr)[5])
{}

//传参不正确
//可以传参,但是在读取的时候会有级别不同的问题
void test(int **arr)
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}

指针

1.一级指针传参

当函数参数部分是一级指针时,可以接受什么参数例如:test(int*p)

(1)可以是一个整形指针 
(2)可以是整型变量地址 
(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;
}

2.二级指针传参

即当函数参数部分是二级指针时,可以接受什么参数例如:test(int**p)

(1)二级指针变量 
(2)一级指针变量地址 
(3)一维指针数组的数组名

#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int num = 10;
int*p = &num;
int **pp = &p;
test(pp);
test(&p);
return 0;
}

 

 

五、数组与指针总结:

1.编译时不会为指针分配存储空间, 但是指针在结构体中, 是会默认分配的,

2.数组的内部实现也是依靠指针, 只是数组名是一个指针常量, 数组一次寻址(即通过数组名直接找到数组元素), 指针需要两次寻址(先确定指针变量的己值,然后定位到指针的他值)

3.指针变量+1, 不是加一个地址量(即一个字节), 而是加一个内存单元, 具体的长度由指针类型决定

4.指针指向数组的时候, 只是指向数组的首地址, 他不代表数组类型, 可以通过 指针++的方式依次获取数组内容

5.数组名在两种情况下不被视为指向起始元素的指针。

    a.作为sizeof运算符的操作数出现时

    b.作为取址运算符&的操作数出现时

6.两个指针不能做加法运算,但是可以做减法运算

7.数组名是指针常量, 所以没有办法再次对一个数组名重新分配内存地址; 换言之, 可以对指针变量赋值, 但是不能对数组名赋值

8. 定义了一个指针, 是可以按数组的方式使用的

9. 数组是比指针更高一层级的定义, 数组作为形参, 将退化为指针,如 int *p; ... p[5]=2;

10.数组存数据,指针存地址

11.指针对比数组的一个优势: 可以扩容, 因为指针的内存可以通过malloc分配, 所以也可以通过realloc扩容

 

参考文献:

https://blog.csdn.net/weixin_44810857/article/details/107597264

https://blog.csdn.net/cherrydreamsover/article/details/81741459

 

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值