GitHub 仓库,求小星星,谢谢 ⭐
C++Primer是基于 C++11标准进行编写的书籍,以 3 位作者 Standley B. Lippman,Josee Lajoie ,Barbara E.Moo在C++语言发展历程中的经历,这本书的权威性自不容置疑的:既有编译器的开发和实践,又参与 C++标准的制定,再加上丰富的 C++ 教学经历。该书是一本由浅入深的教程,同时考虑到该书的全面性,我们也可以当其为教材,以备随时查阅。
如何选择内置类型:
类型转换注意点:
2147483647 INT_MAX
-2147483648 INT_MIN
指定字面值的类型
当使用一个长整型字面值时,请使用大写字母 L 来标记,因为小写字母 1 和数字 1 太容易混淆。
例如: 42LL.
谨言慎行:
初始化不是赋值,初始化的意思是指在创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而是以一个新值来替代。
列表初始化 11新标准
无论是初始化对象还是某些时候为对象赋予新值,都可以使用这一组花括号括起来的初始值。
例如:int num_val = {0};
重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
默认初始化
建议:初始化每一个内置类型的变量,虽然并非必须这么做,但是如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。
标识符
//变量要用描述性名称,不要节约空间,让别人理解你的代码更重要
const int kDaysInWeek = 7; //const 变量为k开头,后跟大写开头单词
int num_entries_; //变量命名:全小写,有意义的单词和下划线,类成员变量下划线结尾
int num_complated_connections_;
名字的作用域
建议:当你第一次使用变量时再定义它
一般来说,在对象第一次使用的地方附近定义他是一种好的选择,因为这样做有助于更容易地找到变量的定义。更重要的是,当变量的定义于它第一次被使用的地方很近时,我们也会赋予其一个比较合理的初始值。
复合类型(compound type)是基于其他类型定义的类型。 例如(指针与引用)。
( rvalue reference ) 右值引用是 C++11新标准新增加的内容
当我们使用术语 “reference” 指的都是 “左值引用”( lvalue reference ),绑定另一种类型的符合类型。
指针是指向另一种类型的符合类型。与引用类似,指针也实现了对其他对象的间接访问。
指针与引用的不同之处:
指针值(即地址)应属于下列4中状态之一:
利用指针访问对象:
空指针:
得到空指针的方式:使用 字面值 nullptr (C++11新标准)初始化指针,nullptr是一种特殊类型的字面值,它可以被转化为任意其他的指针类型。
建议:初始化所有指针
使用未经初始化的指针是引发运行时错误的一大原因。
任何非 0 指针对应的条件值都是 true
void* 指针:
void*指针 是一种特殊的指针类型,可用于存放任意对象的地址,与其他指针不同的是,我们对该指针中到底是一个什么类型的对象并不了解。
复合类型的声明 :
int* p,p2; 其中 p是指针类型,p2是int类型,始终数据类型是 int,(* 或 &)是类型修饰符,并不是数据类型的一部分。
指向指针的指针 :
指向指针的引用 :
指针引用声明: int *& i = p;
存在目的是为了防止程序不小心改变其值。
初始化和 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 指针
由于指针本身就是对象,也允许把指针本身定位常量。
顶层 const
指针本身是不是常量以及指针所指的是不是一个常量就是两个互相独立的问题。
顶层const(top - level const)表示指针本身是一个常量
底层const (low-level const)表示指针所指的对象是一个常量。
当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换,一般来说,非常量能转换为常量,反之不行。
constexpr 和 常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。
constexpr 变量
目的:为了解决一个初始值是不是常量表达式,因为在复杂系统中,很难分辨。
C++11新标准规定,允许将变量声明为 constexpr类型以便由编译器来验证变量的值是否是一个常量表达式,声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
建议:一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr类型。
指针与constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
constexptr会把它所定义的对象 设置为顶层 const
程序越来越复杂,程序中用到的类型也越来越复杂,
类型别名
类型别名是为了让复杂的类型名字变得清晰明了,利于理解与使用。
使用 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 类型说明符。
复合类型,常量与 atuo之间的关系
const int ci = i;
auto e = &ci; //e是一个指向常量的整数指针
decltype 类型指示符
从表达式的类型来推断要定义的变量的类型,而不是用值来推断,使用 C++新标准引入的第二类型说明符 decltype。
decltype 和 引用
注意点:
类数据成员
预处理概述:
由于头文件在程序中多次引用会造成 源文件重新编译获取更新过的声明,这时十分不安全以及不正常的。
确保头文件多次包含仍能安全工作的常用技术是 预处理器。
在C++中,我们用到的一项预处理功能是头文件保护符,头文件保护符依赖与预处理变量。
另外两个指令则分别检查某个指定的预处理变量是否已经定义:
一旦检查结果为真,则执行后续操作直至遇见 #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
#include
/* 每一个限定符内,声明顺序如下
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
习惯地加上头文件保护符是一个明智的决定。
访问库中名字的简单方法,使用作用域运算符 (:: )
std :: cin
另外一种安全的方法:使用 using 声明(using declaration)
使用 using namespace::name 之后就无须专门的前缀,也能使用所需的名字。
每个名字都需要独立的using声明
头文件不应该包含 using 声明
头文件的内容会拷贝到所有引用它的文件里去,如果头文件中某个using声明,那么每个使用了该头文件的文件就都会有这个声明,也许会造成始料未及的名字冲突。
直接初始化与拷贝初始化
读写 string 对象
在执行读取操作时,string 对象会自动忽略开头的空白(空格符,换行符,制表符)并从一个真正的字符开始读起,直到遇见下一处空白为止。
使用getline 读取一整行
getline 函数的参数是一个输入流和一个string对象,函数从给定的流中读入内容,直到遇见换行符为止,
(换行符也被读入),之后把所读的内容存入string对象中(不连换行符)。
string 的 empty 和size操作
empty 函数根据 string 对象是否返回空返回一个对应的布尔值。
size 函数返回 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) 使用范围 for 语句改变字符串中的字符 使用下标 执行随机访问 使用下标时,必须检测其合法性,如果 索引或者下标越界 将会产生错误。 vector 是模板而非类型,由vector 生成的类型必须包含 vector 中元素的类型,如 vector。 C++11 新标准提供了为 vector 对象赋予初始值的方法,列表初始化。 c++ 语言提供了几种不同的初始化方法,在大多数情况下这些初始化方式能相互等价的使用,不过也并非一直如此。 创建指定数量的元素 可以用 vector 对象容纳的元素数量 和 所有元素的统一初始值来初始化 vector 对象。 vector num_vec(10,-1); 值初始化 如果只提供 vector 对象容纳的元素数量而忽略其初始值,库会创建值初始化的元素初值,这个初值由 vector对象中元素的类型决定。 对这种初始化的方式有两个特殊限制: 列表初始值还是元素数量? 一方面情况:初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。 另一方面:如果初始化时使用了 花括号的形式但是提供的值不能来列表初始化,我们需要考虑用这样的值来构造 vector 对象了。 通过列表初始化的方式仅仅能对少量元素进行罗列,但是数量级的元素数量就不合理了,我们可以使用 关键概念: vector 对象能高效增长 c++标准要求 vector 在运行时能快速地添加元素,因此在定义 vector 对象的时候设定其大小可能会导致性能更差,除了初始化的元素的值全部一样,建议设定空 vector 对象,运行时向其动态添加。 当元素的定义了自己的相等性运算符与关系运算符,vector对象 才能支持相等性判断与关系运算等操作。 不能用下标形式添加元素 vector<>对象 以及string 对象的下标 运算符可用于访问已存在的元素,而不能用于添加元素。 提升:只能对确知已存在的元素执行下标操作,如果对不存在的元素去访问将引发错误,(buffer overflow) 确保下标合法的一种有效手段就是尽可能使用 范围for语句。 并不是所有的容器都支持 下标运算,但是所有的容器都支持另一种间接访问元素的机制,迭代器。 迭代器运算符 解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。 将迭代器从一个元素移动到另外一个元素 泛型编程的概念: 由于并非所有的标准库容器都定义了下标运算或者是 迭代器的操作符(<,>),因此我们要养成使用迭代器和 !=,这样就比较有通用性,不太在意用的是那种数据类型。 迭代器类型: 一般情况下,我们的迭代器类型有 iterator 与 const_iterator。 术语:迭代器类型与迭代器是不同的,一个指数据类型,一个指的是迭代器对象。 begin 与 end 结合解引用和成员访问的操作 为了简化 使用解引用符与下标点符获取该指向对象的元素(*iter).elem,C++定义了 箭头运算符(->) 某些对vector对象的操作会使 迭代器失效 谨记:但凡是使用了 迭代器的循环体,都不要向迭代器所属的容器添加元素。 所有的标准库容器都有支持递增运算的迭代器,类似的,也用 == 与 !=对任意的标准库容器进行比较操作。 string 和 vector 提供了额外的运算符(迭代器运算) 如果不清楚元素的确切个数,请使用 vector 显示初始化数组元素 字符数组的特殊性 字符数组可以直接使用字符串字面值对此类数组初始化,注意字符串字面值末尾会有一个 空字符’\0‘, 但是vector 是不支持直接使用字符串字面值对其进行初始化的。 不允许赋值与拷贝 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。 int a[] = {0, 1, 2}; 一些编译器支持数组的赋值,但是这些非标准特性的程序很有可能在其他编译器上无法正常工作。 理解复杂的数组声明 因为数组是可以存放大多数类型的对象,同时本身也是对象,可以定义存放指针的数组,也可以定义指向数组的指针。 要想理解数组声明的含义,最好的办法就是从内向外,从右至左来分析。 int *(& arry) [10] = ptrs ; //从内看,arry是一个引用,从右至左(忽略到括号内)是一个指针数组,那么arry就是对指针数组的引用。 int ia[] = {0,1,2,3,4}; 指针也是迭代器 利用指针也可以完成迭代器的操作,递增,指示等等。 标准库函数 begin 和 end 由于数组的尾后指针(并不存在的元素地址)获取会容易出错,为了让指针的使用更加简单安全,C++11新标准引入了 两个名为 begin 和 end 的函数。 begin(arr) :会得到arr首元素的指针 指针运算 迭代器的所有运算,用在指针上意义完全一致。 下标与指针 内置的下标运算符所用的索引值并不是无符号类型,可以处理负数,这一点与 vector 和 string 不一样。 C++支持 C风格字符串,但在 C++程序中最好还是不要使用他们,C风格字符串极易发生程序漏洞,是诸多安全问题的根本原因。 C 标准库String函数(cstring) 以上函数,均不会去验证字符串参数的正确性。 使用标准库 string 要比使用 C风格字符串更加安全,高效。 混用 strng 对象与 C 风格的字符串 注意点: c_str() 函数返回的数组在改变了字符串对象时会失去效用,我们最好将该数组拷贝一份。 使用数组初始化 vector vector i_vec{begin(int_arr) , end(int_arr)}; 建议:尽量使用 标准库类型而非数组类型,C 程序的底层操作容易引发一些繁琐细节有关的错误。 类型别名简化多维数组的指针 运算对象和求值结果全是右值 逻辑与与逻辑或运算符 左结合律 逻辑非运算符 右结合律; 关系运算符 左结合律, 相等性测试与布尔字面值 因为赋值运算符的优先级低于关系运算符的优先级,所以在条件语句中,赋值部分通常应该加上括号。 复合赋值运算符效率稍高。 因为很多迭代器不支持算术运算,所以递增与递减运算符是必须的 建议:除非必须,否则不使用递增递减后置版本。 后置版本将原始值存储下来以便于返回这个未修改的内容,一般情况下我们不需要保留该值,这就会造成浪费。 在一条语句中混用解引用与递增运算符 建议: 简洁可以成为一种美德。 使用 * p++ :先将p指针加一,返回 p 未增加前的副本,之后解引用,并将指针向前移动一个位置 运算对象可按任意顺序求值 如果一条子表达式改变了某个运算对象的值,另一条子表达式又要用到该值的话,运算对象的求值顺序就很关键了,除非子表达式与另一条子表达式是相连的关系。 点运算符与箭头运算符都可用于 访问成员,点运算符获取类对象的一个成员,箭头运算符与点运算符有关,表达式 ptr-> mem 等价于 (*ptr).mem; 条件运算符 (?:)允许我们把简单的 if- else 逻辑嵌入到单个表达式中: 条件运算符值对 expr1 与 expr2 中的一个求值。 嵌套条件运算符 条件运算符满足右结合律,意味着运算对象从右至左的顺序进行顺序结合。 条件运算的嵌套最好别超过两到三层。 位运算作用域整数类型的运算对象,并把 运算对象看成是 二进制位的集合。 | 是位或运算 注意:位运算符处理运算对象的 ”符号位“依赖于机器。而且此时的左移操作可能会改变符号位的值,因此是一种未定义的行为。 移位运算符 移位运算符是执行二进制位的移动操作,左侧对象按照右侧运算对象的值要求移动位数,然后将经过移动的左侧运算对象的拷贝作为求值结果。(右侧对象不能为负值) 位求反运算符 位求反运算符将运算对象逐位取反后生成一个新值,将1置为 0,0置为1. 位与,位或,位异或运算符 对于位于运算符 (&)来说,如果两个运算对象对应位置都是1,则运算结果中该位 为 1,否则为 0. 在移位运算时加上括号会帮助减少错误(优先级不高不低) sizeof返回的是表达式结果类型的大小。 都好运算符有两个运算对象,首先对左侧的表达式求值,然后把求值结果丢弃掉。 隐式转换:是自动执行的对运算对象进行的类型统一的过程。 何时会发生隐式转换: 在下面这些情况下,编译器就会自动地转换运算对象的类型: 算术转换的含义是把一种算术类型转换成另外一种算术类型。 整型提升 数组转换为指针:在数组的表达式中,数组自动转换成指向数组首元素的指针 指针的转换:C++中还规定了其他的指针转换方式,常量整数 0 或者字面值 nullptr能转换成任意指针类型:指向任意非常量的指针能转换成 void* ;指向任意对象的指针能转换成 const void*. 转换为布尔类型:指针算术类型的值为 0,转换的结果是false,否则转换结果为 true; 转换成常量:允许将指向非常量类型的指针转换为指向相应常量类型的指针,对于引用也是这样。 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过每次只能转环一次。 命名的强制类型转换 一个命名的强制类型转换的格式如下: static_cast: const_cast const_cast 只能改变运算对象的 底层 const reinterpret_cast reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。 reinterpret_cast 本质上依赖于机器。要想安全地使用 reinterpret_cast 必须对涉及的类型和编译器实现转换的过程都非常了解。 建议:避免强制类型转换 强制类型转换干扰了正常的类型检查,因此我们强烈建议 程序员避免使用强制类型转换。 C++ 提供了一组控制流语句以支持更复杂的执行路径。 表达式末尾加上分号就成了表达式语句; 空语句 别漏写分号,也别多写分号 复合语句 在 if,switch,while 和 for 语句中的控制结构内定义变量,定义在控制结构当中的变量只在对应语句的内部可见,语句结束,变量也会超出其作用范围。 因为控制结构定义的对象的值马上要由结构本身使用,所以这些局部变量需要初始化。 悬垂 else 当 if 存在且 if else 语句也存在,这时 C++规定 else 与 离它最近的尚未匹配的 if 匹配,从而消除 程序的二义性。 case 关键字 switch‘ 语句括号内的表达式是可以转换为整数类型的表达式。 switch 内部的控制流 switch 内部的变量定义 异常是指存在于运行时的反常行为,这些行为都超出了函数正常功能的范围。 函数在寻找处理代码的过程中退出 当异常被抛出时,首先搜索抛出异常的函数,没有找到对应的 catch子句,终止该函数,并在外层调用该函数的函数继续搜索,。。。。沿着程序的执行路径逐层回退,直到找到适合类型的 catch 子句为止。 如果最终没有找到匹配的 catch,程序将转到名 为 terminate的标准库函数,(该函数会导致程序非正常退出)。 标准异常 C++ 标准库中定义了一组类,用于报告标准库函数遇到的问题,他们分别定义在 4个头文件中 stdexcept 定义的异常类 函数的构成: 返回类型,函数名字,0或者多个形参组成的列表以及函数体。 调用运算符 :使用括号运算符作用于一个表达式(函数或者是函数指针),圆括号是用逗号分隔的参数列表,我们用实参初始化函数的形参,调用表达式的类型就是函数的返回类型。 调用函数: 调用函数完成了两项工作: return 语句的两项工作: 形参与实参 函数的形参列表 函数返回类型 在C++中语言中,名字有作用域,对象有生命周期 自动对象 只存在于块内执行期间的对象称为自动对象,函数执行结束后,创建的自动对象的值就变成未定义的 了。 对于局部变量对应的自动对象: 局部静态变量 局部静态对象 在程序的执行路径第一次经过该对象时定义语句并将其初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。 在头文件中进行函数声明: 分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。 编译和链接多个源文件 fact 函数 声明于 Chapter6.h 的头文件中,定义于 fact.cc 其中如果要生成 可执行文件,我们需要告诉编译器我们的代码在哪,下面演示: $ cc factMain.cc fact.cc ##generates .exe or a.out cc 是编译器的名字, $ 是系统提示符,#是注释 分离式编译并链接 编译过程: 链接过程: $ cc factMain.o fact.o # generates factMain.exe or a.out 管理指针形参 数组形参与 const 只有当函数确实要改变元素值的时候,我们才把形参定义成指向非常量的指针。 数组引用形参 我们可以将引用形参绑定到数组上,数组的引用。 void print(int (&arr)[ 10 ]); 传递多维数组 void print (int (*matrix)[10], int rowSize); 编译器会自动忽略第一个维度,因此请不要包含它到形参列表中 我们有时需要给 main 传递实参,一种常见的情况是用户通过设置一组选项来确定该函数执行的操作 prog -d -o ofile data0 这些命令行通过两个参数传递给main函数 int main(int argc,char **argv) argc 代表了传递信息的数量,argv代表了命令行的字符串数组 我们有时无法预知 向函数传递几个实参, 因此为了编写能处理不同数量实参的函数, initializer_list 省略符形参 省略符形参是为了便于 C++程序访问某些特殊的 C代码而设置的,这些代码使用了名为 varargs 的 C 标准库的功能。 } 不要返回局部变量的引用或指针 返回类类型的函数和调用运算符 (.) 调用运算符的优先级与 点运算符和箭头运算符相同,且复合结合律,我们可以使用函数返回的结果直接调用其对象的成员。 引用返回左值 调用一个返回引用的函数得到左值,我们能为返回非常量的引用的对象赋值 列表初始化返回值 C++11新规定,函数可以返回花括号包围的值的列表(代表了返回的临时量进行初始化),如果列表为空,临时量指向值初始化,否则,由函数的返回类型决定。 如果返回的是内置类型,则花括号应该仅有一个值,且所占空间不应该大于目标类型的空间,如果返回的是类类型,则由类定义初始值如何使用。 主函数 main 的返回值 递归 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。 利用 类型别名返回数组指针或引用 using arrT = int【10】; arrT * func(int i); 声明一个返回数组指针的函数 我们想定义一个返回数组指针的函数,则数组的维度必须紧跟在函数的名字之后 逐层理解: 使用尾置返回类型 auto func(int i) -> int(*)[10] 为了表示函数真正的返回类型跟在形参列表之后,我们本应该出现返回类型的地方放置一个 atuo; 使用 decltype 利用 decltype()推断数组的类型,之后跟一个指针也可以 int odd[] = {1,3,4,5} 如果同一作用域中的几个函数名字相同但形参列表不同,我们称之为重载函数 定义重载函数 对于重载的函数来说,他们应该在形参数量或形参类型上有所不同。 重载和 const 形参 建议:何时不应该重载函数 重载函数虽然一定程度上减轻我们为函数起名字的负担,但是最好重载那些确实非常相似的操作。 const_cast 和重载 利用 const_cast 能对底层const 与 普通非常量 进行转换 调用重载的函数 在很多次函数的调用中,一些形参被赋予了同一个值,这时,我们将反复出现的值称为函数的默认实参。 using sz = string::size_type ; 使用了默认实参调用函数 默认实参声明 通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。 默认实参初始值 局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。 调用函数比求等价表达式的值要慢一点 内联函数可避免函数调用的开销 内联函数通常是将它在每个调用点上 ”内敛地“展开。 内联函数需要在 函数声明的最前面加入 ”inline“修饰符 constexpr 函数 constexpr 函数是指能用于常量表达式的函数,定义 constexpr 函数的方法与其他函数类似, 把内联函数和 constexpr 函数放在头文件内 对于给定的内联函数或者 constexpr 函数来说,它的多个定义必须完全一致,因此,内联函数和 constexpr函数通常定义在头文件中。 当应用程序准备发布时,要先屏蔽掉调试代码 assert 预处理宏 assrt( expr) NDEBUG 预处理变量 assert 的行为依赖于一个名为 NDEBUG 的预处理变量,如果定义了 NDEBUG 则 assert 什么都不做, 我们可以利用 NDEBUG编写自己的测试代码: 预处理器定义了 4 个对于程序调试很有用的名字: 确定候选函数和可行函数 寻找最佳匹配 含有多个形参的函数匹配· 编译器依次检测每一个实参以确定哪个函数是最佳匹配。如下条件: 如果检测到所有的函数都没有一个脱颖而出,编译器则报告二义性。 调用重载函数时应尽量避免强制类型转换,如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。 为了完成精确匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下: 1.精确匹配 函数匹配与 const 实参 底层 const 形参会优先匹配 常量实参 函数指针指向的是函数而非对象,与其他指针一样,函数指针指向某种特定类型,函数的类型由它的返回类型 和形参类型共同决定,与函数名无关。 bool lengthCompare(const string &,const string &); bool (*pf)(const string &,const string &); 使用函数指针 ( pf = lengthCompare; ) = ( pf = &lengthCompare;) 重载函数的指针 编译器通过指针类型决定选择哪个函数。指针类型必须与重载函数中的某一个精准匹配。 函数指针形参 返回指向函数的指针 将 auto 和 decltype 用于函数指针类型 当我们将decltype作用于某个函数时,它返回 函数类型而非指针类型,因此,我们显式地加上 * 代表我们返回指针而非函数本身。 string::size_type sumLength(const string& , const string&); 类的基本思想是数据抽象(data abstraction) 与 封装(encapsulation),数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。 类的接口包括用户所能执行的操作;类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。 封装实现了类的接口与实现的分离,封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。 类想要实现数据抽象 和 封装,需要先定义一个抽象数据类型(abstract data type)。在抽象数据类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无需了解类型的工作细节。 我们来实现一个 SaleData类,它目前并不是一个抽象数据类型,它允许用户访问它的数据成员,并且由用户来编写操作。 我们需要定义一些操作以供类的用户使用,之后我们封装(隐藏)它的数据成员,保证接口与实现分离,逐渐地完成数据抽象与封装,实现一个抽象数据类型。 SalesData 的接口应该包含以下操作: C++程序员无须刻意区分应用程序的用户以及类的用户。 在一些简单的应用程序中,类的用户和类的设计者常常是同一个人。尽管如此,还是最好把角色区分开来,当我们设计类的接口时,应该考虑如何才能使得类易于使用;当我们使用类时,不应该顾及类的实现机理。 要想开发一款成功的应用程序,其作者必须充分了解并实现用户的需求。同样,优秀的类设计者也应该密切关注那些有可能使用该类程序员的需求。作为一个设计良好的类,既要有直观且易于使用的接口,并且具备高效的实现过程。 定义成员函数 引入 this 在成员函数内部,我们可以直接调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,是因为 成员函数通过一个名为 this 的隐式参数来访问调用 它的那个对象。 当我们调用一个成员函数时,用请求该函数的对象地址初始化 this。 引入 const 成员函数 std::string book_no() const { return book_no_; } 类作用域和成员函数 在类的外部定义成员函数 类外部定义的成员的名字必须包含它所属的类名。 函数名 SalesData::AvgPrice使用作用域运算符来说明该函数被声明在 SalesData的作用域中,函数体内的代码的成员是位于类的作用域内的就不会出错。 定义一个返回 this 对象的函数 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类都在同一个头文件内。 定义 Read函数与 Print 函数 定义 Add 函数 类通过一个或多个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。 合成的默认构造函数 类可以通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。 如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐士地定义一个默认构造函数。 某些类不能依赖于合成的默认构造函数 原因有三: 定义SalesData 的构造函数 我们定义 4 个不同的构造函数 构造函数初始化列表 构造函数初始值是成员名字的一个列表,每个名字紧跟括号括起来的成员初始值,不同成员初始化通过逗号分隔开。 在类的外部定义构造函数 当作用域与函数命字相同时,说明该函数是构造函数。 类除了初始化外,类还需要 控制拷贝,赋值,销毁对象时发生的行为。 一般来说,编译器生成的版本将对对象的每一个成员执行拷贝,赋值和销毁操作。 某些类不能依赖于合成的版本 管理动态内存的类通常是不能依赖上述操作的合成版本,会造成内存问题。 我们已经为类定义了接口,但还没有机制强制用户使用这些接口,我们的类还没有封装,用户可以直达对象内部控制它的具体实现细节。 在C++中,我们使用 访问说明符 加强类的封装性。 使用 struct 或者 class关键字 struct 和 class 唯一的区别就是 默认的访问权限不同。 如果我们使用 struct 关键字,则定义在第一个访问说明符之前的成员是 public的,相反,如果我们使用的是 class 关键字,则这些成员是 private 的。 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数称为它的友元。 封装有两个重要的优点: 友元的声明 令成员作为内联函数 重载成员函数 可变数据成员 我们希望能修改类的某个数据成员,即使是在一个const 成员函数中,我们可以在变量的声明中 加入 一个可变数据成员永远不会 是 const,即使它是 const 对象的成员。 类数据成员的初始值 在C++ 11 新标准中,最好的方式是把默认值声明为 类内初始值。 当我们提供一个类内初始值时,必须用花括号或者 = 表示。 *从 const 成员函数返回 this 一个 const 成员函数如果以引用的方式返回*this,那么他从的返回类型是常量引用。 基于const 的重载 判断是否该函数为常量成员函数: 建议:对于公共代码使用私有功能函数。 公共代码定义成一个单独的函数,是为了在实践中,重复调用这些函数,完成一组一组其他函数的“ 实际”工作。 类的声明 不完全类型的使用情形: 对于一个类来说,创建其对象之前必须被定义过,而不呢仅仅声明,不清楚其存储空间大小。 类可以将其他的类定义成友元,也可以把其他类(已经定义过的)的成员函数定义成友元。 类之间的友元关系 令成员函数作为友元 假设 A 为 B 的 func函数提供友元访问权限。 函数重载与友元 对重载函数声明友元,仍然需要每个单独声明。 友元声明和作用域 作用域和定义在类外部的成员 名字查找(寻找与所用名字最匹配的声明的过程) 类的定义分两步处理: 类型名要特殊处理: 类型名的定义通常出现在类的开始处,这样能确保所有使用该类型的成员都出现在类名的定义之后。 成员定义中的普通块作用域的名字查找 首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。 如果在成员函数中没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。 如果类内也没找到该名字的声明,在成员函数定义之前的作用域继续查找。 不建议将局部变量的名字与成员的名字重复 我们可以显式地使用 this 指针强制访问成员。 类作用域之后,在外围的作用域查找 尽管外层的对象被隐藏掉了,但我们可以用作用域运算符 (::)访问它。 在文件中名字的出现处对其进行解析 构造函数的初始值有时必不可少 如果成员是 const,引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。 建议使用构造函数初始值 当有的类含有需要构造函数初始值的成员时,使用构造函数初始值能避免意想不到的编译错误。 成员初始化的顺序 最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。 默认实参和构造函数 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。 C++ 11 新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它的一些职责委托给了其他构造函数。 当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。 当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生 值初始化: 在数组初始化的过程如果我们提供的初始值数量小于数组的大小 我们不使用初始值定义一个局部静态变量时 当我们书写 T() 的表达式要求显式地进行值初始化时。 在实际开发中,如果定义了其他构造函数,最好也提供一个默认的构造函数。 使用默认构造函数 SaleData obj;//默认初始化,调用默认构造函数 如果一个类只接受一个实参,则它定义了转换为此类类型的隐士转换机制,我们把这种构造函数称作转换构造函数。 我们可以直接使用 一个实参的构造函数来隐式转换 为类类型。 只允许一步转换 我们隐式地把 字面值转换为 常量字符串,之后再隐式地转换为类类型,这时不被允许的 类类型转换不是总有效 是否要从一个 构造函数实参类型转换为 类类型取决于用户使用该转换的看法,该隐式转换的数据可能不符合条件,也可能会正确!!! 抑制构造函数定义的隐式转换 关键字 explict 只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无须指定 explicit。 只能在类内声明构造函数时 使用 explicit 关键字,在类外部定义时不应重复。 当我们用 explicit 关键字声明构造函数时,它将只能以直接初始化的方式使用,拷贝初始化是不被允许的。而且,编译器将不会在自动转换过程中使用该构造函数。 为转换显式地使用构造函数 我们可以直接使用 类的单参数构造函数接收 单参数完成显式构造。 标准库中含有显式构造函数的类 聚合类 使得用户可以直接访问成员,并且具有特殊的初始化语法形式。 Data val = {0, “abcd”}; 显式地初始化类的对象的成员存在 3个明显的缺点: 数据成员都是字面值类型的聚合类是字面值常量类。 复合下面的要求也是字面值常量类: constexpr构造函数 类需要它的一些成员与类本身有直接的关系,但不是与类定各个对象都保持联系。 这时候我们将其 声明为 类的静态成员 声明静态成员 定义静态成员 静态成员的类内初始化 静态成员能用于某些场景,而普通成员不能 IO 类型间的关系 标准库使我们能忽略这些不同类型的流的差异,这都是由继承机制实现的, IO 类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态 查询流的状态 stm::iostate 是与IO定义的与机器无关的类型,它提供了表达流状态的完整功能,它是一个位集合。 管理流的状态 每个输出流都管理着一个缓冲区,用来保存程序读写的数据,有了缓冲的机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作,会带来很大的性能提升。 导致缓冲刷新(即,数据真正写到输出设备或文件),原因有很多: 刷新输出缓冲区 flush 操纵符 刷新缓冲区,但不输出任何的字符 ends 操作符 输出 一个空字符,刷新缓冲区 unitbuf 操纵符 使用 unitbuf 操作符会告诉接下来的每一次写操作之后都会进行一次 flush 操作。而 nounitbuf 操纵符会 如果程序崩溃,输出缓冲区不会被刷新 当一个程序崩溃时,它输出的数据很可能停留在缓冲区中等待打印。 关联输入和输出流 tie函数能使 输入流关联到一个输出流,接受一个流的地址,返回值位 指向这个流的指针,如果对象未关联到流,则返回空指针 除了继承 iostream 类型的行为外, fstream 可以读写给定文件,且增加了一些新的成员来管理 与流关连 的文件。 ifstream in(file); //构造ifstream并打开 file文件 成员函数 open 和 close ofstream out; 自动构造和析构 当一个fstream对象被销毁时,close 会自动被调用。 流通过关联的文件模式来指出 如何使用文件。 指定文件模式有以下限制: 默认文件模式: 以 out 模式打开文件会丢弃已有数据 保留 被 ofstream 打开的文件中已有数据的唯一方式是显式指定 app 或 in模式。 每次调用 open 时都会确定文件模式 每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置,当程序未指定模式时,就使用默认值。 sstream 头文件定义了三个类型来支持 内存 IO,可以向string读写数据,就像string 是一个 IO流。 stringstream 特有的操作 此循环从 string 而不是标准输入中读取数据,当string中的数据全部读取后,会触发 文件结束的信号,再次读取操作会失败。 我们希望逐步构造输出,希望最后一起打印时,ostringstream是很有用的。 例如我们向诸葛验证电话号码并改变其格式,如果所有的号码都是有效的,我们输出它。 所有的顺序容器都提供了快速顺序访问元素的能力。 但是,这些容器都在以下方面有不同的性能折中: 确定使用哪种顺序容器 通常情况下,选用 vector 是最好的选择,除非你有很好的理由选择其他容器。 一般来说,应用中占主导地位的操作(执行的访问操作更多还是插入/删除更多)决定 了容器类型的选择。 建议:当我们不确定应该使用哪种容器,就只适用 vector 和 list就给够了。 当顺序容器构造函数接受一个 容器大小参数,它使用了元素类型的默认构造函数,没有默认构造函数的元素我们给与其初始化。 迭代器范围 迭代器返回的概念是标准库的基础 【begin,end) 使用左闭右开范围蕴含的编程假定 3 种方便的性质 begin 与 end就是指向容器第一个元素与最后一个元素的尾后指针,用途是形成左闭右开的容器元素范围。 每个容器类型都定义了一个默认的构造函数,(除了array)其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小与元素初始值的参数。 列表初始化 使用列表初始化初始化容器,是显式地制定了其容器种每个元素的值。 与顺序容器大小相关的构造函数 如果元素类型是内置类型或具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数,标准库会创建一个值初始化器初始化他们 如果没有默认构造函数,除了大小参数外,再次指定一个显式的初始值。 注意: 赋值运算会导致左边容器内部的迭代器,引用,指针失效,而 swap 操作将容器内容交换并不与会导致指向容器的迭代器,引用和指针失效。(array 和 string 的情况除外) 使用assign (仅顺序容器) seq.assign(il) 允许从 类型不同但是相容的类型赋值,由于旧元素被替换,因此传递给 assign 的迭代器不能指向调用 assign 的容器。 从 list names 元素 assign 给 vector 使用 swap 元素不会移动的事实: 除了 string ,指向容器的迭代器,引用和指针在swap 操作之后不会失效,他们仍然指向swap操作前的那些元素,但是 swap 之后,这些元素已经属于不同的容器了 除了一个forward_list 例外,每个容器都有三个相关的操作 成员函数 每个容器类型都支持 相等运算符 = or != 除了无序关联容器所有的容器都至此关系运算符(>,>=,<,<=),关系运算符两边的运算对象必须是相同类型的容器,且保存相同类型的元素。 比较两个容器: 容器的关系运算符使用元素元素的关系运算符进行比较 只有器元素类型定义了相应 的比较运算符,我们才可以使用关系运算符来比较两个容器。 所有标准库容器都提供灵活的内存管理,在运行时候可以动态添加或删除元素来改变容器的大小。 这些操作会改变容器的大小;array 不支持这些操作。 概念: 容器元素是拷贝,而不是对象本身 使用 emplace 操作 新标准引入了 3个新成员 – emplace_front,emplace 和 emplace_back,这些操作构造而不是拷贝元素。 emplace 函数在容器中直接构造元素,传递给 emplace 函数的参数必须与元素类型的构造函数相匹配。 如果 resize 缩小容器,则指向被删除元素的迭代器,引用指针都会失效,对vector,string,deque进行resize可能导致迭代器,指针引用失效。 添加元素: 删除元素: 注意:当我们删除元素,尾喉迭代器总会失效 建议: 我们在每次改变容器的操作之后都应该正确地重新定位迭代器。 不要保存 end 返回的迭代器 在 进行插入删除操作时,保存的局部 end 可能会失效,因此不应该保存,要重新获取 vector 与 string的实现通常会分配比新的空间需求更大的内存空间,容器预留这些空间作为备用,可以保存更多的新元素。这样,就不需要每次添加新元素都重新分配容器的内存空间了。 管理容器的成员函数 vector 内存分配策略遵守规则:只有当迫不得已时才可以分配新的内存空间。 string 剪切追加替换操作 substr操作: s.substr(pos,n) 返回一个string,从 pos 开始n 个字符的拷贝,pos默认值为0,n默认值为 s.size() =pos append操作 s.append(args) :将 args追加到 字符串尾部 replace 操作 s.replace(range,args) :将范围内的字符删除并替换为args的字符。 string 搜索操作 val 与 string 的相互转换 to_string(val) :得到字符串 大多数算法都定义在 头文件 algorithm中,标准库还在头文件 numeric中定义了一组数值泛型算法 算法如何工作 算法并不依赖容器保存的元素类型,只要有一个迭代器能访问元素即可。 迭代器令算法不依赖容器,但算法依赖于元素类型的操作 大多数算法提供了使用元素类型的运算符来进行的比较,但我们将会看到,我们可以允许自定义的操作来 代替默认的运算符。 读容器的 迭代器应该是常量迭代器类型 介绍 back_inserter 保证算法有足够空间来容纳输出数据的方法是 使用插入迭代器。 通常我们通过一个迭代器向容器元素赋值时,值被赋予迭代器所指的元素,而当我们通过一个 插入迭代器进行赋值,则一个赋值号右侧相等的元素被添加到容器中。 sort: 会重排输入序列的元素,利用 元素之间的 < 运算符实现排序的 使用unique unique 会将相邻的重复项都“”消除“”,返回一个指向不重复值范围末尾的迭代器。 算法使用元素类型的 运算符来完成比较操作,标准库还为这些算法提供了额外的版本,允许我们自定义操作来替换默认运算符。 谓词 排序算法 stable_sort:可以使具有相同长度的元素按照字典序排列。 划分算法 标准库定义了 partition ,接受谓词,对容器内容进行划分,使得谓词等于 true 的 值会排在容器的前半部分,否排在容器后半部分,返回一个指向 最后一个使谓词为true 的元素之后的位置。 有时我们希望谓词能多接受一些参数 介绍lambda 表达式 我们可以向一个算法传递任意类别的可调用对象。 lambda 表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数 向lambda 传递参数 使用捕获列表 一个 lambda 表达式只有在捕获列表中捕获一个它所 在函数中的局部变量,才能使用。 [ sz ] (const string& s) 捕获列表只用于局部非 static变量, lambda 类直接使用 局部 static 变量和它所在函数之外声明的名字。 for_each 算法 利用该算法接受一个可调用对象,并对输入序列中每个元素调用此对象。 当定义一个 lambda时,编译器生成一个与 lambda 对应的新的类类型。 值捕获: 由于 该捕获变量的值是 在 lambda 对象创建前拷贝,此后修改是不会影响到 lambda内对应的值。 引用捕获 我们以 &来进行引用捕捉,但必须保证lambda 执行时变量是存在的。 如果lambda 的返回值作为函数返回值,lambda不能返回局部变量的引用 建议:尽量保持 lambda 的变量捕捉简单化,减少其捕获的数据量,来避免潜在的捕获导致的问题,如果可能的话,避免捕获引用与指针 隐式捕获 我们可以让编译器根据 lambda 体内的代码来推断我们要使用那些变量。 如果我们想混用隐式捕获和显式捕获的时候,有以下规则: 可变 lambda 我们希望以值拷贝的变量能够改变其被捕获的变量的值,要在参数列表前面加上 mutable。 指向lambda 返回类型 lambda 的适用场合: 仅有一两个地方使用的简单操作,我们定义其 lambda 表达式,其他,我们定义为函数。 标准库bind 函数 为了解决能使函数能替换 带捕获 的lambda 表达式,且形参列表数量不被改变,我们引入了 标准库 arg_list 中的参数包含了形如 _n的名字,其中 n 个整数。 这些参数都是占位符,表示 newCallable的参数。 举例子: 其中,_1是占位符,代表了check_size_的第一个参数 使用 placeholders 名字 placeholders 命名空间也定义在 functional 头文件中 名字 _n 都定义在一个名字为 placeholders 的命名空间中,而此命名空间本身定义在 std 命名空间,为了使用该名字,两个命名空间都要写。 using std::placeholders::_1; 用 bind 重排参数顺序 当我们利用占位符 _1,_2无实参传入时,值得是 传入的函数的参数。 我们交换 _2,_1 的位置,能实现参数顺序重排 绑定引用参数 默认情况下, bind 的非占位符的参数被拷贝到 bind 的返回的可调用对象中,如果绑定参数为引用类型 占位符占的是实际 谓词的参数。 新标准的 C++程序应该尽量使用 bind。 标准库在头文件 iterator 中还定义了 额外的迭代器 当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给顶容器的指定位置插入一个元素。 使用插入迭代器插入之后迭代器仍然指向原来的位置 istream_iterator 读取输入流 这些迭代器将他们对应的流 当做一个特定类型的元素序列来处理。使用流迭代器,我们可以用泛型算法 istream_iterator操作 istream_iterator 允许使用懒惰赋值 ostream_iterator 使用: 反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。 反序迭代器递增递减的操作会颠倒过来 反向迭代器只能从 ++ 也支持 – 的迭代器来定义 我们能利用 reverse_iterator 的 base 成员来转换回一个普通正向迭代器,指向的是相邻位置而不是相同位置 反向迭代器的目的是表示元素范围,其不是对称的:当我们从普通迭代器初始化一个反向迭代器,或相反,结果迭代器与原迭代器指向的并不是相同的元素【左闭右开与左开右闭】 当向一个算法提供错误类别的迭代器的问题,大多数不会给出警告。 输入迭代器:可以读取序列中的元素 输入迭代器只支持顺序访问,对于一个输入迭代器 *it++ 保证是有效的,递增他可能导致所有的指向流的迭代器失效,不能保证输入迭代器的状态可以保存下来访问元素,因此只能单遍扫描算法 算法: find accumulate 输出迭代器:可以看作是输入迭代器的补集–只写而不读元素,额外支持 单遍扫描 算法: copy 的第三个参数 前向迭代器 多遍扫描 算法: replace 双向迭代器:可以正向反向读写序列中的元素 算法 reverse 要求双向迭代器,除了forward_list 标准库都符合双向迭代器要求的迭代器 随机访问迭代器: 在常量时间内访问序列中任意元素的能力 支持所有双向迭代器的能力。 大多数算法的形式如下 向输出迭代器中写入数据的算法都假定目标空间足够容纳写入的数据。 使用重载形式传递谓词的算法 unique(beg,end); _if 版本的算法 接受一个元素值的算法通常有另一个不同命的版本,该版本接受一个谓词,并且算法名 有附加的 _if find(beg, end, val); 区分拷贝元素的版本和不拷贝的版本 写到额外的目的空间的算法都在后面附加一个 _copy reverse(beg,end); 一些算法同时提供了 _copy 和 _if 版本,这些版本接受一个 目的位置迭代器与一个谓词。 remove_if(v1.begin(), v1.end() , [ ] (int i) { return i%2; }); 对于 list 与 forward_list,应该优先使用成员函数算法而不是通用算法 splice 成员 我们可以理解其是移动函数,将一个链表的元素移动到指定位置 lst.splice(args), flst.splice_after(args) 关联容器支持高效的关键字查找和访问 关联容器的主要类型是 map与 set 无序容器定义在 unordered_set 与 unordered_map 头文件中 有序容器定义在 map和 set中 使用 map 与 set map 每个关联容器都定义了一个默认构造函数,它创建了一个指定类型的空容器。 初始化 multimap 或 multiset 容器 multimap 与 multiset 没有对关键字的限制,可重复,就像一个特定的单词可能有多种词义。 有序容器 的关键字类型 我们可以向算法提供我们自定义的比较操作,与之类似,也可以提供自己定义的操作来代替关键字的 < 运算符 其中所提供的操作必须在关键字类型上定义一个 严格弱序,可以将严格弱序看作 ”小于等于“。 如果 这两个关键字是等价的,那么容器视为他们是相等的,当用 map 的关键字时,只能选其一来访问对应的值 。 在实际编程中,如果一个类型定义了一个 ”行为正常“ 的 < 运算符,则可以用作关键字类型。 使用关键字类ing的比较函数 pair 标准库类型作为 键值对容器的元素类型,它定义在 utility 中。 pair 是用来生成特定类型的模板,我们需提供两个类型名(不要求相等) pair上的操作 创建 pair 对象的函数 返回值为 pair时,我们可以使用 列表初始化返回 return {p,first,p.second}’ 当解引用 关联容器迭代器时,我们会得到一个类型为容器的 value_type 的 值的引用。 遍历关联容器 获取 有序关联容器 begin 与尾后迭代器,可以对其进行遍历 关联容器的 inert 成员向容器中添加一个元素或一个元素范围 向 map 添加元素 c. emplace(args) : 当关键字不在的时候构造一个元素。 检测 insert 的返回值 insert 或者 emplace 返回的值依赖于容器类型于参数,对于不包含重复关键字的容器 向 multiset 和 multimap 添加元素 map 和 unordered_map 的下标操作 c[ k ] : 返回关键字为 k 的元素,k不在c中,则添加一个关键字为 k 的元素,对其进行值初始化。 在一个关联容器中查找元素操作 在multimap 和 multiset 查找元素 一种不同的,面向迭代器的解决办法。 c.low_bound(k) :会得到第一个关键字不小于 k 的元素,可以作为容器中k关键字的键值对的 begin迭代器 该获取到的迭代器范围就是关键字等于 k 值的 pair序列范围 equlal_range 函数 equal_range(k) :会在容器中返回关键字等于 k 的【b,e)范围 我们以一个程序结束本节的内容,它将展示map 的创建搜素以及遍历. 给定一个 是string,使其转换为另一个 string 程序的输入 是两个文件: c++11 新标准 定义了4个无序关联容器。 建议: 如果关键字类型固有就是无序的,或者上性能测试发小问题可以用哈希技术解决,就可以选择使用无序容器。 管理桶 无序容器在存储组织上为一组桶,每个桶保存 0 个或多个元素,无需容器使用一个 哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,之后指出应该搜索哪一个桶。 无序容器管理操作 无序容器对关键字类型的要求 我们可以利用重载关键字类型的默认比较函数 == 与哈希计算函数实现自定义类类型的无序容器: 除了静态内存和栈内存,每个程序还拥有一个内存池,这部分内存被称为 自由空间 或 堆。 C++ 中,动态内存的管理是通过 new(为对象分配动态内存并返回指向的指针,我们可以选择对对象进行初始化)与 delete 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。 动态内存的使用很容易出现问题: 为了更容易(更安全)地使用动态内存,新的标准库提供了两种 智能指针类型来管理对象。 新标准提供的智能指针的区别在于管理底层指针的方式: 这些类型都定义在 memory 头文件中 shared_ptr 与 unique_ptr 都支持的操作 shared_ptr 独有的操作 make_shared 函数 shared_ptr 的拷贝与赋值 shared_ptr 自动销毁所管理的对象和释放相关联的内存 当一个 shared_ptr 被销毁时,shared_ptr 会递减它所指向的对象的引用计数 shared_ptr 在无用之后不应该被保留。 使用了动态生存期资源的类 程序使用动态内存处以下三种原因之一: 容器类 是处于第一种原因而使用动态内存的典型例子。 而使用一个动态内存的常见原因是允许多个对象共享相同的状态。 定义一个多个对象共享相同状态的类 在一个类的内部初始化一个管理动态内存的指针时,拷贝赋值和销毁该对象都会使引用计数发生改变。 使用 new 动态分配和初始化对象 在自由空间分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针 int *p = new int; 分配内存,返回其指针 默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象将是未定义的,而类类型对象将使用 默认构造函数进行初始化 对于定义了自己的构造函数的类类型,要求值初始化是无意义的,不管什么形式,对象都会通过默认构造函数来初始化,但是对于内置类型,值初始化对象将有良好定义的值。 提供了一个括号包围的初始化器,就可以使用 auto 进行推断我们分配的对象类型,但智能有单一的初始化器。 auto p1 = new auto(obj); 动态分配的 const 对象 内存耗尽 当自由空间被耗尽的情况下,new表达式就会失败 默认情况下,如果 new 不能分配所要求的内存空间,它会抛出一个类型为 bad_alloc 的异常, int *p2 = new (std::nothrow) int(); 这种形式的new 为 定位 new,定位 new 表达式允许我们向 new 传递额外的参数,这里我们传入 bad_alloc 与 nothrow 都定义在头文件 new中。 释放动态内存 我们通过 delete表达式来将 动态内存归还给系统,delete 接受一个指针,指向我们想要释放的对象: delete p 执行两个动作: 销毁给定的指针,释放对应的内存。 动态对象的生存期直到被释放时为止 对于一个内置指针管理的动态对象,直到显式被释放之前它都是存在的 使用 new 和 delete 管理动态内存存在三个常见问题: 坚持只使用智能指针,就可以避免所有的问题,对于一块内存,只有在没有任何智能指针指向它的情况,智能指针才会自动释放它。 delete之后重置指针值 这只是提供了有限的保护 如果是 多个指针指向相同的内存,当释放掉该内存的话,所有的指针都是空悬指针,只置空一个是不合理的。 定义和改变 shared_ptr 的其他方法 不要混合使用普通指针与智能指针 shared_ptr 可以协调对象的析构,但仅 限于 自身的拷贝(也是 shared_ptr)之间,这也就是为什么我们推荐使用 make-shared 而不是 new 的原因。 不要用 get 初始化另一个智能指针或为智能指针赋值 如果使用智能指针,即使程序块过早(异常或者正常处理结束)的结束,智能指针作为局部变量都会被销毁,并释放其内存。 使用普通指针,则必须在程序块出问题前显式地去释放该内存。 智能指针与哑类 在 语言中也有一些没有被 定义析构函数的类,哑类,他们要求用户显式地释放所使用的任何资源。 使用 shared_ptr 来保证 一个没有定义析构函数的类能够自动释放资源。 使用我们自己的释放操作 默认情况下,shared_ptr假设它们指向的是动态内存。因此,当一个 shared_ptr 被销毁时,它默认对他管理的指针进行 delete 操作。 如果用 shared_ptr 来管理一个 例如关闭连接的操作 ,我们需要定义一个函数 替换 delete, 删除器必须接受一个 T* 的参数。 这样能保证,即使发生了错误或者异常, 连接也能正常关闭。 智能指针可以提供对动态分配的内存安全又方便的管理,但这也建立在正确使用的前提下。 一个 unique_ptr ”拥有“ 它所指的对象,只能有一个 unique_ptr 指向一个给顶对象,当unique_ptr unique 操作 注意的是: 不能拷贝和赋值的例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr 向 unique_ptr 传递删除器 我们必须在 尖括号中指向类型之后提供删除器类型,在创建和 reset 一个这种 unique_ptr 类型的对象时 weak_ptr 是一种不控制 所指向对象生存期的智能指针,它指向一个 shared_ptr的对象 weak_ptr: 我们需要一次为很多对象分配内存的功能。 为了至此这种需求,C++ 和标准库提供了两种一次性分配一个对象数组的方法。 建议:大多数容器都应该使用标准库容器而不是动态分配的数组,使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能 为了 new 分配一个对象数组,我们要在 类型名之后跟一对 方括号,在其中指明要分配的对象的数目。 int * p = new int[ size ] 动态数组并不是 数组类型,这点很关键 初始化动态分配对象的数组 默认情况下,new 分配的对象,不管是单个还是数组,全都是执行默认初始化。 我们还可以利用初始化列表进行初始化: bad_array_new_lenth ,在头文件 new 中。 动态分配一个空数组是合法的 动态分配大小为 0 的数组,new 返回一个合法的非空指针,此指针保证与 new 返回的其他任何指针都不同,对于零长度的数组来说, 此指针就像尾后指针一样。 释放动态数组 为了释放动态数组,使用 delete的特殊形式 – 在指针前加上一个空括号对 delete []pa; 智能指针与动态数组 标准库为 unique_ptr 来管理动态数组的版本,为了让unique_ptr管理动态数组,我们必须在对象类型后跟一对空方括号: 指向数组的 unique_ptr 指向数组的 shared_ptr (不建议) shared_ptr 不支持管理动态数组,如果希望使用 shared_ptr 管理动态数组的话,必须提供自定义的删除器。 shared_pt 未提供下标运算符,智能指针也不支持指针算术运算,为了访问数组元素,必须get 获取一个内置指针,用它来访问数组元素。 new 在使用上将 内存分配和对象构造组合在了一起 在实际使用动态数组时,我们往往不会令其等于相同的值,数组因为 new 在创建时执行力 构造,之后我们重复赋新值,会造成一些浪费,更重要的是没有默认构造函数的类不能动态分配数组。 引入需求: allocator 类 标准库 allocator 类定义在头文件 memory中,它帮助我们将内存分配和对象构造分离开。 alloctor 支持的操作: allocator 分配未构造的内存 我们需要在 allocator 分配的内存中构造对象。 拷贝与填充未初始化的算法 标准库还为 allocator 类定义了两个伴随算法,可以在未初始化内存中创建对象,以下算法都定义在 allocator 算法 开始一个程序的设计的的一个好方法是列出程序的操作。了解那些操作可以帮我们分析出需要 什么样的数据结构。从需求入手。 当定义一个类时,我们会显式地隐式地指定在此类型的对象拷贝,移动,赋值和销毁时做什么,一个类通过定义五种 特殊的成员函数来控制这些操作,包括: warning : 在定义任何C++类时,拷贝控制操作都是必要部分, 对初学 C++ 程序员来说,必须定义对象 如果我们不显式定义这些操作,编译器会自动帮我们定义,但编译器定义的版本的行为可能并非我们所想。 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。 合成拷贝构造函数 即使我们定义了构造函数,编译器也会为我们合成一个拷贝构造函数。 合成拷贝构造函数会将给顶对象依次将每个非 static 成员拷贝到正在创建的对象中。 每个成员的类型决定了它如何被拷贝: 类类型的成员会使用拷贝构造函数来拷贝 拷贝初始化 直接初始化: 要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数 拷贝初始化不仅在我们用 = 定义变量的时候会发生,在下列情况下也会发生: 当我们初始化标准库容器或是调用 insert 或 push 成员,容器会对此元素进行拷贝初始化,与之对应 参数与返回值 具有非引用类型的参数进行拷贝初始化 拷贝初始化被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。 :如果参数不是引用类型,初始化类类型对象,我们需要调用形参的拷贝构造函数,调用拷贝构造函数又会进行形参对自己的拷贝构造函数的传递,。。。。。无止境的传递建造 类类型对象形参。 拷贝初始化的限制 如果我们使用的初始化值要求一个 explict 的构造函数来进行类型转换,那么我们使用直接初始化与拷贝初始化就无关紧要了 编译器可以绕过拷贝构造函数 在拷贝初始化时,编译器可以(但不是必须) 跳过拷贝/移动构造函数,直接创建对象。 拷贝,移动构造函数必须是存在的且可访问的 (例如,不能是 private) 重载赋值运算符 重载运算符本质上是函数,其名字有 operator 关键字后接表示定义的符号组成,因此 拷贝赋值运算符接受一个与其类类型相同的参数,作为右侧运算对象,为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。 注意:标准库通常要求保存在容器中的类型要具有赋值运算符,其返回值是左侧运算对象的引用。 合成拷贝赋值运算符 利用编译器生成一个合成拷贝赋值运算符,它会将右侧运算对象的每个 非 static 成员赋值给左侧运算对象的对应对象。 析构函数执行与构造函数相反的操作: 了解析构函数: 析构函数是类的成员函数,名字由波浪号接类名构成,它没有返回值,也不接受参数: 由于析构函数不接受参数,因此它不能被重载,对于一个给定类,只会有唯一一个析构函数。 析构函数完成什么工作 一个构造函数,成员的初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化 析构函数,首先执行函数体,之后按照初始化顺序的逆序销毁成员。 注意: 隐式销毁一个内置指针类型,并不会delete 它所指向的对象,智能指针是一个类类型,所以会在析构阶段自动销毁 什么时候会调用析构函数 无论何时一个对象被销毁,就会自动调用其析构函数 析构函数自动运行,我们无须担心何时释放我们分配的资源。 合成析构函数 析构函数体并不会直接销毁成员,成员是在析构函数体之后隐式析构的。 需要析构函数的类也需要拷贝和赋值操作 当我们决定是否要定义它自身版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个析构函数。 通常情况下,析构函数的需求更加明显,如果一个类需要析构函数,我们可以几乎肯定它也需要一个拷贝构造函数和一个拷贝赋值函数。 需要拷贝操作的类也需要赋值操作,反之亦然 我们可以通过拷贝控制成员定义为 = default 来显式地要求编译器生成合成的版本。 ~SalesData() = default; 有些类拷贝与赋值没有合理的意义,这时,定义类时应该采用某种机制阻止拷贝或赋值。 不定义拷贝控制成员来阻止拷贝是无效的,编译器会自动合成 定义删除的函数 C++ 11新标准通过对拷贝构造函数和拷贝赋值运算符定义为 删除的函数 来阻止拷贝, 析构函数不能被指定删除 如果一个类有某个成员的类型删除了析构函数,我们不能定义该类的变量或临时对象,因为成员无法销毁,如果一个成员无法销毁,则对象整体也就无法销毁。 合成的拷贝控制成员可能是删除的 本质上,当不可能拷贝,赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。 private: 拷贝控制 希望阻止拷贝的类应该使用 = delete来定义他们自己的拷贝构造函数和拷贝赋值运算符,而不应该将他们声明为 private。 一般来说,对于管理类外资源的类的定义,我们需要明确器类型对象的拷贝语义。有两种选择: 值的行为: 意味着它应该有它自己的状态,拷贝一个像值的对象时,副本与原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然。 提供像只的行为,对于类管理的资源,每个对象都应该拥有自己的一份拷贝,这意味着对于资源,每个类的对象都i有一份资源的拷贝: 为了实现类值行为,需要: 类值拷贝赋值运算符 赋值操作会销毁左侧运算对象的资源,并从右侧运算对象拷贝数据。 编写赋值运算符建议: 对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的 资源。 引用计数的作用: 计数器放在动态内存中,由共享对象共享 定义一个使用引用计数的类 处理自赋值,首先递增 右侧对象引用计数,之后在进行递减 左侧引用计数,防止自赋值导致释放自身 除了定义拷贝控制成员,管理资源的类通常还定义一个名为 swap 的函数,与重排元素顺序的算法一起使用的类,这类算法在交换两个元素时会调用 swap 编写我们自己的swap 函数 由于 swap 的存在就是为了优化代码,我们将其声明为 inline函数是更好的 对于分配了资源的类, 定义 swap 操作是一种很重要的优化手段。 swap 函数应该调用 swap,而不是 std::swap 标准库版本的 swap 仅限于内置类型 而如果我们有自己类型特定的 swap函数,则调用 std:::swap 就是错误的,当我们引用了 标准库版本的swap (using std::swap) ,我们进行swap 操作时就会优先匹配自己定义的类类型,不会引发错误 在赋值运算符中使用 swap 定义 swap 的类通常用 swap 来定义他们的赋值运算符。这些运算符使用了拷贝并交换的技术。 这种技术是将左侧对象与右侧对象的一个副本进行交换 这个技术的有趣之处就是它自动处理了自赋值且天然就是异常安全的,它在改变左侧对象的之前拷贝右侧运算对象保证了自赋值的正确。 编写类程序时的步骤: 邮件处理应用: Message: 电子邮件 某个类需要运行时分配可变大小的内存空间,这些类一般可以通过标准库容器来保存他们的数据 当然这一策略并不是对每个类都使用;某些类需要自己进行内存分配,这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存 实现StrVec 类的设计 包含的功能: 包含的成员(猜测) 在重新分配内存的过程中移动而不是拷贝元素 由于我们从旧空间到新空间完毕后,旧空间的元素立马会被销毁,显然使用拷贝是不划算的 ,如果我们能避开分配和释放 这些元素的额外开销, 我们的 Vec 就会i性能上好很多。 移动构造函数和 std::move 新标准的一个主要特征是可以移动而非拷贝对象的能力,因为在很多情况下,对象拷贝之后就被销毁了,这些情况下,使用移动而非拷贝会大幅度提高性能, 所谓右值引用就是必须绑定到右值的引用。 返回左值的函数,赋值,下标,解引用和前置递增,递减运算符,都是返回左值的表达式的例子,我们可以将一个左值引用绑定到这类表达式的结果上。 返回非引用类型的函数,连同算术,关系,位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们能将一个 const 的左值引用或一个右值引用绑定到这类表达式上。 左值持久,右值短暂 左值有持久的状态,而右值不是字母常量就是 表达式求值的临时变量: 这两个特性: 使用右值引用的代码可以自由地接管所引用的对象的资源。 变量是左值 变量是左值, 因此我们不能将一个右值引用绑定到一个变量上,即使这个变量是右值引用也不行,毕竟,变量是持久的,直到离开作用域才被销毁 标准库 move 函数 我们可以将一个 左值显式地转换位对应的右值引用,我们还可以用一个 move 的新标准库函数来获得绑定到左值的 右值引用。 move 告诉编译器,我们有一个左值,我们希望像一个右值一样处理它。 注意:调用 move 就意味着承诺,除了对 源对象进行赋值和销毁它之外,我们不能使用它。 我们可以定义自己的类的移动操作,从给定对象中”窃取“资源而不是拷贝资源,会从中受益。 noexcept :表示标准库我们的构造函数不抛出异常 移动操作,标准库容器与异常 由于 移动操作 ”窃取资源“ ,它通常不分配任何资源,因此移动操作不会排除异常,当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库,我们将看到,除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为我们移动时抛出异常,为了处理这一工作可能性做出额外的工作。 通知标准库我们的移动操作不抛出异常的方式是 指明 noexcept (C++11 新标准),我们在函数的参数列表后指定 noexcept, 不抛出异常的移动构造函数和移动赋值运算符必须标记位 noexcept 为了避免出现元素在容器重新分配内存后使用移动构造函数发生异常,容器如果在没有显式地指出元素类型的移动构造是安全的,会调用拷贝构造函数。 移动赋值运算符 移动赋值运算符完成于 析构函数和移动构造函数相同的工作,标记位 noexcept,且必须正确处理自赋值 移动源对象必须可析构 在移动操作之后, 移动源对象必须保存 有效,可析构的状态,但是用户不能对其值进行任何假设。 合成的移动操作 只有当一个类没有定义自己的拷贝控制成员,且它的所i有数据成员是能够移动构造或是能移动赋值的话,编译器才会为它合成移动构造函数或移动赋值运算符。 原则: 定义了移动构造函数或赋值运算符的类必须定义自己的拷贝操作,否则这些拷贝成员默认是被定义为删除的。 移动右值,拷贝左值,但如果没有移动构造函数,右值也会被拷贝 如果一个类有一个可用的拷贝构造函数而没有移动构造函数,其对象是通过拷贝构造函数 来 ”移动“的。 拷贝并交换赋值运算符和移动操作 当我们定于i一个拷贝并交换的拷贝赋值运算符,那么如果我们为其添加一个移动构造函数,它实际上也会获得一个移动赋值运算符。 所有五个拷贝控制成员都应该看作是一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义五个操作。 某些类(含有动态内存):必须定义拷贝控制函数和析构函数才能正常工作,这些类通常都会有一个资源,而拷贝成员必须拷贝此资源。拷贝一个资源会导致一些额外的开销,在这种拷贝并非必须的情况下,定义了移动操作就可以避免该问题。 移动迭代器 新标准定义了一种移动迭代器适配器 ,通过改变给定迭代器的解引用运算符的行为来适配此迭代器。(移动迭代器解引用为右值引用) 我们通过调用标准库的 make_move_iterator函数来将一个普通迭代器转换为一个移动迭代器。 由于我们传递给 uninitialized_copy 的是移动迭代器,因此我们 construct将使用 移动构造函数来构造元素。 注意: 如果一个成员函数同时提供拷贝和移动版本,那么它也能从中受益。 一般来说,我们可用通过参数为 T& 类型的函数,当需要传递右值时,我们直接将实参 std::move 即可 当使用移动版本时,参数会精准匹配。 右值与左值引用成员函数 新标准仍然允许向右赋值,但是,我们可能希望在自己的类中阻止这种做法。我们希望强制左侧运算对象(this) 是一个左值。 在参数列表后放置一个 引用限定符 可用指出该返回的 this 是可用指向左值还是右值。 类似于 const 限定符,引用限定符只能用于 非 static 成员函数,并且必须同时出现在函数的声明和定义中。 一个成员函数 可用同时用于 const 与引用限定,引用限定符必须跟随在 const 限定之后。 重载与引用函数 引用限定符也可以区分重载版本。 一个一个成员函数有引用限定符,具有相同参数列表的重载版本都必须有引用限定符。 (变量是左值,临时量是右值) 当运算符被用于类类类型的对象时,C++语言允许我们为其指定新的含义,同时,我们也能定义类类型之间的转换规则,和内置类型的转换一样,类类型转换隐式地将一种类型的对象转换成另一种我们所需类型的对象。 直接调用一个重载的运算符函数 我们能像普通函数一样直接调用运算符函数,先指定名字,然后传入数量正确类型适当的实参 如果是像调用成员函数一样显式地调用成员运算符函数,具体做法,指定运行函数的对象的名字,使用点运算符(箭头运算符)访问调用的函数 某些运算符不应该被重载 通常情况下,不应该重载 逻辑与和逻辑或运算符,因为会导致求值顺序无法保留 使用与内置类型一致的含义 在设计类时明确这个类需要的操作,之后再思考是将其操作定义为普通函数还是重载运算符。 在逻辑上与运算符相关的类,他们适合定义成重载的运算符: 只有当操作的含义对于用户来说清晰明了时才使用运算符,如果用户对运算符有几种不同的理解,则使用这样的运算符将产生二义性。 赋值于复合赋值运算符 重载的符合赋值运算符,应该于内置保持一致 选择作为成员或非成员函数 注意: 输出运算符尽量减少格式化操作 通过将对象置为合法的状态,我们能保护使用者免于输入错误的影响,此时的对象处于可用状态,它的成员是被正确定义的,而且对象不会产生误导性的结果。 算术和关系运算符定义成非成员函数以允许左侧或右侧运算对象进行转换,因为这些运算符一般不需要改变运算对象状态,形参均为常量引用。 如果一个类在逻辑上有相等性的含义,则类应该定义 operator==,这样做可用使得用户更容易使用标准库算法来处理该类。 如果存在唯一一种逻辑可靠的 < 定义,则考虑应该为这个类定义 < 运算符,如果类同时还包含 == ,当且仅当 < 的定义和 == 产生的结果一致时才定义 < 运算符。 == 的规则与 < 的规则应该保持一致,否则不能算逻辑可靠 我们可用重载赋值运算符(参数不同)不管形参类型是什么,赋值运算符都必须定义为成员函数。 复合赋值运算符 复合赋值运算符与赋值运算符都是成员函数,这两类运算符都应该返回左侧运算对象的引用。 表示容器的类通常可以通过元素在容器中的位置访问元素,一般会定义下标运算符 【】 包含下标运算符的类,会定义两个版本(一个返回普通引用,另一个是类的常量成员且返回常量引用) 递增递减运算符一般在迭代器类中实现 定义前置递增/递减运算符 区分前置与后置运算符 由于前置后置使用同一种符号,且运算对象的数量与类型都相同,因此为了区分重载, 在迭代器类及智能指针类会用到 解引用运算符(*) 与 箭头运算符(->) 对箭头运算符返回值的限定 箭头运算符不能定义获取成员额外的操作,只能用作成员访问 可以改变箭头从哪个对象中获取成员。 除此之外, point->mem 的执行操作: 注意: 重载箭头运算符必须返回类的指针或者自定义箭头运算符的某个类的对象。 我们 可以像使用函数一样使用重载了函数调用运算符的类的对象,因为这样的类也能存储状态。 含有状态的函数对象类 可以设定数据成员来完成定制调用运算符中的操作 函数对象常常作为泛型算法的实参,满足其实参的类型(参数列表以及返回值)即可 lambda 会被编译器翻译成一个 未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符 也可以说 lambda 表达式就是一个实际上就是一个重载了函数调用符的类 表示 lambda 及相应捕获行为的类 注意: lambda 表达式产生的类不含有默认构造函数,赋值运算符及默认析构函数,它是否含有默认的构造/移动构造函数则通常要视捕获的数据成员类型而定。 在算法中使用标准库函数对象 表示运算符的函数对象常用来替换算法中的默认运算符 标准库规定其函数对象对于指针同样适用。 C++语言中有几种可调用的对象: 函数,函数指针,lambda 表达式,bind 创建的对象以及重载了调用运算符的类。 不同类型的可调用对象却可能共享同一种调用形式,调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。 int (int, int) 不同类型可能具有相同的调用形式 对于共享相同调用形式的可调用对象,我们希望把他们看成具有相同的类型 调用形式: int (int, int ) 如果我们试图利用 调用形式 int(int,int) 来 存储这些可调用对象,就会产生不匹配的情况,(lambda 与 重载了可调用操作符的类与函数指针类型不符) 标准库 function 函数 我们可以使用 名为 function 的新的标准库函数来解决以上问题,定义在 functional 头文件中 function 的操作 重载的函数与function 我们能通过构造函数隐式地将实参类型转换为类类型,也能同样定义对于类类型的类型转换 类型转换运算符 是类的特殊成员函数,它负责将一个类 类型的值转换为其他类型,形式如下 operator type() const; note: 一个类型转换函数必须是成员函数,不能声明返回类型,形参必须为空,且应该是 const 成员 定义含有类型转换运算符的类 使用类型转换运算符 注意:尽管编译器一次只能执行一个用户定义的类型转换,但隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后。 明智地使用类型转换运算符能极大简化类设计者的工作, 同时使得使用类更加容易,然而如果在类类型和转换类型之间不存在明显的映射关系,这样的转换会具有误导性。 类型转换运算符可能产生意外结果 这时 cin会转换为 bool类型,bool类型转换为 0,1, 之后进行移位操作 将 0,1左移 42 显式的类型转换运算符 为了防止这样的异常情况发生, C++11 新标准引入了 显式的类型转换运算符(explict conversion operator): 转换为 bool 像 bool 类型转换通常用于条件部分,因此 operator bool 一般定义为 explicit的 如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型只存在唯一的一种转换方式,否则我们编写的代码很可能有二义性。 一言则蔽之:除了显式地向 bool 类型的转换之外,我们应该避免定义类型转换函数,并尽可能限制那些 “显然正确” 的非显式构造函数。 重载函数与转换构造函数 如果 两个参数或多个类型转换都提供了同一种可行匹配,则这些类型转换一样好。 如果在调用重载函数时我们需要使用构造函数或者强制类型转换来改变实参的类型,这说明我们程序设计存在不足。 重载函数与用户定义 类型转换 因为调用重载函数所请求的用户定义的类型转换不止一个且彼此不同, 所以调用具有二义性。 重载的运算符也是重载的函数,因此,通用的函数匹配规则同样适用与判断给顶表达式中到底应该使用内置运算符还是重载的运算符。 表达式中运算符的候选函数既应该包括成员函数,也应该包括非成员函数 与内置类型函数。 如果我们对同一个类进行了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。 面向对象程序设计(object - oriented programing)的核心思想是 数据抽象,继承和动态绑定。 数据抽象: 可以实将 类接口和实现分离。 继承 动态绑定 在C++语言中,当我们使用基类的引用(指针)调用一个虚函数时发生动态绑定。 基类通常会定义一个虚析构函数,即使该函数不执行任何实际操作也要如此 成员函数与继承 派生类可以继承基类的成员,当需要虚函数时,派生类必须重新提供自己的新定义以覆盖(override)从基类继承而来的旧定义。 C++中,基类需要分开两种函数: 一种是基类希望派生类进行覆盖的函数,这类定义为虚函数,当我们使用指针或基类调用虚函数时,该调用将被动态绑定,根据引用或指针绑定的对象类型不同,该调用执行不同的版本 另一种是基类希望派生类直接继承而不要改变的函数。 访问控制与继承 派生类能访问公有成员与保护成员,而不呢访问私有成员,用户仅能访问公有成员 派生类必须通过类派生列表明确指出它是从哪个基类继承而来。 形式: (首先是冒号,后面紧跟以逗号分割的基类列表, 每个基类前面可以是三种访问说明符中的一个 : 派生类中的虚函数 派生类对象及派生类向基类的类型转换 在一个对象中,继承自基类的部分和派生类定义的部分不一定是连续存储的。 note : 在派生类对象中有基类组成部分, 这一事实是继承的关键所在。 派生类构造函数 每个类控制它自己的成员初始化过程,因此我们必须使用基类的构造函数来初始化派生类的基类组成部分。 note: 首先初始化基类的部分, 然后按声明的顺序依次初始化派生类的成员。 派生类使用基类的成员 派生类可以使用基类的 公有与 保护成员 必须明确: 每个类负责定义各自的接口, 要想与类的对象交互必须使用该类的接口,即使这个对象是派生类的基类部分也是如此。 继承与静态成员 静态成员数据在整个继承体系中只存在该成员的唯一定义。 静态成员遵循通用的访问控制规则, 如果基类中的成员是 private的,则派生类无权访问它。如果静态成员是可访问的,我们则通过基类或者派生类均可访问。 派生类的声明 一条声明语句的目的是让程序知道某个名字的存在 以及 改名字表示一个什么样的实体,如一个类,一个函数,一个变量等,派生列表以及与定义有关的其他细节必须在类的主体一起出现 被用作基类的类 原因: 派生类包含并且可以使用它从基类继承而来的成员,为了使用这些成员, 派生类必须知道他们是什么。 这层含义的隐含意思(一个类不能派生它本身) 防止继承的发生 C++ 11 新标准提供了防止继承发生的方法,在类名后添加一个 关键字 final: 理解基类与派生类之间的类型转换是理解 C++语言面向对象编程的关键所在。 可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义: 当使用基类的指针或引用时,我们不清楚该绑定对象的真实类型,该对象可能是基类对象或派生类对象 note : 智能指针也支持派生类向基类的类型转换, 基类的智能指针能存储派生类对象的指针 静态类型与动态类型 静态类型: 在编译时总是已知的, 它是变量声明时的类型或是表达式生成的类型。 note: 基类的指针或引用的静态类型可能与其动态内存不一致。 不存在从基类到派生类的隐式类型转换 因为一个基类的对象可能是派生类对象的一部分, 也可能不是; 所以不存在从基类向派生类的自动类型转换: 在对象之间不存在类型转换 派生类向基类的自动类型转换只对指针或引用类型有效, 在派生类类型和基类类型之间不存在这样的转换。 代码中演示的操作会忽略掉 派生类的子成员,也可以说 派生类的部分被切掉了,仅传递 其中基类成员。 warning: 当我们用一个派生类对象作为一个基类对象初始化或赋值时, 只有该派生类对象中基类部分会被拷贝, 移动或赋值, 它的派生类部分将被忽略掉。 对虚函数的调用可能在运行时解析 被调用的函数是与绑定到指针或引用上的对象的动态绑定相匹配的哪一个。 OOP 的核心思想是 多态性。 对非虚函数的调用在编译时进行绑定。 类似的,通过对象进行的函数调用也在编译时绑定,对象的类型是确定不变的, 都不可能使得对象的动态类型与静态类型不一致。 notae: 当且仅当通过指针或引用调用虚函数时,才会在运行时解析该调用, 也只有这种情况下对象的动态类型与静态类型可能会不同。 派生类中的虚函数 例外: 当类的虚函数返回类型是类本身的指针或引用时,规则无效,但返回类型要求从派生类本身指针或引用到 基类的指针或引用的类型转换是可访问的。 final 与 override 说明符 派生类定义了一个函数与基类虚函数名字相同但形参列表不同,这仍然是合法行为。 编译器将认为新定义的函数与基类的虚函数是相互独立的,就编程习惯而言,这种声明意味着错误。 调试以上错误十分困难,因此 c++ 11 新标准中我们可以使用 override 关键字来说明派生类中的虚函数,好处是 使得程序员的意图更加清晰的同时让编译器为我们发现了一些错误,如果我们使用 override标记了某个函数, 但该函数没有覆盖已经存在的虚函数,此时编译器报错。 final 可以将某个函数指定为最后覆盖, 之后任何尝试覆盖该函数的操作都将引发错误。 note: final 与 override 说明符在形参列表 (包括 const 和引用修饰符) 以及尾置返回类型之后。 虚函数与默认实参 虚函数的默认实参(可提供),实参值由本次调用的静态类型决定。 如果我们使用基类引用或指针调用函数,则不管运行的是派生类的函数 均使用基类的默认实参 建议: 如果虚函数使用默认实参, 则基类和派生类定义的默认实参最好一致。 回避虚函数的机制 我们希望虚函数的调用不进行动态绑定, 而是强迫其执行虚函数的某个特定执行版本。 通常情况下,只有成员函数(友元)中的代码才需要使用作用域运算符来回避虚函数的机制。 纯虚函数 当我们需要一种 通用概念的类时而非具体操作的类,这种类我们定义其 纯虚函数 来表示这种通用概念。 含有纯虚函数的类是抽象基类 含有(未经覆盖直接继承) 纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口。 派生类构造函数只出始化它的直接基类 派生类除了初始化它自身,也会初始化它的直接基类,之后它的直接基类会初始化它的直接基类。。 从最底层的基类开始初始化逐层到最后的派生类对象。 在基类的继承体系中加入 抽象基类实现了 重构。 重构负责重新设计类的体系以便将操作和 / 数据从一个类移动到另一个类。 注意点: 即使我们改变了整个继承体系, 那些使用了 基类与 派生类的代码无须任何改动,不过一旦类被重构, 我们必须重新编译含有这些类的代码 每个类还分别控制着成员对于派生类是否可访问 受保护的成员 派生类的成员和友元能访问派生类对象中基类部分的受保护成员,对于普通的基类对象的成员不具有特殊的访问控制。 公有,私有和受保护继承 一个类对其继承而来的成员的访问权限受到两个因素影响: 一个是基类中该成员的访问说明符,二是派生类在派生类列表中的访问说明符。 派生类向基类转换的可访问性 派生类向基类的转换是否可访问由使用该转换的代码决定, 同时派生类的派生访问说明符也会有影响。 总结: 对于代码中的某个给顶节点来说,如果基类的公有成员是可访问的, 则派生类向基类的类型转换也是可访问的,反之则不行。 普通用户: 编写 的代码使用类的对象,这部分代码只能访问类的 公有(接口)成员 考虑继承的话会出现第三种用户 : 派生类 基类把它希望派生类能够使用的部分声明为受保护的。 普通用户不能访问受保护的成员,而派生类及其友元仍旧不能访问 私有成员 友元与继承 不能继承友元,每个类负责各自成员的访问权限。 派生类的友元也不能随意访问基类的成员 改变个别成员的可访问性 有时我们需要改变派生类继承的某个名字的访问级别,通过使用 using 声明 默认的继承保护级别 struct 与 class 区别 默认成员访问说明符: struct public class 是 private 在编译时进行名字查找 当我们进行调用成员时,是在编译时进行名字查找,只看当前的静态成员的作用域,如果里面没有,则报错 名字冲突与继承 派生类的成员将因此同名的基类成员 通过作用域运算符来使用隐藏的成员 我们可以使用作用域来使用被隐藏的基类成员: strcut Derived: Base { 作用域运算符将覆盖原有的查找规则,提示编译器从 Base 类的作用域开始查找 mem 建议: 除了覆盖继承而来的虚函数之外, 派生类最好不要重用其他定义在基类中的名字。 假定我们调用 p->mem() (或者 obj.mem()),则依次执行以下 4 个步骤: 一如往常,名字查找先于类型检查 虚函数与作用域 因为名字查找先于类型检查,,我们在派生类中找到该名字后,就不会继续搜索基类的虚函数,如果基类与派生类的虚函数接受的实参不同,就会报错。 通过继承调用隐藏的虚函数 覆盖重载的函数 成员函数无论是否是虚函数都能被重载。 派生类可以覆盖重载函数的 0 个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的, 那么它需要覆盖所有的版本,或者一个也不覆盖。 如果我们不得不覆盖类i中 的每个版本的话,显然操作将极其繁琐。 一种好的解决办法是为重载的成员提供一条 using 声明语句,这样我们就无须覆盖基类中每一个重载版本了。 使用: using 声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的 using 声明语句就把所以重载实例添加到派生类作用域中。此时派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义。 类内 using 声明的一般规则同样适用于重载函数的名字,基类函数的每个实例在派生类中都必须是可访问的 对派生类没有重新定义的重载版本访问实际上是对 using 声明点的访问。 因为 基类指针指向的可能是 派生类,(静态类型与动态类型不符),如果派生类中含有动态内存,当我们 delete 其指针,我们需要确定其执行的析构函数的正确性,所以需要定义虚析构函数,在对象调用时,进行动态绑定,安全地析构 虚析构函数将阻止合成移动操作 如果一个类定义了析构函数,那么即使它通过 = default 使用了合成的版本,编译器也不会为这个类合成移动操作。 派生类的合成的拷贝控制成员,析构函数除了自身的操作之外,还负责使用直接基类中对应的操作对一个对象的直接基类部分进行 初始化赋值,析构的操作 合成的Bulk_quote(第三代) 默认构造函数运行 Disc_quote 的默认构造函数,后者在执行 Quote(d第一代)的默认构造函数 类似的 拷贝控制也是相同 对于派生类的析构函数来说,他除了销毁自身,也负责销毁派生类的直接基类,直到顶端。 派生类中删除的拷贝控制与基类的关系 移动操作与继承 显式定义后,除非该类的派生类含有排斥移动的成员,否则它将自动获得合成的移动操作 定义派生类的拷贝和移动构造函数 在默认情况下,基类的默认构造函数初始化派生类对象的基类部分,如果我们向(拷贝或移动)基类部分,也要在派生类的构造函数初始化列表中显式地使用 基类的拷贝(移动)构造函数 派生类赋值运算符 值得注意的是,无论基类的构造函数或赋值运算符是自定义的版本还是合成的,派生类的对应操作都能使用它们。 例如,对于 Base::operator= 的调用语句将执行Base的拷贝赋值运算符,至于该运算符是显式或编译器合成无关 派生类析构函数 对象销毁的顺序与创建的顺序相反:派生类析构函数首先执行,之后在进行基类的析构,依次向下到基类 在构造函数和析构函数中调用虚函数 如果构造函数或析构函数调用了某个虚函数, 则我们应该执行与 构造函数或析构函数所属类型相对应的虚函数版本 在C++新标准中,派生类能重用其直接基类定义的构造函数,提供一条著名了 (直接)基类名的 using 声明语句。 如果派生类还有自己的数据成员,则这些成员将被默认初始化。 继承的构造函数的特点 一个构造函数的 using 声明不会改变该构造函数的访问级别。 一个using 声明语句不能指定 explict 和 constexpr,如果基类含有这些,那么继承的构造函数也有相同的属性 当一个基类构造函数含有默认实参,这些实参不会被继承,而会生成多个继承的构造函数,一个生成不含默认实参的参数的列表,一个生成去掉带默认实参的参数的列表的构造函数。 如果派生类定义的构造函数与基类构造函数具有相同的参数列表,那么该构造函数不会被继承 默认,拷贝,移动构造函数不会被继承, 这些构造函数按照正常规则被合成,继承的构造函数不会作为用户定义的构造函数来使用,因此,如果一个类只含有 继承的构造函数,那么他也拥有一个合成的默认构造函数。 当派生类对象被赋值给基类对象时,其中的派生类部分被 “切掉”,因此容器和存在继承关系的类型无法兼容 在容器中存放(智能)指针而非对象 当我们希望在容器中存放具有继承关系的对象时,实际上存放的通常是基类的指针(最好选择智能指针)。 面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况,不同之处在于: 模板是C++ 泛型编程的基础,一个模板就是一个创建类与函数的蓝图或者说公式。 对于以上仅仅差异是参数的类型,函数体的执行内容则完全一样,如果我们希望执行的是不同的类型,那么就需要定义完全一样的函数体,是非常繁琐的 我们可以定义一个通用的 函数模板,一个函数模板就是一个公式,来生成针对特定类型的函数版本。 模板定义: 关键字 template开始, 后跟一个模板参数列表,这是一个逗号分割的一个或多个模板参数的列表, 用 (< >)包裹起来。 实例化函数模板 悟道: 小熊饼干是通过小熊模板实例化的,小熊饼干只需要通过 小熊模板的 实参(奶油味与 花生味的调理)生成对应的即可。 模板类型参数 模板类型参数(类型说明符):可以用来指定返回类型与函数的参数类型, 以及函数体内用于变量声明或类型转换。 悟道: class 与 typename 都可以用来定义类型参数,目前是主要以 typename为主。 非类型模板参数 可以在模板定义时 定义非类型参数。 inline 和 constexpr 的函数模板 函数模板也可以声明为 inline 和 constexpr 的,如同非模板函数一样。 inline 或 constexpr 说明符应该放在模板参数之后, 返回类型之前。 编写与类型无关的代码 编写泛型代码的两个重要原则: 模板的函数参数是 const 的引用 函数体中的条件判断仅仅使用 < 比较运算 建议: 模板程序应该尽量减少对实参类型的要求。 模板编译 模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件, 以及用来实例化模板的任何类型的头文件。 大多数编译在实例化期间报告 模板到实例化时才生成代码,这一特性影响了我们何时才获知模板内代码的编译错误。 通常在三个阶段报告错误: 注意点: 保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。 定义 类模板 实例化类模板 在模板作用域中引用模板类型 为了阅读模板类代码, 应该记住类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。 嵌套模板的实参应该是实例的类类型(通过类模板实例化出的) 类模板的成员函数 示例: 类模板成员函数的实例化 在类代码内简化模板类名的使用 我们在使用类模板类型时必须提供模板实参, 但例外是,在类模板自己的作用域中, 我们可以直接使用 模板名而不提供实参: 当我们处于一个类模板的作用域中, 编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。 在类模板外使用类模板名 类模板和友元 一对一的友好关系 类模板 与另一个(类或函数)模板间 的友好关系的最常见的形式是建立对应实例及其友元间的友好关系。 我们将 BlobPtr 与 operator==声明为自己的友元,对其对应实例都为自己的友元,而非相同 类型的实例则没有特殊访问权限 通用和特定的模板友好关系 一个类也可以将另一个模板的每一个实例都声明为自己的友元, 或者限定特定的实例为友元: 令模板自己的类型参数称为友元 在新标准中,我们可以将模板类型参数声明为友元: 模板类型别名 由于模板不是一个类型, 我们不能定义一个typedef 引用一个模板,。 新标准允许我们为类模板定义一个类型别名 由于pair 的成员类型是相同的,我们只需要一次指定, 我们也可以固定一个或多个模板参数 类模板的 static成员 一个模板参数的名字没有什么内在含义,我们将类型参数命名为T,但实际上我们可以使用任何名字: 模板参数与作用域 模板声明 建议:一个特定文件所需的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。 使用类的类型成员 在非模板的代码中,编译器掌握类的定义,因此他知道通过作用域运算符访问的是名字是类型还是 static成员。 默认情况下,C++语言假定通过作用域运算符访问的名字不是类型,因此我们希望使用一个模板类型参数的类型成员, 就必须显式告诉编译器该名字是一个类型。使用 typename 来实现这一点 默认模板实参 在新标准中,我们可以为 函数和类模板提供默认实参。 模板默认实参与类模板 一个类(普通或者类模板)可用包含本身是模板的成员函数,这种成员被称为成员模板,成员模板不能是虚函数。 普通(非模板)类的成员模板 类模板的成员模板 对于类模板,我们可用为其定义成员模板,在此情况下,类和成员各有自己的独立的模板参数。 实例化与成员模板 为了实例化一个类模板的成员模板, 我们必须同时提供类和函数模板的实参。 多文件实例化相同模板的开销是很严重的, 在新标准中,我们通过显式实例化来避免这种开销。 编译器使用一个模板时自动对其实例化, 因此 extern 声明必须出现在任何使用该实例化版本的代码之前: 实例化定义会实例化所有成员 unqiue_ptr 在编译时绑定删除器,避免了间接调用删除器的运行开销 从函数实参来确定模板实参的过程被称为 模板实参推断 将实参传递给带模板类型的函数形参时, 能狗自动转换的只有 const 转换及数组或函数到指针的转换。 //形参或是实参 的顶层 const 都会被忽略掉。算是转换与 派生类到基类的转换用户定义的转换都不能应用于函数模板 使用相同模板参数类型的函数形参 一个模板类型参数可以用于多个函数形参的类型,由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,那么调用就是错误的。 如果希望允许函数实参进行正常的类型转换,将函数模板定义为多个类型参数 正常类型转换应用普通函数实参 如果函数参数类型不是模板参数, 则对实参进行正常的类型转换。 在某些情况下,编译器无法推断模板实参类型,希望允许用户控制模板实例化。 当函数返回类型于参数列表中任何类型都不相同时。 指定显式模板实参 正常类型转换应用于显式指定的实参 当我们希望用户确定返回类型时,用显式模板实参表示其返回类型是很有效的。 进行类型转换的标准库模板类 为了获得 元素类型,我们可用使用 标准库的 类型转换模板。定义在 type_traits中。 如果不能从函数指针的类型确定模板实参(二义性或其他),则发生错误。 这时需要消除二义性,使用显式地模板实参即可。 从左值引用函数参数推断类型 从右值引用函数参数推断类型 当一个函数参数是右值引用,正确绑定规则 于类型推断过程一样,都是需要我们传递给其右值。 引用折叠和右值引用参数 通常我们不能将一个右值引用绑定到一个左值上。 但是,C++语言正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个规则是 move 这种标准库设施正确工作的基础。 T& & = X& T&& && == X&& 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数 这两个规则导致了两个重要结果: note:如果一个函数参数是指向模板参数类型的右值引用(T&&),则可用传递给它任意类型的实参。如果将一个左值传递给这样的参数, 则函数参数被实例化为一个普通的左值引用。 编写接受右值引用参数的模板类型 由于右值引用参数,模板类型会推断 其类型参数为 左值或右值,这在函数体中编写通用的正确的代码就 实际上右值引用通常用于两种情况: 模板转发其实参或模板被重载 使用右值引用的函数模板要进行下面的重载进行区分: 标准库move 函数是使用 右值引用的模板的一个很好的例子。 std::move 是如何定义的 由于 move 的函数参数 T&& 是指向模板类型参数的右值引用,根据折叠原则,它可用接受左值与右值均可。 std::move 是如何工作的 std::move(string(“bye”))! std::move(str) 从一个左值 static_cast 到一个右值引用是允许的 右值引用特许规则: 可用用static_cast 显式地将一个左值转换为一个右值引用。 建议;: 统一地使用 std::move 使得我们在程序中查找潜在的截断左值的代码变得很容易。 某些函数需要将其一个或多个实参连同类型不变地转发给其他函数,在此情况下,我们需要保持被转发实参的所有性质, 包括实参类型是否是 const 的以及实参是左值还是右值。 如果是这样的话,非引用反转,不会改变原值。 定义能保持类型信息的函数参数 如果一个函数参数是指向模板参数的右值引用,它对应的实参的const 属性与左值/右值属性都会得到支持。 这个版本的 flip2 对于接受左值引用的函数工作的很好,但不能用于接受右值引用参数的函数。 在调用中使用 std::forward 保持类型信息 我们可用使用一个名为 forward 的新标准库设施来传递 flip2的参数, 它能保持原始实参的类型。 与std::move相同,要使用 std::forward 而不使用 uisng 声明 函数模板的函数匹配规则: 编写重载模板 重载模板和类型转换 缺少声明可能导致程序行为异常 在定义任何函数之前,记得声明所有重载的函数版本。 这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。 一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。 可变数目的参数称为参数包。 存在两种参数包: 模板参数包 与 函数参数包。 在每个实例中, T 的类型都是从第一个实参类型推断而来的。 剩下的实参提供函数额外实参的数目与类型。 sizeof…运算符 sizeof…() 返回一个常量表达式,不会对实参求值,解析包中元素数量 当我们即不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的 我们可用将 tuple 看成一个 “快速而随意” 的数据结构。 tuple 的用途: 当我们希望将一些数据组合成单一对象, 但又不想麻烦地定义一个新数据结构来表示这些数据时, tuple 是非常有用的。 演示: bitset 类使得 位运算的使用更为容易, 并且能够处理超过最长整型类型大小的位集合。 bitset 定义在 头文件 bitset中 初始化 bitset note: string 的下标编号习惯于 bitset 恰好相反, string 下标最大的字符初始化 bitset 中的低位。 bitset 操作 bitset操作定义了多个检测或者设置一个或多个二进制的方法。 使用 bitset 最全的常用正则表达式大全 史上最全的正则表达式-匹配中英文、字母和数字 正则表达式是一种描述字符序列的方法,是一种及其强大的计算工具。 C++正则表达式库(RE):是新标准的一部分, RE库定义在头文件 regex中,它包含多个组件。 使用步骤: 指定 regex 对象的选项 定义一个 regex 或是 对 regex 调用 assgin 为其赋予新值时,可用指定标志位来影响 regex 如何操作 演示: 识别扩展名以及其他普通文件扩展名 指定或使用正则表达式的错误 演示: 正则表达式的错误类型: 建议: 避免创建不必要的正则表达式,正则表达式的编译是非常慢的操作。 正则表达式类和输入序列类型 RE 为不同的输入序列类型定义了对应的类型,且 RE库必须与输入序列类型匹配。 不匹配的RE库与输入序列组合在一起就会引发错误,这里我们应该使用 cmatch 存储查找后的细节 我们可以使用 Regex 的迭代器来获得输入序列的所有匹配 sregex_iterator 操作 使用 sregex_iterator 使用 匹配的数据 it 解引用是一个 smatch 类型,我们可用使用其匹配的数据 正则表达式中的模式通常包含一个 或 多个 子表达式,一个子表达式是模式的一部分,本身也具有意义。 正则表达式语法通常用括号表示子表达式, 子表达式从 1 索引开始, 0索引代表整个匹配到的表达式 子表达式用于数据验证 下面的代码读取一个文件,并用此模式查找完整的电话号码模式匹配的数据,并调用 valid 函数来检查号码是否有效。 子匹配的操作 当我们希望在输入序列中查找并替换一个正则表达式时,可用调用 regex_replace. 用来控制匹配和格式的标志 定义在头文件 radom 中的随机数库通过一组协作的类来解决这些因为 (非均匀分布的数而导致引入了非随机性)的问题: 随机数引擎类 和 随机数分布类 建议: C++程序不应该使用 库函数 rand,而应该使用 default_random_engine 类和恰当的 分布类对象 引擎提供给分布进行随机 此外还有很多使得不同分布的概率随机不同的模板程序库 本节,我们来学习 三个更特殊的 IO 特性:格式控制,未格式化 IO 和随机访问 控制布尔值的格式 将 bool 类型 输出以 字母方式打印 指定整型值的进制 在输出中指定进制 利用 nouppercase/uppercase showbase/noshowbase dec 能改变输出的格式问题 控制浮点数格式 我们能控制浮点数输出三种格式: 默认情况下,浮点数以六位数字精度打印,如果没有小数部分,则不打印小数点 指定打印精度 在打印浮点值时按 当前精度舍入而非 截断 通过 IO 对象的 precision 成员或使用 setprecision 操纵符来改变精度。 操纵符 setprecision 和其他接受参数的操纵符都定义在 头文件 iomanio 中 指定浮点数计数法 建议:除非你需要控制浮点数的表示形式,不然由标准库选择计数法是最好的方式 打印小数点 showpoint操纵符强制打印小数点,noshowpoint恢复 输出留白 定义在 iomanip 的操纵符 控制输入格式 默认情况下,输入运算符会忽略空白符(空格符, 制表符,换行符,换纸符和回车符) 操纵符 noskipws 会令输入运算符读取空白符, skipws 会恢复默认 大规模编程对程序设计语言的要求更高。 异常处理 机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理。 在C++中,我们通过抛出一条表达式来引发一个异常, 被抛出的表达式的类型以及当前的调用链共同绝对零度哪段处理代码将被用来处理该异常。 栈展开 栈展开过程中对象被自动销毁 析构函数与异常 异常对象 -异常对象是一种特殊的对象, 编译器使用异常抛出表达式对异常对象进行拷贝初始化。 throw 表达式必须拥有完全类型,如果是类类型,则类必须含有一个可访问的 析构函数,和可访问的拷贝或移动构造函数,如果表达式是数组或是函数类型,表达式会转换为指针类型 catch 子句的异常声明看起来像是只包含一个形参的函数形参列表。 建议: 异常声明的静态类型将决定 catch 语句所能执行的操作,如果 catch 接受的异常与某个继承体系有关,最好将 catch 的参数定义为引用类型。 查找匹配的处理代码 异常和 catch 异常声明的匹配规则受到限制,只允许以下精准匹配: 重新抛出 有时一个单独的 catch 语句不能完整地处理某个异常, 在执行了某些校正操作之后,当前的 catch 可能会决定由调用链更上一层的函数接着处理异常。 一条 catch 语句通过重新抛出的操作将异常传递给另一个 catch 语句,这里的重新抛出仍然是一个 throw 语句,只不过不包含任何表达式: throw; 只有当 catch 异常声明是引用类型我们对参数的改变才会保留并传递。 捕获所有异常的处理代码 为了一次性捕获所有异常, 我们使用省略号作为异常声明, 这样的处理代码称为 捕获所有异常的处理代码。 构造函数在进入函数体之前首先执行初始值列表。 因为在初始值列表抛出异常时构造函数体内的 try 语句块还未生效, 所以构造函数体内的 catch 无法处理构造函数初始值列表抛出的异常。 将构造函数写成 函数try语句块 (函数测试块)的形式可用处理(唯一办法) 对于用户和编译器来说, 预先知道某个函数不会抛出异常显然 大有脾益 在 C++11 新标准中, 我们提供 noexcept 说明指定某个函数不会抛出异常,其形式是关键字 noexcept紧跟在函数的参数列表后面,用以标识该函数不会抛出异常: 违反异常说明 一旦一个 noexcept 函数抛出了异常,程序就会调用 terminate 以确保遵守不在运行时抛出异常的承诺。 因此:我们可用在两种情况下使用 noexcept 异常说明的实参 noexcept 说明符接受一个可选的实参, 该实参必须能转换为 bool 类型 : noexcept 运算符 noexcept 说明符的实参与noexcept运算符混合使用。 note: noexcept 有两层含义:跟在函数参数列表之后它是异常说明符,当作 noexcept 异常说明的 bool 实参出现,是一个运算符 异常说明与指针,虚函数和拷贝控制 如果一个虚函数承诺了它不会抛出异常, 则后续派生处的虚函数也需要做同样的承诺,如果基类的虚函数允许抛出异常,派生类的对应函数可抛出异常也可不抛出异常 当编译器合成拷贝控制成员时, 同时也生成异常声明,如果对所有成员和基类的所有操作都承诺了不抛出异常,合成的成员是 noexcept的,如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是 noexcept(false) 实际的应用程序通常会自定义 exception(或者 exception的标准库派生类)的派生类以扩展继承体系 多个库将名字放置在全局命名空间中会引发 命名空间污染 命名空间 为放置名字冲突提供了更加可控的机制,命名空间分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者可用避免全局名字固有 的限制。 关键字 namespace 与 命名空间的名字 加上 花括号括起来的声明和定义 构成了命名空间 每个命名空间都是一个作用域 命名空间可以是不连续的 建议: 定义多个类型不想关的命名空间应该使用单独的文件表示每个类型(关联类型构成的集合) 定义命名空间 注意: 不能把 #include 放在命名空间内部,如果我们这么做了,隐含的意思是把头文件中所有名字定义成该命名空间的成员。 定义命名空间成员 模板特例化 模板特例化必须定义在原始模板所属的命名空间中,和其他命名空间的名字类似,只要我们在命名空间中声明了特例化,就可以在命名空间外部定义他了: 全局命名空间 全局作用域中定义的名字也就是定义在全局命名空间中。 :: member_name 能显式地表示全局命名空间中的一个成员。 嵌套的命名空间 内联命名空间 C++ 11 新标准引入了新的嵌套命名空间,内联命名空间,在关键字 namespace 前加入 inline 即可。 未命名的命名空间 未命名的命名空间是指关键字 namespace 后紧跟花括号括起来的一系列声明语句。 建议: 在文件中进行静态声明的做法已经被 C++ 标准取消了,现在的做法是使用未命名的命名空间 我们可以调用作用域访问符来访问其成员,但有时候显得篇幅太长,这节我们讨论其他的解决办法 命名空间的别名 命名空间的别名可以使得我们为命名空间的名字设定一个短的多的同义词 using 声明: 扼要概述 一条 using 声明语句一次只引入命名空间的一个成员, 他使得我们可以清楚地知道程序用的到底是哪个名字。 using 指示与作用域 using 指示使得某个特定的命名空间中所有名字可见,这样我们无须为其添加任何前缀限定。 由于 using 指示使用后对应用程序的控制变得十分简单,命名空间污染的问题就会出现,我们可以在命名空间本身的实现文件中使用 using 指示。 note :可以从函数的限定名推断出查找名字检查作用域的次序,(相反) 实参相关的查找与类类型形参 operator >> (std::cin, s); 查找与 std::move 和 std::forward 由于 传递右值引用形参 会导致传递任何类型,都被匹配。 因此为了避免这种冲突,我们使用 std::move 让我们明确直到使用的是函数标准库版本。 友元声明与实参相关的查找 对于友元与 接口函数查找规则相同 在命名空间重载的单一调用会默认为 全部重载函数 多重继承 指从多个直接基类产生派生类的能力,多重继承的派生类继承了所有父类的属性。 在给顶的派生列表中,同一个基类出现依次 多重继承的派生类从每个基类中继承状态 在多重继承关系中,派生类对象包含有每个基类的子对象 派生类构造函数初始化所有基类 派生类的构造函数初始化列表将实参分别传递给每个基类,其中基类的构造顺序与派生类列表中基类的出现顺序保持一致。(根据每一个派生类的构造函数列表进行初始化) 继承的构造函数与多重继承 在C++11 中,允许派生类从它的一个或几个基类中继承 构造函数, 但是如果从多个基类中继承了相同的构造函数(形参列表完全相同)。 建议; 最好使用构造函数定义自己的版本 析构函数与多重继承 析构函数的调用顺序与构造函数相反 多重继承的派生类的拷贝与移动操作 与 一个基类的继承相符。 我们可以令某个可访问基类的指针或引用直接指向一个派生类对象 编译器不会在派生类向基类的几种转换中进行比较与选择,因为在它看来转换到任意一种基类都一样好,则在 选择 参数是派生类的重载函数时,会引发二义性错误。 基于指针或引用类型的查找 对象,指针和引用的静态类型决定了我们使用哪些成员 在默认情况下,派生类含有继承链上每个类对应的子部分, 如果某个类在派生过程中出现了多次,那么派生类将包含该类的两份拷贝,这明显会造成一些问题。 note: 虚派生只影响从指定了虚基类的派生类中进一步派生出来的类, 它不会影响派生类本身, 使用虚基类 指定虚基类的方式是在派生类列表中添加关键字 vittual 支持向基类的常规类型转换 虚基类成员的可见性 因为 在每个共享的虚基类中只有唯一一个共享的子对象, 所以该基类的成员可以被直接访问,并且不会产生二义性。 解决二义性的问题最好的方法是在派生类中为成员定义新的实例。 只要我们能创建虚基类的派生类对象, 该派生类的构造函数就必须初始化它的虚基类。 虚继承的对象的构造方式 含有虚基类对象的构造顺序与一般的顺序稍有区别: 首先提供给最底层派生类的构造函数初始值初始化该对象的虚基类子部分,再按照直接基类在派生列表出现的次序依次对其初始化。 构造函数与析构函数的次序 对象的销毁顺序与构造顺序相反。 在语言基本要素之外, C++还定义了 一些非常特殊的性质,对于很多程序员来说,它们一般很少会用到本章介绍的内容。 我们希望自定义内存分配的细节,这时我们会使用关键字 new 将对象放置在 特定的内存空间中,为了实现这个目的, 应用程序需要重载 new 运算符和 delete 运算符以控制内存分配的过程。 new 表达式的三个步骤: delete 表达式的 两个步骤 当我们自定义 全局 operator new 与 operator delete 函数后,我们担负起控制动态内存的责任,这两个函数必须是正确的 编译器对于自定义 运算符重载 的响应 使用作用域运算符可以了令 new 表达式与 delete 表达式忽略掉在类中的函数直接执行全局作用域的版本。 :: new Strlob(); operator new 接口与 operator delete接口 标准库定义了 8 个重载版本,其中前4个版本可能抛出 bad_alloc 异常,其他 4个版本不会抛出异常 如果我们向自定义 operator new 函数,则可以为其提供额外的形参,用到这些自定义函数的 new 表达式必须使用 new 的定位形式,将实参传递给新增的形参。 注意: void operator new(size_t, void); //不能被重载,只供标准库使用 operator delete 函数版本由该对象的动态类型决定 malloc 函数与 free 函数 初衷只是使用特殊定制的 内存分配器,但这两个函数也应该同时满足测试的目的,即检验其分配内存的方式与常规方式类似。 我们使用 malloc 和 free 函数来完成简单的定制内存分配器 。它们定义在 cstdlib 中 operator new 分配的内存空间使用 定位 new 形式构造对象, new 的这种形式为分配函数提供了额外的信息。 其中 place_address 必须是一个指针,同时在 initializers提供一个(可能为空的)以逗号分隔的初始值列表,该列表用于构造一个新分配的对象。 显式的析构函数调用 显式调用析构函数会销毁对象,但是不会释放内存,释放内存由 operator delete 释放 运行时类型识别(run_time type indentification RTTI) 的功能由两个运算符实现: 当我们将两个运算符用于某个类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。 使用情况: 我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数 注意: 使用 RTTI 必须加倍小心, 在可能情况下,最好定义虚函数而非直接接管类型管理的重任。 使用形式: type 必须是一个类类型,且通常情况下含有 虚函数 e 的类型必须符合以下三个条件中的任意一个: 指针类型的 dynamic_cast 引用类型的 dynamic_cast bad_cast 异常定义在 type_info标准库头文件中 为 RTTI 提供的第二个运算符是 typeid 运算符,它允许程序向表达式提问: 你的对象是什么类型? e 可以是任意表达式或类型的名字, typeid 操作的结果是一个常量对象的引用,该对象的类型是标准库类型 type _info 或者 type_info 的公有派生类型。 typeid 运算符可以用作任何类型的表达式,顶层const 被忽略,如果表达式是引用类型,则返回引用所引对象的类型,作用与指针或函数,并不会执行向指针的标准类型转换 当运算对象不属于类类型或者不包含虚函数的类,所求的类型是运算对象的静态类型,否则,typeid 的结果到允许时才会求得。 使用typeid 运算符 通常情况, typeid 用来比较两条表达式的类型是否相等,或一条表达式与指定类型是否相等。 当我们想为具有继承关系的类实现相等运算符时, RTTI 是非常有用的 步骤; type_info 类的精确定义随着编译器的不同略有差异,编译器规定 type_info类必须定义在 typeinfo头文件中,并提供操作 typeid 是type_info 的 公有派生类型 枚举类型使得我们可以将一组整型变量组织在一起。和类一样,枚举定义了一种新类型,枚举属于字面值常量类型 C++ 包含的枚举有两种: 限定作用域的和不限定作用域的。 限定作用域枚举 与 非限定作用域枚举: 限定作用域枚举 :关键字 enum class + 枚举类型名字 + 花括号括起来以逗号分割的 枚举成员列表,最后是一个分号 不限定作用域枚举 : 省略掉 关键字 class,如果enum 是没有名字的,则只能在定义该 enum 时定义其对象, 枚举成员 和类一样,枚举也定义 新的类型 只能使用 枚举类型或类型的对象来初始化 enum 对象 指定 enum 的大小 在 c++ 11新标准中,我们可以在 enum 的名字后加上冒号,表示我们想在 enum 中使用的类型 枚举类型的前置声明 C++ 新标准中,是我们可以提前声明 enum, enum 的前置声明(隐式显式)必须指定其成员的大小 形参匹配与枚举类型 要想初始化一个 enum 对象,必须使用该 enum 类型的另一个对象或者它的一个枚举成员(即使只相等也不能作为 enum实参使用) 用来指向类的成员的指针 嵌套在外层类中的类,主要描述 关系十分密切的类 联合是一种特殊的类,通常嵌套在其他类的内部 定义 union 使用 union 类型 Token t = {‘a’}; //只能初始化一个成员 定义在函数的内部,局部类的所有成员都i必须定义在类内,局部类不能含有静态数据成员 关键字 volatile 告诉编译器不应该对这样的对象进行优化,使得程序更容易访问硬件。 使得程序更容易访问其他语言编写的代码。
cout << c <
3.3 标准库类型 vector
3.31列表初始化 vector 对象
3.3.2 向 vector对象中添加元素
push_back 向其中添加元素。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 相等当且仅当他们的元素的数量相等且对应位置的元素值都相同。
<,<=,>,>= 按照字典序比较
3.4 迭代器介绍
3.4.1使用迭代器
*iter 返回迭代器 iter 所指元素的引用
iter->mem 解引用iter 并获取该元素的名为 mem 的成员,等价于 (*iter).mem
++iter 令 iter 指示容器中的下一个元素。
--iter 令 iter 指示容器的上一个元素
iter1 == iter2 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者他们是同一个容器的iter1 != iter2 尾后迭代器,则相等,否则不相等
为了专门得到常量迭代器类型的返回值,C++ 11 定义了两个新函数,分别是 cbegin() 和 cend();
iter ->elem 来将其操作结合在一起。3.4.2 迭代器运算
iter + n 迭代器加上整数仍得一个迭代器,向前移动 n 个元素
iter - n 迭代器减去整数得到一个迭代器,向后移动 n 个元素
iter += n 迭代器加法的复合赋值语句
iter -= n 迭代器减法的复合赋值语句
iter1 - iter2 迭代器相减的结果是他们之间的举例,参与运算的必须是同一个容器中的元素的迭代器,或者是尾元素的下一位置。
> ,>= ,< ,<= 迭代器的关系运算符,位置在前的迭代器小于位置在后的迭代器
3.5 数组
3.5.1 定义与初始化内置数组
这个空字符也会被拷贝到字符数组中去。
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对数组的引用
3.5.2 访问数组元素
3.5.3 指针与数组
auto 推断数组名为 其数组类型指针 auto ia2(ia); ia2是指针
decltype() 推断数组名 为其数组 decltype(ia) ia2; ia2是数组
end(arr) :会得到 arr 尾元素的下一个位置的指针
cstddef 头文件。
3.5.4 C风格字符串
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.
3.5.5 与旧代码的接口
3.6 多维数组
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 逻辑与关系运算符
短路求值:
逻辑非运算符将运算对象的值取反后返回,
满足即为真,不满足为假
左结合律
4.4 赋值运算符
4.5 递增和递减运算符
4.6 成员访问运算符
成员的所属对象是左值,则结果是左值,所属对象是右值,则结果是右值。4.7 条件运算符
cond ? expr1 : expr2
当条件运算符的两个表达式都是左值或者能转换成同一左值类型时,运算结果是左值,否则是右值。4.8 位运算符
位运算符提供检查和设置 二进制位的功能
位运算符(左结合律)
运算符
功能
用法
~
位求反
~expr
<<
左移
expr1 << expr2
>>
右移
expr1 >> expr2
&
位与
expr & expr
^
位异或
expr ^ expr
强烈建议仅将位运算符用于处理无符号类型。
对于位或运算符(|),如果两个运算对象的对应位置上有一个是 1,则运算结果中该位置为1,否则为0.
对于异或运算符(^),如果两个运算对象的对应位置有且仅有一个为1,则该运算结果为1,否则为 0.
4.9 sizeof 运算符
sizeof 满足右结合律,并且与* 运算符的优先级一样。
4.10 逗号运算符
逗号运算符真正的结果是右侧表达式的值,如果右侧表达式的结果是左值,最终的求值结果也是左值。4.11 类型转换
4.11.1 算术转换
4.11.2 其他隐式转换类型
4.11.3 显示转换
cast-name
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
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<
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
第五关:语句
5.1 简单语句
5.2 语句作用域
5.3 条件语句
case 关键字与它对应的值一起被称为 case 标签,case 标签必须是整型常量表达式。
5.4 迭代语句
5.5 跳转语句
5.6 try 语句块和异常处理。
exception
最常见的问题
runtime_error
只有在运行时才能被检测的问题
range_error
运行错误:生成的结果超出了有意义的值域范围
overflow_error
运行时错误;计算上溢
underflow_error
运行时错误:计算下溢
logic_error
程序逻辑错误
domain_error
逻辑错误,参数对应的结果值不存在
invalid_argument
逻辑错误:无效参数
length_error
逻辑错误:试图创建一个超出该类型最大长度的对象
out_of_range
逻辑错误:使用一个超出有效范围的值
第六关: 函数
6.1 函数基础
6.1.1 局部对象
6.1.2 函数声明
6.1.3 分离式编译
factMain.cc 文件中创建 main 函数,main函数调用 fact 函数。
$ cc factMain.CC fact.cc -o main # generates main or main.exe
如果我们修改了其中一个源文件,那么我们只需要重新编译那个改动了的文件,大多数编译器都会提供分离式编译每个文件的机制,这一过程通常会产生一个 后缀名为 .obj 或者 .o 的文件,后缀名的含义是该文件包含该对象代码。
$ cc -c factMain.cc #generates factMain.o
$ cc -c fact.cc #generates fact.o
$ cc factMain.o fact.o -o main #generates main or main.exe6.2 参数传递
6.2.1 形参与实参
6.2.2 const 形参与实参
6.2.3 指针或引用参数与 const
6.2.4 数组形参
void print (int matrix[][10], int rowSize)6.2.5 main:处理命令行选项
6.2.6 含有可变形参的函数
C++11新标准提供了两种主要的方法
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() : 返回尾后指针
void foo(int a,…) = void foo(int a…)
void foo(…) {6.3 返回类型和return 语句
6.3.1 无返回值
6.3.2 有返回值
6.3.3 返回数组指针
Type(*function(parameter_list))[dimension]
decltype(odd) *arrPtr(int i);6.4 函数重载
6.4.1 重载与作用域
6.5 特殊用途语句特性
6.5.1 默认实参
调用该默认实参的函数可以包含实参也可以省略实参。
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 函数
函数调用的工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。
不过要遵循几项约定:
6.5.3 调试帮助
这种方法用到两项预处理功能: assert 和 NDEBUG
对expr求值,如果表达式为假,assert 输出信息并终止程序的执行,如果表达式为真,则assert什么都不做
如果NDEBUG未定义,则执行 #ifndef 与 #endif之间的代码,这些代码将被忽略掉void print(const int ia[], size_t size) {
#ifndef NDEBUG
cerr << __func__ << ": array size is " << size << endl;
#endif // !NDEBUG
}
6.6 函数匹配
可行函数的特点:
6.6.1 实参类型转换
非const 实参 只会匹配非常量实参。6.7 函数指针
声明一个指向该函数的该函数指针只需要将函数名换成指针就行了。
decltype(sumLength) *getFcn(const string &);
第七关:类
7.1 定义抽象数据类型
7.1.1 设计 SalesData 类
7.1.2 定义改进的SalesData 类
#pragma once
#ifndef CPPPRIMER_SALESDATA_H_
#define CPPPRIMER_SALESDATA_H_
#include
double mynamespace::SalesData::AvgPrice() const {
if (units_sold_)
return revenue_ / units_sold_;
return 0.0;
}
SalesData& SalesData::Combine(const SalesData &rhs) {
units_sold_ += rhs.units_sold_;
revenue_ += rhs.revenue_;
return *this;
}
内置的赋值运算符把它的左侧运算对象当成左值返回。7.1.3 定义类相关的非成员函数
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;
}
SalesData Add(const SalesData &lhs, const SalesData &rhs) {
SalesData sum = lhs;
sum.Combine(rhs);
return sum;
}
7.1.4 构造函数
编译器创建的构造函数又被称为 合成的默认构造函数,它以以下规则初始化类的数据成员:
#pragma once
//Copyright 2020
//License (BSD/GPL/...)
//Author: Handling
//This is CPPPrimer Study
#ifndef CPPPRIMER_SALESDATA_H_
#define CPPPRIMER_SALESDATA_H_
#include
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 访问控制与封装
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);
};
7.2.1 友元
#pragma once
//Copyright 2020
//License (BSD/GPL/...)
//Author: Handling
//This is CPPPrimer Study
#ifndef CPPPRIMER_SALESDATA_H_
#define CPPPRIMER_SALESDATA_H_
#include
7.3 类的其他特性
mutable 关键字做到这一点7.3.2 返回 *this 的成员函数
7.3.3 类类型
7.3.4 友元再探
此外友元函数定义在类的内部,这样的函数是隐式内联的。
A类。
7.4 类的作用域
7.4.1 名字查找与类的作用域
cursor = width * (::height);
7.5 构造函数再探
7.5.1 构造函数初始值列表
SalesData(const std::string &s = ""): book_no_(s) { }
7.5.2 委托构造函数
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 默认构造函数的作用
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"); //这是不对的
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));
7.5.5 聚合类
当一个类满足如下条件时,我们说它是聚合的:
struct Data {
int ival;
std::string s;
};
7.5.6 字面值常量类
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 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
利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。8.1.1 IO对象无拷贝或赋值
8.1.2 条件状态
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)
auto old_state = cin.rdstate(); //返回当前流的状态
cin.clear(); //清除所有错误标志位。调用 good 会返回true
cin.setstate(old_state); //设置流的状态
8.1.3 管理输出缓冲
对 cerr 是设置 unitbuf 的,因此写到 cerr 的内容都是立即刷新的。
cout << flush;
重置流,使其恢复之前的缓冲机制。 cout << std::unitbuf;
cout << std::nounitbuf;
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 文件输入输出
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 使用文件流对象
out.open(file); //打开指定文件
8.2.2 文件模式
文件模式
in 以读方式打开
out 以写方式打开
app 每次进行写操作之前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式 进行 IO
总是以 out 输出方式打开。
8.3 string 流
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 record("sss s s s s ");
string word;
while (record >> word) {
cout << word << ends;
}
8.3.2 使用 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 相似的容器,但专门用来保存字符。随机访问速度块,在尾部插入/删除速度快。
则:
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 不能修改元素的逆序迭代器
c.rbegin(),c.rend() 指向c的尾元素与首元素之前位置的迭代器
c.crbegin(),c.crend() 返回 const_reverse_iterator
9.2.1 迭代器
9.2.2 容器类型成员
9.2.3 begin 和 end成员
9.2.4 容器定义和初始化
C c; 默认构造函数,构造空容器
C c1(c2); 构造 c2 的拷贝 c1
C c(b,e); 构造 c,利用迭代器 b与 e 指定范围内的元素拷贝到 c
C c{a, b, c....} 列表初始化 c
C seq(n) seq包含 n 个元素,都进行了值初始化,此构造函数是 explicit的
C seq(n,t) seq 包含 n 个初始化为值 t 的元素。
C seq(n) seq包含 n 个元素,都进行了值初始化,此构造函数是 explicit的
C seq(n,t) seq 包含 n 个初始化为值 t 的元素。
9.2.5 赋值与 swap
容器赋值与swap操作
c1 = c2 将c1中的元素替换成 c2 的元素
a.swap(b) 交换 a 和 b 的元素
swap(a,b); 与上面等价
assign操作不适合关联容器和 array
seq.assign(b,e) 将 seq 种的元素替换成 b 和 e 所表示的范围种的元素。
迭代器 b 和 e 不能指向 seq 种的元素
seq.assign(il) 将 seq 种的元素替换成初始化列表 il种的元素
seq.assign(n,t) 将seq种的元素替换成 n 个值为 t 的元素。
9.2.6 容器大小操作
size()
emplty()
max_size() 返回一个等于等于该类型容器所能容纳的最大元素数的值。9.27 关系运算符
9.3 顺序容器操作
9.3.1 向顺序容器添加元素
forward_list 有自己专属版本的 insert 和 emplace
forward_list 不支持 push_back 和 emplace_back;
vector 和 string 不支持 push_front 和 emplace_front; 向顺序容器添加元素的操作
c.push_back(t) 在 c 的尾部创建一个值未 t 或由 args创建的元素,返回void
c.emplace_back(args)
c.push_front(t) 在 c 的头部创建一个值未 t 或由 args创建的元素,返回void
c.emplace_front(args)
c.insert(p,t) 在 p 位置之前创建一个 值为 t或者由 args创建的元素,返回新添加的元素迭代器
c.emplace(p,args)
c.insert(p,n,t) 在p位置之前插入 n 个值为t的元素,返回指向新添加的第一个 元素 的迭代器,若 n 为 0,返回 p
c.insert(p,b,e) 将迭代器 b,e 范围内的元素插入到迭代器 p所指向的元素之前,
b和e不能指向 c 中的元素,返回指向新添加的第一个元素的迭代器
c.insert(p,il) il是花括号包围的元素值列表,将这些给定值插入到迭代器 p 指向的元素之前,若列表为空,返回p
向一个vector,string,deque 插入元素会使所有只想容器的迭代器,引用与指针失效。
9.3.2 访问元素
在顺序容器中访问元素的操作
c.back() 返回尾元素的引用
c.front() 返回头元素的引用
c[n] 返回下标为 n 的元素引用,n是无符号整数,n大于 c.size(),函数行为未定义
c.at(n) 返回下标为 n 的元素的引用,如果下标越界,抛出 out_of_range异常
9.3.3 删除元素
在顺序容器中删除元素
c.pop_back() 删除 c 的尾元素,c为空则函数行为未定义。函数返回void
c.pop_front() 删除 c 的首元素,若c为空,则函数行为未定义,函数返回void
c.erase(p) 删除迭代器 p 的元素,返回一个指向被删元素之后的元素的迭代器。
c.erase(b,e) 删除迭代器 b,e 的所指范围元素,指向最好一个被删除元素之后的迭代器
c.clear() 删除所有元素,返回void
9.3.4 特殊的 forward_list 操作
9.3.5 改变容器大小
c.resize(n) 调整c的大小为 n 个元素,若 n<c.size(),则多出的元素被丢弃,若必须添加
新元素,对新元素进行值初始化。
c.resize(n,t) 调整 c 的大小为n个元素,任何新添加的元素都初始化为值 t
9.3.6 容器操作可能使迭代器失效
如果存储空间没有被分配,则插入位置之前的元素的迭代器仍然有效。
9.4 vector对象是如何增长的
容器大小管理操作
c.shrink_to_fit() 请将capacity()减少为 size() 相同大小,仅仅为请求
c.capacity() 不重新分配内存空间的化,c可以保存多少元素
c.reserve(n) 分配至少容纳n个元素的内存空间
9.5 额外的 string 操作
s.find(args) 查找 s 中 args第一次出现的位置
s.rfind(args) 查找 s 中 args 最后一次出现的位置
s.find_first_of(args) s中查找 args 任何一个字符第一次出现的位置
s.find_last_of(args) s中查找 args 任何一个字符最后一次出现的位置
s.find_first_not_of(args) 在 s中查找第一个不在 args 中的字符
s.find_last_not_of(args) 在s中查找最后一个不在 args 中的字符
stoi,stol,stof第十关:泛型算法
10.1 概述
10.2 初识别泛型算法
10.2.1 读元素的算法:
只接受单一迭代器来表示第二序列的算法,都假定第二个序列至少与第一个一样长accumulate(b,e,默认值) numeric 返回容器的和
equal(b1,e1,b2) 两个容器进行对比
10.2.1 写元素的算法:
fill(b,e,设置值) 容器区间的值设置为设置值
fill_n(dest,n,val) 从dest开始的n个元素设置为val,算法不检查 n 是否合法
copy(b1,e1,des) 将容器范围内的元素,拷贝到des所指的目的序列,
传递给copy的目的序列至少要包含于 输入序列一样多的元素
vector<int> vec;
auto iter = back_inserter(vec);
*iter = 10; //向vec尾部插入 10
fill_n(back_inserter(vec),10,0);
10.2.3 重排容器的算法
10.3 定制操作
10.3.1 向算法传递函数
10.3.2 lambda 表达式
[ capture list](parameter list) -> return type { function body }
{ return a.size() > = sz; }10.3.3 lambda 捕获与返回
size_t v1 = 42;
auto f = [v1] { return v1; };
v1 = 0;
为了指示编译器推断捕获列表,应该在捕获列表中写一个 & 或 =。告诉编译器采用的捕获方式。 size_t v1 = 42;
auto f = [=] { return v1; };
lambda捕获列表
[] 空捕获列表,lambda 不能使用所指函数中的变量。一个lambda 只有捕获变量后才能使用他们
[names] names是以逗号分隔的名字列表,这些名字都是lambda所指函数的局部变量,默认情况下拷贝捕获,如果前面加上了&,使用引用捕获
[&] 隐式捕获列表,采用引用捕获方式,lambda体中使用的来自函数的实体都采用引用方式
[=] 隐式捕获列表,采用拷贝捕获方式,lambda体中使用的来自函数的实体的值
[&,identifier_list] identifier_list 是一个逗号分隔的列表,包含 0 个或多个来自 所在函数的变量,这些变量采用值捕获方式,而
任何隐式捕获的变量都采用引用方式捕获,identifier_list 的名字前面不能加上 &
[=,identifier_list] identifier_list 是一个逗号分隔的列表,包含 0 个或多个来自 所在函数的变量,这些变量采用引用捕获方式,而
任何隐式捕获的变量都采用值方式捕获,identifier_list 的名字前面都必须加上 &,名字不能包括this
10.3.4 参数绑定
bind 函数。
auto newCallable = bind(callable, arg_list);auto check6 = bind(check_size,_1,6);
sort (words.begin(), words.end(), isShorter);
sort (words.begin(), word.end() ,bind(isShorter,_2,_1))
我们应该在其参数前面加上(ref 或者 cref),函数 ref 与 cref均定义在头文件 functional中ostream &Print(ostream &os, const string &s, char c) {
return os << s << c ;
}
int main(int argc,char **argv) {
vector<string> words;
std::for_each(words.cbegin(), words.cend(), std::bind(Print, ref(cout), _1,' '));
std::for_each(words.cbegin(), words.cend(), [&os,c](const string& s){ os << s << c;})
}
其余均为 绑定值,可以说是捕捉值10.4 再探迭代器
10.4.1 插入迭代器
it = t 在it指定的当且位置插入值 t。假定 c 是it绑定的容器,
依赖于插入迭代器的不同种类,此赋值分别调用 c.push_back(t),
push_front(t), c.insert(t,p);
*it,++t,it++ 这些操作虽然存在,但不会使it做任何事情,每个操作都返回 it
的迭代器。 元素将被插入 到给定迭代器所表示的元素之前。 vector<string> words;
auto inserter = std::back_inserter(words);
*inserter = "ss";
10.4.2 iostream 迭代器
ostream_iterator 向一个输出流写数据
从流对象读取数据 以及向其写入数据。
istream_iterator<int> in_it(cin); //从cin 读取
istream_iterator<int> int_eof;
ifstream in("afile");
istream_iterator<string> str_it(in) //从文件中读取字符
istream_iterator<int> in_it(cin), eof;
vector<int> ivec(in_iter,eof);
istream_iterator操作
istream_iterator<T> in(is) in从输入流 is 读取类型T的值
istream_iterator<T> end 尾后位置的读取 T 值的输入迭代器
in1 == in2 in1与in2 是相同类型,如果是尾后迭代器或绑定到相同的输入,则两者相等
in1 != in2
*in 返回从流中读取的值
in->mem 与(*in).mem含义相同
++in, in++ 使用元素类型所定义的 >> 运算符从输入流读取下一个值
ostream_iterator 操作
ostream_iterator<T> out(os); out将类型为T 的值写入到输出流 os中
ostream_iterator<T> out(os,d) out将类型为 T 的值写入到输出流 os 中,每一个值后面都跟一个d
out = val 用 << 将val 写入到 out 绑定的流中
*out,++out, out++ out 不做任何事情
std::ostream_iterator<string> out(cout," ");
*out++ = "sss";
10.4.3 反向迭代器
10.5 泛型算法结构(总结)
10.5.1 迭代器类别
输入迭代器 只读,不写,单遍扫描,只能递增
输出迭代器 只写,不读,单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写,多遍扫描,可递增递减
随机访问迭代器 可读写,多遍扫描,支持全部迭代器操作
输入迭代器: istream_iterator
输出迭代器: ostream_iterator
forward_list 上的迭代器
10.5.2 算法形参模式
alg(beg, end, other, args);
alg(beg, end, dest, other args)
alg(beg, end, beg2, other args)
alg(beg, end, beg2, end2, other args)
copy(lst.begin(), lst.end(), inserter(lst3,lst3.begin()))
std::ofstream ofs("",std::ios::app);
if (ofs.good()) {
std::ostream_iterator<string> out_it(ofs);
vector<string> words;
std::copy(words.cbegin(), words.cend(), out_it);
}
10.5.3 算法命名规范
unique(beg,end, comp);
find_if(beg, end, pred);
reverse(beg, end, dest);
remove_if(v1.begin(), v1.end() , back_inserter(v2),[](int i ){ return i%2; });10.6 特定容器算法
list与forward_list成员函数版本的算法
lst.merge(lst2) 将来自 lst2 的元素合并到 lst 中,lst与lst2都必须是有序的
lst.merge(lst2,comp) 元素将从 lst2 删除,合并后,lst2 边为空,第二个版本自定义
lst.remove(val) 调用 erase 删除指定 == 或 令一元谓词为真的每个元素
lst.remove_if(pred)
lst.reverse() 反转lst 中元素的顺序
lst.sort() 使用 < 或给顶操作排序元素
lst.sort(comp)
lst.unique() 调用 erase删除同一个值的连续拷贝
lst.unique(pred)
第十一关:关联容器
map 的元素是一些关键字(key-val)对,关键字起到索引的作用,值表示与索引相关的数据
set 中的元素只包含一个关键字,set 支持高效的关键字查找操作,查找一个关键字是否在 set中
关联容器类型
按关键字有序保存元素
map
关联数组;保存关键字-值对
set
关键字即值,即只保存关键字的容器
multimap
关键字可重复出现的 map
multiset
关键字可重复出现的 set
无序集合
unordered_map
用哈希函数组织的 map
unordered_set
用哈希函数组织的 set
unordered_multimap
用哈希组织是map,关键字可以重复出现
unordered_multiset
用哈希组织的 set;关键字可以重复出现
11.1 使用关联容器
set word_set;11.2 关联容器概述
11.2.1 定义关联容器
std::map<string, size_t> word_count; //空容器
std::map<string, string> authors = { {"joyce", "jance"}, //列表初始化
{"joyce", "jance"}};
std::set<string> exclude = {"the", "but", "and"};
11.2.2 关键字类型的要求
其性质如下:
如果 k1 ”等价于“ k2,且 k2 等价于 'k3",那么 k1 必须 ”等价于“ k3.
bool CompareIsbn(const SalesData &lhs, const SalesData &rhs) {
return lhs.book_no() < rhs.book_no();
}
int main(int argc,char **argv) {
std::multiset<SalesData, decltype(CompareIsbn)*>
book_store{CompareIsbn};
}
12.2.3 pair 类型
pair<T1, T2> p; p是一个pair,两个类型都进行了值初始化
pair<T1, T2> p(v1,v2) p 的first 于 second 对象分别用 v1 v2 初始化
pair<T1, T2> p = {v1,v2} 等价于上面
make_pair(v1, v2) 返回一个 由 v1,v2进行初始化的pair,返回pair类型由 v1,v2类型推断
p.first 返回 p 的名为 first 的公有数据成员
p.second 返回 p 的名为 second 的公有数据成员
p1 relop p2 关系运算符(<,>,<=,>=)按照字典序定义
当 p1.first < p2.second 或者 !(p1.first < p2.first)&&
(p1.second < p1.second ),p1 < p2
关系运算符利用元素的 < 运算符实现
p1 == p2 当 first 和 second 成员分别相等时,两个 pair 相等
p1 != p2
11.3 关联容器操作
关联容器额外的类型别名
key_ype
此容器类型的关键字类型
mapped_type
每个关键字关联的类型,只使用于map
value_type
对于 set ,与key_type 相同,对于 map,为 pair
11.3.1 关联容器迭代器
11.3.2 添加元素
vector<int> vec = {2, 4, 5, 5, 4,8};
std::set<int> set2;
set2.insert(vec.begin(), vec.end());
set2.insert({1,2,3,4});
map.insert({1,2}); //列表初始化
map.insert(make_pair(1,2)); //make_pair
map.insert(map::value_type(1,2)); //显式构造
insert 返回一个 pair
std::multimap<string, string> athors;
athors.insert({"hzj,zcy","c++"});
athors.insert({"hzj,zcy",""});
11.3.3 删除元素
c.erase(k) 从c中删除每个关键字为k的元素,返回一个 size_type值,指出删除的元素的总量
c.erase(p) 删除迭代器 p 指定的元素,返回一个指向 p 之后的元素的迭代器
c.erase(b, e) 删除迭代器对 b,e 所表示的范围,返回 e
11.3.4 map 的下标操作
c.at (k) : 访问关键字为 k 的元素,带参数检查;若 k 不在 c中,抛出一个out_of_range异常。
11.3.5 访问元素
c.find(k) 返回一个迭代器,指向第一个关键字为 k 的元素,若k不在容器中,返回尾后迭代器
c.count(k) 返回关键字等于 k 的元素的数量
c.low_bound(k) 返回迭代器,指向第一个关键字不小于 k 的元素
c.upper_bound(k) 返回迭代器,指向第一个关键字大于 k 的元素
c.equal_range(k) 返回一个迭代器 pair,表示关键字等于 k 的元素的范围,若k不存在,
则pair的两个成员均等于 c.end()
c.upper_bound(k) :会得到第一个关键字大于 k 的元素,可以作为容器中k关键字的键值对的 end 迭代器11.3.6 一个单词转换的 map
每个规则由一个单词,和用来替换的它的短语构成void WordTransform(std::ifstream &rules_ifs, std::ifstream &text_ifs) {
std::map<string, string> word_transform_rule;
string line_rule;
string key;
while (rules_ifs >> key && std::getline(rules_ifs, line_rule)) {
if(line_rule.size() > 1)
word_transform_rule.insert(std::make_pair(key,line_rule.substr(0)));
else
throw std::runtime_error("no rule for" + key);
}
string line_text;
while (std::getline(text_ifs, line_text)) {
istringstream split_word_iss(line_text);
string word;
bool is_first_word = true;
while (split_word_iss >> word) {
if(is_first_word)
is_first_word = false;
else
cout << " ";
string final_word;
auto word_it = word_transform_rule.find(word);
if (word_it != word_transform_rule.cend())
final_word = word_transform_rule[word];
else
final_word = word;
cout << final_word;
}
cout << endl;
}
}
11.4 无序容器
桶接口
c.bucket_count() 正在使用桶的数量
c.max_bucket_cout() 容器能容纳的最多桶的数量
c.bucket_size(n) 第 n 个桶中有多少个元素
c.bucket(k) 关键字为 k 的元素在哪个桶里
桶迭代
local_iterator 可以用来访问桶中元素的迭代器类型
const_local_iterator 桶迭代器的const版本
c.begin(n),c.end(n) 桶 n 的首元素迭代器 和 尾后迭代器
c.cbegin(),c.cend() 与前两个函数类似,但返回 const_local_iterator
哈希策略
c.load_factor() 每个桶的平均元素数量,返回 float 值
c.max_load_factor() c 试图维护的平均桶大小,返回 float 值。c会在需要时添加新的桶,
以使得 load_factor <= max_load_factor
c.rehash(n) 重组存储, 使得 bucket_count >= n
且 bucket_count > size/max_load_factor
c.reserve(n) 重组存储,使得 c 可以保存 n 个元素且不必 rehash
size_t hasher(const SalesData &sd) { //重载hash计算函数
return hash<string>(sd.book_no());
}
bool eqOp(const SaleData &lhs, const SaleData &rhs) {
return lhs.book_no() == rhs.book_no();
}
using SD_multiset = unordered_multiset<SaleData,
decltype(hasher)*, decltype(eqOp)*>;
SD_multiset book_store(42, hasher, eqOp);
第十二关: 动态内存
对于栈对象,仅在其定义的程序块运行时才存在,static 对象在使用之前分配,程序结束时销毁
12.1 动态内存与智能指针
智能指针的行为类似常规指针,重要的区别是它负责 自动释放所指向的对象。
shared_ptr<T> sp 空智能指针,可以指向类型为 T 的对象
unqiue_ptr<T> up
p 将p用作一个条件判断,若p指向一个对象则为true
*p 解引用 p,获得它所指向的对象
p->mem 等价于 (*p).mem
p.get() 返回 p 中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针
所指向的对象也就消失了。
swap(p,q) 交换 p 和 q 的指针
p.swap(q)
12.1.1 shared_ptr 类
make_shared<T>(args) 返回一个 shared_ptr,指向一个动态分配的类型为 T 的对象。使用 args 初始化其对象
shared_ptr<T> p(q) p 是 shared_ptr q 的拷贝;此操作会递增q 中的计数器。q中的指针必须能转换为 T*
p = q p 和 q 都是 shared_ptr,所保存的指针必须能够相互转换。此操作会递减 p 的引用计数
递增 q 的引用计数;若 p 的引用计数为 0,则将其管理的原内存释放掉。
p.unique() 若 p.use_count() 为1,返回true,否则返回 0
p.use_count() 返回 p 的共享对象的智能指针数量;可能很慢,主要用于调试
shared_ptr<int> p3 = make_shared<int>(42); //指向一个值为 42 的int 的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10,'9'); //p4 指向一个值为 “99999999999”的string
auto p5 = make_shared<int>(); //p5 指向一个值初始化的 int,值为 0
auto p(q); //p和q指向同一对象,此对象 有两个引用者
计数器会递减,当计数器为 0 ,它会自动释放其管理的对象
引用计数为0时,shared_ptr 会调用对象的析构函数对其进行销毁并释放内存。
因为其 内存是被操作的其他对象与自身共享的。12.1.2 直接管理内存
int *pi = new int ();
我们可以改变使用new的方式来阻止抛出异常。
nothrow 代表不能抛出异常,这时 new 如果不能分配内存,会返回一个空指针。
小心:动态内存的管理非常容易出错
该指针,自由空间就可能被破坏。
12.1.3 shared_ptr 与 new结合使用
shared_ptr<int> Clone(int p) {
return shared_ptr<int>(new int(p));
}
shared_ptr<T> p(q) p管理内置指针 q 所指向的对象;q必须指向 new 分配的内存,
且能够转换为 T*类型
shared_ptr<T> p(u) p 从 unique_ptr接管了对象的使用权,将 u 置为空
shared_ptr<T> p(q, d) p 接管了内置类型 q 所指的对象的所有权,q必须能转换为 T*类型,p将
使用可调用对象来代替 delete
shared_ptr<T> p(p2, d) p 是 p2 的拷贝,唯一的区别就是 p将调用d来替换 delete
p.reset() 若 p 是唯一指向其对象的 shared_ptr,reset 会释放此对象,若传递了可选的参数
p.reset(q) 内置指针 q,会令 p 指向 q,否则会将 p 置为空。若传递了 d,调用 d 来替换
delete
p.reset(q, d)
后,普通指针是 空悬指针。
12.1.4 智能指针与异常
这个删除器函数必须能够完成对 shared_ptr 中保存的指针进行释放的操作。void f() {
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection); //p接管了 c 中的连接动态内存
}
void end_connection(connect *p) { disconnect(*p); }
智能指针的陷阱
为了正确使用智能指针,我们必须坚持一些基本规范:
12.1.5 unique_ptr
被销毁时,该所指的对象也被销毁。unique_ptr<T> u1 空 unique_ptr,指向类型T 的对象,调用delete释放它的指针
unique_ptr<T, D> u2 u2 使用一个类型 D 的可调用对象来释放它的指针
unique_ptr<T, D> u(d) 空 unique_ptr,指向类型为 T 的对象,用类型为D 的对象 d 代替 delete
u = nullptr 释放 u 指向的对象,将 u 置为空
u.release() u 放弃对指针的控制权,返回指针,并将 u 置为空
u.reset() 释放 u 指向的对象
u.reset(q) 如果提供了内置指针 q,令u 指向这对象;否则将 u 置为空
u.reset(nullptr)
必须提供一个指定类型的可调用对象(删除器)。unique_ptr<objT, delT> p(new objT, fcn);
p 指向一个类型为 objT 的对象,并使用一个类型为 delT 的对象释放 objT对象。
void f() {
connection c = connect(&d);
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
}
12.1.6 weak_ptr
weak_ptr<T> w 空 weak_ptr 可以指向 T 类型的对象
weak_ptr<T> w(sp) 将 weak_ptr 绑定到相同类型 的 shared_ptr所指的对象上
w = p p 可以是一个shared_ptr 或 一个 weak_ptr.赋值后 w 与 p 共享对象
w.reset() 将 w 置为空
w.use_count 与 w 共享对象的shared_ptr 的数量
w.expired() (已过期) 若 w.use_count = 0,返回true,否则返回 false
w.lock() 如果 expired 为 true,返回一个 空 shared_ptr;否则返回一个指向 w 的对象的
shared_ptr
12.2 动态数组
例如当 vector 和 string 重新分配内存时,必须一次性为很多元素分配内存。
12.2.1 new 和 数组
当然我们可以利用在 大小后面加入一对空括号来对其中元素进行值初始化int *pia = new int[10];
int *pia = new int[10]();
auto 分配数组
unique_ptr<int[]> up(new int[10]);
up.release(); //自动调用 delete[] 销毁此指针
我们使用下标运算符来访问数组中的元素。unique_ptr<T[]> u u 可以指向一个动态分配的数组,数组元素类型为 T
unique_ptr<T[]> u(p) u指向内置类型 p 所指向的动态分配的数组, p 必须能转换为类型 T*
u[i] 返回 u 拥有的数组中位置 i 处的对象,u必须指向一个数组
shared_ptr<int[]> sp(new intt[10](), [](int *p) { delete []p;})
sp.reset()
12.2.2 allocator 类
delete 将对象析构与内存释放组合在了一起
当分配大块内存,我们希望将内存分配与对象构造分离,这意味着我们可以分配大块内存,在真正需要才真正执行对象创建操作(同时付出一定开销)
allocator<T> a 定义了一个名为 a 的allocator 对象,它可以为类型 T 的对象分配内存
a.allocate(n) 分配一段原始的,未构造的内存,保存 n 个类型为 T 的对象
a.deallocate(p, n) 释放从 T*指针p中地址开始的内存,这块内存保存了 n 个类型为 T 的对象
p必须是先前 allocate 返回的指针,且 n 必须是 p 创建时所要求的大小
在调用 deallocate之前,用户必须对每个这块内存中创建的对象调用 destroy
a.construct(p, args) p 是类型为 T*的指针,指向一片原始内存,args 被传递给 T 的构造函数,用来在
p 指向的内存构造一个对象
a.destroy(p) p 为 T* 类型的指针,此算法对 p 指向的对象执行析构函数
deallocate 来完成 allocator<string> alloc;
string *p = alloc.allocate(10);
auto q = p;
alloc.construct(q++);
alloc.construct(q++,10,'c');
while (q!=p)
alloc.destroy(--q);
alloc.deallocate(p, 10);
memoryuninitialized_copy(b, e, b2) 从b,e指出的输入范围拷贝元素到迭代器 b2指定的为构造的原始内存中‘
b2 所指的内存应该足够大
uninitialized_copy_n(b,n,b2) 从迭代器 b 指向元素开始,拷贝n个元素到 b2 开始的内存中
uninitialized_fill(b,e,t) 在迭代器 b e指定的原始范围中创建对象,对象的值均为 t 的拷贝
uninitialized_fill_n(b, n, t) 从b开始的内存开始创建 n 个对象,b必须指向足够大的未构造的原始内存
能够容纳给定数量的对象
12.3 使用标准库 :文本查询系统
第十三关: 拷贝控制
拷贝,移动,赋值或销毁时做什么。13.1 拷贝,赋值与销毁
13.1.1 拷贝构造函数
class Foo {
public:
Foo();
Foo(const Foo&); //拷贝构造函数
}
内置类型的成员则直接拷贝(包括数组,对数组元素逐一拷贝)
数组元素是类类型,则调用每个元素的拷贝构造函数来进行拷贝 SalesData(const SalesData& orig);
SalesData::SalesData(const SalesData &orig)
: book_no_(orig.book_no_), units_sold_(orig.units_sold_), revenue_(orig.revenue_) {}
拷贝初始化:我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还会进行类型转换
用 emplace 成员创建的元素都进行直接初始化。
当函数返回非引用类型时,返回值会被用来初始化调用方的结果。string null_book = "999-9-999" 拷贝初始化
string null_book("99999") 编译器略过了 拷贝构造函数
13.1.2 拷贝赋值运算符
赋值运算符就是一个 名为 operator= 的函数,与其他函数一样,该函数有一个返回值与形参列表
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
}
13.1.3 析构函数
class Foo {
public :
~ Foo()
}
通常,析构函数释放对象在生存期分配的所有资源。
13.1.4 三/五法则
13.1.5 使用 = default
13.1.6 阻止拷贝
删除的函数:我们声明了他们,但不能以任何方式使用它,在参数列表后面加上 = delete来指出我们希望它定义为删除的。struct NoCopy {
NoCopy() = default; //使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator=(const NoCopy&)= delete; //阻止赋值
}
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&) = delete; \
TypeName& operator=(const TypeName&) = delete;
//仅在需要拷贝对象时使用拷贝构造函数,不需要拷贝时在使用DISALLOW_COPY_AND_ASSIGN宏
DISALLOW_COPY_AND_ASSIGN(GoogleStyle);
13.2 拷贝控制和资源管理
可以定义拷贝操作,使类的行为看起来像一个值或者一个指针。13.2.1 行为像值的类
class HasPtr {
public:
HasPtr(const std::string &s = std::string())
: ps_(new std::string(s)), i_(0){ }
HasPtr(const HasPtr &p)
: ps_(new std::string(*p.ps_)), i_(p.i_) { }
HasPtr& operator=(const HasPtr &);
~HasPtr() { delete ps_; }
private:
std::string *ps_;
int i_;
};
}
我们应该确保异常发生时左侧运算对象能为一个有意义的状态。
HasPtr &HasPtr::operator=(const HasPtr &rhs) {
auto newp = new string(*rhs.ps_);
delete ps_;
ps_ = newp;
i_ = rhs.i_;
}
13.2.2 定义行为像指针的类
class HasPtr {
public:
HasPtr(const std::string &s = std::string())
: ps_(new std::string(s)), i_(0), use_(new std::size_t(1)) { }
HasPtr(const HasPtr &p)
: ps_(p.ps_), i_(p.i_), use_(p.use_) { ++use_; }
HasPtr& operator=(const HasPtr &);
~HasPtr();
private:
std::string *ps_;
int i_;
std::size_t *use_;
};
HasPtr &HasPtr::operator=(const HasPtr &rhs) {
++ *rhs.use_;
if (--use_ == 0) {
delete ps_;
delete use_;
}
ps_ = rhs.ps_;
i_ = rhs.i_;
use_ = rhs.use_;
return *this;
}
HasPtr::~HasPtr() {
if (-- * use_ == 0) {
delete use_;
delete ps_;
}
}
13.3 交换操作
friend void swap(HasPtr&,HasPtr&);
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps_, rhs.ps_);
swap(lhs.i_, rhs.i_);
}
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps_, rhs.ps_);
swap(lhs.i_, rhs.i_);
swap(lhs.h, rhs.h) //假定h是类类型,会调用 类类型的 swap
}
HasPtr& HasPtr::operator=(HasPar rhs){
swap(*this, rhs);
return *this;
}
13.4 拷贝控制示例 邮件处理应用
Folder :消息目录
说明每一个Folder都存在了一个指向该message指针
每个 message 也保存了它所在的 Folder 的指针class message{
public:
save() 保存自己到其他 folder
remove() 移除自己到其他 folder
message(const message&) 拷贝初始化,将拷贝的元素的保存的folder都添加一个新拷贝的自己,且自己将folder都保存下来
~message() 从给定保存的folder删除自身。
private :
set<folder*>
string content; 实际消息数据
void add_to_Folders(const Message&) //功能的公共部分
void remove_from_Folders();
}
class Folder {
public:
void AddMsg(Message*);
void RemoveMsg(Message*);
private:
std::set<Message*> msgs;
};
13.5 动态内存管理类
- reserve 额外分配更多内存
- Check 检测自身是否可容纳新的元素,空间是否足够
- allocate :分配新空间 construct :构造元素
- destroy 与 deallocate :销毁元素与 释放空间
string* elements;
string* first_free;
string* cap;
static allocator
13.6 对象移动
13.6.1 右值引用
int &&rr1 = 42; //正确
int &&rr2 = rr1; //错误
int &&rr3 = std::move(rr1);
13.6.2 移动构造函数 和移动赋值运算符
StrVec::StrVec(StrVec &&s) noexcept //移动构造函数不应该抛出异常
: elements_(s.elements_), first_free_(s.first_free_), cap(s.cap) {
s.elements_ = s.first_free_ = s.cap = nullptr;
}
StrVec &StrVec::operator=(StrVec &&s) noexcept {
if (&s != this) {
Free();
elements_ = s.elements_;
first_free_ = s.first_free_;
cap = s.cap;
s.elements_ = s.first_free_ = s.cap = nullptr;
}
return *this;
}
建议:(三五法则)
void StrVec::ReAllocate() {
auto new_capacity = Size() ? 2*Size() : 1;
auto new_data = alloc.allocate(new_capacity);
/* auto dest = new_data;
auto elem = elements_;
for (size_t i = 0; i != Size() ; ++i) {
alloc.construct(dest++, std::move(*elements_++));
}*/
auto last = std::uninitialized_copy(std::make_move_iterator(elements_), std::make_move_iterator(first_free_), new_data);
Free();
elements_ = new_data;
first_free_ = last;
cap = elements_ + new_capacity;
}
13.6.3 右值引用 和成员函数
但是,一般拷贝的值参数需要是常量,不允许修改,因此我们使用重载函数,一个拷贝版本一个移动版本。s1 + s2 = "wow!";
Foo &operator=(const Foo&) &; 只能向可修改的左值赋值
Foo sorted() &&;
Foo sorted() const &;
第十四关: 重载运算与类型转换
14.1 基本概念
其他重载运算符不能含有默认实参。
可以被重载的运算符
+ - * / % ^
& | ~ ! , =
< > <= >= ++ --
<< >> == != && ||
+= -= /= %= ^= &=
|= *= <<= >>= [] ()
-> ->* new new[] delete delete()
不能被重载的运算符
:: .= . ?:
data1 + data2
operator+(data1, data2)
data1 += data2
data1.operator+=(data2);
逗号运算符与取地址运算符有着特殊的含义,因为他们有着内置的函数,所以他们不应该被重载,否则他们的行为会非常怪异,导致不适合
提升: 尽量明智地使用运算符重载
14.2 输入输出运算符
14.2.1 重载输出运算符 <<
std::ostream &operator<<(std::ostream &os, SalesData &item) {
os << item.book_no() << " " << item.units_sold_ << " "
<< item.revenue_ << " " << item.AvgPrice();
return os;
}
14.2.2 重载输入运算符 >>
std::istream &operator>>(std::istream &is, SalesData &item) {
double price = 0;
is >> item.book_no_ >> item.units_sold_ >> price;
if(is)
item.revenue_ = price * item.units_sold_;
else
item = SalesData();
return is;
}
14.3 算术和关系运算符
SalesData&& operator+(const SalesData &lhs, const SalesData &rhs) {
SalesData sum = lhs;
sum += rhs;
return std::move(sum);
}
14.3.1 相等运算符
bool operator==(const StrBlobPtr &loo, const StrBlobPtr &roo) {
return (loo.weak_ptr_.lock() == roo.weak_ptr_.lock()) && (loo.curr == roo.curr);
}
bool operator!=(const StrBlobPtr &loo, const StrBlobPtr &roo) {
return !(loo == roo);
}
14.3.2 关系运算符
bool operator==(const StrBlobPtr &loo, const StrBlobPtr &roo) {
return (loo.weak_ptr_.lock() == roo.weak_ptr_.lock()) && (loo.curr == roo.curr);
}
bool operator!=(const StrBlobPtr &loo, const StrBlobPtr &roo) {
return !(loo == roo);
}
bool operator<(const StrBlobPtr &loo, const StrBlobPtr &roo) {
if(loo.weak_ptr_.lock() != roo.weak_ptr_.lock())
throw std::invalid_argument("argument is not match!");
return loo.curr < roo.curr;
}
14.4 赋值运算符
StrVec &StrVec::operator=(std::initializer_list<std::string> il) {
auto data = AllocNCopy(il.begin(), il.end());
Free();
elements_ = data.first;
first_free_ = cap = data.second;
return *this;
}
SalesData &SalesData::operator+=(const SalesData &rhs) {
units_sold_ += rhs.units_sold_;
revenue_ += rhs.revenue_;
return *this;
}
14.5 下标运算符
std::string &StrVec::operator[](size_t n) {
return *(elements_+n);
}
const std::string &StrVec::operator[](size_t n) const {
return *(elements_ + n);
}
14.6 递减与递增运算符
StrBlobPtr &StrBlobPtr::operator++() {
Check(curr,"increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr &StrBlobPtr::operator--() {
Check(--curr, "decrement past begin of StrBlobPtr");
return *this;
}
后置版本接受了一个额外的(不被使用的)int 类型的形参,显式调用要提供该实参。
StrBlobPtr operator++(int); //后置
StrBlobPtr operator--(int);
StrBlobPtr& operator++(); //前置
StrBlobPtr& operator--();
StrBlobPtr StrBlobPtr::operator++(int) {
StrBlobPtr ret = *this;
++*this;
return ret;
}
StrBlobPtr StrBlobPtr::operator--(int) {
auto ret = *this;
--*this;
return ret;
}
14.7 成员访问运算符
std::string &StrBlobPtr::operator*() const {
return DeRef();
}
std::string *StrBlobPtr::operator->() const {
return & this->operator*();
}
(*point).mem //point是一个内置指针类型
point.operator()->mem //point是类对象,执行的操作是从指向类中调用其成员函数
14.8 函数调用运算符
struct absInt {
int operator() (int val) const {
return val < 0 ? -val : val;
}
};
absInt absObj;
int ui = absObj(i);
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' ') : os(o), seq(c) {}
void operator()(const string &s) const { os << s << seq; }
private:
ostream &os;
char seq;
}
PrintString printer;
printer(s);
PrintString errors(cerr, '\n');
errors(s);
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
14.8.1 lambda 是函数对象
stable_sort(words.begin(), words.end(),
[] (const string &a,const string &b) { return a.size() < b.size(); });
其行为类似以下类的未命名对象
class ShorterString {
public:
bool operator() (const string &s1, const string &s2) const
{ return s1.size() < s2.size() ; }
}
14.8.2 标准库定义的函数对象
标准库函数对象
算术 关系 逻辑
plus<T> equal_to<T> logical_and<T>
minus<T> not_equal_to<T> logical_or<T>
multiplies<T> greater<T> logical_not<T>
divides<T> greater_equal<T>
modulus<T> less<T>
negate<T> less_equal<T>
sort(svec.begin(), svec.end(), greater<string>());
std::multiset<int, std::greater<int>> book_store;
book_store.insert(2);
book_store.insert(3);
cout << *book_store.begin();
14.8.3 可调用对象与 function
int add(int i, int j) { return i+j; }
auto mod = [](int i, int j) { return i%j; }
struct divide {
int operator() (int denominator, int divisor)
return denominator / divisor;
}
functio<T> f; f是一个用来存储可调用对象的空 function,这些可调用对象的调用形式类型
应该与函数 T 相同(T 是 retType(args))
function<T> f(nullptr); 显式构造一个空 function
function<T> f(obj); 在 f 中存储一个 可调用对象 obj 的副本
f 将 f 作为条件:当 f 含有一个可调用对象为真,否则为假
f(args) 调用f中的对象,参数是 args
定义为 function<T> 的成员的类型
result_type 该function 类型的可调用对象返回的类型
argument_type 当T有一个或两个实参时定义的类型,如果T有一个实参,那 argument_type
与其是同义词,如果 T 有两个实参,则 first_argument_type 和
second_argument 分别代表两个实参的类型
first_argument_type
second_argument_type
function<int(int, int)>
int add(int, int) {}
double add(double, double)
function<int(int,int)> f(add); //二义性
std::function<int(int, int)> f;
int (*fp)(int, int) = add;
f.operator=(fp);
cout<< f(1,2);
f.operator=([] (int a, int b) { return add(a, b); })
14.9 重载,类型转换与运算符
14.9.1 类型转换运算符
class SmallInt {
public:
SmallInt(int i = 0): val(i) {
if(i <0 || i > 255)
throw std::out_of_range ("");
}
operator int() const { return val; }
private:
std::size_t val;
}
SmallInt s1;
si = 4; //首先将 4 隐式转换为 SmallInt ,然后调用 operator=
si+3 //将 si 隐式转换为 int ,然后执行整数加法
提示: 避免过度使用类型转换函数
int i =42;
cin << i;
explict operator int() const { return val; }
14.9.2 避免有二义性的类型转换
struct B;
struct A {
A() = default;
A(const B&);
}
struct B {
operator A() const ;
}
A f(const A&);
B b;
A a = f(b); //二义性,不知道调用的是 转换构造函数 还是类型转换运算符
解决以上的问题,我们不得不显式调用 类型转换运算符 或者 转换构造函数
A a1 = f(b.operator A())
A a2 = f(A(b));
我们也无法利用强制类型转换解决二义性,因为强制转换类型本身也面临 二义性
struct A {
A(int = 0);
A(double);
operator int() const;
operator double() const;
}
void f2(long double);
A a;
f2(a) //不知道使用的是那一个类型转换运算符
long lg;
A a2(lg); //二义性
二义性的原因是因为他们所需的标准库类型转换级别一致。
当我们使用两个用户定义的类型转换时, 如果转换函数之前或之后存在标准库类型转换,则标准类类型转换将决定最佳匹配到底是哪一个。
提示: 类型转换与运算符
struct C{
C(int);
}
struct D{
D(int);
}
void manip(const C&);
void manip(const D&);
manip(10) 会产生二义性
我们可以通过显式地构造正确的类型消除二义性
manip( C(10))
struct E{
E(dobule);
}
void manip(const E&);
void manip(const D&);
manip(10) 会产生二义性
即使其中一个调用需要额外的标准库类型转换而另外一个精准匹配,编译器也会标识错误。14.9.3 函数匹配 与 重载运算符
a sym b
a.operatorsym(b) //a 有一个 operatorsym 成员函数
operatorsym(a, b ); // operaotrsym 是一个普通函数
我们不能通过调用的形式来区分当前调用是是成员函数还是非成员函数
class SmallInt {
friend SmallInt operator+(const SmallInt& , const SmallInt&);
public:
SmallInt(int = 0);
operator int() const { return val; }
private:
size_t val;
}
SmallInt s1,s2;
auto s3 = s1 +s2;
int i = s3 + 0; //二义性
第十五关 . 面向对象程序设计
15.1 OOP:概述
继承: 可以定义相似的类型并对其相似关系建模。
动态绑定: 可以在一定程度上忽略相似类型的区别, 并以统一的方式来使用他们的对象。
15.2 定义基类和派生类
15.2.1 基类定义
class Quote {
public:
Quote() = default;
Quote(const std::string &book, double sales_price)
: book_no_(book), price_(sales_price) {}
virtual ~Quote() = default; //对析构函数进行动态绑定
std::string isbn() const { return book_no_; };
virtual double net_price(std::size_t n) const
{ return n * price_; }
private:
std::string book_no_;
protected:
double price_ = 0.0;
};
15.2.2 定义派生类
public, protected 或者是 private。)class Bulk_Quote : public Quote {
public:
Bulk_Quote() = default;
Bulk_Quote(const std::string &, double, std::size_t, double);
double net_price(std::size_t) const override;
private:
std::size_t min_qty_ = 0; //使用折扣的最低购买量
double discount = 0.0; //以小数表示的折扣额
};
{ 在形参列表后面,或是在 const 成员函数的const 关键字后面,或者在引用成员函数的引用限定符后面添加 override 关键字}
Quote item;
Bulk_Quote bulk;
Quote *p = &item; //p指向 基类对象
p = &bulk; // p指向 bulk 的基类部分
Quote &r = bulk; // r 绑定到 bulk 的基类部分
Bulk_Quote(const std::string &book, double p, std::size_t qty, double discount)
: Quote(book, p), min_qty_(qty), discount_(discount) {}
double Bulk_Quote::net_price(std::size_t cnt) const {
if (cnt >= min_qty_)
return cnt * (1 - discount_) * price_;
return cnt * price_;
}
关键概念: 遵循基类的接口
class Base{
public :
static void Statement();
}
class Derived : public Base{
void f (const Derived &obj){
Base::statement(); //类作用域访问
Derived::statement();
obj.statement(); //通过对象访问
statement(); //this 访问
}
}
如果我们想让某个类作为基类,该类必须已经定义而非声明
因此最终派生类会包含它直接基类的字对象以及 每个间接基类的子对象。class FinalClass final {} //该类不能作为基类
15.2.3 类型转换与继承
动态类型: 变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。Quote Base;
Bulk_quote* bulkP = &base; //如果转换成功,我们会使用到 base本不存在的成员,是非法的
Quote item;
Bulk_Quote bulk;
Quote *p = &item; //p指向 基类对象
p = &bulk; // p指向 bulk 的基类部分
Bulk_Quote *bq = static_cast<Bulk_Quote *>(p);
Bulk_Quote bulk;
Quote item(bulk);
item = bulk;
关键概念:存在继承关系的类型之间的转换规则
15.3 虚函数
关键概念: C++ 的多态性
(D 是 B 派生类, 则 基类虚函数可以返回B* 而 D可以返回 D*)
Quote item;
Bulk_Quote bulk;
Quote *p = &item; //p指向 基类对象
p = &bulk; // p指向 bulk 的基类部分
p->Quote::net_price(42);
该代码强行调用 Quote 的 net_price 函数, 而不管 baseP 实际指向的对象类型到底是什么。该调用将在编译时完成解析。
15.4 抽象基类
class DiscQuote : public Quote{
public:
DiscQuote() = default;
DiscQuote(const std::string &book, double price, std::size_t qty, double disc): Quote(book, price), quantity_(qty), discount_(disc) {}
double net_price(std::size_t) const = 0;
protected:
std::size_t quantity_ = 0;
double discount_ = 0.0;
};
关键概念:重构
15.5 访问控制与继承
class Base {
protect:
int prot_mem;
}
class Sneay: pulbic Base {
friend void clobber(Sneaky&);
friend void clobber(Base &);
}
void clobbeer(Sneaky & s) { s.j = s.prot_mem = 0;} //正常访问
void clobber(Base &b) { b.prot_mem = 0; } //访问失败
class Base{
public :
void pub_mem;
protected:
int prot_mem;
private:
char priv_mem;
}
struct Pub_Derv : public Base {
//正确,派生类能访问 protected成员
int f() { return prot_mem; }
//错误,派生类不能访问 private 成员
char g() { return priv_mem; }
}
struct Priv_Derv : private Base {
//private 不影响派生类的访问权限
int f1() const { return prot_mem; }
}
Pub_Derv d1;
Priv_Derv d2;
d1.pub_mem(); //正确
d2.pub_mem(); //错误, pub_mem 是在派生类中是私有的。
struct Derived_from_Public : public Pub_Derv {
int use_base() { return prot_mem; } //Base::prot_mem 在 Pub_Derv 仍然是 protected的
}
struct Derived_from_private: public Priv_Derv {
// 错误, ase::prot_mem 在 Priv_Derv 是 private 的
int use_base() { return prot_mem; }
}
假定 D 继承自 B:
关键概念: 类的设计和受保护的成员
实现者:则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的 私有(实现)部分
class Base {
public :
std::size_t size() const { return n; }
protected:
std::size_t n;
}
class Derived : private Base {
public :
using Base::size;
protected:
using Base::n;
}
默认派生访问说明符: struct public class 是 private
15.6 继承中的 类作用域
Bulk_quote bulk;
cout << bulk.isbn();
int get_base_mem() { return Base::mem; }
}关键概念: 名字查找与继承
15.7 构造函数与拷贝控制
15.7.2 合成拷贝控制与继承
class Quote {
public:
Quote() = default;
Quote(const Quote &) = default;
Quote(Quote&&) = default;
Quote& operator=(const Quote &) = default;
Quote& operator=(Quote&&) = default;
virtual ~Quote() = default; //对析构函数进行动态绑定
Quote(const std::string &book, double sales_price)
: book_no_(book), price_(sales_price) {}
std::string isbn() const { return book_no_; };
virtual double net_price(std::size_t n) const
{ return n * price_; }
private:
std::string book_no_;
protected:
double price_ = 0.0;
};
15.7.3 派生类的拷贝控制成员
class Base{}
class D: public Base {
public:
D(const D &d) :Base(d)
{}
D(D &&d) : Base(std::move(d)) {}
}
D &D::operator=(const D &rhs)
{
Base::operator=(rhs); //显式为基类部分赋值
// 按照过去的方式为派生类的成员赋值
// 酌情处理自赋值及释放已有资源等情况
return *this;
}
class D:public Base{
public :
// Base::~被自动调用
~D(){}
}
15.7.4 继承的构造函数
class Bulk_Quote : public DiscQuote {
public:
using DiscQuote::DiscQuote; //继承 DiscQuote的构造函数
double net_price(std::size_t) const override;
private:
std::size_t min_qty_ = 0; //使用折扣的最低购买量
double discount_ = 0.0; //以小数表示的折扣额
};
derived(parms) : base(args){}
15.8 容器与继承
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1",50));
basket.push_back(make_shared<Bulk_Quote>("0-201",50,10,0.25));
15.8.1 编写 Basket 类
//Copyright C++Primer 15
//License BSD
//Author: Handling
//This is OOP test
#ifndef CPPPRIMER_OOPTEST_H
#define CPPPRIMER_OOPTEST_H
#include
//Copyright C++Primer 15
//License BSD
//Author: Handling
//This is OOP test
#include "ooptest.h"
using std::endl;
namespace mynamespace {
double Bulk_Quote::net_price(std::size_t cnt) const {
if (cnt >= min_qty_)
return cnt * (1 - discount_) * price_;
return cnt * price_;
}
double print_total(std::ostream &os, const Quote &item, size_t n) {
double ret = item.net_price(n);
os << "ISBN:" << item.isbn() << " # sold: " << n << "total due: " << ret << endl;
return ret;
}
double Basket::TotalReceipt(std::ostream &os) const {
double sum = 0.0;
for (auto iter = items_.cbegin(); iter != items_.cend(); iter = items_.upper_bound(*iter)) {
sum += print_total(os, **iter, items_.count(*iter));
}
return sum;
}
}
15.9 文本查询程序再谈
#pragma once
//Copyright C++Primer
//license (BSD)
//Author : Handling
//This is TextQuery program
#ifndef CPPPRIMER_TEXTQUERY_H_
#define CPPPRIMER_TEXTQUERY_H_
#include
//Copyright C++Primer
//license (BSD)
//Author : Handling
//This is TextQuery program
#include "textquery.h"
using std::vector;
using std::string;
using std::istringstream;
using std::getline;
using std::make_shared;
using std::set;
using std::shared_ptr;
namespace mynamespace {
TextQuery::TextQuery(std::ifstream &is):file_data_(make_shared<vector<string>>()) {
string text;
while (getline(is, text)) {
file_data_->push_back(text);
int n = file_data_->size() -1;
istringstream line(text);
string word;
while (line >> word) {
auto &lines = wm[word];
if(!lines)
lines.reset(new set<line_no>);
lines->insert(n);
}
}
}
QueryResult TextQuery::Query(const std::string &sought) const {
static shared_ptr<set<line_no>> no_data(new set<line_no>);
auto loc = wm.find(sought);
if (loc != wm.end())
return QueryResult(sought, no_data, file_data_);
else
return QueryResult(sought, loc->second, file_data_);
}
std::ostream &operator<<(std::ostream &os, const Query &query) {
os << query.Rep();
return os;
}
std::ostream &print(std::ostream &os, const QueryResult &qr) {
os << qr.sought_ << " occurs " << qr.line_nos_->size()
<< (qr.line_nos_->size() <= 1 ? "time" : "times") << std::endl;
for (auto num : *qr.line_nos_)
os << "\t (line " << num + 1 << ") " << *(qr.file_->begin() + num) << std::endl;
return os;
}
QueryResult OrQuery::Eval(const TextQuery &text) const {
auto right = rhs_.Eval(text), left = lhs_.Eval(text);
auto ret_lines = make_shared<set<line_no>>(left.begin(), left.end());
ret_lines->insert(right.begin(), right.end());
return QueryResult(Rep(), ret_lines, left.file());
}
QueryResult AndQuery::Eval(const TextQuery &text) const {
auto right = rhs_.Eval(text), left = lhs_.Eval(text);
auto ret_lines = make_shared<set<line_no>>();
std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), std::inserter(*ret_lines, ret_lines->begin()));
return QueryResult(Rep(), ret_lines, left.file());
}
QueryResult NotQuery::Eval(const TextQuery &text) const {
auto result = query.Eval(text);
auto ret_lines = make_shared<set<line_no>>();
auto beg = result.begin(), end = result.end();
auto sz = result.file()->size();
for (size_t n = 0; n != sz; ++n) {
if (beg != end && n == *beg) {
++ beg;
continue;
}
ret_lines->insert(n);
}
return QueryResult(Rep(), ret_lines, result.file());
}
}
第 十六关: 模板与泛型编程
16.1 定义模板
int compare(const string &v1, const string &v2) {
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
int compare(const double &v1, const double &v2) {
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
16.1.1 函数模板
template <typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
vector<int> vec1{1, 2, 3};
vector<int> vec2{4, 5, 6};
//根据模板实例化出了 int compare(const vector
template <typename T>
T Foo(T *p) {
T temp = *p;
//...
return temp;
}
template <typename T, class U> calc(const T&, const U&);
template <unsigned N, unsigned M>
int compare(const char(&p1)[N], const char(&p2)[M]) {
return strcmp(N,M);
}
int main(int argc,char **argv) {
compare("sss", "sss");
}
N = 4, M = 4
template <unsigned N, unsigned M> inline
int compare(const char(&p1)[N], const char(&p2)[M]) {
return strcmp(N,M);
}
关键概念: 模板与头文件
16.1.2 类模板
template <typename T> class Blob {
public:
using value_type = T;
using size_type = typename std::vector<T>::size_type;
//构造函数
Blob();
Blob(std::initializer_list<T> il);
//Blob 的元素数量
size_type size() const { return data_->size(); }
bool empty() const { return data_->empty(); }
//添加与删除元素
void push_back(const T &t) { data_->push_back(t); }
void push_back(T &&t) { data_->push_back(std::move(t)); }
void pop_back();
// 元素访问
T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data_;
void Check(size_type i, const std::string &msg) const;
};
Blob <int> ia;
Blob <int> ia2 = {0,1,2,3,4};
vector<shared_ptr<int>>
ret-type StrBlob::member-name(parm-list)
对应Blob 成员是
template <typename T>
ret-type Blob<T>::member-name(parm-list)
template<typename T>
inline Blob<T>::Blob(std::initializer_list<T> il): data_(std::make_shared<std::vector<T>(il)>) {
}
template<typename T>
inline void Blob<T>::pop_back() {
Check(0, "pop back on empty Blob");
data_->pop_back();
}
template<typename T>
inline T &Blob<T>::back() {
Check(0, "back on empty Blob");
return data_->back();
}
template<typename T>
inline T &Blob<T>::operator[](size_type i) {
Check(i, "subscript out of range");
return (*data_)[i];
}
template<typename T>
inline void Blob<T>::Check(size_type i, const std::string &msg) const {
if (i >= data_->size())
throw std::out_of_range(msg);
}
Blob<int> squares = {0, 1, 2 ,3}; //仅仅实例化 Blob
template <typename T> class BlobPtr {
public:
BlobPtr(): curr_(0) {}
BlobPtr(Blob<T> &a, size_t sz = 0) : wptr_(a.data_), curr_(sz) { }
T& operator*() const { auto p = Check(curr_, "dereference past end"); return (*p)[curr_]; }
BlobPtr& operator++();
BlobPtr& operator--();
private:
std::shared_ptr<std::vector<T>> Check(std::size_t, const std::string &) const;
std::weak_ptr<std::vector<T>> wptr_;
std::size_t curr_;
};
template<typename T>
inline BlobPtr<T> &BlobPtr<T>::operator++() {
Check(curr_, "curr is end");
++ curr_;
return *this;
}
template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T>
bool operator==(const Blob<T> &, const Blob<T> &);
template <typename T> class Blob {
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T> &, const Blob<T> &);
};
template <typename T> class Pal;
class C {
friend class Pal<C>;
template <typename T> friend class Pal2; //Pal2 的所有实例都是 C的友元;这种情况无须前置声明
};
template <typename T> class C2 {
friend class Pal<T>; //C2 的每个实例将相同实例化的 Pal声明为友元,需要前置声明
template <typename X> friend class Pal2; // Pal2的所有实例都是 C2的每个实例的友元,不需要前置声明
friend class Pal3; //非模板类,是C2 所有实例的友元,不需要 Pal3的前置声明
};
template <typename Type> class Bar {
friend type; //将访问权限授予用来实例化 Bar 的类型,也可以用内置类型实例化 Bar这样的类
};
template <typename T> using twin = pair<T, T>;
twin <string> authors;
template <typename T> using partNo = pair<T, unsigned>;
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
}
定义:
template <typename T> size_t Foo<T>::ctr = 0;
使用:
Foo<int> f1;
auto ct = Foo<int>::count();
ct = f1.count();
ct = Foo::count();
16.1.3 模板参数
template <typename Foo> Foo calc(const Foo &a, const Foo &b) {
Foo tmp = a;
return tmp;
}
typedef double A;
template <typename A, typpename B> void f(A a, B b){
A tmp = a; //tmp 的类型为模板参数 A 的类型,而非 double
double B; // 错误:重声明模板参数B
}
template <typename V, typename V> // 错误
template <typename T> int compare(const T &, const T &); //声明但不定义
template <typename T> class Blob;
//3个 calc都指向相同的函数模板
template <typename T> T calc(const T &, const T &); //声明
template <typename U> U calc(const U &, const U &); //声明
template <typename Type> Type calc(const Type &a, const Type &b) {
}
而在 模板代码中, 遇到 T::mem 这样的代码时,它不会知道 mem 是一个类型成员还是一个 static 数据成员,直到实例化才知道。为了处理模板,编译器必须直到名字是否表示一个类型。template <typename T>
typename T::value_type top(const T &c){
if (!c.empty)
return c.back();
else
return typename T::value_type();
}
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F()){
if(f(v1, v2)) return -1;
if(f(v2, v1)) return 1;
return 0;
}
bool j = compare(0, 42);
template <class T = int> class Number {
public:
Numbers(T v = 0): val(v) {}
private:
T Val;
}
Numbers<> average_precision;
16.1.4 成员模板
定义一个 普通(非模板)类的成员模板,用来删除动态内存并打印信息
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr): os(s){}
template <typename T>
void operator()(T *p) const {
os << "deleting unique_ptr "<< std::endl;
delete p;
}
private:
std::ostream &os;
};
使用:
double *p = new double;
DebugDelete d;
d(p);
将其对象作为 unique_ptr 的删除器
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
unique_ptr 析构函数会调用 DebugDelete的调用运算符
template <typename T> class Blob {
template <typename It> Blob(It b, It e);
};
template <typename T>
template <typename It>
Blob<T>::Blob(It b, It e): data(std::make_shared<std::vector<T>>(b,e)) {}
vector<long> vi = {0, 1, 2, 3, 4};
Blob<int> a1(vi.begin(), vi.end());
16.1.5 控制实例化
形式如下:extern template declaration; //实例化声明
template declaration; //实例化定义
extern template class Blob<string> //声明
template int compare(const int &, const int &); //定义
//Application.cpp
//这些模板需要在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int &,const int &);
Blob<string> sa1, sa2; //实例化在其他位置
Blob<int> a1 = {0, 1, 2, 3, 4};
Blob<int> a2(a1); //拷贝构造函数在本文件实例化
int i = compare(a1[0], a2[0]);// 实例化在其他位置
//templateBuild.cpp
//实例化文件必须为每个在其他文件中声明为 extern 的类型和函数提供一个(非 extern)的定义
template int compare(const int &, const int &);
template class Blob<string>;
16.1.6 效率与灵活性
shared_ptr 在运行时绑定删除器, shared_ptr 使用户重载删除器更方便。16.2 模板实参推断
16.2.1 类型转换与模板类型参数
其余编译器不会实参进行类型转换,而是生成一个新的模板实例。
long lng;
compare(lng, 1024); //错误,不能实例化出 compare(long, int)
//实参可以不同,但是依赖于函数模板中的内容,需要兼顾
template <typename A, typename B>
int flexibleCompare(const A &v1, const B &v2) {
if(v1 < v2) return -1;
if(v2 <v1) return 1;
return 0;
}
long lng;
compare(lng, 1024); //正确
template <typename T> ostream &print(ostream &os, const T &obj){
return os << obj;
}
print(cout, 42);
16.2.2 函数模板显式实参
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
16.2.3 尾置返回类型于类型转换
而其他情况下,让用户去确定其返回类型是很痛苦的,且不准确。
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg){
return *beg;
}
template <typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
return *beg;
}
对 Mod,其中Mod是
若T 是
则 Mod::type是
remove_reference
X& 或 X&&
X
否则
T
add_const
X&, const X或函数
T
否则
const T
add_lvalue_reference
X&
T
X&&
X&
否则
T&
add_rvalue_reference
X& 或 X&&
T
否则
T&&
remove_pointer
X*
X
否则
T
add_pointer
X& 或 X&&
X*
否则
T*
make_signed
unsigned X
X
否则
T
make_unsigned
带符号类型
unsigned X
否则
T
remove_extent
X[n]
X
否则
T
remove_all_extents
X[n1][n2]…
X
否则
T
16.2.4 函数指针和实参判断
template <typename T> int compare(const T &, const T &);
int (*pf1)(const int &, const int &) = compare;
void func(int (*)(const string &, const string &));
void func(int(*)(const int &, const int &));
func(compare) //二义性错误,编译失败
func(compare<int>);
16.2.5 模板实参推断于引用
template <typename T> void f3(T &&)
f3(42)
using X = T&;
using X = T&&;
template <typename T> void f3(T &&)
f3(ci); //实参是一个左值,模板参数T 是 int&
以上的f3(i)的实例化是这样推断的
void f3<int&>(int& &&); //当 T 是int&,函数参数为 int& &&
即使f3 的函数参数是 T&& 且 T 是int&, 因此 T&& 是 int& &&,会折叠为 int&
void f3<int&>(int&);
非常困难(remove_reference这样的类型转换类也许有帮助)template <typename T> void f(T&&); //绑定到非const 的右值
template <typename T> void f(const T&); //左值与const 右值(字面值)
16.2.6 理解 std::move
//在返回类型和类型转换中也要用到 typename
template <typename T>
typename remove_reference<T>::type&& move(T&& t){
return static_cast<typename remove_reference<T>::type&& >(t);
}
16.2.7 转发
//接受一个可调用对象 与两个参数的模板
//对 "翻转"的参数调用给顶的可调用对象
//flip1 是一个不完整的实现: 顶层const 和引用丢失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2){
f(t2, t1);
}
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {
f(t2, t1);
}
void g(int &&i, int &j)
flip2(g, i, 42); 因为 t1,t2不管是右值引用还是左值引用,都是左值表达式,因此 当左值表达式作为实参传递给 右值引用形参是不对的。
template <typename Type> intermediary(Type && arg)
{
finalFcn(std::forward<Type>(arg));
}
16.3 重载与模板
但是,如果有多个函数提供同样好的匹配,则:
template <typename T> string debug_rep(const T &t){
ostringstream ret;
ret << t;
return ret.str();
}
template <typename T> string debug_rep(T *p){
ostreamstring ret;
ret << "pointer:" << p;
if (p)
ret << " " <<debug_rep(*p);
else
ret << "null pointer";
return ret.str();
}
16.4 可变参数模板
template <typename T, typename... args>
void foo(const T &t, const args& ... rest);
void foo(const int &, const string &, const int &, const double &);
void foo(const int &, const string &);
int main(int argc,char **argv) {
foo(0, 42,1,3);
}
template <typename T, typename... args>
void foo(const T &t, const args & ... rest) {
cout << sizeof...(args) << endl;
cout << sizeof...(rest) << endl;
}
16.4.1 编写可变参数函数模板
16.4.2 包扩展
16.5 模板特例化
第 十七 关: 标准库特殊设施
17.1 tuple 类型
tuple<T1,T2,T3.....Tn> t; t 使用了可变参数模板, 其所有成员值初始化
tuple<T1,T2,T3.....Tn> t(v1, v2, ....vn); 其成员固定初始值初始化,构造函数是 explicit
make_tuple(v1, v2, v3,....vn) 返回一个用给定初始值值初始化的 tuple,从初始值进行推断
t1 == t2; 成员相等且数量相等,则相同,否则不同
t1 != t2;
t1 relop t2 关系运算符使用字典序
get<i>(t) 索引地 i 个数据元素的引用,t是左值,则为左值引用,否则右值引用
tuple_size<tupleType>::value 获取其 tupleType 的成员数量,其value 是public constexpr static
tuple_element<i, tupleType>::type 给定tuple 类型中指定成员的类型
std::tuple<int, int, unsigned> t;
t = {1, 2, 3};
cout << std::get<1>(t); //输出索引 1 的元素值
std::tuple<int, int, unsigned> t2;
cout << (t < t2);
cout << std::tuple_size<decltype(t)>::value;
std::tuple_element<1, decltype(t)>::type;
typedef tuple<vector<SalesData>::size_type,
vector<SalesData>::const_iterator,
vector<SalesData>::const_iterator> matches;
vector<matches> FindBook(const vector<vector<SaleData>> &files, const string &book);
17.2 bitset 类型
bitset<n> b;
b 有n 位,每一位都为 0 ,默认构造函数是 constexpr
bitset<n> b(u);
b是 ull 值 u 的低 n 位的拷贝,如果 n 大于 ull 的大小,
则 b 超出 ull 的高位 被置为0, 该构造函数也是 constexpr
bitset<n> b(s, pos, m, zero, one); //explicit
b 是string s 从pos 开始的 m 个字符的拷贝,
s 只能包含字符 zero,one,如果存在其他字符,
抛出 invalid_argument异常, pos 默认 0, m string::npos, zero默认为 ’0‘,one为 ’1‘
bitset<n> b(cp, m, zero, one) //explicit
cp是字符数组,未提供 m,cp必须指向一个 c风格字符串,提供了cp至少含义 m 个 zero或one
b.any()
b 中是否存在置位的二进制位
b.all()
b中的所有位都置位了吗
b.none()
b中不存在置位的二进制吗
b.count()
b中置位的位数
b.size()
一个 constexpr函数,返回 b 中的位数
b.test(pos)
若 pos 位是置位的,返回true 否则返回 false
b.set(pos,v)
将 pos 位设置位 bool 值, v默认 true, 未传递实参,则b中所有位置位
b.set()
b.reset(pos) 将位置 pos 处的位复位,或全部复位
b.reset()
b.flip(pos) 改变位置 pos 处位的状态,或者改变 b 的每一位的状态
b.flip()
b[pos] 访问b中位置 pos 处的位,如果 b 是const,则位置位时 返回 true,否则 false
b.to_ulong() 返回一个 unsigned long 或一个 unsigned long long 的值,位模式与 b 相同,
b.to_ullong() 如果 b 中位模式不能放入指定结果类型,则抛出 overflow_error 异常
b.to_string(zero, one) 返回 string,表示 b 中位模式
os << b 将b 中二进制位打印位 字符 1或0,到 流中
is >> b 从is中读取字符存入 b。当下一个字符不是 1或0 或是已经读入 b.size()个位,
结束
17.3 正则表达式
regex
表示有一个正则表达式的类
regex_match
将一个字符序列与一个正则表达式匹配,匹配返回 true
regex_search
寻找第一个与正则表达式匹配的子序列,匹配返回 true
regex_replace
使用给顶格式替换一个正则表达式
sregex_iterator
迭代器适配器,调用 regex_search 来遍历一个 string 中所有匹配的子串
smatch
容器类,保存在 string 中搜素的结果
ssub_match
string 中匹配的子表达式的结果
regex_search 和 regex_match 的参数
注意:这些操作返回 bool 值,指出是否找到匹配
(seq, m, r,mft)
(seq, r, mft)
1. 在字符序列seq中查找 regex 对象 r 中的正则表达式。
seq可用是一个 string,表示范围的一对迭代器或字符数组指针
2. m 是一个match对象, 用来保存匹配结果的相关细节。 m 和 seq 必须具有兼容的类型
3. mft 是一个可选的 regex_constants::match_flag_type 值,它们会影响撇皮过程
17.3.1 使用正则表达式库
regex r(re) re是一个正则表达式,(string,字符数组指针,迭代器对,计数器或花括号列表,)
regex r(re, f) f 是指出对象如何处理的标准,默认未 ECMAScript
r1 = re; 将 r1 中正则表达式替换未 re
r1.assign(re, f) 与赋值运算符相同,
r.mark_count() r中子表达式的数目
r.flags() 返回 r 的标志集
注: 构造函数和赋值操作可能抛出 regex_error异常
定义regex时指定的标志
定义在 regex 和 regex_constants::synatax_option_type中
icase 在匹配过程中忽略大小写
nosubs 不保存匹配的子表达式
optimize 执行速度优于构造速度
ECMAScirpt 使用ECMA-262 指定的语法
basic 使用 POSIX基本的正则表达式语法
extended 使用 POSIX 扩展的正则表达式语法
awk 使用 POSIX 版本的awk语法
grep 使用 POSIX 版本的 grep 语法
egrep 使用 POSIX 版本的 egrep 语法
定义在 regex 和 regex_constants::error_type 中
error_collate 无效的元素校对请求
error_ctype 无效的字符类
error_escape 无效的转移字符或无效的尾置转义
error_backref 无效的向后引用
error_brack 不匹配的方括号([])
error_paren 不匹配的小括号 (())
error_brace 不匹配的花括号({})
error_badbrace {} 中无效的范围
error_range 无效的字符范围 【z-a】
error_space 内存不足,无法处理该正则表达式
error_badrepeat 重复字符(* ? + {)之前没有有效的正则表达式
error_complexity 要求匹配过于复杂
error_stack 栈空间不足,无法匹配
输入序列类型 使用正则表达式类
string regex, smatch, ssub_match, sregex_iterator
const char* regex, cmatch, csub_match, cregex_iterator
wstring wregex, wsmatch, wssub_match, wsregex_iterator
const wchar* wregex, wcmatch, wcsub_match, wcregex_iterator
17.3.2 匹配与 Regex 迭代器类型
操作适用于其他 RE库迭代器
sregex_iterator it(b, e, r)
一个迭代器,遍历迭代器 b,e 表示的 string,调用 sregex_search(b, e, r) 将 it 定位到输入中第一个匹配的位置
sergex_iterator end;
sergex_iterator 的尾后迭代器
*it 根据最后一个调用 regex_search 的结果,返回一个 smatch对象的引用或一个指向 smatch 对象的指针
it->
++it 从输入序列当前匹配位置开始再次调用 regex_match
it++
it1 == it2
it1 != it2
m.ready()
已经通过调用regex_search 或 regex_match 设置了 m,则返回true,否则返回 false,且false 下 不能对m进行操作
m.size()
如果匹配失败,返回0,否则返回最近依次匹配的正则表达式中子表达式的数目
m.empty()
若 m.size() 为 0, 返回 true
m.prefix()
一个 ssub_match 对象,表示当前匹配之前的 序列
m.suffix()
一个 ssub_match 对象,表示当前匹配之后的 序列
m.format(。。。)
m.length(n)
第n 个匹配的子表达式的大小
m.position(n)
第 n 个子表达式据序列开始的举例
m.str(n)
第n个子表达式匹配的 string
m[n]
对应 第 n 个子表达式的 ssub_match 对象
m.begin(),m.end()
表示 m 中 sub_match 元素返回的迭代器,和往常一样,c系列表示const_iterator
m.cbegin(),m.cend()
17.3.3 使用子表达式
string phone = "(\\()?(\\d{3})(\\))?([-. ])?(\\d{3})([-. ]?)(\\d{4})";
std::regex r(phone);
std::smatch m;
string s;
while (std::getline(cin, s)) {
for (std::sregex_iterator it(s.begin(), s.end(), r), end;
it != end; ++it) {
if(valid(*it)) //校验其中的子序列的正确性
cout << "valid: " << it->str() << endl;
else
cout << "not valid" << it->str() << endl;
}
}
matched 一个public bool,指出此 ssub_match 是否匹配了
first public数据成员,指出匹配序列首元素与尾后位置的迭代器,如果未匹配,其相等
second
length() 匹配的大小,如果 matched 为fakse 返回 0
str() 返回一个包含输入匹配部分的string,如果 matched 为 false,返回空 string
s = ssub 将 ssub_match 转换为 string
17.3.4 使用 regex_replace
string phone = "(\\()?(\\d{3})(\\))?([-. ])?";
std::regex r(phone);
std::smatch m;
string s;
string fmt = "$2.$5.$7"; //将号码格式改为 ddd.ddd.ddd
while (std::getline(cin, s))
cout << std::regex_replace(s, r, fmt) <<endl;
return 0;
17.4 随机数
引擎 类型,生成随机 unsigned 整数序列
分布 类型,使用引擎返回服从特定概率分布的随机数
17.4.1 随机数引擎和分布
17.5 IO 库再探
17.5.1 格式化输入输出
标准库选择可读性更好的格式: 非常大或非常小的打印为 科学计数法形式, 其他值打印 定点十进制形式
setfill(ch) 用ch 填充空白
setprecision(n) 将浮点精度设置为n
setw(w) 读值或写值的宽度为 w个字符
setbase(b) 将整数输出为 b 进制
17.5.2 未格式化的输入/输出操作
第十八关: 用于大型程序的工具
大规模应用程序的特殊要求包括:
18.1 异常处理
18.1.1 抛出异常
18.1.2 捕获异常
catch (my_error &eobj) {
eObj.status = errCodes::severeErr;
throw;
} catch (other_error eObj) {
eObj.status = errCodes::badErr;
throw;
}
s
try {
} catch (...) {
}
18.1.3 函数 try 语句块与构造函数
template<typename T>
inline Blob<T>::Blob(std::initializer_list<T> il)
try: data_(std::make_shared<std::vector<T>(il)>) {
} catch(const std::bad_alloc &e) {
//处理内存异常
}
18.1.4 noexcept 异常说明
void recoup(int) noexcept; //不会抛出异常
void alloc(int); //可能会抛出异常
上述过程对是否执行栈展开未作约定。
void f() noexcept(noexcept(g())) ; f 与 g 的异常说明一致
noexcept(e) 如果 e 本身不含有 throw 语句,为 true,否则false
18.1.5 异常类层次
这些面向应用的异常类表示了与应用相关的异常条件。
class out_of_stock : public std::runtime_error {
public:
explicit out_of_stock(const std::string &s): std::runtime_error(s) {}
};
class isbn_mismatch : public std::logic_error { //逻辑错误
public:
explicit isbn_mismatch(const std::string &s): std::logic_error(s) {}
isbn_mismatch(const std::string &s, const std::string &lhs, const std::string &rhs)
: std::logic_error(s), left(lhs), right(rhs) {}
const std::string left, right;
};
SalesData &SalesData::operator+=(const SalesData &rhs) {
if (book_no() != rhs.book_no())
throw isbn_mismatch("wrong isbns", book_no(), rhs.book_no());
units_sold_ += rhs.units_sold_;
revenue_ += rhs.revenue_;
return *this;
}
SalesData item1, item2, sum;
while (cin >> item1 >> item2) {
try {
sum = item1 + item2;
} catch (const mynamespace::isbn_mismatch &e) {
cerr << e.what();
}
}
18.2 命名空间
18.2.1 命名空间定义
namespace nsp {
}
namespace nsp {
}
CPlusPlusPrimer& operator+(const CPlusPlusPrimer &rhs);
CPlusPlusPrimer &CPlusPlusPrimer::operator+(const CPlusPlusPrimer &rhs) {
return *this;
}
如果不在同一命名空间下:
mynamespace::CPlusPlusPrimer& mynamespace::CPlusPlusPrimer::operator+=(const CPlusPlusPrimer &rhs){
return *this;
}
namespace mynamespace {
namespace handling {
const std::string kNameStr;
}
}
std::cout << mynamespace::handling::kNameStr;
namespace {
int vec_count;
}
18.2.2 使用命名空间成员
namespace mynp = mynamespace;
using std::string;
using namespace mynamespace;
提示: 避免使用 using 指示
18.2.3 类,命名空间与作用域
int A::C1::F3() 从 c1 开始 -> A ->全局作用域
std::string s;
std::cin >> s;
18.2.4 重载与命名空间
18.3 多重继承
18.3.1 多重继承
class A : public B, public C {}
18.3.2 类型转换与多个基类
18.3.3 多重继承下的类作用域
18.3.4 虚继承
18.3.5 构造函数与虚继承
第十九关: 特殊工具与技术
19.1 控制内存分配
19.1.1 重载 new 和 delete
void * operator new(size_t);
void * operator new[](size_t);
void *operator delete(void*) noexcept;
void *operaotr delete[](void*) noexcept
//以下版本承诺不会抛出异常
void *operator new(size_t, nothrow_t&) noexcept;
void *operator delete(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete[](size_t, nothrow_t&) noexcept;
auto p = new (nothrow)int;
auto p = new (para) int;
void *operator new (size_t sz){
if (void *mem = malloc(sz))
return mem;
else
throw std::bad_alloc();
}
void operator delete(void *mem) noexcept {
free(mem);
}
19.1.2 定义 new 表达式
new (place _address) type;
new (place _address) type(initializers);
new (place _address) type [size];
new (place _address) type [size] { braced initializer list};
string *sp = new string("a value");
sp -> ~string();
19.2 运行时类型识别
19.2.1 dynamic_cast 运算符
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
if (Derived *dp = dynamic_cast<Derived*>(bp))
{
//转换成功
} else {
//转换失败
}
void f(const Base &b){
try {
const Derived &d = dynamic_cast<const Derived &>(b);
//使用 b 引用的Derived
}
catch (bad_cast){
}
}
19.2.2 typeid 运算符
typeid(e)
Derved *dp = new Derived;
Base *bp = dp;
if (typeid(*bq) == typeid(*dp))
if (typeid(*bq) == typeid(Derived))
19.2.3 使用 RTTI
class Base {
friend bool operator==(const Base &lhs, const Base &rhs);
public:
//Base 的接口成员
protected:
virtual bool Equal(const Base &) const;
};
class Derived: public Base {
protected:
bool Equal(const Base &base) const override;
};
bool Derived::Equal(const Base &base) const {
auto r = dynamic_cast<const Derived&>(base);
return *this == r;
}
bool Base::Equal(const Base &rhs) const {
return *this == rhs;
}
bool operator==(const Base &lhs, const Base &rhs) {
return typeid(lhs) == typeid(rhs) && rhs.Equal(rhs);
}
19.2.4 type_info 类
t1 == t2 表示同一类型相等
t1 != t2
t.name() 返回 C风格字符串, 表示类型名字的可打印形式,因系统而异
t1.before(t2) 返回bool指, t1 是否位于 t2 前
19.3 枚举类型
enum class open_modes{ input, output, append };
enum color { red, yellow, green};
enum {floatPrec = 6, doublePrec = 10 };
在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同enum intValues :unsigned long long {
charTyp = 255,
};
enum intValue: unsigned long long;
enum class open_modes;
19.4 类成员指针
19.5 嵌套类
19.6 union:一种节省空间的类
union Token {
char cval;
int ival;
double dval;
}
19.7 局部类
19.8 固有的不可移值的特性。
19.8.1 位域
19.8.2 volatile 限定符
19.8.3 链接指示: extern “c”