C++ Primer笔记--Part3 C++标准库
第八章 IO库
8.1 IO类
为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数的名字都以一个w开始。
8.1.1 IO对象无拷贝或赋值
进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态。
8.1.2 条件状态
一个流一旦发生错误,其上后续的IO操作都会失败。确定一个流对象的状态最简单的方法是将它当作一个条件来使用。
当到达文件结束位置,eofbit和failbit都会被置为。如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。
使用good()和fail()是确定流的总体状态的正确方式,而eof()和bad()只能表示特定的错误。
管理条件状态
clear()不接受参数的版本清除所有错误标志位。带参数的clear版本接收一个iostate值,表示流的新状态。
setstate(flags)在当前流的原状态上,将给定flags标志位置位。
【讨论】
while(cin >> v1, cin.eof()) //只有遇到文件结束符才会结束。(逗号运算符) //逗号运算符左右两句运算语句都会执行, //但只有最右边的运算语句代表整个逗号表达式的结果, //所以这儿的while只会判断!cin.eof()
【拓】
Windows下使用clear()+ingore()
Linux下使用cin.clear()和clearerr(stdin)
8.1.3 管理输出缓冲
刷新输出缓冲区
cout << endl; //输出一个换行,然后刷新缓冲区 cout << flush; //刷新缓冲区,不附加任何额外字符 cout << ends; //输出一个空字符,然后刷新缓冲区
unitbuf操纵符
cout << unitbuf; //cout的所有输出操作后都会立即刷新缓冲区 cout << nounitbuf; //回到正常的缓冲方式
cerr默认设置了unitbuf。
如果程序崩溃,输出缓冲区不会被刷新。
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。
tie()有两个重载的版本。
无参版本,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个输出流的指针,如果对象未关联,则返回空指针。
第二个版本接收一个指向ostream的指针,将自己关联到此ostream,并返回指向之前绑定的流对象的指针,如果没有,则返回空指针。
每个流同时最多关联到一个流,但多个流可以用时关联都同一个ostream。
单向关联?
8.2 文件输入输出
如果调用open失败,failbit会被置位。
对一个已经打开的文件流调用open会失败,随后的试图使用文件流的操作都会失败。
当一个fstream对象被销毁时,close会自动被调用。
8.2.2 文件模式
8.3 string流
如果打算在多次转换中使用同一个stringstream对象,每次转换前要使用clear()方法。
小结
iostream处理控制台IO。
fstream处理命名文件IO。
stringstream完成内存string的IO。
类fstream和stringstream都继承自类iostream的。输入类都继承自istream,输出类都继承自ostream。
第九章 顺序容器
9.1 顺序容器概述
顺序容器在以下方面都有不同的性能折中:
- 向容器添加或从容器中删除元素的代价
- 非顺序访问容器中元素的代价
string 和 vector 将元素保存在连续的内存空间中。
list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且,与vector、deque和array相比,这两个容器的额外开销也很大。
forward_list和array是新C++标准增加的类型。array不支持添加和删除元素以及改变容器大小的操作。forward_list没有size操作。
选择容器的基本原则:
9.2 容器库概览
forward_list迭代器不支持递减运算符。表3.7列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代器。list迭代器不支持<运算符。
只有顺序容器的构造函数才能接受大小参数,关联容器并不支持。
标准库array具有固定大小
当定义一个array时,除了指定元素类型,还要指定容器大小。
一个默认构造的array是非空的。
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。
9.2.5 赋值和swap
迭代器、引用、指针指向的是元素,而不是容器。
除了array外,交换两个容器内容的操作保证会很快,元素本身并未被交换,swap只是交换了两个容器的内部数据结构。
9.2.7 关系运算符
每个容器类型都支持相等运算符(==和!=);除无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。
比较两个容器实际上是进行元素的逐对比较。
容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。
9.3 顺序容器操作
9.3.1 向顺序容器添加元素
9.3.2 访问元素
下标操作和安全的随机访问
编译器不会检查下标运算符越界,at成员函数会对下标越界抛出一个out_fo_range异常。
9.3.3 删除元素
9.3.4 特殊的forward_list操作
9.3.5 改变容器的大小
9.3.6 容器操作可能使迭代器失效
9.4 vector对象 是如何增长的
在调用reserve()之后,capacity将会大于或等于传递给reserve的参数。
shrink_to_fit()只是一个请求,标准库并不保证退还内存。
9.5 额外的string操作
9.5.1 构造string的其他方法
如果我们未传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。
【拓】
vector::data(),返回指向vector内部用于存储元素的内存数组,使得vector < char > 可以用于string的构造
substr操作
9.5.2 改变string的其他方法
除了接受迭代器的版本外,string提供了接受下标版本的insert和erase;
提供接受C风格字符串的insert和assign版本。
可以指定将来自其他string或子字符串的字符插入到当前string或赋予当前string
append和replace函数
append函数是在string对象末尾进行插入操作的一种简写形式:
replace操作是调用erase和insert的一种简写:
调用replace时,插入的文本与删除的文本可以不一样长。
【拓】 string::npos
9.5.3 string搜索操作
逆向搜索
rfind()
9.5.4 compare函数
9.5.5 数值转换
9.6 容器适配器
stack、queue、priority_queue
默认情况下,stack和queue都是基于deque实现的,priority_queue是在vector之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
对于一个给定适配器,可以使用哪些容器是有限制的。
所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在array之上。且还要求能访问尾元素的能力,也不能用forward_list。
stack只要求push_back、pop_back和back操作,因此可以使用除array和forward_list之外的任何容器类型来构造。
queue适配器要求back、push_back、front和pop_front,因此它可以构造于list和deque之上。
priority_queue除了front、push_back和pop_back操作之外,还要求随机访问能力,因此它可以构造与vector或deque之上,但不能基于list构造。
栈适配器
队列适配器
默认情况下,标准库在元素类型上使用<运算符来确定相对优先级。
第十章 泛型算法
10.1 概述
大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。
标准库算法find
传递给find的前两个参数是表示元素范围的迭代器,第三个参数是一个值。find将范围中每个元素与给定值进行比较。它返回指向第一个等于给定值的元素的迭代器。如果范围中无匹配元素,则find返回第二个参数来表示搜索失败。
find的工作是在一个未排序的元素序列中查找一个特定元素。
标准库begin和end函数
泛型算法本身不会执行容器的操作,它们只会运行与迭代器之上,执行迭代器的操作。这带来一个编程假定:算法永远不会改变底层容器的大小。(此大小指的是size而不是capacity)
标准库定义了一类特殊的迭代器,称为插入器。当算法操作一个这样的迭代器时,迭代器可以完成向容器添加元素的效果,但算法本身永远不会做这样的操作。
10.2 初识泛型算法
10.2.1 只读算法
find和count都是只读算法。另一个只读算法是accumulate,定义于numeric中。
accumulate函数接受三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值。accumulate的第三个参数的类型决定了函数中使用哪个加法运算符以及返回值的类型。
accumulate(v.cbegin,v.cend(),string(""));//将vector中所有string元素连接起来,因为string定义了+运算符。
accumulate(v.cbegin,v.cend(),"");//错误,因为字符串字面值是const char*,该类型没有定义operator +。
操作两个序列的算法
equal(roster1.cbegin(), roster1.cend(), roster2.cbngin())
那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
10.2.2 写容器元素的算法
必须保证写入的容器范围是有效的。
fill()函数接受三个参数,前两个参数表示范围,第三个参数表示填充值。
fill_n()函数接受三个参数,参数一为指定首元素的迭代器,参数二为元素个数,参数三为元素新值。
算法不检查写操作。
back_inserter
#include<iterator>
插入迭代器back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中:
fill_n(back_inserter(vec),10,0); //添加10个元素到vec
拷贝算法
拷贝(copy)算法接受三个迭代器,前两个表示一个输入范围,第三个表示目的序列的起始位置。此算法将输入范围中的元素拷贝到目的序列中。
copy返回的是其目的位置迭代器(递增后)的值。
多个算法都提供所谓的“拷贝”版本。
replace(ilst.begin(),ilst.end(),0,42); //将所有值为0的元素改为42 replace_copy(ilst.cbegin(),ilst.cend(),back_inserter(vec),0,42);
拷贝版本的replace调用后,ilst并未改变,vec包含ilst的一份拷贝,不过原本在ilst中值为0的元素在vec中都变为42
10.2.3 重排容器元素的算法
调用sort会重排输入序列中的元素,使之有序,利用元素类型的<运算符来实现排序。
sort要求迭代器支持随机访问元素的能力。
unique的标准库算法重排,使得不重复的元素出现在容器的开始部分,返回指向不重复部分元素区域之后一个位置的迭代器。
sort(words.begin(),words.end()); auto end_unique = unique(words.begin(),words.end()); words.erase(end_unique,words.end());
//unique的作用是用不重复的元素覆盖相邻的重复元素。而在不重复部分元素区域之后的元素我们知道他们的值是什么
使用容器操作删除元素
10.3 定制操作
10.3.1 向算法传递函数
谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。
一元谓词和二元谓词。
排序算法
stable_sort算法。这种稳定排序算法维持相等元素的原有顺序。
partition算法,接受一个一元谓词,对容器进行划分,使得谓词为true的值会排在容器的前半部分,而使谓词为false的值会排在后半部分。算法返回一个迭代器,指向最后一个使谓词为true的元素之后的位置。
【拓】
find_if()接受一对迭代器表示一个范围,第三个参数是一个一元谓词,返回指向第一个使谓词返回非0值的元素的迭代器。如果不存在,则返回尾后迭代器。
【拓】
stable_partition()
10.3.2 lambda表达式
[capture list](parameter list) -> return type { function body }
capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表。铺货列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所定义函数之外声明的名字。
lambda必须使用尾置返回来指定返回类型
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
lambda的调用方式与普通函数的调用方式相同。
lambda不能有默认参数(在C++14后支持)
一个lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
【拓】for_each()
10.3.3 lambda捕获和返回
当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象。
从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。
值捕获
采用值捕获的变量是在lambda创建时拷贝而不是调用时拷贝。
引用捕获
lambda捕获的都是局部变量,必须确保被引用的对象在lambda执行时是存在的。
隐式捕获
除了显式列出我们希望使用的来自所在函数的变量外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推捕获列表,应在捕获列表中写一个&(表示采用捕获引用方式)或=(表示采用值捕获方式)
可变lambda
对于值捕获,如果希望能修改值捕获的变量的值,则可在参数列表后函数体之前加上关键字mutable。
指定lambda返回类型
lambda只含有单一的return语句,lambda的返回类型从返回的表达式的类型推断而来。
如果lambda体包含return之外的任何语句,则编译器假定此lambda返回void,此时要显式指定返回类型。
【拓】transform()算法接受三个迭代器和一个可调用对象。第三个迭代器指向目标位置的首位置,可调用对象返回操作该元素得到的结果。
10.3.4 参数绑定
标准库bind函数
定义于头文件functional中。可以将bind函数看作一个通用的函数适配器。
auto newCallable = bind(callable, arg_list)
arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可能包含形如_ n的名字,其中n是一个整数。_ n表示第n位参数的占位符。占位符则为newCallable的参数列表。
使用placeholders名字
名字_n都定义在一个名为placeholders的命名空间中,而这个命名空间本身定义在std命名空间。为了使用这些名字,两个命名空间都要写上,如
using std::placeholders::_1; //或者using namespace std::placeholders;
placeholders命名空间也定义在functional头文件中
bind的参数
可以用bind绑定给定可调用对象中的参数或重新安排其顺序
auto new_fanc = bind(old_fanc, a, b ,_ 2, c, _ 1); // new_fanc的第一个参数绑定到_ 1,第二个参数绑定到_ 2
绑定引用参数
ref()和cref()分别返回一个对象,包含给定的引用,此对象是可以拷贝的。
10.4 再探迭代器
标准库在头文件iterator中还定义了额外几种迭代器。
- 插入迭代器:这些迭代器被绑定到一个容器上,可用来向容器插入元素。
- 流迭代器:这些迭代器被绑定到输入或输出流上,可用来遍历所关联的IO流。
- 反向迭代器:这些迭代器向后而不是向前移动。除了forward_list之外的标准库容器都有反向迭代器。
- 移动迭代器:这些专用的迭代器不是拷贝其中的元素,而是移动它们。
10.4.1 插入迭代器
插入器是一种迭代器适配器。
插入器有三种类型,差异在于元素插入的位置:
- back_inserter:创建一个使用push_back的迭代器
- front_inserter:创建一个使用push_front的迭代器
- inserter:创建一个使用insert的迭代器。此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入给定迭代器所表示的元素之前。
front_inserter生成的迭代器会将插入的元素序列的顺序颠倒过来,而inserter和back_inserter则不会。
【拓】
unique_copy接收三个迭代器,表示拷贝不重复元素到目的位置。
10.4.2 iostream迭代器
istream_iterator读取输入流,ostream_iterator向一个输出流写数据。
istream_iterator操作
当创建一个流迭代器时,必须指定迭代器将要读写的对象类型。一个istream_iterator使用>>来读取流。我们可以创建一个istream_iterator时,可以将它绑定到一个流。也可以默认初始化迭代器,创建一个尾后迭代器。
从标准输入中读取数据的案例:
也可以写成下面这样,这体现了istream_iterator更有用的地方
使用算法操作迭代器
istream_iterator允许使用懒惰求值
标准库并不保证迭代器立即从流读取数据。标准库中的实现所保证的是,在我们第一次解引用迭代器之前,从流中读取数据的操作已经完成。
ostream_iterator操作
我们可以对任何具备输出运算符的类型定义ostream_iterator。当创建一个ostream_iterator时,我们可以提供(可选的)第二参数,它是一个C风格字符串(即,一个字符串字面常量或者一个指向以空字符结尾的字符数组的指针),在输出每个元素后都会打印此字符串。
必须将ostream_iterator绑定到一个指定的流,不允许空的或表示尾后位置的ostream_iterator。
当我们向out_iter赋值时,可以忽略解引用和递增运算。运算符*和++实际上对ostream_iterator对象不做任何事情,因此可以忽略。但是,推荐写上,这样子使得流迭代器的使用与其他迭代器的使用保存一致。
10.4.3 反向迭代器
除了forward_list之外,其他容器都支持反向迭代器。我们可以通过调用rbegin、rend、crbegin、crend成员函数来获得反向迭代器。
可以通过向sort传递一对反向迭代器来将vector整理为递减序。
反向迭代器需要递减运算符
因此不可能从一个forward_list或一个流迭代器创建反向迭代器。
反向迭代器和其他迭代器间的关系
我们可以通过调用reverse_iterator的base成员函数来完成这一转换,此成员函数会返回其对应的普通迭代器。
reverse_iterator与reverse_iterator.base()指向相邻位置而不是相同位置。
【拓】
用list和find和反向迭代器查找最后一个指定元素,如果没有指定元素,。。。(list是环状,最后有结束标记end节点)
【拓】
可以用reverse_iterator()获得反向迭代器
10.5 泛型算法结构
10.5.1 5类迭代器
迭代器类别
输入迭代器可以读取序列中的元素。一个输入迭代器必须支持:
输出迭代器可以看作输入迭代器功能上的补集(只读不写元素)。 输出迭代器必须支持:
前向迭代器可以读写元素,
双向迭代器可以正反向读写序列中的元素。
随机访问迭代器提供在常量时间内访问序列中任意元素的能力。随机访问迭代器必须支持:
10.5.2 算法形参模式
大多数算法具有如下4中形式之一:
接受单个目标迭代器的算法
dest参数是一个表示算法可以写入的目的位置的迭代器。
接受两个输入序列的算法
接受单独beg2的算法假定从beg2开始的序列与beg和end所表示的范围至少一样大
10.5.3 算法命名规范
一些算法使用重载版本传递一个谓词
_if版本的算法
接受一个元素值的算法通常有另一个不同名的_if版本,该版本接受一个谓词代替元素值
区分拷贝元素的版本和不拷贝的版本
_copy的命名算法
【拓】remove_if(beg1, end1, pred)
【拓】remove_copy(beg1, end1, dest, pred)
10.6 特定容器算法
对于list和forward_list,应该优先使用成员函数版本的算法而不是通用算法。
链表类型list和forward_list定义了几个成员函数形式的算法。
splice成员
链表类型还定义了splice算法,此算法是链表数据结构所特有的。
链表特有的操作会改变容器
remove的链表版本会删除指定元素。unique的链表版本会删除第二个和后继的重复元素。merge和splice会销毁其参数。
第十一章 关联容器
map中的元素是一些关键字-值对,set中每个元素只包含一个关键字。
无序容器使用哈希函数来组织元素。
类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。
11.1 使用关联容器
11.2 关联容器概述
关联容器不支持顺序容器的位置相关操作,如push_back()等。关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值操作。
关联容器的迭代器都是双向的。
初始u啊multimap或multiset
multi容器允许多个元素具有相同的关键字。
11.2.2 关键字类型的要求
默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。
有序容器的关键字类型
默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
使用decltype来获得一个函数指针类型时,必须加上一个*来指出我们要使用一个给定函数类型的指针。
11.2.3 pair类型
pair的标准库类型定义在头文件utility中。
map的元素是pair类型。
pair的数据成员是public的,可以直接使用数据成员first和second
11.3 关联容器操作
除了表9.2中列出的类型,关联容器还定义了如下类型:
11.3.1 关联容器迭代器
当解引用一个关联容器迭代器时,我们将会得到一个类型为容器的value_type的值的引用。对map而言,value_type是一个pair类型,其first成员保存了const的关键字,second成员保存值。
set的迭代器是const的
set的迭代器只允许只读访问set中的元素。
关联容器和算法
我们一般不对关联容器使用泛型算法。
关联容器可用于只读取元素的算法。
如果我们真要对一个关联容器使用算法,要么将它当作一个源序列,要么当作一个目的位置(使用inserter绑定关联容器)
11.3.2 添加元素
关联容器的insert成员向容器中添加一个元素或一个元素范围。
insert有两个版本,分别接受一对迭代器,或是一个初始化器列表。
【拓】
map<string, size_t>::value_type(s,1);//构造一个恰当的pair类型
检查insert的返回值
insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个指向具有给定关键字的元素的迭代器;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。
对于允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器。(这里无需返回一个bool值,因为insert总是向这类容器中加入一个新元素)
11.3.3 删除元素
关联容器定义了三个版本的erase。
与顺序容器一样,我们可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或者一个元素范围。
关联容器还提供了一个额外的erase操作,它接受一个key_value参数。此版本删除所有匹配给定关键字的元素,返回实际删除元素的数量。
11.3.4 map的下标操作
map和unordered_map容器提供了下标运算符和一个对应的at函数。我们不能对一个multimap或一个unordered_multimap进行下标操作。
(set的类型不支持下标操作,因为set中没有与关键字相关联的“值”)
map下标运算符接受一个索引,获取与此关键字相关联的值;如果关键字不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。
由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
11.3.5 访问元素
如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
如果元素存在,lower_bound返回指向第一个与关键字匹配的元素,upper_bound指向最后一个匹配关键字的元素之后的元素。这两个操作并不报告关键字是否存在,重要的是他们的返回值可作为一个迭代器范围。
使用equal_range:
11.4 无序容器
这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。
管理桶
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
无序容器提供了一组管理桶的函数。
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的==运算符来比较元素,他们还使用一个哈希<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string和智能指针定义了hash。因此,我们可以直接定义关键字为内置类型(包括指针类型)、string还是智能指针类型的无序容器。
但是,我们不能直接定义关键字类型为自定义类类型的无序容器,而必须提供我们自己的hash模板版本和==运算符。
第十二章 动态内存
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。
每个程序还拥有一个内存池,这部分内存被称为自由空间或堆。
12.1 动态内存与智能指针
新标准库提供了两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。
这三种类型都定义在头文件<memory>中。
12.1.1 shared_ptr类
智能指针也是模板。默认初始化的智能指针中保存着一个空指针。
智能指针的使用方法与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
make_shared函数
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
其参数列表与T类的构造函数相关。
shared_ptr的拷贝与赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。
shared_ptr会自动销毁所管理的对象
shared_ptr还会自动释放相关联的内存
使用了动态生存期的资源的类
程序使用动态内存处于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
12.1.2 直接管理内存
使用new动态分配和初始化对象
动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
我们可以使用传统的构造方式(使用圆括号)、和列表初始化(使用花括号):
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的。
但是对于内置类型,两种形式的差别就很大了:值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。
如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。只适用于括号内仅有单一的初始化器才可以使用。
动态分配的const对象
对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
内存耗尽
如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。
我们可以改变使用new的方式来阻止它抛出异常:
int *p1 = new int; //如果分配失败,new抛出std::bad_alloc //默认初始化 int *p2 = new (nothrow)int; //如果分配失败,new返回一个空指针
第二种我们称之为定位new。定位new表达式允许我们向new提供额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow对象。bad_alloc和nothrow都定义在头文件new中。
释放动态内存
delete,delete[]
指针值和delete
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
delete之后重置指针值
动态对象的生存期直到被释放时为止
当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
12.1.3 shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们还可以用new返回的指针来初始化智能指针:
接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式来初始化一个指针:
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。
我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,我们必须提供自己的操作来替代delete。
不要混用普通指针和智能指针
也不要使用get的返回值初始化另一个智能指针或为智能指针赋值
get函数是为了这样一种情况设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
其他shared_ptr操作
我们可以用reset来讲一个新的指针赋予一个shared_ptr。
reset成员经常与unique成员一起使用。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户,如果不是,在改变之前要制作一份新的拷贝:
12.1.4 智能指针和异常
使用智能指针,当代码块抛出异常过早结束,局部对象也会被销毁。
当使用普通指针,在new和delete之间发生异常,且异常未在代码块内被捕获,则内存就永远不会被释放了。
智能指针和哑类
不是所有类都定义了析构函数,特别是那些为了C和C++两种语言设计的类,通常要求用户显示地释放使用的任何资源。
与管理动态内存类似,我们通常可以使用类似的计数来管理不具好定义的析构函数的类。
使用我们自己的释放操作
我们可以在使用shared_ptr时定义一个删除器代替delete。
删除器必须接受单个类型为shared_ptr中保存的指针类型的指针参数。
使用智能指针的基本规范
12.1.5 unique_ptr
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。故,初始化unique_ptr必须采用直接初始化形式:
由于一个unuque_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr。(这是一种特殊的“拷贝”)
向unique_ptr传递删除器
类似shared_ptr,unique_ptr默认情况下用delete,也可以重载删除器。但是,unique_ptr管理删除器的方式与shared_ptr不同,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)。
release成员
release操作是用来将对象的所有权转移给另一个unique_ptr的。
而shared_ptr可以“共享”对象的所有权。需要共享时,可以简单拷贝和赋值,并不需要release操作。
12.1.6 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象。
通过weak_ptr保存,不会影响生存期,但是可以阻止用户访问一个不存在的空间的企图。
12.2 动态数组
C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。
标准库中还包含一个名为allocator的类,允许我们将分配和初始化分离,使用allocator通常会提供更好的性能和更灵活的内存管理能力。
12.2.1 new和数组
new分配一个对象数组,返回指向第一个对象的指针,而不是一个数组类型的对象。
分配一个数组会得到一个元素类型的指针
不能对动态数组调用begin或end,也不能使用范围for语句处理该数组中的元素。
初始化动态分配对象的数组
默认情况下,new分配的对象,都是默认初始化。当然,我们可以对其执行值初始化。
也可以执行列表初始化(不足部分执行值初始化)
虽然我们可以用空括号对数组中元素进行值初始化,但不能在括号中给出初始化去,这意味着不能使用auto分配数组。(因为括号中只允许单一初始化器,所以我们不能使用auto分配数组)
方括号中的大小必须是整数,但不必是常数。
当用new分配一个大小为0的数组时,new会返回一个合法的非空指针。此指针就像尾后指针一样。
释放动态数组
数组中的元素按逆序销毁,即最后一个元素首先被销毁。
智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。
//up指向一个包含10个未初始化int的数组 unique_ptr<int[]> up(new int[10]); up.reset(); // 自动用delete[]销毁其指针
类型说明符必须带上[ ]表示指向的是一个类型数组,而不是某个类型的单一对象,这样在销毁时会自动使用delete[ ]。
与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:
同时,shared_ptr未定义下标运算符,而且智能指针类型不支持指针算数运算。因此,为了访问数组中的元素,必须使用get获得内置指针,然后用它来访问数组元素。
【拓】strcpy和strcat定义在<cstring>中
12.2.2 allocator类
#include <memory>
allocator分配的内存是原始的、未构造的。
allocator是一个模板,我们必须指明allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:
deallocate的第一个参数不能是空指针。它必须指向由allocate分配的内存。
拷贝和填充未初始化内存的算法
标准库为allocator类定义了两个伴随算法,可以在未初始化的内存中船舰对象。
上述算法返回一个指针,指向最后一个构造的元素之后的位置。
12.3 文本查询查询
12.3.1 面向对象编程
使用类管理数据,对于一些共享数据可能得用shared_ptr来管理。
12.3.2 面向过程编程
不使用类管理数据,对于一些共享数据,定义为全局变量,或定义为局部变量再通过函数参数传递。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix