第3章 程序中的数据
本章包括
◆ 常量和变量的概念
◆ 变量的定义、初始化和赋值
◆ const常量的使用
◆ C++中的数据类型
◆ 结构体、共用体、枚举体
◆ 使用宏替换字面常量
处理数据是程序的主要目的,包括输入原始数据、对数据进行各种计算、输出处理的结果。一个什么都不做的程序是没有意义的。当初人们发明计算机,开始编写第一个程序的目的,就是为了计算炮弹的轨迹。当然,现在程序中数据处理的范围已经非常广泛了,从财务统计到航天飞机轨道计算,从计算机图像处理到卫星遥测,无处不见数据处理的身影。本章的重点是理解C++中的各种数据类型,以及用来表示数据的常量和变量的用法。
3.1 常量和变量
现实世界中的事物有很多信息,比如声音、图像、个数、名称等。这些信息在计算机中的表达就是数据。不过,由于计算机本身的限制,不可能完整、清楚地表达整个信息,而且实际上这么做也是不必要的。在程序中只需将代表信息特征的、需要程序处理的部分抽取出来即可。程序中的数据有常量和变量之分,下面将一一介绍。
提示
实际上计算机只能处理二进制数,即由0和1组成的数字,就连计算机本身的指令也是由这样的二进制数表示的。所以,所有计算机能够处理的数据在计算机内部都是二进制数。为了方便人们理解,并提高程序开发的效率,像C++这样的高级语言都会按照自己的方式去定义和组织数据,并使之以符合人类思维的形式出现。
3.1.1 什么是常量
顾名思义,常量的值是不能改变的。C++中的常量有三种:字面常量、符号常量和枚举型常量。这里只介绍字面常量。源代码中出现的数值,如1,2.3,a,Hello World等,称为字面常量。之所以称为“字面”,是因为只能用其值来指代,称之为“常量”,是因为其值不能被改变。例如123这个常量,其值只能是123,而不能被改成456。
字面常量没有名字,要使用一个字面常量就只能通过常量的值。在程序中字面常量的功能是用来初始化变量、给变量赋值、参与表达式计算等。
字面常量是有类型的,这是由其值决定的。例如字面常量1和2.3就是两个不同类型的常量,一个是整数,另一个是小数。另外还有表示单个字符的常量,如a。以及表示一串字符的常量,如Hello World。不同类型的常量,其能够接受的处理方法也不相同,例如1和2.3可以进行四则运算,而Hello World就不行。
为了区分不同类型的常量,在C++中有数据类型的概念,这将在3.2节中进行讲述。
3.1.2 什么是变量
变量,顾名思义,其值是可以改变的。在C++程序中,变量就是用名称标明的一块儿内存,其中可以存储数据。在程序的运行过程中,变量的值会发生改变。假设a是变量,则其典型的使用过程如下:
a = 1; // 用一个字面常量给变量a赋值 a = 1 + 2; // 计算表达式(1+2这个算式是一个表达式),并将结果赋给a a – 1; // 变量a参与表达式计算 2 * a; // 同上
变量有名称,正如上述例子所示,在程序中操作一个变量是通过变量名实现的。变量名不是任意的,必须符合一定的命名规则:变量名只能由字母、数字以及下画线组成,并且必须以字母或下画线开头,字母包括小写字母a~z以及大写字母A~Z,数字是0~9。下面给出几个变量名:
abc // 正确的标识符 _def // 正确的标识符 9gh // 错误的标识符
说明
在C++中,变量名是一种标识符。标识符是对程序中实体名称的统称。这些实体包括符号常量、枚举常量、变量、函数、类、结构体、模板等。标识符的命名规则同变量的命名规则是一样的,即只能由字母、数字和下画线组成,并且不能以数字开头。
变量名区分大小写,如Abc和abc就是两个不同的变量名,代表两个不同的变量。C++语言本身对变量名的长度没有限制,但从阅读和理解程序的角度来讲,变量名不应该过长。命名变量时应该以易记、意思明确为原则,即容易记住,并且能够清晰地表达编程者的意图。如birthDate,sum和idNum就是很好的变量名,而dfds34和really_long_variable_name_not_ok就不是好的变量名。
变量的标识符在其作用域内是唯一的。关于变量的作用域在后文中将有详细的介绍,这里只要保证标识符不重复就可以了。变量除了有名称、值这两个特性之外,还有一个类型属性。同常量的类型一样,变量的类型也是为了区别不同类型的数据,用不同的方法进行处理。
3.1.3 定义变量
要在程序中使用变量,必须先定义变量。在程序中定义变量的目的是让程序分配一块儿内存,并为其命名。这个名字就是变量的标识符。与C语言不同,C++可以随时定义所需的变量,而不必放在函数的开始处。定义变量时,先指定变量的类型,再给出变量名,并以分号“;”结束,格式如下:
类型 变量名;
以分号结束变量的定义,表示变量的定义也是一个语句。其中的“类型”就是这个变量所能处理的数据的类型。为了表示各种数据类型,C++语言设立了多个关键字,或者说是类型名,例如:
◆ short,int和long表示不同长度的整型数。这里的长度指的是变量所占内存的大小,不同的编译器有不同的实现。一般在32位计算机中,short表示2个字节的整数,int表示4个字节的整数,long表示4个字节(或8个字节,一般是4个字节)的整数。
◆ char表示字符型数据。在32位计算机中一般是一个字节。除了char,表示字符型数据的还有wchar_t,不过wchar_t表示的是宽字符。一个宽字符在32位计算机中一般占2个字节。
◆ float,double和long double分别表示单精度、双精度和长双精度的浮点型数据,其长度分别是4字节、8字节和16字节。
由于数据类型众多,而上述关键字远远满足不了程序对数据类型的需求,所以C++也允许用户自定义数据类型。C++中的类就是这样一种用户自定义类型,另外还有结构体、共用体、枚举体等。典型的变量定义如下:
int a; // 定义一个int型变量a char b; // 定义一个char型变量b float c; // 定义一个float型变量c double d; // 定义一个double型变量d Point pt; // 定义一个Point型变量pt,Point是用户自定义的变量类型,不是C++关键字
C++允许在一个语句中定义多个变量,但必须是同一类型的,例如:
int a, b, c = 2; double x = 0.0, y = 1.2; char m, n;
说明
所谓关键字就是C++语言的保留字,用户不能更改其含义,只能按其规定的用途使用。例如上述的各种类型名就是关键字,主函数名main也是关键字,表示函数返回的return也是关键字。关键字不能用做标识符,否则会引起编译错误。
变量也是有生命周期的,每个变量的生命周期都是从其定义时开始。也就是说,一个变量只有被定义了,才能开始使用。至于变量的生命周期什么时候结束,则在以后的章节中进行讲述。不过,可以肯定的一点是,当程序结束时,变量的生命周期也就结束了,不能保留到下次程序运行。
3.1.4 初始化变量
无论开发者是否指定,变量在定义后都会有一个初始值。如果不指定,则这个值就是一个未定义的值。所谓未定义,就是C++标准并没有规定具体的数值,而是由编译器根据需要自行指定。但这个值是没有意义的,开发者在编写程序时不能依赖这个值。
变量的初始化可以在定义变量时进行,只需在变量名后加上等号“=”和一个初始化值即可。这个初始化值可以是一个常量,也可以是一个变量。初始化变量的语法如下:
类型 变量名 = 初始化值;
其含义是定义一个变量,并将该变量初始化为等号右边的值。
例如,定义一个整型变量,并将其初始化为123,语句如下:
int a = 123;
其他类型变量的初始化也是一样的,例如:
char c = 'c'; wchar_t w = L'w'; double d = 1.23;
变量也可以用已经存在的变量进行初始化,例如:
int a = 123; int b = a;
如果变量定义时没有初始化,则其值取决于编译器的实现。这样的值是没有意义的,所以使用未经初始化的变量比较危险,例如:
int sum; // 定义一个变量,用来保存1到100的和。警告:sum没有初始化! for( int i=1; i<=100; i++ ) // 将后面花括号“{”和“}”中的语句重复100遍 { sum += i; // 将当前i的值加到变量sum上 } cout<< sum <<endl
上述代码的本意是求1到100的和,但打印出来的值并不是期望的5050,而是一个只有编译器才能确定的值。
C++的语法允许在一个语句里定义多个变量,也允许在一个语句里初始化多个变量,语法如下:
类型 变量名1 = 值1, 变量名2 = 值2, ⋯⋯, 变量名n = 值n;
其含义是依次定义n个变量,并分别初始化每个变量,例如:
int a = 1, b = 2, c = 3; // 依次定义并初始化变量a,b,c
另外,C++也支持链式初始化,即用等号“=”连接多个变量名,并在最后一个变量名后面加上一个等号和一个初始化值,这样就可以将全部变量都初始化成一个值,其语法如下:
类型 变量名1 = 变量名2 =⋯⋯= 变量名n = 初始化值;
例如:
int a = b = c = 123; double d = e = f = 1.23;
注意,链式初始化的过程是从右往左的,即先定义最右面的变量,并将其初始化为等号右边的值,然后依次向左定义并初始化各个变量。
3.1.5 为变量赋值
既然是变量,则在定义之后,其值就是可以修改的。修改变量值的过程就是赋值,其语法同初始化语法类似,只是没有类型关键字,即:
变量 = 值;
其中等号左边的变量是用变量名标识的,等号右边的值是一个常量或者已经存在的变量。这里的等号称为赋值运算符,其作用就是将等号右边的值赋给等号左边的变量,例如:
a = 345; // 将345赋给整型变量a c = 'd'; // 将'd'赋给字符型变量c w = L'x'; // 将宽字符'x'赋给宽字符型变量w d = 4.56; // 将4.56赋给双精度浮点型变量d
为变量赋值时不仅可以用字面常量,也可以用已经定义好的变量,例如:
int a = 123; int b = a; // 将变量b初始化为a的值,运算后,b的值为123 a = b; // 将变量b的值赋给a
说明
虽然都是使用等号“=”,但初始化和赋值的含义是不一样的。初始化是给未曾使用的变量设定一个值,而赋值则是修改已经在使用的变量的值;初始化只发生一次,即在变量定义时,而赋值则可以发生多次。
另外,为变量赋值也支持链式赋值,即为一系列的变量赋以同样的值。其语法同变量的链式初始化类似,只是没有开始的类型关键字,即:
变量名1 = 值1, 变量名2 = 值2, ⋯⋯, 变量名n = 值n;
例如,将整型变量a,b,c和d都赋值为123:
a = b = c = d = 123;
链式赋值的顺序同链式初始化的顺序相同,都是从右向左的,即先给最右面的变量赋值,然后依次向左给每个变量赋值。
变量的值,无论是初始化的值,还是后来赋的值,其类型都应当与变量的类型相同,或者符合转换规则。我们可以将整数转换为浮点型数,浮点型数也可以转换为整数(存在精度损失),后文中将会详细介绍这个转换规则,这里只要保持类型相同即可。
3.2 数据类型
C++是一种强类型的语言,也就是说程序中用到的数据都是某种类型的数据,不存在某个数据没有类型的情况。C++标准定义了一些常用的数据类型,下面将一一进行讲述。另外,C++也支持用户自定义类型。
3.2.1 整型
表示整数的基本类型有int(整型)、short int(短整型,int可省略)、long int(长整型,int可省略)以及long long int(长长整型,int可省略),分别表示不同长度的整数值。除非特别说明,上述各种整数的类型都可以称为整型。
各种整型字面常量如下:
123 // 整型或短整形 456L // 长整型数,带有后缀“L” 789l // 长整型数,带有后缀“l”(小写的L) 901LL // 长长整型数,带有后缀“LL” 234ll // 长长整型数,带有后缀“ll”
在默认情况下,整型字面常量被当做一个int型值。如果在字面常量后面加一个“L”或“l”字母,即L的大写形式或者小写形式,则将其指定为long型。一般情况下应该避免使用小写字母,因为它很容易被误当做数字“1”。带有两个L后缀(“LL”或者“ll”)的是long long型数。注意,长长整型数的后缀,大小写不可混用,如123Ll就是一个错误的写法。
提示
short型的字面常量没有后缀,其形式同int型的字面常量一致,编译器会根据需要将这个字面常量解释为short型或者int型。
各种整型的字面常量可以被写成十进制、八进制或者十六进制的形式,例如十进制的24可以写成下面几种形式中的任意一种:
24 // 十进制 030 // 八进制,带有前缀“0” 0xF8 // 十六进制,带有前缀“0x” 0XF8 // 十六进制,带有前缀“0X”
如果一个字面常量在程序中写成上述形式,则编译器会自动将其当做整型数对待。在整型字面常量前面加一个“0”,该值将被解释成一个八进制数,而在前面加一个“0x”或“0X”,则会使一个整型字面常量被解释成十六进制数。定义并初始化各种整型变量的例子如下:
short a = 123; // 短整型变量a,与“short int a;”等价 int b = 456; // 整型变量b long c = 789L; // 长整型变量c,与“long int c;”等价 int e = 024; // 整型变量e,初始化为八进制的024,即十进制的20 long f = 0x1A; // 长整型变量f,初始化为十六进制的0x1A,即十进制的26
3.2.2 特殊整型
实际上在C++标准中,并没有具体规定某种类型数据所占内存的大小,而只是规定了其大小与机器字的关系。所谓机器字的大小就是计算机的位数,例如16位计算机的机器字是16位,2个字节;32位计算机的机器字是32位,4个字节。
C++标准规定short的大小是半个机器字,int的大小是一个机器字,而long的大小是一到两个机器字。这就存在一个程序移植方面的问题:如果两个计算机的机器字大小不一样(比如一个是32位,而另一个是64位),则在定义、初始化、赋值时对整型变量的操作就不一样,从而导致这两台计算机的程序不可以互相移植。
为了解决这个问题,C++标准引入了一些特殊类型关键字用来表示确定大小的整型数。如__int8,__int16,__int32和__int64分别表示8位、16位、32位和64位的整数,使用这些类型定义变量的语法同int型一样,如:
__int8 a = 12; __int16 b = 35; __int32 c = 215; __int64 d = 4234;
3.2.3 无符号整型
整数分为无符号整数和有符号整数。之所以要区分有符号和无符号整数,是因为有时需要使用负数,有时则不需要。例如表示温度时应该用有符号数,而计数时则只用无符号数就够了。
如果要明确指明某个类型是无符号类型,则需要在该类型符前面加上unsigned关键字,例如无符号int型就是unsigned int,无符号long型就是unsigned long。有符号整型可以在类型符前面加上signed关键字,但一般可以省略,即signed int和int表示的都是有符号整型。无符号整型字面常量可以在一般字面常量后面加上后缀“u”或“U”来表示,如:
123u,345LU,57uL
定义、初始化和赋值无符号变量的方法如下:
unsigned int x = 123u; unsigned long y = 546LU;
3.2.4 浮点型
小数在计算机中称为浮点型数,其数据类型有float(单精度浮点数)、double(双精度浮点数)和long double(长双精度浮点数,或称扩展精度浮点数)。各种浮点数的字面常量如下:
1.23f // 单精度浮点数,带有后缀“f” 1.23F // 单精度浮点数,带有后缀“F” 4.56 // 双精度浮点数,不带有任何后缀 7. // 双精度浮点数,省略小数点后面的0 8.9L // 长双精度浮点数,带有后缀“L”
没有后缀的浮点型字面常量是double型的。单精度字面常量带有后缀“f”或“F”。类似地,长双精度字面常量带有后缀“l”或“L”。
当心
浮点型字面常量的后缀“f”、“F”、“l”、“L”只能用在十进制形式中。
浮点型字面常量可以被写成普通的十进制形式,或者用科学计数法表示。科学计数法是指数值中带有指数的表示方式,如:
3e2 // 表示3*102
1.2E-2 // 表示1.2*10-2
其中指数的底数是10,用e或E表示,后面的数值是10的次数。使用浮点型定义变量的语法如下:
float a = 1.23f; double b = 3.45; long double c = 5.72L;
3.2.5 字符型
字符的数据类型是char,通常用来表示单个字符或小整数。字符型字面常量用两个单引号('')包围的字符表示,如:
'a' // 字符常量a '1' // 字符常量1 '+' // 字符常量+ ' ' // 字符常量空格
字符型数据的定义、初始化和赋值方法如下:
char c; // 定义一个字符型变量 c = 'A'; // 为字符型变量赋值 char d = 'D'; // 定义并初始化一个字符型变量
字符型数据和整型数有一定的转换关系。计算机不能直接存储字符,所以所有字符都是用数字编码来表示和处理的。最早的编码是ASCII码,例如'a'的ASCII码是97,'b'的ASCII码是98,'+'的ASCII码是43。如果一个字符被当做整数使用,则其值就是对应的ASCII码。如果一个整数被当做字符使用,则该字符就是这个整数在ASCII码表中对应的字符。
一个整型变量可以用一个字符初始化,也可以用一个字符赋值;反之,一个字符变量也可以用一个整数进行初始化和赋值,如:
int x = 'a'; // 定义一个整型变量,并用字符'a'初始化 cout<< x << endl; // 输出的结果是97,因为'a'的ASCII码是97 x = 'A'; // 将变量x赋值为'A' cout<< x << endl; // 输出的结果是65,因为'A'的ASCII码是65 char y = 66; // 定义一个字符型变量,并用整数66初始化 cout<< y << endl; // 输出的结果是'B',因为66对应的字符是'B' y = 98; // 将变量y赋值为98 cout<< y << endl; // 输出的结果是'b',因为98对应的字符是'b'
当心
在用整数表示一个字符时,注意不要超过其取值范围。ASCII码表总共有128个字符,所以与字符数据对应的整数也就有128个,其取值范围是0~127。
下面编写程序依次输出大小写字母对应的ASCII码值,如示例代码3.1所示。
示例代码3.1
#include <cstdlib> #include <iostream> using namespace std; // 使用名称空间std int main(int argc, char *argv[]) // 主函数 { int a = 'a'; // 定义整型变量a,并初始化为字符'a'的ASCII码 for( int i=0; i<26; i++ ) // 遍历26个小写字母 { cout<< char(a+i) <<'='<<a + i<<'\t'; // 输出字母及对应的ASCII码 if( 0 == (i+1) % 5 ) // 每输出5个,换一次行 { cout<<endl; } } cout<<endl; // 分隔小写和大写字母 a = 'A'; // 将变量a赋值为大写字母'A'的ASCII码 for( int i=0; i<26; i++ ) // 遍历26个大写字母 { cout<< char(a+i) <<'='<<a + i<<'\t'; // 输出字母及对应的ASCII码 if( 0 == (i+1) % 5 ) // 每输出5个,换一次行 { cout<<endl; } } cout<<endl; // 为整个列表换行 system("PAUSE"); // 等待用户反应 return EXIT_SUCCESS; // 主函数返回 }
建立一个控制台工程,并将上述代码复制到源代码文件中,编译并运行,结果如图3-1所示。
图3-1 字母与对应的ASCII码
上面代码中“a+i”的作用是求得第i个字符的ASCII码值(一个整数),前面的char(a+i)的作用是将求得的ASCII码值再转换成对应的字符。
3.2.6 无符号字符型
前面的char型是有符号字符型,只不过没有编号为负数的ASCII码字符而已。除了有符号字符型之外,在C++中还有无符号字符型,其类型关键字为unsigned char。无符号字符型的取值,除了包括ASCII码表上的所有字符外,还包括一个扩展ASCII码表上的所有字符。使用目前常见的键盘,无法输入扩展ASCII码表上的字符,但可以通过字符与整数的关系,来初始化或赋值无符号字符型变量,例如:
unsigned char c = 128; // 定义并初始化无符号字符型变量 cout<< c << endl; // 输出无符号型字符
3.2.7 转义字符
在C++中有些字符不能直接表示,只能通过特殊的方法来表示,比如换行符、制表符、响铃符等。对于这类字符可以用在某个字符前加一个反斜杠来表示。反斜杠的含义是:改变其后面字符的意义,用来表示另外的字符。这个表示方法也称为转义。一般的转义字符如表3-1所示。
表3-1 转义字符表1
使用转义字符时,虽然在单引号内有两个字符,但实际上反斜杠表示一个转义的开始。如'\n'就是将原来的字符n转义成一个换行符,'\a'就是将原来的字符a转义成响铃符。
提示
换行和回车是不一样的。换行只是将输出位置换到下一行,不改变输出的横坐标;回车是回到行首,不改变输出的纵坐标。
有些有歧义的字符,也需要用反斜杠开始的转义序列表示。比如,一个反斜杠字符就无法单用一个“\”表示,因为一个反斜杠已经用来表示转义的开始;单引号也无法单独表示,必须借助一个反斜杠进行转义。像这样的字符如表3-2所示。例如,为了输出上述字符,在程序中应当如下书写代码:
表3-2 转义字符表2
cout<< '\\' <<endl; // 输出单个反斜杠 cout<< '\'' <<endl; // 输出单个单引号 cout<< '\123' << endl; // 输出大写字母“S”,八进制数123在ASCII表中对应“S” cout<< '\x42' <<endl; // 输出大写字母“B”,十六进制数42在ASCII表中对应“B”
3.2.8 宽字符型
在C++中,字符型除了char类型,还有一种wchar_t类型,表示宽字符。其字面常量是在char型字符前加上前缀“L”,例如:
L'a' // 宽字符型常量a
所谓宽字符,指的是用两个字节表示的字符。C++中使用宽字符是为了支持Unicode。随着计算机技术的飞速发展,各种语言的数据都需要在计算机中存储和处理。但ASCII码仅能处理英文字符,远远满足不了人们的需要。于是人们发明了一种多字节编码,即用多个变长字节表示字符。但这种方式并没有在各种语言中统一,一个编码在一种语言中代表一个字符,而在另外一种语言中又表示其他的字符。后来人们对各种语言编码的方式做了一次统一,其目的是用同一种编码方式处理世界上所有语言中的字符,这就是Unicode编码。Unicode编码规范非常复杂,读者可以参考相关的资料和书籍,限于篇幅,这里不做深入介绍,这里读者只需知道C++的wchar_t类型是用来表示和处理Unicode字符的即可。宽字符变量的定义、初始化和赋值如下:
wchar_t w; // 定义一个宽字符型变量 w = L'W'; // 为变量赋值 wchar_t v = L'V'; // 定义并初始化一个宽字符型变量
3.2.9 布尔型
布尔型数据的类型用bool表示,其字面常量只有两个:true和false,分别表示逻辑真和逻辑假。布尔型变量的定义、初始化和赋值如下:
bool b; // 定义一个bool类型的变量 b = true; // 为变量赋值 bool c = false; // 定义并初始化一个变量
在程序中,一个布尔型变量常用来表示某个条件是否成立,例如:
int a = 0, b = 0; // 定义两个整型变量 cin>>a; // 由用户输入变量的值 cin>>b; bool flag = false; // 定义一个布尔型变量 if( a > b ) // 如果变量a大于变量b { flag = true; // 将布尔型变量设成true }
在程序中,布尔型变量可以看做整型变量。字面常量true可以当做整数1,false可以当做整数0,例如:
int a = true; // 定义一个int型变量,并初始化为true,其实是被初始化为1 cout<< a <<endl; // 实际输出1 a = false; // 将false赋值给int型变量,其实是将0赋值给变量 cout<< a <<endl; // 实际输出0
当其他类型的数据转换为布尔型数据时,只要是非0的数据都将转换为true,而0则转换为false,例如:
bool b = -123; // true b = 0; // false b = '\0'; // false b = 'a'; // true b = 0.0; // false b = 1.2; // true
3.3 变量与内存的关系
通过前面的学习,读者已经了解到变量就是一段内存。既然是内存,就有地址、大小等方面的属性,下面将一一进行讲述。
3.3.1 变量的地址
计算机的内存是按照字节进行编码的。内存中一个字节的编码就是这个字节的地址。操作系统可以按照内存地址对内存进行存取操作。在C++程序中,定义一个变量就是声明占用一块儿内存,变量名就指向这块儿内存的首地址。变量名与内存地址的关系如图3-2所示。
图3-2 变量名与内存地址的关系
3.3.2 变量的字节长度
变量是用来存储数据的,而不同类型的数据对内存大小的要求是不一样的。数据占用的内存数量称为数据的大小,通常用机器字来衡量。一个机器字即计算机在一个时钟周期内,能够从内存中读取的数据大小,在32位机器中是2个字节(32位),在64位机器中就是4个字节(64位)。
C++标准规定了内建的数据类型的大小。例如,int是一个机器字,short是半个机器字,long是一个或两个机器字;float是一个机器字,double是两个机器字,long double是3或4个机器字。对于某个确定版本的编译器和某种计算机处理器,这些尺寸都是固定的。
说明
虽然数据的尺寸是用机器字来衡量的,但通常在表述数据大小时却是以字节为单位的。在32位机器中,一个机器字是4个字节,所以,在32位平台中常用的int型是4个字节,short型是2个字节,long型一般也是4个字节。
在C++程序中定义一个变量,即表示占用一块儿内存。变量名代表这块儿内存的首地址,变量的类型则决定了这块儿内存的长度。例如,在程序中定义两个变量:
char c = 'c'; int a = 12;
其占用内存情况如图3-3所示。
图3-3 变量占用内存情况
3.3.3 计算数据的字节长度
sizeof运算符的作用是返回一个类型或数据的字节长度。sizeof的使用方法比较灵活,在sizeof关键字后面可带括号,也可不带括号;括号中可以是类型名,也可以是变量名。其格式如下:
sizeof( <类型名> ); sizeof( <变量名或者常量> ); sizeof <变量名或者常量>;
sizeof运算的结果是一个size_t型的数据。size_t是一种内建数据类型的别名,关于类型别名将在以后的章节中说明,目前可以将其作为整型使用。下面使用sizeof运算符计算类型和数据的尺寸,如示例代码3.2所示。
#include <cstdlib> #include <iostream> using namespace std; // 使用名称空间std int main(int argc, char *argv[]) // 主函数 { // 定义各种类型的变量,并初始化 short a0 = 0; int a1 = 1; long a2 = 2L; char c = 'c'; float d0 = 1.23F; double d1 = 4.56; bool b = true; // 计算并输出各种数据类型和数据的字节长度 cout<<"shot: "<<sizeof(short)<<'\t'<<"0: "<<sizeof(0)<<endl; cout<<"int: "<<sizeof(int)<<'\t'<<"1: "<<sizeof(1)<<endl; cout<<"long: "<<sizeof(long)<<'\t'<<"2L: "<<sizeof 2L<<endl; // 注意:sizeof没有用括号 cout<<"char: "<<sizeof(char)<<'\t'<<"\'c\': "<<sizeof('c')<<endl; cout<<"float: "<<sizeof(float)<<'\t'<<"1.23F: "<<sizeof(1.23F)<<endl; cout<<"double: "<<sizeof(double)<<'\t'<<"4.56: "<<sizeof(4.56)<<endl; cout<<"bool: "<<sizeof(bool)<<'\t'<<"true: "<<sizeof(true)<<endl; // cout<<sizeof int<<endl; // 不使用括号的形式,不能用于类型 system("PAUSE"); // 等待用户反应 return EXIT_SUCCESS; // 主函数返回 }
建立一个控制台工程,并将上述代码复制到源代码文件中,编译并运行,结果如图3-4所示。
图3-4 用sizeof运算符计算字节长度
不同的计算机平台,计算的结果可能不一样,这取决于所用计算机的处理器和编译器。
说明
本书中如果没有特别指出,使用的计算机平台都是32位的。
3.3.4 变量的取值范围
变量的类型决定了可取值的类型,而类型的字节长度则决定了变量可取值的范围。如字符型变量只占一个字节,它的值可以是ASCII和扩展ASCII字符集中的256个字符和符号。短整型(short)占两个字节,即16位,可以取值的范围是-216~216-1,即-32768~32767。其他各种数据类型的字节长度及其取值范围如表3-3所示。
表3-3 变量字节长度及其取值范围
3.4 自定义数据类型
C++内置的数据类型远远满足不了人们对数据类型的需要,为此,C++标准也支持用户自定义数据类型。自定义数据类型的种类有结构体、共用体、枚举体和类。在C++语言中,结构体、共用体与类的定义基本相同,只有很小的差别。
3.4.1结构体
结构体是一个包含多个数据成员的集合,到底包含哪些数据成员,开发者可以自己定义。定义结构体的关键字是struct,其后是结构体名称,然后是由两个花括号包围的结构体。花括号中是组成结构体的各种成员,可以是数据,也可以是函数。每个成员用分号“;”作为结尾。定义结构体格式如下:
struct标识符 { 成员列表 };
例如定义一个拥有整型数和浮点数的结构体:
struct SomeStruct // SomeStruct是结构体名称 { float a; // 成员 int b; // 成员 }; // 分号表示结构体定义结束
当心
结构体定义最后的花括号后面必须带有一个分号,表示定义的结束。
由于结构体也是一种数据类型,所以也可以用来定义变量,其方法和定义普通变量的方法一样。例如定义一个SomeStruct结构体的变量:为了访问结构体变量中的成员,可以使用运算符“.”,例如:
SomeStruct s; s.a = 10;
上面是对结构体变量s的成员a进行赋值的操作。结构体类型本身不占用任何内存空间,只有结构体变量才占用内存空间,其占用的内存空间大小是各个数据成员类型大小之和。不过各种编译器出于效率的考虑,往往会对结构体变量进行内存对齐操作,从而使得结构体变量所占内存大小往往大于各个数据成员类型大小的和。
3.4.2 共用体
与结构体不同,共用体的数据成员存储时共享存储空间。共用体在有的书中也被称为联合体类。定义共用体类型用关键字union,形式为:
union标识符 { 成员表 };
例如,定义一个共用体类型,要求包含一个整数、一个字符和一个单精度浮点数:
union SomeUnion { int a; char b; float c; };
定义共用体变量同定义普通类型的变量类似,如:
SomeUnion a;
对于共用体类型数据,其各个数据成员占用的内存是共享的,修改一个成员,就等于修改其他成员,例如:
union INT_UN { int a; int b; }; ⋯⋯ INT_UN x; x.a = 123; cout<< x.b << endl;
上述程序代码的输出结果是123。虽然程序没有直接给x的成员b赋值,但由于是共用体,其成员变量共享内存,所以修改a就会影响到b,所以b的值也是123。
3.4.3 枚举体
如果要使变量只能使用有限的几个值,则应当使用枚举体。之所以叫枚举体,就是因为在定义枚举体类型时,需要将所有可能的值一一列举出来。定义枚举体的关键字是enum。例如定义一个代表星期几的变量,这样的变量的取值范围是固定的,就是星期一到星期天这7个值,所以应当使用枚举体。
enum Day {Mon, Tue, Wed, Thu, Fri, Sat, Sun };
枚举体类型的变量可以如下定义:
Day day = Tue;
其中day是一个枚举体Day类型的变量,其可能的取值就是定义Day时列举的7个值,在这里就是Tue。
虽然枚举体的值是由一些字符组成的,但这些值却可以当成整型数使用。如没有特别指明,则枚举体中的值就是从0开始的整数。例如在Day中,Mon对应的整数值是0,Tue对应的整数值是1,依此类推。
说明
定义枚举体时所列举的各个标识符代表的是一系列常量,在程序中只能当做常量使用,而不能像变量一样为其赋值。
在定义枚举体时,也可以指定每个分量的值。一旦某个分量的值被指定,则其后分量的值就会在这个值的基础上开始递增。例如,可以根据使用习惯将Day中Mon的值改成1:
enum Day {Mon = 1, Tue, Wed, Thu, Fri, Sat, Sun };
如上定义后Tue的值就是2,Wed值是3,依此类推。
3.5 用宏替换字面常量
字面常量,如123,'a'等,虽然简单、直接,但使用起来并不方便。由于没有标识符,所以后来的人在阅读程序时就难以知道其由来。而且,如果一个字面常量被多次用到,还容易写错。例如,圆周率就是这样一个常量:
double radius = 1.2; // 圆半径 double perimeter = radius * 2 * 3.14159265; // 求圆的周长 double area = 3.14159265 * radius * radius; // 求圆的面积 double perigonRadian = 2 * 3.14159265; // 求周角的弧度 double rightRadian = 0.25 * 3.1415927; // 求直角的弧度,这里的圆周率写错了!
在上述代码中有三个主要问题:
◆ 直接使用字面常量不容易理解。例如在程序中直接使用3.14159265,由于这只是个数值,其本身不能自我说明,除程序员自己,别人难以知道这就是圆周率。
◆ 字面常量容易写错。在上面的代码中就是如此,第5行的圆周率跟前面使用的圆周率都不同。虽然都是真实圆周率的近似值,单独使用都没错,但在一个程序中应当保持一致,而直接使用字面常量难以保证这一点。
◆ 如果程序中有多处使用同一个字面常量,那么当程序逻辑发生改变需要修改这个字面常量时就很麻烦,需要手动修改所有用到的地方。例如上述程序,如果实际计算时发现圆周率的精度不够,要改成3.141592653589才合乎要求,则需要查找所有用到原有圆周率的地方,并逐个进行修改,非常烦琐。
要解决这个问题,可以使用宏。宏是一个预编译指令,其语法如下:
#define <宏名> <宏的内容> // 注意这里没有用“;”结尾
上述语法称为宏定义。有了这个宏定义之后,源代码中的“宏的内容”就可以用“宏名”替换。例如,在上述那个圆周率的例子中,源代码可以这么改写:
#define PI 3.14159265 // 用宏替代常量3.14159265 double radius = 1.2; // 圆半径 double perimeter = radius * 2 * PI; // 求圆的周长 double area = PI * radius * radius; // 求圆的面积 double perigonRadian = 2 * PI; // 求周角的弧度 double rightRadian = 0.25 * PI; // 求直角的弧度 // double radian = PL; // 写错了!程序中没有定义PL
说明
宏的名字一般写成大写,用于与一般的标识符区别开来。
与原来不使用宏的代码相比,使用宏有如下好处:
◆ 宏比字面常量好理解。字面常量只有结合其使用情况才能明确其含义。而定义良好的宏,其本身的名字就可以表明其含义。
◆ 字面常量如果写错,则代表的是另外的常量,编译器不会报错。而宏名如果写错,编译就不会通过,程序的开发者就可以按照编译器的提示进行修改。
◆ 使用宏的程序比仅使用字面常量的程序容易修改。例如要提高圆周率的精度,只要在定义宏的语句中进行修改即可,以后代码中使用的PI都会是修改后的值。
源代码中的宏,在编译之前由预处理器进行宏展开。所谓宏展开,就是将源代码中所有的“宏名”用“宏的内容”进行替换,其效果就如同直接书写“宏的内容”一样。宏展开时,预编译器不会对宏的内容进行任何检查,而是忠实地执行文本替换工作。例如,即便将宏PI定义成3.14abc15926也是合法的,但在编译时会报错。
3.6 用const定义常量
尽管宏(#define)的使用非常方便,但宏只是进行文本替换,而替换的内容没有任何限定。这样做虽然灵活,也非常危险。为此,C++引入了另外一种更好的定义常量的方法——const常量,其语法如下:
const <类型> <常量名> = <初始化值>;
上面的语法类似于变量的定义,只是在前面加上了关键字const。const是限定符,表明其后定义的数据是一个常量。const常量在定义时必须进行初始化,因为一个常量定义后,其值是不能修改的。const常量最大的优点是其定义中规定了常量的类型,因此,编译器可以根据其类型来使用。现在用const常量来修改前面那个圆周率的例子:
const double pi = 3.14159265; // 定义const常量pi并初始化为3.14159265 double radius = 1.2; // 圆半径 double perimeter = radius * 2 * pi; // 求圆的周长 double area = pi * radius * radius; // 求圆的面积 double perigonRadian = 2 * pi; // 求周角的弧度 double rightRadian = 0.25 * pi; // 求直角的弧度
其中常量pi的定义已经指定了其类型是double,这样在初始化时pi能够接受的值就只能是double型的数据。
3.7 综合实例
在本节中将通过综合实例,详细讲解本章所讲语法知识的用法。希望读者通过本节的内容,灵活掌握本章的内容。
3.7.1 计算圆的周长和面积
圆的直径可以使用整数表示,但由于圆周率是一个浮点数,所以圆的周长和面积应当使用浮点数表示。下面设计一个程序,要求用户输入圆的直径,由程序输出圆的周长和面积,如示例代码3.3所示。
示例代码3.3
#include <cstdlib> #include <iostream> using namespace std; // 使用标准名称空间 int main(int argc, char *argv[]) // 主函数 { cout<<"——计算圆的周长和面积——"<<endl; // 提示信息 cout<<endl; const double PI = 3.1415926; // 定义常量PI double radius = 0; // 圆半径变量 cout<<"请输入圆的半径"<<endl; // 提示信息 cin>>radius; // 输入半径 cout<<"周长:"<< 2* PI * radius <<endl; // 计算并输出圆的周长 cout<<"面积:"<< PI*radius*radius <<endl; // 计算并输出圆的面积 cout<<endl; system("PAUSE"); // 等待用户输入 return EXIT_SUCCESS; // 主函数返回 }
建立一个控制台工程,并将上述代码复制到源代码文件中,编译并运行,结果如图3-5所示。
图3-5 计算圆的周长和面积结果
3.7.2 三角形的类型判断和面积计算
三角形是比较简单的二维几何图形,具有边长、角度等属性。在程序中可以用一个结构体来表示三角形。在一些有关图形的计算中,常常需要对三角形进行计算和处理,这些处理可以转变成针对这个三角形结构体的处理。下面设计一个程序,要求用户输入三角形的三条边的边长,判断三角形的类型,并求出三角形的面积,如示例代码3.4所示。
示例代码3.4
#include <cstdlib> #include <iostream> using namespace std; // 使用标准名称空间 struct Triangle // 三角形结构体 { double a; // 边长 double b; // 边长 double c; // 边长 }; int main(int argc, char *argv[]) // 主函数 { cout<<"——三角形的计算和表示——"<<endl; // 提示信息 cout<<endl; Triangle t; // 三角形变量 cout<<"依次输入三角形的边长:"<<endl; // 提示信息 cin>>t.a; // 输入 cin>>t.b; cin>>t.c; double max = t.a > t.b ? t.a : t.b; // 求两边中的最大值 double x = t.a + t.b - max; // 求两边中的较小值 double z = max > t.c ? max : t.c; // 求三边中的最大值 double y = t.a + t.b + t.c - x - z; // 求第三边 if( z <= 0 || z >= x + y ) // 判断三角形的合法性 { cout<<"输入的边长不能组成一个三角形。"<<endl; cout<<endl; system("PAUSE"); // 等待用户输入 return EXIT_SUCCESS; } char *pType = NULL; if(z*z == x*x + y*y) // 是否是直角三角形 { pType = "直角"; } else if( z*z < x*x + y*y ) // 是否是锐角三角形 { pType = "锐角"; } else { pType = "钝角"; // 否则是钝角三角形 } cout<<"三角形类型:"<<pType<<endl; // 输出三角形类型 cout<<endl; system("PAUSE"); // 等待用户输入 return EXIT_SUCCESS; // 主函数返回 }
建立一个控制台工程,并将上述代码复制到源代码文件中,编译并运行,结果如图3-6所示。
图3-6 三角形的类型判断和面积计算结果
3.8 小结
本章的重点是C++程序中的各种数据类型以及变量与内存的关系。一般来讲,程序就是用来处理数据的,所以如何表示和存储各种数据就是程序的基础。当然,程序不仅要能表示和存储数据,还要能够对其进行有效的处理。在程序中,处理数据的基本手段就是语句和表达式,这将是下一章要讲解的内容。另外,除了本章所讲的几种基本数据类型外,在C++中还有指针、引用、对象等类型,这些都将在后面的章节中一一进行讲述。