详解数组名和指针的区别

###数组名??指针??
刚开始学c语言时,当时的教材–谭浩强的《c程序设计教程》中是这样描述数组名和指针的:

在c语言中,数组名(不包括形参数组名,形参数组并不占据实际的内存单元)代表数组的首元素(即符号位0的元素)的地址。因此下面两条语句等价:
int *p = &a[0];
int *p = a;

这句话本身是没错的,但是这句话却具有误导性,再加上当时老师是这麽教的

你把数组名看作首元素的地址就好了

于是我(可能好多同学也是)一直就是这么理解的(坑爹啊),直到有一天。。。

#include <stdio.h>

void func(int a[3])
{
	printf("%d\n", sizeof(a));
}

int main()
{
	int a[3] = {1, 2, 3};
	printf("%d\n", sizeof(a));
	func(a);

	return 0;
}

它的输出是这样的
12
4

我擦你tm在逗我?不是说好的数组名就是指针吗?怎么都是数组a, 一个占用大小是12一个是4?赶紧回去看书:

在c语言中,数组名(不包括形参数组名,形参数组并不占据实际的内存单元)代表数组的…

原来如此,形参数组并不占据实际的内存空间,意思就是数组实际上会占用内存空间咯?

如果你认为问题解决了,那就大错特错了,下面这段代码会让你无法入眠:

#include <stdio.h>

int main()
{
	int a[] = {1, 2, 3};

	for (int i = 0; i < *(&a + 1) - a; i++)
	{
		printf("%d\t", a[i]);
	}
	
	retrun 0;
}

没错,这段程序真能run,*(&a + 1) - a求出来的就是数组的大小。


###变量的名本质
要弄清楚这个问题,我们先要弄明白变量名的本质是什么。首先,变量是存在内存中的,访问变量就是访问内存中某个位置的数据。所以变量名首先就要标识这位变量在内存中的“位置”,也就是地址了。然而仅有地址是不够的,因为计算机是按字节编址的,一个地址只能标识某一个字节在内存中的位置;然而int有4字节,double有8字节,仅凭一个地址怎么访问他们?所以变量名还有第二个功能,就是标识这个变量的长度。比如说你定义了int a = 5,他的地址是0xbfffedb4,长度是4,那么计算机要访问的时候,就先在内存中找到0xbfffedb4所指的那个字节,并且把这个地址往后4字节都取出来,输出就可以了。画个图就是这样的:
变量的本质

###数组名的本质
数组名的本质不是指针,而是一个这样的变量:
数组名的本质

这就是为什么sizof(a)会是12的原因了。这个变量指定了它的长度是12,这样做还具有安全性:如果你访问越界,会直接出错,而不会让你肆无忌惮地访问,造成灾难性后果。

但是还有一个问题没解决:为什么在func()中的sizeof(a)却是4呢?

我们知道,函数参数传递的时候实际上做了一个赋值(应该叫初始化)操作,比如说void func(int arg);调用的时候int a = 5; func(a);在调用的时候实际上就执行了int arg = a;然而数组却不能这样做,因为这样的语句是非法的int a[] = {1, 2, 3}; int b[] = a;并且如果有一个很大的数组,如果参数传递的时候进行内存复制,是非常耗费时间和空间的。因此,c语言采取了这样的方法来传递数组参数:即把实参数组的首地址赋值给形参。这样一来,形参中虽然定义了int a[5],但它本质上已经不是一个数组了,而是一个指针,等价于int *a。这就是为什么func()中的sizeof(a)是4的原因了。

###关于指针的运算
我们知道,指针是可以做加减法运算的。指针的加法,本质上是指针的偏移。然而你把指针+1,它到底偏移了多少呢?看下面的例子:

#include <stdio.h>

int main()
{
    int a = 5;
    double b = 3.14;
    int c[] = {1, 2, 3};

    return 0;
}

我们在gdb上对这个程序进行调试:

先在第9行断个点,然后让他跑起来:
调试
给这几个地址都偏移一下,看看结果如何:
指针偏移

可以看出:

  • int 类型的指针偏移了 30 - 2c = 4;
  • double 类型指针偏移了 30 - 38 = 8;
  • int[3] 类型指针偏移了48 - 3c = 12;

得出的结论是:指针加1个单位所偏移的量取决于它们指向的类型的长度。
值得注意的是int c[3]的偏移量,因为&c所指向的类型是一个长度为3的int型数组。所以它的偏移量就是这个数组的长度,正好是12。这再次证明了数组名不是指针

现在又有一个问题来了:既然数组名不是指针,那为什么可以通过这样int a[] = {1, 2, 3}; printf("a[1] = %d",*(a + 1));的姿势来访问来访问数组的元素呢?

为了找到原因,我们来看这个例子:

#include <stdio.h>

int main()
{
	int a[] = {1, 2, 3};
	
	return 0;
}

用gdb调试,断点,运行:
调试
既然可以通过*(a+1)的方式访问,那么我们看看a加上1后是什么东西:
这里写图片描述
可以看到,虽然数组名不是指针,但是当它进行加减法运算后却变成了指针。所以我们能得出结论:数组名在进行加减法运算的时候,会隐式转换成指针类型


现在可以解决第二个问题了:为什么*(&a + 1) - a 可以得出数组的大小?我们一步一步来:

  1. 首先对数组a取地址,&a是一个指向int [3]的指针;
  2. &a + 1,因为int [3]大小为12个字节,所以它将偏移12个字节;
  3. 此时(&a + 1)指向最后一个元素的后面(对于数组a来说这个地址已经越界了),等价于&a[3];(&a + 1)的类型仍然是一个指向int [3]的指针;
  4. 对(&a + 1)取内容,所以*(&a + 1)的类型是一个长度为3的int 型数组,即int[3],而它的它的首地址紧跟着a[3]的后面;
  5. 关键一步:数组名的加减法,被减数、减数的类型都是int [3]型,两者都隐式转换为int *型,做减法*(&a + 1) - a = ?,也就是问a偏移几个单位等于*(&a + 1)。显然可以得出结论是3,也就是数组的大小了。

画个图清楚点:
描述

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值