C++Primer 通关

  • 工程代码链接,求小星星,谢谢 ⭐
  • 第一关:C++Primer 的了解
  • 第二关:基本内置类型与变量
    • 2.1 基本内置类型
    • 2.2 变量
    • 2.3 复合类型
      • 2.3.1 引用
      • 2.3.2 指针
    • 2.4 const 限定符
    • 2.5 处理类型
    • 2.6 自定义数据结构
  • 第三关:字符串、向量和数组
    • 3.1 命名空间的using声明
    • 3.2 标准库类型 string
    • 3.3 标准库类型 vector
      • 3.31列表初始化 vector 对象
      • 3.3.2 向 vector对象中添加元素
      • 3.3.3 其他 vector 操作
    • 3.4 迭代器介绍
      • 3.4.1使用迭代器
      • 3.4.2 迭代器运算
    • 3.5 数组
      • 3.5.1 定义与初始化内置数组
      • 3.5.2 访问数组元素
      • 3.5.3 指针与数组
      • 3.5.4 C风格字符串
      • 3.5.5 与旧代码的接口
    • 3.6 多维数组
  • 第四关:表达式
    • 4.1 基础
    • 4.3 逻辑与关系运算符
    • 4.4 赋值运算符
    • 4.5 递增和递减运算符
    • 4.6 成员访问运算符
    • 4.7 条件运算符
    • 4.8 位运算符
    • 4.9 sizeof 运算符
    • 4.10 逗号运算符
    • 4.11 类型转换
      • 4.11.1 算术转换
      • 4.11.2 其他隐式转换类型
      • 4.11.3 显示转换
    • 4.12 运算符优先级表
  • 第五关:语句
    • 5.1 简单语句
    • 5.2 语句作用域
    • 5.3 条件语句
    • 5.4 迭代语句
    • 5.5 跳转语句
    • 5.6 try 语句块和异常处理。
  • 第六关: 函数
    • 6.1 函数基础
      • 6.1.1 局部对象
      • 6.1.2 函数声明
      • 6.1.3 分离式编译
    • 6.2 参数传递
      • 6.2.1 形参与实参
      • 6.2.2 const 形参与实参
      • 6.2.3 指针或引用参数与 const
      • 6.2.4 数组形参
      • 6.2.5 main:处理命令行选项
      • 6.2.6 含有可变形参的函数
    • 6.3 返回类型和return 语句
      • 6.3.1 无返回值
      • 6.3.2 有返回值
      • 6.3.3 返回数组指针
    • 6.4 函数重载
      • 6.4.1 重载与作用域
    • 6.5 特殊用途语句特性
      • 6.5.1 默认实参
      • 6.5.2 内联函数和 constexpr 函数
      • 6.5.3 调试帮助
    • 6.6 函数匹配
      • 6.6.1 实参类型转换
    • 6.7 函数指针
  • 第七关:类
    • 7.1 定义抽象数据类型
      • 7.1.1 设计 SalesData 类
      • 7.1.2 定义改进的SalesData 类
      • 7.1.3 定义类相关的非成员函数
      • 7.1.4 构造函数
      • 7.1.5 拷贝,赋值,析构
    • 7.2 访问控制与封装
      • 7.2.1 友元
    • 7.3 类的其他特性
      • 7.3.2 返回 *this 的成员函数
      • 7.3.3 类类型
      • 7.3.4 友元再探
    • 7.4 类的作用域
      • 7.4.1 名字查找与类的作用域
    • 7.5 构造函数再探
      • 7.5.1 构造函数初始值列表
      • 7.5.2 委托构造函数
      • 7.5.3 默认构造函数的作用
      • 7.5.4 隐式的类类型转换
      • 7.5.5 聚合类
    • 7.5.6 字面值常量类
    • 7.6 类的静态成员
  • 第八关:IO库
    • 8.1 IO类
      • 8.1.1 IO对象无拷贝或赋值
      • 8.1.2 条件状态
      • 8.1.3 管理输出缓冲
    • 8.2 文件输入输出
      • 8.2.1 使用文件流对象
      • 8.2.2 文件模式
    • 8.3 string 流
      • 8.3.1 使用istringstream
      • 8.3.2 使用 ostringstream
  • 第九关:顺序容器
    • 9.1 顺序容器概述
    • 9.2 容器库概览
      • 9.2.1 迭代器
      • 9.2.2 容器类型成员
      • 9.2.3 begin 和 end成员
      • 9.2.4 容器定义和初始化
      • 9.2.5 赋值与 swap
      • 9.2.6 容器大小操作
      • 9.27 关系运算符
    • 9.3 顺序容器操作
      • 9.3.1 向顺序容器添加元素
      • 9.3.2 访问元素
      • 9.3.3 删除元素
      • 9.3.4 特殊的 forward_list 操作
      • 9.3.5 改变容器大小
      • 9.3.6 容器操作可能使迭代器失效
    • 9.4 vector对象是如何增长的
    • 9.5 额外的 string 操作
  • 第十关:泛型算法
    • 10.1 概述
    • 10.2 初识别泛型算法
      • 10.2.1 读元素的算法:
      • 10.2.1 写元素的算法:
      • 10.2.3 重排容器的算法
    • 10.3 定制操作
      • 10.3.1 向算法传递函数
      • 10.3.2 lambda 表达式
        • 10.3.3 lambda 捕获与返回
      • 10.3.4 参数绑定
    • 10.4 再探迭代器
      • 10.4.1 插入迭代器
      • 10.4.2 iostream 迭代器
      • 10.4.3 反向迭代器
    • 10.5 泛型算法结构(总结)
      • 10.5.1 迭代器类别
      • 10.5.2 算法形参模式
      • 10.5.3 算法命名规范
    • 10.6 特定容器算法
  • 第十一关:关联容器
    • 11.1 使用关联容器
    • 11.2 关联容器概述
      • 11.2.1 定义关联容器
      • 11.2.2 关键字类型的要求
      • 12.2.3 pair 类型
    • 11.3 关联容器操作
      • 11.3.1 关联容器迭代器
      • 11.3.2 添加元素
      • 11.3.3 删除元素
      • 11.3.4 map 的下标操作
      • 11.3.5 访问元素
      • 11.3.6 一个单词转换的 map
    • 11.4 无序容器
  • 第十二关: 动态内存
    • 12.1 动态内存与智能指针
      • 12.1.1 shared_ptr 类
      • 12.1.2 直接管理内存
        • 小心:动态内存的管理非常容易出错
      • 12.1.3 shared_ptr 与 new结合使用
      • 12.1.4 智能指针与异常
        • 智能指针的陷阱
      • 12.1.5 unique_ptr
      • 12.1.6 weak_ptr
    • 12.2 动态数组
      • 12.2.1 new 和 数组
      • 12.2.2 allocator 类
    • 12.3 使用标准库 :文本查询系统
  • 第十三关: 拷贝控制
  • 13.1 拷贝,赋值与销毁
      • 13.1.1 拷贝构造函数
      • 13.1.2 拷贝赋值运算符
      • 13.1.3 析构函数
      • 13.1.4 三/五法则
      • 13.1.5 使用 = default
      • 13.1.6 阻止拷贝
    • 13.2 拷贝控制和资源管理
      • 13.2.1 行为像值的类
      • 13.2.2 定义行为像指针的类
    • 13.3 交换操作
    • 13.4 拷贝控制示例 邮件处理应用
    • 13.5 动态内存管理类
    • 13.6 对象移动
      • 13.6.1 右值引用
      • 13.6.2 移动构造函数 和移动赋值运算符
        • 建议:(三五法则)
      • 13.6.3 右值引用 和成员函数
  • 第十四关: 重载运算与类型转换
    • 14.1 基本概念
      • 提升: 尽量明智地使用运算符重载
    • 14.2 输入输出运算符
      • 14.2.1 重载输出运算符 <<
      • 14.2.2 重载输入运算符 >>
    • 14.3 算术和关系运算符
      • 14.3.1 相等运算符
      • 14.3.2 关系运算符
    • 14.4 赋值运算符
    • 14.5 下标运算符
    • 14.6 递减与递增运算符
    • 14.7 成员访问运算符
    • 14.8 函数调用运算符
      • 14.8.1 lambda 是函数对象
      • 14.8.2 标准库定义的函数对象
      • 14.8.3 可调用对象与 function
    • 14.9 重载,类型转换与运算符
      • 14.9.1 类型转换运算符
        • 提示: 避免过度使用类型转换函数
      • 14.9.2 避免有二义性的类型转换
        • 提示: 类型转换与运算符
      • 14.9.3 函数匹配 与 重载运算符
  • 第十五关 . 面向对象程序设计
    • 15.1 OOP:概述
    • 15.2 定义基类和派生类
      • 15.2.1 基类定义
      • 15.2.2 定义派生类
        • 关键概念: 遵循基类的接口
      • 15.2.3 类型转换与继承
        • 关键概念:存在继承关系的类型之间的转换规则
    • 15.3 虚函数
      • 关键概念: C++ 的多态性
    • 15.4 抽象基类
      • 关键概念:重构
    • 15.5 访问控制与继承
      • 关键概念: 类的设计和受保护的成员
    • 15.6 继承中的 类作用域
      • 关键概念: 名字查找与继承
    • 15.7 构造函数与拷贝控制
      • 15.7.2 合成拷贝控制与继承
      • 15.7.3 派生类的拷贝控制成员
      • 15.7.4 继承的构造函数
    • 15.8 容器与继承
      • 15.8.1 编写 Basket 类
    • 15.9 文本查询程序再谈
  • 第 十六关: 模板与泛型编程
    • 16.1 定义模板
      • 16.1.1 函数模板
        • 关键概念: 模板与头文件
      • 16.1.2 类模板
      • 16.1.3 模板参数
      • 16.1.4 成员模板
      • 16.1.5 控制实例化
      • 16.1.6 效率与灵活性
    • 16.2 模板实参推断
      • 16.2.1 类型转换与模板类型参数
      • 16.2.2 函数模板显式实参
      • 16.2.3 尾置返回类型于类型转换
      • 16.2.4 函数指针和实参判断
      • 16.2.5 模板实参推断于引用
      • 16.2.6 理解 std::move
      • 16.2.7 转发
    • 16.3 重载与模板
    • 16.4 可变参数模板
      • 16.4.1 编写可变参数函数模板
      • 16.4.2 包扩展
    • 16.5 模板特例化
  • 第 十七 关: 标准库特殊设施
    • 17.1 tuple 类型
    • 17.2 bitset 类型
    • 17.3 正则表达式
      • 17.3.1 使用正则表达式库
      • 17.3.2 匹配与 Regex 迭代器类型
      • 17.3.3 使用子表达式
      • 17.3.4 使用 regex_replace
    • 17.4 随机数
      • 17.4.1 随机数引擎和分布
    • 17.5 IO 库再探
      • 17.5.1 格式化输入输出
      • 17.5.2 未格式化的输入/输出操作
  • 第十八关: 用于大型程序的工具
    • 18.1 异常处理
      • 18.1.1 抛出异常
      • 18.1.2 捕获异常
      • 18.1.3 函数 try 语句块与构造函数
      • 18.1.4 noexcept 异常说明
      • 18.1.5 异常类层次
    • 18.2 命名空间
    • 18.2.1 命名空间定义
      • 18.2.2 使用命名空间成员
        • 提示: 避免使用 using 指示
      • 18.2.3 类,命名空间与作用域
      • 18.2.4 重载与命名空间
    • 18.3 多重继承
      • 18.3.1 多重继承
      • 18.3.2 类型转换与多个基类
      • 18.3.3 多重继承下的类作用域
      • 18.3.4 虚继承
      • 18.3.5 构造函数与虚继承
  • 第十九关: 特殊工具与技术
    • 19.1 控制内存分配
      • 19.1.1 重载 new 和 delete
      • 19.1.2 定义 new 表达式
    • 19.2 运行时类型识别
      • 19.2.1 dynamic_cast 运算符
      • 19.2.2 typeid 运算符
      • 19.2.3 使用 RTTI
      • 19.2.4 type_info 类
    • 19.3 枚举类型
    • 19.4 类成员指针
    • 19.5 嵌套类
    • 19.6 union:一种节省空间的类
    • 19.7 局部类
    • 19.8 固有的不可移值的特性。
      • 19.8.1 位域
        • 19.8.2 volatile 限定符
      • 19.8.3 链接指示: extern “c”

工程代码链接,求小星星,谢谢 ⭐

GitHub 仓库,求小星星,谢谢 ⭐

第一关:C++Primer 的了解

C++Primer是基于 C++11标准进行编写的书籍,以 3 位作者 Standley B. Lippman,Josee Lajoie ,Barbara E.Moo在C++语言发展历程中的经历,这本书的权威性自不容置疑的:既有编译器的开发和实践,又参与 C++标准的制定,再加上丰富的 C++ 教学经历。该书是一本由浅入深的教程,同时考虑到该书的全面性,我们也可以当其为教材,以备随时查阅。

第二关:基本内置类型与变量

2.1 基本内置类型

如何选择内置类型:

  • 明确数值不可能为负时,选用无符号类型。
  • 使用int (16位)进行整数运算,超出就使用 long long int(64位)
  • 在算数表达式中不要使用 char 或者 bool
  • 执行浮点数运算使用 double,float通常精度不够,双精度有时比单精度更快。

类型转换注意点:

  • 赋予无符号类型一个超出它表示范围的数值,结果是初始值对无符号类型表示数值总数取模后的余数。
  • 赋予带符号类型一个超出它表示范围的值时,结果是未定义的,此时程序可能继续工作,可能崩溃,也可能产生垃圾。
  • 无符号与int 值进行运算时,int值会转换为无符号数,把int 转化为无符号的过程 与 把int直接赋给无符号变量一样。

2147483647 INT_MAX
-2147483648 INT_MIN

指定字面值的类型

当使用一个长整型字面值时,请使用大写字母 L 来标记,因为小写字母 1 和数字 1 太容易混淆。
例如: 42LL.

2.2 变量

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

列表初始化 11新标准

无论是初始化对象还是某些时候为对象赋予新值,都可以使用这一组花括号括起来的初始值。
例如:int num_val = {0};

重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。

int a = {3.14 L}

默认初始化

  • 定义于任何函数体之外的变量被初始化为 0 ,定义在函数体内部的内置类型变量将不被初始化,
    一个未被初始化的内置类型变量的值是未定义的,试图拷贝或者访问将发生错误。
  • 类的对象如果没有显式地初始化,则其值由类确定。

建议:初始化每一个内置类型的变量,虽然并非必须这么做,但是如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。

标识符

  • 必须以字母或者下划线开头。
  • 用户自定义的标识符中不能连续出现两个下划线,也不能以下划线连接大写字母开头,此外,定义在函数体外的标识符不能以下划线开头
  //变量要用描述性名称,不要节约空间,让别人理解你的代码更重要const int kDaysInWeek = 7;      //const 变量为k开头,后跟大写开头单词int num_entries_;               //变量命名:全小写,有意义的单词和下划线,类成员变量下划线结尾int num_complated_connections_;

名字的作用域

  • 定义在花括号外的名字拥有全局作用域,定义在花括号内的名字拥有作用域。

建议:当你第一次使用变量时再定义它
一般来说,在对象第一次使用的地方附近定义他是一种好的选择,因为这样做有助于更容易地找到变量的定义。更重要的是,当变量的定义于它第一次被使用的地方很近时,我们也会赋予其一个比较合理的初始值。

  • 内部作用域变量会覆盖掉外部作用域的同名变量(就近原则),如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

2.3 复合类型

复合类型(compound type)是基于其他类型定义的类型。 例如(指针与引用)。

2.3.1 引用

( rvalue reference ) 右值引用是 C++11新标准新增加的内容

当我们使用术语 “reference” 指的都是 “左值引用”( lvalue reference ),绑定另一种类型的符合类型。

  • 定义引用时,程序会把引用与它的初始值绑定在一起,而不是将值拷贝。
  • 引用必须被初始化,且无法令引用重新绑定到另外一个对象。
  • 引用并非对象,相反的,它只是为一个已经存在的对象所起的另一个名字。
  • 所有引用的类型都要和与之绑定的对象严格匹配,而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果保存在一起(左值绑定)。
2.3.2 指针

指针是指向另一种类型的符合类型。与引用类似,指针也实现了对其他对象的间接访问。

指针与引用的不同之处:

  1. 指针本身就是对象,允许对指针进行赋值与拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
  2. 指着无须在定义时赋与初始值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也拥有一个不确定的值。
  3. 与引用类似,除特殊情况外(后面会提到)指针的类型都要与它所指向的对象严格匹配。

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

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

利用指针访问对象:

  • 使用解引用符(操作符*)来访问
  • 给解引用的结果赋值,实际上也就是给指针所指的对象赋值。

空指针:

得到空指针的方式:使用 字面值 nullptr (C++11新标准)初始化指针,nullptr是一种特殊类型的字面值,它可以被转化为任意其他的指针类型。

建议:初始化所有指针

使用未经初始化的指针是引发运行时错误的一大原因。

  • 因此建议初始化所有的指针,并且尽量等定义了对象之后再定义指向它的指针。
  • 如果实在不清楚指针应该指向何处,就应该把它初始化为 nullptr 或者 0,这样程序就能检测并指导它没有指向任何具体的对象了。

任何非 0 指针对应的条件值都是 true

void* 指针:

void*指针 是一种特殊的指针类型,可用于存放任意对象的地址,与其他指针不同的是,我们对该指针中到底是一个什么类型的对象并不了解。

复合类型的声明 :

int* p,p2; 其中 p是指针类型,p2是int类型,始终数据类型是 int,(* 或 &)是类型修饰符,并不是数据类型的一部分。

指向指针的指针 :

  • 当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。
  • 通过 * 的个数可以区别指针的级别,**是指向指针的指针,***表示指向指针的指针的指针。
  • 解引用的规则与 指针级别符合

指向指针的引用 :

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

指针引用声明: int *& i = p;

  • 理解其变量的类型到底是什么,采用从右向左阅读 变量的定义,离变量名最近的符号对变量的类型有最直接的影响。

2.4 const 限定符

存在目的是为了防止程序不小心改变其值。

初始化和 const 介绍:

  • const类型的对象上执行不改变其内容的操作,因此const 对象必须被初始化。
  • 利用const 对象去初始化其他对象是无须担忧的,因为其中是利用了其值,而非对象本身

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

当以编译时初始化的方式定义一个 const 对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。
如果想在多个文件中共享该 const对象,必须在变量的定义之前添加 extern 关键字。

const 的引用

可以把引用绑定到const对象上,这样的引用我们称之为对常量的引用(非常量引用)。

  • 与普通引用的区别就是,对常量的引用不能被用作修改它绑定的对象。

初始化对const 的引用

一般来说,引用的类型必须与其所引用对象的类型一致,但是有两种例外:

第一种例外就是:初始化常量的引用时允许用任意表达式作为初始值,只要该表达式的结果能转换引用的类型即可。
例如:可以使常量的引用绑定到 非常量的对象,字面值,表达式。

理解例外发生的原因:

double d_val = 3.14;
const int &ri = d_val;此时为了确保 ri 绑定一个整数,编译器对其进行了如下操作:const int temp = d_val;
const int &ri = temp;ri 绑定了一个 临时量对象,来使其表达的结果可以进行转换为引用的类型,
但我们使用引用就是为了改变其对象的值,这时我们改变的是临时量,这种行为是非法行为。

对 const 的引用可能引用一个并非const 的对象

常量引用仅对引用可参与的操作做了限定,对于引用的对象的本身是不是一个常量未作限定,因为对象也可能是一个非常量,所以允许通过其他途径改变它的值。

指针和 const

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

试试这样想:所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

const 指针

由于指针本身就是对象,也允许把指针本身定位常量。

  1. 常量指针必须被初始化,一旦初始化完成,(存放在指针中的那个地址)就不能被改变了。
  2. 书写: int *const cur_err = &err_numb; // const指针将一直指向 err_numb
  3. 常量指针并不意味着不能通过指针来修改其所指向对象的值,能否这样做完全依赖与所指对象的类型,只是自己不能改变自己的指向而已。

顶层 const

指针本身是不是常量以及指针所指的是不是一个常量就是两个互相独立的问题。

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

当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换,一般来说,非常量能转换为常量,反之不行。

constexpr 和 常量表达式

常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。

  • 字面值属于常量表达式,用常量初始化的const对象也是常量表达式。
  • 直到程序运行才能获取到的的具体值不是常量表达式。

constexpr 变量
目的:为了解决一个初始值是不是常量表达式,因为在复杂系统中,很难分辨。

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

建议:一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr类型。

指针与constexpr

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

constexptr会把它所定义的对象 设置为顶层 const

2.5 处理类型

程序越来越复杂,程序中用到的类型也越来越复杂,

  1. 类型难以“拼写”
  2. 搞不清需要的类型到底是什么

类型别名

类型别名是为了让复杂的类型名字变得清晰明了,利于理解与使用。

  1. 使用 typedef‘

    typedef double wages; // wages是double的同义词
    typedef wages base,p ; //base 是double的同义词,p 是 doule的同义词

  2. 新标准规定的新的方法,使用别名声明(alias declaration)来定义类型的别名:
    using SI = SalesItem; // SI 是 SalesItem的同义词

关键字 using 作为别名声明的开始,紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。

指针,常量与类型别名

类型别名指代的是复合类型或常量,那么他的基本数据类型是指针。
typedef char *p_string; //p_string是 是数据类型(指针)
const p_string cstr 与 const char *cstr 是不同的

前者的数据类型是指针,因此前者的p_string是常量指针,后者的数据类型是 const char,*成为了声明符的一部分,因此后者的p_string是指向 常量字对象 的指针。

atuo 类型说明符

为了解决在声明变量的时候准确地知道变量的类型不那么容易,c++11新标准引入了 auto 类型说明符。

  • auto 定义的变量必须有初始值,因为它需要靠初始值推断变量的类型。

复合类型,常量与 atuo之间的关系

  • auto 一般会忽略掉顶层 const,同时底层 const会被保留下来。

const int ci = i;
auto e = &ci; //e是一个指向常量的整数指针

  • 在一条语句中利用 定义多个变量时,符号& 和 * 只从属于某个声明符,而非基本数据类型的一部分,因此多个变量的初始值应该是同一中类型。

decltype 类型指示符

从表达式的类型来推断要定义的变量的类型,而不是用值来推断,使用 C++新标准引入的第二类型说明符 decltype。

  • decltype处理顶层const 和引用的方式,与 auto方式不同,decltype()使用的如果是表达式或者变量,则decltype() 返回该变量的类型 (包括顶层const 和引用都在内)。

decltype 和 引用

  • 当有些表达式将向 decltype 返回一个引用类型,意味着该表达式的结果对象能作为一条赋值语句的左值。
  • 如果表达式的内容是解引用操作,则decltype得到引用类型。
  • decltype ((variable)) 的结果永远是引用,因为编译器会把加了一层或多层括号的变量当作一个表达式,变量是一种可以作为赋值语句左值的特殊表达式,所以这样的 decltype 就会得到引用类型,
    而decltype(variable)结果只有当 variable 本身就是一个引用时才是引用。

2.6 自定义数据结构

注意点:

  • 类定义的结尾最后加上分号
  • 最好不要把对象的定义和类的定义放在一起,对实体的定义混淆为一条语句中,这时不被推荐的。

类数据成员

  1. 我们的类只有数据成员,类的数据成员定义了类的对象的具体内容,每个对象都各自有自己的一份数据成员拷贝。
  2. C++11新标准,可以为数据成员提供一个类内初始值,创建对象时,类内初始值进行初始化该成员,没有初始值的成员将被默认初始化。

预处理概述:

由于头文件在程序中多次引用会造成 源文件重新编译获取更新过的声明,这时十分不安全以及不正常的。

确保头文件多次包含仍能安全工作的常用技术是 预处理器。

在C++中,我们用到的一项预处理功能是头文件保护符,头文件保护符依赖与预处理变量。

  • 预处理变量有两种状态:已定义与未定义。
  • #define指令把一个名字设定为预处理变量

另外两个指令则分别检查某个指定的预处理变量是否已经定义:

  • #ifdef 当且仅当变量已经定义时为真,#ifndef 当且仅当变量未定义为真时为真。

一旦检查结果为真,则执行后续操作直至遇见 #endif 指令为止。

头文件保护符原理详细解释

第一次包含 以下头文件时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直到遇见 #endif 为止,此时,预处理变量的 CPPPRIMER_SALEDATA_H_ 已经是已定义,如果再一次包含的话 #ifndef 的结果就为假,会忽略掉 #ifndef 到 #endif之间的部分。

#pragma once
//Copyright 2020 Handling
//License (BSD /GPL...)
//Author : Handling
//This is C++Primer
#ifndef CPPPRIMER_SALEDATA_H_
#define CPPPRIMER_SALEDATA_H_
#include <string>
#include <iostream>/* 每一个限定符内,声明顺序如下
1.typedef 和 enums
2.常量
3.构造函数
4.析构函数
5.成员函数,含静态数据成员
6.成员变量,含静态成员变量
*/
struct SaleData {std::string book_no_;unsigned units_sold_;double revenue_ = 0.0;
};#endif // CPPPRIMER_SALEDATA_H

习惯地加上头文件保护符是一个明智的决定。

第三关:字符串、向量和数组

3.1 命名空间的using声明

访问库中名字的简单方法,使用作用域运算符 (:: )
std :: cin

另外一种安全的方法:使用 using 声明(using declaration)
使用 using namespace::name 之后就无须专门的前缀,也能使用所需的名字。

每个名字都需要独立的using声明

  • 每个using声明引入命名空间中的一个成员,例如:可以把用到标准库的名字都以 using 声明的形式表现出来。

头文件不应该包含 using 声明

头文件的内容会拷贝到所有引用它的文件里去,如果头文件中某个using声明,那么每个使用了该头文件的文件就都会有这个声明,也许会造成始料未及的名字冲突。

3.2 标准库类型 string

直接初始化与拷贝初始化

  • 如果使用 等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去,与之相反,如果不使用等号,执行的是直接初始化。

读写 string 对象

在执行读取操作时,string 对象会自动忽略开头的空白(空格符,换行符,制表符)并从一个真正的字符开始读起,直到遇见下一处空白为止。

使用getline 读取一整行

getline 函数的参数是一个输入流和一个string对象,函数从给定的流中读入内容,直到遇见换行符为止,
(换行符也被读入),之后把所读的内容存入string对象中(不连换行符)。

string 的 empty 和size操作

empty 函数根据 string 对象是否返回空返回一个对应的布尔值。
size 函数返回 string 的长度,可以作为限制 string 对象的输出条件。

比较string 对象

  • string 对象相等意味着他们的长度与字母全部相同

小于,大于的规则如下:

  • 如果两个 string 对象的长度不同,而且较短 string 对象的每个字符都与较长 string 对象对应位置上的字符相同,就说较短 string 对象小于 较长 string 对象。
  • 如果两个 string 对象在某些对应的位置上不一致,则string对象比较的结果其实是 string对象中第一对 相异字符比较的结果。

字面值 和 string 对象相加

  • 因为标准库允许 字符字面值 和字符串字面值转换为 string 对象,所以在需要 string 对象的地方就可以使用这两种字面值来代替。

  • 当string对象和字符字面值或者字符串字面值混在一条语句中使用时,必须确保 每个加法运算符的两侧的对象至少有一个是 string。

  • 为了与 C兼容,C++语言中的字符串字面值并不是标准库类型的 string 对象,字符串字面值与 string是不同的类型。

处理 string 中的字符

cctype 头文件中定义了一组标准库函数处理这部分工作

isalnum(c)               当 c 是字母或数字时为真
isalpha(c)               当 c 是数字时为真
iscntrl(c)               当 c 是控制字符时为真
isdigit(c)               当 c 是数字时为真
isgraph(c)               当 c 不是空格但可以打印时为真
islower(c)               当 c 是小写字母时为真
isprint(c)               当 c 是可打印字符时为真 (即 c 是空格 或 c具有可视形式)
ispunct(c)               当 c 是标点符号时为真(不是控制字符,数字,字母,可打印空格)isspace(c)               当 c 是空白时为真(c是空格,横向制表符,纵向制表符,回车符,换行符,进纸符)isupper(c)               当 c 是大写字母时为真
isxdigit(c)              当 c 是十六进制数字时为真tolower(c)               将大写字母变为小写
toupper(c)               将小写字母变大写

建议:使用 c++ 版本的 c 标准库文件

因为 c++版本的头文件中定义的名字从属于 命名空间 std,但是 c不是,所以尽量全部使用
c开头的头文件,而不是选择使用.h结尾的头文件。

处理每个字符?使用基于范围的 for 语句

C++ 11 新标准提供的 :范围 for语句,能遍历其序列的每一个元素,对值进行某种操作。

for (declaration : expression)
statement

例子:

for (auto c : str)
cout << c <<endl;

使用范围 for 语句改变字符串中的字符

  • 如果想要改变 string 对象中 字符的值,必须把血循环变量定义成引用类型从。

使用下标 执行随机访问

使用下标时,必须检测其合法性,如果 索引或者下标越界 将会产生错误。

3.3 标准库类型 vector

vector 是模板而非类型,由vector 生成的类型必须包含 vector 中元素的类型,如 vector。

3.31列表初始化 vector 对象

C++11 新标准提供了为 vector 对象赋予初始值的方法,列表初始化。

c++ 语言提供了几种不同的初始化方法,在大多数情况下这些初始化方式能相互等价的使用,不过也并非一直如此。

  1. 使用拷贝初始化时,(即使用 = 时),只能提供一个初始值。
  2. 如果提供的是一个类内初始值,,则只能使用拷贝初始化或者花括号的形式初始化。
  3. 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在 圆括号里。

创建指定数量的元素

可以用 vector 对象容纳的元素数量 和 所有元素的统一初始值来初始化 vector 对象。

vector num_vec(10,-1);

值初始化

如果只提供 vector 对象容纳的元素数量而忽略其初始值,库会创建值初始化的元素初值,这个初值由 vector对象中元素的类型决定。

对这种初始化的方式有两个特殊限制:

  1. 有些类要求必须明确地提供初始值,如果 vector 对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值,对于这种类型的对象来说,只提供元素的数量不提供初始值就无法完成初始化工作。
  2. 如果只提供了元素的数量而没有设定初始值,只能使用直接初始化: 以()的方式。

列表初始值还是元素数量?

一方面情况:初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。

  • 如果用的是圆括号,可以说提供的值是用来构造 vector 对象的
  • 如果用的是花括号,可以表述为我们想列表初始化该 vector 对象的。

另一方面:如果初始化时使用了 花括号的形式但是提供的值不能来列表初始化,我们需要考虑用这样的值来构造 vector 对象了。

3.3.2 向 vector对象中添加元素

通过列表初始化的方式仅仅能对少量元素进行罗列,但是数量级的元素数量就不合理了,我们可以使用
push_back 向其中添加元素。

关键概念: vector 对象能高效增长

c++标准要求 vector 在运行时能快速地添加元素,因此在定义 vector 对象的时候设定其大小可能会导致性能更差,除了初始化的元素的值全部一样,建议设定空 vector 对象,运行时向其动态添加。

3.3.3 其他 vector 操作
vector<int> v;
v.empty()                   判空
v.size()                    返回v中元素的个数
v.push_back(elem)           添加元素
v[n]                        索引第 n 个位置上的引用。
v1 = v2                     用v2中元素的拷贝替换 v1中的元素
v1 = {a,b,c...}             用列表中元素的拷贝替换 v1 的元素
v1 == v2                    v1 和 v2 相等当且仅当他们的元素的数量相等且对应位置的元素值都相同。
<,<=,>,>=                   按照字典序比较

当元素的定义了自己的相等性运算符与关系运算符,vector对象 才能支持相等性判断与关系运算等操作。

不能用下标形式添加元素

vector<>对象 以及string 对象的下标 运算符可用于访问已存在的元素,而不能用于添加元素。

提升:只能对确知已存在的元素执行下标操作,如果对不存在的元素去访问将引发错误,(buffer overflow)

确保下标合法的一种有效手段就是尽可能使用 范围for语句。

3.4 迭代器介绍

并不是所有的容器都支持 下标运算,但是所有的容器都支持另一种间接访问元素的机制,迭代器。

3.4.1使用迭代器
  • 与指针不同的是,获取迭代器不是使用取地址符,而是有迭代器的类型同时有着返回迭代器的成员。
  • begin(开头迭代器) 与 end(尾后迭代器,指向容器本不存在的 ”尾后“元素),特殊情况下容器为空,begin 与 end 返回的是同一个迭代器,都是尾后迭代器。

迭代器运算符

*iter       返回迭代器 iter 所指元素的引用
iter->mem   解引用iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem
++iter      令 iter 指示容器中的下一个元素。
--iter      令 iter 指示容器的上一个元素
iter1 == iter2 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者他们是同一个容器的iter1 != iter2  尾后迭代器,则相等,否则不相等

解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

将迭代器从一个元素移动到另外一个元素

  • 迭代器可使用递增 ++ 或递减 --运算符来进行从一个元素移动到另一个元素
  • 不能对 end 迭代器进行解引用或者递增操作。

泛型编程的概念:

由于并非所有的标准库容器都定义了下标运算或者是 迭代器的操作符(<,>),因此我们要养成使用迭代器和 !=,这样就比较有通用性,不太在意用的是那种数据类型。

迭代器类型:

一般情况下,我们的迭代器类型有 iterator 与 const_iterator。

术语:迭代器类型与迭代器是不同的,一个指数据类型,一个指的是迭代器对象。

begin 与 end

  • 如果对象只需要读操作而无需写操作的话最好使用常量类型迭代器(const_iterator)
    为了专门得到常量迭代器类型的返回值,C++ 11 定义了两个新函数,分别是 cbegin() 和 cend();

结合解引用和成员访问的操作

为了简化 使用解引用符与下标点符获取该指向对象的元素(*iter).elem,C++定义了 箭头运算符(->)
iter ->elem 来将其操作结合在一起。

某些对vector对象的操作会使 迭代器失效

谨记:但凡是使用了 迭代器的循环体,都不要向迭代器所属的容器添加元素。

3.4.2 迭代器运算

所有的标准库容器都有支持递增运算的迭代器,类似的,也用 == 与 !=对任意的标准库容器进行比较操作。

string 和 vector 提供了额外的运算符(迭代器运算)

iter + n           迭代器加上整数仍得一个迭代器,向前移动 n 个元素
iter - n           迭代器减去整数得到一个迭代器,向后移动 n 个元素
iter += n          迭代器加法的复合赋值语句
iter -= n          迭代器减法的复合赋值语句iter1 - iter2      迭代器相减的结果是他们之间的举例,参与运算的必须是同一个容器中的元素的迭代器,或者是尾元素的下一位置。> ,>= ,< ,<=       迭代器的关系运算符,位置在前的迭代器小于位置在后的迭代器

3.5 数组

如果不清楚元素的确切个数,请使用 vector

3.5.1 定义与初始化内置数组
  • 数组是一种复合类型,声明为: 数据类型 数组名+维度(必须大于0)
  • 编译时数组的维度必须是已知的,维度必须是一个常量表达式。
  • 不设置初始化列表的时候,数组的元素被默认初始化
  • 与内置类型的变量一样,函数内部定义了内置类型的数组,那么默认初始化会令数组含有未定义的值。
  • 不允许用auto 关键字由初始值的列表推断类型。另外和 vector 一样,数组的元素应为对象,因此不存在引用的数组。

显示初始化数组元素

  • 指明维度的,则初始值的总数量不能超过维度,如果小于维度,则提供的初始值初始化靠前的元素,剩下的元素被初始化为默认值。
  • 不指明维度,则按照提供的初始值数量设置维度。

字符数组的特殊性

字符数组可以直接使用字符串字面值对此类数组初始化,注意字符串字面值末尾会有一个 空字符’\0‘,
这个空字符也会被拷贝到字符数组中去。

但是vector 是不支持直接使用字符串字面值对其进行初始化的。

不允许赋值与拷贝

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

int a[] = {0, 1, 2};
int a2[] = a; //错误
a2 = a; //错误

一些编译器支持数组的赋值,但是这些非标准特性的程序很有可能在其他编译器上无法正常工作。

理解复杂的数组声明

因为数组是可以存放大多数类型的对象,同时本身也是对象,可以定义存放指针的数组,也可以定义指向数组的指针。

  int ptrs[10];//int &refs[10] = {};int *ptr[10]; //ptr是存放了10个指针的数组int (*parray)[10] = &ptrs;   //parray 是指向数组的指针int (&arr_ref)[10] = ptrs;  //arr_ref对数组的引用

要想理解数组声明的含义,最好的办法就是从内向外,从右至左来分析。

int *(& arry) [10] = ptrs ; //从内看,arry是一个引用,从右至左(忽略到括号内)是一个指针数组,那么arry就是对指针数组的引用。

3.5.2 访问数组元素
  • 数组下标通常定义为 size_t 类型,size_t 是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在c++的 cstdef中定义了 size_t类型。
  • 注意检查下标的值是否符合合法内存区域。
3.5.3 指针与数组
  • 使用数组的时候编译器会把它转换成为指针。
  • 对数组元素使用取地址符就能得到指向该元素的指针。
  • 在很多用到数组名字的地方,编译器都会自动第将其替换为一个指向数组首元素的指针。

int ia[] = {0,1,2,3,4};
auto 推断数组名为 其数组类型指针 auto ia2(ia); ia2是指针
decltype() 推断数组名 为其数组 decltype(ia) ia2; ia2是数组

指针也是迭代器

利用指针也可以完成迭代器的操作,递增,指示等等。

标准库函数 begin 和 end

由于数组的尾后指针(并不存在的元素地址)获取会容易出错,为了让指针的使用更加简单安全,C++11新标准引入了 两个名为 begin 和 end 的函数。

begin(arr) :会得到arr首元素的指针
end(arr) :会得到 arr 尾元素的下一个位置的指针

指针运算

迭代器的所有运算,用在指针上意义完全一致。

  • 两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,它是一种带符号类型,定义在
    cstddef 头文件。

下标与指针

内置的下标运算符所用的索引值并不是无符号类型,可以处理负数,这一点与 vector 和 string 不一样。

3.5.4 C风格字符串

C++支持 C风格字符串,但在 C++程序中最好还是不要使用他们,C风格字符串极易发生程序漏洞,是诸多安全问题的根本原因。

  • 习惯书写的字符串存放在字符数组中并以 空字符结束。

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.

以上函数,均不会去验证字符串参数的正确性。

使用标准库 string 要比使用 C风格字符串更加安全,高效。

3.5.5 与旧代码的接口

混用 strng 对象与 C 风格的字符串

  • 允许使用以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值。
  • 在 string 对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string 对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
  • 如果程序需要的是 一个 C 风格字符串,那么 可以使用 string .c_str() 来将字符串转换为 C风格的字符数组。

注意点: c_str() 函数返回的数组在改变了字符串对象时会失去效用,我们最好将该数组拷贝一份。

使用数组初始化 vector

vector i_vec{begin(int_arr) , end(int_arr)};

建议:尽量使用 标准库类型而非数组类型,C 程序的底层操作容易引发一些繁琐细节有关的错误。

3.6 多维数组

  • 要使用 范围 for语句处理多维数组,除了最内层的循环外i,其他所有循环的控制变量都应该是引用类型,因为外层的循环获取到的元素如果是数组的话,不使用引用类型的话,这些元素将会被认为是指针类型。

类型别名简化多维数组的指针

  using int_array = int[4];int ia[3][4];for (int_array *p = ia; p != ia + 3; ++p) {for (int *q = *p; q != *p + 4; ++q)cout << *q << ends;cout << endl;}

第四关:表达式

  • 表达式是由一个或多个运算对象组成,对表达式求值将得到一个结果。
  • 字面值和变量是最简单的表达式,其结果就是字面值和变量的值。
  • 把 运算符和一个或多个运算对象组合起来可以生成较为复杂的表达式

4.1 基础

4.3 逻辑与关系运算符

运算对象和求值结果全是右值

逻辑与与逻辑或运算符

左结合律
短路求值:

  • 对于逻辑与运算符来说,当且仅当左侧运算对象为真时候才对右侧运算对象求值。
  • 对于逻辑或运算符来说,当且仅当左侧运算符为假时才对右侧对象求值。

逻辑非运算符

右结合律;
逻辑非运算符将运算对象的值取反后返回,

关系运算符

左结合律,
满足即为真,不满足为假

相等性测试与布尔字面值
左结合律

  • 如果向测试一个算术对象或指针对象的真值,最直接的办法就是使用if 语句条件测试
  • 不要使用布尔字面值 true 和 false 作为运算对象。

4.4 赋值运算符

  • 赋值运算符的左侧对象是可修改的左值,并且右侧运算对象是能够转换为左侧对象的值。
  • 左侧运算对象是内置类型,使用列表初始化时,注意只能包含一个值。
  • 赋值运算符满足右结合律
  • 赋值运算符优先级比较低

因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。

复合赋值运算符效率稍高。

4.5 递增和递减运算符

因为很多迭代器不支持算术运算,所以递增与递减运算符是必须的

建议:除非必须,否则不使用递增递减后置版本。

后置版本将原始值存储下来以便于返回这个未修改的内容,一般情况下我们不需要保留该值,这就会造成浪费。

在一条语句中混用解引用与递增运算符

建议: 简洁可以成为一种美德。

使用 * p++ :先将p指针加一,返回 p 未增加前的副本,之后解引用,并将指针向前移动一个位置

运算对象可按任意顺序求值

如果一条子表达式改变了某个运算对象的值,另一条子表达式又要用到该值的话,运算对象的求值顺序就很关键了,除非子表达式与另一条子表达式是相连的关系。

4.6 成员访问运算符

点运算符与箭头运算符都可用于 访问成员,点运算符获取类对象的一个成员,箭头运算符与点运算符有关,表达式 ptr-> mem 等价于 (*ptr).mem;

  • 点运算符的优先级是低于点运算符的,所以执行解引用的子表达式两端必须加上括号。
  • 箭头运算符作用域一个 指针类型的运算对象,结果是一个左值,而点运算符分为两种情况:
    成员的所属对象是左值,则结果是左值,所属对象是右值,则结果是右值。

4.7 条件运算符

条件运算符 (?:)允许我们把简单的 if- else 逻辑嵌入到单个表达式中:
cond ? expr1 : expr2

条件运算符值对 expr1 与 expr2 中的一个求值。
当条件运算符的两个表达式都是左值或者能转换成同一左值类型时,运算结果是左值,否则是右值。

嵌套条件运算符

条件运算符满足右结合律,意味着运算对象从右至左的顺序进行顺序结合。

条件运算的嵌套最好别超过两到三层。

4.8 位运算符

位运算作用域整数类型的运算对象,并把 运算对象看成是 二进制位的集合。
位运算符提供检查和设置 二进制位的功能

位运算符(左结合律)
运算符 功能 用法
~ 位求反 ~expr
<< 左移 expr1 << expr2
>> 右移 expr1 >> expr2
& 位与 expr & expr
^ 位异或 expr ^ expr

| 是位或运算

注意:位运算符处理运算对象的 ”符号位“依赖于机器。而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。
强烈建议仅将位运算符用于处理无符号类型。

移位运算符

移位运算符是执行二进制位的移动操作,左侧对象按照右侧运算对象的值要求移动位数,然后将经过移动的左侧运算对象的拷贝作为求值结果。(右侧对象不能为负值)

  1. 二进制位移位后,移出边界的位就被舍弃掉。
  2. 左移运算符 << 在右侧插入值为 0 的二进制位,右移运算符的行为则依赖于其左侧运算对象的类型,如果是无符号类型,在左侧插入值位 0 的二进制位,如果该运算对象是带符号的,在左侧插入符号位的副本或值为 0 的二进制位。

位求反运算符

位求反运算符将运算对象逐位取反后生成一个新值,将1置为 0,0置为1.

位与,位或,位异或运算符

对于位于运算符 (&)来说,如果两个运算对象对应位置都是1,则运算结果中该位 为 1,否则为 0.
对于位或运算符(|),如果两个运算对象的对应位置上有一个是 1,则运算结果中该位置为1,否则为0.
对于异或运算符(^),如果两个运算对象的对应位置有且仅有一个为1,则该运算结果为1,否则为 0.

在移位运算时加上括号会帮助减少错误(优先级不高不低)

4.9 sizeof 运算符

sizeof返回的是表达式结果类型的大小。
sizeof 满足右结合律,并且与* 运算符的优先级一样。

  • 在sizeof的运算对象中解引用一个无效指针仍然是一种 安全的行为,因为指针并没有被真正使用,sizeof不需要真的去解引用指针也能知道它所指对象的类型。
  • 对 char 类型或者 char 的表达式执行 sizeof运算,结果为1.
  • 对引用类型执行得到引用的其对象空间的大小
  • 对指针执行sizeof 得到指针本身所占空间的大小
  • 对解引用指针执行得到指针所指对象所占空间的大小
  • 对数组执行得到整个数组所占空间的大小,(sizeof并不会把数组当成指针来处理)
  • 对string对象或 vector 对象执行sizeof运算指挥返回该类型固定部分的大小,不会计算其占用了多少空间。

4.10 逗号运算符

都好运算符有两个运算对象,首先对左侧的表达式求值,然后把求值结果丢弃掉。
逗号运算符真正的结果是右侧表达式的值,如果右侧表达式的结果是左值,最终的求值结果也是左值。

4.11 类型转换

隐式转换:是自动执行的对运算对象进行的类型统一的过程。

何时会发生隐式转换:

在下面这些情况下,编译器就会自动地转换运算对象的类型:

  • 在大多数表达式中,比 int 类型小的整型值首先提升为较大的整数类型
  • 在条件中,非布尔值转换为布尔值
  • 初始化过程中,初始化转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
  • 如果算术运算符或关系运算的运算对象有多种类型,需要转换成同一类型。
  • 函数调用时也会发生类型转换。
4.11.1 算术转换

算术转换的含义是把一种算术类型转换成另外一种算术类型。

整型提升

  1. 整型提升负责把小整数类型转换成较大的整数类型。(short ,char ->转换为 int)
  2. 较大的 char 类型提升称为 int,unsigned int,long 类型中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
4.11.2 其他隐式转换类型
  • 数组转换为指针:在数组的表达式中,数组自动转换成指向数组首元素的指针

  • 指针的转换:C++中还规定了其他的指针转换方式,常量整数 0 或者字面值 nullptr能转换成任意指针类型:指向任意非常量的指针能转换成 void* ;指向任意对象的指针能转换成 const void*.

  • 转换为布尔类型:指针算术类型的值为 0,转换的结果是false,否则转换结果为 true;

  • 转换成常量:允许将指向非常量类型的指针转换为指向相应常量类型的指针,对于引用也是这样。

  • 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过每次只能转环一次。

4.11.3 显示转换

命名的强制类型转换

一个命名的强制类型转换的格式如下:

 cast-name<type>(expression)

static_cast:

  • 任何具有明确定义的类型转换,只要不包含底层 const,都可以使用 static_cast
  • 当需要把一个较大的算术类型赋值给较小的类型时,static_cast 是非常有用的,这是显式地告诉编译器,我们并不在乎潜在的精度损失。
  • static_cast 对于编译器无法自动执行的类型转换也非常有用,但是我们必须确保转换后的类型能够复合左值使用。

const_cast

const_cast 只能改变运算对象的 底层 const

  • 去掉const性质,一旦我们去掉了对象的 const 性质,编译器将不会阻止我们进行写操作。
  • 只有 const_cast 能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误,也不能 使用 const_cast 改变表达式的类型

reinterpret_cast

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

reinterpret_cast 本质上依赖于机器。要想安全地使用 reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解。

建议:避免强制类型转换

强制类型转换干扰了正常的类型检查,因此我们强烈建议 程序员避免使用强制类型转换。

4.12 运算符优先级表

运算符优先级
结合律与运算符 功能 用法
1 左 :: 全局作用域 ::name
1左 :: 类作用域 class::name
1左 :: 命名空间作用域 namespace::name
2左 . 成员选择 object.member
2左 -> 成员选择 pointer->member
2左 [ ] 下标 expr[expr]
2左 () 函数调用 name(expr_list)
2左 () 类型构造 type(expr_list)
3右 ++ 后置递增运算 lvalue++
3右 – 后置递减运算符 lvalue–
3右 typeid 类类型 ID typeid(type)
3右 typeid 运行时类型 ID typeid(expr)
3右 explicit cast 类型转换 cast_name <type.> (expr)
4右 ++ 前置递增运算 ++lvalue
4右 – 前置递减运算 –lvalue
4右 ~ 位求反 ~expr
4右 ! 逻辑非 !expr
4右 - 一元负号 -expr
4右 + 一元正号 +expr
4右 * 解引用 *expr
4右 & 取地址 &lvalue
4右 () 类型转换 (type)expr
4右4sizeof 对象的大小 sizeof expr
4右 4sizeof 类型的大小 sizeof(type)
4右 Sizeof… 参数包的大小 sizeof…(name)
4右 new 创建对象 new type
4右 new[ ] 创建数组 new type[size]
4右 delete 释放对象 delete expr
4右 delete [ ] 释放数组 delete[ ] expr
4右 noexcept 能否抛出异常 noexcept(expr)
5左 ->* 指向成员选择的指针 ptr->*ptr_to_member
5左 .* 指向成员选择的指针 obj.* ptr_to_member
6左 * 乘法 expr * expr
6左 / 除法 expr / expr
6左 % 取模 expr % expr
7左 + 加法 expr + expr
7左 - 减法 expr - expr
8左 << 向左移位 expr<<expr
8左 >> 向右移位 expr>>expr
9左 < = 小于等于 expr <= expr
9左 > = 大于等于 expr >= expr
10左 == 相等 expr == expr
10左 != 不相等 expr!=expr
11左 & 位与 expr & expr
11左 位或
11左 ^ 位异或 expr ^ expr
12左 && 逻辑与 expr && expr
12左
13右 ?: 条件 expr ? expr: expr
14右 = 赋值 lvalue = expr
15右 复合运算符
16左 , 逗号表达式 expr, expr

第五关:语句

C++ 提供了一组控制流语句以支持更复杂的执行路径。

5.1 简单语句

表达式末尾加上分号就成了表达式语句;

空语句

  • 空语句是 只含有一个单独的分号;
  • 使用空语句要加上注释,让其他人知道这句是有意省略的。

别漏写分号,也别多写分号

复合语句

  • 复合语句是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称为块。
  • 一个块就是一个作用域
  • while 或者 for 的循环体只能跟一条语句,但我们可以使用复合语句(块)扩展循环体内做的事情。
  • 块不以分号结尾,仅为对应块的右花括号结束为结束。

5.2 语句作用域

在 if,switch,while 和 for 语句中的控制结构内定义变量,定义在控制结构当中的变量只在对应语句的内部可见,语句结束,变量也会超出其作用范围。

因为控制结构定义的对象的值马上要由结构本身使用,所以这些局部变量需要初始化。

5.3 条件语句

悬垂 else

当 if 存在且 if else 语句也存在,这时 C++规定 else 与 离它最近的尚未匹配的 if 匹配,从而消除 程序的二义性。

case 关键字

switch‘ 语句括号内的表达式是可以转换为整数类型的表达式。
case 关键字与它对应的值一起被称为 case 标签,case 标签必须是整型常量表达式。

switch 内部的控制流

  • 如果某个 case 标签匹配成功,将该标签开始往后顺序执行所有的 case 分支,除非程序显式地中断这个过程,不然switch的结尾处才会停下来。
  • 不要省略 case 分支最后的 break 语句,如果没写 break 语句,要加一段注释说清楚程序的逻辑。
  • 即使不准备在defalult 的标签下做任何工作,定义一个 default 标签也是有必要的。

switch 内部的变量定义

  • 如果在某处一个带初始值的变量位于作用域之外,在另一处变量位于作用域之内,从前一处跳转到后一处的行为是非法行为。
  • 把 变量定义在 switch 内部条件选择的块中,以确保后面的所有 case 标签都在变量的作用域之外。

5.4 迭代语句

  • 在使用迭代器的 for 语句中,预存了 end() 的值,如果序列添加删除元素,那么 end函数的值将变得无效
  • do while 语句应该在括号包围起来的条件后面用一个分号来表示语句结束。
  • do while 条件中使用的变量必须定义在循环体之外。
  • do while 来说先执行语句或者块,后判断条件,所以不允许在条件部分定义变量。

5.5 跳转语句

  • break 语句 负责终止离它最近的 while,do while,for 或 switch语句,并从这些语句后的第一条语句继续执行。
  • continue 语句终止最近的循环中的当前迭代并立即开始下一次迭代,并且当 switch 语句嵌套在迭代语句内部时,才能在 switch 中使用 continue;

5.6 try 语句块和异常处理。

异常是指存在于运行时的反常行为,这些行为都超出了函数正常功能的范围。

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

函数在寻找处理代码的过程中退出

当异常被抛出时,首先搜索抛出异常的函数,没有找到对应的 catch子句,终止该函数,并在外层调用该函数的函数继续搜索,。。。。沿着程序的执行路径逐层回退,直到找到适合类型的 catch 子句为止。

如果最终没有找到匹配的 catch,程序将转到名 为 terminate的标准库函数,(该函数会导致程序非正常退出)。

标准异常

C++ 标准库中定义了一组类,用于报告标准库函数遇到的问题,他们分别定义在 4个头文件中

  • exception 头文件定义了最通用的异常类 exception。它只报告异常的发生,不提供任何额外信息
  • stdexcept 头文件定义了集中常用的异常类
  • new 头文件中定义了 bad_alloc异常类型
  • type_info 头文件定义了 bad_cast 异常类型

stdexcept 定义的异常类

exception 最常见的问题
runtime_error 只有在运行时才能被检测的问题
range_error 运行错误:生成的结果超出了有意义的值域范围
overflow_error 运行时错误;计算上溢
underflow_error 运行时错误:计算下溢
logic_error 程序逻辑错误
domain_error 逻辑错误,参数对应的结果值不存在
invalid_argument 逻辑错误:无效参数
length_error 逻辑错误:试图创建一个超出该类型最大长度的对象
out_of_range 逻辑错误:使用一个超出有效范围的值
  • 异常类型只定义了一个 名为 what 的成员函数,该函数没有任何参数,返回值是一个 c风格字符串,提供了异常的文本信息。
  • what 返回的 C风格字符串的内容于异常对象的类型有关,如果异常类型有一个字符串初始值,则返回该字符串,无则由编译器决定返回什么。

第六关: 函数

6.1 函数基础

函数的构成: 返回类型,函数名字,0或者多个形参组成的列表以及函数体。

调用运算符 :使用括号运算符作用于一个表达式(函数或者是函数指针),圆括号是用逗号分隔的参数列表,我们用实参初始化函数的形参,调用表达式的类型就是函数的返回类型。

调用函数:

调用函数完成了两项工作:

  • 一是用实参初始化函数对应的形参,
  • 二是将控制权转移给被调用函数,主调函数被暂时中断,被调函数开始执行。

return 语句的两项工作:

  • 一是返回语句中的值
  • 二是将控制权从被调函数转移回主调函数。

形参与实参

  • 实参是形参的初始值
  • 编译器能对任意可行的顺序对实参求值,C++并没有规定实参的求值顺序。
  • 实参与形参类型要匹配,形参与实参数量一致,所以形参一定会被初始化。
  • 必须提供的是可转换为形参类型的实参

函数的形参列表

  • 任意两个形参不能重名,函数的局部变量也不能与形参的名字一致
  • 即使形参的不被函数使用,也要为其提供一个实参。

函数返回类型

  • 大多数类型都能作为函数的返回类型,一种特殊的返回类型是 void
  • 函数不能返回数组或者函数类型,但可以返回指向数组或者函数的指针
6.1.1 局部对象

在C++中语言中,名字有作用域,对象有生命周期

  • 形参和函数体内定义的变量统称为局部变量,仅在函数的作用域内可见。
  • 局部变量还会隐藏在外层作用域中同名的其他所有声明中。

自动对象

只存在于块内执行期间的对象称为自动对象,函数执行结束后,创建的自动对象的值就变成未定义的 了。

对于局部变量对应的自动对象:

  • 如果变量定义本身含有初始值,就用这个初始值进行初始化;否则如果变量定义不含初始值,执行默认初始化。
  • 内置类型的未初始化局部变量将产生未定义的结果。

局部静态变量

局部静态对象 在程序的执行路径第一次经过该对象时定义语句并将其初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

6.1.2 函数声明
  • 函数的名字也必须在使用之前声明,函数只能定义一次,但可以声明多次。
  • 如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
  • 函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。

在头文件中进行函数声明:

  • 建议变量在头文件中声明,在源文件中定义,函数也相同。
  • 含义函数声明的头文件应该被包含到定义函数的源文件中
6.1.3 分离式编译

分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

编译和链接多个源文件

fact 函数 声明于 Chapter6.h 的头文件中,定义于 fact.cc
factMain.cc 文件中创建 main 函数,main函数调用 fact 函数。

其中如果要生成 可执行文件,我们需要告诉编译器我们的代码在哪,下面演示:

$ cc factMain.cc fact.cc ##generates .exe or a.out
$ cc factMain.CC fact.cc -o main # generates main or main.exe

cc 是编译器的名字, $ 是系统提示符,#是注释

分离式编译并链接
如果我们修改了其中一个源文件,那么我们只需要重新编译那个改动了的文件,大多数编译器都会提供分离式编译每个文件的机制,这一过程通常会产生一个 后缀名为 .obj 或者 .o 的文件,后缀名的含义是该文件包含该对象代码。

编译过程:
$ cc -c factMain.cc #generates factMain.o
$ cc -c fact.cc #generates fact.o

链接过程:

$ cc factMain.o fact.o # generates factMain.exe or a.out
$ cc factMain.o fact.o -o main #generates main or main.exe

6.2 参数传递

6.2.1 形参与实参
  • 形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。
  • 当实参拷贝给实参,形参实参是相互独立的对象,我们说这样的实参被值传递或函数被传值调用。
  • 熟悉 C 的程序员常常使用指针类型的形参访问函数外部的成员,C++语言则建议使用引用类型的形参代替指针
  • 通过使用非常量引用形参,允许函数改变或多个实参的值。
  • 存在多种类型(IO)不支持拷贝操作时,函数只能通过引用形参进行访问该对象
  • 如果函数无须改变引用形参的值,最好将其声明为常量引用
  • 当函数求得的返回值不能满足目标信息的数据个数,我们可以声明引用类型的形参去保存另外的信息。
6.2.2 const 形参与实参
  • 用实参初始化形参时会忽略掉顶层 const,当形参有顶层const 时,传递给它常量对象或者非常量对象都是可以的。
  • 在C++中,不同函数的形参列表应该有明显的不同,但是 顶层const被忽略掉了,它的参数列表就与 非具有顶层const 的参数列表相同,定义这两个参数的函数是相同的。
6.2.3 指针或引用参数与 const
  • 非常量初始化一个底层 const 对象是合理的,但是底层const对象 初始化一个非常量对象 是不合理的
  • 尽量使用常量引用形参,能极大的避免 实参为 底层const对象或者 非底层const对象 可能带来的一些错误
6.2.4 数组形参
  • 不能拷贝数组以及使用数组时通常会将其转换为指针。
  • 以数组作为实参的函数也必须确保使用数组不会越界。

管理指针形参

  1. 使用标记指定数组长度(c风格字符串以 空字符停止’\0’)
  2. 使用标准库规范,传递执行数组首元素和尾后元素的指针。
  3. 显式地传递一个表示数组大小的形参。

数组形参与 const

只有当函数确实要改变元素值的时候,我们才把形参定义成指向非常量的指针。

数组引用形参

我们可以将引用形参绑定到数组上,数组的引用。

void print(int (&arr)[ 10 ]);

传递多维数组

void print (int (*matrix)[10], int rowSize);
void print (int matrix[][10], int rowSize)

编译器会自动忽略第一个维度,因此请不要包含它到形参列表中

6.2.5 main:处理命令行选项

我们有时需要给 main 传递实参,一种常见的情况是用户通过设置一组选项来确定该函数执行的操作

prog -d -o ofile data0

这些命令行通过两个参数传递给main函数

int main(int argc,char **argv)

argc 代表了传递信息的数量,argv代表了命令行的字符串数组

  • 当使用 argv 的实参时,注意一要从 角标 1开始,因为 argv[0]保存程序的名字,而非用户输入。
6.2.6 含有可变形参的函数

我们有时无法预知 向函数传递几个实参, 因此为了编写能处理不同数量实参的函数,
C++11新标准提供了两种主要的方法

  1. 实参类型相同,传递给 initializer_list 的标准库类型
  2. 实参类型不同,编写另外的一种特殊函数,可变参数模板

initializer_list

 initializer_list<T> lst;   默认初始化:T类型元素的空列表
initializer_list<T> lst{a,b,c....}  lst的元素数量与初始值一样多,lst的元素都是对应初始值的副本,
元素均为constlst2(lst)  :执行拷贝或者赋值对象,但不会拷贝列表中的元素,而是两个对象共享元素
lst2 = lst;lst.size()  :列表中元素数量
lst.begin() :返回 首元素指针
lst.end() : 返回尾后指针

省略符形参

省略符形参是为了便于 C++程序访问某些特殊的 C代码而设置的,这些代码使用了名为 varargs 的 C 标准库的功能。

  • 省略符号形参应该仅仅用于 C 与 C++通用的类型,特别注意,大多数类型的对象在传递给 省略符形参时都无法正确拷贝。
  • 省略符形参对应的实参无须类型检查,且形参声明后面的逗号是可选的
    void foo(int a,…) = void foo(int a…)
    void foo(…) {

}

6.3 返回类型和return 语句

6.3.1 无返回值
  • 无返回值 返回类型为 void,且 return语句能显式地中断函数的进行,使控制流返回到调用函数的地方
6.3.2 有返回值
  • 有返回值时,要确保一定会有返回值返回,循环与if嵌套有可能确保不了程序是否能正确返回。
  • 返回一个值的方式与初始化一个变量或形参的方式相同。

不要返回局部变量的引用或指针

  • 不要返回局部变量的引用或指针,因为函数完成后,它所占用的存储空间也被随之释放掉,函数终止意味着局部变量的引用将指向不再有效的内存区域。
  • 保证返回值安全的方式:确保引用所引的是函数之前已经存在的对象。

返回类类型的函数和调用运算符

(.) 调用运算符的优先级与 点运算符和箭头运算符相同,且复合结合律,我们可以使用函数返回的结果直接调用其对象的成员。

引用返回左值

调用一个返回引用的函数得到左值,我们能为返回非常量的引用的对象赋值

列表初始化返回值

C++11新规定,函数可以返回花括号包围的值的列表(代表了返回的临时量进行初始化),如果列表为空,临时量指向值初始化,否则,由函数的返回类型决定。

如果返回的是内置类型,则花括号应该仅有一个值,且所占空间不应该大于目标类型的空间,如果返回的是类类型,则由类定义初始值如何使用。

主函数 main 的返回值

  • main 函数没有 return 语句也可以直接结束,因为编译器会隐士地自动插入一条返回 0 的语句
  • main 函数的返回值是状态指示器,返回0 代表成功,其他为失败,非 0 的值由机器而定,但是为了避免返回值与机器有关, cstdlib 头文件定义了两个预处理变量来表示成功与失败(EXIT_FALURE EXIT_SUCCESS)

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。

  • 在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到栈空间耗尽。
6.3.3 返回数组指针

利用 类型别名返回数组指针或引用

using arrT = int【10】;

arrT * func(int i);

声明一个返回数组指针的函数

我们想定义一个返回数组指针的函数,则数组的维度必须紧跟在函数的名字之后

Type(*function(parameter_list))[dimension]

逐层理解:

  • func(int i) 表示调用func函数我们需要一个int类型的实参。
  • (*func(int i)) 意味着我们可以对函数调用的结果执行解引用操作。
  • (*func(int i))[ 10 ] 表示解引用 func 的调用将得到一个大小是10的数组。
  • int (*func(int i))[10] 表示数组中的元素是 int 类型

使用尾置返回类型

auto func(int i) -> int(*)[10]

为了表示函数真正的返回类型跟在形参列表之后,我们本应该出现返回类型的地方放置一个 atuo;

使用 decltype

利用 decltype()推断数组的类型,之后跟一个指针也可以

int odd[] = {1,3,4,5}
decltype(odd) *arrPtr(int i);

6.4 函数重载

如果同一作用域中的几个函数名字相同但形参列表不同,我们称之为重载函数

  • main 函数不能重载

定义重载函数

对于重载的函数来说,他们应该在形参数量或形参类型上有所不同。

重载和 const 形参

  • 编译器会忽略掉形参的 顶层 const,编译器对于有无顶层const的 形参是区分不开的
  • 底层 const的形参只能通过 底层const 的实参传递,编译器能区分开来

建议:何时不应该重载函数

重载函数虽然一定程度上减轻我们为函数起名字的负担,但是最好重载那些确实非常相似的操作。

const_cast 和重载

利用 const_cast 能对底层const 与 普通非常量 进行转换

调用重载的函数

  • 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也将发生错误,称为二义性调用。
6.4.1 重载与作用域
  • 如果我们在内层作用域声明函数的名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名。
  • C++中,名字的查找发生在类型检测之前。

6.5 特殊用途语句特性

6.5.1 默认实参

在很多次函数的调用中,一些形参被赋予了同一个值,这时,我们将反复出现的值称为函数的默认实参。
调用该默认实参的函数可以包含实参也可以省略实参。

  • 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

using sz = string::size_type ;
string screen(sz ht = 24, sz width = 80, char background = ’ ')

使用了默认实参调用函数

  • 函数调用时实参按照位置进行解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)
  • 设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参放在后面。

默认实参声明

通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。

  • 在给定的作用域中一个形参只能被赋予一次默认实参,后续更改是不行的,且后续声明仅能为那些没有默认实参的形参进行声明。
using sz = std::string::size_type;
std::string screen(sz, sz, char = ' ');
//std::string screen(sz, sz, char = '*');  //不能修改char的默认实参
std::string screen(sz = 24, sz = 25, char); //不能修改char的默认实参

默认实参初始值

局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。
局部变量隐藏了外层的 变量时,但是局部变量与传递给函数的默认实参没有关系。

6.5.2 内联函数和 constexpr 函数

调用函数比求等价表达式的值要慢一点
函数调用的工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可避免函数调用的开销

内联函数通常是将它在每个调用点上 ”内敛地“展开。

内联函数需要在 函数声明的最前面加入 ”inline“修饰符

  • 内敛机制用于优化规模较小,流程直接,频繁调用的函数,许多编译器都不支持内敛递归函数。
  • 内敛说明仅仅是向编译器发送的一个请求,编译器可选择忽略请求。

constexpr 函数

constexpr 函数是指能用于常量表达式的函数,定义 constexpr 函数的方法与其他函数类似,
不过要遵循几项约定:

  • 函数的返回类型及所有形参的类型都带是字面值类型。
  • 函数体必须有且仅有一条 return 语句。

把内联函数和 constexpr 函数放在头文件内

对于给定的内联函数或者 constexpr 函数来说,它的多个定义必须完全一致,因此,内联函数和 constexpr函数通常定义在头文件中。

6.5.3 调试帮助

当应用程序准备发布时,要先屏蔽掉调试代码
这种方法用到两项预处理功能: assert 和 NDEBUG

assert 预处理宏

assrt( expr)
对expr求值,如果表达式为假,assert 输出信息并终止程序的执行,如果表达式为真,则assert什么都不做

NDEBUG 预处理变量

assert 的行为依赖于一个名为 NDEBUG 的预处理变量,如果定义了 NDEBUG 则 assert 什么都不做,

我们可以利用 NDEBUG编写自己的测试代码:
如果NDEBUG未定义,则执行 #ifndef 与 #endif之间的代码,这些代码将被忽略掉

void print(const int ia[], size_t size) {#ifndef NDEBUGcerr << __func__ << ": array size is " << size << endl;
#endif // !NDEBUG}

预处理器定义了 4 个对于程序调试很有用的名字:

  • _ _ FILE_ _ 存放文件名的字符串字面值;
  • _ _ LINE_ _ 存放当前行号的整型字面值
  • _ _ TIME_ _ 存放文件编译时间的字符串字面值
  • _ _ DATE_ _ 存放文件编译日期的字符串字面值。

6.6 函数匹配

确定候选函数和可行函数

  1. 函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为 候选函数。
  2. 考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
    可行函数的特点:
  • 形参数量与本次调用提供的实参数量相等。
  • 实参的类型与对应的形参类型相同,或者能转换。
  • 如果函数中含有默认参数,则该函数虽然实参数量不够但也可能会是可行函数。

寻找最佳匹配

  1. 在可行函数中选择与本次调用最匹配的函数。(实参类型与形参类型越接近,匹配的越好)

含有多个形参的函数匹配·

编译器依次检测每一个实参以确定哪个函数是最佳匹配。如下条件:

  • 该函数每个实参的匹配 都不劣于其他可行函数需要的匹配。
  • 至少有一个实参的匹配优于其他可行函数提供的匹配。

如果检测到所有的函数都没有一个脱颖而出,编译器则报告二义性。

调用重载函数时应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

6.6.1 实参类型转换

为了完成精确匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下:

1.精确匹配

  • 实参类型与形参类型相同
  • 实参从数组类型或函数类型转换成对应的指针类型
  • 向实参添加顶层 const 或者从实参中删除顶层 const
  1. 通过 const 转换实现的匹配
  2. 通过类型提升实现的匹配
  3. 通过算术类型转换或指针转换实现的匹配
  4. 通过类类型转换实现的匹配。

函数匹配与 const 实参

底层 const 形参会优先匹配 常量实参
非const 实参 只会匹配非常量实参。

6.7 函数指针

函数指针指向的是函数而非对象,与其他指针一样,函数指针指向某种特定类型,函数的类型由它的返回类型 和形参类型共同决定,与函数名无关。

bool lengthCompare(const string &,const string &);
声明一个指向该函数的该函数指针只需要将函数名换成指针就行了。

bool (*pf)(const string &,const string &);

使用函数指针

  • 把函数名作为一个值使用时,该函数自动转化为指针,也可以将 函数的地址赋予指针,取地址符 & 是可选的。

( pf = lengthCompare; ) = ( pf = &lengthCompare;)

  • 指向不同函数类型的指针之间不存在相互转换规则,但是和往常一样,我们可以为函数指针赋予一个 nullptr 或者 值为 0 的整型常量表达式。

重载函数的指针

编译器通过指针类型决定选择哪个函数。指针类型必须与重载函数中的某一个精准匹配。

函数指针形参

  • 虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数类型,实际上却是当成指针使用。

返回指向函数的指针

  • 类型别名将返回值定义为函数指针 using PF = int (*)(int,int);
  • 尾后返回类型 auto f1(int) -> int (*)(int,int);

将 auto 和 decltype 用于函数指针类型

当我们将decltype作用于某个函数时,它返回 函数类型而非指针类型,因此,我们显式地加上 * 代表我们返回指针而非函数本身。

string::size_type sumLength(const string& , const string&);
decltype(sumLength) *getFcn(const string &);

第七关:类

类的基本思想是数据抽象(data abstraction)封装(encapsulation),数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。

类的接口包括用户所能执行的操作;类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数

封装实现了类的接口与实现的分离,封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。

类想要实现数据抽象 和 封装,需要先定义一个抽象数据类型(abstract data type)。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解类型的工作细节。

7.1 定义抽象数据类型

我们来实现一个 SaleData类,它目前并不是一个抽象数据类型,它允许用户访问它的数据成员,并且由用户来编写操作。

我们需要定义一些操作以供类的用户使用,之后我们封装(隐藏)它的数据成员,保证接口与实现分离,逐渐地完成数据抽象与封装,实现一个抽象数据类型。

7.1.1 设计 SalesData 类

SalesData 的接口应该包含以下操作:

  • 一个 isbn 成员函数,用于返回对象的 ISBN编号
  • 一个 combine 成员函数,将一个 SalesData 对象加到另一个对象上
  • 一个 名为 add 的函数,执行两个 SalesData 对象的加法。
  • 一个 read 函数,将数据从 istream 读入到 SalesData 对象中
  • 一个 print 函数,将 SalesData 对象的值输出到 ostream。

C++程序员无须刻意区分应用程序的用户以及类的用户。

在一些简单的应用程序中,类的用户和类的设计者常常是同一个人。尽管如此,还是最好把角色区分开来,当我们设计类的接口时,应该考虑如何才能使得类易于使用;当我们使用类时,不应该顾及类的实现机理。

要想开发一款成功的应用程序,其作者必须充分了解并实现用户的需求。同样,优秀的类设计者也应该密切关注那些有可能使用该类程序员的需求。作为一个设计良好的类,既要有直观且易于使用的接口,并且具备高效的实现过程。

7.1.2 定义改进的SalesData 类
#pragma once
#ifndef CPPPRIMER_SALESDATA_H_
#define CPPPRIMER_SALESDATA_H_#include <string>
#include "goolestyle.h"
namespace mynamespace {struct SalesData {// 新成员: 关于 SalesData 对象的操作std::string book_no() const { return book_no_; }SalesData& Combine(const SalesData &);double AvgPrice() const;std::string book_no_;unsigned units_sold_ = 0;double revenue_ = 0.0;DISALLOW_COPY_AND_ASSIGN(SalesData);
};SalesData Add(const SalesData &, const SalesData &);
std::ostream& Print(std::ostream &, const SalesData &);
std::istream &Read(std::istream &, SalesData &);
}
#endif // !CPPPRIMER_SALESDATA_H_
  • 非通用的函数应该属于类实现的一部分,而非接口的一部分
  • 定义和声明成员函数的方式与普通函数差不多,成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部。
  • 作为类的接口组成部分的非成员函数,add,read,print 他们的定义与声明都在类的外部。

定义成员函数

  • 类的所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。
  • 定义在类内部的函数是隐式的 inline 函数

引入 this

  • 在成员函数内部,我们可以直接调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,是因为 成员函数通过一个名为 this 的隐式参数来访问调用 它的那个对象。

  • 当我们调用一个成员函数时,用请求该函数的对象地址初始化 this。

引入 const 成员函数

std::string book_no() const { return book_no_; }

  • isbn 函数在参数列表后紧跟着一个 const 关键字,这里,const 的作用是修改隐-式 this指针的类型。
  • C++ 语言的做法是允许把 const 关键字放在成员函数的参数列表之后,此时紧跟在参数列表后面的const 表示 this 是一个指向 常量的指针。像这样使用 const 的成员函数被称作 常量成员函数
  • 常量对象,常量对象的引用或指针都只能调用常量成员函数。
  • 常量成员函数内部只能读取对象的数据成员,但是不能写入新值。

类作用域和成员函数

  • 编译器首先编译成员的声明,之后才轮到成员函数体(如果有),因此,成员函数体可以随意使用类中的其他成员,而无须在意这些成员出现的次序。

在类的外部定义成员函数

double mynamespace::SalesData::AvgPrice() const {if (units_sold_)return revenue_ / units_sold_;return 0.0;
}

类外部定义的成员的名字必须包含它所属的类名。

函数名 SalesData::AvgPrice使用作用域运算符来说明该函数被声明在 SalesData的作用域中,函数体内的代码的成员是位于类的作用域内的就不会出错。

定义一个返回 this 对象的函数

SalesData& SalesData::Combine(const SalesData &rhs) {units_sold_ += rhs.units_sold_;revenue_ += rhs.revenue_;return *this;
}
  • 一般来说,当我们定义的函数类似于某个内置运算符时,应该令函数的行为尽量模仿这个运算符。
    内置的赋值运算符把它的左侧运算对象当成左值返回。
7.1.3 定义类相关的非成员函数

一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类都在同一个头文件内。

定义 Read函数与 Print 函数

  • IO类属于不能被拷贝的类型,因此我们只能通过引用来传递他们。
  • Print 函数不设置换行,将主动权尽量交给用户来执行。
std::ostream &Print(std::ostream &os, const SalesData &item) {os << item.book_no() << " " << item.units_sold_ << " "<< item.revenue_ << " " << item.AvgPrice();return os;
}std::istream& Read(std::istream &is, SalesData &item){double price = 0;is >> item.book_no_ >> item.units_sold_ >> price;item.revenue_ = price * item.units_sold_;return is;
}

定义 Add 函数

SalesData Add(const SalesData &lhs, const SalesData &rhs) {SalesData sum = lhs;sum.Combine(rhs);return sum;
}
  • 返回 sum (合并的副本)。
  • 未加入 iostream 头文件的话 unsigned 一些变量的类型会识别为未定义重载运算符。
7.1.4 构造函数

类通过一个或多个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。

  • 构造函数的任务是初始化类对象的数据成员,只要类对象被创建,就会执行构造函数
  • 构造函数的名字与类名相同,构造函数没有返回类型
  • 构造函数也有一个(可能为空的)参数列表与一个(可能为空的)函数体。
  • 类可以包含多个构造函数,和其他重载函数差不多,不同构造函数之间必须在参数数量或参数类型上有所区别。
  • 构造函数不能声明为 const,因为我们创建类的 const对象时,直到构造函数完成初始化过程,对象才能真正取得 ”常量“属性。

合成的默认构造函数

类可以通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。

如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐士地定义一个默认构造函数。
编译器创建的构造函数又被称为 合成的默认构造函数,它以以下规则初始化类的数据成员:

  • 如果存在类内的初始值,让其初始化成员
  • 否则,执行默认初始化该成员。

某些类不能依赖于合成的默认构造函数

原因有三:

  1. 编译器只有在发现类不包含任何构造函数的情况下才会替我们生产一个默认的构造函数
  2. 合成的默认构造函数可能执行错误的操作,如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
  3. 有时候编译器不能为某些类合成默认的构造函数,如果类内包含其他类类型成员,但这个类类型成员没有默认构造函数,那么编译器则无法初始化该成员。

定义SalesData 的构造函数

我们定义 4 个不同的构造函数

  • 一个 istream&,从中读取一条交易信息。
  • 一个 const string& ,表示 ISBN编号,一个unsigned,表示出售的图书数量;以及一个 double,表示图书的售出价格。
  • 一个 const string&,表示ISBN编号,编译器将赋予其他成员默认值。
  • 一个空参数列表(即默认构造函数)
#pragma once
//Copyright 2020
//License (BSD/GPL/...)
//Author: Handling
//This is CPPPrimer Study           #ifndef CPPPRIMER_SALESDATA_H_
#define CPPPRIMER_SALESDATA_H_#include <string>
#include <iostream>
namespace mynamespace {struct SalesData {// 新成员: 关于 SalesData 对象的操作/*1.typedef 和 enums2.常量3.构造函数4.析构函数5.成员函数,含静态数据成员6.成员变量,含静态成员变量*/SalesData() = default;SalesData(const std::string &s): book_no_(s) { }SalesData(const std::string &book_no, unsigned unit_sold, double price) :book_no_(book_no), units_sold_(unit_sold), revenue_(price * unit_sold) { }SalesData(std::istream &);std::string book_no() const { return book_no_; }SalesData& Combine(const SalesData &);double AvgPrice() const;std::string book_no_;unsigned units_sold_ = 0;double revenue_ = 0.0;DISALLOW_COPY_AND_ASSIGN(SalesData);
};SalesData Add(const SalesData &, const SalesData &);
std::ostream& Print(std::ostream &, const SalesData &);
std::istream &Read(std::istream &, SalesData &);
}
#endif // !CPPPRIMER_SALESDATA_H_
  • C++ 11 新标准中,我们需要默认的行为,就可以在参数列表后面写上 = default 来要求编译器生产构造函数。
  • 其中 = default 如果出现在类的内部,代表默认构造函数是内联的。
  • 上面的默认构造函数之所以对SalesData 有效,是因为我们为内置类型的数据成员提供了初始值,如果编译器不支持类内初始值,则需要使用 构造函数初始化列表来初始化每一个成员。

构造函数初始化列表

  SalesData(const std::string &s): book_no_(s) { }SalesData(const std::string &book_no, unsigned unit_sold, double price) :book_no_(book_no), units_sold_(unit_sold), revenue_(price * unit_sold) { }

构造函数初始值是成员名字的一个列表,每个名字紧跟括号括起来的成员初始值,不同成员初始化通过逗号分隔开。

  • 构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同,如果你不能使用类内初始值,则所有构造函数都应该显式地初始化每一个内置类型的成员。

在类的外部定义构造函数

  SalesData::SalesData(std::istream &is) {Read(is,*this);}

当作用域与函数命字相同时,说明该函数是构造函数。

7.1.5 拷贝,赋值,析构

类除了初始化外,类还需要 控制拷贝,赋值,销毁对象时发生的行为。

一般来说,编译器生成的版本将对对象的每一个成员执行拷贝,赋值和销毁操作。

某些类不能依赖于合成的版本

管理动态内存的类通常是不能依赖上述操作的合成版本,会造成内存问题。

7.2 访问控制与封装

我们已经为类定义了接口,但还没有机制强制用户使用这些接口,我们的类还没有封装,用户可以直达对象内部控制它的具体实现细节。

在C++中,我们使用 访问说明符 加强类的封装性。

  • 定义在 public 说明符之后的成员在整个程序内可被访问, public 成员定义类的接口
  • 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了(即隐藏了)类的具体实现细节。
  • 每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者直到达类的结尾为止。
class SalesData {// 新成员: 关于 SalesData 对象的操作/*1.typedef 和 enums2.常量3.构造函数4.析构函数5.成员函数,含静态数据成员6.成员变量,含静态成员变量*/public:SalesData() = default;SalesData(const std::string &s): book_no_(s) { }SalesData(const std::string &book_no, unsigned unit_sold, double price) :book_no_(book_no), units_sold_(unit_sold), revenue_(price * unit_sold) { }SalesData(std::istream &);std::string book_no() const { return book_no_; }SalesData& Combine(const SalesData &);private:double AvgPrice() const{ return units_sold_ ? revenue_/units_sold_ : 0; }std::string book_no_;unsigned units_sold_ = 0;double revenue_ = 0.0;DISALLOW_COPY_AND_ASSIGN(SalesData);
};

使用 struct 或者 class关键字

struct 和 class 唯一的区别就是 默认的访问权限不同。

如果我们使用 struct 关键字,则定义在第一个访问说明符之前的成员是 public的,相反,如果我们使用的是 class 关键字,则这些成员是 private 的。

  • 如果我们希望定义的类的所有成员是 public的时候,使用 struct;反之 如果我们希望成员是 private,使用 class;
7.2.1 友元

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数称为它的友元。

  • 如果类想把一个函数作为它的友元,只需要增加一条以friend 关键字开始的函数声明语句即可。
  • 友元不是类的成员也不受它所在区域访问说明符控制级别的约束,一般在类定义开始或结束前的位置集中声明友元。
#pragma once
//Copyright 2020
//License (BSD/GPL/...)
//Author: Handling
//This is CPPPrimer Study           #ifndef CPPPRIMER_SALESDATA_H_
#define CPPPRIMER_SALESDATA_H_#include <string>
#include <iostream>
#include "goolestyle.h"
namespace mynamespace {class SalesData {// 新成员: 关于 SalesData 对象的操作/*1.typedef 和 enums2.常量3.构造函数4.析构函数5.成员函数,含静态数据成员6.成员变量,含静态成员变量*/friend SalesData Add(const SalesData &, const SalesData &);friend std::ostream &Print(std::ostream &, const SalesData &);friend std::istream &Read(std::istream &, SalesData &);public:SalesData() = default;SalesData(const std::string &s): book_no_(s) { }SalesData(const std::string &book_no, unsigned unit_sold, double price) :book_no_(book_no), units_sold_(unit_sold), revenue_(price * unit_sold) { }SalesData(std::istream &);std::string book_no() const { return book_no_; }SalesData& Combine(const SalesData &);private:double AvgPrice() const{ return units_sold_ ? revenue_/units_sold_ : 0; }std::string book_no_;unsigned units_sold_ = 0;double revenue_ = 0.0;DISALLOW_COPY_AND_ASSIGN(SalesData);
};SalesData Add(const SalesData &, const SalesData &);
std::ostream& Print(std::ostream &, const SalesData &);
std::istream &Read(std::istream &, SalesData &);
}
#endif // !CPPPRIMER_SALESDATA_H_

封装有两个重要的优点:

  • 确保用户代码不会无意间破坏封装对象的状态
  • 被封装的类的具体实现细节可以随时改变,则无须调整用户级别的代码。

友元的声明

  • 友元的声明仅仅指定了访问的权限,我们必须在友元声明之外在专门对函数进行一次声明。

7.3 类的其他特性

令成员作为内联函数

  • 定义在类内部的成员函数是自动 inline 的
  • 我们可以在类定内部把 inline 作为声明的一部分显式地声明成员函数,我们也能在 类的外部用 inline关键字修饰函数的定义。
  • 我们最好只在类外部定义的地方说明 inline,这样可以使类更容易理解
  • inline 成员函数也应该与相应的类定义在同一个文件中。

重载成员函数

可变数据成员

我们希望能修改类的某个数据成员,即使是在一个const 成员函数中,我们可以在变量的声明中 加入
mutable 关键字做到这一点

一个可变数据成员永远不会 是 const,即使它是 const 对象的成员。

类数据成员的初始值

在C++ 11 新标准中,最好的方式是把默认值声明为 类内初始值。

当我们提供一个类内初始值时,必须用花括号或者 = 表示。

7.3.2 返回 *this 的成员函数
  • 返回 *this 的成员函数返回类型引用的话,则返回的是左值,是对象本身,反之是对象的副本。

*从 const 成员函数返回 this

一个 const 成员函数如果以引用的方式返回*this,那么他从的返回类型是常量引用。

基于const 的重载

判断是否该函数为常量成员函数:

  • 常量对象调用非常量版本的函数是不可用的,因此我们只能在一个常量对象上调用 const 成员函数。
  • 非常量对象调用常量函数与非常量函数,最佳匹配是非常量成员函数。

建议:对于公共代码使用私有功能函数。

公共代码定义成一个单独的函数,是为了在实践中,重复调用这些函数,完成一组一组其他函数的“ 实际”工作。

7.3.3 类类型
  • 即使两个类的成员列表完全一致,他们也是不同的类型。对于一个类来说,他们的成员与其他任何类(其他作用域)的成员都不是一回事。

类的声明

  • 在类定义前进行声明是前向声明,在声明之后定义之前是一个不完全的类型,只知道其是一个类类型,但不知道包含那些成员。

不完全类型的使用情形:

  • 可以定义指向这种类类型的指针或引用,也可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数。

对于一个类来说,创建其对象之前必须被定义过,而不呢仅仅声明,不清楚其存储空间大小。

7.3.4 友元再探

类可以将其他的类定义成友元,也可以把其他类(已经定义过的)的成员函数定义成友元。
此外友元函数定义在类的内部,这样的函数是隐式内联的。

类之间的友元关系

  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非共有成员在内的所有成员。
  • 友元不具有传递性,每个类只负责控制自己的友元类或友元函数。

令成员函数作为友元

假设 A 为 B 的 func函数提供友元访问权限。

  • 首先定义 B类,其中声明 func 函数,但不能定义它。在 func使用 A类的成员之前必须先声明
    A类。
  • 定义 A 类,包括对于 func 的友元声明
  • 最后定义 func 此时它才可以使用 Screen 的成员。

函数重载与友元

对重载函数声明友元,仍然需要每个单独声明。

友元声明和作用域

  • 友元本身不一定真的声明在当且作用域中,就算在类的内部中定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。
  • 有些编译器并不强制 上述的友元限定规则。

7.4 类的作用域

作用域和定义在类外部的成员

  • 在类的外部,成员的名字被隐藏起来了,一旦遇到了类名,定义的剩余部分就在类的作用域中
  • 函数的返回类型通常出现在函数名之前,定义在类外部的函数,返回类型在类的作用域之前。
  • 如果想用类内部定义的返回类型作为在类外部定义的成员函数的返回类型,需要在返回类型前指明哪个类定义了它。
7.4.1 名字查找与类的作用域

名字查找(寻找与所用名字最匹配的声明的过程)

  • 首先,在名字所在的块中寻找其声明语句,只考虑名字的使用之前的声明
  • 如果没找到,继续查找外层作用域
  • 如果最终没有找到匹配的声明,程序报错

类的定义分两步处理:

  • 编译成员的声明
  • 知道类全部可见才编译函数体。

类型名要特殊处理:

类型名的定义通常出现在类的开始处,这样能确保所有使用该类型的成员都出现在类名的定义之后。

成员定义中的普通块作用域的名字查找

  • 首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。

  • 如果在成员函数中没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。

  • 如果类内也没找到该名字的声明,在成员函数定义之前的作用域继续查找。

  • 不建议将局部变量的名字与成员的名字重复

  • 我们可以显式地使用 this 指针强制访问成员。

类作用域之后,在外围的作用域查找

尽管外层的对象被隐藏掉了,但我们可以用作用域运算符 (::)访问它。
cursor = width * (::height);

在文件中名字的出现处对其进行解析

  • 如果类内也没找到该名字的声明,在成员函数定义之前的作用域继续查找。

7.5 构造函数再探

7.5.1 构造函数初始值列表
  • 以块内赋值操作初始化 类内成员的构造函数 与 构造函数初始值列表初始化 的区别完全依赖于数据成员的类型。

构造函数的初始值有时必不可少

如果成员是 const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

建议使用构造函数初始值

当有的类含有需要构造函数初始值的成员时,使用构造函数初始值能避免意想不到的编译错误。

成员初始化的顺序

  • 构造函数初始值列表只说明用于初始成员的值,而不限定初始化具体执行顺序。
  • 成员的初始化顺序与他们在类定义的出现顺序一致,第一个成员先被初始化,然后第二个,依次类推。

最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。

默认实参和构造函数

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

  SalesData(const std::string &s = ""): book_no_(s) { }
7.5.2 委托构造函数

C++ 11 新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它的一些职责委托给了其他构造函数。

SalesData(const std::string &book_no, unsigned unit_sold, double price) :book_no_(book_no), units_sold_(unit_sold), revenue_(price * unit_sold) {  }SalesData() : SalesData("",0,0) {}SalesData(const std::string &s) : SalesData(s,0,0) {}SalesData(std::istream &is) : SalesData() { Read(is,*this); }

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
在SalesData类中,受委托的构造函数体恰好是空的,如果有代码的话,先执行受委托的函数体代码,再把控制权交还给委托者的函数体。

7.5.3 默认构造函数的作用

当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生

  • 当我们在块作用域内不使用任何初始值定义一个 非静态变量或者数组时。
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时
  • 当类类型的成员没有在构造函数初始值列表中显式地初始化时。

值初始化:

  • 在数组初始化的过程如果我们提供的初始值数量小于数组的大小

  • 我们不使用初始值定义一个局部静态变量时

  • 当我们书写 T() 的表达式要求显式地进行值初始化时。

  • 在实际开发中,如果定义了其他构造函数,最好也提供一个默认的构造函数。

使用默认构造函数

SaleData obj;//默认初始化,调用默认构造函数
SaleData obj() ;//声明了一个返回值为 SaleData 的 函数

7.5.4 隐式的类类型转换

如果一个类只接受一个实参,则它定义了转换为此类类型的隐士转换机制,我们把这种构造函数称作转换构造函数。

  SalesData& Combine(const SalesData &lhs);SalesData(const std::string &s) : SalesData(s, 0, 0) {}string  null_book = "9-9999";
item.Combine(null_book);

我们可以直接使用 一个实参的构造函数来隐式转换 为类类型。

只允许一步转换

item.Combine("9-9999");  //这是不对的

我们隐式地把 字面值转换为 常量字符串,之后再隐式地转换为类类型,这时不被允许的

类类型转换不是总有效

是否要从一个 构造函数实参类型转换为 类类型取决于用户使用该转换的看法,该隐式转换的数据可能不符合条件,也可能会正确!!!

抑制构造函数定义的隐式转换

  • 关键字 explict 只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无须指定 explicit。

  • 只能在类内声明构造函数时 使用 explicit 关键字,在类外部定义时不应重复。

  • 当我们用 explicit 关键字声明构造函数时,它将只能以直接初始化的方式使用,拷贝初始化是不被允许的。而且,编译器将不会在自动转换过程中使用该构造函数。

  explicit SalesData(const std::string &s) : SalesData(s, 0, 0) {}explicit SalesData(std::istream &is) : SalesData(){ Read(is,*this); }

为转换显式地使用构造函数

我们可以直接使用 类的单参数构造函数接收 单参数完成显式构造。

item.combine (Sales_data(null_book));

标准库中含有显式构造函数的类

  • 接受单参数的 const char * 的string 构造函数不是 explicit
  • 接受一个容量参数的 vector 构造函数是 explicit 的。
7.5.5 聚合类

聚合类 使得用户可以直接访问成员,并且具有特殊的初始化语法形式。
当一个类满足如下条件时,我们说它是聚合的:

  • 所有成员都是 public的
  • 没有定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有 virtual函数
struct Data {int ival;std::string s;
};

Data val = {0, “abcd”};

  • 聚合类的初始值的顺序必须与声明的顺序一致。
  • 如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化,数量不能超过类的成员数量

显式地初始化类的对象的成员存在 3个明显的缺点:

  • 要求类的成员是 public ,不符合封装
  • 将正确初始化每个对象的每个成员的重任交给了类的用户(而非类的作者)。因为用户很容易忘记某个初始值,或者提供一个不恰当的初始值,所以这样的初始化的过程乏味容易出错。
  • 添加删除一个成员,所有的初始化语句都需要更新。

7.5.6 字面值常量类

数据成员都是字面值类型的聚合类是字面值常量类。

复合下面的要求也是字面值常量类:

  • 数据成员全部是字面值类型
  • 类至少含有一个 constexpr 构造函数
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;如果成员属于类类型,则初始值必须用自己的 constexpr 构造函数
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

constexpr构造函数

  • constexpr构造函数 必须初始化所有数据成员,使用初始值或者 constexpr 的构造函数吗,或常量表达式
  • constexpr 构造函数用于生产constexpr对象以及 constexpr函数的参数或返回类型。
class Debug {public:constexpr Debug(bool b = true): hw_(b), io_(b), other_(b){ }void set_io(bool b) { io_ = b; }void set_hw(bool b) { hw_ = b; }void set_other(bool b) { other_ = b; }private:bool hw_;bool io_;bool other_;
};

7.6 类的静态成员

类需要它的一些成员与类本身有直接的关系,但不是与类定各个对象都保持联系。

这时候我们将其 声明为 类的静态成员

声明静态成员

  • 我们通过在成员的声明之前加上关键字 static 使其与类关联在一起。静态成员可以是 public,private,数据类型可以是常量,引用,指针或者类类型。
  • 类的静态成员存在于任何对象之间,对象中不包含任何于静态数据成员有关的数据。
  • 静态成员函数不与任何对象绑定在一起,不包含 this指针,不能声明其const静态成员函数。

定义静态成员

  • 与类成员一样,类外部的静态成员必须指明成员所属的类名,static 关键字则只出现在类内部的声明语句中。
  • 我们必须在类的外部定义和初始化每个静态成员,且只能被定义一次,与非内联的函数定义放在同一文件内部。
  • 静态数据成员定义在任何函数之外,因此它将一直存在于程序的整个生命周期中。
  • 静态类数据成员也能访问类内定义的所有的数据成员。

静态成员的类内初始化

  • 我们可以为静态成员提供 const 的类内初始值,不过要求静态成员必须是字面值常量类型的 constexpr。
  • 即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义以下该成员,目的是防止其值不能替换的场景。
  static constexpr char period = '1';constexpr char SalesData::period;

静态成员能用于某些场景,而普通成员不能

  • 静态数据成员可以是 不完全类型。比如:自身类类型,但是非静态成员则受到限制,只能声明成它所属类的指针或引用。
  • 非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法提供一个对象以便从中获取成员的值,最终引发错误。但是静态成员是可以作为默认实参的。

第八关:IO库

8.1 IO类

IO库了与头文件
头文件 类型
iostream istream,wistream 从流中读取数据
ostream,wostream 向流写入数据
iostream,wiostream 读写流
fstream ifstream,wifstream 从文件读取数据
ofstream,wofstream 向文件写入 数据
fstream,wfstream 读写文件
sstream istringstream,wistringstream 从 string 读取数据
ostringstream,wostringstream 向string 写入数据
stringstream,wstringstream读写string
  • 为了支持宽字符的语言,编制看定义了一组类型和对象来操纵 wcahr_t 类型的数据。
  • 宽字符版本的类型和函数的名字以一个 w 开始。

IO 类型间的关系

标准库使我们能忽略这些不同类型的流的差异,这都是由继承机制实现的,
利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。

8.1.1 IO对象无拷贝或赋值
  • 不能以形参或者返回类型设置为流类型,进行 IO 操作的函数通常以引用方式传递和返回流。
  • 读写一个 IO 对象会改变其状态,因此传递和返回的引用不能是 const 的。
8.1.2 条件状态

IO 类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态

IO库条件状态
stm::iostate srm 是一种 IO类型,iostate 是一种机器相关的类型,提供了表达条件状态的完整功能
strm::badbit strm::badbit 用来指出流已经崩溃
strm::failbit strm::failbit 用来指出一个 IO 操作失败了
strm::eofbit strm::eofbit 用来指出流已

一小时复习完C++Primer!!!相关推荐

  1. 计算机二级OFFICE55小时复习攻略

    今天准备和大家分享一下<计算机二级OFFICE 55小时复习法> 此方法,让有的同学3天通过考试, 有的7天通过考试,更有同学11天从小白拿到了优秀(篇幅有限,截图就不上了). 在此方法出 ...

  2. 半小时复习java全内容

    半小时复习Java全内容 来都来了点个赞呗 o(*≧▽≦)ツ 这段时间要急着考试的同学,可以看我画的重点,目录上有标识,如果时间充裕也可以详细看下去,会很有帮助的.我会用视频加图画来解释.这篇文章中, ...

  3. 看完c++ primer之后看什么

    看完c++ primer之后看什么 不得不说,C++的好书太多了,都不知道从哪个开始看起,而且有些书的内容都差不多,所以不一定每本都看,谁能给我发个看书的顺序,循序渐进的(有些好书我可能没提到,可以补 ...

  4. 2021天梯赛L1-074 两小时学完C语言 题解

    L1-074 两小时学完C语言 (5 分) 题目: 知乎上有个宝宝问:"两个小时内如何学完 C 语言?"当然,问的是"学完"并不是"学会". ...

  5. 功能测试包含哪些测试_一小时复习,期末考试必过 重邮软件测试题总结

    这是我复习一晚上边玩手机边复习的结果 成绩 复习重点 一些选择题和简答题可能需要的 软件测试的概念 软件测试是一组活动,目的是发现程序中潜在的错误,通过测试用例输入和输出结果,观察实际运行结果与期望的 ...

  6. Kaggle官网免费课程:从Python到机器学习,4小时学完一门,48小时掌握数据科学...

    点击我爱计算机视觉标星,更快获取CVML新技术 赖可 发自 凹非寺 量子位 报道 | 公众号 QbitAI 听说过Kaggle官网的免费"微课"吗? 想学Python .机器学习. ...

  7. Oracle 12c 能否在2小时内完成一张14亿条记录的表结构字段类型变更

    原文链接:https://www.modb.pro/db/22757 概述 前面分享过Oracle大表在线修改的脚本(在线重定义),经过几轮的测试发现,都存在些缺陷,效率始终不是很满意.这次把索引和统 ...

  8. L1-074 两小时学完C语言 (5 分)-PAT 团体程序设计天梯赛 GPLT

    知乎上有个宝宝问:"两个小时内如何学完 C 语言?"当然,问的是"学完"并不是"学会". 假设一本 C 语言教科书有 N 个字,这个宝宝每分 ...

  9. 3小时做完3天工作,她是用了什么办法做到的?

    用户案例 场景:销售统计 用户:威高医用制品集团公司-销售计划主管 关于威高集团 威高集团始建于1988年,以一次性医疗器械和药业为主业.2014年8月,全国工商联公布了2014中国民营企业500强. ...

  10. 如何借助SVG+CSS用2个小时撸完一个网易云音乐的动效海报(可控制速度)

    因为平时也关注网易UEDC的订阅号,前几天就看到了这么一个动效,主题是<网易云音乐2018年度听歌报告>,内容是一个人在努力蹬车因为构图简单,创意又不错,所以就试了下用SVG+CSS动画实 ...

最新文章

  1. ASP.NET MVC3细嚼慢咽---(2)模板页
  2. solidworks activator未响应_SolidWorks之初识工程图
  3. 二叉堆(最小堆)(数据结构与算法分析的代码实现)
  4. html 像素跟百分比,html – 将百分比宽度与边距(以像素为单位)组合起来
  5. 使用计算机正确开机方法,电脑开关机的正确步骤
  6. plsql提示列快捷键_PLsql快捷键
  7. 一步一步写算法(之哈夫曼树 上)
  8. Centos7.0 Vmware10.0.3 网络桥接配置
  9. vnpy通过jqdatasdk初始化实时数据及历史数据下载
  10. 后端传输流跨域_Java开发中解决Js的跨域问题过程解析
  11. C++ 实数和二进制操作入门
  12. Emmagee工具的使用以及csv数据分析
  13. HCNP——DR和BDR的概念
  14. 看我是如何严辞拒绝同学借钱的
  15. 微信小程序视频URL测试地址 MP4格式
  16. 考初级计算机证需要考什么,计算机初级证书要考哪些内容
  17. 2017京东春招实习生招聘编程题
  18. 算法中的一些数学问题分享,ICG游戏
  19. RTT学习笔记7-中断管理
  20. 数据库(Mysql)概述

热门文章

  1. 如何搞定团队中的野狗、兔子、黄牛?--基于员工画像的领导力法则
  2. 情绪版(Mood board)---衣服如何搭配的好工具
  3. java怎么自己写代码,HR的话扎心了
  4. 2006年度中国纳税百强出炉
  5. 【计算机毕业设计】微信小程餐厅点餐系统
  6. 现在世界上有多少种操作系统?简要介绍
  7. 国内市场有哪些T+0基金可以买卖? | T+0基金
  8. FPGA SDRAM接口设计(四)板级验证
  9. 44家苹果供应商承诺使用清洁能源生产苹果产品 数量翻一倍
  10. Android面临困境:系统现碎片化nbsp;开…