《C++ Primer 5th》笔记(2 / 19):变量和基本类型 àì夳堔傛蜴生んèń 2023-01-16 11:09 182阅读 0赞 ### 文章目录 ### * * 基本内置类型 * * 算术类型 * * 内置类型的机器实现(类型在物理层面上的说明) * 建议:如何选择类型 * 类型转换 * * 建议:避免无法预知和依赖于实现环境的行为 * 算术表达式里使用布尔值 * 含有无符号类型的表达式(带符号数会自动地转换成无符号数) * 提示:切勿混用带符号类型和无符号类型 * 字面值常量 * * 整型和浮点型字面值 * 字符和字符串字面值 * 转义序列 * 通过前后缀指定字面值的类型 * 布尔字面值和指针字面值 * 变量 * * 变量定义 * * 术语:何为对象? * 初始值 * 列表初始化 * 默认初始化 * 提示:未初始化变量引发运行时故障 * 变量声明和定义的关系 * * 关键概念:静态类型 * 标识符 * 名字的作用域 * * 建议:当你第一次使用变量时再定义它 * 嵌套的作用域 * 复合类型 * * 引用 * * 引用即别名 * 引用的定义 * 指针 * * 获取对象的地址 * 指针值 * 利用指针访问对象 * 关键概念:一符多义(& 与 \*) * 空指针 * 建议:初始化所有指针 * 赋值和指针 * 其他指针操作 * void\* 指针 * 理解复合类型的声明 * * 定义多个变量 * 指向指针的指针 * 指向指针的引用(指针的别名)(从右向左阅读理解) * const限定符 * * 概述 * * 初始化和const * 默认状态下,const对象仅在文件内有效(const 常量在多文件中使用方法) * const的引用 * * 术语:常量引用是对const的引用 * 初始化和对const的引用 * 对const的引用可能引用一个并非const的对象 * 指针和const * * const指针 * 顶层const * constexpr和常量表达式 * * constexpr变量 * 字面值类型 * 指针和constexpr * 处理类型 * * 类型别名 * * 指针、常量和类型别名 * auto类型说明符 * * 复合类型、常量和 auto * decltype类型指示符 * * decltype和引用 * 自定义数据结构 * * 定义Sales\_data类型 * * 类数据成员 * 使用Sales\_data类 * * Sales\_data对象读入数据 * 输出两个Sales\_data对象的和 * 编写自己的头文件 * * 预处理器 **数据类型**决定了程序中**数据**和**操作**的意义。如下所示的语句: i = i + j; 其含义依赖于i和j的数据类型。如果i和j都是整型数,那么这条语句执行的就是最普通的加法运算。然而,如果i和j是Sales\_item类型(上一章内容)的数据则上述语句把这两个对象的成分相加。 ## 基本内置类型 ## C++定义基本数据类型: * 算术类型(arithmetic type) * 字符 * 整型数 * 布尔值 * 浮点数 * 空类型(void) * 空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。 ### 算术类型 ### 算术类型分为: * 整型(integral type,包括字符和布尔类型在内) * 浮点型 算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。 下表列出了C++标准规定的尺寸的**最小值**,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所能表示的数据范围也不一样。 <table> <thead> <tr> <th>类型</th> <th>含义</th> <th>最小尺寸</th> </tr> </thead> <tbody> <tr> <td>bool</td> <td>布尔类型</td> <td>未定义</td> </tr> <tr> <td>char</td> <td>字符</td> <td>8位</td> </tr> <tr> <td>wchar_t</td> <td>宽字符</td> <td>16位</td> </tr> <tr> <td>char16_t</td> <td>Unicode字符</td> <td>16位</td> </tr> <tr> <td>char32_t</td> <td>Unicode字符</td> <td>32位</td> </tr> <tr> <td>short</td> <td>短整型</td> <td>16位</td> </tr> <tr> <td>int</td> <td>整型</td> <td>16位</td> </tr> <tr> <td>long</td> <td>长整型</td> <td>32位</td> </tr> <tr> <td>long long</td> <td>长整型</td> <td>64位</td> </tr> <tr> <td>float</td> <td>单精度浮点数</td> <td>6位有效数字</td> </tr> <tr> <td>double</td> <td>双精度浮点数</td> <td>10位有效数字</td> </tr> <tr> <td>long double</td> <td>扩展精度浮点数</td> <td>10位有效数字</td> </tr> </tbody> </table> **bool** 布尔类型(bool)的取值是真(true)或者假(false)。 **char** C++提供了几种字符类型,其中多数支持国际化。基本的字符类型是char,一个 char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。也就是说,**一个char的大小和一个机器字节一样**。 其他字符类型用于扩展字符集,如 wchar\_t、char16\_t、char32\_t。wchar\_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型 charl6\_t和char32\_t则为Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)。 **int** 除字符和布尔类型之外,其他整型用于表示(可能)不同尺寸的整数。C++语言规定一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大。其中,数据类型long long是在C++11中新定义的。 **float** 浮点型可表示单精度、双精度和扩展精度值。C++标准指定了一个浮点数有效位数的最小值,然而大多数编译器都实现了更高的精度。通常,float 以1个字(32比特)来表示,double 以2个字(64比特)来表示,long double 以3或4个字(96或128比特)来表示。一般来说,类型float和 double分别有7和16个有效位;类型long double则常常被用于有特殊浮点需求的硬件,它的具体实现不同,精度也各不相同。 -------------------- **带符号类型和无符号类型** 除去布尔型和扩展的字符型之外,其他整型可以划分为: 1. 带符号的(signed),可以表示正数、负数或0。 2. 无符号的(unsigned),仅能表示大于等于0的值。 类型int、short、long和 long long 都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。类型unsigned int可以缩写为unsigned。 与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。 特别需要注意的是:类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。 **无符号类型**中所有比特都用来存储值,例如,8比特的unsigned char可以表示0至255区间内的值。 C++标准并没有规定**带符号类型**应如何表示,但是约定了在表示范围内正值和负值的量应该平衡。因此,8比特的signed char理论上应该可以表示-127至127区间内的值,大多数现代计算机将实际的表示范围定为-128至127。 #### 内置类型的机器实现(类型在物理层面上的说明) #### 计算机以比特序列存储数据,每个比特非0即1,例如: 00011011011100010110010000111011… 大多数计算机**以2的整数次幂个比特作为块**来处理内存,可寻址的**最小内存块**称为“字节(byte)”,存储的基本单元称为“字(word)”,它通常由几个字节组成。在C++语言中,一个字节要至少能容纳机器基本字符集中的字符。 大多数机器的字节由8比特(bit)构成(1byte = 8bits),字则由32或64比特构成,也就是4或8字节(1 word = 4 bytes or 8 bytes)。**大多数计算机将内存中的每个字节与一个数字(被称为“地址(address)”)关联起来**。 在**一个字节为8比特、字为32比特**的机器上,我们可能看到**一个字**的内存区域如下所示: ![在这里插入图片描述][20210425151623456.png_pic_center] 其中,**左侧**是字节的**地址**,**右侧**是字节中8比特的具体内容。 我们能够使用某个地址来表示从这个地址开始的大小不同的比特串,例如,我们可能会说地址736424的那个字或者地址736427的那个字节。 **为了赋予内存中某个地址明确的含义**,**必须**首先知道存储在该地址的**数据的类型**。**类型决定了数据所占的比特数以及该如何解释这些比特的内容**: * 如果位置736424处的对象类型是**float**,并且该机器中float以32比特存储,那么我们就能知道这个对象的内容占满了整个字。这个 float 数的实际值依赖于该机器是如何存储浮点数的。 * 如果位置736424处的对象类型是**unsigned char**,并且该机器使用ISO-Latin-1字符集,则该位置处的字节表示一个分号。 Note:这里在物理层面说明一个变量的类型的作用。 #### 建议:如何选择类型 #### 和C语言一样,C的设计准则之一也是尽可能地接近硬件。**C++的算术类型必须满足各种硬件特质,所以它们常常显得繁杂而令人不知所措**。事实上,大多数程序员能够(也应该)对数据类型的使用做出限定从而简化选择的过程。以下是选择类型的一些经验准则: * 当明确知晓数值不可能为负时,选用无符号类型。(Note:无负无符,呜呼呜呼) * 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int 的表示范围,选用long long。(Note:取中庸int,按需用short或long) * 在算术表达式中尽量不要使用 char或bool,而是只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char(If you need a tiny integer, explicitly specify either signed char or unsigned char)。(Note:算数表达式尽量不用char或bool) * 执行浮点数运算选用 double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。(Note:浮点数直接上double) ### 类型转换 ### 对象的**类型**定义了对象能包含的**数据**和能参与的**运算**,其中一种运算被大多数类型支持,就是将对象从一种给定的类型**转换**(convert)为另一种相关类型。 当在程序的某处我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换。 将来第4章会有更详细的介绍类型转换: 此处,**有必要说明当给某种类型的对象强行赋了另一种类型的值时**,到底会发生什么。 bool b = 42; // b is true /* 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为О则结果为false,否则结果为true。 */ int i = b; // i has value 1 /* 当我们把一个布尔值赋给非布尔类型时,初始值为false 则结果为0,初始值为true则结果为1。 */ i = 3.14; // i has value 3 /* 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分(截断取整数部分)。 */ double pi = i; // pi has value 3.0 /* 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。 */ unsigned char c = -1; // assuming 8-bit chars, c has value 255 /* 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。 Note: x mod y = x - y * ⌊x / y⌋ (from:https://blog.csdn.net/weixin_43435790/article/details/83181319) -1 mod 256 = -1 - 256 * ⌊-1 / 256⌋ = -1 - 256 * (-1) = 255 个人认为以底层知识更容易理解这次转换 计算机保存数值都以补码的形式保存。-1的补码是11111111,但char的类型是无符号整形数,编译器就把11111111当作无符号整形来输出。11111111当作正数时值就是255。 */ signed char c2 = 256; // assuming 8-bit chars, the value of c2 is undefined /* 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。 */ **类型所能表示的值的范围决定了转换的过程。** #### 建议:避免无法预知和依赖于实现环境的行为 #### 无法预知的行为源于编译器无须(有时是不能)检测的错误。即使代码编译通过了,如果程序执行了一条未定义的表达式,仍有可能产生错误。 不幸的是,在某些情况和/或某些编译器下,含有无法预知行为的程序也能正确执行。**但是我们却无法保证同样一个程序在别的编译器下能正常工作**,甚至已经编译通过的代码再次执行也可能会出错。此外,也不能认为这样的程序对一组输入有效,对另一组输入就一定有效。 **程序也应该尽量避免依赖于实现环境的行为**。如果我们把 int的尺寸看成是一个确定不变的已知值,那么这样的程序就称作**不可移植的**(nonportable)。当程序移植到别的机器上后,依赖于实现环境的程序就可能发生错误。要从过去的代码中定位这类错误可不是一件轻松愉快的工作。 #### 算术表达式里使用布尔值 #### 当在程序的某处**使用了一种算术类型的值**而其实所需的是**另一种类型**的值时,编译器同样会执行上述的类型转换。 例如,如果我们使用了一个非布尔值作为条件,那么它会被自动地转换成布尔值,这一做法和把非布尔值赋给布尔变量时的操作完全一样: int i = 42; if (i) // if条件的值将为true i = 0; 如果i的值为0,则条件的值为false;i的所有其他取值(非0)都将使条件为true。 以此类推,如果我们把一个布尔值用在算术表达式里,则它的取值非0即1,所以**一般不宜在算术表达式里使用布尔值**。 #### 含有无符号类型的表达式(带符号数会自动地转换成无符号数) #### **记住**:无符号类型与有符号类型混合表达式中,**带符号数会自动地转换成无符号数**。 尽管我们不会故意给无符号对象赋一个负值,却可能(特别容易)写出这么做的代码。 **例一:** 例如,当一个算术表达式中既有无符号数又有int 值时,**那个int值就会转换成无符号数**。把int转换成无符号数的过程和把int直接赋给无符号变量一样: unsigned u = 10; int i = -42; std::cout << i + i << std::endl; // prints -84 std::cout << u + i << std::endl; // if 32-bit ints, prints 4294967264 std::cout << i + u << std::endl; // also prints 4294967264 1. 在第一个输出表达式里,两个(负)整数相加并得到了期望的结果。 2. 在第二、三个输出表达式里,相加前首先把i的-42转换成无符号数。把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。(**有符号数与无符号数相加,先将有符号转换成无符号**)。 **例二:** 当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值: unsigned u1 = 42, u2 = 10; std::cout << u1 - u2 << std::endl; // ok: result is 32 std::cout << u2 - u1 << std::endl; // ok: but the result will wrap around, 4294967264 **例三:** 无符号数不会小于0这一事实同样关系到循环的写法。例如,写一个循环,通过控制变量递减的方式把从10到0的数字降序输出。这个循环可能类似于下面的形式: for (int i = 10; i >= 0; --i) std::cout << i << std::endl; 可能你会觉得反正也不打算输出负数,可以用无符号数来重写这个循环。 然而,这个不经意的改变却意味着**死循环**: // WRONG: u can never be less than 0; the condition will always succeed for (unsigned u = 10; u >= 0; --u) std::cout << u << std::endl; 来看看当u等于0时发生了什么,这次迭代输出0,然后继续执行for语句里的表达式。表达式–u从u当中减去1,得到的结果-1并不满足无符号数的要求,此时像所有表示范围之外的其他数字一样,**-1被自动地转换成一个合法的无符号数**。假设 int类型占32位,则当u等于0时,–u的结果将会是4294967295。 **一种解决的办法**是(不如改回int ╮(╯▽╰)╭),用while语句来代替for语句,因为前者让我们能够在输出变量之前(而非之后)先减去1: unsigned u = 11; // start the loop one past the first element we want to print while (u > 0) { --u; // decrement first, so that the last iteration will print 0 std::cout << u << std::endl; } 改写后的循环先执行对循环控制变量减1的操作,这样最后一次迭代时,进入循环的u值为1。此时将其减1,则这次迭代输出的数就是0:下一次再检验循环条件时,u的值等于0而无法再进入循环。 因为我们要先做减1的操作,所以初始化u的值应该比要输出的最大值大1。这里,u初始化为11,输出的最大数是10。(也就预大一位)。 #### 提示:切勿混用带符号类型和无符号类型 #### 如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为**带符号数会自动地转换成无符号数**。 例如,在一个形如a \* b的式子中,如果a=-1,b=1,而且a和b都是int,则表达式的值显然为-1。 然而,如果a是int,而b是unsigned,则结果须视在当前机器上int所占位数而定。在我们的环境里,结果是4294967295。 ### 字面值常量 ### 一个形如42的值被称作**字面值常量**(literal)。每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。 #### 整型和浮点型字面值 #### 我们可以将整型字面值写作十进制数、八进制数或十六进制数的形式。 以0开头的整数代表八进制数,以0x或0x开头的代表十六进制数。 例如,我们能用下面的任意一种形式来表示数值20: * 20 十进制 * 024 八进制 * 0x14 十六进制 整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号数,**八进制和十六进制字面值既可能是带符号的也可能是无符号的**。十进制字面值的类型是int、long和 long long 中尺寸最小的那个(例如,三者当中最小是int),当然前提是这种类型要能容纳下当前的值。(带负号的八进制、十六进数少见)。 八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long和 unsigned long long中的尺寸最小者。如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误。类型short没有对应的字面值。 以U、L等后缀可以代表相应的字面值类型。 尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。如果我们使用了一个形如-42的负十进制字面值,那个**负号并不在字面值之内**,它的作用仅仅是对字面值取负值而已。 -------------------- 浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识: * 3.14159 * 3.14159E0 * 0. * 0e0 * .001 默认的,浮点型字面值是一个double,我们可以用后缀F等来表示其他浮点型。 #### 字符和字符串字面值 #### 由单引号括起来的一个字符称为**char型字面值**,双引号括起来的零个或多个字符则构成**字符串型字面值**。 * ‘a’:字符字面值 * "Hello world! ":字符串字面值 字符串字面值的类型实际上是由常量字符构成的数组(array)。编译器在每个字符串的结尾处添加一个空字符(’\\0’),因此,字符串字面值的实际长度要比它的内容多1。 例如, * 字面值’A‘表示的就是单独的字符A * 字符串"A"则代表了一个字符的数组,该数组包含两个字符:一个是字母A、另一个是空字符(’\\0’)。 如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。 当书写的字符串字面值比较长,写在一行里不太合适时,就可以采取分开书写的方式: //分多行书写的字符串字面值 std::cout<< "a really, really long string literal " "that spans two lines" << std::endl; //与Java相比,不用+号 #### 转义序列 #### 有两类字符程序员不能直接使用: 1. 不可打印(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符; 2. 在C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。 对于特殊含义的字符需要用到转义序列(escape sequence),转义序列均以反斜线作为开始,C++语言规定的转义序列包括: * 换行符 \\n * 纵向制表符 \\v * 反斜线 \\ \\ * 回车符 \\r * 横向制表符 \\t * 退格符 \\b * 问号 ? * 进纸符 \\f * 报警(响铃)符 \\a * 双引号 \\ " * 单引号 \\ ’ 在程序中,上述转义序列被当作一个字符使用: std::cout << '\n'; // prints a newline std::cout << "\tHi!\n"; // prints a tab followd by "Hi!" and a newline -------------------- (**偏僻语法,不能一见知意,少用**)我们也可以使用泛化的转义序列,其形式是\\x后紧跟1个或多个十六进制数字,或者\\后紧跟1个、2个或3个八进制数字,其中数字部分表示的是字符对应的数值。假设使用的是Latin-1字符集,以下是一些示例: * \\7 (bell) * \\12 (newline) * \\40 (blank) * \\0 (null) * \\115 (‘M’) * \\x4d (‘M’) 我们可以像使用普通字符那样使用C++语言定义的转义序列: std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline std::cout << '\115' << '\n'; // prints M followed by a newline 注意,如果反斜线\\后面跟着的八进制数字超过3个,只有前3个数字与\\构成转义序列。 例如,"\\1234"表示2个字符,即八进制数123对应的字符以及字符4。 相反,\\x要用到后面跟着的所有数字,例如,"\\x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定。 因为大多数机器的char型数据占8位,所以上面这个例子可能会报错。一般来说,超过8位的十六进制字符都是与U等前缀作为开头的扩展字符集一起使用的。 #### 通过前后缀指定字面值的类型 #### 通过添加前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。 L'a' // wide character literal, type is wchar_t u8"hi!" // utf-8 string literal (utf-8 encodes a Unicode character in 8 bits) 42ULL // unsigned integer literal, type is unsigned long long 1E-3F // single-precision floating-point literal, type is float 3.14159L // extended-precision floating-point literal, type is long double (不要用字母l作后缀)**当使用一个长整型字面值时,请使用大写字母L来标记,因为小写字母l和数字1太容易混淆了。** 通过添加前缀或后缀指定字面值的类型 **字符和字符串字面值** <table> <thead> <tr> <th align="center">前缀</th> <th align="center">含义</th> <th align="center">类型</th> </tr> </thead> <tbody> <tr> <td align="center">u</td> <td align="center">Unicode 16字符</td> <td align="center">char16_t</td> </tr> <tr> <td align="center">U</td> <td align="center">Unicode 32字符</td> <td align="center">char32_t</td> </tr> <tr> <td align="center">L</td> <td align="center">宽字符</td> <td align="center">wchar_t</td> </tr> <tr> <td align="center">u8</td> <td align="center">UTF8(仅用于字符串字面常量)</td> <td align="center">char</td> </tr> </tbody> </table> **整型字面值** <table> <thead> <tr> <th align="center">后缀</th> <th align="center">最小匹配类型</th> </tr> </thead> <tbody> <tr> <td align="center">u or U</td> <td align="center">unsigned</td> </tr> <tr> <td align="center">l or L</td> <td align="center">long</td> </tr> <tr> <td align="center">ll or LL</td> <td align="center">long long</td> </tr> </tbody> </table> **浮点数字面值** <table> <thead> <tr> <th align="center">后缀</th> <th align="center">类型</th> </tr> </thead> <tbody> <tr> <td align="center">f or F</td> <td align="center">float</td> </tr> <tr> <td align="center">l or L</td> <td align="center">long double</td> </tr> </tbody> </table> 对于一个整型字面值来说,我们能分别指定它是否带符号以及占用多少空间。如果后缀中有u,则该字面值属于无符号类型,也就是说,以u为后缀的十进制数、八进制数或十六进制数都将从unsigned int、unsigned long和 unsigned long long中选择能匹配的空间最小的一个作为其数据类型。 如果后缀中有L,则字面值的类型至少是long; 如果后缀中有LL,则字面值的类型将是long long和unsigned long long 中的一种。显然我们可以将u与工或LL合在一起使用。例如,以UL为后缀的字面值的数据类型将根据具体数值情况或者取unsigned long,或者取unsigned long long。 #### 布尔字面值和指针字面值 #### true和false是布尔类型的字面值: bool test = false; nullptr是指针字面值。 ## 变量 ## **变量提供一个具名的、可供程序操作的存储空间。** C++中的每个变量都有其**数据类型**,数据类型决定着变量: * 所占内存空间的大小和布局方式、(内存大小) * 该空间能存储的值的范围,(范围) * 以及变量能参与的运算。(运算) 对C++程序员来说,“变量(variable)”和“对象(object)”一般可以互换使用。 ### 变量定义 ### 变量定义的基本形式是:首先是类型说明符(type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。 列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值: int sum = 0, value, // sum, value, and units_sold have type int units_sold = 0; // sum and units_sold have initial value 0 Sales_item item; // item has type Sales_item // string is a library type, representing a variable-length sequence of characters std::string book("0-201-78345-X"); // book initialized from string literal book的定义用到了库类型std: :string,像iostream一样,string 也是在命名空间std中定义的,我们将在第3章中对string类型做更详细的介绍。眼下,只需了解string是一种表示可变长字符序列的数据类型即可。 C++库提供了几种初始化string对象的方法,其中一种是把字面值拷贝给string对象,因此在上例中,book被初始化为0-201-78345-X。 #### 术语:何为对象? #### C++程序员们在很多场合都会使用**对象**(object)这个名词。通常情况下,**对象是指一块能存储数据并具有某种类型的内存空间**。 * 一些人仅在与**类**有关的场景下才使用“对象”这个词。 * 另一些人则把**已命名**的对象和**未命名**的对象区分开来,他们把命名了的对象叫做变量。 * 还有一些人把对象和值区分开来,其中对象指能被程序修改的数据,而**值**(value)指**只读**的数据。 本书遵循大多数人的习惯用法,即认为**对象是具有某种数据类型的内存空间**。我们在使用对象这个词时,并不严格区分是类还是内置类型,也不区分是否命名或是否只读。 (记住:**对象是具有某种数据类型的内存空间**) #### 初始值 #### 当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)了。 用于初始化变量的值可以是任意复杂的表达式。 当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。 因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量。 // ok: price is defined and initialized before it is used to initialize discount double price = 109.99, discount = price * 0.16; // ok: call applyDiscount and use the return value to initialize salePrice double salePrice = applyDiscount(price, discount); 在C++语言中,初始化是一个异常复杂的问题,我们也将反复讨论这个问题。 很多程序员对于用等号=来初始化变量的方式倍感困惑,这种方式容易让人认为初始化是赋值的一种。**事实上在C++语言中,初始化和赋值是两个完全不同的操作**。然而在很多编程语言中二者的区别几乎可以忽略不计,即使在C++语言中有时这种区别也无关紧要,所以人们特别容易把二者混为一谈。 需要强调的是,**这个概念至关重要**,我们也将在后面不止一次提及这一点。 **初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。** (Note: 初始化和赋值是两码事,即使它们很相似。当某一变量首次用=号的就是**初始化**,其他地方用=号的就是**赋值**) #### 列表初始化 #### C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。例如,要想定义一个名为units\_sold的int变量并初始化为0,以下的4条语句都可以做到这一点: int units_sold = 0; int units_sold = { 0}; int units_sold{ 0}; int units_sold(0); 作为**C++11新标准**的一部分,用**花括号**来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某些受限的场合下才能使用。出于3.3.1节将要介绍的原因,这种初始化的形式被称为**列表初始化**(list initialization)。现在,无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。 当用于内置类型的变量时,这种初始化形式有一个重要特点:**如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错**: long double ld = 3.1415926536; int a{ ld}, b = { ld}; // error: narrowing conversion required int c(ld), d = ld; // ok: but value will be truncated 使用long double的值初始化int变量时可能丢失数据,所以编译器拒绝了a和b的初始化请求。 其中,至少ld的小数部分会丢失掉,而且int也可能存不下ld的整数部分。 (Note: 列表初始化变量转型有数据丢失报错功能???) #### 默认初始化 #### 如果定义变量时没有指定初值,则变量被**默认初始化**(default initialized),此时变量被赋予了“默认值”。**默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。** 如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于**任何函数体之外**的变量被初始化为0。 * 一种例外情况是,定义在函数体内部的内置类型变量将不被初始化(uninitialized)。 * 一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。 每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果类允许这种行为,它将决定对象的初始值到底是什么。 绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值。例如,String类规定如果没有指定初值则生成一个空串: std::string empty;//empty非显式地初始化为一个空串 Sales_item item;//被默认初始化的sales_item对象 一些类要求每个对象都显式初始化,此时如果创建了一个该类的对象而未对其做明确的初始化操作,将引发错误。 **定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。** #### 提示:未初始化变量引发运行时故障 #### 未初始化的变量含有一个不确定的值,使用未初始化变量的值是一种错误的编程行为并且很难调试。尽管大多数编译器都能对一部分使用未初始化变量的行为提出警告,但严格来说,编译器并未被要求检查此类错误。 **使用未初始化的变量将带来无法预计的后果**。有时我们足够幸运,一访问此类对象程序就崩溃并报错,此时只要找到崩溃的位置就很容易发现变量没被初始化的问题。另外一些时候,程序会一直执行完并产生错误的结果。更糟糕的情况是,程序结果时对时错、无法把握。而且,往无关的位置添加代码还会导致我们误以为程序对了,其实结果仍旧有错。 **建议初始化每一个内置类型的变量。虽然并非必须这么做,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。** (Note: 创建一个变量都初始化吧。保安全) ### 变量声明和定义的关系 ### 为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。 如果将程序分为多个文件,则需要有在文件间共享代码的方法。例如,一个文件的代码可能需要使用另一个文件中定义的变量。一个实际的例子是std: :cout和std::cin,它们定义于标准库,却能被我们写的程序使用。 **为了支持分离式编译,C++语言将声明和定义区分开来**。 * 声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。 * 定义(definition)负责创建与名字关联的实体。 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。 **如果想声明一个变量而非定义它,就在变量名前添加关键字extern**,而且不要显式地初始化变量: extern int i; // declares but does not define i int j; // declares and defines j 任何包含了显式初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。**extern语句如果包含初始值就不再是声明,而变成定义了**: extern double pi = 3.1416; //定义 在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。 声明和定义的区别看起来也许微不足道,但实际上却非常重要。(重中之重)**如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。** (Note: 变量能且只能被定义一次,但是可以被多次声明(只为使用它)。) 关于C++语言对分离式编译的支持在将来做更详细介绍。 #### 关键概念:静态类型 #### C++是一种静态类型(statically typed)语言,其含义是**在编译阶段检查类型**。其中,检查类型的过程称为类型检查(type checking)。 对象的类型决定了对象所能参与的运算。在C++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。 程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前必须声明其类型。 ### 标识符 ### C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感: // defines four different int variables int somename, someName, SomeName, SOMENAME; 下面两表所示,C++语言保留了一些名字供语言本身使用,这些名字不能被用作标识符。 同时,C++也为标准库保留了一些名字。用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。 **C++关键字** <table> <thead> <tr> <th align="center">-</th> <th align="center">-</th> <th align="center">-</th> <th align="center">-</th> <th align="center">-</th> </tr> </thead> <tbody> <tr> <td align="center">alignas</td> <td align="center">continue</td> <td align="center">friend</td> <td align="center">register</td> <td align="center">true</td> </tr> <tr> <td align="center">alignof</td> <td align="center">decltype</td> <td align="center">goto</td> <td align="center">reinterpret_cast</td> <td align="center">try</td> </tr> <tr> <td align="center">asm</td> <td align="center">default</td> <td align="center">if</td> <td align="center">return</td> <td align="center">typedef</td> </tr> <tr> <td align="center">auto</td> <td align="center">delete</td> <td align="center">inline</td> <td align="center">short</td> <td align="center">typeid</td> </tr> <tr> <td align="center">bool</td> <td align="center">do</td> <td align="center">int</td> <td align="center">signed</td> <td align="center">typename</td> </tr> <tr> <td align="center">break</td> <td align="center">double</td> <td align="center">long</td> <td align="center">sizeof</td> <td align="center">union</td> </tr> <tr> <td align="center">case</td> <td align="center">dynamic_cast</td> <td align="center">mutable</td> <td align="center">static</td> <td align="center">unsigned</td> </tr> <tr> <td align="center">catch</td> <td align="center">else</td> <td align="center">namespace</td> <td align="center">static_assert</td> <td align="center">using</td> </tr> <tr> <td align="center">char</td> <td align="center">enum</td> <td align="center">new</td> <td align="center">static_cast</td> <td align="center">virtual</td> </tr> <tr> <td align="center">char16_t</td> <td align="center">explicit</td> <td align="center">noexcept</td> <td align="center">struct</td> <td align="center">void</td> </tr> <tr> <td align="center">char32_t</td> <td align="center">export</td> <td align="center">nullptr</td> <td align="center">switch</td> <td align="center">volatile</td> </tr> <tr> <td align="center">class</td> <td align="center">extern</td> <td align="center">operator</td> <td align="center">template</td> <td align="center">wchar_t</td> </tr> <tr> <td align="center">const</td> <td align="center">false</td> <td align="center">private</td> <td align="center">this</td> <td align="center">while</td> </tr> <tr> <td align="center">constexpr</td> <td align="center">float</td> <td align="center">protected</td> <td align="center">thread_local</td> <td align="center"></td> </tr> <tr> <td align="center">const_cast</td> <td align="center">for</td> <td align="center">public</td> <td align="center">throw</td> <td align="center"></td> </tr> </tbody> </table> **C++操作符替代名** <table> <thead> <tr> <th align="center">-</th> <th align="center">-</th> <th align="center">-</th> </tr> </thead> <tbody> <tr> <td align="center">and</td> <td align="center">compl</td> <td align="center">or_eq</td> </tr> <tr> <td align="center">and_eq</td> <td align="center">not</td> <td align="center">xor</td> </tr> <tr> <td align="center">bitand</td> <td align="center">not_eq</td> <td align="center">xor_eq</td> </tr> <tr> <td align="center">bitor</td> <td align="center">or</td> <td align="center"></td> </tr> </tbody> </table> **变量命名规范** 变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性: * 标识符要能体现实际含义。 * 变量名一般用小写字母,如 index,不要使用Index或INDEX。 * 用户自定义的类名一般以大写字母开头,如 Sales\_item。 * 如果标识符由多个单词组成,则单词间应有明显区分,如 student\_loan或studentLoan,不要使用studentloan。 **对于命名规范来说,若能坚持,必将有效**。 (Note:使用Java驼峰命名法吧) ### 名字的作用域 ### 不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数、类型等。然而,同一个名字如果出现在程序的不同位置,也可能指向的是不同实体。 **作用域**(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。 同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。 #include <iostream> int main() { int sum = 0; // sum values from 1 through 10 inclusive for (int val = 1; val <= 10; ++val) sum += val;// equivalent to sum = sum + val std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl; return 0; } 这段程序定义了3个名字: main、sum和val,同时使用了命名空间名字std,该空间提供了2个名字cout和 cin供程序使用。 名字main定义于所有花括号之外,它和其他大多数定义在函数体之外的名字一样拥有**全局作用域**(global scope)。一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用。 名字sum定义于main函数所限定的作用域之内,从声明sum开始直到main函数结束为止都可以访问它,但是出了main函数所在的块就无法访问了,因此说变量sum拥有**块作用域**(block scope)。名字val定义于 for语句内,在for语句之内可以访问val,但是在main函数的其他部分就不能访问它了。 #### 建议:当你第一次使用变量时再定义它 #### 一般来说,在对象第一次被使用的地方附近定义它是一种好的选择,因为这样做有助于更容易地找到变量的定义。更重要的是,当变量的定义与它第一次被使用的地方很近时,我们也会赋给它一个比较合理的初始值。 #### 嵌套的作用域 #### 作用域能彼此包含,被包含(或者说被嵌套)的作用域称为**内层作用域**(inner scope),包含着别的作用域的作用域称为**外层作用域**(outer scope)。 作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字: #include <iostream> // Program for illustration purposes only: It is bad style for a function // to use a global variable and also define a local variable with the same name int reused = 42; // reused has global scope int main() { int unique = 0; // unique has block scope // output #1: uses global reused; prints 42 0 std::cout << reused << " " << unique << std::endl; int reused = 0;// new, local object named reused hides global reused // output #2: uses local reused; prints 0 0 std::cout << reused << " " << unique << std::endl; // output #3: explicitly requests the global reused; prints 42 0 std::cout << ::reused << " " << unique << std::endl; return 0; } * 输出\#1出现在局部变量 reused定义之前,因此这条语句使用全局作用域中定义的名字reused,输出42 0。 * 输出\#2发生在局部变量reused定义之后,此时局部变量reused**正在作用域内**(in scope),因此第二条输出语句使用的是局部变量reused而非全局变量,输出0 0。 * 输出\#3使用**作用域操作符**来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。结果是,第三条输出语句使用全局变量reused,输出42 0。 **如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。**(各个变量尽量在可控范围有独一名字) ## 复合类型 ## **复合类型**( compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,本章将介绍其中的两种:引用和指针。 与我们已经掌握的变量声明相比,定义复合类型的变量要复杂很多。 上一节提到,一条简单的**声明语句**由一个**数据类型**和紧随其后的一个**变量名列表**组成。 其实**更通用的描述**是,**一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成**。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。 (Note: ,1条声明语句=1个基本数据类型+1个声明符) **目前**为止,我们所接触的声明语句中,**声明符其实就是变量名**,此时变量的类型也就是声明的基本数据类型。其实还可能有**更复杂的声明符**,它基于基本数据类型得到更复杂的类型,并把它指定给变量。 (系好安全带吧,少年!) ### 引用 ### C++11中新增了一种引用:所谓的“**右值引用**(rvalue reference)”,之后会做更详细的介绍。这种引用主要用于内置类。 严格来说,当我们使用术语“引用(reference)”时,指的其实是“**左值引用**(Ivalue reference)”。 -------------------- **引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型**。 通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名: int ival = 1024; int &refVal = ival; // refVal refers to (is another name for) ival int &refVal2; // error: a reference must be initialized 一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值**绑定**(bind)在一起,**而不是将初始值拷贝给引用**。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。(引用一次性的) #### 引用即别名 #### 引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。 (**对象是具有某种数据类型的内存空间**)(引用只是对象的别名) 定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的: refVal = 2; // assigns 2 to the object to which refVal refers, i.e., to ival int ii = refVal; // same as ii = ival 为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值: // ok: refVal3 is bound to the object to which refVal is bound, i.e., to ival int &refVal3 = refVal; // initializes i from the value in the object to which refVal is bound int i = refVal; // ok: initializes i to the same value as ival 因为引用本身不是一个对象,所以不能定义引用的引用。 #### 引用的定义 #### 允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头: int i = 1024, i2 = 2048; // i and i2 are both ints int &r = i, r2 = i2; // r is a reference bound to i; r2 is an int int i3 = 1024, &ri = i3; // i3 is an int; ri is a reference bound to i3 int &r3 = i3, &r4 = i2; // both r3 and r4 are references 所有引用的类型都要和与之绑定的对象严格匹配。而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起: int &refVal4 = 10; // error: initializer must be an object double dval = 3.14; int &refVal5 = dval; // error: initializer must be an int object (Note: **引用(reference)为对象起了另外一个名字(起别名),引用类型引用(refers to)另外一种类型。**) ### 指针 ### 指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。 然而指针与引用相比又有很多**不同点**: 1. 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后**指向几个不同**的对象。(不像引用那样一次性) 2. 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。 **指针通常难以理解,即使是有经验的程序员也常常因为调试指针引发的错误而被备受折磨。** 定义指针类型的方法将声明符写成\*d的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号\*: int *ip1, *ip2; // both ip1 and ip2 are pointers to int double dp, *dp2; // dp2 is a pointer to double; dp is a double #### 获取对象的地址 #### 指针存放某个对象的地址,要想获取该地址,需要使用**取地址符 address-of operator**(操作符&): int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival 第二条语句把p定义为一个指向 int 的指针,随后初始化p令其指向名为 ival的int对象。 因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。 (Note: **&取地址符** 跟 引用&号是两码事,注意区分) **其他所有指针的类型都要和它所指向的对象严格匹配**:(有两种例外情况,日后再介绍) (**对象是具有某种数据类型的内存空间**) double dval; double *pd = &dval; // ok: initializer is the address of a double double *pd2 = pd; // ok: initializer is a pointer to double //类型不同,不能乱指 int *pi = pd; // error: types of pi and pd differ pi = &dval; // error: assigning the address of a double to a pointer to int 因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。 #### 指针值 #### 指针的值(即地址)应属下列4种状态之一: 1. 指向一个对象。 2. 指向紧邻对象所占空间的下一个位置。(用在数组) 3. 空指针,意味着指针没有指向任何对象。 4. 无效指针,也就是上述情况之外的其他值。 试图拷贝或以其他方式访问无效指针的值都将引发错误。**编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的**。访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效。 尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体对象,所以试图访问此类指针(假定的)对象的行为不被允许。如果这样做了,后果也无法预计。 #### 利用指针访问对象 #### 如果指针指向了一个对象,则允许使用**解引用符 dereference operator**(操作符\*)来访问该对象: int ival = 42; int *p = &ival; // p holds the address of ival; p is a pointer to ival cout << *p; // * yields the object to which p points; prints 42 -------------------- (Note: \* & 位置不同傻傻分不清楚。) (**一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成**) int a = 1; int &aa = a;//这里&为引用声明符(这名我自起的) int ival = 42;jie int *p = &ival;//这里*为指针声明符(这名我自起的),&为取地址符 cout << *p;//这里*为解引用符 -------------------- 对指针解引用会得出所指的对象,因此如果给**解引用**的结果赋值,实际上也就是给指针所指的对象赋值: *p = 0; // * yields the object; we assign a new value to ival through p,这里*是解引用符 cout << *p; // prints 0 如上述程序所示,为\*p赋值实际上是为p所指的对象赋值。 **解引用操作仅适用于那些确实指向了某个对象的有效指针。** #### 关键概念:一符多义(& 与 \*) #### 像&和\*这样的符号,既能用作表达式里的运算符(&**按位与符**或**取址符**,***乘法符**或**解引用符*\*),也能作为声明的一部分出现(&**引用声明符**,\***指针声明符**),符号的上下文决定了符号的意义: int i = 42; int &r = i; //这里&为引用声明符, & follows a type and is part of a declaration; r is a reference int *p; //这里*为指针声明符, * follows a type and is part of a declaration; p is a pointer p = &i; // 这里&为取值符, & is used in an expression as the address-of operator *p = i; // 这里*为解引用符 * is used in an expression as the dereference operator int &r2 = *p; // & is part of the declaration; * is the dereference operator * 在声明语句中,&和\*用于组成复合类型(我个人将它们分别称为**引用声明符**、**指针声明符**)。 * 在表达式中,&和\*又转变成运算符(操作数为1个时,分别称为**取值符**、**解引用符**。操作数为2个时,分别称为**按位与符**、**乘法符**)。 在不同场景下出现的虽然是同一个符号,但是由于含义截然不同,所以我们完全可以把它当作不同的符号来看待。 #### 空指针 #### 空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法: int *p1 = nullptr; // equivalent to int *p1 = 0; int *p2 = 0; // directly initializes p2 from the literal constant 0 // must #include cstdlib int *p3 = NULL; // equivalent to int *p3 = 0; 得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。另一种办法就如对p2的定义一样,也可以通过将指针初始化为字面值0来生成空指针。 过去的程序还会用到一个名为NULL的**预处理变量**(preprocessor variable)来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0。 稍微介绍一点关于预处理器的知识,现在只要知道预处理器是运行于编译过程之前的一段程序就可以了。预处理变量不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无须在前面加上std: :。 当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。在新标准下,**现在的C++程序最好使用nullptr,同时尽量避免使用NULL**。 把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。 int zero = 0; pi = zero; // error: cannot assign an int to a pointer #### 建议:初始化所有指针 #### 使用未经初始化的指针是引发运行时错误的一大原因。(像Java的NullPointerException) 和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。 在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的了。(指针未初始化会出现的糟糕状况) 因此**建议初始化所有的指针**,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。 #### 赋值和指针 #### 指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同: 1. 其中最重要的一点就是**引用**本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。 2. **指针**和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象: int i = 42; int *pi = 0; // pi is initialized but addresses no object int *pi2 = &i; // pi2 initialized to hold the address of i int *pi3; // if pi3 is defined inside a block, pi3 is uninitialized pi3 = pi2; // pi3 and pi2 address the same object, e.g., i pi2 = 0; // pi2 now addresses no object 有时候要想搞清楚一条赋值语句到底是改变了指针的值还是改变了指针所指对象的值不太容易,**最好的办法就是记住赋值永远改变的是等号左侧的对象**。当写出如下语句时, pi = &ival; // value in pi is changed; pi now points to ival 意思是为 pi赋一个新的值,也就是改变了那个存放在pi内的地址值。相反的,如果写出如下语句, *pi = 0; // value in ival is changed; pi is unchanged 则\*pi(也就是指针pi指向的那个对象)发生改变。 #### 其他指针操作 #### 只要指针拥有一个合法值,就能将它用在条件表达式中。和采用算术值作为条件遵循的规则类似,如果指针的值是0,条件取false: int ival = 1024; int *pi = 0; // pi is a valid, null pointer int *pi2 = &ival; // pi2 is a valid pointer that holds the address of ival if (pi) // pi has value 0, so condition evaluates as false // ... if (pi2) // pi2 points to ival, so it is not 0; the condition evaluates as true // ... 任何非0指针对应的条件值都是true。 对于两个类型相同的合法指针,可以用相等操作符(=)或不相等操作符(!=)来比较它们,比较的结果是布尔类型。如果两个指针存放的地址值相同,则它们相等;反之它们不相等。 这里两个指针存放的地址值相同(两个指针相等)有三种可能: 1. 它们都为空、 2. 它们都指向同一个对象, 3. 它们都指向了同一个对象的下一地址(???这情况不太懂) 需要注意的是,一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等。 因为上述操作要用到指针的值,所以不论是作为条件出现还是参与比较运算(再数组中用),都必须使用合法指针,使用非法指针作为条件或进行比较都会引发不可预计的后果。 #### void\* 指针 #### void\*是一种特殊的指针类型,可用于存放任意对象的地址。一个void\*指针存放着一个地址,这一点和其他指针类似。 不同的是,我们对该地址中到底是个什么类型的对象并不了解: double obj = 3.14, *pd = &obj; // ok: void* can hold the address value of any data pointer type void *pv = &obj; // obj can be an object of any type pv = pd; // pv can hold a pointer to any type 利用void\*指针能做的事儿比较有限: * 拿它和别的指针比较、 * 作为函数的输入或输出, * 赋给另外一个void\*指针。 不能直接操作void\*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。 概括说来,以void\*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象,不过,是有获取void\*指针所存地址的方法,日后介绍。 ### 理解复合类型的声明 ### 如前所述,变量的定义包括一个基本数据类型(base type)和一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量: // i is an int; p is a pointer to int; r is a reference to int int i = 1024, *p = &i, &r = i; **很多程序员容易迷惑于基本数据类型和类型修饰符的关系,其实后者不过是声明符的一部分罢了。** #### 定义多个变量 #### 经常有一种观点会误以为,在定义语句中,类型修饰符(\*或s)作用于本次定义的全部变量。造成这种错误看法的原因有很多,其中之一是我们可以把空格写在类型修饰符和变量名中间: int* p; // legal but might be misleading 我们说这种写法可能产生误导是因为int\*放在一起好像是这条语句中所有变量共同的类型一样。其实恰恰相反,基本数据类型是int而非int\*。**\*仅仅是修饰了p**而已,对该声明语句中的其他变量,它并不产生任何作用: int* p1, p2;// p1 is a pointer to int; p2 is an int 涉及指针或引用的声明,**一般有两种写法**。 第一种把修饰符和变量标识符写在一起:(推荐) int *p1, *p2; // both p1 and p2 are pointers to int 这种形式着重强调**变量具有的复合类型**。 第二种把修饰符和类型名写在一起,并且每条语句只定义一个变量: int* p1; // p1 is a pointer to int int* p2; // p2 is a pointer to int 这种形式着重强调**本次声明定义了一种复合类型**。 **上述两种定义指针或引用的不同方法没有孰对孰错之分,关键是选择并坚持其中的一种写法,不要总是变来变去。** 我们接下都采用**第一种写法**,将\*(或是&)与变量名连在一起。(声明符与变量) #### 指向指针的指针 #### 一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。 通过\*的个数可以区分指针的级别。也就是说,\*\*表示指向指针的指针,\*\*\*表示指向指针的指针的指针,以此类推: int ival = 1024; int *pi = &ival; // pi points to an int int **ppi = π // ppi points to a pointer to an int 此处pi是指向int型数的指针,而ppi是指向int型指针的指针,下图描述了它们之间的关系。 ![在这里插入图片描述][20210425151713132.png_pic_center] 解引用int型指针会得到一个int型的数,同样,解引用指向指针的指针会得到一个指针。此时为了访问最原始的那个对象,需要对指针的指针做两次解引用: cout << "The value of ival\n" << "direct value: " << ival << "\n"li << "indirect value: " << *pi << "\n" << "doubly indirect value: " << **ppi//两次解引用 << endl; 该程序使用三种不同的方式输出了变量ival的值: 1. 第一种直接输出; 2. 第二种通过int型指针pi输出; 3. 第三种两次解引用ppi,取得ival的值。 #### 指向指针的引用(指针的别名)(从右向左阅读理解) #### 引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:(指针不能指向引用) int i = 42; int *p; // p is a pointer to int int *&r = p; // r is a reference to the pointer p //对指针的引用 //int &r = *p;//可以有这种写法,有点懵,日后注意 r = &i; // r refers to a pointer; assigning &i to r makes p point to i *r = 0; // dereferencing r yields i, the object to which p points; changes i to 0 要理解r的类型到底是什么,最简单的办法是**从右向左**阅读r的定义。 离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号\*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。 **面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。** -------------------- 我写的小程序来解惑: #include <iostream> #include "Sales_item.h" int main() { int i = 1; int *p = &i; int &r = *p; int *&r2 = p;//对指针的引用 //int &r1 = 1; //error int *p2 = &r; //int *p2 = r; //error std::cout<<r<<std::endl; std::cout<<*p2<<std::endl; std::cout<<*r2<<std::endl; std::cout<<p<<std::endl; std::cout<<&r<<std::endl; std::cout<<r2<<std::endl; std::cout<<p2<<std::endl; return 0; } 输出结果: 1 1 1 0x61fe34 0x61fe34 0x61fe34 0x61fe34 Process returned 0 (0x0) execution time : 0.048 s Press any key to continue. (Note:) (引用与指针一起来声明,有点懵,记住:**引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用**) (引用->指针 OK,指针->引用 NO) int i = 1; int *p = &i; int &r = *p; //int *p2 = r; //指针->引用 NO int *p2 = &r; //这个r还是可以取址的,这是懵点, (引用只是对象的别名) ## const限定符 ## ### 概述 ### 有时我们希望定义这样一种变量,它的值不能被改变。(Note: **只读变量**) 例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量的类型加以限定: const int bufSize = 512; //输入缓冲区大小 这样就把bufSize定义成了一个常量。任何试图为bufSize赋值的行为都将引发错误: bufSize = 1024; //错误:试图向const对象写值 因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往,初始值可以是任意复杂的表达式: const int i = get_size(); // ok: initialized at run time const int j = 42; // ok: initialized at compile time const int k; // error: k is uninitialized const #### 初始化和const #### 正如之前反复提到的,对象的类型决定了其上的操作。与非 const类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合。主要的限制就是只能在const类型的对象上执行不改变其内容的操作。 例如,const int和普通的int一样都能参与算术运算,也都能转换成一个布尔值,等等。 在不改变 const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要: int i = 42; const int ci = i; // ok: the value in i is copied into ci int j = ci; // ok: the value in ci is copied into j 尽管ci是整型常量,但无论如何ci中的值还是一个整型数。ci的常量特征仅仅在执行改变ci的操作时才会发挥作用。当用ci去初始化j时,根本无须在意ci是不是一个常量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。 #### 默认状态下,const对象仅在文件内有效(const 常量在多文件中使用方法) #### 当以编译时初始化的方式定义一个const对象时,就如对bufsize的定义一样: const int bufSize = 512;//输入缓冲区大小 编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。 为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,**默认情况下,const对象被设定为仅在文件内有效**。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。 某些时候有这样一种 const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。 解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了: //file_1.cc定义并初始化了一个常量,该常量能被其他文件访问 extern const int bufSize = fcn(); //file_l.h头文件 extern const int bufSize; //与file_1.cc中定义的bufSize是同一个 如上述程序所示,file\_1.cc定义并初始化了bufsize。因为这条语句包含了初始值,所以它(显然〉是一次定义。然而,因为bufsize是一个常量,必须用extern加以限定使其被其他文件使用。 file\_1.h头文件中的声明也由extern做了限定,其作用是指明bufsize并非本文件所独有,它的定义将在别处出现。 **如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。** ### const的引用 ### 可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象: const int ci = 1024; const int &r1 = ci; // ok: both reference and underlying object are const r1 = 42; // error: r1 is a reference to const int &r2 = ci; // error: non const reference to a const object 因为不允许直接为ci赋值,当然也就不能通过引用去改变ci。因此,对r2的初始化是错误的。假设该初始化合法,则可以通过r2来改变它引用对象的值,这显然是不正确的。 #### 术语:常量引用是对const的引用 #### C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这就是个简称而已。 严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C+语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。 #### 初始化和对const的引用 #### 前文提到,引用的类型必须与其所引用对象的类型一致,但是有两个例外。 **第一种例外情况**就是在**初始化常量引用时允许用任意表达式作为初始值**,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式: int i = 42; const int &r1 = i; // we can bind a const int& to a plain int object const int &r2 = 42; // ok: r1 is a reference to const const int &r3 = r1 * 2; // ok: r3 is a reference to const int &r4 = r * 2; // error: r4 is a plain, non const reference 要想理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到**另外一种类型**上时到底发生了什么: double dval = 3.14; const int &ri = dval; 此处ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式: const int temp = dval;//由双精度浮点数生成一个临时的整型常量 const int &ri = temp;//让ri绑定这个临时量 在这种情况下,ri绑定了一个**临时量**(temporary)对象。**所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象**。C++程序员们常常把临时量对象简称为临时量。 接下来探讨**当ri不是常量**时,如果执行了类似于上面的初始化过程将带来什么样的后果。 如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时量而非dval。程序员既然让 ri引用dval,就肯定想通过ri改变dval的值,否则干什么要给ri赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++语言也就把这种行为归为非法。 (Note: 第二种例外情况在哪???) #### 对const的引用可能引用一个并非const的对象 #### 必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值: int i = 42; int &r1 = i; // r1 bound to i const int &r2 = i; // r2 also bound to i; but cannot be used to change i r1 = 0; // r1 is not const; i is now 0 r2 = 0; // error: r2 is a reference to const r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。(防不胜防)(Note: 真麻烦) ### 指针和const ### 与引用一样,也可以令指针指向常量或非常量。类似于常量引用,**指向常量的指针**(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针: const double pi = 3.14; // pi is const; its value may not be changed double *ptr = π // error: ptr is a plain pointer const double *cptr = π // ok: cptr may point to a double that is const *cptr = 42; // error: cannot assign to *cptr 前文提到,**指针的类型必须与其所指对象的类型一致**,但是有**两个例外**。**第一种例外**情况是允许令一个指向常量的指针指向一个非常量对象: double dval = 3.14; // dval is a double; its value can be changed cptr = &dval; // ok: but can't change dval through cptr 和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。(也就是说还有其他路子改变对象的值,可查看第一节)。 试试这样想吧:所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。It may be helpful to think of pointers and references to const as pointers or references “that think they point or refer to const. (第二中例外没写) #### const指针 #### (上一节讲的是指向常量的指针,这节将常量指针) 指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。**常量指针**(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把\*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值: int errNumb = 0; int *const curErr = &errNumb; // curErr will always point to errNumb const double pi = 3.14159; const double *const pip = π // pip is a const pointer to a const object 前文提到,要想弄清楚这些声明的含义最行之有效的办法是**从右向左**阅读。(Note:**这是理解这些复杂申明语句的关键**) 此例中, 1. 离curErr最近的符号是const,意味着curErr本身是一个**常量对象**,对象的类型由声明符的其余部分确定。 2. 声明符中的下一个符号是\*,意思是curErr是一个**常量指针**。 3. 最后,该声明语句的基本数据类型部分确定了**常量指针指向的是一个int对象**。 **对象是指一块能存储数据并具有某种类型的内存空间**。 与之相似,我们也能推断出,**pip是一个常量指针,它指向的对象是一个双精度浮点型常量。** 指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例如,**pip是一个指向常量的常量指针,则不论是 pip 所指的对象值还是pip自己存储的那个地址都不能改变。** 相反的,curErr指向的是一个一般的非常量整数,那么就完全可以用curErr去修改errNumb 的值: *pip = 2.72; // error: pip is a pointer to const//指向 // if the object to which curErr points (i.e., errNumb) is nonzero if (*curErr) { errorHandler(); *curErr = 0; // ok: reset the value of the object to which curErr is bound curr常量指针指向的是一个int对象,这对象可以改变 } ### 顶层const ### 如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。 用名词**顶层const**(top-level const)表示**指针本身是个常量**,而用名词**底层const** (low-level const)表示**指针所指的对象是一个常量**。 更一般的: **顶层const**可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。 **底层const**则与**指针和引用等复合类型**的基本类型部分有关。比较**特殊**的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显: (助记:顶常底复) int i = 0; //顶层const:表示任意的对象是常量 int *const p1 = &i; // we can't change the value of p1; const is top-level const int ci = 42; // we cannot change ci; const is top-level //底层const:与指针和引用等复合类型的基本类型部分有关 const int *p2 = &ci; // we can change p2; const is low-level const int &r = ci; // const in reference types is always low-level //顶层const又是底层const const int *const p3 = p2; // right-most const is top-level, left-most is not (Note:根据const所在**位置**来判断顶层const或底层const不管用) 当执行对象的**拷贝操作**时,常量是顶层const还是底层const区别明显。 其中,顶层const不受什么影响: i = ci; // ok: copying the value of ci; top-level const in ci is ignored p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored 执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。 另一方面,底层 const 的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行: int *p = p3; // error: p3 has a low-level const but p doesn't p2 = p3; // ok: p2 has the same low-level const qualification as p3 p2 = &i; // ok: we can convert int* to const int* int &r = ci; // error: can't bind an ordinary int& to a const int object const int &r2 = i; // ok: can bind const int& to plain int p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响。 (Note:这节不好懂) ### constexpr和常量表达式 ### 常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如: const int max_files = 20; // max_files is a constant expression const int limit = max_files + 1; // limit is a constant expression int staff_size = 27; // staff_size is not a constant expression const int sz = get_size(); // sz is not a constant expression 尽管staff\_size的初始值是个字面值常量,但由于它的数据类型只是一个普通int而非const int,所以它不属于常量表达式。另一方面,尽管 sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。 #### constexpr变量 #### 在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个 const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。 **C++11新标准**规定,允许将变量声明为**constexpr**类型以便由**编译器来验证变量的值是否是一个常量表达式**。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化: constexpr int mf = 20; // 20 is a constant expression constexpr int limit = mf + 1; // mf + 1 is a constant expression constexpr int sz = size(); // ok only if size is a constexpr function 尽管不能使用普通函数作为constexpr变量的初始值,但是,将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化 constexpr变量了。 **一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr类型。** #### 字面值类型 #### **常量表达式的值需要在编译时就得到计算**,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”( literal type)。 到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。 自定义类sales\_item、IO 库、string 类型则不属于字面值类型,也就**不能**被定义成constexpr。 尽管指针和引用都能定义成constexpr,但它们的初始值却**受到严格限制**。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。 将要提到,**函数体内定义的变量**一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。 将提到,允许函数定义一类有效范围超出函数本身的变量,**这类变量和定义在函数体之外的变量**一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr 指针也能指向这样的变量。 (Note:哪些**能**定义constexpr,哪些**不能**定义constexpr) #### 指针和constexpr #### 必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关: const int *p = nullptr; // p is a pointer to a const int constexpr int *q = nullptr; // q is a const pointer to int p和q的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const。 与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量: constexpr int *np = nullptr; // np is a constant pointer to int that is null int j = 0; constexpr int i = 42; // type of i is const int // i and j must be defined outside any function constexpr const int *p = &i; // p is a constant pointer to the const int i constexpr int *p1 = &j; // p1 is a constant pointer to the int j ## 处理类型 ## 随着程序越来越复杂,程序中用到的类型也越来越复杂,这种复杂性体现在两个方面。 1. 一些类型难于“拼写”,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。 2. 有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文中寻求帮助。 ### 类型别名 ### 类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。 有两种方法可用于定义类型别名。传统的方法是使用关键字**typedef**: typedef double wages; // wages is a synonym for double typedef wages base, *p; // base is a synonym for double, p for double* 其中,关键字typedef作为声明语句中的基本数据类型的一部分出现。含有 typedef 的声明语句定义的不再是变量而是**类型别名**。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。 C++11新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名: using SI = Sales_item; // SI is a synonym for Sales_item 这种方法用关键字using 作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。 类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名: wages hourly, weekly; // same as double hourly, weekly; SI item; // same as Sales_item item #### 指针、常量和类型别名 #### 如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char\*的别名: typedef char *pstring; const pstring cstr = 0; // cstr is a constant pointer to char const pstring *ps; // ps is a pointer to a constant pointer to char//从指向指针的指针 上述两条声明语句的基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。pstring 实际上是指向char 的指针,因此,const pstring 就是指向char的常量指针,而非指向常量字符的指针。 遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子,以理解该语句的含义:(Note:**不能像代数那样代入**) const char *cstr = 0; // wrong interpretation of const pstring cstr 再强调一遍:这种理解是错误的。 声明语句中用到pstring 时,其基本数据类型是**指针**。可是用char\*重写了声明语句后,数据类型就变成了char,\*成为了声明符的一部分。However, this interpretation is wrong. When we use pstring in a declaration, the base type of the declaration is a pointer type. When we rewrite the declaration using char\*, the base type is char and the \* is part of the declarator. 这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了**一个指向char的常量指针**,改写后的形式则声明了**一个指向const char的指针**。 In this case, const char is the base type. This rewrite declares cstr as a pointer to const char rather than as a const pointer to char. (Note:) const (char *)cstr = 0;//我是这样理解的,一个指向char的常量指针 ### auto类型说明符 ### **C++11新特性** 编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了**auto**类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如 double)不同,**auto 让编译器通过初始值来推算变量的类型**。显然,auto定义的变量必须有初始值: // the type of item is deduced from the type of the result of adding val1 and val2 auto item = val1 + val2; // item initialized to the result of val1 + val2 此处编译器将根据val1和val2相加的结果来推断item的类型。如果val1和val2是类Sales \_item(具体查看上一章)的对象,则item的类型就是Sales\_item;如果这两个变量的类型是double,则item的类型就是double,以此类推。 使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样: auto i = 0, *p = &i; // ok: i is int and p is a pointer to int auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi #### 复合类型、常量和 auto #### 编译器推断出来的auto类型有时候和初始值的类型并不完全一样,**编译器会适当地改变结果类型使其更符合初始化规则**。 首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型: int i = 0, &r = i; auto a = r; // a is an int (r is an alias for i, which has type int) 其次,auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时: const int ci = i, &cr = ci; auto b = ci; // b is an int (top-level const in ci is dropped) auto c = cr; // c is an int (cr is an alias for ci whose const is top-level) auto d = &i; // d is an int*(& of an int object is int*) auto e = &ci; // e is const int*(& of a const object is low-level const) 如果希望推断出的auto类型是一个顶层const,需要明确指出: const auto f = ci; // deduced type of ci is int; f has type const int 还可以将引用的类型设为auto,此时原来的初始化规则仍然适用: auto &g = ci; // g is a const int& that is bound to ci auto &h = 42; // error: we can't bind a plain reference to a literal const auto &j = 42; // ok: we can bind a const reference to a literal 设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。 要在一条语句中定义多个变量,切记,**符号&和\*只从属于某个声明符,而非基本数据类型的一部分**,因此初始值必须是同一种类型: auto k = ci, &l = i; // k is int; l is int& auto &m = ci, *p = &ci; // m is a const int&;p is a pointer to const int // error: type deduced from i is int; type deduced from &ci is const int auto &n = i, *p2 = &ci; (Note:**符号&和\*只从属于某个声明符,而非基本数据类型的一部分**,这一句很重要)。 ### decltype类型指示符 ### **C++11新标准** 有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,**但是不想用该表达式的值初始化变量**。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值: decltype(f()) sum = x; // sum has whatever type f returns 编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum 指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。 decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则 decltype返回该变量的类型(包括顶层const和引用在内): const int ci = 0, &cj = ci; decltype(ci) x = 0; // x has type const int decltype(cj) y = x; // y has type const int& and is bound to x decltype(cj) z; // error: z is a reference and must be initialized 因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化。 需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在 decltype 处是一个例外。 #### decltype和引用 #### 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。有些表达式将向decltype返回一个引用类型。一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值: // decltype of an expression can be a reference type int i = 42, *p = &i, &r = i; decltype(r + 0) b; // ok: addition yields an int; b is an (uninitialized) int decltype(*p) c; // error: c is int& and must be initialized 因为r是一个引用,因此 decltype®的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。 另一方面,如果表达式的内容是解引用操作,则decltype 将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype (\*p)的结果类型就是int&,而非int。 **decltype和 auto的另一处重要区别**是,decltype的结果类型与表达式形式密切相关。有一种情况需要特别注意:对于 decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。 * 如果 decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型; * 如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。 变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型: // decltype of a parenthesized variable is always a reference decltype((i)) d; // error: d is int& and must be initialized decltype(i) e; // ok: e is an (uninitialized) int **切记**:decltype ((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当 variable本身就是一个引用时才是引用。 ## 自定义数据结构 ## 从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。 举一个例子,我们的Sales\_item类把书本的ISBN编号、售出量及销售收入等数据组织在了一起,并且提供诸如isbn函数、>>、<<、+、+=等运算在内的一系列操作,sales\_item类就是一个数据结构。 C++语言允许用户以类的形式自定义数据类型,而库类型string、 istream、ostream等也都是以类的形式定义的,就像上一章Sales\_item类型一样。 ### 定义Sales\_data类型 ### 尽管我们还写不出完整的Sales\_item类,但是可以尝试着把那些数据元素组织到一起形成一个简单点儿的类。初步的想法是用户能直接访问其中的数据元素,也能实现一些基本的操作。 既然我们筹划的**这个数据结构不带有任何运算功能**,不妨把它命名为 Sales\_data以示与Sales\_item的区别。Sales\_data初步定义如下: struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; 我们的类以关键字struct开始,紧跟着类名和类体(其中类体部分可以为空)。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。 类体右侧的表示结束的花括号后必须写一个分号,这是因为类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少: struct Sales_data { /* ... */ } accum, trans, *salesptr; // equivalent, but better way to define these objects struct Sales_data { /* ... */ }; Sales_data accum, trans, *salesptr; 分号表示声明符(通常为空)的结束。一般来说,**最好不要把对象的定义和类的定义放在一起**。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。 **很多新手程序员经常忘了在类定义的最后加上分号。** #### 类数据成员 #### 类体定义类的成员,我们的类只有数据成员(data member)。类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他Sales\_data的对象。 定义数据成员的方法和定义普通变量一样:**首先说明一个基本类型,随后紧跟一个或多个声明符**。我们的类有3个数据成员: 1. 一个名为bookNo的string 成员、 2. 一个名为units\_sold的unsigned 成员 3. 一个名为revenue的 double 成员。 每个Sales\_data的对象都将包括这3个数据成员。 **C++11新标准**规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化(函数体内的默认不初始化,函数体外的都默认初始化)。因此当定义Sales\_data的对象时,units\_sold和revenue都将初始化为0,bookNo将初始化为空字符串。 用户可以使用C++语言提供的另外一个关键字class来定义自己的数据结构,到时也将说明现在我们使用struct 的原因。现在使用struct定义自己的数据类型。 和Sales\_item类不同的是,我们自定义的sales\_data类没有提供任何操作,sales\_data类的使用者如果想执行什么操作就必须自己动手实现。例如,写一段程序实现求两次交易相加结果的功能。程序的输入是下面这两条交易记录: 0-201-78345-x 3 20.00 0-201-78345-x 2 25.00 每笔交易记录着图书的ISBN编号、售出数量和售出单价。 ### 使用Sales\_data类 ### 因为sales\_data类没有提供任何操作,所以我们必须自己编码实现输入、输出和相加的功能。假设已知Sales\_data类定义于Sales\_data.h文件内。 #include <iostream> #include <string> #include "Sales_data.h" int main() { Sales_data data1, data2; // code to read into data1 and data2 // code to check whether data1 and data2 have the same ISBN // and if so print the sum of data1 and data2 } #### Sales\_data对象读入数据 #### 在此之前,我们先了解一点儿关于string 的知识以便定义和使用我们的ISBN成员。string类型其实就是字符的序列,它的操作有>>、<<和==等,功能分别是读入字符串、写出字符串和比较字符串。这样我们就能书写代码读入两笔交易了: double price = 0; // price per book, used to calculate total revenue // read the first transactions: ISBN, number of books sold, price per book std::cin >> data1.bookNo >> data1.units_sold >> price; // calculate total revenue from price and units_sold data1.revenue = data1.units_sold * price; // read the second transaction std::cin >> data2.bookNo >> data2.units_sold >> price; data2.revenue = data2.units_sold * price; #### 输出两个Sales\_data对象的和 #### 剩下的工作就是检查两笔交易涉及的工SBN编号是否相同了。如果相同输出它们的和,否则输出一条报错信息: if (data1.bookNo == data2.bookNo) { unsigned totalCnt = data1.units_sold + data2.units_sold; double totalRevenue = data1.revenue + data2.revenue; // print: ISBN, total sold, total revenue, average price per book std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " "; if (totalCnt != 0) std::cout << totalRevenue/totalCnt << std::endl; else std::cout << "(no sales)" << std::endl; return 0; // indicate success } else { // transactions weren't for the same ISBN std::cerr << "Data must refer to the same ISBN" << std::endl; return -1; // indicate failure } ### 编写自己的头文件 ### **函数体内定义类**(先了解一下),但是这样的类毕竞受到了一些限制。所以,类一般都不定义在函数体内。当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义。而且,如果要在不同文件中使用同一个类,类的定义就必须保持一致。 **为了确保各个文件中类的定义一致,类通常被定义在头文件中**,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string 的头文件中定义。又如,我们应该把Sales\_data类定义在名为sales\_data.h 的头文件中。 头文件通常包含那些只能被定义一次的实体,如类、const和 constexpr变量等。 头文件也经常用到其他头文件的功能。 例如,我们的Sales\_data类包含有一个string 成员,所以Sales\_data.h必须包含string.h头文件。同时,使用sales\_data类的程序为了能操作bookNo成员需要再一次包含string.h头文件。 这样,事实上使用sales\_data类的程序就先后**两次包含**了string.h头文件:一次是直接包含的,另有一次是随着包含sales\_data.h 被隐式地包含进来的。有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作。 **头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。** #### 预处理器 #### 确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能\#include,当预处理器看到\#include标记时就会用指定的头文件的内容代替\#include。 C++程序还会用到的一项预处理功能是**头文件保护符**(header guard),头文件保护符依赖于预处理变量。**预处理变量有两种状态**:已定义和未定义。 * **\#define**指令把一个名字设定为预处理变量, 另外两个指令则分别检查某个指定的预处理变量是否已经定义: * **\#ifdef**当且仅当变量已定义时为真, * **\#ifndef**当且仅当变量未定义时为真。 一旦检查结果为真,则执行后续操作直至遇到 **\#endif** 指令为止。 **使用这些功能就能有效地防止重复包含的发生**: #ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; #endif 第一次包含sales\_data.h时,\#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到\#endif 为止。此时,预处理变量SALES\_DATA\_H的值将变为已定义,而且sales\_data. h也会被拷贝到我们的程序中来。 后面如果再一次包含sales\_data.h,则\#ifndef 的检查结果将为假,编译器将忽略\#ifndef到\#endif之间的部分。 **预处理变量无视C++语言中关于作用域的规则。** 整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。 **头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要。** (Note:**日后写头文件都要设置保护符**) [20210425151623456.png_pic_center]: /images/20221022/f9e5f148116f4a8a8f34a6a4e9c77c4b.png [20210425151713132.png_pic_center]: /images/20221022/aa84a3f6d11b48da877fd47257dc907c.png
相关 《C++ Primer 5th》笔记(9 / 19):顺序容器 文章目录 顺序容器概述 确定使用哪种顺序容器 容器库概览 迭代器 迭代器范围 淩亂°似流年/ 2023年01月21日 02:21/ 0 赞/ 13 阅读
相关 《C++ Primer 5th》笔记(8 / 19):IO库 文章目录 IO类 IO类型间的关系 IO对象无拷贝或赋值 条件状态 素颜马尾好姑娘i/ 2023年01月19日 08:19/ 0 赞/ 19 阅读
相关 《C++ Primer 5th》笔记(7 / 19):类 文章目录 定义抽象数据类型 设计Sales\_data类 关键概念:不同的编程角色 缺乏、安全感/ 2023年01月19日 04:47/ 0 赞/ 19 阅读
相关 《C++ Primer 5th》笔记(5 / 19):语句 文章目录 简单语句 空语句 别漏写分号,也别多写分号 复合语句(块) 语句作用域 墨蓝/ 2023年01月18日 04:28/ 0 赞/ 127 阅读
相关 《C++ Primer 5th》笔记(4 / 19):表达式 文章目录 基础 基本概念 组合运算符和运算对象 运算对象转换 缺乏、安全感/ 2023年01月17日 15:00/ 0 赞/ 156 阅读
相关 《C++ Primer 5th》笔记(2 / 19):变量和基本类型 文章目录 基本内置类型 算术类型 内置类型的机器实现(类型在物理层面上的说明) àì夳堔傛蜴生んèń/ 2023年01月16日 11:09/ 0 赞/ 183 阅读
相关 《C++ Primer 5th》笔记(1 / 19):C++基础 文章目录 编写一个简单的C++程序 编译、运行程序 初识输入输出 注释简介 控制流 爱被打了一巴掌/ 2023年01月14日 15:56/ 0 赞/ 163 阅读
相关 《C++ Primer 5th》笔记(12 / 19):动态内存 文章目录 动态内存与智能指针 shared\_ptr类 make\_shared函数 痛定思痛。/ 2022年10月16日 07:33/ 0 赞/ 215 阅读
相关 《C++ Primer 5th》笔记(11 / 19):关联容器 文章目录 使用关联容器 使用map 使用set 关联容器概述 定义关联容器 一时失言乱红尘/ 2022年10月14日 05:41/ 0 赞/ 357 阅读
还没有评论,来说两句吧...