在我们初学C语言时,我们会有这样一个认知或者感觉:指针和数组是相同的。我们也经常干着这样的事情,比如下面这样代码。

1
2
3
4
5
6
7
8
9
int main() {

int a[3] = {1, 2, 3};
int *p = a;
printf("%d\n", p[0]);
printf("%d",*(a+1));

return 0;
}

这样看起来两者似乎确实没有差别,指针可以像数组一样使用[],而数组名也可以解引用,而且它们的结果是一致的。

或者在我们定义函数时,也可能会把两者当成一种处理。

1
2
3
4
5
6
7
8
9
10
11
void foo(int *p) {
printf("foo: %d", p[1]);
}

int main() {

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

return 0;
}

一切看起来都是那么自然。

首先我们要肯定,两者之前确实有相似之处,但是如果我们直接把两者划上一个等号,那就大错特错了,接下来我们就来看看两者之间到底有哪些区别。

数组中到底存了什么

这个问题似乎很简单

比如下面这个数组,只要学过C语言的数组,你几乎可以不假思索的回答出来,存了1,2和3

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

ok,那么继续来回答一个简单的问题,下面这个指针p中存了什么,这个问题也很简单,储存了a的地址

1
int *p = a;

如果对上面的问题没有异议,那么我们可以画出这样的内存图

NGW

ok,我们再回来看前面的赋值,那么问题来了,我们直接把a的值赋给了p,那么a的值是存放在哪里的呢,也就是0x10000这个值原来是存放在了什么地方呢?

1
int *p = a;

我们似乎找不到0x10000的所在位置,而在a这个位置上储存的是1,我们再来看这个赋值语句,是不是突然又困惑起来了,好像=这个操作符也不是那么自然了,按照我们对于=的理解,应该是把1赋值给p

那么问题出在哪里呢,我记得我们初学C语言时,教科书上对于数组名的说明是:数组名代表的是该数组首元素的地址,我们要注意这个代表,也就说这个a所储存的地址在内存中并没有分配空间,a也仅仅只是代表了一个地址。具体为什么会这样,这是C语言内部要做的,就不用我们操心了,我们只需要知道数组名代表了数组首元素的地址,但是并没有给这个地址的存储分配一个空间。

从一点上来看,数组和指针已经有了明显的不同了,而数组名实际上没有用空间来存下地址,所以数组名也是不能够作为左值的,比如下面就是一个错误的例子。

1
2
3
4
5
6
7
8
9
int a[3] = {1, 2, 3};
// 1.
int b[3] = {4, 5, 6};
a = b;
// 2.
int b = 1;
int *p = &b;
a = p;
// 数组名不可被赋值

数组和指针分别是如何工作的

我们再从一个简单的例子来说明如果把数组名和指针等同会闹出怎么样的笑话。

我们先来思考一下,指针是如何工作的

1
printf("%d",*p);

这里我相信大家都很好理解

  1. 取出地址0x10000

  2. 再根据地址0x10000取出地址中的值

这个我相信对于学过C语言指针的大家都很好理解,如果我们换成数组名呢

1
printf("%d",*a);

如果我们按照指针的工作方式去理解它,那么第一步应该是取出地址,那么地址是什么呢,我们会惊人的发现这个地址竟然是1,这毫无疑问是一个无效地址,此时如果程序崩溃我们应该庆幸,否则将会产生无法预料的后果。

但事实上,上面*a的方式也可以正确的取出值,这我们又是一个混淆指针和数组名的原因。

由上我们不难发现,C语言在解引用时,对待数组名和指针的策略是不一样的。那我们不妨再细分一下它们的工作流程,仔细看看它们有什么区别。

我们假设指针P的地址是0x20000

我们先来看看*p的工作流程:

  1. 找到指针P所在的地址0x20000
  2. 取出这个地址的值0x10000
  3. 最后再根据地址0x10000取出值1

再来看看*a的工作流程:

  1. 找到a所在的地址0x10000
  2. 取出0x10000的值1

怎么样,是不是发现两者大不相同,至于是如何找到指针地址0x20000的,这个就要交给C语言本身了,我们不需要关心。

好了,说了这么多,我并不是要说以前的用法都是错误的,事实上,C语言允许我们这样去处理,这样处理起来更方便,其实就够了,我们依然可以用以前的方法去做。主要目的还是希望大家在使用两者的同时更要清楚它们之间的区别,这样一是可以更好的使用它们,二是也减小了我们写出错误代码的可能性。