数组和指针

数组和指针不是一样的

首先我需要抛出这篇文章的重要观点:

C语言数组和指针是完全不同的两种东西

这是对于很多人学习C语言指针容易进入的误区,就是数组和指针在使用上傻傻分不清楚导致认为数组和指针是一样的事物。

从上图中可以看出,数组是一些对象(非C++对象)排列之后形成的,而指针则表示指向某处。至于为什么容易混淆数组和指针的使用呢,我想最多的一定是下面这句话:

在单独使用数组名不加[]的时候,此时的数组名就是表示指向数组初始元素的指针

这个说法是完全错误的,有以下两个原因:

第一:因为我们知道,指针是地址,如果说“ 此时的数组初始元素的地址和数组名代表的地址是一样的 ”,这是没有问题的,注意,这两者是不一样的,会在下面的程序中给出示例。

第二:数组名其实是一个右值,而右值在初始化完成之后是不能改变的,但是指针却是变量,在程序运行过程中是完全可变的。

区分数组和指针是很有必要的!!!

为什么数组下标从0开始

我们知道我们在C语言中定义一个数组arr[n],可使用的内存是arr[0] ~ arr[n-1],而不包括arr[n]

在平时的使用过程中不管你是定义了以下两种情况哪一种,如果把它当成数组来使用,下标总是从0开始,初学者尤其容易犯的迷糊就是为什么不从1开始,而是0?

int arr[5] = {1, 2, 3, 4, 5};
int *p;
p = arr;

printf("%d %d\n", p[0], arr[0]);

解释这个问题之前,先看一下关于C语言指针的语法糖,首先我们需要明确一个观点:C语言中,[]并非是数组的代名词,也就是说它和数组时没有本质联系的,不要想当然认为出现[]就是一个数组,[]仅仅是一个下标运算符。

现在根据上面arr[5]的例子我们使用如下方法将数组元素全部输出:

for(int i = 0; i < 5; i++){
    printf("%d\n", *(arr + i));
}

注意,这里的*(arr + i)arr[i]作用是完全一样的,也就是说*(arr + i)arr[i]是同样的意思,可以认为后者是前者的简便写法,这里可以说,arr[i]*(arr + i)的语法糖。

可是这跟数组下标从0开始有什么关系呢?

有!

回想一下,我们前面说过,(严谨的说法)C数组名称指向的地址和数组初始元素的地址是一样的,那我们如果不使用数组语法糖,我们在上面程序输出数组元素时使用的格式控制其实就是下面这三个:

printf("%d\n", *(arr + 0));
printf("%d\n", *(arr + 1));
printf("%d\n", *(arr + 2));
printf("%d\n", *(arr + 3));
printf("%d\n", *(arr + 4));

也就是说,首先将数组名指向的地址加上相应的步数,再使用解引用*就可以获取数组内相应位置的数据值,这里,数组arr申请的内存地址是从arrarr+2的,看出来了吗,三个地址块分别是:

arr arr+1 arr+2 arr+3 arr+4

所以是没有arr[5]可以用的!

C语言的指针类型有什么用

我相信大家最开始学习指针的时候,都会有这样一个疑问:C指针既然本质是一个地址,那为什么还有不同的地址类型呢,如int *pchar *s等,这有什么关系呢?

结合上面的问题说一下。

上面说到关于数组和指针的语法糖的问题,其中有一个问题,就是说,*(arr+1)中这个1是什么意思,是一个字节?根据数组来看好像是一个数组元素的位置吧。

可是编译器怎么知道我一个前进一个元素要地址前进多少呢,这就用到了指针类型了,根据C数据类型规定,intdoublechar包括复杂一些的结构体类型等占用的内存是不一样的,如一般int占4字节,double占8字节等,所以可以得到:

对指针加N,指针前进“当前指针指向的数据(元素)类型的长度 * N

比如上面定义的数组元素是int类型的,那就自动向前进4个字节,因为数组是按地址排列的,所以加4个字节刚好是下一个元素的地址,这样就使得不同类型的数组+1都能按照以元素为单位前进。

可是,不是说数组和指针不一样吗,是的,但是这是一种两者同用的情况,而真正意义上两者完全意义相同的情况是下面这种。

唯一一种数组名和指针完全一样含义的情况

通常我们将数组作为函数参数传递时,常用的方式,就是类似如下面这种:

void function(int arr[]);

其实,对于C语言来说,以下三种的意义都是完全相同的:

void function(int arr[]);
void function(int arr[n]);
void function(int *arr);

这是因为,这三种情况下,arr都会被解读为指向初始元素的指针,而后在函数中可以直接相应地使用,但是有人会说,可是他怎么知道要传的数组大小呢,万一我在函数使用过程中出现越界怎么办?这是因为他没有这个功能

为什么?

C语言为什么不做数组下标越界检查

我们可以使用 int arr[5]; 这样的方式声明数组,并且通过 arr[i] 的方式引用数组元素的那些编程语言,可以比较容易地进行数组长度范围检查。但是对于 C,当数组出现在表达式中的时候,它会立刻被解读成指针(注意,虽然普通表达式中也会被解读为指针,但跟上述完全意义相同的情况还是有所差别)。此外,使用其他的指针变量也可以指向数组的任意元素,并且这个指针可以随意进行加减运算。引用数组元素的时候,虽然你可以写成 a[i] ,但是它只不过是*(a + i)的语法糖。

还有,当你向一个函数传递数组的时候,实际上你传递的是一个指向初始元素的指针。如果这个函数还存在于其他的代码文件中(另外一个编译单元),那么通过编译器是不可能追踪到数组的。

要求这样的语言在编译时生成检查数组长度的代码,是不是有些强人所难?

而我们在使用C编程的时候,建议在参数中加一个数组长度的参数,另外一种情况,如果一定要将数组进行值传递的情况,建议可以将数组整体整理成结构体成员,当然这是非常消耗性能的,还有一种方式就是使用const,即使用指针作为参数,用const来修饰,将指针指向的对象设定为只读,如C语言的一些库函数:

char *strcpy(char *dest, const char *src);

程序示例

下面我们根据一个例子来说明一些数组和指针在使用过程中容易出现问题的点:

解读一下下面这段程序:

#include <stdio.h>

int main(){
    int arr[2][3] = {1, 3, 5, 7, 9, 11};
    printf("%d\t%d\t%d\t%d\t%d\n", arr, arr + 1, &arr + 1, arr[0], arr[0]+1);
    printf("%d\n", *(arr[0]+1));
    printf("%d\t%d\n", *(arr+1), *arr + 1);
    printf("%d\t%d\n", **arr, *&arr[0][0]);
    return 0;
}
kongds@kongds ~/Desktop/C$ ./hiding                                                                                            
1841296144      1841296156      1841296168      1841296144      1841296148
3
1841296156      1841296148
1           1

首先,我们定义一个二维数组,我们知道,C语言中不存在二维数组,所谓二维数组其实就是数组的数组,再白话一些,就是一维数组,只不过这个一维数组的每个元素都是一个一维数组。

这其实跟我们理解二级指针是一样的,每个一级指针指向的对象都是一个一级指针。这里我们就要借助这个理念来分析:

首先将这个二维数组从最小单位解析,最小单位是每个int类型的元素,再往上一级,是一个一维数组(一行),可以使用嵌套的结构体来理解,这是最内层的结构体,这个结构体每个有三个int元素,最外层也就是二维数组了,也把它看作一个结构体,这个结构体每个实例有两个元素,每个元素是一个内层的结构体实例,这样,就可以将这个二维数组描述为以下这样:

struct 外层结构体{
    struct 内层结构体1{int, int, int};
    struct 内层结构体2{int, int, int};
};

或者用二级指针的形式来理解:

看程序第一个printf输出:

printf("%d\t%d\t%d\t%d\t%d\n", arr, arr + 1, &arr + 1, arr[0], arr[0]+1);

首先arr是数组名,被解读为数组的首地址,注意,我们前面说过,他不是指向数组的初始元素,他不过凑巧跟初始元素地址相同而已。所以它指向的是整个数组的首地址,而这个地址的值是1841296144

而对于arr+1,我们前面提到,数组名加1,指其首地址加上数组元素类型大小的值,可是他是二维数组啊,它加1的元素数据类型是什么?是一个int大小吗?不,二维数组名的元素是谁,是一维数组啊,一维数组有多大,3个int,一共是12字节,所以得到的结果1841296156相对数组首地址偏移12个字节。

看第三个,&arr+1,先看&arr,我们知道&是取地址运算,对二维数组取地址,其实最终的值还是二维数组的首地址,可是这个时候再加1会有什么变化?注意,结果1841296168相对首地址偏移了24个字节,这刚好是整个二维数组的大小,也就是说二维数组本身被当成元素前进了,可是根据前面不管是嵌套结构体还是二级指针,外层都没有东西了呀,这个时候我们前面的问题就暴露出来了?

什么问题,我们说数组名并不是指向数组初始元素的,而是指向数组首地址的,虽然他们在值上是相等的,这个时候就意识到了这种区分的必要性。也就是说,这个时候对arr取地址会使指针指向的单位往外扩一级,这说明这个时候取完地址&arr的元素类型是整个二维数组。

接着看第四个,arr[0],还记得吗,我们前面提到这种形式其实是*(arr+0)的语法糖,而我们分析第二个参数时已经知道,arr加上0其实是加上其元素大小 0,值不变,但是还需要使用一次解引用,但这并不影响其值与数组首地址相等,即1841296144。重要的是我们要分析其中的区别,*&是相对的,&会外扩指针指向的单位,而*则会内缩指针指向指向的单位,所以此时*(arr+0)还是指向整个第一个一维数组,也即arr[0]指向的是整个第一个一维数组元素。

第五个,arr[0]+1,我们分析第四个参数已经知道,arr[0]指向整个第一个一维数组,所以其+1就是加上其元素类型(即int)大小的值,得到1841296148,这个地址与元素3的地址一致,即相对数组首地址偏移4个字节。

再来看第二个printf输出:

printf("%d\n", *(arr[0]+1));

前面已经分析过了arr[0]+1,加上一个取地址,内缩一级指针,使其确切指向了元素3,所以输出结果就是3。

第三个printf输出:

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

第一个*(arr+1),已经分析过,arr+1表示指向的是整个第二个一维数组,对其使用解引用*则内缩一级指针,得到首元素的地址,虽然与整个第二个一维数组首地址相同,但此时*(arr+1)指向的是元素7,而不是整个第二个一维数组。

第二个,*arr+1,首先,arr指向整个二维数组,*arr则指向整个第一个一维数组,对其加1则加上其元素类型(即int)大小的值,即二维数组首地址偏移4个字节,此时*arr+1指向的是元素3。

最后一个printf输出:

printf("%d\t%d\n", **arr, *&arr[0][0]);

首先,arr指向整个二维数组,对其两次解引用即使其二维数组中第一个一维数组的首元素,即1。

对第二个参数,arr[0][0]本身就表示元素1,对其取地址再解引用,相当于不动,结果还是1。

希望通过这个例子,能够更好的理解指针和数组的应用。

推荐书籍:《征服C指针》

该文章内容由作者“樱花狗子”提供,并非商业用途,转载请申明 私自转载将依法追究其法律责任

lionの金库