数组和指针——都是“退化”惹的祸

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). 不要将数组名简单的看作不可修改的相应的指针, 它们还是有很多不同的

posted @ 2010-09-20 15:07  云说风轻  阅读(3516)  评论(0编辑  收藏  举报