游戏开发工具

数组和指针绝不等价,数组是另外一种类型

19 篇文章14 次收藏

对指针数组和数组指针的概念,相信很多C程序员都会混淆。下面通过两个简单的语句来分析一下二者之间的区别,示例代码如下所示:

int *p1[5];
int (*p2)[5];

首先,对于语句“int*p1[5]”,因为“[]”的优先级要比“*”要高,所以 p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int*”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,如图 1 所示,因此,它是一个指针数组。

1.jpg

其次,对于语句“int(*p2)[5]”,“()”的优先级比“[]”高,“*”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图 2 所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。

2.jpg

由此可见,对指针数组来说,首先它是一个数组,数组的元素都是指针,也就是说该数组存储的是指针,数组占多少个字节由数组本身决定;而对数组指针来说,首先它是一个指针,它指向一个数组,也就是说它是指向数组的指针,在 32 位系统下永远占 4 字节,至于它指向的数组占多少字节,这个不能够确定,要看具体情况。


先来个问题,下面的一段代码编译时会存在问题吗?

#include <stdio.h>
#include <string.h>
int main(void)
{
    unsigned char a[2];
    bzero(a, sizeof(a));
    if (a) {
        printf("a is valid!\n");
    }
    return 0;
}

很明显,这个程序是没有问题的,至少语法上没有问题。

选项编译时,会出现如下的编译警告:

warning: the address of ‘a’ will always evaluate as ‘true’ [-Waddress]

自然,判断一个数组是否为真是多余的,因为它确定无疑是真的!另一方面,把判断条件换为

if (a != NULL)

时,是不会产生编译警告的。就是这样一个在语法上没有问题的小程序,引起了我的兴趣。我想就此一探指针与数组之间的微妙关系。


数组不是指针

1. 先上定义

数组,用来存储一个固定大小的相同类型元素的顺序集合。在c语言中,数组属于构造数据类型。

有限个类型相同的变量的集合命名为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素或下标变量。用于区分数组的各个元素的数字编号称为下标。

数组是用于储存多个相同类型数据的集合。

指针是一种保存变量地址的变量。

指针(Pointer)的值直接指向(points to)存在存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。


2. 定义与声明的区分

经常会遇到下面一段程序代码的情况:

/* 文件 1 */
int a[10];
/* 文件 2 */
extern int *a;
/* 下面是开始使用a[i]的代码 */

很多人认为这是没有问题的,毕竟,在c语言中数组与指针非常相似,甚至可以互换。

那么,把定义为数组的变量声明为指针使用有什么问题呢?

对于下面的这段代码,没有人怀疑它是错误的:

/* 文件 1 */
int i;
/* 文件 2 */
extern float i;
/* 下面是开始使用i的代码 */

对于定义与声明类型不匹配的情况,没人指望它能正常运行。但是数组与指针的类型也不相同啊,为什么还要指望它能正常运行呢?


c语言中的变量必须有且只有一个定义,但它可以有多个extern声明,定义创建了一个对象,并为其分配了内存空间。而声明只是简单地说明了在其他地方创建的对象的名字,它允许你使用这个名字而已。


编译器为每个变量分配一个地址,地址编译时可知,并且该变量在运行时一直保存在这个地址。相反,存储在变量中的值只有在运行时才可知。当需要从变量中存储的值时,编译器从指定地址读出变量的值在于寄存器。


对于数组,编译器可以直接对其操作,不需要增加指令首先取得具体的地址。对于指针,必须首先在运行时取得它的当前值,然后才能对它进行解除引用操作。


所以,extern char a[] 和 extern char a[10]等价。编译器并不需要知道数组具体长度,它只产生偏离起始地址的偏移地址。从数组中取一个字符,只要简单地从符号表显示的a的地址加上下标,需要的字符就在这个地址中。


而声明为 extern char *p,编译器认为p是一个指针,它指向的对象是一个字符,为了取到这个字符,要先取得地址p的内容,然后把它作为字符的地址,从而取出字符。

可见,指针的访问比数组增加一次额外的提取。

同样的道理,下面的使用会导致什么后果呢?

/* 文件 1 */
int *a;
/* 文件 2 */
extern int a[];
/* 下面是开始使用a的代码 */

外部数组实际定义为指针,但作为数组使用,需要对内存进行直接的引用,但这里编译器所执行的却是对内存进行间接引用。因为编译器认为它是一个指针,而非数组。


再回过头来看一下这个问题,p声明为 extern char *p,而它原本的定义是’char p[10]’时,当用p[i]使用p时,实际上得到的是一个字符,但编译器会把它当成一个指针。把ASCII码解释成地址显然很荒谬,这时程序崩溃你应该感到很高兴,不然,这个bug可能会破坏程序地址空间的内容,出现莫名其妙的错误。


3. 数组和指针的特性对比表

数组指针
保存数据保存数据的地址
直接访问数据间接访问数据,先取出指针的内容,把它作为地址,然后从该地址取出数据
通常用于存储固定数目且类型相同的元素通常用于动态数据结构
隐式分配和删除使用malloc()和free()
自身为数据名通常指向匿名数据


4. 字符串常量

指针和数组的另一个区别是定义字符串常量时。使用指针定义一个字符串常量,如:

char *p = “abcdefg”;

此时,编译器会为字符串常量分配内存。注意,只有字符串常量才是如此,不会为浮点数之类的常量分配空间。在ANSI C中,初始化指针所创建的字符串常量被定义为只读,如果试图通过指针修改这个字符串常量的值,就会出现未定义的错误。

使用数组定义字符串常量时,如:

char a[] = “abcdefg”;

字符串的值是可以修改的。


三、数组是指针

1. 数组是指针的情况

其实,在实际的应用中,数组和指针可以等同的情况要比它们不能互换的场景多得多。例如,牢记以下准则:

所有作为函数参数的数组名总是可以通过编译器转换为指针。

而在任何其他情况下,数组的声明就是数组,指针的声明就是指针,两者不可互换。但在使用数组时,数组总是可以写成指针的形式,两者可以互换。

数组与指针相同的情况总结如下:

- 表达式中的数组名被编译器当作一个指向该数组第一个元素的指针

- 下标总是与指针的偏移量相同

- 在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针


2. 为什么把数组当成指针

基于以下原因,c语言把数组形参当作指针使用:

- 把传递给函数的数组参数转换为指针是由于效率的考虑

- 把作为形参的数组和指针等同起来也是出于效率的原因。c语言中,所有非数组形式的实参均传值,但如果要拷贝整个数组,开销太大,而且绝大多数情况下,也不需要。


3. 数组可以当作指针的情况总结

用a[i]这样的形式对数组访问总是被编译器解释成按*(a+i)的指针访问

指针始终都是指针,它不会改写成数组。但可以用下标形式访问指针。

在作为函数的参数时,一个数组的声明可以看作是指针,作为函数参数的数组始终会被编译器修改成为指向数组第一个元素的指针。

当把一个数组定义为函数参数时,可以把它定义为数组,也可以定义为指针,不管选择哪种方式,在函数内部事实上获得的都是一个指针。

在其他所有情况下,定义和声明必须一致。


四、总结一下

理解了数组和指针何时可以互换何时必须各自使用之后,对于下面语句的含义应该很清楚了:

/* 数组名和指针都可以作为函数的参数使用 */
char a[10];
char *p;
i = strlen(a);
j = strlen(p);

/* 这个语句清晰地展示了数组和指针的可互换性 */
printf("%s %s", a, p);

/* 下面这行语句要仔细理解其原理哦 */
printf("array at location 0x%x holds string %s", a, a);

/* 下面两行可互换的程序也超常见 */
int main(int argv, char *argv[]);
int main(int argv, char **argv);

今后,程序出现错误时认真分析,避免因误用导致的程序错误,遇到指针和数组混合使用的情况时多多总结,相信一定会有更多收获和进步!