Skip to content

Latest commit

 

History

History
1602 lines (1117 loc) · 91.3 KB

File metadata and controls

1602 lines (1117 loc) · 91.3 KB

三、探索 C++ 类型

在最后两章中,您已经学习了如何编写 C++ 程序,了解了您使用的文件,以及控制执行流的方法。 本章介绍您将在程序中使用的数据:数据类型以及保存这些数据的变量。

变量可以处理具有特定格式和特定行为的数据,这由变量的类型决定。 变量的类型决定了在用户输入或查看数据时可以对数据和数据格式执行的操作。

实际上,您可以查看三种常见的类型类别:内置类型、自定义类型和指针。 指针一般将在下一章中介绍,自定义类型或类以及指向它们的指针将在第 6 章中介绍。 本章将介绍作为 C++ 语言一部分提供的类型。

探索内置类型

C++ 提供整数、浮点和布尔类型。 char类型是整数,但它可用于保存单个字符,因此其数据可视为数字或字符。 C++ 标准库提供了string类,允许您使用和操作字符串。 字符串将在中使用字符串进行深入介绍。

顾名思义,整数类型包含没有小数部分的整数值。 如果您使用整数执行计算,除非您采取措施保留小数部分(例如,通过余数运算符%),否则任何小数部分都将被丢弃。 浮点类型保存可能包含小数部分的数字;因为浮点类型可以保存尾数指数格式的数字,所以它们可以保存非常大的数字,也可以保存非常小的数字。

变量是类型的实例;它是为保存该类型可以保存的数据而分配的内存。 可以修改整数和浮点变量声明,以告知编译器要分配多少内存,从而告知变量可以保存的数据的限制以及对变量执行的计算的精度。 此外,您还可以指示变量是否会在符号重要的地方保存一个数字。 如果数字用于保存位图(其中位不构成数字,但有其自己的单独含义),则使用有符号类型通常没有意义。

在某些情况下,您将使用 C++ 从文件或网络流解压缩数据,以便您可以对其进行操作。 在这种情况下,您需要知道数据是浮点型的还是整型的、有符号的还是无符号的、使用了多少字节以及这些字节的顺序。 字节的顺序(多字节数中的第一个字节是数字的低位还是高位)由您正在编译的处理器决定,在大多数情况下,您不需要担心这个问题。

同样,有时您可能需要了解变量的大小以及它在内存中的对齐方式;特别是当您使用数据记录(在 C++ 中称为structs)时。 C++ 提供了sizeof运算符来提供用于保存变量的字节数,并提供了alignof运算符来确定内存中类型的对齐方式。 对于基本类型,sizeofalignof运算符返回相同的值;只需在自定义类型上调用alignof运算符,它将返回类型中最大数据成员的对齐。

整数

顾名思义,整数保存整数数据,即没有小数部分的数字。 因此,在小数部分很重要的情况下,使用整数进行任何算术都没有什么意义;在这种情况下,您应该使用浮点数。 上一章给出了一个这样的例子:

    int height = 480;  
    int width = 640; 
    int aspect_ratio = width / height;

这给出的纵横比为 1,这显然是不正确的,也没有任何作用。 即使将结果赋给浮点数,也会得到相同的结果:

    float aspect_ratio = width / height;

原因是算术是在表达式width / height中执行的,该表达式将对将丢弃结果的任何小数部分的整数使用除法运算符。 要使用浮点除法运算符,必须将一个或另一个操作数转换为浮点数,因此使用浮点运算符:

    float aspect_ratio = width / (float)height;

这将为aspect_ratio变量赋值 1.3333(或 4:3)。 这里使用的 CAST 操作符是 C CAST 操作符,它强制将一种类型的数据用作另一种类型的数据。 (之所以使用它,是因为我们还没有引入 C++ CAST 运算符,并且 C CAST 运算符的语法很清楚。)。 此强制转换中没有类型安全。 C++ 提供了强制转换运算符,下面将讨论这些操作符,其中一些将以类型安全的方式强制转换,这在使用指向自定义类型的对象的指针时变得很重要。

C++ 提供了各种大小的整数类型,如下表所述。 这是五种标准整数类型。 该标准规定,int是处理器的自然大小,将具有介于(包括)INT_MININT_MAX(在<climits>头文件中定义)之间的值。 整数类型的大小至少与列表中它前面的那些类型一样大,因此int至少与short intlong long int个类型一样大,后者至少与long int类型一样大。 如果类型都相同,那么短语至少和一样大就没有多大用处了,所以<climits>头文件也定义了其他基本整数类型的范围。 具体需要多少字节来存储这些整数范围,具体取决于具体实现。 下表列出了 x86 32 位处理器上的基本类型和大小范围:

| 类型 | 范围 | 字节大小 | | signed char | -128 至 127 | 1. | | short int | -32768 至 32767 | 2 个 | | int | -2147483648 至 2147483647 | 4. | | long int | -2147483648 至 2147483647 | 4. | | long long int | -9223372036854775808 至 9223372036854775807 | 8 个 |

实际上,您使用的不是short int类型,而是short;对于long int,您将使用long;对于long long int,您通常将使用long long。 从本表可以看到,intlong int的类型大小相同,但它们仍然是两种不同的类型。

char类型外,默认情况下整数类型是有符号的,即它们既可以包含正数也可以包含负数(例如,类型为short的变量可以具有介于-32,768 和 32,767 之间的值)。 您可以使用signed关键字显式指示该类型是有符号的。 您还可以通过使用unsigned关键字来获得无符号的等价物,这将为您提供额外的位,但也意味着按位运算符和移位运算符将按照您的预期工作。 您可能会发现unsigned使用时没有类型,在这种情况下它指的是unsigned int。 类似地,不带类型使用的signed指的是signed int

char类型是与unsigned charsigned char不同的类型。 该标准规定,char中的每一位都用来保存字符信息,因此是否可以将char视为可以保存负数取决于实现。 如果你想让char保存一个有符号的数字,你应该特别使用signed char

该标准对标准整数类型的大小并不精确,如果您正在编写包含字节流的代码(例如,访问文件或网络流中的数据),这可能是一个问题。 <cstdlib>头文件定义将保存特定数据范围的命名类型。 这些类型的名称具有范围内使用的位数(尽管实际类型可能需要更多位)。 因此,存在名为int16_tuint16_t的类型,其中第一种类型是包含 16 位值范围的有符号整数,第二种类型是无符号整数。 还有为 8 位、32 位和 64 位值声明的类型。

下面显示了 x86 计算机上由sizeof运算符确定的这些类型的实际大小:

    // #include <cstdint> 
    using namespace std;               // Values for x86 
    cout << sizeof(int8_t)  << endl;   // 1 
    cout << sizeof(int16_t) << endl;   // 2 
    cout << sizeof(int32_t) << endl;   // 4 
    cout << sizeof(int64_t) << endl;   // 8

此外,<cstdlib>头文件使用与前面相同的命名方案定义了名称为int_least16_tuint_least16_t的类型,并且定义了 8 位、16 位、32 位和 64 位版本。 名称的least部分表示该类型将保存至少具有指定位数的值,但可能会有更多位数。 还有一些名称为int_fast16_tuint_fast16_t的类型,其版本为 8 位、16 位、32 位和 64 位,它们被认为是可以容纳该位数的最快类型。

指定整型文字

要为整数变量赋值,需要提供一个没有小数部分的数字。 编译器将以数字表示的最接近精度标识类型,并尝试赋值整数,如有必要则执行转换。

要显式指定文字是long值,可以使用lL后缀。 同样,对于unsigned long,您可以使用后缀:ulUL。 对于long long值,可以使用llLL后缀,对unsigned long long使用ullULLu(或U)后缀用于unsigned(即unsigned int),您不需要为int添加后缀。 下面使用大写后缀说明了这一点:

    int i = 3; 
    signed s = 3; 
    unsigned int ui = 3U; 
    long l = 3L; 
    unsigned long ul = 3UL; 
    long long ll = 3LL; 
    unsigned long long ull = 3ULL;

使用以 10 为基数的数字系统来指定位图形式的数字是令人困惑和麻烦的。 位图中的位是 2 的幂,所以使用 2 的幂的数字系统更有意义。C++ 允许您提供八进制(以 8 为基数)或十六进制(以 16 为基数)的数字。 要以八进制形式提供文字,您需要在数字前面加上一个零字符(0)。 要提供十六进制的文字,需要在数字前面加上0x字符序列。 八进制数使用数字 0 到 7,但十六进制数字需要 16 位,这意味着 0 到 9 和 a 到 f(或 A 到 F),其中 A 是 10 为基数的 10,F 是 10 为基数的 15:

    unsigned long long every_other = 0xAAAAAAAAAAAAAAAA; 
    unsigned long long each_other  = 0x5555555555555555; 
    cout << hex << showbase << uppercase; 
    cout << every_other << endl; 
    cout << each_other  << endl;

在此代码中,为两个 64 位整数(在 Visual C++ 中)分配了位图值,每隔一位设置为 1。第一个变量从底位设置开始,第二个变量从未设置的底位开始,第二个变量从设置的第二位开始。 在插入数字之前,使用三个操纵器修改流。 第一个hex表示应在控制台上将整数打印为十六进制,showbase表示将打印前导0x。 默认情况下,字母数字(A 到 F)将以小写形式提供,要指定必须使用大写字母,请使用uppercase。 修改流后,该设置将一直保留,直到更改。 要随后将流更改为对字母十六进制数字使用小写,将nouppercase插入到流中,并打印不带基数的数字,请插入noshowbase操纵器。 要使用八进制数字,请插入oct操纵器,要使用小数,请插入dec操纵器。

当您指定这样的大数字时,很难看到您是否指定了正确的位数。 您可以使用单引号(')将数字组合在一起:

    unsigned long long every_other = 0xAAAA'AAAA'AAAA'AAAA; 
    int billion = 1'000'000'000;

编译器会忽略引号;它只是一种直观的辅助工具。 在第一个示例中,引号将数字分组为两个字节组;在第二个示例中,引号将十进制数分组为千位和百万位。

使用位集显示位模式

没有操纵器可以告诉cout对象将整数打印为位图,但您可以使用bitset对象模拟该行为:

    // #include <bitset> 
    unsigned long long every_other = 0xAAAAAAAAAAAAAAAA; 
    unsigned long long each_other  = 0x5555555555555555; 
    bitset<64> bs_every(every_other); 
    bitset<64> bs_each(each_other); 
    cout << bs_every << endl; 
    cout << bs_each << endl;

结果是:

    1010101010101010101010101010101010101010101010101010101010101010    
    0101010101010101010101010101010101010101010101010101010101010101

这里的bitset类是参数化的,表示您通过尖括号(<>)提供一个参数,在本例中使用 64,表示bitset对象可以容纳 64 位。 在这两种情况下,bitset对象的初始化都是使用看起来像函数调用的语法执行的(实际上,它确实调用了一个称为构造函数的函数),这是初始化对象的首选方式。 将bitset对象插入流中,打印出从最高位开始的每一位。 (原因是定义了一个operator <<函数,它接受一个bitset对象,就像大多数标准库类一样)。

作为使用位运算符的替代方法,bitset类对于访问和设置单个位很有用:

    bs_every.set(0); 
    every_other = bs_every.to_ullong(); 
    cout << bs_every << endl; 
    cout << every_other << endl;

set函数将指定位置的位设置为值 1。to_ullong函数将返回bitset表示的long long数字。

set函数的调用和赋值具有相同的结果,如下所示:

    every_other |= 0x0000000000000001;

确定整数字节顺序

整数中的字节顺序取决于实现;它取决于处理器处理整数的方式。 在大多数情况下,您不需要知道。 但是,如果您以二进制模式从文件读取字节,或从网络流读取字节,并且需要将两个或更多字节解释为整数的一部分,则需要知道它们的顺序,并在必要时将它们转换为处理器可识别的顺序。

C 网络库(在 Windows 上称为Winsock库)包含一个函数集合,用于将unsigned shortunsigned long类型从网络顺序转换为主机顺序(即当前计算机上处理器使用的顺序),反之亦然。 网络订单是大端的。 大端表示第一个字节将是整数中的最高字节,而小端表示第一个字节是最小字节。 将整数传输到另一台计算机时,首先将源计算机的处理器使用的顺序(主机顺序)转换为网络顺序,接收机在使用数据之前将整数从网络顺序转换为接收机的主机顺序。

改变字节顺序的功能是ntohsntohl;用于将unsigned shortunsigned long从网络命令转换为主机命令的函数,以及用于从主机命令转换为网络命令的函数htonshtonl。 当您在调试代码时查看内存时,了解字节顺序将非常重要(例如,如第 10 章诊断和调试中所述)。

很容易编写代码来颠倒字节顺序:

    unsigned short reverse(unsigned short us)  
    { 
        return ((us & 0xff) << 8) | ((us & 0xff00) >> 8); 
    }

这使用按位运算符将假定组成unsigned short的两个字节分隔成左移 8 位的低位字节和右移 8 位的高位字节,并使用按位 OR 运算符|将这两个数字重新组合为unsigned short。 为 4 字节和 8 字节整数编写此函数的版本非常简单。

浮点类型

有三种基本的浮点类型:

  • float(单精度)
  • double(双精度)
  • long double(扩展精度)

所有这些都是签字的。 内存中数字的实际格式和使用的字节数特定于 C++ 实现,但<cfloat>头文件给出了范围。 下表列出了 x86 32 位处理器上使用的正范围和字节数:

| 类型 | 范围 | 字节大小 | | 漂浮 / 浮动 / 使漂浮 / 实行 | 1.175494351e-38 至 3.402823466e+38 | 4. | | 两倍物 / 成对物 / 两倍 / 双精度型 | 2.2250738585072014e-308 至 1.7976931348623158e+308 | 8 个 | | 长双倍 | 2.2250738585072014e-308 至 1.7976931348623158e+308 | 8 个 |

如您所见,在 Visual C++ 中,doublelong double具有相同的范围,但它们仍然是两个截然不同的类型。

指定浮点文字

用于初始化double的文字通过使用科学格式或简单地提供小数点来指定为浮点:

    double one = 1.0; 
    double two = 2.; 
    double one_million = 1e6;

第一个示例指示变量one被赋给浮点值 1.0。 尾随零并不重要,如第二个变量two所示;但是,尾随零确实使代码更具可读性,因为句点很容易被忽略。 第三个例子使用科学记数法。 第一部分是尾数,可以签名,e后面的部分是指数。 指数是数字的 10 次方(可以是负数)。 将变量赋给尾数乘以 10 的值,并将其提升到指数。 虽然不建议这样做,但您可以编写以下内容:

    double one = 0.0001e4; 
    double one_billion = 1000e6;

编译器将适当地解释这些数字。 第一个示例不合常理,但第二个示例有一定的意义;它在您的代码中显示 10 亿就是 10 亿。

这些示例将双精度浮点值赋给double变量。 要为单精度变量指定值以便可以分配float变量,请使用f(或F)后缀。 同样,对于long double文字,请使用l(或L)后缀:

    float one = 1.f; 
    float two = 2f; // error 
    long double one_million = 1e6L;

如果您使用这些后缀,您仍然需要以正确的格式提供数字。 文字2f不正确;您必须提供小数点2.f。 当您指定具有大量数字的浮点数时,可以使用单引号(')对数字进行分组。 如前所述,这只是对程序员的视觉帮助:

    double one_billion = 1'000'000'000.;

字符和字符串

string类和 C 字符串函数将在使用字符串中介绍;本节介绍代码中字符变量的基本用法。

字符类型

char类型是整数,所以也存在signed charunsigned char。 这是三种不同的类型;signed charunsigned char类型应该被视为数值类型。 char类型用于保存实现的字符集中的单个字符。 在 Visual C++ 中,这是一个 8 位整数,可以容纳 ISO-8859 或 UTF-8 字符集的字符。 这些字符集能够表示英语和大多数欧洲语言中使用的字符。 来自其他语言的字符占用多于一个字节,C++ 提供char16_t类型来保存 16 位字符,char32_t类型来保存 32 位字符。

还有一种名为wchar_t(宽字符)的类型,可以保存最大扩展字符集中的字符。 通常,当您看到带有w前缀的 C 运行时库或 C++ 标准库函数时,它将使用宽字符串,而不是char字符串。 因此,cout对象将允许您插入char字符串,wcout对象将允许您插入宽字符串。

C++ 标准规定,char函数中的每一位都用来保存字符信息,因此是否可以将char视为可以保存负数取决于实现。 以下内容说明了这一点:

    char c = '~'; 
    cout << c << " " << (signed short)c << endl; 
    c += 2; 
    cout << c << " " << (signed short)c << endl;

signed char的范围是-128 到 127,但此代码使用单独的类型char,并尝试以相同的方式使用它。 首先将变量c赋给 ASCII 字符~(126)。 当您将字符插入到输出流中时,它将尝试打印字符而不是数字,因此下一行将此字符打印到控制台,为了获得数字值,代码将变量转换为signed short整数。 (同样,为了清晰起见,使用了 C 造型。)。 接下来,变量递增 2,也就是说,字符在字符集中又增加了两个字符,这意味着扩展 ASCII 字符集中的第一个字符;结果如下所示:

    ~ 126
    C -128

扩展字符集中的第一个字符是 C-cedilla。

很不直观的是,126 的值加上两个结果就是-128,这是由带符号类型的溢出计算产生的。 即使这是故意的,也最好避免这样做。

在 Visual C++ 中,C-cedilla 字符被视为-128,因此您可以编写以下代码以达到相同的效果:

    char c = -128;

这是特定于实现的,因此对于可移植代码,您不应该依赖它。

使用字符宏

<cctype>头包含各种宏,您可以使用这些宏检查 achar包含的字符类型。 这些是在<ctype.h>中声明的 C 运行时宏。 下表介绍了一些用于测试字符值的更有用的宏。 请记住,因为这些是 C 例程,所以它们不会返回bool值;相反,它们返回一个int,对于true返回值为非零值,对于false返回值为零。

| 发文:2013-06-04 晚上 9:00 | 测试字符是否为: | | isalnum | 字母数字字符,从 A 到 Z,从 a 到 z,从 0 到 9 | | isalpha | 字母字符,从 A 到 Z,从 a 到 z | | isascii | ASCII 字符,0x00 到 0x7f | | isblank | 空格或水平制表符 | | iscntrl | 控制字符,0x00 到 0x1f 或 0x7f | | isdigit | 十进制数字 0 到 9 | | isgraph | 空格以外的可打印字符,0x21 到 0x7e | | islower | 小写字符,a 到 z | | isprint | 可打印字符,0x20 到 0x7e | | ispunct | 标点符号,! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ { | } ~ ` | | isspace | 一个空格 | | isupper | 大写字符,从 A 到 Z | | isxdigit | 十六进制数字,0 到 9,a 到 f,A 到 F |

例如,下面的代码在从输入流中读取单个字符时循环(在每个字符之后,您需要按Enter键)。 当提供非数字值时,循环结束:

    char c; 
    do 
    { 
       cin >> c 
    } while(isdigit(c));

还可以使用宏来更改字符。 同样,这些函数将返回int值,您应该将其转换为char

| 发文:2013-06-04 晚上 9:00 | 返回 | | toupper | 字符的大写版本 | | tolower | 字符的小写版本 |

在以下代码中,将回显在控制台键入的字符,直到用户键入qQ。 如果键入的字符是小写字符,则回显的字符将转换为大写:

    char c; 
    do 
    { 
        cin >> c; 
        if (islower(c)) c = toupper(c); 
        cout << c << endl; 
    } while (c != 'Q');

指定字符文字

您可以使用原义字符初始化char变量。 这将是支持的字符集中的字符。 ASCII 字符集包括一些无法打印的字符,为了使用这些字符,C++ 提供了两个使用反斜杠字符(\)的字符序列。

| 名称 | 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | C++ 序列 | | 纽兰。 | 低频率 / 同 low frequency | \n | | 水平选项卡 | 高电压 / 同 high tension . | \t | | 垂直选项卡 | 佛蒙特州 / 同 Vermont | \v | | 退格键 | 英国标准 / 理学士 / 外科学士 / 圣礼 | \b | | 归还 / 返回 / 回车 / 同 return | 复活社 / 哥斯达黎加 | \r | | 换页 | 极强的 / 非常嘹亮的 | \f | | 提醒 / 警告 / 使警觉,使意识到 | 丰饶之神(古代腓尼基人和迦南人信奉的主神) | \a | | 反斜杠 | \ | \\ | | 问号 / 疑问 | ? | \? | | 女性单人配额 | ‘ | \' | | 双配额双配额双配额 | “ | \" |

此外,您还可以将该字符的数值指定为八进制或十六进制数字。 要提供一个八进制数字,您可以将数字指定为三个字符(如果需要,可以加上一个或两个0字符作为前缀),并以反斜杠为前缀。 对于十六进制数,您可以在其前面加上\x。 字符M是十进制的字符数字 77、八进制的 115 和十六进制的 4d,因此可以通过三种方式使用M字符初始化字符变量:

    char m1 = 'M'; 
    char m2 = '\115'; 
    char m3 = '\x4d';

为完整起见,值得指出的是,您可以将字符初始化为整数,因此以下代码也会将每个变量初始化为M字符:

    char m4 = 0115; // octal 
    char m5 = 0x4d; // hexadecimal

所有这些方法都是有效的。

指定字符串文字

字符串由一个或多个字符组成,您也可以在字符串文字中使用转义字符:

    cout << "This is \x43\x2b\05\3n";

这个相当难读的字符串将在控制台上打印为This is C++ ,后跟一个换行符。 大写 C 是十六进制的 43,+符号是十六进制的 2b 和八进制的 53。 \n字符是换行符。 转义字符对于打印不在 C++ 编译器使用的字符集中的字符以及某些不可打印的字符(例如,按\t插入水平制表符)很有用。 cout对象在将字符写入输出流之前缓冲字符。 如果使用\n作为换行符,则会将其视为缓冲区中的任何其他字符。 endl操纵器会将\n插入缓冲区,然后将其刷新,因此字符会立即写入控制台。

NULL字符是\0。 这是一个重要的字符,因为它是不可打印的,除了标记字符串中字符序列的结尾外,它没有任何用处。 空字符串是"",但是因为字符串是由NULL字符分隔的,所以用空字符串初始化的字符串变量占用的内存将有一个字符,即\0

换行符允许您在字符串中放入换行符。 如果您要执行的唯一格式设置是段落,并且打印的是短段落,则此功能非常有用:

    cout << "Mary had a little lamb,n its fleece was white as snow."  
         << endl;

这将在控制台上打印两行:

 Mary had a little lamb,
 its fleece was white as snow.

但是,您可能希望使用长字符序列来初始化字符串,并且您使用的编辑器的限制可能意味着您希望将字符串拆分成几行。 为此,您可以将字符串的每个片段放在双引号内:

    cout << "And everywhere that Mary went, " 
            "the lamb was sure to go."  
         << endl;

您将在控制台上看到以下内容:

 And everywhere that Mary went, the lamb was sure to go.

除了在末尾用endl明确要求的行外,没有打印任何换行符。 此语法允许您在代码中使长字符串更具可读性;当然,您可以在此类字符串中使用换行符\n

Unicode 文字

也可以用字符初始化wchar_t变量,编译器将通过使用字符的字节并将剩余的(较高)字节赋值为零来将字符提升为宽字符。 但是,使用宽字符为这样的变量赋值更有意义,您可以使用L前缀来实现这一点。

    wchar_t dollar = L'$'; 
    wchar_t euro = L'\u20a0'; 
    wcout << dollar;

请注意,此代码使用的不是cout对象,而是宽字符版本wcout。 在引号中使用\u前缀的语法表示下面的字符是 Unicode 字符。

请记住,要显示 Unicode 字符,您需要使用将显示 Unicode 字符的控制台,并且在默认情况下,Windows 控制台设置为代码页 850,它不会显示 Unicode 字符。 您可以通过在标准输出流stdout上调用_setmode(在<io.h>中定义),指定 UTF-16 文件模式(使用在<fcntl.h>中定义的_O_U16TEXT)来更改输出控制台的模式:

    _setmode(_fileno(stdout), _O_U16TEXT);

You can find a list of all of the characters supported by Unicode at http://unicode.org/charts/.

也可以将 UTF-16 字符分配给char16_t变量,将 UTF-32 字符分配给char32_t变量。

原始字符串

当您使用原始字符串文字时,您基本上关闭了转义字符的含义。 您在原始字符串中键入的任何内容都将成为其内容,即使您使用包含换行符的空格。 原始字符串用R"()"分隔。 也就是说,字符串位于内括号之间。

    cout << R"(newline is \n in C++ and "quoted text" use quotes)";

请注意,()是语法的一部分,不是字符串的一部分。 前面的代码将以下内容打印到控制台:

 newline is \n in C++ and "quoted text" use quotes

通常,字符串中的\n是转义字符,将被翻译为换行符,但在原始字符串中,它不会被翻译,而是被打印为两个字符。

在普通的 C++ 字符串中,您必须转义某些字符;例如,双引号必须转义为\",反斜杠转义为\\。 在不使用原始字符串的情况下,下面的结果是相同的:

    cout << "newline is \\n in C++ and \"quoted text\" use quotes";

您还可以在原始字符串中包含换行符:

    cout << R"(Mary had a little lamb,  
                             its fleece was white as snow)" 
    cout << endl;

在此代码中,逗号后的换行符将打印到控制台。 遗憾的是,控制台上将打印所有空格,因此假设在前面的代码中缩进为三个空格,而cout缩进一次,您将在控制台上看到以下内容:

 Mary had a little lamb,
 its fleece was white as snow

its前面有 14 个空格,因为在源代码中,its前面有 14 个空格。 因此,您应该谨慎使用原始字符串。

也许,原始字符串的最佳用法是在 Windows 上使用文件路径初始化变量。 Windows 中的文件夹分隔符是反斜杠,这意味着对于表示文件路径的文字字符串,您必须对每个分隔符进行转义;因此,该字符串将有很多双反斜杠,可能会缺少一个。 对于原始字符串,这种转义不是必需的。 下面的两个字符串变量表示相同的字符串:

    string path1 = "C:\\Beginning_C++ \\Chapter_03\\readme.txt"; 
    string path2 = R"(C:\Beginning_C++ \Chapter_03\readme.txt)";

这两个字符串具有相同的内容,但第二个字符串的可读性更好,因为 C++ 文字字符串没有转义反斜杠。

只有在代码中声明的文字字符串才需要转义反斜杠的要求;它是编译器如何解释字符的指示。 如果从函数(或通过argv[0])获取文件路径,分隔符将是反斜杠。

字符串字节顺序

扩展字符集使用每个字符一个以上的字节。 如果这些字符存储在文件中,字节的顺序就变得很重要。 在这种情况下,角色的作者必须使用与潜在读者将使用的顺序相同的顺序。

一种方法是使用字节顺序标记(BOM)。 这是具有已知模式的已知字节数,通常作为流中的第一项放置,以便流的读取器可以使用它来确定流中其余字符的字节顺序。 Unicode 将 16 位字符\uFEFF和非字符\uFFFE定义为字节顺序标记。 在\uFEFF的情况下,除位 8 以外的所有位都被设置(如果最低位被标记为位 0)。 此 BOM 可以作为机器之间传递的数据的前缀。 目标机器可以将 BOM 读入 16 位变量并测试这些位。 如果位 8 为零,则意味着两台机器具有相同的字节顺序,因此可以按照流中的顺序将字符读取为两个字节值。 如果位 0 为零,则意味着目标计算机以与源相反的顺序读取 16 位变量,因此必须采取措施确保以正确的顺序读取具有 16 位字符的字节。

Unicode 字节顺序标记(BOM)按如下方式序列化(十六进制):

| 字符集 | 字节顺序标记 | | UTF-8 | 电炉 BB 高炉 | | UTF-16 高位序 | FE FF | | UTF-16 小端字节序 | FF FE | | UTF-32 高位序 | 00 00 FE FF | | UTF-32 小端 | FF FE 00 00 |

请记住,当您从文件中读取数据时。 字符序列 FE FF 在非 Unicode 文件中非常少见,因此如果您将它们读作文件中的前两个字节,则意味着该文件是 Unicode 文件。 由于\uFEFF\uFFFE不是可打印的 Unicode 字符,这意味着以这两个字符中的任何一个开头的文件都有字节顺序标记,然后您可以使用 BOM 来确定如何解释文件中的其余字节。

布尔代数学(或逻辑)体系的

bool类型保存一个布尔值,即只包含以下两个值之一:truefalse。 C++ 允许您将 0(零)视为false,将任何非零值视为true,但这可能会导致错误,因此最好养成显式检查值的习惯:

    int use_pointer(int *p) 
    { 
        if (p)            { /* not a null pointer */ } 
        if (p != nullptr) { /* not a null pointer */ }   
        return 0; 
    }

这两个中的第二个更可取,因为它更清楚您要比较的是什么。 请注意,即使指针不是nullptr,它也可能不是有效的指针,但通常的做法是将指针赋给nullptr以传达一些其他含义,也许是说指针操作不合适。

您可以将布尔值插入到输出流中。 但是,默认行为是将布尔值视为整数。 如果希望cout输出带有字符串名的bool值,则将操纵器boolalpha插入到流中;这将使流打印truefalse到控制台。 默认行为可以通过使用noboolalpha操纵器来实现。

无效的 / 空的 / 空无所有的 / 缺门的

在某些情况下,需要指明函数没有参数或不返回值;在这两种情况下,都可以使用关键字void

    void print_message(void) 
    { 
        cout << "no inputs, no return value" << endl; 
    }

在参数列表中使用void是可选的;可以使用空的一对圆括号,最好是这样。 这是指示函数除了返回void之外不返回值的唯一方式。

请注意,void不是真正的类型,因为您不能创建void变量;它是缺少类型。 您将在下一章中了解到,您可以创建类型为void的指针,但如果不强制转换为类型化指针,您将无法使用此类指针所指向的内存:要使用内存,您必须决定内存保存的数据的类型。

初始化器

初始化器在上一章中已经提到,但我们将在这里更深入地讨论。 对于内置类型,必须在使用变量之前对其进行初始化。 对于自定义类型,类型可以定义默认值,但这样做会出现一些问题,这将在第 6 章中介绍。

在所有版本的 C++ 中,有三种方法初始化内置类型:赋值、函数语法或调用构造函数。 在 C++ 11 中引入了另一种初始化变量的方法:通过列表初始化器进行构造。 这四种方式如下所示:

    int i = 1; 
    int j = int(2); 
    int k(3); 
    int m{4};

这三个中的第一个是最清楚的;它使用易于理解的语法显示变量正在被初始化为一个值。 第二个示例通过像调用函数一样调用类型来初始化变量。 第三个示例调用int类型的构造函数。 这是初始化自定义类型的典型方式,因此最好只为自定义类型保留此语法。

第四种语法是 C++ 11 中的新语法,它使用花括号({})之间的初始化列表来初始化变量。 稍微混淆一下,您还可以使用与赋给单个项目列表相同的语法来初始化内置类型:

    int n = { 5 };

这真是令人困惑,类型n是整数,而不是数组。 回想一下,在上一章中,我们创建了一个包含披头士出生日期的数组:

    int birth_years[] = { 1940, 1942, 1943, 1940 };

这将创建一个由四个整数组成的数组;每一项的类型为int,但数组变量的类型为int*。 该变量指向保存四个整数的内存。 同样,您也可以将变量初始化为一项的数组:

    int john[] = { 1940 };

这与 C++ 11 允许初始化单个整数的初始化代码完全相同。 此外,使用相同的语法来初始化记录类型(structs)的实例,增加了对语法含义的另一层潜在混淆。

最好避免使用花括号语法进行变量初始化,而只将其用于初始化列表。 但是,这种用于强制转换的语法有一些优点,稍后将对此进行说明。

大括号语法可用于为 C++ 标准库中的任何集合类以及 C++ 数组提供初始值。 即使在用于初始化集合对象时,也存在混淆的可能性。 例如,考虑vector集合类。 它可以容纳通过一对尖括号(<>)提供的类型的集合。 此类对象的容量可以随着向该对象添加更多项目而增加,但您可以通过指定初始容量来优化其使用:

    vector<int> a1 (42); 
    cout << " size " << a1.size() << endl; 
    for (int i : a1) cout << i << endl;

这段代码的第一行是:创建一个可以容纳整数的vector对象,并从为 42 个整数预留空间开始,每个整数都被初始化为零值。 第二行将向量的大小打印到控制台(42),第三行将数组中的所有项打印到控制台,它将打印 42 个零。

现在考虑以下几点:

    vector<int> a2 {42}; 
    cout << " size " << a2.size() << endl; 
    for (int i : a2) cout << i << endl;

这里只有一个更改:圆括号已更改为大括号,但这意味着初始化已完全更改。 第一行现在的意思是:创建一个可以容纳整数的vector,并用单个整数 42 对其进行初始化。 a2的大小为 1,最后一行将仅打印一个值 42。

C++ 的强大之处在于,它应该很容易编写正确的代码,并说服编译器帮助您避免错误。 使用大括号进行单项初始化增加了很难找到错误的可能性。

默认值

内置类型的变量应在首次使用之前进行初始化,但在某些情况下,编译器会提供默认值。

如果在文件范围内或在项目中全局声明变量,并且没有为其提供初始值,则编译器将为其提供默认值。 例如:

    int outside; 

    int main() 
    { 
        outside++ ; 
        cout << outside << endl; 
    }

此代码将编译并运行,并打印值 1;编译器已将outside初始化为 0,然后将其递增为 1。以下代码将不会编译:

    int main() 
    { 
        int inside; 
        inside++ ; 
        cout << inside << endl; 
    }

编译器将报告递增运算符正在未初始化的变量上使用。

在上一章中,我们看到了编译器提供默认值static的另一个示例。

    int counter() 
    { 
        static int count; 
        return ++ count; 
    }

这是一个维护计数的简单函数。 变量countstatic存储类修饰符标记,这意味着该变量与应用具有相同的生存期(在代码启动时分配,在程序结束时释放);但是,它有内部链接,这意味着该变量只能在声明它的范围内使用,即counter函数。 编译器将用默认值 0 来初始化count变量,以便在第一次调用counter函数时返回值 1。

C++ 11 的新初始化列表语法为您提供了一种声明变量并指定希望由编译器将其初始化为该类型的默认值的方法:

    int a {};

当然,在阅读这段代码时,您必须知道int的默认值是什么(它是零)。 同样,简单地将变量初始化为一个值要容易得多,也更显式:

    int a = 0;

默认值的规则很简单:值为零。 整数和浮点数的默认值为 0,字符的默认值为\0bool的默认值为false,,指针的默认值为常量nullptr

没有类型的声明

C++ 11 引入了一种机制,用于声明一个变量的类型应该根据它被初始化的数据来确定,也就是说,它是auto

这里有一点混淆,因为在 C++ 11 之前,auto键用于声明自动变量,即在函数中自动分配到堆栈上的变量。 除了在文件作用域声明的变量或声明为static的变量外,本书中到目前为止所有其他变量都是自动变量,自动变量是使用最广泛的存储类(稍后解释)。 由于auto关键字是可选的且适用于大多数变量,因此在 C++ 中很少使用,因此 C++ 11 利用了这一点,去掉了旧的含义,并赋予了auto新的含义。

如果使用 C++ 11 编译器编译旧 C++ 代码,而旧代码使用auto,则会出现错误,因为新编译器将假定auto将与未指定类型的变量一起使用。 如果发生这种情况,只需搜索并删除auto的每个实例;它在 C++ 11 之前的 C++ 中是多余的,开发人员几乎没有理由使用它。

关键字auto表示编译器应该使用分配给它的数据类型创建一个变量。 变量只能有一个类型,编译器决定的类型是分配给它的数据所需的类型,并且您不能在其他地方使用该变量来保存不同类型的数据。 因为编译器需要从初始值设定项确定类型,所以这意味着所有auto变量都必须初始化:

    auto i  = 42;    // int 
    auto l  = 42l;   // long 
    auto ll = 42ll;  // long long 
    auto f  = 1.0f;  // float 
    auto d  = 1.0;   // double 
    auto c  = 'q';   // char 
    auto b  = true;  // bool

请注意,没有语法指定整数值是单字节还是双字节,因此不能以这种方式创建unsigned char变量或short变量。

这是对auto关键字的简单使用,您不应该以这种方式使用它。 AUTO 的强大之处在于,当您使用容器时,可能会产生一些外观相当复杂的类型:

    // #include <string> 
    // #include <vector> 
    // #include <tuple> 

    vector<tuple<string, int> > beatles; 
    beatles.push_back(make_tuple("John", 1940)); 
    beatles.push_back(make_tuple("Paul", 1942)); 
    beatles.push_back(make_tuple("George", 1943)); 
    beatles.push_back(make_tuple("Ringo", 1940)); 

    for (tuple<string, int> musician : beatles) 
    { 
        cout << get<0>(musician) << " " << get<1>(musician) << endl; 
    }

此代码使用我们以前使用过的vector容器,但它使用tuple存储两个值项。 tuple类很简单;您可以在尖括号之间的声明中声明tuple对象中的项类型列表。 因此,tuple<string, int>声明说明该对象将按该顺序保存一个字符串和一个整数。 make_tuple函数由 C++ 标准库提供,将创建包含这两个值的tuple对象。 函数push_back将项目放入向量容器。 在四次调用push_back函数之后,beatles变量将包含四个项目,每个项目都是一个带有姓名和出生年份的tuple

范围for遍历容器,并在每个循环中将musician变量赋给容器中的下一项。 tuple中的值在for循环中的语句中打印到控制台。 使用get参数化函数(来自<tuple>)访问tuple中的项,其中尖括号中的参数表示要从圆括号中作为参数传递的tuple对象中获取的项的索引(从零开始索引)。 在本例中,对get<0>的调用获取打印出来的名称,然后是一个空格,然后get<1>获取tuple中的年份项。 此代码的结果是:

    John 1940 
    Paul 1942 
    George 1943 
    Ringo 1940

此文本的格式较差,因为它没有考虑名称的长度。 这可以通过使用字符串在中解释的操纵器来解决。

再看一看for循环:

    for (tuple<string, int> musician : beatles) 
    { 
        cout << get<0>(musician) << " " << get<1>(musician) << endl; 
    }

音乐家的类型是tuple<string, int>;,这是一个相当简单的类型,当您更多地使用标准模板时,可能会得到一些复杂的类型(特别是当您使用迭代器时)。 这就是auto变得有用的地方。 以下代码相同,但更易于阅读:

    for (auto musician : beatles) 
    { 
        cout << get<0>(musician) << " " << get<1>(musician) << endl; 
    }

MUSIC 变量仍然是类型化的,它是一个tuple<string, int>,但是auto意味着您不必显式地对其进行编码。

存储类

在声明变量时,您可以指定它的存储类,该存储类指示变量的生存期、链接(哪些其他代码可以访问它)和内存位置。

您已经看到了一个存储类static,当它应用于函数中的变量时,意味着该变量只能在该函数内访问,但其生存期与程序相同。 但是,static可以用于在文件作用域声明的变量,在这种情况下,它指示变量只能在当前文件中使用,这称为内部链接。 如果在文件范围内定义的变量上省略了关键字*static,则它有一个外部链接,,这意味着该变量的名称对其他文件中的代码是可见的。 关键字static可以用在类的数据成员上,也可以用在类上定义的方法上,这两个关键字都有有趣的效果,将在第 6 章中描述。

关键字static表示该变量只能在当前文件中使用。 关键字extern则相反;变量(或函数)具有外部链接,可以在项目中的其他文件中访问。 在大多数情况下,您将在一个源文件中定义一个变量,然后在头文件中将其声明为extern,以便同一变量可以在其他源文件中使用。

最终的存储类说明符是thread_local。 这是 C++ 11 的新特性,仅适用于多线程代码。 本书不涉及线程化,因此这里只作简要说明。

线程是执行和并发的单位。 一个程序中可以有多个线程运行,也可以有两个或多个线程同时运行相同的代码。 这意味着两个不同的执行线程可以访问和更改同一变量。 由于并发访问可能会产生不良影响,因此多线程代码通常需要采取措施来确保任何时候只有一个线程可以访问数据。 如果不小心编写这样的代码,就会有死锁的危险,线程的执行会因为独占访问变量而暂停(在最坏的情况下是无限期的),从而抵消了使用线程的好处。

thread_local存储类表示每个线程都有自己的变量副本。 因此,如果两个线程访问同一个函数,并且该函数中的一个变量被标记为thread_local,,这意味着每个线程只看到它所做的更改。

您有时会看到旧 C++ 代码中使用的存储类register。 这一点现在已弃用。 它被用来提示编译器该变量对程序的性能有重要影响,并建议编译器如果可能的话,应该使用 CPU 寄存器来保存该变量。 编译器可以忽略此建议。 事实上,在 C++ 11 中,编译器确实忽略了关键字;带有register变量的代码编译时不会出现错误或警告,编译器会根据需要优化代码。

尽管volatile关键字不是存储类说明符,但它对编译器代码优化有影响。 关键字volatile表示变量(可能通过对某些硬件的直接内存访问(DMA)可以通过外部操作进行更改,因此编译器不应用任何优化非常重要。

还有另一个名为mutable的存储类修饰符。 这只能用于类成员,因此将在第 6 章中介绍。

使用类型别名

有时,类型的名称可能会变得相当繁琐。 如果使用嵌套命名空间,则类型的名称包括使用的所有命名空间。 如果定义参数化类型(本章到目前为止使用的示例是vectortuple),则参数会增加类型的名称。 例如,前面我们看到一个容器,里面放着音乐家的名字和出生年份:

    // #include <string> 
    // #include <vector> 
    // #include <tuple> 

    vector<tuple<string, int> > beatles;

这里,容器是一个vector,它保存的项是tuple项,每个项都包含一个字符串和一个整数。 要使该类型更易于使用,您可以定义一个预处理器符号:

    #define name_year tuple<string, int>

现在,您可以在代码中使用name_year而不是tuple,并且预处理器将在编译代码之前将符号替换为该类型:

    vector<name_year> beatles;

但是,因为#define是一个简单的搜索和替换,所以可能会出现本书前面解释的问题。 C++ 提供typedef语句为类型创建别名:

    typedef tuple<string, int> name_year_t; 
    vector<name_year_t> beatles;

这里,为tuple<string, int>创建了一个名为name_year_t的别名。

对于typedef,,别名通常出现在行尾,前面是它的别名类型。 这与#define,的顺序相反,在#define,中,您要定义的符号在#define之后,然后是其定义。 另请注意,typedef以分号结尾。 使用函数指针会变得复杂得多,正如您将在第 5 章使用函数中看到的那样。

现在,无论您想在哪里使用tuple,都可以使用别名:

    for (name_year_t musician : beatles) 
    { 
        cout << get<0>(musician) << " " << get<1>(musician) << endl; 
    }

您可以typedef别名:

    typedef tuple<string, int> name_year_t; 
    typedef vector<name_year_t> musician_collection_t; 
    musician_collection_t beatles2;

beatles2变量的类型为vector<tuple<string, int>>。 需要注意的是,typedef会创建别名;它不会创建新类型,因此您可以在原始类型及其别名之间切换。

关键字typedef是在 C++ 中创建别名的成熟方法。

C++ 11 引入了另一种创建类型别名的方法,即using语句:

    using name_year = tuple<string, int>;

同样,这不会创建新类型,它会为相同类型创建一个新名称,并且在语义上与typedef相同。 using语法比使用typedef更具可读性,而且它还允许您使用模板。

创建别名的using方法比typedef更具可读性,因为赋值的使用遵循变量的约定,即左边的新名称用于=右边的类型。

在记录类型中聚合数据

通常,您将拥有相关且必须一起使用的数据:聚合类型。 这样的记录类型允许您将数据封装到单个变量中。 C++ 继承了 Cstructunion,作为提供记录的方式。

构筑物

在大多数应用中,您需要将几个数据项关联在一起。 例如,您可能希望定义一个时间记录,该时间记录包含以下各项的整数:指定时间的小时、分钟和秒。 您可以这样声明它们:

    // start work 
    int start_sec = 0; 
    int start_min = 30; 
    int start_hour = 8; 

    // end work 
    int end_sec = 0 
    int end_min = 0; 
    int end_hour = 17;

这种方法变得相当麻烦且容易出错。 没有封装,也就是说,_min变量可以与其他变量隔离使用。 如果在没有它所指的小时的情况下使用,小时之后的分钟是否有意义? 您可以定义与以下项目相关联的结构:

    struct time_of_day 
    { 
        int sec; 
        int min; 
        int hour; 
    };

现在,您已将这三个值作为一条记录的一部分,这意味着您可以声明此类型的变量;尽管您可以访问单个项,但很明显数据与其他成员相关联:

    time_of_day start_work; 
    start_work.sec = 0; 
    start_work.min = 30; 
    start_work.hour = 8; 

    time_of_day end_work; 
    end_work.sec = 0; 
    end_work.min = 0; 
    end_work.hour = 17; 

    print_time(start_work); 
    print_time(end_work);

现在我们有两个变量:一个表示开始时间,另一个表示结束时间。 struct的成员封装在struct中,也就是说,您可以通过struct的实例访问该成员。 为此,您可以使用点运算符。 在这段代码中,start_work.sec表示您正在访问名为start_worktime_of_day结构实例的sec成员。 默认情况下,结构的成员是public,也就是说,struct之外的代码可以访问这些成员。

Classes and structures can indicate the level of member access, and Chapter 6, Classes, will show how to do this. For example, it is possible to mark some members of a struct as private, which means that only code that is a member of the type can access the member.

调用名为print_time的助手函数将数据打印到控制台:

    void print_time(time_of_day time) 
    { 
        cout << setw(2) << setfill('0') << time.hour << ":"; 
        cout << setw(2) << setfill('0') << time.min << ":"; 
        cout << setw(2) << setfill('0') << time.sec << endl; 
    }

在这种情况下,setwsetfill操作符用于将下一个插入项的宽度设置为两个字符,并用零填充任何未填充的位置(更多详细信息将在第 9 章中使用字符串给出;实际上,setw给出了下一个插入数据占据的列的大小,setfill指定了使用的填充字符)。

第 5 章使用函数将更详细地介绍将结构传递给函数的机制和最有效的方法,但出于本节的目的,我们将在这里使用最简单的语法。 重要的是,调用者使用struct将三项数据关联在一起,并且所有项都可以作为一个单元传递给函数。

正在初始化

有几种方法可以初始化结构的实例。 前面的代码显示了一种方法:使用点运算符访问成员,并为其赋值。 还可以通过专门提供的称为构造函数的函数为struct的实例赋值。 由于有关于如何命名构造函数以及可以在其中做什么的特殊规则,因此这将留到第 6 章CLASS

还可以使用大括号({})使用列表初始化式语法来初始化结构。 大括号中的项应该按照声明的成员顺序匹配struct的成员。 如果提供的值少于成员的数量,则剩余的成员将初始化为零。 实际上,如果在花括号之间没有提供任何项,则所有成员都设置为零。 提供的初始值设定项多于成员数量是错误的。 因此,请使用前面定义的time_of_day记录类型:

    time_of_day lunch {0, 0, 13}; 
    time_of_day midnight {}; 
    time_of_day midnight_30 {0, 30};

在第一个示例中,将lunch变量初始化为 1 PM。 请注意,因为hour成员被声明为类型中的第三个成员,所以它是使用初始化列表中的第三项进行初始化的。 在第二个示例中,所有成员都设置为零,当然,零小时是午夜。 第三个示例提供了两个值,因此它们用于初始化secmin

您可以拥有struct的成员,该成员本身就是struct,这是使用嵌套大括号进行初始化的:

    struct working_hours 
    { 
        time_of_day start_work; 
        time_of_day end_work; 
    }; 

    working_hours weekday{ {0, 30, 8}, {0, 0, 17} }; 
    cout << "weekday:" << endl; 
    print_time(weekday.start_work); 
    print_time(weekday.end_work);

结构场

结构可以具有小到单个位的成员,称为位字段。 在本例中,您使用成员将占用的位数声明一个整数成员。 您可以声明未命名的成员。 例如,您可能有一个结构,其中包含有关项目长度以及项目是否已更改(脏)的信息。 此引用的项的最大大小为 1,023,因此您需要一个宽度至少为 10 位的整数来保存它。 您可以使用unsigned short来保存长度和脏信息:

    void print_item_data(unsigned short item) 
    { 
        unsigned short size = (item & 0x3ff); 
        char *dirty = (item > 0x7fff) ? "yes" : "no"; 

        cout << "length " << size << ", "; 
        cout << "is dirty: " << dirty << endl; 
    }

这段代码将这两段信息分开,然后将它们打印出来。 像这样的位图对代码非常不友好。 您可以使用 astruct保存此信息,使用 aunsigned short保存 10 位长度信息,使用 abool保存脏信息。 使用位域可以定义如下结构:

    struct item_length 
    { 
        unsigned short len : 10; 
        unsigned short : 5; 
        bool dirty : 1; 
    };

len成员被标记为unsigned short,但只需要 10 位,因此使用冒号语法来说明这一点。 类似地,布尔的 yes/no 值可以仅包含在一位中。 该结构表明这两个值之间有 5 位未使用,因此没有名称。

字段只是为了方便。 尽管看起来item_length结构应该只占用 16 位(unsigned short),但不能保证编译器会这样做。 如果您从文件或网络流接收到unsigned short,则必须自己解压比特:

    unsigned short us = get_length(); 
    item_length slen; 
    slen.len = us & 0x3ff; 
    slen.dirty = us > 0x7fff;

使用结构名称

在某些情况下,可能需要在实际定义类型之前使用该类型。 只要不使用成员,就可以在定义类型之前声明它:

    struct time_of_day; 
    void print_day(time_of_day time);

这可以在标题中声明,在标题中说明在其他地方定义了一个函数,该函数获取time_of_day记录并将其打印出来。 为了能够声明print_day函数,您必须声明time_of_day名称。 在定义函数之前,必须在代码中的其他位置定义time_of_day结构,否则将出现未定义类型错误。

但是,有一个例外:在完全声明类型之前,类型可以保存指向同一类型实例的指针。 这是因为编译器知道指针的大小,因此可以为成员分配足够的内存。 只有在定义了整个类型之后,才能创建该类型的实例。 这方面的经典例子是链表,但由于这需要使用指针和动态分配,因此将留待下一章讨论。

确定路线

结构的用途之一是,如果您知道数据是如何保存在内存中的,就可以将结构作为内存块来处理。 如果您有一个映射到内存的硬件设备,其中不同的内存位置指的是控制该设备的值或从该设备返回值,这将非常有用。 访问设备的一种方法是定义一个结构,该结构与设备对 C++ 类型的直接内存访问的内存布局相匹配。 此外,结构对于文件或需要通过网络传输的数据包也很有用:您可以操作结构,然后将结构占用的内存复制到文件或网络流中。

结构的成员按照它们在类型中声明的顺序在内存中排列。 根据每种类型的需要,这些项目将至少占用大小的内存。 一个成员可能会占用比类型要求更多的内存,其原因是一种称为对齐的机制。

*就内存使用或访问速度而言,编译器将以最高效的方式将变量放置在内存中。 各种类型将与路线边界对齐。 例如,32 位整数将与 4 字节边界对齐,如果下一个可用内存位置不在此边界上,编译器将跳过几个字节,并将该整数放在下一个对齐边界。 您可以使用传递类型名称的alignof运算符测试特定类型的对齐:

    cout << "alignment boundary for int is "  0
        << alignof(int) << endl;                     // 4 
    cout << "alignment boundary for double is "  
        << alignof(double) << endl;                  // 8

int的对齐方式为 4,这意味着将在内存中的下一个四字节边界放置一个int变量。 double的对齐方式是 8,这是有意义的,因为在 Visual C++ 中,adouble占用 8 个字节。 到目前为止,alignof的结果看起来与sizeof相同;但事实并非如此。

    cout << "alignment boundary for time_of_day is "  
        << alignof(time_of_day) << endl;             // 4

此示例打印time_of_day字符串结构的对齐方式,我们之前将其定义为三个整数。 此struct的对齐方式为 4,即struct中最大项的对齐方式。 这意味着time_of_day的一个实例将被放置在 4 字节边界上;它没有说明time_of_day变量中的项将如何对齐。

例如,考虑下面的struct,它有四个成员,分别占用一个、两个、四个和八个字节:

    struct test 
    { 
        uint8_t  uc; 
        uint16_t us; 
        uint32_t ui; 
        uint64_t ull; 
    }

编译器会告诉您对齐方式是 8(最大项的对齐方式,ull),但是大小是 16,这可能看起来有点奇怪。 如果每个项目都在 8 字节边界上对齐,则大小必须为 32(4 乘以 8)。 如果这些项存储在内存中并尽可能有效地打包,则大小将为 15。相反,发生的情况是第二个项在两个字节的边界上对齐,这意味着在ucus之间有一个字节的未使用空间。

如果您想要将内部项对齐到(比方说)与uint32_t变量使用的边界相同的边界上,您可以用alignas标记一个项并给出所需的对齐方式。 请注意,因为 8 大于 4,所以在 8 字节边界上对齐的任何项目也将在 4 字节边界上对齐:

    struct test 
    { 
        uint8_t  uc; 
        alignas(uint32_t) uint16_t us; 
        uint32_t ui; 
        uint64_t ull; 
    }

uc项将在 4 字节边界上对齐(alignof(test)将为 8),它将占用一个字节。 us成员是uint16_t,但它被标记为alignas(uint32_t),也就是说,它应该以与uint32_t相同的方式对齐,即在 4 字节边界上对齐。 这意味着ucus都将位于提供填充的 4 字节边界上。 当然,ui成员也将在 4 字节边界上对齐,因为它是uint32_t

如果struct只有这三个成员,那么大小应该是 12。但是,struct还有另一个成员,即 8 字节的ull成员。 这必须在 8 字节边界上对齐,这意味着从struct开始的 16 个字节,要做到这一点,在uiull之间需要有 4 个字节的填充。 因此,test的大小现在报告为 24:对于ucus是 4 字节(因为下面的项ui必须在下一个 4 字节边界上对齐),对于ull是 8 字节(因为它是 8 字节整数),对于ui是 8 字节,因为下面的项(ull)必须在下一个 8 字节边界上。

下图显示了test类型的各种成员在内存中的位置:

不能使用alignas放宽对齐要求,因此不能将uint64_t变量标记为在两字节边界(也不是八字节边界)上对齐。

在大多数情况下,您不需要担心对齐问题;但是,如果您要访问内存映射设备或来自文件的二进制数据,则如果您可以将此数据直接映射到struct会很方便,在这种情况下,您会发现您必须密切注意对齐。 这称为普通旧数据,您经常会看到称为POD 类型的结构。

POD is an informal description, and sometimes it is used to describe types that have a simple construction and do not have virtual members (see Chapter 6Classes and Chapter 7, Introduction to Object-Oriented Programming). The standard library provides a function in <type_traits> called is_pod that tests a type for these members.

使用联合将数据存储在同一内存中

联合是一个结构,其中所有成员占用相同的内存。 这种类型的大小是最大成员的大小。 由于联合只能保存一项数据,因此它是一种以多种方式解释数据的机制。

联合的一个例子是VARIANT类型,它用于在 Microsoft 的组件对象模型(COM)中的对象链接和嵌入(OLE)对象之间传递数据。 VARIANT类型可以保存 COM 能够在 OLE 对象之间传输的任何数据类型的数据。 有时 OLE 对象会在同一进程中,但它们可能在同一台计算机或不同计算机上的不同进程中。 COM 保证它可以在不需要开发者提供任何额外网络代码的情况下传输VARIANT数据。 结构很复杂,但编辑后的版本如下所示:

    // edited version 
    struct VARIANT 
    { 
        unsigned short vt; 
        union 
        { 
            unsigned char bVal; 
            short iVal; 
            long lVal; 
            long long llVal; 
            float fltVal; 
            double dblVal; 
       }; 
    };

请注意,您可以使用没有名称的联合:这是一个匿名的union,从成员访问的角度来看,您可以访问该联合的成员,就像它是包含它的VARIANT的成员一样。 对于可以在 OLE 对象之间传输的每种类型,union都包含一个成员,而vt成员则指示使用哪种类型。 创建VARIANT实例时,必须将vt设置为适当的值,然后初始化相关成员:

    enum VARENUM 
    { 
        VT_EMPTY = 0,  
        VT_NULL = 1,  
        VT_UI1 = 17,  
        VT_I2 = 2,  
        VT_I4 = 3,  
        VT_I8 = 20, 
        VT_R4 = 4,  
        VT_R8 = 5  
    };

该记录确保只使用所需的内存,并且将数据从一个进程传输到另一个进程的代码将能够读取vt成员,以确定需要如何处理数据以便可以传输:

    // pseudo code, real VARIANT should not be handled like this 
    VARIANT var {}; // clear all items 
    var.vt = VT_I4; // specify the type 
    var.lVal = 42;  // set the appropriate member 
    pass_to_object(var);

请注意,您必须遵守规则,并且只能初始化适当的成员。 当您的代码接收到VARIANT,时,您必须读取vt以查看应该使用哪个成员来访问数据。

通常,在使用联合时,您应该只访问您初始化的项目:

    union d_or_i {double d; long long i}; 
    d_or_i test; 
    test.i = 42; 
    cout << test.i << endl; // correct use 
    cout << test.d << endl; // nonsense printed

访问运行时类型信息

C++ 提供了一个名为typeid的运算符,它将在运行时返回有关变量(或类型)的类型信息。 运行时类型信息(RTTI)在您使用可以以多态方式使用的自定义类型时非常重要;详细信息将留待后面章节讨论。 RTTI 允许您在运行时检查变量的类型,并相应地处理变量。 RTTI 通过type_info对象(在<typeinfo>头文件中)返回:

    cout << "int type name: " << typeid(int).name() << endl; 
    int i = 42; 
    cout << "i type name: " << typeid(i).name() << endl;

在这两种情况下,您都会看到 int 被打印为文字。 type_info类定义比较运算符(==!=),以便您可以比较类型:

    auto a = i; 
    if (typeid(a) == typeid(int)) 
    { 
        cout << "we can treat a as an int" << endl; 
    }

确定类型限制

<limits>头包含一个名为numeric_limits的模板类,它通过为每个内置类型提供的专门化来使用。 使用这些类的方法是在尖括号中提供所需信息的类型,然后使用作用域解析操作符(::)调用类上的static成员。 (有关类的static函数的详细信息将在中给出)。 以下命令将int类型的限制打印到控制台:

    cout << "The int type can have values between "; 
    cout << numeric_limits<int>::min() << " and  "; 
    cout << numeric_limits<int>::max() << endl;

在类型之间转换

即使您非常努力地在代码中使用正确的类型,有时您也会发现必须在类型之间进行转换。 例如,您可能正在使用返回特定类型的值的库函数,或者您可能正在从与例程类型不同的外部源读取数据。

对于内置类型,有关于不同类型之间转换的标准规则,其中一些规则将是自动的。 例如,如果您有一个类似a + b的表达式,并且ab是不同的类型,那么,如果可能的话,编译器会自动将一个变量的值转换为另一个变量的类型,并调用该类型的+运算符。

在其他情况下,您可能需要强制将一种类型转换为另一种类型,以便调用正确的运算符,这将需要某种类型的强制转换。 C++ 允许您使用类似 C 的强制转换,但是这些类型没有运行时测试,所以使用 C++ 强制转换要好得多,它具有不同级别的运行时检查和类型安全。

类型转换

内置转换可能只有两种结果之一:提升或缩小。 升级是指将较小的类型升级为较大的类型,并且您不会丢失数据。 当将较大类型的值转换为可能会丢失数据的较小类型时,会发生缩小转换。

促进转换

在混合类型表达式中,编译器将尝试将较小的类型提升为较大的类型。 因此,可以在需要int的表达式中使用charshort,因为它可以升级为更大的类型,而不会丢失数据。

考虑一个声明为接受int参数的函数:

    void f(int i);

我们可以这样写:

    short s = 42; 
    f(s); // s is promoted to int

在这里,变量s被静默转换为int。 有些情况可能看起来很奇怪:

    bool b = true; 
    f(b); // b is promoted to int

同样,转换是沉默的。 编译器假设您知道自己在做什么,并且您的意图是希望将false视为 0,将true视为 1。

缩小转换范围

在某些情况下,会出现变窄。 请非常小心,因为它会丢失数据。 下面,我们尝试将double转换为int

    int i = 0.0;

这是允许的,但编译器将发出警告:

C4244: 'initializing': conversion from 'double' to 'int', possible loss of data

此代码显然是错误的,但该错误不是错误,因为它可能是故意的。 例如,在下面的代码中,我们有一个函数,其参数是浮点数,在例程中,该参数用于初始化int

    void calculation(double d) 
    { 
        // code 
        int i = d; 

        // use i 
        // other code 
    }

这可能是故意的,但是因为会损失精确度,所以您应该记录下为什么要这样做。 至少,使用强制转换操作符,这样您就可以很明显地理解操作的结果。

缩小到布尔值

如前所述,指针、整数和浮点值可以隐式转换为bool,其中非零值转换为true,零值转换为false。 这可能会导致很难注意到的令人讨厌的错误:

    int x = 0; 
    if (x = 1) cout << "not zero" << endl; 
    else       cout << "is zero" << endl;

在这里,编译器看到赋值表达式x = 1,这是一个错误;它应该是比较x == 1。 但是,这是有效的 C++,因为表达式的值是 1,而编译器很有帮助地将其转换为booltrue。 这段代码将在没有任何警告的情况下编译,它不仅会产生与预期相反的结果(您将在控制台上看到打印的not zero),而且赋值还会更改在整个程序中传播错误的变量的值。

通过养成总是构造比较的习惯,以便潜在赋值的 r 值在左边,很容易避免这个错误。 相比之下,将不会有右值或左值的概念,因此当赋值不是预期的时候,这将使用编译器来捕获赋值:

    if (1 = x) // error 
    cout << "not zero" << endl;

转换带符号的类型

签名到未签名的转换可能会发生,并可能导致意外的结果。 例如:

    int s = -3; 
    unsigned int u = s;

将为unsigned short变量赋值0xfffffffd,即两个变量的补值为 3。这可能是您想要的结果,但这是一种奇怪的获取方式。

有趣的是,如果您尝试比较这两个变量,编译器将发出警告:

    if (u < s) // C4018 
    cout << "u is smaller than s" << endl;

这里给出的 Visual C++ 警告 C4018 是'<': signed/unsigned mismatch,它说明不能比较有符号类型和无符号类型,为此需要强制转换。

铸件,铸造品

在某些情况下,您必须在类型之间进行转换。 例如,这可能是因为提供数据的类型与您用来处理数据的例程不同。 您可能有一个库将浮点数处理为float,但您的数据输入为double。 您知道转换将失去精度,但知道这对最终结果影响不大,因此您不希望编译器警告您。 您要做的是告诉编译器一种类型到另一种类型的强制是可以接受的。

下表总结了您可以在 C++ 11 中使用的各种强制转换操作:

| 名称 | 语法 | | 建造 / 建筑物 / 解释 / 造句 | {} | | 删除const要求 | const_cast | | 不带运行时检查的强制转换 | static_cast | | 类型的按位强制转换 | reinterpret_cast | | 类指针之间的强制转换,带有运行时检查 | dynamic_cast | | C 样式 | () | | 函数样式 | () |

抛开恒久不变

正如上一章所提到的,const说明符用于向编译器指示项不会更改,并且代码更改该项的任何尝试都是错误的。 还有另一种使用此说明符的方法,这将在下一章中介绍。 将const应用于指针时,表示该指针所指向的内存不能更改:

    char *ptr = "0123456"; 
    // possibly lots of code 
    ptr[3] = '\0'; // RUNTIME ERROR!

这段写得很糟糕的代码告诉编译器创建一个值为0123456的字符串常量,然后将该内存的地址放入字符串指针ptr。 最后一行尝试写入字符串。 这将进行编译,但会在运行时导致访问冲突。 将const应用于指针声明将确保编译器检查此类情况:

    const char *ptr = "0123456";

更典型的情况是将const应用于作为函数参数的指针,其目的是相同的:它向编译器指示指针指向的数据应该是只读的。 但是,在某些情况下,您可能需要删除此类指针的const属性,这是使用const_cast运算符执行的:

    char * pWriteable = const_cast<char *>(ptr); 
    pWriteable[3] = '\0';

语法很简单。 尖括号(<>)中给出了要转换为的类型,括号中提供了变量(它是const指针)。

还可以将指针转换为const指针。 这意味着您可以使用一个指针来访问内存,以便对其进行写入,然后在进行更改后,您可以创建一个指向内存的const指针,实际上是通过该指针使内存成为只读的。

显然,一旦丢弃了指针的不变性,您就需要为写入内存所造成的损害负责,因此代码中的const_cast运算符是您在代码审查期间检查代码的一个很好的标记。

在没有运行时检查的情况下进行强制转换

大多数强制转换都是使用static_cast运算符执行的,这可用于将指针转换为相关指针类型以及在数值类型之间进行转换。 没有执行运行时检查,因此您应该确保转换是可接受的:

    double pi = 3.1415; 
    int pi_whole = static_cast<int>(pi);

这里,adouble被转换为int,这意味着小数部分被丢弃。 通常,编译器会发出数据丢失的警告,但static_cast操作符表明这是您的意图,因此没有给出警告。

运算符通常用于将void*指针转换为类型化指针。 在下面的代码中,unsafe_d函数假定参数是指向内存中双精度值的指针,因此它可以将void*指针转换为double*指针。 与pd指针一起使用的*运算符取消引用指针,以给出它所指向的数据。 因此,*pd表达式将返回double

    void unsafe_d(void* pData) 
    { 
       double* pd = static_cast<double*>(pData); 
       cout << *pd << endl; 
    }

这是不安全的,因为您依赖调用方来确保指针实际指向double。 可以这样称呼它:

    void main() 
    { 
       double pi = 3.1415; 
       unsafe_d(&pi);       // works as expected 

       int pi_whole = static_cast<int>(pi); 
       unsafe_d(&pi_whole); // oops! 
    }

&运算符将操作数的内存地址作为类型化指针返回。 在第一种情况下,获取double*指针并将其传递给unsafe_d函数。 编译器会自动将此指针转换为void*参数。 编译器自动执行此操作,而不检查指针是否在函数中正确使用。 第二次调用unsafe_d说明了这一点,其中将int*指针转换为void*参数,然后在unsafe_d函数中,即使指针指向int,它也会由static_cast强制转换为double*。 因此,取消引用将返回不可预测的数据,cout将打印无稽之谈。

在不进行运行时检查的情况下强制转换指针

reinterpret_cast运算符允许将指向一种类型的指针转换为另一种类型的指针,它可以从指针转换为整数,也可以将整数转换为指针:

    double pi = 3.1415; 
    int i = reinterpret_cast<int>(&pi); 
    cout << hex << i << endl;

static_cast不同,此运算符始终涉及指针:在指针之间转换,从指针转换为整型,或从整型转换为指针。 在本例中,指向double变量的指针被转换为int,并将值打印到控制台。 实际上,这会打印出变量的内存地址。

使用运行时检查进行强制转换

dynamic_cast运算符用于在相关类之间转换指针,因此将在第 6 章中进行说明。 该运算符涉及运行时检查,因此只有在操作数可以转换为指定类型时才执行转换。 如果无法进行转换,则操作符返回nullptr,使您有机会只使用指向该类型的实际对象的已转换指针。

使用列表初始值设定项进行强制转换

C++ 编译器将允许一些隐式转换;在某些情况下,它们可能是有意的,在某些情况下,它们可能不是。 例如,下面的代码类似于前面显示的代码:将变量初始化为double值,然后在代码中使用它来初始化int。 编译器将执行转换,并发出警告:

    double pi = 3.1415; 
    // possibly loss of code 
    int i = pi;

如果忽略警告,则可能不会注意到这种精度损失,这可能会导致问题。 解决此问题的一种方法是使用大括号进行初始化:

    int i = {pi};

在这种情况下,如果pi可以无损失地转换为int(例如,如果pishort),则代码将在没有任何警告的情况下编译。 但是,如果pi是不兼容的类型(在本例中为double),则编译器将发出错误:

C2397: conversion from 'double' to 'int' requires a narrowing conversion

这里有一个有趣的例子。 char类型是整数,但osteam类中char<<运算符将char变量解释为字符,而不是数字,如下所示:

    char c = 35; 
    cout << c << endl;

这将在控制台上打印#,而不是 35,因为 35 是“#”的 ASCII 代码。 要将变量作为数字处理,可以使用以下方法之一:

    cout << static_cast<short>(c) << endl; 
    cout << short{ c } << endl;

如您所见,第二个版本(构造)同样具有可读性,但比第一个版本短。

使用 C 语言强制转换

最后,您可以使用 C 样式转换,但提供这些转换只是为了编译遗留代码。 您应该改用一个 C++ 强制转换。 为完整起见,下面显示了 C 样式的强制转换:

    double pi = 3.1415; 
    float f1 = (float)pi; 
    float f2 = float(pi);

有两个版本:第一个强制转换操作符包含要强制转换到的类型周围的圆括号,而在第二个版本中,强制转换看起来像是一个函数调用。 在这两种情况下,最好使用static_cast,以便进行编译时检查。

使用 C++ 类型

在本章的最后一部分中,我们将开发一个命令行应用,它允许您以字母数字和十六进制混合格式打印文件内容。

应用必须使用文件名运行,但您也可以指定要打印的行数。 应用将在控制台上打印文件内容,每行 16 字节。 在左边,它给出十六进制表示,在右边,它给出可打印的表示(如果字符不在可打印的非扩展 ASCII 范围内,则给出一个点)。

C:\Beginning_C++ 下创建一个名为Chapter_03的新文件夹。 启动 Visual C++ 并创建一个 C++ 源文件,并将其保存到您刚刚创建的文件夹hexdump.cpp。 添加一个简单的main函数,该函数允许应用接受参数,并支持使用 C++ 流进行输入和输出:

    #include <iostream> 

    using namespace std; 

    int main(int argc, char* argv[]) 
    { 
    }

应用最多有两个参数:第一个是文件名,第二个是要在命令行上打印的 16 字节块的数量。 这意味着您需要检查参数是否有效。 首先添加usage函数以提供应用参数,如果使用非空参数调用,则打印出一条错误消息:

    void usage(const char* msg) 
    { 
        cout << "filedump filename blocks" << endl; 
        cout << "filename (mandatory) is the name of the file to dump"  
            << endl; 
        cout << "blocks (option) is the number of 16 byte blocks " 
            << endl; 
        if (nullptr == msg) return; 
        cout << endl << "Error! "; 
        cout << msg << endl; 
    }

main函数之前添加此函数,以便可以从那里调用它。 可以使用指向 C 字符串的指针或使用nullptr调用该函数。 该参数为const,向编译器表明该字符串不会在函数中更改,因此如果有人试图更改该字符串,编译器将生成错误。

将以下行添加到main函数:

    int main(int argc, char* argv[]) 
    { 
 if (argc < 2) { usage("not enough parameters"); return 1; } if (argc > 3) { usage("too many parameters"); return 1; } // the second parameter is file name string filename = argv[1]; 
    }

编译文件并确认没有打字错误。 由于此应用使用 C++ 标准库,因此您必须使用/EHsc开关提供对 C++ 异常的支持:

cl /EHsc hexdump.cpp

您可以使用 0、1、2 和 3 个参数测试从命令行调用它的应用。 确认应用只允许在命令行上使用一个或两个参数调用它(这实际上意味着两个或三个参数,因为argcargv包括应用名称)。

下一项任务是确定用户是否提供了一个数字来指示要转储到控制台的 16 字节块的数量,如果提供了,则将命令行提供的字符串转换为整数。 此代码将使用istringstream类执行从字符串到数字的转换,因此您需要包括定义该类的头文件。 将以下内容添加到文件顶部:

    #include <iostream>
 #include <sstream>

在声明filename变量之后,添加以下突出显示的代码:

    string filename = argv[1]; 
 int blocks = 1;  // default value if (3 == argc) { // we have been passed the number of blocks istringstream ss(argv[2]); ss >> blocks; if (ss.fail() || 0 >= blocks) { // cannot convert to a number usage("second parameter: must be a number," "and greater than zero"); return 1; } }

默认情况下,应用将从文件中转储一行数据(最多 16 个字节)。 如果用户提供了不同的行数,则使用istringstream对象将字符串格式的数字转换为整数。 这是用参数初始化的,然后从流对象中提取数字。 如果用户键入的值为零,或者如果他们键入的值无法解释为字符串,则代码将打印一条错误消息。 错误字符串被分成两行,但它仍然是一个字符串。

请注意,if语句使用短路;也就是说,如果表达式的第一部分(ss.fail(),表示转换失败)是true,则不会计算第二个表达式(0 >= blocks,即blocks必须大于零)。

编译此代码并尝试多次。 例如:

 hexdump readme.txt
 hexdump readme.txt 10
 hexdump readme.txt 0
 hexdump readme.txt -1

前两个命令运行时应该没有错误;后两个命令应该会产生错误。

Don't worry that readme.txt does not exist, as it is only here as a test parameter.

接下来,您将添加打开文件并对其进行处理的代码。 由于您将使用ifstream类从文件输入数据,因此请将以下头文件添加到该文件的顶部:

    #include <iostream> 
    #include <sstream> 
 #include <fstream>

然后在main函数的底部添加打开文件的代码:

    ifstream file(filename, ios::binary); 
    if (!file.good()) 
    { 
        usage("first parameter: file does not exist"); 
        return; 
    } 

    while (blocks-- && read16(file) != -1);  
    file.close();

第一行创建名为file的流对象,并将其附加到通过filename中给出的路径指定的文件。 如果找不到文件,good函数将返回false。 此代码使用!运算符对该值求反,以便如果文件不存在,则执行if后面大括号中的语句。 如果文件存在并且ifstream对象可以打开它,则在while循环中一次读取 16 个字节的数据。 注意,在这段代码的末尾,在file对象上调用了close函数。 使用完资源后显式关闭资源是一种良好的做法。

文件将由read16函数逐字节访问,包括不可打印的字节,因此像\r\n这样的控制字符没有特殊意义,仍然会被读入。 但是,STREAM 类以一种特殊的方式处理\r字符:这被视为行尾,通常流将静默使用该字符。 为了防止出现这种情况,我们使用ios::binary以二进制模式打开文件。

再次查看while语句:

    while (blocks-- && read16(file) != -1);

这里有两个表达式。 第一个表达式递减blocks变量,该变量保存将打印的 16 字节块的数量。 后缀递减表示表达式的值是递减之前的变量的值,因此如果在blocks为零时调用该表达式,则整个表达式将短路,while循环结束。 如果第一个表达式非零,则调用read16函数,如果返回-1 值(到达文件末尾),则循环结束。 循环的实际工作发生在read16函数中,因此whileLOOP 语句是空语句。

现在,您必须在main函数的正上方实现read16函数。 此函数将使用一个常量来定义每个块的长度,因此在文件顶部附近添加以下声明:

    using namespace std; 
 const int block_length = 16;

就在main函数之前,添加以下代码:

    int read16(ifstream& stm) 
    { 
        if (stm.eof()) return -1; 
        int flags = cout.flags(); 
        cout << hex; 

        string line; 

        // print bytes 

        cout.setf(flags); 
        return line.length(); 
    }

这只是该函数的框架代码。 稍后您将添加更多代码。

此函数一次最多读取 16 个字节,并将这些字节的内容打印到控制台。 返回值是读取的字节数,如果到达文件末尾,则返回值为-1。 请注意用于将流对象传递给函数的语法。 这是一个引用,是一种指向实际对象的指针类型。 使用引用的原因是,如果我们不这样做,函数将获得流的副本。 引用将在下一章中介绍,将对象引用用作函数参数将在使用函数中介绍。

此函数测试的第一行是验证是否已到达文件末尾,如果已到达,则不能再进行任何处理,并返回-1 的值。 代码将操作cout对象(例如,插入hex操纵器);这样您就可以始终知道该对象在函数外部的状态,该函数可以确保当它返回cout对象时,它与调用函数时处于相同的状态。 通过调用flags函数获得cout对象的初始格式化状态,该状态用于在函数返回之前通过调用setf函数重置cout对象。

此函数不执行任何操作,因此可以安全地编译文件并确认没有输入错误。

read16函数执行三项操作:

  1. 它逐个字节地读入,最多 16 个字节。
  2. 它打印出每个字节的十六进制值。
  3. 它打印出该字节的可打印值。

这意味着每行都有两个部分:左侧的十六进制部分和右侧的可打印部分。 用突出显示的代码替换函数中的注释:

    string line; 
 for (int i = 0; i < block_length; ++ i) { // read a single character from the stream unsigned char c = stm.get(); if (stm.eof()) 
            break; // need to make sure that all hex are printed   
        // two character padded with zeros cout << setw(2) << setfill('0'); cout << static_cast<short>(c) << " "; if (isprint(c) == 0) line += '.'; else                 line += c; }

for循环最多循环block_length次。 第一个语句从流中读取单个字符。 该字节作为原始数据读入。 如果get发现流中没有更多的字符,它将在流对象中设置一个标志,并通过调用eof函数来测试这一点。 如果eof函数返回true,则表示已到达文件末尾,因此for循环结束,但函数不会立即返回。 原因是可能已经读取了个字节,因此必须执行更多处理。

循环中的其余语句做两件事:

  • 控制台上有打印字符十六进制值的语句
  • line变量中有一条语句以可打印的形式存储字符

我们已经将cout对象设置为输出十六进制值,但是如果字节小于 0x10,则不会以零为前缀打印值。 要获得这种格式,我们插入setw操纵器,表示插入的数据将占据两个字符位置,插入setfill,表示使用0字符填充字符串。 这两个操纵器位于<iomanip>标题中,因此请将它们添加到文件的顶部:

    #include <fstream> 
 #include <iomanip>

通常,当您将char插入到流中时,会显示字符值,因此char变量会转换为short,这样流就会打印十六进制数值。 最后,在每个项目之间打印一个空格。

for循环中的最后几行如下所示:

    if (isprint(c) == 0) line += '.'; 
    else                 line += c;

此代码使用isprint宏检查字节是否为可打印字符(从“”到“~”),如果字符可打印,则将其附加到line变量的末尾。 如果字节不可打印,则在line变量的末尾追加一个点作为占位符。

到目前为止,代码将一个接一个地将字节的十六进制表示形式打印到控制台,唯一的格式是字节之间的空格。 如果您想测试代码,可以编译以下代码并在源文件上运行:

hexdump hexdump.cpp 5

您将看到一些难以理解的内容,如下所示:

    C:\Beginning_C++ \Chapter_03>hexdump hexdump.cpp 5 
23 69 6e 63 6c 75 64 65 20 3c 69 6f 73 74 72 65 61 6d 3e 0d 0a 
23 69 6e 63 6c 75 64 65 20 3c 73 73 74 72 65 61 6d 3e 0d 0a 23 
69 6e 63 6c 75 64 65 20 3c 66 73 74 72 65 61 6d 3e 0d 0a 23 69 
6e 63 6c 75 64 65 20 3c 69 6f 6d 61 6e 69 70 3e 0d

23值是#,20是空格,0d0a是回车和换行符。

现在我们需要打印line变量中的字符表示形式,执行一些格式化,并添加换行符。 在for循环之后,添加以下内容:

    string padding = " "; 
    if (line.length() < block_length) 
    { 
        padding += string( 
            3 * (block_length - line.length()), ' '); 
    } 

    cout << padding; 
    cout << line << endl;

十六进制显示和字符显示之间将至少有两个空格。 一个空格来自for循环中打印出的最后一个字符,第二个空格在padding变量的初始化中提供。

每行的最大字节数应为 16 字节(block_length),因此控制台上打印 16 个十六进制值。 如果读取的字节数较少,则需要额外的填充,以便在连续的行上字符表示对齐。 实际读取的字节数将是通过调用length函数获得的line变量的长度,因此丢失的字节数是表达式block_length - line.length()。 由于每个十六进制表示占用三个字符(两个用于数字,一个用于空格),因此所需的填充是丢失字节数的三倍。 要创建适当数量的空格,需要使用两个参数调用字符串构造函数:副本数和要复制的字符。

最后,此填充字符串被打印到控制台,后跟字节的字符表示形式。

此时,您应该能够编译代码,而不会出现错误或警告。 在源文件上运行代码时,您应该看到如下所示:

    C:\Beginning_C++ \Chapter_03>hexdump hexdump.cpp 5 
23 69 6e 63 6c 75 64 65 20 3c 69 6f 73 74 72 65  #include <iostre
61 6d 3e 0d 0a 23 69 6e 63 6c 75 64 65 20 3c 73  am>..#include <s
73 74 72 65 61 6d 3e 0d 0a 23 69 6e 63 6c 75 64  stream>..#includ
65 20 3c 66 73 74 72 65 61 6d 3e 0d 0a 23 69 6e  e <fstream>..#in
63 6c 75 64 65 20 3c 69 6f 6d 61 6e 69 70 3e 0d  clude <iomanip>.

现在字节变得更有意义了。 由于应用不会更改其转储的文件,因此对二进制文件(包括其自身)使用此工具是安全的:

    C:\Beginning_C++ \Chapter_03>hexdump hexdump.exe 17 
4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00  MZ..............
b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  ........@.......
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00  ................
0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68  ........!..L.!Th
69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f  is program canno
74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20  t be run in DOS
6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00  mode....$.......
2b c4 3f 01 6f a5 51 52 6f a5 51 52 6f a5 51 52  +.?.o.QRo.QRo.QR
db 39 a0 52 62 a5 51 52 db 39 a2 52 fa a5 51 52  .9.Rb.QR.9.R..QR
db 39 a3 52 73 a5 51 52 b2 5a 9a 52 6a a5 51 52  .9.Rs.QR.Z.Rj.QR
6f a5 50 52 30 a5 51 52 8a fc 52 53 79 a5 51 52  o.PR0.QR..RSy.QR
8a fc 54 53 54 a5 51 52 8a fc 55 53 2f a5 51 52  ..TST.QR..US/.QR
9d fc 54 53 6e a5 51 52 9d fc 53 53 6e a5 51 52  ..TSn.QR..SSn.QR
52 69 63 68 6f a5 51 52 00 00 00 00 00 00 00 00  Richo.QR........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
50 45 00 00 4c 01 05 00 6b e7 07 58 00 00 00 00  PE..L...k..X....

MZ 表示这是 Microsoft 的Portable Executable(PE)文件格式的 DOS 头部分。 实际的 PE 标头从最下面一行开始,字符为 PE。

简略的 / 概括的 / 简易判罪的 / 简易的

在本章中,您已经了解了 C++ 中的各种内置类型,如何初始化它们,以及如何使用它们。 您还学习了如何使用强制转换操作符将变量转换为不同的类型。 本章还向您介绍了记录类型,该主题将在第 6 章中展开。 最后,您已经看到了指针的各种示例,这一主题将在下一章中更详细地讨论。*