C++ Primer Plus第6版1~3章

C + + 是在 C 语言基础上开发的一种集

  • 面向对象编程、
  • 泛型编程和
  • 过程化编程

于一体的编程语言, 是 C 语言的超集。

本书介绍的是通用 C + +, 不依赖于特定实现。 本书不要求学生了解 C 语言, 但如果有一定的编程经验则更好。

第 1 章预备知识

C + + 在 C 语言的基础上添加了对面向对象编程和泛型编程的支持:

  • C + + 继承了 C 语言高效、 简洁、 快速和可移植性的传统。
  • C + + 面向对象的特性带来了全新的编程方法, 这种方法是为应付复杂程度不断提高的现代编程任务而设计的。
  • C + + 的模板特性提供了另一种全新的编程方法—— 泛型编程。

这三件法宝既是福也是祸, 一方面让 C + + 语言功能强大, 另一方面则意味着有更多的东西需要学习。

C + + 融合了 3 种不同的编程方式: C 语言代表的过程性语言、 C + + 在 C 语言基础上添加的类代表的面向对象语言、 C + + 模板支持的泛型编程。

除了提供结构化编程工具外, C 还能生成简洁、 快速运行的程序, 并提供了处理硬件问题的能力, 如管理通信端口和磁盘驱动器。 这些因素使 C 语言成为 20 世纪 80 年代占统治地位的编程语言。

一般来说, 计算机语言要处理两个概念—— 数据和算法。 数据是程序使用和处理的信息, 而算法是程序使用的方法

从概念上说, 过程化编程首先要确定计算机应采取的操作, 然后使用编程语言来实现这些操作。 程序命令计算机按一系列流程生成特定的结果, 就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。

数据 + 算法 = 程序:

虽然结构化编程的理念提高了程序的清晰度、 可靠性, 并使之便于维护, 但它在编写大型程序时, 仍面临着挑战。 为应付这种挑战, OOP 提供了一种新方法。 与强调算法的过程性编程不同的是, OOP 强调的是数据。 OOP 不像过程性编程那样, 试图使问题满足语言的过程性方法, 而是试图让语言来满足问题的要求。 其理念是设计与问题的本质特性相对应的数据格式。

在 C + + 中, 类是一种规范, 它描述了这种新型数据格式, 对象是根据这种规范构造的特定数据结构。

OOP 程序设计方法首先设计类, 它们准确地表示了程序要处理的东西。 例如, 绘图程序可能定义表示矩形、 直线、 圆、 画刷、 画笔的类。 类定义描述了对每个类可执行的操作, 如移动圆或旋转直线。 然后您便可以设计一个使用这些类的对象的程序。 从低级组织( 如类) 到高级组织( 如程序) 的处理过程叫做自下向上( bottom-up) 的编程。

OOP 编程并不仅仅是将数据和方法合并为类定义。 例如, OOP 还有助于创建可重用的代码, 这将减少大量的工作。 信息隐藏可以保护数据, 使其免遭不适当的访问。 多态让您能够为运算符和函数创建多个定义, 通过编程上下文来确定使用哪个定义。 继承让您能够使用旧类派生出新类。 正如接下来将看到的那样, OOP 引入了很多新的理念, 使用的编程方法不同于过程性编程。 它不是将重点放在任务上, 而是放在表示概念上。 有时不一定使用自上向下的编程方法, 而是使用自下向上的编程方法。 本书将通过大量易于掌握的示例帮助读者理解这些要点。

设计有用、 可靠的类是一项艰巨的任务, 幸运的是, OOP 语言使程序员在编程中能够轻松地使用已有的类。 厂商提供了大量有用的类库, 包括设计用于简化 Windows 或 Macintosh 环境下编程的类库。 C + + 真正的优点之一是: 可以方便地重用和修改现有的、 经过仔细测试的代码。

泛型编程( generic programming) 是 C + + 支持的另一种编程模式。 它与 OOP 的目标相同, 即使重用代码和抽象通用概念的技术更简单。 不过 OOP 强调的是编程的数据方面, 而泛型编程强调的是独立于特定数据类型。 它们的侧重点不同。 OOP 是一个管理大型项目的工具, 而泛型编程提供了执行常见任务( 如对数据排序或合并链表) 的工具。 术语泛型( generic) 指的是创建独立于类型的代码。 C + + 的数据表示有多种类型—— 整数、 小数、 字符、 字符串、 用户定义的、 由多种类型组成的复合结构。 例如, 要对不同类型的数据进行排序, 通常必须为每种类型创建一个排序函数。 泛型编程需要对语言进行扩展, 以便可以只编写一个泛型( 即不是特定类型的) 函数, 并将其用于各种实际类型。 C + + 模板提供了完成这种任务的机制。

与 C 语言一样, C + + 也是在贝尔实验室诞生的, Bjarne Stroustrup 于 20 世纪 80 年代在这里开发出了这种语言。 用他自己的话来说,“ C + + 主要是为了我的朋友和我不必再使用汇编语言、 C 语言或其他现代高级语言来编程而设计的。 它的主要功能是可以更方便地编写出好程序, 让每个程序员更加快乐”。

C++ 设计的初衷是解放程序员.

Stroustrup 比较关心的是让 C + + 更有用, 而不是实施特定的编程原理或风格。 在确定 C + + 语言特性方面, 真正的编程需要比纯粹的原理更重要。

C + + 是 C 语言的超集, 这意味着任何有效的 C 程序都是有效的 C + + 程序。 它们之间有些细微的差异, 但无足轻重。 C + + 程序可以使用已有的 C 软件库。

名称 C + + 来自 C 语言中的递增运算符 + +, 该运算符将变量加 1。 名称 C + + 表明, 它是 C 的扩充版本。

计算机程序将实际问题转换为计算机能够执行的一系列操作。 OOP 部分赋予了 C + + 语言将问题所涉及的概念联系起来的能力, C 部分则赋予了 C + + 语言紧密联系硬件的能力, 这种能力上的结合成就了 C + + 的广泛传播。

在 C + + 获得一定程度的成功后, Stroustrup 才添加了模板, 这使得进行泛型编程成为可能。 在模板特性被使用和改进后, 人们才逐渐认识到, 它们和 OOP 同样重要—— 甚至比 OOP 还重要, 但有些人不这么认为。 C + + 融合了 OOP、 泛型编程和传统的过程性方法, 这表明 C + + 强调的是实用价值, 而不是意识形态方法, 这也是该语言获得成功的原因之一。

C + + 的二重性:

假设您编写了一个 C + + 程序。 如何让它运行起来呢? 具体的步骤取决于计算机环境和使用的 C + + 编译器, 但大体如下

  • 1. 使用文本编辑器编写程序, 并将其保存到文件中, 这个文件就是程序的源代码。
  • 2. 编译源代码。 这意味着运行一个程序, 将源代码翻译为主机使用的内部语言—— 机器语言。 包含了翻译后的程序的文件就是程序的目标代码( object code)。
  • 3. 将目标代码与其他代码链接起来。 例如, C + + 程序通常使用库。 C + + 库包含一系列计算机例程( 被称为函数) 的目标代码, 这些函数可以执行诸如在屏幕上显示信息或计算平方根等任务。 链接指的是将目标代码同使用的函数的目标代码以及一些标准的启动代码( startup code) 组合起来, 生成程序的运行阶段版本。 包含该最终产品的文件被称为可执行代码。

编程步骤:

Windows 编译器

IDE 通常提供了多个菜单项, 如 Compile( 编译)、 Build( 建立)、 Make( 生成)、 Build All( 全部建立)、 Link( 链接)、 Execute( 执行)、 Run( 运行) 和 Debug( 调试), 不过同一个 IDE 中, 不一定包含所有这些选项。

  • Compile 通常意味着对当前打开的文件中的代码进行编译。
  • Build 和 Make 通常意味着编译项目中所有源代码文件的代码。 这通常是一个递增过程, 也就是说, 如果项目包含 3 个文件, 而只有其中一个文件被修改, 则只重新编译该文件。
  • Build All 通常意味着重新编译所有的源代码文件。
  • Link 意味着( 如前所述) 将编译后的源代码与所需的库代码组合起来。
  • Run 或 Execute 意味着运行程序。 通常, 如果您还没有执行前面的步骤, Run 将在运行程序之前完成这些步骤。
  • Debug 意味着以步进方式执行程序。
  • 编译器可能让您选择要生成调试版还是发布版。 调试版包含额外的代码, 这会增大程序、 降低执行速度, 但可提供详细的调试信息。

有时, 编译器在不完全地构建程序后将出现混乱, 它显示无法改正的、 无意义的错误消息。 在这种情况下, 可以选择 Build All, 重新编译整个程序, 以清除这些错误消息。 遗憾的是, 这种情况和那些更常见的情况( 即错误消息只是看上去无意义, 实际上有意义) 很难区分。

第 2 章开始学习 C + +

#include <iostream> // include 后加空格清晰一些
using namespace std; // 习惯上,命名空间的位置: 引用系统的写在外边,引用自身的写在内部

int main() {
    cout << "Hello World!" << endl;

}

C + + 对大小写敏感, 也就是说区分大写字符和小写字符。

C + + 能够使用 printf( )、 scanf( )和其他所有标准 C 输入和输出函数, 只需要包含常规 C 语言的 stdio.h 文件。 不过本书介绍的是 C + +, 所以将使用 C + + 的输入工具, 它们在 C 版本的基础上作了很多改进。

通常, C + + 函数可被其他函数激活或调用, 函数头描述了函数与调用它的函数之间的接口。 位于函数名前面的部分叫做函数返回类型, 它描述的是从函数返回给调用它的函数的信息。 函数名后括号中的部分叫做形参列表( argument list) 或参数列表( parameter list); 它描述的是从调用函数传递给被调用的函数的信息。 这种通用格式用于 main( )时让人感到有些迷惑, 因为通常并不从程序的其他部分调用 main( )。

然而, 通常, main( )被启动代码调用, 而启动代码是由编译器添加到程序中的, 是程序和操作系统( UNIX、 Windows 7 或其他操作系统) 之间的桥梁。 事实上, 该函数头描述的是 main( )和操作系统之间的接口。

ANSI/ ISO C + + 标准对那些抱怨必须在 main( )函数最后包含一条返回语句过于繁琐的人做出了让步。 如果编译器到达 main( )函数末尾时没有遇到返回语句, 则认为 main( )函数以如下语句结尾:

return 0;

这条隐含的返回语句只适用于 main( )函数, 而不适用于其他函数。

为什么 main( )不能使用其他名称

通常, C + + 程序必须包含一个名为 main( )的函数, 在运行 C + + 程序时, 通常从 main( )函数开始执行。 因此, 如果没有 main( ), 程序将不完整, 编译器将指出未定义 main( )函数。

存在一些例外情况。

  • 例如, 在 Windows 编程中, 可以编写一个动态链接库( DLL) 模块, 这是其他 Windows 程序可以使用的代码。 由于 DLL 模块不是独立的程序, 因此不需要 main( )。
  • 用于专用环境的程序— 如机器人中的控制器芯片— 可能不需要 main( )。 有些编程环境提供一个框架程序, 该程序调用一些非标准函数, 如_tmain( )。 在这种情况下, 有一个隐藏的 main( ), 它调用_tmain( )。
  • 但常规的独立程序都需要 main( ), 本书讨论的都是这种程序。

#include <iostream>,该编译指令导致预处理器将 iostream 文件的内容添加到程序中。 这是一种典型的预处理器操作: 在源代码被编译之前, 替换或添加文本。 iostream 文件的内容将取代程序中的代码行#include <iostream>。 原始文件没有被修改, 而是将源代码文件和 iostream 组合成一个复合文件, 编译的下一阶段将使用该文件。

像 iostream 这样的文件叫做包含文件( include file)— 由于它们被包含在其他文件中; 也叫头文件( header file)— 由于它们被包含在文件起始处。

C 语言的传统是, 头文件使用扩展名 h, 将其作为一种通过名称标识文件类型的简单方式。 例如, 头文件 math.h 支持各种 C 语言数学函数, 但 C + + 的用法变了。 现在, 对老式 C 的头文件保留了扩展名 h( C + + 程序仍可以使用这种文件), 而 C + + 头文件则没有扩展名。

有些 C 头文件被转换为 C + + 头文件, 这些文件被重新命名, 去掉了扩展名 h( 使之成为 C + + 风格的名称), 并在文件名称前面加上前缀 c( 表明来自 C 语言)。 例如, C + + 版本的 math.h 为 cmath。

有时 C 头文件的 C 版本和 C + + 版本相同, 而有时候新版本做了一些修改。 对于纯粹的 C + + 头文件( 如 iostream) 来说, 去掉 h 不只是形式上的变化, 没有 h 的头文件也可以包含名称空间

头文件命名约定 :

头文件类型 约定 示例 说明
C + + 旧式风格 以. h 结尾 iostream.h C + + 程序可以使用
C 旧式风格 以. h 结尾 math.h C、 C + + 程序可以使用
C + + 新式风格 没有扩展名 iostream C + + 程序可以使用, 使用 namespace std
转换后的 C 加上前缀 c, 没有扩展名 cmath C + + 程序可以使用, 可以使用不是 C 的特性, 如 namespace std

名称空间

如果使用 iostream, 而不是 iostream.h, 则应使用下面的名称空间编译指令来使 iostream 中的定义对程序可用:using namespace std;

名称空间支持是一项 C + + 特性, 旨在让您编写大型程序以及将多个厂商现有的代码组合起来的程序时更容易, 它还有助于组织程序。

一个潜在的问题是, 可能使用两个已封装好的产品, 而它们都包含一个名为 wanda( )的函数。 这样, 使用 wanda( )函数时, 编译器将不知道指的是哪个版本。 名称空间让厂商能够将其产品封装在一个叫做名称空间的单元中, 这样就可以用名称空间的名称来指出想使用哪个厂商的产品。

因此, Microflop Industries 可以将其定义放到一个名为 Microflop 的名称空间中。 这样, 其 wanda( )函数的全称为 Microflop:: wanda( );

同样, Piscine 公司的 wanda( )版本可以表示为 Piscine:: wanda( )。 这样, 程序就可以使用名称空间来区分不同的版本了:

Microflog::wanda(); // 使用 Microflog 命名空间的 wanda() 函数
Piscine::wanda(); // 使用 Piscine 命名空间的 wanda() 函数

头文件没有扩展名 h 的 C + + 编译器的标准组件都被放置在名称空间 std 中, 这意味着在 iostream 中定义的用于输出的 cout 变量实际上是 std:: cout, 而 endl 实际上是 std:: endl, 这些定义实际是属于 std 这家 公司 的.

所以如果不使用 using namespace std; 指定命名空间,则必须这样写:

#include <iostream> // include 后加空格清晰一些

int main() {
    std::cout << "Hello World!" << std::endl;  //Hello World!

    return 0;
}

下面的一行代码表明, 可以使用 std 名称空间中定义的名称, 而不必使用 std:: 前缀:

using namespace std;

这个 using 编译指令使得 std 名称空间中的所有名称都可用。 这是一种偷懒的做法, 在大型项目中一个潜在的问题。 更好的方法是, 只使所需的名称可用, 这可以通过使用 using 声明来实现:

using  std::cout;
using  std::endl;

完整实例:

#include <iostream> 
using  std::cout; // 没有关键字 namespace 了
using  std::endl;

int main() {
    cout << "Hello World!" << endl;  //Hello World!

    return 0;
}

从概念上看, 输出是一个流, 即从程序流出的一系列字符。 cout 对象表示这种流, 其属性是在 iostream 文件中定义的。 cout 的对象属性包括一个插入运算符( < <), 它可以将其右侧的信息插入到流中。 请看下面的语句( 注意结尾的分号): 它将字符串“ Come up and C + + me some time.” 插入到输出流中。 因此, 与其说程序显示了一条消息, 不如说它将一个字符串插入到了输出流中。

cout << "Hello World!";

使用 cout 显示字符串:

endl 是一个特殊的 C + + 符号, 表示一个重要的概念: 重起一行。 和 cout 一样, endl 也是在头文件 iostream 中定义的, 且位于名称空间 std 中。

与老式 C 语言的区别在于 cout 的聪明程度。 在 C 语言中, 要打印字符撇开 printf( )的复杂性不说, 必须用特殊代码(% s 和% d) 来指出是要打印字符串还是整数。 如果让 printf( )打印字符串, 但又错误地提供了一个整数, 由于 printf( )不够精密, 因此根本发现不了错误。 它将继续处理, 显示一堆乱码。

cout 的智能行为源自 C + + 的面向对象特性。 实际上, C + + 插入运算符( < <) 将根据其后的数据类型相应地调整其行为, 这是一个运算符重载的例子。

如果喜欢 printf( )提供的细致的控制功能, 可以使用更高级的 cout 来获得相同的效果( 参见第 17 章)。

就像 C + + 将输出看作是流出程序的字符流一样, 它也将输入看作是流入程序的字符流。 iostream 文件将 cin 定义为一个表示这种流的对象。 输出时, < < 运算符将字符串插入到输出流中; 输入时, cin 使用 > > 运算符从输入流中抽取字符。 通常, 需要在运算符右侧提供一个变量, 以接收抽取的信息( 符号 < < 和 > > 被选择用来指示信息流的方向)。

与 cout 一样, cin 也是一个智能对象。 它可以将通过键盘输入的一系列字符( 即输入) 转换为接收信息的变量能够接受的形式。

#include <iostream> 
using  namespace std;

int main() {
    int age;

    cout << "How old are you?\n";
    cin >> age;
    cout << "You are " << age << " years old!\n";

    return 0;
}
/*
How old are you?
12
You are 12 years old!
*/

类简介

类是 C + + 中面向对象编程( OOP) 的核心概念之一。 类是用户定义的一种数据类型。 要定义类, 需要描述它能够表示什么信息和可对数据执行哪些操作。 类之于对象就像类型之于变量。 也就是说, 类定义描述的是数据格式及其用法, 而对象则是根据数据格式规范创建的实体。

cout 是一个 ostream 类对象。 ostream 类定义( iostream 文件的另一个成员) 描述了 ostream 对象表示的数据以及可以对它执行的操作, 如将数字或字符串插入到输出流中。 同样, cin 是一个 istream 类对象, 也是在 iostream 中定义的。

类描述了一种数据类型的全部属性( 包括可使用它执行的操作), 对象是根据这些描述创建的实体.

就像函数可以来自函数库一样, 类也可以来自类库。事实上, C + + 当前之所以如此有吸引力, 很大程度上是由于存在大量支持 UNIX、 Macintosh 和 Windows 编程的类库。

类描述指定了可对类对象执行的所有操作。 要对特定对象执行这些允许的操作, 需要给该对象发送一条消息。 例如, 如果希望 cout 对象显示一个字符串, 应向它发送一条消息, 告诉它,“ 对象! 显示这些内容!”

C + + 提供了两种发送消息的方式:

  • 一种方式是使用类方法( 本质上就是稍后将介绍的函数调用);
  • 另一种方式是重新定义运算符, cin 和 cout 采用的就是这种方式。 因此, 下面的语句使用重新定义的 < < 运算符将“ 显示消息” 发送给 cout: cout << "How old are you?\n";

函数

C + + 函数分两种: 有返回值的和没有返回值的。

C + + 程序应当为程序中使用的每个函数提供原型。函数原型之于函数就像变量声明之于变量— 指出涉及的类型。

原型结尾的分号表明它是一条语句, 这使得它是一个原型, 而不是函数头。 如果省略分号, 编译器将把这行代码解释为一个函数头, 并要求接着提供定义该函数的函数体。

不要混淆函数原型和函数定义。 可以看出, 原型只描述函数接口。 也就是说, 它描述的是发送给函数的信息和返回的信息。 而定义中包含了函数的代码。 C 和 C + + 将库函数的这两项特性( 原型和定义) 分开了。 库文件中包含了函数的编译代码, 而头文件中则包含了原型。

应在首次使用函数之前提供其原型。 通常的做法是把原型放到 main( )函数定义的前面。

#include <iostream> 
#include <cmath>  // 该头文件包含了sqrt的原型定义
using  namespace std;

int main() {
    int pingfang;

    cout << "99的平方根是:" << sqrt(99) << endl; //99的平方根是:9.94987

    return 0;
}

只包含 cmath 头文件可以提供原型, 但不一定会导致编译器搜索正确的库文件。 C + + 库函数存储在库文件中。 编译器编译程序时, 它必须在库文件搜索您使用的函数。 至于自动搜索哪些库文件, 将因编译器而异。

让程序能够访问名称空间 std 的方法有多种, 下面是其中的 4 种。

  • 将 using namespace std; 放在函数定义之前, 让文件中所有的函数都能够使用名称空间 std 中所有的元素。
  • 将 using namespace std; 放在特定的函数定义中, 让该函数能够使用名称空间 std 中的所有元素。
  • 在特定的函数中使用类似 using std:: cout; 这样的编译指令, 而不是 using namespace std;, 让该函数能够使用指定的元素, 如 cout。
  • 完全不使用编译指令 using, 而在需要使用名称空间 std 中的元素时, 使用前缀 std::, 如下所示:
std::cout<<"Hello";

第 3 章处理数据

面向对象编程( OOP) 的本质是设计并扩展自己的数据类型。 设计自己的数据类型就是让类型与数据匹配。 如果正确做到了这一点, 将会发现以后使用数据时会容易得多。

内置的 C + + 类型分两组: 基本类型和复合类型。

  • 本章将介绍基本类型, 即整数和浮点数。
  • 第 4 章将介绍在基本类型的基础上创建的复合类型, 包括数组、 字符串、 指针和结构。

为把信息存储在计算机中, 程序必须记录 3 个基本属性:

  • 类型:存储何种类型的信息
  • 值:要存储什么值
  • 地址:信息将存储在哪里;

变量名

必须遵循几种简单的 C + + 命名规则。

  • 在名称中只能使用字母字符、 数字和下划线(_)。
  • 名称的第一个字符不能是数字。
  • 区分大写字符与小写字符。
  • 不能将 C + + 关键字用作名称。
  • 以两个下划线或下划线和大写字母打头的名称被保留给实现( 编译器及其使用的资源) 使用。 以一个下划线开头的名称被保留给实现, 用作全局标识符。
  • C + + 对于名称的长度没有限制, 名称中所有的字符都有意义, 但有些平台有长度限制。

倒数第二点与前面几点有些不同, 因为使用像_time_stop 或_Donut 这样的名称不会导致编译器错误, 而会导致行为的不确定性。 换句话说, 不知道结果将是什么。 不出现编译器错误的原因是, 这样的名称不是非法的, 但要留给实现使用。 全局名称指的是名称被声明的位置, 这将在第 4 章讨论。

如果想用两个或更多的单词组成一个名称, 通常的做法是用下划线字符将单词分开, 如 my_onions; 或者从第二个单词开始将每个单词的第一个字母大写, 如 myEyeTooth。

与函数命名一样, 大写在变量命名中也是一个关键问题( 参见第 2 章的注释“ 命名约定”), 但很多程序员可能会在变量名中加入其他的信息, 即描述变量类型或内容的前缀。 例如, 可以将整型变量 myWeight 命名为 nMyWeight, 其中前缀 n 用来表示整数值, 在阅读代码或变量定义不是十分清楚的情况下, 前缀很有用。 另外, 这个变量也可以叫做 intMyWeight, 这将更精确, 而且容易理解, 不过它多了几个字母( 对于很多程序员来说, 这是非常讨厌的事)。

常以这种方式使用的其他前缀有: str 或 sz( 表示以空字符结束的字符串)、 b( 表示布尔值)、 p( 表示指针) 和 c( 表示单个字符)。

随着对 C + + 的逐步了解, 将发现很多有关前缀命名风格的示例( 包括漂亮的 m_lpctstr 前缀— 这是一个类成员值, 其中包含了指向常量的长指针和以空字符结尾的字符串), 还有其他更奇异、 更违反直觉的风格, 采不采用这些风格, 完全取决于程序员。 在 C + + 所有主观的风格中, 一致性和精度是最重要的。 请根据自己的需要、 喜好和个人风格来使用变量名( 或必要时, 根据雇主的需要、 喜好和个人风格来选择变量名)。

整数

如果将无限大的整数看作很大, 则不可能用有限的计算机内存来表示所有的整数。 因此, 语言只能表示所有整数的一个子集。 有些语言只提供一种整型( 一种类型满足所有要求!), 而 C + + 则提供好几种, 这样便能够根据程序的具体要求选择最合适的整型。

术语宽度( width) 用于描述存储整数时使用的内存量。 使用的内存越多, 则越宽。 C + + 的基本整型( 按宽度递增的顺序排列) 分别是 char、 short、 int、 long 和 C + + 11 新增的 long long, 其中每种类型都有符号版本和无符号版本, 因此总共有 10 种类型可供选择。

C + + 提供了一种灵活的标准, 它确保了最小长度( 从 C 语言借鉴而来), 如下所示:

  • short 至少 16 位;
  • int 至少与 short 一样长;
  • long 至少 32 位, 且至少与 int 一样长;
  • long long 至少 64 位, 且至少与 long 一样长。

short 是 short int 的简称, 而 long 是 long int 的简称, 但是程序设计者们几乎都不使用比较长的形式。

通常, 在老式 IBM PC 的实现中, int 的宽度为 16 位( 与 short 相同).

要知道系统中整数的最大长度, 可以在程序中使用 C + + 工具来检查类型的长度。

#include <iostream> 
#include <climits>

using  namespace std;

int main() {
    cout << "char 长度为 " << sizeof(char) << " bytes(字节)" << endl;
    cout << "short 长度为 " << sizeof(short) << " bytes(字节)" << endl;
    cout << "int 长度为 " << sizeof(int) << " bytes(字节)" << endl;
    cout << "long 长度为 " << sizeof(long) << " bytes(字节)" << endl;
    cout << "long long 长度为 " << sizeof(long long) << " bytes(字节)" << endl;
    /*
    char 长度为 1 bytes(字节)
    short 长度为 2 bytes(字节)
    int 长度为 4 bytes(字节)
    long 长度为 4 bytes(字节)
    long long 长度为 8 bytes(字节)
    */

    cout << "char,short,int,long,long long 各类型最大值:" << " " << CHAR_MAX << " " << SHRT_MAX << " " << INT_MAX << " " << LONG_MAX << " " << LLONG_MAX << endl;
    // short,int,long,long long 各类型最大值:127 32767 2147483647 2147483647 9223372036854775807

    //CHAR_BIT 为字节的位数。
    cout << "每byte的bit数:" << CHAR_BIT; //每byte的bit数:8

    return 0;
}

climits 文件中包含与下面类似的语句行:

#define INT_MAX 32767

在 C + + 编译过程中, 首先将源代码传递给预处理器。 在这里,# define 和# include 一样, 也是一个预处理器编译指令。 该编译指令告诉预处理器: 在程序中查找 INT_MAX, 并将所有的 INT_MAX 都替换为 32767。 因此# define 编译指令的工作方式与文本编辑器或字处理器中的全局搜索并替换命令相似。 修改后的程序将在完成这些替换后被编译。 预处理器查找独立的标记( 单独的单词), 跳过嵌入的单词。 也就是说, 预处理器不会将 PINT_MAXTM 替换为 P32767IM。 也可以使用# define来定义自己的符号常量。 然而,# define 编译指令是 C 语言遗留下来的。 C + + 有一种更好的创建符号常量的方法( 使用关键字 const, 将在后面的一节讨论), 所以不会经常使用# define。 然而, 有些头文件, 尤其是那些被设计成可用于 C 和 C + + 中的头文件, 必须使用# define。

如果不对函数内部定义的变量进行初始化, 该变量的值将是不确定的。 这意味着该变量的值将是它被创建之前, 相应内存单元保存的值。一般编译器会检查出这种错误.

通过使用 C + + 新增的大括号初始化器, 初始化常规变量的方式与初始化类变量的方式更像。 C + + 11 使得可将大括号初始化器用于任何类型( 可以使用等号, 也可以不使用), 这是一种通用的初始化语法。

#include <iostream> 
using  namespace std;

int main() {
    short i{ 1 }, j{}, k = { 2 };

    cout << "i,j,k=" << i << " " << j << " " << k << endl; //i,j,k=1 0 2

    return 0;
}

整型字面值( 常量) 是显式地书写的常量, 如 212 或 1776。 与 C 相同, C + + 能够以三种不同的计数方式来书写整数: 基数为 10、 基数为 8( 老式 UNIX 版本) 和基数为 16( 硬件黑客的最爱)。

头文件 iostream 提供了控制符 dec、 hex 和 oct, 分别用于指示 cout 以十进制、 十六进制和八进制格式显示整数。 程序使用了 hex 和 oct 以上述三种格式显示十进制值 42。 默认格式为十进制, 在修改格式之前, 原来的格式将一直有效。

#include <iostream> 
using  namespace std;

int main() {
    short i{ 42 };

    // dec, hex,oct: 注意, cout输出并不会加上 0,0x或 0X的前缀
    cout << i << hex << " " << i << oct << " " << i << dec << " " << i; // 42 2a 52 42

    return 0;
}

cout中整形常量的数据类型

除非有理由存储为其他类型( 如使用了特殊的后缀来表示特定的类型, 或者值太大, 不能存储为 int), 否则 C + + 将整型常量存储为 int 类型。

后缀是放在数字常量后面的字母, 用于表示类型。

  • 整数后面的 l 或 L 后缀表示该整数为 long 常量,
  • u 或 U 后缀表示 unsigned int 常量,
  • ul( 可以采用任何一种顺序, 大写小写均可) 表示 unsigned long 常量( 由于小写 l 看上去像 1, 因此应使用大写 L 作后缀)。
  • C + + 11 提供了用于表示类型 long long 的后缀 ll 和 LL, 还提供了用于表示类型 unsigned long long 的后缀 ull、 Ull、 uLL 和 ULL。
#include <bits/stdc++.h>
using  namespace std;

int main() {
    // typeid(variable).name() 输出变量类型的名称
    cout << typeid(12).name()<<endl;
    cout << typeid(12L).name() << endl;
    cout << typeid(12U).name() << endl;
    cout << typeid(12UL).name() << endl;
    cout << typeid(12LL).name() << endl;
    cout << typeid(12ULL).name() << endl;
    /*
    int
    long
    unsigned int
    unsigned long
    __int64
    unsigned __int64
    */
    return 0;
}

char 类型: 字符和小整数

char 类型是专为存储字符( 如字母和数字) 而设计的。

存储数字对于计算机来说算不了什么, 但存储字母则是另一回事。 编程语言通过使用字母的数值编码解决了这个问题。 因此, char 类型是另一种整型。 它足够长, 能够表示目标计算机系统中的所有基本符号— 所有的字母、 数字、 标点符号等。 实际上, 很多系统支持的字符都不超过 128 个, 因此用一个字节就可以表示所有的符号。 因此, 虽然 char 最常被用来处理字符, 但也可以将它用做比 short 更小的整型。

C + + 实现使用的是其主机系统的编码— 例如, IBM 大型机使用 EBCDIC 编码。 ASCII 和 EBCDIC 都不能很好地满足国际需要, C + + 支持的宽字符类型可以存储更多的值, 如国际 Unicode 字符集使用的值。 本章稍后将介绍 wchar_t 类型。

cout.put( )函数, 该函数显示一个字符。

#include <bits/stdc++.h>
using  namespace std;

int main() {
    char c;
    int i;
    cout << "请输入一个字符" << endl;
    cin >> c;
    cout << "输入的字符为:" << c << endl;
    cout<< "输入的字符对应的整数:" << int(c) << endl;
    printf("printf打印整数: %d \n", c);
    printf("printf打印字符: %c \n", c);

    i = c;
    cout <<"将字符赋予整数,整数:"<< i << endl;
    cout << "使用cout.put()将整数显示为字符:";
    cout.put(i);

    return 0;
}
/*
请输入一个字符
hello
输入的字符为:h
输入的字符对应的整数:104
printf打印整数: 104
printf打印字符: h
将字符赋予整数,整数:104
使用cout.put()将整数显示为字符:h
*/

成员函数 cout.put( )

函数 cout.put()是一个重要的 C + + OOP 概念— 成员函数— 的第一个例子。

类定义了如何表示和控制数据。 成员函数归类所有, 描述了操纵类数据的方法。 例如类 ostream 有一个 put( )成员函数, 用来输出字符。

只能通过类的特定对象( 例如这里的 cout 对象) 来使用成员函数。 要通过对象( 如 cout) 使用成员函数, 必须用句点将对象名和函数名称( put( )) 连接起来。 句点被称为成员运算符。

cout.put( )的意思是, 通过类对象 cout 来使用函数 put( )。 第 10 章介绍类时将更详细地介绍这一点。 现在, 您接触的类只有 istream 和 ostream, 可以通过使用它们的成员函数来熟悉这一概念。

cout.put( )成员函数提供了另一种显示字符的方法, 可以替代 < < 运算符。

char 字面值

在 C + + 中, 书写字符常量的方式有多种。 对于常规字符( 如字母、 标点符号和数字), 最简单的方法是将字符用单引号括起。 这种表示法代表的是字符的数值编码。

ASCII 系统中的对应情况如下:

  • 'A' 为 65, 即字符 A 的 ASCII 码;
  • 'a' 为 97, 即字符 a 的 ASCII 码

这种表示法优于数值编码, 它更加清晰, 且不需要知道编码方式。 如果系统使用的是 EBCDIC, 则 A 的编码将不是 65, 但是' A' 表示的仍然是字符 A。

现代系统并非都支持所有的转义序列。 例如, 输入振铃字符时, 有些系统保持沉默。

\nendl

  • 可以将换行符嵌入到较长的字符串中, 这通常比使用 endl 方便。在字符串末尾添加一个换行符所需的输入量要少些
  • 显示数字时, 使用 endl 比输入“\n” 或‘\n’ 更容易些

通用字符名

通用字符名的用法类似于转义序列。 通用字符名可以以\u\U 打头。\ u 后面是 8 个十六进制位,\ U 后面则是 16 个十六进制位。 这些位表示的是字符的 ISO 10646 码点( ISO 10646 是一种正在制定的国际标准, 为大量的字符提供了数值编码, 请参见本章后面的“ Unicode 和 ISO 10646”)。 如果所用的实现支持扩展字符, 则可以在标识符( 如字符常量) 和字符串中使用通用字符名。

#include <iostream>

using  namespace std;

int main() {
    cout << "Let's eat some g\u00E2teau.\n"; // Let's eat some gâteau.

    return 0;
}

如果系统不支持 ISO 10646, 它将显示其他字符或 gu00E2teau, 而不是 â。

windows的 VisualStudio2019 并不支持ISO 10646, 关于 __STDC_ISO_10646__ 的说明参考 https://www.gnu.org/software/libc/manual/html_node/Extended-Char-Intro.html:

The standard defines at least a macro __STDC_ISO_10646__ that is only defined on systems where the wchar_t type encodes ISO 10646 characters. If this symbol is not defined one should avoid making assumptions about the wide character representation. If the programmer uses only the functions provided by the C library to handle wide character strings there should be no compatibility problems with other systems.

测试:

#include <iostream>
#include <string>

using  namespace std;

int main() {

#ifdef __STDC_ISO_10646__
    cout << "__STDC_ISO_10646__ == " << __STDC_ISO_10646__<<endl;
#else
    cout << "NO DEFINE OF __STDC_ISO_10646__"<<endl;
#endif
//NO DEFINE OF __STDC_ISO_10646__

    auto S3 = u"hello"; 
    wcout << S3 << endl;
    string t = "\u00E2";
    cout << t << endl;

    cout << "â" << endl;
    string s = "â";
    cout << s << endl;

    s = "Let's eat some g\u00E2teau.";
    cout << s << endl;

    cout << "Let's eat some g\u00E2teau.\n"; // Let's eat some gâteau.
    cout << sizeof(wchar_t);
    cout << "test";
    return 0;
}
/*
VisualStudio2019的输出:
NO DEFINE OF __STDC_ISO_10646__
0074DDF4
?
â
â
Let's eat some g?teau.
Let's eat some g?teau.
2test


ubuntu g++ 的输出:
__STDC_ISO_10646__ == 201706
0x55d63508d01c
â
â
â
Let's eat some gâteau.
Let's eat some gâteau.
*/

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。

#include <iostream>
using  namespace std;

int main() {
    char i = -100;
    signed char j = -100;
    unsigned char k = -100;

    printf("%d %d %d \n", i, j, k); //-100 -100 156

    return 0;
}

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 来指示宽字符常量和宽字符串。

在支持两字节 wchar_t 的系统中, 上述代码将把每个字符存储在一个两个字节的内存单元中。 本书不使用宽字符类型, 但读者应知道有这种类型, 尤其是在进行国际编程或使用 Unicode 或 ISO 10646 时。

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 也都有底层类型— 一种内置的整型, 但底层类型可能随系统而已。

bool 类型

  • 布尔变量的值可以是 true 或 false。
  • C + + 将非零值解释为 true, 将零解释为 false。
  • 可以使用 bool 类型来表示真和假 , 它们分别用预定义的字面值 true 和 false 表示。
  • 字面值 true 和 false 都可以通过提升转换为 int 类型, true 被转换为 1, 而 false 被转换为 0
  • 任何数字值或指针值都可以被隐式转换( 即不用显式强制转换) 为 bool 值。 任何非零值都被转换为 true, 而零被转换为 false

const 限定符

  • 一种常见的做法是将名称的首字母大写, 以提醒您 Months 是个常量。 这决不是一种通用约定, 但在阅读程序时有助于区分常量和变量。
  • 另一种约定是将整个名称大写, 使用 #define 创建常量时通常使用这种约定。
  • 还有一种约定是以字母 k 打头, 如 kmonths。keep?
  • 如果在声明常量时没有提供值, 则该常量的值将是不确定的, 且无法修改。

const 比 #define:

  • 首先, 它能够明确指定类型。
  • 其次, 可以使用 C + + 的作用域规则将定义限制在特定的函数或文件中
  • 第三, 可以将 const 用于更复杂的类型, 如第 4 章将介绍的数组和结构。

浮点数

如果数字很大, 无法表示为 long 类型, 如人体的细菌数( 估计超过 100 兆), 则可以使用浮点类型来表示。

使用浮点类型可以表示诸如 2.5、 3.14159 和 122442.32 这样的数字, 即带小数部分的数字。

计算机将这样的值分成两部分存储。 一部分表示值, 另一部分用于对值进行放大或缩小。

下面打个比方。 对于数字 34.1245 和 34124.5, 它们除了小数点的位置不同外, 其他都是相同的。 可以把第一个数表示为 0.341245( 基准值) 和 100( 缩放因子), 而将第二个数表示为 0.341245( 基准值相同) 和 10000( 缩放因子更大)。 缩放因子的作用是移动小数点的位置, 术语浮点因此而得名。 C + + 内部表示浮点数的方法与此相同, 只不过它基于的是二进制数, 因此缩放因子是 2 的幂, 不是 10 的幂。

幸运的是, 程序员不必详细了解内部表示。 重要的是, 浮点数能够表示小数值、 非常大和非常小的值, 它们的内部表示方法与整数有天壤之别。

E 表示法最适合于非常大和非常小的数。 E 表示法确保数字以浮点格式存储, 即使没有小数点。 注意, 既可以使用 E 也可以使用 e, 指数可以是正数也可以是负数。

d.dddE + n 指的是将小数点向右移 n 位, 而 d.dddE ~ n 指的是将小数点向左移 n 位。 之所以称为“ 浮点”, 就是因为小数点可移动。

浮点类型

和 ANSI C 一样, C + + 也有 3 种浮点类型: float、 double 和 long double。 这些类型是按它们可以表示的有效数位和允许的指数最小范围来描述的。

有效位( significant figure) 是数字中有意义的位。 例如, 加利福尼亚的 Shasta 山脉的高度为 14179 英尺, 该数字使用了 5 个有效位, 指出了最接近的英尺数。 然而, 将 Shasta 山脉的高度写成约 14000 英尺时, 有效位数为 2 位, 因为结果经过四舍五入精确到了千位。 在这种情况下, 其余的 3 位只不过是占位符而已。

有效位数不依赖于小数点的位置。 例如, 可以将高度写成 14.162 千英尺。 这样仍有 5 个有效位, 因为这个值精确到了第 5 位。

C 和 C + + 对于有效位数的要求是:

  • float 至少 32 位,
  • double 至少 48 位, 且不少于 float,
  • long double 至少和 double 一样多。
  • 这三种类型的有效位数可以一样多。
  • 然而, 通常, float 为 32 位, double 为 64 位, long double 为 80、 96 或 128 位。
  • 另外, 这 3 种类型的指数范围至少是 − 37 到 37。
  • 可以从头文件 cfloat 或 float.h 中找到系统的限制。( cfloat 是 C 语言的 float.h 文件的 C + + 版本。)

ostream 方法 setf( ) 迫使输出使用定点表示法, 以便更好地了解精度, 它防止程序把较大的值切换为 E 表示法, 并使程序显示到小数点后 6 位。 参数 ios_base:: fixed 和 ios_base:: floatfield 是通过包含 iostream 来提供的常量。

在程序中书写浮点常量的时候, 程序将把它存储为哪种浮点类型呢? 在默认情况下, 像 8.24 和 2.4E8 这样的浮点常量都属于 double 类型。 如果希望常量为 float 类型, 请使用 f 或 F 后缀。 对于 long double 类型, 可使用 l 或 L 后缀( 由于 l 看起来像数字 1, 因此 L 是更好的选择)。

与整数相比, 浮点数有两大优点。 首先, 它们可以表示整数之间的值。 其次, 由于有缩放因子, 它们可以表示的范围大得多。 另一方面, 浮点运算的速度通常比整数运算慢, 且精度将降低。

#include <iostream>
#include <cfloat>

using  namespace std;

int main() {
    // 有效数字的位数,注意不是小数点后面的有效位数,而是所有位数. (基数 10)
    cout << "float,double,long double 的有效数字位数(基数10): " << FLT_DIG << ' ' << DBL_DIG << ' ' << LDBL_DIG << endl;
    // 基数中的位数, 基数 2
    cout << "float,double,long double 的有效数字位数(基数2): " << FLT_MANT_DIG << ' ' << DBL_MANT_DIG << ' ' << LDBL_MANT_DIG << endl;

    // 基数为 10 时的指数的最小负整数值
    cout << "float,double,long double 基数为 10 时的指数的最小负整数值: " << FLT_MIN_10_EXP << ' ' << DBL_MIN_10_EXP << ' ' << LDBL_MIN_10_EXP << endl;

    // 基数为 10 时的指数的最大整数值
    cout << "float,double,long double 基数为 10 时的指数的最大整数值: " << FLT_MAX_10_EXP << ' ' << DBL_MAX_10_EXP << ' ' << LDBL_MAX_10_EXP << endl;

    /*
    float,double,long double 的有效数字位?基数10): 6 15 15
    float,double,long double 的有效数字位?基数2): 24 53 53
    float,double,long double 基数?10 时的指数的最小负整数? -37 -307 -307
    float,double,long double 基数?10 时的指数的最大整数? 38 308 308
    */

    cout.setf(ios_base::fixed, ios_base::floatfield);

    float f = 10.0 / 3;
    double d = 10.0 / 3;
    const float million = 1.0e+6; //百万

    cout << "f=" << f << " d=" << d << endl;
    // 该系统确保 float 至少有 6 位有效位, 但这是最糟糕的情况)。
    cout << "百万*f =" << f * million << endl;
    cout << "千万*f =" << f * 10*million << endl;
    cout << "千万*f =" << 10 * million * f << endl;
    cout << "百万*d=" << d * million << endl;
    /*
    f=3.333333 d=3.333333
    百万*f =3333333.250000
    千万*f =33333332.000000
    千万*f =33333332.000000
    百万*d=3333333.333333
    */

    // 2.34e22 是一个小数点左边有 23 位的数字。 加上 1, 就是在第 23 位加 1。 
    // 但 float 类型只能表示数字中的前 6 位或前 7 位, 因此修改第 23 位对这个值不会有任何影响。
    float a = 2.34e22;
    float b = a + 1.0;
    cout << "a=" << a << " b= a+1.0, b-a=" << b - a << endl;
    // a=23400001102275227418624.000000 b= a+1.0, b-a=0.000000

    return 0;
}

数据类型分类

C + + 对基本类型进行分类, 形成了若干个族。

  • 整数和浮点型统称算术( arithmetic) 类型。
    • float、 double 和 long double 统称为浮点型。
    • bool、 char、 wchar_t、 符号整数和无符号整型统称为整型;
    • 类型 signed char、 short、 int 和 long 统称为符号整型; 它们的无符号版本统称为无符号整型;
    • C + + 11 新增了 long long。
    • C + + 11 新增了 char16_t 和 char32_t。

对于 float, C + + 只保证 6 位有效位。 如果将 61.419998 四舍五入成 6 位, 将得到 61.4200, 这是保证精度下的正确值。 如果需要更高的精度, 请使用 double 或 long double。

对不同类型进行运算时, C + + 将把它们全部转换为同一类型。

浮点常量在默认情况下为 double 类型。

求模运算符返回整数除法的余数。 它与整数除法相结合, 尤其适用于解决要求将一个量分成不同的整数单元的问题, 例如将英寸转换为英尺和英寸, 或者将美元转换为元、 角、 分、 厘。

潜在的数值转换问题

转换 潜在的问题
将较大的浮点类型转换为较小的浮点类型, 如将 double 转换为 float 精度( 有效数位) 降低, 值可能超出目标类型的取值范围, 在这种情况下, 结果将是不确定的
将浮点类型转换为整型 小数部分丢失, 原来的值可能超出目标类型的取值范围, 在这种情况下, 结果将是不确定的
将较大的整型转换为较小的整型, 如将 long 转换为 short 原来的值可能超出目标类型的取值范围, 通常只复制右边的字节

将 0 赋给 bool 变量时, 将被转换为 false; 而非零值将被转换为 true。

将浮点值赋给整型将导致两个问题。

  • 首先, 将浮点值转换为整型会将数字截短( 除掉小数部分)。
  • 其次, float 值对于 int 变量来说可能太大了。 在这种情况下, C + + 并没有定义结果应该是什么; 这意味着不同的实现的反应可能不同。
#include <iostream>

using  namespace std;

int main() {
    double d = 7.2e12;
    int i = d;
    long long j = d;
    cout << i << endl; // -2147483648, 不可预料的结果
    cout << j << endl; // 7200000000000
    return 0;
}

以{ }方式初始化时进行的转换( C + + 11)

C + + 11 将使用大括号的初始化称为列表初始化( list-initialization), 因为这种初始化常用于给复杂的数据类型提供值列表。 它对类型转换的要求更严格。 具体地说,

  • 列表初始化不允许缩窄( narrowing), 即变量的类型可能无法表示赋给它的值。 例如, 不允许将浮点型转换为整型。
  • 在不同的整型之间转换或将整型转换为浮点型可能被允许, 条件是编译器知道目标变量能够正确地存储赋给它的值。 例如, 可将 long 变量初始化为 int 值, 因为 long 总是至少与 int 一样长; 相反方向的转换也可能被允许, 只要 int 变量能够存储赋给它的 long 常量

当同一个表达式中包含两种不同的算术类型时, 将出现什么情况呢? 在这种情况下, C + + 将执行两种自动转换: 首先, 一些类型在出现时便会自动转换; 其次, 有些类型在与其他类型同时出现在表达式中时将被转换。

先来看看自动转换。 在计算表达式时, C + + 将 bool、 char、 unsigned char、 signed char 和 short 值转换为 int。 具体地说, true 被转换为 1, false 被转换为 0。 这些转换被称为整型提升( integral promotion)。

通常将 int 类型选择为计算机最自然的类型, 这意味着计算机使用这种类型时, 运算速度可能最快。

还有其他一些整型提升: 如果 short 比 int 短, 则 unsigned short 类型将被转换为 int; 如果两种类型的长度相同, 则 unsigned short 类型将被转换为 unsigned int。 这种规则确保了在对 unsigned short 进行提升时不会损失数据。

同样, wchar_t 被提升成为下列类型中第一个宽度足够存储 wchar_t 取值范围的类型: int、 unsigned int、 long 或 unsigned long。 将不同类型进行算术运算时, 也会进行一些转换, 例如将 int 和 float 相加时。 当运算涉及两种类型时, 较小的类型将被转换为较大的类型。

编译器通过校验表来确定在算术表达式中执行的转换。 C + + 11 对这个校验表稍做了修改, 下面是 C + + 11 版本的校验表, 编译器将依次查阅该列表。

  • (1) 如果有一个操作数的类型是 long double, 则将另一个操作数转换为 long double。
  • (2) 否则, 如果有一个操作数的类型是 double, 则将另一个操作数转换为 double。
  • (3) 否则, 如果有一个操作数的类型是 float, 则将另一个操作数转换为 float。
  • (4) 否则, 说明操作数都是整型, 因此执行整型提升。
  • (5) 在这种情况下, 如果两个操作数都是有符号或无符号的, 且其中一个操作数的级别比另一个低, 则转换为级别高的类型。
  • (6) 如果一个操作数为有符号的, 另一个操作数为无符号的, 且无符号操作数的级别比有符号操作数高, 则将有符号操作数转换为无符号操作数所属的类型。
  • (7) 否则, 如果有符号类型可表示无符号类型的所有可能取值, 则将无符号操作数转换为有符号操作数所属的类型。
  • (8) 否则, 将两个操作数都转换为有符号类型的无符号版本。

C + + 还允许通过强制类型转换机制显式地进行类型转换。( C + + 认识到, 必须有类型规则, 而有时又需要推翻这些规则。)

强制类型转换不会修改 thorn 变量本身, 而是创建一个新的、 指定类型的值, 可以在表达式中使用这个值。

强制类型转换的格式有两种。 例如, 为将存储在变量 thorn 中的 int 值转换为 long 类型, 可以使用下述表达式中的一种:

(long) i;
long (i);

第一种格式来自 C 语言, 第二种格式是纯粹的 C + +。 新格式的想法是, 要让强制类型转换就像是函数调用。 这样对内置类型的强制类型转换就像是为用户定义的类设计的类型转换。

Stroustrup 认为, C 语言式的强制类型转换由于有过多的可能性而极其危险, 这将在第 15 章更深入地讨论。 运算符 static_cast <> 比传统强制类型转换更严格。

#include <iostream>

using  namespace std;

int main() {
    const int code = 66;
    int x = 66;
    //char c1 = { 31325 }; // 错误
    char c2 = { 66 };
    char c3 = { code };
    // 初始化 c4 时, 您知道 x 的值为 66, 但在编译器看来, x 是一个变量, 其值可能很大。
    //  编译器不会跟踪下述阶段可能发生的情况: 从 x 被初始化到它被用来初始化 c4。
    //char c4 = { x }; // 错误, 编译器认为 int -> char 

    x = 31325;
    // 对于x, c++ 允许如下方式的赋值
    char c5 = x;
    cout << c5; // ]


    short a = 20, b = 30;
    short c = a + b; // a,b 先转为int 参与计算, 结果int 再转为short

    return 0;
}

C + + 11 中的 auto 声明

C + + 11 新增了一个工具, 让编译器能够根据初始值的类型推断变量的类型。 为此, 它重新定义了 auto 的含义。 auto 是一个 C 语言关键字, 但很少使用, 有关其以前的含义, 请参阅第 9 章。 在初始化声明中, 如果使用关键字 auto, 而不指定变量的类型, 编译器将把变量的类型设置成与初始值相同:

处理复杂类型, 如标准模块库( STL) 中的类型时, 自动类型推断才更有用.

© Licensed under CC BY-NC-SA 4.0

价值投资不能保证我们盈利, 但价值投资给我们提供了通向成功的唯一机会。——巴菲特

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!