- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
3.1 简单变量
程序通常都需要存储信息—如 Google 股票当前的价格、纽约市 8 月份的平均湿度、美国宪法中使用最多的字母及其相对使用频率或猫王模仿者的数目。为把信息存储在计算机中,程序必须记录 3 个基本属性:
- 信息将存储在哪里;
- 要存储什么值;
- 存储何种类型的信息。
到目前为止,本书的示例采取的策略都是声明一个变量。声明中使用的类型描述了信息的类型和通过符号来表示其值的变量名。例如,假设实验室首席助理 Igor 使用了下面的语句:
这些语句告诉程序,它正在存储整数,并使用名称 braincount 来表示该整数的值(这里为 5)。实际上,程序将找到一块能够存储整数的内存,将该内存单元标记为 braincount,并将 5 复制到该内存单元中;然后,您可在程序中使用 braincount 来访问该内存单元。这些语句没有告诉您,这个值将存储在内存的什么位置,但程序确实记录了这种信息。实际上,可以使用&运算符来检索 braincount 的内存地址。下一章介绍另一种标识数据的方法(使用指针)时,将介绍这个运算符。
3.1.1 变量名
C++提倡使用有一定含义的变量名。如果变量表示差旅费,应将其命名为 cost_of_trip 或 costOfTrip,而不要将其命名为 x 或 cot。必须遵循几种简单的 C++命名规则。
- 在名称中只能使用字母字符、数字和下划线(_)。
- 名称的第一个字符不能是数字。
- 区分大写字符与小写字符。
- 不能将 C++关键字用作名称。
- 以两个下划线或下划线和大写字母打头的名称被保留给实现(编译器及其使用的资源)使用。以一个下划线开头的名称被保留给实现,用作全局标识符。
- 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++所有主观的风格中,一致性和精度是最重要的。请根据自己的需要、喜好和个人风格来使用变量名(或必要时,根据雇主的需要、喜好和个人风格来选择变量名)。
3.1.2 整型
整数就是没有小数部分的数字,如 2、98、-5286 和 0。整数有很多,如果将无限大的整数看作很大,则不可能用有限的计算机内存来表示所有的整数。因此,语言只能表示所有整数的一个子集。有些语言只提供一种整型(一种类型满足所有要求!),而 C++则提供好几种,这样便能够根据程序的具体要求选择最合适的整型。
不同 C++整型使用不同的内存量来存储整数。使用的内存量越大,可以表示的整数值范围也越大。另外,有的类型(符号类型)可表示正值和负值,而有的类型(无符号类型)不能表示负值。术语宽度(width)用于描述存储整数时使用的内存量。使用的内存越多,则越宽。C++的基本整型(按宽度递增的顺序排列)分别是 char、short、int、long 和 C++11 新增的 long long,其中每种类型都有符号版本和无符号版本,因此总共有 10 种类型可供选择。下面更详细地介绍这些整数类型。由于 char 类型有一些特殊属性(它最常用来表示字符,而不是数字),因此本章将首先介绍其他类型。
3.1.3 整型 short、int、long 和 long long
计算机内存由一些叫做位(bit)的单元组成(参见本章后面的旁注“位与字节”)。C++的 short、int、long 和 long long 类型通过使用不同数目的位来存储值,最多能够表示 4 种不同的整数宽度。如果在所有的系统中,每种类型的宽度都相同,则使用起来将非常方便。例如,如果 short 总是 16 位,int 总是 32 位,等等。不过生活并非那么简单,没有一种选择能够满足所有的计算机设计要求。C++提供了一种灵活的标准,它确保了最小长度(从 C 语言借鉴而来),如下所示:
- short 至少 16 位;
- int 至少与 short 一样长;
- long 至少 32 位,且至少与 int 一样长;
- long long 至少 64 位,且至少与 long 一样长。
位与字节
计算机内存的基本单元是位(bit)。可以将位看作电子开关,可以开,也可以关。关表示值 0,开表示值 1。8 位的内存块可以设置出 256 种不同的组合,因为每一位都可以有两种设置,所以 8 位的总组合数为 2×2×2×2×2×2×2×2,即 256。因此,8 位单元可以表示 0-255 或者-128 到 127。每增加一位,组合数便加倍。这意味着可以把 16 位单元设置成 65 536 个不同的值,把 32 位单元设置成 4 294 672 296 个不同的值,把 64 位单元设置为 18 446 744 073 709 551 616 个不同的值。作为比较,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 的系统。
我们来看一下该程序的主要编程特性。
1.运算符 sizeof 和头文件 limits
sizeof 运算符指出,在使用 8 位字节的系统中,int 的长度为 4 个字节。可对类型名或变量名使用 sizeof 运算符。对类型名(如 int)使用 sizeof 运算符时,应将名称放在括号中;但对变量名(如 n_short)使用该运算符,括号是可选的:
头文件 climits 定义了符号常量(参见本章后面的旁注“符号常量—预处理器方式”)来表示类型的限制。如前所述,INT_MAX 表示类型 int 能够存储的最大值,对于 Windows 7 系统,为 2 147 483 647。编译器厂商提供了 climits 文件,该文件指出了其编译器中的值。例如,在使用 16 位 int 的老系统中,climits 文件将 INT_MAX 定义为 32 767。表 3.1 对该文件中定义的符号常量进行了总结,其中的一些符号常量与还没有介绍过的类型相关。
表 3.1climits 中的符号常量
符 号 常 量 | 表 示 |
---|---|
CHAR_BIT | char 的位数 |
CHAR_MAX | char 的最大值 |
CHAR_MIN | char 的最小值 |
SCHAR_MAX | signed char 的最大值 |
SCHAR_MIN | signed char 的最小值 |
UCHAR_MAX | unsigned char 的最大值 |
SHRT_MAX | short 的最大值 |
SHRT_MIN | short 的最小值 |
USHRT_MAX | unsigned short 的最大值 |
INT_MAX | int 的最大值 |
INT_MIN | int 的最小值 |
UNIT_MAX | unsigned int 的最大值 |
LONG_MAX | long 的最大值 |
LONG_MIN | long 的最小值 |
ULONG_MAX | unsigned long 的最大值 |
LLONG_MAX | long long 的最大值 |
LLONG_MIN | long long 的最小值 |
ULLONG_MAX | unsigned long long 的最大值 |
符号常量—预处理器方式
climits 文件中包含与下面类似的语句行:
在 C++编译过程中,首先将源代码传递给预处理器。在这里,#define 和#include 一样,也是一个预处理器编译指令。该编译指令告诉预处理器:在程序中查找 INT_MAX,并将所有的 INT_MAX 都替换为 32767。因此#define 编译指令的工作方式与文本编辑器或字处理器中的全局搜索并替换命令相似。修改后的程序将在完成这些替换后被编译。预处理器查找独立的标记(单独的单词),跳过嵌入的单词。也就是说,预处理器不会将 PINT_MAXTM 替换为 P32767IM。也可以使用#define 来定义自己的符号常量(参见程序清单 3.2)。然而,#define 编译指令是 C 语言遗留下来的。C++有一种更好的创建符号常量的方法(使用关键字 const,将在后面的一节讨论),所以不会经常使用#define。然而,有些头文件,尤其是那些被设计成可用于 C 和 C++中的头文件,必须使用#define。
2.初始化
初始化将赋值与声明合并在一起。例如,下面的语句声明了变量 n_int,并将 int 的最大取值赋给它:
也可以使用字面值常量来初始化。可以将变量初始化为另一个变量,条件是后者已经定义过。甚至可以使用表达式来初始化变量,条件是当程序执行到该声明时,表达式中所有的值都是已知的:
如果将 uncles 的声明移到语句列表的最后,则另外两条初始化语句将非法,因为这样当程序试图对其他变量进行初始化时,uncles 的值是未知的。
前面的初始化语法来自 C 语言,C++还有另一种 C 语言没有的初始化语法:
警告:
如果不对函数内部定义的变量进行初始化,该变量的值将是不确定的。这意味着该变量的值将是它被创建之前,相应内存单元保存的值。
如果知道变量的初始值应该是什么,则应对它进行初始化。将变量声明和赋值分开,可能会带来瞬间悬而未决的问题:
然而,在声明变量时对它进行初始化,可避免以后忘记给它赋值的情况发生。
3.C++11 初始化方式
还有另一种初始化方式,这种方式用于数组和结构,但在 C++98 中,也可用于单值变量:
将大括号初始化器用于单值变量的情形还不多,但 C++11 标准使得这种情形更多了。首先,采用这种方式时,可以使用等号(=),也可以不使用:
其次,大括号内可以不包含任何东西。在这种情况下,变量将被初始化为零:
第三,这有助于更好地防范类型转换错误,这个主题将在本章末尾讨论。
为何需要更多的初始化方法?有充分的理由吗?原因是让新手更容易学习 C++,这可能有些奇怪。以前,C++使用不同的方式来初始化不同的类型:初始化类变量的方式不同于初始化常规结构的方式,而初始化常规结构的方式又不同于初始化简单变量的方式;通过使用 C++新增的大括号初始化器,初始化常规变量的方式与初始化类变量的方式更像。C++11 使得可将大括号初始化器用于任何类型(可以使用等号,也可以不使用),这是一种通用的初始化语法。以后,教材可能介绍使用大括号进行初始化的方式,并出于向后兼容的考虑,顺便提及其他初始化方式。
3.1.4 无符号类型
前面介绍的 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 典型的整型溢出行为
3.1.5 选择整型类型
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,这将稍后介绍。
3.1.6 整型字面值
整型字面值(常量)是显式地书写的常量,如 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 用作变量名。
3.1.7 C++如何确定常量的类型
程序的声明将特定的整型变量的类型告诉了 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。这是因为十六进制常用来表示内存地址,而内存地址是没有符号的,因此,usigned int 比 long 更适合用来表示 16 位的地址。
3.1.8 char 类型:字符和小整数
下面介绍最后一种整型: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 中程序的输出:
1.程序说明
在程序清单 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 和一个字符常量。
2.成员函数 cout.put( )
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 章介绍了循环后再来讨论这个主题。
3.char 字面值
在 C++中,书写字符常量的方式有多种。对于常规字符(如字母、标点符号和数字),最简单的方法是将字符用单引号括起。这种表示法代表的是字符的数值编码。例如,ASCII 系统中的对应情况如下:
- 'A'为 65,即字符 A 的 ASCII 码;
- 'a'为 97,即字符 a 的 ASCII 码;
- '5'为 53,即数字 5 的 ASCII 码;
- ' '为 32,即空格字符的 ASCII 码;
- '!'为 33,即惊叹号的 ASCII 码。
这种表示法优于数值编码,它更加清晰,且不需要知道编码方式。如果系统使用的是 EBCDIC,则 A 的编码将不是 65,但是'A'表示的仍然是字符 A。
有些字符不能直接通过键盘输入到程序中。例如,按回车键并不能使字符串包含一个换行符;相反,程序编辑器将把这种键击解释为在源代码中开始新的一行。其他一些字符也无法从键盘输入,因为 C++语言赋予了它们特殊的含义。例如,双引号字符用来分隔字符串字面值,因此不能把双引号放在字符串字面值中。对于这些字符,C++提供了一种特殊的表示方法—转义序列,如表 3.2 所示。例如,\a 表示振铃字符,它可以使终端扬声器振铃。转义序列 n\表示换行符,\”将双引号作为常规字符,而不是字符串分隔符。可以在字符串或字符常量中使用这些表示法,如下例所示:
最后一行的输出如下:
表 3.2C++转义序列的编码
字 符 名 称 | ASCII 符号 | C++代码 | 十进制 ASCII 码 | 十六进制 ASCII 码 |
---|---|---|---|---|
换行符 | NL (LF) | \n | 10 | 0xA |
水平制表符 | HT | \t | 9 | 0x9 |
垂直制表符 | VT | \v | 11 | 0xB |
退格 | BS | \b | 8 | 0x8 |
回车 | CR | \r | 13 | 0xD |
振铃 | BEL | \a | 7 | 0x7 |
反斜杠 | \ | \ | 92 | 0x5C |
问号 | ? | \? | 63 | 0x3F |
单引号 | ' | \ ' | 39 | 0x27 |
双引号 | " | \ " | 34 | 0x22 |
注意,应该像处理常规字符(如 Q)那样处理转义序列(如\n)。也就是说,将它们作为字符常量时,应用单引号括起;将它们放在字符串中时,不要使用单引号。
转义序列的概念可追溯到使用电传打字机与计算机通信的时代,现代系统并非都支持所有的转义序列。例如,输入振铃字符时,有些系统保持沉默。
换行符可替代 endl,用于在输出中重起一行。可以以字符常量表示法(‘\n’)或字符串方式(“n”)使用换行符。下面三行代码都将光标移到下一行开头:
可以将换行符嵌入到较长的字符串中,这通常比使用 endl 方便。例如,下面两条 cout 语句的输出相同:
显示数字时,使用 endl 比输入“\n”或‘\n’更容易些,但显示字符串时,在字符串末尾添加一个换行符所需的输入量要少些:
最后,可以基于字符的八进制和十六进制编码来使用转义序列。例如,Ctr+Z 的 ASCII 码为 26,对应的八进制编码为 032,十六进制编码为 0x1a。可以用下面的转义序列来表示该字符:\032 或\x1a。将这些编码用单引号括起,可以得到相应的字符常量,如'\032',也可以将它们放在字符串中,如"hi\x1a there"。
提示:
在可以使用数字转义序列或符号转义序列(如\0x8 和\b)时,应使用符号序列。数字表示与特定的编码方式(如 ASCII 码)相关,而符号表示适用于任何编码方式,其可读性也更强。
程序清单 3.7 演示了一些转义序列。它使用振铃字符来提请注意,使用换行符使光标前进,使用退格字符使光标向左退一格(Houdini 曾经在只使用转义序列的情况下,绘制了一幅哈得逊河图画;他无疑是一位转义序列艺术大师)。
程序清单 3.7 bondini.cpp
注意:
有些基于 ANSI C 之前的编译器的 C++系统不能识别\a。对于使用 ASCII 字符集的系统,可以用\007 替换\a。有些系统的行为可能有所不同,例如可能将\b 显示为一个小矩形,而不是退格,或者在退格时删除,还可能忽略\a。
运行程序清单 3.7 中的程序时,将在屏幕上显示下面的文本:
打印下划线字符后,程序使用退格字符将光标退到第一个下划线处。读者可以输入自己的密码,并继续。下面是完整的运行情况:
4.通用字符名
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 年开始合作,以确保他们的标准同步。
5.signed char 和 unsigned char
与 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。
6.wcha_t
程序需要处理的字符集可能无法用一个 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 时。
7.C++11 新增的类型:char16_t 和 char32_t
随着编程人员日益熟悉 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 也都有底层类型—一种内置的整型,但底层类型可能随系统而已。
3.1.9 bool 类型
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。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论