C3P阅读笔记

第一部分

第二章——变量和基本类型

char类型及其扩展

类型 含义 最小尺寸
char 字符 8bit
wchar_t 宽字符 16bit
char16_t Unicode字符 16bit
char32_t Unicode字符 32bit

wchar_t确保可以存放机器最大扩展字符集中的任一个字符,char16_t和char32_t为Unicode字符集服务

类型转换

如果赋予一个取值区间之外的值,如unsigned char(8bit)表示0-255,赋予-2,则-2对256取模后的余数,实际结果为254(数据类型所占位数而定)

字面值常量

默认的,整形字面值数据类型是带符号数,即int;浮点型默认数据类型为double,因为float精度不够且双精度浮点数计算代价与单精度差不多;字符串字面值结尾处会多一个空字符\0,字符串字面值的实际长度要比它的内容多1;若两个字符串字面值位置紧邻且仅由空格、缩进和换行符分割时,实际上是一个整体

转义序列

1
\n 换行符		\t 横向制表符		\v 纵向制表符		\b 退格符		\r 回车符(清空当前行并回到行首)

如果反斜线后的八进制数字超过三个,只有前三个与反斜线构成转义序列;如果跟着十六进制数字,则所有十六进制数字都参与构成转义序列

指定字面值的类型

前缀/后缀 含义 类型
u Unicode16字符 char16_t
U Unicode32字符 char32_t
L 宽字符 wchar_t
u8 UTF-8 char
整型后缀
u/U 无符号型 unsigned
l/L long
ll/LL long long
浮点型后缀
f/F float
l/L long double

整型字面值不能添加浮点型后缀F

变量

如果使用列表初始化且初始值存在信息丢失的风险,则会报错,如int a = {3.14}

  1. 在任何函数体之外的变量会被初始化为0
  2. 在函数体内部的内置型变量不会被初始化,其未定义,不可被访问
  3. 类不初始化则由类自己定义

变量声明规定了变量的类型和名字,定义则为其开辟空间

变量声明:在变量名前添加关键字 extern,如extern int i;。但如果包含了初始值,就变成了定义:extern double pi = 3.14;

变量只能被定义一次,但是可以多次声明。定义只出现在一个文件中,其他文件使用该变量时需要对其声明。

名字的作用域

同时存在全局和局部变量时,已定义局部变量的作用域中可用::reused显式访问全局变量reused。

引用和指针

引用类型的初始值必须是一个对象(不能是字面值),必须在定义时给引用赋初值

指针本身就是一个对象,可以不赋初值

引用自加——引用的对象加一

指针自加——指针指向下一个类型,即向后偏移一个类型的大小

void*指针

可以存放任意对象的地址,但不能访问对象

  1. 与其它指针比较
  2. 当做函数的输入或输出
  3. 赋值给另一个void*指针

指向指针的引用

int i = 0;

int* b = &i;

int * & a = b;

i是整型变量,b是指向整型变量i的整型指针,a是指向整型指针b的整型引用

*a = 1;通过引用对整型变量进行访问和修改

const限定符

const对象一旦创建就不能被修改,因此在定义时必须赋初值

const int ci = 0; int i = ci; const的常量特征只在被修改时显示,因此可以用常量值赋初值

const的引用

即为对常量的引用,且无法对引用对象进行修改(常量引用)

const int ci = 30; const int & cr = ci

cr为对ci的常量引用

常量引用初始化时能够用字面值赋值,而引用不能,且能够用任意表达式进行初始化

const引用的也可以不是常量,因为引用对象本身不是常量,即时不能通过引用修改对象,但能通过其他途径来修改。

常量指针和指针常量

const int ci = 30; int const* cptr =&ci;cptr为指向常量的int指针,且因为其是指针,可以不用初始化

常量指针指向的也可以不是常量,只是不能通过指针改变对象的值,但仍可以通过其他途径修改。底层const

const int ci = 30; const int *const ptrc = &ci;ptrc为指针常量,是一个指向整型常量的const int指针常量

int i = 30; int *const ptri = &i;prti为指针常量,是一个指向整型字面量的int指针常量。顶层const

相同地,指针常量指向的也可以不是常量,只是不能更改指针常量本身的指向,但仍可以通过其他途径修改对象的值

常量表达式

声明为constexpr的变量一定是一个常量(顶层const),且必须用常量表达式初始化。一个constexpr指针的初始值必须为0或者nullptr或者是存在某个固定地址的对象;定义于所有函数体之外的对象其地址固定不变,此时能用于初始化constexpr指针。

constexpr int *q = nullptr则q为指向整数的指针常量

处理类型

using SI = Sales_item; 类型别名

typedef char *pstring; pstringchar*类型,指向char类型的指针

const pstring cstr = 0; cstr为指向char的指针常量,注意与下一个式子的区别

const char *cstr = 0; cstr为指向char的常量指针

decltype返回表达式结果或变量对应的类型;如果表达式的内容是解引用操作,则decltype将得到引用类型;如果表达式或者变量加多一层括号,则也将得到引用类型

const int ci = 0;decltype(ci) x = 0则x的类型为const int

int i = 30; decltype(*p) c = i则p的类型为引用类型

decltype ((i)) d = i则d的类型也为引用类型

预处理器

  • #indef已定义时为真
  • #inndef未定义时为真
  • 头文件保护符的名称需要唯一,且保持全部大写。养成良好习惯,不论是否该头文件被包含,要加保护符。

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

string对象

如果提供一个字符串字面值给string对象初始化,则该字面值中除了最后的空字符其他所有字符都被拷贝到string对象

  • string io:

    • string word; cin>>word忽略掉开头的空白(包括空格、换行符和制表符),直到遇到下一处空白为止
    • string word; while(cin>>word)读取到文件结束符或非法输入时停止 Ctrl+Z
    • getline:读取一整行,包括空白符,直到换行符,但不会将换行符存到string对象中
    • string line; while(getline(cin,line))
  • s.size()返回的时string::size_type类型,记住是一个无符号类型的值,不要和int混用

    1
    2
    3
    4
    5
    6
    void check (size_type i, const string &msg) const
    {
    if(i>=data->size())
    throw out_of_range(msg);
    }
    当i小于0,会自动转换成正整数,即i = i + UINT_MAX;
  • s1+s2使用时,保证至少一侧是string类型。string s1 = "hello" + "world" // 错误,两侧均为字符串字面值

cctype头文件中的标准函数

函数 解释
isalnum(c) c是字母或数字时为真
isalpha(c) c是字母时为真
iscntrl(c) c是控制字符时为真
isdigit(c) c是数字时为真
isgraph(c) c不是空格但可以打印时为真
islower(c) c是小写字母时为真
isprint(c) c是可打印字符时为真
ispunct(c) c是标点符号时为真
isspace(c) c是空白时为真(空格、横向制表符、纵向制表符、回车符、换行符、进纸符)
isupper(c) c是大写字母时为真
isxdigit(c) c是十六进制数字时为真
tolower(c) c是大写字母,输出对应的小写字母;否则原样输出c
toupper(c) c是小写字母,输出对应的大写字母;否则原样输出c

vector对象

1
2
vector<int> v1(10);		//v1中有10个元素,默认值为0
vector<int> v2{10}; //v2中有1个元素,值为10

如果初始化用的是圆括号,则提供的值是用来构造vector对象;

如果初始化用的是花括号,则表示列表初始化vector对象

1
vector<string> v3{10};		//v3中有10个元素,默认值为空

当初始化用了花括号的形式,但提供的值又无法列表初始化,则编译器会尝试用默认值初始化vector对象

因为范围for循环语句内不应改变其所遍历序列的大小,因此不能用范围for循环向vector添加元素

size返回值的类型是由vector定义的size_type类型即vector<int>::size_type

用下标访问vector,下标类型应是size_type类型,可以通过auto或者decltype获取

1
2
for(decltype(ivec.size()) index=0;index<=10;index++)
ivec.push_back(ix);
### vector迭代器

*iter.mem等同于iter->mem

vector<int>::iterator it1; //it1能够读写元素

vector<int>::const_iterator it2; //it2只能读元素,不能写

两个迭代器之间的运算只有减法运算,返回值类型为difference_type

difference_type返回带符号整数,所得结果是右侧迭代器向前移动能追上左侧迭代器的距离

迭代器与整数值之间有加法运算

数组

数组的大小确定不变,不能随意向数组中增加元素

数组声明时,其中维度必须是常量表达式

1
2
3
4
5
unsigned cnt = 30;
constexpr unsigned sz = 30;
int arr[30]; //正确
int arr[cnt]; //报错,cnt不是常量表达式
int ve[sz]; //正确

字符数组在用字符串字面值初始化时,会自动添加表示字符串结束的空字符

不允许数组拷贝初始化,也不允许用数组给其他数组赋值

1
2
3
int a[] = {0, 1, 2};
int a2[] = a; //报错,不允许数组拷贝初始化
a2 = a; //报错,不允许用数组赋值

数组下标类型为size_t(头文件cstddef),而vector和string的下标类型为size_type

复杂数组

1
2
3
int *ptrs[10];
int (*parray)[10]=&arr;
int (&array)[10]=arr;

ptrs是一个数组,包含了10个整型指针——指针数组

parray是一个指针,指向一个有10个整型元素的数组——数组指针

array是一个引用,引用对象是一个有10个整型元素的数组

int *(&array)[10]=ptrs则是一个引用,引用对象是一个有10个整型指针的数组

指针和数组

数组名就是数组首元素的指针

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

auto可以推断数组名得到指针类型

1
2
3
int ia[] = {0,1,2,3,4};
auto ia2(ia); //ia2是一个指针,指向数组ia首元素
auto ia3(&ia[0]); //同上

但当用decltype时则不一样

1
decltype(ia) ia4;		//获取到的是ia的数组类型,即ia4是一个整型数组

数组的库函数

1
2
3
4
#include<iterator>
int ia[] = {0,1,2,3,4};
int *beg = begin(ia); //beg指向首元素
int *last = end(ia); //last指向尾元素

当两个数组指针相减时,返回的类型为ptrdiff_t;vector和string返回的类型为difference_type,都是一种带符号类型

数组下标可以是负数,而vector的下标只能是正整数

1
2
int *p = &ia[2];			//p指向数组第三个元素
int j = p[-2]; //指向数组ia的首元素

C风格的字符串(不推荐使用)

会在字符串最后一个字符后面跟着一个空字符(\0)

1
2
const char ca[] = {'k','e','v','\0'};
const char ca1[] = "hello";

C标准库String函数,定义在<cstring> 中:

函数 介绍
strlen(p) 返回p的长度,空字符不计算在内
strcmp(p1, p2) 比较p1p2的相等性。如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值。
strcat(p1, p2) p2附加到p1之后,返回p1
strcpy(p1, p2) p2拷贝给p1,返回p1

C风格字符串不支持运算符操作,如+,-,==,因为操作的是指针而非字符串本身,并无意义

与旧代码的接口

允许使用string对象给C风格字符串初始化

1
2
string s = "hello!";
const char *str = s.c_str(); //c_str()返回一个C风格的字符串,即一个指针,并在数组最后加上空字符结束

使用数组给vector初始化

1
2
int ia[] = {1,2,3,4,5};
vector<int> vec(begin(ia),end(ia));

多维数组

数组的数组,由内而外的顺序理解

使用范围for循环访问多维数组时,外层循环需要使用引用类型,这是为了在内层循环数组被自动转成指针

1
2
3
4
5
6
7
8
9
10
11
int ia[] = {1,2,3,4,5};
for(const auto &row: ia)
{
for(auto col:row)
cout << col << endl;
}

// 如果不加引用
for(auto row: ia)
for(auto col:row)
//此时row转成指针,类型为int* 因此在内层循环时就会报错

利用别名代替,使多维数组降维访问

using int_array int[4];

typedef int int_array[4];

编写3个不同版本的程序,令其均能输出ia的元素。
版本1使用范围for语句管理迭代过程;版本2和版本3都使用普通for语句,其中版本2要求使用下标运算符,版本3要求使用指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
using std::cout; using std::endl;

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

//范围for
for(auto& p:arr)
for(auto q:p)
cout << q << endl;
//范围for
for(const int (&p)[4]:arr)
for(int q:p)
cout << q << endl;

//下标for
for(size_t i = 0; i < arr.size(); i++)
{
for(size_t j = 0; j<arr[0].size(); j++)
cout << arr[i][j] << endl;
}

//指针for
for(int *p = begin(arr); p! = end(arr); p++)
{
for(int *q = begin(*p);q!= end(*p);q++)
cout << *q << endl;
}
//指针for
for(int(*p)[4]=arr;p < arr.size();p++)
for(int *q = *p;q < arr[0].size();q++)
cout << *q <<endl;
}

第四章——表达式

左值和右值

当一个对象被用作左值时,使用的是对象的身份(在内存中的位置);

当一个对象被用作右值时,使用的是对象的值

解引用符返回的结果是左值

取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值

1
2
decltype(*p);		//返回引用类型int&
decltype(&p); //返回指针类型int**,即指向整型指针的指针

表达式的结果是左值,decltype得到引用类型;表达式的结果是右值,decltype得到指针类型

算数运算符

运算对象和运算结果都是右值

1
2
bool b = true;
bool b2 = -b; //b2依然为true

参与取余运算的运算对象都为整数类型

(-m)/n,m/(-n)结果都为-(m/n)

m%(-n)结果为m%n(-m)%n结果为-(m%n)

逻辑和关系运算符运算对象和运算结果都是右值

递增递减运算符

前置递增运算符将对象本身作为左值返回,后置递增则将对象原始值的副本作为右值返回。后置需要将原始值存储下来以便返回这个未修改的内容,如果不需要修改前的值,应直接使用前置递增

1
2
3
auto iter = vi.begin();
while (iter!=vi.end()&&*iter>=0)
cout<<*iter++<<endl; // 输出当前值,指针向前移1

后置递增运算符高于解引用运算符

1
2
vec[ival++]<=vec[ival];		//表达式未定义,应改为
vec[ival]<=vec[ival+1];

不应在运算符左右两边都用到一个变量,并且右侧还改变其值

1
2
while(beg!=s.end() && !isspace(*beg))
*beg = toupper(*beg++); //未定义

假设iter的类型是vector::iterator, 说明下面的表达式是否合法。如果合法,表达式的含义是什么?如果不合法,错在何处?

1
2
3
4
5
6
(a) *iter++;
(b) (*iter)++;
(c) *iter.empty();
(d) iter->empty();
(e) ++*iter;
(f) iter++->empty();

解:

  • (a)合法。返回迭代器所指向的元素,然后迭代器递增。
  • (b)不合法。因为vector元素类型是string,没有++操作。
  • (c)不合法。这里应该加括号。
  • (d)合法。判断迭代器当前的元素是否为空。
  • (e)不合法。string类型没有++操作。
  • (f)合法。判断迭代器当前元素是否为空,然后迭代器递增。

sizeof运算符

  • 对引用类型执行sizeof运算得到引用对象的类型大小
  • 对解引用指针类型执行sizeof运算得到指针指向对象的类型大小,指针可以不有效
  • 对数组执行sizeof运算得到整个数组所占空间的大小,而不会把数组转换成指针
  • 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,与对象中元素个数或所占空间大小无关

通过查看STL源码可以看到vector有四个成员变量
_A allocator;

iterator _First, _Last, _End;

所以sizeof(vec)会返回4个字节即16bit

类型转换

隐式转换

  • 常见的char、bool、short能存在int就会转换成int,否则提升为unsigned int

  • wchar_t,char16_t,char32_t提升为整型中int,long,long long ……最小的,且能容纳原类型所有可能值的类型

  • 当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeoftypeid等运算符的运算对象时,转换不发生

显示转换

  • static_cast:任何明确定义的类型转换,只要不包含底层const,都可以使用。 double slope = static_cast<double>(j);

  • dynamic_cast:支持运行时类型识别

  • const_cast:只能改变运算对象的底层const,一般可用于去除const性质。 const char *pc; char *p = const_cast<char*>(pc)只有其可以改变常量属性,通常用在函数重载

  • reinterpret_cast:通常为运算对象的位模式提供低层次上的重新解释,实际上仍为原类型

第五章——语句

悬垂else

有多个if时,else只与离它最近的尚未匹配的if匹配或者在想要匹配的if语句下加上花括号,以控制执行路径

switch语句

case关键字和它对应的值一起被称为case标签,其必须是整型常量表达式

如果某个case标签匹配成功,将从改标签开始往后顺序执行所有case分支,因此需要添加break语句

若以一个空的default标签作为结束,则必须跟上一个空语句或者空块

for语句

1
2
for(init-statement;condition;expression)
statement;

在init-statement中可以定义多个对象,但只能有一条声明语句,即所有变量的基础类型都必须相同

1
2
for(decltype(v.size() ) i=0,sz = v.size(); i != sz; ++i)
v.push_back(v[i]);

for语句也可以省略掉任意一部分,但要以一条空语句代替,即要保留分号

if(cin.eof())可以读取eof文件结束符,可以用Ctrl+z代替

异常处理

异常处理包括:

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

try语句块内声明的变量在块外部无法访问,在catch子句内也无法访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while(cin >> item1 >> item2){
try{
if(item1.isbn()!=item2.isbn())
throw runtime_error("Data must refer to same ISBN");
//如果没有抛出异常,则表示两个isbn是相同的
cout << item1 + item2 << endl;
}catch(runtime_error err){
//异常处理代码,提醒用户异常,询问是否继续
cout << err.what() << endl
<< "Try again? Enter y or n" << endl;
char c;
cin >> c;
if(!cin || c == 'n')
break;
}
}

处理程序中,err.what()的返回值是C风格字符串,返回初始化runtime_error对象时的string对象的副本

如果异常抛出,但最终没有找到任何匹配的catch语句,程序转到名为terminate的标准库函数,一般将导致程序非正常退出

头文件定义的异常类

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

对于exceptionbad_allocbad_cast只能默认初始化而不允许提供初始值

第六章——函数

指针形参

当执行指针拷贝操作时,拷贝的是指针的值,可以通过指针改变所指的对象的值,但不改变指针本身

编写一个函数,使用指针形参交换两个整数的值

1
2
3
4
5
6
7
8
9
10
11
12
13
void swap(int* p,int* q)
{
int tmp = *p;
*p = *q;
*q = tmp;
}
int main()
{
int a = 1, b = 2;
swap(&a,&b);
cout << a << " " << b << endl;
return 0;
}

传引用参数

  • 使用引用可以避免额外空间开销

  • 还可以利用引用形参返回额外信息

右值实参不能用作引用形参

常量形参

尽量使用常量引用,因为如果是普通引用如字符串,将无法接收字符字面值常量以及常量字符串

不能把const对象、结果为该类型的表达式、字面值或者需要类型转换的对象传递给普通的引用形参

数组形参

数组——不允许拷贝,不允许赋值给其他数组,使用数组时通常会将其转换成指针

数组形参只能以const int *类型传参,但可以有不同形式

1
2
3
void print(const int* pa);
void print(const int pa[]);
void print(const int pa[10]); //维度以实参为主

三种形式等价

管理数组指针形参

  1. 使用标记指定数组长度

    如C风格字符串,自带有空字符,遇到空字符时即停止,但数组没有明显标记

    void print(const char *cp)

  2. 使用标准库规范

    传递指向数组首元素和尾元素的指针,beginend函数

    void print(const int *beg,const int *end)

  3. 显示传递表示数组大小的形参

    void print(const int ia[],size_t size)

数组引用形参

形参可以是数组的引用,且维度是类型的一部分,但绑定了维度就意味着函数只能作用于固定维度的数组

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

多维数组形参

数组第二维以及后面所有维度的大小都是数组类型的一部分,不能省略

void print(int (*arr)[10])指针指向10个整数数组,等价于void print(int arr[][10])

void print(int *arr [10])则代表10个整型指针的数组

处理命令行选项

int main(int argc,char* argv[])等价于int main(int argc,char**argv)

其中第一个形参表示数组中字符串的数量,第二个形参表示数组,元素是指向C风格字符串的指针

argv中的实参0保存程序的名字,往后是用户输入的实参,最后一个指针之后的元素值保证是0

可变形参的函数

  • 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型,表示某种特定类型的值的数组

    initializer_list对象中的元素永远是常量值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    #include <initializer_list>

    int sum(std::initializer_list<int> const& il)
    {
    int sum = 0;
    for (auto i : il) sum += i;
    return sum;
    }

    int main(void)
    {
    auto il = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //可变长度
    std::cout << sum(il) << std::endl;
    return 0;
    }
  • 如果类型不相同,可以用可变参数模板

  • 省略符,一般只用于与C函数机交互的接口程序

函数返回类型

调用一个返回引用的函数得到左值,其他返回类型得到右值

函数返回类型不能是数组或者函数,但可以是指向数组或者函数的指针

返回数组指针

  • Type (*function (parameter_list))[dimension]int (*func(int i)[10])表明需要实参类型是int,函数调用结果返回的是指针类型,指针指向的是个大小为10的整型数组,
  • 使用类型别名: typedef int arrT[10]; 或者 using arrT = int[10;],然后 arrT* func() {...}
  • 使用 decltypeint odd[] = {1,2,3,4,5}; decltype(odd) *arrPtr(int i) {...}
  • 尾置返回类型: 在形参列表后面以一个->开始:auto func(int i) -> int(*)[10]

编写一个函数声明,使其返回数组的引用并且该数组包含10个string对象

1
2
3
4
5
6
7
8
9
string (&fun())[10];				//初始版本

typedef string str_arr[10]; //类型别名
str_arr& fun();

auto fun()->string(&)[10]; //尾置返回类型

string s[10]; //decltype关键字
decltype(s)& fun();

函数重载

不允许两个函数除了返回类型外其他都一样,这不是重载的函数

顶层const不影响传入函数的对象,因此不能用顶层const来进行函数重载,而底层const则可以

1
2
3
4
5
void add(const int*);		//形参为常量指针,是底层const,可以对函数重载
void add(int*); //函数重载为普通指针的形参

void add(int* const); //形参为指针常量,是顶层const
void add(int*); //等价于上式,重复声明函数

const形参重载可借助const_cast

1
2
3
4
5
6
7
8
9
10
const string &shorterString(const string &s1,const string& s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}

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

当普通引用传入时,调用第二个版本函数,将普通引用强制转换成const引用,再调用第一个版本函数,返回得到const引用绑定为一个引用上,再将其转换回普通引用返回

若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名

特殊用途语言特性

  • 默认实参

    string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');

    一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值

    局部变量不能作为默认实参

  • 内联函数

    可避免函数调用的开销

    inline const string &shorterString(const string &s1,const string& s2)

  • constexpr函数

    函数的返回类型及所有形参的类型都得是字面值类型,不一定返回常量表达式

  • 调试帮助

    • assert预处理宏

      assert(expr)如果expr为真则忽略,为假则输出信息并终止程序

      通常用于检查不能发生的条件

    • NDEBUG预处理变量

      CC -D NDEBUG main.c可以定义这个变量NDEBUG,等价于在main.c一开始写# define NDEBUG

      1
      2
      3
      4
      5
      6
      7
      8
      9
      void print(){
      #ifndef NDEBUG
      cerr << "Error:" << __FILE__
      << ": in funciton " << __func__
      << "at line " << __LINE__ << endl
      << " Compiled on " << __DATE__
      << "at " << __TIME__ << endl
      #endif
      }

      分别输出报错的文件名的字符串字面值、当前调试的函数名的字符串字面值、行号的整型字面值、编译日期和编译时间的字符串字面值

函数匹配

  • 重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
  • 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数
  • 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数
  • 寻找最佳匹配:实参转换等级:精确匹配-const转换的匹配-类型提升-算术类型转换-类类型转换

函数指针

函数名作为一个值使用,该函数自动转换成指针

声明一个指向函数的指针,需包括返回类型和形参类型

1
2
bool lengthCompare(const string &,const string&);			//函数声明
bool (*pf)(const string&, const string&); //函数指针,用指针替换函数名

使用函数指针时,解引用与不解引用指针等价。bool flag = pf("hh","kk");等价于bool flag = (*pf)("hh","kk");

把函数指针用作形参时,使用函数定义和使用函数指针定义等价

1
2
3
4
void useBigger(const string& s1,const string&s2, bool pf(const string&,const string&));
//形参为函数类型,将自动转换成函数指针类型
void useBigger(const string& s1,const string&s2, bool (*pf)(const string&,const string&));
//形参为显式定义函数指针

另外,可以用类型别名或者decltype简化需要用到函数指针的函数声明

函数指针当返回值

  1. 普通版本

    1
    int (*f(int))(int*,int);

    首先,这是一个函数指针,且函数形参为int类型,指针本身也有形参列表,所以该指针指向函数,函数返回类型为int

  2. 类型别名

    1
    2
    3
    4
    5
    using F = int(int* ,int);			//F是函数类型
    using PF = int(*)(int* ,int); //PF是函数指针类型

    PF f(int);
    F* f(int); //两种均等价于普通版本
  3. 尾置返回类型

    与返回数组指针类型类似

    1
    auto f(int) ->int(*)(int*,int);
  4. auto和decltype

    如果明确知道返回函数,可以使用decltype简化

    1
    decltype(f1)* f(int* ,int);			//f1即为需要返回的函数类型

第七章——类

类的基本思想是数据抽象封装

数据抽象依赖于接口实现 技术

定义在类内部的函数都是隐式的inline函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Sales_data{
//成员函数
std::string isbn() const {return bookNo;}
//返回书本的ISBN编号
Sales_data &combine(const Sales_data&);
//两个类对象属性相加,返回原对象
doule avg_price() const;
//返回平均价格

std::string bookNo;
//ISBN编号
unsigned units_sold = 0;
//数量
double revenue = 0;
//总收入
};
//声明非成员函数,即接口
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream& print(std::ostream& os,const Sales_data&);
std::istream& add(std::istream& is,Sales_data&);

this指针

成员函数通过this隐式参数来访问调用的对象,当调用成员函数时,隐式地先将类对象的地址初始化this

struct *const this,this是一个指针常量,指针指向不变,即一直保存对象的地址

成员函数

目的并非通用,属于类的实现部分

成员函数的声明需在类内,函数声明在其他成员之后,因此不用考虑函数体和其他成员的次序

默认情况下,this是指向非常量的指针常量,意味着不能将this绑定到常量对象上,则无法正常使用到该成员函数,因此应该在函数声明时,在函数形参列表后函数体前加上const,std::string isbn() const{return bookNo;}

1
2
3
4
5
6
7
double Sales_data::avg_price() const
{
if(units_sold)
return revenue/units_sold;
else
return 0;
}

返回this对象的函数

return *this语句返回调用该函数的对象,令函数能够被连续调用

1
2
3
4
5
6
7
8
9
10
Sales_data &Sales_data::combine(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

//调用代码
Sales_data sd1,sd2,sd3;
sd1.combine(sd2).combine(sd3);

非成员函数

属于类的接口的组成部分,非成员函数声明与类声明在同一个文件,但不在类内

1
2
3
4
5
6
7
8
//输入书本ISBN编号,数量以及单价
std::istream& read(std::istream& is,Sales_data& item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
1
2
3
4
5
6
7
8
//打印输出书本ISBN编号、数量、总销售额及均价
std::ostream& print(std::ostream& os, const Sale_data& item)
{
os << item.bookNo << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
//执行输出任务的函数尽量减少对格式控制,让用户代码决定是否换行
1
2
3
4
5
6
7
//add函数,输入两个类类型,返回一个新类的副本
Sales_data add(const Sales_data&item1,const Sales_data&item2)
{
Sales_data tmp = item1;
item1.combine(item2);
return tmp;
}

构造函数

  • 构造函数是特殊的成员函数
  • 无返回值
  • 构造函数放在类的public部分
  • 直到构造函数完成,对象才会有常量属性,因此构造函数可以向const对象写值
  • =default要求编译器合成默认的构造函数
  • 初始化列表:冒号和花括号之间的代码: Sales_item(): units_sold(0), revenue(0.0) { }

class和struct默认访问权限不同,struct则在第一个访问说明符之前的成员是public的,class则是private

友元

允许特定的非成员函数访问一个类的私有成员,通常将友元声明成组地放在类定义的开始或者结尾

必须在友元声明之外再对函数进行一次声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Sales_data{
friend Sales_data add(const Sales_data&,const Sales_data&);
friend std::istream& read(std::istream& is, Sales_data&);
friend std::ostream& print(std::ostream& os,const Sales_data&);
public:
Sales_data() = default;
Sales_data(const std::string &s,unsigned n,douple p):
boodNo(s),revenue(p),units_sold(n) {}
Sales_data(const std::string &s):bookNo(s) {}
Sales_data(std::istream&);//类外实现构造函数
//类内实现构造函数 Sales_data(std::istream& is) {return read(is,*this);}

std::string& isbn() const {return bookNo};
Sales_data& combine(const Sales_data&);
double& avg_price(const Sales_data&) const;
private:
std::string bookNo;
double revenue = 0.0;
unsigned units_sold = 0;
};
//类外接口函数声明
Sales_data add(const Sales_data&,const Sales_data&);
std::istream& read(std::istream& is, Sales_data&);
std::ostream& print(std::ostream& os,const Sales_data&);

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员

也可以只对声明另一个类中的某个成员函数为友元,并说明该成员函数的作用域

若想要把一组重载函数声明为友元,则需要对每一个分别声明

类的其他特性

用来定义类型的成员必须先定义后使用

1
2
3
4
5
6
7
8
class Screen{
public:
typedef std::string::size_type pos;
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
};

定义在类内部的成员函数都是自动inline的,可以在类内部显式inline声明,也能在类外部定义的时候说明inline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Screen{
public:
typedef std::string::size_type pos;
Screen() = default;//有另一个构造函数时,若需要默认构造函数,必须显式声明
Screen(pos ht,pos wd,char c):height(ht),width(wd),contents(ht*wd,c) {}

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;
std::string contents;
};
inline
char Screen::get(pos ht,pos wd) const //因为已经在类内声明过inline,此处可以省略
{
return contents[ht*width+wd];
}
inline
Screen &move(pos r, pos c)
{
cursor = r * width + c;
return *this;
}

若希望能修改类的const成员函数中的某个成员变量,可以通过变量声明前加入mutable实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Screen{
public:
typedef std::string::size_type pos;
Screen() = default;//有另一个构造函数时,若需要默认构造函数,必须显式声明
Screen(pos ht,pos wd,char c):height(ht),width(wd),contents(ht*wd,c) {}

char get() const {return contents[cursor];}
//获取光标当前的内容,隐式内联
inline char get(pos ht,pos wd) const;//显式内联,在类外定义
Screen &move(pos r,pos c);

void some_member() const;//修改mutable变量函数
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;

mutable size_t access_ctr;//被修改mutable变量
};
inline
char Screen::get(pos ht,pos wd) const //因为已经在类内声明过inline,此处可以省略
{
return contents[ht*width+wd];
}
inline
Screen &move(pos r, pos c)
{
cursor = r * width + c;
return *this;
}
void some_member() const
{
++access_ctr;//记录被调用次数
}

返回*this的成员函数,返回对象本身,则可以连续调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Screen{
public:
typedef std::string::size_type pos;
Screen() = default;//有另一个构造函数时,若需要默认构造函数,必须显式声明
Screen(pos ht,pos wd,char c):height(ht),width(wd),contents(ht*wd,c) {}

char get() const {return contents[cursor];}
//获取光标当前的内容,隐式内联
inline char get(pos ht,pos wd) const;//显式内联,在类外定义
Screen &move(pos r,pos c);
void some_member() const;//修改mutable变量函数

Screen& set(pos,pos,char);
Screan& set(char);
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;

mutable size_t access_ctr;//被修改mutable变量
};
inline
char Screen::get(pos ht,pos wd) const //因为已经在类内声明过inline,此处可以省略
{
return contents[ht*width+wd];
}
inline
Screen &move(pos r, pos c)
{
cursor = r * width + c;
return *this;
}
void some_member() const
{
++access_ctr;//记录被调用次数
}
inline
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
inline
Screen& screen::set(pos r,pos col, char c)
{
contents[r * width + col] = c;
return *this;
}

//调用set myScreen.move(4,0).set('#');

基于const的重载

若有常量成员函数,即使返回对象本身,也不能与非常量成员函数一起连续调用,则应该对该常量成员函数进行非常量版本的重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Screen{
public:
typedef std::string::size_type pos;
Screen() = default;//有另一个构造函数时,若需要默认构造函数,必须显式声明
Screen(pos ht,pos wd,char c):height(ht),width(wd),contents(ht*wd,c) {}

char get() const {return contents[cursor];}
//获取光标当前的内容,隐式内联
inline char get(pos ht,pos wd) const;//显式内联,在类外定义
Screen &move(pos r,pos c);
void some_member() const;//修改mutable变量函数
Screen& set(pos,pos,char);
Screen& set(char);

//display函数重载
Screen& display(std::ostream &os) {do_display(os);return *this;}
const Screen& display(std::ostream &os) const {do_display(os);return *this;}

private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
mutable size_t access_ctr;//被修改mutable变量

void do_display(std::ostream& os) const {os >> contents;}
};
inline
char Screen::get(pos ht,pos wd) const //因为已经在类内声明过inline,此处可以省略
{
return contents[ht*width+wd];
}
inline
Screen &move(pos r, pos c)
{
cursor = r * width + c;
return *this;
}
void some_member() const
{
++access_ctr;//记录被调用次数
}
inline
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
inline
Screen& screen::set(pos r,pos col, char c)
{
contents[r * width + col] = c;
return *this;
}

//此时可以调用 myScreen.display(cout).set('*');

以上,display函数有常量和非常量版本,对于常量版本,调用do_display函数,常量对象再被调用成常量对象,即直接对常量对象操作,常量对象传递到函数内部并打印并返回常量对象;对于非常量版本,调用do_display函数,非常量对象被传递,此时传递的是对该非常量对象的常量调用,传递到函数内部并打印,返回该调用的常量对象,即原来的非常量对象。也就是说,将非常量对象通过多一次的函数调用统一成常量对象的调用,而对常量对象不影响。

每个类都是唯一的类型,类在声明后定义前,是一个不完全类型,可以定义指向给类的指针或引用,也可以声明以该类类型作为参数或返回类型的函数。

Person类程序

编写一个Person类,表示人员的姓名和住址,string来存放,能够返回姓名和住址,能够读取和打印Person对象,构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person{
friend std::istream& read(std::istream&, Person&);
friend std::ostream& pritn(std::ostream&,const Person&);
public:
auto get_name() const -> std::string const& {return name;}
auto get_address() const -> std::string const& {return address;}
//const位置不影响,也可以在std::string之前
//等价于
const std::string& get_neme() const {return name;}
std::string const& get_address() const {return address;}

Person() = default;
Person(const string& s1,const string& s2): name(s1),name(s2) {}
Person(std::istream&){};//类外定义
//类内定义 Person(std::istream& is) {read(is,*this);}
private:
string name,address;
};

std::istream& read(std::istream& is, Person& p)
{
is << p.name << p.address;
return is;
}
std::ostream& print(std::ostream& os, const Person& p)
{
os >> p.name >> p.address;
return os;
}
Person::person(std::istream& is)
{
read(is,*this);
}

函数的返回类型通常在函数名前面,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外

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

1
2
3
4
5
6
7
8
9
class NoDefault{
NoDefault(int){}
};

//NoDefault类没有提供默认构造函数,此时必须给C加上构造函数
class C{
C(int x): NoDefault(x) {}
NoDefault nf;
};

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

委托构造函数

委托构造函数使用所属类中其他的构造函数来执行自己的初始化过程,受委托的构造函数的初始值列表和函数体被依次执行,最后再执行委托者的函数体

隐式的类类型转换

如果构造函数只接受一个实参,则实际上定义了转换为此类类型的隐式转换机制,编译器只会自动地执行仅一步类型转换

1
2
string null_book = "9999-99";
item.combine(null_book);

string实参传入combine成员函数时,string会自动创建一个Sales_data对象

编译器只会执行一步类型转换

1
2
3
item.combine("9999-99");//此时会报错
item.combine(string("9999-99"));
item.combine(Sales_data("9999-99"));//加上一步显式类型转换则不报错

通过对成员函数声明explicit可以阻止隐式类型转换,explicit只能出现在类内函数的声明中。但可以使用构造函数进行显式的类型转换

1
2
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));

explicit构造函数只能用于直接初始化,不能用于拷贝形式的初始化

聚合类 (aggregate class)

  • 满足以下所有条件:
    • 所有成员都是public的。
    • 没有定义任何构造函数。
    • 没有类内初始值。
    • 没有基类,也没有virtual函数。
  • 可以使用一个花括号括起来的成员初始值列表,初始值的顺序必须和声明的顺序一致。

字面值常量类

  • constexpr函数的参数和返回值必须是字面值。
  • 字面值类型:除了算术类型、引用和指针外,某些类也是字面值类型。
  • 数据成员都是字面值类型的聚合类是字面值常量类。
  • 如果不是聚合类,则必须满足下面所有条件:
    • 数据成员都必须是字面值类型。
    • 类必须至少含有一个constexpr构造函数。
    • 如果一个数据成员含有类内部初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
    • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

静态成员

  • static数据成员存在于类类型的每个对象中。
  • 成员函数可以直接使用静态成员
  • 每个static数据成员是与类关联的对象,并不与该类的对象相关联。
  • 声明:
    • 声明之前加上关键词static
  • 使用:
    • 成员算符**::直接访问静态成员:r = Account::rate();
    • 也可以使用对象访问:r = ac.rate();
  • 定义:在类外部定义时不用加static
  • 初始化:
    • 通常不在类的内部初始化,而是在定义时进行初始化,如 double Account::interestRate = initRate();
    • 如果一定要在类内部定义,则要求必须是字面值常量类型的constexpr

第二部分

第八章——IO库

标准库定义的IO类型

  • iostream头文件:从标准流中读写数据,istreamostream等。
  • fstream头文件:从文件中读写数据,ifstreamofstream等。
  • sstream头文件:从字符串中读写数据,istringstreamostringstream

IO对象不可复制或赋值

  • 1.IO对象不能存在容器里.
  • 2.形参和返回类型也不能是流类型
  • 3.形参和返回类型一般是流的引用
  • 4.读写一个IO对象会改变其状态,因此传递和返回的引用不能是const

条件状态

strm是一种IO类型,(如istream), s是一个流对象

状态 解释
strm:iostate 是一种机器无关的类型,提供了表达条件状态的完整功能
strm:badbit 用来指出流已经崩溃
strm:failbit 用来指出一个IO操作失败了
strm:eofbit 用来指出流到达了文件结束
strm:goodbit 用来指出流未处于错误状态,此值保证为零
s.eof() 若流seofbit置位,则返回true
s.fail() 若流sfailbit置位,则返回true
s.bad() 若流sbadbit置位,则返回true
s.good() 若流s处于有效状态,则返回true
s.clear() 将流s中所有条件状态位复位,将流的状态设置成有效,返回void
s.clear(flags) 将流s中指定的条件状态位复位,返回void
s.setstate(flags) 根据给定的标志位,将流s中对应的条件状态位置位,返回void
s.rdstate() 返回流s的当前条件状态,返回值类型为strm::iostate

从给定流中读取数据,直至遇到文件结束符时停止,然后对流进行按所需位复位

1
2
3
4
5
std::string buf;
while(cin >> buf)
std::cout << buf << std::endl;
cin.clear(cin.rdstate() & ~cin.eobit);
//cin.clear();直接将failbit,badbit,eofbit复位

管理输出缓冲

缓冲刷新的原因

  • 程序正常结束

  • 缓冲区满

  • endl显式刷新缓冲区

  • 操纵符unitbuf设置流的内部状态来清空缓冲区,对cerr默认设置unitbuf,即cerr是立即刷新的

    1
    2
    cout << unitbuf; //所有输出操作都会立即刷新缓冲区
    cout << nounitbuf; //恢复正常缓冲方式
  • 当读写被关联的流时,关联到的流的缓冲区会被刷新。如cin和cerr关联到cout,读cin或写cerr都导致cout刷新缓冲区

  • flush刷新缓冲区,不附加任何额外字符

  • ends插入一个空字符,再刷新缓冲区

交互式系统通常应该关联输入流和输出流,则所有输出包括用户提示,都会在读操作之前被打印出来

文件输入输出

fstream是头文件fstream中定义的一个类型,fstrm是一个文件流对象

操作 解释
fstream fstrm; 创建一个未绑定的文件流。
fstream fstrm(s); 创建一个文件流,并打开名为s的文件,s可以是string也可以是char指针
fstream fstrm(s, mode); 与前一个构造函数类似,但按指定mode打开文件
fstrm.open(s) 打开名为s的文件,并和fstrm绑定
fstrm.close() 关闭和fstrm绑定的文件
fstrm.is_open() 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭

在要求用到基类对象的地方,可以用继承类型对象来替代。则接受iostream引用参数的函数可以用一个fstream类型调用

当一个fstream对象被销毁时,close自动会被调用

文件模式 解释
in 以读的方式打开
out 以写的方式打开
app 每次写操作前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式进行IO操作。

默认情况下,out模式打开会是trunc模式,out模式打开同时指定app模式或者in模式才不会被截断

string流

sstream是头文件sstream中任意一个类型。s是一个string

操作 解释
sstream strm 定义一个未绑定的stringstream对象
sstream strm(s) s初始化对象
strm.str() 返回strm所保存的string的拷贝
strm.str(s) s拷贝到strm中,返回void

istringstream

对整行文本进行读取,对其中行内的单词进行处理,则可以使用istringstream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct PersonInfo{
string name;
vector<string> phones;
};
string line, word;
vector<PersonInfo> people;
while(getline(cin,line))
{
PersonInfo info;
istringstream record(line);
record >> info.name;
while(record >> word)
info.phones.push_back(word);
people.push_back(info);
}

第九章——顺序容器

value_type 元素类型

reference 元素的左值类型,和value_type &含义相同

const_reference 元素的const左值类型,即const value_type &

迭代器

forward list迭代器不支持递减运算符

将一个容器复制给另一个容器时,类型必须匹配:容器类型和元素类型都必须相同;而当使用迭代器来拷贝一个范围时,就不要求容器类型相同

array容器

定义array时,不仅要指定元素类型,还要指定容器大小,array<int,42> arr,默认构造的array是非空的,包含了与其大小一样多的元素

不能对内置数组进行拷贝或对象赋值操作,但array可以

1
2
array<int, 10> digits = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> copy = digits;

赋值和swap

c.assign(b, e)c中的元素替换成迭代器be表示范围中的元素,be不能指向c中的元素

c.assign(il)c中的元素替换成初始化列表il中的元素

c.assign(n, r)c中的元素替换为n个值是t的元素

array容器不能使用assign;由于值列表对象的大小与原对象大小不一样,也不允许用花括号包围的值列表进行赋值。

1
2
3
4
list<string> names;
vector<const char*> oldstyle;
names = oldstyle;// 报错,容器类型不匹配
name.assign(oldstyle.begin(),oldstyle.end());

赋值运算会导致指向左边容器的迭代器、引用和指针都失效,swap则不会(array和string除外)这是因为swap不对元素进行交换,只交换两个容器内部的数据结构,时间复杂度为O(1)。而swap对string会导致失效;对array进行swap会真正交换它们的元素

容器操作——insert

c.insert(p, t) 在迭代器p指向的元素之前创建一个值是t的元素,返回指向新元素的迭代器

c.insert(p, n, t) 在迭代器p指向的元素之前插入n个值为t的元素,返回指向第一个新元素的迭代器;如果n是0,则返回p

c.insert(p, b, e) 将迭代器be范围内的元素,插入到p指向的元素之前;如果范围为空,则返回p

c.insert(p, il) il是一个花括号包围中的元素值列表,将其插入到p指向的元素之前;如果il是空,则返回p

insert函数将元素插入到迭代器指定位置之前,时间复杂度为O(n),对于list时间复杂度则为O(1)

vector,string,deque容器中插入元素都会使所有迭代器、指针和引用失效,向deque首尾位置添加元素,迭代器失效,但指针和引用不失效

利用insert的返回值,可以在容器中一个特定位置反复插入元素

1
2
3
4
list<string> lst;
auto iter = lst.begin();
while(cin >> word)
iter = lst.insert(iter,word);//不断在链表头插入元素

容器操作——emplace

emplace_back(args)在尾部创建一个由args创建的元素

emplace_front(args)在头部创建一个由args创建的元素

emplace(p,args)在迭代器p指向的元素之前创建一个由args创建的元素,返回指向新添加元素的迭代器

emplace开头的函数是新标准引入的,这些操作是构造而不是拷贝元素,传递给emplace的参数必须和元素类型的构造函数相匹配

1
2
3
4
5
//c是一个Sales_data对象
c.emplace_back("989-32000",25,15.99);
//在c的末尾构造一个Sales_data对象
c.push_back(Sales_data("989-32000",25,15.99));
//相当于创建一个临时的Sales_data对象传递给push_back

上述语句都会创建新的Sales_data对象,在调用emplace_back时,会在容器管理的内存空间中直接创建对象,而调用push_back则会创建一个局部临时对象,并将其压入容器中。

访问元素

操作 解释
c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
c.front() 返回c中头元素的引用。若c为空,函数行为未定义
c[n] 返回c中下标是n的元素的引用,n时候一个无符号证书。若n>=c.size(),则函数行为未定义
c.at(n) 返回下标为n的元素引用。如果下标越界,则抛出out_of_range异常

访问成员函数返回的是引用

如果希望确保下标是合法的,可以使用at成员函数

删除元素

c.erase(p) 删除迭代器p指向的元素,返回一个指向被删除元素之后的元素的迭代器,若p本身是尾后迭代器,则函数行为未定义

c.erase(b, e) 删除迭代器be范围内的元素,返回指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则返回尾后迭代器

删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效,删除deque的尾元素会导致尾后迭代器失效。指向vector、string被删除元素位置之后的迭代器、引用和指针都会失效

forward_list容器

操作 解释
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器,此迭代器不能解引用。
lst.cbefore_begin() 同上,但是返回的是常量迭代器。
lst.insert_after(p, t) 在迭代器p之后插入元素。t是一个对象。返回最后一个插入元素的迭代器
lst.insert_after(p, n, t) 在迭代器p之后插入元素。t是一个对象,n是数量。若n是0则函数行为未定义
lst.insert_after(p, b, e) 在迭代器p之后插入元素。由迭代器be指定范围,不能指向lst内
lst.insert_after(p, il) 在迭代器p之后插入元素。由il指定初始化列表。
emplace_after(p, args) 使用argsp之后的位置,创建一个元素,返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义。
lst.erase_after(p) 删除p指向位置之后的元素,返回一个指向被删元素之后的元素的迭代器,若p指向lst的尾元素或者是一个尾后迭代器,则函数行为未定义。
lst.erase_after(b, e) 类似上面,删除对象换成从be指定的范围。

当在forward_list中添加或删除元素时,需要关注两个迭代器,指向当前元素和指向其前驱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//删除所有奇数元素
forward_list<int> flst = {0,1,2,3,4,5,6,7,8,9};
auto prev = flst.begin_before();
auto curr = flst.begin();
while(curr!=flst.end())
{
if(*curr&0x1)
curr = flst.erase_after(prev);
else
{
prev = curr;
++curr;
}
}

删除奇数元素程序

编写程序,查找并删除forward_list<int>中的奇数元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <forward_list>

using std::forward_list;
using std::cout;

auto remove_odds(forward_list<int>& flist)
{
auto is_odd = [] (int i) { return i & 0x1; };
flist.remove_if(is_odd);
}

int main()
{
forward_list<int> data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
remove_odds(data);
for (auto i : data)
cout << i << " ";

return 0;
}

remove_if是list容器的特定算法,让谓词为真时删除该元素

容器大小管理操作

调用reserve不会减少容器所占用的内存空间,即capacity,将会大于或等于当前的容量大小传递给reserve的参数;resize只会改变容器中元素的数量,即size而不改变容器的容量;shrink_to_fit则会请求退回多余的内存空间,但不保证一定退回

string的额外操作

构造string

操作 解释
string s(cp, n) scp指向的数组中前n个字符的拷贝,此数组
string s(s2, pos2) sstring s2从下标pos2开始的字符的拷贝。若pos2 > s2.size(),则构造函数的行为未定义。
string s(s2, pos2, len2) sstring s2从下标pos2开始的len2个字符的拷贝。

从一个const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符串时停止;如果构造函数中还有计数值作为形参,数组则不必以空字符结尾

substr操作

s.substr(pos, n) 返回一个string,包含spos开始n个字符的拷贝。pos的默认值是0,n的默认值是s.size() - pos,即拷贝从pos开始的所有字符

改变string的其他方法

操作 解释
s.insert(pos, args) pos之前插入args指定的字符。pos可以使是下标或者迭代器。接受下标的版本返回指向s的引用;接受迭代器的版本返回指向第一个插入字符的迭代器。
s.erase(pos, len) 删除从pos开始的len个字符,如果len被省略,则删除后面所有字符,返回指向s的引用。
s.assign(args) s中的字符替换成args指定的字符。返回一个指向s的引用。
s.append(args) args指定的字符追加到s,返回一个指向s的引用。
s.replace(range, args) 删除s中范围range中的字符,替换成args指定的字符。返回一个指向s的引用。

编写一个函数,接受三个string参数是soldValnewVal。使用迭代器及inserterase函数将s中所有oldVal替换为newVal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iterator>
#include <iostream>
#include <string>
#include <cstddef>

using std::cout;
using std::endl;
using std::string;

auto replace_with(string &s, string const& oldVal, string const& newVal)
{
for (auto cur = s.begin(); cur <= s.end() - oldVal.size(); )
if (oldVal == string{ cur, cur + oldVal.size() })
cur = s.erase(cur, cur + oldVal.size()),
cur = s.insert(cur, newVal.begin(), newVal.end()),
cur += newVal.size();
else
++cur;
}

int main()
{
string s{ "To drive straight thru is a foolish, tho courageous act." };
replace_with(s, "tho", "though");
replace_with(s, "thru", "through");
cout << s << endl;

return 0;
}

重写上一题的函数,这次使用一个下标和replace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

auto replace_with(string &s, string const& oldVal, string const& newVal)
{
for (size_t pos = 0; pos <= s.size() - oldVal.size();)
if (s[pos] == oldVal[0] && s.substr(pos, oldVal.size()) == oldVal)
s.replace(pos, oldVal.size(), newVal),
pos += newVal.size();
else
++pos;
}

int main()
{
string str{ "To drive straight thru is a foolish, tho courageous act." };
replace_with(str, "tho", "though");
replace_with(str, "thru", "through");
cout << str << endl;
return 0;
}

string的搜索操作

每个操作都接受一个可选的第二参数,表示从什么位置开始搜索

搜索操作 解释
s.find(args) 查找sargs第一次出现的位置
s.rfind(args) 查找sargs最后一次出现的位置
s.find_first_of(args) s中查找args中任何一个字符第一次出现的位置
s.find_last_of(args) s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args) s中查找第一个不在args中的字符
s.find_first_not_of(args) s中查找最后一个不在args中的字符

每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜索失败则返回一个名为string::nposstatic成员(类型是string::size_type,初始化值是-1,npos是一个无符号类型,也就是string最大的可能大小)

string的比较操作

参数形式 解释
s2 比较ss2
pos1, n1, s2 比较spos1开始的n1个字符和s2
pos1, n1, s2, pos2, n2 比较spos1开始的n1个字符和s2
cp 比较scp指向的以空字符结尾的字符数组
pos1, n1, cp 比较spos1开始的n1个字符和cp指向的以空字符结尾的字符数组
pos1, n1, cp, n2 比较spos1开始的n1个字符和cp指向的地址开始n2个字符

数值转换

to_string(val) 一组重载函数,返回数值valstring表示。val可以使任何算术类型。对每个浮点类型和int或更大的整型,都有相应版本的to_string()。和往常一样,小整型会被提升。

stoi(s, p, b) 返回s起始子串(表示整数内容)的数值,ps中第一个非数值字符的下标,默认是0,即函数不保存下标。b是转换所用的基数,默认是10。返回int

要转换为数值的string中第一个非空表字符必须是数值中可能出现的字符

1
2
string s2 = "pi = 3.14";
d = stod(s2.substr(s2.find_first_of("+-.0123456789")));

容器适配器

适配器是使一事物的行为类似于另一事物的行为的一种机制。函数、容器和迭代器都有适配器

三个顺序容器适配器:stack、queue和priority_queue

默认下,stack和queue是基于deque实现的,priority_queue是基于vector实现的,可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,实现对默认容器类型的重载

1
stack<int,<vector<int>> stk;

stack需要实现后进先出,即push_backpop_back的操作,则可以使用vectordeque实现,而不可以用forward_list构建

queue需要实现先进先出,即push_backpop_front的操作,那么就不可以使用vector而可以使用dequelist构建

priority_queue需要有随机访问的能力,因此可使用vectordeque构建

对于每个容器适配器,只可以使用适配器操作,而不能使用底层容器类型的操作

第十章——泛型算法

泛型算法本身不执行容器操作,只是单独依赖迭代器和迭代器操作实现

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

只读算法

accumulate(在numeric中定义),接受三个参数,前两个指出需要求和的元素的范围,第三个是求和的初值。由于在string中定义了“+”运算符,则可以通过accumulate将string元素连接起来;如果传入一个字符串字面值给第三个参数,类型将是const char *,并没有“+”运算符,则会报错

find_first_of,输入:两对迭代器标记两段范围,在第一段中找第二段中任意元素,返回第一个匹配的元素,找不到返回第一段的end迭代器。

对于只读取而不改变元素的算法,通常最好使用cbegincend

equal:确定两个序列是否保存相同的值,前两个表示第一个序列中的范围,第三个表示第二个序列的首元素。若在传入C风格字符串,则比较的是两个指针指向的地址

只接受单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长

写容器的算法

fillfill(vec.begin(), vec.end(), 0); 将每个元素重置为0

fill_nfill_n(vec.begin(), 10, 0);将给定值赋予迭代器指向的元素开始的指定个元素,但要确保容器能容纳要写入的元素

插入迭代器back_inserter

用来确保算法有足够的空间存储数据。#include <iterator>

1
2
3
4
vector<int> vec;				//空vector
auto it = back_inserter(vec); //创建插入迭代器
*it = 42; //添加一个元素
fill_n(back_inserter(vec),10,0); //添加10个元素

拷贝算法copy,前两个参数指定输入范围,第三个指向目标序列。传递给copy的目的序列至少要包含与输入序列一样多的元素,返回目的位置迭代器递增后的位置。可以用copy实现对内置数组的拷贝

1
2
3
4
int a1[] = {1,2,3,4,5};
int a2[sizeof(a1)/sizeof(*a1)];
auto ret = copy(begin(a1),end(a1),a2);
//ret指向拷贝到a2的尾元素之后的位置

replace算法将输入序列中所有等于给定值的元素都改为另一个值

1
replace(ilst.begin(),ilst.edn(),0,42);

上述语句将该序列中所有0替换成42,若希望原序列不变,则用replace_copy

1
replace_copy(ilst.cbegin(),ilst.cend(),back_inserter(ivec),0,42);

ivec包含ilst的一份拷贝,且将所有0换成42

重排容器元素的算法

1
2
3
sort(words.begin(),words.end());//只根据首字母排字典序
auto end_unique = unique(words.begin(),words.end());//返回的是指向不重复区域之后第一个位置的迭代器
words.erase(end_unique,words.end());//要删除重复元素必须要用到容器操作

向算法传递函数

谓词是一个可调用的表达式,返回结果是一个能用作条件的值

1
2
3
4
5
6
bool isShorter(const string& s1,const string& s2)
{
return s1.size() < s2.size();
}
sort(words.begin(),words.end(),isShorter);//按照单词长短排序
stable_sort(words.begin(),words.end(),isShorter);//单词长度相同的则会保持字典序

lambda表达式

可以向一个算法传递任何类别的可调用对象,包括函数、函数指针、重载函数调用运算符的类和lambda表达式

lambda表达式表示一个可调用的代码单元,可以理解成是一个未命名的内联函数

[capture list](parameter list) -> return type {function body}

  • capture list捕获列表

    不可忽略。为空时表明不使用它所在函数中的任何局部变量。捕获列表只用于局部非static变量,lambda可直接使用局部static变量和它所在函数之外声明的名字

  • return type是返回类型。可忽略

    如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void;当需要为一个lambda定义返回类型时,必须使用尾置返回类型

  • parameter是参数列表。可忽略

  • function body是函数体。不可忽略

1
2
3
4
stable_sort(words.begin(),words.end(),[](const string&a, const string& b){return a.size()<b.size();});
auto wc = find_if(words.begin(), words.end(), [sz](const string &a){return a.size() >= sz;});
//获取一个迭代器,指向第一个满足size>=sz的元素
for_each(wc, words.end(), [](const string &s){cout << s << " ";})

使用partition算法

1
2
3
4
5
auto pivot = partition(vs.begin(), vs.end(), [sz](const std::string &s){
return s.size() >= sz;});//相当于排序好,符合谓词的元素排在前,返回最后一个符合谓词元素的后一个位置迭代器

for(auto it = vs.cbegin(); it != pivot; ++it)
std::cout << *it << " ";

lambda捕获和返回

定义lambda时会生成一个新的类类型和该类型的一个对象

当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的

隐式捕获:&告诉编译器采用捕获引用方式,=则表示值捕获方式

1
wc = find_if(words.begin(), words.end(), [=](const string &a){return a.size() >= sz;});

混合使用隐式捕获和显式捕获时,捕获列表第一个元素必须是一个&或=,显式捕获的变量必须使用与隐式捕获不同的方式,即如果隐式捕获采用引用方式(&),则显式捕获命名变量必须采用值方式。

可变lambda

如果希望能改变一个被捕获的变量的值(不改变原来的值),就必须在参数列表首加上mutable

1
2
3
4
size_t v1 = 42;
auto f = [v1] () mutable {return ++v1;};
v1 = 0;
auto j = f(); //j为43

参数绑定

标准库bind函数,定义在头文件functional中,可以看做为一个通用的函数适配器。接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表

auto newCallable = bind(callable, arg_list);

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些是占位符。_n代表第n个位置的参数。定义在placeholders的命名空间中。using std::placeholder::_1;using namespace std::placeholders;使得由placeholders定义的所有名字都可用

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

想要将这个函数作为find_if(接受一个一元谓词)的参数,则可以绑定check_size的sz参数

1
2
3
4
auto check6 = bind(check_size,_1,6);

string s = "hello";
bool b1 = check6(s); //相当于调用check_size(s,6);

auto g = bind(f, a, b, _2, c, _1);,调用g(_1, _2)实际上调用f(a, b, _2, c, _1)

非占位符的参数要使用引用传参,必须使用标准库ref函数或者cref函数

1
2
3
4
5
ostream &print(ostream &os,const string &s,char c)
{
return os << s << c;
}
for_each(words.begin(),words.end(),bind(print,ref(os),_1,' '));

插入迭代器

插入器是一种迭代器适配器,接受一个容器,生成一个迭代器,能实现向给定容器添加元素

  • back_inserter:创建一个使用push_back的迭代器。
  • front_inserter创建一个使用push_front的迭代器。元素总是插入到容器的第一个元素之前
  • inserter创建一个使用insert的迭代器。接受第二个参数,即一个指向给定容器的迭代器,元素会被查到迭代器所指向的元素之前
1
2
3
4
list<int> lst = {1,2,3,4};
list<int> lst2,lst3;
copy(lst.begin(),lst.end(),front_inserter(lst2)); //生成{4,3,2,1}
copy(lst.begin(),lst.end(),inserter(lst3,lst.begin())); //生成{1,2,3,4}

front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserterback_inserter则不会

流迭代器

迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流

1
2
3
4
5
6
7
8
istream_iterator <int> in_iter(cin);	//从cin读入int
istream_iteraator <int> eof; //尾后迭代器
while(in_iter!=eof)
{
vec.push_back(*in_iter++);
}
//另一种方式
vector<int> vec(in_iter,eof);

ostream_iterator提供可选第二参数,是一个C风格字符串,每各元素输出后都打印此字符串

1
2
3
4
5
6
ostream_iterator<int> out_iter(cout," ");
for(auto e:vec)
*out_iter+= = e;
cout << endl;
//另一种方式
copy(vec.begin(),vec.end(),out_iter);

其中解引用和递增运算可以忽略,不产生作用。

反向迭代器

递增一个反向迭代器会移动到前一个元素

1
2
3
string line = "first,second,third";
auto comma = find(line.cbegin(),line.cend(),','); //comma指向第一个逗号
auto rcomma = find(line.crbegin(),line.crend(),','); //rcomma指向的是最后一个逗号

当需要打印出第一个和最后一个单词时

1
2
3
cout << string(line.cbegin(),comma);
cout << string(line.rcbegin(),rcomma);//这里打印出来的单词逆序打印
cout << string(rcomma.base(),line.rcend());//正序打印最后一个单词

特定容器算法

对于listforward_list,优先使用成员函数版本的算法而不是通用算法

操作 解释
lst.merge(lst2) 将来自lst2的元素合并入lst,二者都必须是有序的,元素将从lst2中删除。
lst.merge(lst2, comp) 同上,给定比较操作。
lst.remove(val) 调用erase删除掉与给定值相等(==)的每个元素
lst.remove_if(pred) 调用erase删除掉令一元谓词为真的每个元素
lst.reverse() 反转lst中元素的顺序
lst.sort() 使用<排序元素
lst.sort(comp) 使用给定比较操作排序元素
lst.unique() 调用erase删除同一个值的连续拷贝。使用==
lst.unique(pred) 调用erase删除同一个值的连续拷贝。使用给定的二元谓词。

list和forward_list的splice成员函数版本的参数

参数 解释
(p, lst2) p是一个指向lst中元素的迭代器,或者一个指向flst首前位置的迭代器。函数将lst2中的所有元素移动到lstp之前的位置或是flstp之后的位置。将元素从lst2中删除。lst2的类型必须和lst相同,而且不能是同一个链表。
(p, lst2, p2) 同上,p2是一个指向lst2中位置的有效的迭代器,将p2指向的元素移动到lst中,或将p2之后的元素移动到flst中。lst2可以是于lstflst相同的链表。
(p, lst2, b, e) be表示lst2中的合法范围。将给定范围中的元素从lst2移动到lstfirst中。lst2lst可以使相同的链表,但p不能指向给定范围中的元素。
  • 使用lst.splice(args)flst.splice_after(args)

链表特有版本与通用版本的区别在于链表版本会改变底层的容器

第十一章——关联容器

关联容器中的元素时按照关键字来保存和访问的,支持通过关键字来高效地查找和读取元素

集合 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
std::set 红黑树 有序 O(log n) O(log n)
std::multiset 红黑树 有序 O(logn) O(logn)
std::unordered_set 哈希表 无序 O(1) O(1)
映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
std::map 红黑树 key有序 key不可重复 key不可修改 O(logn) O(logn)
std::multimap 红黑树 key有序 key可重复 key不可修改 O(log n) O(log n)
std::unordered_map 哈希表 key无序 key不可重复 key不可修改 O(1) O(1)

关键字类型的要求

对于有序容器,关键字类型必须定义元素比较的方法。默认是<

对于自定义类型,如果想传递一个比较的函数,所提供的操作必须在关键字类型上定义一个严格弱序(小于等于),可以这样定义,提供关键字类型(Sales_data)以及比较操作类型(函数指针类型)

1
2
3
4
5
6
7
8
9
10
11
12
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() < rhs.isbn();
}
//1.
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
//2.
bool (*pf) (const Sales_data &,const Sales_data &) = &compareIsbn;//&可忽略
multiset<Sales_data, pf> bookstore(pf);
//3.
using pf = bool (*) (const Sales_data &,const Sales_data &);
multiset<Sales_data, pf> bookstore(pf);

compareIsbn来初始化bookstore对象,表明在向对象添加元素时,将调用compareIsbn函数来进行排序。且可以代替&compareIsbn,当使用一个函数名字时,会自动转换成指针类型

pair类型

utility头文件中定义

操作 解释
pair<T1, T2> p; p是一个pair,两个类型分别是T1T2的成员都进行了值初始化。
pair<T1, T2> p(v1, v2); firstsecond分别用v1v2进行初始化。
pair<T1, T2>p = {v1, v2}; 等价于`p(v1, v2)
make_pair(v1, v2); pair的类型从v1v2的类型推断出来。

当函数需要返回一个pair时,可以直接对返回值进行列表初始化

return {v.back(),v.back().size()};

关联容器操作

类型别名 解释
key_type 此容器类型的关键字类型
mapped_type 每个关键字关联的类型,只适用于map
value_type 对于map,是pair<const key_type, mapped_type>; 对于set,和key_type相同。

关联容器迭代器

解引用一个关联容器迭代器时,会得到一个类型为容器的value_type的值的引用

虽然set定义了iteratorconst_iterator类型,但两种类型都只允许只读访问

遍历mapmultimapsetmultiset时,迭代器按关键字升序遍历元素

使用关联容器定义的专用算法会比泛型算法快很多

insert操作 关联容器
c.insert(v) c.emplace(args) vvalue_type类型的对象;args用来构造一个元素。 对于mapset,只有元素的关键字不存在c中才插入或构造元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。对于multimapmultiset则会插入范围中的每个元素。
c.insert(b, e) c.insert(il) be是迭代器,表示一个c::value_type类型值的范围;il是这种值的花括号列表。函数返回void。对于 mapset,只插入关键字不在c中的元素。
c.insert(p, v) c.emplace(p, args) 类似insert(v),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。

map添加元素:

1
2
3
4
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
`word_count.insert(map<string, size_t>::value_type (word, 1));`

以上四种添加方式等价

1
2
while (cin >> word)
++word_count.insert({word, 0}).first->second;

这条语句等价于

1
2
3
4
5
while (cin >> word)
{
auto result = word_count.insert({word, 0});
++(result.first->second);
}

insert成功时,返回指向该元素位置的迭代器,并对其值加一操作,即insert一个{word,1};

insert失败时,返回的是指向map容器中已有的该元素位置的迭代器,并对其值加一操作

删除元素

操作 解释
c.erase(k) c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量。
c.erase(p) c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end()
c.erase(b, e) 删除迭代器对be所表示范围中的元素。返回e

下标操作

只有**mapunordered_map(非const)有下标运算符和对应的at函数**,set类型不支持下标,multimapunordered_multimap可能有多个值与关键字相关联,也不支持下标操作

如果使用下标操作时,关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化

访问查找元素

操作 解释
c.find(k) 返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器
c.count(k) 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1。
c.lower_bound(k) 返回一个迭代器,指向第一个关键字不小于k的元素。
c.upper_bound(k) 返回一个迭代器,指向第一个关键字大于k的元素。
c.equal_range(k) 返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end()

lower_boundupper_bound不适用于无序容器

打印作者的所有著作

使用find和count函数

1
2
3
4
5
6
7
8
9
string search_item("Kevin");//要查找的作者
auto entries = authors.count(search_item);
auto iter = authors.find(search_item);
while(entries)
{
cout << iter->second << endl;
++iter;
--entries;
}

使用lower_boundupper_bound

1
2
3
for(auto beg = authors.lower_bound(search_item);beg!=authors.upper_bound(search_item);beg++)
cout << beg->second << endl;
//lower_bound 返回的是第一个等于查找值的迭代器,upper_bound 返回的是最后一个等于查找值的后一个元素的迭代器

使用equal_range

1
2
for(auto pos = authors.equal_range(search_item);pos.first!=pos.second;++pos.first)
cout << pos.first->second << endl;

单词转换程序???

here is the link to [istringistream](# istringstream)

通过读取map_file文件获得单词转换规则,在读入input文件过程中将根据映射返回转换后的内容,即打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
map<string, string> buildMap(ifstream &map_file)
{

}

const string& transform(const string& s, const map<string,string> &m)
{

}

void word_transform(ifstream &map_file,ifstream &input)
{

}

无序容器

有序容器使用比较运算符来组织元素;无序容器使用哈希函数和关键字类型的==运算符

无序容器在存储上组织为一组桶(bucket),每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶

操作 解释
桶接口
c.bucket_count() 正在使用的桶的数目
c.max_bucket_count() 容器能容纳的最多的桶的数目
c.bucket_size(n) n个桶中有多少个元素
c.bucket(k) 关键字为k的元素在哪个桶中
桶迭代
local_iterator 可以用来访问桶中元素的迭代器类型
const_local_iterator 桶迭代器的const版本
c.begin(n)c.end(n) n的首元素迭代器
c.cbegin(n)c.cend(n) 与前两个函数类似,但返回const_local_iterator
哈希策略
c.load_factor() 每个桶的平均元素数量,返回float值。
c.max_load_factor() c试图维护的平均比桶大小,返回float值。c会在需要时添加新的桶,以使得load_factor<=max_load_factor
c.rehash(n) 重组存储,使得bucket_count>=n,且bucket_count>size/max_load_factor
c.reverse(n) 重组存储,使得c可以保存n个元素且不必rehash

无序容器的性能依赖于哈希函数的质量和桶的数量和大小

无序类型对关键字类型的要求

不能直接定义关键字类型为自定义类类型的无序容器,需要提供自定义的哈希值函数以及提供函数替代==运算符

1
2
3
4
5
6
7
8
9
10
11
size_t hasher(const Sales_data &sd)
{
return hash<string>() (sd.isbn());
}
bool eqOp(const Sales_data &lhs,const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
//使用重载的哈希值计算函数和比较函数来定义无序容器
using SD_multiset = unordered_multiset<Sales_data,decltype(hasher)*,decltype(eqOp)*>;
SD_multiset bookstore(42,hasher,eqOp);

第十二章——动态内存

  • 静态内存用来保存局部static对象、类static对象、定义在任何函数之外的变量(全局)。
  • 栈内存用来保存定义在函数内的非static对象(局部)。
  • 堆内存,又称自由空间,用来存储动态分配的对象,即在程序运行时分配的对象,需要显式地销毁

动态内存管理通过new为对象分配空间并返回一个指向该对象的指针,通过delete接收一个动态对象的指针,销毁对象并释放内存。

shared_ptr

shared_ptr和unique_ptr都支持的操作

操作 解释
shared_ptr<T> sp unique_ptr<T> up 空智能指针,可以指向类型是T的对象
p p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象。
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针,要小心使用,若智能指针释放了对象,返回的指针所指向的对象也就消失了。
swap(p, q) p.swap(q) 交换pq中的指针

shared_ptr独有的操作

操作 解释
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象。
shared_ptr<T>p(q) pshared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T*
p = q pq都是shared_ptr,所保存的指针必须能互相转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。
p.unique() p.use_count()是1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试。

make_shared用其参数来构造给定类型的对象

1
auto p = make_shared<string>(5,'5'); 

每个shared_ptr都有一个关联的计数器,称为引用计数,记录有多少个shared_ptr指向相同的对象。当指向一个对象的最后一个shared_ptr被销毁时,会自动销毁此对象,即释放相关联的内存。

直接管理内存

用new动态分配和初始化对象

1
2
3
4
string *ps1 = new string;		//默认初始化为空
string *ps2 = new string(); //值初始化为空
int *pi1 = new int; //默认初始化,未定义
int *pi2 = new int(); //值初始化为0,*pi2 为0

对于内置类型来说,值初始化有定义的值,默认初始化的对象的值则是未定义;对于定义了构造函数的类类型,都会通过默认构造函数来初始化

由于要用初始化器的类型来推断要分配的类型,只有当括号中有单一初始化器才可以使用auto

1
auto p1 = new auto(obj);

默认下,new不能分配所要求的内存空间,会抛出bad_alloc的异常,可以将nothrow传递给new阻止其抛出异常

1
int *p1 = new (nothrow) int;

delete要求必须指向动态分配的内存或者是一个空指针

shared_ptr和new结合使用

接受指针参数的智能指针构造函数是explicit的,不能将一个内置指针隐式转换成智能指针,只能直接初始化,即用new返回的指针来初始化

1
2
3
4
shared_ptr<int> p1 = new int(1024);
//错误,不能隐式转换,类似于函数传参
shared_ptr<int> p2(new int(1024));
//正确

定义和改变shared_ptr的其他方法

操作 解释
shared_ptr<T> p(q) p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u) punique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d) p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d) pshared_ptr p2的拷贝,唯一的区别是p将可调用对象d来代替delete
p.reset() p是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,则会调用d而不是delete来释放q
p.reset(q) 同上
p.reset(q, d) 同上

get函数返回一个内置指针,指向智能指针管理的对象。只有在确定代码不会delete指针的情况下才使用。

1
2
3
if(!p.unique())
p.reset(new int(*p));
//如果不是唯一用户,则将一份拷贝到新内存上并使其成为唯一用户

不使用get()初始化或reset另一个智能指针

1
process(shared_ptr<int>(p.get()));

p.get()返回内置指针,然后创建了新的临时shared_ptr智能指针,也是指向p指向的对象,但这是新的shared_ptr,此时计数值为1,当执行完该语句后,临时智能指针被销毁,所指向的内存也被释放,则原有的p指针成为空悬指针,再对p进行操作会出现错误

只有shared_ptr<int> p2(p),此时p2与p共同指向同一个对象,计数值为2

使用自己的释放操作

如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>
#include <string>

struct connection
{
std::string ip;
int port;
connection(std::string i, int p) : ip(i), port(p) {}
};

struct destination
{
std::string ip;
int port;
destination(std::string i, int p) : ip(i), port(p) {}
};

connection connect(destination* pDest)
{
std::shared_ptr<connection> pConn(new connection(pDest->ip, pDest->port));
std::cout << "creating connection(" << pConn.use_count() << ")" << std::endl;
return *pConn;
}

void disconnect(connection pConn)
{
std::cout << "connection close(" << pConn.ip << ":" << pConn.port << ")" << std::endl;
}

void end_connection(connection* pConn)
{
disconnect(*pConn);
}

void f(destination &d)
{
connection conn = connect(&d);
std::shared_ptr<connection> p(&conn, end_connection);
std::cout << "connecting now(" << p.use_count() << ")" << std::endl;
}

int main()
{
destination dest("220.181.111.111", 10086);
f(dest);

return 0;
}

当p被销毁时,会调用删除器函数end_connection,即使发生异常,p同样会被销毁从而关闭连接

unique_ptr

某一个时刻只能有一个unique_ptr指向一个给定的对象,不支持拷贝或者赋值操作

unique_ptr操作:

操作 解释
unique_ptr<T> u1 unique_ptr,可以指向类型是T的对象。u1会使用delete来是释放它的指针。
unique_ptr<T, D> u2 u2会使用一个类型为D的可调用对象来释放它的指针。
unique_ptr<T, D> u(d) unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u = nullptr 释放u指向的对象,将u置为空。
u.release() u放弃对指针的控制权,返回指针,并将u置空。
u.reset() 释放u指向的对象
u.reset(q) u指向q指向的对象
u.reset(nullptr) u置空

可以通过调用releasereset将指针的所有权转移给另一个unique_ptr

1
2
3
unique_ptr<int> p1(new int (42));
unique_ptr<int> p2(p1.release());//release返回悬空指针
p2.reset(p1.release());//reset释放掉原来的对象,并指向给定的指针,即p1指针release之后的指针,指向p1原来的对象

可以拷贝或赋值一个将要被销毁的unique_ptr,即通过函数的返回值返回

shared_ptr不同,unique_ptr管理删除器的方式必须在指向类型之后提供删除器类型(可调用对象)

1
unique_ptr<connection,decltype(end_connection)*> p(&c,end_connection);

weak_ptr

操作 解释
weak_ptr<T> w weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) shared_ptr指向相同对象的weak_ptrT必须能转换为sp指向的类型。
w = p p可以是shared_ptr或一个weak_ptr。赋值后wp共享对象。
w.reset() w置为空。
w.use_count() w共享对象的shared_ptr的数量。
w.expired() w.use_count()为0,返回true,否则返回false
w.lock() 如果expiredtrue,则返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

创建一个weak_ptr时,需要用shared_ptr来初始化它

1
2
auto p = make_shared<int>(42);
weak_ptr<int> wk(p);

wk和p指向相同的对象,但wk不影响p的引用计数。wk指向的对象可能会被释放掉

StrBlob和StrBlobPtr程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class StrBlobPtr;
class StrBlob{
friend class StrBlobPtr;
public:
string& begin();
string& end();

StrBlob() : data(make_shared<vector<string>()){}
StrBlob(initializer_list<string> il) : data(make_shared<vector<string>(il)) {}

vector<string>::size_type size() const {return data->size;}
bool empty() const {return data->empty(}

string& front() {check(0,"no front");return data->front();}
string& back() {check(0,"no back");return data->back();}
const string& front() const {check(0,"no front");return data->front();}
const string& back() const {check(0,"no back");return data->back();}

void push_back(const string& s) {data->push_back(s);}
void pop_back() {check(0,"no back to pop"); data->pop_back();}
private:
void check(vector<string>::size_type t, const string& msg) const
{
if(t >= data->size())
throw out_of_range(msg);
}
shared_ptr<vector<string>> data;
};

StrBlobPtr StrBlob::begin()
{
return StrBlobPtr(*this);
}

StrBlobPtr StrBlob::end()
{
return StrBlobPtr(*this, data->size());
}

定义StrBlob类,用shared_ptr存储数组,提供访问元素和始末迭代器接口,声明friend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class StrBlobPtr{
public:
StrBlobPtr(): cur(0) {}
StrBlobPtr(StrBlob& sb, size_type t = 0): wkptr(sb.data),cur(t) {}
bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }
string& deref() const
{
//检查指针是否存在以及是否超出范围
auto p = check(cur, "dereference past end");
return (*p)[cur];
}

StrBlobPtr& incr()
{
//检查是否超出范围,如果已经指向尾后迭代器则不能递增
check(cur, "increase past end");
cur++;
return *this;
}
private:
shared_ptr<vector<string>> check(size_type t, const string& msg) const
{
auto p = wkptr.lock();
if(!p) //如果p为空指针
throw runningtime_error("unbound");
if(i >= p->size())
throw out_of_range(msg);
return p;
}

weak_ptr<vector<string>> wkptr;
vector<string>::size_type cur;
};

核查指针类,定义一个weak_ptrStrBlob共享存储数组,要么指向为空,要么指向vector,但不增加引用数

bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }重载运算符!=判断两个指针类是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
ifstream ifs("books.txt");
StrBlob sb;
string s;
while (getline(ifs, s))
{
sb.push_back(s);
}
for (StrBlobPtr sbp = sb.begin(); sbp != sb.end(); sbp.incr())
{
cout << sbp.deref() << endl;
}

return 0;
}

对两个类的使用,类似于指针与指针指向对象的操作

动态数组

new一个动态数组:类型名之后加一对方括号,指明分配的对象数目(必须是整型,不必是常量),返回指向第一个元素类型的指针,而不是数组类型

1
2
int *p = new int[10];	//未初始化
int *p2 = new int[10]();//值初始化为0

因为在值初始化的括号中未能提供初始化器,因此不能用auto

当用new分配一个大小为0的数组时,返回一个合法的非空指针,但不能解引用,类似于尾后迭代器

delete一个动态数组:

delete [] p;

智能指针和动态数组

1
unique_ptr<int []> up(new int[10]);//up指向一个包含10个元素的未初始化数组

指向数组的unique_ptr不支持成员访问运算符(点和箭头),可以用下标访问数组中的元素

shared_ptr不直接支持管理动态数组,需要提供自定义的删除器,且shared_ptr未定义下标运算符,则需要通过get函数获取一个内置指针来访问数组元素

1
2
3
4
5
shared_ptr<int> sp(new int[10],[](int*p){delete[] p;});
for(size_t i = 0; i != 10; ++i)
{
*(sp.get() + i) = i;
}

allocator类

定义在头文件memory中,将内存分配和对象构造分离开

标准库allocator类及其算法

操作 解释
allocator<T> a 定义了一个名为aallocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象。
a.deallocate(p, n) 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针。且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args) p必须是一个类型是T*的指针,指向一块原始内存;args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象。
a.destroy(p) pT*类型的指针,此算法对p指向的对象执行析构函数。

allocator伴随算法

操作 解释
uninitialized_copy(b, e, b2) 从迭代器be给定的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中。b2指向的内存必须足够大,能够容纳输入序列中元素的拷贝。返回目的位置迭代器,指向最后一个构造的元素之后的位置
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中。
uninitialized_fill(b, e, t) 在迭代器be执行的原始内存范围中创建对象,对象的值均为t的拷贝。
uninitialized_fill_n(b, n, t) 从迭代器b指向的内存地址开始创建n个对象。b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main()
{
int n = 5;
allocator<string> alloc;
auto p = alloc.allocate(n);
string s;
auto q = p;
while (cin >> s && q != p + n)
{
alloc.construct(q++, s);
}
while (q != p)
{
std::cout << *--q << " ";
alloc.destroy(q);
}
alloc.deallocate(p, n);

return 0;
}

文本查询程序

  • 当程序读取输入文件时,需记住单词出现的每一行
  • 输出需提取每个单词所关联的行号,行号升序出现且只出现一次,还要打印给定行号的文本

对应地,

  • 将使用istringstream读取文件中的每一行,并拆分成逐个单词,再用vector<string>进行保存
  • map保存与行号的映射,set保存一个单词对应的所有行号

将上述定义为一个抽象的解决方案,定义一个保存输入文件的类TextQuery,包含vectormap,还有读取给定输入文件的构造函数和查询函数,查询函数返回单词出现次数,行号以及每行的文本;又将返回的结果定义成另一个类QueryResult,用来保存输出结果,包含一个print函数,完成结果的打印

显然,需要在两个类之间共享数据,如果只是将数据拷贝传输,则会浪费;如果是返回迭代器或指针,则可能会导致空悬指针;因此将用到shared_ptr

1
2
3
4
5
6
7
8
9
10
11
void run(ifstream &infile)
{
TextQuery tq(infile);
while(true)
{
cout << "enter word to look for, or q to quit:";
string s;
if(!(cin >> s) || s == 'q') break;
print(cout, tq.query(s)) << endl;
}
}

infile初始化TextQuery对象,构造函数读取输入文件,保存在成员变量vector中,并建立单词行号映射的map

1
2
3
4
5
6
7
8
9
10
11
class QueryResult;
class TextQuery{
public:
using line_no = vector<string>::size_type;
TextQuery() = default;
TextQuery(ifstream& infile);
QueryResult query(const string&) const;
private:
shared_ptr<vector<string>> file;
map<string,shared_ptr<set<line_no>>> wm;
};

考虑与QueryResult类对象共享数据的需求,QueryResult需要共享保存输入文件的vector,用于输入同行的文本;QueryResult还需要共享保存单词关联的行号的set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TextQuery::TextQuery(ifstream& infile): file(new vector<string>)//file(make_shared<vector<string>>())
{
string text;
while(getline(infile,text))
{
file->push_back(text);
int n = file->size() - 1;
istringstream line(text);
string word;
while(line >> word)
{
auto& lines = wm[word]; //返回的是shared_ptr指针
if(!lines) //指针为空即第一次出现该单词,则新建一个
lines.reset(new set<line_no>);
lines->insert(n);
}
}
}

构造函数分配一个vector保存输入文件的文本。getline逐行读取文件并存入vector中,再用一个istringstream处理刚读入的一行中的每个单词,内层whileistringstream的输入运算符从当前行读取每个单词存入word中。当map中不存在word,即第一次出现该单词,下标运算符创建了一个word,返回一个空的智能指针,则再将指针reset,分配新的set,令其指向该set,再向set中插入行号

1
2
3
4
5
6
7
8
9
class QueryResult{
friend ostream& print (ostream& os, const QueryResult& qr);
public:
QueryResult(string s, shared_prt<set<TextQuery::line_no>> l, shared_ptr<vector<string>> p): sought(s), file(p), lines(l) {}
private:
string sought; //搜索的单词
shared_ptr<vector<string>> file; //输入文件
shared_ptr<set<TextQuery::line_no>> lines; //记录的行号
};

声明友元函数print,定义构造函数,将参数保存在数据成员中

1
2
3
4
5
6
7
8
9
10
QueryResult TextQuery::query(const string& s) const
{
// static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto pos = wm.find(s);
if(pos == wm.end()) //找不到单词
return QueryResult(s,make_shared<set<line_no>>(),file);
//return QuertResult(s,nodata,file);
else //找到单词
return QueryResult(s, pos->second, file);
}

query函数接受一个单词,在map中定位其对应的行号,并构造一个QueryResult对象返回,包括string, file, setTextQuery成员,并且共享着相同的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string make_plural(size_t ctr, const string& word, const string& ending)
{
return (ctr > 1) ? word + ending : word;
}

ostream& print(ostream& os, const QueryResult& qr)
{
os << qr.sought << "occurs " << qr.lines->size() << " "
<< make_plural(qr.lines->size(), "times", "s") << endl;
for(auto num : *qr.lines)
{
os << "\t(line" << num + 1 << ")"
<< *(qr.file->begin() + num) << endl;
}
return os;
}

qr.lines是指向记录的行号的智能指针,需要先解引用或者用->运算符,范围for循环中解引用得到的num是取出set的每一个行号,对应类型为set<TextQuery::line_no>>::size_type,而qr.file指向记录文本的数组,用qr.file->begin()+num则指向对应num行号的文本,同样,解引用即可获得整行文本。另外,程序能正确处理未找到单词的情况,只打印第一条输出语句,次数为0不进入范围for循环


C3P阅读笔记
https://kevin346-sc.github.io/2022/10/18/C3P阅读笔记/
作者
Kevin Huang
发布于
2022年10月18日
许可协议