GitVP开源文摘
全部文章/编程语言

Cpp Primer 笔记 & 答案

搞定C++:punch:。C++ Primer 中文版第5版学习仓库,包括笔记和课后练习答案。

作者applenob 仓库applenob/Cpp_Primer_Practice ↗ 星标★ 8,769 字数4,389 阅读1
GitHub 原文 ↗
摘要《C++ Primer 中文版(第 5 版)》学习仓库,包括**笔记**和**课后练习答案**。

Cpp Primer 笔记 & 答案

简介

《C++ Primer 中文版(第 5 版)》学习仓库,包括笔记和课后练习答案。

环境

  • system: ubuntu 16.04
  • IDE: VS Code
  • compiler: g++

豆瓣链接

目录

参考

参与贡献

本仓库由多位小伙伴一起参与编写,欢迎大家对本仓库进行补充,一起帮大家更好地理解这本“大部头”。


ch01

第一章 开始

练习1.1

查阅你使用的编译器的文档,确定它所使用的文件名约定。编译并运行第2页的main程序。

解:

  • `g++ --std=c++11 ch1.cpp -o main`
  • `./main`

练习1.2

改写程序,让它返回-1。返回值-1通常被当做程序错误的标识。重新编译并运行你的程序,观察你的系统如何处理main返回的错误标识。

解:

  • 在ubuntu下,使用g++,返回-1,`./main`没有发现任何异常。
  • `echo $?`,返回255。

练习1.3

编写程序,在标准输出上打印Hello, World。

解:

#include <iostream>

int main()
{
	std::cout << "Hello, World" << std::endl;
	return 0;
}

练习1.4

我们的程序使用加法运算符+来将两个数相加。编写程序使用乘法运算符*,来打印两个数的积。

解:

#include <iostream>

int main()
{
    std::cout << "Enter two numbers:" << std::endl;
    int v1 = 0, v2 = 0;
    std::cin >> v1 >> v2;
    std::cout << "The product of " << v1 << " and " << v2
              << " is " << v1 * v2 << std::endl;
}        

练习1.5

我们将所有的输出操作放在一条很长的语句中,重写程序,将每个运算对象的打印操作放在一条独立的语句中。

解:

#include <iostream>

int main()
{
    std::cout << "Enter two numbers:" << std::endl;
        int v1 = 0, v2 = 0;
        std::cin >> v1 >> v2;
        std::cout << "The product of ";
        std::cout << v1;
        std::cout << " and ";
        std::cout << v2;
        std::cout << " is ";
        std::cout << v1 * v2;
        std::cout << std::endl;
}    

练习1.6

解释下面程序片段是否合法。

std::cout << "The sum of " << v1;
          << " and " << v2;
          << " is " << v1 + v2 << std::endl;

如果程序是合法的,它的输出是什么?如果程序不合法,原因何在?应该如何修正?

解:

程序不合法,有多余的分号,修改如下:

std::cout << "The sum of " << v1
          << " and " << v2
          << " is " << v1 + v2 << std::endl;

练习1.7

编译一个包含不正确的嵌套注释的程序,观察编译器返回的错误信息。

解:

/* 正常注释 /* 嵌套注释 */ 正常注释*/

错误信息:

  /* 正常注释 /* 嵌套注释 */ 正常注释*/
                                     ^
ch1.cpp:97:37: error: stray ‘\255’ in program
ch1.cpp:97:37: error: stray ‘\243’ in program
ch1.cpp:97:37: error: stray ‘\345’ in program
ch1.cpp:97:37: error: stray ‘\270’ in program
ch1.cpp:97:37: error: stray ‘\270’ in program
ch1.cpp:97:37: error: stray ‘\346’ in program
ch1.cpp:97:37: error: stray ‘\263’ in program
ch1.cpp:97:37: error: stray ‘\250’ in program
ch1.cpp:97:37: error: stray ‘\351’ in program
ch1.cpp:97:37: error: stray ‘\207’ in program
ch1.cpp:97:37: error: stray ‘\212’ in program
ch1.cpp: In function ‘int main()’:
ch1.cpp:97:50: error: expected primary-expression before ‘/’ token
  /* 正常注释 /* 嵌套注释 */ 正常注释*/
                                                  ^
ch1.cpp:98:5: error: expected primary-expression before ‘return’
     return 0;
     ^

练习1.8

指出下列哪些输出语句是合法的(如果有的话):

std::cout << "/*";
std::cout << "*/";
std::cout << /* "*/" */;
std::cout << /* "*/" /* "/*" */;

预测编译这些语句会产生什么样的结果,实际编译这些语句来验证你的答案(编写一个小程序,每次将上述一条语句作为其主体),改正每个编译错误。

解:

只有第三句编译出错,改成如下即可:

std::cout << /* "*/" */";

第四句等价于输出 " /* "。

练习1.9

编写程序,使用while循环将50到100整数相加。

解:

#include <iostream>

int main()
{
    int sum = 0, val = 50;
    while (val <= 100){
        sum += val;
        val += 1;
    }
    std::cout << "Sum of 50 to 100 inclusive is "
              << sum << std::endl;
}    

练习1.10

除了++运算符将运算对象的值增加1之外,还有一个递减运算符--实现将值减少1.编写程序与,使用递减运算符在循环中按递减顺序打印出10到0之间的整数。

解:

#include <iostream>

int main()
{
    int val = 10;
    while (val >= 0){
        std::cout << val << " ";
        val -= 1;
    }
    std::cout << std::endl;
}  

练习1.11

编写程序,提示用户输入两个整数,打印出这两个整数所指定的范围内的所有整数。

解:

#include <iostream>

int main()
{
    int start = 0, end = 0;
    std::cout << "Please input two num: ";
    std::cin >> start >> end;
    if (start <= end) {
        while (start <= end){
            std::cout << start << " ";
            ++start;
        }
        std::cout << std::endl;
    }
    else{
        std::cout << "start should be smaller than end !!!";
    }
}  

练习1.12

下面的for循环完成了什么功能?sum的终值是多少?

int sum = 0;
for (int i = -100; i <= 100; ++i)
	sum += i;

解:

从-100加到100,sum的终值是0。

练习1.13

使用for循环重做1.4.1节中的所有练习(练习1.9到1.11)。

解:

练习1.9

#include <iostream>

int main()
{
    int sum = 0;
    for (int val = 50; val <= 100; ++val){
        sum += val;
    }
    std::cout << "Sum of 50 to 100 inclusive is "
              << sum << std::endl;
}    

练习1.10

#include <iostream>

int main()
{
    for (int val = 10; val >=0; --val){
        std::cout << val << " ";
    }
    std::cout << std::endl;
}  

练习1.11

#include <iostream>

int main()
{
    int start = 0, end = 0;
    std::cout << "Please input two num: ";
    std::cin >> start >> end;
    if (start <= end) {
        for (; start <= end; ++start){
            std::cout << start << " ";
        }
        std::cout << std::endl;
    }
    else{
        std::cout << "start should be smaller than end !!!";
    }
}  

练习1.14

对比for循环和while循环,两种形式的优缺点各是什么?

解:

The main difference between the `for`'s and the `while`'s is a matter of pragmatics: 
we usually use `for` when there is a known number of iterations, 
and use `while` constructs when the number of iterations in not known in advance. 
The `while` vs `do ... while` issue is also of pragmatics, 
the second executes the instructions once at start, 
and afterwards it behaves just like the simple `while`.

练习1.15

编写程序,包含第14页“再探编译”中讨论的常见错误。熟悉编译器生成的错误信息。

解:

编译器可以检查出的错误有:

  • 语法错误
  • 类型错误
  • 声明错误

练习1.16

编写程序,从cin读取一组数,输出其和。

解:

#include <iostream>

int main()
{
    int sum = 0;
    for (int value = 0; std::cin >> value; )
        sum += value;
    std::cout << sum << std::endl;
    return 0;
}

练习1.17

如果输入的所有值都是相等的,本节的程序会输出什么?如果没有重复值,输出又会是怎样的?

练习1.18

编译并运行本节的程序,给它输入全都相等的值。再次运行程序,输入没有重复的值。

解:

全部重复:

1 1 1 1 1 
1 occurs 5 times 

没有重复:

1 2 3 4 5
1 occurs 1 times 
2 occurs 1 times 
3 occurs 1 times 
4 occurs 1 times 
5 occurs 1 times 

练习1.19

修改你为1.4.1节练习1.11(第11页)所编写的程序(打印一个范围内的数),使其能处理用户输入的第一个数比第二个数小的情况。

解:

#include <iostream>

int main()
{
    int start = 0, end = 0;
    std::cout << "Please input two num: ";
    std::cin >> start >> end;
    if (start <= end) {
        while (start <= end){
            std::cout << start << " ";
            ++start;
        }
        std::cout << std::endl;
    }
    else{
        std::cout << "start should be smaller than end !!!";
    }
}  

练习1.20

在网站http://www.informit.com/title/032174113 上,第1章的代码目录包含了头文件 Sales_item.h。将它拷贝到你自己的工作目录中。用它编写一个程序,读取一组书籍销售记录,将每条记录打印到标准输出上。

解:

#include <iostream>
#include "Sales_item.h"

int main()
{
	for (Sales_item item; std::cin >> item; std::cout << item << std::endl);
	return 0;
}

命令:

./main < data/add_item

输出:

0-201-78345-X 3 60 20
0-201-78345-X 2 50 25

练习1.21

编写程序,读取两个 ISBN 相同的 Sales_item 对象,输出他们的和。

解:

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item item_1;
    Sales_item item_2;
    std::cin >> item_1;
    std::cout << item_1 << std::endl;
    std::cin >> item_2;
    std::cout << item_2 << std::endl;
    std::cout << "sum of sale items: " << item_1 + item_2 << std::endl;
	return 0;
}

命令:

./main < data/add_item

输出:

0-201-78345-X 3 60 20
0-201-78345-X 2 50 25
sum of sale items: 0-201-78345-X 5 110 22

练习1.22

编写程序,读取多个具有相同 ISBN 的销售记录,输出所有记录的和。

解:

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item sum_item;
    std::cin >> sum_item;
    std::cout << sum_item << std::endl;
    for (Sales_item item; std::cin >> item; std::cout << item << std::endl){
        sum_item += item;
    }
    std::cout << "sum of sale items: " << sum_item << std::endl;
	return 0;
}

命令:

./main < data/add_item

输出:

0-201-78345-X 3 60 20
0-201-78345-X 2 50 25
sum of sale items: 0-201-78345-X 5 110 22

练习1.23

编写程序,读取多条销售记录,并统计每个 ISBN(每本书)有几条销售记录。

练习1.24

输入表示多个 ISBN 的多条销售记录来测试上一个程序,每个 ISBN 的记录应该聚在一起。

解:

#include <iostream>
#include "Sales_item.h"

int main()
{
    Sales_item total;
    if (std::cin >> total){
        Sales_item trans;
        while (std::cin >> trans){
            if (total.isbn() == trans.isbn()) {
                total += trans;
            }
            else {
                std::cout << total << std::endl;
                total = trans;
            }
        }
        std::cout << total << std::endl;
    }
    else {
        std::cerr << "No data?!" << std::endl;
        return -1;
    }
    return 0;
}

命令:

./main < data/book_sales

输出:

0-201-70353-X 4 99.96 24.99
0-201-82470-1 4 181.56 45.39
0-201-88954-4 16 198 12.375
0-399-82477-1 5 226.95 45.39
0-201-78345-X 5 110 22

练习1.25

借助网站上的Sales_item.h头文件,编译并运行本节给出的书店程序。


ch02

第二章 变量和基本类型

任何常用的编程语言都具备一组公共的语法特征,最基本的特征包括:

  • 整型、字符型等内置类型
  • 变量,用来为对象命名
  • 表达式和语句,用于操作上述数据类型的具体值
  • if 或 while 等控制结构,有选择地执行一些语句或重复地执行一些语句
  • 函数,用于定义可供随时调用的计算单元

大多数编程语言通过两种方式来进一步补充其基本特征:

  • 自定义数据类型,实现对语言的扩展
  • 将一些有用的功能封装成库函数

基本内置类型

基本算数类型:

类型含义最小尺寸
bool布尔类型8bits
char字符8bits
wchar_t宽字符16bits
char16_tUnicode字符16bits
char32_tUnicode字符32bits
short短整型16bits
int整型16bits (在32位机器中是32bits)
long长整型32bits
long long长整型64bits (是在C++11中新定义的)
float单精度浮点数6位有效数字
double双精度浮点数10位有效数字
long double扩展精度浮点数10位有效数字

如何选择类型

  • 1.当明确知晓数值不可能是负数时,选用无符号类型;
  • 2.使用int执行整数运算。一般long的大小和int一样,而short常常显得太小。除非超过了int的范围,选择long long。
  • 3.算术表达式中不要使用char或bool。
  • 4.浮点运算选用double。

类型转换

  • 非布尔型赋给布尔型,初始值为0则结果为false,否则为true。
  • 布尔型赋给非布尔型,初始值为false结果为0,初始值为true结果为1。

字面值常量

  • 一个形如42的值被称作字面值常量(literal)。
    • 整型和浮点型字面值。
    • 字符和字符串字面值。
- 使用空格连接,继承自C。
- 字符字面值:单引号, `'a'`
- 字符串字面值:双引号, `"Hello World"`
- 分多行书写字符串。
      std:cout<<"wow, a really, really long string"
                "literal that spans two lines" <<std::endl;
  • 转义序列。\n、\t等。
  • 布尔字面值。true,false。
  • 指针字面值。nullptr
字符串型实际上时常量字符构成的数组,结尾处以'\0'结束,所以字符串类型实际上长度比内容多1。

变量

变量提供一个具名的、可供程序操作的存储空间。 C++中变量和对象一般可以互换使用。

变量定义(define)

  • 定义形式:类型说明符(type specifier) + 一个或多个变量名组成的列表。如int sum = 0, value, units_sold = 0;
  • 初始化(initialize):对象在创建时获得了一个特定的值。
    • 初始化不是赋值!:
    • 初始化 = 创建变量 + 赋予初始值
    • 赋值 = 擦除对象的当前值 + 用新值代替
    • 列表初始化:使用花括号{},如int units_sold{0};
    • 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化。
    • 建议初始化每一个内置类型的变量。

变量的声明(declaration) vs 定义(define)

  • 为了支持分离式编译,C++将声明和定义区分开。声明使得名字为程序所知。定义负责创建与名字关联的实体。
  • extern:只是说明变量定义在其他地方。
  • 只声明而不定义: 在变量名前添加关键字 extern,如extern int i;。但如果包含了初始值,就变成了定义:extern double pi = 3.14;
  • 变量只能被定义一次,但是可以多次声明。定义只出现在一个文件中,其他文件使用该变量时需要对其声明。
- 同时存在全局和局部变量时,已定义局部变量的作用域中可用`::reused`显式访问全局变量reused。
- **但是用到全局变量时,尽量不适用重名的局部变量。**

变量命名规范

  1. 需体现实际意义
  2. 变量名用小写字母
  3. 自定义类名用大写字母开头:Sales_item
  4. 标识符由多个单词组成,中间须有明确区分:student_loan或studentLoan,不要用studentloan。

左值和右值

  • 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;
  • 右值(r-value)只能出现在赋值语句的右边,比如常量。

复合类型

引用

一般说的引用是指的左值引用
  • 引用:引用是一个对象的别名,引用类型引用(refer to)另外一种类型。如int &refVal = val;。
  • 引用必须初始化。
  • 引用和它的初始值是绑定bind在一起的,而不是拷贝。一旦定义就不能更改绑定为其他的对象

指针

int *p; //指向int型对象的指针
  • 是一种 "指向(point to)"另外一种类型的复合类型。
  • 定义指针类型: int *ip1;,从右向左读有助于阅读,ip1是指向int类型的指针。
  • 指针存放某个对象的地址。
  • 获取对象的地址: int i=42; int *p = &i;。 &是取地址符。
  • 指针的类型与所指向的对象类型必须一致(均为同一类型int、double等)
  • 指针的值的四种状态:
    • 1.指向一个对象;
    • 2.指向紧邻对象的下一个位置;
    • 3.空指针;
    • 4.无效指针。
    • >对无效指针的操作均会引发错误,第二种和第三种虽为有效的,但理论上是不被允许的
  • 指针访问对象: cout << *p;输出p指针所指对象的数据, *是解引用符。
  • 空指针不指向任何对象。使用int *p=nullptr;来使用空指针。
  • > 指针和引用的区别:引用本身并非一个对象,引用定义后就不能绑定到其他的对象了;指针并没有此限制,相当于变量一样使用。
  • > 赋值语句永远改变的是左侧的对象。
  • void*指针可以存放任意对象的地址。因无类型,仅操作内存空间,对所存对象无法访问。
  • 其他指针类型必须要与所指对象严格匹配。
  • 两个指针相减的类型是ptrdiff_t。
  • 建议:初始化所有指针。
  • int* p1, p2;//*是对p1的修饰,所以p2还是int型

const限定符

  • 动机:希望定义一些不能被改变值的变量。

初始化和const

  • const对象必须初始化,且不能被改变。
  • const变量默认不能被其他文件访问,非要访问,必须在指定const定义之前加extern。要想在多个文件中使用const变量共享,定义和声明都加extern关键字即可。

const的引用

  • reference to const(对常量的引用):指向const对象的引用,如 const int ival=1; const int &refVal = ival;,可以读取但不能修改refVal。
  • 临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
  • 对临时量的引用是非法行为。

指针和const

  • pointer to const(指向常量的指针):不能用于改变其所指对象的值, 如 const double pi = 3.14; const double *cptr = &pi;。
  • const pointer:指针本身是常量,也就是说指针固定指向该对象,(存放在指针中的地址不变,地址所对应的那个对象值可以修改)如 int i = 0; int *const ptr = &i;

顶层const

  • 顶层const:指针本身是个常量。
  • 底层const:指针指向的对象是个常量。拷贝时严格要求相同的底层const资格。

constexpr和常量表达式(▲可选)

  • 常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式。
  • C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量的表达式。

处理类型

类型别名

  • 传统别名:使用typedef来定义类型的同义词。 typedef double wages;
  • 新标准别名:别名声明(alias declaration): using SI = Sales_item;(C++11)
// 对于复合类型(指针等)不能代回原式来进行理解
typedef char *pstring;  // pstring是char*的别名
const pstring cstr = 0; // 指向char的常量指针
// 如改写为const char *cstr = 0;不正确,为指向const char的指针

// 辅助理解(可代回后加括号)
// const pstring cstr = 0;代回后const (char *) cstr = 0;
// const char *cstr = 0;即为(const char *) cstr = 0;

auto类型说明符 c++11

  • auto类型说明符:让编译器自动推断类型。
  • 一条声明语句只能有一个数据类型,所以一个auto声明多个变量时只能相同的变量类型(包括复杂类型&和*)。auto sz = 0, pi =3.14//错误
  • int i = 0, &r = i; auto a = r; 推断a的类型是int。
  • 会忽略顶层const。
  • const int ci = 1; const auto f = ci;推断类型是int,如果希望是顶层const需要自己加const

decltype类型指示符

  • 从表达式的类型推断出要定义的变量的类型。
  • decltype:选择并返回操作数的数据类型。
  • decltype(f()) sum = x; 推断sum的类型是函数f的返回类型。
  • 不会忽略顶层const。
  • 如果对变量加括号,编译器会将其认为是一个表达式,如int i-->(i),则decltype((i))得到结果为int&引用。
  • 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。
  • C++11

自定义数据结构

struct

尽量不要吧类定义和对象定义放在一起。如struct Student{} xiaoming,xiaofang;
  • 类可以以关键字struct开始,紧跟类名和类体。
  • 类数据成员:类体定义类的成员。
  • C++11:可以为类数据成员提供一个类内初始值(in-class initializer)。

编写自己的头文件

  • 头文件通常包含哪些只能被定义一次的实体:类、const和constexpr变量。

预处理器概述:

  • 预处理器(preprocessor):确保头文件多次包含仍能安全工作。
  • 当预处理器看到#include标记时,会用指定的头文件内容代替#include
  • 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。
    • #indef已定义时为真
    • #inndef未定义时为真
    • 头文件保护符的名称需要唯一,且保持全部大写。养成良好习惯,不论是否该头文件被包含,要加保护符。
#ifndef SALES_DATA_H  //SALES_DATA_H未定义时为真
#define SALES_DATA_H
strct Sale_data{
    ...
}
#endif

ch02

第二章 变量和基本类型

练习2.1

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

解:

C++ 规定 short 和 int 至少16位,long 至少32位,long long 至少64位。 带符号类型能够表示正数、负数和 0 ,而无符号类型只能够表示 0 和正整数。浮点数的取值范围和精度不同,计算效率也有差异。

练习2.2

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

解:

使用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

练习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) Who goes with Fergus?(换行),string 类型
  • (b) long double
  • (c) 无效,因为后缀f只能用于浮点字面量,而1024是整型。
  • (d) long double

练习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): 在这里wage是未定义的,应该在此之前将其定义。

double 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_str和global_int是全局变量,所以初值分别为空字符串和0。 local_int是局部变量并且没有初始化,它的初值是未定义的。 local_str 是 string 类的对象,它的值由类确定,为空字符串。

练习2.11

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

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

解:

(a): 定义 (b): 定义 (c): 声明

练习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), (c), (d) 非法。

练习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 。

练习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

练习2.18

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

解:

int a = 0, b = 1;
int *p1 = &a, *p2 = p1;

// change the value of a pointer.
p1 = &b;
// change the value to which the pointer points
*p2 = b;

练习2.19

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

解:

引用是另一个对象的别名,而指针本身就是一个对象。 引用必须初始化,并且一旦定义了引用就无法再绑定到其他对象。而指针无须在定义时赋初值,也可以重新赋值让其指向其他对象。

练习2.20

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

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

解:

让指针 pi 指向 i,然后将 i 的值重新赋值为 42 * 42 (1764)。

练习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 是不是一个空指针, 第二句判断 p 所指向的对象的值是不是为0

练习2.23

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

解:

能,可以使用try catch的异常处理来分辨指针p是否指向一个合法的对象,但通过普通控制结构无法实现。

练习2.24

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

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

解:

void *是从C语言那里继承过来的,可以指向任何类型的对象。 而其他指针类型必须要与所指对象严格匹配。

练习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 必须引用一个对象
int *const p2 = &i2;        // 合法,常量指针
const int i = -1, &r = 0;   // 合法
const int *const p3 = &i2;  // 合法
const int *p1 = &i2;        // 合法
const int &const r2;        // 不合法, r2 是引用, 引用自带顶层 const, 第二个const写法多余但合法, 但引用需要初始化.
const int i2 = i, &r = i;   // 合法

练习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 是常量指针且指向常量, 故p3 不能被修改, 本句赋值语句正在修改
p2 = p1;    // 不合法, p2是常量指针, 有顶层const, 不能被修改
ic = *p3;   // 不合法, 对 p3 取值后是一个 int 然后赋值给 ic, 但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 是顶层const,p2 是底层const,p3 既是顶层const又是底层const,r2 是底层const。

练习2.31

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

解:

r1 = v2; // 合法, 顶层const在拷贝时不受影响
p1 = p2; // 不合法, p2 是底层const,如果要拷贝必须要求 p1 也是底层const
p2 = p1; // 合法, int* 可以转换成const int*
p1 = p3; // 不合法, p3 是一个底层const,p1 不是
p2 = p3; // 合法, p2 和 p3 都是底层const,拷贝时忽略掉顶层const

练习2.32

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

int null = 0, *p = null;

解:

非法,即使int的值恰好是0,也不能直接给指针赋值int变量。应改为

int null = 0, *p = &null;

而且应该注意到,null都是小写,并不是关键字或者预处理变量。

练习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,所以不能被赋值

练习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;

解:

c 是 int 类型,值为 4。d 是 int & 类型,绑定到 a,a 的值为 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和引用保留起来。

int i = 0, &r = i;
//相同
auto a = i;
decltype(i) b = i;

//不同 d 是一个 int&
auto c = r;
decltype(r) d = r;

练习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页)的练习。


ch03

第三章 字符串、向量和数组

using声明

  • 使用某个命名空间:例如 using std::cin表示使用命名空间std中的名字cin。
  • 头文件中不应该包含using声明。这样使用了该头文件的源码也会使用这个声明,会带来风险。

string

  • 标准库类型string表示可变长的字符序列。
  • #include <string>,然后 using std::string;
  • string对象:注意,不同于字符串字面值。

定义和初始化string对象

初始化string对象的方式:

方式解释
string s1默认初始化,s1是个空字符串
string s2(s1)s2是s1的副本
string s2 = s1等价于s2(s1),s2是s1的副本
string s3("value")s3是字面值“value”的副本,除了字面值最后的那个空字符外
string s3 = "value"等价于s3("value"),s3是字面值"value"的副本
string s4(n, 'c')把s4初始化为由连续n个字符c组成的串
  • 拷贝初始化(copy initialization):使用等号=将一个已有的对象拷贝到正在创建的对象。
  • 直接初始化(direct initialization):通过括号给对象赋值。

string对象上的操作

string的操作:

操作解释
os << s将s写到输出流os当中,返回os
is >> s从is中读取字符串赋给s,字符串以空白分割,返回is
getline(is, s)从is中读取一行赋给s,返回is
s.empty()s为空返回true,否则返回false
s.size()返回s中字符的个数
s[n]返回s中第n个字符的引用,位置n从0计起
s1+s2返回s1和s2连接后的结果
s1=s2用s2的副本代替s1中原来的字符
s1==s2如果s1和s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感
s1!=s2同上
<, <=, >, >=利用字符在字典中的顺序进行比较,且对字母的大小写敏感(对第一个不相同的位置进行比较)
  • string io:
- 执行读操作`>>`:忽略掉开头的空白(包括空格、换行符和制表符),直到遇到下一处空白为止。
- `getline`:读取一整行,**包括空白符**。
  • s.size()返回的时string::size_type类型,记住是一个无符号类型的值,不要和int混用
  • s1+s2使用时,保证至少一侧是string类型。string s1 = "hello" + "world" // 错误,两侧均为字符串字面值
  • 字符串字面值和string是不同的类型。

处理string对象中的字符

  • ctype.h vs. cctype:C++修改了c的标准库,名称为去掉.h,前面加c。 > 如c++版本为cctype,c版本为ctype.h
    • 尽量使用c++版本的头文件,即cctype

cctype头文件中定义了一组标准函数:

函数解释
isalnum(c)当c是字母或数字时为真
isalpha(c)当c是字母时为真
iscntrl(c)当c是控制字符时为真
isdigit(c)当c是数字时为真
isgraph(c)当c不是空格但可以打印时为真
islower(c)当c是小写字母时为真
isprint(c)当c是可打印字符时为真
ispunct(c)当c是标点符号时为真
isspace(c)当c是空白时为真(空格、横向制表符、纵向制表符、回车符、换行符、进纸符)
isupper(c)当c是大写字母时为真
isxdigit(c)当c是十六进制数字时为真
tolower(c)当c是大写字母,输出对应的小写字母;否则原样输出c
toupper(c)当c是小写字母,输出对应的大写字母;否则原样输出c
  • 遍历字符串:使用范围for(range for)语句: for (auto c: str),或者 for (auto &c: str)使用引用直接改变字符串中的字符。 (C++11)
  • str[x],[]输入参数为string::size_type类型,给出int整型也会自动转化为该类型

vector

  • vector是一个容器,也是一个类模板;
  • #include <vector> 然后 using std::vector;
  • 容器:包含其他对象。
  • 类模板:本身不是类,但可以实例化instantiation出一个类。 vector是一个模板, vector<int>是一个类型。
  • 通过将类型放在类模板名称后面的尖括号中来指定类型,如vector<int> ivec。

定义和初始化vector对象

初始化vector对象的方法

方法解释
vector<T> v1v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1)v2中包含有v1所有元素的副本
vector<T> v2 = v1等价于v2(v1),v2中包含v1所有元素的副本
vector<T> v3(n, val)v3包含了n个重复的元素,每个元素的值都是val
vector<T> v4(n)v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a, b, c...}v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5={a, b, c...}等价于v5{a, b, c...}
  • 列表初始化: vector<string> v{"a", "an", "the"}; (C++11)

向vector对象中添加元素

  • v.push_back(e) 在尾部增加元素。

其他vector操作

vector支持的操作:

操作解释
v.emtpy()如果v不含有任何元素,返回真;否则返回假
v.size()返回v中元素的个数
v.push_back(t)向v的尾端添加一个值为t的元素
v[n]返回v中第n个位置上元素的引用
v1 = v2用v2中的元素拷贝替换v1中的元素
v1 = {a,b,c...}用列表中元素的拷贝替换v1中的元素
v1 == v2v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
v1 != v2同上
<,<=,>, >=以字典顺序进行比较
  • 范围for语句内不应该改变其遍历序列的大小。
  • vector对象(以及string对象)的下标运算符,只能对确知已存在的元素执行下标操作,不能用于添加元素。

迭代器iterator

  • 所有标准库容器都可以使用迭代器。
  • 类似于指针类型,迭代器也提供了对对象的间接访问。

使用迭代器

  • vector<int>::iterator iter。
  • auto b = v.begin();返回指向第一个元素的迭代器。
  • auto e = v.end();返回指向最后一个元素的下一个(哨兵,尾后,one past the end)的迭代器(off the end)。
  • 如果容器为空, begin()和 end()返回的是同一个迭代器,都是尾后迭代器。
  • 使用解引用符*访问迭代器指向的元素。
  • 养成使用迭代器和!=的习惯(泛型编程)。
  • 容器:可以包含其他对象;但所有的对象必须类型相同。
  • 迭代器(iterator):每种标准容器都有自己的迭代器。C++倾向于用迭代器而不是下标遍历元素。
  • const_iterator:只能读取容器内元素不能改变。
  • 箭头运算符: 解引用 + 成员访问,it->mem等价于 (*it).mem
  • 谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。

标准容器迭代器的运算符:

运算符解释
*iter返回迭代器iter所指向的元素的引用
iter->mem等价于(*iter).mem
++iter令iter指示容器中的下一个元素
--iter令iter指示容器中的上一个元素
iter1 == iter2判断两个迭代器是否相等

迭代器运算

vector和string迭代器支持的运算:

运算符解释
iter + n迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置和原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置。
iter - n迭代器减去一个整数仍得到一个迭代器,迭代器指示的新位置比原来向后移动了若干个元素。结果迭代器或者指向容器内的一个元素,或者指示容器尾元素的下一位置。
iter1 += n迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1
iter1 -= n迭代器减法的复合赋值语句,将iter2减n的结果赋给iter1
iter1 - iter2两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置。
>、>=、<、<=迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说明前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或尾元素的下一位置。
  • difference_type:保证足够大以存储任何两个迭代器对象间的距离,可正可负。

数组

  • 相当于vector的低级版,长度固定。

定义和初始化内置数组

  • 初始化:char input_buffer[buffer_size];,长度必须是const表达式,或者不写,让编译器自己推断。
  • 数组不允许直接赋值给另一个数组。

访问数组元素

  • 数组下标的类型:size_t 。
  • 字符数组的特殊性:结尾处有一个空字符,如 char a[] = "hello"; 。
  • 用数组初始化 vector: int a[] = {1,2,3,4,5}; vector<int> v(begin(a), end(a)); 。

数组和指针

  • 使用数组时,编译器一般会把它转换成指针。
  • 标准库类型限定使用的下标必须是无符号类型,而内置的下标可以处理负值。
  • 指针访问数组:在表达式中使用数组名时,名字会自动转换成指向数组的第一个元素的指针。

C风格字符串

  • 从C继承来的字符串。
  • 用空字符结束(\0)。
  • 对大多数应用来说,使用标准库 string比使用C风格字符串更安全、更高效。
  • 获取 string 中的 cstring : const char *str = s.c_str(); 。

C标准库String函数,定义在<cstring> 中:

函数介绍
strlen(p)返回p的长度,空字符不计算在内
strcmp(p1, p2)比较p1和p2的相等性。如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值。
strcat(p1, p2)将p2附加到p1之后,返回p1
strcpy(p1, p2)将p2拷贝给p1,返回p1

尽量使用vector和迭代器,少用数组

多维数组

  • 多维数组的初始化: int ia[3][4] = {{0,1,2,3}, ...}。
  • 使用范围for语句时,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

指针vs引用

  • 引用总是指向某个对象,定义引用时没有初始化是错的。
  • 给引用赋值,修改的是该引用所关联的对象的值,而不是让引用和另一个对象相关联。

指向指针的指针

  • 定义: int **ppi = &pi;
  • 解引用:**ppi

动态数组

  • 使用 new和 delete表达和c中malloc和free类似的功能,即在堆(自由存储区)中分配存储空间。
  • 定义: int *pia = new int[10]; 10可以被一个变量替代。
  • 释放: delete [] pia;,注意不要忘记[]。

ch03

第三章 字符串、向量和数组

练习3.1

使用恰当的using 声明重做 1.4.1节和2.6.2节的练习。

解:

1.4.1

#include <iostream>

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

int main()
{
	int sum = 0;
	for (int val = 1; val <= 10; ++val) sum += val;

	cout << "Sum of 1 to 10 inclusive is " << sum << endl;
	
	return 0;
}

2.6.2 类似

练习3.2

编写一段程序从标准输入中一次读入一行,然后修改该程序使其一次读入一个词。

解:

一次读入一行:

#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::endl;
using std::getline;

int main()
{
	string s;
	while (getline(cin,s))
	{
		cout << s << endl;
	}
	return 0;
}

一次读入一个词

#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::endl;
using std::getline;

int main()
{
	string s;
	while (cin >> s)
	{
		cout << s << endl;
	}
	return 0;
}

练习3.3

请说明string类的输入运算符和getline函数分别是如何处理空白字符的。

解:

  • 类似is >> s的读取:string对象会忽略开头的空白并从第一个真正的字符开始,直到遇见下一空白为止。
  • 类似getline(is, s)的读取:string对象会从输入流中读取字符,直到遇见换行符为止。

练习3.4

编写一段程序读取两个字符串,比较其是否相等并输出结果。如果不相等,输出比较大的那个字符串。改写上述程序,比较输入的两个字符串是否等长,如果不等长,输出长度较大的那个字符串。

解:

比较大的

#include <iostream>
#include <string>
using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string str1, str2;
	while (cin >> str1 >> str2)
	{
		if (str1 == str2)
			cout << "The two strings are equal." << endl;
		else
			cout << "The larger string is " << ((str1 > str2) ? str1 : str2);
	}

	return 0;
}

长度大的

#include <iostream>
#include <string>
using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string str1, str2;
	while (cin >> str1 >> str2)
	{
		if (str1.size() == str2.size())
			cout << "The two strings have the same length." << endl;
		else
			cout << "The longer string is " << ((str1.size() > str2.size()) ? str1 : str2) << endl;
	}

	return 0;
}

练习3.5

编写一段程序从标准输入中读入多个字符串并将他们连接起来,输出连接成的大字符串。然后修改上述程序,用空格把输入的多个字符串分割开来。

解:

未隔开的:

#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string result, s;
	while (cin >> s)
	{
		result += s;
	}
	cout << result << endl;

	return 0;
}

隔开的:

#include <iostream>
#include <string>

using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string result, s;
	while (cin >> s)
	{
		result += s + " ";
	}
	cout << result << endl;

	return 0;
}

练习3.6

编写一段程序,使用范围for语句将字符串内所有字符用X代替。

解:

#include <iostream>
#include <string>
#include <cctype>

using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string s = "this is a string";

	for (auto &x : s)
	{
		x = 'X';
	}

	cout << s << endl;
	return 0;
}

练习3.7

就上一题完成的程序而言,如果将循环控制的变量设置为char将发生什么?先估计一下结果,然后实际编程进行验证。

解:

如果设置为char,那么原来的字符串不会发生改变。

练习3.8

分别用while循环和传统for循环重写第一题的程序,你觉得哪种形式更好呢?为什么?

解:

#include <iostream>
#include <string>
#include <cctype>

using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string s = "this is a string";

	decltype(s.size()) i = 0;
	while (i != s.size())
	{
		s[i] = 'X';
		++i;
	}
	cout << s << endl;
	for (i = 0; i != s.size(); ++i)
	{
		s[i] = 'Y';
	}
	cout << s << endl;
	return 0;
}

范围for语句更好,不直接操作索引,更简洁。

练习3.9

下面的程序有何作用?它合法吗?如果不合法?为什么?

string s;
cout << s[0] << endl;

解:

不合法。使用下标访问空字符串是非法的行为。

练习3.10

编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。

解:

#include <iostream>
#include <string>
#include <cctype>

using std::string;
using std::cin;
using std::cout;
using std::endl;

int main()
{
	string s = "this, is. a :string!";
	string result;

	for (auto x : s)
	{
		if (!ispunct(x))
		{
			result += x;
		}
	}
	
	cout << result << endl;
	return 0;
}

练习3.11

下面的范围for语句合法吗?如果合法,c的类型是什么?

const string s = "Keep out!";
for(auto &c : s){ /* ... */ }

解:

要根据for循环中的代码来看是否合法,c是string 对象中字符的引用,s是常量。因此如果for循环中的代码重新给c赋值就会非法,如果不改变c的值,那么合法。

练习3.12

下列vector对象的定义有不正确的吗?如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。

vector<vector<int>> ivec;         // 在C++11当中合法
vector<string> svec = ivec;       // 不合法,类型不一样
vector<string> svec(10, "null");  // 合法

练习3.13

下列的vector对象各包含多少个元素?这些元素的值分别是多少?

vector<int> v1;         // size:0,  no values.
vector<int> v2(10);     // size:10, value:0
vector<int> v3(10, 42); // size:10, value:42
vector<int> v4{ 10 };     // size:1,  value:10
vector<int> v5{ 10, 42 }; // size:2,  value:10, 42
vector<string> v6{ 10 };  // size:10, value:""
vector<string> v7{ 10, "hi" };  // size:10, value:"hi"

练习3.14

编写一段程序,用cin读入一组整数并把它们存入一个vector对象。

解:

#include <iostream>
#include <string>
#include <cctype>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::vector;

int main()
{
	vector<int> v;
	int i;
	while (cin >> i)
	{
		v.push_back(i);
	}
	return 0;
}

练习3.15

改写上题程序,不过这次读入的是字符串。

解:

#include <iostream>
#include <string>
#include <cctype>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<string> v;
	string i;
	while (cin >> i)
	{
		v.push_back(i);
	}
	return 0;
}

练习3.16

编写一段程序,把练习3.13中vector对象的容量和具体内容输出出来

解:

#include <iostream>
#include <string>
#include <cctype>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<int> v1;         // size:0,  no values.
	vector<int> v2(10);     // size:10, value:0
	vector<int> v3(10, 42); // size:10, value:42
	vector<int> v4{ 10 };     // size:1,  value:10
	vector<int> v5{ 10, 42 }; // size:2,  value:10, 42
	vector<string> v6{ 10 };  // size:10, value:""
	vector<string> v7{ 10, "hi" };  // size:10, value:"hi"

	cout << "v1 size :" << v1.size() << endl;
	cout << "v2 size :" << v2.size() << endl;
	cout << "v3 size :" << v3.size() << endl;
	cout << "v4 size :" << v4.size() << endl;
	cout << "v5 size :" << v5.size() << endl;
	cout << "v6 size :" << v6.size() << endl;
	cout << "v7 size :" << v7.size() << endl;

	cout << "v1 content: ";
	for (auto i : v1)
	{
		cout << i << " , ";
	}
	cout << endl;

	cout << "v2 content: ";
	for (auto i : v2)
	{
		cout << i << " , ";
	}
	cout << endl;

	cout << "v3 content: ";
	for (auto i : v3)
	{
		cout << i << " , ";
	}
	cout << endl;

	cout << "v4 content: ";
	for (auto i : v4)
	{
		cout << i << " , ";
	}
	cout << endl;

	cout << "v5 content: ";
	for (auto i : v5)
	{
		cout << i << " , ";
	}
	cout << endl;

	cout << "v6 content: ";
	for (auto i : v6)
	{
		cout << i << " , ";
	}
	cout << endl;

	cout << "v7 content: ";
	for (auto i : v7)
	{
		cout << i << " , ";
	}
	cout << endl;
	return 0;
}

练习3.17

从cin读入一组词并把它们存入一个vector对象,然后设法把所有词都改为大写形式。输出改变后的结果,每个词占一行。

解:

#include <iostream>
#include <string>
#include <cctype>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<string> v;
	string s;

	while (cin >> s)
	{
		v.push_back(s);
	}

	for (auto &str : v)
	{
		for (auto &c : str)
		{
			c = toupper(c);
		}
	}

	for (auto i : v)
	{
		cout << i << endl;
	}
	return 0;
}

练习3.18

下面的程序合法吗?如果不合法,你准备如何修改?

vector<int> ivec;
ivec[0] = 42;

解:

不合法。应改为:

ivec.push_back(42);

练习3.19

如果想定义一个含有10个元素的vector对象,所有元素的值都是42,请例举三种不同的实现方法,哪种方式更好呢?

如下三种:

vector<int> ivec1(10, 42);
vector<int> ivec2{ 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 };
vector<int> ivec3;
for (int i = 0; i < 10; ++i)
	ivec3.push_back(42);

第一种方式最好。

练习3.20

读入一组整数并把他们存入一个vector对象,将每对相邻整数的和输出出来。改写你的程序,这次要求先输出第一个和最后一个元素的和,接着输出第二个和倒数第二个元素的和,以此类推。

解:

#include <iostream>
#include <string>
#include <cctype>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<int> ivec;
	int i;
	while (cin >> i)
	{
		ivec.push_back(i);
	}

	for (int i = 0; i < ivec.size() - 1; ++i)
	{
		cout << ivec[i] + ivec[i + 1] << endl;
	}
	
	//---------------------------------
	cout << "---------------------------------" << endl;
	
	int m = 0;
	int n = ivec.size() - 1;
	while (m < n)
	{
		cout << ivec[m] + ivec[n] << endl;
		++m;
		--n;
	}
	return 0;
}

练习3.21

请使用迭代器重做3.3.3节的第一个练习。

解:

#include <vector>
#include <iterator>
#include <string>
#include <iostream>

using std::vector;
using std::string;
using std::cout;
using std::endl;

void check_and_print(const vector<int>& vec)
{
	cout << "size: " << vec.size() << "  content: [";
	for (auto it = vec.begin(); it != vec.end(); ++it)
		cout << *it << (it != vec.end() - 1 ? "," : "");
	cout << "]\n" << endl;
}

void check_and_print(const vector<string>& vec)
{

	cout << "size: " << vec.size() << "  content: [";
	for (auto it = vec.begin(); it != vec.end(); ++it)
		cout << *it << (it != vec.end() - 1 ? "," : "");
	cout << "]\n" << endl;
}

int main()
{
	vector<int> v1;
	vector<int> v2(10);
	vector<int> v3(10, 42);
	vector<int> v4{ 10 };
	vector<int> v5{ 10, 42 };
	vector<string> v6{ 10 };
	vector<string> v7{ 10, "hi" };

	check_and_print(v1);
	check_and_print(v2);
	check_and_print(v3);
	check_and_print(v4);
	check_and_print(v5);
	check_and_print(v6);
	check_and_print(v7);

	return 0;
}

练习3.22

修改之前那个输出text第一段的程序,首先把text的第一段全部改成大写形式,然后输出它。

解: 略

练习3.23

编写一段程序,创建一个含有10个整数的vector对象,然后使用迭代器将所有元素的值都变成原来的两倍。输出vector对象的内容,检验程序是否正确。

解:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> v(10, 1);
    for (auto it=v.begin(); it!=v.end(); it++){
        *it *= 2;
    }
    for (auto one : v){
        cout << one <<endl;
    }
	return 0;
}

练习3.24

请使用迭代器重做3.3.3节的最后一个练习。

解:

#include <iostream>
#include <string>
#include <cctype>
#include <vector>

using std::cin;
using std::cout;
using std::endl;
using std::vector;
using std::string;

int main()
{
	vector<int> ivec;
	int i;
	while (cin >> i)
	{
		ivec.push_back(i);
	}

	for (auto it = ivec.begin(); it != ivec.end() - 1; ++it)
	{
		cout << *it + *(it + 1) << endl;
	}

	//---------------------------------
	cout << "---------------------------------" << endl;

	auto it1 = ivec.begin();
	auto it2 = ivec.end() - 1;
	while (it1 < it2)
	{
		cout << *it1 + *it2 << endl;
		++it1;
		--it2;
	}
	return 0;
}

练习3.25

3.3.3节划分分数段的程序是使用下标运算符实现的,请利用迭代器改写该程序实现完全相同的功能。

解:

#include <vector>
#include <iostream>

using std::vector; using std::cout; using std::cin; using std::endl;

int main()
{
	vector<unsigned> scores(11, 0);
	unsigned grade;
	while (cin >> grade)
	{
		if (grade <= 100)
			++*(scores.begin() + grade / 10);
	}

	for (auto s : scores)
		cout << s << " ";
	cout << endl;

	return 0;
}

练习3.26

在100页的二分搜索程序中,为什么用的是 mid = beg + (end - beg) / 2, 而非 mid = (beg + end) / 2 ; ?

解:

因为两个迭代器相互之间支持的运算只有 - ,而没有 + 。 但是迭代器和迭代器差值(整数值)之间支持 +。

练习3.27

假设txt_size是一个无参函数,它的返回值是int。请回答下列哪个定义是非法的,为什么?

unsigned buf_size = 1024;
(a) int ia[buf_size];
(b) int ia[4 * 7 - 14];
(c) int ia[txt_size()];
(d) char st[11] = "fundamental";

解:

  • (a) 非法。维度必须是一个常量表达式。
  • (b) 合法。
  • (c) 非法。txt_size() 的值必须要到运行时才能得到。
  • (d) 非法。数组的大小应该是12。

练习3.28

下列数组中元素的值是什么?

string sa[10];
int ia[10];
int main() {
	string sa2[10];
	int ia2[10];
}

解:

数组的元素会被默认初始化。 sa的元素值全部为空字符串,ia 的元素值全部为0。 sa2的元素值全部为空字符串,ia2的元素值全部未定义。

练习3.29

相比于vector 来说,数组有哪些缺点,请例举一些。

解:

  • 数组的大小是确定的。
  • 不能随意增加元素。
  • 不允许拷贝和赋值。

练习3.30

指出下面代码中的索引错误。

constexpr size_t array_size = 10;
int ia[array_size];
for (size_t ix = 1; ix <= array_size; ++ix)
	ia[ix] = ix;

解:

当ix增长到 10 的时候,ia[ix]的下标越界。

练习3.31

编写一段程序,定义一个含有10个int的数组,令每个元素的值就是其下标值。

#include <iostream>
using std::cout; using std::endl;

int main()
{
    int arr[10];
    for (auto i = 0; i < 10; ++i) arr[i] = i;
    for (auto i : arr) cout << i << " ";
    cout << endl;

    return 0;
}

练习3.32

将上一题刚刚创建的数组拷贝给另一数组。利用vector重写程序,实现类似的功能。

#include <iostream>
#include <vector>
using std::cout; using std::endl; using std::vector;

int main()
{
    // array
    int arr[10];
    for (int i = 0; i < 10; ++i) arr[i] = i;
    int arr2[10];
    for (int i = 0; i < 10; ++i) arr2[i] = arr[i];

    // vector
    vector<int> v(10);
    for (int i = 0; i != 10; ++i) v[i] = arr[i];
    vector<int> v2(v);
    for (auto i : v2) cout << i << " ";
    cout << endl;

    return 0;
}

练习3.33

对于104页的程序来说,如果不初始化scores将会发生什么?

解:

数组中所有元素的值将会未定义。

练习3.34

假定p1 和 p2 都指向同一个数组中的元素,则下面程序的功能是什么?什么情况下该程序是非法的?

p1 += p2 - p1;

解:

将 p1 移动到 p2 的位置。任何情况下都合法。

练习3.35

编写一段程序,利用指针将数组中的元素置为0。

解:

#include <iostream>
using std::cout; using std::endl;

int main()
{
    const int size = 10;
    int arr[size];
    for (auto ptr = arr; ptr != arr + size; ++ptr) *ptr = 0;

    for (auto i : arr) cout << i << " ";
    cout << endl;

    return 0;
}

练习3.36

编写一段程序,比较两个数组是否相等。再写一段程序,比较两个vector对象是否相等。

解:

#include <iostream>
#include <vector>
#include <iterator>

using std::begin; using std::end; using std::cout; using std::endl; using std::vector;

// pb point to begin of the array, pe point to end of the array.
bool compare(int* const pb1, int* const pe1, int* const pb2, int* const pe2)
{
    if ((pe1 - pb1) != (pe2 - pb2)) // have different size.
        return false;
    else
    {
        for (int* i = pb1, *j = pb2; (i != pe1) && (j != pe2); ++i, ++j)
            if (*i != *j) return false;
    }

    return true;
}

int main()
{
    int arr1[3] = { 0, 1, 2 };
    int arr2[3] = { 0, 2, 4 };

    if (compare(begin(arr1), end(arr1), begin(arr2), end(arr2)))
        cout << "The two arrays are equal." << endl;
    else
        cout << "The two arrays are not equal." << endl;

    cout << "==========" << endl;

    vector<int> vec1 = { 0, 1, 2 };
    vector<int> vec2 = { 0, 1, 2 };

    if (vec1 == vec2)
        cout << "The two vectors are equal." << endl;
    else
        cout << "The two vectors are not equal." << endl;

    return 0;
}

练习3.37

下面的程序是何含义,程序的输出结果是什么?

const char ca[] = { 'h', 'e', 'l', 'l', 'o' };
const char *cp = ca;
while (*cp) {
    cout << *cp << endl;
    ++cp;
}

解:

会将ca 字符数组中的元素打印出来。但是因为没有空字符的存在,程序不会退出循环。

练习3.38

在本节中我们提到,将两个指针相加不但是非法的,而且也没有什么意义。请问为什么两个指针相加没有意义?

解:

将两个指针相减可以表示两个指针(在同一数组中)相距的距离,将指针加上一个整数也可以表示移动这个指针到某一位置。但是两个指针相加并没有逻辑上的意义,因此两个指针不能相加。

练习3.39

编写一段程序,比较两个 string 对象。再编写一段程序,比较两个C风格字符串的内容。

解:

#include <iostream>
#include <string>
#include <cstring>
using std::cout; using std::endl; using std::string;

int main()
{
    // use string.
    string s1("Mooophy"), s2("Pezy");
    if (s1 == s2)
        cout << "same string." << endl;
    else if (s1 > s2)
        cout << "Mooophy > Pezy" << endl;
    else
        cout << "Mooophy < Pezy" << endl;

    cout << "=========" << endl;

    // use C-Style character strings.
    const char* cs1 = "Wangyue";
    const char* cs2 = "Pezy";
    auto result = strcmp(cs1, cs2);
    if (result == 0)
        cout << "same string." << endl;
    else if (result < 0)
        cout << "Wangyue < Pezy" << endl;
    else
        cout << "Wangyue > Pezy" << endl;

    return 0;
}

练习3.40

编写一段程序,定义两个字符数组并用字符串字面值初始化它们;接着再定义一个字符数组存放前面两个数组连接后的结果。使用strcpy和strcat把前两个数组的内容拷贝到第三个数组当中。

解:

#include <iostream>
#include <cstring>

const char cstr1[]="Hello";
const char cstr2[]="world!";

int main()
{
    constexpr size_t new_size = strlen(cstr1) + strlen(" ") + strlen(cstr2) +1;
    char cstr3[new_size];
    
    strcpy(cstr3, cstr1);
    strcat(cstr3, " ");
    strcat(cstr3, cstr2);
    
    std::cout << cstr3 << std::endl;
}

练习3.41

编写一段程序,用整型数组初始化一个vector对象。

#include <iostream>
#include <vector>
using std::vector; using std::cout; using std::endl; using std::begin; using std::end;

int main()
{
    int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    vector<int> v(begin(arr), end(arr));

    for (auto i : v) cout << i << " ";
    cout << endl;

    return 0;
}

练习3.42

编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。

解:

#include <iostream>
#include <vector>
using std::vector; using std::cout; using std::endl; using std::begin; using std::end;

int main()
{
    vector<int> v{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int arr[10];
    for (int i = 0; i != v.size(); ++i) arr[i] = v[i];

    for (auto i : arr) cout << i << " ";
    cout << endl;

    return 0;
}

练习3.43

编写3个不同版本的程序,令其均能输出ia的元素。 版本1使用范围for语句管理迭代过程;版本2和版本3都使用普通for语句,其中版本2要求使用下标运算符,版本3要求使用指针。 此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto关键字和decltype关键字。

解:

#include <iostream>
using std::cout; using std::endl;

int main()
{
    int arr[3][4] = 
    { 
        { 0, 1, 2, 3 },
        { 4, 5, 6, 7 },
        { 8, 9, 10, 11 }
    };

    // range for
    for (const int(&row)[4] : arr)
        for (int col : row) cout << col << " ";
    cout << endl;

    // for loop
    for (size_t i = 0; i != 3; ++i)
        for (size_t j = 0; j != 4; ++j) cout << arr[i][j] << " ";
    cout << endl;

    // using pointers.
    for (int(*row)[4] = arr; row != arr + 3; ++row)
        for (int *col = *row; col != *row + 4; ++col) cout << *col << " ";
    cout << endl;

    return 0;
}

练习3.44

改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。

解:

#include <iostream>
using std::cout; using std::endl;

int main()
{
    int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

    // a range for to manage the iteration
    // use type alias
    using int_array = int[4];
    for (int_array& p : ia)
        for (int q : p)
            cout << q << " ";
    cout << endl;

    // ordinary for loop using subscripts
    for (size_t i = 0; i != 3; ++i)
        for (size_t j = 0; j != 4; ++j)
            cout << ia[i][j] << " ";
    cout << endl;

    // using pointers.
    // use type alias
    for (int_array* p = ia; p != ia + 3; ++p)
        for (int *q = *p; q != *p + 4; ++q)
            cout << *q << " ";
    cout << endl;

    return 0;
}

练习3.45

再一次改写程序,这次使用 auto 关键字。

解:

#include <iostream>
using std::cout; using std::endl;

int main()
{
    int ia[3][4] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };

    // a range for to manage the iteration
    for (auto& p : ia)
        for (int q : p)
            cout << q << " ";
    cout << endl;

    // ordinary for loop using subscripts
    for (size_t i = 0; i != 3; ++i)
        for (size_t j = 0; j != 4; ++j)
            cout << ia[i][j] << " ";
    cout << endl;

    // using pointers.
    for (auto p = ia; p != ia + 3; ++p)
        for (int *q = *p; q != *p + 4; ++q)
            cout << *q << " ";
    cout << endl;

    return 0;
}

ch04

第四章 表达式

表达式基础

  • 运算对象转换:小整数类型会被提升为较大的整数类型
  • 重载运算符:当运算符作用在类类型的运算对象时,用户可以自行定义其含义。
  • 左值和右值:
- C中原意:左值**可以**在表达式左边,右值不能。
- `C++`:当一个对象被用作**右值**的时候,用的是对象的**值**(内容);
- 被用做**左值**时,用的是对象的**身份**(在内存中的位置)。
  • 求值顺序:int i = f1() + f2()
    • 先计算f1() + f2(),再计算int i = f1() + f2()。但是f1和f2的计算先后不确定
    • 但是,如果f1、f2都对同一对象进行了修改,因为顺序不确定,所以会编译出错,显示未定义

算术运算符

  • 溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。
  • bool类型不应该参与计算
  bool b=true;
  bool b2=-b;   //仍然为true
  //b为true,提升为对应int=1,-b=-1
  //b2=-1≠0,所以b2仍未true
  • 取余运算m%n,结果符号与m相同

逻辑运算符

  • 短路求值:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。先左再右
  • 小技巧,声明为引用类型可以避免对元素的拷贝,如下,如string特别大时可以节省大量时间。
vector<string> text;
for(const auto &s: text){
  cout<<s;
}

赋值运算符

  • 赋值运算的返回结果时它的左侧运算对象,且是一个左值。类型也就是左侧对象的类型。
  • 如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
  • 赋值运算符满足右结合律,这点和其他二元运算符不一样。 ival = jval = 0;等价于ival = (jval = 0);
  • 赋值运算优先级比较低,使用其当条件时应该加括号。
  • 复合赋值运算符,复合运算符只求值一次,普通运算符求值两次。(对性能有一点点点点影响) 任意复合运算符op等价于a = a op b;

递增递减运算符

  • 前置版本j = ++i,先加一后赋值
  • 后置版本j = i++,先赋值后加一

优先使用前置版本,后置多一步储存原始值。(除非需要变化前的值)

混用解引用和递增运算符

*iter++等价于*(iter++),递增优先级较高

auto iter = vi.begin();
while (iter!=vi.end()&&*iter>=0)
	cout<<*iter++<<endl;	// 输出当前值,指针向前移1
简介是一种美德,追求简洁能降低程序出错可能性

成员访问运算符

ptr->mem等价于(*ptr).mem

注意.运算符优先级大于*,所以记得加括号

条件运算符

  • 条件运算符(?:)允许我们把简单的if-else逻辑嵌入到单个表达式中去,按照如下形式:cond? expr1: expr2
  • 可以嵌套使用,右结合律,从右向左顺序组合
  • ```c++
finalgrade = (grade > 90) ? "high pass"
    : (grade < 60) ? "fail" : "pass";
//等价于
finalgrade = (grade > 90) ? "high pass"
    : ((grade < 60) ? "fail" : "pass");

- 输出表达式使用条件运算符记得加括号,条件运算符优先级太低。

## 位运算符

用于检查和设置二进制位的功能。

- 位运算符是作用于**整数类型**的运算对象。
- 二进制位向左移(`<<`)或者向右移(`>>`),移出边界外的位就被舍弃掉了。
- 位取反(`~`)(逐位求反)、与(`&`)、或(`|`)、异或(`^`)

有符号数负值可能移位后变号,所以强烈建议**位运算符仅用于无符号数**。

应用:

c++ unsigned long quiz1 = 0; // 每一位代表一个学生是否通过考试 1UL << 12; // 代表第12个学生通过 quiz1 |= (1UL << 12); // 将第12个学生置为已通过 quiz1 &= ~(1UL << 12); // 将第12个学生修改为未通过 bool stu12 = quiz1 & (1UL << 12); // 判断第12个学生是否通过


> 位运算符使用较少,但是重载cout、cin大家都用过

位运算符满足左结合律,优先级介于中间,使用时尽量加括号。

## sizeof运算符

- 返回一条表达式或一个类型名字所占的**字节数**。
- 返回的类型是 `size_t`的常量表达式。
- `sizeof`并不实际计算其运算对象的值。
- 两种形式:
  1. `sizeof (type)`,给出类型名
  2. `sizeof expr`,给出表达式
- 可用sizeof返回数组的大小

c++ int ia[10]; // sizeof(ia)返回整个数组所占空间的大小 // sizeof(ia)/sizeof(*ia)返回数组的大小 constexpr size_t sz = sizeof(ia)/sizeof(*ia); int arr[sz];


## 逗号运算符

从左向右依次求值。

左侧求值结果丢弃,逗号运算符**结果是右侧表达式**的值。

## 类型转换

### 隐式类型转换

> 设计为尽可能避免损失精度,即转换为更精细类型。

- 比 `int`类型小的整数值先提升为较大的整数类型。
- 条件中,非布尔转换成布尔。
- 初始化中,初始值转换成变量的类型。
- 算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。
- 函数调用时也会有转换。

#### 算术转换

##### 整型提升

* 常见的char、bool、short能存在int就会转换成int,否则提升为`unsigned int`
* `wchar_t,char16_t,char32_t`提升为整型中`int,long,long long ……`最小的,且能容纳原类型所有可能值的类型。

#### 其他转换

> p143

### 显式类型转换(尽量避免)

- **static_cast**:任何明确定义的类型转换,只要不包含底层const,都可以使用。 `double slope = static_cast<double>(j);`

- **dynamic_cast**:支持运行时类型识别。

- **const_cast**:只能改变运算对象的底层const,一般可用于去除const性质。 `const char *pc; char *p = const_cast<char*>(pc)`

  > 只有其可以改变常量属性

- **reinterpret_cast**:通常为运算对象的位模式提供低层次上的重新解释。

#### 旧式强制类型转换

`type expr`

## 运算符优先级表

> p147

---

## ch04

# 第四章 表达式

## 练习4.1

表达式`5 + 10 * 20 / 2`的求值结果是多少?

解:

等价于`5 + ((10 * 20) / 2) = 105`

## 练习4.2

根据4.12节中的表,在下述表达式的合理位置添加括号,使得添加括号后运算对象的组合顺序与添加括号前一致。
(a) `*vec.begin()`
(b) `*vec.begin() + 1`

解:

cpp *(vec.begin()) (*(vec.begin())) + 1


## 练习4.3

C++语言没有明确规定大多数二元运算符的求值顺序,给编译器优化留下了余地。这种策略实际上是在代码生成效率和程序潜在缺陷之间进行了权衡,你认为这可以接受吗?请说出你的理由。

解:

可以接受。C++的设计思想是尽可能地“相信”程序员,将效率最大化。然而这种思想却有着潜在的危害,就是无法控制程序员自身引发的错误。因此 Java 的诞生也是必然,Java的思想就是尽可能地“不相信”程序员。

## 练习4.4
在下面的表达式中添加括号,说明其求值过程及最终结果。编写程序编译该(不加括号的)表达式并输出结果验证之前的推断。

`12 / 3 * 4 + 5 * 15 + 24 % 4 / 2`

解:

`((12 / 3) * 4) + (5 * 15) + ((24 % 4) / 2) = 16 + 75 + 0 = 91`

## 练习4.5

写出下列表达式的求值结果。

cpp -30 * 3 + 21 / 5 // -90+4 = -86 -30 + 3 * 21 / 5 // -30+63/5 = -30+12 = -18 30 / 3 21 % 5 // 1021%5 = 210%5 = 0 -30 / 3 21 % 4 // -1021%4 = -210%4 = -2


## 练习4.6

写出一条表达式用于确定一个整数是奇数还是偶数。

解:

cpp if (i % 2 == 0) / ... /

或者

cpp if (i & 0x1) / ... /


## 练习4.7

溢出是何含义?写出三条将导致溢出的表达式。

解:

当计算的结果超出该类型所能表示的范围时就会产生溢出。

cpp short svalue = 32767; ++svalue; // -32768 unsigned uivalue = 0; --uivalue; // 4294967295 unsigned short usvalue = 65535; ++usvalue; // 0


## 练习4.8

说明在逻辑与、逻辑或及相等性运算符中运算对象的求值顺序。

解:

- 逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为 **短路求值**。
- 相等性运算符未定义求值顺序。

## 练习4.9

解释在下面的`if`语句中条件部分的判断过程。

cpp const char *cp = "Hello World"; if (cp && *cp)


解:

首先判断`cp`,`cp` 不是一个空指针,因此`cp`为真。然后判断`*cp`,`*cp` 的值是字符`'H'`,非0。因此最后的结果为真。

## 练习4.10

为`while`循环写一个条件,使其从标准输入中读取整数,遇到`42`时停止。

解:

cpp int i; while(cin >> i && i != 42)


## 练习4.11

书写一条表达式用于测试4个值a、b、c、d的关系,确保a大于b、b大于c、c大于d。

解:

cpp a>b && b>c && c>d


## 练习4.12

假设`i`、`j`和`k`是三个整数,说明表达式`i != j < k`的含义。

解:

这个表达式等于`i != (j < k)`。首先得到`j < k`的结果为`true`或`false`,转换为整数值是`1`或`0`,然后判断`i`不等于`1`或`0` ,最终的结果为`bool`值。

## 练习4.13

在下述语句中,当赋值完成后 i 和 d 的值分别是多少?

cpp int i; double d; d = i = 3.5; // i = 3, d = 3.0 i = d = 3.5; // d = 3.5, i = 3


## 练习4.14

执行下述 if 语句后将发生什么情况?

cpp if (42 = i) // 编译错误。赋值运算符左侧必须是一个可修改的左值。而字面值是右值。 if (i = 42) // true.


## 练习4.15

下面的赋值是非法的,为什么?应该如何修改?

cpp double dval; int ival; int *pi; dval = ival = pi = 0;


解:
`p`是指针,不能赋值给`int`,应该改为:

cpp dval = ival = 0; pi = 0;


## 练习4.16

尽管下面的语句合法,但它们实际执行的行为可能和预期并不一样,为什么?应该如何修改?

cpp if (p = getPtr() != 0) if (i = 1024)


解:

cpp if ((p=getPtr()) != 0) if (i == 1024)


## 练习4.17

说明前置递增运算符和后置递增运算符的区别。

解:

前置递增运算符将对象本身作为左值返回,而后置递增运算符将对象原始值的副本作为右值返回。

## 练习4.18

如果132页那个输出`vector`对象元素的`while`循环使用前置递增运算符,将得到什么结果?

解:

将会从第二个元素开始取值,并且最后对`v.end()`进行取值,结果是未定义的。

## 练习4.19

假设`ptr`的类型是指向`int`的指针、`vec`的类型是`vector`、`ival`的类型是`int`,说明下面的表达式是何含义?如果有表达式不正确,为什么?应该如何修改?

cpp (a) ptr != 0 && *ptr++ (b) ival++ && ival (c) vec[ival++] <= vec[ival]


解:

- (a) 判断`ptr`不是一个空指针,并且`ptr`当前指向的元素的值也为真,然后将`ptr`指向下一个元素
- (b) 判断`ival`的值为真,并且`(ival + 1)`的值也为真
- (c) 表达式有误。C++并没有规定`<=`运算符两边的求值顺序,应该改为`vec[ival] <= vec[ival+1]`

## 练习4.20

假设`iter`的类型是`vector<string>::iterator`, 说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?

cpp (a) *iter++; (b) (*iter)++; (c) *iter.empty(); (d) iter->empty(); (e) ++*iter; (f) iter++->empty();


解:

- (a)合法。返回迭代器所指向的元素,然后迭代器递增。
- (b)不合法。因为`vector`元素类型是`string`,没有`++`操作。
- (c)不合法。这里应该加括号。
- (d)合法。判断迭代器当前的元素是否为空。
- (e)不合法。`string`类型没有`++`操作。
- (f)合法。判断迭代器当前元素是否为空,然后迭代器递增。

## 练习4.21

编写一段程序,使用条件运算符从`vector`中找到哪些元素的值是奇数,然后将这些奇数值翻倍。

解:

cpp #include #include

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

int main() {

vector<int> ivec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
for (auto &i : ivec)
{
	cout << ((i & 0x1) ? i * 2 : i) << " ";
}
cout << endl;
return 0;

}


## 练习4.22

本节的示例程序将成绩划分为`high pass`、`pass` 和 `fail` 三种,扩展该程序使其进一步将 60 分到 75 分之间的成绩设定为`low pass`。要求程序包含两个版本:一个版本只使用条件运算符;另一个版本使用1个或多个`if`语句。哪个版本的程序更容易理解呢?为什么?

解:

cpp #include using std::cout; using std::cin; using std::endl;

int main() {

for (unsigned g; cin >> g;)
{
	auto result = g > 90 ? "high pass" : g < 60 ? "fail" : g < 75 ? "low pass" : "pass";
	cout << result << endl;
	// -------------------------
	if (g > 90)         cout << "high pass";
	else if (g < 60)    cout << "fail";
	else if (g < 75)    cout << "low pass";
	else                cout << "pass";
	cout << endl;
}
return 0;

}

第二个版本容易理解。当条件运算符嵌套层数变多之后,代码的可读性急剧下降。而`if else`的逻辑很清晰。

## 练习4.23

因为运算符的优先级问题,下面这条表达式无法通过编译。根据4.12节中的表指出它的问题在哪里?应该如何修改?

cpp string s = "word"; string pl = s + s[s.size() - 1] == 's' ? "" : "s" ;


解:

加法运算符的优先级高于条件运算符。因此要改为:

cpp string pl = s + (s[s.size() - 1] == 's' ? "" : "s") ;


## 练习4.24

本节的示例程序将成绩划分为`high pass`、`pass`、和`fail`三种,它的依据是条件运算符满足右结合律。假如条件运算符满足的是左结合律,求值的过程将是怎样的?

解:

如果条件运算符满足的是左结合律。那么

`finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";`
等同于
`finalgrade = ((grade > 90) ? "high pass" : (grade < 60)) ? "fail" : "pass";`
假如此时 `grade > 90` ,第一个条件表达式的结果是 `"high pass"` ,而字符串字面值的类型是 `const char *`,非空所以为真。因此第二个条件表达式的结果是 `"fail"`。这样就出现了自相矛盾的逻辑。

## 练习4.25

如果一台机器上`int`占32位、`char`占8位,用的是`Latin-1`字符集,其中字符`'q'` 的二进制形式是`01110001`,那么表达式`~'q' << 6`的值是什么?

解:

首先将`char`类型提升为`int`类型,即`00000000 00000000 00000000 01110001`,然后取反,再左移6位,结果是-7296。

## 练习4.26

在本节关于测验成绩的例子中,如果使用`unsigned int` 作为`quiz1` 的类型会发生什么情况?

解:

在有的机器上,`unsigned int` 类型可能只有 16 位,因此结果是未定义的。

## 练习4.27

下列表达式的结果是什么?

cpp unsigned long ul1 = 3, ul2 = 7; (a) ul1 & ul2 (b) ul1 | ul2 (c) ul1 && ul2 (d) ul1 || ul2


解:

- (a) 3
- (b) 7
- (c) true
- (d) ture

## 练习4.28

编写一段程序,输出每一种内置类型所占空间的大小。

解:

cpp #include

using namespace std;

int main() {

cout << "bool:\t\t" << sizeof(bool) << " bytes" << endl << endl;
cout << "char:\t\t" << sizeof(char) << " bytes" << endl;
cout << "wchar_t:\t" << sizeof(wchar_t) << " bytes" << endl;
cout << "char16_t:\t" << sizeof(char16_t) << " bytes" << endl;
cout << "char32_t:\t" << sizeof(char32_t) << " bytes" << endl << endl;
cout << "short:\t\t" << sizeof(short) << " bytes" << endl;
cout << "int:\t\t" << sizeof(int) << " bytes" << endl;
cout << "long:\t\t" << sizeof(long) << " bytes" << endl;
cout << "long long:\t" << sizeof(long long) << " bytes" << endl << endl;
cout << "float:\t\t" << sizeof(float) << " bytes" << endl;
cout << "double:\t\t" << sizeof(double) << " bytes" << endl;
cout << "long double:\t" << sizeof(long double) << " bytes" << endl << endl;
return 0;

}


输出:

bool: 1 bytes

char: 1 bytes wchar_t: 4 bytes char16_t: 2 bytes char32_t: 4 bytes

short: 2 bytes int: 4 bytes long: 8 bytes long long: 8 bytes

float: 4 bytes double: 8 bytes long double: 16 bytes


## 练习4.29

推断下面代码的输出结果并说明理由。实际运行这段程序,结果和你想象的一样吗?如不一样,为什么?

cpp int x[10]; int *p = x; cout << sizeof(x)/sizeof(*x) << endl; cout << sizeof(p)/sizeof(*p) << endl;


解:

第一个输出结果是 10。
第二个结果取决于机器。sizeof(p)即int\*的内存空间大小, 32位机4B, 64位机8B; sizeof(\*p)即sizeof(int), 通常为4B, C++标准规定其不得小于2B。

## 练习4.30

根据4.12节中的表,在下述表达式的适当位置加上括号,使得加上括号之后的表达式的含义与原来的含义相同。

cpp (a) sizeof x + y (b) sizeof p->mem[i] (c) sizeof a < b (d) sizeof f()


解:

cpp (a) (sizeof x) + y (b) sizeof(p->mem[i]) (c) sizeof(a) < b (d) sizeof(f())


## 练习4.31

本节的程序使用了前置版本的递增运算符和递减运算符,解释为什么要用前置版本而不用后置版本。要想使用后置版本的递增递减运算符需要做哪些改动?使用后置版本重写本节的程序。

解:

在4.5节(132页)已经说过了,除非必须,否则不用递增递减运算符的后置版本。在这里要使用后者版本的递增递减运算符不需要任何改动。

## 练习4.32
解释下面这个循环的含义。

cpp constexpr int size = 5; int ia[size] = { 1, 2, 3, 4, 5 }; for (int *ptr = ia, ix = 0;

ix != size && ptr != ia+size;
++ix, ++ptr) { /* ... */ }

解:

这个循环在遍历数组`ia`,指针`ptr`和整型`ix`都是起到一个循环计数的功能。

## 练习4.33

根据4.12节中的表说明下面这条表达式的含义。

cpp someValue ? ++x, ++y : --x, --y


解:

逗号表达式的优先级是最低的。因此这条表达式也等于:

cpp (someValue ? ++x, ++y : --x), --y

如果`someValue`的值为真,`x` 和 `y` 的值都自增并返回 `y` 值,然后丢弃`y`值,`y`递减并返回`y`值。如果`someValue`的值为假,`x` 递减并返回`x` 值,然后丢弃`x`值,`y`递减并返回`y`值。



## 练习4.34

根据本节给出的变量定义,说明在下面的表达式中将发生什么样的类型转换:

cpp (a) if (fval) (b) dval = fval + ival; (c) dval + ival * cval;


需要注意每种运算符遵循的是左结合律还是右结合律。

解:

cpp (a) fval 转换为 bool 类型 (b) ival 转换为 float ,相加的结果转换为 double (c) cval 转换为 int,然后相乘的结果转换为 double


## 练习4.35

假设有如下的定义:

cpp char cval; int ival; unsigned int ui; float fval; double dval;


请回答在下面的表达式中发生了隐式类型转换吗?如果有,指出来。

cpp (a) cval = 'a' + 3; (b) fval = ui - ival * 1.0; (c) dval = ui * fval; (d) cval = ival + fval + dval;


解:

- (a) `'a'` 转换为 `int` ,然后与 `3` 相加的结果转换为 `char`
- (b) `ival` 转换为 `double`,`ui` 转换为 `double`,结果转换为 `float`
- (c) `ui` 转换为 `float`,结果转换为 `double`
- (d) `ival` 转换为 `float`,与`fval`相加后的结果转换为 `double`,最后的结果转换为`char`

## 练习4.36

假设 `i` 是`int`类型,`d` 是`double`类型,书写表达式 `i*=d` 使其执行整数类型的乘法而非浮点类型的乘法。

解:

cpp i *= static_cast(d);


## 练习4.37

练习4.37
用命名的强制类型转换改写下列旧式的转换语句。

cpp int i; double d; const string ps; char pc; void *pv; (a) pv = (void*)ps; (b) i = int(*pc); (c) pv = &d; (d) pc = (char*)pv;


解:

cpp (a) pv = static_cast>(const_cast>(ps)); (b) i = static_cast(*pc); (c) pv = static_cast(&d); (d) pc = static_cast(pv);


## 练习4.38
说明下面这条表达式的含义。

cpp double slope = static_cast(j/i);


解:

将`j/i`的结果值转换为`double`,然后赋值给`slope`。




---

## ch05

# 第五章 语句

## 简单语句

- **表达式语句**:一个表达式末尾加上分号,就变成了表达式语句。
- **空语句**:只有一个单独的分号。
- **复合语句(块)**:用花括号 `{}`包裹起来的语句和声明的序列。一个块就是一个作用域。

## 条件语句

- **悬垂else**(dangling else):用来描述在嵌套的`if else`语句中,如果`if`比`else`多时如何处理的问题。C++使用的方法是`else`匹配最近没有配对的`if`。

## 迭代语句

- **while**:当不确定到底要迭代多少次时,使用 `while`循环比较合适,比如读取输入的内容。
- **for**: `for`语句可以省略掉 `init-statement`, `condition`和 `expression`的任何一个;**甚至全部**。
- **范围for**: `for (declaration: expression) statement`

## 跳转语句

- **break**:`break`语句负责终止离它最近的`while`、`do while`、`for`或者`switch`语句,并从这些语句之后的第一条语句开始继续执行。
- **continue**:终止最近的循环中的当前迭代并立即开始下一次迭代。只能在`while`、`do while`、`for`循环的内部。

## try语句块和异常处理

- **throw表达式**:异常检测部分使用 `throw`表达式来表示它遇到了无法处理的问题。我们说 `throw`引发 `raise`了异常。
- **try语句块**:以 `try`关键词开始,以一个或多个 `catch`字句结束。 `try`语句块中的代码抛出的异常通常会被某个 `catch`捕获并处理。 `catch`子句也被称为**异常处理代码**。
- **异常类**:用于在 `throw`表达式和相关的 `catch`子句之间传递异常的具体信息。



---

## ch05

# 第五章 语句

## 练习5.1

什么是空语句?什么时候会用到空语句?

解:

只含义一个单独的分号的语句是空语句。如:`;`。

如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。

cpp while (cin >> s && s != sought)

;

## 练习5.2

什么是块?什么时候会用到块?

解:

用花括号括起来的语句和声明的序列就是块。

cpp {

// ...

}

如果在程序的某个地方,语法上需要一条语句,而逻辑上需要多条语句,此时应该使用块

cpp while (val <= 10) {

sum += val;
++val;

}


## 练习5.3
使用逗号运算符重写1.4.1节的`while`循环,使它不再需要块,观察改写之后的代码可读性提高了还是降低了。

cpp while (val <= 10)

sum += val, ++val;
代码的可读性反而降低了。

## 练习5.4

说明下列例子的含义,如果存在问题,试着修改它。

cpp (a) while (string::iterator iter != s.end()) { / . . . / }

(b) while (bool status = find(word)) { / . . . / }

	if (!status) { /* . . . */ }

解:

- (a) 这个循环试图用迭代器遍历`string`,但是变量的定义应该放在循环的外面,目前每次循环都会重新定义一个变量,明显是错误的。
- (b) 这个循环的`while`和`if`是两个独立的语句,`if`语句中无法访问`status`变量,正确的做法是应该将`if`语句包含在`while`里面。

## 练习5.5
写一段自己的程序,使用`if else`语句实现把数字转换为字母成绩的要求。

cpp #include #include #include using std::vector; using std::string; using std::cout; using std::endl; using std::cin;

int main() {

vector<string> scores = { "F", "D", "C", "B", "A", "A++" };
for (int g; cin >> g;)
{
    string letter;
    if (g < 60)
    {
        letter = scores[0];
    }
    else
    {
        letter = scores[(g - 50) / 10];
        if (g != 100)
            letter += g % 10 > 7 ? "+" : g % 10 < 3 ? "-" : "";
    }
    cout << letter << endl;
}
return 0;

}


## 练习5.6

改写上一题的程序,使用条件运算符代替`if else`语句。

cpp #include #include #include using std::vector; using std::string; using std::cout; using std::endl; using std::cin;

int main() {

vector<string> scores = { "F", "D", "C", "B", "A", "A++" };
int grade = 0;
while (cin >> grade)
{
    string lettergrade = grade < 60 ? scores[0] : scores[(grade - 50) / 10];
    lettergrade += (grade == 100 || grade < 60) ? "" : (grade % 10 > 7) ? "+" : (grade % 10 < 3) ? "-" : "";
    cout << lettergrade << endl;
}
return 0;

}


## 练习5.7
改写下列代码段中的错误。

cpp (a) if (ival1 != ival2)

	ival1 = ival2
else 
	ival1 = ival2 = 0;

(b) if (ival < minval)

	minval = ival;
	occurs = 1;

(c) if (int ival = get_value())

	cout << "ival = " << ival << endl;
if (!ival)
	cout << "ival = 0\n";

(d) if (ival = 0)

	ival = get_value();

解:

- (a) `ival1 = ival2` 后面少了分号。
- (b) 应该用花括号括起来。
- (c) `if (!ival)` 应该改为 `else`。
- (d) `if (ival = 0)` 应该改为 `if (ival == 0)`。

## 练习5.8
什么是“悬垂else”?C++语言是如何处理else子句的?

解:

用来描述在嵌套的`if else`语句中,如果`if`比`else`多时如何处理的问题。C++使用的方法是`else`匹配最近没有配对的`if`。

## 练习5.9
编写一段程序,使用一系列`if`语句统计从`cin`读入的文本中有多少元音字母。

解:

cpp #include

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

int main() {

unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (cin >> ch)
{
	if (ch == 'a') ++aCnt;
	else if (ch == 'e') ++eCnt;
	else if (ch == 'i') ++iCnt;
	else if (ch == 'o') ++oCnt;
	else if (ch == 'u') ++uCnt;
}
cout << "Number of vowel a: \t" << aCnt << '\n'
	<< "Number of vowel e: \t" << eCnt << '\n'
	<< "Number of vowel i: \t" << iCnt << '\n'
	<< "Number of vowel o: \t" << oCnt << '\n'
	<< "Number of vowel u: \t" << uCnt << endl;
return 0;

}


## 练习5.10
我们之前实现的统计元音字母的程序存在一个问题:如果元音字母以大写形式出现,不会被统计在内。编写一段程序,既统计元音字母的小写形式,也统计元音字母的大写形式,也就是说,新程序遇到'a'和'A'都应该递增`aCnt`的值,以此类推。

解:

cpp #include using std::cin; using std::cout; using std::endl;

int main() {

unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (cin >> ch)
	switch (ch)
{
	case 'a':
	case 'A':
		++aCnt;
		break;
	case 'e':
	case 'E':
		++eCnt;
		break;
	case 'i':
	case 'I':
		++iCnt;
		break;
	case 'o':
	case 'O':
		++oCnt;
		break;
	case 'u':
	case 'U':
		++uCnt;
		break;
}
cout << "Number of vowel a(A): \t" << aCnt << '\n'
	<< "Number of vowel e(E): \t" << eCnt << '\n'
	<< "Number of vowel i(I): \t" << iCnt << '\n'
	<< "Number of vowel o(O): \t" << oCnt << '\n'
	<< "Number of vowel u(U): \t" << uCnt << endl;
return 0;

}


## 练习5.11
修改统计元音字母的程序,使其也能统计空格、制表符、和换行符的数量。

解:

cpp #include

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

int main() {

unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, spaceCnt = 0, tabCnt = 0, newLineCnt = 0;
char ch;
while (cin >> std::noskipws >> ch)  //noskipws(no skip whitespce)
	switch (ch)
{
	case 'a':
	case 'A':
		++aCnt;
		break;
	case 'e':
	case 'E':
		++eCnt;
		break;
	case 'i':
	case 'I':
		++iCnt;
		break;
	case 'o':
	case 'O':
		++oCnt;
		break;
	case 'u':
	case 'U':
		++uCnt;
		break;
	case ' ':
		++spaceCnt;
		break;
	case '\t':
		++tabCnt;
		break;
	case '\n':
		++newLineCnt;
		break;
}
cout << "Number of vowel a(A): \t" << aCnt << '\n'
	<< "Number of vowel e(E): \t" << eCnt << '\n'
	<< "Number of vowel i(I): \t" << iCnt << '\n'
	<< "Number of vowel o(O): \t" << oCnt << '\n'
	<< "Number of vowel u(U): \t" << uCnt << '\n'
	<< "Number of space: \t" << spaceCnt << '\n'
	<< "Number of tab char: \t" << tabCnt << '\n'
	<< "Number of new line: \t" << newLineCnt << endl;
return 0;

}


其中,使用 `std::noskipws`可以保留默认跳过的空格。

## 练习5.12
修改统计元音字母的程序,使其能统计含以下两个字符的字符序列的数量:`ff`、`fl`和`fi`。

解:

cpp #include

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

int main() {

unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, spaceCnt = 0, tabCnt = 0, newLineCnt = 0, ffCnt = 0, flCnt = 0, fiCnt = 0;
char ch, prech = '\0';
while (cin >> std::noskipws >> ch)
{
	switch (ch)
	{
	case 'a':
	case 'A':
		++aCnt;
		break;
	case 'e':
	case 'E':
		++eCnt;
		break;
	case 'i':
		if (prech == 'f') ++fiCnt;
	case 'I':
		++iCnt;
		break;
	case 'o':
	case 'O':
		++oCnt;
		break;
	case 'u':
	case 'U':
		++uCnt;
		break;
	case ' ':
		++spaceCnt;
		break;
	case '\t':
		++tabCnt;
		break;
	case '\n':
		++newLineCnt;
		break;
	case 'f':
		if (prech == 'f') ++ffCnt;
		break;
	case 'l':
		if (prech == 'f') ++flCnt;
		break;
	}
	prech = ch;
}
cout << "Number of vowel a(A): \t" << aCnt << '\n'
	<< "Number of vowel e(E): \t" << eCnt << '\n'
	<< "Number of vowel i(I): \t" << iCnt << '\n'
	<< "Number of vowel o(O): \t" << oCnt << '\n'
	<< "Number of vowel u(U): \t" << uCnt << '\n'
	<< "Number of space: \t" << spaceCnt << '\n'
	<< "Number of tab char: \t" << tabCnt << '\n'
	<< "Number of new line: \t" << newLineCnt << '\n'
	<< "Number of ff: \t" << ffCnt << '\n'
	<< "Number of fl: \t" << flCnt << '\n'
	<< "Number of fi: \t" << fiCnt << endl;
return 0;

}


## 练习5.13
下面显示的每个程序都含有一个常见的编码错误,指出错误在哪里,然后修改它们。

cpp (a) unsigned aCnt = 0, eCnt = 0, iouCnt = 0;

char ch = next_text();
switch (ch) {
    case 'a': aCnt++;
    case 'e': eCnt++;
    default: iouCnt++;
}

(b) unsigned index = some_value();

switch (index) {
    case 1:
        int ix = get_value();
        ivec[ ix ] = index;
        break;
    default:
        ix = ivec.size()-1;
        ivec[ ix ] = index;
}

(c) unsigned evenCnt = 0, oddCnt = 0;

int digit = get_num() % 10;
switch (digit) {
    case 1, 3, 5, 7, 9:
        oddcnt++;
        break;
    case 2, 4, 6, 8, 10:
        evencnt++;
        break;
}

(d) unsigned ival=512, jval=1024, kval=4096;

unsigned bufsize;
unsigned swt = get_bufCnt();
switch(swt) {
    case ival:
        bufsize = ival * sizeof(int);
        break;
    case jval:
        bufsize = jval * sizeof(int);
        break;
    case kval:
        bufsize = kval * sizeof(int);
        break;
}

解:

(a) 少了`break`语句。应该为:

cpp unsigned aCnt = 0, eCnt = 0, iouCnt = 0;

char ch = next_text();
switch (ch) {
	case 'a': aCnt++; break;
	case 'e': eCnt++; break;
	default: iouCnt++; break;
}
	
(b) 在`default`分支当中,`ix`未定义。应该在外部定义`ix`。

cpp unsigned index = some_value();

int ix;
switch (index) {
    case 1:
        ix = get_value();
        ivec[ ix ] = index;
        break;
    default:
        ix = static_cast<int>(ivec.size())-1;
        ivec[ ix ] = index;
}
    
(c) `case`后面应该用冒号而不是逗号。

cpp unsigned evenCnt = 0, oddCnt = 0;

int digit = get_num() % 10;
switch (digit) {
    case 1: case 3: case 5: case 7: case 9:
        oddcnt++;
        break;
    case 2: case 4: case 6: case 8: case 0:
        evencnt++;
        break;
}
    
(d) `case`标签必须是整型常量表达式。

cpp const unsigned ival=512, jval=1024, kval=4096;

unsigned bufsize;
unsigned swt = get_bufCnt();
switch(swt) {
    case ival:
        bufsize = ival * sizeof(int);
        break;
    case jval:
        bufsize = jval * sizeof(int);
        break;
    case kval:
        bufsize = kval * sizeof(int);
        break;
}
    
## 练习5.14
编写一段程序,从标准输入中读取若干`string`对象并查找连续重复出现的单词,所谓连续重复出现的意思是:一个单词后面紧跟着这个单词本身。要求记录连续重复出现的最大次数以及对应的单词。如果这样的单词存在,输出重复出现的最大次数;如果不存在,输出一条信息说明任何单词都没有连续出现过。
例如:如果输入是:

how now now now brown cow cow

那么输出应该表明单词now连续出现了3次。

解:

cpp #include #include

using std::cout; using std::cin; using std::endl; using std::string; using std::pair;

int main() {

pair<string, int> max_duplicated;
int count = 0;
for (string str, prestr; cin >> str; prestr = str)
{
    if (str == prestr) ++count;
    else count = 0; 
    if (count > max_duplicated.second) max_duplicated = { prestr, count };
}

if (max_duplicated.first.empty()) cout << "There's no duplicated string." << endl;
else cout << "the word " << max_duplicated.first << " occurred " << max_duplicated.second + 1 << " times. " << endl;

return 0;

}


## 练习5.15
说明下列循环的含义并改正其中的错误。

cpp (a) for (int ix = 0; ix != sz; ++ix) { / ... / }

if (ix != sz)
	// . . .

(b) int ix;

for (ix != sz; ++ix) { /* ... */ }

(c) for (int ix = 0; ix != sz; ++ix, ++sz) { /.../ }


解:

应该改为下面这样:

cpp (a) int ix;

for (ix = 0; ix != sz; ++ix)  { /* ... */ }
if (ix != sz)
// . . .

(b) int ix;

for (; ix != sz; ++ix) { /* ... */ }

(c) for (int ix = 0; ix != sz; ++ix) { /.../ }


## 练习5.16

`while`循环特别适用于那种条件不变、反复执行操作的情况,例如,当未达到文件末尾时不断读取下一个值。
`for`循环更像是在按步骤迭代,它的索引值在某个范围内一次变化。根据每种循环的习惯各自编写一段程序,然后分别用另一种循环改写。
如果只能使用一种循环,你倾向于哪种?为什么?

解:

cpp int i; while ( cin >> i )

// ...

for (int i = 0; cin >> i;)

// ...

for (int i = 0; i != size; ++i)

// ...

int i = 0; while (i != size) {

// ...
++i;

}

如果只能用一种循环,我会更倾向使用`while`,因为`while`显得简洁,代码可读性强。

## 练习5.17

假设有两个包含整数的`vector`对象,编写一段程序,检验其中一个`vector`对象是否是另一个的前缀。
为了实现这一目标,对于两个不等长的`vector`对象,只需挑出长度较短的那个,把它的所有元素和另一个`vector`对象比较即可。
例如,如果两个`vector`对象的元素分别是0、1、1、2 和 0、1、1、2、3、5、8,则程序的返回结果为真。

解:

cpp #include #include

using std::cout; using std::vector;

bool is_prefix(vector const& lhs, vector const& rhs) {

if(lhs.size() > rhs.size())
    return is_prefix(rhs, lhs);
for(unsigned i = 0; i != lhs.size(); ++i)
    if(lhs[i] != rhs[i]) return false;
return true;

}

int main() {

vector<int> l{ 0, 1, 1, 2 };
vector<int> r{ 0, 1, 1, 2, 3, 5, 8 };
cout << (is_prefix(r, l) ? "yes\n" : "no\n");
return 0;

}


## 练习5.18
说明下列循环的含义并改正其中的错误。

cpp (a) do { // 应该添加花括号

    int v1, v2;
    cout << "Please enter two numbers to sum:" ;
    if (cin >> v1 >> v2)
        cout << "Sum is: " << v1 + v2 << endl;
}while (cin);

(b) int ival;

do {
    // . . .
} while (ival = get_response()); // 应该将ival 定义在循环外

(c) int ival = get_response();

do {
    ival = get_response();
} while (ival); // 应该将ival 定义在循环外

## 练习5.19
编写一段程序,使用`do while`循环重复地执行下述任务:
首先提示用户输入两个`string`对象,然后挑出较短的那个并输出它。

解:

cpp #include #include

using std::cout; using std::cin; using std::endl; using std::string;

int main() {

string rsp;
do {
    cout << "Input two strings: ";
    string str1, str2;
    cin >> str1 >> str2;
    cout << (str1 <= str2 ? str1 : str2) 
         << " is less than the other. " << "\n\n"
         << "More? Enter yes or no: ";
    cin >> rsp;
} while (!rsp.empty() && tolower(rsp[0]) == 'y');
return 0;

}


## 练习5.20
编写一段程序,从标准输入中读取`string`对象的序列直到连续出现两个相同的单词或者所有的单词都读完为止。
使用`while`循环一次读取一个单词,当一个单词连续出现两次时使用`break`语句终止循环。
输出连续重复出现的单词,或者输出一个消息说明没有任何单词是连续重复出现的。

解:

cpp #include #include using std::cout; using std::cin; using std::endl; using std::string;

int main() {

string read, tmp;
while (cin >> read)
    if (read == tmp) break; else tmp = read;
if (cin.eof())  cout << "no word was repeated." << endl; //eof(end of file)判断输入是否结束,或者文件结束符,等同于 CTRL+Z
else            cout << read << " occurs twice in succession." << endl;
return 0;

}


## 练习5.21
修改5.5.1节练习题的程序,使其找到的重复单词必须以大写字母开头。

解:

cpp #include using std::cin; using std::cout; using std::endl; #include using std::string;

int main() {

string curr, prev;
bool no_twice = true;
while (cin >> curr) 
{
    if (isupper(curr[0]) && prev == curr)
    {
        cout << curr << ": occurs twice in succession." << endl;
        no_twice = false;
        break;
    }
    prev = curr;
}
if (no_twice)
    cout << "no word was repeated." << endl;
return 0;

}


## 练习5.22
本节的最后一个例子跳回到`begin`,其实使用循环能更好的完成该任务,重写这段代码,注意不再使用`goto`语句。

cpp // 向后跳过一个带初始化的变量定义是合法的 begin:

int sz = get_size();
if (sz <= 0) {
    goto begin;
}

解:

用 for 循环修改的话就是这样

cpp for (int sz = get_size(); sz <=0; sz = get_size())

;

## 练习5.23
编写一段程序,从标准输入读取两个整数,输出第一个数除以第二个数的结果。

解:

cpp #include using std::cin; using std::cout; using std::endl;

int main() {

int i, j; 
cin >> i >> j;
cout << i / j << endl;
return 0;

}


## 练习5.24
修改你的程序,使得当第二个数是0时抛出异常。先不要设定`catch`子句,运行程序并真的为除数输入0,看看会发生什么?

解:

cpp #include #include

int main(void) {

int i, j;
std::cin >> i >> j;
if (j == 0)
    throw std::runtime_error("divisor is 0");
std::cout << i / j << std::endl;
return 0;

}


## 练习5.25
修改上一题的程序,使用`try`语句块去捕获异常。`catch`子句应该为用户输出一条提示信息,询问其是否输入新数并重新执行`try`语句块的内容。

解:

cpp #include #include using std::cin; using std::cout; using std::endl; using std::runtime_error;

int main(void) {

for (int i, j; cout << "Input two integers:\n", cin >> i >> j; )
{
    try 
    {
        if (j == 0) 
            throw runtime_error("divisor is 0");
        cout << i / j << endl;
    }
    catch (runtime_error err) 
    {
        cout << err.what() << "\nTry again? Enter y or n" << endl;
        char c;
        cin >> c;
        if (!cin || c == 'n')
            break;
    }
}
return 0;

}


---

## ch06

# 第六章 函数

## 函数基础

- **函数定义**:包括返回类型、函数名字和0个或者多个**形参**(parameter)组成的列表和函数体。
- **调用运算符**:调用运算符的形式是一对圆括号 `()`,作用于一个表达式,该表达式是函数或者指向函数的指针。
- 圆括号内是用逗号隔开的**实参**(argument)列表。
- 函数调用过程:
  - 1.主调函数(calling function)的执行被中断。
  - 2.被调函数(called function)开始执行。
- **形参和实参**:形参和实参的**个数**和**类型**必须匹配上。
- **返回类型**: `void`表示函数不返回任何值。函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。
- **名字**:名字的作用于是程序文本的一部分,名字在其中可见。

### 局部对象

- **生命周期**:对象的生命周期是程序执行过程中该对象存在的一段时间。
- **局部变量**(local variable):形参和函数体内部定义的变量统称为局部变量。它对函数而言是局部的,对函数外部而言是**隐藏**的。
- **自动对象**:只存在于块执行期间的对象。当块的执行结束后,它的值就变成**未定义**的了。
- **局部静态对象**: `static`类型的局部变量,生命周期贯穿函数调用前后。

### 函数声明

- **函数声明**:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称**函数原型**。
- **在头文件中进行函数声明**:建议变量在头文件中声明;在源文件中定义。
- **分离编译**: `CC a.cc b.cc`直接编译生成可执行文件;`CC -c a.cc b.cc`编译生成对象代码`a.o b.o`; `CC a.o b.o`编译生成可执行文件。

## 参数传递

- 形参初始化的机理和变量初始化一样。
- **引用传递**(passed by reference):又称传引用调用(called by reference),指**形参是引用类型**,引用形参是它对应的实参的别名。
- **值传递**(passed by value):又称传值调用(called by value),指实参的值是通过**拷贝**传递给形参。

### 传值参数

- 当初始化一个非引用类型的变量时,初始值被拷贝给变量。
- 函数对形参做的所有操作都不会影响实参。
- **指针形参**:常用在C中,`C++`建议使用引用类型的形参代替指针。

### 传引用参数

- 通过使用引用形参,允许函数改变一个或多个实参的值。
- 引用形参直接关联到绑定的对象,而非对象的副本。
- 使用引用形参可以用于**返回额外的信息**。
- 经常用引用形参来避免不必要的复制。
- `void swap(int &v1, int &v2)`
- 如果无需改变引用形参的值,最好将其声明为常量引用。

### const形参和实参

- 形参的顶层`const`被忽略。`void func(const int i);`调用时既可以传入`const int`也可以传入`int`。
- 我们可以使用非常量初始化一个底层`const`对象,但是反过来不行。
- 在函数中,不能改变实参的**局部副本**。
- 尽量使用常量引用。

### 数组形参

- 当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
- 要注意数组的实际长度,不能越界。

### main处理命令行选项

- `int main(int argc, char *argv[]){...}`
- 第一个形参代表参数的个数;第二个形参是参数C风格字符串数组。

### 可变形参

`initializer_list`提供的操作(`C++11`):

| 操作 | 解释 |
|-----|-----|
| `initializer_list<T> lst;` | 默认初始化;`T`类型元素的空列表 |
| `initializer_list<T> lst{a,b,c...};` | `lst`的元素数量和初始值一样多;`lst`的元素是对应初始值的副本;列表中的元素是`const`。 |
| `lst2(lst)` | 拷贝或赋值一个`initializer_list`对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。 |
| `lst2 = lst` | 同上 |
| `lst.size()` | 列表中的元素数量 |
| `lst.begin()` | 返回指向`lst`中首元素的指针 |
| `lst.end()` | 返回指向`lst`中微元素下一位置的指针 |

`initializer_list`使用demo:

cpp void err_msg(ErrCode e, initializer_list il){

cout << e.msg << endl;
for (auto bed = il.begin(); beg != il.end(); ++ beg)
    cout << *beg << " ";
cout << endl;

}

err_msg(ErrCode(0), {"functionX", "okay});


- 所有实参类型相同,可以使用 `initializer_list`的标准库类型。
- 实参类型不同,可以使用`可变参数模板`。
- 省略形参符: `...`,便于`C++`访问某些C代码,这些C代码使用了 `varargs`的C标准功能。

## 返回类型和return语句

### 无返回值函数

没有返回值的 `return`语句只能用在返回类型是 `void`的函数中,返回 `void`的函数不要求非得有 `return`语句。

### 有返回值函数

- `return`语句的返回值的类型必须和函数的返回类型相同,或者能够**隐式地**转换成函数的返回类型。
- 值的返回:返回的值用于初始化调用点的一个**临时量**,该临时量就是函数调用的结果。
- **不要返回局部对象的引用或指针**。
- **引用返回左值**:函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型得到右值。
- **列表初始化返回值**:函数可以返回花括号包围的值的列表。(`C++11`)
- **主函数main的返回值**:如果结尾没有`return`,编译器将隐式地插入一条返回0的`return`语句。返回0代表执行成功。

### 返回数组指针

- `Type (*function (parameter_list))[dimension]`
- 使用类型别名: `typedef int arrT[10];` 或者 `using arrT = int[10;]`,然后 `arrT* func() {...}`
- 使用 `decltype`: `decltype(odd) *arrPtr(int i) {...}`
- **尾置返回类型**: 在形参列表后面以一个`->`开始:`auto func(int i) -> int(*)[10]`(`C++11`)

## 函数重载

- **重载**:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。
- `main`函数不能重载。
- **重载和const形参**:
  - 一个有顶层const的形参和没有它的函数无法区分。 `Record lookup(Phone* const)`和 `Record lookup(Phone*)`无法区分。
  - 相反,是否有某个底层const形参可以区分。 `Record lookup(Account*)`和 `Record lookup(const Account*)`可以区分。
- **重载和作用域**:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。

## 特殊用途语言特性

### 默认实参

- `string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');`
- 一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。

### 内联(inline)函数

- 普通函数的缺点:调用函数比求解等价表达式要慢得多。
- `inline`函数可以避免函数调用的开销,可以让编译器在编译时**内联地展开**该函数。
- `inline`函数应该在头文件中定义。

### constexpr函数

- 指能用于常量表达式的函数。
- `constexpr int new_sz() {return 42;}`
- 函数的返回类型及所有形参类型都要是字面值类型。
- `constexpr`函数应该在头文件中定义。

### 调试帮助

- `assert`预处理宏(preprocessor macro):`assert(expr);`

开关调试状态:

`CC -D NDEBUG main.c`可以定义这个变量`NDEBUG`。

cpp void print(){

#ifndef NDEBUG
    cerr << __func__ << "..." << endl;
#endif

}


## 函数匹配

- 重载函数匹配的**三个步骤**:1.候选函数;2.可行函数;3.寻找最佳匹配。
- **候选函数**:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
- **可行函数**:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
- **寻找最佳匹配**:基本思想:实参类型和形参类型越接近,它们匹配地越好。

## 函数指针

- **函数指针**:是指向函数的指针。
- `bool (*pf)(const string &, const string &);` 注:两端的括号不可少。
- **函数指针形参**:
  - 形参中使用函数定义或者函数指针定义效果一样。
  - 使用类型别名或者`decltype`。
- **返回指向函数的指针**:1.类型别名;2.尾置返回类型。


---

## ch06

# 第六章 函数

## 练习6.1
实参和形参的区别的什么?

解:

实参是函数调用的实际值,是形参的初始值。

## 练习6.2
请指出下列函数哪个有错误,为什么?应该如何修改这些错误呢?

cpp (a) int f() {

     string s;
     // ...
     return s;

} (b) f2(int i) { / ... / } (c) int calc(int v1, int v1) { / ... / } (d) double square (double x) return x * x;


解:

应该改为下面这样:

cpp (a) string f() {

     string s;
     // ...
     return s;

} (b) void f2(int i) { / ... / } (c) int calc(int v1, int v2) { / ... / return ; } (d) double square (double x) { return x * x; }


## 练习6.3
编写你自己的`fact`函数,上机检查是否正确。注:阶乘。

解:

cpp #include

int fact(int i) {

if(i<0)
{
    std::runtime_error err("Input cannot be a negative number");
    std::cout << err.what() << std::endl;
}
return i > 1 ? i * fact( i - 1 ) : 1;

}

int main() {

std::cout << std::boolalpha << (120 == fact(5)) << std::endl;
return 0;

}


启用`std::boolalpha`,可以输出 `"true"`或者 `"false"`。

## 练习6.4
编写一个与用户交互的函数,要求用户输入一个数字,计算生成该数字的阶乘。在main函数中调用该函数。

cpp #include #include

int fact(int i) {

return i > 1 ? i * fact(i - 1) : 1;

}

void interactive_fact() {

std::string const prompt = "Enter a number within [1, 13) :\n";
std::string const out_of_range = "Out of range, please try again.\n";
for (int i; std::cout << prompt, std::cin >> i; )
{
    if (i < 1 || i > 12)
    {
        std::cout << out_of_range; 
        continue;
    }
    std::cout << fact(i) << std::endl;
}

}

int main() {

interactive_fact();
return 0;

}


## 练习6.5
编写一个函数输出其实参的绝对值。

cpp #include

int abs(int i) { return i > 0 ? i : -i; }

int main() { std::cout << abs(-5) << std::endl; return 0; }


## 练习6.6
说明形参、局部变量以及局部静态变量的区别。编写一个函数,同时达到这三种形式。

解:

形参定义在函数形参列表里面;局部变量定义在代码块里面;
局部静态变量在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止时才被销毁。

cpp // 例子 int count_add(int n) // n是形参 {

static int ctr = 0;    // ctr 是局部静态变量
ctr += n;
return ctr;

}

int main() {

for (int i = 0; i != 10; ++i)  // i 是局部变量
  cout << count_add(i) << endl;
return 0;

}


## 练习6.7
编写一个函数,当它第一次被调用时返回0,以后每次被调用返回值加1。

解:

cpp int generate() {

static int ctr = 0;
return ctr++;

}


## 练习6.8
编写一个名为Chapter6.h 的头文件,令其包含6.1节练习中的函数声明。

解:

cpp

int fact(int val); int func();

template //参考:https://blog.csdn.net/fightingforcv/article/details/51472586

T abs(T i) {

return i >= 0 ? i : -i;

}


## 练习6.9 : fact.cc | factMain.cc
编写你自己的fact.cc 和factMain.cc ,这两个文件都应该包含上一小节的练习中编写的 Chapter6.h 头文件。通过这些文件,理解你的编译器是如何支持分离式编译的。

解:

fact.cc:

cpp #include "Chapter6.h" #include

int fact(int val) {

if (val == 0 || val == 1) return 1;
else return val * fact(val-1);

}

int func() {

int n, ret = 1;
std::cout << "input a number: ";
std::cin >> n;
while (n > 1) ret *= n--;
return ret;

}


factMain.cc:

cpp #include "Chapter6.h" #include

int main() {

std::cout << "5! is " << fact(5) << std::endl; 
std::cout << func() << std::endl; 
std::cout << abs(-9.78) << std::endl;

}


编译: `g++ factMain.cpp fact.cpp -o main`

## 练习6.10
编写一个函数,使用指针形参交换两个整数的值。
在代码中调用该函数并输出交换后的结果,以此验证函数的正确性。

解:

cpp #include #include

void swap(int lhs, int rhs) {

int tmp;
tmp = *lhs;
*lhs = *rhs;
*rhs = tmp;

}

int main() {

for (int lft, rht; std::cout << "Please Enter:\n", std::cin >> lft >> rht;)
{
	swap(&lft, &rht);
	std::cout << lft << " " << rht << std::endl;
}
return 0;

}


## 练习6.11
编写并验证你自己的reset函数,使其作用于引用类型的参数。注:reset即置0。

解:

cpp #include

void reset(int &i) {

i = 0;

}

int main() {

int i = 42;
reset(i);
std::cout << i  << std::endl;
return 0;

}


## 练习6.12
改写6.2.1节练习中的程序,使其引用而非指针交换两个整数的值。你觉得哪种方法更易于使用呢?为什么?

cpp #include #include

void swap(int& lhs, int& rhs) {

int temp = lhs;
lhs = rhs;
rhs = temp;

}

int main() {

for (int left, right; std::cout << "Please Enter:\n", std::cin >> left >> right; )
{
    swap(left, right);
    std::cout << left << " " << right << std::endl;
}
return 0;

}

很明显引用更好用。

## 练习6.13
假设`T`是某种类型的名字,说明以下两个函数声明的区别:
一个是`void f(T)`, 另一个是`void f(&T)`。

解:

`void f(T)`的参数通过值传递,在函数中`T`是实参的副本,改变`T`不会影响到原来的实参。
`void f(&T)`的参数通过引用传递,在函数中的`T`是实参的引用,`T`的改变也就是实参的改变。

## 练习6.14
举一个形参应该是引用类型的例子,再举一个形参不能是引用类型的例子。

解:

例如交换两个整数的函数,形参应该是引用

cpp void swap(int& lhs, int& rhs) {

int temp = lhs;
lhs = rhs;
rhs = temp;

}


当实参的值是右值时,形参不能为引用类型

cpp int add(int a, int b) {

return a + b;

}

int main() {

int i = add(1,2);
return 0;

}


## 练习6.15
说明`find_char`函数中的三个形参为什么是现在的类型,特别说明为什么`s`是常量引用而`occurs`是普通引用?
为什么`s`和`occurs`是引用类型而`c`不是?
如果令`s`是普通引用会发生什么情况?
如果令`occurs`是常量引用会发生什么情况?

解:

- 因为字符串可能很长,因此使用引用避免拷贝;
- 而在函数中我们不希望改变`s`的内容,所以令`s`为常量。
- `occurs`是要传到函数外部的变量,所以使用引用,`occurs`的值会改变,所以是普通引用。
- 因为我们只需要`c`的值,这个实参可能是右值(右值实参无法用于引用形参),所以`c`不能用引用类型。
- 如果`s`是普通引用,也可能会意外改变原来字符串的内容。
- `occurs`如果是常量引用,那么意味着不能改变它的值,那也就失去意义了。

## 练习6.16
下面的这个函数虽然合法,但是不算特别有用。指出它的局限性并设法改善。

cpp bool is_empty(string& s) { return s.empty(); }


解:

局限性在于常量字符串和字符串字面值无法作为该函数的实参,如果下面这样调用是非法的:

cpp const string str; bool flag = is_empty(str); //非法 bool flag = is_empty("hello"); //非法


所以要将这个函数的形参定义为常量引用:

cpp bool is_empty(const string& s) { return s.empty(); }


## 练习6.17
编写一个函数,判断`string`对象中是否含有大写字母。
编写另一个函数,把`string`对象全部改写成小写形式。
在这两个函数中你使用的形参类型相同吗?为什么?

解:

两个函数的形参不一样。第一个函数使用常量引用,第二个函数使用普通引用。

## 练习6.18
为下面的函数编写函数声明,从给定的名字中推测函数具备的功能。

- (a) 名为`compare`的函数,返回布尔值,两个参数都是`matrix`类的引用。
- (b) 名为`change_val`的函数,返回`vector`的迭代器,有两个参数:一个是`int`,另一个是`vector`的迭代器。

解:

cpp (a) bool compare(matrix &m1, matrix &m2); (b) vector::iterator change_val(int, vector::iterator);


## 练习6.19
假定有如下声明,判断哪个调用合法、哪个调用不合法。对于不合法的函数调用,说明原因。

cpp double calc(double); int count(const string &, char); int sum(vector::iterator, vector::iterator, int); vector vec(10); (a) calc(23.4, 55.1); (b) count("abcda",'a'); (c) calc(66); (d) sum(vec.begin(), vec.end(), 3.8);


解:

- (a) 不合法。`calc`只有一个参数。
- (b) 合法。
- (c) 合法。
- (d) 合法。

## 练习6.20
引用形参什么时候应该是常量引用?如果形参应该是常量引用,而我们将其设为了普通引用,会发生什么情况?

解:

应该尽量将引用形参设为常量引用,除非有明确的目的是为了改变这个引用变量。
如果形参应该是常量引用,而我们将其设为了普通引用,那么常量实参将无法作用于普通引用形参。

## 练习6.21
编写一个函数,令其接受两个参数:一个是`int`型的数,另一个是`int`指针。
函数比较`int`的值和指针所指的值,返回较大的那个。
在该函数中指针的类型应该是什么?

解:

cpp #include using std::cout;

int larger_one(const int i, const int *const p) {

return (i > *p) ? i : *p;

}

int main() {

int i = 6;
cout << larger_one(7, &i);
return 0;

}


应该是`const int *`类型。

## 练习6.22
编写一个函数,令其交换两个`int`指针。

解:

cpp #include #include

void swap(int& lft, int& rht) {

auto tmp = lft;
lft = rht;
rht = tmp;

}

int main() {

int i = 42, j = 99;
auto lft = &i;
auto rht = &j;
swap(lft, rht);
std::cout << *lft << " " << *rht << std::endl;
return 0;

}


## 练习6.23
参考本节介绍的几个`print`函数,根据理解编写你自己的版本。
依次调用每个函数使其输入下面定义的`i`和`j`:

cpp int i = 0, j[2] = { 0, 1 };


解:

cpp #include using std::cout; using std::endl; using std::begin; using std::end;

void print(const int *pi) {

if(pi)
    cout << *pi << endl;

}

void print(const char *p) {

if (p)
    while (*p) cout << *p++;
cout << endl;

}

void print(const int beg, const int end) {

while (beg != end)
    cout << *beg++ << endl;

}

void print(const int ia[], size_t size) {

for (size_t i = 0; i != size; ++i) {
    cout << ia[i] << endl;
}

}

void print(int (&arr)[2]) {

for (auto i : arr)
    cout << i << endl;

}

int main() {

int i = 0, j[2] = { 0, 1 };
char ch[5] = "pezy";

print(ch);
print(begin(j), end(j));
print(&i);
print(j, end(j)-begin(j));
print(j);

return 0;

}


## 练习6.24
描述下面这个函数的行为。如果代码中存在问题,请指出并改正。

cpp void print(const int ia[10]) {

for (size_t i = 0; i != 10; ++i)
	cout << ia[i] << endl;

}


解:

当数组作为实参的时候,会被自动转换为指向首元素的指针。
因此函数形参接受的是一个指针。
如果要让这个代码成功运行(不更改也可以运行),可以将形参改为数组的引用。

cpp void print(const int (&ia)[10]) {

for (size_t i = 0; i != 10; ++i)
	cout << ia[i] << endl;

}


## 练习6.25
编写一个`main`函数,令其接受两个实参。把实参的内容连接成一个`string`对象并输出出来。

## 练习6.26
编写一个程序,使其接受本节所示的选项;输出传递给`main`函数的实参内容。

解:

包括6.25

cpp #include #include

int main(int argc, char **argv) {

std::string str;
for (int i = 1; i != argc; ++i)
    str += std::string(argv[i]) + " ";
std::cout << str << std::endl;
return 0;

}


## 练习6.27
编写一个函数,它的参数是`initializer_list`类型的对象,函数的功能是计算列表中所有元素的和。

解:

cpp #include #include

int sum(std::initializer_list const& il) {

int sum = 0;
for (auto i : il) sum += i;
return sum;

}

int main(void) {

auto il = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
std::cout << sum(il) << std::endl;
return 0;

}


## 练习6.28
在`error_msg`函数的第二个版本中包含`ErrCode`类型的参数,其中循环内的`elem`是什么类型?

解:

`elem`是`const string &`类型。

## 练习6.29
在范围`for`循环中使用`initializer_list`对象时,应该将循环控制变量声明成引用类型吗?为什么?

解:

应该使用常量引用类型。`initializer_list`对象中的元素都是常量,我们无法修改`initializer_list`对象中的元素的值。

## 练习6.30
编译第200页的`str_subrange`函数,看看你的编译器是如何处理函数中的错误的。

解:

编译器信息:

g++ (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609


编译错误信息:

ch6.cpp:38:9: error: return-statement with no value, in function returning ‘bool’ [-fpermissive]


## 练习6.31
什么情况下返回的引用无效?什么情况下返回常量的引用无效?

解:

当返回的引用的对象是局部变量时,返回的引用无效;当我们希望返回的对象被修改时,返回常量的引用无效。

## 练习6.32
下面的函数合法吗?如果合法,说明其功能;如果不合法,修改其中的错误并解释原因。

cpp int &get(int *array, int index) { return array[index]; } int main() {

int ia[10];
for (int i = 0; i != 10; ++i)
    get(ia, i) = i;

}


解:

合法。`get`函数根据索引取得数组中的元素的引用。

## 练习6.33
编写一个递归函数,输出`vector`对象的内容。

解:

cpp #include #include using std::vector; using std::cout; using Iter = vector::const_iterator;

void print(Iter first, Iter last) {

if (first != last)
{
    cout << *first << " ";
    print(++first, last);
}

}

int main() {

vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
print(vec.cbegin(), vec.cend());
return 0;

}


## 练习6.34
如果`factorial`函数的停止条件如下所示,将发生什么?

cpp if (val != 0)


解:
如果`val`为正数,从结果上来说没有区别(多乘了个1); 
如果`val`为负数,那么递归永远不会结束。

## 练习6.35
在调用`factorial`函数时,为什么我们传入的值是`val-1`而非`val--`?

解:

如果传入的值是`val--`,那么将会永远传入相同的值来调用该函数,递归将永远不会结束。

## 练习6.36
编写一个函数声明,使其返回数组的引用并且该数组包含10个`string`对象。
不用使用尾置返回类型、`decltype`或者类型别名。

解:

cpp string (&fun())[10];


## 练习6.37
为上一题的函数再写三个声明,一个使用类型别名,另一个使用尾置返回类型,最后一个使用`decltype`关键字。
你觉得哪种形式最好?为什么?

解:

cpp typedef string str_arr[10]; str_arr& fun();

auto fun()->string(&)[10];

string s[10]; decltype(s)& fun();


我觉得尾置返回类型最好,就一行代码。

## 练习6.38
修改`arrPtr`函数,使其返回数组的引用。

解:

cpp decltype(odd)& arrPtr(int i) {

return (i % 2) ? odd : even;

}


## 练习6.39
说明在下面的每组声明中第二条语句是何含义。
如果有非法的声明,请指出来。

cpp (a) int calc(int, int);

int calc(const int, const int);

(b) int get();

double get();

(c) int reset(int );

double *reset(double *);
	
解:

- (a) 非法。因为顶层const不影响传入函数的对象,所以第二个声明无法与第一个声明区分开来。
- (b) 非法。对于重载的函数来说,它们应该只有形参的数量和形参的类型不同。返回值与重载无关。
- (c) 合法。

## 练习6.40
下面的哪个声明是错误的?为什么?

cpp (a) int ff(int a, int b = 0, int c = 0); (b) char *init(int ht = 24, int wd, char bckgrnd);


解:
	
(a) 正确。
(b) 错误。因为一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。

## 练习6.41
下面的哪个调用是非法的?为什么?哪个调用虽然合法但显然与程序员的初衷不符?为什么?

cpp char *init(int ht, int wd = 80, char bckgrnd = ' '); (a) init(); (b) init(24,10); (c) init(14,'*');


解:

- (a) 非法。第一个参数不是默认参数,最少需要一个实参。
- (b) 合法。
- (c) 合法,但与初衷不符。字符`*`被解释成`int`传入到了第二个参数。而初衷是要传给第三个参数。

## 练习6.42
给`make_plural`函数的第二个形参赋予默认实参's', 利用新版本的函数输出单词success和failure的单数和复数形式。

解:

cpp #include #include

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

string make_plural(size_t ctr, const string& word, const string& ending = "s") {

return (ctr > 1) ? word + ending : word;

}

int main() {

cout << "single: " << make_plural(1, "success", "es") << " "
	<< make_plural(1, "failure") << endl;
cout << "plural : " << make_plural(2, "success", "es") << " "
	<< make_plural(2, "failure") << endl;
return 0;

}


## 练习6.43
你会把下面的哪个声明和定义放在头文件中?哪个放在源文件中?为什么?

cpp (a) inline bool eq(const BigInt&, const BigInt&) {...} (b) void putValues(int *arr, int size);


解:

全部都放进头文件。(a) 是内联函数,(b) 是声明。

## 练习6.44
将6.2.2节的`isShorter`函数改写成内联函数。

解:

cpp inline bool is_shorter(const string &lft, const string &rht) {

return lft.size() < rht.size();

}


## 练习6.45
回顾在前面的练习中你编写的那些函数,它们应该是内联函数吗?
如果是,将它们改写成内联函数;如果不是,说明原因。

解:

一般来说,内联机制用于优化规模小、流程直接、频繁调用的函数。

## 练习6.46
能把`isShorter`函数定义成`constexpr`函数吗?
如果能,将它改写成`constxpre`函数;如果不能,说明原因。

解:

不能。`constexpr`函数的返回值类型及所有形参都得是字面值类型。

## 练习6.47
改写6.3.2节练习中使用递归输出`vector`内容的程序,使其有条件地输出与执行过程有关的信息。
例如,每次调用时输出`vector`对象的大小。
分别在打开和关闭调试器的情况下编译并执行这个程序。

解:

cpp #include #include using std::vector; using std::cout; using std::endl;

void printVec(vector &vec) { #ifndef NDEBUG

cout << "vector size: " << vec.size() << endl;

#endif

if (!vec.empty())
{
    auto tmp = vec.back();
    vec.pop_back();
    printVec(vec);
    cout << tmp << " ";
}

}

int main() {

vector<int> vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
printVec(vec);
cout << endl;
return 0;

}


## 练习6.48
说明下面这个循环的含义,它对assert的使用合理吗?

cpp string s; while (cin >> s && s != sought) { } //空函数体 assert(cin);


解:

不合理。从这个程序的意图来看,应该用

cpp assert(s == sought);


## 练习6.49
什么是候选函数?什么是可行函数?

解:

候选函数:与被调用函数同名,并且其声明在调用点可见。
可行函数:形参与实参的数量相等,并且每个实参类型与对应的形参类型相同或者能转换成形参的类型。

## 练习6.50
已知有第217页对函数`f`的声明,对于下面的每一个调用列出可行函数。
其中哪个函数是最佳匹配?
如果调用不合法,是因为没有可匹配的函数还是因为调用具有二义性?

cpp (a) f(2.56, 42) (b) f(42) (c) f(42, 0) (d) f(2.56, 3.14)


解:

- (a) `void f(int, int);`和`void f(double, double = 3.14);`是可行函数。
该调用具有二义性而不合法。
- (b) `void f(int);` 是可行函数。调用合法。
- (c) `void f(int, int);`和`void f(double, double = 3.14);`是可行函数。
`void f(int, int);`是最佳匹配。
- (d) `void f(int, int);`和`void f(double, double = 3.14);`是可行函数。
`void f(double, double = 3.14);`是最佳匹配。

## 练习6.51
编写函数`f`的4版本,令其各输出一条可以区分的消息。
验证上一个练习的答案,如果你的回答错了,反复研究本节内容直到你弄清自己错在何处。

解:

cpp #include using std::cout; using std::endl;

void f() {

cout << "f()" << endl;

}

void f(int) {

cout << "f(int)" << endl;

}

void f(int, int) {

cout << "f(int, int)" << endl;

}

void f(double, double) {

cout << "f(double, double)" << endl;

}

int main() {

//f(2.56, 42); // error: 'f' is ambiguous.
f(42);
f(42, 0);
f(2.56, 3.14);

return 0;

}


## 练习6.52
已知有如下声明:

cpp void manip(int ,int); double dobj;

请指出下列调用中每个类型转换的等级。

cpp (a) manip('a', 'z'); (b) manip(55.4, dobj);


解:

- (a) 第3级。类型提升实现的匹配。
- (b) 第4级。算术类型转换实现的匹配。

## 练习6.53
说明下列每组声明中的第二条语句会产生什么影响,并指出哪些不合法(如果有的话)。

cpp (a) int calc(int&, int&);

int calc(const int&, const int&); 

(b) int calc(char, char);

int calc(const char*, const char*);

(c) int calc(char, char);

int calc(char* const, char* const);
	
解:

(c) 不合法。顶层const不影响传入函数的对象。

## 练习6.54
编写函数的声明,令其接受两个`int`形参并返回类型也是`int`;然后声明一个`vector`对象,令其元素是指向该函数的指针。

解:

cpp int func(int, int); vector v;


## 练习6.55
编写4个函数,分别对两个`int`值执行加、减、乘、除运算;在上一题创建的`vector`对象中保存指向这些函数的指针。

解:

cpp int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int multiply(int a, int b) { return a * b; } int divide(int a, int b) { return b != 0 ? a / b : 0; }

v.push_back(add); v.push_back(subtract); v.push_back(multiply); v.push_back(divide);


## 练习6.56
调用上述`vector`对象中的每个元素并输出结果。

解:

cpp std::vector vec{ add, subtract, multiply, divide }; for (auto f : vec)

      std::cout << f(2, 2) << std::endl;

---

## ch07

# 第七章 类 (Class)

## 定义抽象数据类型

- **类背后的基本思想**:**数据抽象**(data abstraction)和**封装**(encapsulation)。
- 数据抽象是一种依赖于**接口**(interface)和**实现**(implementation)分离的编程技术。

### 类成员 (Member)

- 必须在类的内部声明,不能在其他地方增加成员。
- 成员可以是数据,函数,类型别名。

### 类的成员函数

- 成员函数的**声明**必须在类的内部。
- 成员函数的**定义**既可以在类的内部也可以在外部。
- 使用点运算符 `.` 调用成员函数。
- 必须对任何`const`或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。
- `ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }`
- 默认实参: `Sales_item(const std::string &book): isbn(book), units_sold(0), revenue(0.0) { }`
- `*this`:
  - 每个成员函数都有一个额外的,隐含的形参`this`。
  - `this`总是指向当前对象,因此`this`是一个常量指针。
  - 形参表后面的`const`,改变了隐含的`this`形参的类型,如 `bool same_isbn(const Sales_item &rhs) const`,这种函数称为“常量成员函数”(`this`指向的当前对象是常量)。
  - `return *this;`可以让成员函数连续调用。
  - 普通的非`const`成员函数:`this`是指向类类型的`const`指针(可以改变`this`所指向的值,不能改变`this`保存的地址)。
  - `const`成员函数:`this`是指向const类类型的`const`指针(既不能改变`this`所指向的值,也不能改变`this`保存的地址)。

### 非成员函数

- 和类相关的非成员函数,定义和声明都应该在类的外部。

### 类的构造函数

- 类通过一个或者几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做**构造函数**。
- 构造函数是特殊的成员函数。
- 构造函数放在类的`public`部分。
- 与类同名的成员函数。
- `Sales_item(): units_sold(0), revenue(0.0) { }`
- `=default`要求编译器合成默认的构造函数。(`C++11`)
- 初始化列表:冒号和花括号之间的代码: `Sales_item(): units_sold(0), revenue(0.0) { }`

## 访问控制与封装

- **访问说明符**(access specifiers):
  - `public`:定义在 `public`后面的成员在整个程序内可以被访问; `public`成员定义类的接口。
  - `private`:定义在 `private`后面的成员可以被类的成员函数访问,但不能被使用该类的代码访问; `private`隐藏了类的实现细节。
- 使用 `class`或者 `struct`:都可以被用于定义一个类。唯一的却别在于访问权限。
  - 使用 `class`:在第一个访问说明符之前的成员是 `priavte`的。
  - 使用 `struct`:在第一个访问说明符之前的成员是 `public`的。

### 友元

- 允许特定的**非成员函数**访问一个类的**私有成员**.
- 友元的声明以关键字 `friend`开始。 `friend Sales_data add(const Sales_data&, const Sales_data&);`表示非成员函数`add`可以访问类的非公有成员。
- 通常将友元声明成组地放在**类定义的开始或者结尾**。
- 类之间的友元:
  - 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

### 封装的益处

- 确保用户的代码不会无意间破坏封装对象的状态。
- 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。

## 类的其他特性

- 成员函数作为内联函数 `inline`:
  - 在类的内部,常有一些规模较小的函数适合于被声明成内联函数。
  - **定义**在类内部的函数是**自动内联**的。
  - 在类外部定义的成员函数,也可以在声明时显式地加上 `inline`。
- **可变数据成员** (mutable data member):
  - `mutable size_t access_ctr;`
  - 永远不会是`const`,即使它是`const`对象的成员。
- **类类型**:
  - 每个类定义了唯一的类型。

## 类的作用域

- 每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由引用、对象、指针使用成员访问运算符来访问。
- 函数的**返回类型**通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种**类型**,则类不能在之后重新定义该名字。
- 类中的**类型名定义**都要放在一开始。

## 构造函数再探

- 构造函数初始值列表:
  - 类似`python`使用赋值的方式有时候不行,比如`const`或者引用类型的数据,只能初始化,不能赋值。(注意初始化和赋值的区别)
  - 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
  - 如果一个构造函数为所有参数都提供了默认参数,那么它实际上也定义了默认的构造函数。

### 委托构造函数 (delegating constructor, `C++11`)

- 委托构造函数将自己的职责委托给了其他构造函数。
- `Sale_data(): Sale_data("", 0, 0) {}`

### 隐式的类型转换

- 如果构造函数**只接受一个实参**,则它实际上定义了转换为此类类型的**隐式转换机制**。这种构造函数又叫**转换构造函数**(converting constructor)。
- 编译器只会自动地执行`仅一步`类型转换。
- 抑制构造函数定义的隐式转换:
  - 将构造函数声明为`explicit`加以阻止。
  - `explicit`构造函数只能用于直接初始化,不能用于拷贝形式的初始化。

### 聚合类 (aggregate class)

- 满足以下所有条件:
  - 所有成员都是`public`的。
  - 没有定义任何构造函数。
  - 没有类内初始值。
  - 没有基类,也没有`virtual`函数。
- 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。

### 字面值常量类

- `constexpr`函数的参数和返回值必须是字面值。
- **字面值类型**:除了算术类型、引用和指针外,某些类也是字面值类型。
- 数据成员都是字面值类型的聚合类是字面值常量类。
- 如果不是聚合类,则必须满足下面所有条件:
  - 数据成员都必须是字面值类型。
  - 类必须至少含有一个`constexpr`构造函数。
  - 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的`constexpr`构造函数。
  - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

## 类的静态成员

- 非`static`数据成员存在于类类型的每个对象中。
- `static`数据成员独立于该类的任意对象而存在。
- 每个`static`数据成员是与类关联的对象,并不与该类的对象相关联。
- 声明:
  - 声明之前加上关键词`static`。
- 使用:
  - 使用**作用域运算符**`::`直接访问静态成员:`r = Account::rate();`
  - 也可以使用对象访问:`r = ac.rate();`
- 定义:
  - 在类外部定义时不用加`static`。
- 初始化:
  - 通常不在类的内部初始化,而是在定义时进行初始化,如 `double Account::interestRate = initRate();`
  - 如果一定要在类内部定义,则要求必须是字面值常量类型的`constexpr`。
  

---

## ch07

# 第七章 类

## 练习7.1

使用2.6.1节定义的`Sales_data`类为1.6节的交易处理程序编写一个新版本。

解:

cpp #include #include using std::cin; using std::cout; using std::endl; using std::string;

struct Sales_data {

string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

};

int main() {

Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue)
{
    Sales_data trans;
    while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) 
    {
        if (total.bookNo == trans.bookNo) 
        {
            total.units_sold += trans.units_sold;
            total.revenue += trans.revenue;
        }
        else
        {
            cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
            total = trans;
        }
    }
    cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else
{
    std::cerr << "No data?!" << std::endl;
    return -1;
}
return 0;

}


## 练习7.2

曾在2.6.2节的练习中编写了一个`Sales_data`类,请向这个类添加`combine`函数和`isbn`成员。

解:

cpp #include

struct Sales_data {

std::string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);

std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

};

Sales_data& Sales_data::combine(const Sales_data& rhs) {

units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;

}


## 练习7.3

修改7.1.1节的交易处理程序,令其使用这些成员。

解:

cpp #include using std::cin; using std::cout; using std::endl;

int main() {

Sales_data total;
if (cin >> total.bookNo >> total.units_sold >> total.revenue)
{
    Sales_data trans;
    while (cin >> trans.bookNo >> trans.units_sold >> trans.revenue) {
        if (total.isbn() == trans.isbn())
            total.combine(trans);
        else {
            cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
            total = trans;
        }
    }
    cout << total.bookNo << " " << total.units_sold << " " << total.revenue << endl;
}
else
{
    std::cerr << "No data?!" << std::endl;
    return -1;
}
return 0;

}


## 练习7.4

编写一个名为`Person`的类,使其表示人员的姓名和地址。使用`string`对象存放这些元素,接下来的练习将不断充实这个类的其他特征。

解:

cpp #include

class Person {

std::string name;
std::string address;

};


## 练习7.5

在你的`Person`类中提供一些操作使其能够返回姓名和地址。
这些函数是否应该是`const`的呢?解释原因。

解:

cpp #include

class Person {

std::string name;
std::string address;

public:

auto get_name() const -> std::string const& { return name; }
auto get_addr() const -> std::string const& { return address; }

};

应该是`const`的。因为常量的`Person`对象也需要使用这些函数操作。

## 练习7.6

对于函数`add`、`read`和`print`,定义你自己的版本。

解:

cpp #include #include

struct Sales_data {

std::string const& isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

};

// member functions. Sales_data& Sales_data::combine(const Sales_data& rhs) {

units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;

}

// nonmember functions std::istream &read(std::istream &is, Sales_data &item) {

double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;

}

std::ostream &print(std::ostream &os, const Sales_data &item) {

os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;

}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {

Sales_data sum = lhs;
sum.combine(rhs);
return sum;

}


## 练习7.7

使用这些新函数重写7.1.2节练习中的程序。

cpp int main() {

Sales_data total;
if (read(std::cin, total))
{
    Sales_data trans;
    while (read(std::cin, trans)) {
        if (total.isbn() == trans.isbn())
            total.combine(trans);
        else {
            print(std::cout, total) << std::endl;
            total = trans;
        }
    }
    print(std::cout, total) << std::endl;
}
else
{
    std::cerr << "No data?!" << std::endl;
    return -1;
}

return 0;

}


## 练习7.8

为什么`read`函数将其`Sales_data`参数定义成普通的引用,而`print`函数将其参数定义成常量引用?

解:

因为`read`函数会改变对象的内容,而`print`函数不会。

## 练习7.9

对于7.1.2节练习中代码,添加读取和打印`Person`对象的操作。

解:

cpp #include #include

struct Person {

std::string const& getName()    const { return name; }
std::string const& getAddress() const { return address; }

std::string name;
std::string address;

};

std::istream &read(std::istream &is, Person &person) {

return is >> person.name >> person.address;

}

std::ostream &print(std::ostream &os, const Person &person) {

return os << person.name << " " << person.address;

}


## 练习7.10

在下面这条`if`语句中,条件部分的作用是什么?

cpp if (read(read(cin, data1), data2)) //等价read(std::cin, data1);read(std::cin, data2);


解:

`read`函数的返回值是`istream`对象,
`if`语句中条件部分的作用是从输入流中读取数据给两个`data`对象。

## 练习7.11 : 

在你的`Sales_data`类中添加构造函数,
然后编写一段程序令其用到每个构造函数。

解:

头文件:

cpp #include #include

struct Sales_data {

Sales_data() = default;
Sales_data(const std::string &s):bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
Sales_data(std::istream &is);

std::string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);

std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

};

// nonmember functions std::istream &read(std::istream &is, Sales_data &item) {

double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;

}

std::ostream &print(std::ostream &os, const Sales_data &item) {

os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;

}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {

Sales_data sum = lhs;
sum.combine(rhs);
return sum;

}

// member functions. Sales_data::Sales_data(std::istream &is) {

read(is, *this);

}

Sales_data& Sales_data::combine(const Sales_data& rhs) {

units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;

}


主函数:

cpp int main() {

Sales_data item1;
print(std::cout, item1) << std::endl;

Sales_data item2("0-201-78345-X");
print(std::cout, item2) << std::endl;

Sales_data item3("0-201-78345-X", 3, 20.00);
print(std::cout, item3) << std::endl;

Sales_data item4(std::cin);
print(std::cout, item4) << std::endl;

return 0;

}


## 练习7.12

把只接受一个`istream`作为参数的构造函数移到类的内部。

解:

cpp #include #include

struct Sales_data; std::istream &read(std::istream&, Sales_data&);

struct Sales_data {

Sales_data() = default;
Sales_data(const std::string &s):bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
Sales_data(std::istream &is) { read(is, *this); }

std::string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);

std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

};

// member functions. Sales_data& Sales_data::combine(const Sales_data& rhs) {

units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;

}

// nonmember functions std::istream &read(std::istream &is, Sales_data &item) {

double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;

}

std::ostream &print(std::ostream &os, const Sales_data &item) {

os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;

}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {

Sales_data sum = lhs;
sum.combine(rhs);
return sum;

}


## 练习7.13
使用`istream`构造函数重写第229页的程序。

解:

cpp int main() {

Sales_data total(std::cin);
if (!total.isbn().empty())
{
    std::istream &is = std::cin;
    while (is) {
        Sales_data trans(is);
        if (!is) break;
        if (total.isbn() == trans.isbn())
            total.combine(trans);
        else {
            print(std::cout, total) << std::endl;
            total = trans;
        }
    }
    print(std::cout, total) << std::endl;
}
else
{
    std::cerr << "No data?!" << std::endl;
    return -1;
}

return 0;

}


## 练习7.14
编写一个构造函数,令其用我们提供的类内初始值显式地初始化成员。

cpp Sales_data() : units_sold(0) , revenue(0) { }


## 练习7.15
为你的`Person`类添加正确的构造函数。

解:

cpp #include #include

struct Person; std::istream &read(std::istream&, Person&);

struct Person {

Person() = default;
Person(const std::string& sname, const std::string& saddr) :name(sname), address(saddr) {}
Person(std::istream &is) { read(is, *this); }
std::string getName() const { return name; }
std::string getAddress() const { return address; }
std::string name;
std::string address;

};

std::istream &read(std::istream &is, Person &person) {

is >> person.name >> person.address;
return is;

}

std::ostream &print(std::ostream &os, const Person &person) {

os << person.name << " " << person.address;
return os;

}


## 练习7.16
在类的定义中对于访问说明符出现的位置和次数有限定吗?
如果有,是什么?什么样的成员应该定义在`public`说明符之后?
什么样的成员应该定义在`private`说明符之后?

解:

在类的定义中对于访问说明符出现的位置和次数**没有限定**。

每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者达到类的结尾处为止。

如果某个成员能够在整个程序内都被访问,那么它应该定义为`public`; 
如果某个成员只能在类内部访问,那么它应该定义为`private`。

## 练习7.17
使用`class`和`struct`时有区别吗?如果有,是什么?

解:

`class`和`struct`的唯一区别是默认的访问级别不同。

## 练习7.18
封装是何含义?它有什么用处?

解:

将类内部分成员设置为外部不可见,而提供部分接口给外面,这样的行为叫做封装。

用处:

- 1.确保用户的代码不会无意间破坏封装对象的状态。
- 2.被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。

## 练习7.19
在你的`Person`类中,你将把哪些成员声明成`public`的?
哪些声明成`private`的?
解释你这样做的原因。

构造函数、`getName()`、`getAddress()`函数将设为`public`。
`name`和 `address` 将设为`private`。
函数是暴露给外部的接口,因此要设为`public`;
而数据则应该隐藏让外部不可见。

## 练习7.20
友元在什么时候有用?请分别举出使用友元的利弊。

解:

当其他类或者函数想要访问当前类的私有变量时,这个时候应该用友元。

利:

与当前类有关的接口函数能直接访问类的私有变量。

弊:

牺牲了封装性与可维护性。

## 练习7.21
修改你的`Sales_data`类使其隐藏实现的细节。
你之前编写的关于`Sales_data`操作的程序应该继续使用,借助类的新定义重新编译该程序,确保其正常工作。

解:

cpp #include #include

class Sales_data {

friend std::istream &read(std::istream &is, Sales_data &item);
friend std::ostream &print(std::ostream &os, const Sales_data &item);
friend Sales_data add(const Sales_data &lhs, const Sales_data &rhs);

public:

Sales_data() = default;
Sales_data(const std::string &s):bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n), revenue(n*p){ }
Sales_data(std::istream &is) { read(is, *this); }
std::string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);

private:

std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;

};

// member functions. Sales_data& Sales_data::combine(const Sales_data& rhs) {

units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;

}

// friend functions std::istream &read(std::istream &is, Sales_data &item) {

double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;

}

std::ostream &print(std::ostream &os, const Sales_data &item) {

os << item.isbn() << " " << item.units_sold << " " << item.revenue;
return os;

}

Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {

Sales_data sum = lhs;
sum.combine(rhs);
return sum;

}


## 练习7.22
修改你的`Person`类使其隐藏实现的细节。

解:

cpp #include #include

class Person {

friend std::istream &read(std::istream &is, Person &person);
friend std::ostream &print(std::ostream &os, const Person &person);

public:

Person() = default;
Person(const std::string sname, const std::string saddr):name(sname), address(saddr){ }
Person(std::istream &is){ read(is, *this); }
std::string getName() const { return name; }
std::string getAddress() const { return address; }

private:

std::string name;
std::string address;

};

std::istream &read(std::istream &is, Person &person) {

is >> person.name >> person.address;
return is;

}

std::ostream &print(std::ostream &os, const Person &person) {

os << person.name << " " << person.address;
return os;

}


## 练习7.23
编写你自己的`Screen`类型。

解:

cpp #include

class Screen {

public:
    using pos = std::string::size_type;
    Screen() = default;
    Screen(pos ht, pos wd, char c):height(ht), width(wd), contents(ht*wd, c){ }
    char get() const { return contents[cursor]; }
    char get(pos r, pos c) const { return contents[r*width+c]; }
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;

};


# 练习7.24
给你的`Screen`类添加三个构造函数:一个默认构造函数;另一个构造函数接受宽和高的值,然后将`contents`初始化成给定数量的空白;第三个构造函数接受宽和高的值以及一个字符,该字符作为初始化后屏幕的内容。

解:

cpp #include

class Screen {

public:
    using pos = std::string::size_type;
    Screen() = default; // 1
    Screen(pos ht, pos wd):height(ht), width(wd), contents(ht*wd, ' '){ } // 2
    Screen(pos ht, pos wd, char c):height(ht), width(wd), contents(ht*wd, c){ } // 3
    char get() const { return contents[cursor]; }
    char get(pos r, pos c) const { return contents[r*width+c]; }
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;

};


## 练习7.25
`Screen`能安全地依赖于拷贝和赋值操作的默认版本吗?
如果能,为什么?如果不能?为什么?

解:

能。 `Screen`的成员只有内置类型和`string`,因此能安全地依赖于拷贝和赋值操作的默认版本。

管理动态内存的类则不能依赖于拷贝和赋值操作的默认版本,而且也应该尽量使用`string`和`vector`来避免动态管理内存的复杂性。

## 练习7.26
将`Sales_data::avg_price`定义成内联函数。

解:

在头文件中加入:

cpp inline double Sales_data::avg_price() const {

return units_sold ? revenue/units_sold : 0;

}


## 练习7.27 
给你自己的`Screen`类添加`move`、`set` 和`display`函数,通过执行下面的代码检验你的类是否正确。

cpp Screen myScreen(5, 5, 'X'); myScreen.move(4, 0).set('#').display(cout); cout << "\n"; myScreen.display(cout); cout << "\n";


解:

增加代码:

cpp #include #include

class Screen { public:

... ...
inline Screen& move(pos r, pos c);
inline Screen& set(char c);
inline Screen& set(pos r, pos c, char ch);
const Screen& display(std::ostream &os) const { do_display(os); return *this; }
Screen& display(std::ostream &os) { do_display(os); return *this; }

private:

void do_display(std::ostream &os) const { os << contents; }
... ...

};

inline Screen& Screen::move(pos r, pos c) {

cursor = r*width + c;
return *this;

}

inline Screen& Screen::set(char c) {

contents[cursor] = c;
return *this;

}

inline Screen& Screen::set(pos r, pos c, char ch) {

contents[r*width+c] = ch;
return *this;

}


测试代码:

cpp int main() {

Screen myScreen(5, 5, 'X');
myScreen.move(4, 0).set('#').display(std::cout);
std::cout << "\n";
myScreen.display(std::cout);
std::cout << "\n";
return 0;

}


## 练习7.28
如果`move`、`set`和`display`函数的返回类型不是`Screen&` 而是`Screen`,则在上一个练习中将会发生什么?

解:

如果返回类型是`Screen`,那么`move`返回的是`*this`的一个副本,因此`set`函数只能改变临时副本而不能改变`myScreen`的值。

## 练习7.29
修改你的`Screen`类,令`move`、`set`和`display`函数返回`Screen`并检查程序的运行结果,在上一个练习中你的推测正确吗?

解:

推测正确。

#with '&' XXXXXXXXXXXXXXXXXXXX#XXXX XXXXXXXXXXXXXXXXXXXX#XXXX

                ^

without '&'

XXXXXXXXXXXXXXXXXXXX#XXXX XXXXXXXXXXXXXXXXXXXXXXXXX

                ^

## 练习7.30
通过`this`指针使用成员的做法虽然合法,但是有点多余。讨论显示使用指针访问成员的优缺点。

解:

优点:

程序的意图更明确

函数的参数可以与成员同名,如

cpp void setAddr(const std::string &addr) { this->addr = addr; }


缺点:

有时候显得有点多余,如

cpp std::string getAddr() const { return this->addr; }


## 练习7.31
定义一对类`X`和`Y`,其中`X`包含一个指向`Y`的指针,而`Y`包含一个类型为`X`的对象。

解:

cpp class Y;

class X{

Y* y = nullptr;	

};

class Y{

X x;

};


## 练习7.32
定义你自己的`Screen`和`Window_mgr`,其中`clear`是`Window_mgr`的成员,是`Screen`的友元。

解:

cpp #include #include #include

class Screen;

class Window_mgr { public:

using ScreenIndex = std::vector<Screen>::size_type;
inline void clear(ScreenIndex);

private:

std::vector<Screen> screens;

};

class Screen {

friend void Window_mgr::clear(ScreenIndex);

public:

using pos = std::string::size_type;
Screen() = default;
Screen(pos ht, pos wd) :height(ht), width(wd), contents(ht*wd,' ') {}
Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht*wd, c) {}
char get() const { return contents[cursor]; }
char get(pos r, pos c) const { return contents[r*width + c]; }
inline Screen& move(pos r, pos c);
inline Screen& set(char c);
inline Screen& set(pos r, pos c, char ch);
const Screen& display(std::ostream& os) const { do_display(os); return *this; }
Screen& display(std::ostream& os) { do_display(os); return *this; }

private:

void do_display(std::ostream &os) const { os << contents; }

private:

pos cursor = 0;
pos width = 0, height = 0;
std::string contents;

};

inline void Window_mgr::clear(ScreenIndex i) {

Screen& s = screens[i];
s.contents = std::string(s.height*s.width,' ');

}

inline Screen& Screen::move(pos r, pos c) {

cursor = r*width + c;
return *this;

}

inline Screen& Screen::set(char c) {

contents[cursor] = c;
return *this;

}

inline Screen& Screen::set(pos r, pos c, char ch) {

contents[r*width + c] = ch;
return *this;

}


## 练习7.33
如果我们给`Screen`添加一个如下所示的`size`成员将发生什么情况?如果出现了问题,请尝试修改它。

cpp pos Screen::size() const {

return height * width;

}


解:

纠正:错误为 error: extra qualification 'Screen::' on member 'size' [-fpermissive]
则应该去掉Screen::,改为

cpp pos size() const{

        return height * width;
    }
##  练习7.34
如果我们把第256页`Screen`类的`pos`的`typedef`放在类的最后一行会发生什么情况?

解:

在 dummy_fcn(pos height) 函数中会出现 未定义的标识符pos。

类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

## 练习7.35
解释下面代码的含义,说明其中的`Type`和`initVal`分别使用了哪个定义。如果代码存在错误,尝试修改它。

cpp typedef string Type; Type initVal(); class Exercise { public:

typedef double Type;
Type setVal(Type);
Type initVal(); 

private:

int val;

}; Type Exercise::setVal(Type parm) {

val = parm + initVal();     
return val;

}

解:

书上255页中说:

然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。


因此重复定义`Type`是错误的行为。

虽然重复定义类型名字是错误的行为,但是编译器并不为此负责。所以我们要人为地遵守一些原则,在这里有一些讨论。

## 练习7.36
下面的初始值是错误的,请找出问题所在并尝试修改它。

cpp struct X {

X (int i, int j): base(i), rem(base % j) {}
int rem, base;

};


解:

应该改为:

cpp struct X {

X (int i, int j): base(i), rem(base % j) {}
int base, rem;

};


## 练习7.37
使用本节提供的`Sales_data`类,确定初始化下面的变量时分别使用了哪个构造函数,然后罗列出每个对象所有的数据成员的值。

解:

cpp Sales_data first_item(cin); // 使用 Sales_data(std::istream &is) ; 各成员值从输入流中读取 int main() {

// 使用默认构造函数  bookNo = "", cnt = 0, revenue = 0.0
Sales_data next;
// 使用 Sales_data(std::string s = "");   bookNo = "9-999-99999-9", cnt = 0, revenue = 0.0
Sales_data last("9-999-99999-9"); 

}


## 练习7.38
有些情况下我们希望提供`cin`作为接受`istream&`参数的构造函数的默认实参,请声明这样的构造函数。

解:

cpp Sales_data(std::istream &is = std::cin) { read(is, *this); }


## 练习7.39
如果接受`string`的构造函数和接受`istream&`的构造函数都使用默认实参,这种行为合法吗?如果不,为什么?

解:

不合法。当你调用`Sales_data()`构造函数时,无法区分是哪个重载。

## 练习7.40
从下面的抽象概念中选择一个(或者你自己指定一个),思考这样的类需要哪些数据成员,提供一组合理的构造函数并阐明这样做的原因。

(a) Book (b) Data (c) Employee (d) Vehicle (e) Object (f) Tree


解:

(a) Book.

cpp class Book { public:

Book(unsigned isbn, std::string const& name, std::string const& author, std::string const& pubdate)
    :isbn_(isbn), name_(name), author_(author), pubdate_(pubdate)
{ }
explicit Book(std::istream &in) 
{ 
    in >> isbn_ >> name_ >> author_ >> pubdate_;
}

private:

unsigned isbn_;
std::string name_;
std::string author_;
std::string pubdate_;

};


## 练习7.41
使用委托构造函数重新编写你的`Sales_data`类,给每个构造函数体添加一条语句,令其一旦执行就打印一条信息。用各种可能的方式分别创建`Sales_data`对象,认真研究每次输出的信息直到你确实理解了委托构造函数的执行顺序。

解:

- [头文件](https://github.com/applenob/Cpp_Primer_Practice/tree/master/cpp_source/ch07/ex_7_41.h)
- [源文件](https://github.com/applenob/Cpp_Primer_Practice/tree/master/cpp_source/ch07/ex_7_41.cpp)
- [主函数](https://github.com/applenob/Cpp_Primer_Practice/tree/master/cpp_source/ch07/ex_7_41_main.cpp)

总结:使用委托构造函数,调用顺序是:
- 1.实际的构造函数的函数体。
- 2.委托构造函数的函数体。

## 练习7.42
对于你在练习7.40中编写的类,确定哪些构造函数可以使用委托。如果可以的话,编写委托构造函数。如果不可以,从抽象概念列表中重新选择一个你认为可以使用委托构造函数的,为挑选出的这个概念编写类定义。

解:

cpp class Book { public:

Book(unsigned isbn, std::string const& name, std::string const& author, std::string const& pubdate)
    :isbn_(isbn), name_(name), author_(author), pubdate_(pubdate)
{ }
Book(unsigned isbn) : Book(isbn, "", "", "") {}
explicit Book(std::istream &in) 
{ 
    in >> isbn_ >> name_ >> author_ >> pubdate_;
}

private:

unsigned isbn_;
std::string name_;
std::string author_;
std::string pubdate_;

};


## 练习7.43
假定有一个名为`NoDefault`的类,它有一个接受`int`的构造函数,但是没有默认构造函数。定义类`C`,`C`有一个 `NoDefault`类型的成员,定义`C`的默认构造函数。

cpp class NoDefault { public:

NoDefault(int i) { }

};

class C { public:

C() : def(0) { } 

private:

NoDefault def;

};


## 练习7.44
下面这条声明合法吗?如果不,为什么?

cpp vector vec(10);//vec初始化有10个元素


解:

不合法。因为`NoDefault`没有默认构造函数。

## 练习7.45
如果在上一个练习中定义的vector的元素类型是C,则声明合法吗?为什么?

合法。因为`C`有默认构造函数。

## 练习7.46
下面哪些论断是不正确的?为什么?

- (a) 一个类必须至少提供一个构造函数。
- (b) 默认构造函数是参数列表为空的构造函数。
- (c) 如果对于类来说不存在有意义的默认值,则类不应该提供默认构造函数。
- (d) 如果类没有定义默认构造函数,则编译器将为其生成一个并把每个数据成员初始化成相应类型的默认值。

解:

- (a) 不正确。如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,并称之为合成的默认构造函数。
- (b) 不完全正确。为每个参数都提供了默认值的构造函数也是默认构造函数。
- (c) 不正确。哪怕没有意义的值也需要初始化。
- (d) 不正确。只有当一个类没有定义**任何构造函数**的时候,编译器才会生成一个默认构造函数。

## 练习7.47
说明接受一个`string`参数的`Sales_data`构造函数是否应该是`explicit`的,并解释这样做的优缺点。

解:

是否需要从`string`到`Sales_data`的转换依赖于我们对用户使用该转换的看法。在此例中,这种转换可能是对的。`null_book`中的`string`可能表示了一个不存在的`ISBN`编号。

优点:

可以抑制构造函数定义的隐式转换

缺点:

为了转换要显式地使用构造函数

## 练习7.48
假定`Sales_data`的构造函数不是`explicit`的,则下述定义将执行什么样的操作?

解:

cpp string null_isbn("9-999-9999-9"); Sales_data item1(null_isbn); Sales_data item2("9-999-99999-9");

这些定义和是不是`explicit`的无关。

## 练习7.49
对于`combine`函数的三种不同声明,当我们调用`i.combine(s)`时分别发生什么情况?其中`i`是一个`Sales_data`,而` s`是一个`string`对象。

解:

cpp (a) Sales_data &combine(Sales_data); // ok (b) Sales_data &combine(Sales_data&); // error C2664: 无法将参数 1 从“std::string”转换为“Sales_data &” 因为隐式转换只有一次 (c) Sales_data &combine(const Sales_data&) const; // 该成员函数是const 的,意味着不能改变对象。而 combine函数的本意就是要改变对象


## 练习7.50
确定在你的`Person`类中是否有一些构造函数应该是`explicit` 的。

解:

cpp explicit Person(std::istream &is){ read(is, *this); }


## 练习7.51
`vector`将其单参数的构造函数定义成`explicit`的,而`string`则不是,你觉得原因何在?

假如我们有一个这样的函数:

cpp int getSize(const std::vector&);

如果`vector`没有将单参数构造函数定义成`explicit`的,我们就可以这样调用:

cpp getSize(34);

很明显这样调用会让人困惑,函数实际上会初始化一个拥有34个元素的`vecto`r的临时量,然后返回34。但是这样没有任何意义。而`string`则不同,`string`的单参数构造函数的参数是`const char *`,因此凡是在需要用到`string`的地方都可以用` const char *`来代替(字面值就是`const char *`)。如:

cpp void print(std::string); print("hello world");


## 练习7.52
使用2.6.1节的 `Sales_data` 类,解释下面的初始化过程。如果存在问题,尝试修改它。

cpp Sales_data item = {"987-0590353403", 25, 15.99};


解:

`Sales_data` 类不是聚合类,应该修改成如下:

cpp struct Sales_data {

std::string bookNo;
unsigned units_sold;
double revenue;

};


## 练习7.53
定义你自己的`Debug`。

解:

cpp class Debug { public:

constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw(r), io(i), other(0) { }
constexpr bool any() { return hw || io || other; }
void set_hw(bool b) { hw = b; }
void set_io(bool b) { io = b; }
void set_other(bool b) { other = b; }

private:

bool hw;        // runtime error
bool io;        // I/O error
bool other;     // the others

};


## 练习7.54
`Debug`中以 `set_` 开头的成员应该被声明成`constexpr` 吗?如果不,为什么?

解:

不能。`constexpr`函数必须包含一个返回语句。

## 练习7.55
7.5.5节的`Data`类是字面值常量类吗?请解释原因。

解:

不是。因为`std::string`不是字面值类型。

## 练习7.56
什么是类的静态成员?它有何优点?静态成员与普通成员有何区别?

解:

与类本身相关,而不是与类的各个对象相关的成员是静态成员。静态成员能用于某些场景,而普通成员不能。

## 练习7.57
编写你自己的`Account`类。

解:

cpp class Account { public:

void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double newRate) { interestRate = newRate; }

private:

std::string owner;
double amount;
static double interestRate;
static constexpr double todayRate = 42.42;
static double initRate() { return todayRate; }

};

double Account::interestRate = initRate();


## 练习7.58
下面的静态数据成员的声明和定义有错误吗?请解释原因。

cpp //example.h class Example { public:

static double rate = 6.5;
static const int vecSize = 20;
static vector<double> vec(vecSize);

};

//example.c #include "example.h" double Example::rate; vector Example::vec;


解:

`rate`应该是一个**常量表达式**。而类内只能初始化整型类型的静态常量,所以不能在类内初始化`vec`。修改后如下:

cpp // example.h class Example { public:

static constexpr double rate = 6.5;
static const int vecSize = 20;
static vector<double> vec;

};

// example.C #include "example.h" constexpr double Example::rate; vector Example::vec(Example::vecSize);


---

## ch08

# 第八章 IO库

## 前面章节已经在用的IO库设施

- **istream**:输入流类型,提供输入操作。
- **ostream**:输出流类型,提供输出操作
- **cin**:一个`istream`对象,从标准输入读取数据。
- **cout**:一个`ostream`对象,向标准输出写入数据。
- **cerr**:一个`ostream`对象,向标准错误写入消息。
- **>>运算符**:用来从一个`istream`对象中读取输入数据。
- **<<运算符**:用来向一个`ostream`对象中写入输出数据。
- **getline函数**:从一个给定的`istream`对象中读取一行数据,存入到一个给定的`string`对象中。
  
## IO类

### 标准库定义的IO类型

- `iostream`头文件:从标准流中读写数据,`istream`、`ostream`等。
- `fstream`头文件:从文件中读写数据,`ifstream`、`ofstream`等。
- `sstream`头文件:从字符串中读写数据,`istringstream`、`ostringstream`

### IO对象不可复制或赋值

- 1.IO对象不能存在容器里.
- 2.形参和返回类型也不能是流类型。
- 3.形参和返回类型一般是流的**引用**。
- 4.读写一个IO对象会改变其状态,因此传递和返回的引用不能是`const`的。

### 条件状态

| 状态      | 解释 |
| ----------- | ----------- |
| `strm:iostate` | 是一种机器无关的**类型**,提供了表达条件状态的完整功能 |
| `strm:badbit` | 用来指出流已经崩溃 |
| `strm:failbit` | 用来指出一个IO操作失败了 |
| `strm:eofbit` | 用来指出流到达了文件结束 |
| `strm:goodbit` | 用来指出流未处于错误状态,此值保证为零 |
| `s.eof()` | 若流`s`的`eofbit`置位,则返回`true` |
| `s.fail()` | 若流`s`的`failbit`置位,则返回`true` |
| `s.bad()` | 若流`s`的`badbit`置位,则返回`true` |
| `s.good()` | 若流`s`处于有效状态,则返回`true` |
| `s.clear()` | 将流`s`中所有条件状态位复位,将流的状态设置成有效,返回`void` |
| `s.clear(flags)` | 将流`s`中指定的条件状态位复位,返回`void` |
| `s.setstate(flags)` | 根据给定的标志位,将流`s`中对应的条件状态位置位,返回`void` |
| `s.rdstate()` | 返回流`s`的当前条件状态,返回值类型为`strm::iostate` |

上表中,`strm`是一种IO类型,(如`istream`), `s`是一个流对象。

### 管理输出缓冲

- 每个输出流都管理一个缓冲区,执行输出的代码,文本串可能立即打印出来,也可能被操作系统保存在缓冲区内,随后再打印。
- 刷新缓冲区,可以使用如下IO操纵符:
  - `endl`:输出一个换行符并刷新缓冲区。
  - `flush`:刷新流,单不添加任何字符。
  - `ends`:在缓冲区插入空字符`null`,然后刷新。
  - `unitbuf`:告诉流接下来每次操作之后都要进行一次`flush`操作。
  - `nounitbuf`:回到正常的缓冲方式。

## 文件输入输出

- 头文件`fstream`定义了三个类型来支持文件IO:
  - `ifstream`从一个给定文件读取数据。
  - `ofstream`向一个给定文件写入数据。
  - `fstream`可以读写给定文件。
- **文件流**:需要读写文件时,必须定义自己的文件流对象,并绑定在需要的文件上。

### fstream特有的操作

| 操作      | 解释 |
| ----------- | ----------- |
| `fstream fstrm;` | 创建一个未绑定的文件流。 |
| `fstream fstrm(s);` | 创建一个文件流,并打开名为`s`的文件,`s`可以是`string`也可以是`char`指针 |
| `fstream fstrm(s, mode);` | 与前一个构造函数类似,但按指定`mode`打开文件 |
| `fstrm.open(s)` | 打开名为`s`的文件,并和`fstrm`绑定 |
| `fstrm.close()` | 关闭和`fstrm`绑定的文件 |
| `fstrm.is_open()` | 返回一个`bool`值,指出与`fstrm`关联的文件是否成功打开且尚未关闭 |

上表中,`fstream`是头文件`fstream`中定义的一个类型,`fstrm`是一个文件流对象。

### 文件模式

| 文件模式 | 解释 |
| ----------- | ----------- |
|`in` | 以读的方式打开 |
| `out` | 以写的方式打开 |
| `app` | 每次写操作前均定位到文件末尾 |
| `ate` | 打开文件后立即定位到文件末尾 |
| `trunc` | 截断文件 |
| `binary` | 以二进制方式进行IO操作。 |

## string流

- 头文件`sstream`定义了三个类型来支持内存IO:
  - `istringstream`从`string`读取数据。
  - `ostringstream`向`string`写入数据。
  - `stringstream`可以读写给定`string`。

### stringstream特有的操作

| 操作 | 解释 |
| ----------- | ----------- |
|`sstream strm` | 定义一个未绑定的`stringstream`对象 |
| `sstream strm(s)` | 用`s`初始化对象 |
| `strm.str()` | 返回`strm`所保存的`string`的拷贝 |
| `strm.str(s)` | 将`s`拷贝到`strm`中,返回`void` |

上表中`sstream`是头文件`sstream`中任意一个类型。`s`是一个`string`。


---

## ch08

# 第八章 IO库

## 练习8.1
> 编写函数,接受一个`istream&`参数,返回值类型也是`istream&`。此函数须从给定流中读取数据,直至遇到文件结束标识时停止。它将读取的数据打印在标准输出上。完成这些操作后,在返回流之前,对流进行复位,使其处于有效状态。

解:

cpp std::istream& func(std::istream &is) {

std::string buf;
while (is >> buf)
    std::cout << buf << std::endl;
is.clear();
return is;

}


## 练习8.2
> 测试函数,调用参数为`cin`。

解:

cpp #include using std::istream;

istream& func(istream &is) {

std::string buf;
while (is >> buf)
    std::cout << buf << std::endl;
is.clear();
return is;

}

int main() {

istream& is = func(std::cin);
std::cout << is.rdstate() << std::endl;
return 0;

}


## 练习8.3
> 什么情况下,下面的`while`循环会终止?

cpp while (cin >> i) / ... /


解:

如`badbit`、`failbit`、`eofbit` 的任一个被置位,那么检测流状态的条件会失败。

## 练习8.4
> 编写函数,以读模式打开一个文件,将其内容读入到一个`string`的`vector`中,将每一行作为一个独立的元素存于`vector`中。

解:

cpp void ReadFileToVec(const string& fileName, vector& vec) {

ifstream ifs(fileName);
if (ifs)
{
    string buf;
    while (getline(ifs, buf))
        vec.push_back(buf);
}

}


## 练习8.5
> 重写上面的程序,将每个单词作为一个独立的元素进行存储。
解:

cpp void ReadFileToVec(const string& fileName, vector& vec) {

ifstream ifs(fileName);
if (ifs)
{
    string buf;
    while (ifs >> buf)
        vec.push_back(buf);
}

}


## 练习8.6
> 重写7.1.1节的书店程序,从一个文件中读取交易记录。将文件名作为一个参数传递给`main`。

解:

cpp #include #include

#include "../ch07/ex7_26.h" using std::ifstream; using std::cout; using std::endl; using std::cerr;

int main(int argc, char **argv) {

ifstream input(argv[1]);

Sales_data total;
if (read(input, total))
{
    Sales_data trans;
    while (read(input, trans))
    {
        if (total.isbn() == trans.isbn())
            total.combine(trans);
        else
        {
            print(cout, total) << endl;
            total = trans;
        }
    }
    print(cout, total) << endl;
}
else
{
    cerr << "No data?!" << endl;
}

return 0;

}


## 练习8.7
> 修改上一节的书店程序,将结果保存到一个文件中。将输出文件名作为第二个参数传递给`main`函数。

解:

cpp #include #include

#include "../ch07/ex7_26.h" using std::ifstream; using std::ofstream; using std::endl; using std::cerr;

int main(int argc, char **argv) {

ifstream input(argv[1]);
ofstream output(argv[2]);

Sales_data total;
if (read(input, total))
{
    Sales_data trans;
    while (read(input, trans))
    {
        if (total.isbn() == trans.isbn())
            total.combine(trans);
        else
        {
            print(output, total) << endl;
            total = trans;
        }
    }
    print(output, total) << endl;
}
else
{
    cerr << "No data?!" << endl;
}

return 0;

}


## 练习8.8
> 修改上一题的程序,将结果追加到给定的文件末尾。对同一个输出文件,运行程序至少两次,检验数据是否得以保留。

解:

cpp #include #include

#include "../ch07/ex7_26.h" using std::ifstream; using std::ofstream; using std::endl; using std::cerr;

int main(int argc, char **argv) {

ifstream input(argv[1]);
ofstream output(argv[2], ofstream::app);

Sales_data total;
if (read(input, total))
{
    Sales_data trans;
    while (read(input, trans))
    {
        if (total.isbn() == trans.isbn())
            total.combine(trans);
        else
        {
            print(output, total) << endl;
            total = trans;
        }
    }
    print(output, total) << endl;
}
else
{
    cerr << "No data?!" << endl;
}

return 0;

}



## 练习8.9
> 使用你为8.1.2节第一个练习所编写的函数打印一个`istringstream`对象的内容。

解:

cpp #include #include using std::istream;

istream& func(istream &is) {

std::string buf;
while (is >> buf)
    std::cout << buf << std::endl;
is.clear();
return is;

}

int main() {

std::istringstream iss("hello");
func(iss);
return 0;

}


## 练习8.10
> 编写程序,将来自一个文件中的行保存在一个`vector`中。然后使用一个`istringstream`从`vector`读取数据元素,每次读取一个单词。

解:

cpp #include #include #include #include #include

using std::vector; using std::string; using std::ifstream; using std::istringstream; using std::cout; using std::endl; using std::cerr;

int main() {

ifstream ifs("../data/book.txt");
if (!ifs)
{
    cerr << "No data?" << endl;
    return -1;
}

vector<string> vecLine;
string line;
while (getline(ifs, line))
    vecLine.push_back(line);

for (auto &s : vecLine)
{
    istringstream iss(s);
    string word;
    while (iss >> word)
        cout << word << endl;
}

return 0;

}


## 练习8.11
> 本节的程序在外层`while`循环中定义了`istringstream`对象。如果`record`对象定义在循环之外,你需要对程序进行怎样的修改?重写程序,将`record`的定义移到`while`循环之外,验证你设想的修改方法是否正确。

解:

cpp #include #include #include #include using std::vector; using std::string; using std::cin; using std::istringstream;

struct PersonInfo {

string name;
vector<string> phones;

};

int main() {

string line, word;
vector<PersonInfo> people;
istringstream record;
while (getline(cin, line))
{
    PersonInfo info;
    record.clear();
    record.str(line);
    record >> info.name;
    while (record >> word)
        info.phones.push_back(word);
    people.push_back(info);
}

for (auto &p : people)
{
    std::cout << p.name << " ";
    for (auto &s : p.phones)
        std::cout << s << " ";
    std::cout << std::endl;
}

return 0;

}


## 练习8.12
> 我们为什么没有在`PersonInfo`中使用类内初始化?

解:

因为这里只需要聚合类就够了,所以没有必要在`PersionInfo`中使用类内初始化。

## 练习8.13
> 重写本节的电话号码程序,从一个命名文件而非`cin`读取数据。

解:

cpp #include #include #include #include #include

using std::vector; using std::string; using std::cin; using std::istringstream; using std::ostringstream; using std::ifstream; using std::cerr; using std::cout; using std::endl; using std::isdigit;

struct PersonInfo {

string name;
vector<string> phones;

};

bool valid(const string& str) {

return isdigit(str[0]);

}

string format(const string& str) {

return str.substr(0,3) + "-" + str.substr(3,3) + "-" + str.substr(6);

}

int main() {

ifstream ifs("../data/phonenumbers.txt");
if (!ifs)
{
    cerr << "no phone numbers?" << endl;
    return -1;
}
string line, word;
vector<PersonInfo> people;
istringstream record;
while (getline(ifs, line))
{
    PersonInfo info;
    record.clear();
    record.str(line);
    record >> info.name;
    while (record >> word)
        info.phones.push_back(word);
    people.push_back(info);
}
for (const auto &entry : people)
{
    ostringstream formatted, badNums;
    for (const auto &nums : entry.phones)
        if (!valid(nums)) badNums << " " << nums;
        else formatted << " " << format(nums);
    if (badNums.str().empty())
        cout << entry.name << " " << formatted.str() << endl;
    else
        cerr << "input error: " << entry.name
             << " invalid number(s) " << badNums.str() << endl;
}
return 0;

} ```

练习8.14

我们为什么将entry和nums定义为const auto&?

解:

它们都是类类型,因此使用引用避免拷贝。 在循环当中不会改变它们的值,因此用const。

本文由 GitVP 从 GitHub 收录并在站内全文呈现,版权归原作者所有。

← 回到全部文章

同分类还有