程序通常都需要存储信息—如Google股票当前的价格、纽约市8月份的平均湿度、美国宪法中使用最多的字母及其相对使用频率或猫王模仿者的数目。为把信息存储在计算机中,程序必须记录3个基本属性:
到目前为止,本书的示例采取的策略都是声明一个变量。声明中使用的类型描述了信息的类型和通过符号来表示其值的变量名。例如,假设实验室首席助理Igor使用了下面的语句:
这些语句告诉程序,它正在存储整数,并使用名称braincount来表示该整数的值(这里为5)。实际上,程序将找到一块能够存储整数的内存,将该内存单元标记为braincount,并将5复制到该内存单元中;然后,您可在程序中使用braincount来访问该内存单元。这些语句没有告诉您,这个值将存储在内存的什么位置,但程序确实记录了这种信息。实际上,可以使用&运算符来检索braincount的内存地址。下一章介绍另一种标识数据的方法(使用指针)时,将介绍这个运算符。
C++提倡使用有一定含义的变量名。如果变量表示差旅费,应将其命名为cost_of_trip或costOfTrip,而不要将其命名为x或cot。必须遵循几种简单的C++命名规则。
倒数第二点与前面几点有些不同,因为使用像_time_stop或_Donut这样的名称不会导致编译器错误,而会导致行为的不确定性。换句话说,不知道结果将是什么。不出现编译器错误的原因是,这样的名称不是非法的,但要留给实现使用。全局名称指的是名称被声明的位置,这将在第4章讨论。
最后一点使得C++与ANSI C(C99标准)有所区别,后者只保证名称中的前63个字符有意义(在ANSI C中,前63个字符相同的名称被认为是相同的,即使第64个字符不同)。
下面是一些有效和无效的C++名称:
如果想用两个或更多的单词组成一个名称,通常的做法是用下划线字符将单词分开,如my_onions;或者从第二个单词开始将每个单词的第一个字母大写,如myEyeTooth。(C程序员倾向于按C语言的方式使用下划线,而Pascal程序员喜欢采用大写方式。)这两种形式都很容易将单词区分开,如carDrip和cardRip或boat_sport和boats_port。
命名方案
变量命名方案和函数命名方案一样,也有很多话题可供讨论。确实,该主题会引发一些最尖锐的反对意见。同样,和函数名称一样,只要变量名合法,C++编译器就不会介意,但是一致、精确的个人命名约定是很有帮助的。
与函数命名一样,大写在变量命名中也是一个关键问题(参见第2章的注释“命名约定”),但很多程序员可能会在变量名中加入其他的信息,即描述变量类型或内容的前缀。例如,可以将整型变量myWeight命名为nMyWeight,其中前缀n用来表示整数值,在阅读代码或变量定义不是十分清楚的情况下,前缀很有用。另外,这个变量也可以叫作intMyWeight,这将更精确,而且容易理解,不过它多了几个字母(对于很多程序员来说,这是非常讨厌的事)。常以这种方式使用的其他前缀有:str或sz(表示以空字符结束的字符串)、b(表示布尔值)、p(表示指针)和c(表示单个字符)。
随着对C++的逐步了解,将发现很多有关前缀命名风格的示例(包括漂亮的m_lpctstr前缀—这是一个类成员值,其中包含了指向常量的长指针和以空字符结尾的字符串),还有其他更奇异、更违反直觉的风格,采不采用这些风格,完全取决于程序员。在C++所有主观的风格中,一致性和精度是最重要的。请根据自己的需要、喜好和个人风格来使用变量名(或必要时,根据雇主的需要、喜好和个人风格来选择变量名)。
整数就是没有小数部分的数字,如2、98、-5286和0。整数有很多,如果将无限大的整数看作很大,则不可能用有限的计算机内存来表示所有的整数。因此,语言只能表示所有整数的一个子集。有些语言只提供一种整型(一种类型满足所有要求!),而C++则提供好几种,这样便能够根据程序的具体要求选择最合适的整型。
不同C++整型使用不同的内存量来存储整数。使用的内存量越大,可以表示的整数值范围也越大。另外,有的类型(符号类型)可表示正值和负值,而有的类型(无符号类型)不能表示负值。术语宽度(width)用于描述存储整数时使用的内存量。使用的内存越多,则越宽。C++的基本整型(按宽度递增的顺序排列)分别是char、short、int、long和C++11新增的long long,其中每种类型都有符号版本和无符号版本,因此总共有10种类型可供选择。下面更详细地介绍这些整数类型。由于char类型有一些特殊属性(它最常用来表示字符,而不是数字),因此本章将首先介绍其他类型。
计算机内存由一些叫作位(bit)的单元组成(参见本章后面的旁注“位与字节”)。C++的short、int、long和long long类型通过使用不同数目的位来存储值,最多能够表示4种不同的整数宽度。如果在所有的系统中,每种类型的宽度都相同,则使用起来将非常方便。例如,如果short总是16位,int总是32位,等等。不过生活并非那么简单,没有一种选择能够满足所有的计算机设计要求。C++提供了一种灵活的标准,它确保了最小长度(从C语言借鉴而来),如下所示:
位与字节
计算机内存的基本单元是位(bit)。可以将位看作电子开关,可以开,也可以关。关表示值0,开表示值1。8位的内存块可以设置出256种不同的组合,因为每一位都可以有两种设置,所以8位的总组合数为2×2×2×2×2×2×2×2,即256。因此,8位单元可以表示0~255或者-128~127。每增加一位,组合数便加倍。这意味着可以把16位单元设置成65 536个不同的值,把32位单元设置成4294967296个不同的值,把64位单元设置为18446744073709551616个不同的值。作为比较,unsigned long存储不了地球上当前的人数和银河系的星星数,而long long能够。
字节(byte)通常指的是8位的内存单元。从这个意义上说,字节指的就是描述计算机内存量的度量单位,1KB等于1024字节,1MB等于1024KB。然而,C++对字节的定义与此不同。C++字节由至少能够容纳实现的基本字符集的相邻位组成,也就是说,可能取值的数目必须等于或超过字符数目。在美国,基本字符集通常是ASCII和EBCDIC字符集,它们都可以用8位来容纳,所以在使用这两种字符集的系统中,C++字节通常包含8位。然而,国际编程可能需要使用更大的字符集,如Unicode,因此有些实现可能使用16位甚至32位的字节。有些人使用术语八位组(octet)表示8位字节。
当前很多系统都使用最小长度,即short为16位,long为32位。这仍然为int提供了多种选择,其宽度可以是16位、24位或32位,同时又符合标准;甚至可以是64位,因为long和long long至少长64位。通常,在老式IBM PC的实现中,int的宽度为16位(与short相同),而在Windows XP、Windows Vista、Windows 7、Macintosh OS X、VAX和很多其他微型计算机的实现中,为32位(与long相同)。有些实现允许选择如何处理int。(读者所用的实现使用的是什么?下面的例子将演示如何在不打开手册的情况下,确定系统的限制。)类型的宽度随实现而异,这可能在将C++程序从一种环境移到另一种环境(包括在同一个系统中使用不同编译器)时引发问题。但只要小心一点(如本章后面讨论的那样),就可以最大限度地减少这种问题。
可以像使用int一样,使用这些类型名来声明变量:
实际上,short是short int的简称,而long是long int的简称,但是程序设计者们几乎都不使用比较长的形式。
这4种类型(int、short、long和long long)都是符号类型,这意味着每种类型的取值范围中,负值和正值几乎相同。例如,16位的int的取值范围为-32768~+32767。
要知道系统中整数的最大长度,可以在程序中使用C++工具来检查类型的长度。首先,sizeof运算符返回类型或变量的长度,单位为字节(运算符是内置的语言元素,对一个或多个数据进行运算,并生成一个值。例如,加号运算符+将两个值相加)。前面说过,“字节”的含义依赖于实现,因此在一个系统中,两字节的int可能是16位,而在另一个系统中可能是32位。其次,头文件climits(在老式实现中为limits.h)中包含了关于整型限制的信息。具体地说,它定义了表示各种限制的符号名称。例如,INT_MAX为int的最大取值,CHAR_BIT为字节的位数。程序清单3.1演示了如何使用这些工具。该程序还演示如何初始化,即使用声明语句将值赋给变量。
程序清单3.1 limits.cpp
注意:
如果您的系统不支持类型long long,应删除使用该类型的代码行。
下面是程序清单3.1中程序的输出:
这些输出来自运行64位Windows 7的系统。
我们来看一下该程序的主要编程特性。
sizeof运算符指出,在使用8位字节的系统中,int的长度为4个字节。可对类型名或变量名使用sizeof运算符。对类型名(如int)使用sizeof运算符时,应将名称放在括号中;但对变量名(如n_short)使用该运算符,括号是可选的:
头文件climits定义了符号常量(参见本章后面的旁注“符号常量—预处理器方式”)来表示类型的限制。如前所述,INT_MAX表示类型int能够存储的最大值,对于Windows 7系统,为2147483647。编译器厂商提供了climits文件,该文件指出了其编译器中的值。例如,在使用16位int的老系统中,climits文件将INT_MAX定义为32767。表3.1对该文件中定义的符号常量进行了总结,其中的一些符号常量与还没有介绍过的类型相关。
表3.1climits中的符号常量
符号常量—预处理器方式
climits文件中包含与下面类似的语句行:
在C++编译过程中,首先将源代码传递给预处理器。在这里,#define和#include一样,也是一个预处理器编译指令。该编译指令告诉预处理器:在程序中查找INT_MAX,并将所有的INT_MAX都替换为32767。因此#define编译指令的工作方式与文本编辑器或字处理器中的全局搜索并替换命令相似。修改后的程序将在完成这些替换后被编译。预处理器查找独立的标记(单独的单词),跳过嵌入的单词。也就是说,预处理器不会将PINT_MAXTM替换为P32767IM。也可以使用#define来定义自己的符号常量(参见程序清单3.2)。然而,#define编译指令是C语言遗留下来的。C++有一种更好的创建符号常量的方法(使用关键字const,将在后面的一节讨论),所以不会经常使用#define。然而,有些头文件,尤其是那些被设计成可用于C和C++中的头文件,必须使用#define。
初始化将赋值与声明合并在一起。例如,下面的语句声明了变量n_int,并将int的最大取值赋给它:
也可以使用字面值常量来初始化。可以将变量初始化为另一个变量,条件是后者已经定义过。甚至可以使用表达式来初始化变量,条件是当程序执行到该声明时,表达式中所有的值都是已知的:
如果将uncles的声明移到语句列表的最后,则另外两条初始化语句将非法,因为这样当程序试图对其他变量进行初始化时,uncles的值是未知的。
前面的初始化语法来自C语言,C++还有另一种C语言没有的初始化语法:
警告:
如果不对函数内部定义的变量进行初始化,该变量的值将是不确定的。这意味着该变量的值将是它被创建之前,相应内存单元保存的值。
如果知道变量的初始值应该是什么,则应对它进行初始化。将变量声明和赋值分开,可能会带来瞬间悬而未决的问题:
然而,在声明变量时对它进行初始化,可避免以后忘记给它赋值的情况发生。
还有另一种初始化方式,这种方式用于数组和结构,但在C++98中,也可用于单值变量:
将大括号初始化器用于单值变量的情形还不多,但C++11标准使得这种情形更多了。首先,采用这种方式时,可以使用等号(=),也可以不使用:
其次,大括号内可以不包含任何东西。在这种情况下,变量将被初始化为零:
第三,这有助于更好地防范类型转换错误,这个主题将在本章末尾讨论。
为何需要更多的初始化方法?有充分的理由吗?原因是让新手更容易学习C++,这可能有些奇怪。以前,C++使用不同的方式来初始化不同的类型:初始化类变量的方式不同于初始化常规结构的方式,而初始化常规结构的方式又不同于初始化简单变量的方式;通过使用C++新增的大括号初始化器,初始化常规变量的方式与初始化类变量的方式更像。C++11使得可将大括号初始化器用于任何类型(可以使用等号,也可以不使用),这是一种通用的初始化语法。以后,教材可能介绍使用大括号进行初始化的方式,并出于向后兼容的考虑,顺便提及其他初始化方式。
前面介绍的4种整型都有一种不能存储负数值的无符号变体,其优点是可以增大变量能够存储的最大值。例如,如果short表示的范围为−32768到+32767,则无符号版本的表示范围为0~65535。当然,仅当数值不会为负时才应使用无符号类型,如人口、粒数等。要创建无符号版本的基本整型,只需使用关键字unsigned来修改声明即可:
注意,unsigned本身是unsigned int的缩写。
程序清单3.2演示了如何使用无符号类型,并说明了程序试图超越整型的限制时将产生的后果。最后,再看一看预处理器语句#define。
程序清单3.2 exceed.cpp
下面是该程序的输出:
该程序将一个short变量(sam)和一个unsigned short变量(sue)分别设置为最大的short值,在我们的系统上,是32767。然后,将这些变量的值都加1。这对于sue来说没有什么问题,因为新值仍比无符号整数的最大值小得多;但sam的值从32767变成了−32768!同样,对于sam,将其设置为0并减去1,也不会有问题;但对于无符号变量sue,将其设置为0并减去后,它变成了65535。可以看出,这些整型变量的行为就像里程表。如果超越了限制,其值将为范围另一端的取值(参见图3.1)。C++确保了无符号类型的这种行为;但C++并不保证符号整型超越限制(上溢和下溢)时不出错,而这正是当前实现中最为常见的行为。
图3.1 典型的整型溢出行为
C++提供了大量的整型,应使用哪种类型呢?通常,int被设置为对目标计算机而言最为“自然”的长度。自然长度(natural size)指的是计算机处理起来效率最高的长度。如果没有非常有说服力的理由来选择其他类型,则应使用int。
现在来看看可能使用其他类型的原因。如果变量表示的值不可能为负,如文档中的字数,则可以使用无符号类型,这样变量可以表示更大的值。
如果知道变量可能表示的整数值大于16位整数的最大可能值,则使用long。即使系统上int为32位,也应这样做。这样,将程序移植到16位系统时,就不会突然无法正常工作(参见图3.2)。如果要存储的值超过20亿,可使用long long。
图3.2 为提高可移植性,请使用long
如果short比int小,则使用short可以节省内存。通常,仅当有大型整型数组时,才有必要使用short。(数组是一种数据结构,在内存中连续存储同类型的多个值。)如果节省内存很重要,则应使用short而不是使用int,即使它们的长度是一样的。例如,假设要将程序从int为16位的系统移到int为32位的系统,则用于存储int数组的内存量将加倍,但short数组不受影响。请记住,节省一点就是赢得一点。
如果只需要一个字节,可使用char,这将稍后介绍。
整型字面值(常量)是显式地书写的常量,如212或1776。与C相同,C++能够以三种不同的计数方式来书写整数:基数为10、基数为8(老式UNIX版本)和基数为16(硬件黑客的最爱)。附录A介绍了这几种计数系统;这里将介绍C++表示法。C++使用前一(两)位来标识数字常量的基数。如果第一位为1~9,则基数为10(十进制);因此93是以10为基数的。如果第一位是0,第二位为1~7,则基数为8(八进制);因此042的基数是8,它相当于十进制数34。如果前两位为0x或0X,则基数为16(十六进制);因此0x42为十六进制数,相当于十进制数66。对于十六进制数,字符a~f和A~F表示了十六进制位,对应于10~15。0xF为15,0xA5为165(10个16加5个1)。程序清单3.3演示了这三种基数。
程序清单3.3 hexoct.cpp
在默认情况下,cout以十进制格式显示整数,而不管这些整数在程序中是如何书写的,如下面的输出所示:
记住,这些表示方法仅仅是为了表达上的方便。例如,如果CGA视频内存段为十六进制B000,则不必在程序中使用之前将它转换为十进制数45056,而只需使用0xB000即可。但是,不管把值书写为10、012还是0xA,都将以相同的方式存储在计算机中—被存储为二进制数(以2为基数)。
顺便说一句,如果要以十六进制或八进制方式显示值,则可以使用cout的一些特殊特性。前面指出过,头文件iostream提供了控制符endl,用于指示cout重起一行。同样,它还提供了控制符dec、hex和oct,分别用于指示cout以十进制、十六进制和八进制格式显示整数。程序清单3.4使用了hex和oct以上述三种格式显示十进制值42。默认格式为十进制,在修改格式之前,原来的格式将一直有效。
程序清单3.4 hexoct2.cpp
下面是运行该程序时得到的输出:
诸如cout<<hex;等代码不会在屏幕上显示任何内容,而只是修改cout显示整数的方式。因此,控制符hex实际上是一条消息,告诉cout采取何种行为。另外,由于标识符hex位于名称空间std中,而程序使用了该名称空间,因此不能将hex用作变量名。然而,如果省略编译指令using,而使用std::cout、std::endl、std::hex和std::oct,则可以将hex用作变量名。
程序的声明将特定的整型变量的类型告诉了C++编译器,但编译器是如何知道常量的类型呢?假设在程序中使用常量表示一个数字:
程序将把1492存储为int、long还是其他整型呢?答案是,除非有理由存储为其他类型(如使用了特殊的后缀来表示特定的类型,或者值太大,不能存储为int),否则C++将整型常量存储为int类型。
首先来看看后缀。后缀是放在数字常量后面的字母,用于表示类型。整数后面的l或L后缀表示该整数为long常量,u或U后缀表示unsigned int常量,ul(可以采用任何一种顺序,大写小写均可)表示unsigned long常量(由于小写l看上去像1,因此应使用大写L作后缀)。例如,在int为16位、long为32位的系统上,数字22022被存储为int,占16位,数字22022L被存储为long,占32位。同样,22022LU和22022UL都被存储为unsigned long。C++11提供了用于表示类型long long的后缀ll和LL,还提供了用于表示类型unsigned long long的后缀ull、Ull、uLL和ULL。
接下来考察长度。在C++中,对十进制整数采用的规则,与十六进制和八进制稍微有些不同。对于不带后缀的十进制整数,将使用下面几种类型中能够存储该数的最小类型来表示:int、long或long long。在int为16位、long为32位的计算机系统上,20000被表示为int类型,40000被表示为long类型,3000000000被表示为long long类型。对于不带后缀的十六进制或八进制整数,将使用下面几种类型中能够存储该数的最小类型来表示:int、unsigned int long、unsigned long、long long或unsigned long long。在将40000表示为long的计算机系统中,十六进制数0x9C40(40000)将被表示为unsigned int。这是因为十六进制常用来表示内存地址,而内存地址是没有符号的,因此,unsigned int比long更适合用来表示16位的地址。
下面介绍最后一种整型:char类型。顾名思义,char类型是专为存储字符(如字母和数字)而设计的。现在,存储数字对于计算机来说算不了什么,但存储字母则是另一回事。编程语言通过使用字母的数值编码解决了这个问题。因此,char类型是另一种整型。它足够长,能够表示目标计算机系统中的所有基本符号—所有的字母、数字、标点符号等。实际上,很多系统支持的字符都不超过128个,因此用一个字节就可以表示所有的符号。因此,虽然char最常被用来处理字符,但也可以将它用作比short更小的整型。
在美国,最常用的符号集是ASCII字符集(参见附录C)。字符集中的字符用数值编码(ASCII码)表示。例如,字符A的编码为65,字母M的编码为77。为方便起见,本书在示例中使用的是ASCII码。然而,C++实现使用的是其主机系统的编码—例如,IBM大型机使用EBCDIC编码。ASCII和EBCDIC都不能很好地满足国际需要,C++支持的宽字符类型可以存储更多的值,如国际Unicode字符集使用的值。本章稍后将介绍wchar_t类型。
程序清单3.5使用了char类型。
程序清单3.5 chartype.cpp
同样,\n在C++中表示换行符。下面是该程序的输出:
有趣的是,程序中输入的是M,而不是对应的字符编码77。另外,程序将打印M,而不是77。通过查看内存可以知道,77是存储在变量ch中的值。这种神奇的力量不是来自char类型,而是来自cin和cout,这些工具为您完成了转换工作。输入时,cin将键盘输入的M转换为77;输出时,cout将值77转换为所显示的字符M;cin和cout的行为都是由变量类型引导的。如果将77存储在int变量中,则cout将把它显示为77(也就是说,cout显示两个字符7)。程序清单3.6说明了这一点,该程序还演示了如何在C++中书写字符字面值:将字符用单引号括起,如'M'(注意,示例中没有使用双引号。C++对字符用单引号,对字符串使用双引号。cout对象能够处理这两种情况,但正如第4章将讨论的,这两者有天壤之别)。最后,程序引入了cout的一项特性—cout.put( )函数,该函数显示一个字符。
程序清单3.6 morechar.cpp
下面是程序清单3.6中程序的输出:
在程序清单3.6中,'M'表示字符M的数值编码,因此将char变量ch初始化为'M',将把c设置为77。然后,程序将同样的值赋给int变量i,这样ch和i的值都是77。接下来,cout把ch显示为M,而把i显示为77。如前所述,值的类型将引导cout选择如何显示值—这是智能对象的另一个例子。
由于ch实际上是一个整数,因此可以对它使用整数操作,如加1,这将把ch的值变为78。然后,程序将i重新设置为新的值(也可以将i加1)。cout再次将这个值的char版本显示为字符,将int版本显示为数字。
C++将字符表示为整数这一事实,使得操纵字符值很容易。不必使用笨重的转换函数在字符和ASCII码之间来回转换。
即使通过键盘输入的数字也被视为字符。请看下面的代码:
如果您输入5并按回车键,上述代码将读取字符“5”,并将其对应的字符编码(ASCII编码53)存储到变量ch中。请看下面的代码:
如果您也输入5并按回车键,上述代码将读取字符“5”,将其转换为相应的数字值5,并存储到变量n中。
最后,该程序使用函数cout.put( )显示变量ch和一个字符常量。
cout.put( )到底是什么东西?其名称中为何有一个句点?函数cout.put( )是一个重要的C++ OOP概念—成员函数—的第一个例子。类定义了如何表示和控制数据。成员函数归类所有,描述了操纵类数据的方法。例如类ostream有一个put( )成员函数,用来输出字符。只能通过类的特定对象(例如这里的cout对象)来使用成员函数。要通过对象(如cout)使用成员函数,必须用句点将对象名和函数名称(put( ))连接起来。句点被称为成员运算符。cout.put( )的意思是,通过类对象cout来使用函数put( )。第10章介绍类时将更详细地介绍这一点。现在,您接触的类只有istream和ostream,可以通过使用它们的成员函数来熟悉这一概念。
cout.put( )成员函数提供了另一种显示字符的方法,可以替代<<运算符。现在读者可能会问,为何需要cout.put( )。答案与历史有关。在C++的Release 2.0之前,cout将字符变量显示为字符,而将字符常量(如'M'和'N')显示为数字。问题是,C++的早期版本与C一样,也把字符常量存储为int类型。也就是说,'M'的编码77将被存储在一个16位或32位的单元中。而char变量一般占8位。下面的语句从常量'M'中复制8位(左边的8位)到变量ch中:
遗憾的是,这意味着对cout来说,'M'和ch看上去有天壤之别,虽然它们存储的值相同。因此,下面的语句将打印$字符的ASCII码,而不是字符$:
但下面的语句将打印字符$:
在Release 2.0之后,C++将字符常量存储为char类型,而不是int类型。这意味着cout现在可以正确处理字符常量了。
cin对象有几种不同的方式可以读取输入的字符。通过使用一个利用循环来读取几个字符的程序,读者可以更容易地领会到这一点。因此在第5章介绍了循环后再来讨论这个主题。
在C++中,书写字符常量的方式有多种。对于常规字符(如字母、标点符号和数字),最简单的方法是将字符用单引号括起。这种表示法代表的是字符的数值编码。例如,ASCII系统中的对应情况如下:
这种表示法优于数值编码,它更加清晰,且不需要知道编码方式。如果系统使用的是EBCDIC,则A的编码将不是65,但是'A'表示的仍然是字符A。
有些字符不能直接通过键盘输入到程序中。例如,按回车键并不能使字符串包含一个换行符;相反,程序编辑器将把这种键击解释为在源代码中开始新的一行。其他一些字符也无法从键盘输入,因为C++语言赋予了它们特殊的含义。例如,双引号字符用来分隔字符串字面值,因此不能把双引号放在字符串字面值中。对于这些字符,C++提供了一种特殊的表示方法—转义序列,如表3.2所示。例如,\a表示振铃字符,它可以使终端扬声器振铃。转义序列\n表示换行符,\"将双引号作为常规字符,而不是字符串分隔符。可以在字符串或字符常量中使用这些表示法,如下例所示:
最后一行的输出如下:
表3.2C++转义序列的编码
注意,应该像处理常规字符(如Q)那样处理转义序列(如\n)。也就是说,将它们作为字符常量时,应用单引号括起;将它们放在字符串中时,不要使用单引号。
转义序列的概念可追溯到使用电传打字机与计算机通信的时代,现代系统并非都支持所有的转义序列。例如,输入振铃字符时,有些系统保持沉默。
换行符可替代endl,用于在输出中重起一行。可以以字符常量表示法('\n')或字符串方式("n")使用换行符。下面三行代码都将光标移到下一行开头:
可以将换行符嵌入到较长的字符串中,这通常比使用endl方便。例如,下面两条cout语句的输出相同:
显示数字时,使用endl比输入“\n”或‘\n’更容易些,但显示字符串时,在字符串末尾添加一个换行符所需的输入量要少些:
最后,可以基于字符的八进制和十六进制编码来使用转义序列。例如,Ctrl+Z的ASCII码为26,对应的八进制编码为032,十六进制编码为0x1a。可以用下面的转义序列来表示该字符:\032或\0x1a。将这些编码用单引号括起,可以得到相应的字符常量,如'\032',也可以将它们放在字符串中,如"hi\0x1a there"。
提示:
在可以使用数字转义序列或符号转义序列(如\0x8和\b)时,应使用符号序列。数字表示与特定的编码方式(如ASCII码)相关,而符号表示适用于任何编码方式,其可读性也更强。
程序清单3.7演示了一些转义序列。它使用振铃字符来提请注意,使用换行符使光标前进,使用退格字符使光标向左退一格(Houdini曾经在只使用转义序列的情况下,绘制了一幅哈得逊河图画;他无疑是一位转义序列艺术大师)。
程序清单3.7 bondini.cpp
注意:
有些基于ANSI C之前的编译器的C++系统不能识别\a。对于使用ASCII字符集的系统,可以用\007替换\a。有些系统的行为可能有所不同,例如可能将\b显示为一个小矩形,而不是退格,或者在退格时删除,还可能忽略\a。
运行程序清单3.7中的程序时,将在屏幕上显示下面的文本:
打印下划线字符后,程序使用退格字符将光标退到第一个下划线处。读者可以输入自己的密码,并继续。下面是完整的运行情况:
C++实现支持一个基本的源字符集,即可用来编写源代码的字符集。它由标准美国键盘上的字符(大写和小写)和数字、C语言中使用的符号(如{和=)以及其他一些字符(如换行符和空格)组成。还有一个基本的执行字符集,它包括在程序执行期间可处理的字符(如可从文件中读取或显示到屏幕上的字符)。它增加了一些字符,如退格和振铃。C++标准还允许实现提供扩展源字符集和扩展执行字符集。另外,那些被作为字母的额外字符也可用于标识符名称中。也就是说,德国实现可能允许使用日耳曼语的元音变音,而法国实现则允许使用重元音。C++有一种表示这种特殊字符的机制,它独立于任何特定的键盘,使用的是通用字符名(universal character name)。
通用字符名的用法类似于转义序列。通用字符名可以以\u或\U打头。\u后面是8个十六进制位,\U后面则是16个十六进制位。这些位表示的是字符的ISO 10646码点(ISO 10646是一种正在制定的国际标准,为大量的字符提供了数值编码,请参见本章后面的“Unicode和ISO 10646”)。
如果所用的实现支持扩展字符,则可以在标识符(如字符常量)和字符串中使用通用字符名。例如,请看下面的代码:
ö的ISO 10646码点为00F6,而â的码点为00E2。因此,上述C++代码将变量名设置为körper,并显示下面的输出:
如果系统不支持ISO 10646,它将显示其他字符或gu00E2teau,而不是â。
实际上,从易读性的角度看,在变量名中使用\u00F6没有多大意义,但如果实现的扩展源字符集包含ö,它可能允许您从键盘输入该字符。
请注意,C++使用术语“通用编码名”,而不是“通用编码”,这是因为应将\u00F6解释为“Unicode码点为U-00F6的字符”。支持Unicode的编译器知道,这表示字符ö,但无需使用内部编码00F6。无论计算机使用是ASCII还是其他编码系统,都可在内部表示字符T;同样,在不同的系统中,将使用不同的编码来表示字符ö。在源代码中,可使用适用于所有系统的通用编码名,而编译器将根据当前系统使用合适的内部编码来表示它。
Unicode和ISO 10646
Unicode提供了一种表示各种字符集的解决方案——为大量字符和符号提供标准数值编码,并根据类型将它们分组。例如,ASCII码为Unicode的子集,因此在这两种系统中,美国的拉丁字符(如A和Z)的表示相同。然而,Unicode还包含其他字符,如欧洲使用的拉丁字符、来自其他文字(如希腊文、西里尔字母、希伯来文、切罗基文、阿拉伯文、泰文和孟加拉文)中的字符,以及象形文字(如中国和日本的汉字)。到目前为止,Unicode可以表示109000多种符号和90多个手写符号(script),它还在不断发展中。
Unicode给每个字符指定一个编号——码点。Unicode码点通常类似于下面这样:U-222B。其中U表示这是一个Unicode字符,而222B是该字符(积分正弦符号)的十六进制编号。
国际标准化组织(ISO)建立了一个工作组,专门开发ISO 10646——这也是一个对多种语言文本进行编码的标准。ISO 10646小组和Unicode小组从1991年开始合作,以确保他们的标准同步。
与int不同的是,char在默认情况下既不是没有符号,也不是有符号。是否有符号由C++实现决定,这样编译器开发人员可以最大限度地将这种类型与硬件属性匹配起来。如果char有某种特定的行为对您来说非常重要,则可以显式地将类型设置为signed char 或unsigned char:
如果将char用作数值类型,则unsigned char和signed char之间的差异将非常重要。unsigned char类型的表示范围通常为0~255,而signed char的表示范围为−128到127。例如,假设要使用一个char变量来存储像200这样大的值,则在某些系统上可以,而在另一些系统上可能不可以。但使用unsigned char可以在任何系统上达到这种目的。另一方面,如果使用char变量来存储标准ASCII字符,则char有没有符号都没关系,在这种情况下,可以使用char。
程序需要处理的字符集可能无法用一个8位的字节表示,如日文汉字系统。对于这种情况,C++的处理方式有两种。首先,如果大型字符集是实现的基本字符集,则编译器厂商可以将char定义为一个16位的字节或更长的字节。其次,一种实现可以同时支持一个小型基本字符集和一个较大的扩展字符集。8位char可以表示基本字符集,另一种类型wchar_t(宽字符类型)可以表示扩展字符集。wchar_t类型是一种整数类型,它有足够的空间,可以表示系统使用的最大扩展字符集。这种类型与另一种整型(底层(underlying)类型)的长度和符号属性相同。对底层类型的选择取决于实现,因此在一个系统中,它可能是unsigned short,而在另一个系统中,则可能是int。
cin和cout将输入和输出看作是char流,因此不适于用来处理wchar_t类型。iostream头文件的最新版本提供了作用相似的工具—wcin和wcout,可用于处理wchar_t流。另外,可以通过加上前缀L来指示宽字符常量和宽字符串。下面的代码将字母P的wchar_t版本存储到变量bob中,并显示单词tall的wchar_t版本:
在支持两字节wchar_t的系统中,上述代码将把每个字符存储在一个两个字节的内存单元中。本书不使用宽字符类型,但读者应知道有这种类型,尤其是在进行国际编程或使用Unicode或ISO 10646时。
随着编程人员日益熟悉Unicode,类型wchar_t显然不再能够满足需求。事实上,在计算机系统上进行字符和字符串编码时,仅使用Unicode码点并不够。具体地说,进行字符串编码时,如果有特定长度和符号特征的类型,将很有帮助,而类型wchar_t的长度和符号特征随实现而异。因此,C++11新增了类型char16_t和char32_t,其中前者是无符号的,长16位,而后者也是无符号的,但长32位。C++11使用前缀u表示char16_t字符常量和字符串常量,如u'C'和u"be good";并使用前缀U表示char32_t常量,如U'R'和U"dirty rat"。类型char16_t与\u00F6形式的通用字符名匹配,而类型char32_t与\U0000222B形式的通用字符名匹配。前缀u和U分别指出字符字面值的类型为char16_t和char32_t:
与wchar_t一样,char16_t和char32_t也都有底层类型—一种内置的整型,但底层类型可能随系统而异。
ANSI/ISO C++标准添加了一种名叫bool的新类型(对C++来说是新的)。它的名称来源于英国数学家George Boole,是他开发了逻辑律的数学表示法。在计算中,布尔变量的值可以是true或false。过去,C++和C一样,也没有布尔类型。在第5章和第6章中将会看到,C++将非零值解释为true,将零解释为false。然而,现在可以使用bool类型来表示真和假了,它们分别用预定义的字面值true和false表示。也就是说,可以这样编写语句:
字面值true和false都可以通过提升转换为int类型,true被转换为1,而false被转换为0:
另外,任何数字值或指针值都可以被隐式转换(即不用显式强制转换)为bool值。任何非零值都被转换为true,而零被转换为false:
在第6章介绍if语句后,示例中将经常使用数据类型bool。