STL对比解说——顺序容器

STL对比解说——顺序容器

 

1. 顺序容器种类

 

  (1)  array(since TR1, C++11)

  (2)  vector

  (3)  deque

  (4)  list(singly(since C++ 11) and double linked)

  注:本文主要讲解:array, vector, deque, list,forword_list五个。

            vector<bool>(在开头叙述,后面讲解不包括它)

           string另作叙述

 

   //array: 静态数组,提供了STL容器接口的普通C-style array的外裹类。

  //具有静态大小,不能添加也不能删除元素。只能替换元素。

  #include <array>

  namespace std {

    template <typename T, size_t N>                   //N: 非类型模版形参:编译时常量表达式

    class array;

  }

  //可以swap,但是是线性复杂度。不交换内部指针,而是交换元素。

  //arr1 = std::move(arr2);        //c = rc;方式

  //array<elem, 0> arr; 也可以,arr常是empty。使用begin(), end()等产生唯一的值,但是不可以解引用,如:arr.front(), arr.back()

  //特有的:array提供tuple interface: tuple_size<>::value, tuple_element<>::type, get<>() 等。

 

  //vector: 动态数组,使用动态C-style数组管理元素的抽象(但是,标准没说一定要用动态数组实现)

  #include <vector>

  namespace std {

  template <typename T,

    typename Allocacator = allocator<T> >           //默认配置器:std::allocator, 实际中不一定使用它。vs使用,gcc不用。下同

  class vector;
  }

  //特有的:vector具有容量

  //在插入元素时,元素大小超过容量时,需要重新分配内存(reallocator)

  //reallocator问题:

  reallocator 使所有引用,指针,迭代器失效

  reallocator浪费时间

      //解决方法:

  可以预先分配内存reserve(size)

  可以直接给出大小值:

  //size() == 3, 使用3次T的默认构造函数。内置类型初始化为0

  //但是若T比较复杂,初始化可能比较费时间,所以最好用reserve()

  Vector<T> ivec(3);     

  Vector<T> ivec.resize(3);

  //string也有容量,与vector类似。但是,

  vector不可以使用reserve(size)压缩容量,当sie小于vector现有容量时no-op。

  vector的内存和速度方面的优化由实现定义。现在的许多实现为了避免内存碎片,第一次插入元素时会分配一块内存(such as 2K),比较浪费内存。

  //C++11中可以压缩内存

  vec.shrink_to_fit();            //不保证之后capacity() == size()一定成立,大多数都可以(vs, gcc都可以)。

  C++11之前:

  vector<T>(v).swap(v);  

  vector<T>().swap(v);       //清空vector使用的所有内存

     

  //deque: 使用动态数组管理内存,与vector类似,只不过有两个端口。

  deque典型的是用一组独立的块实现,第一个块在一个方向增长,最后一个块在另一个方向

  #include <deque>

  namespace std {

    template <typename T,

    typanema Allocator = alloacator<T> >

  class deque;

  }

  //特点

  在两端插入删除更快

  迭代器是smart point不能是普通指针,因为它需要在不同块间跳跃,因此访问元素是一种间接的方式,元素访问和移动有一点点慢

  系统的内存块有大小限制时,deque可以使用不止一个内存块,因此max_size()可能更大

  deque不能控制容量,任何在非首尾的插入和删除操作都会使所有的指针,引用,迭代器失效。重新分配可能比vector更快,因为它不需要复制所有元素。

  内存块在不使用时可能被释放,deque的内存大小可能减小(但是,具体由实现指定)。

 

  //list: 使用双向链表管理元素

  #include <list>

  namespace std {

  template <typename T,

    typename Allocactor = allocator<T> >

  class list;
  }

  //改变元素顺序方面有优势,在这方面有一些特殊成员函数,速度要比algorithm中相同的要快

  c.unique()              //删除相邻元素的重复

  c.unique(op)

 

  //这三项在forward_list中为splice_after

  c.splice(pos, c2)         //移动c2中所有元素到c中pos之前,之后c2变为空

  c.splice(pos, c2, c2pos)

  c.splice(pos, c2, c2beg, c2end)

 

  c.sort()     

  c.sort(op)

 

  c.merge(c2)           //移动c2中元素到c中,若c和c2都是已序的,合并后仍为已序。之后,c2为空

  c.merge(c2, op)

 

  c.reverse()             //元素顺序反向

 

  //forward_list:  since C++11单向链表管理元素

  #include <forward_list>

  namespace std {

  template <typename T,

    typename Allocator = allocator<T> >

  class fordward_list;
  }

  //特点:比list消耗更少内存,更好的运行时行为。sizeof(forward_list) = 4(list=8), 内部只有一个指针,指向首元素标准描述:相对于手写的list,forward_list没有更多空间和时间消耗。

  //相对于list的限制:

  单项链表仅有前向迭代器(forward iterator)。因此,它没有反向迭代器的支持,缺少reverse_iterator, rbegin(), rend(), crbegin(), crend()。

  没有size(),由于sizeof()具有线性复杂度,考虑到性能,舍弃。

  没有最后一个元素的指针,因此,缺少back(), push_back(), pop_back()。

  由于迭代器不能向前走,用后缀_after作为某些成员函数名字。insert_after代替insert, 还另外提供了before_begin(), cbefore_begin()产生第一个元素之前的位置

 

  //改变元素顺序方面和list一样有优势,在这方面有一些特殊成员函数,速度要比algorithm中相同的要快,参见list,注意splice_after()

 

  //vector<bool>, 后面讲述不包括它

  每个元素使用1 bit存储,由于最小的地址位要有1 byte大小,因此需要对引用和迭代器进行特殊处理。

  不满足其它vector的要求,性能可能比通常的是实现更差,因为元素操作需要转换为位操作。

  vector<bool>具有动态大小,可以增加和删除bits, 若需要静态大小则用bitset。

  c.flip();            //所有元素取反

  c[idx].flip();     //某个元素取反

  c[idx] = val;

  c[idx] = c[idx2];

  //可以使用[], at(), front(), back()

 

2. 初始化

 

  //vector, deque, list, forward_list提供的

  Container c;      //容器都为空, 值采用默认构造函数。注释为红色说明较常用,下同。

  Container c(c2);    //Copy constructor; 创建另一个容器的副本

  Container c = c2;      //Copy constructor

  Container c(n);     //创建n个使用默认构造函数初始化元素的容器,容器size()=n

  Container c(n, elem); //创建n个elem元素的容器

  Container c(beg, end); //使用一对迭代器中元素进行初始化

  //since C++ 11

  Container c(rv);        //Move constructor; 创建一个rvalue rv 内容的新容器

  Container c =rv;     //Move constructor

  Container c(initlist);  //使用初始化列表中的元素进行初始化

  Container c = initlist;  //初始化列表

 

  

  //array(POD类型),上面红色array不提供的

  Container<Elem, N> c;      //默认构造函数初始化元素(int:0, string:""等等)

  Container<Elem, N> c(c2);    //Copy constructor; 创建另一个容器的副本

  Containe<Elem, N>r c = c2;      //Copy constructor

  Container<Elem, N> c(rv);        //Move constructor; 创建一个rvalue rv 内容的新容器

  Container<Elem, N> c =rv;     //Move constructor

  Container<Elem, N> c = initlist; //初始化列表,array<Elem, N>类似于普通数组的初始化,不属于C++11

  注:array<Elem, N>有一些特殊的性质,某些初始化方式不提供。

    array是一个静态数组,是提供了STL容器接口的普通C-style array的外包(wrap)类。

    是一个常量大小的顺序容器不能删除元算,改变容器大小,但是可以置换元素

    因此除了初始化使容器添加元素外,还可以c=c2, c=rv, fill(),swap()进行赋值,不存在替他方式。

 

 

3. 赋值

 

  //vector, deque, list, forward_list提供的

  c = c2;

  c = rv;        //Move 赋值rvalue rv到c,since C++11

  c = initlist;      //since C++11

  c.assign(n, elem);  //使用相应的constructor

  c.assign(beg, end);  //使用相应的constructor

  c.assign(initlist);   //使用相应的constructor

  c1.swap(c2);

  swap(c1, c2);

  

  //array特殊性

  不提供以下:

  c = initlist;      //需要constructor(initlist),隐式转换initlist为array类型,但是array不提供

  c.assign(n, elem);  //使用相应的constructor,但是array缺少

  c.assign(beg, end);  //使用相应的constructor,但是array缺少

  c.assign(initlist);   //使用相应的constructor,但是array缺少

  提供一个:

  array.fill(val);    //将array中所有元素赋值为val

  

 

  注:c = c2, 当赋值后不再使用c2时,应该使用c = rv右值语义

    当要交换容器元素时应使用swap

    当要赋值时,尽量使用assign, c=initlist这种范围赋值的方式,避免一个一个的赋值。

 

4. 插入

 

   //vector,deque,list都提供的

  c.push_back(elem);    //在结尾添加元素elem的副本

  c.insert(pos, elem);

  c.insert(pos, n, elem);

  c.insert(pos, beg, end);

  //since C++11

  c.insert(pos, initlist);

  c.emplace(pos, args...);

  c.emplace_back(args...);

 

   //对于dequelist这种双头增加

  c.push_front(elem);  

  c.emplace_front(args...);  

 

  //forward_list提供特殊的_after, insert只能_after, 只能在_front放置元素

      不提供c.push_back(), 提供

  c.push_front(elem);                            //只能在前端插入,不能在后端

      c.insert_after(pos, elem);

  c.insert_after(pos, n, elem);

  c.insert_after(pos, beg, end);

  //since C++11

  c.insert_after(pos, initlist);

  c.emplace_after(pos, args...);

  c.emplace_front(args...);                  //只能在前端插入,不能在后端

 

      注:对于dequelist这种双头和forward_list都提供

  c.push_front(elem);  

  c.emplace_front(args...);  

 

  //对于array不提供插入元素,可以初始化,赋值改变容器元素。不提供任何其它变动性操作。

 

5. 访问

 

   //随机访问迭代器的容器arrayvector deqe:

  c[idx];    //返回索引的元素值,不检测参数有效性

  c.at(idx);   //唯一的检测参数有效性的函数

  c.front();

  c.back();

 

  //双向迭代器的容器list, 不能随机访问 

  c.front();

  c.back();

 

      //前向迭代器的forward_list, 不能随机访问

      fwlist.front();

 

  //通用的访问方式:迭代器

  c.begin();

  c.end();

  c.rbegin();

  c.rend();

  //since c++11, forward_list不提供反向

  c.cbegin();

  c.cend();

  c.crbegin();

  c.crend();

  //forward_list提供before_begin(),只有一个指针只向开始,不知结尾在哪

  fwlist.before_begin();              //返回第一个元素位置之前的前向迭代器

  fwlist.cbefore_begin();

 

  //array(since C++11), vector(since C++11), string提供特殊的data(), 返回首元素地址

 

6. 删除

 

   //vector, deque, list都支持的操作

  c.pop_back();    //删除最后一个元素,但是不返回它,返回值void

  c.erase(pos);    //删除迭代器所指元素,返回下一个元素位置

  c.erase(beg, end);  //删除[beg, end)所指元素,返回下一个元素位置

  c.clear();      //清空容器

  

  //双头的deque,list,增加

  c.pop_front();    //删除第一个元素,不返回它, 一般front(), pop_front()

  

    //forward_list特殊的_after,但是没有_back

      fwlist.pop_front();

      fwlist.erase_after(pos);

      fwlist.erase_after(beg, end);

      fwlist.clear();

 

  //以上是根据位置删除元素要想根据值删除一个元素:

  //对于listforward_list(链表删除元素有自己特殊的方式):

  list.remove(val);    //删除值等于val的所有元素

  list.remove_if(op);  

   //对于vector,deque

  //删除所有值为val(erase-remove)

  //remove()移动val后面的元素覆盖val的值,

  //返回移除后的最后一个元素的位置,

  //最后容器大小不改变。

  //原因:算法不了解他所操作的容器,不能直接删除值

  //好处:可以使算法适应各种容器

  //         可以使用容器的一段区间作为操作区间

  c.erase(remove(c.begin(), c.end(), val), c.end());

  

  //删除第一个值val的操作

  c::iterator pos = find(c.begin(), c.end(), val);

  if (pos != c.end()) {

    c.erase(pos);
  }

 

  

7. 修改

 

  可以通过:查找 + 访问,或者algorithm中算法加以修改。

 

8. 查找

 

  //由于所有顺序容器本身无特殊结构,使用algorithm中的find,不做多余叙述

 

9. 排序

 

  //array,vector,deque

  使用algorithm中sort,开优化条件下性能优于qsort。

 

  //list, forward_list结构特殊,使用自己的sort

  list.sort();

  list.sort(op);

 

10. 大小

 

   //vector,deque, list都提供的,array当然不提供啦

  c.empty();      //可能比size() == 0更快

  c.size();                     

  c.max_size();     //在没有reallocation情况下,最大可能的元素数目,由于deque采用分块的结构,它的这个值可能更大

      注:forward_list没有size(), 有另外两个。因为,这需要线性复杂度,不符合设计初衷,可以使用distance()获得大小。

 

  //改变元素数目,使用默认构造函数初始化

  //当现有元素数目<num时,增加元素数目到num使用默认构造函数初始化多的元素

  //现有元素数目>num,元素数目变为num多的元素删除

  c.resize(num);  

  c.resize(num, elem);  //多的元素初始化为elem

 

   //特有的: vector具有容量概念,提供capacity();reserver(num);

  //deque在非开头和结尾插入元素时,所有的指针,引用,迭代器失效,

  //但是由于它特殊的内部结构,重新分配时比vector快,因为它不是必须复制所有的元素。所以它也不需要提供容量概念。

  //deque在不在被使用时,块的内存可能被释放,它的内存大小可能减少(是否一定减少根据根据具体实现)。

  //list为链表插入时不会引起重新分配内存,不需要容量概念。

  c.capacity();

  c.reserver(num);  //为容器分配num*元素大小容量的空间,但是没有初始化

 

   //vector,deque提供,listforward_list不需要

  c.shrink_to_fit();  //since C++11,减小容量到可以存放容器中所有元素。

 

  

11. 算法

 

  对于顺序容器本身不提供的操作,多数都可以通过algorithm实现

 

12. 常见操作

 

  //在大多数情况下,选用array或者vector

  //不需要动态改变大小时:array

  #include <array>

  using std::array;

  //在知道初值的情况下,使用初值进行初始化,例如:

  //array<int, 5>作为POD类型,可以静态初始化,与C中struct具有相同的内存布局。

  //此时采用的静态初始化方式,{1, 2, 3, 4, 5}就是arr在内存中布局方式。

  //从&arr -- &arr + sizeof(arr)的内存空间就是arr中数据的内存布局,

  //arr内存都在栈上,这与string,vector等有很大不同,

  //它们的对象本身内存可能在栈上,但是代表的数据是在堆上分配(使用指针代表分配的空间)。

  array<int, 5> arr = {1, 2, 3, 4, 5};              //若是array<string, 5>就不是POD类型, 也不是triviall类型,is_standard_layout。因为string不是POD。

  //ERROR

  array<int, 5> arr({1, 2, 3, 4, 5});  //没有这种初始化方式,其它容器可以。

  //不知道初值

  array<int, 5> arr = { };    //所有元素初始化为0(int()),使用默认构造函数

  array<int, 5> arr;                   //arr是未定义的值,int为POD类型,具有未定义的初值。POD生存期: 对象被定义 —— 被释放或者被重新利用。

  array<string, 5> arr;               //arr有5个空的string。string非POD类型,生存期: 构造函数 —— 析构函数

  arr.fill(val);          //所有元素赋值为val

  //对于array<int, 5>这种POD

  memset(&arr,  val, sizeof(arr));

  memcpy(&arr, src_des, sizeof(arr));

  memmove(&arr, src_des, sizeof(arr));

 

  //需要动态改变大小vector

  vector<int> ivec;      //ivec为空,就是没有元素,此时对ivec的所有元素访问都是错误的。

  //一般在知道vector需要使用的大小时,需要

  ivec.reserve(大小值);    //避免可能的内存重新分配

  //给容器添加元素,若允许,就使用assign(调用构造函数),一般vector为类成员时

  ivec.assign(n, elem);

  ivec.assign(beg, end);

  ivec.assign(initlist);

  ivec = initlist;

  //一般情况下:

  ivec.push_back(elem);   //添加elem

 

  

  //arrayvector当作普通数组使用

  //arrayvector中元素在连续的内存单元中

  &arr[i] = &arr[0] + i;    //只是当作地址,不能当作指针。数组名可以当作首地址,array,vector对象不可以,它们是类。

      //对于array,可以

      &arr[i] = &arr + i;        // vector不可以

      //但是这样不可以,arr是一种数据类型,不是数组不会退化为首元素地址

      &arr[i]  != arr + i;    //数组可以

 

  //作为C-style array

  array<char, 50> arr = {0};  or vector<char> arr;  arr.reserve(50);

  strcpy(&arr[0], "hancm, hi");  

  printf("%s\n", &arr[0]);

  <=> printf("%s\n", arr.data());  //since c++11使用arr.data()获取首地址,更好的。

  注意:

  printf("%s\n", arr.begin());    //不保证可以工作,array和vector的迭代器类型大多数为普通的指针,但是不是所有的都是(某些为一个类)。

 

  //当需要:在头和尾插入和删除元素时(最常用情况)

         不需要引用容器的元素

       在不再使用时,看重容器是否释放内存(但是标准没有保证这点)

  //时,使用deque

  //deque的接口与vector大部分相同,但是不提供容量概念,即capacity(), reserve(),可以选择使用resize();

 

  //当需要:不只是在容器的头和尾中插入和删除元素(插入和删除元素在常量时间内完成)

         想要绝大多数操作要么成功,要么no—op(这是list支持的异常处理方式,另外的不具有)

  //时,使用list

  //list是双向迭代器,不提供[]at(),不提供容量capacity(),reserve,因为不需要的。它的每个元素都有自己的内存空间。

  //移动和删除元素方面,list提供了一些特殊成员函数,它们都比算法中相同的函数更快,因为list只需重定向指针,不需要复制和移动元素

  //使用方式与vector类似。

 

posted on 2014-04-16 11:25  hancmhi  阅读(583)  评论(0编辑  收藏  举报