第2章 变量和基本类型

第2章 变量和基本类型

2.1 基本内置类型

2.1.1 算术类型

基本数据类型:算术类型(arithmetic type)和空类型(void)。

算术类型整型(integral type,包含字符和布尔类型在内)和浮点型
bool 布尔类型 未定义
char 字符 8位
short 短整型 16位
int 整型 16位
long 长整型 32位
long long 长整型 64位
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

执行浮点数运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。

带符号类型和无符号类型

除去布尔类型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。

带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值

2.1.2 类型转换

当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。P33

含有无符号类型的表达式

(1) 把int转换成无符号数的过程和把int直接赋给无符号变量一样

unsigned u = 10;
int i = 42;
std::cout << u + i << std::endl; // 如果int占32位,输出4294967264
>>> 2**32 - 42 + 10
4294967264

(2) 从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值

unsigned u1 = 42, u2 = 10;
std::cout << u2 - u1 << std::endl;
// 4294967264

(3) 用无符号数来写循环,可能会意味着死循环

// 错误:变量 u 永远也不会小于 0,循环条件一直成立
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的结果将会是 2**32 - 1 = 4294967295

解决方法:用while语句来代替for语句,因为前者让我们能够在输出变量之前(而非之后)先减去1

unsigned u = 11; // 确定要输出的最大值,从比它大1的数开始
while (u > 0) {
    --u;         // 先减1,这样最后一次迭代时就会输出0
    std::cout << u << std::endl;
}

分析:
改写后的循环先执行对循环控制变量减 1 的操作,这样最后一次迭代时,进入循环的 u 值为 1。
此时将其减 1,则这次迭代输出的数就是 0;下一次再检验循环条件时,u 的值等于 0 而无法再进入循环。
因为我们要先做减 1 的操作,所以初始化 u 的值应该比要输出的最大值大 1。这里,u 初始化为 11,输出的最大数是 10。

2.1.3 字面值常量

字面值常量(literal)

整型和浮点型字面值
20 /*十进制*/   024 /*八进制*/  0x14 /*十六进制*/
字符和字符串字面值
'a'             // 字符字面值
"Hello World!"  // 字符串字面值

字符串字面值的类型实际上是由常量字符构成的数组(array)。编译器在每个字符串的结尾处添加一个空字符('\0'),因此,字符串字面值的实际长度要比它的内容多1。例如,字面值'A'表示的就是单独的字符A,而字符串"A"则代表了一个字符的数组,该数组包含两个字符:一个是字母A、另一个是空字符。

// 分多行书写的字符串字面值
std::cout << "a really, really long string literal "
             "that spans two lines" << std::endl;
转义序列

换行符 \n\12
横向制表符 \t
纵向制表符 \v
反斜线 \\
回车符 \r
退格符 \b
问号 \?
进纸符 \f
双引号 \"
单引号 \'
报警响铃符 \a\7
空格 \40
空字符 \0

std::cout << '\n';      // 转到新一行
std::cout << "\tHi!\n"; // 输出一个制表符,输出"Hi!",转到新一行

注意:如果反斜线\后面跟着的八进制数字超过 3 个,只有前3个数字与\构成转义序列。例如,"\1234"表示 2 个字符,即八进制数 123 对应的字符以及字符 4。

布尔字面值和指针字面值

truefalse是布尔类型的字面值;nullptr是指针字面值。

2.2 变量

2.2.1 变量定义

初始值
// 正确:price 先被定义并赋值,随后被用于初始化 discount
double price = 109.99, discount = price * 0.16;
// 正确:调用函数 applyDiscount,然后用函数的返回值初始化 salePrice
double salePrice = applyDiscount(price, discount);

注意:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

列表初始化

列表初始化(list initialization),可以用花括号来初始化变量。
特点是:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。

// 这4条语句都可以定义一个名为 units_sold 的int变量并初始化为0
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
默认初始化

如果定义变量时没有指定初值,则变量被默认初始化(default initialized)。

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。
定义域任何函数体之外的变量被初始化为 0。
定义在函数体内部的内置类型变量将不被初始化(uninitialized)。

练习2.9:

(a) std::cin >> int input_value;
// 错误。输入运算符的右侧需要一个明确的变量名称,而非定义变量的语句。改正后的结果是:
int input_value;
    std::cin >> input_value;

(b) int i = { 3.14 };
// 引发警告。该语句定义了一个整形变量 i,但是试图通过列表初始化的方式把浮点数 3.14 赋给 i,这样做将造成小数部分丢失,是一种不被建议的窄化操作。

(c) double salary = wage = 9999.99;
// 错误。该语句试图将 9999.99 分别赋给 salary 和 wage,但是在声明语句中声明多个变量时需要用逗号将变量名隔开,而不能直接用赋值运算符连接。改正后的结果是:
double salary, wage;
    salary = wage = 9999.99;

(d) int i = 3.14;
// 引发警告。该语句定义了一个整型变量 i,但是试图把浮点数 3.14 赋给 i,这样做将造成小数部分丢失,与(b)一样是不被建议的窄化操作。
/* 练习2.10:下列变量的初始值分别是什么?*/

#include <string>

std::string global_str; // 空字符串

int global_int; // 0

int main()
{
    int local_int;  // 不被初始化
    std::string local_str;  // 空字符串
}

string类型的变量来说,因为string类型本身接受无参数的初始化方式,所以不论变量定义在函数内还是函数外,都被默认初始化为空串。

对于内置类型int来说,变量global_int定义在所有函数体之外,根据C++的规定,global_int默认初始化为0;而变量local_int定义在main函数的内部,将不被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到一个未定义的奇异值。

2.2.2 变量声明和定义的关系

声明(declaration) vs 定义(definition)

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:

extern int i; // 声明 i 而非定义 i
int j;        // 声明并定义 j

我们能给由extern关键字标记的变量赋一个初始值,大师这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了。

extern double pi = 3.1416; // 定义

变量能且只能被定义一次,但是可以被多次声明。声明和定义的区别:
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件里,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

关键概念:静态类型: C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查(type checking)。

2.2.3 标识符(identifier)

变量命名规范
  • 变量名一般用小写字母,如index,不要使用IndexINDEX
  • 用户自定义的类名一般以大写字母开头,如Sales_item
  • 如果标识符由多个单词组成,则单词间应有明显区分,如student_loanstudentLoan,不要使用studentloan
  • 标识符必须以字母或下划线开头,不能以数字开头,如int 1_or_2 = 1;是非法的,而int _;是合法的。

C++关键字:

alignascontinuefriendregistertrue
alignofdecltypegotoreinterpret_casttry
asmdefaultifreturntypedef
autodeleteinlineshorttypeid
booldointsignedtypename
breakdoublelongsizeofunion
casedynamic_castmutablestaticunsigned
catchenumnewstatic_castvirtual
char16_texplicitnoexceptstructvoid
char32_texportnullptrswitchvolatile
classexternoperatortemplatewchar_t
constfalseprivatethiswhile
constexprfloatprotectedthread_local
const_castforpublicthrow

C++操作符替代名:

andbitandcomplnot_eqor_eqxor_eq
and_eqbitornotorxor

2.2.4 名字的作用域

#include <iostream>
int main()
{
    int sum = 0;
    for (int val = 1; val <= 10; ++val)
        sum == val;
    std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
    return 0;
}

全局作用域(global scope) vs 块作用域(block scope):

  • 名字main定义于花括号之外,拥有全局作用域(global scope)。
  • 名字sum定义于main函数所限定的作用域之内,从声明 sum 开始直到 main 函数结束为止都可以访问它,但是出了 main 函数所在的块就无法访问了,因此说变量sum拥有块作用域(block scope)。
  • 名字val定义于 for 语句内,在 for 语句之内可以访问 val,但是在 main 函数的其他部分就不能访问它了。

建议:当你第一次使用变量时再定义它。

嵌套的作用域

内层作用域(inner scope) vs 外层作用域(outer scope)

#include <iostream>
// 该程序仅用于说明:函数内部不宜定义与全局变量同名的新变量
int reused = 42;    // reused 拥有全局作用域
int main()
{
    int unique = 0; // unique 拥有块作用域
    // 输出 #1:使用全局变量 reused;输出 42 0
    std::cout << reused << " " << unique << std::endl;
    int reused = 0; // 新建局部变量 reused,覆盖了全局变量 reused
    // 输出 #2:使用局部变量 reused;输出 0 0
    std::cout << reused << " " << unique << std::endl;
    // 输出 #3:显式地访问全局变量 reused;输出 42 0
    std::cout << ::reused << " " << unique << std::endl;
    return 0;
}

输出#3 使用作用域操作符::来覆盖默认的作用域规则,因为全局作用本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。结果是,第三条输出语句使用全局变量reused,输出42 0

2.3 复合类型(compound type)

一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。

2.3.1 引用(reference)

引用(reference)通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:

int ival = 1024;
int &refVal = ival; // refVal 指向 ival (是 ival 的另一个名字)
int &refVal2;       // 报错:引用必须被初始化

refVal = 2;         // 把 2 赋给 refVal 指向的对象,此处即是赋给了 ival
int ii = refVal;    // 与 ii = ival 执行结果一样

int &refVal3 = refVal; // 正确:refVal3 绑定到了那个与 refVal 绑定的对象上,这里就是绑定到 ival 上
int i = refVal;     // 利用与 refVal 绑定的对象的值初始化变量 i。i 被初始化为 ival 的值

在初始化变量时,初始值会被拷贝到新建的对象中。
然而定义引用时,程序把引用和它的初始值 绑定(bind) 在一起。
因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。

int &refVal4 = 10;      // 错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval;    // 错误:此处引用类型的初始值必须是 int 型对象
/*练习2.17 执行下面的代码段将输出什么结果?*/
int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;

解答:程序的输出结果是 10 10 10 10 10 10
引用不是对象,它只是为已经存在的对象起了另外一个名字,因此ri实际上是i的别名。在上述程序中,首先将i赋值为 5,然后把这个值更新为 10。因为rii的引用,所以它们的输出结果是一样的。

2.3.2 指针(pointer)

指针与引用相比有很多不同点:

指针“指向”内存中的某个对象,而引用“绑定到”内存中的某个对象,它们都实现了对其他对象的间接访问,二者的区别主要有两方面:

  • 第一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以指向几个不同的对象;引用不是一个对象,无法令引用重新绑定到另外 一个对象。
  • 第二,指针无须在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值:引用则必须在定义时赋初值。

(1) 指针本身就是一个对象,允许对指针复制和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。

(2) 指针无需在定义时就赋初值。

(3) 和其他内置类型一样,在块作用域内定义的指针,如果没有被初始化,也将拥有一个不确定的值。

定义指针类型的方法将声明符号写成*d的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*

int *ip1, *ip2;     // ip1 和 ip2 都是指向 int 型对象的指针
double dp, *dp2;    // dp2 是指向 double 型对象的指针,dp 是 double 型对象
获取对象的地址

指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&

int ival = 42;
int *p = &ival;     // p 存放变量 ival 的地址,或者说 p 是指向变量 ival 的指针

因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

指针类型要和它所指的对象严格匹配:

double dval;
double *pd = &dval; // 正确:初始值是 double 型对象的地址
double *pd2 = pd;   // 正确:初始值是指向 double 对象的指针

int *pi = pd;       // 错误:指针 pi 的类型和 pd 的类型不匹配
pi = &dval;         // 错误:试图把 double 型对象的地址赋给 int 型指针
指针值

指针的值(即地址)应属下列 4 种状态之一:

    1. 指向一个对象
    1. 指向紧邻对象所占空间的下一个位置。
    1. 空指针,意味着指针没有指向任何对象。
    1. 无效指针,也就是上述情况之外的其他值。

访问无效指针 或者 第2种和第3种形式的指针,这些指针没有指向任何具体的对象,所以访问这些指针后果无法预计。

利用指针访问对象

如果指针指向了一个对象,则允许使用 解引用符(操作符* 来访问该对象:

int ival = 42;
int *p = &ival;     // p 存放着变量 ival 的地址,或者说 p 是指向变量 ival 的指针
cout << *p;         // 由符号 * 得到指针 p 所指的对象,输出 42

对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值:

// 为 *p 赋值实际上是为 p 所指的对象赋值
*p = 0;             // 由符号 * 得到指针 p 所指的对象,即可经由 p 为变量 ival 赋值
cout << *p;         // 输出 0

关键概念:某些符号有多重含义:

&*这样的符号,既能用作表达式里的运算符,也能作为声明的一部分出现,符号的上下文决定了符号的意义。
声明语句中,&*用于组成复合类型;(& 引用,* 指针)
表达式中,它们的角色又转变成运算符。(& 取地址符,* 解引用符)

int i = 42;
int &r = i;     // & 紧随类型名出现,因此是声明的一部分,r 是一个引用
int *p;         // * 紧随类型名出现,因此是声明的一部分,p 是一个指针
p = &i;         // & 出现在表达式中,是一个取地址符
*p = i;         // * 出现在表达式中,是一个解引用符
int &r2 = *p;   // & 是声明的一部分,* 是一个解引用符
空指针 (null pointer)

以下列出几个声称空指针的方法:

/* 现在的C++程序最好使用 nullptr */
int *p1 = nullptr;  // 等价于 int *p1 = 0;
int *p2 = 0;        // 直接将 p2 初始化为字面常量 0 来生成空指针
// 需要首先 #include cstdlib
int *p3 = NULL;     // 等价于 int *p3 = 0;

过去的程序还会用到一个名为 NULL 的预处理变量(preprocessor variable)来给指针赋值,这个变量在头文件 cstdlib 中定义,它的值就是 0 0 0

建议:初始化所有指针。使用未经初始化的指针是引发运行时错误的一大原因。尽量等定义了对象之后在定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为 nullptr 或者 0,这样程序就能监测并指导它没有指向任何具体的对象了。

理由:在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看做一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当做了某个地址,我们就很难分清它到底是合法的还是非法的了。

赋值和指针

指针和引用的差异:

引用本身并非一个对象,一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。

int i = 42;
int *pi = 0;    // pi 被初始化,但没有指向任何对象
int *pi2 = &i;  // pi2 被初始化,存有 i 的地址
int *pi3;       // 如果 pi3 定义于块内,则 pi3 的值是无法确定的
pi3 = pi2;      // pi3 和 pi2 指向同一个对象 i
pi2 = 0;        // 现在 pi2 不指向任何对象了

到底是改变了指针的值还是改变了指针所指对象的值

(1) pi = &ival;     // pi 的值被改变,现在 pi 指向了 ival [ & 为取地址符]

(2) *pi = 0;        // ival 的值被改变,指针 pi 并没有改变 [ * 为解引用符]

(1) 意思是为 pi 赋一个新的值,也就是改变了那个存放在pi内的地址值。

(2) 意思是 *pi(也就是指针pi指向的那个对象)发生改变。

练习2.18: 编写代码分别更改指针的值以及指针所指对象的值。

#include <iostream>

int main()
{
    int i = 5, j = 10;
    int *p = &i;    // 初始情况下令指针 p 指向变量 i
    std::cout << p << " " << *p << std::endl;
    p = &j;     // p=&j;更改了指针的值,令指针p指向另外一个整数对象j。
    std::cout << p << " " << *p << std::endl;
    *p = 20;    // 更改指针所指对象值的方式,显式地更改指针 p 所指的内容
    std::cout << p << " " << *p << std::endl;
    j = 30;     // 更改变量 j 的值,更改指针所指对象值的方式
    std::cout << p << " " << *p << std::endl;
    return 0;
}
/*
0x61ff08 5
0x61ff04 10
0x61ff04 20
0x61ff04 30
*/
其他指针操作
int ival = 1024;
int *pi = 0;        // pi 合法,是一个空指针
int *pi2 = &ival;   // pi2 是一个合法的指针,存放着 ival 的地址
if (pi)             // pi 的值是 0,因此条件的值是 false
    // ...
if (pi2)            // pi2 指向 ival,因此它的值不是 0,条件的值是 true
    // ...
void* 指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。但是,我们对该地址中到底是个什么类型的对象并不了解。
概括说来,以 void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。

double obj = 3.14, *pd = &obj;  // 正确:void* 能存放任意类型对象的地址
void *pv = &obj;                // obj 可以是任意类型的对象
pv = pd;                        // pv 可以存放任意类型的指针

练习 2.23: 给定指针p,你能知道它是指向了一个合法的对象吗?如果能,叙述判断的思路;如果不能,说明原因。

判断p是否指向合法的对象,只需把p作为if语句的条件即可,如果p的值是nullptr,则条件为假;反之,条件为真。

如果不注意初始化所有指针而贸然判断指针的值,则有可能引发不可预知的结果。一种处理的办法是把if(p)置于try结构中,当程序块顺利执行时,表示p指向了合法的对象;当程序块出错跳转到catch语句时,表示p没有指向合法的对象。

2.3.3 理解复合类型的声明

在同一条定义语句中,虽然基本数据类型(base type)只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量:

// i 是一个 int 型的数,p 是一个 int 型指针,r 是一个 int 型引用
// [int 是基本数据类型;i, *p, &r 是声明符]
int i = 1024, *p = &i, &r = i;
定义多个变量

类型修饰符(*&)

设计指针或引用的声明,一般有两种写法。
(1) 第一种把修饰符和变量标识符写在一起,这种形式着重强调变量具有的复合类型。
(2) 第二种把修饰符和类型名写在一起,并且每条语句只定义一个变量,这种形式着重强调本次声明定义了一种复合类型。

(1) int *p1, *p2;   // p1 和 p2 都是指向 int 的指针 (喜欢这种)

(2) int* p1;    // p1 是指向 int 的指针
    int* p2;    // p2 是指向 int 的指针
指向指针的指针

指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。

通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:

int ival = 1024;
int *pi = &ival;    // pi 指向一个 int 型的数;pi是指向 int 型数的指针。
int **ppi = &pi;    // ppi 指向一个 int 型的指针;ppi 是指向 int 型指针的指针。

cout << "The value of ival\n"
     << "direct value: " << ival << "\n"    // 第一种直接输出
     << "indirect value: " << *pi << "\n"   // 第二种通过 int 型指针 pi 输出
     << "doubly indrect value: " << **ppi   // 第三种两次解引用 ppi,取得 ival 的值
     << endl;

举例:上次阿欣遇到的问题小金帮忙回答的,这里常量是元素,指针不是常量:

指针的指针

指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:

int i = 42;
int *p;       // p 是一个 int 型指针
int *&r = p;  // r 是一个对指针 p 的引用

r = &i;       // r 引用了一个指针,因此给 r 赋值 &i 就是令 p 指向 i
*r = 0;       // 解引用 r 得到 i,也就是 p 指向的对象,将 i 的值改为 0

2.4 const 限定符

因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。

const int i = get_size();   // 正确:运行时初始化
const int j = 42;           // 正确:编译时初始化
const int k;        // 错误: k 是一个未经初始化的常量

默认状态下,const 对象仅在文件内有效:

默认情况下,const 对象被设定为仅在文件内有效,当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。

如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize;   // 与 file_1.cc 中定义的 bufSize 是同一个

2.4.1 const 的引用

可以把引用绑定到 const 对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)

const int ci = 1024;
const int &rl = ci; // 正确:引用及其对应的对象都是常量

rl = 42;        // 错误:rl 是对常量的引用
int &r2 = ci;   // 错误:试图让一个非常量引用指向一个常量对象
// 假设该初始化合法,则可以通过 r2 来改变它所引用对象的值,这显然是不正确的。

2.4.2 指针和 const

指向常量的指针 (pointer to const) 不能用于改变其所指对象的值。
要想存放常量对象的地址,只能使用指向常量的指针。

const double pi = 3.14;     // pi 是个常量,它的值不能改变
double *ptr = &pi;          // 错误:ptr 是一个普通指针
const double *cptr = &pi;   // 正确:cptr 可以指向一个双精度常量
*cptr = 42;                 // 错误:不能给 *cptr 赋值

例外情况,允许令一个指向常量的指针指向一个非常量对象:

double dval = 3.14; // dval 是一个双精度浮点数,它的值可以改变
cptr = &dval;       // 正确:但是不能通过 cptr 改变 dval 的值

这里 const double *cptr = &pi;cptr 是一个指向双精度常量的指针,即指向常量的指针。我们不能通过该指针cptr改变对象pi的值,但我们允许指向常量的指针 cptr 指向一个非常量对象 dval

注意:所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
可以这样想,所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

const 指针

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。

常量指针(const pointer) 必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把*放在 const 关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值。

int errNumb = 0;
int *const curErr = &errNumb;   // curErr 将一直指向 errNumb
const double pi = 3.14159;
const double *const pip = &pi;  // pip 是一个指向常量对象的常量指针

练习2.27 下面的哪些初始化是合法的?请说明原因 (P64):

/*练习2.27 下面的哪些初始化是合法的?请说明原因 (P64) */

(a) int i = -1, &r = 0;
// 是非法的,非常量引用 r 不能引用字面常量 0

(b) int *const p2 = &i2;
// 是合法的,p2 是一个常量指针,p2 的值永不改变,即 p2 永远指向变量 i2

(c) const int i = -1, &r = 0;
// 是合法的,i 是一个常量, r 是一个常量引用,此时 r 可以绑定到字面值常量 0

(d) const int *const p3 = &i2;
// 是合法的,p3 是一个常量指针,p3 的值永不改变,即 p3 永远指向变量 i2

(e) const int *p1 = &i2;
// 是合法的,p1 指向一个常量,即我们不能通过 p1 改变所指对象的值

(f) const int &const r2;
// 是非法的,引用本身不是对象,因此不能让引用恒定不变

(g) const int i2 = i, &r = i;
// 是合法的,i2 是一个常量,r 是一个常量引用

/* 总结:
- 非常量引用 不能引用字面常量如 0,常量引用 可以绑定到字面值常量。看(a)和(c)
- 引用不是对象,因此不能让引用恒定不变。看(f)
*/

练习2.28:说明下面的这些定义是什么意思,挑出其中不合法的:

/* 练习2.28:说明下面的这些定义是什么意思,挑出其中不合法的。*/

(a) int i, *const cp;
// 是非法的,cp 是一个常量指针,因其值不能被改变,所以必须初始化。

(b) int *p1, *const p2;
// 是非法的,p2 是一个常量指针,因其值不能被改变,所以必须初始化。

(c) const int ic, &r = ic;
// 是非法的,ic 是一个常量,因其值不能被改变,所以必须初始化。

(d) const int *const p3;
// 是非法的,p3 是一个常量指针,因其值不能被改变,所以必须初始化;
// 同时 p3 指向的是常量,即我们不能通过 p3 改变所指对象的值。

(e) const int *p;
// 是合法的,指向常量的指针 p,但是 p 没有指向任何实际的对象。

/*总结:
- 常量指针,因其值不能被改变,所以必须初始化。看(a)、(b)、(d)
- 常量,因其值不能被改变,所以必须初始化。看(c)
- 常量指针和常量都必须初始化。
*/

练习 2.29:假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?请说明原因。

/*练习 2.29:假设已有上一个联系中定义的那些变量,则下面的哪些语句是合法的?请说明原因。*/

(a) i = ic;
// 是合法的,常量 ic 的值赋给了非常量 i。

(b) p1 = p3;
// 是非法的,普通指针 p1 指向了一个常量,从语法上说,p1 的值可以随意改变,显然是不合理的。<<<第1行>>>

(c) p1 = &ic;
// 是非法的,普通指针 p1 指向了一个常量,错误情况与上一条类似。

(d) p3 = &ic;
// 是非法的,p3 是一个常量指针,不能被赋值。

(e) p2 = p1;
// 是非法的,p2 是一个常量指针,不能被赋值。


(f) ic = *p3;
// 是非法的,*p3 是解引用符,ic 是常量,不能被赋值。

/*总结:
- 不能用普通指针指向一个常量(比如常量地址)。看(b)和(c)
- 常量指针不能被赋值,看(d)和(e)
*/

【总结】:

  • 非常量引用不能引用字面常量,如int &r = 0; 非法;常量引用可以绑定到字面值常量,如const int &r = 0; 合法
  • 引用本身不是对象,因此不能让引用恒定不变;如const int &const r2; 非法
  • 指针是对象,因此可以让指针恒定不变。
  • 常量指针因其值不能被改变,所以必须初始化,如int *const cp; 非法
  • 不能用普通指针指向一个常量(比如常量地址)
  • 常量指针不能被赋值
总结
  • 常量指针 是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
  • 指针常量 是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴随其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。

1. 常量指针。const 修饰指针指向的内容,则内容为不可变量。

int a = 10;
int b = 10;
int *p = &a;
const int *p = &a;
/* 常量指针
   特点:指针的指向可以修改,
   但是指针指向的值不可以改。*/
*p = 20; //错误,指针指向的值不可以改
p = &b;  //正确,指针指向可以改

常量指针

写法也可以不同,如:

int const *p1 = &b; //const在前,定义为常量指针
const int *p1 = &b; //和上面相同意思

int *const p2 = &c; //*在前,定义为指针常量

2. 指针常量。const 修饰指针,则指针为不可变量

int *const p = &a;
/*指针常量
特点:指针的指向不可以改,指针指向的值可以改*/

*p = 20; //正确,指向的值可以改
p = &b;  //错误:指针指向不可以改

3. const 既修饰指针,又修饰指针指向的内容,则指针和指针指向的内容都为不可变量

const int * const p = &a;
// 特点:指针的指向和指针指向的值都不可以改

*p = 20; //错误
p = &b;  //错误

2.4.3 顶层 const

指针本身是一个对象,它又可以指向另一个对象。因此,指针本身是不是常量以及指针所指的是不是常量就是两个相互独立的问题。

  • 用名词 顶层 const (top-level const) 表示指针本身是个常量;
  • 用名词 底层 const (low-level const) 表示指针所指的对象是一个常量。

顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。
底层 const 则与指针和引用等复合类型的基本类型部分有关。
比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const,这一点和其它类型相比区别明显:

int i = 0;
int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层 const
const int ci = 42;  // 不能改变 ci 的值,这是一个顶层 const
const int *p2 = &ci;// 允许改变 p2 的值,这是一个底层 const
const int *const p3 = p2;   // 靠右的 const 是顶层 const,靠左的是底层 const
const int &r = ci;  // 用于声明引用的 const 都是底层 const

i = ci;     // 正确:拷贝 ci 的值,ci 是一个顶层 const,对此操作无影响
p2 = p3;    // 正确:p2 和 p3 指向的对象类型相同,p3 顶层 const 的部分不影响  <<<第2行>>>

–微信群里大佬解释为什么上面 p2 = p3

因为 p2p3 的指针指向的类型相同,再上面的 p1p3 指向的类型不同,
一个const变量是可以赋值给该类型的非const,
指针之间能不能赋值是看指向的变量类型

–书上总结:

当执行对象的拷贝操作时,顶层 const 不受什么影响。
另一方面,底层 const 的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:

int *p = p3;    // 错误:p3 包含底层 const 的定义,而 p 没有
p2 = p3;        // 正确:p2 和 p3 都是底层 const
p2 = &i;        // 正确:int* 能转换成 const int*
int &r = ci;    // 错误:普通的 int& 不能绑定到 int 常量上;(引用本身不是对象,因此不能让引用恒定不变)
const int &r2 = i;  // 正确:const int& 可以绑定到一个普通 int 上;(常量引用可以绑定到字面值常量,如`const int &r = 0; 合法`)

注意:

p3 既是顶层 const 也是底层 const,拷贝 p3 时可以不在乎它是一个顶层 const,但是必须清楚它指向的对象得是一个常量。因此,不能用 p3 去初始化 p,因为 p 指向的是一个普通的(非常量)整数。

另一方面,p3 的值可以赋给 p2,是因为这两个指针都是底层 const,尽管 p3 同时也是一个常量指针(顶层 const),仅就这次赋值而言不会有什么影响。

练习2.30:对于下面的这些语句,请说明对象被声明成了顶层 const 还是底层 const?

/* 练习2.30:对于下面的这些语句,请说明对象被声明成了顶层 const 还是底层 const? */

(1) const int v2 = 0;   int v1 = v2;
(2) int *p1 = &v1, &r1 = v1;
(3) const int *p2 = &v2, *const p3 = &i, &r2 = v2;

// v2 和 p3 是顶层 const,分别表示一个整形常量和一个整形常量指针;
// p2 和 r2 是底层 const,分别表示它们所指(所引用)的对象是常量。

练习2.31:假设已有上一个练习中所做的那些声明,则下面的哪些语句是合法的?请说明顶层 const 和底层 const 在每个例子中有何体现。

/* 练习2.31:假设已有上一个练习中所做的那些声明,则下面的哪些语句是合法的?请说明顶层 const 和底层 const 在每个例子中有何体现。 */

(1) r1 = v2;
// 合法,r1 是一个非常量引用,v2 是一个常量(顶层const),把 v2 的值拷贝给 r1 不会对 v2 有任何影响。

(2) p1 = p2; p2 = p1;

// p1 = p2; 是非法的,p1 普通指针,p2 指向常量的指针;
/*(官方解释:)p1 是普通指针,指向的对象可以是任意值,p2 是指向常量的指针(底层const),令 p1 指向 p2 所指的内容,有可能错误地改变常量的值。(问号???)*/

// p2 = p1; 是合法的,与上一条语句相反,指向常量的指针p2 可以指向一个非常量p1,只不过我们不会通过 p2 再更改它所指的值。
/* 注意:所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。*/

(3) p1 = p3; p2 = p3;
// p1 = p3; 是非法的,指向常量的常量指针 p3 不能赋给 普通指针 p1。(官方解释:)p3 包含底层 const 定义(p3 所指的对象是常量),不能把 p3 的值赋给普通指针。

// p2 = p3; 是合法的,常量指针 p3 与 指向常量的指针 p2 包含相同的底层 const,p3 的顶层 const 则可以忽略不计。

2.4.4 constexpr 和常量表达式

常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。
字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。

const int max_fiels = 20;       // max_files是常量表达式
const int limit = max_files + 1;// limit 是常量表达式
int staff_size = 27;            // staff_size 不是常量表达式。
// 尽管 staff_size 的初始值是个字面值常量,但由于它的数据类型只是一个普通 int 而非 const int,所以它不属于常量表达式。
const int sz = get_size();      // sz 不是常量表达式
// 尽管 sz 本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
constexpr 变量

C++11新标准允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

constexpr int mf = 20;      // 20 是常量表达式
constexpr int limit = mf + 1; // mf + 1 是常量表达式
constexpr int sz = size();  // 只有当 size 是一个 constexpr 函数时才是一条正确的声明语句
字面值类型(literal type)

算术类型、引用和指针都属于字面值类型。自定义类、IO库、string类型则不属于字面值类型,也就不能被定义成constexpr

指针和 constexpr

constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。

const int *p = nullptr;     // p 是一个指向(整型)常量的指针
constexpr int *q = nullptr; // q 是一个指向整数的常量指针
/* p 和 q 的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,关键在于 constexpr 把它所定义的对象置为了顶层const。*/
/* 与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量。*/
constexpr int *np = nullptr;    // np 是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42;           // i 的类型是整型常数
// i 和 j 都必须定义在函数体之外
constexpr const int *p = &i;    // p 是常量指针,指向整型常量 i
constexpr int *p1 = &j;         // p1 是常量指针,指向整数 j
/* 练习2.32:下面的代码是否合法?如果非法,请设法将其修改正确。*/
int null = 0, *p = null;

// 解答:上述代码是非法的,null 是一个 int 变量,p 是一个 int 指针,而这不能直接绑定。仅从语法角度来说,可以将代码修改为:
int null = 0, *p = &null;

// 另一种改法是使用 nullptr:
int null = 0, *p = nullptr;

2.5 处理类型

2.5.1 类型别名

类型别名(type alias)。有两种方法可用于定义类型别名。

  • 传统的方法是使用关键字typedef
  • 新的方法使用**别名声明(alias declaration)**来定义类型的别名,如:using SI = Sales_item; SI 是 Sales_item 的同义词

类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名:

wages hourly, weekly;   // 等价于 double hourly、weekly;
SI item;                // 等价于 Sales_item item;
指针、常量和类型别名
typedef char *pstring;  // pstring 实际上是指向 char 的指针

const pstring cstr = 0; // const 是指向 char 的常量指针
/* const 是对给定类型的修饰,pstring 实际上是指向 char 的指针,因此 const pstring 就是指向 char 的常量指针,而非指向常量字符的指针。*/

const pstring *ps;      // ps 是一个指针,它的对象是指向 char 的常量指针

注意:遇到使用了类型别名的声明语句时,不要错误地尝试把类型别名替换成它本来的样子以理解该语句的含义,会出错的:

const char *cstr = 0;   // 是对 const pstring cstr 的错误理解

这种理解是错误的:
声明语句中用到pstring时,其基本数据类型是指针。可是char*重写了声明语句后,数据类型就变成了char*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。
前后两种声明含义截然不同,前者pstring声明了一个指向char的常量指针,改写后的形式则声明了指向const char的指针。

2.5.2 auto 类型说明符

使用auto能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i = 0, *p = &i;    // 正确:i 是整数、p 是整型指针
auto sz = 0, pi = 3.14; // 错误:sz 和 pi 的类型不一样
复合类型、常量和 auto

(1) 当引用被用作初始值时,真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为 auto 的类型:

int i = 0, &r = i;
auto a = r;     // a 是一个整数(r 是 i 的别名,而 i 是一个整数)

(2) auto一般会忽略掉顶层 const,同时底层 const 则会保留下来,比如当初始值是一个指向常量的指针时:

const int ci = i, &cr = ci; // ci 是一个针型常量,i 是一个整数,cr是ci的别名
auto b = ci;    // b 是一个整数(ci 的顶层const特性被忽略掉了)
auto c = cr;    // c 是一个整数(cr 是 ci 的别名,ci 本身是一个顶层 const)
auto d = &i;    // d 是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci;   // e 是一个指向整型常量的指针(对常量对象取地址是一种底层 const)

(3) 还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:

auto &g = ci;       // g 是一个整型常量引用,绑定到整型常量 ci,不能修改它的值
auto &h = 42;       // 错误:不能为非常量引用绑定字面值
const auto &j = 42; // 正确:可以为常量引用绑定字面值

如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。

(4) 要在一条语句中定义多个变量,切记,符号&*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型:

auto k = ci, &l = i;    // k 是整数,l 是整型引用
auto &m = ci, *p = &ci; // m 是对整型常量的引用,p是指向整型常量的指针
// 错误:i 的类型是 int 而 &ci 的类型是 const int
auto &n = i, *p2 = &ci;

练习 2.35:判断下列定义推断出的类型是什么,然后编写程序进行验证。

/* 练习 2.35:判断下列定义推断出的类型是什么,然后编写程序进行验证。*/
#include <iostream>
#include <typeinfo>

int main()
{
    const int i = 42;
    auto j = i;
    const auto &k = i;
    auto *p = &i;
    const auto j2 = i, &k2 = i;
    std::cout << typeid(i).name() << std::endl; // i 是整型常量
    std::cout << typeid(j).name() << std::endl; // j 是整数
    std::cout << typeid(k).name() << std::endl; // k 是整型常量
    std::cout << typeid(p).name() << std::endl; // p 是指向整型常量的指针
    std::cout << typeid(j2).name() << std::endl;// j2 是整数
    std::cout << typeid(k2).name() << std::endl;// k2 是整数
    return 0;
}

/* Output:
i
i
i
PKi
i
i
*/

/*备注:`i` 表示 "int";`PK` 表示 "pointer to const" */

2.5.3 decltype 类型指示符

解决的情形:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。
作用是选择并返回操作数的数据类型,编译器分析表达式并得到它的类型,却不世纪计算表达式的值。

decltype 处理顶层 const 和引用的方式与 auto 有些许不同。如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内):

const int ci = 0, &cj = ci;
decltype(ci) x = 0;     // x 的类型是 const int
decltype(cj) y = x;     // y 的类型是 const int&,y 绑定到变量 x
decltype(cj) z;         // 错误:z 是一个引用,必须初始化
decltype 和引用

如果 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型。一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:

// decltype 的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b;  // 正确:加法的结果是 int,因此 b 是一个(未初始化的)int,是个具体的值而非一个引用
decltype(*p) c;     // 错误:c 是 int&,引用类型,必须初始化

如果表达式的内容是解引用操作,则 decltype 将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此 decltype(*p)的结果类型就是int&,而非int。(问号??? P71)

decltype 的参数既可以是普通变量,也可以是一个表达式。

  • 当参数是普通变量时,推断出的类型就是该变量的类型;
  • 当参数是表达式时,推断出的类型是引用。

decltype 和 auto 的一个重要区别是,decltype的结果类型与表达式形式密切相关。

切记: decltype((variable)) (注意是双层括号)的结果永远是引用,而 decltype(variable) 结果只有当 variable 本身就是一个引用才是引用。

练习2.37:赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。

/* 练习2.37:赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。
也就是说,如果 i 是 int,则表达式 i = x 的类型是 int&。
根据这一特点,请指出下面的代码中每一个变量的类型和值。

【出题思路】:decltype 的参数既可以是普通变量,也可以是一个表达式。
当参数是普通变量时,推断出的类型就是该变量的类型;
当参数是表达式时,推断出的类型是引用。*/

int a = 3, b = 4;       // a = 3, b = 4
decltype(a) c = a;      // c 的类型是 int, c = 3
decltype(a = b) d = a;  // 表达式 a=b 作为 decltype 的参数,d 的类型是 int&
/* 编译器分析表达式并得到它的类型作为 d 的推断类型,但是不实际计算该表达式,所以
a 的值不发生改变,仍然是 3;
d 的类型是 int&,d 是 a 的别名,值是 3;
b 的值一致没有发生改变,为 4
*/

练习 2.38:说明由decltype指定类型和由auto指定类型有何区别。

/* 练习 2.38:说明由`decltype`指定类型和由`auto`指定类型有何区别。
请举出一个例子,decltype 指定的类型与 auto 指定的类型一样;
再举一个例子,decltype 指定的类型与 auto 指定的类型不一样。*/

// 一个用以说明的示例如下所示:
#include <iostream>
#include <typeinfo>

int main()
{
    int a = 3;
    auto c1 = a;            // a 是一个非常量整数,c1 的推断结果是整数
    decltype(a) c2 = a;     // c2 的推断结果也是整数
    decltype((a)) c3 = a;   // c3 的推断结果由于变量 a 额外加了一对括号所以是整数引用

    const int d = 5;        // d 是一个常量整数,含有顶层 const
    auto f1 = d;            // 使用 auto 推断类型自动忽略掉顶层 const,因此 f1 的推断结果是整数
    decltype(d) f2 = d;     // 保留顶层 const,所以 f2 的推断结果是整数常量

    std::cout << typeid(c1).name() << std::endl;
    std::cout << typeid(c2).name() << std::endl;
    std::cout << typeid(c3).name() << std::endl;
    std::cout << typeid(f1).name() << std::endl;
    std::cout << typeid(f2).name() << std::endl;

    c1++;
    c2++;
    c3++;
    f1++;
    // f2++;   // 错误:f2 是整型常量,不能执行自增操作
    std::cout << a << " " << c1 << " " << c2 << " " << c3 << " " << f1 << " " << f2 << std::endl;
    return 0;
}

/*Output:
i
i
i
i
i
4 4 4 4 6 5
对于第二组类型推断中,f1 可以正常执行自增操作,而常量 f2 的值不能被改变,所以无法自增。
*/

(1) auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。

(2) 编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层 const,而把底层 const 保留下来。与之相反,decltype会保留变量的顶层 const。

(3) 与auto不同,decltype的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。

2.6 自定义数据结构

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。Sales_item类就是一个数据结构。

2.6.1 定义Sales_data类型

既然我们筹划的这个数据结构不带有任何运算功能,不妨把它命为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;

// 与上一条语句等价,但可能更好一些
struct Sales_data { /*...*/ }
Sales_data accum, trans, *salesptr;

分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。
这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。

练习2.40:根据自己的理解写出 Sales_data 类,最好与书中的例子有所区别。

/* 练习2.40:根据自己的理解写出 Sales_data 类,最好与书中的例子有所区别。*/

struct Sales_data {
    std::string bookNo;         // 书籍编号
    unsigned units_sold = 0;    // 销售量
    double sellingprice = 0.0;  // 零售价
    double saleprice = 0.0;     // 实售价
    double discount;            // 折扣
};
类数据成员

类体定义类的成员,我们的类只有数据成员(data member)。

定义数据成员的方法和定义普通变量一样:首先说明一个基本类型,随后紧跟一个或多个声明符。

C++11新标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。没有初始值的成员将被默认初始化。

2.6.2 使用Sales_data

添加两个Salese_data对象
#include <iostream>
#include <string>
#include "Sales_data.h"
int main()
{
    Sales_data data1, data2;
    // 读入 data1 和 data2 的代码
    // 检查 data1 和 data2 的 ISBN 是否相同的代码
    // 如果相同,求 data1 和 data2 的总和
}
Salese_data对象读入数据
double price = 0;   // 书的单价,用于计算销售收入
// 读入第 1 笔交易:ISBN、销售数量、单价
std::cin >> data1.bookNo >> data1.units_sold >> price;
// 计算销售收入
data1.revenue = data1.units_sold * price;
输出两个Salese_data对象的和

剩下的工作就是检查两笔交易涉及的 ISBN 编号是否相同。如果相同输出它们的和,否则输出一条报错信息:

if (data1.bookNo == data2.bookNo) {
    unsigned totalCnt = data1.units_sold + data2.units_sold;
    double totalRevenue = data1.revenue + data2.revenue;
    // 输出:ISBN、总销售量、总销售额、平均价格
    std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
    if (totalCnt != 0)
        std::cout << totalRevenue / totalCnt << std::endl;
    else
        std::cout << "(no sales)" << std::endl;
    return 0;   // 标示成功
} else {        // 两笔交易的 ISBN 不一样
    std::cerr << "Data must refer to the same ISBN" << std::endl;
    return -1;  // 标示失败
}

2.6.3 编写自己的头文件

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。

头文件通常包含哪些只能被定义一次的实体,如类、constconstexpr变量等。
头文件也经常用到其他头文件的功能。
例如,Sales_data类包含有一个string成员,所以Sales_data.h必须包含string.h头文件。
同时,Sales_dta类的程序为了能操作bookNo成员需要再一次包含string.h头文件。
这样,事实上使用Sales_data类的程序就先后两次包含了stirng.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之间的部分。
为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值