1 只有序列容器支持push_front或push_back,
只有关联容器支持count和lower_bound,等等.
2 (条款1解释了deque是惟一一个在迭代器失效的
状况下指针和引用仍然有效的东西) 【不做为特例】
3 迭代器/指针/参考的失效规则
4 typedef代码封装
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
更高等级的封装,限制权限,使用class CustomList含有List
5 nth_element算法 【让第n+1大/小的数据,出如今v[n],且它是数据的大小分水岭,两边数据不保证有序】
6 拷进去,拷出来。这就是STL的方式。是的,拷贝对象是STL的方式。
7 基类对象容器,固然因为继承的存在,拷贝会致使分割。【splice】
8 咱们也能够创建一个能够足够包含maxNumWidgets个Widget的空vector,但没有构造Widget:
vector<Widget> vw;
vw.reserve(maxNumWidgets); // reserve的详细信息请参见条款14
【比数组文明多了(数组直接用默认构造函数构造对象了)】
9 尽可能调用empty(splice致使不能常数时间知道size是否为0,必需要去计数)【区间插入?】
10 第一,它提供给我一个机会来提醒你assign成员函数的存在,
太多的程序员没注意到这是一个很方便的方法。
它对于全部标准序列容器(vector,string,deque和list)都有效。
【替代数据集】
11 v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
写这些仍然比写assign的调用要作更多的工做。
此外,虽然在这段代码中没有表现出循环,在copy中的确存在一个循环(参见条款43)。
结果,效率损失仍然存在。在这里,我要离题一下来指出几乎全部目标区间是经过
插入迭代器(好比,经过inserter,back_inserter或front_inserter)指定的copy的使用
均可以——应该——经过调用区间成员函数来代替。
好比这里,这个copy的调用能够用一个insert的区间版本代替:
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
12 通常来讲使用区间成员函数能够输入更少的代码。
区间成员函数会致使代码更清晰更直接了当。
13 ● 区间构造。全部标准容器都提供这种形式的构造函数:
container::container(InputIterator begin, // 区间的起点
InputIterator end); // 区间的终点
若是传给这个构造函数的迭代器是istream_iterators或istreambuf_iterators(参见条款29),
你可能会遇到C++的最惊异的解析,缘由之一是你的编译器可能会由于把这个构造看做一个函数
声明而不是一个新容器对象的定义而中断。条款6告诉你须要知道全部关于解析的东西,包括怎么对付它。
● 区间插入。全部标准序列容器都提供这种形式的insert:
void container::insert(iterator position, // 区间插入的位置
InputIterator begin, // 插入区间的起点
InputIterator end); // 插入区间的终点
关联容器使用它们的比较函数来决定元素要放在哪里,因此它们了省略position参数。
void container::insert(lnputIterator begin, InputIterator end);
当寻找用区间版本代替单元素插入的方法时,不要忘记有些单元素变量用采用不一样的函数名假装它们本身。
好比,push_front和push_back都把单元素插入容器,即便它们不叫insert。若是你看见一个循环调用push_front或
push_back,或若是你看见一个算法——好比copy——的参数是front_inserter或者back_inserter,你就发现了一个
insert的区间形式应该做为优先策略的地方。
● 区间删除。每一个标准容器都提供了一个区间形式的erase,可是序列和关联容器的返回类型不一样。
序列容器提供了这个:
iterator container::erase(iterator begin, iterator end);
而关联容器提供这个:
void container::erase(iterator begin, iterator end);程序员
为何不一样?解释是若是erase的关联容器版本返回一个迭代器(被删除的那个元素的下一个)会招致一个没法
接受的性能降低。我是众多发现这个徒有其表的解释的人之一,但标准说的就是标准说的,标准说erase的序列
和关联容器版本有不一样的返回类型。算法
这个条款的对insert的性能分析大部分也一样能够用于erase。单元素删除的函数调用次数仍然大于一次调用区间
删除。当使用单元素删除时,每一次元素值仍然必须向它们的目的地移动一位,而区间删除能够在一个单独的
移动中把它们移动到目标位置。编程
关于vector和string的插入和删除的一个论点是必须作不少重复的分配。(固然对于删除,会发生重复的回收。)
那是由于用于vector和string的内存自动增加来适应于新元素,但当元素的数目减小时它不自动收缩。(条款17描
述了你怎么减小被vector或string持有的没必要要的内存。)
一个很是重要的区间erase的表现是erase-remove惯用法。你能够在条款32了解到全部关于它的信息。
● 区间赋值。就像我在这个条款的一开始提到的,全部标准序列容器都提供了区间形式的assign:
void container::assign(InputIterator begin, InputIterator end);
因此如今咱们明白了,尽可能使用区间成员函数来代替单元素兄弟的三个可靠的论点。设计模式
区间成员函数更容易写,它们更清楚地表达你的意图,并且它们提供了更高的性能。那是很难战胜的三驾马车。
14 ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
命名迭代器对象的使用和普通的STL编程风格相反,可是你得判断这种方法对编译器和必须使用编译器的人
都模棱两可的代码是一个值得付出的代价。数组
【注意stream相似的迭代器,都给予命名】
15 它们能告诉你所容纳的对象类型(经过value_type的typedef)
16 销毁new 使用foreach 和 模板函数对象
struct xx
{
template<typename T>
void operator()(const T *p) const
{
delete p;
//p = NULL; //置null没意义,且是const,当心屡次delete
}
}
若是在foreach以前抛异常,可使用智能指针boost shared_ptr来避免泄漏。
17 若是你有一个连续内存容器(vector、deque或string——参见条款1),安全
最好的方法是erase-remove惯用法(参见条款32):
c.erase(remove(c.begin(), c.end(), 1963), // 当c是vector、string
c.end()); // 或deque时,
// erase-remove惯用法
// 是去除特定值的元素
// 的最佳方法
这方法也适合于list,可是,正如条款44解释的,list的成员函数remove更高效:
c.remove(1963); // 当c是list时,
// remove成员函数是去除
// 特定值的元素的最佳方法数据结构
对于关联容器,解决问题的适当方法是调用erase:
c.erase(1963); // 当c是标准关联容器时
// erase成员函数是去除
// 特定值的元素的最佳方法
18 由于它为vector、string和deque产生未定义的行为!要记得对于那样的容器,多线程
调用erase不只使全部指向被删元素的迭代器失效,也使被删元素以后的全部迭代器失效。less
19 若是咱们观察在本条款中提到的全部东西,咱们得出下列结论:函数
● 去除一个容器中有特定值的全部对象:
若是容器是vector、string或deque,使用erase-remove惯用法。
若是容器是list,使用list::remove。
若是容器是标准关联容器,使用它的erase成员函数。
● 去除一个容器中知足一个特定断定式的全部对象:
若是容器是vector、string或deque,使用erase-remove_if惯用法。
若是容器是list,使用list::remove_if。
若是容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
● 在循环内作某些事情(除了删除对象以外):
若是容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器。
若是容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
20 当涉及到线程安
全和STL容器时,你能够肯定库实现容许在一个容器上的多读取者和不一样容器上的多写入者
【使用锁 RAII机制在构造函数里上锁(传容器为参数)】
21 3. 你必须确保只delete一次。若是一个分配被删除了不止一次,结果也会未定义。
【屡次delete空不会产生问题】
22 string使用引用计数的话,多线程下不安全。能够用vector<char> 替代。
【对于多线程,咱们应该设计尽可能避免竞态条件,此是根本,谨慎设计共享变量】
23 使用reserve来避免没必要要的从新分配
24 事实上,让C风格API把数据放入一个vector,而后拷到你实际想要的STL容器中的主意老是有效的。
【同理,能够将stl容器经过vector再转回C Api中,vector是桥】
if( !v.empty() )
{
dosomething(&v[0],v.size()); //不要用v.begin()
}
25 vector<Contestant>(contestants).swap(contestants); ???
因此当你想对vector和string进行“收缩到合适”时,就考虑“交换技巧”。
string().swap(s); // 清除s并且最小化它的容量
26 标准库提供了两个替代品,它们能知足几乎全部须要。第一个是deque<bool>。deque提供了几乎全部vector所
提供的(惟一值得注意的是reserve和capacity),而deque<bool>是一个STL容器,它保存真正的bool值。当
然,deque内部内存不是连续的。因此不能传递deque<bool>中的数据给一个但愿获得bool数组的C API[1](参
见条款16),但你也不能让vector<bool>作这一点,由于没有可移植的取得vector<bool>中数据的方法。
【vector<bool>不知足STL容器的必要条件,你最好不要使用它。】
【vector<bool>的位域实现。】
若是不在意没有迭代器和动态改变大小,你也许会发现bitset正合你意。
27 《绿野仙踪》。全世界最有名的童话
片之一,荣获1939年奥斯卡最佳电影歌曲和最佳电影配乐。其中的多色马,关联容器是不一样颜色的生物。真
的,它们共享了序列容器的不少特性,但它们在不少基本方法上不一样
28 find
对“相同”的定义是相等,基于operator==。set::insert对“相同”的定义是等价,一般基于operator<。
等价通常在每种标准关联容器(好比,set、multiset、map
和multimap)的一部分——排序顺序方面有意义
29 。set<Widget>的默认比较函数是less<Widget>,而默认的less<Widget>简单地对Widget调用
operator<,因此w1和w2关于operator<有等价的值若是下面表达式为真:
!(w1 < w2) // w1 < w2时它非真
&& // 并且
!(w2<w1) // w2 < w1时它非真
在通常状况下,用于关联容器的比较函数不是operator<或甚至less,它是用户定义的判断式。(关于判断式的
更多信息参见条款39。)每一个标准关联容器经过它的key_comp成员函数来访问排序判断式,因此若是下式求
值为真,两个对象x和y关于一个关联容器c的排序标准有等价的值:
!c.key_comp()(x, y) && !c.key_comp()(y, x) // 在c的排序顺序中
// 若是x在y以前它非真,
// 同时在c的排序顺序中
// 若是y在x以前它非真
30 if (ciss.find("persephone") != ciss.end())... // 这个测试会成功
但若是咱们用非成员的find算法,搜索会失败:
if (find(ciss.begin(), ciss.end(),
"persephone") != ciss.end())... // 这个测试会失败
那是由于“persephone”等价于“Persephone”(关于比较仿函数CIStringCompare),但不等于它(由于string
("persephone") != string("Persephone"))。这个例子演示了为何你应该跟随条款44的建议优先选择成员函数
(就像set::find)而不是非成员兄弟(就像find)的一个理由。
容器的比较函数对象。算法find用的==
31 经过只使用一个比较函数并使用等价做为两个值“相等”的意义的仲裁者
32 你应该回忆起
set<string*> ssp;
是这个的简写:
set<string*, less<string*> > ssp;
好,为了彻底准确,它是
set<string*, less<string*>, allocator<string*> > ssp;
33 使人遗憾的是,stringPtrLess不是一种类型,它是一个
函数。这就是为何尝试使用stringPtrLess做为set的比较函数不能编译的缘由,set不要一个函数,它要的是能在内部用实例化创建函数的一种类型。
【for_each可使用函数】
struct DereferenceLess {
template <typename PtrType>
bool operator()(PtrType pT1, // 参数是值传递的,
PtrType pT2) const // 由于咱们但愿它们
{ // 是(或行为像)指针
return *pT1 < *pT2;
}
};
34 比较函数总应该对相等的值返回false。
35 强制类型转换,产生一个临时对象
if (i != se.end()) { // 同上,
((Employee)(*i)).setTitle("Corporate Deity"); // 但使用C
} // 强转语法
【相似于】
if (i != se.end()){
Employee tempCopy(*i); // 把*i拷贝到tempCopy
tempCopy.setTitle("Corporate Deity"); // 修改tempCopy
}
36 若是你要老是能够工做并且老是安全地改变set、multiset、
map或multimap里的元素,按五个简单的步骤去作:
1. 定位你想要改变的容器元素。若是你不肯定最好的方法,条款45提供了关于怎样进行适当搜寻的指导。
2. 拷贝一份要被修改的元素。对map或multimap而言,肯定不要把副本的第一个元素声明为const。毕竟,你想要改变它!
3. 修改副本,使它有你想要在容器里的值。
4. 从容器里删除元素,一般经过调用erase(参见条款9)。
5. 把新值插入容器。若是新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert的“提示”形式把插入的效率从对数时间改进到分摊的常数时间。使用你从第一步得到的迭代器做为提示。
37 EmpIDSet se; // 同前,se是一个以ID号
// 排序的雇员set
Employee selectedID; // 同前,selectedID是一个带有
// 须要ID号的雇员
...
EmpIDSet::iterator i =
se.find(selectedID); // 第一步:找到要改变的元素
if (i!=se.end()){
Employee e(*i); // 第二步:拷贝这个元素
se.erase(i++); // 第三步:删除这个元素;
// 自增这个迭代器以
// 保持它有效(参见条款9)
e.setTitle("Corporate Deity"); // 第四步:修改这个副本
se.insert(i, e); // 第五步:插入新值;提示它的位置
// 和原先元素的同样
}
你将原谅我以这种方法放它,但要记得关键的事情是对于set和multiset,若是你进行任何容器元素的原地修改,你有责任确保容器保持有序
【原地修改不要修改set的key】
38 一旦你写了DataCompare,东西都很好的依序排列了。而一旦位置合适了,只要你的程序按照
101页描述的阶段方式使用数据结构,它们每每比相应的使用真的map的设计运行得更快并且使用更少内存。
若是你的程序不是按照阶段的方式操做数据结构,那么使用有序vector代替标准关联容器几乎能够肯定是在
浪费时间。
39 若是你要更新已存在的map元素,operator[]更好,但若是你要增
加一个新元素,insert则有优点。
【人如其名】
40 尽可能用iterator代替const_iterator,reverse_iterator和
const_reverse_iterator
41 由于它可能花费线性时间的代价来产生一个和const_iterator等价的iterator,而且由于若是不能访问
const_iterator所属的容器这个操做就没法完成。从这个角度出发,也许你须要从新审视你从const_iterator产生
iterator的设计。事实上那样的考虑帮助激发了条款26,它建议你当处理容器时尽可能用iterator代替const和reverse迭代器。
42 reverse_iterator的base成员函数返回一个“对应的”iterator的说法并不许确。对于插入操
做而言,的确如此;可是对于删除操做,并不是如此。当须要把reverse_iterator转换成iterator的时候,有一点非
常重要的是你必须知道你准备怎么处理返回的iterator,由于只有这样你才能决定你获得的iterator是不是你需
要的。
【并非真正对着同一个位置】
43 当你了解它以后,你也应该考虑把ostreambuf_iterator用于相应的无格式一个一个字符输出的做。它们没有了
ostream_iterator的开销(和灵活性),因此它们一般也作得更好。
44
不管什么时候你使用一个要求指定目的区间的算法,确保目的区间已经足够大或者在算法执行时能够增长大小。
若是你选择增长大小,就使用插入迭代器,
好比ostream_iterators或从back_inserter、front_inserter或inserter返回的迭代器。
这是全部你须要记住的东西。
45
● 若是你须要在vector、string、deque或数组上进行彻底排序,你可使用sort或stable_sort。
● 若是你有一个vector、string、deque或数组,你只须要排序前n个元素,应该用partial_sort。
● 若是你有一个vector、string、deque或数组,你须要鉴别出第n个元素或你须要鉴别出最前的n个元素,
而不用知道它们的顺序,nth_element是你应该注意和调用的。
● 若是你须要把标准序列容器的元素或数组分隔为知足和不知足某个标准,你大概就要找partition或stable_partition。
● 若是你的数据是在list中,你能够直接使用partition和stable_partition,你可使用list的sort来代替sort和stable_sort。
若是你须要partial_sort或nth_element提供的效果,你就必须间接完成这个任务,但正如我在上面勾画的,会有不少选择。
46 须要更少资源(时间和空间)的算法列在须要更多的前面:
1. Partition
2. stable_partition
3. nth_element
4. partial_sort
5. sort
6. stable_sort
47 一旦你知道了remove不能“真的”从一个容器中删除东西,和erase联合使用就变成理所固然了。你要记住的
惟一其余的东西是remove不是惟一这种状况的算法。另外有两种“相似remove”的算法:remove_if和unique。
remove和remove_if之间的类似性很直截了当。因此我不会细讲,但unique行为也像remove。它用来从一个区
间删除东西(邻近的重复值)而不用访问持有区间元素的容器。结果,若是你真的要从容器中删除元素,你
也必须成对调用unique和erase,unique在list中也相似于remove。
正像list::remove真的删除东西(并且比eraseremove惯用法高效得多)。
list::unique也真的删除邻近的重复值(也比erase-unique高效)。
【成员函数好,成员函数好】
48 无论你怎么选择处理动态分配指针的容器,经过引用计数智能指针、在调用相似remove的算法前手动删除和
废弃指针或者一些你本身发明的技术,本条款的指导意义依然同样:提防在指针的容器上使用相似remove的算法。
没有注意这个建议的人只能形成资源泄漏
【remove的机制形成】
49 我知道大家中的一部分会用蛮力记忆,因此这里有一个只能操做有序数据的算法的表:
binary_search lower_bound
upper_bound equal_range
set_union set_intersection
set_difference set_symmetric_difference
merge inplace_merge
includes
50 11个名字带“copy”的算法:
copy copy_backward
replace_copy reverse_copy
replace_copy_if unique_copy
remove_copy rotate_copy
remove_copy_if partial_sort_copy
unintialized_copy
51 用accumulate或for_each来统计区间
52 BPFC::operator()的实现例证了BPFC全部的虚函数是怎么实现的:它们调用了在BPFCImpl中它们真的虚函
数。结果是仿函数类(BPFC)是小而单态的,但能够访问大量状态并且行为多态。
我在这里忽略了不少细节,由于我勾勒出的基本技术在C++圈子中已经广为人知了。《Effective C++》的条款34中有。
在Gamma等的《设计模式》[6]中,这叫作“Bridge模式”。Sutter在他的《Exceptional C++》[8]中叫它“Pimpl惯用法”.
从STL的视角看来,要记住的最重要的东西是使用这种技术的仿函数类必须支持合理方式的拷贝。若是你是
上面BPFC的做者,你就必须保证它的拷贝构造函数对指向的BPFCImpl对象作了合理的事情。也许最简单的合理的东西是引用计数,使用 相似Boost的shared_ptr,你能够在条款50中了解它.
实际上,对于本条款的目的,惟一你必须担忧的是BPFC的拷贝构造函数的行为,由于当在STL中被传递或从
一个函数返回时,函数对象老是被拷贝——值传递,记得吗?那意味着两件事。让它们小,并且让它们单态。
53 (not一、not二、bind1st和bind2nd)都须要存在某些typedef
54 这是STL里的一个广泛习惯:函数和函数对象总使用用于非成员函数
的语法形式调用
55 了解使用ptr_fun、mem_fun和mem_fun_ref的缘由
56 不要经过把less的定义当儿戏来误导那些程序员。若是你使用less(明确或者隐含),保证它表示operator<。
若是你想要使用一些其余标准排序对象,创建一个特殊的不叫作less的仿函数类
57
● 效率:算法一般比程序员产生的循环更高效。
● 正确性:写循环时比调用算法更容易产生错误。
● 可维护性:算法一般使代码比相应的显式循环更干净、更直观。
58
几乎不可能被战胜的sort及其同族算法(好比,
stable_sort(),nth_element()等,参见条款31);适用于有序区间的搜索算法(好比,binary_search,
lower_bound等,参见条款34和35)也同样好;就算是很平凡的任务,好比从连续内存容器中除去一些对象,
使用erase-remove惯用法都比绝大多数程序员写的循环更高效
【尽可能使用stl提供的算法,人那是计算机科学家写的】
59 在算法调用与手写循环正在进行的较量中,关于代码清晰度的底线是:这彻底取决于你想在循环里作的是什
么。若是你要作的是算法已经提供了的,或者很是接近于它提供的,调用泛型算法更清晰。若是循环里要作
的事很是简单,但调用算法时却须要使用绑定和适配器或者须要独立的仿函数类,你恐怕仍是写循环比较
好。最后,若是你在循环里作的事至关长或至关复杂,天平再次倾向于算法。由于长的、复杂的一般总应该
封装入独立的函数。只要将循环体一封装入独立函数,你几乎总能找到方法将这个函数传给一个算法(一般
是for_each),以使得最终代码直截了当。
只要能用高层次的术语——如insert、find和for_each,
取代了低层次的词汇——如for、while和do,咱们就提高了软件的【【抽象】】层次
60 当面临着STL算法和同名的容器成员函数间进行选择时,你应该尽可能使用成员函数。几
乎能够确定它更高效,并且它看起来也和容器的惯常行为集成得更好。
【私人定制服务】
61 count和find是线性时间的,但有序区间的
搜索算法(binary_search、lower_bound、upper_bound和equal_range)是对数时间的。
62 我会简单地说明count和 find算法都用相等来搜索,
而binary_search、lower_bound、upper_bound和equal_range则用等价
63 lower_bound回答这个问题:“它在吗?若是
是,第一个拷贝在哪里?若是不是,它将在哪里?”
保持比
较函数同步不是火箭发射,但倒是另外一个要记住的东西,并且我想你已经有不少须要你记的东西了。
64 使用equal_range。equal_range返回一对迭代器,第一个等于lower_bound返回的迭代
器,第二个等于upper_bound返回的(也就是,等价于要搜索值区间的末迭代器的下一个)。所以,
equal_range,返回了一对划分出了和你要搜索的值等价的区间的迭代器。一个名字很好的算法,不是吗?
65 一般咱们有一个容器,而不是一个区间。在这种状况下,咱们必须区别序列和关联容器。对于标准的序列容器(vector、string、deque和list),你
应该遵循我在本条款提出的建议,使用容器的begin和end迭代器来划分出区间。
这种状况对标准关联容器(set、multiset、map和multimap)来讲是不一样的,由于它们提供了搜索的成员函
数,它们每每是比用STL算法更好的选择。条款44详细说明了为何它们是更好的选择,简要地说,是由于
它们更快行为更天然。幸运的是,成员函数一般和相应的算法有一样的名字,因此前面的讨论推荐你使用的
算法count、find、equal_range、lower_bound或upper_bound,在搜索关联容器时你均可以简单的用同名的成员
函数来代替。
调用binary_search的策略不一样,由于这个算法没有提供对应的成员函数。要测试在set或map中是否存在某个
值,使用count的惯用方法来对成员进行检测:
。。。。
要测试某个值在multiset或multimap中是否存在,find每每比count好,由于一旦找到等于指望值的单个对象,
find就能够停下了,而count,在最遭的状况下,必须检测容器里的每个对象。(对于set和map,这不是问
题,由于set不容许重复的值,而map不容许重复的键。)
可是,count给关联容器计数是可靠的。特别,它比调用equal_range而后应用distance到结果迭代器更好。首
先,它更清晰:count 意味着“计数”。第二,它更简单;不用创建一对迭代器而后把它的组成(译注:就
条款45:注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别
是first和second)传给distance。第三,它可能更快一点。
你想知道的
使用的算法使用的成员函数
在无序区间在有序区间在set或map上在multiset或multimap上
指望值是否存在? find binary_search count find
指望值是否存在?
若是有,第一个等
于这个值的对象在
哪里?
find equal_range find find或lower_bound(参见下面)
第一个不在指望值
以前的对象在哪
里?
find_if lower_bound lower_bound lower_bound
第一个在指望值之
后的对象在哪里?
find_if upper_bound upper_bound upper_bound
有多少对象等于期
望值?
count equal_range,而后distance count count
等于指望值的全部
对象在哪里?
find(迭代) equal_range equal_range equal_range
66 把函数对象做为算法的参数所带来的不只是巨大的效率提高。在让你的代码能够编译方面,它们也更稳健。
固然,真函数颇有用,可是当涉及有效的STL编程时,函数对象常常更有用。
【他们可能会内联,而普通函数是函数指针,每次的指针调用】 67 写可读性高的代码 68 在这里,它叫作_Tree,但我知道的其余实现使用__tree或__rb_tree,后者反映出 使用红-黑树——在STL实现中最常使用的平衡树类型。