c语言中数组和指针的区别与联系

 

本文参考<c 专家编程>

1.引例1:

extern int *x;
extern int x[];

以上两句代码可以看出什么区别么?或许你觉得都一样嘛。但是这里还是得强调,不一样。
第一句,声明了一个类型为整型指针的变量;
第二句,声明了一个长度未知的整型数组,只不过我们是在另一个地方定义该数组的定义啦。

 

 

2.引例2:

我们这里有俩文件:
file1.c:
int joy[100];
file2.c:
extern int *joy;

这里文件file1定义了长度为100的整型数组joy,file2声明了一个整型变量joy。这样的程序可能编译通过,但任何对joy变量的引用,运行时都会失败。(我在cygwin里运行提示:segmentation fault错误.)

数组和指针非常相似,但确实不同。这就好像定义了一个int变量,然后在别处给他的声明却是float型的,类型不匹配。
指针和数组是可以互换的,原因是对数组的引用总可以写成对指针的引用,也确实存在一种指针和数组的定义完全相同的上下文环境。可是呢, 这只是数组的一种很普通的用法,并非所有情况都如此。有些程序员却天真的以为所有情况下指针和数组都是等同的。上面的例子就是证明。

 

3.c语言的“声明”与"定义"

有必要搞清楚这两个词的意思。

  • c语言的对象(例如函数和变量,区别c++)有且只有一个定义,但可以有多个extern的声明。
  • 定义是一种特殊的声明,他创建了一个对象,声明则简单的说明在其他地方该对象被创建了,因此可以使用此对像的名称。

我们总结下二者区别:

定义          只能出现在一个地方       去顶对象类型并分配内存,用于创建新的对象
声明          可以多次出现                   描述对象的类型,用于指代其他地方定义的对象。


值得一提的是对于多维数组,声明是需要提供除最左边第一维之外的其他维的长度。

 

 

4.数组和指针的访问

x = y;
这条赋值语句很常见吧。可是您知道x和y具体代表什么意思吗?
就上面简单的赋值语句的而言,符号x表示变量x的地址,这被称为“左值”,左值表示存储结果的地方。
符号y表示y地址的内容。这被称为右值。
c语言里引入了“可修改的左值”这个术语。表示左值允许出现在赋值语句的左边。这个术语与数组名区分,数组名也用于确定对象在内存的位置,也是左值,但是他不能作为赋值的对象,因为数组名是左值但不可修改。c标准规定只能给修改的东西赋值。编译器为每个变量分配一个地址(左值),这个地址在编译时可知,而且该变量在运行时一直保存于这个地址。相反,存储于变量中的值(右值)只有在运行时才可知。如果需要存储变量中存储的值,编译器就发出指令从指定地址读入变量值并存入寄存器中。
这里的关键之处在于每个符号的地址在编译时可知。如果编译器想执行某些操作,直接在这个地址基础上加个偏移量就ok了,不需要具体的地址。相反,对于指针首先得取得他的当前值(一个地址),然后再对它解除引用操作。举个例子:

 

 

 

char a[9]=“abcdefg",c=a[2];

 

 

此时编译器符号表可能具有一个地址9980是数组a首地址, 获取a[2],怎么执行:
步骤1:首先计算2+9980
步骤2:取地址9982的内存的内容。

 

----------------------------------------------------------

 

|    a   |     b    |     c    |    d    |     e     |     f    | ....
----------------------------------------------------------
9980     +1        +2        +3
 
图A    数组下表的引用


这就是为什么extern char a[] 和extern char a[100]等价的原因。这两个声明都表示a是一个数组,也就是一个内存地址,数组内的字符可以从这个地址找到。编译器并不需要知道数组总共多长,因为由他产生的偏移地址即可获取到对应字符。
相反,如果声明extern char *p;他告诉编译器,这是一个指针,指向的对象是一个字符,为取得这个字符,必须得到p的内容--是一个地址,然后在从这个地址中取得字符。
指针的访问比较灵活,但额外增加了一次地址的提取。

 

 

char *p, c=*p;

 

 

编译器符号表只有一个符号p,他的地址可能是4624:
运行时步骤1:取地址4624的内容,就是5081
运行时步骤2:去地址5081的内容。
----------------------------------                                    --------------
|   5   |   0   |   8   |   1   |                     -》                |                  |
---------------------------------                                     ---------------
4624                                                                                 5081
 
图B 对指针的引用
 


5.当”定义为指针,但以数组方式引用“发生了什么
现在让我们看一下当一个外部数组实际定义是一个指针,但却是以数组方式对其引用,会引起的问题。需要对内存进行直接的引用(图A所示),但这是编译器执行的确实对内存进行间接引用(图B所示),之所以会这样,是因为我们告诉编译器我们拥有的是一个指针,如图C所示:

 


char *p="abcdefg",c=p[2];

 

编译器符号表具有一个p,地址可能为4624:
运行时步骤1:取地址4624的内容,即”5081“
运行时步骤2:计算2+5081=5083,产生一个新地址。
运行时步骤3:取地址5083的内存单元内容。
 
-------------------------------------                             ------------------------------------------------------------
|   5   |   0   |   8   |   1   |                       -》           |    'a'   |    'b'   |   'c'    |    'd'    |   'e'   |   'f'   |   'g'    |
-------------------------------------                               ------------------------------------------------------------
4624                                                                      5081      +1      +2       +3                                       
 
图C 对指针进行下标引用


对照图C的访问方式:
char *p="abcdefgh"; p[3]
和图A的访问方式:
char p[]="abcdefgh"; p[3]
这两种情况下都能取得'd',但途径非常不一样。

当书写了extern char *p;用p[3]来引用其中的元素时,实质是图A和图B的结合。首先,进行图B的所示的间接引用,然后如图A所示用下标作为偏移量进行直接访问,描述成如下3步:
1.取得符号表中的p的地址,提取存储于此处的指针。
2.把下标所表示的偏移量与指针的值相加,产生一个地址。
3.访问上面的地址表示的内存,取得字符。


编译器已被告知p是指向字符的指针(自然,数组定义会告诉编译器p是一个字符序列)。p[2]表示从p所指地址开始,前进2步,每一步都是sizeof(char)长。其他类型的指针,步长肯定也不一样。
既然把p声明为指针,那么管它在定义时是数组还是指针,像p[3]之类的引用都会按照上述3步进行。但只有当p被定义为指针是才正确。
那么考虑之前提到的引例1:
file1.c:
int joy[100];
file2.c:
extern int *joy;

当用joy[i]这种形式提取这个声明的内容时,实际得到的是一个字符。说了,声明joy为指针,就会按照如上3步,编译器把joy当做指针,可是其定义确是一个数组。在第一步时,
实际提取的是一个字符,却被当成指针,然后继续后续步骤。很明显把ascII字符当做指针,然后再做其他的操作,如果程序挂掉了,您应该庆幸吧。否则,很可能程序跑飞,出现莫名其妙的bug。

 

 

建议:使声明与定义匹配
 

指针的外部声明和数组定义不匹配问题很容易修正,就是使定义与声明匹配即可。
引例2在file2中应该声明为extern int joy[];即可。

指针和数组的区别总结:

前面我们定义一个数组:
int joy[100];
然后外部声明:
extern int joy[];
这表示joy数组分配了一个长度为100的int空间。
然后我们定义一个整型变量:
int *raisin;
这表示定义了一个"指针变量"(一般都会简称"指针"),如所有变量一样,首先会在内存中申请一个4字节(一般指针类型的大小)的空间分配给它,如果是全局的,这个空间的位置永远不会变化。然后,这个空间所放的数据肯定是一个整型变量的地址(你要强制转换一个char*型的变量或者一个整数为此类型我也没办法),这个地址是可以变化的。形如前面的joy数组的地址是不可变化的。

其他区别:

表1
----------------------------------------------------------------------------------------------------------------------------------------------
指针                                                                                  |                                     数组
---------------------------------------------------------------------------------------------------------------------------------------------
保存数据的地址                                                              |         保存数据
----------------------------------------------------------------------------------------------------------------------------------------------
间接访问数据,首先取得指针的内容把它作为地址  |         直接访问数据,a[i]只是简单的以a+i为地址取得数据
,然后从这个地址提取数据。如果指针有一个下标  |
,就把指针的内容加上下标作为地址,                      |
从中提取数据                                                                  |    
----------------------------------------------------------------------------------------------------------------------------------------------
通常用于动态数据结构                                                  |        通常用于存储固定数目且数据类型相同的元素
----------------------------------------------------------------------------------------------------------------------------------------------
相关的函数为malloc(),free()                                        |        隐式非配和删除
---------------------------------------------------------------------------------------------------------------------------------------------
通常指向匿名数据                                                          |        自身即为数据名
----------------------------------------------------------------------------------------------------------------------------------------------


数组和指针都可以在他们的定义中用字符串常量进行初始化,尽管如此底层机制却不尽相同。
定义指针时,编译器并不为指针所指向的对象分配空间,他只是分配指针本身的空间,除非在定义是同时赋给指针一个字符串常量进行初始化,例如:

char *p="breakfruit";

注意,只有字符串常量如此,不能指望浮点数之类的常量分配空间:例如:

float *p=3.141; /* 这是错误的,编译通不过 */

ansi c中,初始化字符串常量是只读额,如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。有些编译器中字符串常量就被存在只读存储区,防止修改。
数组也可以用字符串常量初始化:

char a[]="abcdefg";
与指针相反,由字符串常量初始化的数组是可以修改的,其中,单个字符在以后可以改变,比如:
strncpy(a, "cba", 3);
就将数组a修改为"cbadefg"了。

指针是c语言最难正确理解和使用的部分之一,可能只有声明的语法比它跟烦了。然而,他们也是c语言最重要的部分之一。专业c程序员必须熟练掌握malloc()函数,并且学会用指针操纵匿名内存。

--------------------------------------------------------------------------------
补充:以上大部分内容摘自《c专家编程》的第4章。

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值