![322d87c8d0a3604992aed92236109fcd.png](https://img-blog.csdnimg.cn/img_convert/322d87c8d0a3604992aed92236109fcd.png)
指针是多个概念的简称,有文章把指针数据简称为指针,也有文章把指针变量简称为指针,还有文章把指针数据的类型简称为指针。本文不使用指针简称,尽可能使用特定名称来描述具体概念。
语言和机器
不言而明,C语言使用符号来描述具体的机器运作,正如人类使用语言来表达行为活动。编译器便是将符号映射成机器部件或机器运动的实体程序,且编译器的行为是由c标准规则规定的。编译器的强大使我们往往忽略了符号转换成机器实体的过程。如我们经常会把变量名和数值绑定,事实上变量是C语言层面的内容,它与机器的存储单元对应绑定,变量名通过左值转换获得变量内的数值。
![1d13534b694370c18a70ff01bc6d8dea.png](https://img-blog.csdnimg.cn/img_convert/1d13534b694370c18a70ff01bc6d8dea.png)
( 变量字面有太多含义,在 C 语言标准中使用 object (对象)这一词汇替代了变量。)正因为变量和存储单元是语言层面和机器层面绑定的两个概念,又因为存储单元这种实体概念更符合人类认知,在解释 C 语言层面的原理时,利用机器层面的存储单元概念反而更被广泛接受和使用。
左值和值转换
通过变量的标识符即变量名可以对应一个变量或存储单元。有时变量没有对应的名称,但可以通过表达式对应表示,我们称能够标注变量的表达式为左值,变量的标识符是一个基本表达式,自然,变量的标识符也是左值。左值是语言层面的概念。
int a;
a = 1; // 变量标识符 a 是左值
int b[5] = {0};
b[0] = a; // 数组下标表达式 b[0] 是左值
int *p = &a;
*p = 3; // 间接访问表达式 *p 是左值
因为左值是变量的符号(当然也可以认为是机器层面存储单元的符号),通过左值获得变量内的数值这个过程称为左值转换。上述程序中的 b[0] = a;
左值 a 通过左值转换获得变量的数值 1 。左值转换属于 C 语言中值转换的一种,还有两种值转换在数组和函数应用中起到极为重要的作用。
字节的地址和对象的地址
既然变量和存储单元是绑定的,那么存储单元含有的属性,变量也应该含有。一个存储单元至少拥有名字和大小两个属性,那么变量自然也拥有这两个属性。在随后我们分析存储单元属性时,相当于在分析变量的属性。
存储器在机器中抽象认为是字节的数组,数组中的每个元素即字节都有各自的名字,通常我们使用整数来标注这些名字,这便是我们经常所说的地址。注意这里的地址是每一个字节的名字,为了区分我们把它称为字节的地址 AB ,见下图(a)图。
![c22d41f93a5f454657b92dc61ee4f209.png](https://img-blog.csdnimg.cn/img_convert/c22d41f93a5f454657b92dc61ee4f209.png)
C 语言中大部分类型的数据都占用多个字节,如 int 类型就会占用四个字节。我们把一个数据占用一个或多个字节的区域称为存储单元。存储单元的属性又是如何定义的?在 C 语言中把存储单元的最低位的字节的地址定义为该单元的名字,我们把它称为 对象的地址 AO ,见上图(b)(c)图。AO 和 AB 的区别在它所代表的那个存储单元的大小是不定的。当机器访问存储单元时需要两个属性,通过 AO 找到对应的存储单元之外,还需要给出该存储单元的大小。在机器层面的指令提供大小的方法是:每个机器指令都会有针对一个字节,两个字节,四个字节,八个字节四种指令变型(ATT)。
直接访问和间接访问
回到语言层面,在 C 代码中如何通过符号访问变量? C 语言提供了两种方式,一种是直接访问,我们为某个变量绑定一个标识符,通过访问该标识符来直接访问对应的变量(也可以认为通过标识符访问存储单元)。回想变量是存储单元的映射,存储单元的访问方式也可以作用于变量,因此访问变量的另一种方式是通过变量的地址 AO 找到该变量并给出变量的大小信息,称这种访问方式为间接访问。
指针数据和指针数据的类型
变量的地址和变量的大小如何获得?C 语言提供了 & 运算符,&运算符的操作数必须是左值,& 运算符返回变量的地址 AO 和变量的大小信息,而承载这两个信息的数据就是指针数据。根据之前所学内容我们清楚的知道变量的大小信息包含在变量的类型中,因此请在分析 C 语言数据时留意值和类型这两个内容,这种分析方式在运用指针数据时尤其重要。
正如姓名可以指向某人,我们称存放某变量地址 AO 的指针数据指向该变量。C 语言规定指针数据的数值存放它所指向变量的地址 AO,指针数据的类型表明该变量的大小,由此可知变量的类型和指向它的指针数据的类型相关。确实在 C 语言中指针数据的类型由它所指向的变量类型派生,所谓的指针数据类型派生本质上就是传递变量的大小信息。
指针变量和声明理解
和整数数据,浮点数数据一样指针数据也可以存放在变量里,我们把存放指针数据的变量称为指针变量。在声明的声明符中符号 * 指明所修饰的对象是指针变量。
int a = 3;
int *p = &a;
在 C 语言中声明形式和使用形式一致,所以经常会把声明符中的符号 * 看做成运算符 *,这种误解也容易发生在数组和函数中。声明符中的符号 *,[],()只用来修饰声明的对象而不是运算符,其中符号 []和()的读顺序优先级比符号*高。声明的读技巧可参考 [1](go right when you can, go left when you must)。
指针数据的类型没有对应的类型说明符,只有派生类型说明符,它可以作为强制类型转换运算符的类型名和用在typedef类型声明中。数组和函数同为派生类型,他们也遵循上述规定。
返回左值的 * 运算符
引入指针数据的目的是实现变量的间接访问,从而完成不同类型变量的统一访问方式。C 语言提供了 * 运算符配合指针数据完成间接访问,* 运算符的操作数必须是指针变量的左值或代表指针数据的符号,* 运算符表达式返回一个左值,容易产生的错觉是 * 运算符表达式返回的是指向变量的数值。
int a = 3, c;
int *p = &a;
c = *p; // 错误:认为 *p 表达式返回 3
上述程序 *p
返回变量 a
的左值,然后根据左值转换得到数值 3 。其次容易误解的是认为 *p
返回存储单元,这是把语言层面和机器层面混淆了。
另外两个值转换
左值可以通过左值转换得到对应变量的数值,但该规则无法胜任数组和函数。数组的标识符在语义上可以看作为数组变量的左值,不过数组变量有多个子元素变量组成无法进行左值转换,因此 C 语言规定代表 n 维数组的左值(一维就是数组名)会隐式转换成指向 n-1 维数组的指针,且该指针指向 n 维数组的首个元素,并不再是左值,除特殊情况(&, sizeof, ...),此值转换称为数组到指针的转换。与数组类似代表函数的函数指示符除特殊情况外会隐式的转换成指向函数的指针,这称为函数到指针的转换。
几个典型事例
int a[2][2] = {1, 2, 3, 4};
int (*p)[2] = a;
*a
的类型是? *p
的类型是? 当然他们的类型一致,他们的类型都是 int *
,根据数组到指针的转换可知 a
和 p
的类型都是指向子数组的数组指针,对数组指针进行 * 运算符运算时得到的结果是代表子数组的左值,然后进行数组到指针的转换,转换成指向子数组的首元素指针。
int sum(int,int);
int (*p)(int, int);
p = sum;
******p
是什么? 根据函数到指针的转换 sum
和 p
是指向函数的指针, *p
得到代表函数的左值即函数指示符,函数指示符进行函数到指针的转换,再次得到指向函数的指针,**p
,***p
… 最终得到的都是指向函数的指针。
总结仓促,内容不准确请留言回复。
参考
- ^Reading C type declarations http://unixwiz.net/techtips/reading-cdecl.html