1.类型
1.1 基本内置类型
基本内置类型有算术类型和空类型。算术类型分两类为整型和浮点型,下图显示了C++的算数类型:

1.2 复合类型
c++有几种复合类型:数组、结构、string、引用和指针。这里讨论引用和指针。
1.2.1 引用
引用为对象起了另外一个名字。引用类型必须与其所引用的对象类型一致,通过&d的形式定义引用类型。
1
2
3
4int value=20;
int &d=value; //引用必须被初始化
d=20; //即value=d=20
int i=d; //即i=value;
1.2.2 指针
与引用类似,指针也实现了对其他对象的间接访问。但与引用相比也有不同,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象,指针也无须在定义时赋初值。 指针的值应是下列4种状态之一:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针
- 无效指针,即上述情况外的其他值
2.类型转换
如果两种类型可以相互转换,那么它们就是关联的。
2.1 隐式转换
如int ival=3.14+3;。编译器会自动转换运算对象的类型的情况:
- ①在大多数表达式中,比int型小的整型值首先提升为较大的整数类型
- ②条件语句中,非bool类型转化为bool类型
- ③初始化中,右值转换为左值类型
- ④算术运算和关系运算的对象要转化为同一种类型:先整型提升、再看是否为带符号运算(有符号<不带符号,应当减少带符号与不带符号的混用)
- ⑤函数调用也会发生转换
- ⑥*void类型指针的转换
1 | int main(){ |
2.2 显示转换
强制类型转换形式是cast_name<type>(expression)(一般多为右值,但当type为引用类型时,为左值)。
type是转换的目标类型,expression是要转换的值。cast_name:static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。- ①
static_cast:任何有明确定义的类型转换,只要不包含底层const,均可使用 - ②
const_cast:只能改变运算对象的底层const,即只改变常量属性,不能改变类型 - ③
dynamic_cast:用于将基类的指针或者引用安全地转换成派生类的指针或引用。 - ④
reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释。reinterpret_cast可以用来在任意类型间进行转换,转换后其正确性由程序员保证。
- ①
1 | const char *pc; |
3. const关键字
使用关键字const对变量类型加以限定,它的值不能被改变。注意在默认情况下,const对象仅在文件内有效。如果像让const对象能在文件间共享,加extern:
1
2const int buffer=25;
extern const int buffer=25;
3.1 const引用
把引用绑定在const对象上,称为对常量的引用。与普通引用所不同的是,对常量的引用不能被用作修改它所绑定的对象:(非常量引用不能绑定常量对象,但允许常量引用绑定非常量对象):
1
2
3
4
5
6
7onst int ci=1024;
const int &ri=ci;
int &a=ci; //错误,不能用非常量引用绑定常量对象
int i=40;
r1=42; //错误,r1是对常量的引用,不能被修改所绑定的对象ci
int &r=ci; //错误,不能非常量引用绑定常量
const int &d=i; //允许const int& 绑定在一个int对象上1
2
3
4
5
6
7double i=3;
const int &p=i;
/*实际的底层:
double i=3;
int temp=i;
const int &p=temp;
*/p绑定的是整型,编译器会把上述的代码中的i由双精度浮点数生产一个临时整型(常)量,此时p就绑定了一个临时量对象temp。如果p不是常量,此时不能对p赋值来改变i的值(因为实际改变的是零时temp的值),在c++看来这是非法的。
3.2指针和const
与引用一样,也可以让指针指向常量或非常量。指向常量的指针不能改变其所指向对象的值。且常量指针必须初始化。
1
2const char *p //表示 指向的内容不能改变。
char * const p //就是将P声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。
3.3顶层const和底层const
一般来说对于指针而言才有底层和顶层这个区分。顶层const表示指针本身是一个常量,底层const表示指针所指向的对象是一个常量。
1
2
3
4
5
6int i=0;
const int ci=42; //顶层const,不能修改ci的值
int const cr=40; //顶层const
int *const p=&i; //顶层const,不允许修改p的值
int const *p1=&i; //底层const,允许修改p1的值,但不能修改ci的值
const int *p2=&ci; //底层const,允许修改p2的值,但不能修改ci的值1
2
3int const *p=&i; //底层const
int* f = p; //不允许
const int* g = i; //合法
4.标准库string
C++大大增强了对字符串的支持,除了可以使用C风格的字符串,还可以使用内置的
string 类。string
类处理起字符串来会方便很多,完全可以代替C语言中的字符数组或字符串指针。string类重载许多运算符,如<,>,<=,>=,==,!=,[],+,以及输出流<<和>>
4.1 string的构造
1 |
|
4.2 string关于长度的函数
string支持较多的关于长度的函数,这里通过表格形式列举:
| 函数名 | 功能 | 调用 |
|---|---|---|
size() |
返回字符数量 | s.size() |
length() |
返回字符数量 | s.length() |
empty() |
判空操作 | s.empty() |
capacity |
返回字符容量 | s.capacity() |
reserve(size_t) |
保留内存以存储一定数量的字符 | s.reserve(20) |
resize(size_t) |
改变字符数量 | s.reszie(20) |
4.3 string的增删
一般来说相同名称的接口有很多个重载,因此列举的只是一部分,读者可以在实际开发中依据编辑器给的提示选择:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//插入
string s1 = "hello";
string s2="world";
s1=s1+" "+s2; //重载+运算符
s1.insert(1,"ins"); //从s1的1位置开始,插入"ins"字符串,即s1="hinsello";
s1.insert(1, "ins", 2);//从s1的1位置开始,插入"ins"字符串的前2个字符,即s1="hinello";
s1.insert(1, "ins", 1, 2);//从s1的1位置开始,插入"ins"字符串的从1位置开始的2个字符,即s1="hnsello";
iterator insert(iterator it, char c);//在it处插入字符c,返回插入后迭代器的位置,该insert不是string的insert,而是算法层的函数
s1.append(str);
s1.push_back(char c);
//特殊
s1.assign(str); //赋新值,旧值全部删除
//删除
iterator s1.erase(iterator first, iterator last);//删除[first,last)之间的所有字符,返回删除后迭代器的位置
iterator s1.erase(iterator it);//删除it指向的字符,返回删除后迭代器的位置
string& s1.erase(int pos = 0, int n = npos);//删除pos开始的n个字符,返回修改后的字符串
4.4 string的查找
string类的查找函数提供了比较多的接口,以下是列举的一些常用find函数
1
2
3
4
5
6
7
81. size_t find (const char* s, size_t pos = 0) const;//在当前字符串的pos索引位置开始,查找子串s,返回找到的位置索引,-1表示查找不到子串
2. size_t find (charc, size_t pos = 0) const;//在当前字符串的pos索引位置开始,查找字符c,返回找到的位置索引,-1表示查找不到字符
3. size_t rfind (const char* s, size_t pos = npos) const;//在当前字符串的pos索引位置开始,反向查找子串s,返回找到的位置索引,-1表示查找不到子串
4. size_t rfind (charc, size_t pos = npos) const;//在当前字符串的pos索引位置开始,反向查找字符c,返回找到的位置索引,-1表示查找不到字符
5. size_t find_first_of (const char* s, size_t pos = 0) const;//在当前字符串的pos索引位置开始,查找子串s的字符,返回找到的位置索引,-1表示查找不到字符
6. size_t find_first_not_of (const char* s, size_t pos = 0) const;//在当前字符串的pos索引位置开始,查找第一个不位于子串s的字符,返回找到的位置索引,-1表示查找不到字符
7. size_t find_last_of(const char* s, size_t pos = npos) const;//在当前字符串的pos索引位置开始,查找最后一个位于子串s的字符,返回找到的位置索引,-1表示查找不到字符
8. size_t find_last_not_of (const char* s, size_t pos = npos) const;//在当前字符串的pos索引位置开始,查找最后一个不位于子串s的字符,返回找到的位置索引,-1表示查找不到子串
4.5 string的遍历和排序
string支持下标[]随机访问,也支持迭代器遍历,因此对于遍历string可通过这些功能来遍历。排序我们使用algorithm头文件内的sort,sort内部的实现机制是快排,并且通过修改避免快排中复杂都最高的情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17string s(str);
//foreach循环
for(auto c:s)
printf(c);
for(int i=0;i<s.size();i++){
cout<<c<<ends;
}
string::iterator it=s.begin();
while(it<s.end()){
cout<<*it<<ends;
it++;
}
sort(s.begin(),s.end());
4.6 其他操作

注意:在c++中存在一个从const char*到string的隐式类型转换,却不存在从一个string对象到C_string的自动类型转换。对于string类型的字符串,可以通过c_str()函数返回string对象对应的C_string.
通常,程序员在整个程序中应坚持使用string类对象,直到必须将内容转化为char*时才将其转换为C_string.(即string类型有向const
char*的转换,没有string向非const char的转换)
5. 初见迭代器
这里尽对迭代器做一个简单的介绍,更加具体的见STL源码剖析。我们可通过下标运算符来访问string和vector对象的元素。还有一种是通过迭代器(iterator)进行访问,在c++中我们强烈推荐使用迭代器而不是下标,因为标准库几乎为每一种容器都提供了迭代器,而迭代器提供了对对象的间接访问。
使用迭代器:有迭代器的类型同时拥有返回迭代器成员的函数,如
begin和end,其中begin成员负责返回指向第一个元素;end成员负责返回尾元素的下一个位置。和指针类似,也能通过解引用迭代器来获取它所指示的元素。1
2
3
4
5string s("some string");
if(s.begin()!=s.end()){ //确保s非空
auto it=s.begin(); //it表示s的第一个字符地址
*it=toupper(*it); //将第一个改为大写
}将迭代器从一个元素移动到另外一个元素:使用递增++运算符达到这个目的,如下
1
2for(auto it=s.begin();it!=s.end()&&!isspace(*it);it++)to
*it=toupper(*it);
5.1迭代器类型
拥有迭代器的标准库类类型使用iterator和const_iterator来表示迭代器类型.所以如果对象是一个常量,只能用const_iterator。
1
2
3
4vector<int>::iterator it; //it能读写vector<int>的有元素
string::iterator it2; //it2能读写string对象中的字符
vector<int>::const_iterator it3; //it3只能读元素,不能写元素
string::const_iterator it4; //只能读,无法写
5.2 迭代器运算
迭代器的递增运算令迭代器每次移动一个元素,所有标准库都支持递增运算,也能用==和!=对两个有效迭代器进行比较。以下是vector和string提供的个更多的关系运算:

6.异常处理
异常处理包括:
throw表达式:异常检测部分使用throw表达式来表示它遇到了无法处理的问题。try语句块:异常处理部分使用try语句块处理异常,try语句块代码中抛出的异常会被某个catch子句处理。(catch为异常处理代码)- 一套异常类:用于
throw语句和相关catch子句之间传递异常的具体信息
1)throw表达式 throw表达式引发一个异常。
1
2
3
4if(item1.isbn()!=item2.isbn())
throw runtime_error("Data must refer to same ISBN")
else
cout<<item1+item2<<endl;
- ①
exception - ②
stdexcept - ③
new头文件定义的bad_alloc - ④
type_inof头文件定义的bad_cast

7.函数
7.1 局部对象
在c++中,名字有作用域,对象有生命周期。
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在时间
形参和函数体内部定义的变量统称为局部变量。所有在函数体之外定义的对象存在于程序的整个执行过程。
- 自动对象:只存在于块执行期间的对象为自动对象。即对于局部变量,当函数的执行路径经过变量定义语句时创建该对象,到达块末尾时销毁它。
- 局部静态对象:将局部变量定义成static类型从而可获得。在定义中初始化时生产该对象,直到程序终止才被销毁(即static修饰得变量只会被初始化一次)
1 | size_t count(){ |
7.2 参数传递
形参的类型决定了形参与实参得交互方式。若形参为引用类型,则对应得实参被引用传递,引用形参是它对应实参得别名。当实参的值被拷贝给形参时,形参和实参是两个独立的对象,此时实参被值传递。
- ①指针形参:和其他非引用形参一样,形参的指针执行拷贝时,拷贝的是实参指针,两个指针是不同的指针,但他们都指向同一对象,因此可修改指向对象的值。
- ②引用行参:引用参数绑定初始化它的对象,改变形参也就改变了所引对象的值。使用引用能避免拷贝。
7.2.1 指针形参与const
达到不改变实参和形参所指对象的值,而且调用时,若改变了值,编译器报错。但需注意(于底层const)允许非常量初始化常量对象(形参),但不允许常量(实参)初始化一个非常量对象(形参)
1 | void func1(int* const a); //顶层 |
7.2.2 引用形参与const
常量引用时候我们经常要用到的东西。同const指针形参一样,同样不能修改值。普通引用(即没有const)的限制:
- 不允许实参为const对象(即非常量不能初始化常量对象)
- 不允许实参为字面值
- 不能提供类型转换
const引用形参却能克服这些限制。但就是不能改变绑定的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void func1(int &a);
void func2(const int &a);
int main(){
int const a=10;
int b=20;
//func1(a) 不允许,普通引用不允许实参为const
//func1(10) 不允许,普通引用形参不允许实参为字面值
func1(b);
func2(a);
func2(b);
}
void func1(int &a){
cout<<"这里是func1"<<endl;
a=30;
}
void func2(const int &a){
cout<<"这里是function2"<<endl;
//a=40; 不允许对const引用的修改
}
7.2.3 数组形参
因为数组不能被拷贝,所以无法以值传递的方式使用数组参数。但数组会被转换为指针,所以当我们传递一个数组时,实际上传递的指向数组首元素的指针。
1
2
3
4
5
6//这三个函数声明均为const int*类型。
void print(const int*);
void print(const int[]);
void print(const int[10]);
//还有数组引用形参
void func(int (&arry)[]);
7.2.4 可变形参函数
有时我们无法预知应该向函数传递几个参数,所以可使用可变形参函数.c++11新标准提供了以下主要方法:
- 若所以实参类型相同,可传递一个名为initializer_list的标准库类型
- 若实参类型不同,编写一个可变参数模板
- c++还有一种形参类型(省略符),用它来传递可变数量的实参。
1.initializer_list形参
它是一种标准库类型,定义在同名(initializer_list)头文件中。它提供的操作有:
和vector不同的是,initializer_list对象中的元素是常量值,我们无法修改它们的值。
1
2
3
4void msg(initializer_list<string> li) //均为string类型
{
functionbody;
}
2.省略符形参
省略符形参应该仅仅用于c和c++的通用的类型(因为省略符的实际就是为了c++能够访问特殊c代码所设计的,这些代码使用了varargs的c标准库功能)。使用参数是用位置数字代号作为形参变量
1
2
3
4
5
6
7//省略符形参只有两种形式:
foo(parm_list,...); //指定部分形参类型
foo(...); //均无指定
//用数字作为代号
void func4(...) {
cout << 1 << endl;
}
7.3返回值
- ①c++11新标准规定,函数可以返回花括号包围的列表
1 | vector<string> process(){ |
- c++11新标准规定,main函数的类型如果不为void,也可没有return语句(编译器会自己添加return 0;)
7.4函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,称为重载函数(函数的重载应该使用于那些操作非常类似的函数),返回值也可不同(但不是必须)。如:
1
2
3void print(const char*p);
void print(const int* beg,const int *end);
void print(const int aa[],size_t size);
7.4.1 重载和const形参
- 拥有顶层const和没有顶层const的形参无法区分
1 | int lookup(int); |
- 底层const可区分
1 | int lookup(int&); |
7.4.2 const_cast和重载
const_cast在重载函数的情景中最有用。例子如下 1
2
3const string& shorterString(const string& s1,const string& s2){
return s1.size<=s2.size()?s1:s2;
}const string的引用。当我们传入的参数时非常量的时候,我们当然希望返回的是string的引用,而不是const string的引用,这时候就用到显示类型转换const_cast.改进如下:重载另一shorterString函数:
1
2
3
4string& shorterString(string& s1,string& s2){
auto& r=shorterString(const_cast<const string&>(s1),const_cast<const string&>(s2));
return const_cast<string&>(r);
}string&。在该函数内部调用const的版本,参数强制转换为const string&型
执行后返回后auto& r。无const版本再强制转为string&。
你觉得很不应该,直接在无const函数实现不就行了吗,其实不是的,这样写是为了方便代码的维护,要修改的时候我们只有修改const版本
7.5特殊用途语言特性
这里介绍默认实参、内联函数和constexpr函数。
7.5.1 默认实参
某些函数再多次调用时都被赋予一个相同值,这个反复出现的值就为默认实参。其声明定义如下:
1
2
3
4
5
6
7
8
9
10string screen(int ht=24,int wid=10,char backgrd=' ');
//或
typedef string::szie_type sz;
string screen(sz ht=24,sz wid=10,char backgrd=' ');
//调用函数时:
string window;
window=screen(); //默认实参
window=screen(60); //等价于screen(60,10,' ');
window=screen(60,10,'#');
7.5.2 内联函数和constexptr函数
因为调用函数要先保存寄存器,并在返回时恢复。有一定的时间开销。而内联函数可避免调用函数的开销。关键字是inline.
1
2
3inline const string& shorterString(const string& s1,const string& s2){
return s1.size<=s2.size()?s1:s2;
}
constexptr函数:指能用于常量表达式的函数。函数的返回类型及所有形参的的类型都是字面值,函数体中有且仅有一条return语句。
7.6 lambda表达式
我们可以向一个算法传递任何类别可调用对象,如果可以对其使用调用运算符(),则称它为可调用的。c++中可调用对象有函数、函数指针、重载函数调用运算符类、lambda表达式。
一个lambda表达式表示一个可调用的代码单元,可将其理解为一个未命名的内联函数。一个lambda具有一个返回类型、一个参数列表和一个函数体(同函数一样)。与函数不同的是,lambda可定义在函数内部,有捕获列表:
1
[capture list] (parameter list)->return type{ function body };
captue list(捕获列表)是一个lambda所在函数中定义的局部变量列表(通常为空)return type为返回类型,parameter list为参数列表、function body为函数体
可以忽略参数列表(等价于指定一个空参数列表)和返回类型(此时根据代码推断,有return返回相应类型,没有为void),但必须包含捕获列表和函数体:
1
2
3
4
5
6
7auto f=[] {return 42;};
cout<<f()<<endl; //调用时也有调用运算符()
//lambda不能设默认参数,因此一个lambda调用时实参数目必须与形参一一对应。
[](const string &s1,const string &s2)
{
return s1.size()<s2.size();
}
7.6.1 捕获规则
lambda表达式的捕获列表有值捕获和引用捕获
我们可以在捕获列表中写一个&或者=,指示编译器推断捕获列表。&告诉编译器采用捕获引用方式,=则表示采用值捕获方式。我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获:
- 当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个
&或=(必须隐式) - 当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式
1 | void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os,string c=" "){ |
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18int sum(int& a, int& b, int& c)
{
c = 10;
a = 11;
b = 12;
cout << a<<" " << b<<" " << c << endl;
return a + b + c;
}
void lambdaTest(int a, int b)
{
int c = 20;
auto f = [&,c]()mutable {return sum(a, b, c); };
int ret = f();
cout << ret <<" " << a <<" "<<b <<" " << c << endl;
}
输出:
11 12 10
33 11 12 20
8.类
c++语言中,使用类定义自己的数据类型,是我们更容易编写、调试和修改程序。类的基本思想是数据抽象和封装。 数据抽象是一种依赖接口和实现的分离编程技术。类的接口包括用户所能执行的操作,类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。要实现数据抽象,首先要定义抽象数据类型。
8.1 定义抽象数据类型
成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部。而作为接口组成部分的非成员函数,他们的定义和声明都在类的外部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Sales_data{ //Sales_data类
std::string isbn() const{
return BookNo;
}
Sales_data &combine(const Sales_data&);
double avg_price() const;
std::string BookNo;
unsigned units_sold=0;
double revenue=0.0;
};
Sales_data add(const Sales_data&,const Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);
std::istream &read(std::istream&,Sales_data&);isbn、combine、avg_prince。其中isbn在类的内部定义,combine和avg_price定义在类外部(声明于类内部)。
8.2 成员函数
8.2.1 const修饰函数---常量成员函数
在类中将成员函数修饰为
const表明在该函数体内,不能修改对象的数据成员而且它不能调用非const函数。为什么不能调用非
const函数:因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。同时这里的
const也有修改隐式this指针的作用,使T*const register this为const T*const register this,这样this指针就能绑定到一个常量(const)对象上。1
2
3
4
5
6
7
8
9
10
11
12
13
using namespace std;
class A{
private:
int i;
public:
void set(int n){ //set函数设置值,不能声明为const函数
i=n;
}
int get()const{ //get函数只取值,声明为const符合规范设计
return i;
}
}
8.2.2 this指针
上面提到了this指针,它指的是对于类的内部成员函数,其参数列表的第一若不显式写出总是隐藏有一个this指针:
- 成员函数this指针:成员函数通过一个名为this的隐式参数来访问调用它的那个对象。
this指针是一个指向当前对象的指针,或者说当前对象的地址。 - this指针只能在一个类的非静态成员函数中使用(全局函数、静态函数不能使用)。
- C++的非静态成员函数的第一个默认并且被隐藏的参数是
T *const register this。比如我们在Student这个类里声明这样一个函数:int SetName(const char *name);其实编译器处理的时候会变为int SetName(Student *const register this, const char *name);
8.2.3 类的静态函数没有this指针
静态函数如同静态变量一样,他不属于具体的哪一个对象而是属于类,静态函数表示了整个类范围意义上的信息。而this指针却实实在在的对应一个对象,所以this指针不能被静态函数使用了,同理,全局函数也一样。
注意:在静态成员函数的实现中不能直接引用类中说明的非静态成员,但可以引用类中说明的静态成员。非静态成员函数即可以引用类中静态成员和非静态成员(这点非常重要)
8.2.4 this指针什么时候创建的
this在成员函数的开始执行前构造的,在成员的执行结束后清除。this指针只有在成员函数中才有定义。因此你获得一个对象后,不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置,this指针存于寄存器中)。
当然,在成员函数里,是可以知道this指针的位置的(可以&this获得),也可以直接使用的:
1
2
3
4
5
6
7//定义一个返回this对象的函数:
Sales_data &Sales_data::combine(const Sales_data& rhs)
{}
units_sold+=rhs.units_sold; //把rhs的成员加到this对象的成员上
revenue+=rhs.revenue; //同理
return *this; //返回调用该函数的对象(引用)
}
8.3类相关的非成员函数
类的定义常常需要辅助函数(非成员函数),它们的声明要与类声明在同一头文件中。最常用的就是输入输出的非成员函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21istream& read(istream &is,Sales_data& item)
{
double price=0;
is>>item.BookNO>>item.units_sold>>price;
item.revenue=price*item.units_sold
return is
}
ostream& print(ostream &os,Sales_data &item)
{
os<<item.isbn()<<" "<<item.units_sold<<" "
<<item.revenue<<" "<<item.avg_price();
return os;
}
Sales_data add(const Sales_data& lhs,const Sales_data &rhs)
{
Sales_data sum=lhs; //lhs拷贝给sum
sum.combine(rhs); //将rhs的数据加到sum中,sum存放lhs和rhs和
return sum;
}
8.4类的静态成员
8.4.1 什么是静态成员(声明)
有时候我们需要一些成员与类本身直接相关,而不是与类的各个对象保持关联。在该成员变动时,希望每个对象都能使用新值,就如银行利率。这个时候就引入了类的静态成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Account{
public:
void calculate(){
amount+=amoubt*interestRate;
}
static double rate(){
return interestRate;
}
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static doubleinitRate();
}Account的对象只包含了两个数据成员owner和amount。interestRate被所有对象共享。
静态成员函数也不与对象绑定,它们不包含this指针,所以静态成员函数不能被声明为const。
8.4.2 定义静态成员及类外初始化(静态成员变量和静态成员函数)
和其他成员函数一样,可在内部定义也可在外部定义(在外部时,不可重复使用static关键字,static只出现在内部声明语句)
1
2
3
4void Account::rate(double newRate)
{
interestRate=newRate;
}1
double Account::interestRate=initRate(); //定义并初始化一个静态成员
8.4.3 使用静态成员
1 | //使用域运算符直接访问静态成员 |
8.5 构造函数
每个类都定义了它的对象被初始化的方式,类是通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数就是构造函数。构造函数的任务就是初始化对象的数据成员,只要类的对象被创建,就会执行构造函数---->拷贝。
- 构造函数:构造函数的名字与类名相同,没有返回(值)类型,不能被声明为
const(const对象执行构造函数时,执行完毕才成为const对象)。 - 拷贝和赋值:拷贝是构造行为,状态取决于用于构造的对象;赋值是对已构造对象进行状态更新。赋值侧重于更新,构造侧重于构造。
8.5.1 默认构造函数-->也可写成默认实参的构造函数(还是默认构造函数)
在没有为对象提供初始值,类没有显示地定义任何构造函数,编译器会为我们隐式的定义一个构造函数,称为合成的默认构造函数。其初始化规则:
- 类内如果存在初始值,用它来初始化成员
- 没有,默认初始化(为0)
1 | class Sales_data{ //Sales_data类 |
c++11新标准,可以通过=default来要求编译器生成合成的默认构造函数(default在类内部声明为内联的,在外部就不是)。
1
Salse_data(const std::string& s):BookNo(s){};
units_sold,revenue被忽略,那么他等价于Salse_data(const std::string& s):BookNo(s),units_sold(0),revenue(0) {};
8.5.2 构造函数初始值列表
- 冒号和花括号之间的部分。其负责为新创建的对象的一个或几个数据成员赋值。列表是是类内置成员的名字,其()括号内就为初始值(注意不是赋值而是初始化)。
- 注意:列表对成员变量的初始化是按其类内声明顺序初始化,而不是列表顺序。
- 使用初始化列表是直接初始化,因此具有更高的效率,而在函数体内是先初始化再赋值。存在效率的差异,如果是类对象,那么效率更低
- 构造函数的初始值列表解决了初始值必不可少的三种情况:
- 一是const或者引用成员,他们必须被初始化,因为在函数体内是赋值而不是初始化。
1 | //情况1 |
2.二是当成员属于某种类类型且该类型没有默认构造函数,也必须将这个成员初始化。尝试在内部通过赋值的方式初始化,但没有默认的构造函数,即参数列表为空,那么test就会首先被默认初始化,但是Test类没有默认的构造函数从而出现错误,所以初始化只能放在列表中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Test
{ //无默认构造函数
public:
Test (int, int, int){
cout <<"Test" << endl;
}
private:
int x;
int y;
int z;
};
class Mytest
{
public:
Mytest():test(1,2,3){ //初始化ok
//test(Test(1,2,3));//error: no match for call to '(Test) (Test)'
//test=Test(12,3,4);//error: no matching function for call to 'Test::Test()'
//test(1,2,3);//error: no match for call to '(Test) (int, int, int)'
}
private:
Test test; //声明
};1
2
3
4
5
6
7
8
9
10
11
12
13
14class Test{
public:
Test(){}
Test (int x){ int_x = x;}
void show(){cout<< int_x << endl;}
private:
int int_x;
};
class Mytest:public Test{
public:
Mytest():Test(110){//打印出110
//Test(110); // 构造函数只能在初始化列表中被显示调用,不能在构造函数内部被显示调用 。不报错但打印随机数:14887136
}
};
注意:从无到有叫初始化,初始化(调用拷贝构造函数)创建了新对象;赋值(调用赋值操作符)没有创建新对象,而是对已有的对象赋值。
8.5.3 类外构造函数的定义
以istream为参数的的构造函数因为要执行一些实际操作。 1
2
3
4
5
6
7
8
9
10
11
12
13//非成员函数
istream& read((istream &is,Sales_data& item)
{
double price=0;
is>>item.BookNO>>item.units_sold>>price;
item.revenue=price*item.units_sold
return is
}
//构造函数
Sales_data::Sales_data(std::istream &is)
{
read(is,*this); //read函数的作用是从is中读取一条交易信息后存入this
}this是一个Sale_data对象的引用。
其他的诸如拷贝构造、赋值构造、析构和移动构造放在后面的拷贝控制详细讲述。
8.5.4 委托构造函数
c++11新标准扩展了构造函数初始值的功能,使得我们可以定义委托构造函数。所谓的委托构造函数其实就是一个构造函数的任务交给零一构造函数去完成,
1
2
3
4
5
6
7
8
9class Sales_data{
public:
//非委托构造函数使用对应的实参初始化成员
Sales_data(sting s,unsigned cnt,double price):
bookNo(s),units_sold(cnt),revenue(cny*price){}
//委托构造函数
Sales_data():Sales_data("",0,0){}
Sales_data(string s):Sales_data(s,0,0){}
}
8.5.5转换构造函数
如果构造函数只接受一个实参,那么实际上定义了转换为此类类型的隐式转换机制。这种构造函数称为转换构造函数,但只允许一步类类型转换。如:
1
Salse_data(const std::string& s):BookNo(s){};//这个构造函数。支持了转换构造,参数为const
Sales_data &combine(const Sales_data&);函数,参数为const,如下:
1
2
3Sales_data item; //执行了默认构造函数
string null_book="9-999-999";
item.combine(null_book);//在这里combine函数的参数时Sales_data类型,因此这里用单参转换构造函数构造了一零时Sales_data对象,即null_book隐式转换为Sales_data类型combine函数的参数实际是Sales_data类型,因此这里调用单参转换构造函数Salse_data(const std::string& s):BookNo(s){};构造了一零时Sales_data对象,即null_book隐式转换为Sales_data类型,然后在执行该语句。
注意:形参必须声明为const
8.5.6 深浅拷贝区别
背景:
- 默认的拷贝构造函数进行了简单的赋值操作(浅拷贝)
- 浅拷贝的问题:当多个对象执行默认拷贝构造函数时会多次释放同一个分配的内存空间
1 | class Student{ |
如下图,两个对象的name指针指向同一块内存。在释放时,这会导致同一块内存会被释放两次,这是不允许的。
深拷贝解决浅拷贝问题:自己写拷贝构造函数
1
2
3
4
5
6
7
8
9
10
11//深拷贝
Student(const Student &stu)
{
cout << "自己的拷贝构造函数" << endl;
//1.申请空间
pName = (char*)malloc(strlen(stu.pName) + 1);
//2.拷贝数据
strcpy(pName, stu.pName);
age = stu.age;
}
8.6 访问控制和封装
8.6.1 封装
我们为类定义了接口,但并没有机制制约用户使用这些接口。使用访问说明符加强类的封装性:
public:public说明符之后的成员在整个程序可被访问private:可以被类的成员函数访问,但不能被使用该类的代码访问。protected:被该关键词修饰的成员不能被外部访问,但是能够给成员函数和继承的类访问class:class和struct一样的作用,声明一个类,唯一不同是定义在第一个访问说明符之前的成员的访问权限不同struct是public,class是private
8.6.2 友元
在真正的项目中,有一些一起声明在头文件的类的非成员函数,它们作为类的接口的一部分,想要访问该类的私有成员,但不是类的的成员,因此无法做到,而友元可以很好的解决这类问题:
类可以允许其他类或者函数访问它的非公有成员,只要让这些类或函数成为该类的友员(friend)即可。友元函数必须被声明过。友元分为非成员函数友元、类友元和成员函数友元。
非成员函数友元:即非成员函数在类内部声明
friends。增加一条friend关键字开头的函数声明即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23struct Sales_data{ //Sales_data类
//构造函数
public:
//三个非成员函数声明友元
friend Sales_data add(const Sales_data&,const Sales_data&);
friend std::istream &read(std::istream&,const Sales_data&);
friend std::ostream &print(std::ostream&,const Sales_data&);
Sales_data()=default; //默认构造函数
Salse_data(const std::string& s):BookNo(s){};
Sales_data(const std::stsring& s,unsigned n,double p):
BookNo(s),units_sold(n),revenue(p*n) {};
Sales_data(std::istream &);
std::string isbn() const{return BookNo;}
Sales_data &combine(const Sales_data&);
private:
double avg_price() const;
std::string BookNo;
unsigned units_sold=0;
double revenue=0.0;
};
Sales_data add(const Sales_data&,const Sales_data&);
std::istream &read(std::istream&,const Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);类友元:即在类A中声明另一个类B为它的友元,那么类B可以访问类A的私有成员,但是A不能访问B的私有成员,如果要访问,那么应该也罢A声明为B的友元,即友元使单向的。
1
2
3
4
5
6
7
8
9
10class B;
class A{
friends class B;
void clearA();
....
}
class B{
....
void clearB();
}成员函数友元:即将类A中声明一个类B的函数为友元,该函数能够访问A的私有成员
1
2
3
4class A{
friend void B::clearB();
void clearA();
};
注意:要使某个类成员函数为另一类的友元,则必须组织程序的结构使声明和定义符合
- 首先要定义B类,其中声明
clearB函数但不定义。(在clearB使用A的成员之前必须先声明A) - 定义
A类,包括对clearB的友元声明 - 最后定义
clearB
8.7 类的其他特性
8.7.1 成员函数的重载
成员函数可支持重载,参数不同即可以。 1
2
3
4
5
6class Screen{
public:
Screen()=defalut;
char get(char s);
inline char get(char s,size_t i); //成员函数重载
}
8.7.2 内联函数
在类中成为内联函数有两种:
- 在类内定义的(隐式内联)
- 有inline关键字(显示内联)
8.7.3 可变数据成员
在类中,存在const函数防止类内成员在此函数的修改,但有时候对于有些成员我们希望在任何函数都能够得到可变的保证,即使是在const函数总,因此出现了mutable关键字。只有被该关键字修饰的成员变量无论在类的哪个成员函数内都能得到可修改的保证:
1
2
3
4
5
6
7
8
9
10
11class A{
public:
A()=default;
size_t get()const{
acces_time++;
return acces_time;
}
private:
mutable size_t acces_time=0;
};
8.8 聚合类
当一个类满足下述条件使,是聚合类(使用户可以直接访问其成员):
- 所有成员都是public的
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数。
9.IO库
9.1 IO类
IO库设施和主要的类包括:
ostream类型,提供输出操作;istream类型,提供输入操作cin,一个istream对象,从标准输入读取数据;cout,一个ostream对象,向标准输出写入数;cerr,一个ostream对象,用于输出程序错误信息,写入到标准错误>>运算符,用于一个istream对象读取输入数据;<<运算符,用于一个ostream对象写入输出数据getline(),从一个给的的istream读取一行数据,存入一个给定的string对象。getline(cin,s1); 读取内容,直到遇到换行符(换行符也被读取,但s1不存取换行符)。

iostream定义了读写了基本类型fstream定义了读写命名文件的类型sstream定义了读写内存string对象的类型
9.2 条件状态
以前用条件判断语句while(cin>>i); //EOF时结束,即达到了文件结束跳出循环(ctrl+z)。来判断流是否有效,但我们无法知道流的具体状态。所以有如下IO库条件状态。
IO库定义了一个与机器无关的iostate类型。它提供了表达流状态的完整功能(badbit、failbit、eofbit、goodbit:
badbit表示发生系统级的错误,如不可恢复的读写错误。通常情况下一旦badbit被置位,流就无法再使用了--4(代表数字)。failbit表示发生可恢复的错误,如期望读取一个数值,却读出一个字符等错误。这种问题通常是可以修改的,流还可以继续使用---2。- 当到达文件的结束位置时,
eofbit和failbit都会被置位---1。 goodbit被置位表示流未发生错误。如果badbit failbit 和eofbit任何一个被置位,则检查流状态的条件会失败---0。
对应的bad(), fail(), eof(), good()能检查对应位是否被置位,返回1表示被置位。但是,badbit被置位时,fail()也会返回1。所以使用good()和fail()是确定流能否使用的正确方法。实际上,流当做条件使用的代码就等价于!fail()。而且eof()
和bad() 操作只能表示特定的错误.
1 |
|
9.3 管理输出缓冲
输出缓存刷新是指数据数据真正写到输出设备和文件,在C++中导致输出缓存刷新的的原因有:
- 程序正常结束,作为
main函数的retrun操作的一部分,缓存刷新执行 - 缓冲区满使,需要刷新,而后新的数据才能继续写入缓冲区
- 显式调用
endl刷新缓冲区 - 每个输出操作之后,可使用操纵符
unitbuf设置流的内部状态,来清空缓冲区。默认下,对cerr是设置unitbuf的,因此对于cerr来说写入到cerr的内容都是立即刷新的 - 一个输出流可能关联到另一个流。当读写被关联流时,关联到的流的缓冲区会被刷新。比如默认情况下,
cin和cerr都被关联到cout,因此读cin或cerr都会导致cout的缓冲区被刷新
每个输出流都管理一个缓冲区,最重要得是刷新缓冲区:
endl:输出操作后刷新缓冲区(会额外输出换行符)flush:输出操作后刷新缓冲区(没有额外字符)ends:输出操作后刷新缓冲区(额外输出空格符)
9.3.1 nitbuf操作符:每次输出都会flush
1 | cout<<unitbuf; //所有输出操作后会立即刷新缓冲区 |
9.3.2 关联输入输出流
标准库默认将cout于cin关联在一起。手动关联操作tie,有带参和不带参两个版本
1
2
3//不带参:返回指向输出流得指针
//带参:接受一个ostream得指针参数,将对象关联到该输出流
cin.tie(&cerr);
9.4 文件输入输出
- ifstream从一个给定文件读取数据
- ofstream向一个给定文件写入数据
- fstream向给定文件读写
除了继承了iostream类型的行为之外,fstream中定义的类型还增加了新的成员来管理与流关联的文件。如下:

9.4.1 继承关系
因为fstream继承自iostream,ifstream继承自istream,ofstream继承自ostream。所以由继承机制的:派生类(继承类)的对象可以当作其基类(被继承类)的对象来使用。所以在参数为ostream&的函数中允许我们传入一个ofstream型参数,同理其他两个也是一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using namespace std;
int main(){
string s="xxx/xxxx/xxx.txt";
fstream f(s);
func(f); //允许传入一个fstream参数,派生类像基类转换
}
void func(iostream& io){
...
}
9.4.2 open和close
若我们定义了一个空文件流对象: 1
fstream file;
open来将file与相应的文件关联起来
1
file.open(filename); //若调用open失败,则failbit会被置位
close
1
2
3
4
5
6
7
8
9if(file){
....
}
//或
if(!file.failbit){
....
}
//关闭
file.close();ofstream是对写文件,从程序写出到文件,ifstream是从文件读入到程序中。这点不要搞混
9.4.3 文件模式
每个流都有关联的文件模式,用来指出如何使用文件。ofstream关联out、ifstream关联in、fstream关联in和out。

- 只可以对
ofstream或fstream对象设定out模式;只可以对ifstream或fstream对象设定in模式 - 只有当
out被设定时才可设定trunc模式 trunc没被设定,app就可设定。且在app模式下,文件也是out模式下打开(open)- 即使没有设定
trunc模式,out模式打开(open)的文件也会被截断。所以为保留out模式打开的文件,需要指定app模式(或同时指定in模式) ate和binary模式可用于任何文件流,与任何模式组合。
1 | using namespace std; |
out会发生截断是因为每次写操作没有定位到文件末尾,所以防止截断是设定app模式
9.5 string流
与fstream类似,string也分别继承于iostream。istringstream从string读取数据,ostringstream向string写入,stringstream即可写入也可读取。

9.6 常用成员函数
9.6.1 istream的成员函数:
1 | cin.get() //一次只能读取一个字符 |
9.6.2 ostream可用
1 | //通过流成员函数实现格式化的输出 |
9.6.3 fstrem常用
主要读写:其有write()/read()成员函,重载<>运算符
1
2
3
4
5
6
7
8ofstream ofs;
ofs.open("test.txt", ios::out | ios::trunc);
ofs << "姓名:悟空" << endl;
Maker m1("悟空",18);
ofstream ofs;
ofs.open("test.txt", ios::out | ios::trunc | ios::binary);
ofs.write((const char *)&m1, sizeof(Maker));
- ①string类中有一个成员指针char*,该指针指向存储字符串的空间
- ②当我们把string类的数据存储到文件中,再读出来时,不能保证指针有效
10.动态内存
10.1 生命周期
目前的我们接触到的对象或者静态static都有着严格的生命周期:
- 全局:程序启动时自动分配,程序结束时销毁
- 局部对象:进入其所定义的程序时被创建,离开块时销毁
- 静态:第一次使用前分配,程序结束时销毁
上述中的变量只使用了静态内存和栈内存。它们会自动创建和销毁。静态内存保存局部static、类static成员以及定义在任何函数之外的变量。栈内存保存定义在函数内的非static对象
除了上述的自动分配外,c++还支持动态分配对象。(其生命周期与它们在哪创建无关,只有显式的被释放时,这些对象才会被销毁)。它们被分配在内存池,称作自由空间或堆。程序用堆来存储动态分配。
10.2 new动态内存
10.2.1 直接动态内存管理
c++的动态内存管理是通过一对运算符来完成:
new,在动态内存中为对象分配空间并返回一个指向该对象的指针delete,接受一个动态对象的指针,销毁该对象,并释放关联的内存。
不再使用的动态内存应及时释放,否则会造成内存泄漏。释放delete的时机要适宜,否则在还有指针引用内存的时候释放,会导致引用非法内存的指针错误.内存泄漏:分配内存使用完毕后不释放将引起内存泄漏,会榨干内存。
相对于智能指针,直接管理内存的类与使用智能指针的类不同,他们不能依赖类拷贝、赋值和销毁操作的任何默认定义。虽然如此,但有时候我们不得不用的new与delete。在后面我们还会介绍跟高级的内存分配工具allocator类
10.2.2 使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将默认构造函数进行初始化:
1
2string *ps=new string; //初始化为空的string。类类型-->默认构造,等价于与值初始化
int *p=new int; //p指向一个动态分配、未初始化的无名对象。内置类型-->值初始化1
2
3int *pi=new int(20);
string *ps=new string("trluper");
vector<int>* pv=new vector<int> {0,1,2,3,4,5};
- 对于定义了自己的构造函数的类类型(如string),要求值初始化是没有意义的,因为不管采用什么形式,对象都会通过默认构造函数来初始化;
- 对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。
- 对于类中那些依赖于编译器合成的默认构造函数的内置类型对象,如果它们未在类内初始化,它们的值也是未定义的
如果我们提供了一个括号包围的初始化器,就可以使用auto,此时初始化器可以推断我们想要分配的对象的类型,只有当括号中仅有单一初始化器才能使用auto:
1
2auto p1=new auto(obj);
auto p2=new auto{a,b,c}; //错误,括号中只能有单一初始化器
10.2.3 动态分片的const对象
动态分配的const对象必须进行初始化。对于定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化:
1
2const int* pci=new const int(1024); //显示初始化
const string *pcs=new const string; //隐式
10.2.4 内存耗尽
默认情况下,如果new不能分配所要求的内存空间,会抛出一个类型为bad_alloc的异常。我们也可以改变使用new的方式来阻止它抛出异常(称为定位new)。bad_alloc和nothrow都定义在头文件new中:
1
2
3
int *p1=new int;//如果分配失败,会抛出一个类型为`bad_alloc`的异常
int *p2=new(nothrow) int;//如果分配失败,new返回一个空指针
10.2.5 释放内存
delete表达式接受一个指针,指向我们想要释放的对象:delete p。
指针值和delete:传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。虽然一个const对象的值不能被改变,但它本身是可以被销毁的.delete条件有:
- 应与
new配对使用,既只能释放new分配得内存 - 不要再次释放已经释放得内存
- 如果使用
new[]分配动态数组,应用delete[]释放 - 对空指针使用
delete是安全的
不需要再使用该动态分配的内存时,必须释放,否则容易内存泄漏!!以下是两个版本的use_factory函数。(p是已经new分配好的返回指针)
1
2
3
4
5
6
7
8
9
10
11
12
13void use_factory(T *p){
//使用p
....
//函数结束,不再需要,则释放
delete p;
}
T* use_factory(T *p){
//使用p
....
//函数结束,仍然需要,则返回后由调用这释放
return p;
}
10.2.6 动态数组
某些应用需要一次性为很对对象分配内存(如vector)。为了支持这种需求,c++语言和标准库提供两者方法:
- 分配和初始化一个对象数组
- 应用allocator类
通常第二种法方会提供更好的性能和更灵活的管理内存能力,我们将在后面介绍,同时“STL源码剖析”会更详细。这类我们说说new[]
new和数组 1
2
3
4
5
6
7
8
9
10
11
12int *p=new int[size]; //返回的指向第一个元素对象指针
//初始化动态分配对象的数组
//不加括号——默认初始化
int *p=new int[10]; //默认初始化
//大小之后加一对空括号——值初始化
int *p=new int[10](); //10个值初始化为0
//大小之后跟一个花括号列表——初始化器初始化
int *p=new int[10]{1,2,3,4,5,6,7,8};
//释放,使用特殊的delete来释放动态数组,在delete前加上一个空方括号对(方括号必须加上)
int *p=new int[10];
delete[] p;
注意:我们得到的时数组元素的指针,而不是数组的对象,所以我们不能调用标准库函数中的begin()和end(),也不能使用范围for循环。
10.2.7 placement new
placement new相当于C语言中的realloc,在已有空间的基础上,重新分配一个空间,可以不破坏原来数据,也可以把数据全部用新值覆盖。这个操作就是把已有的空间当成一个缓冲区来使用,这样子就减少了分配空间所耗费的时间,因为直接用new操作符分配内存的话,在堆中查找足够大的剩余空间速度是比较慢的。
1
2
3
4
5
6
7//就是在指针p所指向的内存空间创建一个T1类型的对象,但是对象的内容是从T2类型的对象转换过来的,
//就是在已有空间的基础上重新调整分配的空间,类似于realloc函数
template <class T1, class T2>
inline void _construct(T1 * p, const T2& value)
{
new(p) T1(value);
}
10.2.8 三种new(重要)
new存在三种操作符,其含义和应用的场景都不同。在这里我们必须再次提到new operator、operator new和placement new三种new。在前面我们介绍的都是具有构造效果的new operator
new operator指的就是new操作,使用它会经过两个步骤:一是调用::operator new操作符申请内存;二是使用类型的构造函数对内存地址进行构造。‘new operator`操作符不能被重载1
classA* p=new classA(5);
operator new操作符是单纯的申请内存,相当于C当中的malloc函数,operator new可以重载。::operator new和::operator delete前面加上::表示全局,使用时就像malloc1
int *tmp=(int*)(::operator new((size_t)(size*siezeof(int))));
placement new仅仅返回已经申请好内存的指针,它通常应用在对效率要求高的场景下,提前申请好内存,能够节省申请内存过程中耗费的时间
10.3 new operator与C的malloc的比较
首先我们来看operator new生成的源码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
while (__builtin_expect ((p = malloc (sz)) == 0, false))//底层仍然使用malloc
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}
return p;
}malloc,也就不奇怪new的行为像malloc。
这里主要介绍new operator与malloc主要区别如下:
new operator分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。new不仅分配一段内存,而且会调用构造函数,malloc不会。new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。new是一个操作符可以重载,内部实现仍然使用malloc这个库函数。malloc分配的内存不够的时候,可以用realloc扩容。new则能使用replacement new方式来到底realloc功能new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。- 8、申请数组时:
new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc是通过free(p)来释放。
10.4 shared_ptr智能指针
为了更安全地使用动态内存,新标准库提供了两种智能指针。智能指针类似于常规指针,但区别是它负责自动释放所指向的对象:
shared_ptr,它允许多个指针指向同一个对象unique_ptr,独占一个对象(一个指针指向一个对象)weak_ptr,弱引用,指向shared_ptr所过来的对象
上述的三种类型都定义在memory头文件中
10.4.1 shared_ptr类
类似于vector,智能指针也是模板。使用该类的理由有以下几点:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
1 | shared_ptr<T> p1; //p1指向string |

10.4.2 make_shared函数
最安全的分配和使用动态内存的方法是调用该函数。make_shared<T>(args)函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
1
2
3
4
5
6
7
8hared_ptr<int> p3=make_shared<int>(43);
//指向一个值为42的int的shared_ptr
shared_ptr<string> p4=make_shared<string>(10,'9');
//指向一个值为9999999999的string
share_ptr<int> p5=make_shared<int>();
//指向值初始化为0的int
auto p5=make_shared<int>();
//指向值初始化为0的int
10.4.3 shared_ptr的拷贝和赋值和释放
当进行拷贝和赋值操作时,每个
shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。1
2auto p=make_shared<int>(42);
auto q(p); //q是p的拷贝,递增了的计数器。对象此时有俩引用者当给相应的
shared_ptr赋予一个新值,计数器递减,当为0时,自动释放自己所管理的对象。1
2
3
4
5auto p=make_shared<int>(43); //创建shared_ptr,为值43动态分配内存,拷贝
p=r; //给p新赋值r,令他指向了另一个地址,此时
//递增r所指向的引用计数
//递减p原来的指向的对象的引用计数
//若递减后为0,已没有引用者,自动释放
销毁\释放原理:通过一个特殊的成员函数————析构函数完成销毁工作(每个类都有一个析构函数)。析构函数一般用来来释放对象所分配的的资源。shared_ptr的析构函数会递减它所指向对象的引用计数,当为0时,就会销毁对象,释放内存。
- 当对象被销毁时,将递减其引用引用计数并检查它是否为0,如下这个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15shared_ptr<foo> factory(T arg){
//该函数返回shared_ptr
return make_shared<foo>(arg)
}
void use_factory(T arg)
{
shared_ptr<Foo> p=factory(arg);
//为arg用智能指针动态分配内存,达到能够自动释放的目的
}//对象p离开了此作用域被销毁,此时计数减一(此例为0-->释放内存)
void use_factory(T arg)
{
shared_ptr<Foo> p=factory(arg)
//为arg用智能指针动态分配内存,达到能够自动释放的目的
return p; //引用加1,为2
} //此时p减一,但不为0,不释放
10.4.4 shared_ptr的共享数据
到目前为止,我们使用的类中,分配的资源都与对应对象生存期一致。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的:
1
2
3
4
5
6vector<string> v1;
{//新作用域
vector<string> v2={"a","an","the"};
v1=v2; //从v2拷贝元素到v1
} //v2被销毁,其中的元素也被销毁
//v1依然有三个元素V2只是V1的一份赋值过来的值。指向的不是共同地址的数据。(这里的共同是指内存地址是同一个)。为了达到这个目的,shared_ptr就排上了用场-->多个对象共享数据。
10.4.5 定义StrBlob类
下面的是创建一个类模板(实现多个对象共享数据),每个strBlob对象设置一个shared_ptr来管理动态分配的vector。
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
56
57
58
59
60
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
//默认构造函数
StrBlob();
//可变形参构造函数,内元素均为字面值且为strig
StrBlob(std::initializer_list<std::string> il);
//容器大小和判空
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
//添加和删除元素
void push_back(const std::string &t) { data->push_back; }
void pop_back();
//外部获得类的data
std::shared_ptr<std::vector<std::string>>* get(){
return data;
}
//设置data
void set(shared_ptr<std::vector<std::string>>* p){
data=p;
}
//元素访问
std::string &front();
std::string &back();
private:
//声明智能指针data
std::shared_ptr<std::vector<std::string>> data;
//如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
//构造函数
StrBlob::StrBlob():data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il)) {}
//检查函数,i>size,抛出异常
void StrBlob::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
//取头元素
string &StrBlob::front() const
{
check(0,"front on empty StrBlob");
return data->front();
}
//取尾元素
string& StrBlob::back() const
{
check(0,"back on empty StrBlob");
return data->back();
}
//弹出尾部元素
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
定义了该类后,我们创建类的对象,可以通过get函数获得智能指针,通过set赋给类的新对象即可.这样实现了类多个对象的数据共享。这要看很像类的静态成员,但它比静态成员有一个好处就是,当没有对象引用时,会释放,而不像静态成员持续到程序结束时才释放
10.4.6 shared_ptr和new结合使用
我们可以用new返回的指针来初始化智能指针。因为接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式而且使用字面值:
1
2shared_ptr<int> p1=new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); //正确:使用了直接初始化形式shared_ptr的函数不能在其返回语句中隐式转换成一个普通指针:
1
2
3
4
5
6
7shared_ptr<int> clone(int p){
return new int(p); //错误:隐式转换为shared_ptr<int>
}
shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p)); //正确:显式地用int*创建shared_ptr<int>
}delete释放它所关联的对象。
注意:内置指针是指内置类型(如int、char)的指针,一般没有默认构造函数。普通指针是普通类型的指针),一般有默认构造函数

10.4.7 莫交错使用new和shared_ptr
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这么做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。如下列子:
1
2
3
4
5
6
7void process(shared_ptr<int> p){
//空函数,离开时p对象被销毁
}
int *x(new int(24)); //危险:x是一个普通指针,而不是一个智能指针
//process(x); //错误:不能将int*转换成一个shared_ptr<int>
process(shared_ptr<int>(x)); //临时shared_ptr,合法的,但内存会被释放,引用计数变为0
int j=*x; //未定义的:x是一个空悬指针
上述代码中x是一个普通指针,当把x传给process时,报错,因为普通指针不能隐式的转换为智能指针。传入的实参显示转换为智能指针,此时x就将内存交给了shared_ptr管理,当该函数执行完毕时,该指针指针shared_ptr被销毁(x所指向的内存没了),x也就成了空悬指针。
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。而且内置指针很可能成为空悬指针
10.4.8 智能指针和异常
使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
1
2
3
4
5
6
7
8
9
10void f(){
shared_ptr<int> p=make_shared<int>(43);
//如果在这里抛出了异常,其内存也会释放
}
//下面这种清空就不会释放:
void f(){
int *p=new int(43);
//如果在这里抛出了异常
delete p;
}//因为抛出了异常,无法执行delete p语句
10.4.9 删除器
某些类没有定义析构函数,此时我们可以使用shared_ptr来保证该类生成的对象的内存被正确释放,首先定义一个函数(删除器)来代替得delete。下面以连接为例子,destination类是连接信息类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//声明一些类和接口
struct destination; //该类标识我们连接的信息,如端口,地址
struct connnection; //连接类,已连接信息记录
connection connect(destionatin *p); //请求连接,返回一个连接类记录信息
void disconnection(connection); //关闭连接,内含delete操作
//对声明的类和接口定义
....
//定义删除器
void end_connecttion(connection *p){
disconnection(*p);
}
//shared_ptr使用删除
void f(destination &d){
connection c=connect(&d); //返回一个连接类信息
shared_ptr<connection> p(&c,end_connnection); //shared_ptr的用法,自定义删除器
//这样。当f函数退出时,即使是由异常引起的,Connection也会被释放关掉
}
10.4.10 shared_ptr与数组
与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果我们希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器(因为删除是我们默认的是delete而不是delete[]):
1
2
3//为了使用shared_ptr,必须提供一个删除器
shared_ptr<int> sp(new int[10],[](int *p){delete [] p;});
sp.reset(); //使用我们提供的lambda释放数组,它使用delete[]shared_ptr未定义下标运算符,而且智能指针类型不支持指针算数运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素:
1
2for(size_t i=0;i!=10;++i)
*(sp.get()+i)=i; //使用get获取一个内置指针
10.5 unique_ptr
unique_ptr是C++的另一个智能指针,与shared_ptr不同的是,任何时刻,都至多只能有一unique_ptr智能指针指向一个对象,当unique_ptr指针被销毁时,其对象也被销毁。
10.5.1 unique_ptr的初始化
与shared_ptr不同,没有类似make_shared的标准函数返回一个unique_ptr。因此,当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似于shared_ptr(接受参数的构造函数有explicit修饰),所以初始化unique_ptr必须采用直接初始化方式:
1 | unique_ptr<int> p; //定义 |

虽然我们无法拷贝或者赋值,但我们可以通过调用release或reset将指针所有权从一个(const)unique_ptr转移给另一个unique_ptr:
1
2
3
4unique_ptr<string> p2(p1.release()); //p1转移给p2,p1置空
unique_ptr<string>p3(new string("Hello"));
//p3转移给p2ertyui
p2.reset(p3.release()); //p2释放原来的,p3被置空,p2指向的p3的release函数会切断智能指针和它原来管理的对象的联系,它返回的指针通常用来初始化另一个智能指针或给另一个智能指针赋值。
如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
1
auto p=p2.release(); //后面程序应该有delete(p);操作
10.5.2 向unique_ptr传递删除器
和shared_ptr一样,unique_ptr默认情况(源代码)使用delete释放它指向的对象。我们可和shared_ptr一样重载一个unique_ptr中的删除器。
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象:我们必须在尖括号中unique_ptr指向类型之后提供删除器类型,即在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)
1
2
3
4
5
6
7
8
9
10//p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
//它会调用一个名为fcn的delT类型对象
unique_ptr<objT,delT> p(new objT,fcn);
void f(desitination &d){
connection c=connect(&d); //返回一个连接类信息
//当p被销毁时,连接会关闭
unique_ptr<connection,decltype(end_connection)*> p(&c,end_connnection);
//这样。当f函数退出时,即使是由异常引起的,Connection也会被释放关掉
}decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。
1
2auto varName=value;
decltype(exp) varName=value;
auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟``=右边的value没有关系auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导- 而
decltype不要求,因此可以写成如下形式
1 decltype(exp) varName;
10.5.3 指向数组的unique_ptr
标准库提供了一个可以管理new分配的数组的unique_ptr版本。使用unique_ptr管理动态数组时,我们必须在对象类型后面跟一对方括号,下面是用法介绍:

1 | unique_ptr<int[]> up(new int[10]); |
另外一方面,当一个unique_tr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:
1
2for(size_t i=0;i!=10;++i)
up[i]=i; //为每个元素赋予一个新值
10.6 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,是弱用智能指针,它指向一个有shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变share_ptr的引用计数。
当我们创建一个weak_ptr时,我们要用以shared_ptr初始化它:
1
2auto p=make_shared<int> (43);
weak_ptr wp(p); //wp若共享p,p的引用计数不变weak_ptr直接访问对象,必须调用lock()函数!该函数会检查weak_ptr指向的对象是否存在,若存在,则返回一个指向共享对象的shared_ptr。如:
1
2
3
4if(shared_ptr<int> q=wp.lock())
{
//使用q访问对象
}
10.7 allocator类
在前面我们主要介绍了new,delete和智能指针。但他们分配的内存不是原始的,它们在分配的时候要对内存进行构造。标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

10.7.1 allocate:分配未构造内存
1 | allocator<string> allco; //定义可以分配string的allocator对象 |
alloc完成了分配n个string的连续内存的工作,并且返回一个指向这一块内存的首地址给指针p。我希望p记住这个首地址在哪免得我后边找不到了,所以把它设为const的。
10.7.2 construct:创建对象
下面我让alloc为我在这些内存上构造对象:alloc.construct(内存地址,参数......),括号里的“参数”是给我这块内存的对象类型的构造函数的参数,比如这里对于string,可以这样:
1
2
3string *S=p; //将首地址给S
alloc.constrcut(S,10,'A'); //该内存构造string "AAAAAAAAAA"
S++; //把内存地址往后挪,以便后续的构造1
2cout<<*p<<endl; //正确,p是指向首地址
cout<<*S<<endl; //错误,还没构造
10.7.3 destroy:摧毁对象
当我们用完对象后,必须对每个构造的元素调用destory来摧毁它们。我们只能对真正构造了的元素进行destory操作,而且只有摧毁的内存或未构造的内存才能被deallocate回收。destroy参数接受一指针,对指向的对象执行析构函数:
1
2while(q!=p)
alloc.destroy(--q);1
alloc.dealloccate(p,n); //p必须是allocate返回的指针,n必须是分配时指定的n
10.7.4 拷贝和填充未初始化的内存算法
allocator还有两个伴随算法,可以在未初始化内存中创建对象。它们都定义在头文件memory中。
1
2
3
4
5
6//分配动态内存
auto p=alloc.allocate(v.size()*2);
//拷贝vi的元素到未构造内存,返回下一个未构造地址
auto q=uninitialized_copy(vi.begin(),vi.end(),p);
//将剩余空间构造为42
uninitialized_fill_n(q,vi.size(),42)
11.拷贝控制
如何控制类型对象拷贝、赋值、移动和销毁,有对应的五种特殊成员函数来控制这些操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数
11.1 拷贝构造函数
11.1.1 拷贝构造函数
个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值
1
2
3
4
5class foo{
foo(); //默认构造函数
foo(const foo&); //拷贝构造函数
.....
}const的(虽然可以不为const),拷贝构造函数在几种情况下都会被隐式的调用,因此拷贝构造函数通常不应该是explicit(explicit修饰的不能拷贝初始化,只能直接初始化,不能隐式转换类类型)。
11.1.2 合成拷贝构造函
如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义默认的拷贝构造函数(即使有其他的构造函数)。但有些类,合成的拷贝构造函数用来阻止我们拷贝该类类型的对象。但一般情况下,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中(从给定对象依次将每个非static成员拷贝到正在创建的对象中)。 如何拷贝:
- 对类类型(需要include<>的就为类类型)的成员,会使用其拷贝构造函数来拷贝
- 内置类型的成员则直接拷贝
- 对数组,合成拷贝构造函数会逐元素拷贝数组类型成员
1 | class Sales_data{ |
11.1.3 拷贝初始化
- 使用
=进行的初始化是拷贝形式的初始化,编译器将等号右边的初始值拷贝到新创建的对象上去。这时将调用对象定义的拷贝构造函数进行操作,只要这些构造函数满足这样的调用就会被隐式的调用。但是被explicit修饰的拷贝构造函数将会禁止该构造函数进行这样隐式的调用(因为可能由隐式类型转换)。 - 不使用
=进行初始化的操作就是直接初始化,采用()直接进行初始化。这里调用的构造函数就是直接使用对应的构造函数进行初始化。explicit修饰的构造函数能用于直接初始化。这种形式没有什么限制。
总结: 直接初始化:根据提供的参数选择最匹配的构造函数 拷贝初始化:右侧运算对象拷贝到正在创建的对象中,通常由拷贝构造函数完成。
11.1.4 拷贝构造函数的参数必须是引用
因为在函数调用中,非引用类型的的参数要进行拷贝初始化;函数返回一个非引用类型,调用方的返回结果也是一个拷贝,所以拷贝函数此时被用来初始化非引用类型的数据。如果拷贝构造函数的参数不是引用类型都是类型形参,为获得它的实参,那么它自身就会无限的调用自身的死循环。P(442)
11.1.5 构造函数不能为虚函数
我们知道,每个有虚函数的类都有属于自己的虚函数表vtbl,虚函数表在编译器构建好。当我们的派生类重写了虚函数,那么在虚表中就会替换掉父类的虚函数指针为子类的虚函数指针。当我们创建一个对象时,会在对象的内存模型中有自己的指向虚表的指针vtpr,对象通过虚表才知道调用的是哪一版本的虚函数
如果构造函数是虚函数,那么也会如上面的机制一样在虚表有一个指向自己构造函数版本的虚函数指针。现在有两个类A和B,B是A的派生类,在构造B的对象的时候发现继承于A的部分要先构造,它就要求调用A的构造函数,但是这些构造函数已经是虚函数了,而虚表指针必须是在构造一个对象的时候分配了内存才能得到,而你有调用不到构造函数,这就陷入了一个矛盾的处境:你要用虚表指针vtpr去调用构造函数,而虚表指针vtpr只有当年调用构造申请了内存后才能得到,这形成了一个死结。
附加:vptr的初始化工作早于构造函数中的初始化列表
11.2 拷贝赋值运算符
与类控制对象如何初始化一样,类也可以控制对象如何赋值:
1
2Sales_data trans,accum;
trans=accum; //使用Sales_data的拷贝运算符
11.2.1 重载赋值运算符
重载运算符的本质是函数。其名由operator关键字接要定义的运算符组成。赋值运算符operator=的函数,其也有返回类型和参数(参数表示要运算的对象),运算符如果是成员函数,则运算对象就绑定在隐式的this指针上。
1
2
3
4class foo{
public:
foo& operator=(const foo&); //赋值运算符
}
11.2.2 合成拷贝赋值运算符
与拷贝构造函数一样,在没有定义自己的拷贝运算符时,编译器会生成一个合成拷贝赋值运算符。同样,对于某些类,合成拷贝赋值运算符会禁止该类型对象的赋值,如果不是此目的,它会赋值(非static)。
11.3 析构函数
析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数时类的成员函数,名字由波浪号~接类名构成,没有返回值,也不接受参数,既不能重载但一般定义未虚函数重写,类只有一个析构函数:
1
2
3
4class foo{
public:
~foo(); //析构函数
}
11.3.1 定义析构函数
析构函数同构造函数一样,也由一函数体和一个析构部分。在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。析构部分是隐式的,销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数,因此销魂内置类型成员什么也不需要做(隐式的)。
- 隐式销毁一个内置指针类型的成员不会delete它所指向的对象
- 智能指针式类类型的,所以具有析构函数,所以智能指针在析构阶段被自动销毁
什么时候调用析构函数:无论何时一个对象被销毁,就会自动调用其析构函数:
- 变量离开作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器被销毁时,其元素被销毁
- 对于动态分配对象,当指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
由于析构函数的自动允许,我们程序可以按需要分配资源,无需担心何时释放这些资源。
1
2
3
4
5
6
7
8
9{//语句块作用域
Sales_data* p=new Sales_data; //p是一个普通指针
auto p2=make_shared<Sales_data>(); //p2是一个智能指针
Sales_data item(*p); //使用拷贝构造
vector<Sales_data>vec;
vec.push_back(*p2);
delete p; //释放p
//离开作用域后,p2的计数归0,自动释放
}
11.3.2 合成析构函数
当一个类未定义析构函数时,编译器会为它定义一个合成析构函数。同时同拷贝构造函数、拷贝赋值运算符一样,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。析构函数体自身不直接销毁成员,而是在析构函数体之后隐含的析构阶段中被销毁
11.4 C++对这些构造函数的法则
目前为止介绍了三个拷贝控制操作:拷贝构造函数、拷贝赋值运算符、析构函数,知道我们若是不显示定义,编译器会自动合成默认的拷贝构造和赋值构造函数以及析构函数。c++新标准还引入了:一个类还可以定义一个移动构造函数、一个移动赋值运算符。对于它们,编译器有自己的一套法则
11.4.1 三/五法则
对于这些构造函数,我们有时候不必全部定义,有如下的法则:
- (1)需要析构函数的了也需要拷贝和赋值操作
- (2)如果类需要一个析构函数,那么几乎可以肯定它也需要拷贝构造函数和拷贝赋值运算符
- (3)需要拷贝操作的类也需要赋值操作,反之亦然
总结:即一般来说类中一般以三
11.4.2 =default:默认合成
同默认合成构造函数一样,可以用=default来显示要求合成拷贝构造函数,默认为内联的。若不要内联,则在类的外部定义为=default
1
Sales_data(const Sales_data&)=default;
11.4.3 =delete:阻止拷贝
上面我们总是讲到有些拷贝构造、赋值构造和析构会阻止它们的应用功能,这就是用=delete定义的函数。虽然大多数类定义了拷贝构造函数和拷贝赋值运算符,但对于某些类,这些操作没有实际意义,如iostream类阻止了拷贝以避免多个对象写入或读取相同的IO缓冲。所以这种情况就要阻止拷贝的发生。在新标准之前(=delete前),类是通过将其拷贝构造函数和拷贝赋值运输符声明为private来阻止拷贝:
1 | class A{ |
新标准下,我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
删除的函数:我们声明了它,但不能以任何方式使用它,我们在相应的拷贝构造和拷贝运算符后加=delete即可
1
2
3
4
5
6class mm{
mm()=default; //合成构造函数
mm(const mm&)=delete; //阻止拷贝
mm &operator=(const mm &)=delete; //阻止赋值
~mm(); //析构函数
}default不同:
delete必须出现在函数第一声明的时候;- 还有一点不同的是我们可以对任何函数
=delete,而default只能是默认合成构造函数和拷贝控制成员,但一般而言,对于析构函数我们不应该使用=delete(因为这样我们就无法销毁类型对象了)
11.4.4 合成的拷贝控制成员可能是删除的
正如前面所将的,合成的拷贝控制操作可能是阻止类型的:
- 如果类的某个成员的析构函数是删除或不可访问的(如
private),则类的合成析构函数被定义为删除的 - 如果类的某个成员的拷贝构造函数是删除或不可访问的,则类的合成拷贝构造函数被定义为删除。同样,析构函数是删除或不可访问,合成拷贝构造函数被定义为删除
- 如果类的某个成员的拷贝赋值运算符是删除或不可访问的,或者类有一个
const的或者引用的成员,则类的合成拷贝赋值运算符被定义为删除 - 如果类的某个成员的析构函数是删除或不可访问的、或者类有一个引用成员,它没有类内初始器、或是类内有一个
const成员,他没有类内初始器且其类型为显示定义默认构造函数,则该类的默认构造函数被定义为删除的
即:如果一个类有数据成员不能默认构造、拷贝、赋值或销毁,则对应的成员函数被定义为删除
11.5 对象移动
新标准一个最主要的特征是可以移动而非拷贝对象的能力。使用移动而不是拷贝:
- ①在对象较大时,进行拷贝代价很高。
- ②对于像IO类和unique_ptr这些类,这些类都包含了不能被共享的资源,因此不能拷贝但可以移动。
该特性主要针对这样一种场景:一个对象在被拷贝之后就不在使用了或者马上就会被析构掉,这种情况下,使用移动操作而非拷贝操作将会大幅度提升性能。移动操作的思想是接管源对象的内容。
11.5.1 右值与左值
- 左值:返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式。
- 右值:右值要么是字面常量,要么是在常量表达式求职过程中创建的临时对象。
左值持久;右值短暂。
11.5.2 右值引用和左值引用
为了支持移动,新标准引入了新类型的引用——右值引用。通过&&获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此我们可以将一个右值引用资源“移动”到另一个对象去。不难知道,不管是左值引用还是右值引用都是变量的一个别名:
- 左值引用:不能将其绑定到要求转换的公式、字面常量或者是返会右值的表达式,即左值引用是绑定对象(左值)的。
- 右值引用:与左值引用恰恰相反,可绑定右值,但不能绑定左值
1 | int i=42; |
由于右值引用只能绑定到临时对象,我们可以知道:①所引用的对象将要被销毁,②该对象没有其他用户。这两个特性意味着使用右值引用的代码可以自由地接管所引用的对象的资源。
记住变量是左值,我们不能将右值引用绑定到变量上,即使这个变量本身是右值引用也不行!
1
2int &&r1=42; //正确
int &&r2=r1; //错误
11.5.3 move函数
新标准提供了一个函数解决右值引用无法绑定左值的函数,通过调用新标准库函数move来获得绑定到左值的右值引用。该函数定义在头文件utility
1
int &&r2=std::move(r1); //可行
move之后,我们可以销毁一个移后员对象,也可以赋予它新值,但不能使用一个移后源对象的值。
有了这些知识我们就能看接下来的移动拷贝构造函数和移动赋值运算符。类似string类,自定义的类支持移动和拷贝,会十分方便。它们从指定对象“窃取”资源,而不是拷贝资源。
11.5.4 移动拷贝构造函数
类似于拷贝构造函数,第一个参数是该类类型的右值引用,其他额外参数必须有默认值。移动构造函数需要确保移后源对象处于这样一个状态:销毁它是无害的。
1
2
3
4
5
6
7StrVec::StrVec(StrVec &&s) noexcept //移动操作不应抛出任何异常
//成员初始化器接管s中的资源
:elements(s.elements), first_free(s.first_free), cap(s.cap)
{
//另s进入这样一个状态——对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}nullptr,这样就完成了移动操作。此源对象继续存在(但已经没有管理任何内存),当允许其析构函数时,源对象被销毁。注意:不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
为什么要声明
noexcept虽然移动操作通常不会异常抛出,但抛出异常时允许的。而且标准库容器能对异常发生时其自身的行为提供保障。>noexcept它告诉编译器该函数不会抛出异常,否则编译器会认为移动操作可能会发生异常,并且为了处理这种可>能性做一些额外的工作。
11.5.5 移动赋值运算符
移动赋值运算符指向析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
1
2
3
4
5
6
7
8
9
10
11
12
13StrVec& StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs)
{
free(); //释放已有元素
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
11.5.6 移动操作的要求
- 移动源对象必须可析构:当我们编写一个移动操作,必须保证移动源进入一个可析构状态,如上面的例子中,我们将移动源数据置为
nullptr。其次,还应保证,对象移动后还是有效的,既可以重新赋值。 - 合成的移动操作:只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时(指针型),编译器才会为它合成移动构造函数或移动赋值运算符。
- 移动右值,拷贝左值:在一个类中,既定义了移动又定义了拷贝,编译器使用匹配规则进行匹配。在定义了拷贝但没有定义移动,右值也使用拷贝构造函数。
1 | //编译器会为X和hasX合成移动操作 |
移动操作永远不会隐式定义为删除的函数。如果我们显示地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。移动构造函数被定义为删除的函数的条件是:
- 有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者有类成员未定义自己的拷贝构造函数且编译器不能为其合成构造函数。移动赋值运算符的情况类似。
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
- 如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
- 如果有类成员是
const或是引用,则类的移动赋值运算符被定义为删除的。
1 | //假定Y是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数 |
如果类定义了一个移动构造拷贝和/或一个移动赋值运算符,该类的合成拷贝构造函数和拷贝赋值运算符将会被定义为删除的。因此定义了一个移动构造函数或者移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员都被默认为删除的。(三/五法则)
11.5.7 移动迭代器
新标准库定义了一种移动迭代器适配器。移动迭代器的解引用生成一个右值引用(其他的迭代器一般时指向元素的左值)。
通过调用标准库函数的make_move_iterator函数将以普通迭代器转换为移动迭代器。移动迭代器支持正常迭代器的工作。但值得注意的是只有在确定算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,能可以使用移动迭代器。否则使用移动迭代器销毁了原来数据,还想使用是错误的行为。
11.6 实际运用
在我们已经运用的类中,如string、标准库容器它们的对象行为像一个值,即我们拷贝这些类的对象是,副本和原对象是独立的,互不影响。
但像shared_ptr类,这些智能指针的对象的行为像一个指针,副本和原对象使用相同的底层数据,即数据共享,它们之间会互相影响。
11.6.1 行为像值的类
这种行为的类,它们得对象都有着自己的一份数据: 1
2
3
4
5
6
7
8
9
10
11
12class HasPtr{
public:
HasPtr(const string& s=string())
:ps(new string(s)),i(0){} //ps指向自己分配的空间,构造函数
HasPtr(const HasPtr& p)
:ps(new string(*p.ps)),i(p.i){} //拷贝构造函数
HasPtr& operator=(const HasPtr &); //赋值运算符
~HasPtr(){delete ps;} //析构函数
private:
string *ps;
int i;
};string,以及分配自己栈内存给整型i;赋值运算符通常组合了析构函数和构造函数的操作,我们编写的赋值运算符还应该是异常安全的--当异常发生时,仍能将左侧运算对象置于一个有意义的状态。
1
2
3
4
5
6
7HasPtr& HasPtr::operator=(const HasPtr& p){
auto newp=new string(*p.ps);
delete ps;
ps=newp;
i=p.i;
return *this;
}string,再将newp的管理权给它。在编写赋值运算符时,需要注意:
- 如果将一个对象赋予它自身,赋值运算符必须能正确工作
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
一个好的赋值运算符,往往会借助一个局部零时对象拷贝,如上的newp。(就如在上面的程序中,如果p和this是同一个对象,如果不借助中间newp而直接delete ps将会发生错误)。你也可以增加判断语句if(this!=p)避免增加一个零时量
11.6.2 行为像指针的类
令一个类展现类似指针行为的最好方法是shared_ptr来管理类中的资源,拷贝一个shared_ptr会拷贝(赋值)所指向的指针,共享数据,只要当share_ptr引用计数为0时才会释放资源。
另一个方法是不用shared_ptr,而是设计自己的引用计数,这种情况下,我们可以自己直接管理资源
引用计数:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还有创建一个引用计数,用来记录多少对象与正在创建的对象共享数据状态。
- 拷贝构造函数不分配计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户共享
- 析构函数递减计数器,指出gong共享状态的用户少了一个。若为0,则释放
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧。若左侧为0,则销毁释放。
怎么存放计数器:将计数器保存在动态内存中,当创建一个对象时,我们分配一个计数器。当拷贝或赋值对象时,我们拷贝指向计数器的指针。使用这种方法,副本和原对象都会指向相同的计数器
1 | class HasPtr{ |
12.重载运算与类型转换
当运算符被用于类类型对象时,c++语言允许我们为其指定新的含义,同时我们也能自定义类类型转换规则。重载运算符:
它的名字由operator和其后要定义的运算符共同组成,它们的参数数量应该与运算符作用的运算对象数量一样多。
调用时左侧元素对象传递给第一个参数(若为成员函数则传给隐式this指针),右侧运算对象传递给之后的参数。重载不改变运算符原有的优先级
不能重载运算对象全为内置类型的运算符,既一定要包含至少一个类类型参数。
1
int operator+(int,int); //错误

12.1 输入和输出运算符
类需要定义适合其对象的新版本以支持IO操作,方便!
12.1.1 重载输出运算符<<
通常情况下第一个形参是非常量对象ostream的引用,之所以是非常量是因为向流写入对象会改变其状态,第二个参数(要IO操作的)是常量的引用.返回ostream对象。
- 输出运算不要考虑格式化操作,尤其不会打印换行符,应当主要负责打印对象的内容。
- 与
iostream兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数,否则左侧的运算对象是我们类的一个对象。因此如果想为自定义类重载IO运算符就要定义成非成员函数,但是为了读取私有数据成员,常声明为友元
1 | ostream& operator<<(ostream& os,const Sales_data &item){ //非成员函数 |
12.1.2 重载输入运算符>>
输入运算符的第一个形参是将要读取的流的引用,第二个形参是将要读取到的对象的引用,返回流的引用.输入运算符负担从流中读取数据到对象的工作,需要注意的是,应当处理输入可能失败的状态:
1
2
3
4
5
6
7
8
9istream& operator>>(ostream& is, Sales_data &item){ //非成员函数
double price; //临时变量
is>>item.units_sold>>item.bookNo>>price; //读取
if(is) //检查输入是否成功
item.revenue=item.unit_sold*price;
else //失败,则置为默认状态
item=Sales_data();
retuen is;
}
12.2 算术和关系运算符
我们把算术和关系运算符定义为非成员函数来允许对左侧或右侧的运算对象进行转换,因为不需要改变运算对象的状态,形参都是常量的引用。
算术运算符计算它的两个对象并得到一个新值,有区别于任意一个运算对象,位于一个局部变量内,操作完成后返回该局部变量的副本
1
2
3
4
5Sales_data operator+(const Sales_data& lhs,const Sales_data&,rhs){
Sales_data sum=lhs;
sum+=rhs;
return sum;
}
12.2.1 相等运算符
C++中的相等运算符应当比较每一个数据成员,当对应的成员都相等时才认为两个对象相等,所以我们的相等运算符不但应当比较bookNo,还应当比较具体的销售数据。
1
2
3
4
5
6
7
8bool operator==(const Sales_data& lhs,const Sales_data& rhs){
return lhs.isbn()==rhs.isbn()&&lhs.units_sold()==rhs.units_sold()
&&lhs.revenue()==rhs.revenue();
}
bool operator!=(const Sales_data& lhs,const Sales_data& rhs){
return !lhs==rhs;
}
- 显然如果一个类有判断两个对象是否相等的操作,我们应当重载运算符而不是新增函数,更容易使用
- 定义了
operator==应当能判断一组给定的对象中是否有重复数据 - 相等运算符应当具有传递性,既
a==b、b==c为真,那么a==c也该为真 - 定义了
operator==则应该定义operator!=并且可以利用已经重载的运算符来实现另一个运算符(如上例)
12.2.2 关系运算符
关系运算符应当定义顺序关系,令其与关联容器中对关键字的要求一致 , 如果类同时含有==运算符的话,应当定义关系与其保持一致,如果两个对象是
!=的,那么一个对象应当<另外一个需要指出的是,
Sales_data是不存在逻辑可靠的<定义的,首先,我们不能只比较ISBN,如果ISBN相同但revenue和units_sold是不相等的,但一个对象units_sold大,一个revenue大,所以一个对象并不比另一个小(任意对象不比另一个小,按道理讲这两个对象是相等的),但对象其实又不是相等的,所以逻辑会出现问题。所以像这种类不定义<比较好
12.3 赋值运算符
将类的一个对象赋值给另一个对象,类也可以定义其他赋值运算符来使用a值运算符:它可以使用别的类型作为右值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class StrVec{
public:
StrVec& operator=(Strvec& li){
if(this!=&li){
this.elements=li.elements;
return this;
}
}
StrVec& operator+=(Strvec& li){
if(this!=&li){
this.elements+=li.elements;
return this;
}
}
};
12.4 下标运算符
下标运算符必须是成员函数,通常以所访问的元素的引用作为返回值,进一步,最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,返回常量引用来确保不会对返回的对象赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class StrVec{
public:
string& operator[](size_t index){
if(elements.size()<index-1)
return "index is out of range";
else
return elements[index];
}
string& operator[](size_t index)const{
if(elements.size()<index-1)
return "index is out of range";
else
return elements[index];
}
};
12.5 递增和递减运算符
在迭代器类中通常会实现递增运算符++和递减运算符--,着两种运算符使得类可以在元素序列中前后移动,建议将其设为成员函数。其有前置和后置版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class A{
public:
A()=default;
A& operator++();
A& operator--();
A& operator++(int); //后置版本
A& operator--(int); //后置版本
private:
int num;
}
A& operator++(){
++num;
return this;
}
A& operator--(){
--num;
return this;
}1
2
3
4
5
6
7
8
9
10
11A& operator++(int){
A* ret=this;
++*this;
return *ret;
}
A& operator++(int){
A* ret=this;
--*this;
return *ret;
}
12.6 成员访问运算符
在迭代器类和智能指针类中,常常会用到解引用运算符*和箭头运算符。类成员访问运算符*和->可以被重载,但它较为麻烦。它被定义用于为一个类赋予"指针"行为。运算符->
必须是一个成员函数。如果使用了->
运算符,返回类型必须是指针或者是类的对象。
运算符 -> 通常与指针引用运算符 *
结合使用,用于实现"智能指针"的功能。这些指针是行为与正常指针相似的对象,唯一不同的是,当您通过指针访问对象时,它们会执行其他的任务。比如,当指针销毁时,或者当指针指向另一个对象时,会自动删除对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class A{
class B {
public:
B(vector<int> vec) :vec(vec) {};
int operator*() { //返回类型
return this->vec[current++];
}
int operator->(){
return this->operator*();
}
private:
vector<int> vec;
int current=0;
};
int main(){
vector<int> vec{ 1,2,3,4,5 };
vector<int>* ve = &vec;
B m(vec);
*m;
B* x = &m;
x->operator->();
}->的重载比较特别,它只能是非静态的成员函数形式,而且没有参数。如果返回值是一个原始指针,那么就将运算符的右操作数当作这个原始指针所指向类型的成员进返回;如果返回值是另一个类型的实例,那么就继续调用这个返回类型的
operator->()
,直到有一个调用返回一个原始指针为止,然后按第一种情况处理。
既然->
的重载可以返回一个类型的实例而非指针,那如果返回本身的类型呢,它会继续调用自己的operator->(),永无止尽
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Joke {
int i;
Joke& operator->() {
return *this;
}
};
int main() {
Joke j;
std::cout << j->i;
}j->i 会导致自身的 operator->()
被无限调用。但编译器不是傻子,在使用GCC 4.8.2编译的时候,直接报错:
1
error: circular pointer delegation detected
12.7 函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类得对象成员函数:
1
2
3
4
5
6
7class absInt{
int operator() (int value){
return value<0?-value:value;
}
//使用:
absInt abs;
int ui=abs(-40); //对类对象像调用函数一样调用
12.8 类型转换运算符
在讲类的时候,提到单个参数的构造函数定义了一种隐式类型转换。这里,我们通过类型转换运算符和转换构造函数共同定义类类型转换,也被称为用户定义的类型转换
12.8.1 类型转换运算符
是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型。operator type()const该运算符没有显式的返回类型,形参,必须定义成类的成员函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class SmallInt {
public:
SmallInt(const int& i = 0) :val(i) { //转换构造函数
if (i < 0 || i>255)
throw out_of_range("Bad_int_value");
}
int aadd(const SmallInt& s) { return val + s.get(); }
int get()const { return val; }
operator int()const { return val; } //类型转换运算符
private:
size_t val;
};1
2
3
4
5
6 int main() {
SmallInt si; //构造函数 si=0,si为Smallint对象
si = 4; //合成默认赋值运算符 si=4,si为Smallint对象
int i=si+3; //调用类型转换运算符,si转换为int
si.aadd(i); //会调用单参const构造函数构造零时对象,后在调用aadd
}
类型转换运算符可能有意外结果,因为如果类型转换自动发生,用户可能感觉意外。我们经常会定义向bool的转换,但是类类型的对象转换为bool后就能被用在任何需要算术类型的地方int i=42; cin<<i;会造成cin转换为bool类型,然后左移位i个位置。为防止这样的情况发生,C++11=-引入了显式类型转换运算符:
1
2
3
4explicit operator int()const { return val; } //类型转换运算符
//显示调用
int i=si.operator int() + 3; //si转换为intif while do的条件部分,for语句头的条件表达式,逻辑与或非,显式类型转换将会被隐式执行。
12.8.2 避免二义性的类型转换
需要确保在类类型和目标类型之间只存在唯一一种转换方式,否则代码会有二义性。
- 比如两个类提供了相同的类型转换方式,或者一个类定义了多个转换运算符。存在二义性,就必须显式地调用类型转换运算符或者转换构造函数。
- 如果定义了多个参数都为算术类型的构造函数和对应类型转换运算符,有可能会产生二义性,原因是在隐式类型转换时,标准类型转换级别一致,这决定了编译器选择最佳匹配的过程。如果转换级别有一个更高,则不会出现二义性错误
13. 函数对象
13.1 lambda是函数对象
我们编写一个lambda后,编译器将表达式翻译成一个未命名类的未命名对象,这个类中有一个重载的函数调用运算符。如下面这个lambda:
1
sort(vec.begin(),vec.end(),[](const string&a,const string&b){return a.size()<b.size();});
lambda不能改变它捕获的变量,因此在默认情况下lambda生成的类当中的函数调用运算符是const成员函数:
1
2
3
4
5
6class shorter{
public:
bool operator()(const string&a,const string&b)const{
return a.size()<b.size();
}
};
13.2 生成类对于lambda的值捕获与引用捕获的不同
当lambda表达式通过引用捕获变量时,程序确保lambda执行引用时所引用的对象确实存在,编译器可以直接使用该引用而无需再lambda产生的类中将其存储。但是通过值捕获时,在lambda生成的类中需要为值捕获的变量生成数据成员,创建构造函数:
1
auto w=find_if(vec.begin(),vec.end(),[sz](const string&a,){return a.size()>=sz;});
lambda值捕获sz,则其产生的类将形如:
1
2
3
4
5
6
7
8class Sizecmp{
public:
Sziecamp(size_t n):sz(n){}
bool operator()(const string&a)const{
return a.size()>=sz;
private:
size_t sz;
};
13.3 标准库定义的函数对象
标准库定义了一组表示算术、关系、逻辑运算符的类,都被定义成模板的形式,可以为其指定具体的应用类型即调用运算符的形参类型。
1
2plus<int> intadd;
int sum=intadd(10,20);1
2//使用greater代替默认的,此时执行降序排序
sort(vec.begin(),vec.end(),greater<string>);greater<string>类型的一个未命名的对象,需要注意的是,标准库定义的函数对象也适用于指针,但不能用大于小于号而是用less函数对象
13.4 可调用对象与function
function函数是一直通用、多态的函数封装,它的实例可以对任何可以调用的目标进行存储、赋值和调用操作。是C++现有的对可以调用实体的一种安全的包裹,简而言之,function就是可调用对象的容器。
进一步讲,function模板类就是为了解决lambda表达式的存入问题,因为每个lambda有它自己的类型,该类型会与map中的定好的值的类型不匹配,步骤:
- C++的可调用对象有:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类,其实可调用对象也有类型,每个
lambda有它自己唯一的类类型,函数及函数指针的类型由其返回值和实参类型决定 - 不同类型的可调用对象可能共享同一种调用形式,指明了返回类型和传递给调用的实参类型。例如普通函数加、
lambda表达式减和函数对象类除法的调用方法都为:mod(a,b)为了利用这些可调用对象,可以定义一个函数表来存储指向这些可调用对象的指针。 - 用运算符符号的
string对象作为关键字,用实现运算符的函数作为值构建运算符到函数指针的映射map<string, function<int(*)(int,int)>> binops; - 再利用
function<T> f来创建可放入容器中的类型,无论函数指针,lambda表达式,函数对象、类的对象都可保存,但也不能把重载函数的名字直接存入map中,而是要利用函数指针或者lambda表达式;
1 | int add(int i,int j){return i+j;} |

14.容器概述
14.1 STL提供了六大组件
- 容器:各种数据结构,如
vector、list、deque、set、map等,用来存放数据 - 算法:各种常用的算法(冒泡,排序),如
sort、find、copy、for_each - 迭代器:扮演了容器与算法之间的胶合剂(类似于指针等)
- 仿函数:行为类似函数,可作为算法的某种策略
- 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西
- 空间配置器:负责空间的配置与管理。注意一般都伴随着重新分配空间,那么原来的迭代器就会失效
STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。
14.2 三大重点组件
容器有序列式容器和关联式容器:
- 序列式容器:序列式容器就是容器元素在容器中的位置是由元素进入容器的时间和地点来决定
- 关联式容器:关联式容器是指容器已经有了一定的规则,容器元素在容器中的位置由容器的规则来决定
算法分为质变算法和非质变算法:
- 质变算法::是指运算过程中会更改区间内的元素的内容
- 非质变算法:是指运算过程中不会更改区间内的元素内容
迭代器
- 输入迭代器:提供对数据的只读访问
只读,支持
++、==、!= - 输出迭代器:提供对数据的只写访问
只写,支持
++ - 前向迭代器:提供读写操作,并能向前推进迭代器
读写,支持
++、==、!= - 双向迭代器:提供读写操作,并能向前和向后操作
读写,支持
++、--, - 随机访问迭代器:提供读写操作,并能在数据中随机移动
读写,支持
++、--、[n]、+n、-n、<、<=、>、>=
重点学习双向迭代器和随机访问迭代器
- 双向迭代器:++,--可以访问下一个元素和上一个元素(list、forward_list、关联容器set/map)
- 随机访问迭代器:+2,可以跳2个元素访问元素、下标访问(vector、deque、string、array)
三大组件的关系:容器存储数据,并且提供迭代器,算法使用迭代器来操作容器中的元素
14.3 STL优点
- STL是 C++的一部分,因此不用额外安装什么,它被内建在你的编译器之内。
- STL的一个重要特点是数据结构和算法的分离。尽管这是个简单的概念,但是这种分离使得
STL 变得非常通用。例如:在 STL
的
vector容器中,可以放入元素、基础数据类型变量、元素的地址;STL 的sort()排序函数可以用来操作ector,list等容器。 - 程序员可以不用思考 STL 具体的实现过程,只要能够熟练使用 STL 就行了。这样他们就可以把精力放在程序开发的别的方面。
- STL 具有高可重用性,高性能,高移植性,跨平台的优点。
高可重用性:STL 中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。关于模板的知 识,已经给大家介绍了。
高性能:如map可以高效地从十万条记录里面查找出指定的记录,因为map是采用红黑树的变体实现的。(红黑树是平横二叉树的一种)
高移植性:如在项目 A 上用 STL 编写的模块,可以直接移植到项目 B 上。
15. 序列式式容器
所有容器类都有共享公共接口,不同容器按不同方式对其进行扩展
15.1 顺序容器种类
所有顺序容器都提供了快速顺序访问元素的能力。下表中的string我们已经在前面介绍,这里不重复做介绍:

string和vector将元素保存在连续的内存空间内,因此由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时。即随机访问迭代器list和forward_list在任何位置添加/删除元素都非常快速。作为代价,这两个容器不支持元素的随机访问。而且,与vector、array、deque相比,这两个容器的额外内存开销(指针开销)也很大。另外forward_list没有size操作。
确定使用哪种顺序容器:
- 通常使用vector是最好的选择,除非你有更好的理由选择其他容器
- 注重空间开销的,不要使用list或forward_list
- 只在头尾,不在中间插入/删除元素的,使用deque
- 在中间插入/删除元素的,使用list或forward_list
总之选择哪一种容器需要依据需求而决定
15.2 序列容器支持的操作概览
15.2.1 容器操作
对与序列式容器,它们有一些公共接口,支持相同的操作方式(除特别标注):

15.2.2 顺序容器操作:迭代器
标准容器类型上所有迭代器都允许我们访问容器中的元素(通过解引用运算符来实现),所有迭代器都定义了递增运算符。
个迭代器的范围由一对迭代器表示,两个迭代器分别指向同一容器中的首元素begin和尾元素之后end的位置,其标准的数学表达为[begin,end)。
对一个非常量对象调用begin、end、rbegin、rend(反向跌代),得到的是返回iterator的版本;对一个const对象调用这些函数时,才会得到一个const版本。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么,因此当不需要写访问时,应使用cbegin和cend.
1
2
3
4
5list<string> a={"trluper","making","track","2025"};
auto it1=a.begin(); //list<string>::iterator
auto it1=a.rbegin(); //list<string>::reverse_iterator
auto it1=a.cbegin(); //list<string>::const_iterator
auto it1=a.crbegin(); //list<string>::const_reverse_iterator
15.2.3 顺序容器操作:定义和初始化
每个容器都定义了一个默认构造函数(除array,它按默认方式初始化):

1.
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了,元素类型也可以不同,只要能将元素类型转换即可:
1
2
3
4
5
6list<string> ls={"trluper","making","track","2025"};
vector<const char*>vec={"a","an","the"};
list<string> ls_1(ls); //正确,类型匹配
//deque<string> dq(ls); //错误,容器类型不匹配
//vector<string> ves(vec); //错误,元素类型不匹配
vector<string> ves(vec.begin(),vec.end()); //正确,元素类型const char*可向string转换1
2
3vector<int> veci(10,-1); //10个int元素,初始值为-1
list<string> ls(10,"hi"); //10个string元素,初始值为hi
forward_list<int> fls(10); //10个int元素,初始值为0,注意区分()和{}1
2
3
4
5
6
7array<int,10> ia1; //默认初始化10个int
array<int,10> ia2={1,2,3,4,5,6,7,8,9,0}; //列表初始化
array<int 10>ia3={10}; //第一个元素围为10,其余为0
//但ia3定义后在这样赋值不允许:(因为array不允许插入和删除)
ia3={10}; //错误
//数组类型无法拷贝和赋值,但array可以
array<int,10> ia4=ia2; //只有数组类型匹配array<int,10>即合法
15.2.4 顺序容器操作:添加元素
1.
除array和forward_list之外,每个顺序容器(包括string类型)都支持push_back,它将一个元素加到容器尾部:
1
2
3
4
5void(size_t cnt, string &word)
{
if(cnt>1)
word.push_back('s');
}
2.
除string和vector不支持push_front外,list、forward_list、deque容器都支持push_front,即将元素插入到容器头部
1
2
3list<int> p;
for(size_t ix=0;ix!=4;++ix)
p.push_front(ix);
3.
特定位置插入元素insert。forward_list有自己版本的insert
1
2
3
4
5
6
7
8
9vector<string> sece;
sece.insert(sece.begin(),"Hello");
//插入范围元素
ist<string> p={"wwj","love"};
sece.insert(sece.begin(),10,"CRF"); //10个“CRF"插入到容器头部
sece.insert(sece.begin()+3,p.begin(),p.end());
//p容器的内容拷贝插入到了sece第四个元素前
sece.insert(sece.end(),{"these","word","are","finished"});
//将元素值列表插入到尾部
4.c++新标准引入三个新成员:emplace_front对应push_front、emplace_back对应push_back、emplace对应insert。主要区别是这些操作是构造不是拷贝元素。也就说明传入给emplace的参数必须与元素类型的构造函数相匹配(即个数、类型一样),先假设有定义的Sales_data类型有一个三参构造函数:
1
2
3vector<Sales_data> c;
c.emplace_back("p78",20,30);
//该式子等价于c.push_back(Sales_data("p78",20,30));
15.2.5 顺序容器操作:访问元素
这些访问操作(back、front、下标、at)返回的都是引用,如果容器是一个const对象,则返回的是const的引用,不是则返回普通的引用(可以改变元素值)。
1
2
3
4
5
6
7if(!c.empty()){
c.front()=42; //将42赋予c的第一个元素
auto &v1=c.back();
v1=1024; //改变最后一个元素的值
auto v2=c.back();
v2=0; //无法改变值
}
15.2.6 顺序容器操作:删除元素
1
2
3
4
5
6
7
8
9
10//删除list中的所有奇数:
list<int> lst{0,1,2,3,4,5,6,7,8,9};
auto it=lst.begin();
while(it!=lst.end()){
if(*it%2!=0){
it=lst.erase(it);
}
else
++it;
}
15.2.7 顺序容器操作:赋值和swap
该表中的赋值运算符可用于所有容器:

1.
赋值运算符=要求左边和右边的运算对象具有相同的类型。assign允许我们从一个相容
的类型赋值,或者从容器一个子序列赋值(由于左右两边的运算容器大小可能不同,所有array无法使用assign)。
1
2
3
4
5
6
7
8//版本一:
list<string>names;
vector<const char*>oldstyle{"an","the","man"};
names.assign(oldstyle.cbegin(),oldstyle.cend()); //names的元素替换为oldstyle中的元素
//版本二:
list<string> slist(1); //1个元素且为空字符串
slist.assign(10,"Hello"); //10个元素且都为Hello
swap操作交换两个相同类型容器的内容。除array外,交换两个容器内容的操作保证会很快——元素本身未交换,swap只是交换了两个容器的内部数据结构(即只是交换了第一次指向的指针)。即也代表着指向容器的迭代器、引用和指针在swap前后都不会改变(除string、array容器)
1
2
3vector<string> s1(42);
vector<string>s2(30);
swap(s1,s2); //执行完后s1指向的是30个元素,s2是(42)array,swap会真正交换它们的元素。
15.2.8 顺序容器操作:改变容器大小
1
2
3
4list<int> lst(10,42);
lst.resize(20); //将后10个值赋值为0
lst.resize(30,-1); //将10个值为-1的添加到尾部
lst.resize(10); //删除后20个元素
15.2.9 容器操作可能使迭代器失效
在对容器进行添加删除可能会使迭代器失效。一个失效的指针、引用和迭代器只是不再表示任何元素。此题的vector的end作为条件,要时时刻刻在插入或删除后更新。
1
2
3
4
5
6
7
8
9
10
11vector<int> vi{0,1,2,3,4,5,6,7,8,9};
auto iter=vi.begin();
while(iter!=vi.end()){
if(*iter%2){
iter=vi.insert(iter,*iter); //返回插入后的的迭代器
iter+=2;
}
else
iter=vi.erase(iter);
}
15.2.10 关系运算符
- 关系运算符两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素
- 比较两个容器实际上是进行元素的逐对比较,比较方式与string比较类似。
- 容器的关系运算符使用元素的关系运算符完成比较
- 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
15.3vector容器
vector是STL常用的容器之一,它和数组一样拥有连续的内存空间,但却比数组更好用。String和vector均使用随机访问迭代器。vector在空间配置器下重新分配空间,那么原来的迭代器就会失效.

15.3.1 vector的扩容机制
对于能够快速随机访问的(下标访问),其内存是连续的,当我们的内存不够时,容器必须分配新的内存空间来保存已有的和新的元素(即将旧内存的元素拷贝到新内存,添加新元素,释放旧内存),所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)。。
当不得不获取新的内存空间时,vector和string的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可以来保存更多的新元素。这样,就不需要每次添加新元素都重新分配容器的内存空间了。在不同的编译器中,vector的扩容策略不相同,msvc编译器每次是以1.5倍且向下取整的策略进行扩容,gcc编译器则是每次以2.0倍的策略进行扩容。
1
2
3
4
5
6
7//MSVC
vector<int> vec;
int n = 34;
while (n--) {
vec.push_back(n);
cout <<"size:" << vec.size() <<" capacity:" << vec.capacity() << endl;
}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
34size:1 capacity:1
size:2 capacity:2
size:3 capacity:3
size:4 capacity:4
size:5 capacity:6
size:6 capacity:6
size:7 capacity:9
size:8 capacity:9
size:9 capacity:9
size:10 capacity:13
size:11 capacity:13
size:12 capacity:13
size:13 capacity:13
size:14 capacity:19
size:15 capacity:19
size:16 capacity:19
size:17 capacity:19
size:18 capacity:19
size:19 capacity:19
size:20 capacity:28
size:21 capacity:28
size:22 capacity:28
size:23 capacity:28
size:24 capacity:28
size:25 capacity:28
size:26 capacity:28
size:27 capacity:28
size:28 capacity:28
size:29 capacity:42
size:30 capacity:42
size:31 capacity:42
size:32 capacity:42
size:33 capacity:42
size:34 capacity:42
15.3.2 管理容量的成员函数

reverse并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。- 如果需求大小小于或等于当前容量,
reverse什么也不做。且需求大小小于当前容量,容器不会退回内存空间。
源码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void resize(size_type __new_size)
{
if (__new_size > size())
_M_default_append(__new_size - size());
else if (__new_size < size())
_M_erase_at_end(this->_M_impl._M_start + __new_size);
}
//reserve
void reserve(size_type __n)
{
if (__n > max_size())
__throw_length_error(__N("vector::reserve"));
if (capacity() < __n)
_M_reallocate(__n);
}
15.3.3 vector构造函数
1 | vector<T> v; //采用模板实现类实现,默认构造函数 |
15.3.4 vector数据存取操作
1 | at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。 |
15.3.5 vector插入和删除操作
1 | insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele. |
15.4 deque容器
Vector容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector容器也可以在头尾两端插入元素,但是在其头部操作效率奇差,无法被接受

虽然deque容器也提供了Random Access
Iterator,但是它的迭代器并不是普通的指针,其复杂度和vector不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该尽可能的使用vector,而不是deque。对deque进行的排序操作,为了最高效率,可将deque先完整的复制到一个vector中,对vector容器进行排序,再复制回deque.
15.4.1 deque容器实现原理
deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。
Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。

15.4.2 deque常用API
1. deque构造函数 1
2
3
4deque<T> deqT;//默认构造形式
deque(beg, end);//构造函数将[beg, end)区间中的元素拷贝给本身。
deque(n, elem);//构造函数将n个elem拷贝给本身。
deque(const deque &deq);//拷贝构造函数。
2.deque赋值操作 1
2
3
4assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
deque&operator=(const deque &deq); //重载等号操作符
swap(deq);// 将deq与本身的元素互换
3. deque双端插入和删除操作 1
2
3
4
5
6
7push_back(elem);//在容器尾部添加一个数据
push_front(elem);//在容器头部插入一个数据
pop_back();//删除容器最后一个数据
pop_front();//删除容器第一个数据
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
4. deque访问操作 1
2
3
4at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。
operator[];//返回索引idx所指的数据,如果idx越界,不抛出异常,直接出错。
front();//返回第一个数据。
back();//返回最后一个数据
5. deque插入操作 1
2
3insert(pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
15.5 list容器
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
相较于vector的连续线性空间,list就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素的移除,list永远是常数时间。
list和vector是两个最常被使用的容器。list容器是一个双向链表:
- 采用动态存储分配,不会造成内存浪费和溢出
- 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
- 链表灵活,但是空间和时间额外耗费较大

15.5.1 list容器的迭代器
list容器不能像vector一样以普通指针作为迭代器,因为其节点不能保证在同一块连续的内存空间上。list迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员
由于list是一个双向链表,迭代器必须能够具备前移、后移的能力,所以list容器提供的是Bidirectional
Iterators.list有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这与vector是不同的,vector插入操作可能造内存重新配置导致原有的迭代器全部失效。
15.5.2 list常用API
1. list构造函数 1
2
3
4list<T> lstT;//list采用采用模板类实现,对象的默认构造形式:
list(beg,end);//构造函数将[beg, end)区间中的元素拷贝给本身。
list(n,elem);//构造函数将n个elem拷贝给本身。
list(const list &lst);//拷贝构造函数。
2. list数据元素插入和删除操作 1
2
3
4
5
6
7
8
9
10
11push_back(elem);//在容器尾部加入一个元素
pop_back();//删除容器中最后一个元素
push_front(elem);//在容器开头插入一个元素
pop_front();//从容器开头移除第一个元素
insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
insert(pos,n,elem);//在pos位置插入n个elem数据,无返回值。
insert(pos,beg,end);//在pos位置插入[beg,end)区间的数据,无返回值。
clear();//移除容器的所有数据
erase(beg,end);//删除[beg,end)区间的数据,返回下一个数据的位置。
erase(pos);//删除pos位置的数据,返回下一个数据的位置。
remove(elem);//删除容器中所有与elem值匹配的元素。
3. list赋值操作 1
2
3
4assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。
assign(n, elem);//将n个elem拷贝赋值给本身。
list&operator=(const list &lst);//重载等号操作符
swap(lst);//将lst与本身的元素互换。
4. list数据的存取、反转和排序 1
2
3
4front();//返回第一个元素。
back();//返回最后一个元素。
reverse();//反转链表,比如lst包含1,3,5元素,运行此方法后,lst就包含5,3,1元素。
sort(); //list排序
5. list和forward_list独有的函数
与其他容器不同,链表类型的list和forward_list定义了几个成员函数形式的算法。特别的是,它们定义可独有的sort、merge、remove、reverse、unique。
| 函数名 | 功能 |
|---|---|
lst.merge(lst2) |
将lst2合并到lst,lst和lst2必须都是有序的,合并后lst2为空,默认为<即升序合并 |
lst.merge(lst2,cmp) |
使用给定的cmp合并 |
lst.remove(val) |
内部调用erase删除给的值 |
lst.remove_if(pred) |
使用一元谓词为真时删除 |
lst.reverse() |
反转 |
lst.sort() |
升序排序 |
lst.sort(cmp) |
使用谓词为真时排序 |
lst.unique() |
去重,调用前必须有序 |
lst.unique(pred) |
使用一元谓词为真时去重 |
16. 适配器
除了顺序容器,还定义了三个顺序容器适配器:stack(栈)、queue(队列)和priority_queue。本质上,适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。

16.1 适配器使用概览
16.1.1 定义适配器
每个适配器都定义了两个构造函数:默认构造函数创建一个空对象;接受一个容器的构造函数拷贝该容器来初始化适配器。(下面的API介绍)
1
stack<int> stk(deq); //接受一个容器对象初始化适配器,deq是一个deque<int>
stack和queue是基于deque实现的,priority_queue是在vector之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型:
1
2stack<string,vector<string>> str_stk; //在vector上实现空栈
stack<string,vector<string>> str_stk(sevc); //构造在vector上实现,并初始化
16.1.2 适配器限制
所有的适配器都要求能够删除和添加元素的能力,因此适配器不能构造在array上实现:
stack——要求有pop、push和top能力,所以可用(除array、forward_list)的容器构造queue——要求有back、push、front、pop能力,所以可构造在list、deque,不能vectorpriority_queue——要求有front、push、pop和top,所以可用vector、deque,但不能list
16.2 stacks适配器
stack是一种先进后出(First In Last
Out,FILO)的数据结构,它只有一个出口,形式如图所示。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。有元素推入栈的操作称为:push,将元素推出的操作称为pop.
栈适配器默认基于deque实现,也可以在list和vector之上实现。虽然stack是基于deque实现的,但我们不能调用push_back等等这些操作,我们必须调用它自己的操作。
16.2.1 stack常用API
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。
1. stack构造函数
1 | stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式: |
2. stack数据存取操作
1 | push(elem);//向栈顶添加元素 |
3. stack赋值操作
1 | stack&operator=(const stack &stk);//重载等号操作符 |
16.3 queue适配器
queue是一种先进先出(First In First
Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。queue默认基于deque实现,priority_queue默认基于vector实现

16.3.1 queue常用API
queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。queue不提供遍历功能,也不提供迭代器。
1. queue构造函数 1
2queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式:
queue(const queue &que);//拷贝构造函数
2. queue赋值操作 1
queue&operator=(const queue &que);//重载等号操作符
3.queue存取、插入和删除操作 1
2
3
4push(elem);//往队尾添加元素
pop();//从队头移除第一个元素
back();//返回最后一个元素
front();//返回第一个元素
17. 关联式容器
17.1 关联容器概述
与顺序容器不同,关联容器中的元素是按着元素的关键字来保存和访问的。顺序容器中的元素是按着它们在容器中的位置来顺序保存和访问。c++两个主要的关联容器是map和set。

map中的元素是关键字-值key-value对。关键字起到索引的作用,值则是与索引相关联的数据。set中只包含一个关键字,set支持高效的关键字查询操作——检查一给定关键字是否在set中- 标准库提供8个关联容器,它们的不同体现在三个维度上:①每个容器或者是个set或者是个map;②或者要求不重复的关键字,或者允许重复关键字;③按顺序保存元素,或者无序保存元素
- 允许重复关键字关键字的容器名字中都包含
multi;不保持关键字按顺序存储的容器的名字都以单词unordered开头。无序容器使用哈希函数来组织元素。 - 类型
map和multimap定义在头文件map中,set和multiset定义在头文件set中。无序容器则定义在头文件unordered_map和unordered_set中 map、multimap、set、multiset都是以红黑树为底层实现机制。unordered_map、unordered_set底层哈希表的实现机理
17.2 关键字类型要求
对于有序容器map、multimap、set、multiset,关键字类型必须定义元素比较的方式。默认情况下,标准库使用关键字类型的<运算来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。
传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。我们可以自定义操作来代替关键字上的<操作,但所提供的操作必须在关键字类型上定义一个严格弱序——可以看做“小于等于”。在实际编程中,重要的是,如果一个类型定义了“行为正常”的<运算符,则它可以用作关键字类型。
当关键字类型使用自己定义的操作时,必须在定义关联容器类型时提供此操作的类型。即用尖括号指出要定义哪种类型的容器,自定义的操作必须在尖括号中紧跟着元素类型给出。
1
2
3
4
5bool compareIsbn(const Sales_data& a,const Sales_data& b){
return a,isbn()<b.isbn();
}
multiset<Sales_data,decltype(compareIsbn)*> bookstore(compareIsbn);
17.3 pair类型
对组pair将一对值组合成一个值,这一对值可以具有不同的数据类型,两个值可以分别用pair的两个公有属性first和second访问。pair与map、multimap和unorderedmap等联合使用.

17.4 都支持的操作
1.
关联容器(有序和无序)都支持表9.2的容器操作。但不支持顺序容器的位置相关操作。如push和pop,因为关联容器是根据关键字存储的。关联容器也不支持构造函数或者插入操作这些接受一个元素值和一个数量值得操作。关联容器的迭代器都是双向的。

2. 除了上面表9.2操作,还有如下操作:
1
2
3
4set<string>::value_type v1; //v1时一个string
set<string>::key_type v2; //v2是一个string
map<string,int>::value_type v3; //v3是一个pair<const string,int>
map<string,int>::mapped_type v4; //v4是一个int
17.5 map和multimap
map类型通常被称为关联数组,其与普通数组类似,不同之处在于其下标不必是整数,我们通过一个关键字而不是位置来查找值。如查找电话号码:我们可以把联系人名字作为关键字,电话号码作为值。map与multimap的不同就是map不允许关键字重复,而multimap则相反。它们存储的元素是pair.
我们不可以通过的迭代器改变它们的的键值,因为map和multimap的键值关系到元素的排列规则,任意改变键值将会严重破坏组织。如果想要修改元素的实值,那么是可以的。
17.5.1 map定义
支持空容器、拷贝初始化、范围初始化、值初始化,赋值初始化
1
2
3
4map<string,size_t> word_count_1; //空map
map<string,size_t> word_count_2{{"trluper",1},{"github",2}}; //初始化列表
map<string,size_t> word_count_3(word_count_2) //拷贝初始化
map<string,size_t> word_count_4(word_count.begin(),word_count.end()); //迭代器初始化
17.5.2 map和multimap迭代器
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。必须记住,一个map的value_type是一个pair,我们可以改变pair的值,但是不能改变关键字成员的值,因为它是个const。
1
2
3
4
5//获得map迭代器
auto map_it=mymap.begin();
//map_it是指向一个pair<const string,size_t>对象的迭代器
cout<<map_it->fist<<"---"<<map_it->second;
++map_it->second;
17.5.3 添加元素
对map执行insert,必须记住元素类型是pair,且返回的是pair<iterator,boo>,第一个插入后返回的迭代器,指向指定关键字的迭代器,第二个是插入是否成功。有以下4中添加方法
1
2
3
4
5
6
7
8
9
10
11map.insert(...); //往容器插入元素,返回pair<iterator,bool>
map<int, string> mapStu;
// 第一种 通过pair的方式插入对象
mapStu.insert(pair<int, string>(3, "小张"));
// 第二种 通过pair的方式插入对象
mapStu.inset(make_pair(-1, "校长"));
// 第三种 通过value_type的方式插入对象
mapStu.insert(map<int, string>::value_type(1, "小李"));
// 第四种 通过数组的方式插入值
mapStu[3] = "小刘";
mapStu[5] = "小王";
insert(v)(和emplace(args))返回的是pair类型,如果关键字已在容器中,则insert什么事情也不用做,且返回值中的bool部分为false;如果关键字不存在,则元素被插入容器中,且bool值为true。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16map<string,size_t> word_count;
string word;
while(cin>>word){
//插入一个元素,关键字为word,初始值为1
auto ret=word_count.insert({word,1});
//如果键值对早已存在,则对相应的键值对的值++
if(!ret.second)
++ret.first->second;
}
/*
ret——是一个pair类型,<map<string,size_t>::iterator,bool>
ret.first——是一个map迭代器,指向具有指定关键字的元素
ret.first->——是个pair,<string,size_t>
ret.first->second——map中元素的值部分
++ret.first->second——递增此值
*/
17.5.4 删除元素
1 | clear();//删除所有元素 |
17.5.5 查找操作
由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作:
1
2
3
4
5
6
7
8
9
10//下标操作
c[k]; //返回关键字为k的值,若k不存在则添加键为k的元素,对其值进行初始化
c.at(k);//访问关键字元素为k的元素,若不在,抛出out_of_range异常
//lower_bound和upper_bound不适用于无序容器
c.find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回map.end();
c.count(keyElem);//返回容器中key为keyElem的对组个数。对map来说,要么是0,要么是1。对multimap来说,值可能大于1。
c.lower_bound(keyElem);//返回第一个key>=keyElem元素的迭代器。
c.upper_bound(keyElem);//返回第一个key>keyElem元素的迭代器。
c.equal_range(keyElem);//返回pair,pair中的元素为容器中key与keyElem相等的上下限的两个迭代器。
注意:下标操作只有非常量map和unordered_map能用。set类型不支持下标,因为set中没有与关键字相关联的“值”。multimap和undered_multimap不支持下标操作,因为这些容器中可能有多个值与一个关键字相关联
17.6 set和multiset
set就是关键字的简单集合,当只想知道一个值是否存在时,set是最为恰当的。set是不可重复,而multiset可重复。set的许多操作同map一样
17.6.1 set定义
同map一样支持支持空容器、拷贝初始化、范围初始化、值初始化,赋值初始化。
1
2
3
4set<string> set1{"trluper","github"};
set<string> set2(set1);
set<string> set3(set1.begin(),set1.end());
...
17.6.2 迭代器
set的迭代器是const的,同map一样set中的关键字也是const的,可以用一个set迭代器来读取元素的值,但不能修改。
1
2
3
4
5
6set<int> iset={1,2,3,4,5,6,7};
set<int>::iterator set_it=iset.begin();
if(set_it!=iset.end()){
//*set_it=42; //非法,set的关键字是const,不能修改
cout<<*set_it<<endl;
}
17.7 关联无序容器
新标准定义了4个无序unordered容器,这些容器不是使用比较运算符来组织元素的,而是使用一个哈希函数hash function和关键字类型的==运算符。除了哈希管理外,无序容器还提供了与有序容器相同的操作find,insert等。
17.7.1 管理桶
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器采用哈希算法(函数)将元素映射到桶。为了访问一个元素,容器首先要计算元素的哈希值,它指出了应该搜索哪个桶。该无序容器会将一个特定哈希值的所有元素都保存在相同的桶中。(如果容器允许重复关键字,自然而然相同关键字的元素也都会在同一个桶中)。所有,无序容器的性能依赖哈希函数和桶的数量及大小。
对于相同的参数,哈希函数必然产生相同的结果。计算一个元素的哈希值和在桶中的搜索是很快的,但如果一个桶存放太多的元素,则查找一个特定的元素会进行大量比较操作。
无序容器提供了一组管理桶函数,这些成员函数允许我们查询容器状态以及在必要时强制容器重组。

18. 泛型算法
标准库容器定义的操作集合惊人的小。标准库并未给每个容器添加大量功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。这些算法是通用的(generic,或称泛型的):它们可以用于不同类型的容器和不同类型的元素
大多数算法都定义在头文件algorithm中。标准库还在头文件中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历两个迭代器指定的一个元素范围来操作。通常情况下,算法遍历范围,对其中每个元素进行一些处理。
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作算法永远不会改变底层容器的大小。算法可能改变容器中保存的元素,也可能在容器中移动元素,但永远不会直接添加或删除元素。
18.1 初始泛型算法
18.1.1 只读算法
find、count、accumulate,accumulate定义在头文件numeric中,其只读取输入范围内的元素,而不改变元素.
1.
accumulate函数接收3个参数,前两个指出了是需要求和的元素范围,第三个参数是求和的初值。第三个参数的类型决定了函数中使用哪个加法运算符和返回值类型,这个特性蕴含着:将元素类型加到和的类型上的操作使可行的,如下面string允许+,但const char*不允许,没有定义+
1
2
3
4int sum=accumulate(vi.begin(),vi.end(),0);
string sum=accumulate(vs.begin(),vs.end(),string(""));
//const char* 没有+运算符,不能使用
//string sum=accumulate(vc.begin(),v.end()."");
2.
只读算法equal,用于确定两个序列是否保存相同的值,它将第一个序列中的每个元素与第二个序列中的对应元素比较。如果所有对应元素相等,则返回true,否则,返回false。要求内部元素支持==运算符
1
equal(v1.cbegin(),v1.cend(),v2.cbegin());
18.1.2 写容器元素算法
一些算法将新值赋予序列中的元素。必须注意保存序列原大小至少不小于我们要求算法写入的元素数目。算法不会执行容器操作,所以它们自身无法改变容器大小。
1
2
3
4
5
6
7
8
9
10fill(vec.begin(),vec.end(),0); //每个元素重置为0
fill(vec.begin(),vec.begin()+vec.size()/2,10); //将一半元素置为10
//一些算法接受一个迭代器来指出一个单独的目的位置,新值从目的迭代器的位置开始插入。
vector<int>vec; //空vector
fill_n(vec.begin(),vec.size(),0); //所有元素置为0,vec.size()=0,所以对于空容器没有发生错误
//向目的位置迭代器写入数据的算法假定目的位置足够大,能容纳要写入的元素:(举个反例)
vector<int>vec; //空vector
fill_n(vec.begin(),10,0); //不允许,至少容器容量为10
18.1.3 back_inserter
一种保证算法有足够的元素空间来容纳输出数据的方法是使用插入迭代器。插入迭代器是一种向容器添加元素的迭代器。
back_inserter定义在iterator头文件中。back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中,并会返回下一个插入迭代器:
1
2
3
4
5vector<int> vec;
auto it=back_inserter(vec);;
*it=42; //42
*it=50; //42 50
fill_n(back_insertor(vec),10,0); //添加10个元素到vec:42 50 0 0 0 0 0 0 0 0 0 0
18.1.4 拷贝算法
1.
拷贝算法copy接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置,返回拷贝后的下一个元素的迭代器。此算法使将输入范围的元素拷贝到目的序列中。所以目的序列的元素(内存空间)至少要和输入范围元素数量一样。
1
2
3
4vector<int> vec{1,2,3,4,5,6,7,8,9};
vector<int> vec_c(10);
//ret为vec_c拷贝元素的下一个元素的迭代器
auto ret=copy(vec.begin(),vec.end(),vec_c.begin());
2.
replace算法读入一个序列,并将其所有等于给定值得元素都改为另一个值,接受4个参数:前两个为迭代器,表示输入序列,后两个是要搜索得值、替换得值,位于algorithm头文件
1
2vector<int> vec(10,0);
replace(vec.begin(),vec.end(),0,9);
3.replace_copy,与replace不同,该算法保持原序列不变,而是生成了新序列将搜索值变为替换值。
1
2list<int> Mlist;
replace_copy(vec.begin(),vec.end(),back_inserter(Mlist),0,10);vec并未改变,Mlist包含vec的一份拷贝,不过原来在vec中值为0的元素在Mlist中都变为10.
18.1.5 重排容器元素的算法
- (1)某些算法会重排容器中元素的顺序,代表函数是
sort。调用sort会重排输入序列中的元素,使之有序 - (2)
unique算法重拍序列,其把相邻重复项“消除”,并返回一个指向不重复值范围末尾的迭代器(但此words的大小没有改变)此位置之后的元素仍然存在,但是我们并不知道它们的值是什么。(去重)

这是需要用到容器操作erase删除该元素。代码如下: 1
2
3
4
5void elimDups(vector<sting>& words){
sort(words.begin(),words.end());
auto it=unique(words.begin(),words.end());
words.erase(it,words.end());
}
18.2 定制操作
很多算法都会比较输入序列的元素,这类算法使用元素类型的<或==运算符完成比较。标准库还未这些算法定义了额外的版本,允许我们定义的操作来代替默认运算符。
18.2.1 用函数做谓词
向算法传递函数:
我们可以按照长度重排vector,所以要重载sort。此时,sort接受第三个参数,最后一个参数称为谓词。谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。根据它们接受的参数数量,谓词可分为一元谓词和二元谓词。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
谓词:指普通函数或者重载了operator()且返回值是bool类型的的函数对象。函数对象和普通函数的区别:
- 1.函数对象可以有自己的状态
- 2.普通函数没有类型,函数对象有类型
- 3.函数对象比普通函数执行效率有可能更高(成员函数自动申请为内联函数)
1 | bool isShorter(const string &s1,const string &s2){ |
我们将words按大小重排的同时,还希望将具有相同长度的元素按字典排序。我们可以使用stable_sort算法。这种稳定的排序算法维持了相等元素的原有顺序。
1
stable_sort(words.begin(),words.end(),isShorter);
18.2.2 使用lambda
lambda的主要时解决传递谓词时参数的限制,其可通过捕获列表来传递多个想要传给函数体的参数。就如find_if,find_if只接受一元谓词,因此传递给find_if的可调用对象必须接受单一参数。此时可调用对象(一元谓词)无法传递别的参数。用lambda表达式便能解决:
1
2
3
4
5
6
7
8void findLength(vector<string>& words,vector<string>::size_type sz){
elimDups(words);
stable_sort(words.begin(),words.end(),isShorter);
auto s=find_if(words.begin(),words.end(),
[sz](const string s1){return s1.size()>sz;}); //使用lambda捕获sz,符合要求便返回迭代器
for_each(s,words.end(),
[](const string& a){cout<<s<<endl;});
}
18.3 bind:使函数更加灵活的函数适配器
对于lambda,如果捕获列表是空的,推荐不使用它而是使用函数。如果不为空,而那些定制的函数只要求传入一元或二元谓词的时候,使用lambda尤为重要。但是我们可能要在很多地方使用相同的操作,编写相同的lambda表达式会过于麻烦,此时应该定义一个函数。为解决上面的要求,我们引入了标准库bind函数
bind()函数作为函数的适配器,它可以扩大函数是使用场合,使得函数更加灵活的被使用。bind定义在头文件functional中。它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表(即适用上述只能传入一元和二元谓词的要求)
18.3.1 bind的一般形式
1 | auto newCallable=bind (callable, arg_list); |
newcallable是一个可调用对象,当newcallable被调用时,newcallable就调用callable,并将参数列表arg_list中的参数依次序给callable(其中可能会包含占位符_n表示newcallable的第n个参数)。占位符定义在placeholders的命名空间中,因此使用时要声明命名空间using namespace std::placeholders;
1
2
3
4
5
6using namespace std::placeholders;
bool islength(const string &s1,size_t sz){
return s1.size()>sz;
}
auto f=bind(islength,_1,6); //s1=_1,sz=6
auto s=find_if(words.begin(),words.end(),f(string &s)); //克服了find_if只能接受元谓词的缺点_1=s;1
2
3
4
5using namespace std::placeholders;
auto fn1 = bind(func, _1, 2, 3);
auto fn2 = bind(func, 2, _1, 3);
fn1(10); //调用func(10,2,3)
fn2(10); //调用func(2,10,3);
18.3.2 绑定引用参数
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。因为bind拷贝其参数,而我们不能拷贝一个ostream。如果我们希望传递给bind一个对象而又不拷贝它,就必须使用标准库ref函数:
1
2
3
4
5
6ostream& print(ostream& os,string& s,char c){
os<<s<<c<<endl;
return os;
}
auto f=bin(print,ref(os),_1,' ');
f("trluper"); //print(os,"trluper",' ');
18.4 泛型算法的结构
18.4.1 算法支持的迭代器
任何算法的最基本的特性是它要求其迭代器提供哪些操作。算法所要求的迭代器操作可以分为5个迭代器类别:
- 输入迭代器:提供对数据的只读访问
只读,支持
++、==、!= - 输出迭代器:提供对数据的只写访问
只写,支持
++ - 前向迭代器:提供读写操作,并能向前推进迭代器
读写,支持
++、==、!= - 双向迭代器:提供读写操作,并能向前和向后操作
读写,支持
++、--, - 随机访问迭代器:提供读写操作,并能在数据中随机移动
读写,支持
++、--、[n]、+n、-n、<、<=、>、>=
算法还共享一组参数传递规范和一组命名规范。
18.4.2 形参规范
大多数算法有下列4中形式
alg(beg,end,other args);alg(beg,end,dest,other args);alg(beg,end,beg2,other args);alg(beg,end,beg2,end2,other args);
alg为算法名称,beg和end表示算法所操作的输入范围(几乎所有算法均支持接受一个输入范围)。同时这里还列出了dest(目标迭代器)、beg2、end2、它们都是迭代器参数,如果用到了这些参数,它们分别承担了指定目的位置和第二个范围的角色。
注意:
接受单个目标迭代器的算法:向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据
接受第二个输入序列的算法:接受单独beg2的算法假定从beg2开始的序列与beg与end所表示的范围至少一样大
18.4.3 命名规范
除了上述的参数规范,算法还遵循一套命名和重载规范。这些规范处理:提供一个操作去代替默认的<或者==运算符,以及算法是将输出数据写入输入序列还是一个分离目的位置
1. 重载形式
接受谓词参数来代替算法默认的<或者其他运算符,以及那些不接受额外参数的算法,通常都是重载的函数。函数一个版本用来元素类型运算符来比较元素,另一个版本接受一个额外的谓词来代替默认的<或者==。
1
2unique(beg,end,val); //使用默认==比较元素
unique(beg,end,compare); //使用传入的谓词compare比较
2. _if版本的算法
接受一个元素值的算法通常会有另一个不同名(不是重载版本)版本,该版本接受一个谓词代替元素值,接受谓词参数的算法都有附加_if后缀:
1
2find(beg,end,val); //查找范围内val第一次出现的位置
find_if(beg,end,pred); //查找第一个另pred谓词为真的元素
3. 区分拷贝元素的版本和不拷贝的版本
默认情况下,那些能重排元素的算法(sort,stable_sort等)将重排后的元素写回给定的输入序列。这些算法还提供另一版本:将元素写道一个指定的输出目的位置,写到额外目的空间的算法都在名字后面附加一个_copy:
1
2reverse(beg,end); //反转
reverse_copy(beg,end,dest); //反转拷贝到dest
一些算法同时有_if和_copy:它们接受一个目的迭代器和一个谓词:
1
remove_coy_if(beg,end,dest,pred);
19 再探迭代器
除了为每个容器定义的迭代器外,标准库在iterator头文件还定义了额外几种迭代器。
- 插入迭代器:绑定在容器上时,可用来向容器插入元素
- 流迭代器:绑定在容器上时,可用来遍历元素
- 反向迭代器(forward_list除外):这些迭代器是向后而不是向前移动
- 移动迭代器:不是拷贝元素,而是移动它们
19.1 插入迭代器
插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素。如back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。插入迭代器有三种类型:
back_inserter:创建一个使用push_back的迭代器front_inserter:创建一个使用push_front的迭代器inserter:创建一个使用insert的迭代器。接受第二个参数,元素将被插入到给定迭代器表示的元素之前
只有在容器支持push_front的情况下,我们才可以使用front_inserter。类似的,只有在容器支持push_back的情况下,我们才能使用back_inserter。当调用insert(c,iter)时,我们得到一个迭代器,使用它时,会将元素插入到iter原来指向的元素之前的位置(iter一直指向固定的一个元素)
1
2
3
4
5
6list<int> lst{1,2,3,4};
list<int>lst2,lst3;
//拷贝完成后,lst2为4,3,2,1
copy(lst.begin(),lst.end(),front_inserter(lst2));
//拷贝完成后,lst3为1,2,3,4
copy(lst.begin(),lst.end(),inserter(lst3,lst.begin()));
19.2 反向迭代器
除了forward_list之外,其他容器都支持反向迭代器。rbegin、crbegin返回指向容器尾元素的迭代器rend、crend返回指向首元素之前一个位置的迭代器。
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。对于反向迭代器,递增++和递减--的含义是反过来的。即递增++iter是移动到前一个元素,递减--iter是移到下一个元素。
1
2sort(vec.begin(),vec.end()); //正常升序排序
sort(vec.rbegin(),vec.rend()); //反过来了,降序排序
19.2.1 反向迭代器和其他迭代器之间的关系
我们想在存放了words,lanuage,last的string类型的line中输出打印最后一个单词last,使用find函数查找最后一个“,”
1
2
3
4
5
6auto rcomm=find(line.crbegin(),line.crend(),','); //此时rcomm指向的是最后“,”的位置
//如果我们这样调用印,会打印出tsal逆序单词:
cout<<string(line.crbegin(),rcomm)<<endl;
//我们可以通过调用reverse_iterator的base成员函数可以将其转换成普通迭代器,从而正确打印:
cout<<string(rcomm.base(),line.cend())<<endl;
注意:关键点在于[line.crbegin(),rcomma)和[rcomma.base(),line.cend())指向line中相同的元素范围。为了实现这一点,rcomma和rcomma.base()必须生成相邻位置而不是相同位置rcomma.base()在rcomma的下一个位置),crbegin()和cend()也是如此。