1、关于容器的两个概念
1、连续内存容器(基于数组的容器)
将其元素放在一块或多块(动态分配)的内存中,每块内存中有多个元素。当有新元素插入或者已有元素删除时,统一内存块中的其他元素要向前或向后移动,以便为新元素让出空间,或者填补删除元素的空间。这种移动会影响效率和异常安全,标准的连续容器:vector list deque。
2、关联容器(基于节点的容器)
基于节点的容器在每一个(动态分配)内存块上只存储一个元素。元素的插入和删除只会影响指向节点的指针,并不会影响节点本身的内容。因此当插入或者删除时,元素的值不需要移动。
2、容器的选择和容器切换之间的问题
日常经验中,容器的选择是个艰难的过程,虽然我们按照自己的需求确定好了容器,但无可避免的,我们可能会意识到自己的选择并不是最佳答案的时候,改变容器的类型就会是一个痛苦的过程(比如我们从vector容器切换为map容器)。
我们不仅要修改编译器诊断出的问题,还要进行一次比较详细的检查,因为新容器的性能特点,它使迭代器、指针、应用可能无效的规则。
我们考虑用封装来解决这个问题。
class Widget{...}
vector<Widget> vw;
Widget bestWidget;
vector<Widget>::iterator iter = find(vw.begin(), vw.end(), bestWidget);
上面的例子定义了一个普通的vector容器,也是我们在日常中最常用的一种方式,但是如果我们按照上面的案例编写写代码,并不可避免的进行容器的更换,那么我们就会解决上面提到的所有问题。
class Widget{...}
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer cw;
Widget bestWidget;
WCIterator iter = find(cw.begin(), cw.end(), bestWidget);
上面提到的解决办法是,我们将容器进行封装,这样,在我们能切换容器的时候,或许只是会修改容器类型。
3、区间成员函数优先于针对单元素的成员函数
为什么说区间成员函数优先于与之对应的单元素的成员函数?好处不仅仅是只有一点。
如果需要对矢量进行赋初值或者进行拷贝的时候,区间成员函数,显示出了较单元素来说非常强大的代码能力。
通常我们的做法会如下:
vector<int> v1;
vector<int> v2;
v1.clear();
for(vector<int>::iterator iter = v2.bengin(); iter != v2.end(); ++iter)
{
v1.push_back(*iter);
}
通常的我们的做法是写一个循环,对每一个元素进行插入,而这样就是我们不可避免的要使用循环。就算我们使用一个其他的方法,比如:
vector<int> v1;
vector<int> v2{1, 3, 4, 6};
copy(v2.bengin(), v2.end(), back_inserter(v1));
或者
v1.insert(v1.end(), v2.bengin(), v2.end());
如上,上述的算法例子,虽然我们表面看不到有循环的出现,但是函数的背后肯定是有的。如下,我们直接使用assign成员函数,直接简明。
vector<int> v1;
vector<int> v2;
v1.assign(v2.bengin(), v2.end());
1、能够减少代码
2、能够是代码意图更明显
4、stl容器线程安全性
- 多线程读取时安全的。多个线程和同时读取同一个容器,但在读的过程中不能有写操作。
- 多个线程对不同的容器做写操作时安全的。
因此,在多线程中,我们需要考虑:
- 对容器成员函数的每次调用,都锁住容器直到调用结束;
- 对容器返回的每个迭代器的生命周期中,都锁住容器;
- 对于作用于容器的每个算法,都锁住该容器,直到算法调用结束。
5、对容器判空时使用empty()而非size() == 0
对于一个容器,其实if( size() == 0 ){...}
和 if( empty() ){...}
本质上等价的。
那么为什么会推荐使用empty()
呢?主要是因为:该方法对任何一个容器来说,时间消耗都是常数级的,而 size()
对有些list的实现来说,耗时是线性的。
因为 list 是可以 splice 的,也就是list的拼接时并不知道自己本身元素的大小,如果想让list也能够有 size()常数级的操作,那就要保证list的每个成员函数都需要更新其size的大小。