3.3 C语言中的变量

3.3.1 变量的本质

在C语言中,要完成运算等功能,需要对若干个内存空间进行读取、修改等操作,为方便起见可以用一个名字来表示该内存空间,这个名字便称为变量。一个变量一旦被建立,在消除之前一直是不变的,如对于图3-5中,变量a对应的地址是0xFF00,变量b对应的地址是0xFF04,在编程中,只需要使用这个名称即可操作相应的内存。

图3-5 内存中的变量(8位)

图3-5中,每个内存空间包含8个二进制数,即8个 bit,两个变量 a和b对应的地址分别为0xFF00和0xFF04。假如变量a和b分别均为1个字节,则变量a和b的值均为10100001(二进制)。

本书给出了其执行时和执行后各变量在内存中的分布,其中的内存地址仅具有一般意义,因为不同的软硬件条件、不同的编译器版本,可能会影响运行时各变量在内存中的位置;但是各个变量所占的字节个数、各个变量的相对位置关系是固定的。

3.3.2 变量的名称规范

C语言中的变量名称需要符合相应的规范,具体包括以下内容。

(1)变量名称以英文字母或下划线开头。

(2)变零名称除开头外,其余字符可以用字母、数字或下划线构成。

(3)变量名称不能是关键字。

(4)变量名称区分大小写,如变量a和变量A是两个变不同的变量。

其中关键字是C语言中保留的单词或字母组合,表3-1给出了几种变量名称及相关分析。

表3-1 几种变量名称诊断

3.3.3 变量的声明和赋值

C语言规定,在使用一个变量之前必须对其进行声明。声明一个变量的方式如图3-6所示,其中数据类型可以是C语言支持的基本数据类型如整型、字符型等,也可以是高级数据类型如数组、结构体、指针等。结尾应分号表示语句结束。C语言规定各语句之间均应用分号隔开,为了便于阅读,一般一条语句占据一行。声明之后的变量就可以使用了,其数值为随机数,这是因为声明一个变量实际上是给一段内存起了个名字,这段内存之前的数据是随机的。

图3-6 变量的声明和赋值方法

图3-6(a)和图3-6(b)介绍了如何声明一个变量以及多个变量,当同时声明多个变量时,各变量之间用逗号进行分隔。

图3-6(c)、图3-6(d)则显示了如何给一个已经声明的变量进行赋值。赋值语句的左侧为要进行赋值的变量,语句执行后变量的数值便与等号右侧的数值相等。程序中,变量可以进行多次赋值,完成数据记录、更新等操作。

可以将变量的声明和赋值结合在一个语句中,如图3-6(e)所示,该语句既声明了变量,同时变量也被赋值为指定值。C语言中的数字既支持十进制,又支持十六进制,其中十六进制数以0x作开头,如0x10表示16,0x0f表示15。

表3-2给出了声明变量并进行赋值的代码示例,共有9条语句。其中语句(4)声明了整型变量a;语句(5)声明了整型变量b;语句(6)声明了一个整型变量c并将其数值修改为0x10。语句(7)将1赋值给变量a;语句(8)通过赋值,将a的数值赋给b;在语句(8)执行后,内存中一共有a、b、c 3个变量,值分别为1、1、16。3个变量的生存期是由语句(3)和语句(9)中的一对大括号包含的区域。

表3-2 变量声明和赋值代码示例

3.3.4 基本数据类型

C语言支持多种数据类型,不同的数据类型在数据长度,是否有符号等方面各有不同,在使用各变量时候根据其数据类型即可确定其大小。例如,当规定a为字符型(char)时候,a就表示了一个内存单元的内容。

C语言中的基本数据类型如下表所示,主要有无值型(void)、字符型(char)、整型(int)、单精度浮点型(float)和双精度浮点型(double)。

3.3.4.1 无值型

void属于一种特殊的数据类型,类型为void的变量所占的字节数为0,因此这个变量实际上是不存在的,所以void是不能直接用来声明一个变量。

3.3.4.2 字符型

C语言中的字符型变量所占的空间是一个字节。ASCII码表建立了字符与数据之间的关系,将英文字母、数字、英文标点等多种字符分别与0x00~0xff之间的唯一的数字对应(称此数字为该字符的ASCII码)。

根据ASCII码表,字符“A”小于“a”,因为“A”的ASCII码为65,而“a”的ASCII码为97。字符“B”大于数字10,因为字符“B”的ASCII码为66。字符“9”大于数字9,因为字符“9”的ASCII码为57。

字符型数据类型还可以在前面加上前缀unsigned,构成无符号字符型(unsigned char)数据类型。例如对于字符型变量 a和无符号字符型变量 b,二者对应的内存空间中的数据均为0xff,则在实际运用中,a为-1(此时a是有符号数,-1的补码为0xff,0xff的原码是-1), b为255(此时b是无符号数,255的补码是0xff, 0xff的原码是255)。这是因为二者的数据类型不同,编译器会相应编译出不同的二进制代码。

字符型数据类型在前面加上前缀signed,构成有符号字符型(signed char)数据类型,与char型数据类型等价。

3.3.4.3 整数型

C语言中整型数据所占的字节个数与编译器版本、操作系统版本、硬件版本有关。虽然有些环境下整型数据所占的字节个数是2(16位),有些环境下所占的字节个数是4(32位),但是原理都是相通的。本文介绍的是整型数据所占字节个数为4的情形。

整数型变量类型根据前缀是signed还是unsigned可以分为有符号整数(signed int)型和无符号整数(unsigned int)型。二者的区别是所代表的数字是否有符号,即是否有正数、负数之分。例如对于两个整型变量a和b,分别是有符号整数型和无符号整数型,则假设二者对应的内存空间中的数据均为0xffffffff,则a为-1(此时因为a为有符号数,-1的补码是0xffffffff,0xffffffff的原码是-1), b为4294967295 (此时因为 b为无符号数,4294967295的补码是0xffffffff,0xffffffff的原码是4294967295,232-1=4294967295)。

有符号整数型与整型等价,在使用整型数据中,如果不写前缀,则默认是有符号整数型。

int类型前面还可以加上long和short前缀,从而形成新的数据类型,改变所占的字节数,起到提高效率、节约内存等作用。具体来说,添加long的int变量类型占4个字节,而添加short的int变量类型占2个字节。

3.3.4.4 单精度浮点型

C语言中,单精度浮点型占据的字节数为4,且均为有符号数,也就是说用unsigned或signed来做前缀修饰单精度浮点型是没有意义的。

3.3.4.5 双精度浮点型

双精度浮点型与单精度浮点型很相似,不同的是,双精度浮点型占据的字节数为8,因而可以表达更高精度的小数。双精度浮点型同样是有符号数。

3.3.5 基本数据类型变量的声明和赋值

3.3.5.1 字符型变量的声明和赋值

字符型变量主要包括有符号和无符号两种类型。可以用等号对字符型变量进行赋值,等号右侧的字符型变量或常数的值将会传递给等号左侧的字符型变量。

表3-3中,语句(4)声明了 a、b、c三个有符号字符型变量;语句(5)声明了一个无符号字符型变量;对于能用符号表示的字符可直接用单引号括起来表示,如“'! '”“'@'”“ '#'”“ 'a'”“'9'”“'Z'”等,语句(6)示范了如何将字符“! ”赋给变量a;语句(7)是采用字符的ASCII码的形式进行赋值,实现了将“! ”赋给变量b;语句(8)则示范了在变量之间的赋值,将变量a赋给了变量c,语句(8)执行完毕后,变量a、b、c的值均为“! ”,而变量d的值为随机数。查看a、b、c的内存空间,三者的二进制数据均为“00100001”。

表3-3 字符型变量的操作代码举例

一些不能用符号表示的控制符,只能用 ASCII码值来表示,如需要将换行符赋值给一个字符型变量时,可以将其相应的ASCII码(换行符对应10)赋值给该变量,当然期间也可以用十六进制的方式表示常数10,为0xA。此外,对于某些特殊符号,C语言提供了相应的替代符号。表3-4列出了一些典型的特殊符号的信息。

表3-4 特殊符号的名称和数值信息

表3-4中,为了对指定的特殊符号进行表示,C语言采用的方法是利用符号“\”和特殊字符一同组成新的字符组合来表示新的意义。符号“\”的这种功能称为转义,所以 C语言中,成符号“\”为转义符。C语言编译器在编译源代码时,如果发现“\”,则判断其与后续的字符是否形成固定的组合,如果出现,则将该组合看作一个字符,用相应的ASCII码进行替代,否则才将“\”看作斜杠符号。

表3-5中,声明了3个字符型变量,分别为a、b和c。分别采用了三种形式对变量进行赋值,执行完毕后3个变量的数值均为13。

表3-5 转义符的使用方法示例

图3-7为各变量在内存中的分布,可以看出,3个变量所对应的内存中的数据相同,且各占据一个字节的空间。

图3-7 变量分布图

3.3.5.2 整型变量的声明和赋值

表3-6给出了声明整型变量的方法以及如何对整型变量进行操作。使用等号对整型变量进行赋值,等号右侧的变量、常数或者表达式的值将会传递给等号左侧的变量。

表3-6 整型变量的声明与操作示例

表3-6中,语句(4)声明了四个有符号整型变量;语句(5)声明了两个无符号整型变量d和e;语句(6)将十进制数100的值赋给了a;语句(7)将八进制数144的值赋给了b;语句(8)将十六进制数64的值赋给了c;语句(9)则示范了在变量之间的赋值,将变量 a的值赋给了变量 f。执行完毕后,a、b、c和f的值均为100,而d和e为随机数。查看变量a、b、c和f的内存空间可知,四个变量的二进制数据均为“0000000000000000000001100100”。

表3-6同时也演示了常数的三种表示方法,分别为:十进制数(以非0开始的数,如220、-560、45900),八进制数(以0开始的数,如06、0106、057),十六进制数(以0X或0x开始的数,如0X0D、0XFF、0x4e)。另外,可在整型常数后添加一个“L”或“l”字母表示该数为长整型数,如22L、0773L、0Xae4L等。

图3-8为中各变量在内存中的分布,可以看出,变量a、b和c所对应的内存中的数据相同,且各占据4个字节的空间。

图3-8 整型变量内存分布图

3.3.5.3 浮点型变量的声明和赋值

浮点型数据类型分为单精度浮点型和双精度浮点型,二者的主要区别在于所占的字节数和精度不同,其中单精度浮点型占4个字节,而双精度浮点型占8个字节。浮点型变量的声明如表3-7中的代码所示。

表3-7 浮点型变量的声明与操作示例

图3-7中,语句(4)声明了 a、b、c三个单精度浮点型变量;语句(5)将0.22的值赋给了a;语句(6)将-0.22赋给了b,绝对值小于1的浮点数,其小数点前面的零可以省略,因此此处写成了-.22;语句(7)将-0.0035赋给了 c,这里采用了科学计数法的一种浮点数的表达方法;语句(8)将-0.0035赋给了 d,这里仍然使用科学计数法,但是省略了小数点前面位的0;语句(9)则示范了在变量之间的赋值,将变量a赋给了变量e,语句(9)执行完毕后,a为0.22, b为-0.22, c和d均为-0.0034, e为0.22。浮点数只能使用十进制表述方式,双精度浮点型变量的声明和赋值方法与单精度浮点型变量的声明和赋值方法基本相同,将 float用double替换即可。

3.3.5.4 基本数据类型之间的相互转换

C语言支持基本数据类型的相互转换,其方法为在需要进行数据类型转换的变量前加上数据类型(数据类型用小括号包含),具体的示例代码如表3-8所示。

表3-8 基本数据类型之间的相互转换

3.3.6 高级数据类型

3.3.6.1 枚举型

存在某种类型的变量,其仅可以取若干个数值中的一个。例如有一种用来表示身高的变量类型,a是该类型的一个变量,其取值为0、1或者2,共三种值。其中0表示矮,1表示中等,2代表高,显然这不同于之前介绍的变量类型。虽然此时仍可以采用折中的办法,将变量a仍然声明为int型,在赋值的时候记得只可以赋0、1、2中的某个值。但是这样应用很不方便,而且在阅读程序的时候还要查阅相关的程序说明才能知道0、1、2究竟代表什么意思。

使用C语言中的枚举型变量是解决该问题的一个好办法。若变量a只可以取0、1、2三个值,分别代表瘦、中等和胖,则可以应用枚举建立相应的标识符(如THIN、MIDDLE、FAT,标识符的名称规范同变量的名称规范),并在之后用等号将THIN、MIDDLE和FAT的值赋给变量a。请看enum类型变量的声明。

图3-9给出了enum类型变量的声明格式,其中a为声明的变量。其中大括号内为取值列表,具体来说也就是之前列举的THIN、MIDDLE等标识符,在声明的同时还可以为各标识符赋值。

图3-9 enum类型变量的声明

图3-10为相应的代码示例,其中用person作为类型名,该类型可取的三个值分别用THIN、MIDDLE和FAT表示,同时三者分别赋值0、1、2。这样声明的变量a仅可以取THIN、MIDDLE和FAT三者之一,同时其取值具有可读性。当省略了对THIN、MIDDLE、FAT的赋值时,默认从0开始。图中还给出了在声明变量a后,将a设置为FAT和THIN的操作语句。

图3-10 enum类型变量声明代码举例

3.3.6.2 数组型

数组型变量在编程中应用十分广泛,其本质是内存中的若干连续的空间,每个空间的长度和数据类型由数组的声明语句决定,通过对内存中数组的分布即可理解数组型变量的原理。C语言对数组提供了很好的支持,访问数组成员的方法多种多样,是编写C语言程序常常用到的数据类型。

1.概念及声明

数组是一种数据组合,其声明方式为“数据类型+变量名+[数组长度]”,声明一个数组变量后,便在内存中出现了“数组长度”个连续的空间,每个空间的数据类型由声明语句中的“数据类型”项指定,如图3-11所示。图中给出了(a)、(b)、(c)、(d)四种声明数组的方式,其中(c)和(d)还同时完成了对数组成员的赋值。

图3-11 数组型变量的声明和赋值

图3-11(a)中声明了一个数组a,数组长度为10;图3-11(b)中声明了两个数组变量a和b,其中数组a的数组长度为5, b的数组长度为10;图3-11(c)中声明了一个数组变量a,同时用三个值1、2、3给数组赋值(三个值用大括号包含,各值之间用“, ”分隔),该语句虽然没有指明数组长度,但是由于值的个数为3,所以数组的长度被限制为3;图3-11(d)中声明了一个数组变量a,指明了数组a的长度为5,同时将5个值1、2、3、4、5赋给了数组a,此处由于已经指明了数组长度,因此等号右侧的数值的个数不能多于5个,当少于5个时,会从第一个成员开始赋值,不足的部分会自动补零。

声明数组变量后,变量在内存中的分布如图3-12所示,同时还可以用变量+[序号]的方式访问各个数组成员。引入数组后不但增加了很多可用变量,而且由于这些变量是连续的,通过修改序号便可以访问各个变量,因此为实现一些循环、数据记录等功能提供了便利。

图3-12 数组在内存中的分布

图3-12演示了数组在内存中的分布,由于每个char型变量所占字节个数为1,因此数组a从内存地址0xEE00开始,数组的5个成员所占的内存空间分别为0xEE00、0xEE01、0xEE02、0xEE03、0xEE04,访问各数据成员依次用a[0]、a[1]、a[2]、a[3]、a[4]即可。

2.多维数组

数组变量的声明语句中,若变量后只有一对中括号,这种数组称为一维数组。例如语句int a[5]即声明了一维数组变量,数组的成员为a[0]、a[1]、a[2]、a[3]、a[4]。其中,中括号内的数字为数组下标。在多维数组情况下,变量后的中括号多于一组。图3-13所示即为二维数组的声明和赋值,相应的代码示例如图3-14所示。

图3-13 二维数组的声明和赋值

图3-14 二维数组的声明和赋值代码

图3-13(a)介绍了如何声明一个二维数组,图3-14(a)为相应的代码,该代码声明了一个3×2的二维数组;图3-13(b)示范了在一条语句中声明多个二维数组变量,图3-14(b)为相应代码;图3-13(c)介绍了如何在声明二维数组的同时为其赋值,图3-14(c)为相应代码,声明了一个3×2的数组,等号右侧的大括号内包含三个数据{1,1}、{2,2}和{3,3},这三个数据分别给二维数组的3个成员a[0]、a[1]和a[2]赋值(此时可以将a[0]、a[1]和a[2]看作3个变量,从而将二维数组退化为一维数组);图3-13(d)为图3-13(c)的变种形式:省略了第一个方括号中的数组长度;图3-13(e)在声明二维数组的同时,采用了连续赋值的方式给二维数组赋值,图3-14(e)为相应的代码;图3-13(f)为图3-13(e)的变形:省略了第一个方括号内的数组长度。图3-13的(b)、(c)、(d)、(e)中,如果等号右侧提供的数值不够用,则会自动补零以实现对数组成员的赋值。

图3-15所示为采用图3-14(c)语句后二维数组a在内存中的分布图,在这里假定计算机从内存0xEE00处开始为二维数组分配空间。数组变量的声明语句中,第一个方括号内数字为3,第二个方括号内数字为2。因此内存中会有3个内存块,每个内存块中包含2个char型数据。a[0]、a[1]和a[2]分别代表了上述三个内存块的起始地址。对于a[i][j]来说(i = 0,1,2且j = 0,1),它代表了二维数组中第i个内存块的第j个成员的值。

图3-15 二维数组在内存中的分布图

根据二维数组在内存中的分布规律可知,最右侧的下标变化最快。多维数组在内存中的分布与二维数组在内存中的分布原理相同,一个三维数组的语句如图3-16中所示。其中字符型数组变量a的后方有3个方括号,因此数组为三维数组。此时,第一个方括号中的数字2对应于内存中的2个“数组内存块”,每个“数组内存块”中又包含3个“中等的内存块”,每个“中等的内存块”中又包含2个“最小的内存块”。每个最小的内存块即1个char型数据。

图3-16 三维数组的声明和赋值

举例来说,图3-16中三维数组a的内存空间为0xEE00~0xEE0B,共计2×3×2 = 12个char型数据空间(此处由于一个char型变量占8位,所以刚好也是12个字节),其内存分布图如图3-17所示。

图3-17 三维数组在内存空间中的分配

对于三维数组a来说,a[i][j][k](i = 0、1, j = 0、1、2, k = 0、1)代表了三维数组中第i个“数组内存块”的第j个“中等的内存块”的第k个“最小内存块”的数值,或者说是三维数组中第(i×N + j×M + k)个成员,这里N为中等内存块的个数,即2, M是最小内存块的个数,即3。

多维数组的内存分布规律可由三维数组的内存分布规律推出:内存块由数组声明语句中最右侧的方括号开始进行分配,逐步扩大,各内存块之间连续。例如对于声明的n维数组type a[N1][N2]…[Nn],其中type为C语言支持的任意一种变量类型,如int、char、double等。其内存分配情况如图3-18所示。

图3-18 N维数组的内存分配图

图3-18中,首先Nn个type型内存空间构成一个内存块,之后Nn-1个内存块构成一个上级内存块,之后 Nn-2个新的内存块构成一个更上一级的内存块,依次不断扩展,最后 N1个内存块构成总的多维数组内存空间,大小为 N1×N2×…×Nn个type型变量。

3.应用举例

本节给出一个一维数组的应用示例。假如一个班级有5名同学参加数学竞赛,其学号为1~5,每名同学有各自考试成绩,这样便可以用数组来表示每个同学的学号和成绩。由于每名同学的学号和成绩都在100以内且为非负数,所以用unsigned char型数组即可,如图3-19所示。

图3-19 数组应用举例

图3-19(a)列出了5名同学的学号和成绩,图3-19(b)声明了2个无符号字符型数组,分别用来存储5名同学的学号和成绩。这样对应一个下标i(i = 0、1、2、3、4),则number[i]表示了学号,score[i]表示了成绩。图3-19(c)对数组的各个成员赋初值。这样通过数组便将5名同学的学号和成绩存储了起来。相应的内存分布图如图3-20所示。

图3-20 数组示例在内存中的分配

图3-20中,数组score开始于0xEE00,各成员变量score[0]~score[4]的地址依次为0xEE00~0xEE04,值依次为88、89、87、95、92;数组number开始于0xEE05,各成员变量number[0]~number[4]的地址依次为0xEE05~0xEE09,值依次为1、2、3、4、5。

5.3.6.3 自定义数据类型

C语言提供了一种机制,可以使编程人员自定义数据类型,主要包括结构体和联合体两种。

1.结构体

1)结构体的概念。

打个比方:程序中需要这样一种数据类型,该类型的一个变量占据7个字节的长度,可以存储3个字符和1个有符号的整型数。图3-21中声明了两个变量a和b,这两个变量就属于我们希望的数据类型。其中使用了关键字struct,所以称呼这种自定义的数据类型为“结构体”,这种结构体类型的变量称为“结构体变量”,在图3-21中的“新数据类型”就是“结构体”,而a、b就是“结构体变量”。

图3-21 结构体及结构体变量的常规声明方式

图3-21给出了结构体以及结构体变量的声明方式,共计(a)、(b)、(c)三种,均可以声明a、b两个变量。例如采用图3-21(a)中所示的格式,struct关键字后面接着的是一种数据类型名称,然后是一对大括号内,括号内声明了多个变量(这些变量称为成员变量,其类型可以是C语言中的一些基本数据类型如int、char等,也可以是高级类型如数组以及本节介绍的结构体、指针等),大括号后结尾是分号,表示本条语句结束。在声明了该“结构体”之后,便可以在程序中用来声明变量了,声明变量的方法与之前介绍的变量的声明方法相似。

与图3-21相对应的代码如图3-22所示。图3-22(a)中定义的结构体名称为“charnew”(这里自定义的数据类型的名称要符合一定的规范,规范与变量命名的规范相同),大括号内的内容表明,charnew这种数据类型包含3个char型数据和1个int型数据。在图3-22(a)定义了结构体“charnew”之后,便可以用来声明变量a和b了。

图3-22 定义结构体及声明结构体变量示例代码

图3-22(b)是另一种定义结构体的形式,将定义“结构体”与声明变量a和b放在了同一条语句中,最后以分号结束。还可以进一步简化省去“新数据类型名称”,如图3-22(c)所示,虽然这样的代码使得定义的结构体没有名称,但是a、b两个变量依然包括“3个char型变量和1个int型变量”,代码简洁。

结构体变量在内存中的分布如图3-23所示。可以看出,从一个结构体变量的起始位置开始,按照声明的顺序依次分布各个成员变量。例如对于图3-21(a)中自定义的数据类型“新数据类型名称”和由此声明的变量a、b,由b的起始位置开始,依次分布了 b的成员变量:变量1,变量2, ……,直至变量n,每个变量所占的空间大小由各变量的数据类型决定。这里需要注意的是,访问各成员变量采用的方法是用“.”操作符,例如用“b.变量1”访问b的成员变量“变量1”,用“b.变量2”访问b的成员变量“变量2”,依次类推。当结构体内又有其他自定义类型的变量时,如变量m,则对变量m成员变量的访问仍然用“.”操作符,如变量m内部成员变量“变量k”,则可以用“b.变量m.变量k”来访问。上述讨论对于变量a来说,亦成立。

图3-23 结构体变量在内存中的分布

因此,图3-22中声明的变量a和b在内存中的分布方式可表示为图3-24。

图3-24 结构体“charnew”变量在内存中的分布

图3-24中,用户定义的数据类型(结构体)由3个char型成员变量和1个int型成员变量构成。该结构体的2个实例为变量a和b,若a的地址为0xEE07,则0xEE07、0xEE08、0xEE09、0xEE0A分别为a的成员变量c1、c2、c3、n的地址,各成员变量的访问方式为a.c1、a.c2、a.c3、a.n。变量a所占的内存空间为0xEE07~0xEE0D。变量b地址为0xEE00,连续分布b的3个char型成员变量为c1、c2、c3和一个int型变量n,变量b所占的内存空间为0xEE00~0xEE06。

因此,对于一个结构体变量来说,其在内存中的分布即从变量的地址开始按照地址增加的方向顺次安排各成员变量,访问各个成员变量可以用变量名 +“.”的方式实现。

2)结构体的位成员变量。

由于在C语言提供的基本数据类型中,最短的数据类型为char,占据1个字节,即由8个二进制数构成。而如果每个成员变量的取值范围很小时,会带来较大的内存浪费。图3-25中所示的一个结构体password,其内部各成员变量的取值都在0~2,即便每个变量都采用char型,变量a也要占据12个字节的内存空间。

图3-25 结构体password

在结构体的成员变量后可以规定该变量所占的二进制位数,从而形成“位成员变量”,相应的结构体成为“位结构”,避免内存浪费问题,其实现方式如图3-26所示,其中数据类型必须是int(unsigned或signed),整型常数必须是非负的整数,范围是0~15,表示二进制位的个数,即表示有多少位。

图3-26 结构体中的位成员变量

图3-27中结构体的各成员变量均规定了位数,由于每个变量的取值范围为0~2,因此用两位二进制数完全足够,这样整个结构体所占的内存空间为2×12/8 = 3个字节,显著节省了内存。此时结构体各成员变量的访问方式仍然是用“.”操作符。例如访问其中的key1成员可写成“a.key1”。

图3-27 含有为成员变量的结构体

结构体中的“位成员变量”可定义为unsigned,也可定义为signed,要根据成员变量具体的取值范围和需求来确定,如两位二进制数表示0、1或2,则应声明为无符号型。注意当成员变量的长度为1时,会被认为是unsigned类型。因为一位二进制数是不可能具有符号的。

在实际应用中,可以将“位成员变量”与一般的成员变量混合使用出现在一个结构体中,达到节省内存和系统资源的目的。

3)结构体应用举例。

本节给出一个应用示例,该例子在介绍数组类型时已经有所交代,现重新给出,并采用结构体的方法描述。

假如一个班级有5名同学参加数学竞赛,其学号为1~5,每名同学有各自的考试成绩,则可以定义“学生”这样一种结构体,将每名同学的学号和成绩作为其成员变量,如图3-28所示。

图3-28 自定义数据类型举例

图3-28(a)列出了5名同学各自的学号和成绩,图3-28(b)定义了一种自定义的数据类型“student”,也就是定义了结构体“student”,该结构体含有两个成员变量:“number”和“score”,分别表示学号和成绩,并声明了用数组 a[5]来表示5名学生。图3-28(b)中使用“unsigned char”型数据类型是因为每名同学的学号和成绩都是小于100的非负数,与使用“int”型数据相比还可以节省内存。图3-28(c)为每个变量赋值。“student”型数组 a声明和赋值完毕后,在内存中的分布情况如图3-29所示。

图3-29 自定义数据类型“学生”型变量的内存分布

图3-29表示出了“student”型数组变量在内存中的分布,可以看出在内存中连续分配了5个student类型的数据空间,从0xEE00至0xEE09共10个字节。通过改变数组a的下标即可访问各个数组成员a[0]~a[4],应用更加方便。灵活运用结构体,可以将底层数据进行综合和归纳,如本例中即将学生的学号和成绩综合到了每个student类型成员的内部,便于进行程序编写。

2.联合体

可以在结构体内部声明多个不同类型的成员变量,并根据需要使用其中的一种。由此形成的变量可称为多功能变量,因为它既可以存储整型数据,又可以存储浮点型数据,甚至可以存储之前介绍过的自定义数据类型。例如,图3-30中声明的变量a,可以存储char、int、double和student类型,其在内存中的分布如图3-31所示。当使用a存储char型变量时,可以用a.c1,当存储int型变量时,可以用a.c2,诸此类推,便可以用a存储多种类型的变量。

图3-30 “多功能”结构体

图3-31 “多功能”结构体类型变量的内存分布

该方案有一个缺点,那就是此时变量a所占的空间是所有成员变量所占空间的和,即在满足了自身需求的同时,却带来了很大和不必要的内存浪费。而本节介绍的“联合体”则很好地解决了这一问题,其对应的关键字为“union”,用法与关键字“struct”相同。

图3-32定义了一种联合体,其成员变量有4个,分别是一个char型变量c1,一个int型变量c2,一个double型变量c3和一个student型变量c4。a是该联合体的一个实例。图3-33为变量a在内存中的分布图,此时变量a的空间与成员变量中占空间最大者保持一致,每个成员变量的开始地址均与变量a的地址相同,由此实现了内存空间的复用。此时采用a.c1、a.c2、a.c3和a.c4分别可以存储char, int, double和student类型的变量。

图3-32 “多功能”联合体

图3-33 “多功能”联合体变量在内存中的分布

参考关键字“struct”的用法,将其中的关键字“struct”改为“union”即可定义相应的联合体。采用联合体定义数据类型的格式和示例分别如图3-34和图3-35所示。

图3-34 联合体及联合体体变量的常规声明方式

图3-35 定义联合体及声明联合体变量示例代码

图3-34给出了联合体的常规声明方式,共计(a)、(b)、(c)三种,均可以实现声明两个联合体变量:变量a和变量b两个变量。例如采用图3-34(a)中所示的格式,union关键字后接一种数据类型名称,然后接一对大括号,括号内给出了多个变量,这些变量称为成员变量,其类型可以是基本数据类型如int、char等,也可以是高级类型如数组以及本节介绍的结构体、指针等。大括号后接分号表示本语句结束。之后便可以在程序中使用这种新定义的数据类型(联合体)来声明变量了,声明变量的方法和之前介绍的基本类型变量的声明方法相同。与图3-34(a)相对应的代码如图3-35(a)所示,其中联合体类型定义为 varnew(这里自定义的数据类型的名称要符合一定的规范,规范与变量命名的规范相同),大括号内的内容表明,varnew这种联合体包含1个char型变量c1、1个int型变量c2、1个double型变量c3和1个student型变量c4。定义了charnew数据类型后,便可以用来声明变量a和b了。

图3-34(b)是另外一种定义联合体的形式,即在一条语句中完成联合体的定义和变量的声明,其代码如图3-35(b)所示,联合体名称为 varnew,在大括号结束后,紧接着写变量a和b,完成了联合体的定义和变量a、b的声明。可以进一步简化省去联合体的名称,如图3-34(c)所示,代码更加简洁。

联合体变量在内存中的分布如图3-36所示,其中联合体以及变量 a、b的定义如图3-34(a)所示。可以看出,联合体变量的起始位置也即各个成员变量的地址。当联合体内又有其他自定义类型的变量时,比如变量 m,则对变量 m成员变量的访问仍然用“.”操作符,例如变量m内部成员变量“变量k”,则可以用“b.变量m.变量k”来访问。上述讨论对于变量a来说,亦成立。

图3-36 联合体变量在内存中的分布

图3-35中声明的变量a和b在内存中的分布如图3-37所示,若a的地址为0xEE04,则各成员变量c1、c2、c3、c4的地址也是0xEE04,各成员变量的访问方式为 a.c1、a.c2、a.c3、a.c4。变量 a所占的内存空间为0xEE04~0xEE07。变量 b的地址为0xEE00,成员变量c1、c2、c3和c4的地址也是0xEE00,变量b所占的内存空间为0xEE00~0xEE03。

图3-37 联合体“varnew”型变量的内存分布

由此可知,对于一个联合体类型的变量来说,其所占的内存等于各成员变量中所占内存的最大者,各成员变量的地址与该变量相同,复用内存空间从而节省了内存,访问成员变量用变量名 +“.”的方式即可。

3.3.6.4 指针型

1.概念

C语言中,可以用“&”取得一个变量的地址,这个变量可以是任何一种C语言支持的数据类型,包括基本的数据类型如char、int等以及高级的数据类型如枚举、结构体、联合体和指针等。假设变量a为字符型,变量p为指针型,则各表达式的关系如表3-9所示。

表3-9 变量及变量地址的值

对于一个指针类型的变量,其内存单元中的数据是一个地址值,如在图3-38中,变量a为字符型,其地址为0xFF00,则a的值是10100001,对应的十六进制是0xA1。变量p为指针型,其地址为0xFF10,由于地址总线为16位,因此一个指针类型的变量其内容应该由两个单元的变量构成,即由内存地址为0xFF10和0xFF11两个单元内的数据构成(不同的硬件和操作系统,指针型数据类型所占的字节数不同,但其长度都要满足能够存储一个内存地址)。所以 p的值为0000000010100001,对应的十六进制为0x00A1,0x00A1是一个内存的地址。

图3-38 指针数据类型原理示意图

对于一个指针型变量来说,其地址(例如图3-38中的0xFF10)在内存中是确定的,其内容可以通过赋值进行修改。例如可以将图3-38中 p的内容由0000000010100001(0x00A1)改为0000000010100010(0x00A2)。

因此,一个指针型的变量隐含的意义是:它的内容(数值)应该被看作一个内存地址。

2.声明和赋值

在C语言中,声明一个指针类型的变量可以用图3-39所示的方法,即采用“方框+*+指针变量名称”的格式。其中,方框内是“所指内存的数据类型”,可以是C语言中的基本数据类型,如char、int、double等,也可以是一些高级的类型,如指针、结构等;方框后的“*”,表示声明的变量是一个指针型,此时“变量1”的内容是一个内存地址,且该内存地址处的数据是“方框型”。

图3-39 指针类型变量的声明方法

图3-39中,方框内的数据类型便决定了“变量1”这个指针型变量的内容所确定的内存空间的数据类型,或者方框内的数据类型决定了“变量1”所指内存的数据类型。

图3-40给出了一个指向字符型变量的指针的声明方法,其中方框内的数据类型决定了指针的内容所对应的内存处数据的类型;“*”号与方框一起表明声明的变量是指针型,因而称为“指针标记符”;“变量1”为变量名称。

图3-40 声明指针类型的语句的各部分功能

当采用“char *p; ”声明一个指针型变量p的时候,可以解释为:p为一个“指向char型的指针”型变量,p的内容0000000010100001(0x00A1)是一个具体的内存地址,该内存地址0x00A1处的数据为char型,值为00000001(0x01);当采用“char **p; ”声明一个指针型变量 p的时候,可以理解为:p的内容0000000010100001(0x00A1)是一个具体的内存地址,该内存地址0x00A1处的数据是一个指针,其值0001001000000001(0x1201)是一个内存地址值,而内存地址0x1201处是一个char型变量。

一个指针声明后,其内容是随机数,或者理解为该指针指向一个随机的内存地址,此时的指针被形象地称为“野指针”,要对该指针进行赋值后才能使用,否则当使用“*”获取其内容时,会导致错误。通常可以用操作符“&”对变量取地址,并将该地址赋给指针。

C语言中取得一个指定内存地址内容的操作符为“*”。例如对于图3-38中的指针p, “*p”便表示了p所指内存空间的内容。具体来说,如果p是指向字符型的指针变量(声明语句为“char *p”),则此时自0x00A1起一个单元长度的数据即是“*p”的值;如果p是指向int型的指针变量(声明语句为“int *p”),则此时自0x00A1起四个单元长度的数据即是“*p”的值;如果p是指向指针的指针变量(声明语句为“char **p”“int **p”等),则此时自0x00A1起四个单元长度的数据即是“*p”的值,且该值应看作是一个内存地址。

总结关于指针型数据类型的声明和赋值方法,如图3-41所示。其中,(a)直接声明了一个指针型变量“变量1”; (b)声明了两个指针型变量“变量1”和“变量2”,变量之间用逗号隔开;(c)为指针型变量“变量1”进行赋值;(d)将“变量2”的值赋给了“变量1”; (e)则声明了指针型变量“变量1”,同时对其进行了赋值。

图3-41 指针型变量的声明和赋值

(c)和(e)对指针型变量进行了赋值,其中等号右侧的“值”是一个地址值,就如图3-38中所示的0x00A1和0x00A2等,也可以用“&”来求取变量的地址。

表3-10为对指针型变量进行声明和赋值的代码。其中语句(2)声明了一个字符型变量a;语句(3)声明了两个“字符型”的指针型变量p1和p2;语句(6)将变量a设置成0x1;语句(7)将变量a的地址赋给了p1;语句(8)将变量p1的值赋给了p2。各语句执行完毕后,p1和p2所存储的内容为变量a的地址,a的内容为0x1。

表3-10 指针型变量声明和赋值代码

3.指针类型的相互转换

C语言支持不同类型的指针的相互转换,只需要将新的指针类型置于小括号内,并放置在一个指针变量左侧即可,如表3-11所示。

表3-11 指针类型相互转换

表3-11中的语句(2)声明了一个整型变量a并将其赋值为0x1234, a的内存地址假设为0x3300,相应的内存空间0x3300和0x3301分别赋值为0x34和0x12。表3-11中的语句(3)声明了一个短整型的指针型变量 p1,语句(4)声明了一个长整型的的指针型变量 p2,语句(7)将a的地址赋给了p1,则p1的内容为0x3300,也就是变量a的地址。如图3-42所示。

图3-42 各变量的内存分布

指针变量p1的地址为0x3302,指针变量p2的地址为0x3304。表3-11中语句(8)将p1的值赋给了p2,这里采用了指针类类型的转换。由于p1是“short*”型,而p2是“char *”型,因此“*p1”的值为0x1234,而“*p2”的值为0x34。

4.多维指针

采用数据类型和多个“*”的组合可声明多维指针变量。例如语句“char **p”中,p为二维指针变量,表示变量p的内容是一个内存地址,而该内存地址存储的数据仍然是一个指针。如图3-43所示。

图3-43 二维指针内存示意图

其中,二维指针变量p的内容为二进制0010001000000000(0x2200),该值是一个内存地址0x2200,地址0x2200处的数值仍然是一个指针,指针的值为二进制0011001100000000(0x3300),0x3300仍然是一个地址,地址0x3300处的数值,根据p的定义语句,为char型,也就是二进制00000000。

指针变量的内容可以使用操作符“*”获取,使用方法如表3-12所示。

表3-12 二维指针的表达式取值

多维指针的操作方法与二维指针相类似,无论指针的维数是多少,其本质仍然是一个指针,其数值是一个内存地址。

5.应用举例

1)内存共享和修改。

由于指针的数值是一个内存地址,因此不同的程序模块只要传递共同的一个指针,便可以在不同模块中对该指针的内容进行修改,便于实现数据的共享和共同维护。举例来说,在内存的一个位置有n个字节的数据,程序中的某个模块只需要一个指针变量便可以对该段内存进行访问和修改。

2)无值指针。

以一维无值指针为例,其声明方式为“void *p”,此时指针p的内容是一个地址,但是并未指定该地址的数据是何种类型。借助于C语言中指针类型的强制转换,可以根据需要将无值指针转化为需要的指针类型。

3)存储指针的数组。

在声明数组时,将数据类型设定为指针型,这样数组中的各个元素的内容都是一个内存地址。相应的代码如表3-13所示。

表3-13 存储指针的数组的代码示例

表3-13中,语句(2)声明了1个指针数组p,数组的3个成员为p[0]、p[1]、p[2],均为字符型指针变量,其内容均表示1个内存地址;语句(3)、(4)、(5)定义了3个字符型的变量a、b、c并分别赋值为0x00、0x01、0x02;语句(8)将变量a的内存地址赋给了p[0];语句(9)将变量b的内存地址赋给了p[1];语句(10)将变量c的内存地址赋给了p[2]。

图3-44为各变量在内存中的分布。这里假设变量a的地址为0x3306,变量b和变量c的地址分别为0x3307和0x3308。指针数组p的地址为0x3300, p的三个成员p[0]、p[1]和p[2]的地址分别为0x3300、0x3302和0x3304,内容分别为0x3306、0x3307和0x3308,内容分别表示了变量a、b和c的地址。

图3-44 存储指针的数组的内存分布

4)指向数组的指针。

指针的内容(是1个内存地址)是数组的地址,示范代码如表3-14所示。

表3-14 指向数组的指针的用法

表3-14中的语句(2)声明了1个字符型的数组a,并给a的3个成员分别赋值为0、1、2。语句(3)声明了1个字符型的指针p1。语句(4)声明了1个字符型的指针p2。语句(7)将数组a的地址赋给了p1。语句(8)将数组a的第一个元素a[0]的地址赋给了p2。

图3-45给出了变量在内存中的分布图。假设数组的地址为0x3300,则a[0]、a[1]、a[2]的地址分别为0x3300,0x3301和0x3302。变量p1的地址为0x3303,且p1的内容为0x3300, p1的内容是一个内存地址,也就是数组a的内存地址,它是数组a的指针。变量p2的地址为0x3305,且p2的内容为0x3300。

图3-45 指向数组的指针的内存分布

5)字符指针。

在C语言中可以利用指针灵活操作字符串,图3-46为利用字符指针操作字符串的原理。

图3-46 字符串指针示例代码及内存分布

图3-46中的左图为字符指针的示例代码,其中语句(3)声明了一个char型指针并将字符串“hi! ”的值赋给了p,这里的p是一个字符指针,字符串用双引号包含。该段程序经过编译器编译后生成的机器代码的操作是:在内存中分配一段空间,该段空间的字节数是字符串“hi! ”的长度再加1,将字符串“hi”存储到这段内存后,在多出的那个字节内存入0,这么做的目的是在使用该字符串的时候,知道是否到了字符串的结尾。

相应的内存分布如图3-46的右图所示,设字符串“hi! ”由内存地址0x4400开始存放,则0x4400、0x4401、0x4402、0x4403处的数据分别为0x68、0x69、0x21和0x00,分别对应字符“h”、“i”、“! ”和“空字符”。而指针p其实是一个字符指针,假设它的地址是0x3300,它的内容是字符串“hi! ”的起始地址,即字符“h”的地址,亦即0x4400。这样,由p便可以操作字符串“hi! ”了。

6)地址运算。

由于一个指针型变量的数值是一个地址值,所以可以对一个指针型变量的数值进行加减等操作,表3-15给出了具体的代码示例。

表3-15 利用指针进行地址运算的代码

表3-15中,语句(2)声明了1个字符型变量c;语句(3)声明了1个字符型变量b;语句(4)声明了1个短整型指针变量q;语句(5)声明了1个字符型指针变量p;语句(6)声明了1个字符型数组a, a的3个成员分别为1、2、3;语句(9)设置指针p为数组a的地址,因此p指向数组a的地址亦即数组a的第一个元素 a[0]的地址;语句(10)将指针 q的值设置为数组 a的地址,因此 q指向数组a的地址亦即数组a的第一个元素a[0]的地址;语句(11)将指针p加1后的地址的数据赋给字符b;语句(12)将指针q加1后的地址的数据赋给字符c,此时为了取出(q+1)处的字符值,采用了指针类型的强制转换:先将(q+1)转换为char型指针,之后再用操作符“*”取其内容,这里还用到了小括号来保证表达式的优先级。

图3-47所示的代码执行之后各变量在内存中的分布。假设数组a在内存中的地址为0x3306,数组 a的三个成员 a[0]、a[1]和a[2]的地址分别为0x3306、0x3307和0x3308,值分别为1、2和3。指针p的地址为0x3304,值为0x3306,即数组a的地址。指针q的地址为0x3302,值为0x3306,亦为数组a的地址。字符型变量b的地址为0x3301,其值为指针p加1后取内容,由于p是字符型指针,所以对p加1为0x3307,此时用“*”取内容,为2,因此b的值为2。字符型变量c的地址为0x3300,其值为指针q加1后取内容,此时由于q是short型指针,则q加1为0x3308强制转化为字符型指针,并取内容后得3。

图3-47 地址运算内存分布

一个指针加1后,其结果是指针的内容加上该指针的内容所对应的空间的数据类型的字节数,即对于一个指针p,如果其定义为“type *p”(其中type为int, char,short, …),则p + 1的值为p的内容加上sizeof(type),其中sizeof(type)表示一个type类型变量所占的字节数。

7)指针与数组。

C语言中的指针与数组有很多相似的地方,二者都可以表示1个内存地址。表3-16给出了操作指针和数组的示例代码,其中语句(2)声明了1个字符型变量e,语句(3)声明了1个字符型变量d,语句(4)声明了1个字符型变量c,语句(5)声明了1个字符型变量b,语句(6)声明了1个字符型指针p,语句(7)声明了字符型数组a,3个成员变量a[0]、a[1]和a[2]分别赋为1、2和3。

表3-16 操作指针与数组的代码示例

表3-16中,语句(10)将a的地址赋给了p,指针p的内容是一个地址值,该地址为数组a的地址,也即数组a的第一个成员a[0]的地址;语句(11)将数组a的成员a[1]赋给字符b;语句(12)对表达式(a+1)取内容,并赋给字符c,这里需要注意的是,C语言中声明一个数组后,这个数组名(比如本例中的数组a)是作为一个地址看待的,所以(a+1)代表了一种地址运算;语句(13)将指针p加1后取内容再赋给字符d;语句(14)将指针p用操作符“[]”进行取内容,并赋给字符e。

图3-48为各变量在内存中的分布图,假设数组 a的地址为0x3306,则地址0x3306、0x3307、0x3308分别是数组a的3个成员 a[0]、a[1]、a[2],其值分别为1、2、3。指针p的地址是0x3304,内容为0x3306,即数组a的地址。变量b的地址为0x3303,其值为2,即a[1]。变量c的地址是0x3302,其值为2,因为(a+1)的结果为地址值0x3307,该值为一个内存地址,所以取内容后为2。语句(13)执行后d的值为2, d的地址为0x3301。语句(14)中,字符变量e的地址为0x3300,对指针采用了“[]”操作符,由于此时指针的值为0x3306,该操作符意味着从地址0x3306开始,取第2个成员(第一个成员为p[0]),而由于指针p为字符型指针,所以p [1]的地址为0x3307,值为2。

图3-48 指针与数组示范代码的内存分布

指针和数组有很大的相似性,二者均表示了地址,也均可以用“*”操作符和“[]”操作符。例如对于数组a,其地址为0x3306,则(a+n)所表示的地址为a[n]的地址,即(a+n)与&a[n]等价;对于指针p, p的内容是0x3306, p[n]所表示的即地址p+n*sizeof(type)处的值,其中sizeof为求取type所占的字节数,type由指针的类型决定,如p定义为字符型指针,则type为char,当p定义为int型指针,则type为int。

然而指针和数组也有一些区别,如对于一个指针来说,它占据一定的字节个数,有自身的地址值,而其内容是一个内存地址,如图3-48中,字符指针p的地址是0x3304,而其内容为0x3306;而数组在程序看来,其名称是一个地址值,占内存空间的是其三个成员a[0]、a[1]和a[2]。

8)自定义数据类型的指针。

既然一个指针变量的值表示一个内存地址,而其值是任意的,这也就是说理论上一个指针变量可以表示任何一个内存地址。这里需要引起注意的是,这些内存地址可能是非法的、受保护的,或者是被其他程序使用的,对这些内存空间进行读取和修改可能会影响系统的正常运行。

现在假设student是一个结构体类型,可以用语句“student a; ”声明一个类型为student的变量a,并用“student *p = &a; ”声明一个指向变量a的指针p(注意“student *p; ”的意思是声明一个指针,指针名称为p,且p所指向的内存里存放的是一个student类型的变量)。此时语句“a.score”和“p->score”均可以访问成员变量score。

表3-17中,语句(2)声明了一个字符型变量x1;语句(3)声明了一个字符型变量x2;语句(4)定义了一个名称为student的结构体,并声明一个student类型的变量a和一个指针型的变量p, p的内容是一个内存地址;语句(7)将a的地址赋给p,这样p的值便是a的内存地址;语句(8)将student类型变量a的成员变量score赋给x1;语句(9)用指针p来访问变量a的成员变量,并将其值赋给变量x2,方法是用“->”操作符。因此,用指针p配合“->”操作符可以访问p的内容所表示的内存区域的结构体变量的成员变量。

表3-17 自定义的结构体变量的指针用法示例

代码执行完毕后,各变量在内存中的分布如图3-49所示。假设 a的地址为0x3306,则p的地址为0x3304, p的内容为0x3306,其中0x3306即a的地址。假设内存0x3306处的数据为0x01,内存0x3307处的数据为0x50。变量x1的地址为0x3302,变量x2的地址为0x3303,二者的值均为0x50。

图3-49 自定义结构变量指针的内存分布

因此,访问一个自定义数据类型的变量的成员变量(如 student类型变量 a的两个成员变量 number和score),除了用变量配合“.”的方式来访问外,还可以用指向该种变量的指针配合“->”来实现。

9)安全使用指针的规则。

由于指针的特殊型,其值表示了一个内存地址,对该指针进行的操作例如“*”(取内容)和“->”(访问成员变量)等均需要该内存地址真实存在且合法。而在程序中,一个指针被声明后,其值是随机的,必须对其进行赋值后才能使用;有时候,一个指针所指向的内存空间虽然是合法的,但是经过一些代码后,该段内存空间可能已经被释放,此时再使用指针也会引发程序错误,所以使用指针需要遵守一定的规则。该规则可以概括为三点:在声明指针后,立即进行归0操作;在使用指针前,进行合法检验;在内存释放后,对指针进行归0操作。

C语言中声明一个变量,只是为某块内存区域起了名字。声明一个指针类型的变量后,将其值赋为0,这个操作称为归0操作。当使用该指针的时候,首先判断其是否为0,这样可以避免未对其进行正确赋值而引发的程序错误。

表3-18给出了一个安全使用指针的示例。首先语句(2)和语句(3)声明了字符型变量a和b。语句(4)声明了一个字符型指针p并赋值为0;语句(7)将变量a的地址赋值给了指针p;语句(8)在使用指针p的时候,首先判断是否为0,避免了访问非法地址。在语句(10)中,由于指针p已经使用完毕,所以将其重新赋值为0,方便以后的代码重新使用该指针时可以判断该指针是否合法。

表3-18 安全使用指针的代码示例

3.3.7 变量的生存期

在C语言中,程序的入口是main( )函数,之后一对大括号括起了main( )函数的所有代码,其间声明的所有变量的生存期由这对大括号确定。

也就是说,声明在main( )函数的内部的变量,其生存期由main( )函数的一对大括号确定,此时的变量为局部变量,用关键字auto进行说明,当auto省略时,所有的非全程变量都被认为是局部变量。局部变量在函数内部声明时产生,但不会自动初始化,随着函数的结束,这个变量也将消失。

那么在main( )函数之外声明的变量呢?C语言中声明在main( )函数外部的变量,也就是说不受任何大括号约束的变量,称为全局变量,其生存期是整个程序运行期间,也就是说它可以在程序的任何位置被使用,并且在整个程序的运行中都保留其值,全局变量习惯上通常在程序的主函数前说明。

图3-50给出全局变量和局部变量的相关示范代码和内存分布,其中(a)中的语句(1)声明了一个char型变量gName,这里 gName即为全局变量,其类型属于有符号字符型,生存期为整个程序运行期。语句(4)声明了一个局部变量a,类型亦为有符号字符型,生存期为整个 main( )函数。语句(4)执行完毕后,两变量在内存中的分布如图3-50(b)所示,其中 a的地址为0x3300,其数据为随机数据;变量gName作为全局变量,已经被初始化为0,且所处的内存区域与局部变量不同。

图3-50 全局变量和局部变量

可以在声明全局变量的时候为变量名增加前缀“g”,这样在使用该变量时,编程人员根据“g”便可以得知此变量为全局变量,慎重进行修改等操作以避免给程序其他部分带来影响,这一点在程序规模很大的时候尤其有用。

用static关键字声明的变量,其生存期是整个程序运行过程,却只能在声明该变量的代码块中进行访问。这种变量称为静态变量,在程序开始运行前已经分配了内存,并且仅在程序开始运行之前初始化一遍,因此可以看作一种特殊的全局变量。例如在表3-19的代码中,变量i会不断自增到100,之后重新归0并继续增加。需要注意的是在第6行代码中为将0赋值给变量i,仅是为静态变量i设置了初值,之后不再起作用。

表3-19 利用静态变量进行计数的程序示例

作为对比,在表3-20中的代码中,变量i为临时变量,i的值只有0和1两种。在每次运行到第6条语句的时候,系统重新产生变量i并为其赋初值0。

表3-20 临时变量的用法示例