数组与指针---都是"退化"惹的祸

来源:互联网 发布:画算法流程图 编辑:程序博客网 时间:2024/05/22 00:34

个人的浅显认识, 欢迎批评指正.

1. 什么是数组类型?

下面是C99中原话:
An array type describes a contiguously allocated nonempty set of objects with a
particular member object type, called the element type.36) Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T , the array type is sometimes called ‘‘array of T ’’. The construction of an array type from an element type is called ‘‘array type derivation’’.

很显然, 数组类型也是一种数据类型, 其本质功能和其他类型无异:定义该类型的数据所占内存空间的大小以及可以对该类型数据进行的操作(及如何操作).

2. 数组类型定义的数据是什么?它是变量还是常量?
char s[10] = "china";
在这个例子中, 数组类型为 array of 10 chars(姑且这样写), 定义的数据显然是一个数组s.

下面是C99中原话:
  An lvalue is an expression with an object type or an incomplete type other than void; if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const-qualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a const-qualified type.

看了上面的定义, 大家应该明白了modifiable lvalue和lvalue的区别, 大家也应该注意到array type定义的是lvalue而不是modifiable lvalue.所以说s是lvalue.

s指代的是整个数组, s的内容显然是指整个数组中的数据, 它是china/0****(这里*表示任意别的字符).s的内容是可以改变的, 从这个意义上来说, s显然是个变量.

3. 数组什么时候会"退化"

下面是C99中原话:
Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue.

上面这句话说的很清楚了, 数组在除了3种情况外, 其他时候都要"退化"成指向首元素的指针.
比如对 char s[10] = "china";
这3中例外情况是:
(1) sizeof(s)
(2) &s;
(3) 用来初始化s的"china";

除了上述3种情况外,s都会退化成&s[0], 这就是数组变量的操作方式

4. 数组与指针有什么不同?
4.1 初始化的不同
char s[] = "china";
char *p = "china";

在第一句中,以&s[0]开始的连续6个字节内存分别被赋值为:
'c', 'h', 'i', 'n', 'a', '/0'

第二句中,p被初始化为程序data段的某个地址,该地址是字符串"china"的首地址

4.2 sizeof的不同

sizeof就是要求一种数据(类型)所占内存的字节数. 对于4.1中的s和p
sizeof(s)应为6, 而sizeof(p)应为一个"指针"的大小.

这个结果可以从1中对于数组类型的定义和3中数组什么时候不会"退化"中得出来.

4.3 &操作符

对于&操作符, 数组同样不会退化.
4.1中的s和p分别取地址后,其意义为:
&s的类型为pointer to array of 6 chars.
&p的类型为pointer to pointer to char.

4.4 s退化后为什么不可修改

除3种情况外,数组s在表达式中都会退化为"指向数组首元素的指针", 既&s[0]

举个例子
int a;
(&a)++; //你想对谁++? 这显然是不对的

对(&s[0])++操作犹如(&a)++, 同样是不对的,这就导致退化后的s变成不可修改的了.

4.5 二维数组与二级指针

char s[10];与char *p;
char s2[10][8];与char **p2;

s与p的关系,s2与p2的关系,两者相同吗?
紧扣定义的时候又到了.
除3种情况外,数组在表达式中都会退化为"指向数组首元素的指针".

s退化后称为&s[0], 类型为pointer to char, 与p相同
s2退化后称为&s2[0], 类型为pointer to array of 8 chars, 与p2不同

4.6 数组作为函数参数

毫无疑问, 数组还是会退化.

void func(char s[10]); <===> void func(char *s);

void func(char s[10][8]); <===> void func(char (*s)[8]);

4.7 在一个文件中定义char s[8], 在另外一个文件中声明extern char *s. 这样可以吗?

---------file1.c---------
char s[8];

---------file2.c---------
extern char *s;


答案是不可以. 一般来说,在file2.c中使用*s会引起core dump, 这是为什么呢?

先考虑int的例子.
---------file1.c---------
int a;

---------file2.c---------
extern int a;

file1.c和file2.c经过编译后, 在file2.o的符号表中, a的地址是尚未解析的
file1.o和file2.o在链接后, file2.o中a的地址被确定.假设此地址为0xbf8eafae

file2.o中对该地址的使用,完全是按照声明extern int a;进行的,即0xbf8eafae会被认为是整形a的地址
比如 a = 2; 其伪代码会对应为 *((int *)0xbf8eafae) = 2;

现在再看原来的例子.

---------file1.c---------
char s[8];

---------file2.c---------
extern char *s;

同样, file1.c和file2.c经过编译后, 在file2.o的符号表中, s的地址是尚未解析的
file1.o和file2.o在链接后, file2.o中s的地址被确定.假设此地址为0xbf8eafae

file2.o中对该地址的使用,完全是按照声明extern char *s;进行的,即0xbf8eafae会被认为是指针s的地址
比如 *s = 2; 其伪代码会对应为 *(*((char **)0xbf8eafae)) = 2;

*((char **)0xbf8eafae)会是什么结果呢?
这个操作的意思是:将0xbf8eafae做为一个二级字符指针, 将0xbf8eafae为始址的4个字节(32位机)作为一级字符指针
也就是将file1.o中的s[0], s[1], s[2], s[3]拼接成一个字符指针.


那么*(*((char **)0xbf8eafae)) = 2;的结果就是对file1.o中s[0], s[1], s[2], s[3]拼接成的这个地址对应
的内存赋值为2.
这样怎么会正确呢?


下面看看正确的写法:

---------file1.c---------
char s[8];

---------file2.c---------
extern char s[];


同样, file1.c和file2.c经过编译后, 在file2.o的符号表中, s的地址是尚未解析的
file1.o和file2.o在链接后, file2.o中s的地址被确定.假设此地址为0xbf8eafae

file2.o中对该地址的使用,完全是按照声明extern char s[];进行的,即0xbf8eafae会被认为是数组s的地址
比如 *s = 2; 其伪代码会对应为 *(*((char (*)[])0xbf8eafae)) = 2;

*((char (*)[])0xbf8eafae)会是什么结果呢?
这个操作的意思是:将0xbf8eafae做为一个指向字符数组的指针, 然后对该指针进行*操作.
这就用到了数组的一个重要性质:
对于数组 char aaa[10];来说, &aaa[0], &aaa, *(&aaa)在数值上是相同的(其实, *(&aaa)之所以在程序中
会在值上等于&aaa[0], 这也是退化的结果: *(&aaa)就是数组名aaa, aaa退化为&aaa[0]).
所以, *((char (*)[])0xbf8eafae)的结果在值上还是0xbf8eafae, 在类型上退化成"指向数组首元素的指针"


那么*(*((char (*)[])0xbf8eafae)) = 2;
其伪代码就成为*((char *)0xbf8eafae) = 2; 即将数组s的第一个元素设为2


5. 小结论

(a). 数组类型是一种特殊类型, 它定义的是数组变量, 是lvalue但不是modifiable lvalue
(b). 除了3种情况外(sizeof, &, 用做数组初始化的字符串数组), 数组会退化成"指向数组首元素的指针"
(c). 不要将数组名简单的看作不可修改的相应的指针, 它们还是有很多不同的

 

 

1.   引言  
指针是C/C++语言的特色,而数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。于是乎,很多
程序设计者就被搞糊涂了。而许多的大学老师,他们在C   语言的教学过程中也错误得给学生讲解:“数组名就是指针”。
很幸运,我的大学老师就是其中之一。时至今日,我日复一日地进行着C/C++项目的开发,而身边还一直充满这样的
程序员,他们保留着“数组名就是指针”的误解。  
想必这种误解的根源在于国内某著名的C   程序设计教程。如果这篇文章能够纠正许多中国程序员对数组名和指针的误
解,笔者就不甚欣慰了。借此文,笔者站在无数对知识如饥似渴的中国程序员之中,深深寄希望于国内的计算机图书
编写者们,能以“深入探索”的思维方式和精益求精的认真态度来对待图书编写工作,但愿市面上多一些融入作者思
考结晶的心血之作!  
2.   魔幻数组名  
请看程序(本文程序在WIN32   平台下编译):  
1.   #include   <iostream.h>  
2.   int   main(int   argc,   char*   argv[])  
3.   {
4.   char   str[10];  
5.   char   *pStr   =   str;  
6.   cout   < <   sizeof(str)   < <   endl;  
7.   cout   < <   sizeof(pStr)   < <   endl;
8.   return   0;
9.   }
2.1   数组名不是指针  
我们先来推翻“数组名就是指针”的说法,用反证法。  
证明   数组名不是指针
假设:数组名是指针;  
则:pStr和str   都是指针;  
因为:在WIN32   平台下,指针长度为4;  
所以:第6   行和第7   行的输出都应该为4;  
实际情况是:第6   行输出10,第7   行输出4;  
所以:假设不成立,数组名不是指针  
2.2   数组名神似指针  
上面我们已经证明了数组名的确不是指针,但是我们再看看程序的第5   行。该行程序将数组名直接赋值给指针,这显
60  


得数组名又的确是个指针!  
我们还可以发现数组名显得像指针的例子:  

1.   #include   <string.h>  
2.   #include   <iostream.h>  
3.   int   main(int   argc,   char*   argv[])  
4.   {
5.   char   str1[10]   =   "I   Love   U";
6.   char   str2[10];  
7.   strcpy(str2,str1);
8.   cout   < <   "string   array   1:   "   < <   str1   < <   endl;  
9.   cout   < <   "string   array   2:   "   < <   str2   < <   endl;  
10.   return   0;  
11.   }  
标准C   库函数strcpy   的函数原形中能接纳的两个参数都为char   型指针,而我们在调用中传给它的却是两个数组名!
函数输出:  
string   array   1:   I   Love   U  
string   array   2:   I   Love   U  
数组名再一次显得像指针!  
既然数组名不是指针,而为什么到处都把数组名当指针用?于是乎,许多程序员得出这样的结论:数组名(主)是(谓)
不是指针的指针(宾)。  
整个一魔鬼。  
3.   数组名大揭密  
那么,是揭露数组名本质的时候了,先给出三个结论:  
(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;
(2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量;
(3)指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址!
3.1   数组名指代一种数据结构:数组  
现在可以解释为什么第1   个程序第6   行的输出为10   的问题,根据结论1,数组名str   的内涵为一种数据结构,即一
个长度为10   的char   型数组,所以sizeof(str)的结果为这个数据结构占据的内存大小:10字节。
再看:  
1.   int   intArray[10];
2.   cout   < <   sizeof(intArray)   ;
第2   行的输出结果为40(整型数组占据的内存空间大小)。  
如果C/C++程序可以这样写:  
1.   int[10]   intArray;
2.   cout   < <   sizeof(intArray)   ;
我们就都明白了,intArray定义为int[10]这种数据结构的一个实例,可惜啊,C/C++目前并不支持这种定义方式。  
3.2   数组名可作为指针常量  
根据结论2,数组名可以转换为指向其指代实体的指针,所以程序1   中的第5   行数组名直接赋值给指针,程序2   第7
行直接将数组名作为指针形参都可成立。  
下面的程序成立吗?  
1.   int   intArray[10];
2.   intArray++;  
读者可以编译之,发现编译出错。原因在于,虽然数组名可以转换为指向其指代实体的指针,但是它只能被看作一个
61  


指针常量,不能被修改。  
而指针,不管是指向结构体、数组还是基本数据类型的指针,都不包含原始数据结构的内涵,在WIN32   平台下,sizeof
操作的结果都是4。  
顺便纠正一下许多程序员的另一个误解。许多程序员以为sizeof是一个函数,而实际上,它是一个操作符,不过其
使用方式看起来的确太像一个函数了。语句sizeof(int)就可以说明sizeof   的确不是一个函数,因为函数接纳形参
(一个变量),世界上没有一个C/C++函数接纳一个数据类型(如int)为“形参”。  

3.3   数据名可能失去其数据结构内涵  
到这里似乎数组名魔幻问题已经宣告圆满解决,但是平静的湖面上却再次掀起波浪。请看下面一段程序:  
1.   #include   <iostream.h>  
2.   void   arrayTest(char   str[])
3.   {
4.   cout   < <   sizeof(str)   < <   endl;  
5.   }
6.   int   main(int   argc,   char*   argv[])  
7.   {
8.   char   str1[10]   =   "I   Love   U";
9.   arrayTest(str1);
10.   return   0;
11.   }  
程序的输出结果为4。不可能吧?  
4,一个可怕的数字,前面已经提到其为指针的长度!  
结论1   指出,数据名内涵为数组这种数据结构,在arrayTest   函数体内,str   是数组名,那为什么sizeof   的结果却
是指针的长度?这是因为:  
(1)数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;
(2)很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
所以,数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4
个字节的平民。  
以上就是结论4。