张天友
(无锡科技职业学院智能制造学院,无锡214000)
C语言语法简洁、运算符丰富、编程灵活、可移植性高,是一门重要的计算机语言。在C语言中,通过指针可以实现硬件的访问、动态分配和收回内存、减少全局变量的使用、实现函数的回调功能等,被称为C语言的“灵魂”;但是指针概念抽象,难以把握,使用不当会导致程序退出和内存泄露,甚至系统崩溃,成为学习C语言的难点。
在C Primer Plus[1]一书中,将指针定义为一种变量,其值为内存地址(Basically,a pointer is a variable,(or more generally,a data object),whose value is a memory address)。通过这个定义,理解指针的前提是,理解变量和变量值,内存和内存地址。Data object is a general term for a region of data storage that can be used to hold values.The C standard uses just the term object for this concept.One way to identify an object is by using the name of a variable。数据对象指的是内存的一部分,在C语言标准中称为对象,定位该对象的方法之一是通过变量名。在A Reference Manual[2]中,指针被定义为,指向类型的对象,指针本身也是一种对象,该对象的值为内存的地址(For any type T,a pointer type“pointer to T”may be formed.A value of pointer type is the address of an object or function of type T.)类型的定义:A type is a set of value and a set of operations on those values。类型是一个数值集合以及对这个数值集合的操作。指针在The C Programming Language[3]一种中的定义为:指针是一种变量,该变量的值时其他变量的内存地址。(A pointer is a variable that contains the address of a vari⁃able.)Variables and constants are the basic data objects manipulated in a program。变量和常量是程序处理的两种基本对象。A data object is a named region of storage.一个对象是一个命名的存储区域。
分析上述C语言领域经典资料对指针及其指针相关概念的定义可知,内存是把握和理解“指针”概念的核心所在。在阎石教授所著的《数字电子技术基础》中[4],把内存定义为:一种能够存储大量二值信息(或称为数据)的器件[5]。本文利用开关的“闭”“开”两种状态,来表示存储器中的“二值信息”。利用64个“开关”,构建了可论述的内存模型。以此为基础介绍了C语言中变量、类型、指针等概念的核心特点。
对于一个普通的物理开关,存在两种状态,开和关;即通电和断电两种状态。假设“开”的状态,用“0”来表示,“关”的状态用“1”来表示,那么开关的两种状态就可以表示“1”和“0”。表述了内存的特点:“二值信息”。若将8个开关排成一排,得到的结果如图2所示。图2中,底部的一行全部为关闭状态;中间一行,部分处于打开,部分处于关闭;上面一行处于全部打开状态。因为每个开关存在0和1两种状态,8个开关组合到一起,共存在256个状态。即从全部关闭(0000 0000)到全部打开(1111 1111),共256种状态。若将64个开关,按照每一排有8个,则可以得到8行,得到结果如图3所示,它形象地表示了内存基本模型。
图1 开关示意图
图2 开关不同状态示意图
图3 开关“内存模型”示意图
结合图2和图3可以得出,这些开关共有256×8个状态。我们把这些所有可能的状态统称为:“内存状态信息”。按照8个一排、纵向对其的规则,对64个开关进行统一编码,得到的结果如图3所示,右侧是按照10进制编码的结果:0~7。这种结果称为:内存“地址信息”。
由图3可知,所有的开关处于打开状态。现在将第2行第3列、第5列和第7列(从左至右:0~7列)的按钮闭合,得到的结果如图4所示。上述描述的过程,在计算机领域用“操作”一词来表述。这个操作过程有两个基本的步骤:①选择某些按钮,②设置选择按钮的状态(“开”、“闭”)。换而言之,通过内存的“地址信息”,选择某些按钮;而后设置这些按钮的状态信息(“开”、“闭”),即选中内存的“状态信息”。这就是内存操作的最本质特点。
图4 开关“内存模型”操作示意图
前述分析可知,一排8个的“开关”(如图5黑色实线部分所示,标记为:内存区域A),有256种状态。换而言之,最多可以表示256个数。超过256,一排8个“开关”无法表达。现实中需要表达的数量远不止256个。为了增加可以表达的数量,可以增加“开关”个数。一种方式如图5下部虚线方框所示“内存区域B”(第1排和第0排))。当数量范围在0~255时,可用一排的“开关”表示。视它们“开关”视为一组(图5第5排)。在C语言中,用“内存区域”指称这一组开关(如图5所示内存区域A)。在对它们进行操作是,首选选中它们,而后改变它们的状态。当数量范围在0~255×255(65025),可以用 2排“开关”(图 5 第 1排和第 0排,内存区域B)进行表示,视它们为一组。在对它们进行操作时,首选选中它们,而后改变它们的状态。
在C语言中,通过类型来表达所用“开关”的数量。例如:unsigned char类型,表示视8个“开关”为一组(内存区域A)。那么char可以表示的状态总数为256。就C语言中char类型而言,用“取值范围”一词,指称前述状态总数。再例如,unsigned int16类型,表示视16个开关为一组(内存区域B),可以表示的状态总数为 65025,取值范围为:0~65025。
当然,C语言中还有其他的数据类型,如浮点数、数组、结构体等。为了降低论述的复杂程度,本文不再论述。注意,多数情况下,一次最小选择的数量是1个开关,这个叫位选。但是多数情况下,一次选择的数量为8个开关。这里面涉及架构知识,本文也不予展开。本文的目的理清指针概念的核心侧面,而不是指针概念的全部。
图5 开关“内存模型”中的内存区域示意图
可以通过内存的“地址信息”(图5第5排),来选择所需操作“开关”数量(即内存区域);但是不方便而且也容易出错。一个解决办法就是,给相应内存区域命名。一种命名的结果如图6所示。用“Char-1”表示含有8个开关的内存区域,用“Int-1”表示含有16个开关的内存区域。上述两个名字,在C语言中称为变量。由图6可知,变量“Char-1”包含了相应内存区域的“地址信息”和“状态信息”。换而言之,通过变量“Char-1”,可以获得相应内存区域的地址编码“5”,还可以获得相应内存区域的状态“闭闭闭闭闭闭闭开”。假设“闭”用“1”来表,“开”用“0”来表示,相应内存区域的状态可以表示为“00000001”。若将这种状态视为“2进制”,则表示的数值为:00000001;对应的“10进制”为1。在C语言中,我们说变量“Char-1”的值为1。同样的方法可以分析“Int-1”对应的二进制数值为:1011101100001111,对应的十进制为:47887。在C语言中,我们说变量“Int-1”的值为47887。
由图6可知,变量“Char-1”对应的内存地址信息为5(十进制)。前述可知,变量“Char-1”的值为1(十进制)。在C语言中,变量“Char-1”的“数值”可以用Char-1来表示,而变量“Char-1”的地址,可用“&Char-1”来表示。换而言之,在C语言中,“Char-1”等价于“1”,表达的是“Char-1”对应的内存状态信息;而“&Char-1”等价“5”,表达的是“Char-1”对应的内存地址信息。一个变量是一个命名了内存区域(如内存区域A)[1],包含了两个基本的方面“内存地址信息”和“内存状态信息”。
图6 开关“内存模型”中的变量示意图
由图6可知,变量“Int-1”对应的地址信息为1和0(十进制),前述分析可知,变量“Int-1”的值为47887(十进制)。在 C 语言中“Int-1”等价于“47887”,“&Int-1”指称/选中的内存区域如由图6实线方框所示。但是“&Int-1”得到的“值”可能是“0”,也可能是“1”。这与具体的CPU构架有关,不是本文关注的重点。本文关注的重点是“&Int-1”所指称的内存区域B。为了,论述方便,本文假设“&Int-1”得到的值为“1”。
在进行内存操作时,存在通过“内存区域A”找到“内存区域B”的需求。换而言之,希望“内存区域A”和“内存区域B”相关联。假如内存区域A中,有“内存区域B”的地址信息,就可以实现两个内存区域的关联。前述分析可以,一个内存区域包含两个基本的方面:“地址信息”和“状态信息”。内存的地址信息,一般是不能改变的。换而言之,当把“开关”按照一定规则编码后,这个编码信息是基本不变的。方便改变的只有“状态信息”,若内存的状态信息,可以表示内存的地址信息,就可以实现两个内存区域的关联。如内存区域A的状态信息,用二进制表示:0000 0001;内存区域B的地址信息,用二进制表示也为:0000 0001。这样就实现内存区域A和内存区域B的相互关联。在本文中,开关代表的是二值信息,如果用开用“0”表示,关用“1”表示,得到的结果如图8所示。直观地表示了内存地址信息,可以用内存状态信息来表达。上述内容在C语言中表述为,指针变量的值用于存储内存地址[6]。
图7 开关“内存模型”中的指针变量示意图
图8 二值信息“内存模型”中的字符型指针变量示意图
在C语言中,内存区域A和内存区域B相互关联的实现方式为:指针。下述语句“unsigned char*PChar-1”,定义了一个字符型指针变量“PChar-1”。假设变量“PChar-1”对应的地址信息为:00000101,即内存区域A标记的内存单元。“PChar-1”对应的状态信息为:00000001。这个状态信息代表的是存地址信息,即00000001,即“内存区域B”标记的内存单元。在C语言中,“*PChar-1”用以表示“内存区域B”对应状态信息,即 10111011。而“&(*PChar-1)”用以表示内存区域B的地址信息。PChar-1称为指针变量,简称指针。实际上“*PChar-1”代表的就是一个字符变量。当需要改变内存区域B的状态信息时,通过给“*PChar-1”赋值即可完成。例如,*PChar-1=255的结果如图9所示。
如图10所示,“内存区域A”和“内存区域B”的大小不一致。在这种情况下如将内存区域A与内存区域B联系起来。在C语言中,也是通过指针实现两个内存区域的关联。在这一关联的过程中,需要知道内内存区域A和内存区域B的一些基本信息,例如内存区域B的大小。由前述分析可知,内存区域的大小可以通过类型来确定。
图9 二值信息“内存模型”中的无符号字符型指针变量示意图
图10 二值信息“内存模型”中的无符号整型指针变量示意图
在 C 语言中,下述语句“unsigned int*PInt-1”,定义了一个无符号的16位整型指针变量“PInt-1”。假设变量“PInt-1”对应的地址信息为:00000101,即内存区域A标记的内存单元。“PInt-1”对应的状态信息为:00000001。这个状态信息代表的是存地址信息,即00000001,即“内存区域B”标记内存单元的第一行。在C语言中“*PInt-1”,指称/选中的是内存区域B对应的区域,尽管“&(*PInt-1)”得到的结果是 0000 0001。与“*PChar-1”类似,“*PInt-1”代表的就是一个 16 位的整型变量。当需要改变内存区域B的状态信息时,通过给“*PInt-1”赋值即可完成。例如,*PInt-1=65280的结果(图10,内存区域B所示)。注意PChar-1和PInt-1 对应的值都是“1”,但是“*PChar-1”和“*PInt-1”的结果明显不同是,原因是它们的类型不同,更根本的原因是PChar-1和PInt-1指称/选中的内存区域大小不同。
一般情况下,当内存的位宽和大小确定后,指针变量占用的内存区域大小是确定的。换而言之,当内存的地址编码结束之后,存储每一个地址编码信息所需内存区域是一定的。在本文中,内存是有64个开关,按照8个一排构成的模型(8,经常被称为位宽)。内存的地址信息是:0~7。当编码结束后,存储内存地址信息所需的开关个数也就确定了。如前述的PChar-1和PInt-1,存放它们所需的内存区域大小是相同的。但是*PChar-1和*PInt-1所指称/选中的内存区域是不一样。
前述分析可知,PChar-1的值为“1”,PInt-1的值也为“1”。PChar-1+1 的值为“2”,但是 PInt-1+1 的值为3。这是因为,它们指称/选中的内存区域大小不同;PChar-1指称/选中的内存区域是一排,而PInt-1选中的内存区域是“两排”。指针变量的运算结果,与指针变量所指内存区域的大小有关,如图11和图12所示。
本文以“开关”为元素,构建了简易的内存模型。以该模型为基础,介绍了变量概念所包含的两个基本侧面“地址信息”和“状态信息”,导出了内存区域“状态信息”表述内存区域“地址信息”,是指针概念的本质所在。从内存的视角,分析了“变量类型”与“指针类型”的关系,进而解析了。C语言中“指针概念”的基本侧面。
图11 二值信息“内存模型”中无符号字符型指针运算示意图
图12 二值信息“内存模型”中无符号整型(uint-16) 指针变量的运算