如何在vector、list、deque中做出选择:
连续内存容器:把它的元素存放在一块或者多块内存中,每块内存中存多个元素。但有新元素插入或者已有的元素被删除时,同一内存块中的其他元素要向前或者向后移动,以便为新元素让出空间,或者填充被删除元素留下的空隙。这种移动影响到效率和异常安全性。
标准的连续内存容器包括:vector、string、deque。
基于节点的容器:在每一个内存块中只存放一个元素。容器中元素的插入或者删除只影响到指向节点的指针,而不影响节点本身的内容,所以当有插入或者删除操作时,元素的值不需要移动。
基于节点的容器:list、所有标准的关联容器。
对插入和删除操作,如果需要事务语义(也就是说,在插入和删除操作失败时,需要回滚的能力),你就要使用基于节点的容器。如果对多个元素的插入操作需要事务语义,则你需要选择list,因为在标准容器中,只有list对多个元素的插入操作提供了事务语义。对那些希望编写异常安全代码的程序员,事务语义显得尤为重要。
string是STL中在swap过程中会导致迭代器、指针和引用变为无效的唯一容器。
STL是以泛化原则为基础的:数组被泛化为“以其包含的对象类型为参数”的容器;函数被泛化为“以其使用的迭代器的类型为参数”的算法;指针被泛化为“以其指向的对象的类型为参数”的迭代器。
很多程序员想以一种不同的方式做泛化。他们在自己的软件中不是针对某种具体的容器,而是想把容器的概念泛化,这样他们就能使用,比如vector,而仍保留以后将其换成deque或list的选择——但不必改变使用该容器的代码。这种想法是不正确的!!!
试图编写独立于容器类型的代码——是不正确的!
容器中保存了对象,但并不是你提供给容器的那些对象。当你通过insert或者push_back向容器中加入对象时,存入容器的是你所指定的对象的拷贝。
如果向容器中填充对象,而对象的拷贝又非常耗时,那么向容器中填充对象这一操作可能成为程序的性能瓶颈。
在继承体系下,拷贝动作可能导致剥离(slicing)。也就是说,如果你创建一个存放基类对象的容器,却向其中插入派生类对象,那么派生类对象被拷贝进容器时,它所有的特性部分将被丢失。
剥离问题意味着向基类对象容器中插入派生类对象几乎总是错的。
使拷贝动作高效、正确并防止剥离问题发生的一个简单办法就是使容器包含指针而非对象。
empty对所有标准容器都是常数时间操作,而对于一些list实现,size()耗费线性时间。
给定v1和v2两个向量(vector),使v1的内容和v2的后半部分相同的最简单操作是什么?
v1.assign(v2.begin()+v2.size()/2 , v2.end());
使用区间成员函数的优点:
当处理标准序列容器时,为了取得同样的结果,使用单元素的成员函数比使用区间成员函数需要更多地调用内存分配子,更频繁地拷贝对象,而且/或者做冗余的操作。
class Widget{...};
Widget w();
此处并没有声明一个名为w的Widget类型变量,而是声明了一个名为w的函数,该函数不带任何参数。
C++中的一条普遍规律相符,即尽可能地解释为函数声明。
指针容器在自己被析构时会析构掉包含的每一个元素,当指针的析构函数什么都不做,当然也不会调用delete。
从没有析构函数的类进行共有继承是C++中的一项重要禁忌。
STL容器很智能,但没有智能到知道是否该删除自己所包含的指针的程度。当你使用指针的容器,而其中的指针应该被删除时,为了避免资源泄漏,你必须或者用引用计数形式的智能指针对象(比如Boost的shared_ptr)代替指针,或者当容器被析构时手工删除其中的每个指针。
auto_ptr的容器(简称COAP)是被禁止的。
对于删除特定元素:
对于连续内容容器(vector、deque、string)——使用erase-remove的方式进行删除元素
c.erase(remove(c.begin(),c.end(), 1963), c.end());
对于list,直接调用remove()接口进行元素删除。
对于标准关联容器,使用erase()接口删除元素。只需要对数时间开销。
关联容器的erase函数是基于等价而不是相等。
对于删除符合条件的元素:
对于连续内容容器(vector、deque、string)——使用erase-remove_if的方式进行删除元素
c.erase(remove_if(c.begin(),c.end(), Func), c.end());
对于list,直接调用remove_if()接口进行元素删除。
对于标准关联容器:有两种方式可以解决问题。
for(auto iter = c.begin(); iter!=c.end();)
{
if(func(*iter))
{
c.erase(iter++);
}
else
{
iter++;
}
}
对于标准序列容器,使用for进行元素删除时,一定要利用erase()函数的返回值。
for(auto iter = c.begin(); iter!=c.end();)
{
if(func(*iter))
{
iter = c.erase(iter);
}
else
{
iter++;
}
}
对于标准序列容器,一旦erase完成,其返回值指向被删除元素的下一个元素。
当涉及STL容器和线程安全性时,你可以指望一个STL库允许多个线程同时读一个容器,以及多个线程对不同的容器做写入操作。你不能指望STL库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。
当决定使用new来动态分配内存时,将要承担以下责任:
当需要动态申请一个数组时,应该考虑使用vector或者string来代替。
对于vector和string,每当需要更多的空间时,其操作分为四步:
每当vector或者string扩容之后,所有指针、迭代器和引用都会变得无效。
reserve成员函数能使重新分配内存的次数减少到最低限度,从而避免了重新分配和指针、迭代器和引用失效带来的开销。
vector和string提供了4个函数:
通常有两种方式来使用reserve以避免不必要的重新分配:
string的实现比乍看上去有更多的自由度;同样明显的是,不同的实现以不同的方式利用了这种设计上的灵活性。这些区别总结如下:
针对vector:使用&v[0]作为指向第一个元素的指针。从而用来适配C语言接口。但是注意要先判空。
if(not v.empty())
{
doSomething(&v[0], v.size());
}
当你需要一个指向vector中的数据的指针时,永远不应该使用begin()。如果为了某种原因决定用v.begin(),那么请使用&*v.begin(),因为这和&v[0]产生同样的指针。
针对string:使用c_str()成员函数返回的结果传递给C语言接口。即便字符串的长度为0,这种方式也没问题。
对于vector,如果你传递的C API改变了v中元素值的话,通常是没有问题的,但被调用的函数不能试图改变向量中元素的个数。
使用swap方式将vector或者string中去除多余的容量:
vector(v).swap(v);
这一技术并不保证一定能除去多余的容量。它意味着“在容器当前的大小确定的情况下,使容量在该实现下变为最小”。
swap技巧的一种变化形式可以用来清除一个容器,并使其容量变为该实现下的最小值。
vector().swap(v);
vector的clear()不会改变vector的容量,只会清空其中的元素。
在做swap的时候,不仅两个容器的内容被交换,同时它们的迭代器、指针和引用也将被交换(string除外)。在swap发生后,原先指向某容器中元素的迭代器、指针和引用依然有效,并指向同样的元素——但是,这些元素已经在另一个容器中了。
作为一个STL容器,vector只有两点不对。首先,它不是一个STL容器。其次,它并不存储bool。除此以外,一切正常。
STL中不同的函数对于“相同”的定义是不同的。find算法对相同的定义是“相等”,是以operator==为基础的;set::insert()对相同的定义是“等价”,是以operator<为基础的。
相等:的概念是基于operator==的。
等价:的概念是以“在已排序的区间中对象值的相对顺序”为基础的。
对于两个对象x和y,如果按照关联容器c的排列顺序,每一个都不在另一个的前面,那么称这两个对象按照c的排列顺序有 等价 的值。
使用单一的比较函数,并把等价关系作为判定两个元素是否“相同”的依据,使得标准关联容器避免了一大堆“若使用两个比较函数将带来的问题”。
对于关联容器,判断两个元素是否相同是通过等价来判断的,而等价的判断条件是 A不大于B 且 B 不大于A
,如果比较函数在等值的情况下返回true,则会导致等价条件判断错误。从而使关联容器发生错误。
set或者multiset中的值不能是const的,因为将其定义为const并不合理。
set或者multiset中的键指的是用于判断小于
所使用的部分,一定不能修改set或者multiset中的键,因为这部分心意会影响容器的排序性。
所以,试图修改set或multiset中元素的代码将是不可移植的。因为不同厂商是set或者multiset使用的迭代器类型可能不能。
为了使修改set或者multiset中的元素可移植,可以使用强制类型转换将迭代器解引用并转换成引用类型
。注意此处必须转换成引用类型,因为转换成非引用类型会创建新的对象。
在排序的vector中存储数据可能比在标准关联容器中存储同样的数据要耗费更少的内存,而考虑到页面错误的因素,通过二分搜索法来查找一个排序的vector可能比查找一个标准关联容器要更快一些。
查找操作几乎从不跟插入和删除操作混在一起”时,再考虑使用排序的vector而不是关联容器才是合理的。
所以,当使用vector来模仿map
map::operator[]的设计目的是为了提供“添加和更新”(add or update)的功能。
具体的工作方式是这样的,operator[] 返回一个引用,它指向与k相关联的值对象。然后v被赋给该引用(operator[]返回的那个引用)所指向的对象。
当向映射表中添加元素时,要优先选用insert而不是operator[] ;当更新已经在映射表中的元素的值时,要优先选择operator[]。
对容器类container而言,iterator类型的功效相当于T*,而const_iterator则相当于const T*。
reverse_iterator与const_reverse_iterator同样分别对应于T*和const T*,所不同的是,对这两个迭代器进行递增的效果是由容器的尾部反向遍历到容器头部。
C++98中容器的insert()和erase()的参数类型都是iterator类型,但是C++11中insert()和erase()的参数类型全都改成了const_iterator类型。
对于这些容器类型,iterator和const_iterator是完全不同的类。试图将一种类型转换为另一种类型是毫无意义的,这就是const_cast转换被拒绝的原因。
通过base()函数可以得到一个与reverse_iterator“相对应的”iterator的说法并不准确。reverse_interator::base()得到的iterator指向的元素是reverse_interator指向元素的下一个元素。
无论何时,如果所使用的算法需要指定一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增大。
要在算法执行过程中增大目标区间,请使用插入型迭代器,比如ostream_iterator,或者由back_inserter、front_inserter和inserter返回的迭代器。
如果需要对vector、string、deque或者数组中的元素执行一次完全排序,那么可以使用sort或者stable_sort。
如果有一个vector、string、deque或者数组,并且只需要对等价性最前面的n个元素进行排序,那么可以使用partial_sort。
如果有一个vector、string、deque或者数组,并且需要找到第n个位置上的元素,或者,需要找到等价性最前面的n个元素但又不必对这n个元素进行排序,那么,nth_element正是你所需要的函数。
如果需要将一个标准序列容器中的元素按照是否满足某个特定的条件区分开来,那么,partition和stable_partition可能正是你所需要的。
如果你的数据在一个list中,那么你仍然可以直接调用partition和stable_partition算法;可以用list::sort来替代sort和stable_sort算法。但是,如果你需要获得partial_sort或nth_element算法的效果,那么,你可以有一些间接的途径来完成这项任务。
因为从容器中删除元素的唯一方法是调用该容器的成员函数,而remove并不知道它操作的元素所在的容器,所以remove不可能从容器中删除元素。
用remove从容器中删除元素,而容器中的元素数目却不会因此而减少。
remove不是真正意义上的删除,因为它做不到。
remove移动了区间中的元素,其结果是,“不用被删除”的元素移到了区间的前部(保持原来的相对顺序)。它返回的一个迭代器指向最后一个“不用被删除”的元素之后的元素。这个返回值相当于该区间“新的逻辑结尾”。
如果你真想删除元素,那就必须在remove之后使用erase。
vector v
v.erase(remove(v.begin(),v.end(),2) , v.end());
对于list,可以直接调用其remove成员函数。
但容器中存储指针时,在调用remove时就可能造成内存泄漏。
如果容器中存放的不是普通指针,而是具有引用计数功能的智能指针,那么与remove相关的困难就不再存在了。
有些算法要求排序的区间,即区间中的值是排过序的。当使用这些算法的时候,遵循这条规则尤为重要,因为违反这一规则并不会导致编译器错误,而会导致运行时的未确定行为。
目前stl支持copy_if
使用accumulate统计容器中所有字符串的长度。
由于函数对象往往会按值传递和返回,所以,你必须确保你编写的函数对象在经过了传递之后还能正常工作。这意味着两件事:首先,你的函数对象必须尽可能地小,否则复制的开销会非常昂贵;其次,函数对象必须是单态的(不是多态的),也就是说,它们不得使用虚函数。
一个判别式(predicate)是一个返回值为bool类型(或者可以隐式地转换为bool类型)的函数。
一个纯函数(pure function)是指返回值仅仅依赖于其参数的函数。
在C++中,纯函数所能访问的数据应该仅局限于参数及常量(在函数生命期内不会被改变,自然地,这样的常量数据应该被声明为const)。如果一个纯函数需要访问那些可能会在两次调用之间发生变化的数据,那么用相同的参数在不同的时刻调用该函数就有可能会得到不同的结果,这将与纯函数的定义相矛盾。