数组与指针那点事

前些日子听过了学长关于数组和指针的一些讲解,消除了我的一些困惑,所以在这里我想做一个总结,写下我的所得,并聊聊我的新的看法。


首先,我们先来聊聊数组名

对于数组名呢?绝大多数初学者是感到非常疑惑的,哪怕是学习了好几年自以为懂的人,恐怕也不能够说得明明白白、清清楚楚。那么,它到底是个什么东西呢?

首先,我想先声明一点:数组名绝不是指针,也并不是常量指针。

而后,我想先说一下左值和右值。


我们以一个简单的赋值为例:

x = y;
这个语句的含义是:把y中的值放入x所代表的地址中去
左值 右值
在这个上下文环境里,符号x的含义是x所代表的地址 在这个上下文环境中,符号y的含义是y所代表的地址中的内容
左值在编译时可知,表示存储结果的地方,即内存中的位置 右值直到运行时才可知,表示的是一个“值”

然后就出现了一个奇怪的现象:我们都知道,只有变量才可以作为左值,而数组名是不能作为左值的。


那么数组名不是变量的话,是什么呢?

如果我们查看一下对应的汇编代码,结果是显而易见的。我们会发现,汇编代码里并没有出现数组名这个东西,数组名只是一个符号而已,标识数组在内存中的位置。 但是在某些情况下,编译器会对它进行隐式的转换。或者我们可以像指针类型一样,将数组名理解为一个数据类型——数组类型,表示一段连续的内存空间。

int a[5];
a表示:int [5],即可以存储5个整形元素的一段连续的内存空间。
  • sizeof(a),返回整个数组的长度。
  • &a,得到一个指向整个数组的指针,即int (*)[]。
  • 在函数参数中,数组名被转换为一个普通的指针
  • 在除此之外的其他情况下,数组名将被隐式转换为一个指向数组首元素的常量指针。

这里,我们再给出有效的指针运算方式:

相同类型指针之间的赋值运算。
指针同整数间的加法或减法运算。
指向相同数组中元素的两个指针间的减法或比较运算。
将指针赋为0或指针与0之间的比较运算。
最后注意一点:(void *类型指针可以与其他类型指针混用)

那么,数组和指针到底有什么不同呢?

我们先看一下这样一组声明:

extern int *x;
extern int y[];

第一条语句声明x是一个指向int类型的指针;第二条语句声明y是一个int类型的数组,长度尚未确定,其存储在别处定义。


那么,什么是声明,什么是定义呢?

声明 定义
可以出现在多个地方 只能出现在一个地方
描述对象的类型,用于指代其他地方定义的对象 确定对象的类型并为之分配内存

两者的主要区别在于:

  • 声明:它所说明的并非自身,而是描述其他地方创建的对象。
  • 定义:它为对象分配内存。

总而言之,定义是声明的一个子集。

extern对象声明只是告诉编译器对象的类型和名字,对象的内存分配则在别处进行。


我们再看一个这样的问题:

file 1: int logo[100];
file 2: extern int *logo;

很明显,在file 2中的写法是错误的。正确写法应当是:

file 2: extern int logo[];或者extern int logo[100];

可能有人会问:这有什么错误?数组和指针不是可以互换吗?答案是数组下标表达式总是可以改写为指针加偏移量的表达式。而且也确实存在一种数组和指针完全相同的上下文环境,但并不是所有情况下都是如此。

在[k&r]第二版[99]中,有这样一句描述:作为函数定义的形式参数,char s[] 和 char *s是等价的。


那么,数组和指针到底是如何访问的呢?

编译器为每个变量分配一个地址,这个地址在编译时可知,而且该变量在运行时一直保存于该地址。相反,存储于变量中的值只有在运行时才可知。如果需要用到变量中存储的值,编译器就发出指令从指定地址读入变量值并将它放入寄存器中。

这里的关键之处在于:如果编译器需要一个地址(可能还需要加上偏移量)来执行某种操作,它就可以直接进行操作,并不需要额外的指令首先取得具体的地址。相反,对于指针,必须在运行是取得它的当前值,然后才能对它进行解引用操作。


对数组下标的引用

char s[] = "hello";
c = s[i];

假设编译器符号表中有一个地址100

取出i的值,将它与100相加
取出地址(100 + i)中的内容

这就是为什么extern int logo[100]与extern int logo[]等价的原因。这两个声明都提示logo是一个数组,也就是一个内存地址。编译器并不需要知道数组有多长,因为它只产生偏离起始地址的偏移地址(是否会越界访问取决于程序员本身,编译器并不会进行这样的检查)。访问数组中的一个字符,只需简单地将logo的地址加上下标即可,需要的字符就在该地址中。


对指针的引用

char *p = "hello";
c = *p;

假设编译器符号表中有一个地址1024

取出地址1024中的内容,假设是5076
取出地址5076中的内容,就是'h'

如果定义为指针,却以数组下标方式引用,会发生什么?

我们都知道,数组的引用是对内存的直接引用,而指针是对内存执行间接引用的。下面我们来分析一下对一个指针执行直接引用操作,会发生什么?

char *p = "hello";
c = p[i];

假设编译器符号表中有一个p,它的地址为1024

取出地址1024中的内容,假设为5076
取得i的值,并将它与5076相加
取出地址(5076 + i)中的内容

可见,

char *p = "hello"; ... p[1];
char a[] = "hello"; ... a[1];

这两种情况下,都可以取得字符’e’,但两者的途径非常不一样。

当书写了extern char *p,然后以p[1]的方式来引用其中的元素时,其实质是对数组下标的引用和对指针的引用两种访问方式的组合。

总结一下上面的访问过程:

取得符号表中p的地址,提取存储于此处的指针
把下标所表示的偏移量与指针的值相加,产生一个偏移地址
取得该地址中的内容

如果把p声明为一个指针,那么不管p原先的定义是指针还是数组,都会按照上面所示的三个步骤进行操作,但只有当p原来定义为指针时这个方法才是正确的。


那么,让我们考虑一下定义为数组,却以指针的方式引用,会发生什么?

即下列情况:

file 1: char p[10];
file 2: extern char *p;

当以p[i]的方式访问数组中的内容时:首先,我们取得符号表中p的地址,提取该地址中的内容,很明显会是一个字符。但按照上面的方法,编译器却会把它当作一个地址,把ascii字符解释为地址显然是驴头不对马嘴的。如果真在程序中这样做的话,很可能会污染程序地址空间中的内容,并在以后出现莫名其妙的错误。


下面,我们来说说数组和指针在什么情况下是相同的

在实际应用中,数组和指针可以互换的情况要比两者不可互换的情况更为常见。

  • 数组
    • 声明
      • extern,如extern char a[]; 不能改写成指针的形式
      • 定义,如char a[10]; 不能改写成指针的形式
      • 函数参数,你可以随自己喜欢,选择数组或指针形式
    • 在表达式中使用
      • 如c = a[i]; 你可以随自己喜欢,选择数组或指针形式

所有作为函数参数的数组名总是可以通过编译器转换为指针。在其他情况下,数组的声明就是数组,指针的声明就是指针,两者不能混淆。但在使用数组时,两者是可以互换的。


然后,我们来解释三个规则:

1. 在表达式中,对数组的引用总是可以改写为对指针的引用

int a[10], *p, i = 2;

我们可以通过以下任何一种方式访问a[i]

p = a; p[i];
p = a; *(p + i);
p = a + i; *p;

事实上,可以采用的方法更多。对数组的引用a[i]在编译时总是被编译器改写为*(a + i)的形式。C标准要求编译器必须具有这个概念性的行为。也许遵循这个规则的捷径就是记住 [] 表示一个取下标运算符,就像 + 表示加法运算符一样。

取下标运算符取一个整数和一个指向T类型的指针,所产生的结果类型是T,一个在表达式中的数组名就变成了一个指针(除过本文开始描述的几种特殊情况)。你只要记住:在表达式中,指针和数组是可以互换的,因为它们在编译器中的最终形式都是指针,而且都可以进行取下标操作。就像加法一样,取下标操作符的操作数是可以互换的,a[5] 和 5[a] 这两种情况下都是正确的。第二种情况除了能把新手搞晕以外,实在没什么实际意义。

编译器自动把下标值的步长调整为数组元素的大小,在对地址进行加法操作之前,编译器会负责计算每次增加的步长,这也就是指针之所以有类型限制的原因所在。


2. C语言把数组下标作为指针的偏移量

在通常情况下,使用指针比使用数组”更有效率“这个说法是错误的。现代的编译器常常会对代码进行优化,两种访问方式所产生的代码不具有显著的差别。但不管怎样,数组下标是定义在指针的基础上的,所以编译器常常将它转换为更有效率的指针表达式形式。

C语言把数组下标改写成指针加偏移量的形式的根本原因是:指针和偏移量是底层硬件所使用的基本模型。


3. 作为函数参数的数组名等同于指针

我们首先解释两个术语:

  • 实参:在实际调用一个函数时传递给它的值
  • 形参:它是一个变量,在函数定义或函数声明中定义

当用一个数组名作为参数时,由于效率原因,编译器只向该函数传递数组的地址,而不是整个数组的拷贝。


那么,数组形参是如何被引用的呢?

fun(char p[]);
fun(char *p);

假设编译器符号表显示p可以取地址,从堆栈指针偏移一定位置

从堆栈指针偏移一定位置的地方找到函数的活动记录(栈桢),取出实参,假设为1024
取得i的值,并与1024相加
取出地址(1024 + i)中的内容

猜你喜欢

转载自blog.csdn.net/qq_41145192/article/details/80847818