返回随笔C++
旧稿归档ENGINEERING NOTE

STL:顺序容器

对 vector、deque、list、array 等顺序容器的初始化、赋值、迭代器与常见操作进行横向整理。

C++STL容器

顺序容器的分类

容器特定对象(包括内置数据类型,结构,类等)的集合。顺序容器则提供了按顺序储存或访问它们的能力。

理解为一个很牛逼的数组就行了。

下面是顺序容器的类型和特点:

容器类型 特点
vector 大小可变,可以增添或删除,可以随机访问。在尾部之外的地方插入数据可能很慢。
deque 双端队列,在头部插入数据很快。
list 双向链表,在任何位置插入数据很快,但访问很慢。
forward_list 单向链表,只支持单向数据访问,在任何位置插入数据很快。
array 固定数组,无法变化大小,可随机访问。
string 类似于vector

应在不同情况下选择不同的容器,一般除非有特殊理由,则使用vector。避免使用数组,容器的数据处理效率要大于数组。如果不确定应选择何种容器,则在只使用vector和list的情况下使用迭代器(而不是下标访问)。

顺序容器的迭代器

所有的容器迭代器都有++操作,–仅forward_list不支持(无法双向访问)。支持随机访问的迭代器才能使用+, -运算符。

可以对迭代器使用<, <=, >, =>运算符,但list和forward_list不支持。

顺序容器定义和初始化

初始化方法:

list<int> a;                       //名称为a的空容器
list<int> c{ 1, 2, 3 };            //列表初始化
//或list<int> c = { 1, 2, 3 };
list<int> d(c);                    //将d初始化为c的拷贝
//或list<int> d = c;
list<int> e(c.begin(), c.end());   //将e初始化为两个迭代器中的元素

//仅顺序容器可用
list<int> (5);                     //五个元素的空容器
list<int> f(5);                    //含有五个元素的容器
list<int> h(5, 1145);              //含有五个元素,值都为1145的容器

将一个容器初始化为另一个容器的拷贝,容器类型必须相同,但使用迭代器则不要求。程序员要保证类型匹配。

vector<double> vecDou{ 1, 2 ,3 };
vector<int> vecInt2(vecDou);  //非法!
vector<int> ve2Int(vecDou.begin(), vecDou.end());  //合法
	
vector<string> vecString{ "addd", "add" };
vector<const char*> vecChar(vecString);  //非法!
vector<const char*> vecChar2(vecString.begin(), vecString.end());  //合法
vector<int> vecInt(vecString.begin(), vecString.end());  //合法,但绝对不能这样做。

array容器的特殊初始化

array容器可以看作定长数组,在初始化时必须传递长度数据。

array<int, 3> arr;   //长度为3,储存int的array
array<int>  arr;     //非法!array<int>不是类型

无法对内置数组类型进行拷贝,但array可以。

int a[3] = { 1, 2 };
int b[3] = a;  //非法!

array<int, 3> a = { 1, 2 ,3 };   
array<int, 3> b = a;  //合法,只要类型匹配

在拷贝时,必须保证数据类型和大小完全一致。迭代器方法在此无效。

顺序容器的赋值

使用=运算符的前提是容器类型一致,赋值后左边容器和右边容器完全相同。赋值操作会使得指向左边容器的指针,迭代器和引用失效。

swap()和assign()函数可用于更改容器的值。且指针,迭代器和引用不会失效。

swap(a, b);   //交换两个容器的值,要求类型相同
a.swap(b);    //使用成员函数法,两种方法都比直接拷贝快

a.assign(b.begin(), b.end());  //将指定迭代器范围内的数据赋给a
a.assign({ 1, 2, 3 });         //将a替代为初始化列表
a.assign(4, 2);                //将a替代为四个为2的元素

这两个函数都不能直接传递类型不匹配的容器。

书后习题:将一个list<char指针>赋给vector

list<const char*> liChar(3);  //为了可以赋值,这里用了const char*
liChar.assign({ "addd", "adddd", "add" });

vector<string> vecString(liChar.begin(), liChar.end());
vecString.assign(liChar.begin(), liChar.end());

关系运算符

所有容器都支持等于和不等,除了无序关联容器以外都支持大于小于,大于等于小于等于。

只有当两个容器类型完全相同时才能使用关系运算符。

书后习题:比较list和vector中的元素

bool operator<(list<int> listInt, vector<int> vecInt) {
	list<int>::iterator iterList = listInt.begin();
	vector<int>::iterator iterVec = vecInt.begin();

	for (iterList, iterVec; iterList != listInt.end() || iterVec != vecInt.end(); iterList++, iterVec++) {
		if (*iterList < *iterVec)
			return true;
	}
	return false;
}
//其实就是给一个运算符函数

顺序容器操作

向容器中增加元素

push_front和push_back

向容器前和后增加元素,其中vector和string不支持push_front。

list<int> lis;
lis.push_front(1);
lis.push_back(1);
insert

前者为在首位增添元素提供了便捷的方法,而insert则更加泛用。不支持push_front的vector和string也可以通过insert来向首添加元素。

vector<int> vec;
vec.insert(vec.begin(), 1);
vec.insert(vec.begin(), vec2.begin(), vec2.end());

vec.insert(vec.begin(), 2, 2);   //增加两个为2的元素
vec.insert(vec.begin(), { 1, 2, 3 });  //列表初始化

insert()函数返回一个指向插入元素头的迭代器,可以用这个特性编写循环,使得向一个地方持续插入数据。

int a;
auto iterList = lisInt.begin();
while (cin >> a) {
	iterList = lisInt.insert(iterList, a);
}
emplace

emplace有emplace,emplace_front,emplace_back三种,对应于insert,push_back,push_front。使用emplace时,参数将传递给构造函数,将构造一个临时对象压入容器,而push则是直接对内存操作。

vector<int> vec;
vec.emplace(vec.begin(), 1);  //等于vec.insert(vec.begin(), 1)

当容器元素是用户自定义对类时,可能要求传递多个参数,push没有多个参数的版本,只能传递构造好的对象。而emplace整合了构造函数的用法。也可以这样用:

//构造一个传递三个变量的addd对象
//使用emplace
vec.emplace("ni", "shuo", "de");
//或者
vec.push_back(addd("ni", "shuo", "de"));

//不能:
vec.push_back("ni", "shuo", "de");

访问元素

容器类提供了直接访问元素的方法和使用迭代器访问的方法。

vector<int> vec;

if (!vec.empty()) {
    int a = vec.front();
    int b = vec.back();
    
    int c = *vec.begin();
    int d = *vec.end();
    
    int e = vec[n];
    int d = vec.at(n);
}

其中,front()返回首元素的引用,back()返回尾元素的引用,与end()不同的是,后者返回超尾元素(的迭代器)。forward_list容器没有back()。

支持随机访问的容器可以使用下标访问法和一个返回引用的函数at()。

应在使用这些函数之前检查容器是否为空,且下标不能越界。

删除元素

容器类提供了删除元素的方法:

vector<int> vec;

vec.pop_back();    //删除第一个元素,返回void
vec.pop_front();   //删除最后一个元素,返回void

vec.erase(p);      //删除迭代器p指向的元素,返回指向p后的迭代器
vec.erase(a, b);   //删除迭代器a,b区间内的元素,返回指向b后的迭代器。若b为end(),返回尾后迭代器
vec.clear();       //删除vec中的所有元素。返回void

这些操作不适用于array和forward_list。前者是由于删除操作改变了容器大小,后者是因为链表的特殊性。

可以通过erase()返回尾后迭代器的特性编写代码,遍历容器,并对指定数据操作:

vector<int> vec;
auto iter = vec.begin();

while (iter != vec.end()) {
    if (*iter % 2)
        iter = vec.erase(iter);
    else
        iter++;
}

如果*iter为奇数,删除该元素,返回的迭代器指向下一个元素;如果不是,则将迭代器递增。代码等同于:

if (*iter % 2) {
    vec.erase(iter);
    iter++;
}
特殊的forward_list操作

对于一个链表来说,更改元素需要改变前一元素的属性,而forward_list不能递减访问。它给出的删除函数都只能删除某元素后的内容。

forward_list<int> vec;

vec.before_begin();         //特殊的before_begin(),返回首前迭代器
vec.begin();

vec.insert_after(p, t);     //p为位置,t为插入的对象
vec.insert_after(p, a, b);  //p为位置,a,b为迭代器
vec.insert_after(p, n, t);  //p为位置,n为数量,t为对象
vec.insert_after(p, li);    //p为位置,li为初始化列表

vec.emplace_after(p, args); //p为位置,args为构造函数参量

vec.erase_after(p);         //删除指向p后的元素,若p为尾元素,则函数行为未定义
vec.erase_after(a, b);      //删除b到e中的元素

若想要向上一点一样遍历+删除容器内容,则需要这样做:

auto iterBefore = vec.before_begin();
auto iterCurrent = vec.begin();

while (iterCurrent != vec.end()) {
    if (*iterCurrent % 2)
        iterCurrent = vec.erase_after(iterBefore);
    else {
        iterBefore = iterCurrent;
        iterCurrent++;
    }
}

改变容器类大小

//array不支持
vec.resize(n);   //大小调整为n个元素,若为对象,调用默认构造函数
vec.resize(n, t);  //大小调整为n个元素,初始化为t

容器操作会使迭代器失效

由于如删除,添加元素等操作会使得迭代器指向的内存空间改变或失效,当改变一个容器的内容时,必须重新定位迭代器。使用失效的指针和迭代器是一种严重的程序设计错误。

例:改变容器的循环程序

添加删除vector,string,deque容器时必须考虑更新迭代器。

vector<int> vi{ 1, 2, 3, 4, 5, 6 };
auto iter = vi.begin();

while (iter != vi.end()) {
	if (*iter % 2) {
		iter = vi.insert(iter, *iter);
		iter += 2;                       //在元素之前插入
	}
	else
		iter = vi.erase(iter);
}

insert()函数返回的是插入元素的迭代器,这个插入元素位于iter之前,也就意味着需要iter+=2才能使iter指向下一个元素(iter++指向的是原来的iter,原iter在插入操作后失效。)

注意到其中iter = vi.insert(iter, *iter);,如果不接受insert()返回的迭代器,仅将iter++,将会出现严重错误,因为插入操作后iter迭代器失效,必须重新指定。

不要保存end返回的迭代器

end返回的迭代器会随着插入,删除等操作不断变化,将end迭代器保存是完全错误的。

vector<int> vi{ 1, 2, 3, 4, 5, 6 };
auto iter = vi.begin();

while (iter != vi.end()) {
	iter++;
	iter = vi.insert(iter, 4);    //在元素之后插入
	iter++;
}

绝对不能写成:

auto end = vi.end();
while (iter != end) {
}

管理容器容量的函数

可以随机访问的容器在管理内存空间时通常分为两部分:储存元素的部分,预留给元素的部分。通常预留给元素的部分不为空,能通过函数来操作。

vector<int> vi;
vi.size();           //返回元素数量
vi.capacity();       //返回总内存空间的数量

vi.resize(n);        //重新分配元素数量为n
vi.reserve(n);       //重新分配内存空间数量为n(可能会更大),如果小于size,则不会处理请求
vi.shrink_to_fit();  //将内存空间调整为正好合适。只是一个请求,不保证退还内存

在必须要扩大内存时,容器会一次请求很多内存。请求内存较慢,但向内存中压入元素很快,所以vector和string容器采用这样的方法。list并不需要连续的内存空间,array长度为定值,都不需要这样的内存管理。