一、C的数据类型的模型
1. 什么是指向数组的指针
“数组”和“指针”都是派生类型,它们都是由基本类型开始重复派生生成的。也就是说,派生出“数组”后,再派生出“指针”,就可以生成“指向数组的指针”。在表达式中,数组可以被解读成指针。但是,这不是“指向数组的指针”,而是“指向数组初始元素的指针”。
// 实际地声明一个“指向数组的指针”:
int (*array_p)[3]; // array_p 是指向 int 数组(元素个数3)的指针。
根据 ANSI C 的定义,在数组前加上&,可以取得“指向数组的指针”(这里是“数组可以解读成指向它初始元素的指针”这个规则的一个例外)。因此,
int array[3];
int (*array_p)[3];
array_p = &array; // 数组添加&,取得“指向数组的指针”
这样的赋值是没有问题的,因为类型相同。 可是,如果进行 array_p = array;
这样的赋值,编译器就会报出警告。“指向 int 的指针”和“指向 int 的数组(元素个数 3)的指针”是完全不同的数据类型。 但是,从地址的角度来看,array 和&array`也许就是指向同一地址。但要说起它们的不同之处,那就是它们在做指针运算时结果不同。
在我的机器上,因为 int 类型的长度是 4 个字节,所以给“指向 int 的指针”加 1,指针前进 4 个字节。但对于“指向 int 的数组(元素个数 3)的指针”,这个指针指向的类型为“int 的数组(元素个数 3)”,当前数组的尺寸为 12 个字节(如果 int 的长度为 4 个字节),因此给这个指针加 1,指针就前进 12 个字节。
2. C语言中不存在多维数组
在 C 中,可以通过下面的方式声明一个多维数组: int hoge[3][2]
按照C 的声明的解读方法,上面的声明应该怎样解读呢?是“int 类型的多维数组”吗? 这是不对的。应该是“int 的数组(元素个数 2)的数组(元素个数 3)”。也就是说,即使 C 中存在“数组的数组”,也不存在多维数组。“数组”就是将一定个数的类型进行排列而得到的类型。“数组的数组”也只不过是派生源的类型恰好为数组。
对于下面的这个声明: int hoge[3][2];
可以通过 hoge[i][j]的方式去访问,此时,hoge[i]是指“int 的数组(元素个数 2)的数组(元素个数 3)”中的第 i 个元素,其类型为“int 数组(元素个数 2)”。当然,因为是在表达式中,所以在此时此刻,hoge[i]也可以被解读成“指向 int 的指针”。
那么,如果将这个“伪多维数组”作为函数的参数进行传递,会发生什么呢?
试图将“int 的数组”作为参数传递给函数,其实可以直接传递“指向 int 的指针”。这是因为在表达式中,数组可以解释成指针。因此,在将“int 的数组”作为参数传递的时候,对应的函数的原型如下:void func(int *hoge);
在“int 的数组(元素个数 2)的数组(元素个数 3)”的情况下,假设使用同样的方式来考虑, int 的数组(元素个数 2)的数组(元素个数 3) ,其中下划线部分,在表达式中可以解释成指针,所以可以向函数传递指向 int 的数组(元素个数 2)的指针。这样的参数,说白了它就是“指向数组的指针”。
也就是说,接收这个参数的函数的原型为: void func(int (*hoge)[2]);
直到现在,有很多人将这个函数原型写成下面这样:
void func(int hoge[3][2]);
或者这样:
void func(int hoge[][2]);
其实,void func(int (*hoge)[2]);
就是以上两种写法的语法糖,它和上面两种写法完全相同。
3. 函数类型派生
函数类型也是一种派生类型,“参数(类型)”是它的属性。可是,函数类型和其他派生类型有不太相同的一面。 无论是 int 还是 double,亦或数组、指针、结构体,只要是函数以外的类型,大体都可以作为变量被定义。而且,这些变量在内存占用一定的空间。因此,通过 sizeof 运算符可以取得它们的大小。 像这样,有特定长度的类型,在标准中称为对象类型。
可是,函数类型不是对象类型。因为函数没有特定长度。 所以 C 中不存在“函数类型的变量”(其实也没有必要存在)。 数组类型就是将几个派生类型排列而成的类型。因此,数组类型的全体长度为: 派生源的类型的大小×数组的元素个数。
可是,函数类型是无法得到特定长度的,所以从函数类型派生出数组类型是不可能的。也就是说,不可能出现“函数的数组”这样的类型。 可以有“指向函数的指针”类型,但不幸的是,对指向函数类型的指针不能做指针运算,因为我们无法得到当前指针类型的大小。 此外,函数类型也不能成为结构体和共用体的成员。另外,函数类型也不可以从数组类型派生。 可以通过“返回~的函数”的方式派生出函数类型,不过在 C 中,数组是不能作为函数返回值返回的。
4. 不完全类型
不完全类型指“函数之外、类型的大小不能被确定的类型”。
总结一下,C 的类型分为:
对象类型(char、int、数组、指针、结构体等);
函数类型;
不完全类型(结构体标记的声明就是一个不完全类型的典型例子)。
在 C 标准中,void 类型也被归类为不完全类型。
二、表达式
1. “左值”是什么——变量的两张面孔
假设有下面这样一个声明: int hoge;
因为此时 hoge 是 int 类型,所以,只要是可以写 int 类型的值的地方,hoge 就可以像常量一样使用。 比如,
将 5 赋予 hoge 之后,下面的语句
piyo = hoge * 10;
理所当然地可以写成
piyo = 5 * 10;
但是,在 hoge = 10;
的情况下,即使此时 hoge 的值为 5,
5 = 10;
这样的置换也是非法的。
也就是说,作为变量,它有作为“自身的值”使用和作为“自身的内存区域”使用两种情况。
表达式代表某处的内存区域的时候,我们称当前的表示式为左值(lvalue);相对的是,表达式只是代表值的时候,我们称当前的表达式为右值(rvalue)。
2. 将数组解读成指针
正如在前面翻来覆去提到的那样,在表达式中,数组可以解读成指针。
int hoge[10];
以上的声明中,hoge 等同于&hoge[0]。
hoge 原本的类型为“int 的数组(元素个数 10)”,但并不妨碍将其类型分类“数组”变换为“指针”。
此外,数组被解读成指针的时候,该指针不能作为左值。
这个规则有以下的例外情况:
数组为 sizeof 运算符的操作数
在通过“sizeof 表达式”的方式使用 sizeof 运算符的情况下,如果操作数是“表达式”,此时即使对数组使用sizeof,数组也会被当成指针,得到的结果也只是指针自身的长度。照理来分析,应该是这样的吧?可是,当数组成为 sizeof 的操作数时,“数组解读为指针”这个规则会被抑制,此时返回的是数组全体的大小。
数组为&运算符的操作数
通过对数组使用&,可以返回指向整体数组的指针。
初始化数组时的字符串常量
我们都知道字符串常量是“char 的数组”,在表达式中它通常被解读成“指向 char 的指针”。其实,初始化char 的数组时的字符串常量,作为在花括号中将字符用逗号分开的初始化表达式的省略形式,会被编译器特别解释。
3. 数组和指针相关的运算符
在标准中,似乎并没有定义->运算符的名称,现实中有时它被称为“箭头运算符”。 通过指针访问结构体的成员的时候,会使用->运算符。
p->hoge;
是 (*p).hoge;
的语法糖。
利用p 的,从指针 p 获得结构体的实体,然后引用成员 hoge。
4. 多维数组
int hoge[3][5];
对于上面这个“数组的数组”,使用 hoge[i][j]这样的方式进行访问,如下图
hoge 的类型为“int 的数组(元素个数 5)的数组(元素个数 3)”。 尽管如此,在表达式中数组可以被解读成指针。因此,hoge 的类型为“指向 int 的数组(元素个数 5)的指针”。
hoge[i]是*(hoge + i)的语法糖。 给指针加上 i,就意味着指针前移它指向的类型×i 的距离。
hoge 指向的类型为“int 的数组(元素个数 5)”,因此,hoge + i 让指针前移了 sizeof(int[5])×i 的距离。
通过*(hoge + i)
中的**,去掉一个指针,*(hoge + i)
的类型就是“指向 int 的数组(元素个数 5)”。 尽管如此,由于在表达式中,数组可以解读成指针,所以*(hoge + i)
的最终类型为“指向 int 的指针”。
(*(hoge + i))[j]
和*((*(hoge + i)) + j)
其实是相等的,因此,(*(hoge + i))[j]
就是“对指向 int 的指针加上 j 后得到的地址上的内容”,其类型为 int。