第1条:慎重选择容器类型
不同容器之间的差异很大,要根据需要选择适当的容器;考虑因素:是否与标准相符;迭代器类型(单向,双向,随机);元素布局与c的兼容性;容器内元素是否有序;查找速度;引用计数引起的反常;是否实现事务语义;在何种情况下会使迭代器,引用,指针无效;不同容器的内存分配策略;等等
第2条:不要试图编写独立于容器类型的代码
标题的意思是:由于不同的容器之间差异很大,优缺点很明显,因此不要试图去编写适用于所有容器的代码。比如它们所支持的成员函数;支持的迭代器类型;使迭代器,指针,引用无效的操作;是否与c接口兼容;容器的存储对象类型是否是bool;各种操作的耗费时间;若必须从一种容器转到另一种,可以使用封装技术,减少客户代码对于具体容器的使用;
对容器类型和其迭代器类型使用typedef把具体使用的容器隐藏到一个类的private中,并且尽可能减少可以访问到此容器的类接口。万一需要修改容器,此时需要查看该类的每个成员函数和友元受到的影响,总之客户代码可以尽量减小修改量;
第3条:确保容器中的对象拷贝正确而高效
类的拷贝动作:拷贝构造函数;拷贝赋值操作符可能的出错情况和限制:
直接拷贝对象时:容器所含的对象越多,时间和空间成本越高;拷贝包含auto_ptr的容器对象会出问题;存在继承关系时,提供一个derived对象给一个需要基类对象的地方,会导致对象切割;为了避免1,可以拷贝对象的指针,更好的选择是智能指针
vector比内置数组的优点:
Widger
w(maxNums
);
vector
<Widget
> v
;
v
.push_back(W1
);
vector
<Widget
> v
;
v
.reserve(maxNums
);
第4条:调用empty而不是检查size()是否为0
empty()对于所有标准容器都是O(1)操作;size()对于一些list有时候是O(n)操作,有时候是O(1)操作,在不同的STL平台上作者可能有不同的实现;
第5条:区间成员函数优先于与之对应的单元素成员函数
insert的区间成员函数版本和单元素成员函数版本
vector
<int> v1
{1, 2, 3};
vector
<int> v2
{11, 12, 13};
v1
.insert(v1
.begin(), v2
.begin(), v2
.end());
vector
<int>::iterator it
= v1
.begin();
for(int i
= 0; i
< v2
.size(); ++i
){
it
= v1
.insert(it
, v2
[i
]);
++it
;
}
使用insert区间函数版本比重复调用单元素版本的优点:
效率(写起来更容易,且不容易出错): ①对于insert函数的调用次数:区间函数只需一次,单元素版本需要多次;通过将函数声明为内联可以避免这种影响;所有顺序容器都适用 ②v1中元素需要移动的次数:调用区间函数一次性移动到最终位置,单元素版本只能一步一步的移动,造成了很大的拷贝构造和赋值开销;vector,string,deque适用,list不适用(因为list无需移动元素,只需修改指针的值) ③可以避免更多的重新分配内存的出现:当v1内存满了,再向其中insert新元素,会有几步:开辟新内存,拷贝旧元素到新内存,销毁旧元素,释放旧内存,将新元素插入新内存的正确位置处;若只调用一次区间函数,则一开始就知道需要多大内存,会一次性分配够,防止多次开辟新内存的情况出现;vector和string适用,deque和list由于管理内存的方式不同,因此不会出现这种情况 注:对于关联容器,选择区间函数的效率一定不会比重复调用单元素函数差;更易懂,能简洁的表达意图增强软件的可维护性
总结:
container
::container(InputIterator begin
, InputIterator end
);
void container
::insert(iterator pos
, InputIterator begin
, InputIterator end
);
iterator container
::erase(iterator begin
, iterator end
);
void container
::erase(iterator begin
, iterator end
);
注:erase操作不会在内存中元素数量少时自动缩小内存
void container
::assign(InputIterator begin
, InputIterator end
);
第6条:当心C++编译器最烦人的分析机制
C++的一个普遍规律:尽可能的解释为函数声明;因此需要时刻注意自己写的表达式是否有二义性;
int f(double (d
));
int f(double d
);
int f(double);
int g(double (*p
)());
int g(double p());
int g(double ());
易错点:
class Widget{};
Widget
w();
Widget w
;
ifstream
datafile("ints.dat");
list
<int> data(istream_iterator
<int>(datafile
), istream_iterator
<int>());
istream_iterator
<int> dataBegin(datafile
);
istream_iterator
<int> dataEnd
;
list
<int> data(dataBegin
, dataEnd
);
第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
如果容器中的对象包含了new创建的指针,要在容器对象析构前将指针delete掉,否则容器仅会将指针本身析构,而不会析构这些指针指向的那些动态分配的内存;
1:
void doSomething(){
vector
<Widget
*> v
;
v
.reverse(10);
for(int i
= 0; i
< 10; ++i
){
v
.push_back(new Widget
);
}
...
措施
1:手写循环,依次
delete(可以用算法for_each代替)
for(vector
<Widget
*>::iterator it
= v
.begin(); it
!= v
.end(); ++it
){
delete *it
;
}
}
2:
template<typename T
>
struct DeleteObjet
: public unary_function
<const T
*, void>{
void operator()(const T
* ptr
)const{
delete ptr
;
}
}
void doSomething(){
vector
<Widget
*> v
;
v
.reverse(10);
for(int i
= 0; i
< 10; ++i
){
v
.push_back(new Widget
);
}
...
措施
2:for_each
for_each(v
.begin(), v
.end(), DeleteObject
<Widget
>());
}
3.
struct DeleteObjet
{
template<typename T
>
void operator()(const T
* ptr
)const{
delete ptr
;
}
}
void doSomething(){
deque
<SpecialString
*> v
;
v
.reverse(10);
for(int i
= 0; i
< 10; ++i
){
v
.push_back(new SpecialString
);
}
...
措施
3:改进版的for_each
for_each(v
.begin(), v
.end(), DeleteObject
<string
>());
}
4.
void doSomething(){
措施
4best practice:使用带有引用计数的shared_ptr的自动析构的优点
;解决了前面几种措施中在
new和
delete中间可能抛出异常的问题
shared_ptr
<Widget
> spW
;
vector
<spW
> v
;
v
.reverse(10);
for(int i
= 0; i
< 10; ++i
){
v
.push_back(spW(new Widget
));
}
}
第8条:切勿创建包含auto_ptr的容器对象
包含auto_ptr的容器(COAP)被C++标准禁止auto_ptr独占对所指对象的所有权
auto_ptr
<Widegt
> p1(new Widget
);
auto_ptr
<Widegt
> p2
= p1
;
p1
= p2
;
若容器中存储auto_ptr,在进行一些赋值,交换等等操作时,会产生非预期效果,使容器中的若干auto_ptr置为NULL容器中可以使用shared_ptr等其它智能指针,但一定不要包含auto_ptr!
第9条:慎重选择删除元素的方法
若删除容器中等于特定值的所有对象
container
<int> c
;
c
.erase(remove(c
.begin(), c
.end()), c
.end());
c
.remove(1963);
c
.erase(1963);
若删除容器中满足特定条件(判别式)的所有对象
container
<int> c
;
bool f(int){...}
erase(remove_if(c
.begin(), c
.end(), f
), c
.end());
c
.remove_if(f
);
1.
container
<int> temp
;
remove_copy_if(c
.begin(), c
.end(), inserter(temp
, temp
.end()), f
);
c
.swap(temp
);
2.注意:对于关联容器调用erase,要对传入erase参数的迭代器进行后缀递增
for(container
<int>::iterator it
= c
.begin(); it
!= c
.end(); ){
if(f(*it
)) c
.erase(it
++);
else it
++;
}
若对顺序容器通过手写循环调用erase:要用erase的返回值更新迭代器it 若对关联容器通过手写循环调用erase:要把传给erase的迭代器参数作后缀递增
for(container
<int>::iterator it
= c
.begin(); it
!= c
.end(); ){
if(f(*it
)) it
= c
.erase(it
);
else it
++;
}
第10条:了解分配子(allocator)的约定和限制
第11条:理解自定义分配子的合理用法
第12条:切勿对STL容器的线程安全性有不切实际的依赖
STL容器在多线程环境下,有的STL实现支持,有的不支持,最多最多只能奢望以下两点,其余关于多线程安全性的问题只能手动来,不能指望STL自带 ①多个线程同时读一个同容器的内容,但不能对该容器同时写入 ②多个线程可以同时对不同的容器写入