vector
、string
、deque
、list
、forward_list
(C++11)、array
(C++11)。【提供随机访问迭代器】
vector
可以作为 string
的替代。【条款 13】vector
作为标准关联容器的替代。【条款 23】set
、multiset
、map
、multimap
、unordered_set
(C++11)、unordered_multiset
(C++11)、unordered_map
(C++11)、unordered_multimap
(C++11)。【提供双向迭代器】slist
(单向链表,即提供单向迭代器)和 rope
(“重型” string)。【条款 50】hash_set
、hash_multiset
、hash_map
和 hash_multimap
。【条款 25】bitset
(include
)、valarray
(include
)、stack
(include
)、queue
(include
) 和 priority_queue
(include
)。【本书不多讨论,因为是非 STL 容器】【条款 16 提及数组】【条款 18 提及 bitset
】slist
和 rope
非标准。vector
、deque
和 string
(还有一个 rope
);如果要求使用双向迭代器,则必须避免 slist
以及哈希容器的一个常见实现。vector
。vector
和标准关联容器(按顺序先后优先选择)。string
(rope
也需要避免);如果避免使用 string
的情况下,可考虑 vector
来表示字符串。list
,因为在标准容器中,只有 list
对多个元素的插入操作提供了事务语义。【事务语义对于编写异常安全(exception-safe)的代码有帮助】【使用连续内存的容器也可以获得事务语义,但是要付出性能上的代价,代码也不直观(《Exceptional C++》第 17 条)】deque
是你所希望的容器。【当插入操作仅在容器末尾发生时,deque
的迭代器有可能会变为无效;deque
是唯一的、迭代器可能会变为无效而指针和引用不会变为无效的 STL 标准容器】vector
支持这一点),比如 vector
实际上并不存储 bool
类型变量(因此也无法直接用 bool
变量实例化某些容器)。typedef
无关。
typedef
其实只是类型别名,它所带来的封装效果只是词法上的。list
;而是创建一个新类,将 list
隐藏在其 private 部分中。vector
),客户所受影响比较少。insert
或 push_back
之类的操作)时,存入容器的是所指定的对象的拷贝;当从容器中取出一个对象(通过如 front
或 back
之类的操作)时,所得到的是容器中所保存的对象的拷贝。
Widget*
的容器,而不是 Widget
的容器。vector
会自动扩容,它与数组相比,已经迈出了一大步】if(c.size() == 0)...
if(c.empty())...
// 在实现上,size()是返回distance(begin(), end())【其实就是list调用distance是线性时间】
// empty()是返回begin()==end()
empty()
(它本身是 inline),它所做的仅仅是返回 size
是否为 0
。【书中如此写,但是其实 empty()
的实现是判断 begin()
是否等于 end()
】【即大多数情况下,调用 empty()
就是相当于调用 size()
】
empty()
对所有的标准容器都是常数时间操作。list
(双向循环链表)实现,size()
耗费线性时间(list
调用 distance()
需要耗费线性时间)。【C++11 之前,C++11 之后 list::size()
已经同样是常数时间操作了】list
不提供常数时间的 size()
函数,原因如下:
list
所独有的链接操作 splice()
需要保证,只能舍弃常数时间的取 size()
行为。【前面说到 C++11 之后 list::size()
已经同样是常数时间操作,则应该就是舍弃了 splice()
行为的特殊看待】list::splice
实现 list
拼接的功能;将源 list
的内容部分或全部元素删除,拼插入到目的 list
。【重要的是 splice()
只用常数时间拼接了两个 list
,这个操作对 merge()
、reverse()
、`sort() 有很大的帮助】【在实现上,它只是把两个节点相连,过程中没有成员被拷贝或被移动】splice()
函数的其中一个声明如下:void splice ( iterator position, list<T,Allocator>& x );
// position是要操作的list对象的迭代器,x是被剪的对象
// 即x转到position上
list
中为了保证独特的 splice()
操作,所以舍弃了随机访问的便利。size()
的实现中,它调用的是 distance(c.begin(), c.end())
。【也就是说,vector::size()
和 list::size()
代码实现上是一样的,都是调用 distance()
,关键在于 distance()
的实现】
vector
),distance()
实现上就直接 _end - _begin
,这就是常数时间的操作。【vector
提供随机访问迭代器,而随机访问迭代器能像指针一样进行算术计算,而不仅仅是单步向前】list
,即链表),distance()
在实现上需要麻烦一些:需要从 _begin
位置开始,开始遍历(其实就是递增迭代器),走完全程。【list
的迭代器是双向迭代器,只能单步前进】size()
的作用也只是拿来判断是否为空,如果使用容器是 list
,尽量直接调用 empty()
,不然容易出现性能问题。如题,优先使用针对区间的成员函数。
区间成员函数是指使用两个迭代器参数来确定该成员操作所执行的区间这样的成员函数。
本条款有个实例,考虑的场景是:将一个 int
数组拷贝到 vector
的前端(旧式 C API 可能遗留下来的数据就存放在 int
数组中);考虑的两个函数是:区间函数 copy()
和单元素函数 insert()
。【注意 insert()
也有区间版本,与单元素版本相比,参数个数不同】
首先,先看下区间版本的 insert()
解决这个问题的代码:【没有问题】
// 数据定义如下:
int data[numValues]; // 假设numValues在其他地方定义
vector<int> v;
// 区间版本的insert使用如下:
v.insert(v.begin(), data, data + numValues); // 把data中的int插入v前部
接着,看看如何使用单元素版本的 insert()
来解决这个问题:【要注意的细节很多】
// 单元素版本的insert使用如下:
vector<int>::iterator insertLoc(v.begin());
for (int i = 0; i < numValues; ++i) {
insertLoc = v.insert(insertLoc, data[i]);// 第一个点:需要更新迭代器,不然会失效
++insertLoc;// 第二个点:需要移动迭代器,没有这行,插入的数据会以相反的顺序拷贝到v的前面
}
最后,看看使用区间版本的 copy()
的情况:【没有问题】【但实现上调用 copy()
函数和循环单元素版本 insert()
其实是一致的】
// 区间版本的copy使用如下:
copy(data, data + numValues, inserter(v, v.begin()));
那么究竟单元素版本的 insert()
与区间版本的 insert()
相比,付出了什么代价呢?【除了第三条代价之外,前两个讨论也使用于 deque
】
insert()
花费了 numValues
次函数调用;区间版本的 insert()
只花费了 1
次函数调用。v
中有 n
个元素,那么,单元素版本的 insert()
要花费 n*numValues
那么多次的移动,区间版本的 insert()
只花费了 n
次移动(因为区间版本其实就是提前告诉编译器要插入 numValues
个,然后直接在 vector
中构造 numValues
次,紧接着把原先前 n
个位置搬到后 n
个位置,所以只拷贝了 n
次)。insert()
策略,区间 insert()
少执行了 n*(numValues-1)
次移动。insert()
而带来的高效率)在某个特定的场景下会失效,也就是当传入的迭代器是输入迭代器的时候;这是因为输入迭代器没有提供前向迭代器的功能,不能在一次移动中把一个元素移动到它的最终位置,导致还是需要一步步移动,所以期望中的优点就会消失。【输出迭代器虽然也没有提供前向迭代器,但它和这个问题没有关联,因为输出迭代器不能用于为 insert()
指定一个区间】vector
和 string
都有动态扩容机制】
vector
的动态扩容机制就导致了最好不要过多、不要单次的插入元素;因为内存满了的时候,vector
会尝试分配更多内存(从旧内存把它的元素拷贝到新内存,销毁旧内存里的元素,回收旧内存)。distance()
),便可一次性分配。最后,标准序列容器中,还剩下 list
未讨论,我们可以分析一下:在同样的场景下使用 list
,前面的三条代价哪条仍适用。
list
是链表管理方式。list
中的一些节点的 next
和 prev
指针赋值。
pre
和 next
节点,然后它的 pre
也要更新 pre.next
节点,它的 next
也要更新 next.pre
节点。list
的单元素 insert()
一个接一个添加时,除了最后一个以外的其他新节点都会设置它的 next
指针两次。
numValues
个节点插入 A
前面,插入节点的 next
指针会发生 numValues-1
次多余的赋值,而且 A
的 prev
指针会发生 numValues-1
次多余的赋值;合计 2*(numValues-1)
次没有必要的指针赋值。最后总结一下区间函数:区间构造(所有标准容器都提供)、区间插入(部分版本的 insert()
,所有标准序列容器都提供)、区间删除(部分版本的 erase()
,每个标准容器都提供,但序列和关联容器的返回类型不同)和区间赋值(部分版本的 assign()
,所有标准容器都提供)。
erase()
函数,序列和关联容器的返回类型不同;序列容器的 erase()
返回一个迭代器,关联容器的 erase()
返回 void
。【erase()
的关联容器版本返回一个迭代器(被删除的那个元素的下一个)会招致一个无法接受的性能下降】问题提出:
// 以下代码会带来问题:
std::ifstream dataFile("ints.dat");
std::list<int> data(std::istream_iterator<int>(dataFile),
std::istream_iterator<int>());
// 在这里,程序员的原意是:
// 把一个存有整数(int)的文件ints.dat拷贝到一个list中
// 但这段代码虽然通过了编译,但是运行期什么也不会发生
为了讲解问题产生的原因以及引入后面的解决方法,首先需要枚举函数参数声明的方式。【分析下面的代码中函数是如何声明,而编译器又是如何看待它们的】
// 下面三行代码是一样的结果:
// 都声明了一个带double参数并返回int的函数
int f1(double d);
int f2(double(d)); // d 两边的括号被忽略,即可以给参数名加上圆括号
int f3(double); // 这里只是传参可以没有名字,说明这个参数在函数中没有意义
// 另一种情况,下面三行代码也是一样的结果:
// 都声明了同一种函数
// 参数是一个指向不带任何参数的函数的指针
// 返回double值
int g1(double(*pf)()); // g1以指向函数的指针为参数
int g2(double pf()); // 同上,pf为隐式指针
int g3(double()); // 同上,省去参数名
// 这里的分析就是在于:括号中有无参数
// f2其实就代表了f1和f3,因为参数名称d两边的括号可以省略
// g1、g2和g3中参数pf并不重要,重要的是后面的空括号,它决定了这里参数是指针
有了这些前置知识,我们再回头分析场景问题中的那个代码出现了什么问题:
std::ifstream dataFile("ints.dat");
std::list<int> data(std::istream_iterator<int>(dataFile), std::istream_iterator<int>());
// 前面说了,程序员的原意是:
// 把一个存有整数(int)的文件ints.dat拷贝到一个list中。
// 但是,编译器的理解是:
// 声明了一个函数,名称为data;
// 返回类型是list;
// 第一个参数名称为dataFile,类型为istream_iterator;
// 第二个参数无名,类型是函数指针,这个指针指向的函数不带参数、返回值是istream_iterator。
所以现在对这个问题的分析已经几乎结束了(除了未提出解决方案),但目前应该可以理解下面代码的问题:
class Widget{...};// 声明一个Widget类,里面有默认构造函数
// 想要声明一个类,需要如此做:
Widget w;
// 但有人可能写出如此代码:
Widget w_error();// 这行代码声明了一个名叫w_error的函数
// 这可能是C++11增加列表初始化(list-initialization)的其中一个原因
Widget w_right{};// 和前一个代码理念上相近
最后的最后,提出解决方案:
// 方案一:加多一对括号,强迫编译器按照我们的方式来工作。
// 之所以新添的括号不会被省略,我猜测是因为括号内有类型名称,而不是只有变量名称
std::list<int> data1((std::istream_iterator<int>(dataFile)),
std::istream_iterator<int>());
// 方案二:避免使用匿名对象,给这些迭代器一个名称
// 虽然与通常的STL风格有些违背,但为了让代码对于所有编译器都没有二义性,这一代价是值得的
std::istream_iterator<int> dataBegin(dataFile);
std::istream_iterator<int> dataEnd;
std::list<int> data2(dataBegin, dataEnd);
STL 的容器已经比较智能,能做的事情特别多;但是当容器里包含的是通过 new 的方式而分配的指针时,就需要程序员自己解决善后清理工作。【解决方法也很直白】
方法一:手动循环 delete。
for_each()
函数。for_each()
举的例子,写了一个函数子类(function class)去 delete,但是该函数子类是通过继承 unary_function
来实现的:【本条款的题外话】
unary_function
或者 binary_function
;继承之后,它就与其他的一元、二元运算符统一,同时也能继续扩展。【不继承,其实也能编译运行,但其他的配接器没法获得此运算符类的参数型别】unary
是一元运算符,unary_function
类内有两个 typedef,一个是传入参数,一个是返回类型;同理,binary
是二元运算符,binary_function
类内有三个 typedef,两个是传参,一个是返回类型。C++11
之后,unary_function
和 binary_function
被移除了。for_each()
函数调用自定义类来 delete 指针的代码中,还有一个值得注意的地方:// 书中讨论的第一套代码:
template<typename T>// 模板类
struct DeleteObject :
public unary_function<const T*, void> {
void operator()(const T* ptr) const
{
delete ptr;
}
};
// 在使用的时候,就需要标明要删除对象的类型是什么
vector<Widget*> v;
for_each(v.begin(), v.end(), DeleteObject<Widget>());// 要删除Widget*类型的指针
// 容易出问题的是下面这种使用情况:
class SpecialString : public string{...};// 这里本身有问题,这样的public继承不提倡
// string无虚析构函数,而SpecialString从string的继承是public
deque<SpecialString*> d;
for_each(d.begin(), d.end(), DeleteObject<string>());// 注意这里
// 考虑的是程序员把Derived类指针错写成Base类指针。
// 我猜测主要还是因为没有虚析构函数,所以如果告诉编译器只删除base类,
// 就会有一部分derived类资源没有被delete,因为调用的根本不是派生类的析构。
// 书中讨论的第二套代码:
struct DeleteObject {// 注意既没有类的模板化,也没有继承
template<typename T>
void operator()(const T* ptr) const
{
delete ptr;
}
};
// 最后这套代码对SpecialString的使用方式如下:
for_each(d.begin(), d.end(), DeleteObject());// 如此程序员不会像前面说的那样主动犯错了
方法二:容器一开始就只存放智能指针。
for_each()
版本始终是非异常安全的。boost::shared_ptr
来进行管理。auto_ptr
的容器就能使指针被自动销毁。【下一条款就讨论这个问题】auto_ptr
的容器是被禁止的。【auto_ptr
本身禁止拷贝】auto_ptr
意味着将其自身的值转移走。auto_ptr<Widget> pw1(new Widget);
auto_ptr<Widget> pw2(pw1);// 拷贝构造,pw1的值转移到了pw2中,pw1置为null
pw1 = pw2;// 拷贝赋值,pw2的值又转移回去了
auto_ptr
的容器排序的过程中,容器里的对象会被置为空。本条款描述了三种场景。
场景一:要删除容器中特定值的所有对象。【例如删除容器中所有值为 1963
的元素】
如果容器是 vector
、string
或 deque
,则使用 erase-remove 习惯用法:
// erase-remove习惯:
std::vector<int> c1;
c1.erase(std::remove(c1.begin(), c1.end(), 1963), c1.end());
如果容器是 list
,则使用 list::remove()
:
// 容器是list,用list的成员函数remove效果最好
std::list<int> c2;
c2.remove(1963);
如果容器是一个标准关联容器,则使用它的 erase()
成员函数:
// 容器是一个标准关联容器,使用成员函数erase最好
std::set<int> c3;
c3.erase(1963);
场景二:要删除容器中满足特定判别式(条件)的所有对象。【例如删除判别式返回 true
的每一个对象】
vector
、string
或 deque
,则使用 erase-remove_if 习惯用法;list
,则使用 list::remove_if()
;remove_copy_if()
和 swap()
;或者写一个循环来遍历容器中的元素,记住当把迭代器传给 erase()
时,要对它进行后缀递增。场景三:要在循环内做某些(除了删除对象之外的)操作。【例如每次元素被删除时,都向一个日志文件中写一条信息】
erase()
时,要用它的返回值更新迭代器;erase()
时,要对迭代器做后缀递增。near
和 far
指针的区别),但这个目的并没有达到;另一个目的是将分配器设计成促进全功能内存管理器的发展,但事实表明那种方法在 STL 的一些部分会导致效率损失。operator new
和 operator new[]
,STL 分配器负责分配(和回收)原始内存;但分配器的客户接口与 operator new
、operator new[]
甚至 malloc
几乎没有相似之处。T
代表为它分配内存的对象的类型。T*
,reference 为 T&
。
T
的对象,它的默认分配子(allocator
)提供了两个类型定义,分别是 allocator::pointer
和 allocator::reference
;用户定义的分配子也应该提供这些类型定义。operator.
(点操作符),而这种重载是被禁止的。T*
的同义词,每个分配器的 reference typedef 与 T&
相同。【这段话的意思是说,C++ 标准允许库实现者可以忽略 typedef 而直接使用原始指针和引用;所以即使程序员提供了新的指针和引用类型,也有可能被忽略】template<typename T> // 一个用户定义的分配器
class SpecialAllocator {...}; // 模板
typedef SpecialAllocator<Widget> SAW; // SAW = “SpecialAllocator
// for Widgets”
list<Widget, SAW> L1;
list<Widget, SAW> L2;
...
L1.splice(L1.begin(), L2); // 把L2的节点移到L1前端
// 在前面提到splice的时候就说过,它没有拷贝什么
// 所以这个操作是迅速且异常安全的
// 重点在于,当L1被销毁的时候,它要销毁它的所有节点(回收内存)
// 而L1的一部分其实是从L2来的
// 所以对于L1来说,只有相同类型的分配器对象都是等价的,它才能安全地回收L2的对象
allocate()
成员函数的是那些要求内存的对象的个数,而不是所需的字节数;同时要记住,这些函数返回 T*
指针(通过 pointer 类型定义),即使尚未有 T
对象被构造出来。【参数类型和返回类型都不同】
operator new
,但它们的接口不同,代码如下:void* operator new(size_t bytes);
pointer allocator<T>::allocate(size_type numObjects);
// 记住事实上“pointer”总是T*的typedef
// 对于operator new,参数(size_t类型)指定的是字节数;
// 而对于allocator::allocate,它指定的是内存里要能容纳多少个T对象。
allocator::allocate
返回的指针并不指向一个 T
对象,因为 T
还没有被构造。(意思即是只是头指针?)operator new
的知识无法应用于分配子中。【因为它们大体都不同】rebind
模板,因为标准容器依赖该模板。
之所以一定要提供 rebind
,是因为前面说到的:大多数标准容器从未调用它们相关的分配器。
尤其是对于 list
和所有标准关联容器来说,分配器无法分配这些基于节点的容器:【allocator
直观上根本没法满足 list
的实现,但它内部实现了 rebind
,所以我们只是进去拿到了 rebind
里的一些东西】
template<typename T, // list的可能
typename Allocator = allocator<T> > // 实现
class list{
private:
Allocator alloc; // 用于T类型对象的分配器
struct ListNode{ // 链表里的节点
T data:
ListNode *prev;
ListNode *next;
};
...
};
// 在这里可以看出,我们想要的不是T的分配子运作
// 而是希望它能够分配一个ListNode的内存(ListNode包含了一个T类元素)
list
需要的是从它的分配器类型那里获得用于 ListNode
的对应分配器的方法。
现在就要介绍 rebind
了。
// 前面刚说到,list 从分配器里获得 ListNode 的对应分配器,所以分配器中又给予了一个新的 typedef,叫做 other
// allocator的实现:
template<typename T> // 标准分配器像这样声明,
class allocator { // 但也可以是用户写的
public: // 分配器模板
template<typename U>
struct rebind{
typedef allocator<U> other;
}
...
};
// 这里的神奇之处在于:
// T的分配器对应的分配器类型是Allocator。
template<typename T,
typename Allocator = allocator<T> > // 是这个语句中的Allocator
class list{...}
// ListNodes的对应分配器类型是:
Allocator::rebind<ListNode>::other
所以结果就是:list
可以通过 Allocator::rebind
从它用于 T
对象的分配器(叫做 Allocator
)获取对应的 ListNode
对象分配器。
自定义分配子适合的场景有:
allocator
太慢,或者太占内存,或者导致了太多内存碎片。【可以实现更好的分配器】alloctor
是线程安全的(即为了维护线程安全必定付出了额外的代价),而程序员所关注的是单线程的环境,所以不愿意为线程同步付出额外的开销。【节省开销】场景一:例如考虑如何采用 malloc
和 free
内存模型来管理一个位于共享内存的堆。【就需要自定义一个分配子类】
代码如下:【容器 v
本身是栈中元素;但容器 v
中的元素是通过分配子在共享内存中取得】【所以可以看出,容器只是管理元素,而真正存储元素的位置是由分配子来决定】
void* mallocShared(size_t bytesNeeded)
{
return malloc(bytesNeeded);
}
void freeShared(void* ptr)
{
free(ptr);
}
template<typename T>
class SharedMemoryAllocator { // 把STL容器的内容放到共享内存(即由mallocShared生成的)中去
public:
typedef T* pointer; // pointer是个类型定义,它实际上总是T*
typedef size_t size_type; // 通常情况下,size_type是size_t的一个类型定义
typedef T value_type;
pointer allocate(size_type numObjects, const void* localityHint = 0)
{
return static_cast<pointer>(mallocShared(numObjects * sizeof(T)));
}
void deallocate(pointer ptrToMemory, size_type numObjects)
{
freeShared(ptrToMemory);
}
template<typename U>
struct rebind {
typedef std::allocator<U> other;
};
};
int test_item_11()
{
typedef std::vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;
// v所分配的用来容纳其元素的内存将来自共享内存
// 而v自己----包括它所有的数据成员----几乎肯定不会位于共享内存中,v只是普通的基于栈(stack)的对象,所以,像所
// 有基于栈的对象一样,它将会被运行时系统放在任意可能的位置上。这个位置几乎肯定不是共享内存
SharedDoubleVec v; // 创建一个vector,其元素位于共享内存中
// 为了把v的内容和v自身都放到共享内存中,需要这样做
void* pVectorMemory = mallocShared(sizeof(SharedDoubleVec)); // 为SharedDoubleVec对象分配足够的内存
SharedDoubleVec* pv = new (pVectorMemory)SharedDoubleVec; // 使用"placement new"在内存中创建一个SharedDoubleVec对象
// ... // 使用对象(通过pv)
pv->~SharedDoubleVec(); // 析构共享内存中的对象
freeShared(pVectorMemory); // 释放最初分配的那一块共享内存
return 0;
}
场景二:想把一些 STL 容器的内容放在不同的堆中。
allocator
时,需要遵守同一类型的 allocator
必须是等价的】【在这里的 Heap1
和 Heap2
都只是类型,不是对象;否则的话就将是不等价的分配器】// 假设有两个堆,命名为heap1和heap2类
// 每个堆类有用于进行分配和回收的静态成员函数
class Heap1 {
public:
...
static void* alloc(size_t numBytes, const void *memoryBlockToBeNear);
static void dealloc(void *ptr);
...
};
class Heap2 { ... }; // 有相同的alloc/dealloc接口
// 接下来设计一个分配器,使用像Heap1和Heap2那样用于真实内存管理的类:
template<typenameT, typename Heap>
class SpecificHeapAllocator {
public:
pointer allocate(size_type numObjects, const void *localityHint = 0)
{
return static_cast<pointer>(Heap::alloc(numObjects * sizeof(T),
localityHint));
}
void deallocate(pointer ptrToMemory, size_type numObjects)
{
Heap::dealloc(ptrToMemory);
}
...
};
// 最后使用SpecificHeapAllocator来把容器的元素集合在一起:
vector<int, SpecificHeapAllocator<int, Heap1 > > v; // 把v和s的元素都放进Heap1
set<int, SpecificHeapAllocator<int Heap1 > > s;
list<Widget, SpecificHeapAllocator<Widget, Heap2> > L; // 把L和m的元素放进Heap2
map<int, string, less<int>, SpecificHeapAllocator<pair<const int, string>,
Heap2> > m;
本节讲解 STL 容器对于线程安全性的知识。
对于一个 STL 实现,最多只能期望:
当一个库试图实现完全的容器线程安全时可能采取以下方案:【但其实很轻易就知道,这些都有问题,并不能解决并发问题,这是为了引出后面的RAII思想】
从一个场景代码中进行分析这三个方案是否就已经可行,代码如下:
```cpp
vector v;
vector::iterator first5(find(v.begin(), v.end(), 5)); // 行1
if (first5 != v.end()){ // 行2
*first5 = 0; // 行3
}
// 要让上面的代码成为线程安全的,v必须从行1到行3保持锁定
// 所以其实STL无法自动推断出这个
// 在多线程环境中:
// 另一个线程可能在行 1完成后执行,再返回到行 2 的时候,这里的 first5 可能失效。
// 另一个线程可能在行 2 和行 3 之间执行,也可能令 first5 失效。
// 用前面的三种方案能解决这些问题吗?答案是否。
// 行1中begin和end调用都返回得很快,以至于不能提供任何帮助;(即方案二无法起作用,因为begin和end生存期很短)
// 它们产生的迭代器只持续到这行的结束,而且find也在那行返回。(即方案三也无作用,因为find直接返回了)
```
真正的解决方案是手动进行同步控制。
方案一:手动添加互斥量。
vector<int> v;
...
getMutexFor(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()) { // 这里现在安全了
*first5 = 0; // 这里也是
}
releaseMutexFor(v);
方案二:面向对象地去解决。
Lock
类在它的构造函数里获得互斥量,并在它的析构函数里释放它。【其实就是 RAII 思想】// 创建一个Lock类:
template<typename Container> // 获取和释放容器的互斥量
class Lock { // 的类的模板核心;
public: // 忽略了很多细节
Lock(const Containers container)
: c(container)
{
getMutexFor(c); // 在构造函数获取互斥量
}
~Lock()
{
releaseMutexFor(c); // 在析构函数里释放它
}
private:
const Container& c;// 置为const
};
// 使用如下:
// 注意要建立新块
// 建立一个新快{}的作用是让互斥量的释放来得更快
vector<int> v;
...
{ // 建立新块;
Lock<vector<int> > lock(v); // 获取互斥量
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()) {
*first5 = 0;
}
} // 关闭块,自动释放互斥量