C++Primer 通关
-
- 工程代码链接,求小星星,谢谢 ⭐
- 第一关:C++Primer 的了解
- 第二关:基本内置类型与变量
- 第三关:字符串、向量和数组
- 第四关:表达式
- 第五关:语句
- 第六关: 函数
- 第七关:类
- 第八关:IO库
- 第九关:顺序容器
- 第十关:泛型算法
- 第十一关:关联容器
- 第十二关: 动态内存
- 第十三关: 拷贝控制
- 13.1 拷贝,赋值与销毁
- 第十四关: 重载运算与类型转换
- 第十五关 . 面向对象程序设计
- 第 十六关: 模板与泛型编程
- 第 十七 关: 标准库特殊设施
- 第十八关: 用于大型程序的工具
- 第十九关: 特殊工具与技术
工程代码链接,求小星星,谢谢 ⭐
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 指针
指针是指向另一种类型的符合类型。与引用类似,指针也实现了对其他对象的间接访问。
指针与引用的不同之处:
- 指针本身就是对象,允许对指针进行赋值与拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指着无须在定义时赋与初始值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也拥有一个不确定的值。
- 与引用类似,除特殊情况外(后面会提到)指针的类型都要与它所指向的对象严格匹配。
指针值(即地址)应属于下列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 指针
由于指针本身就是对象,也允许把指针本身定位常量。
- 常量指针必须被初始化,一旦初始化完成,(存放在指针中的那个地址)就不能被改变了。
- 书写: int *const cur_err = &err_numb; // const指针将一直指向 err_numb
- 常量指针并不意味着不能通过指针来修改其所指向对象的值,能否这样做完全依赖与所指对象的类型,只是自己不能改变自己的指向而已。
顶层 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 处理类型
程序越来越复杂,程序中用到的类型也越来越复杂,
- 类型难以“拼写”
- 搞不清需要的类型到底是什么
类型别名
类型别名是为了让复杂的类型名字变得清晰明了,利于理解与使用。
-
使用 typedef‘
typedef double wages; // wages是double的同义词
typedef wages base,p ; //base 是double的同义词,p 是 doule的同义词 -
新标准规定的新的方法,使用别名声明(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 自定义数据结构
注意点:
- 类定义的结尾最后加上分号
- 最好不要把对象的定义和类的定义放在一起,对实体的定义混淆为一条语句中,这时不被推荐的。
类数据成员
- 我们的类只有数据成员,类的数据成员定义了类的对象的具体内容,每个对象都各自有自己的一份数据成员拷贝。
- 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++ 语言提供了几种不同的初始化方法,在大多数情况下这些初始化方式能相互等价的使用,不过也并非一直如此。
- 使用拷贝初始化时,(即使用 = 时),只能提供一个初始值。
- 如果提供的是一个类内初始值,,则只能使用拷贝初始化或者花括号的形式初始化。
- 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在 圆括号里。
创建指定数量的元素
可以用 vector 对象容纳的元素数量 和 所有元素的统一初始值来初始化 vector 对象。
vector num_vec(10,-1);
值初始化
如果只提供 vector 对象容纳的元素数量而忽略其初始值,库会创建值初始化的元素初值,这个初值由 vector对象中元素的类型决定。
对这种初始化的方式有两个特殊限制:
- 有些类要求必须明确地提供初始值,如果 vector 对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值,对于这种类型的对象来说,只提供元素的数量不提供初始值就无法完成初始化工作。
- 如果只提供了元素的数量而没有设定初始值,只能使用直接初始化: 以()的方式。
列表初始值还是元素数量?
一方面情况:初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。
- 如果用的是圆括号,可以说提供的值是用来构造 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 |
| 是位或运算
注意:位运算符处理运算对象的 ”符号位“依赖于机器。而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。
强烈建议仅将位运算符用于处理无符号类型。
移位运算符
移位运算符是执行二进制位的移动操作,左侧对象按照右侧运算对象的值要求移动位数,然后将经过移动的左侧运算对象的拷贝作为求值结果。(右侧对象不能为负值)
- 二进制位移位后,移出边界的位就被舍弃掉。
- 左移运算符 << 在右侧插入值为 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 算术转换
算术转换的含义是把一种算术类型转换成另外一种算术类型。
整型提升
- 整型提升负责把小整数类型转换成较大的整数类型。(short ,char ->转换为 int)
- 较大的 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 数组形参
- 不能拷贝数组以及使用数组时通常会将其转换为指针。
- 以数组作为实参的函数也必须确保使用数组不会越界。
管理指针形参
- 使用标记指定数组长度(c风格字符串以 空字符停止’\0’)
- 使用标准库规范,传递执行数组首元素和尾后元素的指针。
- 显式地传递一个表示数组大小的形参。
数组形参与 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新标准提供了两种主要的方法
- 实参类型相同,传递给 initializer_list 的标准库类型
- 实参类型不同,编写另外的一种特殊函数,可变参数模板
initializer_list
initializer_list<T> lst; 默认初始化:T类型元素的空列表
initializer_list<T> lst{
a,b,c....} lst的元素数量与初始值一样多,lst的元素都是对应初始值的副本,
元素均为const
lst2(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 NDEBUG
cerr << __func__ << ": array size is " << size << endl;
#endif // !NDEBUG
}
预处理器定义了 4 个对于程序调试很有用的名字:
- _ _ FILE_ _ 存放文件名的字符串字面值;
- _ _ LINE_ _ 存放当前行号的整型字面值
- _ _ TIME_ _ 存放文件编译时间的字符串字面值
- _ _ DATE_ _ 存放文件编译日期的字符串字面值。
6.6 函数匹配
确定候选函数和可行函数
- 函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为 候选函数。
- 考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。
可行函数的特点:
- 形参数量与本次调用提供的实参数量相等。
- 实参的类型与对应的形参类型相同,或者能转换。
- 如果函数中含有默认参数,则该函数虽然实参数量不够但也可能会是可行函数。
寻找最佳匹配
- 在可行函数中选择与本次调用最匹配的函数。(实参类型与形参类型越接近,匹配的越好)
含有多个形参的函数匹配·
编译器依次检测每一个实参以确定哪个函数是最佳匹配。如下条件:
- 该函数每个实参的匹配 都不劣于其他可行函数需要的匹配。
- 至少有一个实参的匹配优于其他可行函数提供的匹配。
如果检测到所有的函数都没有一个脱颖而出,编译器则报告二义性。
调用重载函数时应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
6.6.1 实参类型转换
为了完成精确匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下:
1.精确匹配
- 实参类型与形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型
- 向实参添加顶层 const 或者从实参中删除顶层 const
- 通过 const 转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配。
函数匹配与 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对象时,直到构造函数完成初始化过程,对象才能真正取得 ”常量“属性。
合成的默认构造函数
类可以通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。
如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐士地定义一个默认构造函数。
编译器创建的构造函数又被称为 合成的默认构造函数,它以以下规则初始化类的数据成员:
- 如果存在类内的初始值,让其初始化成员
- 否则,执行默认初始化该成员。
某些类不能依赖于合成的默认构造函数
原因有三:
- 编译器只有在发现类不包含任何构造函数的情况下才会替我们生产一个默认的构造函数
- 合成的默认构造函数可能执行错误的操作,如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
- 有时候编译器不能为某些类合成默认的构造函数,如果类内包含其他类类型成员,但这个类类型成员没有默认构造函数,那么编译器则无法初始化该成员。
定义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 和 enums
2.常量
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 和 enums
2.常量
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 和 enums
2.常量
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 用来指出流已经到达了文件的结束 |
strm::goodbit | strm::goodbit 用来指出流未处于错误状态。此值保证为 零 |
s.eof() | 若流 s 的eofbit置位,则返回 true |
s.fail() | 若流 s 的failbit置位或 badbit置位,则返回 true |
s.bad() | 若流 s 的badbit置位,则返回 true |
s.good() | 若流 s 处于有效状态,则返回 true |
s.clear() | 将流 s 的所有条件状态位复位,将流的状态设置位有效,返回 void |
s.clear(flags) | 根据给顶的 falgs标志位,将流 s 中对应条件状态位复位。flags 的类型为 strm:;iostate.返回void |
s.setstate(flags) | 根据给定的 flags 标志位,将流 s 中对应条件状态位置位。flags 的类型位 strm::iostate.返回void。 |
s.rdstate() | 返回 流的当前条件状态,返回值位 strm::iostate |
- 确定一个流对象状态的简单办法就是将其当作一个条件来使用
if(strm >> word)
查询流的状态
stm::iostate 是与IO定义的与机器无关的类型,它提供了表达流状态的完整功能,它是一个位集合。
- 可以使用位运算符一起来使用来一次性检测或设置多个标志位。
- 如果 badbit failbit eofbit 任意一个被置位,则检测流状态的条件都会失败。
- 使用 good 或 fail 是确定流总体状态的正确方法。
管理流的状态
auto old_state = cin.rdstate(); //返回当前流的状态
cin.clear(); //清除所有错误标志位。调用 good 会返回true
cin.setstate(old_state); //设置流的状态
8.1.3 管理输出缓冲
每个输出流都管理着一个缓冲区,用来保存程序读写的数据,有了缓冲的机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作,会带来很大的性能提升。
导致缓冲刷新(即,数据真正写到输出设备或文件),原因有很多:
- 程序正常结束,作为 main 函数的return操作的一部分,缓冲刷新被执行。
- 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
- 我们可以使用操作符入 endl 来显式地刷新缓冲区
- 在每个输出操作之后,可以利用 unitbuf 设置流的内部状态,来清空缓冲区,默认情况
对 cerr 是设置 unitbuf 的,因此写到 cerr 的内容都是立即刷新的。 - 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新,默认情况下,cin 和 cerr 都关联到 cout ,因此读写 cin 或 写 cerr 都会导致 cout 的缓冲区刷新。
刷新输出缓冲区
-
flush 操纵符 刷新缓冲区,但不输出任何的字符
cout << flush; -
ends 操作符 输出 一个空字符,刷新缓冲区
unitbuf 操纵符
使用 unitbuf 操作符会告诉接下来的每一次写操作之后都会进行一次 flush 操作。而 nounitbuf 操纵符会
重置流,使其恢复之前的缓冲机制。
cout << std::unitbuf;
cout << std::nounitbuf;
如果程序崩溃,输出缓冲区不会被刷新
当一个程序崩溃时,它输出的数据很可能停留在缓冲区中等待打印。
关联输入和输出流
tie函数能使 输入流关联到一个输出流,接受一个流的地址,返回值位 指向这个流的指针,如果对象未关联到流,则返回空指针
ostream *old_tie1 = cin.tie(); //返回当且关联的输出流指针
ostream *old_tie2 = cin.tie(nullptr); //cin不再与其他输出流关联,返回的是之前关联到 cin 的流
cin.tie(&cerr);//cin与cerr关联
cin.tie(old_tie2); //cin重新与cout进行关联
8.2 文件输入输出
除了继承 iostream 类型的行为外, fstream 可以读写给定文件,且增加了一些新的成员来管理 与流关连 的文件。
fstream fstrm; 创建一个未绑定的文件流,fstream是头文件 fstream中定义的一个类型
fstream fstrm(s); 创建一个 fstream,并打开名未 s 的文件。
s 可以是 string 类型。或者是一个指向C风格字符串的指针,这些构造函数都是
expllicit的,默认的文件模式 mode 依赖于 fstream 的类型
fstream fstrm(s,mode); 与之前的构造函数类似,按指定 mode 打开文件
fstrm.open(s) 打开名为 s 的文件,并将文件与 fstrm绑定。 s 可以是一个string或者是c风格字符串
默认文件 mode 依赖于 fstream 类型。返回 void
fstrm.close() 关闭与 fstrm 绑定的文件。返回 void
fstrm.is_open() 返回一个 bool 值,指出与 fstrm关联的文件是否成功打开且尚未关闭。
8.2.1 使用文件流对象
- 当我们读写一个文件时,定义一个文件流对,并将对象与文件关联起来。
- 文件流对象定义了一个 open 成员函数,完成一些系统的操作,视情况为读和写。
ifstream in(file); //构造ifstream并打开 file文件
成员函数 open 和 close
ofstream out;
out.open(file); //打开指定文件
- 如果 open 失败,failbit 会被置位,进行 open 是否成功的检测通常是一个好习惯。
- 为了文件流关联到另一个文件,必须先关闭已经关联的文件。
自动构造和析构
当一个fstream对象被销毁时,close 会自动被调用。
8.2.2 文件模式
流通过关联的文件模式来指出 如何使用文件。
文件模式
in 以读方式打开
out 以写方式打开
app 每次进行写操作之前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式 进行 IO
指定文件模式有以下限制:
- 只可以对 ofstream或 fstream 对象设定 out 模式
- 只可以对 ifstream 或 fstream 对象设定 in 模式
- 只有当 out 也被设定时才可设定 trunc 模式。
- 只要 trunc 没被设定,就可以设定 app 模式。 在app模式下,即使没有显式指定 out 模式,文件也
总是以 out 输出方式打开。 - 默认情况下,即使我们没有指定 trunc,以out模式打开的文件也会被截断,为了保留 out 模式之前打开的文件的内容,我们必须指定 app 模式,在文件末尾进行追加。
- ate 和 binary 模式可用于 任何类型的文件流对象,且可以与其他任何文件模式组合使用。
默认文件模式:
- ifstream 默认以 in模式打开
- ofstream 默认以 out 模式打开
- fstream 关联的文件以 in 和 out 模式打开。
以 out 模式打开文件会丢弃已有数据
保留 被 ofstream 打开的文件中已有数据的唯一方式是显式指定 app 或 in模式。
每次调用 open 时都会确定文件模式
每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置,当程序未指定模式时,就使用默认值。
8.3 string 流
sstream 头文件定义了三个类型来支持 内存 IO,可以向string读写数据,就像string 是一个 IO流。
stringstream 特有的操作
sstream strm; strm是一个未绑定的stringstream 对象。sstream是头文件 sstream 中定义的一个类型
sstream strm(s); strm 是一个 stringstream对象,保存 string s 的一个拷贝。此构造函数是 explicit
strm.str() 返回 strm所保存的string 拷贝
strm.str(s) 将string s 拷贝到 strm 中。返回 void
8.3.1 使用istringstream
- 如果是对整个文本进行整理或处理,而其他一些工作是处理行内的单个单词时,通常可以使用 istringstream。
istringstream record("sss s s s s ");
string word;
while (record >> word) {
cout << word << ends;
}
此循环从 string 而不是标准输入中读取数据,当string中的数据全部读取后,会触发 文件结束的信号,再次读取操作会失败。
8.3.2 使用 ostringstream
我们希望逐步构造输出,希望最后一起打印时,ostringstream是很有用的。
例如我们向诸葛验证电话号码并改变其格式,如果所有的号码都是有效的,我们输出它。
ostringstream formatted_people;
for (const string& nums : people_phones) {
formatted_people << nums << " ";
}
cout << formatted_peopl.str();
第九关:顺序容器
- 容器就是特定类型对象的集合。
- 顺序容器为程序员提供了控制元素存储和访问顺序的能力
9.1 顺序容器概述
所有的顺序容器都提供了快速顺序访问元素的能力。
但是,这些容器都在以下方面有不同的性能折中:
- 向容器添加或从容器中删除元素的代价
- 非顺序访问容器中元素的代价。
vector 可变大小数组,支持快速随机访问。在尾部之外的位置插入删除可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除很快
list 双向链表,只支持双向顺序访问,在list任何位置进行插入删除都很快
forword_list 单向链表,只支持单向顺序访问,在链表任何位置进行插入/删除操作速度都很快
array 固定大小数组,支持快速随机访问,不能添加或者删除元素
string 与vector 相似的容器,但专门用来保存字符。随机访问速度块,在尾部插入/删除速度快。
确定使用哪种顺序容器
通常情况下,选用 vector 是最好的选择,除非你有很好的理由选择其他容器。
- 要求随机访问,选择 vector deque
- 程序要求在容器的中间插入或者删除,使用 list 或 forward_list.
- 程序需要在容器的头尾插入删除,而不会在中间插入删除选择 deque
- 如果程序只有在读取时才在容器中间位置插入元素,随后需要随机访问元素
则:
- 确定是否在中间位置添加元素,处理数据时 通常很容易在 vector 追加数据,sort排序避免中间位置插入数据
- 必须在中间位置插入数据,考虑输入阶段使用 list,一旦输入完成,将 list 的内容拷贝到vector 中
一般来说,应用中占主导地位的操作(执行的访问操作更多还是插入/删除更多)决定 了容器类型的选择。
建议:当我们不确定应该使用哪种容器,就只适用 vector 和 list就给够了。
9.2 容器库概览
- 某些操作是所有容器类型都提供的
- 另外一些操作仅仅针对于 顺序容器 ,关联容器,无序容器
- 还有一些操作只适合一小部分的容器。
当顺序容器构造函数接受一个 容器大小参数,它使用了元素类型的默认构造函数,没有默认构造函数的元素我们给与其初始化。
类型别名
iterator 此容器类型的迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
size_type 无符号整数类型,足够保存此种容器类型最大可能容器的大小
difference_type 带符号整数类型,足够保存两个迭代器之间的举例。
value_type 元素类型
reference 元素的左值类型,于 value_type& 含义相同
const_reference 元素的 const 左值类型,const value_type&
构造函数
C c; 默认构造函数,构造空容器
C c1(c2); 构造 c2 的拷贝 c1
C c(b,e); 构造 c,利用迭代器 b与 e 指定范围内的元素拷贝到 c
C c{
a, b, c....} 列表初始化 c
赋值与swap
c1 = c2 将c1中的元素替换成 c2 的元素
a.swap(b) 交换 a 和 b 的元素
swap(a,b); 与上面等价
大小
c.size() c中元素数目 ,forward_list 不支持
c.max_size() c中可保存的最大元素数目
c.empty() 若 c 中存储了元素,返回 false,否则返回 true
添加/删除元素(不适用于 array)
//在不同容器中,这些操作的接口都不同
c.insert(args) 将args 的元素拷贝进 c
c.emplace(inits) 使用 inits 构造 c 中的一个元素
c.erase(args) 删除 args 中的指定元素
c.clear() 删除 c 的全部元素,返回 void
关系运算符
== != 所有容器都支持该运算符
<,<=,>,>= 关系运算符(无序容器不支持)
获取迭代器
c.begin(),c.end() 指向c首元素与尾元素之后位置的迭代器
c.cbegin() , c.cend() 返回 const_iterator
反向容器的额外成员 (不支持 forward_list)
reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 不能修改元素的逆序迭代器