C++ Primer 中文版 第5版 读书笔记

读书过程中发现,读得越多,忘得越多。因此记录读书笔记

目录

1.2 初始输入输出

向流写入数据

<<运算符(输出运算符)接受两个运算对象:左侧的运算对象必须是一个ostream对象,右侧的运算对象是要打印的值。此运算符将给定的值写到给定的ostream对象中。输出运算符的计算结果就是其左侧运算对象。即,计算结果就是我们写入给定值的那个ostream对象。

std::cout<<"Enter two numbers:"<<std::endl;
// 等价于
(std::cout<<"Enter two numbers:")<<std::endl;

第一个输出运算符给用户打印一条消息。这个消息是一个字符串字面值常量(stringliteral),是用一对双引号包围的字符序列。在双引号之间的文本被打印到标准输出。
第二个运算符打印endl,这是一个被称为操纵符(manipulator)的特殊值。写入endl的效果是结束当前行,并将与设备关联的缓冲区(buffer)中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。
在这里插入图片描述

1.4.3 读取数量不定的输入数据

int a = 0;
while (std::cin >> a)
{
    // todo
}

while (std::cin >> a)此表达式从标准输入读取下一个数,保存在value中。输入运算符返回其左侧运算对象。因此,此循环条件实际上检测的是std::cin。当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符(end-of-file),或遇到一个无效输入时(例如读入的值不是一个整数),istream对象的状态会变为无效

2.1 基本内置类型

2.1.1 算术类型

在这里插入图片描述

2.1.3 字面值常量

转义序列

在这里插入图片描述
在这里插入图片描述
对于一个整型字面值来说,我们能分别指定它是否带符号以及占用多少空间。如果后缀中有U,则该字面值属于无符号类型,也就是说,以U为后缀的十进制数、八进制数或十六进制数都将从unsigned int、unsigned long和unsigned long long中选择能匹配的空间最小的一个作为其数据类型。如果后缀中有L,则字面值的类型至少是long;如果后缀中有LL,则字面值的类型将是long long和unsigned long long中的一种。显然我们可以将U与L或LL合在一起使用。例如,以UL为后缀的字面值的数据类型将根据具体数值情况或者取unsigned long,或者取unsigned long long。
在这里插入图片描述
在这里插入图片描述

列表初始化

在这里插入图片描述

2.2.2 变量声明和定义的关系

分离式编译
如果想声明一个变量而非定义它,就在变量前添加关键字extern,而且不要显示地初始化变量:

extern int i;	// 声明i,而非定义i
int j;		// 声明并定义j

任何包含了显示初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再时声明,而变成了定义:

extern double pi = 3.14159;		// 定义

变量能且只能被定义一次,但是可以多次声明

2.3.1 引用

在这里插入图片描述

2.3.2 指针

void* 指针

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

double obj = 3.14, *pd = &obj;	// 正确:void* 可以存放任意类型对象的地址

void *pv = &obj;		// obj可以是任意对象的类型
pv = pd;

利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针,或者进行强制类型转换。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对。

指向指针的指针

一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推:
在这里插入图片描述

指向指针的引用

在这里插入图片描述
要理解r的类型到底是什么,最简单的办法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号。)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引l用的是一个int指针。
在这里插入图片描述

2.4 const限定符

有时我们希望定义这样一种变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量的类型加以限定:

const int bufSize =512;//输入缓冲区大小

const int i=get size();	//正确:运行时初始化
const int j=42;	//正确:编译时初始化
const int k;	//错误:k是一个未经初始化的常量

在这里插入图片描述

2.4.1 const 的引用

可以把引用绑定到const 对象上,就像绑定到其他对象上一样,我们呢称之为对象常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:

const int ci = 1024;
const int &r1 = ci;		// 正确:引用及其对应的对象都是常量

r1 = 42;		// 错误:r1是对常量的引用
int &r2 = ci;	// 错误:图让一个非常量引用指向一个常量对象
初始化和对const的引用

在这里插入图片描述

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

在这里插入图片描述

2.4.4 constexpr 和常量表达式

常量表达式(constexpression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:

const int max_files = 20;	// max_files 是常量表达式
const int limit = max_files + 1;	// limit 是常量表达式
int staff_size = 27;		// staff_size 不是常量表达式
const int sz = get_size();	// sz不是常量表达式

尽管staffsize的初始值是个字面值常量,但由于它的数据类型只是一个普通int而非constint,所以它不属于常量表达式。另一方面,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

constexpr 变量

在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

constexpr int mf = 20;	// 20是常量表达式
constexpr int limit = mf + 1;	// 是常量表达式
constexpr int sz  = size();		// 只有当size是一个constexpr函数时,才是一条正确的声明语句

尽管不能使用普通函数作为constexpr变量的初始值,但是正如6.5.2节(第214页)将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
在这里插入图片描述

指针和constexpr

必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效(相当于底层const),与指针所指的对象无关:

const int *p=nullptr;	//p是一个指向整型常量的指针
constexpr int *g=nullptr;	//g是一个指向整数的常量指针

p和g的类型相差甚远,p是一个指向常量的指针,而g是一个常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const(参见2.4.3节,第57页)。

2.5.1 类型别名

类型别名(typealias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。有两种方法可用于定义类型别名。传统的方法是使用关键字typedef

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

基本类型和复合类型均可使用
新标准规定了一种新的方法,使用别名声明(aliasdeclaration)来定义类型的别名:

using SI=Sales_item;		//SI是Sales_item的同义词
2.5.2 auto 类型说明符

编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:

//由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2;	//item初始化为val1和val2相加的结果

使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i = 0*p = &i;	//正确:i是整数、p是整型指针
auto sz = 0,pi = 3.14;	//错误:sz和pi的类型不一致

auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:

const int ci = i;
auto b = ci;	// b 的类型是int

如果希望推断出的auto类型是一个顶层const,需要明确指出:

const auto f = ci;//ci的推演类型是int,f是constint

还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:

auto &g = ci;	//g是一个整型常量引用,绑定到ci
auto &h = 42;	//错误:不能为非常量引用绑定字面值
const auto &j =42;	//正确:可以为常量引用绑定字面值

decltype 类型指示符

有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:

decltype(f()) sum = x;		//sum的类型就是函数f的返回类型

编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型。换句话说,编译器为sum指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。

decltype处理顶层const和引l用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):

const int ci = 0, &cj = ci;
decltype(ci) x = 0;		// x 的类型是const int
decltype(cj) y = x;		// y 的类型是const int&,y 绑定到变量x
decltype 和引用

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。如4.1.1节(第120页)将要介绍的,有些表达式将向decltype返回一个引用类型。一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:

//decltype的结果可以是引用类型
int i = 42, *p = &i,&r = i;
decltype(r+0) b;	//正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c;		//错误:c是int&,必须初始化

因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
**decltype和auto的另一处重要区别是,decltype的结果类型与表达式形式密切相关。**有一种情况需要特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到引用:

//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d;		// 错误: d是int&,必须初始化

在这里插入图片描述

3.1 命名空间的using声明

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

在这里插入图片描述
在这里插入图片描述

3.2.1 定义和初始化string对象

标准库类型string表示可变长的字符序列
在这里插入图片描述

直接初始化和拷贝初始化

使用=则是拷贝初始化,反之,则是直接初始化

string s5 = "hiya";		// 拷贝初始化
string s6("hiya");		// 直接初始化

3.2.2 string对象上的操作

在这里插入图片描述

string::size_type 类型

string::size()返回值类型为size_type,他是一个无符号类型的值,而且能够存放下任何string对象的大小。
如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果,如:
假设n是一个具有负值,那么s.size()<n几乎肯定是true。因为负值会自动转换成一个比较大的符号值。
在这里插入图片描述

比较string对象

比较字典顺序
在这里插入图片描述
在这里插入图片描述

字面值和string对象相加

当把string对象和字符字面值即字符串字面值均在一条语句中使用时,必须保证每个加法的两侧运算对象至少有一个时string

string s2 = "world";
string s7 = "hello" + ", " + s2;		// 错误

在这里插入图片描述

3.2.3 处理string 对象中的字符

头文件:cctype
在这里插入图片描述

3.3 标准库类型vector

标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被称作容器(container)。
C++语言既有类模板(classtemplate),也有函数模板,其中vector是一个类模板。
在这里插入图片描述
在这里插入图片描述

3.3.1 定义和初始化vector 对象

在这里插入图片描述

列表初始化vector对象
vector<string> v1{ "a", "an", "the" };		// 列表初始化
vector<string> v2("a", "an");		// 错误
列表初始值还是元素数量

如果用的是花括号,可以表述成我们想列表初始化(listinitialize)该vector对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式

vector<int> v1(10);		// 10个元素,每个都是0
vector<int> v2{10};		// 	一个元素,值为10
vector<string> v5{"hi"};	// 列表初始化
vector<string> v6("hi");	// 错误

vector<int> v3(10, 1);		// 10个元素,每个都是1
vector<int> v4{10, 1};		// 两个元素
vector<string> v7{10};		// 有10个默认初始化的元素
vector<string> v8{10"hi"};	// 有10个值为“hi”的元素

3.3.3 其他vector操作

在这里插入图片描述
在这里插入图片描述

3.4 迭代器

类似指针类型,迭代器提供了对对象的间接访问。就选代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其他所有情况都属于无效。

3.4.1 使用迭代器

和指针不一样的是,获取迭代器不是用取地址符,迭代器拥有自己的成员函数。
在这里插入图片描述

迭代器运算符

在这里插入图片描述

迭代器类型

就像不知道string和vector的size_type成员到底是什么类型一样,一般也不需要知道

vector<int>::iterator it;
vector<int>::const_iterator it3;	// 常量指针,不能修改指向的元素值,等于底层const
某些对vector对象的操作会使迭代器失效
  1. 不能在循环中向vector添加元素
  2. 任何一种可能改变vector对象容量的操作,都会使迭代器失效
    在这里插入图片描述

3.4.2 迭代器运算

在这里插入图片描述

迭代器的算术运算

在这里插入图片描述

3.5 数组

数组是一种类似于标准库类型vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同。与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。因为数组的大小固定,因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。
在这里插入图片描述

3.5.1 定义和初始化内参数组

在这里插入图片描述

显式初始化数组元素

在这里插入图片描述

字符数组的特殊性

注意字符串字面值的结尾处还有一个空字符
在这里插入图片描述

不允许拷贝和赋值

在这里插入图片描述

理解复杂的数组声明

难点
在这里插入图片描述
在这里插入图片描述

3.5.3 指针和数组

通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:

string nums[] = {"one", "two"};
string *p = &nums[0];

然而,数组还有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:

string *p2 = nums;	// 等价于 p2 = &nums[0];

在这里插入图片描述
auto 会将数组名转为数组首地址,而decltype则不会

int ia[] = {0, 1, 2, 3};
auto ia2(ia);	// ia2是一个整形指针,指向ia的第一个元素,int*
ia2 = 42;	// 错误:ia2是指针

auto ia2(&ia[0]);	// ia2为 int*

decltype(ia) ia3 = {0, 1, 2, 3};	// ia3 的类型为int[]
ia3 = p;	// 错误:不能用整型指针给数组赋值
指针也是迭代器

能。vector和string的迭代器支持的运算,数组的指针全都支持。通过数组名字或者数组中首元素的地址都能得到指向首元素的指针;不过获取尾后指针就要用到数组的另外一个特殊性质了。我们可以设法获取数组尾元素之后的那个并不存在的元素的地址:

int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = arr;
++p;
int *e = &arr[10];		// 指向arr尾元素的下一位置的指针

这里显然使用下标运算符索引了一个不存在的元素,arr有10个元素,尾元素所在位置的索引是9,接下来那个不存在的元素唯一的用处就是提供其地址用于初始化e。就像尾后迭代器一样,尾后指针也不指向具体的元素。因此,不能对其进行解引用

标准库函数 begin 和 end

尽管能计算得到尾后指针,但这种用法极易出错。**为了让指针的使用更简单、更安全,C+11新标准引入了两个名为begin和end的函数。**这两个函数与容器中的两个同名成员功能类似。

int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *beg = std::begin(ia);
int *end = std::end(ia);

begin函数返回指向ia首元素的指针,end函数返回指向ia尾元素下一位置的指针,这两个函数定义在iterator头文件中。
PS:此外还有std::cbegin()、std::cend()

解引用和指针运算的交互

指针加上一个整数所得的结果还是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针:

int ia[] = {0, 2, 4, 6, 8};
int last = *(ia + 4);	// 把last初始化成8,等于ia[4]
int tLast = *ia + 4;	// last = ia[0] + 4
下标和指针

在这里插入图片描述

3.5.4 C风格字符串

在这里插入图片描述字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的C风格字符串(C-stylecharacterstring)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(nullterminated)。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符(“\0”)。一般利用指针来操作这些字符串。

char ca[] = {'C', '+', '+'};		// 错误:不以空字符结束
C标准库String 函数

在这里插入图片描述

比较字符串

比较两个C风格字符串的方法和之前学习过的比较标准库string对象的方法大相径庭。

string sl="Astring example";
string s2="A different string";
// s2 小于 s1

如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身:

const char cal[]="A string example";
const char ca2[]="A different string";
if (cal < ca2)		//未定义的:试图比较两个无关地址
if (strcmp(ca1, ca2) < 0)	// 和两个string对象的比较s1<s2效果一样

谨记之前介绍过的,当使用数组的时候其实真正用的是指向数组首元素的指针。

目标字符串的大小由调用者指定

正确的方法是使用strcat函数和strcpy函数。

//如果我们计算错了largeStr的大小将引发严重错误
strcpy(largeStr, cal);//把ca1拷贝给largeStr
strcat(largeStr, " ");//在largeStr的末尾加上一个空格
strcat(largeStr, ca2);//把ca2连接到largeStr后面

一个潜在的问题是,我们在估算largeStr所需的空间时不容易估准,而且1argeStr所存的内容一旦改变,就必须重新检查其空间是否足够。不幸的是,这样的代码到处都是,程序员根本没法照顾周全。这类代码充满了风险而且经常导致严重的安全泄漏。
在这里插入图片描述

3.5.5 与旧代码的接口

混用string对象和C风格字符串
string s("HelloWorld");	//s的内容是Hello World
char*str=s;					//错误:不能用string对象初始化char*
const char*str=s.c_str();	//正确

在这里插入图片描述

使用数组初始化 Vector 对象
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(std::begin(int_arr), std::end(int_arr));
vector<int> subVec(int_arr + 1, int_arr + 4);

在这里插入图片描述

3.6 多维数组

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用多维数组大有益处。当一个数组的元素仍然是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:

int ia[3][4];	//大小为3的数组,每个元素是含有4个整数的数组
				//大小为10的数组,它的每个元素都是大小为20的数组,
				//这些数组的元素是含有30个整数的数组
int arr[10][20][30]={0};		//将所有元素初始化为0

按照由内而外的顺序阅读此类定义有助于更好地理解其真实含义。在第一条语句中,我们定义的名字是ia,显然ia是一个含有3个元素的数组。接着观察右边发现,ia的元素也有自己的维度,所以ia的元素本身又都是含有4个元素的数组。再观察左边知道,真正存储的元素是整数。因此最后可以明确第一条语句的含义:它定义了一个大小为3的数组,该数组的每个元素都是含有4个整数的数组。使用同样的方式理解arr的定义。首先arr是一个大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素又都是含有30个整数的数组。实际上,定义数组时对下标运算符的数量并没有限制,因此只要愿意就可以定义这样一个数组:它的元素还是数组,下一级数组的元素还是数组,再下一级数组的元素还是数组,以此类推。对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

多维数组初始化
int ia[3][4] = {
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11}
};

其中内层嵌套着的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,完成的功能和上面这段代码完全一样:

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

类似于一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化列表之内。如果仅仅想初始化每一行的第一个元素,通过如下的语句即可:

//显式地初始化每行的首元素
int ia[3][4]={{ 0 }, { 4 }, { 8 }};

其他未列出的元素执行默认值初始化,这个过程和一维数组(参见3.5.1节,第102页)一样。在这种情况下如果再省略掉内层的花括号,结果就大不一样了。下面的代码

// 显示初始化第一行
int ix[3][4] = {0, 3, 6, 9};
多维数组的下标引用

可以使用下标运算符来访问多维数组的元素,此时数组的每个维度对应一个下标运算符。如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是给定索引处的一个内层数组

//用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1];	//把row绑定到ia的第二个4元素数组上
使用范围 for 语句处理多维数组

由于在C++11新标准中新增了范围for语句,所以前一个程序可以简化为如下形式:

size_tcnt=0;
for (auto &row : ia)	//对于外层数组的每一个元素
	forauto &col : row) {		//对于内层数组的每一个元素
		col=cnt;				//将下一个值赋给该元素
		++cnt;//将cnt加1
		}
	}
}

这个循环赋给ia元素的值和之前那个循环是完全相同的,区别之处是通过使用范围for语句把管理数组索引的任务交给了系统来完成。因为要改变元素的值,所以得把控制变量row和col声明成引用类型
在这里插入图片描述

指针和多维数组

当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
在这里插入图片描述
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:

intia[3][4];	//大小为3的数组,每个元素是含有4个整数的数组
int(*p)[4] = ia;	//p指向含有4个整数的数组
p = &ia[2];			//p指向ia的尾元素

*们首先明确(p)意味着p是一个指针。接着观察右边发现,指针p所指的是一个维度为4的数组;再观察左边知道,数组中的元素是整数。因此,p就是指向含有4个整数的数组的指针。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述

4.1.1 运算符基本概念

C++定义了一元运算符(unaryoperator)和二元运算符(binaryoperator)。作用于个运算对象的运算符是一元运算符,如取地址符(&)和解引用符*作用于两个运算对象的运算符是二元运算符,如相等运算符(==)和乘法运算符(*)。除此之外,还有一个作用于三个运算对象的三元运算符。函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。
一些符号既能作为一元运算符也能作为二元运算符。以符号*为例,作为一元运算符时执行解引用操作,作为二元运算符时执行乘法操作。一个符号到底是一元运算符还是二元运算符由它的上下文决定。对于这类符号来说,它的两种用法互不相干,完全可以当成两个不同的符号。

重载运算符

C++语言定义了运算符作用于内置类型和复合类型的运算对象时所执行的操作。**当运算符作用于类类型的运算对象时,用户可以自行定义其含义。**因为这种自定义的过程事实上是为已存在的运算符赋予了另外一层含义,所以称之为重载运算符(overloadedoperator)。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算符都是重载的运算符。

左值和右值

C++ 表达式要不然是右值,要不然就是左值。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
左值可以出现在等号两边,但右值只能出现在等号右边

在C++中,左值(Lvalue)和右值(Rvalue)是两种重要的表达式分类。它们的主要区别如下:

  • 左值(Lvalue):左值是一个表达式,它指向内存中的一个固定地址,并且可以出现在赋值操作的左侧。左值通常指的是变量的名字,它们在程序的整个运行期间都存在。左值的判断标准:可以取地址、有名字的就是左值

  • 右值(Rvalue):右值是一个临时的、不可重复使用的表达式,它不能出现在赋值操作的左侧。**右值通常包括字面量、临时生成的对象以及即将被销毁的对象。**右值的判断标准:不可以取地址、没有名字的就是右值

这两种表达式的分类有助于理解C++中的一些重要概念,如右值引用和移动语义。例如,右值引用允许我们将一个对象的资源(如动态内存)从一个对象转移到另一个对象,而不是进行复制。这可以提高代码效率,特别是在涉及到资源管理(如动态内存、文件句柄等)的场景中¹。

以下是一个简单的示例,展示了左值和右值的区别:

#include <iostream>
void printLvalueRvalue(int& lvalue,  int&& rvalue) {
    std::cout <<  "Lvalue: "  << lvalue << std::endl;
    std::cout <<  "Rvalue: "  << rvalue << std::endl;
}
int main() {
    int  a =  10;  // a 是一个左值
    printLvalueRvalue (a, std:: move (a));  //std::move将左值转换为右值,后进行右值传递
    return 0;
}

在这个例子中,a 是一个左值,因为它有一个固定的内存地址并且可以出现在赋值操作的左侧。而 std::move(a) 是一个右值,因为它是一个临时的、不可重复使用的表达式。这个例子也展示了 std::move 的用法,它可以将一个左值转换为右值¹。这在实现移动语义时非常有用,因为它允许我们将一个对象的资源从一个对象转移到另一个对象,而不是进行复制。

4.1.2 优先级与结合律

复合表达式(compoundexpression)是指含有两个或多个运算符的表达式。

4.1.3 求值顺序

优先级规定了运算对象的组合方式,但是没有说明运算对象按照什么顺序求值。在大多数情况下,不会明确指定求值的顺序。对于如下的表达式

int i = f1() * f2();

我们知道f1和f2一定会在执行乘法之前被调用,因为毕竟相乘的是这两个函数的返回值。但是我们无法知道到底f1在f2之前调用还是f2在f1之前调用。

int i = 0;
std::cout << i << " " << ++i << std::endl;		// 可能输出1 1

因为程序是未定义的,所以我们无法推断它的行为。编译器可能先求++i的值再求i的值,此时输出结果是11:也可能先求i的值再求++i的值,输出结果是01:甚至编译器还可能做完全不同的操作。因为此表达式的行为不可预知,因此不论编译器生成什么样的代码程序都是错误的。
有4种运算符明确规定了运算对象的求值顺序。

  1. 逻辑与(&&),它规定了先求左侧运算对象的值,只有当左侧运算对象的值为真才继续求右侧的值
  2. 逻辑或(||),左侧对象为假时才求右侧对象值
  3. 三目运算符
  4. 逗号运算符
求值顺序、优先级、结合律

运算对象的求值顺序与优先级和结合律无关,在一条形如f() + g() * h() + j()的表达式中:

  • 优先级规定,g()的返回值和h()的返回值相乘。
  • 结合律规定,f()的返回值先与g()和h()的乘积相加,所得结果再与j()的返回值相加。
  • 对于这些函数的调用顺序没有明确规定。
    如果f、g、h和j是无关函数,它们既不会改变同一对象的状态也不执行1O任务,那么函数的调用顺序不受限制。反之,如果其中某几个函数影响同一对象,则它是一条错误的表达式,将产生未定义的行为。

4.2 算术运算符

在这里插入图片描述

4.3 逻辑和关系运算符

关系运算符作用于算术类型或指针类型,逻辑运算符作用于任意能转换成布尔值的类型。逻辑运算符和关系运算符的返回值都是布尔类型。值为0的运算对象(算术类型或指针类型)表示假,否则表示真。对于这两类运算符来说,运算对象和求值结果都是右值。
在这里插入图片描述

4.4 赋值运算符

赋值运算符的左侧运算对象必须是一个可修改的左值。如果给定

int i = 0, j = 0, k = 0;		// 初始化而非赋值
const int ci = i;				// 初始化而非赋值

i + j = k;		// 算数表达式时右值
ci = k;			// ci时常量左值

C++11新标准允许使用花括号括起来的初始值列表作为赋值语句的右侧运算对象

k = {3.14};		// 错误:窄化转换
std::vector<int> vi;
vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};	// vi含有10个元素

如果左侧运算对象是内置类型,那么初始值列表最多只能包含一个值,而且该值即使转换的话其所占空间也不应该大于目标类型的空间。
无论左侧运算对象的类型时什么,初始化列表都可以为空。此时,编译器创建一个值初始化的临时变量并将其赋值给左侧运算对象。

赋值运算满足右结合律
int ival, jval;
ival = jval = 0;		// 正确:都被赋值为0

**因为赋值运算符满足右结合律,**所以靠右的赋值运算jval=0作为靠左的赋值运算符的右侧运算对象。又因为赋值运算返回的是其左侧运算对象,所以靠右的赋值运算的结果(即jval)被赋给了ival。
对于多重赋值语句中的每一个对象,它的类型或者与右边对象的类型相同、或者可由右边对象的类型转换:

int ival, *pval;
ival = pval = 0;		// 错误:(int*)无法转为int
std::string s1, s2;
s1 = s2 = "OK";			// 正确:字符串字面量转换成string对象

因为ival和pval的类型不同,而且pval的类型(int*)无法转换成ival的类型(int),所以尽管0这个值能赋给任何对象,但是第一条赋值语句仍然是非法的。

赋值运算符的优先级较低

赋值语句经常会出现在条件当中。因为赋值运算的优先级相对较低,所以通常需要给赋值部分加上括号使其符合我们的原意。

int i = get_value();
while(i != 42) {
	// do something
	i = get_value();
}

// batter
int i;
while((i = get_value()) != 42) {
	// do something
}

这个版本的while条件更容易表达我们的真实意图:不断循环读取数据直至遇到42为止。其处理过程是首先将get_value函数的返回值赋给i,然后比较i和42是否相等。
如果不加括号的话含义会有很大变化,比较运算符!=的运算对象将是get_value函数的返回值及42,比较的结果不论真假将以布尔值的形式赋值给i,这显然不是我们期望的结果。
在这里插入图片描述

复合赋值运算符

在这里插入图片描述
唯一的区别是左侧运算对象的求值次数:**使用复合运算符只求值一次,使用普通的运算符则求值两次。**这两次包括:一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。其实在很多地方,这种区别除了对程序性能有些许影响外几乎可以忽略不计。

4.5 递增和递减运算符

递增运算符(++)和递减运算符(–)为对象的加1和减1操作提供了一种简洁的书写形式。这两个运算符还可应用于迭代器,因为很多迭代器本身不支持算术运算,所以此时递增和递减运算符除了书写简洁外还是必须的。
在这里插入图片描述

在一条语句中昏庸解引用和递增运算符

如果我们想在一条复合表达式中既将变量加1或减1又能使用它原来的值,这时就可以使用递增和递减运算符的后置版本。

auto pbeg = v.begin();

while (pbeg != v.end() && *pbeg > 0) 
	std::cout << *pbeg++ << std::endl;

后置递增运算符的优先级高于解引l用运算符,因此pbeg++等价于(pbeg++)。pbeg++把pbeg的值加1,然后返回pbeg的初始值的副本作为其求值结果,此时解引用运算符的运算对象是pbeg未增加之前的值。最终,这条语句输出pbeg开始时指向的那个元素,并将指针向前移动一个位置。
在这里插入图片描述

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

在这里插入图片描述

4.6 成员访问运算符

点运算符(参见1.5.2节,第21页)和箭头运算符(参见3.4.1节,第98页)都可用于访问成员,其中,点运算符获取类对象的一个成员:箭头运算符与点运算符有关,表达式ptr->mem等价于(*ptr).mem

std::string s1 = "a string", *p = &s1;
auto n = s1.size();
n = (*p).size();
n = p->size();

因为解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。

4.7 条件运算符(三目运算符)

cond ? expr1 : expr2;

在这里插入图片描述

在输出表达式中使用条件运算符

条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。例如,有时需要根据条件值输出两个对象中的一个,如果写这条语句时没把括号写全就有可能产生意想不到的结果:

std::cout << ((grade < 60) ? "fail : "pass");		// 输出pass或者fail
std::cout << (grade < 60) ? "fail : "pass";		// 输出1或者0
std::cout << grade < 60 ? "fail : "pass";		// 错误:试图比较cout和60

在这里插入图片描述

4.8 位运算符

位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能,如17.2节(第640页)将要介绍的,一种名为bitset的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用于bitset类型。
在这里插入图片描述
在这里插入图片描述

位移运算符

在这里插入图片描述

位求反运算符

在这里插入图片描述
在这里插入图片描述

位与、位或、位异或运算符

在这里插入图片描述

位移运算符满足左结合律

尽管很多程序员从未直接用过位运算符,但是几乎所有人都用过它们的重载版本来进行I0O操作。重载运算符的优先级和结合律都与它的内置版本一样,因此即使程序员用不到移位运算符的内置含义,也仍然有必要理解其优先级和结合律。
因为移位运算符满足左结合律,所以表达式

std::cout << "hi" << "there" << std::endl;
// 等同于
((std::cout << "hi") << "there" ) << std::endl;

**移位运算符的优先级不高不低,介于中间:比算术运算符的优先级低,但比关系运算符、赋值运算符和条件运算符的优先级高。**因此在一次使用多个运算符时,有必要在适当的地方加上括号使其满足我们的要求。

4.9 sizeof 运算符

sizeof运算符返回一条表达式或一个类型名字所占的字节数。sizeof运算符满足右结合律,其所得的值是一个sizet类型(参见3.5.2节,第103页)的常量表达式(参见2.4.4节,第58页)。运算符的运算对象有两种形式:

sizeof(type);
sizeof expr;

在第二种形式中,sizeof返回的是表达式结果类型的大小。与众不同的一点是,sizeof并不实际计算其运算对象的值:

Sales_data data, *p;

sizeof(Sales_data);		// 存储Sales_data类型的对象所占的空间大小
sizeof data;			// data的类型的大小,即sizeof(Sales_data)
sizeof p;				// 指针所占的空间大小
sizeof *p;				// p所指类型的空间大小,即sizeof(Sales_data)
sizeof data.revenue;	// ales_data的revenue成员对应类型的大小

这些例子中最有趣的一个是sizeof*p。首先,因为sizeof满足右结合律并且与*运算符的优先级一样,所以表达式按照从右向左的顺序组合。也就是说,它等价于sizeof(*p)。其次,因为sizeof不会实际求运算对象的值,所以即使p是一个无效(即未初始化)的指针(参见2.3.2节,第47页)也不会有什么影响。
在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用。sizeof不需要真的解引用指针也能知道它所指对象的类型。

4.11类型转换

在C++语言中,某些类型之间有关联。如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。换句话说,如果两种类型可以相互转换(conversion),那么它们就是关联的。
int ival = s.541 + 3;
加法的两个运算对象类型不同:3.541的类型是double,3的类型是int。C++语言不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动执行的,无须程序员的介入,有时甚至不需要程序员了解。因此,它们被称作隐式转换(implicitconversion)。

4.11.1 算术转换

算术转换(arithmeticconversion)的含义是把一种算术类型转换成另外一种算术类型,这一点在2.1.2节(第32页)中已有介绍。算术转换的规则定义了一套类型转换的层次,其中运算符的运算对象将转换成最宽的类型。例如,如果一个运算对象的类型是long double,那么不论另外一个运算对象的类型是什么都会转换成long double。还有种更普遍的情况,当表达式中既有浮点类型也有整数类型时,整数值将转换成相应的浮点类型。

整型体征

整型提升(integralpromotion)负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都能存在int里,它们就会提升成int类型:否则,提升成unsigned int类型就如我们所熟知的,布尔值false提升成0、true提升成1。较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。

无符号类型的运算对象

在这里插入图片描述

理解算术转换

在这里插入图片描述
在这里插入图片描述

4.11.2 其他隐式类型转换

除了算术转换之外还有几种隐式类型转换,包括如下几种。
数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:

int ia[10];
int *ip = ia;		// 转换成指向数组首元素的指针

当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。
指针的转换:C++还规定了几种其他的指针转换方式,包括常量整数值0或者字面值nullptr能转换成任意指针类型;指向任意非常量的指针能转换成void*:指向任意对象的指针能转换成const void*
转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true:

char *cp = get_string();
if (cp){}		// 如果指针cp不是0,条件为真
while (*cp) {}	// cp不是空字符,条件为真

转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针引用。

int i;
const int &j = i;		// 非常量转换成constint的引用
const int *p = &i;		// 非常量的地址转换成const的地址
int &r = j, *p = q;		// 错误:不允许const转换成非常量

相反的转换并不存在,因为它试图删除掉底层const。
类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行种类类型的转换。在7.5.4节(第263页)中我们将看到一个例子,如果同时提出多个转换请求,这些请求将被拒绝。
在这里插入图片描述

4.11.3 显示转换

有时我们希望显式地将对象强制转换成另外一种类型。称之为强制类型转换(cast)。
在这里插入图片描述

强制类型转换的命名

一个强制类型转换格式如下
cast_name<type>(expression)
static_castdynamic_castconst_castreinterpret_cast

static_cast

任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。例如,通过将一个运算对象强制转换成double类型就能使表达式执行浮点数除法:

int a = -10;
size_t b = 5;

auto d = a / static_cast<int>(b);

当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息:但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
static_cast对于编译器无法自动执行的类型转换也非常有用。例如,我们可以使用static_cast找回存在于void*指针中的值:

double d;
void *p = &d;
double *dp = static_cast<double*>(p);
const_cast

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

const char *pc;
char *p = const_cast<char*>(pc);

对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(castawaytheconst)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。如果对象本身不是一个常量,使用强制类型转换获得写权限是合法的行为。然而如果对象是一个常量,再使用const_cast执行写操作就会产生未定义的后果。
只有const_cast能改变表达式的常量属性,使用其他形式的命名强制类型转换改164变表达式的常量属性都将引l发编译器错误。同样的,也不能用const_cast改变表达式的类型:
在这里插入图片描述

reinterpret_cast

reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
在这里插入图片描述
在这里插入图片描述

4.12 运算符优先级表

在这里插入图片描述
在这里插入图片描述

5 语句

5.3.2 switch 语句

case关键字和它对应的值一起被称为case标签(caselabel)。case标签必须是整型常量表达式,任何两个case标签的值不能相同,否则就会引发错误。另外,default也是一种特殊的case标签。

switch内部的控制流

理解程序在case标签之间的执行流程非常重要。如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch的结尾处才会停下来。要想避免执行后续case分支的代码,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个case标签之前应该有一条break语句。
然而,也有一些时候默认的switch行为才是程序真正需要的。每个case标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉break语句,使得程序能够连续执行若干个case标签。

漏写break容易引发缺陷

有一种常见的错觉是程序只执行匹配成功的那个case分支的语句。例如,下面程序的统计结果是错误的:
在这里插入图片描述
在这里插入图片描述

switch内部的变量定义

如前所述,switch的执行流程有可能会跨过某些case标签。如果程序跳转到了某个特定的case,则switch结构中该case标签之前的部分会被忽略掉。这种忽略掉一部分代码的行为引出了一个有趣的问题:如果被略过的代码中含有变量的定义该怎么办?答案是:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。
在这里插入图片描述
假设上述代码合法,则一旦控制流直接跳到false分支,也就同时略过了变量file_name和ival的初始化过程。此时这两个变量位于作用域之内,跟在false之后的代码试图在尚未初始化的情况下使用它们,这显然是行不通的。因此C++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。

case true:
	{
		std::string fileName = getFileName();
	}
	break;
case false:
	if(fileName.empty())	// 错误:fileName不在作用域范围内

5.5.1 break语句

break语句(breakstatement)负责终止离它最近的while、dowhile、for或switch语句,并从这些语句之后的第一条语句开始继续执行。

5.5.2 continue 语句

continue语句(continuestatement)终止最近的循环中的当前迭代并立即开始下一次选代。continue语句只能出现在for、while和dowhile循环的内部,或者嵌套在此类循环里的语句或块的内部。和break语句类似的是,出现在嵌套循环中的continue语句也仅作用于离它最近的循环。和break语句不同的是,只有当switch语句嵌套在迭代语句内部时,才能在switch里使用continue。

try 语句块和异常处理

异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。
异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:

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

5.6.1 throw 表达式

程序的异常检测部分使用throw表达式引发一个异常。throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw表达式后面通常紧跟一个分号,从而构成一条表达式语句。

Sales_item item1, item2;
cin >> item1 >> item2;
if(item1.isbn() == item2.isbn())
	throw runtime_error("Data must refer to same ISBN");
std::cout << item1 + item2;

在这段代码中,如果ISBN不一样就抛出一个异常,该异常是类型runtime_error的对象。抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码。

5.6.2 try 语句块

在这里插入图片描述
try语句块的一开始是关键字try,随后紧跟着一个块,这个块就像大多数时候那样是花括号括起来的语句序列。
跟在try块之后的是一个或多个catch子句。catch子句包括三部分:关键字catch、括号内一个(可能未命名的)对象的声明(称作异常声明,exceptiondeclaration)以及一个块。当选中了某个catch子句处理异常之后,执行与之对应的块。catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。

处理异常代码

示例如下

while(cin >> item1 >> item2) {
	try {
		// do
	} catch (runtime_error err) {
		// do
	}
}

程序本来要执行的任务出现在try语句块中,这是因为这段代码可能会抛出一个runtime_error类型的异常。
try语句块对应一个catch子句,该子句负责处理类型为runtime_error的异常。如果try语句块的代码抛出了runtime_error异常,接下来执行catch块内的语句。

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.6.3 异常标准

C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:

  • exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。
  • stdexcept头文件定义了几种常用的异常类,详细信息在表5.1中列出。
  • new头文件定义了bad_alloc异常类型,这种类型将在12.1.2节(第407页)详细介绍。
  • type_info头文件定义了bad_cast异常类型,这种类型将在19.2节(第731页)详细介绍。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6. 1 函数基础

一个典型的函数(function)定义包括以下部分:返回类型(return type)、函数名字、由0个或多个形参(parameter)组成的列表以及函数体。函数执行的操作在语句块中说明,该语句块称为函数体(function body)。
我们通过调用运算符(calloperator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

函数返回类型

大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组(参见3.5节,第101页)类型或函数类型,但可以是指向数组或函数的指针。我们将在6.3.3节(第205页)介绍如何定义一种特殊的函数,它的返回值是数组的指针(或引用),在6.7节(第221页)将介绍如何返回指向函数的指针。

6.1.1 局部对象

自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当达到所在的块末尾时销毁它。我们把只存在于块执行期间的对象成为自动对象。
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。

局部静态对象

某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。**局部静态对象(localstaticobject)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,**在此期间即使对象所在的函数结束执行也不会对它有影响。
如果局部静态变量没有显式的初始值,它将执行值初始化(参见3.3.1节,第88页),内置类型的局部静态变量初始化为0。

6.1.2 函数声明

和其他名字一样,**函数的名字也必须在使用之前声明。类似于变量(参见2.2.2节,第41页),函数只能定义一次,但可以声明多次。**唯一的例外是如15.3节(第535页)将要介绍的,如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义。
因为函数的声明不包含函数体,所以也就无须形参的名字。事实上,**在函数的声明中经常省略形参的名字。尽管如此,写上形参的名字还是有用处的,**它可以帮助使用者更好地理解函数的功能。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型(function prototype)。

6.2 参数传递

在这里插入图片描述
和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,他将绑定到对应的实参上;否则,将实参的值拷贝后赋值给形参。
当形参是引用类型时,我们说它对应的实参被引用传递或者函数被引用调用。引用形参时它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个互相独立的对象。我们说这样的实参被值传递或者函数被传值调用,

6.2.1 传值参数

指针形参

指针的行为和其他引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后两个指针时不同的指针

void reset(int *ip)
{
    *ip = 0; // 改变了指针ip所指向对象的值
    ip = 0;  // ip是拷贝的,只改变了ip的局部拷贝,实参未被改变
}

在这里插入图片描述

6.2.2 传引用参数

回忆过去所学的知识,我们知道对于引用的操作实际上是作用在引用所引的对象上。

使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。直接使用引用比较明智
在这里插入图片描述

使用引用形参返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。
该如何定义函数使得它能够既返回位置也返回出现次数呢?

  1. 一种方法是定义一个新的数据类型,让它包含位置和数量两个成员。
  2. 还有另一种更简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:

6.2.3 const 形参和实参

当形参是const,必须要注意关于顶层const的讨论。
顶层const作用与对象本身
**和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。**换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:

void func(const int i) {}

调用fcn函数时,既可以传入const int也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:

void func(const int i){}

void func(int i){}		// 错误:重复定义了func(int)

在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。

指针或引用形参与const

可以使用非常量初始化一个底层const 对象,但是反过来不行
可以使用非常量初始化一个顶层const对象,但是反过来不行

void reset(int *ip)
{
    *ip = 0; // 改变了指针ip所指向对象的值
    ip = 0;  // ip是拷贝的,只改变了ip的局部拷贝,实参未被改变
}

在这里插入图片描述

尽量使用常量引用

把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组(参见3.5.1节,第102页)以及使用数组时(通常)会将其转换成指针(参见3.5.3节,第105页)。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

void print(const int *);
void print(const int[]);		// 函数的意图是作用于一个数组
void print(const int[10]);		// 这里的维度表示我们期望数组含有多少元素,实际不一定

如果我们传给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。
在这里插入图片描述
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。

使用标记指定数组长度

管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串(参见3.5.4节,第109页)。C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止:

void print(const char *cp)
{
    if (cp)
        while (*cp)
        {
            std::cout << *cp++;
        }
}

这种方法适用于那些有明显结束标记且该标记不会与普通数据混淆的情况,但是对于像int这样所有取值都是合法值的数据就不太有效了。

使用标准库规范

管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发,关于其细节将在第Ⅱ部分详细介绍。使用该方法,我们可以按照如下形式输出元素内容:

void print(const int *beg, const int *end)
{
    while (beg != end)
        std::cout << *beg++;
}

//
print(std::begin(arr), std::end(arr));

为了调用这个函数,我们需要传入两个指针:一个指向要输出的首元素,另一个指向尾元素的下一位置。

显式传递一个表示数组大小的形参

第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。使用该方法,可以将print函数重写成如下形式:

void print(const int ia[], size_t size)
{
    for (size_t i = 0; i < size; i++)
    {
        std::cout << ia[i];
    }
}
数组形参和const

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

数组引用形参

C++语言允许将变量定义成数组的引引用(参见3.5.1节,第101页),基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上:

void print(int (&arr)[10])
{
    for (auto elem : arr)
        std::cout << elem;
}

在这里插入图片描述
在这里插入图片描述

传递多维数组

在C++中实际上没有真正的多维数组,所谓的多维数组其实是数组的数组.
和所有数组一样,将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维的大小都是数组类型的一部分。

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

上述语句将matrix声明成指向含有10个整数的数组的指针。
在这里插入图片描述
在这里插入图片描述

6.2.5 main 处理命令行选项

int main(int argc, char *argv[]){}

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针:第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,所以main函数也可以定义成:

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

其中argv指向char*。

当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。
在这里插入图片描述

6.2.6 含有可变形参的函数

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

  1. 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板,关于它的细节将在16.4节(第618页)介绍。
  2. C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参。本节将简要介绍省略符形参,不过需要注意的是,这种功能一般只用于与C函数交互的接口程序。
initializer_list 形参

如果函数的实参数量未知,但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于表示某种特定类型的值的数组(参见3.5节,第101页)。initializer_list类型定义在同名的头文件中,它提供的操作如表6.1所示。
在这里插入图片描述
vector一样,initializer_list也是一种模板类型。定义initializer_list对象时必须说明列表中所含元素的类型:

initializer_list<std::string> ls;
initializer_list<int> li;

vector不一样的是,initializer_list对象中的元素永远是常量值。

void error_msg(std::initializer_list<std::string> il)
{
    for (auto beg = il.begin(); beg != il.end(); ++beg)
        std::cout << *beg;
}

// 
error_msg({"functionX", "test"});
省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs。
在这里插入图片描述
省略符形参只能出现在形参列表的最后一个位置,他的形式如下:

void foo(parm_list, ...);
void foo(...);

第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。

6.3.2 有返回值函数

值是如何被返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
返回非引用类型时,将返回局部变量的副本
如果返回的是引用时,那么应该返回形参的引用,且形参也是引用类型

不要返回局部对象的引用或指针

函数完成后,它所占用的存储空间也随之被释放掉(参见6.1.1节,第184页)。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:
在这里插入图片描述
在这里插入图片描述

引用返回左值

函数的返回类型决定函数调用是否是左值(参见4.1.1节,第121页)。**调用一个返回引用的函数得到左值,其他返回类型得到右值。**可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:
PS:左值可以在等号两侧,右值只能在等号右侧

char &get_val(std::string &str, std::string::size_type ix)
{
    return str[ix];
}

int main()
{
    std::string s("a value");
    std::cout << s << std::endl;
    get_val(s, 0) = 'A';
    std::cout << s << std::endl;
}
// a value
// A value

把函数调用放在赋值语句的左侧可能看起来有点奇怪,但其实这没什么特别的。返回值是引用,因此调用是个左值,和其他左值一样它也能出现在赋值运算符的左侧。

列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化(参见3.3.1节,第88页):否则,返回的值由函数的返回类型决定。

主函数main 的返回值

之前介绍过,如果函数的返回类型不是void,那么它必须返回一个值。但是这条规则有个例外:我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回o的return语句。

递归

如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数recursive function)。举个例子,我们可以使用递归函数重新实现求阶乘的功能:
在这里插入图片描述

6.3.3 返回指针数组

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用(参见3.5.1节,第102页)。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名

typedef int arrT[10];   // arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10];   // arrT的等价声明
arrT *func(int i);      // func返回一个指向含有10个整数的数组指针

其中arrT是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。因此,func函数接受一个int实参,返回一个指向包含10个整数的数组的指针。

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

要想在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:

int arr[10];    // arr是一个含有10个整数的数组
int *p1[10];    // p1是一个含有10个指针的数组
int (*p2)[10] = &arr;   // p2是一个指针,它指向含有10个整数的数组

和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:

Type (*func(param_list))[dimension];
int (*func(int i))[10];

类似于其他数组的声明,Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list)两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。
可以按照以下的顺序来逐层理解该声明的含义:
在这里插入图片描述
在这里插入图片描述

使用尾置返回类型

在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailingreturntype)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个>符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:

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

因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。

使用decltype

还有一种情况,如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。

decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even;
}

arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。有一个地方需要注意:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

6.4 函数重载

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。例如,在6.2.4节(第193页)中我们定义了几个名为print的函数:

重载和const 形参

顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另外一个没有顶层const的形参区分开来,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
在这里插入图片描述
在这里插入图片描述

const_cast 和重载
const std::string &shorterString(const std::string &s1, const std::string &s2)
{
    return s1.size() > s2.size() ? s2 : s1;
}

std::string &shorterString(std::string &s1, std::string &s2)
{
    auto &r = shorterString(const_cast<const std::string &>(s1), const_cast<const std::string &>(s2));
    return const_cast<std::string &>(r);
}

在这个版本的函数中,首先将它的实参强制转换成对const的引用,然后调用了shorterString函数的const版本。const版本返回对const string的引用,这个引用事实上绑定在了某个初始的非常量实参上。因此,我们可以再将其转换回一个普通的string&,这显然是安全的。

6.4.1 重载与作用域

在这里插入图片描述
对于刚接触C++的程序员来说,不太容易理清作用域和重载的关系。其实,重载对作用域的一般性质并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名:
在这里插入图片描述

6.5.1 默认实参

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(defaultargument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

默认实参初始化

局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

//wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = '';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen() ;//调用screen(ht(),80,'')

用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:

void f2()
{
	char def = '*';
	sz wd = 100;
	window = screen();//调用screen(ht(),100,*)
}

6.5.2 内联函数和constexpr函数

在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复:可能需要拷贝实参:程序转向一个新的位置继续执行。

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

将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。假设我们把shorterString函数定义成内联函数,则如下调用
在这里插入图片描述
在这里插入图片描述

constexpr 函数

constexpr函数(constexprfunction)是指能用于常量表达式(参见2.4.4节,第58页)的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:**函数的返回类型及所有形参的类型都得是字面值类型(参见2.4.4节,第59页),而且函数体中必须有且只有一条returm语句:
**
在这里插入图片描述
在这里插入图片描述

6.5.3 调试帮助

在这里插入图片描述
在这里插入图片描述

6.6.1 实参类型转换

在这里插入图片描述

需要类型提升和算术类型转换的匹配

分析函数调用前,我们应该知道小整型一般都会提升到int类型或更大的整数类型。
在这里插入图片描述

函数匹配和const实参

如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数:
在这里插入图片描述

6.7 函数指针

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

// pf指向一个函数,该函数的参数是两个const string 引用,返回值是bool类型
bool (*pf)(const std::string &, const std::string &);

从我们声明的名字开始观察,pf前面有个,因此pf是指针;右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数的返回类型是布尔值。因此,pf就是一个指向函数的指针,其中该函数的参数是两个conststring的引l用,返回值是bool类型。*
在这里插入图片描述

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,按照如下形式我们可以将lengthCompare的地址赋给pf:
在这里插入图片描述
在这里插入图片描述

重载函数的指针

当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针
在这里插入图片描述

函数指针形参

在这里插入图片描述
这两个声明语句声明的是同一个函数,在第一条语句中,编译器自动地将Func表示的函数类型转换成指针。

返回指向函数的指针

在这里插入图片描述

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

在这里插入图片描述

7.1.4 构造函数

如果已经声明一个构造函数并且有形参,那么编译器不会自动生成默认构造函数,需要我们手动显式声明
在这里插入图片描述

= default 的含义

首先请明确一点:因为该构造函数不接受任何实参,所以它是一个默认构造函数。我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中,=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default在类的内部,则默认构造函数是内联的:如果它在类的外部,则该成员默认情况下不是内联的
在这里插入图片描述

在类外部定义构造函数

执行构造函数就会初始化对象成员
在这里插入图片描述

7.1.5 拷贝、赋值和析构

除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等(参见6.2.1节,第187页和6.3.2节,第200页)。当我们使用了赋值运算符(参见4.4节,第129页)时会发生对象的赋值操作。当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁(参见6.1.1节,第184页),当vector对象(或者数组)销毁时存储在其中的对象也会被销毁。
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。例如在7.1.1节(第229页)的书店程序中,当编译器执行如下赋值语句时。

7.2 访问控制与封装

到目前为止,我们已经为类定义了接口,但并没有任何机制强制用户使用这些接口。我们的类还没有封装,也就是说,用户可以直达Sales_data对象的内部并且控制它的具体实现细节。在C++语言中,我们使用访问说明符(access specifiers)加强类的封装性:

  • 定义为public 的成员可在整个程序访问
  • 定义为protected 的成员继承和自己访问
  • 定义为private 的成员只能被自己访问

ublic members of a class A are accessible for all and everyone.

Protected members of a class A are not accessible outside of A’s code, but is accessible from the code of any class derived from A.

Private members of a class A are not accessible outside of A’s code, or from the code of any class derived from A.

So, in the end, choosing between protected or private is answering the following questions: How much trust are you willing to put into the programmer of the derived class?

By default, assume the derived class is not to be trusted, and make your members private. If you have a very good reason to give free access of the mother class’ internals to its derived classes, then you can make them protected.

A 类的公共成员可供所有人使用。
类 A 的受保护成员在 A 的代码之外不可访问,但可以从 A 派生的任何类的代码访问。
类 A 的私有成员不能在 A 的代码之外或从 A 派生的任何类的代码中访问。
因此,最终,在 protected 或 private 之间进行选择是在回答以下问题:您愿意对派生类的程序员给予多少信任?
默认情况下,假设派生类不可信,并将您的成员设置为私有。如果您有充分的理由允许其派生类自由访问母类的内部结构,那么您可以将它们设置为受保护的。

使用class或struct关键字

出于统一编程风格的考虑,当我们希望定义的类的所有成员是public的时,使用struct;反之,如果希望成员是private的,使用class。
在这里插入图片描述

7.2.1 友元

既然Sales_data的数据成员是private的,我们的read、print和add函数也就无法正常编译了,这是因为尽管这几个函数是类的接口的一部分,但它们不是类的成员。
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.3 类的其他特性

可变数据成员

有时(但并不频繁)会发生这样一种情况,我们希望能修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字做到这点。
一个可变数据成员永远不会是const,即使他是const对象的成员。

基于const的重载

**通过区分成员函数是否是const的,我们可以对其进行重载,**其原因与我们之前根据指针参数是否指向const(参见6.4节,第208页)而重载函数的原因差不多。

类的声明

就像可以把函数的声明和定义分离开来一样(参见6.1.2节,第186页),我们也能仅仅声明类而暂时不定义它:

class Screen;		// Screen 类的声明

这种声明有时被称作前向声明(forward declaration),它向程序中引l入了名字Screen并且指明screen是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型(incomplete type),也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。
不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。

7.3.4 友元再探

举个友元类的例子,我们的Window_mgr类(参见7.3.1节,第245页)的某些成员可能需要访问它管理的Screen类的内部数据。例如,假设我们需要为Window_mgr添加一个名为clear的成员,它负责把一个指定的Screen的内容都设为空白。为了完成这一任务,clear需要访问Screen的私有成员:而要想令这种访问合法,Screen需要把Windowmgr指定成它的友元:
在这里插入图片描述

7.5.1 构造函数初始化列表

构造函数初始化列表和构造函数内赋值初始化不同
在这里插入图片描述

构造函数的初始值又是必不可少

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

成员初始化的顺序

显然,在构造函数初始值中每个成员只能出现一次。否则,给同一个成员赋两个不同的初始值有什么意义呢?
不过让人稍感意外的是,构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。
成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
一般来说,初始化的顺序没什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。
在这里插入图片描述

默认实参和构造函数

在这里插入图片描述

7.5.2 委托构造函数

C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数(delegating constructor)。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
和其他构造函数一样,一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。
在这里插入图片描述

7.5.3 默认构造函数的作用

在这里插入图片描述

使用默认构造函数

在这里插入图片描述

7.5.4 隐式的类类型转换

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

在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止:
在这里插入图片描述

explicit 构造函数只能用于直接初始化

发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=)(参见3.2.1节,第76页)。此时,我们只能使用直接初始化而不能使用explicit构造函数:
在这里插入图片描述

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

在这里插入图片描述

7.5.5 聚合类

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

  • 所有成员都是public的
  • 没有定义任何的构造函数
  • 没有类内初始值
  • 没有基类,也没有virtual函数
    在这里插入图片描述

7.5.6 字面值常量

在这里插入图片描述

constexpr 构造函数

尽管构造函数不能是const的(参见7.1.4节,第235页),但是字面值常量类的构造函数可以是constexpr(参见6.5.2节,第213页)函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。
在这里插入图片描述

7.6 类的静态成员

有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。

声明静态成员

我们通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样,静态成员可以是public的或private的。静态数据成员的类型可以是常量、引用、指针、类类型等。
在这里插入图片描述

8.1 IO 类

在这里插入图片描述
在这里插入图片描述

8.1.1 IO 对象无拷贝或赋值

在这里插入图片描述

8.1.2 条件状态

IO操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。表8.2列出了10类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态(conditionstate)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

刷新输出缓冲区

我们已经使用过操纵符endl,它完成换行并刷新缓冲区的工作。IO库中还有两个类似的操纵符:flush和ends。flush刷新缓冲区,但不输出任何额外的字符:ends向缓冲区插入一个空字符,然后刷新缓冲区:
在这里插入图片描述

unitbuf 操纵符

在这里插入图片描述
在这里插入图片描述

8.2 文件输入输出

在这里插入图片描述

8.2.1 使用文件流对象

当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
创建文件流对象时,我们可以提供文件名(可选的)。如果提供了一个文件名,则open会自动被调用:
在这里插入图片描述

用fstream代替iostream&

在这里插入图片描述
在这里插入图片描述

8.2.2 文件模式

在这里插入图片描述

以out模式打开文件会丢弃已有数据

在这里插入图片描述

每次调用open时都会确定文件模式

在这里插入图片描述

8.3 string 流

在这里插入图片描述

8.3.1 使用istringstream

在这里插入图片描述

使用ostringstream

在这里插入图片描述
在这里插入图片描述

9.1 顺序容器概述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

确定使用哪种顺序容器

在这里插入图片描述

对容器可以保存的元素类型的限制

虽然我们可以在容器中保存几乎任何类型,但某些容器操作对元素类型有其自已的特殊要求。我们可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。
例如,顺序容器构造函数的一个版本接受容器大小参数(参见3.3.1节,第88页),它使用了元素类型的默认构造函数。但某些类没有默认构造函数。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:
在这里插入图片描述
在这里插入图片描述

9.2.1 迭代器

标准容器类型上的所有迭代器都允许我们访问容器中的元素,而所有迭代器都是通过解引用运算符来实现这个操作的。

迭代器范围

在这里插入图片描述
在这里插入图片描述

9.2.4 容器定义和初始化

在这里插入图片描述

将一个容器初始化为另一个容器的拷贝

为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配;范围拷贝(新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转为要初始化的容器的元素类型即可。)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

标准库array具有固定大小

与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:

std::array<int, 42>;	// 保存42个int的数组
std::array<std::string, 10>;	// 保存10个std::string 的数组

**由于大小是array类型的一部分,**array不支持普通的容器构造函数。这些构造函数都会确定容器的大小,要么隐式地,要么显式地。而允许用户向一个array构造函数传递大小参数,最好情况下也是多余的,而且容易出错。
在这里插入图片描述
在这里插入图片描述

9.2.5 赋值和swap

表9.4中列出的与赋值相关的运算符可用于所有容器。赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝:
在这里插入图片描述

9.2.7 关系运算符

每个容器类型都支持相等运算符;除了无序关联容器外的所有容器都支持关系运算符。算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
在这里插入图片描述

容器的关系运算符使用元素的关系运算符完成比较

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.3 顺序容器操作

顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。

9.3.1 向顺序容器添加元素

除array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小
push_back进去的是副本
在这里插入图片描述

9.3.2 访问元素

如果容器中没有元素,访问操作的结果是未定义的。访问元素返回的均是引用
在这里插入图片描述

9.3.3 删除元素

在这里插入图片描述

9.3.5 改变容器大小

在这里插入图片描述
在这里插入图片描述

9.3.6 容器操作可能使迭代器失效

向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或选代器失效。
在这里插入图片描述

不要保存end 返回的迭代器

当我们添加/删除vector或string的元素后,或在deque中首元素之外任何位置添加/删除元素后,**原来end返回的迭代器总是会失效。**因此,添加或删除元素的循环程序必须反复调用end,而不能在循环之前保存end返回的选代器,一直当作容器末尾使用。通常C++标准库的实现中end()操作都很快,部分就是因为这个原因。
错误:
在这里插入图片描述
正确:
在这里插入图片描述

9.4 vector 对象是如何增长的

为了支持快速随机访问,vector将元素连续存储一每个元素紧挨着前一个元素存储。
假定容器中元素是连续存储的,且容器的大小是可变的,考虑向vector或string中添加元素会发生什么:**如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置一因为元素必须连续存储。容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。**如果我们每添加一个新元素,vector就执行一次这样的内存分配和释放操作,性能会慢到不可接受。
为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略
在这里插入图片描述
size是指已经保存的元素数量
capacity是指在不重新分配内存空间的前提下它最多可以保存多少元素
reservecapacity作用相似

9.5.1 构造string 的其他方法

在这里插入图片描述

substr 操作

substr操作(参见表9.12)返回一个string,它是原始string的一部分或全部的拷贝。可以传递给substr一个可选的开始位置和计数值:
在这里插入图片描述

9.5.2 改变string 的其他方法

在这里插入图片描述

9.5.3 string 的搜索操作

在这里插入图片描述
在这里插入图片描述

9.5.4 compare 函数

在这里插入图片描述

9.5.5 数值转换

在这里插入图片描述

9.6 容器适配器

在这里插入图片描述

10 泛型算法

10.1 概述

大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。

// std::find  =>希望知道vector中是否包含一个特定值。
int val = 42;
auto res = std::find(vec.begin(), vec.end(), val);

int ia[] = {27, 210, 12, 47, 109, 83};
val = 83;
int *result = std::find(std::begin(ia), std::end(ia), val);

auto r = std::find(ia + 1, ia + 4, val);

// std::accumulate 计算一定范围内的和
int sum = std::accumulate(vec.cbegin(), vec.cend(), 0);

std::string s = std::accumulate(vec.cbegin(), vec.cend(), std::string(""));

// std::fill 填充算法
std::fill(vec.begin(), vec.end(), 0);

在这里插入图片描述
在这里插入图片描述

10.3.2 lambda 表达式

在这里插入图片描述
在这里插入图片描述

10.3.3 lambda 捕获和返回

值捕获

与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

void fcn1()
{
    auto vl = 42;
    auto f = [vl]
    { return vl; };
    vl = 0;
    auto j = f(); // j为42,值拷贝
}

由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。
在使用时可以不存在

引用捕获
void fcn1()
{
    auto vl = 42;
    auto f = [&vl]
    { return vl; };
    vl = 0;
    auto j = f(); // j为0,引用
}

一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象

引用捕获与返回引用(参见6.3.2节,第201页)有着相同的问题和限制。**如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。**lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
在这里插入图片描述
在这里插入图片描述

隐式捕获

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可变lambda

默认情况下,**对于一个值被拷贝的变量,lambda不会改变其值。**如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:

void fcn1()
{
    auto vl = 42;
    auto f = [vl] mutable
    { return ++vl; };
    vl = 0;
    auto j = f(); // j为43
}

引用捕获修改时无需添加mutable

10.3.4 参数绑定

标准库bind函数

作用:修改参数长度
使用一个新的名为bind的标准库函数,它定义在头文件functional中。可以将bind函数看作一个通用的函数适配器(参见9.6节,第329页),它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。调用bind的一般形式为:

auto newCallable = std::bind(callable, arr_list);
auto g = std::bind(func, a, b, std::placeholders::_2, c, std::placeholders::_1);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。

绑定引用参数

ref
**函数ref返回一个对象,包含给定的引用,此对象是可以拷贝的。**标准库中还有一个cref函数,生成一个保存const引用的类。与bind一样,函数ref和cref也定义在头文件functional中。
在这里插入图片描述

10.4.1 插入迭代器

插入器是一种迭代器适配器(参见9.6节,第329页),它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。当我们通过一个插入迭代器进行赋值时,该迭代器调用容器操作来向给定容器的指定位置插入一个元素。表10.2列出了这种迭代器支持的操作。
在这里插入图片描述
插入器有三种类型,差异在于元素插入的位置:

  • back_inserter创建一个使用push_back的迭代器
  • front_inserter创建一个使用push_front的迭代器
  • inserter创建一个使用insert的迭代器。

11.1 关联容器

在这里插入图片描述

map 和 vector 的区别

在这里插入图片描述

11.2 关联容器概述

**关联容器都支持普通容器操作。关联容器不支持顺序容器的位置相关的操作,例如push_back、push_front。**因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作和类型别名(参见表11.3,第381页)。此外,无序容器还提供一些用来调整哈希性能的操作,我们将在11.4节中介绍。
关联容器的迭代器都是双向的。

11.2.1 定义关联容器

初始化map时,必须提供关键字类型的值类型。我们讲每个关键字-值对包围在花括号中
{key, value}

初始化multimap或multiset

一个map或set中的关键字必须是唯一的,即对于一个给定的关键字,只能有一个元素的关键字等于它。容器multimapmultiset没有此限制,它们都允许多个元素具有相同的关键字。

11.2.2 关键字类型的要求

关联容器对其关键字类型有一些限制。对于有序容器——mapmultimapset以及multiset,关键字类型必须定义元素比较的方法。
在这里插入图片描述

有序容器的关键字类型

在这里插入图片描述

11.2.3 pair 类型

一个pair保存两个数据成员,当创建pair时,必须提供两个类型名

std::pair<std::string, std::string> anon;
std::pair<std::string, std::string> anon{"James", "Joyuce"};
std::pair<std::string, std::size_t> world_count;
std::pair<std::string, std::vector<int>> line;

pair的默认构造函数对数据成员进行值初始化。
在这里插入图片描述

创建pair对象的函数
std::pair<std::string, int> process(std::vector<std::string> &v)
{
    if (!v.empty())
        return {v.back(), v.back().size()}; // 隐式构造
    else
        return std::pair<std::string, int>();
}

11.3 关联容器操作

关联容器定义了类型,表示容器关键字和值的类型
在这里插入图片描述

11.3.1 关联迭代容器

当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。对map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值:

// 获得执行word_count 中一个元素的迭代器
auto map_it = word_count.begin();
map_it->first = "new key";		// 错误,关键字时const的

在这里插入图片描述

set的迭代器时const

虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。

11.3.2 添加元素

关联容器的insert成员向容器中添加一个元素或一个元素范围。由于map和set(以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。
insert有两个版本,分别接受一对迭代器,或是一个初始化器列表

map添加元素
world_count.insert({word, 1});
world_count.insert(std::make_pair(word, 1));
world_count.insert(std::pair<std::string, std::size_t>(word, 1));
world_count.insert(std::map<std::string, std::size_t>::value_type(word, 1));

在这里插入图片描述

multimapmultiset添加元素

在这里插入图片描述

11.3.3 删除元素

在这里插入图片描述

11.3.4 下标操作

在这里插入图片描述

11.3.5 访问元素

在这里插入图片描述

multimapmultiset中查找元素

在这里插入图片描述

11.4 无序容器

无序关联容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的。在某些应用中,维护元素的序代价非常高昂,此时无序容器也很有用。
在这里插入图片描述

使用无序容器

除了哈希管理操作外,无需容器提供了与有序容器相同的操作(find、insert等)。意味着用于mapset的操作也能用于unordered_mapunordered_set。同样的,无序容器允许重复关键字版本
因此,通常可以用一个无序容器替换对应的有序容器,反之亦然。但是,由于元素未按顺序存储,一个使用无序容器的程序的输出(通常)会与使用有序容器的版本不同。
Unordered_map允许重复元素

管理桶

无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
在这里插入图片描述
在这里插入图片描述

12.1 动态内存与智能指针

在这里插入图片描述

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数一析构函数(destructor)完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。
**析构函数一般用来释放对象所分配的资源。**例如,string的构造函数(以及其他string成员)会分配内存来保存构成string的字符。string的析构函数就负责释放这些内存。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

12.1.5 unique_ptr

在这里插入图片描述

12.1.6 weak_ptr

我们知道std::shared_ptr会共享对象的所有权,但是有一些场景如果有一个像std::shared_ptr但是又不参与资源所有权共享的指针是很方便的。类似std::shared_ptr但不影响对象引用计数的指针。不参与资源所有权就意味着不会对资源的生命周期产生影响,有利于对象之间的解耦。解决循环引用
在这里插入图片描述

12.2 动态数组

在这里插入图片描述

12.2.2 allocator

new有一些灵活性上的限制。其一它将内存分配和对象构造组合在一起,delete将对象析构和内存释放组合在一起。
这样的话,每次使用到的元素都被赋值了两次,第一次在默认初始化时,随后在赋值时

allocator

allocator可以将内存分配和对象构造分离。它分配的内存时原始的、未构造的

std::allocator<std::string> alloc;  // 分配string的allocator对象
auto const p = alloc.allocate(10);  // 分配n个未初始化的string,仅分配了内存

在这里插入图片描述

allocator分配未构造的内存

在这里插入图片描述
在这里插入图片描述

13.1 拷贝、赋值与销毁

13.1.1 拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。

class Foo
{
public:
    Foo();            // 默认构造函数
    Foo(const Foo &); // 拷贝构造函数
};

**拷贝构造函数的第一个参数必须是一个引用类型。**虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数几乎总是一个const的引l用。拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit的。

如果我们没有为类定义拷贝构造函数,编译器会自己定义一个。

拷贝初始化

直接初始化,要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。
拷贝初始化,要求编译器将右侧运算对象拷贝到正在创建的对象中,如果有需要还要进行类型转换。

拷贝初始化通常使用拷贝构造函数来完成。但是,如我们将在13.6.2节(第473页)所见,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。

拷贝初始化不仅在用=时发生,以下情况也会发生

  • 将一个对象作为实参传递非引用形参
  • 从一个返回类型为非引用的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素
拷贝初始化的限制

如果我们使用的初始化值要求通过一个explicit 的构造函数来进行类型转换,那么使用拷贝初始化和直接初始化就无关了
在这里插入图片描述

13.1.2 拷贝赋值运算符

重载赋值运算符

重载运算符本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。
重载运算符的参数表示运算符的运算对象。
在这里插入图片描述

13.1.3 析构函数

析构函数完成什么工作

如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个析构函数中,成员的初始化 是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按照初始化是顺序的逆序销毁。

在析构函数中,成员销毁时发生什么完全依赖于成员的类型。销毁类类型的成员需要执行成员自己的析构函数。内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
在这里插入图片描述

合成析构函数

编译器自动定义个析构函数。
在这里插入图片描述
在这里插入图片描述
上面的例子,之所以会奔溃,是因为没有显式声明拷贝构造函数,使用了编译器自己声明的合成拷贝构造(指针浅拷贝)。

13.1.5 使用=default

我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本

13.2.6 阻止拷贝

大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式还是显式地。

虽然大多数类应该定义(而且通常也的确定义了)拷贝构造函数和拷贝赋值运算符,但对某些类来说,这些操作没有合理的意义。在此情况下,定义类时必须采用某种机制阻止拷贝或赋值。

class Foo
{
public:
    Foo() = default;
    Foo(const Foo &) = delete;
    Foo operator=(const Foo &) = delete;
};
析构函数不能是删除的成员

**值得注意的是,我们不能删除析构函数。**如果析构函数被删除,就无法销毁此类型的对象

private 拷贝控制

在这里插入图片描述

13.2.1 行为像值的类

类值拷贝赋值运算符

赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源。
在这里插入图片描述

13.3 交换操作

编写我们自己的swap函数

在这里插入图片描述

13.6.1 右值引用

右值引用是指必须绑定到右值的引用。
右值引用有一个重要的性质一只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

int i = 42;
int &r = i;             // 正确
int &&rr = i;           // 错误,无法将右值引用绑定到左值
int &r2 = i * 42;       // 错误,i*42是右值
const int &r3 = i * 42; // 正确
int &&rr2 = i * 42;     // 正确

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

左值持久、右值短暂

左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知

  • 所引用的对象将要被销毁
  • 该对象没有其他用户

这意味着,使用右值引用的代码可以自由接管所引用的对象的资源
在这里插入图片描述

变量是左值

变量可以看作只有一个运算对象而没有运算符的表达式。

int &&rr1 = 42;		// 正确
int &&rr2 = rr1;	// 错误,表达式rr1是左值

在这里插入图片描述

move 函数

显式地将一个左值转换为对应的右值引用类型。
std::move的库函数获得绑定到左值上的右值引用。

int &&rr3 = std::move(rr1);		// 正确

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。
我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
移动后的左值可能已经被销毁,无法使用值
在这里插入图片描述

13.6.2 移动构造函数和移动赋值运算符

类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。
不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。
与拷贝构造函数一样,任何额外的参数都必须有默认实参。

除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态一销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源一这些资源的所有权已经归属新创建的对象。

class Vector{
    int num;
    int* a;
public:
    void ShallowCopy(Vector& v);
    void DeepCopy(Vector& v);
};

//拷贝构造函数:这意味着深拷贝
Vector::Vector(Vector& v){
    this->num = v.num;
    this->a = new int[num];
    for(int i=0;i<num;++i){a[i]=v.a[i]}
}
//移动构造函数:这意味着浅拷贝
Vector::Vector(Vector&& temp) noexcept{
    this->num = temp.num;
    this->a = temp.a;
    temp.a = nullptr;    //实际上Vector一般都会在析构函数来释放指向的内存,所以需赋值空地址避免释放
}
移动操作、标准库容器和异常

**由于移动操作“窃取”资源,它通常不分配任何资源。因此移动操作通常不会抛出任何异常。**当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库

一种通知标准库的方法是在我们的构造函数中指明noexcept

在这里插入图片描述

移动赋值运算符

移动赋值运算符执行与析构函数和移动构造函数相同的工作。
类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值。

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
	if(this!= & rhs) {
		free();
		elements = rhs.elements;
		first_free = rhs.first_free;
		cap = rhs.cap;
		rhs.elements = rhs.first_free = rhs.cap = nullptr;	// 将rhs置于可析构状态
	}
	return *this;
}
移后源对象必须可析构

从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。我们的StrVec的移动操作满足这一要求,这是通过将移后源对象的指针成员置为nullptr来实现的。

除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的。一般来说,对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据。

未理解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

13.6.3 右值引用和成员函数

在这里插入图片描述

引用限定符

在这里插入图片描述

14.1 重载运算符基本概念

重载运算符函数的参数数量与该运算符作用过的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个。
数除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。

如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上。
在这里插入图片描述
在这里插入图片描述

直接调用一个重载的运算符函数

通常情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式“调用”重载的运算符函数。然而,我们也能像调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:

data1 + data2;
operator+(data1, data2);	// 等价的函数调用
某些运算符不应该被重载

回忆之前介绍过的,某些运算符指定了运算对象的求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。
逻辑与运算符、逻辑或运算符(参见4.3节,第126页)和逗号运算符(参见4.10节,第140页)的运算对象求值顺序规则无法保留下来。除此之外,&&||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。

使用与内置类型一致的含义

在这里插入图片描述
在这里插入图片描述

赋值和复合赋值运算符

赋值运算符的行为与符合版本类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用,重载的运算符应该继承而非违背其内置版本的含义。

选择作为成员或非成员

在这里插入图片描述
在这里插入图片描述

class Foo
{
public:
    Foo() = default;
    Foo(const Foo &) = delete;
    Foo operator=(const Foo &) = delete;

    friend void swap(Foo &l, Foo &r);

    std::ostream &operator<<(std::ostream &os)
    {
        os << i;
        return os;
    }

    std::ostream &operator<<(const int &i)
    {
        std::cout << i;
        return std::cout;
    }

private:
    std::string *p;
    int i;
};

14.8.1 lambda 是函数对象

lambda表达式产生的类中含有一个重载的函数表用运算符,如

stable_sort(words.begin(), words.end(), [](const std::string &s1, const std::string &s2) const
    {
        return s1.size() < s2.size();
    });

class ShorterString
{
public:
    bool operator()(const std::string &s1, const std::string &s2) const
    {
        return s1.size() < s2.size();
    }
};

// 可替换为如下
stable_sort(words.begin(), words.end(), ShorterString());

产生的类只有一个函数调用运算符成员,它负责接受两个string并比较它们的长度,它的形参列表和函数体与lambda表达式完全一样。

表示lambda 及相应捕获行为的类

如我们所知,当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用所引的对象确实存在(参见10.3.3节,第350页)。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。

相反,通过值捕获的变量被拷贝到lambda

// 值捕获的lambda
class SizeComp
{
public:
    SizeComp(size_t n) : sz(n) {}
    bool operator()(const std::string &s) const
    {
        return s.size() >= sz;
    }

private:
    size_t sz;
};
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));

15.1 OOP:概述

面向对象程序设计的核心思想时数据抽象、继承和动态绑定。
使用数据抽象,可以将类的接口与实现分离;
使用继承,可以定义相似的类型并对其相似关系建模;
使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用他们对象;

动态绑定

在这里插入图片描述

15.2.1 定义基类

在这里插入图片描述

成员和继承

派生类可以继承其基类的成员。
任何构造函数之外的非静态函数都可以是虚函数。
如果基类把一个函数声明成虚函数,则该函数在培盛磊中隐式地也是虚函数。

15.2.2 定义派生类

派生类中的虚函数

派生类经常覆盖它继承的虚函数。
如果派生类没有覆盖基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员。

派生类对象及派生类向基类的类型转换

因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上

class Quote
{
};

class Bulk_quote : public Quote
{
};

Quote item;
Bulk_quote bulk;
Quote *p = &item;
p = &bulk;
Quote &r = bulk;

这种转换通常称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。
在这里插入图片描述

派生类构造函数

尽管在派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。
每个类控制它自己的成员初始化过程。
在这里插入图片描述
在这里插入图片描述

继承与静态成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。

派生类的声明

派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表
在这里插入图片描述

防止继承发生 final

在这里插入图片描述

15.2.3 类型转换与继承

通常情况下,**如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型应该与对象的类型一致,**或者对象的类型中含有一个可接受的const类型转换规则。
可以将基类的指针或引用绑定到派生类对象上。
可以将基类的指针或引用绑定到派生类对象上有一层极其重要的含义:当使用基类的引用时,实际上我们并不清楚该引用所绑定对象的真实类型,该对象可能是基类的对象,也可能时派生类的对象。
在这里插入图片描述

静态类型与动态类型

当我们使用存在继承关系的类型时,必须将一个或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型,动态类型知道运行时才可知。

不存在从基类向派生类的隐式类型转换

因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换。

在对象之间不存在类型转换

派生类向基类的自动类型转换只针对指针和引用类型有效。
在这里插入图片描述
在这里插入图片描述

15.3 虚函数

如前所述,在C++中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为直到运行时才知道到底调用了哪个版本的虚函数,所以所有的虚函数都必须有定义。

对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该带哦用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象动态类型相匹配的那个一。

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用
在这里插入图片描述

派生类中的虚函数

基类某个函数被声明成虚函数,则在所有派生类中都是虚函数
在这里插入图片描述

final 和 override 说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍是合法行为。
编译器认为新定义的函数与基类中的函数时相互独立的。
但可能存在一些问题。
在这里插入图片描述
我们还能把某个函数指定为final,尝试覆盖该函数的操作均发生错误

虚函数与默认实参

虚函数也可以拥有实参。如果某次函数调用了默认实参,则该实参由本次调用的静态类型决定。
在这里插入图片描述

回避虚函数的机制

我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个版本。使用作用域运算符即可
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

那么什么时候需要回避虚函数的默认机制呢?
当一个派生类的虚函数调用它覆盖的基类虚函数版本时
在这里插入图片描述

15.4 抽象基类

含有纯虚函数的类时抽象基类

含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口。
不能直接创建一个抽象基类的对象

派生类构造函数只能初始化它的直接基类

在这里插入图片描述

15.5 访问控制与继承

受保护的成员(不解)
  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的
  • 和共有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的
  • 派生类的成员和友元只能通过派生类对象来访问基类受保护成员

在这里插入图片描述

友元(不解)
改变个别成员的可访问性

使用using改变派生类继承的某个名字的访问级别
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

15.6 继承中的类作用域

名字冲突与继承

和其他作用域一样,派生类也能重用定于在其直接基类或间接基类中的名字。
在这里插入图片描述

通过作用域运算符来使用隐藏的成员

可以使用作用域运算符来使用一个被隐藏的基类成员
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
在这里插入图片描述

名字查找优先于类型检查

在这里插入图片描述

虚函数与作用域

如果基类与派生类的虚函数接受的实参不同,我们就无法进行基类的引用或指针调用派生类的虚函数了
在这里插入图片描述
在这里插入图片描述

15.7.1 虚析构函数

和其他虚函数一样,析构函数的虚属性也会被继承。
无论派生类使用合成的析构函数还是自己定义的析构函数,都将是虚析构函数
在这里插入图片描述

虚析构函数将阻止合成移动操作(不解)

基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动

15.7.2 合成拷贝与集成(不解)

15.7.3 派生类的拷贝控制成员(不解)

在这里插入图片描述
在这里插入图片描述

15.7.4 继承的构造函数(不解)

派生类继承基类构造函数的方式是提供一条注明了直接基类名的using声明语句
在这里插入图片描述

15.8 容器与继承(不解)

16.1 定义模板

16.1.1 函数模板

我们可以定义一个通用的函数模板,而不是为每个类型都定义一个新函数。
一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。

template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2)
        return -1;
    if (v1 > v2)
        return 1;
    return 0;
}

函数模板以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表。在模板定义中模板参数列表不能为空

模板类型参数

函数有一个模板类型参数,一般来说,可以将类型参数看作类型说明符,像内置类型或类类型说明符一样使用,类型参数可以用来指定返回类型或函数的参数类型,以及在函数提内用与变量或类型转换

template <typename T>
T foo(T *p)
{
    T temp = *p;
    return temp;
}

在模板参数列表中,关键字的含义(typenameclass)相同,可以呼唤使用

template<typename T, class U> calc(const T&, const &U);
非类型模板参数

除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。
通过特定的类型名而非关键字classtypename来指定类型

template <unsigned N1, unsigned N2>
int compare(const char (&p1)[N1], const char (&p2)[N2])
{
    return strcmp(p1, p2);
}

在这里插入图片描述

inlineconstexpr的函数模板

在这里插入图片描述

编写类型无关的代码

上述compare仍有改进的地方

template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2)
        return -1;
    if (v2 < v1)
        return 1;
    return 0;
}
  • 模板中的函数参数是const引用
  • 函数体中的条件判断仅使用<比较运算
    使用const引用,保证了函数可以用于不能拷贝的类型
    使用<,保证类型只需支持<即可

    在这里插入图片描述
模板编译

当编译器遇到一个模板定义时,并不生成代码。只有当实例化模板时,编译器才会生成代码。这一特性影响了我们如何组织代码以及错误何时被检测到。
普通函数调用,编译器只需掌握函数的声明。类似地,当使用类类型时,类的定义必须是可用的,但成员函数的定义不必已经实现。因此可以将类定义和函数声明放在头文件中,普通函数和类的成员函数的定义放在源文件中。
模板则有区别,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此模板的头文件通常既包含声明也包括定义。
在这里插入图片描述

大多数编译错误在实例化期间报告

模板直到实例化时才会生成代码,这影响了我们何时才会获知模板内代码的编译错误。编译器会在三个阶段报告错误

  1. 编译模板本身时。编译器通常不会发现很多错误,可以检测语法错误
  2. 模板使用时。对于函数模板调用,编译器通常会检查实参数目是否正确,参数类型是否匹配
  3. 模板实例化时。发现类型相关的错误。可能在链接时才报告

16.1.2 类模板

类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。

定义类模板

在这里插入图片描述

实例化类模板

实例化类模板时,必须提供显式模板实参列表,编译器用模板实参来实例化特定的类。

Blob<int> ia;
Blob<int> ia2 = {0, 1, 2, 3, 4};
Blob<string> name;
Blob<double> prices;

在这里插入图片描述

在模板作用域中引用模板类型

一个类模板中的代码如果使用了另一个模板,通常**将模板自己的参数当作被使用模板的实参。**例如,data成员使用了两个模板,vectorshare_ptr 。无论何时使用模板都必须提供模板实参,因此提供外层模板参数

std::shared_ptr<std::vevtor<T>> data;

它使用了Blob的类型参数来声明data是一个shared_ptr的实例,此shared_ptr指向一个保存类型为T的对象的vector实例。当我们实例化一个特定类型的Blob,例如Blob<string>时,data会成为:shared_ptr<vector<string>>

16.1.3 模板参数

模板参数与作用域

在这里插入图片描述

模板声明

模板声明必须包含模板参数
在这里插入图片描述

默认模板实参

16.2.1 类型转换与模板类型参数

与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参。如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。

  • const转换:可以将一个非const的引用或指针传递给一个const的引用或指针形参
  • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针,一个函数实参可以转换为一个该函数类型的指针
使用相同模板参数类型的函数形参

一个模板类型参数可用作多个函数形参的类型。
如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数

template <typename A, typename B>
int flexibleCompare(const A& v1, const B&  v2)
{
	if (v1 < v2) return -1;
	return 0;
}
正常类型转换应用于普通函数实参

在这里插入图片描述

16.2.2 函数模板显式实参

在某些情况下,编译器无法推断出模板实参的类型。希望允许用户控制模板实例化。

指定显示模板实参
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

auto val = sum<long long>(i, lng);	// long long sum(int, long);

显式指定类型。显式模板实参按左至右的顺序与对应的模板参数匹配
在这里插入图片描述

正常类型转换应用于显式指定的实参

在这里插入图片描述

16.2.3 尾置返回类型与类型转换

使用显式模板实参表示模板函数的返回类型是很有效的。也可以使用类型推断表示返回值的类型
在这里插入图片描述

16.2.5 模板实参推断和引用

template <typename T>
void f(T &p);

其中函数参数p是一个模板类型参数T的引用。
编译器会应用正常的引用绑定规则;

从左值引用函数参数推断类型

当一个函数参数是模板类型参数的一个左值引用时,形如T &,绑定规则告诉我们,只能传递一个左值。实参可以时const,也可以不是

template <typename T>
void f1(T &);
f1(i);		// decltype(i)=int, T为int
f1(ci);		// decltype(ci)=const int, T 为 const int
f1(5);		// 错误,实参必须是左值

函数参数的类型是const T& 时,正常的绑定规则告诉我们可以传递给它任何类型的实参(const 或 非const、临时对象或字面常量)

template <typename T>
void f2(const T &);
f2(i);		// decltype(i) = int; T 为 int
f2(ci);		// decltype(ci) = const int; T 为 int
f2(5);		// 正确,可以绑定到右值,T 为 const
从右值引用函数参数推断类型

当一个函数参数是一个右值引用时,可以传递给一个右值

template <typename T>
void f3(T &&);
引用折叠和右值引用参数

通常不能将右值引用绑定绑定到左值上。但有两个例外规则,允许这种绑定
第一个规则,影响右值引用参数的推断如何进行。**当将左值传递给函数的右值引用参数,且此时右值引用执行模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。**结果就是 int & &&,这样是非法的,通常不能定义一个引用的引用,但可以通过类型别名或模板类型参数间接定义实现。
第二个规则,如果间接创建一个引用的引用,就形成了折叠,折叠规则如下

  • X& &、X& &&、X&& & ->X &
  • X&& && ->X&&
    在这里插入图片描述
    example:
f3(i);
-> void f3<int &>(int& &&);		// 中间过程
-> void f3(int &)(int&);

在这里插入图片描述

template <typename T>
void f(T &&);

template <typename T>
void f(const T &);

int x = 0;  
f(x);        // 调用 f(const T&),因为 x 是一个左值  
f(std::move(x)); // 调用 f(T&&),因为 std::move(x) 是一个右值  
f(10);       // 调用 f(T&&),因为 10 是一个右值

16.2.6 理解std::move

标准库move函数,是使用右值引用的模板的例子。

std::move 是如何定义的

在这里插入图片描述
move 的函数参数T&& 是指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。

std::string s1("hi"), s2;
s2 = std::move(std::string("bye"));		// 正确,从右值移动数据
s2 = std::move(s1);		// 正确,赋值之后,s1的值不确定
从一个左值static_cast 到一个右值引用是允许的

通常情况下,static_cast只能用于其他合法的类型转换。
但是,这里又有一条针对右值引用的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用staticcast显式地将一个左值转换为一个右值引用。

16.2.7 转发

编写一个函数,接受一个可调用表达式和两个额外实参。我们的函数将调用给定的可调用对象,将两个额外参数逆序传递给它,如下

template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2)
{
	f(t2, t1);
}

在这里插入图片描述
在这里插入图片描述

定义能保持类型信息的函数参数->引用折叠

为了通过翻转函数传递一个引用,需要重新实现。
通过将一个函数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
	f(t2, t1);
}

在这里插入图片描述

使用std::forword 保持类型信息

在这里插入图片描述
在这里插入图片描述

16.5 模板特例化

在这里插入图片描述
在这里插入图片描述

17.1 tuple

在这里插入图片描述

17.1.1 定义和初始化tuple

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

17.1.2 使用tuple返回多个值

在这里插入图片描述

C++ Primer中文版(第5)[203M]分3个压缩包 本书是久负盛名的C++经典教程,其内容是C++大师Stanley B. Lippman丰富的实践经验和C++标准委员会原负责人Josée Lajoie对C++标准深入理解的完美结合,已经帮助全球无数程序员学会了C++。 对C++基本概念和技术全面而且权威的阐述,对现代C++编程风格的强调,使本书成为C++初学者的最佳指南;对于中高级程序员,本书也是不可或缺的参考书。 目录 第1章 开始 1   1.1 编写一个简单的C++程序 2   1.1.1 编译、运行程序 3   1.2 初识输入输出 5   1.3 注释简介 8   1.4 控制流 10   1.4.1 while语句 10   1.4.2 for语句 11   1.4.3 读取数量不定的输入数据 13   1.4.4 if语句 15   1.5 类简介 17   1.5.1 Sales_item类 17   1.5.2 初识成员函数 20   1.6 书店程序 21   小结 23   术语表 23   第Ⅰ部分 C++基础 27   第2章 变量和基本类型 29   2.1 基本内置类型 30   2.1.1 算术类型 30   2.1.2 类型转换 32   2.1.3 字面值常量 35   2.2 变量 38   2.2.1 变量定义 38   2.2.2 变量声明和定义的关系 41   2.2.3 标识符 42   2.2.4 名字的作用域 43   2.3 复合类型 45   2.3.1 引用 45   2.3.2 指针 47   2.3.3 理解复合类型的声明 51   2.4 const限定符 53   2.4.1 const的引用 54   2.4.2 指针和const 56   2.4.3 顶层const 57   2.4.4 constexpr和常量表达式 58   2.5 处理类型 60   2.5.1 类型别名 60   2.5.2 auto类型说明符 61   2.5.3 decltype类型指示符 62   2.6 自定义数据结构 64   2.6.1 定义Sales_data类型 64   2.6.2 使用Sales_data类 66   2.6.3 编写自己的头文件 67   小结 69   术语表 69   第3章 字符串、向量和数组 73   3.1 命名空间的using声明 74   3.2 标准库类型string 75   3.2.1 定义和初始化string对象 76   3.2.2 string对象上的操作 77   3.2.3 处理string对象中的字符 81   3.3 标准库类型vector 86   3.3.1 定义和初始化vector对象 87   3.3.2 向vector对象中添加元素 90   3.3.3 其他vector操作 91   3.4 迭代器介绍 95   3.4.1 使用迭代器 95   3.4.2 迭代器运算 99   3.5 数组 101   3.5.1 定义和初始化内置数组 101   3.5.2 访问数组元素 103   3.5.3 指针和数组 105   3.5.4 C风格字符串 109   3.5.5 与旧代码的接口 111   3.6 多维数组 112   小结 117   术语表 117   第4章 表达式 119   4.1 基础 120   4.1.1 基本概念 120   4.1.2 优先级与结合律 121   4.1.3 求值顺序 123   4.2 算术运算符 124   4.3 逻辑和关系运算符 126   4.4 赋值运算符 129   4.5 递增和递减运算符 131   4.6 成员访问运算符 133   4.7 条件运算符 134   4.8 位运算符 135   4.9 sizeof运算符 139   4.10 逗号运算符 140   4.11 类型转换 141   4.11.1 算术转换 142   4.11.2 其他隐式类型转换 143   4.11.3 显式转换 144   4.12 运算符优先级表 147   小结 149   术语表 149   第5章 语句 153   5.1 简单语句 154   5.2 语句作用域 155   5.3 条件语句 156   5.3.1 if语句 156   5.3.2 switch语句 159   5.4 迭代语句 165   5.4.1 while语句 165   5.4.2 传统的for语句 166   5.4.3 范围for语句 168   5.4.4 do while语句 169   5.5 跳转语句 170   5.5.1 break语句 170   5.5.2 continue语句 171   5.5.3 goto语句 172   5.6 TRY语句块和异常处理 172   5.6.1 throw表达式 173   5.6.2 try语句块 174   5.6.3 标准异常 176   小结 178   术语表 178   第6章 函数 181   6.1 函数基础 182   6.1.1 局部对象 184   6.1.2 函数声明 186   6.1.3 分离式编译 186   6.2 参数传递 187   6.2.1 传值参数 187   6.2.2 传引用参数 188   6.2.3 const形参和实参 190   6.2.4 数组形参 193   6.2.5 main:处理命令行选项 196   6.2.6 含有可变形参的函数 197   6.3 返回类型和return语句 199   6.3.1 无返回值函数 200   6.3.2 有返回值函数 200   6.3.3 返回数组指针 205   6.4 函数重载 206   6.4.1 重载与作用域 210   6.5 特殊用途语言特性 211   6.5.1 默认实参 211   6.5.2 内联函数和constexpr函数 213   6.5.3 调试帮助 215   6.6 函数匹配 217   6.6.1 实参类型转换 219   6.7 函数指针 221   小结 225   术语表 225   第7章 类 227   7.1 定义抽象数据类型 228   7.1.1 设计Sales_data类 228   7.1.2 定义改进的Sales_data类 230   7.1.3 定义类相关的非成员函数 234   7.1.4 构造函数 235   7.1.5 拷贝、赋值和析构 239   7.2 访问控制与封装 240   7.2.1 友元 241   7.3 类的其他特性 243   7.3.1 类成员再探 243   7.3.2 返回*this的成员函数 246   7.3.3 类类型 249   7.3.4 友元再探 250   7.4 类的作用域 253   7.4.1 名字查找与类的作用域 254   7.5 构造函数再探 257   7.5.1 构造函数初始值列表 258   7.5.2 委托构造函数 261   7.5.3 默认构造函数的作用 262   7.5.4 隐式的类类型转换 263   7.5.5 聚合类 266   7.5.6 字面值常量类 267   7.6 类的静态成员 268   小结 273   术语表 273   第Ⅱ部 C++标准库 275   第8章 IO库 277   8.1 IO类 278   8.1.1 IO对象无拷贝或赋值 279   8.1.2 条件状态 279   8.1.3 管理输出缓冲 281   8.2 文件输入输出 283   8.2.1 使用文件流对象 284   8.2.2 文件模式 286   8.3 string流 287   8.3.1 使用istringstream 287   8.3.2 使用ostringstream 289   小结 290   术语表 290   第9章 顺序容器 291   9.1 顺序容器概述 292   9.2 容器库概览 294   9.2.1 迭代器 296   9.2.2 容器类型成员 297   9.2.3 begin和end成员 298   9.2.4 容器定义和初始化 299   9.2.5 赋值和swap 302   9.2.6 容器大小操作 304   9.2.7 关系运算符 304   9.3 顺序容器操作 305   9.3.1 向顺序容器添加元素 305   9.3.2 访问元素 309   9.3.3 删除元素 311   9.3.4 特殊的forward_list操作 312   9.3.5 改变容器大小 314   9.3.6 容器操作可能使迭代器失效 315   9.4 vector对象是如何增长的 317   9.5 额外的string操作 320   9.5.1 构造string的其他方法 321   9.5.2 改变string的其他方法 322   9.5.3 string搜索操作 325   9.5.4 compare函数 327   9.5.5 数值转换 327   9.6 容器适配器 329   小结 332   术语表 332   第10章 泛型算法 335   10.1 概述 336   10.2 初识泛型算法 338   10.2.1 只读算法 338   10.2.2 写容器元素的算法 339   10.2.3 重排容器元素的算法 342   10.3 定制操作 344   10.3.1 向算法传递函数 344   10.3.2 lambda表达式 345   10.3.3 lambda捕获和返回 349   10.3.4 参数绑定 354   10.4 再探迭代器 357   10.4.1 插入迭代器 358   10.4.2 iostream迭代器 359   10.4.3 反向迭代器 363   10.5 泛型算法结构 365   10.5.1 5类迭代器 365   10.5.2 算法形参模式 367   10.5.3 算法命名规范 368   10.6 特定容器算法 369   小结 371   术语表 371   第11章 关联容器 373   11.1 使用关联容器 374   11.2 关联容器概述 376   11.2.1 定义关联容器 376   11.2.2 关键字类型的要求 378   11.2.3 pair类型 379   11.3 关联容器操作 381   11.3.1 关联容器迭代器 382   11.3.2 添加元素 383   11.3.3 删除元素 386   11.3.4 map的下标操作 387   11.3.5 访问元素 388   11.3.6 一个单词转换的map 391   11.4 无序容器 394   小结 397   术语表 397   第12章 动态内存 399   12.1 动态内存与智能指针 400   12.1.1 shared_ptr类 400   12.1.2 直接管理内存 407   12.1.3 shared_ptr和new结合使用 412   12.1.4 智能指针和异常 415   12.1.5 unique_ptr 417   12.1.6 weak_ptr 420   12.2 动态数组 423   12.2.1 new和数组 423   12.2.2 allocator类 427   12.3 使用标准库:文本查询程序 430   12.3.1 文本查询程序设计 430   12.3.2 文本查询程序类的定义 432   小结 436   术语表 436   第Ⅲ部分 类设计者的工具 437   第13章 拷贝控制 439   13.1 拷贝、赋值与销毁 440   13.1.1 拷贝构造函数 440   13.1.2 拷贝赋值运算符 443   13.1.3 析构函数 444   13.1.4 三/五法则 447   13.1.5 使用=default 449   13.1.6 阻止拷贝 449   13.2 拷贝控制和资源管理 452   13.2.1 行为像值的类 453   13.2.2 定义行为像指针的类 455   13.3 交换操作 457   13.4 拷贝控制示例 460   13.5 动态内存管理类 464   13.6 对象移动 470   13.6.1 右值引用 471   13.6.2 移动构造函数和移动赋值运算符 473   13.6.3 右值引用和成员函数 481   小结 486   术语表 486   第14章 操作重载与类型转换 489   14.1 基本概念 490   14.2 输入和输出运算符 494   14.2.1 重载输出运算符<> 495   14.3 算术和关系运算符 497   14.3.1 相等运算符 497   14.3.2 关系运算符 498   14.4 赋值运算符 499   14.5 下标运算符 501   14.6 递增和递减运算符 502   14.7 成员访问运算符 504   14.8 函数调用运算符 506   14.8.1 lambda是函数对象 507   14.8.2 标准库定义的函数对象 509   14.8.3 可调用对象与function 511   14.9 重载、类型转换与运算符 514   14.9.1 类型转换运算符 514   14.9.2 避免有二义性的类型转换 517   14.9.3 函数匹配与重载运算符 521   小结 523   术语表 523   第15章 面向对象程序设计 525   15.1 OOP:概述 526   15.2 定义基类和派生类 527   15.2.1 定义基类 528   15.2.2 定义派生类 529   15.2.3 类型转换与继承 534   15.3 虚函数 536   15.4 抽象基类 540   15.5 访问控制与继承 542   15.6 继承中的类作用域 547   15.7 构造函数与拷贝控制 551   15.7.1 虚析构函数 552   15.7.2 合成拷贝控制与继承 552   15.7.3 派生类的拷贝控制成员 554   15.7.4 继承的构造函数 557   15.8 容器与继承 558   15.8.1 编写Basket类 559   15.9 文本查询程序再探 562   15.9.1 面向对象的解决方案 563   15.9.2 Query_base类和Query类 567   15.9.3 派生类 568   15.9.4 eval函数 571   小结 575   术语表 575   第16章 模板与泛型编程 577   16.1 定义模板 578   16.1.1 函数模板 578   16.1.2 类模板 583   16.1.3 模板参数 592   16.1.4 成员模板 595   16.1.5 控制实例化 597   16.1.6 效率与灵活性 599   16.2 模板实参推断 600   16.2.1 类型转换与模板类型参数 601   16.2.2 函数模板显式实参 603   16.2.3 尾置返回类型与类型转换 604   16.2.4 函数指针和实参推断 607   16.2.5 模板实参推断和引用 608   16.2.6 理解std::move 610   16.2.7 转发 612   16.3 重载与模板 614   16.4 可变参数模板 618   16.4.1 编写可变参数函数模板 620   16.4.2 包扩展 621   16.4.3 转发参数包 622   16.5 模板特例化 624   小结 630   术语表 630   第Ⅳ部分 高级主题 633   第17章 标准库特殊设施 635   17.1 tuple类型 636   17.1.1 定义和初始化tuple 637   17.1.2 使用tuple返回多个值 638   17.2 BITSET类型 640   17.2.1 定义和初始化bitset 641   17.2.2 bitset操作 643   17.3 正则表达式 645   17.3.1 使用正则表达式库 646   17.3.2 匹配与Regex迭代器类型 650   17.3.3 使用子表达式 653   17.3.4 使用regex_replace 657   17.4 随机数 659   17.4.2 其他随机数分布 663   bernoulli_distribution类 665   17.5 IO库再探 666   17.5.1 格式化输入与输出 666   17.5.2 未格式化的输入/输出操作 673   17.5.3 流随机访问 676   小结 680   术语表 680   第18章 用于大型程序的工具 683   18.1 异常处理 684   18.1.1 抛出异常 684   18.1.2 捕获异常 687   18.1.3 函数try语句块与构造函数 689   18.1.4 noexcept异常说明 690   18.1.5 异常类层次 693   18.2 命名空间 695   18.2.1 命名空间定义 695   18.2.2 使用命名空间成员 701   18.2.3 类、命名空间与作用域 705   18.2.4 重载与命名空间 708   18.3 多重继承与虚继承 710   18.3.1 多重继承 711   18.3.2 类型转换与多个基类 713   18.3.3 多重继承下的类作用域 715   18.3.4 虚继承 717   18.3.5 构造函数与虚继承 720   小结 722   术语表 722   第19章 特殊工具与技术 725   19.1 控制内存分配 726   19.1.1 重载new和delete 726   19.1.2 定位new表达式 729   19.2 运行时类型识别 730   19.2.1 dynamic_cast运算符 730   19.2.2 typeid运算符 732   19.2.3 使用RTTI 733   19.2.4 type_info类 735   19.3 枚举类型 736   19.4 类成员指针 739   19.4.1 数据成员指针 740   19.4.2 成员函数指针 741   19.4.3 将成员函数用作可调用对象 744   19.5 嵌套类 746   19.6 union:一种节省空间的类 749   19.7 局部类 754   19.8 固有的不可移植的特性 755   19.8.1 位域 756   19.8.2 volatile限定符 757   19.8.3 链接指示:extern "C" 758   小结 762   术语表 762   附录A 标准库 765   A.1 标准库名字和头文件 766   A.2 算法概览 770   A.2.1 查找对象的算法 771   A.2.2 其他只读算法 772   A.2.3 二分搜索算法 772   A.2.4 写容器元素的算法 773   A.2.5 划分与排序算法 775   A.2.6 通用重排操作 776   A.2.7 排列算法 778   A.2.8 有序序列的集合算法 778   A.2.9 最小值和最大值 779   A.2.10 数值算法 780   A.3 随机数 781   A.3.1 随机数分布 781   A.3.2 随机数引擎 783   C++11的新特性   2.1.1 long long类型 31   2.2.1 列表初始化 39   2.3.2 nullptr常量 48   2.4.4 constexpr变量 59   2.5.1 类型别名声明 60   2.5.2 auto类型指示符 61   2.5.3 decltype类型指示符 62   2.6.1 类内初始化 65   3.2.2 使用auto或decltype缩写类型 79   3.2.3 范围for语句 82   3.3 定义vector对象的vector(向量的向量) 87   3.3.1 vector对象的列表初始化 88   3.4.1 容器的cbegin和cend函数 98   3.5.3 标准库begin和end函数 106   3.6 使用auto和decltype简化声明 115   4.2 除法的舍入规则 125   4.4 用大括号包围的值列表赋值 129   4.9 将sizeof用于类成员 139   5.4.3 范围for语句 168   6.2.6 标准库initializer_list类 197   6.3.2 列表初始化返回值 203   6.3.3 定义尾置返回类型 206   6.3.3 使用decltype简化返回类型定义   6.5.2 constexpr函数 214   7.1.4 使用=default生成默认构造函数 237   7.3.1 类对象成员的类内初始化 246   7.5.2 委托构造函数 261   7.5.6 constexpr构造函数 268   8.2.1 用string对象处理文件名 284   9.1 array和forward_list容器 293   9.2.3 容器的cbegin和cend函数 298   9.2.4 容器的列表初始化 300   9.2.5 容器的非成员函数swap 303   9.3.1 容器insert成员的返回类型 308   9.3.1 容器的emplace成员的返回类型 308   9.4 shrink_to_fit 318   9.5.5 string的数值转换函数 327   10.3.2 Lambda表达式 346   10.3.3 Lambda表达式中的尾置返回类型 353   10.3.4 标准库bind函数 354   11.2.1 关联容器的列表初始化 377   11.2.3 列表初始化pair的返回类型 380   11.3.2 pair的列表初始化 384   11.4 无序容器 394   12.1 智能指针 400   12.1.1 shared_ptr类   12.1.2 动态分配对象的列表初始化 407   12.1.2 auto和动态分配 408   12.1.5 unique_ptr类 417   12.1.6 weak_ptr类 420   12.2.1 范围for语句不能应用于动态分配数组 424   12.2.1 动态分配数组的列表初始化 424   12.2.1 auto不能用于分配数组 424   12.2.2 allocator::construct可使用任意构造函数 428   13.1.5 将=default用于拷贝控制成员 449   13.1.6 使用=default阻止拷贝类对象 449   13.5 用移动类对象代替拷贝类对象 469   13.6.1 右值引用 471   13.6.1 标准库move函数 472   13.6.2 移动构造函数和移动赋值 473   13.6.2 移动构造函数通常应该是noexcept 473   13.6.2 移动迭代器 480   13.6.3 引用限定成员函数 483   14.8.3 function类模板 512   14.9.1 explicit类型转换运算符 516   15.2.2 虚函数的override指示符 530   15.2.2 通过定义类为final来阻止继承 533   15.3 虚函数的override和final指示符 538   15.7.2 删除的拷贝控制和继承 553   15.7.4 继承的构造函数 557   16.1.2 声明模板类型形参为友元 590   16.1.2 模板类型别名 590   16.1.3 模板函数的默认模板参数 594   16.1.5 实例化的显式控制 597   16.2.3 模板函数与尾置返回类型 605   16.2.5 引用折叠规则 609   16.2.6 用static_cast将左值转换为右值 612   16.2.7 标准库forward函数 614   16.4 可变参数模板 618   16.4 sizeof...运算符 619   16.4.3 可变参数模板与转发 622   17.1 标准库Tuple类模板 636   17.2.2 新的bitset运算 643   17.3 正则表达式库 645   17.4 随机数库 659   17.5.1 浮点数格式控制 670   18.1.4 noexcept异常指示符 690   18.1.4 noexcept运算符 691   18.2.1 内联名字空间 699   18.3.1 继承的构造函数和多重继承 712   19.3 有作用域的enum 736   19.3 说明类型用于保存enum对象 738   19.3 enum的提前声明 738   19.4.3 标准库mem_fn类模板 746   19.6 类类型的联合成员 75
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值