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...);
//对于deque,list这种双头增加:
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...); //只能在前端插入,不能在后端
注:对于deque,list这种双头和forward_list都提供
c.push_front(elem);
c.emplace_front(args...);
//对于array不提供插入元素,可以初始化,赋值改变容器元素。不提供任何其它变动性操作。
5. 访问
//随机访问迭代器的容器array,vector, 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();
//以上是根据位置删除元素,要想根据值删除一个元素:
//对于list,forward_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提供,list,forward_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
//把array和vector当作普通数组使用
//array和vector中元素在连续的内存单元中
&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类似。