变量和基本类型

练习2.1

类型 int、long、long long 和 short 的区别是什么?无符号类型和带符号类型的区别是什么?float 和 double的区别是什么?

解:

在C++中,int、long、long long和short都属于整型,区别是C++规定的尺寸的最小值(即该类型在内存中所占的比特数)不同,其中, short 和 int 至少16位,long 至少32位,long long 至少64位。 C++标准允许不同的编译器赋予这些类型更大的尺寸。某一类型栈的比特数不同,它所能表示的数据范围也不一样。

大多数整数都能划分为无符号类型和带符号类型,在无符号类型中所有比特都用来存储数值,但只能表示大于等于0的值,带符号类型则可以表示正数、负数或0.

float 和 double 分别是单精度浮点数和双精度浮点数,区别主要是在内存中所占的比特数不同,以及默认规定的有效位数不同。

练习2.2

计算按揭贷款时,对于利率、本金和付款分别应选择何种数据类型?说明你的理由。

解:

在实际应用中,利率、本金和付款既有可能是整数,也有可能是普通的实数。因此应该选择一种浮点类型来表示。在三种可供选择的浮点类型float、double和long double中double和float的计算代价比较接近且表示范围更广,long double的计算代价则相对较大,一般情况下没有选择的必要。综合以上分析,选择 double 是比较恰当的。

练习2.3

读程序写结果。

unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl;
std::cout << u - u2 << std::endl;
int i = 10, i2 = 42;
std::cout << i2 - i << std::endl;
std::cout << i - i2 << std::endl;
std::cout << i - u << std::endl;
std::cout << u - i << std::endl;

解:

输出:

32
4294967264
32
-32
0
0

0 u和u2都是无符号整数,因此u2-u得到了正确的结果(42-10=32);u-u2也能正确计算,但是因为直接计算的结果是-32,所以在表示为无符号整数时自动加上模,在作者的编译环境中int占32位,因此加模的结果是 4294967264。

i和i2 都是带符号整数,因此中间两个式子的结果比较直观,42-10=32,10-42=-32。

在最后两个式子中,u和i分别是无符号整数和带符号整数,计算时编译器先把带符号数转换为无符号数,幸运的是,i本身是一个正数,因此转换后不会出现异常情况,两个式子的计算结果都是0。

不过需要提醒读者注意的是,一般情况下请不要在同一个表达式中混合使用无符号类型和带符号类型。因为计算前带符号类型会自动转换成无符号类型,当带符号类型取值为负时就会出现异常结果。

练习2.4

编写程序检查你的估计是否正确,如果不正确,请仔细研读本节直到弄明白问题所在。

练习2.5

指出下述字面值的数据类型并说明每一组内几种字面值的区别:

(a) 'a', L'a', "a", L"a"
(b) 10, 10u, 10L, 10uL, 012, 0xC
(c) 3.14, 3.14f, 3.14L
(d) 10, 10u, 10., 10e-2

解:

  • (a): 字符字面值,宽字符字面值,字符串字面值,宽字符串字面值。
  • (b): 十进制整型,十进制无符号整型,十进制长整型,十进制无符号长整型, 八进制整型,十六进制整型。
  • (c): double, float, long double
  • (d): 十进制整型,十进制无符号整型,double, double

练习2.6

下面两组定义是否有区别,如果有,请叙述之:

int month = 9, day = 7;
int month = 09, day = 07;

解:

第一行定义的是十进制的整型,第二行定义的是八进制的整型。但是month变量有误,八进制不能直接写9。

练习2.7

下述字面值表示何种含义?它们各自的数据类型是什么?

(a) "Who goes with F\145rgus?\012"
(b) 3.14e1L
(c) 1024f
(d) 3.14L

解:

  • (a)是一个字符串,包含两个转义字符,其中\145表示字符e,\012 表示一个换行符,因此该字符串的输出结果是Who goes with Fergus?
  • (b)是一个科学计数法表示的扩展精度浮点数,大小为3.14*10=31.4。
  • (c)试图表示一个单精度浮点数,但是该形式在某些编译器中将报错,因为后缀f直接跟在了整数1024后面;改写成1024f就可以了。
  • (d)是一个扩展精度浮点数,类型是long double,大小为3.14。

练习2.8

请利用转义序列编写一段程序,要求先输出 2M,然后转到新一行。修改程序使其先输出 2,然后输出制表符,再输出 M,最后转到新一行。

解:

#include <iostream>
int main()
{
   std::cout << 2 << "\115\012";
   std::cout << 2 << "\t\115\012";
   return 0;
}

练习2.9

解释下列定义的含义,对于非法的定义,请说明错在何处并将其改正。

  • (a) std::cin » int input_value;
  • (b) int i = { 3.14 };
  • (c) double salary = wage = 9999.99;
  • (d) int i = 3.14;

解:

(a): 应该先定义再使用。

int input_value = 0;
std::cin >> input_value;

(b): 用列表初始化内置类型的变量时,如果存在丢失信息的风险,则编译器将报错。

double i = { 3.14 };

(c): 声明多个变量时应该用逗号将变量名隔开,不能用赋值运算符连接。

double salary,wage;
double salary = wage = 9999.99;

(d): 不报错,但是小数部分会被截断。

double i = 3.14;

练习2.10

下列变量的初值分别是什么?

std::string global_str;
int global_int;
int main()
{
    int local_int;
    std::string local_str;
}

解:

global_strglobal_int是全局变量,所以初值分别为空字符串和0。 local_int是局部变量并且没有初始化,它的初值是未定义的。 local_strstring 类的对象,它的值由类确定,为空字符串。

练习2.11

指出下面的语句是声明还是定义:

  • (a) extern int ix = 1024;
  • (b) int iy;
  • (c) extern int iz;

解: 声明与定义的关系是:声明使得名字为程序所知,而定义负责创建与名字关联的实体。 (a): 定义变量ix (b): 声明并定义变量iy (c): 声明变量iz

练习2.12

请指出下面的名字中哪些是非法的?

  • (a) int double = 3.14;
  • (b) int _;
  • (c) int catch-22;
  • (d) int 1_or_2 = 1;
  • (e) double Double = 3.14;

解:

(a)是非法的,因为double是C++关键字,代表一种数据类型,不能作为变量的名字。 (c)是非法的,在标识符中只能出现字母、数字和下画线,不能出现符号-,如果改成"int catch 22;"就是合法的了。 (d)是非法的,因为标识符必须以字母或下画线开头,不能以数字开头。(b)和(e)是合法的命名。

练习2.13

下面程序中j的值是多少?

int i = 42;
int main()
{
    int i = 100;
    int j = i;
}

解:

j的值是100,局部变量i覆盖了全局变量i

练习2.14

下面的程序合法吗?如果合法,它将输出什么?

int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
    sum += i;
std::cout << i << " " << sum << std::endl;

解:

合法。输出是 100 45 。

该程序存在嵌套的作用域,其中for循环之外是外层作用域,for循环内部是内层作用域。首先在外层作用域中定义了i和sum,但是在for循环内部i被重新定义了,因此for循环实际上是从i=0循环到了i=9,内层作用域中没有重新定义 sum,因此sum的初始值是0并在此基础上依次累加。最后一句输出语句位于外层作用域中,此时在for循环内部重新定义的i已经失效,因此实际输出的仍然是外层作用域的i,值为100;而sum经由循环累加,值变为了 45。

练习2.15

下面的哪个定义是不合法的?为什么?

  • (a) int ival = 1.01;
  • (b) int &rval1 = 1.01;
  • (c) int &rval2 = ival;
  • (d) int &rval3;

解:

(b)和(d)不合法,(b)引用必须绑定在对象上,(d)引用必须初始化。

练习2.16

考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了哪些操作?

int i = 0, &r1 = i; 
double d = 0, &r2 = d;
  • (a) r2 = 3.14159;
  • (b) r2 = r1;
  • (c) i = r2;
  • (d) r1 = d;

解:

  • (a): 合法。给 d 赋值为 3.14159。
  • (b): 合法。会执行自动转换(int->double)。
  • (c): 合法。会发生小数截取。
  • (d): 合法。会发生小数截取。

练习2.17

执行下面的代码段将输出什么结果?

int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;

解:

程序的输出结果是10 10。

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

练习2.18

编写代码分别改变指针的值以及指针所指对象的值。

解:

int main() {
    int i = 1, j = 2;
    int *p  = &i;
    std::cout << p << " " << *p << std::endl;
    p = &j; //更改指针的值,令p指向另一个整数对象j
    std::cout << p << " " << *p << std::endl;
    *p = 10; // 显式改变指针p指向的内容
    std::cout << p << " " << *p << std::endl;
    j = 20; // 通过改变变量j的值
    std::cout << p << " " << *p << std::endl;
    return 0;
}

输出结果:

0x61fe14 1
0x61fe10 2
0x61fe10 10
0x61fe10 20

练习2.19

说明指针和引用的主要区别

解:

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

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

练习2.20

请叙述下面这段代码的作用。

int i = 42;
int *p1 = &i; 
*p1 = *p1 * *p1;

解:

这段代码首先定义了一个整数变量 i 并设其初值为 42;接着定义了一个整型指针 p1,令其指向变量 i;最后取出 p1 所指的当前值,计算平方后重新赋给 p1 所指的变量 i

第二行的 * 表示声明一个指针,第三行的 * 表示解引用运算,即取出指针 p1 所指对象的值。

练习2.21

请解释下述定义。在这些定义中有非法的吗?如果有,为什么?

int i = 0;

  • (a) double* dp = &i;
  • (b) int *ip = i;
  • (c) int *p = &i;

解:

  • (a): 非法。不能将一个指向 double 的指针指向 int
  • (b): 非法。不能将 int 变量赋给指针。
  • (c): 合法。

练习2.22

假设 p 是一个 int 型指针,请说明下述代码的含义。

if (p) // ...
if (*p) // ...

解:

指针 p 作为 if 语句的条件时,实际检验的是指针本身的值,即指针所指的地址值。如果指针指向一个真实存在的变量,则其值必不为 0,此时条件为真;如果指针没有指向任何对象或者是无效指针,则对 p 的使用将引发不可预计的结果。

解引用运算符 *p 作为 if 语句的条件时,实际检验的是指针所指的对象内容,在上面的示例中是指针p所指的int值。如果该 int 值为0,则条件为假;否则。如果该 int 值不为0,对应条件为真。

练习2.23

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

解:

在 C++ 程序中,应该尽量初始化所有指针,并且尽可能等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为 nullptr 或者 0,这样程序就能检测并知道它有没有指向一个具体的对象了。其中,nullptr 是C++11新标准刚刚引入的一个特殊字面值,它可以转换成任意其他的指针类型。在此前提下,判断p是否指向合法的对象,只需把 p 作为 if 语句的条件即可,如果 p 的值是 nullptr,则条件为假;反之,条件为真。 如果不注意初始化所有指针而贸然判断指针的值,则有可能引发不可预知的结果。一种处理的办法是把if(p)置于try结构中,当程序块顺利执行时,表示p指向了合法的对象:当程序块出错跳转到catch语句时,表示p没有指向合法的对象。

练习2.24

在下面这段代码中为什么 p 合法而 lp 非法?

int i = 42;
void *p = &i;
long *lp = &i;

解:

void *是一种特殊的指针类型,可用于存放任意对象的地址。 而其他指针类型必须要与所指对象严格匹配。

练习2.25

说明下列变量的类型和值。

(a) int* ip, i, &r = i;
(b) int i, *ip = 0;
(c) int* ip, ip2;

解:

  • (a): ip 是一个指向 int 的指针, i 是一个 int, r 是 i 的引用。
  • (b): i 是 int , ip 是一个空指针。
  • (c): ip 是一个指向 int 的指针, ip2 是一个 int。

练习2.26

下面哪些语句是合法的?如果不合法,请说明为什么?

解:

const int buf;      // 不合法, const 对象必须初始化
int cnt = 0;        // 合法
const int sz = cnt; // 合法
++cnt; ++sz;        // 不合法, const 对象不能被改变

练习2.27

下面的哪些初始化是合法的?请说明原因。

解:

int i = -1, &r = 0;         // 不合法, 非常用引用 r 不能引用字面值常量 0
int *const p2 = &i2;        // 合法,常量指针
const int i = -1, &r = 0;   // 合法, r 是一个常量引用,可以绑定到字面值常量 0
const int *const p3 = &i2;  // 合法
const int *p1 = &i2;        // 合法
const int &const r2;        // 不合法, r2 是引用,引用本身不是对象,不能让引用恒定不变
const int i2 = i, &r = i;   // 合法,i2是一个常量,r是一个常量引用

练习2.28

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

解:

int i, *const cp;       // 不合法, const 指针必须初始化
int *p1, *const p2;     // 不合法, const 指针必须初始化
const int ic, &r = ic;  // 不合法, const int 必须初始化
const int *const p3;    // 不合法, const 指针必须初始化
const int *p;           // 合法. 一个指针,指向 const int

练习2.29

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

解:

i = ic;     // 合法, 常量赋值给普通变量
p1 = p3;    // 不合法, p3 是const指针不能赋值给普通指针
p1 = &ic;   // 不合法, 普通指针不能指向常量
p3 = &ic;   // 不合法, p3 是一个常量指针,不能被赋值
p2 = p1;    // 不合法, p2 是一个常量指针,不能被赋值
ic = *p3;   // 不合法, ic是常量,不能被赋值

练习2.30

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

const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;

解:

v2 和 p3 是顶层const,分别表示一个整型常量和一个整型常量指针;p2 和 r2 是底层const,分别表示他们所指的对象是常量。

练习2.31

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

解:

r1 = v2; 
p1 = p2; 
p2 = p1; 
p1 = p3; 
p2 = p3; 

在执行拷贝操作时,顶层const和底层const区别明显。其中,顶层 const不受影响,这是因为拷贝操作并不会改变被拷贝对象的值。底层const的限制则不容忽视,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行。

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

pl=p2;是非法的,p1是普通指针,指向的对象可以是任意值,p2是指向常量的指针(底层const),令p1指向p2所指的内容,有可能错误地改变常量的值。

p2=p1;是合法的,与上一条语句相反,p2可以指向一个非常量,只不过我们不会通过p2更改它所指的值。

pl=p3;是非法的,p3包含底层const定义(p3所指的对象是常量),不能把 p3的值赋给普通指针。

p2=p3;是合法的,p2和p3包含相同的底层const,p3的顶层const则可以忽略不计。

练习2.32

下面的代码是否合法?如果非法,请设法将其修改正确。

int null = 0, *p = null;

解:

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

显然,这种改法与代码的原意不一定相符。另一种改法是使用nullptr: int null=0,*p=nullptri

练习2.33

利用本节定义的变量,判断下列语句的运行结果。

解:

a=42; // a 是 int
b=42; // b 是一个 int,(ci的顶层const在拷贝时被忽略掉了)
c=42; // c 也是一个int
d=42; // d 是一个 int *,所以语句非法
e=42; // e 是一个 const int *, 所以语句非法
g=42; // g 是一个 const int 的引用,引用都是底层const,所以不能被赋值

【出题思路】

本题旨在考查auto说明符与复合类型、常量混合使用时的各种情形。首先,使用引用其实是使用引用的对象,所以当引用被用作初始值时,真正参与初始化的其实是引用对象的值,编译器以引用对象的类型作为auto的推断类型。其次,auto一般会忽略掉顶层const,同时保留底层const。

【解答】 前3条赋值语句是合法的,原因如下: r是i的别名,而i是一个整数,所以a的类型推断结果是一个整数;ci是一个整型常量,在类型推断时顶层const被忽略掉了,所以b是一个整数;cr是ci的别名,而ci是一个整型常量,所以c的类型推断结果是一个整数。因为a、b、c都是整数,所以为其赋值 42 是合法的。 后3条赋值语句是非法的,原因如下: i是一个整数,&i是i的地址,所以d的类型推断结果是一个整型指针;ci是一个整型常量,&ci是一个整型常量的地址,所以e的类型推断结果是一个指向整型常量的指针:ci是一个整型常量,所以a的类型推断结果是一个整型常量引用。因为d和e都是指针,所以不能直接用字面值常量为其赋值;g绑定到了整型常量,所以不能修改它的值。

练习2.34

基于上一个练习中的变量和语句编写一段程序,输出赋值前后变量的内容,你刚才的推断正确吗?如果不对,请反复研读本节的示例直到你明白错在何处为止。

练习2.35

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

const int i = 42;
auto j = i; const auto &k = i; auto *p = &i; 
const auto j2 = i, &k2 = i;

解:

j 是 int,k 是 const int的引用,p 是const int *,j2 是const int,k2 是 const int 的引用。

练习2.36

关于下面的代码,请指出每一个变量的类型以及程序结束时它们各自的值。

int a = 3, b = 4;
decltype(a) c = a;
decltype((b)) d = a;
++c;
++d;

解:

在本题的程序中,初始情况下a的值是3、b的值是4。decltype(a)c=ai使用的是一个不加括号的变量,因此c的的类型就是a的类型,即该语句等同于int c=a;,此时c是一个新整型变量,值为 3. decltype((b)) d=a;使用的是一个 加了括号的变量,因此d的类型是引用, 即该语句等同于int &d=a;,此时d是变量a的别名。 执行++c;++d;时,变量c的值自增为4,因为d是a的别名,所以d自增1 意味着a的值变成了4。当程序结束时, a、b、c、d的值都是 4。

练习2.37

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

int a = 3, b = 4;
decltype(a) c = a;
decltype(a = b) d = a;

解:

c 是 int 类型,值为 3。d 是 int& 类型,绑定到 a。

练习2.38

说明由decltype 指定类型和由auto指定类型有何区别。请举一个例子,decltype指定的类型与auto指定的类型一样;再举一个例子,decltype指定的类型与auto指定的类型不一样。

解:

decltype 处理顶层const和引用的方式与 auto不同,decltype会将顶层const和引用保留起来。

auto和decltype的区别主要有三个方面:

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

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

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

#include <iostream>


int main() {
    int a = 1;
    auto c1 = a;
    decltype(a) c2 = a;
    decltype((a)) c3 = a;

    const int d = 3;
    auto f1 = d;
    decltype(d) f2 = d;

    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 << c1 << " " << c2 << " "<< c3 << " "<< f1 << " "<< f2 << " " << std::endl;
    return 0;
}

练习2.39

编译下面的程序观察其运行结果,注意,如果忘记写类定义体后面的分号会发生什么情况?记录下相关的信息,以后可能会有用。

struct Foo { /* 此处为空  */ } // 注意:没有分号
int main()
{
    return 0;
}

解:

提示应输入分号。

练习2.40

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

struct Sale_data
{
    std::string bookNo;
    std::string bookName;
    unsigned units_sold = 0;
    double revenue = 0.0;
    double price = 0.0;
    //...
}

练习2.41

使用你自己的Sale_data类重写1.5.1节(第20页)、1.5.2节(第21页)和1.6节(第22页)的练习。眼下先把Sales_data类的定义和main函数放在一个文件里。

// 1.5.1

#include <iostream>
#include <string>

struct Sale_data
{
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

int main()
{
    Sale_data book;
    double price;
    std::cin >> book.bookNo >> book.units_sold >> price;
    book.revenue = book.units_sold * price;
    std::cout << book.bookNo << " " << book.units_sold << " " << book.revenue << " " << price;

    return 0;
}

// 1.5.2

#include <iostream>
#include <string>

struct Sale_data
{
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

int main()
{
    Sale_data book1, book2;
    double price1, price2;
    std::cin >> book1.bookNo >> book1.units_sold >> price1;
    std::cin >> book2.bookNo >> book2.units_sold >> price2;
    book1.revenue = book1.units_sold * price1;
    book2.revenue = book2.units_sold * price2;

    if (book1.bookNo == book2.bookNo)
    {
        unsigned totalCnt = book1.units_sold + book2.units_sold;
        double totalRevenue = book1.revenue + book2.revenue;
        std::cout << book1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
        if (totalCnt != 0)
            std::cout << totalRevenue / totalCnt << std::endl;
        else
            std::cout << "(no sales)" << std::endl;
        return 0;
    }
    else
    {
        std::cerr << "Data must refer to same ISBN" << std::endl;
        return -1;  // indicate failure
    }
}

// 1.6

#include <iostream>
#include <string>

struct Sale_data
{
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

int main()
{
    Sale_data total;
    double totalPrice;
    if (std::cin >> total.bookNo >> total.units_sold >> totalPrice)
    {
        total.revenue = total.units_sold * totalPrice;

        Sale_data trans;
        double transPrice;
        while (std::cin >> trans.bookNo >> trans.units_sold >> transPrice)
        {
            trans.revenue = trans.units_sold * transPrice;

            if (total.bookNo == trans.bookNo)
            {
                total.units_sold += trans.units_sold;
                total.revenue += trans.revenue;
            }
            else
            {
                std::cout << total.bookNo << " " << total.units_sold << " " << total.revenue << " ";
                if (total.units_sold != 0)
                    std::cout << total.revenue / total.units_sold << std::endl;
                else
                    std::cout << "(no sales)" << std::endl;

                total.bookNo = trans.bookNo;
                total.units_sold = trans.units_sold;
                total.revenue = trans.revenue;
            }
        }

        std::cout << total.bookNo << " " << total.units_sold << " " << total.revenue << " ";
        if (total.units_sold != 0)
            std::cout << total.revenue / total.units_sold << std::endl;
        else
            std::cout << "(no sales)" << std::endl;

        return 0;
    }
    else
    {
        std::cerr << "No data?!" << std::endl;
        return -1;  // indicate failure
    }
}

练习2.42

根据你自己的理解重写一个Sales_data.h头文件,并以此为基础重做2.6.2节(第67页)的练习。