顺序容器
1.顺序容器操作
前面介绍的那些是所有容器都支持的,我们接下来介绍的只适用于顺序容器(以后还会介绍关联容器)。
(1)添加元素
不知道为啥原书篇幅超多,我觉得直接看代码就很明了,所以我就写代码了:
list<int> a = {1, 2, 3}; //注释为a的元素内容{1, 2, 3}
a.push_back(4); //{1, 2, 3, 4},array和forwar_list不支持
a.push_front(0); //{0, 1, 2, 3, 4},只有list,forward_list和deque支持
以上两种可以方便地在头尾添加元素,更进一步,insert允许我们在容器任意位置中插入多个元素,vector,deque,list,string都支持insert:每个insert接受一个迭代器作为第一个参数:
list<string> slist = {Jay"};
auto iter = slist.begin();
slist.insert(iter, "Hello"); //将Hello添加到iter之前的位置
//insert还有重载版本
vector<string> svec;
svec.insert(svec.end(), 10, "May"); //在末尾插10个May
vector<string> a = {"1", "2"};
slist.insert(slist.begin(), a.end()-1, a.end()); //插入了"2"
slist.insert(slist.begin(), slist.begin(), slist.end());
//错误,要拷贝的迭代器不能指向自己
现在我要在vector中实现一个功能,每次插入的单词都插在头部:
vector<string> a;
auto it = a.begin();
string word;
while(cin >> word)
{
it = a.insert(it, word);
}
因为insert会返回当前位置,调用insert会在前一个位置插入,相当于调用了push_front
新标准还引入了三个新成员-emplace_front,emplace和emplace_back,这些操作时构造元素,不是拷贝元素,它们仨分别于push_front,insert和push_back对应:
vector<Sales_data> c;
c.emplace_back("1", 25, 16.8);
//在c的末尾构造一个Sales_data对象(调用三个参数的构造函数)
c.emplace_back("1", 25, 16.8); //这样不对的
c.emplace_back(Sales_data("1", 25, 16.8)) //这样可以
(2)访问元素
每个顺序容器都有一个front函数,返回首元素的引用;除forward_list之外的都有一个back,返回尾元素的引用,还是看代码好理解:
vector<string> c = {"1", "2"};
if(!c.empty)
{
auto v1 = *c.begin(), v2 = c.front(); //v1,v2都是首元素的拷贝
auto last = c.end();
auto v3 = *(--last); //除forward_list之外
auto v4 = c.back(); //也除forward_list之外
}
c[n]和c.at(n) 返回下标为n的元素的引用:
if(!c.empty)
{
auto &v = c.back(); //获得尾元素的引用
v = “3”; //改变了c中的元素
auto v2 = c.back();
v2 = "0"; //没改c的元素,是个引用
}
与以前一样,如果用auto变量保存这些函数的返回值,并且要改变容器内的值,必须要将变量定义为引用类型。 下标一定要在合理范围内,这个程序员自己要注意,来个错误示范:
vector<int> a;
cout << a[0]; //错了
(3)删除元素
vector<int> a = {1, 2, 3, 4, 5, 6};
a.pop_front(); //删除首元素
a.pop_back(); //删除尾元素
//删除a中所有奇数
auto it = a.begin();
while(it != a.end())
{
if(*it % 2)
{
it = a.eraser(it); //删除奇数
}
else
{
++it;
}
}
//删除所有元素,两种方式
a.clear();
a.eraser(a.begin(), a.end());
(4)内存管理
vector对象是如何增长的
这一部分我们来仔细研究一下vector在内存中的管理。
我们已经学过,vector的元素在内存中是连续存储的,这样一来,如果我在添加元素的时候,没有空间去容纳新元素应该怎么办呢?vector是这么做的:
- 把已有元素移动到新空间中
- 然后添加新元素
- 释放旧存储空间 那么问题又来了,我们的新空间要分配多大呢?一般来说貌似是旧空间的两倍,反正就是会预留一些空间。
管理容量的成员函数
成员函数 | 作用 |
---|---|
c.size() | 目前含有元素的数量 |
c.capacity() | 所能保存的最大元素数量(不重新分配内存空间),只适用vector和string |
c.shrink_to_fit() | 将capacity()减小为size()相同大小(适用vector、string、deque) |
c.reserve(n) | 分配至少能容纳n个元素的内存空间 |
reserve():只有当当前内存空间不够用的时候,reserve才会重新分配内存空间(可能比n还要大),够用的话这个函数什么也不做,这样的话,reserve永远不会减少容器所占用的内存空间。
还有一个resize函数已经用过的,它只改变容器中元素的个数,不会改变容器的容量。
capacity和size
直接看代码:
vector<int> ivec;
ivec.push_back(1);
cout << ivec.size() << ivec.capacity();
size肯定是1,capacity依赖于标准库的具体实现,肯定大于等于1
(5)改变容器大小
除了array之外,我们可以用resize来改变容器大小:
- 如果当前大小大于所要求的的大小,容器后面的元素会被删除
- 如果当前大小小于新大小,会将新元素添加到容器后部
list<int> a(10, 42); //10个42 a.resize(15); //后面再加5个0,是默认初始化的, //如果是类类型,要么就有默认构造函数,要么就提供初始值 a.resize(25, -1); //后面再加10个-1 a.resize(5); //从末尾删除20个元素,就剩5个42
缩小容器,指向被删除元素的迭代器、引用和指针都会失效;对vector、string和deque进行resize也可能导致它们失效。
容器操作可能使迭代器失效
使用失效的指针、引用或迭代器是很严重的错误,我们来仔细分析一下,分添加和删除两种情况
添加元素:
- 容器是vector或string:
- 如果存储空间被重新分配,则指针等全部失效
- 未重新分配,插入位置之后的那些都失效
总之就是内存位置变了就不行了 - 对于deque,插入除了首尾之外的位置就会失效;如果在首尾添加,迭代器失效,引用和指针不会失效
- 对于list和forward_list,都还是有效的
删除元素:
(被删除的元素对应的那三样肯定挂了)
- 对于list和forward_list都有效
- 对于deque,
- 如果在首尾之外的任何位置删除,都失效;
- 如果删除尾元素,尾后迭代器失效,其他不影响,如果删除首元素
- 如果删除首元素,都不受影响
- 对于vector和string,被删除元素之前的都有效,所以尾后迭代器总会失效
注意函数返回迭代器的位置
我们来写一个函数,删除偶数元素,复制每个奇数元素:
vector<int> vi = {0, 1, 2, 3, 4, 5};
auto iter = vi.begin();
while(iter != vi.end())
{
if(*iter % 2)
{
iter = vi.insert(iter, *iter);
iter += 2; //跳过当前元素和复制元素
}
else
{
iter = vi.eraser(iter); //删除偶数元素
//不用向前移动迭代器,因为eraser返回删除元素的下一个
}
}
不要保存end返回的迭代器
vector<int> vi = {0, 1, 2, 3, 4, 5};
auto begin = vi.begin();
auto end = vi.end();
while(begin != end)
{
begin = v.insert(begin, 42);
++begin; //跳过刚刚加入的元素
}
有问题的,因为end记住了一个以前的end,更好的应该这样:
vector<int> vi = {0, 1, 2, 3, 4, 5};
auto begin = vi.begin();
while(begin != vi.end())
{
begin = v.insert(begin, 42);
++begin;
}