C++ primer读书记录包括第一部分和第二部分 第二十二次更新2021.9.10

个人向学习 专栏收录该内容
14 篇文章 0 订阅
                                                                                                                                                                                                                                                                                                                  需要对程序进行恰当的缩进保持程序的可读性

第一章 开始

cout<<c1<<endl;可以视为(cout<<c1)<<endl;
cin>>v1>>v2;可以视为(cin>>v1)>>v2;

endl的作用是将与设备相关联的缓冲区(buffer)的内容美国都刷到设备中去

//1.2节练习
#include <iostream>
using namespace std;

int main()
{
   cout << "Hello World!\n";
   cout << "Please enter two numbers" << endl;
   int c1, c2;
   cin >> c1 >> c2;
   cout << "The sum of two numbers is " << c1 + c2 << endl;
   cout << "The * of two numbers is" << c1 * c2 << endl;
   return 0;
}

在多行注释中,一个注释不能嵌套另一个注释

//1.4.1练习
int i = 50;
int sum = 0;
while (i <= 100)
	{
		sum += i;
		++i;
	}
cout << "The sum of 50 to 100 is " << sum << endl;
int j = 10;
while (j >= 0)
	{
		cout << j << " ";
		--j;
	}
cout << "Please enter two numbers" << endl;
int c1, c2;
cin >> c1 >> c2;
while (c2 >= c1)
	{
		cout << c1 << " ";
		++c1;
	}
//1.4.4练习
int cout_value= 0, value = 0;
if (cin >> cout_value)
{
   int cnt = 1;
   while (cin >> value)
	{
		if (cout_value == value)
		{
			++cnt;
		}
		else {
			cout << cout_value << " occurs" << cnt << " times" << endl;
			cnt = 1;
			cout_value = value;
		}
	}
	cout<< cout_value << "occurs" << cnt << "times" << endl;
}

如果1.4.4练习中输入相同的数字,那么程序会一直循序下去

点运算符只能用于类类型的对象,即左边运算对象必须是一个类类型的对象,右侧运算对象必须是该类型的一个成员名

第二章 变量和基本类型

一个字节占8比特,一个字通常由32或64比特构成,即4或8字节

2.1 基本内置类型

当函数不返回任何值时使用空类型作为返回类型

使用short进行整型运算,如果数值超过了int的表示范围,选用long long。

执行浮点数运算一般选用double,因为float通常精度不够而且双精度和单精度浮点数的计算代价相差无几

short<=int<=long

不要混用有符号数和无符号数

	unsigned u = 10, u2 = 42;
	int i = 10, i2 = 42;
	cout << u2 - u << endl;//32 正
	cout << u - u2 << endl;//4294967264 错
	cout << i2 - i << endl;//32 正
	cout << i - u << endl;//0 正
	cout << u - i << endl;//0 正

以0开头的数代表8进制数
以0x或0X开头的代表十六进制数
在默认情况下,十进制数是带符号数,8进制和十六进制没有明确规定

由单引号括起来的一个字符叫char型字面值
双引号括起来的零个或者多个字符叫做字符串型字面值,且编译器会在每个字符串的结尾处加上空字符(‘\0’),所以字符串实际长度会多1

转义字符也是字符,输出需要在引号内
将转义字符收集如下:
转义字符 意义 ASCII码值(十进制)
\a 响铃(BEL) 007
\b 退格(BS) 008
\f 换页(FF) 012
\n 换行(LF) 010
\r 回车(CR) 013
\t 水平制表(HT) 009
\v 垂直制表(VT) 011
\ 反斜杠 092
? 问号字符 063
’ 单引号字符 039
" 双引号字符 034
\0 空字符(NULL) 000
\ddd 任意字符 三位八进制
\xhh 任意字符 二位十六进制

不过要注意:

1,\v垂直制表和\f换页符对屏幕没有任何影响,但会影响打印机执行响应操作。

2,\n其实应该叫回车换行。换行只是换一行,不改变光标的横坐标;回车只是回到行首,不改变光标的纵坐标。

3,\t 光标向前移动四格或八格,可以在编译器里设置

4,’ 在字符里(即单引号里)使用。在字符串里(即双引号里)不需要,只要用 ’ 即可。

5,? 其实不必要。只要用 ? 就可以了(在windows VC6 和tc2 中验证)。

6, 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。

泛化的转义字符是\x后紧跟1个或多个十六进制数字,或者\后跟1-3个八进制数字(不能超过三个)
在这里插入图片描述

2.2 变量

对象是具有某种数据类型的内存空间
初始化和赋值是两个完全不同的操作,虽然在很多编程语言中二者的区别可以忽略不计

初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代
列表初始化是使用花括号来初始化数值,例如int i={0}; int i{0};
如果使用列表初始化且存在丢失信息的风险,则编译器将报错。

一般类会将未初始化的变量进行默认初始化,但有些类并不是这样。使用未初始化变量的值是一种错误的编程行为且很难调试。
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义

声明变量规定变量的类型和名字,定义是在声明的基础上申请存储空间也可能为变量赋予一个初始值

如果想声明一个变量而不定义它,就加上extern,例如extern int i;但如果对此进行初始化就变成了定义。

定义只能由一次,声明可以有多次
如果需要在多个文件中使用同一个变量,那么就必须将声明和定义分离开来,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对此进行声明,却不能进行重复定义。

允许嵌套的作用域定义外层作用域已有的名字

2.3 复合类型

复合类型是指基于其他类型定义的类型,如引用和指针

引用就是为对象起了另一个名字,且引用必须被初始化。在定义引用的时候,程序把引用和它的初始值绑定在一起,且引用无法重新绑定对象。
引用的初始值必须是对象且必须与其数据类型相同。

指针本身是一个对象,允许对指针进行赋值和拷贝。指针可以先后指向不同的对象且无需在定义时就赋值。
指针用于存放某个对象的地址,如果想要获取该地址需要使用取地址符(&),例如int *ival=42; int *p=&ival;此时p被定义为指向变量ival的指针。
指针的值应属于下列4种状态之一
1.指向一个对象
2.指向紧邻对象所占空间的下一个位置
3.空指针,意味着指针没有指向任何对象
4.无效指针,也就是上述情况之外的其他值。

&紧随类型名出现,因此是声明的一部分,属于引用
*紧随类型名出现,因此是声明的一部分,属于指针

&出现在表达式中,说明是一个取地址符
*出现在表达式中,说明是一个解引用符

以下三个方法可以生成空指针
int *p1=nullptr;
int *p1=0;
int *p1=NULL;
三个生成空指针的方法,但是要避免使用NULL,最好使用nullptr

赋值永远改变的是左边的值

只要指针拥有一个合法值,哪怕它是一个空指针(false),也能用在条件表达式中

void指针是一种特殊类型的指针,它可以存放任意对象的地址,但是我们不了解它存放了什么类型的对象所以无法确定能做什么操作。
只能拿void指针做以下操作:拿它和其他指针比较,作为函数的输入输出,赋值给另外一个void
指针。

练习2.3.2
	int i = 0, j = 1;
	int* vi = &i;
	cout << *vi << endl;//0
	*vi = 2;
	cout << *vi<<endl;//2
	vi = &j;
	cout << *vi << endl;//1

不能定义指向引用的指针,但是可以定义指向指针的引用
例如:int* p;int*& r=p;此时r就是指向指针p的引用

最简单判断变量类型是什么的方法就是从右向左阅读变量的定义,距离变量名最近的符号对变量的类型有最直接的影响。

2.4 const限定符

可以通过加上const将变量限定为一个常量,例如const int i=1;
由于const对象一旦创建之后其值就不能再修改,所以const对象必须初始化

可以通过添加extern关键字使得const变量只在一个文件中定义,在其他文件中声明且使用它

file.cc
extern const int i=9;
file.h
extern const int i;

file.cc用于定义且初始化变量i,加上extern使其可以被其他文件使用,file,h的extern指明其定义在别处出现。

不能用非常量引用指向常量对象

初始化常量引用(const引用)时候允许使用任何表达式作为初始值,只要该表达式可以转换成引用的类型即可。
允许为一个const引用绑定非常量对象、字面值甚至是一般表达式
如果不是相同类型的变量,那么常量引用绑定的是一个临时量
例如:

int i=42const int &r1=i;
const int &r2=42const int&r3=r1*2

要想存放常量对象的地址,只能使用常量指针。
常量指针可以指向非常量对象。

**const在指针上分为顶层指针和底层指针,顶层const表示指针本身是一个常量,底层const表示所指对象是一个常量
顶层指针例如:int const i=&j,不允许改变指向的对象,但是可以通过指针改变对象的值
底层指针例如:const int i=&j,不允许改变指向对象的值,但是可以指向其他对象

底层const对指针的限制是:拷入拷出的对象必须具有相同的底层const资格,或者两个的数据类型必须能够转换 。

常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
一个对象是不是常量表达式是由它的数据类型和初始值共同决定的。

constexpr类型用于让编译器验证变量的值是否是一个常量表达式,声明为constexpr类型的变量一定是一个常量。

算术类型,引用和指针都属于字面值类型。因为这些类型比较简单,容易得到。

一个constexpr指针的初始值必须是nullptr或者0或者是存储与某个固定地址的对象(定义于所有函数体之外的对象其地址固定不变,能用于初始化constexpr指针)
constexpr仅仅对指针起作用,定义的指针就是常量指针即指定固定的对象,相当于顶层const的指针

2.5 处理类型

类型别名是通过typedef关键字给类型起另一个名字,例如typedef double wages;
可以通过别名声明来定义别名:例如using wages = double;

typedef char* prstring;
const pstring cstr=0;//cstr是指向char的常量指针
const pstring ps;//ps是一个指针,它的对象是指向char的常量指针
此时不能将const pstring cstr理解为 const char
cstr。由于pstring本身代表的是指向char的指针,const pstring就是指向char的常量指针。
const char是基本数据类型,*是声明符(变量名)的一部分。pstring是一个整体

关键字auto定义的变量必须有初始值。
auto一般会忽略顶层const,同时底层const会被保留下来。
对常量对象取地址是一种底层const

decltype的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
例如:decltype(f()) sum = x;sum的类型就是函数f的返回类型

int i=42,*p=&i;
decltype(*p) c;此时出错,因为c是一个int&,必须初始化
这是因为解引用指针可以得到指针所指对象,而且可以给这个对象赋值,因此结果类型就是int&,而不是int

decltype对括号很敏感,例如:decltype((i)),此时的类型就是int&,因为(i)是一个表达式
双层括号的结果永远是引用,单层括号的时候只有当里面的值是引用才会是引用

2.6 自定义数据结构

struct Sales_data {//} accum,trans,*salesptr;
类后面可以紧跟变量名以表示对该类型对象的定义,所以分号必不可少。
但是最好不要把对象的定义和类的定义放在一起。

struct Sales_data{
	string bookNo;
	double reveue = 0.0;
	unsigned units_sold = 0;
};
int main()
{
	Sales_data data1, data2;
	cout << "Please input data1" << endl;
	cin >> data1.bookNo >> data1.reveue >> data1.units_sold;
	cout << "Please input data2" << endl;
	cin >> data2.bookNo >> data2.reveue >> data2.units_sold;
	if (data2.bookNo == data1.bookNo)
	{
		unsigned total_sold = data1.units_sold + data2.units_sold;
		double tatal_reve = data1.reveue + data2.reveue;
		cout << "bookNo:" << data1.bookNo << "sold number is" << total_sold << "reveue is " << tatal_reve;
	}
   return 0;

头文件通常包含那些只能被定义一次的实体,例如类、const和constexpr变量。

C++还会用到头文件保护符
通过#define将一个名字设为预处理变量,另外两个指令分别检查预处理变量是否被定义,#indef即已经被定义为真,#ifndef即未被定义为真,一旦检查为真,则执行后续操作指导遇到#endif指令为止。
一般把预处理变量的名字全部大写,一般是通过头文件中类的名字来构件保护符的名字。

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

3.1 命名空间的using声明

通过using std::cout类似的方式使用std的成员,也可以通过using namespace std;使用命名空间std的所有成员
如果头文件中有某个using声明,那么每个使用该头文件的文件都会有这个声明。

3.2标准库类型 string

string定义在命名空间std中
初始化string对象的方式如下

string s1;
string s2(s1);
string s2=s1;
string s3("value");直接初始化
string s3="value";拷贝初始化
string s4(n,'c');  把s4初始化为由连续的n个字符c组成的串

使用cin操作读取字符串的时候,遇到空白就会停止,string对象会自动忽略开头的空白。如输入“ hello world “的时候,输入进去的仅仅是”hello”。可以利用string对象的这个性质进行连续输入。

getline()函数用于从string中读取一整行数据,一遇到换行符就结束读取操作并返回结果。如果输入一开始就是换行符,那么所得结果就是一个空的string。
通过getline(cin,line)可以获得输入进去line的一整行,出发getline函数返回的换行符实际上被丢弃了

string对象的size和length用于计算string对象长度的操作,都不会包括最后的终止符\0,例如”value"只会返回5字符。

size返回的其实是size_type类型的值,它是一个无符号类型的值而且可以存放下任何string对象的大小。

关于string的比较其实是string对象第一个相异字符比较的结果,如果字符相同则比较长短。例如Hiya>Hello world>Hello

string对象相加就是拼接两个string对象,string对象也可以与字面值相加,如string s4=s1 + ”,“;但是两侧的运算对象只有有一个是string对象,string s4=“ni”+“,”就是错误的。

字面值不能直接相加,否则会出错。
字面值不是string对象

getline每次读取一整行,cin每次读取一个词。如果出现空格getline照常输入,cin则会将停止

P82页用于处理string中的字符
使用基于范围的for语句来处理每个字符,语法形式为:
for(变量:序列)
循环语句
该变量用于访问循环序列中的基础元素

如果想通过for语句改变字符串中的字符,那么需要将循环变量定义为引用类型。例如

for(auto &c: s)
	c=toupper(c);//把c换成大写

如果需要访问字符串其中一个字符或者访问多个字符的时候遇到某个条件暂停,可以使用下标或者迭代器。

练习3.2.3
	string line("xi nan da xue");
	for (char c : line)
		c = 'x';
	cout << line;

string line("xi nan da xue");
	decltype(line.size()) c = 0;
	while (c < line.size())
	{
		line[c] = 'x';
		++c;
	}
	cout << line;

3.3 标准库类型vector

vector表示对象的集合,其中所有对象的类型都相同。集合中每个对象都有一个与之对应的索引,索引用于访问对象
vector是一个类模板

引用不是对象,所以不存在包含引用的vector。
有些编译器需要在外层vector尖括号和元素类型之间添加一个空格。

vector初始化方法如下

vector<T> v1;
vector<T> v2(v1);
vector<T> v2=v1;
vector<T> v3(n,val);
vector<T> v4(n);
vector<T> v5{a,b,c.....};
vector<T> v5={a,b,c.....};

vector对象进行拷贝的时候,两者的类型必须保持相同。
vector进行列表初始化的时候,只能放在花括号之中。例如vector<.string> v1{“a”,“b”,“cde”};
vector<int.> v3{10,1};//有两个元素:10和1
vector<int.> v3(10,1);//有10个元素:都是1
vector<string.> v7{10};//会创建10个默认初始化的对象,因为10不能作为string值
vector<string.> v8{10,“hi”};//同理10个“hi”

vector添加和删除元素的方法有两种。push_back从后方添加元素,push_front从前面添加元素
如果循环体内部包含向vector对象添加元素的语句,则不能使用范围for循环

练习3.3.2
	vector<int> c;
	int i;
	while (cin >> i)
		c.push_back(i);
	for (int i : c)
		cout << i << " ";
	vector<string> m;
	string n;
	while (cin >> n)
		m.push_back(n);
	for (string s : m)
		cout << s << "  ";

vector的下标运算符可以用于访问已经存在的元素而不能用于添加元素

练习3.3.3
	vector<int> c;
	int i;
	while (cin >> i)
		c.push_back(i);
	for (decltype(c.size()) m = 0; m <= c.size()/2; ++m)//(decltype(c.size()) m = 1; m < c.size(); ++m)
	{ 
	
		//int sum = c[m] + c[m - 1];
		int sum = c[m] + c[c.size() - 1 - m];
		cout << sum << " ";
	}

3.4 迭代器介绍

类似于指针类型,迭代器提供了对对象的间接访问。
有些类型提供了begin和end成员用于返回迭代器,其中begin返回指向第一个元素的迭代器,end返回的是尾元素的下一位置的迭代器。
如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。

和指针类似,迭代器也能通过解引用来获取它所指示的元素,执行解引用的迭代器必须合法并确实指示着某个元素。
迭代器可以通过加减数字移动在容器中的位置,因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作

所有标准库容器的迭代器都定义了==和!=,但是它们大多数都没有定义<运算符

容器<类型>::iterator it;此时it就是该容器的迭代器。
const_iterator和常量指针差不多,能读取但是不能修改所指的元素值。
如果容器对象是一个常量,只能用const_iterator,如果非常量,那么既可以使用iterator也能使用const_iterator。
为了便于得到const_iterator类型的返回值,C++11引入了两个新函数,分别是是cbegin和cend。

解引用可以得到迭代器所指的对象,如果该对象的类型恰好是类,就可能希望进一步访问它的成员。例如(*it).empty();此时的括号必不可少,否则试图访问的是it的名为empty的成员。it->man和(*it).man表达的意思相同

某些对vector对象的操作会使得迭代时失效
1.不能在范围for循环中向vector对象添加元素。
2.任何一种可能改变vector对象容量的操作,例如push_back都会使得该vector对象的迭代器失效。

在这里插入图片描述
两个迭代器相减的结果是它们之间的距离,距离指的是右侧的迭代器向前移动多少位置可以追上左侧的迭代器,其类型为difference_type的带符号整型数。
使用迭代器运算进行查找最经典的算法是二分查找。

3.5 数组

数组是一种类似于标准库类型vector的数据结构,与vector不同的地方是,数组的大小确定不变,不能所以向数组中增加元素。
如果不清楚元素的确切个数,请使用vector。
数组声明如a[d]所示,其中a是数组的名字,d是数组的维度。维度说明的数组的大小,它必须大于0且必须是一个常量表达式。
不存在引用的数组。

可以对数组进行列表初始化,此时允许忽略数组的维度。
可以用字符串字面值对字符数组进行初始化,例如char a[]=“C++”;此时数组的维度是4,因为会自动添加结束符号的空字符。
数组不允许拷贝和赋值

int *ptr[10]; 此时ptr是含有10个整型指针的数组
int (*ptr)[10]=&arr ;此时ptr指向一个含有10个整数的数组
int (&ptr)[10]=arr ;此时ptr引用一个含有10个整数的数组

对于数组而言,由内而外阅读要比从右向左阅读要好。一般来说,先读括号再读右侧再读左侧的类型。
例如int *(&arry)[10]=ptrs; arry就是一个含有10个int类型指针的数组的引用。

数组除了大小固定的特点,其他用法与vector基本类似。最好的遍历方法也是使用范围for循环。

练习
int a[10];
	int i = 0;
	for (auto &c : a)
	{
		c = i++;
	}
	for (auto c : a)
	{
		cout << c << "  ";
	}

使用数组的时候,编译器一般会把它转换成指针,通常情况下使用取地址符来获取指向某个对象的指针。
例如:string *p=&num[0];此时p指向数组num的第一个元素。数组还有一个特性是,当很多用到数组名字的地方,编译器都会自动地将其替换成一个指向数组首元素的指针
例如:string *p=num;等价于string *p=&num[0];

由上可知,一些情况下数组的操作实际上是指针的操作。
当使用数组名称作为一个auto变量的初始值的时候,推断获得的类型是指针而不是数组。使用decltype返回的类型是数组。
指针也是迭代器如果获取到指向数组首元素的指针和尾元素下一位的指针也可以遍历数组中的元素。
C++11新标准引入了begin和end两个函数,可以通过将数组作为它们的参数来获取首元素指针和尾元素下一位置指针
例如:int *b=begin(ia); int *a =end(ia);
这两个函数定义在iterator头文件中。

和迭代器一样,两指针相减的结果是它们之间的距离,参与运算的指针必须指向同一个数组当中的元素。两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型。

只要指针指向的是数组中的元素,都可以执行下标运算。例如:int *p=ia;int k=p[-2];p指向ia的首元素,k是ia[0]表示的那个元素。

C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法,按此习惯书写的字符串存放再字符数组中并以空字符结束即最后一个字符后跟着(‘\0’)
即char ca1=“sdadsad”;

strlen§//返回p的长度,空字符不计算在内
strcmp(p1,p2)//比较p1和p2的相等性。相等返回0,大于返回正值,小于返回负值
strcat(p1,p2)//把p2附加到p1之后并返回给p1
strcpy(p1,p2)//把p2拷贝给p1
以上操作必须对于C风格字符串才能生效,如果不是以空字符作为结束的数组会产生错误。
如果将比大小的运算符作用在C风格字符串上,比较的会是指针而非字符串本身,所以只能通过strcmp进行比较。

练习
    char a1[12] = "hello";
	char a2[] = "world";
	char a3[11] ;
	strcat(a1, a2);
	strcpy(a3, a1);
	for (auto c : a3)
		cout << c;
   return 0;

允许以空字符结束的字符数组来初始化string对象或者为string对象赋值
在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象,在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
反过来上述性质不成立,C++提供了c_str()方法辅助用string对象初始化C风格字符串

允许使用数组初始化vector对象,反过来则不允许例如vector ivec(begin(xxx),end(xxx));

现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;
尽量使用string对象,避免使用C风格基于数组的字符串

3.6 多维数组

多维数组就是数组的数组。
例如:int ia[3].[4]; 代表大小为3的数组,每个元素是含有4个整数的数组。
int ia[10].[20].[30] 代表的就是大小为10的数组,每个元素是大小为20的数组,这些数组的元素都是含有30个整数的数组。
int (&row)[4]=ia[1]; row是一个含有4个int元素的数组的引用,绑定到ia的第二行

可以使用范围for循环和嵌套for循环处理多维数组
for(auto &row:ia)对于外层数组的每一个元素
for(auto col:row)对于内层数组的每一个元上都
将外层变量声明为引用类型是为了防止数组自动被转换成指针
要使用for语句处理多维数组,除了最内层的循环外,其他所有循环控制变量都应该是引用类型

for(auto p=ia;p!=ia+3;++p)//p指向含有四个整数的数组
	for(auto q=*p;q!=*p+4;++q)//q指向一个整数,*p是一个含有4个整数的数组

第四章 表达式

作用于一个运算对象是一元运算符,如取地址符&和解引用符*。
作用域两个运算对象是二元运算符,如相等运算符==和加法运算符+

用户可以自定义运算符含义,称之为重载运算符。
我们使用重载运算符时,其包括运算对象的类型和返回值的类型都是由该运算符定义的。

当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
使用decltype的时候,如果表达式的求值结果是左值,decltype作用于该表达式得到一个引用类型。
例如,p的类型是int*,由于解引用运算符生成左值,所以decltype(*p)的结果是int&
由于取地址运算符生成右值,所以decltype(&p)的结果是指针的指针

对于没有执行顺序的运算符来说,如果表达式指向并修改了同一个对象,将会引发错误并产生未定义的行为。例如cout<<i<<++i;
拿不准的时候最好用括号来强制让表达式的组合关系符合程序逻辑要求。
如果改变了某个运算对象的值,在表达式其他地方不再使用。当改变运算对象的表达式本身就是另一个子表达式的运算对象时该规则无效,如
++i
*

所有不等于0的数字转换成bool值后都会变成1。
参与取余运算的对象必须是整数类型。取余运算符号%

关系运算符不能连续使用,如:i<j<k,此时拿k与i<j的结果比大小即bool值(0或1),需要使用逻辑运算符。

if(val)//如果val非0则条件为真
if(!val)//如果val值为0则第二个条件为真

递增递减运算符
前置运算符会先将运算对象+1(-1),然后将改变后的对象作为求值结果
后置运算符会也会使得运算对象+1(-1),但是求值结果是运算对象改变之前的那个值的副本
例如

int i=0,j=0;
j=++i;//j=1,i=1
j=i++;//j=1,i=2

除非必须,否则不适用递增递减运算符的后置版本
如果我们想在一条复合表达式中既将变量+1或者-1又能使用他原来的值,此时可以使用后置版本

例如pdeg=v.begin; cout<<*pdeg++;

此时相当于*(pdeg++),它先把pdeg+1,然后返回pdeg的初始值的副本作为其求值结果
应该熟练掌握简洁写法

点运算符用于获取类对象的一个成员;
条件运算符允许我们把简单的if-else逻辑嵌入到单个表达式中,条件运算符按照如下形式使用:cond?expr1:expr2
允许条件运算符进行嵌套使用.

string final=(grade<60)?"fail":"success";
string final=(grade>90)?"high pass":(grade<60)?"fail":"success";

条件运算的嵌套最好不要超过两到三层,保证代码的可读性

练习
vector<int> a = { 1,2,3,4,5,6,7,8,9 };
	for (auto& c : a)
	{
		c = (c % 2 == 0) ? c : 2 * c;
		cout << c << "  ";
	}

类型bitset的标准库类型可以表示任意大小的二进制位集合
右侧的运算对象一定不能为负

移位运算符有<<和>>
它们要求左侧运算对象的内容按照右侧运算对象的拷贝作为求值结果。其中右侧运算对象一定不能为负,而且值必须严格小于结果的位数

例如 unsigened char bits=0233; 其二进制为1001 1011
bits<<8;左移8位,结果为
0000 0000 / 0000 0000 / 1001 1011 /0000 0000
bits>>3;右移三位,结果为
0000 0000 / 0000 0000 / 0000 0000 /0001 0011

位求反运算符为~,将0置为1,将1置为0
位与运算符&,同1为1,否则为0
位或运算符|,有1为1,否则为0
位异或运算符^,不同为1,相同为0
例如

unsigned long result=0;//result至少32位
unsigned long IU=1;
//如果假设30个学生,第27个学生合格了,需要将result的第27位置为1
result |= IU<<27;
//核算过程中,第27位同学没合格
result &= ^(IU<<27);

移位运算符又称IO运算符满足左结合律
例如 cout<<“hello”<<“world”;
比算数运算符优先级低,但是比关系运算符、赋值运算符和条件运算符优先级高

sizeof运算符返回一条表达式或者一个类型名字所占的字节数。sizeof运算符满足右结合律
对char类型的表达式执行sizeof运算,结果为1
对引用类型会得到被引用对象所占空间大小
对指针类型会得到指针本身所占空间大小
对解引用指针会得到指针所指对象所占空间大小,指针无需有效
对数组会得到整个数组所占空间大小
对string对象或者vector对象会返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
sizeof的返回值是一个常量表达式

逗号运算符的求值结果取决于右侧的表达式

运算符的运算对象将转换成最宽的类型。
指向任意非常量的指针能转换成void*
指向任意对象的指针能转换成const void*

允许指向非常量类型的指针转换成指向相应的常量类型的指针,例如const int *p=&i;

显式转换需要命名的强制类型转换
即cast-name<type.>(expression),其中type是转换的目标类型,expression是需要转换的值
cast-name是static_cast , dynamic_cast , const_cast 和 reinterpret_cast其中一种。

static_cast,任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast
const_cast只能改变运算对象的底层const,可以将常量对象转换成非常量对象。
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释

例如
double s=static_cast<double>(j)/i;

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

int *ip;
char *pc=reinterpret_cast<char*>(ip);//pc指向的实际对象是一个int,但是编译器会觉得它的值是char*类型

早期版本的强制转换为 type(expr); (type)expr;

第五章 语句

空语句出现在如果语法上需要一条语句但是逻辑上不需要的地方。例如循环的全部工作在条件部分已经被全部完成了,我们通常会用到空语句。
复合语句是指用花括号括起来的语句和声明的序列,也称作块。在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。

5.3 条件语句

C++提供了两种按条件执行的语句,一种是if语句,它根据条件决定控制流;另一种是switch语句,它根据值选择执行路径

if else语句的形式为

if(condition)
	statement1
else(condition)
	statement2

嵌套if else语句

if(condition)
	statement1
else if(condition)
	statement2

if(condition)
	statement1
else
{
	statement2
	if(condition)
		statement3
	else(condition)
		statement4
}

就C++而言,它规定else与离它最近的尚未匹配的if匹配,从而消除程序的二义性。

switch语法如下

switch(ch){
	case 'x1':
	state1;
	break;
	case 'x2':
	state2;
	break;
}

switch语句首先对括号内的表达式求值,该表达式紧跟在关键字switch的后面可以是一个初始化的变量声明,然后与每个case标签的值比较。
break语句的作用是中断当前的控制流。
case标签必须是整型常量表达式,任何两个case标签的值不能相同,否则就会引发错误。
整型如下所示
在这里插入图片描述

如果需要两个或更多的值共享同一组case操作,可以故意省略break语句,这样知道switch的结尾处才会停止,使得程序可以执行若干个case标签。

switch(ch)
{
	case 'a':
	case 'e':
	case 'i':
	case 'o':
	case 'u':
		++result;
		break;
}

也可以将上述case写在一行内,强调这些case代表的是某个范围内的值

如何没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签后面的语句。

如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。
C++规定不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置
如果需要为某个case内定义并初始化一个变量,应该把变量定义在块内,从而保证后面所有的case标签都在变量的作用域之外。

5.4 迭代语句

迭代语句通常称为循环语句。while和for语句在执行循环体之前检查条件,do while语句先执行循环体,然后再检查条件。

while(condition) statement
当不确定到底要迭代多少次的时候,使用while循环比较合适。
想要再循环解释后访问循环控制变量也应当使用while

for(init-statement;condition;expersion)
statement;
其中init-statement必须是声明语句、表达式语句和空语句其中的一种
for语句头中定义的对象只在for循环体内可见。init-statement可以定义多个对象,但是只能有一条声明语句,因此所有变量的基础类型必须相同。

for语句头中可以省略init-statement、condition和expression中的任意一个或者全部,但必须用空语句代替。
省略condition相当于条件的值永远是true
省略expression需要在循环体内有终止循环的条件存在。

	const vector<int>  a = { 0,1,1,2 };
	const vector<int>  b = { 0,1,1,2,5,5,6 };
	for (int i = 0; i < a.size(); ++i)
	{
		
		if (a[i] == b[i])
		{
			if (i == a.size()-1)
				cout << "True";
			continue;
		}
		else
		{
			cout << "False";
			break;
		}
	}

范围for循环

for(declaration:expression)
	statement;

expression表示的必须是一个序列,比如花括号括起来的初始值列表、数组或者vector或string等类型的对象
declaration定义一个变量,序列中的每个元素都得能转换成该变量的类型。最简单的操作是使用auto类型说明符
如果需要改变序列中的值,可以使用auto&的形式。

在范围for循环中,预存了end()的值,一旦在序列中添加或删除元素,end函数的值就可能变得无效了。

do while语句和while语句非常相似,do while语句先执行循环体后检查条件。不管条件的值如何,我们都至少执行一次循环。

do 
	statement;
while(condition)

循环的条件不能在do的内部,而且由于先执行语句块后判断条件,所以不允许在条件部分定义变量。

5.5 跳转语句

break语句负责终止离他最近的while,do-while,for或switch语句,并从这些语句之后的第一条语句开始继续执行。

continue语句终止最近的循环中的当前迭代并立即开始下一次迭代。continue语句只能出现在for、while和do-while循环的内部,或者嵌套在此类循环里的语句或块的内部
continue语句中断当前的迭代,但是仍然继续执行迭代

goto语句的作用是从goto语句无条件跳转到同一函数内的另一条语句。不要在程序中使用,因为它使得程序即难理解又难修改
语法形式为:goto label;label是用于标识一条语句的标识符。带标签语句是一种特殊的语句,在它之前有一个标识符以及一个冒号:例如end:return
标签标识符独立于变量和其他标识符的名字,因此可以和程序中其他实体的标识符使用同一个名字。goto语句和控制权转向的那条带标签的语句必须位于同一个函数中

begin: 
	int sz=1;
	if(sz<0)
		goto begin;

5.6 try语句块和异常处理

异常指的是存在于运行时的反常行为,这些行为超过了函数正常功能的范围。
当程序的某部分检测到一个它无法处理的问题时候,需要用到异常处理。如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。

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

以下抛出异常代码都相当于以下代码
Sale_item it1,it2;
cin>>it1>>it2;
if(it1.isbn()==it2.isbn(){
	cout<<it1+it2<<endl;
	return 0;//表示成功
}else{
	cout<<"Data must refer to same isbn"<<endl;
	return -1;//表示失败
}

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

if(it1.isbn()!=it2.isbn())
	throw runtime_error("Data must refer to same isbn");
//如果程序执行到这里,表明两个isbn是相同的
cout<<it1+it2<<endl;

抛出异常会终止当前的函数,并把控制权转移给能处理该异常的代码。runtime_error必须被初始化,方法是提供给一个string对象或者一个C风格的字符串。

try{
	program-statements
catch(exception-declaration){
	handler-statements
}catch(exception-declaration){
	handler-statements
}
}....

catch关键字后面跟的是括号内一个(可能未命名的)对象的声明(异常声明)以及一个完整的块。
try语句中的program-statements组成程序的正常逻辑,try块中声明的变量在块外部也无法访问,catch中也无法访问

while(cin>>it1>>it2){
	try{
		cout<<it1+it2<<endl;
		//如果相加失败抛出一个runtime_error的异常
	}catch(runtime_error err){
		cout<<err.what()
			<<"\nTry again?Enter y or n"<<endl;
		char c;
		cin>>c;
		if(!cin||c=='n')
			break;
	}
}

what是runtime_error的成员函数,每个标准库异常类都定义了名为what的成员函数,这些函数没有参数,返回的是C风格字符串(即const char *)。其中runtime_error的what成员返回的是初始化一个具体对象时候所用的string对象的副本。如果上述throw抛出异常,则try返回的语句为:
Data must refer to same isbn
Try again?Enter y or n
当异常被抛出的时候,首先搜索抛出该异常的函数。如果没找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。
如果还是没找到匹配的catch子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推,直到找到适当类型的catch子句为止。
如果都没找到,程序转到名为terminat的标准库函数。一般情况下,执行该函数将导致程序非正常退出。
对于那些没有任何try语句块定义的异常,也按照类似的方式处理

exception头文件定义了最通用的异常类exception,仅仅用于报告异常的发生。
new头文件定义了bad_alloc异常类型
type_info头文件定义了bad_cast异常类型
我们只能用默认初始化的方式初始化 exception、bad_cast和bad_alloc对象,不允许提供初始值
stdexcept定义了几种常用的异常类型。
应该使用string对象或者c风格字符串初始化其他异常类型的对象,不允许使用默认初始化的方式

stdexcept定义的异常类

exception 最常见的问题
runtime_error 只有在运行时才能检测出的问题
range_error 运行时错误:生成的结果超出了有意义的值域范围
overflow_error 运行时错误:计算上溢
underflow_error 运行时错误:计算下溢
logic_error 程序逻辑错误
domain_error 逻辑错误:参数对应的结果值不存在
invalid_argument 逻辑错误:无效参数
length_error 逻辑错误:试图创建一个超出该类型最大长度的对象
out_of_range 逻辑错误:使用一个超出有效范围的值

异常类型只有what一个成员函数,其没有任何参数,返回值是一个指向c风格字符串的const char*。该字符串的目的是提供关于异常的一些文本信息。
what函数返回的C风格字符串的内容与异常对象类型有关,如果异常类型有一个字符串初始值,则what返回该字符串。对于其他无初始值的异常类型来说,what返回的内容由编译器决定。

第六章 函数

6.1 函数基础

一个该函数需要包括:返回类型、函数名字、由0个或多个形参组成的列表、函数体。

例子求阶乘
int fact(int val)
{
	int ret=1;
	while(val>1)
		ret*=val--;
	return ret;
}
int main(){
	int j=fact(5);
	cout<<"5! is "<<j<<endl;
	return 0;
}

函数的调用1.需要用实参初始化函数对应的形参,2.将控制权转移给被调用函数。
实参是形参的初始值,第一个实参初始化第一个形参,第二个实参初始化第二个形参。实参的类型必须与对象的形参匹配。
即使两个形参的类型一样,也必须把两个类型都写出来。
任意两个形参都不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。

函数的形参列表可以为空,但是不能省略即需要加上括号。也可以使用关键字void表示函数没有形参
void f1(){} 隐性的定义空形参列表
void f2(void){} 显性地定义空形参列表
函数的返回值不能是数组类型或者函数类型,但是可以是指向数组或者函数的指针

形参和函数体内部定义的变量统称为局部变量,局部变量还会隐藏在外层作用域中同名的其他所有声明。
只存在于块执行期间的对象称为自动对象。

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

作用域:形参的作用域为整个函数体;而普通(非静态)局部变量和静态局部变量的作用域为:从定义处到包含该变量定义的块的结束处。
初始化:形参由调用函数时所传递的实参初始化;而普通(非静态)局部变量和静态局部变量通常用初始化式进行初始化,且均在程序执行流程第一次经过该对象的定义语句时惊醒初始化。静态局部变量的初始化在整个程序执行过程中只进行一次。
生命周期:形参和普通(非静态)局部变量均属于自动变量,在每次调用函数时创建,并在函数结束时撤销;静态局部变量的生命期跨越了函数的多次调用,它在创建后直到程序结束时才撤销。

函数的声明不包含函数体,所以无需形参的名字。
函数的三要素为:返回类型、函数名、形参类型。它们描述了函数的接口,说明了调用该函数所需要的全部信息,函数声明也称为函数原型。

void print(vector<int>::const_iterator beg,vector<int>::const_iterator end);

6.2 参数传递

形参的类型决定了形参和实参的交互方式。
如果形参是引用类型,它将绑定到对应的实参上;否则将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。当实参的值被拷贝后赋给形参时,形参和实参是两个相互独立的对象,我们称这样的实参被值传递或者函数被传值调用。

当初始化一个非引用类型的变量时,初始值被拷贝给变量,对变量的改变不会影响初始值。即当值传递的时候,对形参的改变不会影响实参。
当执行指针拷贝操作的时候,拷贝的是指针的值即指向对象的地址,拷贝之后,两个指针是不同的指针。

指针形参
void reset(int *ip)
{
	*ip=0;//改变了ip所指对象的值
	ip=0;//仅仅改变了ip的局部拷贝,实参未被改变
}
int i=42;
reset(&i);
cout<<i;//i=0
引用形参
void reset(int &ip)
{
	ip=0;//改变了所引对象的值

}
int i=42;
reset(i);
cout<<i;//i=0

使用引用避免拷贝
如果需要返回多个值的时候,最好使用引用传参
当形参是const时要注意const是否是顶层的。顶层的const作用于对象本身
当形参有顶层const时,传给它常量对象或者是非常量对象都是可以的。
C++允许我们重载函数,但是形参必须有明显的区别,在实参初始化形参时会忽略掉顶层const,因此例如const int i和int i作为形参是不能同时出现的。

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

int i=42;
const int *cp=&i;//正确,cp不能改变i
const int &r=i;//正确,r不能改变i
const int &r2=42;//正确
int *p=cp;//错误
int &r3=r;//错误
int &r4=42;//错误

形参传递遵循同样的规律。
普通引用也不能绑定const对象,C++允许用字面值初始化常量引用
当不改变实参的值的时候尽量使用常量引用

#ifndef FIRST_TRY
#define FIRST_TRY
#include <string>
using namespace std;

bool StringUpper(const string& );
void String_xiaoxie(string&);

inline bool
StringUpper(const string& s)
{
	for (auto i : s)
	{
		if (i >= 'A' && i <= 'Z')
			return false;
	}
	return true;
}
inline void
String_xiaoxie(string& s)
{
	for (auto& i : s)
		i = tolower(i);
}
#endif // !FIRST_TRY


string s = "Hello WorlD!";
	cout<<StringUpper(s)<<endl;
	String_xiaoxie(s);
	cout << s << endl;

尽管形式不同,一下三个print函数是等价的
void print(const int*);
void print(const int[]);
void print(const int[10]);//这里的维度表示我们期望数组含有多少元素,实际上不一定。
每个函数的唯一形参都是const int*
例如:int j[2]={0,1}; print(j);此时会将j转化为int*并指向j[0]

由于函数一开始不知道数组的确切大小,因此可以通过三种方法说明数组大小。
1.使用标记指定数组长度,如让数组指针向后移一位即在最后一个字符后面跟着一个空字符。这种方法是用于那些有明显结束标记且标记不会与普通数据混淆的情况。
如空字符情况,函数遇到空字符就停止。

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

2.使用标准库规范,传递指向数组首元素和尾后元素的指针。

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

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

void print(const int ia[],size_t size)
{	for(size_t i=0;i!=size;++i){
		cout<<ia[i]<<endl;
}
}

当函数不需要对数组元素执行写操作的时候,数组的形参应该是指向const的指针。
形参可以是数组的引用,写法为int (&arr)[10];

传递多维数组的时候,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个指向数组的指针。
因此传递形式如下所示
void print(int (*matrix[10]);

main的参数列表为
int main(int argc,char*argv[]){…}
int main(int argc,char **argv){…}
以上两者意义相同。

initializer_list形参,如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用Initializer_list类型的形参,它用于表示某种特定类型的值的数组,例如

void error_msg(initializer_list<string> il)
{	for(auto beg=il.begin();beg!=il.end();++beg){
		cout<<*beg<<"   ";
	cout<<endl;
}
}
if(expected!=actual)//两个变量都是string对象
	error_msg({"functionx",excepted,actual)l;
else
	error_msg({"functionx","Equal!");
initializer_list<T> lst;
initializer_list<T> lst{a,b,c,d,e.....};
lst2(lst);//拷贝或者赋值一个initializer_list对象不会拷贝列表中的元素。拷贝后,原始列表和副本共享元素
lst.size();
lst.begin();
lst.end();

和vector一样,initializer_list也是一种模板类型,但是它里面的元素永远是常量值,无法改变。

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。大多数类型的对象在传递给省略符形参时都无法正常拷贝。
省略符形参只能出现在形参列表的最后一个位置。

6.3 返回类型和return语句

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
只有void返回类型的函数才没有返回值。void函数如果想在其中间位置退出可以使用return语句。这种用法类似于使用break语句退出循环

编译器尽量确保具有返回值的函数只能通过一条有效的return语句退出。含有return语句的循环之后必须提供return语句否则可能尚未返回任何值就结束了函数的执行,必须有return语句处理这种情况。
返回值不能是局部对象的引用或指针,因为当函数结束时临时对象占用的空间就随之释放了

返回值是引用,返回的是左值。
函数可以返回花括号包围的值的列表
例如如果返回类型是vector类型,可以return {};
main函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败

可以通过递归实现阶乘

inline int&
get(int* arry, int index) {
	return arry[index];
}

int ia[10];
	for (int i = 0; i != 10; ++i)
	{
		get(ia, i) = i;
		cout << ia[i] << "  ";
	}

数组作为函数参数传递,实际上传递了一个指向数组的指针,在c编译器中,当数组名作为函数参数时,在函数体内数组名自动退化为指针

函数可以返回数组的指针或引用,最直接的方法是使用类型别别名

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

如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字后面。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度,于是返回数组指针的函数形式如下所示:
TYPE (*function(形参))[ 维度]
TYPE表示元素的类型,维度表示数组的大小。

int (*func(int i))[10];

func(int i)代表需要一个int类型的实参,(*func(int i))表示可以对函数调用的结果进行解引用, (*func(int i))[10]表示解引用的结果式一个大小为10 的数组。

任何函数的定义都能用尾置返回,这种形式对于返回值比较复杂的函数最有效。例如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->符号开头

auto func(int i)->int(*)[10];//此时把函数的返回类型放在形参列表之后,我们在本该出现返回类型的地方放一个auto

使用decltype,例如

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

使用decltype表示返回类型是一个指针且指针所指对象与odd保持一致。由于odd是数组,所以arrPtr返回一个指向含有6个整数的数组的指针。
decltype并不负责把数组类型转换成对应类型的指针,所以decltype的结果仍然是一个数组,需要声明返回的是指针还必须在函数声明时加一个符号

6.4函数重载

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

void print(const char*cp);
void print(const int *beg,const int *end);
void print(const int ia[],size_t size);

对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。
不允许两个函数除了返回类型其他要素都相同。

顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有const的形参区分开来

Record lookup(Phone);
Record lookup(const Phone);重复声明!!

Record lookup(Phone*);
Record lookup(Phone* const);重复声明!!

如果形参是某种类型的指针或引用,通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时const是底层的

Record lookup(Accont&);//作用于Account引用
Record lookup(const Accont&);//作用于常量引用

Record lookup(Accont*);//作用于指向Account的指针
Record lookup(const Accont*);//作用于指向常量的指针

由于非常量可以转换成const,所以上面4个函数都能作用域非常量对象或者指向非常量对象的指针,但是当我们传递非常量的时候会优先选择非常量版本的函数。

const_cast可以将非常量类型的引用强制转换成const的引用,也可以将其转换回一个普通的引用。

当调用重载函数的时候,编译器首先将调用的实参与重载集合中的每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。 这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫重载确定。

如果我们在内层作用域声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。即使其形参类型不一样,外层作用域的重名函数也会被隐藏。

6.5 特殊用途语言特性

本节介绍默认实参,内联函数和constexpr函数
默认实参
默认实参即在声明函数时作为形参的初始值出现在形参列表中。
一旦某个形参被赋予了默认值,它后面所有的形参必须有默认值。

函数调用实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。
char类型可以转换成十进制,例如’?'在机器的十六进制为0x3F,即十进制64。
多次声明同一个函数是合法的,在给定作用域中一个形参只能被赋予一次默认实参

string screen(sz,sz,char=' ');
string screem(sz,sz,char='*');此时出错,因为重复声明
string screen(sz=30,sz=80,char);正确,声明添加了默认实参

局部变量不能作为默认实参,只要表达式的类型可以转换成形参所需类型,该表达式就能作为默认实参。
内联函数和constexpr函数
调用函数一般比求等价表达式的值要慢一些。
内联函数可避免函数调用的开销。将函数指定为内联函数(inline),通常就是将它在每个调用点上’内联地‘展开。
内联机制是用于优化规模较小、流程直接、频繁调用的函数。

constexpr函数是指能用于常量表达式的函数。其函数的返回类型及所有形参的类型都需要是字面值类型,而且函数体中必须有且只有一条return语句。

constexpr int new_sz(){return 42;}
constexpr int foo=new_sz();//foo是一个常量表达式

constexpr函数被隐式的指定为内联函数。
允许constexpr函数的返回值并非是一个常量,若传入实参是常量表达式则返回值也是常量表达式,反之则不然。

//scale是constexpr函数
int i=scale(2);//常量表达式
int j=2;
int m=scale(j);//不是常量表达式,会报错!!!!

调试帮助
我们用于调试的代码在准备发布时需要屏蔽掉调试代码,可以用上两项预处理办法:assert和NDEBUG

assert是一种预处理宏。
预处理宏就是一个预处理变量,他的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:
assert(expr);首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果为真,则什么也不做
和预处理变量一样,宏名字在程序内必须唯一。
assert常用于检查不能发生的条件。
assert不能代替真正运行时的逻辑检查,也不能替代程序本身应该包含的错误检查。

NDEBUG预处理变量如果被定义了,则assert什么也不做。默认情况下没有定义NDEBUG,此时assert将执行运行时检查。

__func__ 存放当前调试函数的名字
__FILE__ 存放文件名的字符串字面值
__TIME__ 存放文件编译时间的字符串字面值
__DATE__ 存放文件编译日期的字符串字面值
__LINE__ 存放当前行号的整型字面值

6.6 函数匹配

函数匹配的条件是
每个实参的匹配都不劣于其他可行函数需要的匹配
至少有一个实参的匹配优于其他可行函数提供的匹配
如果每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断优劣,编译器会因为存在二义性而拒绝其请求

如果重载函数的区别在于他们的引用类型的形参是否引用了const或者指针类型的形参是否指向const,编译器会通过实参是否是常量来决定调用哪个函数。

一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来,但是,我们可以通过底层const来进行函数重载。

6.7 函数指针

想要声明一个可以指向该函数的指针,只需要用指针替换函数名即可;

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

当我们把函数名作为一个值用,该函数自动转换成指针。
pf=lengthCompare ; pf=&lengthCompare二者是等价的

下面三者是等价的
b1=pf("hello","goodbye");
b2=*pf("hello","goodbye");
b3=lengCompare("hello","goodbye")

定义函数指针的时候,可以为函数指针分配nullptr或者值为0的常量变道时,表示该指针没有指向任何一个函数。
但如果需要指向函数,需要和它声明的返回类型、形参类型和数目匹配。

形参可以是指向函数的指针。

void B(const string &,const string &,bool (*pf)(const string &,const string &));*pf可以换成pf,等价

B(s1,s2,lengthCompare)

可以把函数直接作为实参使用,他会自动转换成指向该函数的指针

//func和func2是函数类型
typedef bool func(const string &,const string &);
typedef decltype(lengthCompare) func2;
//func3和func4是指向函数的指针
typedef bool (*func3)(const string &,const string &);
typedef decltype(lengthCompare) *func4;

想要声明一个返回函数指针的函数,最简单的方法是使用类型别名

using F=int(int*,int);F是函数类型
using PF=int(*)(int*,int);PF是指针类型

返回类型不会自动的转换成指针,需要显式的把返回类型指定为指针。

PF f1(int);
F *(int);
int (*f1(int))(int*,int);
三者等价

由内而外分析f1,f1有形参列表,f1是一个函数;f1前面有*,所以f1返回一个指针,指针的类型本身也包含形参列表,因此指针指向函数,该函数返回类型是int。

也可以用尾置返回类型的方式定义

auto f1(int)->int(*)(int*,int);

decltype需要显式的加上*表明我们需要返回指针,而非函数本身。

第七章 类

类的基本思想是数据抽象和封装。
数据抽象是一个依赖于接口和实现分离的编程技术。
类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

类需要首先定义一个抽象数据类型才能首先数据抽象和封装。

7.1定义抽象数据类型

定义在类内部的函数时隐式的inline函数
成员函数体可以定义在类内也可以定义在类外。

成员函数通过对一个名为this的额外的隐式参数来访问调用它的哪个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化this。

不能在一个const对象上调用普通的成员函数。
C++允许把const关键字放在成员函数的参数列表后面,以表示this是一个指向常量的指针,像这样使用const的成员函数被称作常量成员函数
常量成员函数不能改变调用它的对象的内容。
常量对象和常量对象的引用或指针都只能调用常量成员函数

编译器分两步处理类:首先编译成员的声明,然后编译成员函数体
调用一个返回类型为引用的得到左值,其他返回类型得到右值

class Person {
public:
	string get_name() const { return name; }
	string get_address() const { return address; }
	string name;
	string address;
};
iostream &read(iostream& is,Person& w){
	is >> w.name >> w.address;
	return is;
}
ostream& print(ostream& os, const Person& w) {
	os << w.name << "  " << w.address;
	return os;
}

IO类属于不允许被拷贝的类型,因此只能通过引用来传递它们

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员。
一个类可以拥有多个构造函数,不同的构造函数之间必须在参数数量或者参数类型上有所区别。

默认构造函数只适合非常简单的类,编译器只要在发现类不包含任何构造函数的情况下才会生成默认构造函数。
含有内置类型或者复合类型成员的类应该在类内部初始化这些成员,否则在创建类的对象的时候就可能得到未定义的值。

如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,必须自定义默认构造函数。

Person()=default

表示该构造函数不接受任何实参,它是一个默认构造函数

class Person {
public:
	Person()=default;
	Person(const string &s):name(s){}
	Person(const string &s,const string &t):name(s),address(t){}
	Person(iostream &s);
	string get_name() const { return name; }
	string get_address() const { return address; }
	string name;
	string address;
};
Person::Person(iostream &s)
{
	read(is,*this);
}

iostream &read(iostream& is,Person& w){
	is >> w.name >> w.address;
	return is;
}
ostream& print(ostream& os, const Person& w) {
	os << w.name << "  " << w.address;
	return os;
}

没有出现在函数初始值列表中的成员将通过相应的类内初始值初始化,或者执行默认初始化。

类还需要控制拷贝、赋值和销毁对象时发生的行为。
如果不主动定义这些操作,则编译器将替我们合成他们。一般来说,编译器生成的版本将对象的每个成员执行拷贝、赋值和销毁操作。但对于合成的版本无法正常工作

7.2 访问控制与封装

在C++中,我们使用访问说明符加强类的封装性。
public说明符之后的成员在整个程序内可以被访问,public成员定义类的接口。
private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节
每个访问说明符指定了接下来的成员的访问级别,其有效范围知道出现下一个访问说明符或者到达类的结尾处为止。

struct和class的唯一区别是他们的默认访问权限不一样
类可以在第一个访问说明符之前定义成员。struct定义在第一个访问说明符之前的成员是public的,而使用class这些成员是private。

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。友元只需要增加一条以friend开始的函数声明数据即可。
友元声明只能出现在类定义的内部。
友元的声明仅仅指定了访问的权限,如果希望类的某个用户可以调用,那就必须专门对函数进行一次声明

class Person {

public:
	string get_name() const { return name; }
	string get_address() const { return address; }
private:
	friend iostream& read(iostream&, Person&);
	friend ostream& print(ostream& os, const Person& w);
	string name;
	string address;
};
iostream &read(iostream& is,Person& w){
	is >> w.name >> w.address;
	return is;
}
ostream& print(ostream& os, const Person& w) {
	os << w.name << "  " << w.address;
	return os;
}

7.3 类的其他特性

这些特性包括:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、如何定义并使用类类型及友元类。
最好只在类外部定义的地方说明inline,这样使得类更容易理解。

只需要函数之间在参数的数量和/或类型上有所区别,成员函数也可以重载。
一个可变数据成员永远不会是const,即使它是const对象的成员。

类内初始值必须使用=的初始化形式或者花括号括起来的直接初始化形式

#include<string>
#include<vector>
using namespace std;
class Screen;
class Window_mgr;
class Screen {
public:
	typedef string::size_type pos;
	Screen() = default;
	Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht* wd, c) {}
	Screen(pos ht,pos wd): height(ht),width(wd),contents(ht*wd,' '){}
	char get() const { return contents[cursor]; }
	inline char get(pos ht, pos wd) const;
	Screen& move(pos r, pos c);
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	string contents;
};

inline 
Screen& Screen::move(pos r, pos c) {
	pos row = r * width;
	cursor = row + c;
	return *this;
}
char Screen::get(pos r, pos c) const {
	pos row = r * width;//row是行的意思
	return contents[row + c];
}
class Window_mgr {
private:
	vector<Screen> screens{ Screen(24,80, ' ') };
};

如果返回this的成员函数返回类型不是引用,那么返回值将会是this 的副本
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

通过区分成员函数是否是const的,我们可以将其进行重载。
非常量版本的函数对于常量对象是不可用的
虽然可以在非常量对象上调用常量版本或者非常量版本,但显然此时非常量版本是更好的引用。

当非常量成员函数调用常量成员函数的时候,它的this指针将隐式地从指向非常量的指针转换成指向常量的指针。(常量指针无法转换成非常量指针)

class Screen {
public:
	typedef string::size_type pos;
	Screen() = default;
	Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht* wd, c) {}
	Screen(pos ht,pos wd): height(ht),width(wd),contents(ht*wd,' '){}
	char get() const { return contents[cursor]; }
	inline char get(pos ht, pos wd) const;
	Screen& move(pos r, pos c);
	Screen& set(char);
	Screen& set(pos, pos, char);
	Screen& display(ostream& os) { do_display(os); return *this; }
	Screen& display(ostream& os)const{ do_display(os); return *this;
	friend class Window_mgr;																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																																											`xxxxxxxxdfc}
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	string contents;
	void do_display(ostream& os)const { os << contents; }
};

inline 
Screen& Screen::move(pos r, pos c) {
	pos row = r * width;
	cursor = row + c;
	return *this;
}
char Screen::get(pos r, pos c) const {
	pos row = r * width;//row是行的意思
	return contents[row + c];
}
Screen& Screen::set(char c) {
	contents[cursor] = c;
	return *this;
}
Screen& Screen::set(pos r, pos col, char ch) {
	contents[r * width + col] = ch;
	return *this;
}

class Window_mgr {
private:
	vector<Screen> screens{ Screen(24,80, ' ') };
};

class Screen;这种声明叫做前向声明,在它声明之后定义之前是一个不完全类型。
不完全类型的使用:可以定义指向这种类型的指针或引用,也可以声明(但不能定义)以不完全类型为参数或者返回类型的函数

一旦一个类的名字出现后,他就被认为是声明过来,因此类允许包含指向它自身类型的引用或指针。

可以通过friend来声明类之间的友元关系
一旦指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
如果友元类有它自己的友元,这些友元也不能访问类的特权

每个类控制自己的友元类或者友元函数
同样每个类可以给其他类的成员函数指定为友元。

类和非成员函数的声明不是必须在它们的友元声明之前。
当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中是可见的。友元本身不一定真的声明在当前作用域中。
当我们想要调用某个函数,它必须是声明过的
友元声明的作用是影响访问权限,并不是真正意义上的声明

7.4 类的作用域

一个类就是一个作用域,在类的外部定义成员函数必须调试提供类名和函数名。
函数的返回类型需要出现在函数名之前,必须要明确返回类型是哪个类的成员

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

成员函数的定义只有当处理完类中的全部声明后才会处理成员函数的定义。
声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见
如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能再之后重新定义该名字。

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

成员函数中使用的名字查找方式如下:成员函数中——》类中——》成员函数定义之前的作用域
建议不要把成员名字作为参数或其他局部变量使用

7.5 构造函数再探

如果没有在构造函数的初始值列表中显式的初始化成员,则该成员将在构造函数体之前执行默认初始化
如果成员是const或者是引用,那么必须将其初始化。如果没有提供构造函数初始值的话将会引发错误。必须通过标准的构造函数初始化才能初始化它们,即必须使用构造函数初始值列表为这些函数提供初始值

成员的初始化顺序与它们在类中定义的出现顺序一样,如果是一个成员使用另一个成员初始化,顺序就很重要。

class x{
 int i;
 int j;
public:
	x(int val):i(val),j(i){}//如果使用val初始化j,用j初始化i,那么就会报错。 
}

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

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

#include <iostream>
#include <string>
using namespace std;
class X {
public:
    string s;
    X(istream& is = cin) {
        is >> s;
    };
    void print() {
        cout << s << endl;
    }
};
int main() {
    X x;
    x.print();
}

一个委托构造函数使用它所属的类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些或全部职责委托给了其他构造函数。
一个委托构造函数也有一个成员初始值的列表和一个函数体。
假如被委托的函数体包含有代码的话,将先执行这些代码,然后将控制权还给委托者的函数体。

可以通过加上explicit抑制构造函数的隐式转换
关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit的
explicit只能用于直接初始化,不能用于拷贝形式的初始化过程
可以进行显式转换来使用explicit构造函数

聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
聚合类的要求如下:
所有成员都是public的
没有定义任何构造函数
没有类内初始值
没有基类,也没有virtual函数

struct Data{
	int iaval;
	string s;
};

Data vall={0,'Anna'};

初始化的执行顺序必须与声明的顺序一致,也就是说,第一个成员的初始值要放在第一个,以此类推

数据成员都是字面值类型的聚合类是字面值常量类。
字面值常量类的要求如下:
数据成员必须都是字面值类型
类必须至少含有一个constexpr构造函数
如果数据成员含有类内初始值,则内置数据成员的初始值必须是一条常量表达式。
类必须使用析构函数的默认定义。

constexpr构造函数体一般来说是空的。

7.6 类的静态成员

通过在成员的声明之前加上关键字 static使得其与类关联在一起。和其他成员一样,静态成员可以是public或是private的。
类的静态成员被所有类的对象共享。

静态函数不包含this指针,不能被声明成const的。
在类的外部定义静态成员时,不能重复static关键字,该关键字只能出现在类内部的声明语句
必须在类的外部定义和初始化每个静态成员。
可以为静态成员提供const整数类型的类内初始值,静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式

即使一个常量静态数据成员在类的内部被初始化了,通常情况下也应该在类的外部定义一下该成员。

可以使用静态成员作为默认实参。非静态数据成员不能成为默认实参

第八章 IO库

概念上,设备类型和字符大小都不会影响我们要执行的IO操作。
标准库使我们能忽略这些不同类型的流之间的差异,我们通常可以将一个派生类对象当作其基类来使用。

IO对象是不能被拷贝的,所以也不能将形参或者返回类型设置为流类型。读写一个IO对象会改变其状态,传递和返回的引用不能是const的
将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。

IO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。
流对象的rdstate成员返回一个iostate值,对应流当前的状态。setstate操作将给定条件位置位,表示发生了对应错误。clear不接受参数的版本清楚所有错误标志位

std::istream &iofunction(std::istream &is)
{
	std::string s;
	while(is >> s)
		std::cout << s << std::endl;
	is.clear();
	return is;
}
#include <iostream>
#include <string>

std::istream &iofunction(std::istream &is)
{
	std::string s;
	while(is >> s)
		std::cout << s << std::endl;
	is.clear();
	return is;
}

int main()
{
	iofunction(std::cin);

	std::string s1;
	while(std::cin >> s1)
		std::cout << s1 << std::endl;

	return 0;
}

每个输出操作后,可以使用操作符unitbuf设置流的内部状态,来清空缓冲区。
flush刷新缓冲区,但不输出任何额外的字符,ends向缓冲区插入一个空字符,然后刷新缓冲区。

如果想在每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符,它告诉流在接下来的每次写操作之后都进行一次flush操作,而nounitbuf操作符则重置流,使得其恢复使用正常的系统管理的缓冲区刷新机制。
如果程序异常终止,输出缓冲区是不会被刷新的,当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。当调试一个已经崩溃的程序时候,需要确认那些你认为输出的数据确实已经刷新了。

任何试图从输入流读取数据的操作都会先刷新关联的输出流。

tie有两个重载的版本,一个版本不带参数,返回指向输出流的指针,如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。
tie第二个版本接受一个指向ostream&的指针,将直接关联到此ostream,即x.tie(&o)将流x关联到输出流o。

ostream *old_tie=cin.tie(nullptr);//此时old_tie指向当前关联到cin的流,然后cin不再与任何流关联

8.2文件输入输出

头文件fstream定义了三种类型来支持文件IO。ifstream用于从给定文件读取数据,ofstream用于向给定文件写入数据,以及fstream可以读写给定文件。

在要求使用基类型对象的地方,我们可以用基础类型的对象来替代。
为了将文件流关联到另一个文件,必须首先关闭已经关联的文件。

ifstream(ifile);
in.close();
in.open(ifile+"2");

当一个fstream对象离开其作用域的时候,与之关联的文件会自动关闭,在下一步循环中,input将会被再次创建。

每个流都有关联的文件模式,用来指出如何使用文件。
in以读方式打开,out以写方式打开,app每次写操作前均定位到文件末尾,ate打开文件后立即定位到文件末尾,trunc截断文件,binary以二进制方式进行IO。

只有当out也被设定的时才可以设定trunc模式。只要trunc未被设定,就可以设定app模式。
为了保留以out模式打开的文件的内容,我们必须同时指定app模式,或者同时指定in模式。

默认情况下,打开一个ostream时,文件的内容会被丢弃。阻止一个ofstream清空给定文件内容的方式是指定app模式或in模式。
ofstream out; out.open(“precious”,ofstream::app);

8.3 string流

istringstream读取数据,ostringstream写入数据,stringstream可读可写。

sstream strm;
sstream strm(s);
strm.str();返回strm所保存的string的拷贝
strm.str(s);将string s拷贝到strm中,返回void

第九章 顺序容器

9.1 顺序容器概述

vector  可变大小数组,支持快速随机访问
deque   双端队列
list    双向链表,只支持双向顺序访问
forward_list  单向链表,只支持单向顺序访问
array         固定大小数组,支持快速随机访问。不能添加和删除元素
string        与vector相似的容器,但专门用于保存字符

通常情况下,使用vector是最好的选择,除非你有很好的理由选择其他容器
如果程序有很多小元素且空间的额外开销很重要,不要使用链表
要求随机访问元素的容器应该选择vector或者deque

如果不确定使用那种容器,那么可以在程序中使用vector和list公共的操作:使用迭代器,不使用下标操作,避免随机访问。

9.2 容器库概览

顺序容器几乎可以保存任意类型的元素,我们在构造容器时不可以只传递给他一个元素数目参数。

vector<nodefault> v1(10,init);

reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator,不能修改元素的逆序迭代器
c.rbegin()和c.rend()返回指向c的尾元素和首元素之前位置的迭代器

所有迭代器都是通过解引用运算符来实现访问容器中的元素,标准库容器的所有迭代器都定义了递增运算符,从当前元素移动到下一个元素。
一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者尾元素之后的位置。它们通常被称为begin和end。这种元素范围是左闭合区间。

对一个反向迭代器执行++操作,会得到上一个元素
与const指针和引用类似,可以将一个普通的iterator转换成对应的const的iterator,但反之不行
auto it7=a.begin();仅当a是const 的时候,it7是const_iterator。
不管容器是什么,以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么。
容器的定义与初始化

C c;
C c1(c2);
C c1=c2;
C c{a,b,c};
C c={a,b,c};
C c(b,e);c初始化为迭代器b和e指定范围中的元素的拷贝
C seq(n);seq包含n个元素,这些元素进行了值初始化,此构造函数时explicit的
C seq(n,t);seq包含n个初始化为值t的元素

当传递迭代器参数来拷贝一个范围的时候,就不要求容器的类型是相同的了(vector、deque属于容器类型)
当将一个容器初始化为另一个容器的拷贝的时候,两个容器的容器类型和元素类型都必须相同

除了与关联容器相同的构造函数外,顺序容器(array除外)还提供了另一个构造函数,它接受一个容器大小和一个(可选的)元素初始值,如果不提供元素初始值,则标准库会创建一个值初始化器,如果元素类型没有默认构造函数,除了大小参数外,还需要指定一个显式的元素初始值。
只有顺序容器的构造函数才接受大小参数,关联容器并不支持。

标准库array的大小也是类型的一部分,当定义一个array时,除了指定元素类型,还要指定容器大小。
array<int,42>
不允许使用一个数组初始化另一个数组,array例外。
虽然不能对内置数组类型进行拷贝或者对象赋值操作,但是array无此限制

标准库array允许赋值,赋值号左右两边的运算对象必须具有相同的类型,array不支持assign,也不允许用花括号包围的值列表进行赋值
assign不适用于关联容器和array

a.assign(b,e);//将a中的元素替换为迭代器b和e所表示范围内的元素,b和e不能指向a中的元素
a.assign(il);//将a中的元素替换为初始化列表il中的元素
a.assign(n,t);//将a中的元素替换为n个值为t的元素

assign操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素

list<string> names;
vector<char *>oldstyle;
names=oldstyle;//错误!!!!
names.assign(oldstyle.cbegin(),oldstyle.cend());//正确!!!

assign的参数决定了容器中将有多少个元素以及它们的值都是什么。
传递给assign的迭代器不可以再指向调用assign的容器。

vector<string> svec1;
vector<string> svec2;
swap(svec1,svec2);

调用swap后,svec1将会包含24个元素,svec2将会包含10个string。
除了array外,交换两个容器内容的操作会很快,因为元素本身并未交换,swap只交换了两个容器的内部数据结构。
除了array外,swap不对任何元素进行拷贝,删除或插入操作,因此可以保证再常数时间内完成。
除了string外,指向容器的迭代器,引用和指针在swap操作之后都不会失效,它们仍指向那些元素,但是这些元素已经属于不同的容器了。例如iter是指向svec1[3]的string,swap之后它指向svec2[3]的元素。
但是对一个string 调用swap会导致迭代器、引用和指针失效。

对于array,swap操作后,迭代器,引用和指针绑定元素不变,但是元素值已经和另一个array中对于元素的值发生交换。

#include <iostream>
#include <string>
#include<list>
#include<vector>
using namespace std;
int main() {
	list<const char*> m = { "a","an","annn" };
	vector<string> n(m.cbegin(),m.cend());
	for (auto& s : n)
	{
		cout << s << ends;
	}
	
	return 0;
}

如果两个容器都不是另一个容器的前缀子序列,那么它们的比较结果取决于第一个不相等元素的比较。
只有当元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。

#include <iostream>
#include<vector>
using namespace std;
int main() {
	vector<int> v1 = { 1,2,3,4,5,6 };
	vector<int> v2 = { 1,2,3,4,5 };
	cout << (v1 == v2) << endl;
	vector<int>::iterator vt1, vt2;
	vt1 = v1.begin(); vt2 = v2.begin();
	int i = 1;
	while (vt1 != v1.end() || vt2 != v2.end())
	{
		cout << (*vt1 == *vt2) << "  迪" << i << endl;
		++vt1;
		++vt2;
		++i;
	}
	
	return 0;
}

9.3 顺序容器操作

除了array外,所有标准库容器都提供灵活的内存管理。在运行时可以动态添加或删除元素来改变容器大小。
向一个vector、string或者deque中插入元素会使得所有指向容器的迭代器、引用和指针失效。

除了array和forward_list之外,每个顺序容器都支持push_back操作。
当我们用一个对象来初始化容器的时候,或者将一个对象插入容器中时,实际上放入到容器中的是对象值得一个拷贝,而不是对象本身。
list、forward_list和deque容器还支持名为push_front的类似操作
insert操作允许在容器的任意位置插入0个或多个元素。vector、deque、list和string都支持insert成员。forward_list提供特殊版本的insert。
每个insert都接受一个迭代器作为其第一个参数,迭代器指出在容器的什么位置之前放置新元素。

slist.push_front("hello");
slist.insert(slist.begin(),"hello");两者等价

slist.insert(slist.end(),10,"Anna");//在末尾插入10个“Anna”的string元素
slist.insert(slist.end(),v.end()-2,v.end());//在末尾插入容器v的最后两个元素

如果我们传递给insert一对迭代器,它们不能指向添加元素的目标容器
接受元素个数或者范围的insert操作会返回指向第一个新加入元素的迭代器,如果范围为空,不插入任何元素,insert将会把第一个参数返回。

list<string> lst;
auto iter=lst.begin();
while(cin>>word)
	iter=lst.insert(iter,word);//相当于调用push_front
#include <iostream>
#include <string>
#include<list>
#include<vector>
using namespace std;
int main() {
	vector<int> v1 = { 1,2,3,4,5,6 };
	vector<int> v2 = { 7,8,9,10 };
	auto i = v1.begin();
	auto m = v1.insert(i, v2.begin(), v2.begin() + 2);
	cout << *m << endl;//7
	cout <<  *v1.begin() << endl;//7
	cout << *(v1.begin()+1) << endl;//8
	
	return 0;
}

新标准引入了emplace,emplace_front和emplace_back,这些操作是构造而不是拷贝元素
push和insert操作是将传递过来的元素类型的对象拷贝到容器中,调用emplace成员是将参数传递给元素类型的构造函数。
emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配

假设c是一个Sales_data的对象
c.emplace_back();调用c的默认构造函数
c.emplace(iter,"00000000");调用Sales_data(string)
c.emplace_front("999999",25,16.99);调用Sales_data(string,int,double)的构造函数
string m;
	list<string> v;
	while (cin>>m)
	{
		string s = m;
		v.push_back(s);
	}
	auto iter = v.begin();
	while (iter != v.end())
		cout << *iter++ << " ";
	return 0;

包括array在内的每个顺序容器都有一个front成员函数,而除了forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用。
调用front和back之前(或者解引用begin和end返回的迭代器)需要保证c非空,如果容器为空,if中的操作将会是未定义的。
at和下标操作只适合string,vector,deque和array
at(n)返回的是下表为n的元素的引用。如果下表越界,则抛出一个out_of_range的异常
访问成员函数返回的是引用

c.pop_back();//删除尾元素,c为空则行为未定义,返回void
c.pop_front();//删除首元素
c.erase(p);//删除迭代器p指定的元素,返回被删除元素之后的元素的迭代器
c.erase(b,e);//删除迭代器b和e所指定范围内的元素,返回删除元素之后元素的迭代器,如果范围相同则不删除
c.clear();//删除所有元素

forward_list有特殊版本的erase
forward_list不支持pop_back,vector和string不支持pop_front

forward_list并未定义insert、emplace和erase操作,而是定义了名为insert_after、emplace_after和erase_after操作,即如果参数是elem2的话会删除elem3的元素
forward_list还提供了before_begin返回一个首前迭代器,允许我们在首元素之前删除或添加元素

#include<forward_list>
using namespace std;
int main() {
	forward_list<int> ifo = { 1,2,3,4,5,6,7,8 };
	auto pre = ifo.before_begin();
	auto start = ifo.begin();
	while (start != ifo.end())
	{
		if (*start % 2 == 0)
		{
			pre = start;
			++start;
		}
		else
			start = ifo.erase_after(pre);
	}
	for (auto& s : ifo)
		cout << s << " ";
	return 0;
}

resize可以用于改变容器大小,array不支持resize

list<int> ilist(10,42);10个值为42int
ilist.resize(15);5个值为0的元素加到ilist后面
ilist.resize(5);删除10个元素
ilist.resize(25,-1);20个值为-1的元素加到ilist后面

resize缩小容器,那么指向被删除元素的迭代器,引用、指针都会失效

向容器添加元素后
如果容器是vector或者string,且存储空间被重新分配,则指向容器的迭代器指针和引用都会失效。若未被重新分配,那么只有插入位置之后的元素的迭代器、指针和引用会失效
对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。
对于list和forward_list,指向容器的迭代器、指针和引用仍然有效
删除一个元素
对于list和forward_list,都仍然有效
对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、指针和引用也会失效,删除尾元素只会导致尾后迭代器失效。
对于vector和string,指向被删除元素之前的元素迭代器、指针和引用仍然有效
当我们删除元素的时候,尾后迭代器总是会失效
必须保证每次改变容器的操作之后都正确的重新定位迭代器,不要保存end返回的迭代器

list和forward_list不支持迭代器复合运算,iter+=2需要改成iter++;iter++;

iter = vi.insert(iter, *iter++);
不合法。因为参数的求值顺序是未指定,无法保证括号内的计算顺序

9.4 vector对象是如何增长的

当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。
虽然vector在每次重新分配内存空间时都要移动所有元素,但是使用此策略后,其扩张操作通常比list和deque还要快

shrink_to_fit只适用于vector、string和deque
capacity和reserve只适用于vector和string
c.shrink_to_fit()请将capacity减少为与size相同的大小
c.capacity()不重新分配内存空间的话,c可以保留多少元素
c.reserve()分配至少能容纳n个元素的空间

reserve并不改变容器中元素的数量,它仅仅影响vector预先分配多大的内存空间
只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。
resize成员函数只改变容器中元素的数目,而不是容器的容量。

可以调用shrink_to_fit来退回不需要的内存空间。

容器的size是指它已经保存的元素的数目,而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素
只要没有操作需求超过vector的容量,vector就不能重新分配空间,即只有当迫不得已的时候才可以分配新的内存空间

9.5 额外的string操作

构造string的其他方法

string s(cp,n);//s时cp指向的数组中前n个字符的拷贝,此数组至少应该包含n个字符
string s(s2,pos2);//s是string s2从下表pos2开始的字符的拷贝。若pos2>s2.size(),则行为未定义
string s(s2,pos2,len2);//s是strig s2从下标pos2开始len2个字符的拷贝,若pos2>s2.size(),则行为未定义,之多拷贝s2.size()-pos2个字符

如果仅仅从一个const char*创建string时,指针指向的数组必须以空字符结束,拷贝操作遇到空字符时停止。如果传递给构造函数一个计数值,数组就不必以空字符结尾。
在这里插入图片描述

substr操作返回一个string,它是原始string的一部分或者全部。可以传递给substr一个可选的开始位置和计数值
如果开始位置超过string的大小,则抛出一个out-of-range的异常

	vector<char> b = { 'H','e','l','l','o',' ','w','o','r' ,'l','d'};
	string s(b.begin(),b.end());

可以使用assign替换string元素中的内容

s.insert(0,s2,0,s2.size());//在s[0]之前插入s2中s2[0]开始的s2.size()字符

string类中定义了两个额外的成员函数:append和replace,这两个函数可以改变string的内容。append操作实在string末尾进行插入操作的一种简写形式

s.insert(s.size(),"4th Ed");
s.apend("4th Ed");两者等价

replace是调用erase和insert的一种简写,调用replace的时候可以插入一个更长或更短的string
在这里插入图片描述
在这里插入图片描述
assign总是替换string中的所有内容,append总是将新字符追加到string末尾
replace可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定
当新的字符来自于一个string或者一个字符指针的时候,我们可以传递一个额外的参数来控制是拷贝部分或者全部字符

void strFunc(string& s,const string oldVal,const string newVal)
{
	int nSSize = s.length();
	int nOldLen = oldVal.length();
 
	if (nSSize <= 0 || nSSize < nOldLen)
	{
		return;
	}
	auto iters = s.begin();
	while (iters != s.end())
	{
		string str = s.substr(iters - s.begin(), nOldLen);
		if (str == oldVal)
		{
			s.erase(iters, iters + nOldLen);
			for (auto iterss = newVal.begin(); iterss != newVal.end(); iterss++)
			{
				s.insert(iters, *iterss);
				iters++;
			}
		}
		else
		{
			iters++;
		}
	}
}
 
void strFuncTwo(string& s, const string oldVal, const string newVal)
{
	int nSSize = s.length();
	int nOldLen = oldVal.length();
 
	if (nSSize <= 0 || nSSize < nOldLen)
	{
		return;
	}
 
	for (int i = 0; i < nSSize; i++)
	{
		string str = s.substr(i, nOldLen);
		if (str == oldVal)
		{
			s.replace(i, nOldLen, newVal);
			i = i + nOldLen;
		}
	}


int main(int argc, char** argv)
{
	string s("ingthohdufb");
	cout << "执行前" << s << endl;
	//strFunc(s, "tho", "through");
	strFuncTwo(s, "tho", "through");
	cout << "执行后" << s << endl;
 
	system("pause");
	return 0;
}

查找s中args第一次出现的位置:s.find(args)
在s中找args中任意一个字符第一次出现的位置:s.find_first_of(args)
在s中查找第一个不在args中的字符:s.find_first_not_of(args)

逆向搜索:
查找s中args最后一次出现的位置:s.rfind(args)
在s中找args中任意一个字符最后一次出现的位置:s.find_last_of(args)
在s中查找最后一个不在args中的字符:s.find_last_not_of(args)

args可以是什么呢:

c, pos : 从s中位置pos开始查找字符c。pos默认为0
s2,pos : 从s中位置pos开始查找字符串s2。pos默认为0
cp,pos : 从s中位置pos开始查找指针cp指向的以空字符结尾的c风格字符串。pos默认0
cp,pos,n:从s中位置pos开始查找指针cp指向的数组的前n个字符

void find_int(string& s) {
	string n("0123456789");
	decltype(s.size()) k = 0;
	while (k != s.size()) {
		auto pos = s.find_first_of(n, k);
		cout << s[pos] << "   ";
		++k;
	}
}
void find_not_int(string& s) {
	string n("0123456789");
	decltype(s.size()) k = 0;
	while (k != s.size()) {
		auto pos = s.find_first_not_of(n, k);
		cout << s[pos] << "   ";
		++k;
	}
}

int main() {
	
	string s("ab2c3d7R4E6");
	find_int(s);
	cout << endl;
	find_not_int(s);
	return 0;
}

compare成员函数用于比较字符串。
to_string(i)将整数i转换成字符表示形式
stod(s)将字符串s转换成浮点数
如果string不能转换成数值,则会抛出一个异常。

int main(int argc, char** argv)
{
	vector<string> strVec = { "1","2","3","4","5" };
	int sum = 0;
	for (auto i = strVec.begin(); i != strVec.end(); i++)
	{
		int a = stoi(*i);
		sum = sum + a;
	}
	cout << "和为:" << sum << endl;
 
	float fSum = 0.0f;
	for (auto i = strVec.begin(); i != strVec.end(); i++)
	{
		float a = stof(*i);
		fSum = fSum + a;
	}
	cout << "和为:" << fSum << endl;
 
	system("pause");
	return 0;
}

#include<iostream>
#include<fstream>
#include<sstream>
#include<string>
#include<vector>
#include<forward_list>
using namespace std;
 
class Date
{
public://class默认是私有继承,记得要加public
	unsigned _year;
	unsigned _month;
	unsigned _day;
	void _show()
	{
		cout<<_year<<"年"<<_month<<"月"<<_day<<"日"<<endl;
	}
	//构造函数
	Date(string);
};
 
Date::Date(string s)
{	
	int flag = 0;
	string number = "0123456789/";
	string coma = ",";
	string month;	
	unsigned pos,pos1,pos2,pos3;	
	unsigned _pos,_pos1;
 
	/*利用一个判断,现判定怎样初始化*/
	if ((pos = s.find_first_not_of(number)) == string::npos)//没找到不属于number的数
	{
		flag = 1;
	}
	if ((pos = s.find_first_of(coma)) != string::npos)//找到,
	{
		flag = 2;
	}
 
	switch (flag)
	{
	case 1:/*处理1/1/1991的格式*/
		pos1 = 0;
		pos1 = s.find_first_of("/",pos1);
		_day = stoul(s.substr(0,pos1));//先截取目标字符串,再将字符串转化为unsigned
		pos2 = ++pos1;
		pos1 = s.find_first_of("/",pos1);//找到第二个/
		_month = stoul(s.substr(pos2,pos1));
		pos3 = ++pos1;
		_year = stoul(s.substr(pos3,s.size()-1));
		break;
	case 2:/*处理January 1,1900的格式*/
		_pos;
		_pos = s.find_first_of(number);
		month = s.substr(0,_pos);
		//本来想用switch,表达式的结果的类型可以是 整数类型,枚举类型,或者类类型
		//(但该类需要有单一的转换到整数类型或(可以是字符类型,但不能是浮点类型、字符串、指针类型等)
		if (month == "January ") _month = 1;
		if (month == "February ") _month = 2;
		if (month == "March ") _month = 3;
		if (month == "April ") _month = 4;
		if (month == "May ") _month = 5;
		if (month == "June ") _month = 6;
		if (month == "July ") _month = 7;
		if (month == "August ") _month = 8;
		if (month == "September ") _month = 9;
		if (month == "October ") _month = 10;
		if (month == "November ") _month = 11;
		if (month == "December ") _month = 12;
 
		_pos1 = ++_pos;
		_pos = s.find_first_of(number,_pos);
		_day = stoul(s.substr(_pos1-1,_pos));
 
		_year = stoul(s.substr(_pos,s.size()-1));
		break;
	case 0:/*处理Jan 1 1995的格式*/
		_pos;
		_pos = s.find_first_of(number);
		month = s.substr(0,_pos);
		if (month == "Jan ") _month = 1;
		if (month == "Feb ") _month = 2;
		if (month == "Mar ") _month = 3;
		if (month == "Apr ") _month = 4;
		if (month == "May ") _month = 5;
		if (month == "Jun ") _month = 6;
		if (month == "Jul ") _month = 7;
		if (month == "Aug ") _month = 8;
		if (month == "Sep ") _month = 9;
		if (month == "Oct ") _month = 10;
		if (month == "Nov ") _month = 11;
		if (month == "Dec ") _month = 12;
 
		_pos1 = ++_pos;
		_pos = s.find_first_of(number,_pos);
		_day = stoul(s.substr(_pos1-1,_pos));
 
		_year = stoul(s.substr(_pos,s.size()-1));
		break;
	}	
}
 
int main(int argc, char**argv)
{
	Date _today("25/2/2017");
	_today._show();
 
	Date _tomorrow("January 1,1995");
	_tomorrow._show();
 
	Date _2tomorrow("Jan 1 1995");
	_2tomorrow._show();
	
	return 0;
}

9.6 容器适配器

三个顺序容器适配器:stack,queue,priority_queue
适配器即基于其他容器构造的容器类型

一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
每个适配器都有两个构造函数:默认构造函数创建一个空对象
接受一个容器的构造函数拷贝该容器来初始化适配器。

deque<int> deq;
stack<int> stk(deq);

默认情况下,stack和queue是基于deque实现的,priority_queue是基于vector实现的

可以创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型

stack<string,vector<string>> str;//在vector实现的空栈
stack<string,vector<string>> str(svec);//初始化时保存svec的拷贝

所有适配器都要求容器具有添加和删除元素以及访问尾元素的能力,所以适配器不能构造在array之上,也不能用forward_list来构造适配器

queue是先进先出的适配器,栈stack是先进后出的适配器
priority_queue允许为对列的元素建立优先级,优先级高的元素会排在优先级低的元素之前

第十章 泛型算法

10.1 概述

这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。例如调用标准库算法find查找vector中是否有特定值

int val=42;
auto result=find(vec.cbegin(),vec.cend(),val);

int ia[]={........};
int *result=find(begin(ia),end(ia),val);//获取指向ia中首元素和尾元素的指针并传递给find中的参数
auto result=find(ia+1,ia+4,val);

虽然迭代器的使用令算法不依赖于容器类型,但大多数算法都使用了一个或多个元素类型上的操作

vector<int> mm;
	int x;
	while (cin >> x) {
		mm.push_back(x);
	}
	cout<<count(mm.cbegin(), mm.cend(), 6);
	
list<string> ss;
	string ssr;
	while (cin >> ssr ) {
		ss.push_back(ssr);
	}
	cout<<count(ss.cbegin(), ss.cend(), "qq");

算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素的值,也可能在容器内移动元素,但永远不会直接添加或者删除元素。

10.2 初识泛型算法

只读算法
对于只读算法,最好使用cbegin()和cend()
一些算法指挥读取器输入范围内的元素,从而不改变元素,例如find算法。
accumulate也是只读算法,它用于计算范围内的元素的和

int sum=accumulate(vec.cbegin(),vec.cend(),0);

0代表求和的初值设置为0
accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型
第三个参数的元素类型必须可以与容器内的元素类型相加,如字符串字面值就不能与string元素相加

另一个只读算法是equal,用于确定两个序列是否保持相同的值,此算法接受三个参数,前两个表示第一个序列容器的元素范围,第三个表示的是第二个容器的首元素。
相比较的元素类型也不必相同,只要可以通过==来标记两个元素类型即可。
如果全部元素相同,equal会返回true,否则返回false。(equal是假定第二容器和第一容器一样长的

只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少于第一个序列一样长,如果第二个序列小于第一个序列,则会试图访问第二个序列末尾之后不存在的元素

	vector<int> m = { 1,2,3,4,5,6,7,8,9 };
	int sum = accumulate(m.cbegin(), m.cend(), 0);
	cout << sum<<endl;
	vector<double> c = {1.2,3.6,7,9,6.5,1,2,3,4.3};
	double sum1 = accumulate(c.cbegin(), c.cend(), double(0));
	cout << sum1 << endl;

如果容器元素是double,但是accumulate第三个参数是int,那么结果是int类型的。

写容器元素的算法
算法本身不可能改变元素大小。
例如算法fill接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill将给定的这个值赋予输入序列中的每个元素

fill(vec.begin(),vec.end(),0);

fill(vec.begin(),vec.begin()+vec.size()/2,10);

操作两个序列的算法之间的区别在于我们如何传递第二个序列,有的使用单一迭代器表示,有的是用两个迭代器表示元素的范围。

sort可以重新排列,排序完毕后可以使用unique重排输入范围使得每个单词只出现一遍,unique返回的是指向不重复区域之后一个位置的迭代器。
sort接受两个迭代器,表示要排序的元素范围。
标准库算法都是对迭代器而不是对容器进行操作,算法不能直接进行添加或者删除元素

void elimDups(vector<string>& s) {
	sort(s.begin(), s.end());
	auto m = unique(s.begin(), s.end());
	s.erase(m,s.end());
	for (auto& mm : s) {
		cout << mm << "  ";
	}
}

10.3定制操作

**谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。**标准库算法所使用的谓词分为两类:一元谓词(只接受单一参数)和二元谓词(意味着它们有两个参数)。

接受二元谓词参数的sort版本用这个谓词来代替<比较元素
例如

bool isshort(const string s1,const string s2)
{
	return s1.size()<s2.size();
}
sort(s.begin(),s.end(),isshort);

为了保持相同长度的单词按字典序排列,可以使用stable_sort算法,这种稳定排序算法维持相等元素的原有顺序

void elimDups(vector<string>& s) {
	sort(s.begin(), s.end(),isshorter);
	auto m = unique(s.begin(), s.end());
	s.erase(m,s.end());
	for (auto& mm : s) {
		cout << mm << "  ";
	}
}
bool length_five(const string& s) {
	return s.size() < 5;
}


int main() {

	vector<string> s = { "the","mm","gg","the","fuck","what","you","mm" };
	elimDups(s);
	stable_sort(s.begin(), s.end(), isshorter);
	auto m=partition(s.begin(), s.end(), length_five);
	while (m != s.end()) {
		cout << *m << "  ";
		++m;
	}

10.3附 lambda表达式

我们可以向算法传递任何类别的可调用对象。对于一个对象或者变道时,如果可以对齐使用调用运算符,则称它为可调用的。
例如,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。

四种可调用对象分别是:函数,函数指针,重载了函数调用运算符的类以及lambda表达式
与任何函数类似,一个lambda具有一个返回类型,一个参数列表和一个函数体,但与函数不同,lambda可能定义在函数内部。

lambda表达式
[capture list](parameter list)->return type{function body}

capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空)
lambda必须使用尾置类型来指定返回类型
lambda可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
例如

auto f=[]{return 42;}//f为lambda表达式
cout<<f()<<endl;

lambda调用方式和普通函数调用方式相同,都是使用调用运算符。
忽略参数列表等于指定一个空参数列表,忽略返回类型则会根据函数体中的代码推断出返回类型。
如果函数体只是一个return语句,则返回类型从返回的表达式类型推断而来。否则返回类型为void

一个lambda调用的实参数目永远与形参数目相等,不存在默认参数。空捕获列表表示lambda不适用它所在函数的任何局部变量

[](const string& s,const string& m){return s.size()<m.size();}

bool isshort(const string s1,const string s2)
{
	return s1.size()<s2.size();
}
stable_sort(s.begin(), s.end(), isshorter);
stable_sort(s.begin(), s.end(), [](const string& s,const string& m){return s.size()<m.size();});//两者等价

虽然lambda可以出现在一个函数中使用其局部变量,但是它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获变量指引lambda在其内部包含访问局部变量所需的信息。

虽然一个lambda可以出现在一个函数中使用其局部变量,但它只能使用那些明确指明的变量。
一个lambda通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需信息。

[sz](const string&a){return a.size()>=sz;}

for_each算法可接受一个可调用对象,并对输入序列中每个元素调用此对象。

for_each(wc,words.end(),[](const string &s){cout<<s<<" ";});//打印wc到end之间的元素,每个元素中间有一个空格

捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字

void test_lambda_int(const int i,const int j) {
	auto f = [i, j] {return i + j; };
	int sum = f();
	cout << sum;
}

当定义一个lambda时,编译器会生成一个与lambda对应的新的类类型。
当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。
lambda捕获变量的方式分为值或者引用。
局部变量用什么方式捕获取决于捕获列表里是值还是引用。

捕获iostream对象的唯一方式就是捕获其引用

当我们向一个函数传递一个lambda时,lambda会立即执行。我们也可以从一个函数返回lambda,函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。
当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的

捕获一个普通变量,通常可以采用值捕获方式。
如果捕获指针或迭代器,或者采用引用捕获方式,就必须确保在lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。

当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或者=。此符号制定了默认捕获方式为引用或值
显示捕获必须使用与隐式捕获不同的方式

如果希望改变一个被捕获变量的值,必须要在参数列表首加上关键字mutable

[]()mutable{return 1;};

transform接受三个迭代器和一个可调用对象,第三个迭代器表示目的位置,前两个表示输入序列。transform将输入序列都写到目的位置,目的位置可以与输入序列开始位置相同

bind函数类似于一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。

auto newC=bind(c,arg_list);

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

bool check_size(const string&s,string::size_type sz){
	return s.size()>=sz;
}

auto check6=bind(check_size,_1,6);
bool b1=check6(s);相当于调用check_size(s,6);

_1是占位符,此bind只有一个占位符,表示check6只接受单一参数即_1位置的参数。

名字_n都定义在一个名为placeholders的命名空间中,对于每一个占位符名字,我们都必须提供一个单独的using声明,但这种方式容易出错,可以使用另外一种不同形式的using语句。

using std::placeholders::_1;

using namespace std::placeholders;
auto g=bind(f,a,b,_2,c,_1);

g(X,Y);
f(a,b,Y,c,X);

不能直接用bind来代替对os的捕获,如果希望传递给bind一个对象而又不拷贝他,那么必须使用标准库ref函数。
ref函数返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库还有一个cref函数,生成一个保存const引用的类。

	string s = "hello";
	auto g = bind(check_size, s, _1);
	vector<int> test_bind_vector = { 1,2,3,4,5,6,7,8,9 };
	bool test_b_b = true;
	for (int& i : test_bind_vector) {
		test_b_b = g(i);
		if (test_b_b == false)
		{
			cout << i << "  ";
			break;
		}
	}

10.4 再探迭代器

迭代器包括插入迭代器、流迭代器、反向迭代器、移动迭代器。

插入迭代器
插入迭代器接受一个容器,生成一个迭代器,能实现向给定容器添加元素。·

插入迭代器分为back_inserter、front_inserter和inserter。
back_inserter迭代器使用push_back功能
front_inserter使用push_front功能
这两个迭代器只接受一个容器作为参数
inserter接受两个参数,第一个参数是插入的容器,第二个参数是该容器的迭代器。

iostream迭代器
istream_iterator读取输入流
ostream_iterator向一个输出流写数据

当创建一个流迭代器时,必须指定迭代器将要读写的对象类型

istream_iterator<int> int_it(cin);//从cin读取int
istream_iterator<int> int_eof;//尾后迭代器
ifstream in("afile);
istream_iterator<string> str_it(in);//从afile读取字符串

istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while(in_iter!=eof)
	vec.push_back(*in_iter++);
//此循环从cin读取int值,保持在vec中,循环体检查in——iter是否等于eof
//eof被定义为空的istream_iterator,从而可以当做尾后迭代器使用

对于一个绑定到流的迭代器,一旦其关联的流遇到文件尾或者遇到IO错误,迭代器的值与尾后迭代器相等

istream_iterator<T> in(is);in从输入流is读取类型为T的值

in->men;//相等于(*in).mem

++in;in++//使用元素类型所定义的>>运算符从输入流中读取下一个值。与以往一样,前置版本返回一个指向递增后迭代器的引用,后置版本返回旧值

使用算法操作流迭代器

istream_iterator<int> in(cin),eof;
cout<<accumulate(in,eof,0);//accumulate接受三个参数,头两个形参指定容器范围,第三个形参指定累加的初始值
//例如,输入1,2,3,4,5,6.则输出为21

istream 允许使用懒惰求值
当我们将一个istream_iterator绑定到一个流时,标准库并不保证迭代器立即从流读取数据。
具体实现可以推迟从流中读取数据,直到我们使用迭代器时才真正读取

void test_istream_iterator() {
	istream_iterator<int> in(cin), eof;
	cout << "Please cin some numbers";
	cout << "cin>>1"<<endl;
	int i;
	cin >> i;
	cout << accumulate(in, eof, 0) << endl;
}
//初始创建的时候读取一个数,之后使用istream迭代器的时候再次读取

ostream_iterator<T> out(os);//out将类型为T的值写到输出流os
ostream_iterator<T> out(os,d);//out将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符结尾的字符数组
out=val;//用<<运算符将val写入到out所绑定的ostream中。val的类型必须与out可写的类型兼容
*out、out++++out//这些运算符不对out做任何事情,每个运算符都返回out

我们可以用ostream_iterator来输出值的序列:

ostream_iterator<int> out_iter(cout," ");
for(auto e:vec){
	*out_iter++=e;//实际上将值写到cout,等同于out_iter=e;
}

此程序将vec中的每个元素写道cout,每个元素后面加一个空格。每次向out_iter赋值时,写操作就会被提交
运算符*和++实际上对ostream迭代器不做任何事情,因此忽略它们对我们的程序没有任何影响,但他们的用法与其他迭代器类似,便于修改。

void test_vector_istream() {
	vector<string> s;
	istream_iterator<string> s_is(cin),eof;
	while(s_is!=eof)
		s.push_back(*s_is++);
	for (auto m : s) {
		cout << m << "  ";
	}
}

void test_is_os_sort_copy() {
	istream_iterator<int> in(cin), eof;
	ostream_iterator<int> out(cout," ");
	vector<int> inn;
	while (in != eof)
		inn.push_back(*in++);
	sort(inn.begin(),inn.end());
	copy(inn.cbegin(),inn.cend(), out);
}

反向迭代器
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增以及递减的操作含义会颠倒过来
除了forward_list,其他容器都支持反向迭代器。我们可以通过调用rbegin,rend,crbegin,crend来获得反向迭代器。

line=(FIRST,SECOND,LAST);

auto comma=find(line.cbegin(),lin.cend(),',');
cout<<string(line.cbegin(),comma)<<endl;//将为输出line的开始到遇到第一个逗号之前的字符

auto rcomma=find(line.crbegin(),lin.crend(),',');
cout<<string(line.crbegin(),comma)<<endl;//将会输出TSAL
//如果想要正常输出LAST需要将rcomma转换成一个普通的迭代器,能在line中正向移动,可以通过反向迭代器的base成员函数来完成这一转换
cout<<string(rcomma.base(),line.cend())<<endl;//左闭右开区间,comma指向,   rcomma指向,之后的字符

10.5 泛型算法结构

迭代器分为五种
输入迭代器:只读,不写;单遍扫描,只能递增
输出迭代器:只写,不读;单遍扫描,只能递增
前向迭代器:可读写;多遍扫描,只能递增
双向迭代器:可读写;多遍扫描,可递增递减
随机访问迭代器:可读写,多遍扫描,支持全部迭代器运算

_if版本的算法接受一个谓词代替元素值

find(beg,end,val);//找第一个值为val的位置
find_if(beg,end,preg);//找第一个使得preg为真的元素

默认情况下,重排元素的算法将重排后的元素写回给定的输入序列中,写到额外空间的算法都在名字后面加一个_copy

10.6特定容器算法

链表类型定义了几个成员函数形式的算法。

lst.merge(lst2)//将来自lst2的元素合并入lst,两个链表都必须是有序的,使用默认的<进行合并,合并后lst2的元素删除
lst.merge(lst2,comp)

lst.remove(val)
lst.remove_if(pred)

lst.reverse()//反转元素

lst.sort()
lst.sort(comp)//使用<或者给定比较操作排序元素

lst.unique()
lst.unique(pred)调用erase删除同一个值的连续拷贝。第一个版本使用==,第二个版本使用给定的二元谓词

splice成员是链表结构特有的,有splice函数和splice_after函数

lst.splice参数如下,p是指向lst元素的迭代器或者是指向flst首前位置的肚饿带去,函数将lst2的所有元素移动到p前位置或者移动flst中p后位置。lst2中元素会被删除

(p.lst2)
(p,lst2,p2)
(p,lst2,b,e)

第11章 关联容器

两个主要的关联容器时map和set。
map中的元素是一些key-value对。
set每个元素只包含一个关键字,即key和value是同一个值
按关键字有序保存元素
map:保存键值对
set:关键字即值,只保存关键字
multimap:关键字可重复的map
multiset:关键字可重复的set
无序集合
unordered_map:用哈希函数组织的map
unordered_set:用哈希函数组织的set
unordered_multimap:哈希组织的map:关键字可以重复出现
unordered_multiset:哈希组织的set:关键字可以重复出现

11.1 使用关联容器

使用map

void test_map() {
	map<string, size_t> map_string;
	string word;
	while (cin>>word)
	{
		++map_string[word];
	}
	for (const auto& m : map_string) {
		cout << m.first << "occurs" << m.second << (m.second > 1 ? "times" : "time") << endl;
	}
}

使用set

void test_set() {
	map<string, size_t> map_string;
	string word;
	set<string> set_string = { "The","the","word","words" };//保存需要忽略的元素
	while (cin >> word) {
		if (set_string.find(word) == set_string.end())
			++map_string[word];
	}
	for (const auto& m : map_string) {
		cout << m.first << "  occurs  " << m.second << (m.second > 1 ? "  times  " : "  time  ") << endl;
	}
}

11.2 关联容器概述

关联容器不支持顺序容器的位置相关的操作,例如push_back或者push_front。
关联容器的迭代器都是双向的
定义关联容器
每个关联容器都定义了一个默认的构造函数,它创建一个指定类型的空容器

如果利用花括号初始化map容器的话,必须提供键值对即{key,value}的形式
一个map或者set的关键字必须是唯一的,对于给定的一个关键字,只能有一个元素的关键字与其相等。
multimap和multiset都允许多个元素具有相同的关键字。

void map_family() {
	map<string, vector<string>> family;
	cout << "Please enter the family's name" << endl;
	string family_name;
	while (cin >> family_name) {
		cout << "Please enter the children name" << endl;
		string child_name; 
		while (cin >> child_name)
		{
			if (child_name == "Z")
				break;
			family[family_name].push_back(child_name);
		}cout << "Please enter the family's name" << endl;

	}
	for (const auto& m : family) {
		cout << m.first << "      ";
		for (auto& s : m.second) {
			cout << s << "   ";
		}
	}
}

**关键字类型的要求 **
传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求
可以提供自定义的操作代替关键字的<运算符进行有序运算,但所提供的自定义操作必须定义一个严格弱序。

为了使用自定义的操作,定义multiset必须提供两个类型:关键字类型和比较操作类型(函数指针类型)

multiset(Sales_data,decltype(conpareIsbn)*>  bookstore(compareIsbn);//bookstore中的元素将按它们的ISBN成员的值排序

pair类型
一个pair保存两个数据成员。当创建一个pair的时候,我们必须提供两个类型名,pair的数据成员将具有对应的类型。
pair的数据成员是public的,两个成员分别命名为first和second

pair<T1,t2> p;
pair<t1,t2> p(v1,v2);
pair<t1,t2>p={v1,v2};
make_pair(v1,v2);//返回一个v1,v2初始化的pair,pair的类型由v1,v2类型决定
p1 relop p2 //当p1.first<p2.firsr或者(p1.first<p2.first)&&p1.second<p2.second成立时,p1<p2为true

	void writein_stringint_vector() {
	string s;
	int i;
	vector<pair<string, int>> result;
	cout << "Please enter number"<<endl;
	while (cin >> s) {
		cout << "Please enter int" << endl;
		cin >> i;
		result.push_back(make_pair(s, i));
		cout << "Please enter number" << endl;
	}
	for (const auto& m : result) {
		cout << m.first << "   " << m.second << endl;
	}

}

11.3 关联容器操作

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值