容器:1~12

第1条:慎重选择容器类型

  • 不同容器之间的差异很大,要根据需要选择适当的容器;
  • 考虑因素:是否与标准相符;迭代器类型(单向,双向,随机);元素布局与c的兼容性;容器内元素是否有序;查找速度;引用计数引起的反常;是否实现事务语义;在何种情况下会使迭代器,引用,指针无效;不同容器的内存分配策略;等等

第2条:不要试图编写独立于容器类型的代码

  • 标题的意思是:由于不同的容器之间差异很大,优缺点很明显,因此不要试图去编写适用于所有容器的代码。比如它们所支持的成员函数;支持的迭代器类型;使迭代器,指针,引用无效的操作;是否与c接口兼容;容器的存储对象类型是否是bool;各种操作的耗费时间;
  • 若必须从一种容器转到另一种,可以使用封装技术,减少客户代码对于具体容器的使用;
  1. 对容器类型和其迭代器类型使用typedef
  2. 把具体使用的容器隐藏到一个类的private中,并且尽可能减少可以访问到此容器的类接口。万一需要修改容器,此时需要查看该类的每个成员函数和友元受到的影响,总之客户代码可以尽量减小修改量;

第3条:确保容器中的对象拷贝正确而高效

  • 类的拷贝动作:拷贝构造函数;拷贝赋值操作符
  • 可能的出错情况和限制:
  1. 直接拷贝对象时:容器所含的对象越多,时间和空间成本越高;拷贝包含auto_ptr的容器对象会出问题;存在继承关系时,提供一个derived对象给一个需要基类对象的地方,会导致对象切割;
  2. 为了避免1,可以拷贝对象的指针,更好的选择是智能指针
  • vector比内置数组的优点:
Widger w(maxNums); //无论需要的是几个,在此处都会调用maxNums个Widget默认构造函数来创建maxNums个对象

vector<Widget> v;   //创建一个空vector,内含0个Widget对象
v.push_back(W1);  //当需要时,会自动变长;在此处只调用一个Widget的默认构造函数;

vector<Widget> v;
v.reserve(maxNums); //提供了足够容纳maxNums个Widget对象的空间,但并没有创建任何WIdget对象

第4条:调用empty而不是检查size()是否为0

  • empty()对于所有标准容器都是O(1)操作;
  • size()对于一些list有时候是O(n)操作,有时候是O(1)操作,在不同的STL平台上作者可能有不同的实现;

第5条:区间成员函数优先于与之对应的单元素成员函数

  • insert的区间成员函数版本和单元素成员函数版本
//目标:将v2的所有内容插入到v1头部; v1结果:11, 12, 13, 1, 2, 3
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;//若不加此句,会导致插入的v2内容逆序
}
  • 使用insert区间函数版本比重复调用单元素版本的优点:
  1. 效率(写起来更容易,且不容易出错):
    ①对于insert函数的调用次数:区间函数只需一次,单元素版本需要多次;通过将函数声明为内联可以避免这种影响;所有顺序容器都适用
    ②v1中元素需要移动的次数:调用区间函数一次性移动到最终位置,单元素版本只能一步一步的移动,造成了很大的拷贝构造和赋值开销;vector,string,deque适用,list不适用(因为list无需移动元素,只需修改指针的值)
    ③可以避免更多的重新分配内存的出现:当v1内存满了,再向其中insert新元素,会有几步:开辟新内存,拷贝旧元素到新内存,销毁旧元素,释放旧内存,将新元素插入新内存的正确位置处;若只调用一次区间函数,则一开始就知道需要多大内存,会一次性分配够,防止多次开辟新内存的情况出现;vector和string适用,deque和list由于管理内存的方式不同,因此不会出现这种情况
    注:对于关联容器,选择区间函数的效率一定不会比重复调用单元素函数差;
  2. 更易懂,能简洁的表达意图
  3. 增强软件的可维护性
  • 总结:
//区间创建:所有标准容器都支持用一对迭代器参数的构造函数来创建新容器对象;
container::container(InputIterator begin, InputIterator end);
//区间插入:所有标准顺序容器都支持下面的insert
void container::insert(iterator pos, InputIterator begin, InputIterator end);
//区间删除:所有标准容器都支持erase一个区间内的元素;但顺序和关联容器的返回值不同
iterator container::erase(iterator begin, iterator end); //顺序容器支持,返回一个指向插入新元素之后的首个元素的迭代器
void container::erase(iterator begin, iterator end);//关联容器不反悔迭代器(因为性能开销很大)
注:erase操作不会在内存中元素数量少时自动缩小内存
//区间赋值:所有标准容器都支持用一个迭代器范围去assign一个容器(原本的内容被clear,只剩下新assign过来的这些元素)
void container::assign(InputIterator begin, InputIterator end);

第6条:当心C++编译器最烦人的分析机制

  • C++的一个普遍规律:尽可能的解释为函数声明;因此需要时刻注意自己写的表达式是否有二义性;
//等价的三种函数声明:f函数接受一个double类型参数,返回一个int值
int f(double (d));
int f(double d);
int f(double);

//等价的三种函数声明:g函数接受一个函数指针参数,返回一个int值;这个函数指针p指向一个不带任何参数且返回类型为double的函数
int g(double (*p)());
int g(double p());
int g(double ());
  • 易错点:
class Widget{};  //假设该类含有默认构造函数
Widget w();  //会被编译器解释为一个函数声明
Widget w;  //调用Widget的默认构造函数创建一个新变量w

ifstream datafile("ints.dat");//该文件中存有int
//目标是将一堆istream_iterator传入list的区间构造函数,用整个文件中int内容初始化list
list<int> data(istream_iterator<int>(datafile), istream_iterator<int>()); //错误,能通过编译器,但什么也没做,未创建list,也未读取文件中的数据
//best practice:在list对象data的声明中避免使用匿名的istream_iterator对象
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); //vecroe容器中存储了指向动态分配内存的指针
	}
	...   //使用v
	//为防止doSomething函数结束,出现资源泄露,要设法delete这些内存
	措施1:手写循环,依次delete(可以用算法for_each代替)
	//在执行下面的手动delete之前,若抛出异常则仍会资源泄露
	for(vector<Widget*>::iterator it = v.begin(); it != v.end(); ++it){
		delete *it;
	}
}

2:
template<typename T> //用于delete任何类型的template class
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); //vecroe容器中存储了指向动态分配内存的指针
	}
	...
	措施2:for_each
	//缺点:同上,在delete之前可能抛出异常,则会delete失败;
	//虽然template可以指定类型提供了通用性,但同时可能会因指定了错误的类型,如误把基类指针当成派生类指针传入,且万一基类的析构函数是non-virtual,则会发生析构错误(p30)
	for_each(v.begin(), v.end(), DeleteObject<Widget>()); //对于v中的每个元素都调用该函数对象
}

3.
struct DeleteObjet{//外面去掉模板化和基类
	template<typename T> //将模板化移到里面,舍弃了可指定任意类型的能力,但更安全;编译器可以推断出传给DeleteObjet::operator()的指针类型
	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); //容器中存储了指向动态分配内存的派生类SpecialString指针
	}
	...
	措施3:改进版的for_each
	//缺点:同上,在delete之前可能抛出异常,则会delete失败
	for_each(v.begin(), v.end(), DeleteObject<string>()); //对于v中的每个元素都调用该函数对象,此处尽管错误指定了基类string指针,但实际传入operator()的参数为SpecialString*,编译器会推断出我们实际传入的指针类型
}

4.
void doSomething(){
	措施4best practice:使用带有引用计数的shared_ptr的自动析构的优点;解决了前面几种措施中在newdelete中间可能抛出异常的问题
	shared_ptr<Widget> spW;//智能指针
	vector<spW> v;//容器中存储的不再是野生指针,而是智能指针
	v.reverse(10);
	for(int i = 0; i < 10; ++i){
		v.push_back(spW(new Widget)); //注:每当获取一份资源时,要立即用管理类(此处是shared-ptr)管理起来
	}
}//当函数结束时,便无需手动析构那些new得到的内存了

第8条:切勿创建包含auto_ptr的容器对象

  • 包含auto_ptr的容器(COAP)被C++标准禁止
  • auto_ptr独占对所指对象的所有权
auto_ptr<Widegt> p1(new Widget); //p1指向一个Widget对象
auto_ptr<Widegt> p2 = p1;  //所有权交给p2,p1置为NULL
p1 = p2;  //所有权又归p1,p2置为NULL
  • 若容器中存储auto_ptr,在进行一些赋值,交换等等操作时,会产生非预期效果,使容器中的若干auto_ptr置为NULL
  • 容器中可以使用shared_ptr等其它智能指针,但一定不要包含auto_ptr!

第9条:慎重选择删除元素的方法

  • 若删除容器中等于特定值的所有对象
container<int> c;  //想删除c中所有值为1963的元素

//vector,string,deque采用remove-erase
c.erase(remove(c.begin(), c.end()), c.end());  

//list直接使用成员函数remove,会真正删除元素(而其他STL容器的remove仅仅是调整元素的位置)
c.remove(1963); 

//对于关联容器,调用erase成员函数 
c.erase(1963);  
  • 若删除容器中满足特定条件(判别式)的所有对象
container<int> c; 
bool f(int){...}  //特定条件,删除容器中使f返回true的所有元素

//vector,string,deque使用remove_if-erase
erase(remove_if(c.begin(), c.end(), f), c.end()); 

//list使用remove_if成员函数
c.remove_if(f); 

//关联容器使用remove_copy_if-swap或者手写循环
1.
container<int> temp; //用于存储不删除的值的临时容器
remove_copy_if(c.begin(), c.end(), inserter(temp, temp.end()), f);//将[c,begin(), c.end())内的元素复制到temp.end()处,除了使f返回true的元素
c.swap(temp);//交换两容器内容,现在c中不再有使f返回true的元素了
2.注意:对于关联容器调用erase,要对传入erase参数的迭代器进行后缀递增
for(container<int>::iterator it = c.begin(); it != c.end(); ){//it的遍历不能在此递增,因为erase迭代器it指向的元素之后,it会失效,则对it递增是无定义的(若对顺序容器调用erase,则it和it后面的迭代器都会失效)
	if(f(*it)) c.erase(it++);//传入erase参数的是it,但在erase执行前,it就递增指向了it的后面那个元素,因此不会失效;若等erase删完,则it失效,再对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自带
    ①多个线程同时读一个同容器的内容,但不能对该容器同时写入
    ②多个线程可以同时对不同的容器写入
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值