1
C/C ++程序设计
1.2.2.3 2.3 数据角色

2.3 数据角色

学习了数据类型,就知道了C语言中的数据的种类,那么在C语言中如何定义一个具体数据角色呢?C语言是通过常量和变量来实现的。

2.3.1 概述

通过上述讨论,我们已经熟知了数据的不同种类,那么在本节中将看到C语言是如何产生不同种类的具体数据。好比戏剧中有生、旦、净、丑四种基本角色,但是要想演出一场真实的戏曲,还要有若干活生生的人物。虽然这些人物丰富多彩,但是他们一定没有摆脱生、旦、净、丑的角色范围。那么在计算机世界中,C语言是如何打造出这些活生生的人物呢?

C语言刻画的“活生生的人物”就是我们说的变量和常量,其实C语言的变量和常量是没有本质区别的,仅仅在于程序运行期间其值是否可以改变而已。数据存储在特定的空间中,根据实际的需求将不同的数据归于不同的数据类型,例如我们到底将圆的半径定义为整型还是浮点型呢?从程序的角度来看没有什么对错界限,完全可以自由选择,具体的类型就看你的需求了。从中可以看到,数据类型的选取完全来自于现实的需求。

当确定了数据类型,对于一个数据来讲意味着什么呢?意味着存储它们的空间大小已经定义好了。万事俱备了,东风是什么啊?就是分配空间。C语言中,对一个变量或者常量进行定义时,空间分配也就完成了。这就是接下来要讨论的核心问题。

2.3.2 变量

本节将探讨变量的声明和定义。这里将C语言中所有的变量放到一起进行探讨。当然包括整型、浮点型、字符型还有枚举、数组、结构体和共用体,以及指针。

(1)声明和定义

首先澄清两个基本的重要的也是容易混淆的概念———声明和定义。

所谓声明就是仅仅告诉编译器(可以读懂C语言并能将其转化为汇编语言的程序)将有这么一个变量被使用。注意,仅仅是告诉而已,除此以外并没有做其他的任何事情,这样编译器就知道在不久的将来,会有这么一个变量被使用,也就是说编译器能够在碰到这个变量的时候认出它来。而定义就不是这么简单了,相对声明而言,定义更具有现实意义,因为定义不但告诉编译器有这么一个变量,而且让编译器为其分配存储空间。举个就餐的例子来简单类比,声明好比电话预约,定义好比当面就餐。

在C语言中,可以这样定义一个变量:

数据类型变量名称;

这是最简单的也是最常见的形式。由此可以定义一个变量,比如:

int intA;

这句话就定义了一个名为intA的整型变量,系统将为其再分配特定大小的连续存储单元(具体由系统来定)。

不难想象,如果有如下定义:

img19

然后有如下定义:

struct Book bokSanguo;

这样将会产生什么变化呢?系统将定义一个结构体变量bokSanguo,并为其分配存储空间。

大家可以仔细比较一下上述两个变量的定义形式,虽然整型intA和结构体bokSanguo在类型上讲有很大的差别,但是在定义形式上是如此的一致,这就是C语言的魅力所在。其实,在C语言的后续版本中,完全可以将“struct Book bokSanguo;”语句中的struct省略掉。由此我们可以看出,如果掌握了数据类型,变量的定义就很容易了。

我们有更为一般的定义:

[类型修饰符]<数据类型>[*]<变量1名称>[[<常量表达式1>]],[*]<变量2名称>[[<常量表达式2>]],……[*]<变量n名称>[[<常量表达式n>]];

其中类型修饰符可以为static、const、extern等;static表示变量是静态变量,即变量一旦定义就不会消失(地址回收),其值在程序运行期间将一直保存在为其分配的地址空间中;extern表示变量是引用其他文件里面的,不是重新定义的新变量,编译器将不为其分配储存空间,对于const在后面将有详细的讨论。如果有*出现,说明定义的是一个指针变量,如果有常量表达式出现,说明是一个数组。还可以将若干个同一个数据类型的变量放在一起进行定义。

例如:

Book*pbokClassical,bokShuihu,bokClassical[4];

这样就分别定义了一个Book型(自定义结构体)的指针变量、普通变量和长度为4的一维数组。

其实还可以在自定义类型的同时定义变量,例如:

img20

如果这样的话,Book可以省略。即:

img21

由此不难想象,枚举变量也可用不同的方式说明,即先定义后说明、同时定义说明或直接说明。

设有变量a,b,c被定义为在2.2.3节中的Sex枚举类型,可采用下述任意一种方式:

img22

或者为:

enum Sex{Man,Woman}a,b,c;

或者为:

enum{Man,Woman}a,b,c;

由此我们可以看到语言学习的特点:举一反三,由已知推未知,从语法表面进行分析,由表及里,得到规律,进而将相关的知识联系起来,这样才能提高我们的学习能力和分析解决问题的能力,同时也能更为深入地学习C语言课程。

(2)变量命名———规范化编程

对于变量的命名,这个看似是最简单的问题,但是涉及编程的规范性。一个良好的程序开发者应该具有良好的编程习惯,应该遵守一定的编程规范,本书不打算深入讨论规范化编程,如果想深入学习这方面的知识,可以参考《规范化程序设计》一书。这里仅仅探讨两个问题:命名规则和注释。

其实任何一门语言的学习,都涉及命名问题,从某种意义上说,命名可以体现一个编程者的能力水平和科学态度。对于C语言来讲,说到命名不仅仅包含变量的命名,还有函数的命名、文件的命名、工程的命名等。其中变量的命名是我们最为关心也是最为重要的。

常用的变量命名法则有三种:即骆驼式(Camel)命名法、帕斯卡(Pascal)命名法和匈牙利命名法。

骆驼式命名法,正如它的名称所表示的那样,是指混合使用大小写字母来构成变量的名字。首字母小写,接下来的单词都以大写字母开头,这样便于区分单词,进而可以很容易地看出变量表示的意义,例如:

int dayOfYear;

可以很明确地看出这个整型变量表示的意义。骆驼式命名法近年来越来越流行,广泛地应用于许多新的函数库和Microsoft Windows这样的环境中。

帕斯卡命名法与骆驼命名法类似,只不过骆驼命名法是首字母小写,而帕斯卡命名法是首字母大写。例如:

int DayOfYear;

匈牙利命名法是由Microsoft程序员查尔斯·西蒙尼(Charles Simonyi)提出的。该命名法通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域、类型等。这些符号可以多个同时使用,顺序是先指针,再简单数据类型,再其他。基本原则是:变量名=属性+类型+对象描述,其中每一对象的名称都要求有明确含义,可以取对象名字全称或名字的一部分。命名要基于容易记忆容易理解的原则,保证名字的连贯性是非常重要的。其关键是:标识符的名字以一个或者多个小写字母开头作为前缀,前缀之后的是首字母大写的一个单词或多个单词组合,该单词要指明变量的用途。

例如:

int iDayOfYear;

表2-5列出了部分匈牙利命名法中常用的小写字母的前缀。

表2-5 常见的匈牙利命名法前缀

img23

(3)一个例子

讨论了相关概念后,下面我们来看一个实际的例子。

【例2.1】定义并输出整型变量和自定义变量。

①打开Visual C++6.0环境,建立一个控制台工程,其名称为Example_2_1_DefinedVariable;

②添加C源文件,并编写如下代码:

img24

③编译链接并运行,查看结果。

④运行结果如图2-2所示。

img25

图2-2

我们看到了什么?结果完全出乎我们的意料,出现了严重的错误。其实错误并不可怕,而且有的时候我们还要感谢错误,这完全在于我们自己看待错误的态度。分析原因,找到出错的原因,进而发现新的问题,我们借此不断地进步。

其实,细心的同学会发现在编译的时候,输出窗口就提示了如下的错误:

img26

这两行提示告诉我们很多有用的信息,在以后的学习过程中一定要重视这样的警告和错误提示。它们是我们编程的利器,为我们查找程序中的错误起到十分重要的引导和帮助作用。下面我们就分析源代码、警告和错误。

⑤代码分析

现在从头分析代码,从第1行到第5行,我们定义了结构体struct Book这个新的数据类型,在Visual C++6.0中,struct可以省略。第9行和第10行在主函数中定义了两个变量,分别为整型变量iTest和结构体Book变量booShuihu,本书中规定自定义类型变量名的前缀取类型名的前三个小写字母。

第12行和13行分别输出整型变量iTest的值和结构体变量booShuihu的书名的值。从结果中可以看出,无法输出信息。为什么?我们来看下一个问题———变量的初始化。

(4)变量的初始化

任何事物都是有初始形态的,变量也是有初始形态的,即变量的初值。C语言中通过给变量初始化操作给变量赋初值。语法如下:

数据类型变量名称=变量初值|初始化列表;

其中变量初值是变量允许的范围内的一个值,初始化列表是用“{}”括起来的一系列特定的值。一般来讲,变量初值对变量进行初始化,初始化列表对结构体进行初始化。例如:

int iTest=0;

在定义变量的同时进行了初始化。

我们还可以对数组进行初始化,方法如下:

数据类型变量名称[<常量表达式>][<常量表达式>]…[<常量表达式>]=初始化列表;

这里的最左边的常量表达式可以省略,因为可以根据初始化列表决定其值,但是后面的常量表达式不能省略。而且初始化列表可以嵌套使用,并常常是嵌套使用的。例如:

img27

在前面讨论数组的时候任何数组都可以看做是一维数组,对于这一点,有人会问:“既然是一维数组,那么可以这样初始化么?”

img28

可自行验证一下,结果证明两者是一样的。再来考虑以下问题:

问题1:两种初始化方式完全等同吗?

问题2:第一维的长度多少?

问题3:第一维的长度如果不省略的话,情况会有什么变化?

这三个问题实际上是相辅相成、环环相扣的。首先来看第一个问题,从形式上来看并不相同,主要是“{}”的问题,我们认为第一种方式是较为科学的,因为嵌套列表的形式从逻辑上更为清晰,更能反映三维数组的事实。afTest是三维数组,首先可以看做是数组元素为二维数组的一维数组,为了方便阐述问题,不妨设两个辅助数组,定义如下:

img29

这样的话,我们似乎可以这样看待afTest:

double afTest[][3][2]={afHelp1,afHelp2};

因为afTest可以看做是数组元素为二维数组的一维数组。不过计算机给出了很好的评判:

error C2440:“初始化”:无法从“double[3][2]”转换为“double”

在编程世界里面,千万不能忽视编译错误,更应该养成良好的分析错误的习惯。

下面我们一起讨论出现的编译错误,虽然有些知识还显得为时过早。

上述错误能准确显示在何处错误,而且错在何处。这个显然是初始化的问题,double[3][2]在这里似乎成了一种类型,这一点大家务必注意,它可以帮助我们打开思维的大门,让我们得到一个重要的结论———数组确实是一种类型,这也证实了上述的论断。

尽管在C语言中不能这样显式地定义:

double[3][2]afTest;

但是可以看到编译器已经将其视为一种类型了。同时从中可以得到另外一条更为重要的信息,double才是编译器认同的,不然它不会认为不能转为double而报错了。这就很明确地告诉我们,需要的是double的值,那么这就为第二种形式提供了很好的证据,因为第二种形式全部是double的值。这些都是从错误提示中得到的。

我们继续回答第一个问题,既然形式上不能这样表明,那么可以用代数的思想处理,就是将afHelp1和afHelp2的初始化列表代入其中,这样就得到了第一种形式。

第二种形式更能从本质上说明三维数组和一维数组的关系。这一点在前面已经做了阐述,不再赘述。综上所述,两者在这里虽然起到了相同的功效,对数组做了相同的初始化,但是形式不同,第一种逻辑性强,第二种更反映本质。其他的区别我们接着看后面的问题。

再来看第二个问题,先看第一种形式,可以从列表的结构看出第一维长度为2,因为最外层的列表有两个元素,这个可以通过“{}”的匹配看出来。再看第二种形式,可以由初始化列表的长度除以第二维和第三维长度之积得到第一维的长度。

现在看最后一个问题,通过上面的分析,可以得到第一维的长度是2,当然可以在定义的时候直接加上第一维的长度。但是要注意,加上以后,初始化列表不能再增加数据了。因为12个(各个维的长度之积)元素已全部指定了值,从1.1到12.12。那么将其中的几个值去掉又如何呢?具体结果在例2.2中有详细探讨。这里只简单地说明一下原因:如果采用第一种形式,由于“{}”进行了“隔断”,所以值相对来讲就受到了一定的限制,换句话说归属更明晰了。实际上对于每一个“{}”,其实际长度已经由数组的定义完全确定了。如果其值个数超过了这个长度,将发生编译错误。如果小于这个长度,将后面的全部按0进行补齐。

如果不进行初始化,变量的值就无法确定,在不同情况下,对该问题有不同的解决办法。一般的方案是提供系统默认值,或者是在变量数据类型的范围内给出随机的值,但是在Visual C++6.0中,规定必须给变量赋初值。不然将产生运行时错误,所以我们不难发现例2.1错误的原因。

2.3.3 常量

C语言中的常量是在程序运算过程中值不能被改变的量。显而易见,常量是不接受程序修改的固定值,常量可为任意数据类型。

(1)目的和意义

大家可能会问,为什么要引入常量呢?其实,C语言引入常量的原因很多,具体说来有以下几种:

①增强程序可读性。符号常量可以提醒我们它的作用。例如:

tax=income*PERCENT;

tax=income*0.1;

第二个语句中的0.1让人摸不着头脑,不知道它是什么意思,而第一个语句,看到PERCENT就可以知道这是一个百分比。

②便于程序移植。

③便于程序更新和维护。如程序中要提高百分比,只需要修改常量定义,而不需修改程序中进行百分比计算的所有地方。

(2)声明和定义

在C语言中,常量可以表示为:ˊaˊ、ˊnˊ、ˊ9ˊ、123、2100、-234、35000、-34、10、-12、90、123.23、4.34e-3、123.23、12312333、-0.9876234。

C语言还支持另一种预定义数据类型的常量,这就是串。所有串常量括在双撇号之间,例如"Thisisatest"。切记,不要把字符和串相混淆,单个字符常量是由单撇号括起来的,例如ˊaˊ。

常量定义是指定义符号常量,用一个标识符来代表一个常量,通过宏定义预处理指令来实现,其格式如下:

#define标识符 常量

由用户命名的标识符是符号常量名。符号常量名一般用大写,以便和普通变量区别。符号常量一旦定义,在程序中凡是出现常量的地方均可用符号常量名来代替。

【例2.2】符号常量的使用。

①打开Visual C++6.0环境,建立一个控制台工程,其名称为Example_2_2_DefinedConst;

②添加C源文件,并编写如下代码:

img30

③运行结果如图2-3所示。

img31

图2-3

④程序分析

#define PERCENT 0.1定义了一个符号常量名PERCENT,它的值为0.1。当程序被编译时,程序中所有在#define PERCENT 0.1后面的PERCENT都会被替换成0.1。要注意必须完全匹配才会被替换。这种替换就是所谓的编译时替换。从源代码到可执行文件要经历三个阶段:预处理、编译、链接。编译时替换是在预处理阶段,由预处理器完成的。上述程序预处理后变成:

img32

在C语言中,我们还可以利用const修饰符来定义常量:

const int inCome=4000;//inCome是常量,它的值是4000

以上语句声明了一个int型常量。只能读取inCome(即4000)的值,而不能修改它的值。通常,把这种常量也称为符号常量。

如果程序中多个地方使用了同一个常量(如0.1),当我们需要修改这个常量时,必须修改用到该常量的所有地方。工作量大暂且不提,更可怕的是我们可能会漏改、错改,或者误改了某些不该改的常量。而使用符号常量就不同了。我们只需把符号常量的值修改即可。

const和define的最大不同是:define在编译时只进行字符的替换,将程序中出现的PERCENT用0.1替换,在程序的运行期间没有PERCENT这个东西。而const则定义了一个变量,并且它的值是固定的,可以得到这个变量的地址。

如果定义了一个名为PI的变量,但是没有指定类型,那么编译器就默认为PI为int型,这样经过类型转换PI的值就是3。应该这样定义:

const double PI=3.1415;

define定义的是宏,不是变量。有什么区别呢?如这个程序中,使用#define时,0.1会直接替换掉程序中的PERCENT,注意是原封不动的替换,相当于是写的0.1*income。