三、顺序容器

本文适合对于c++有一些基础的人阅读,指出一些容易被忽略的点。
本文包括顺序容器(array/vector/string/deque/list/forward_list)和他们的迭代器大部分的使用方法,注意事项,顺序容器的限制;对指针、数组、和数组指针的解释;stack和queue容器适配器。

3.1 顺序容器

顺序容器理解

元素是由存储位置来排序的(关联容器是根据元素值来排序的)。

顺序容器种类

内存连续型:array/string/vector,特点:随机访问快,尾部插入快,中间插入慢。

内存不连续性型:list/forward_list,特点:无法随机访问,插入快。

中间类型:deque,特点,是vector和list的结合,随机访问和插入速度都较快。

顺序容器的限制

应该有默认构造函数:因为有resize等功能,需要调用默认构造函数。

vector必须有拷贝构造函数:因为动态分配空间时需要拷贝对象。

vector最好有移动构造函数:会增加拷贝时的效率。

注意:容器应该放对象,尽量不要放指针,但是不放引用。

案例1:

    deque<int&> dq;	//无法编译通过
    for (int i; i <= 10; ++i) {
        dq.emplace_back(i);
    }
    for (auto i : dq) {
        cout << i << endl;
    }

解释:引用的使命周期一定要小于等于被引用的对象,但是放在容器中会出现被引用的对象已经无效,但他的引用还在。

案例2:

    deque<int*> dq;
    for (int i; i <= 10; ++i) {
        dq.emplace_back(&i);
    }
    for (auto i : dq) {
        cout << i << endl;
    }

解释:指针可以放在容器中,但是我们要维护指针所指向的内存有效性,这样就出像了指针悬挂(野指针)的问题,在容器应用时应慎重存放指针。

3.2 顺序容器的基本操作

通过迭代器操作容器

为什么要引入迭代器?

    vector<int> v{1,2,3,4,5,6,7,8,9,10};
    for (auto itr = v.begin(); itr != v.end(); ++itr) {
        ++(*itr);
    }
    deque<int> dq{1,2,3,4,5,6,7,8,9,10};
    for (auto itr = dq.begin(); itr != dq.end(); ++itr) {
        ++(*itr);
    }
    list<int> l{1,2,3,4,5,6,7,8,9,10};
    for (auto itr = l.begin(); itr != l.end(); ++itr) {
        ++(*itr);
    }

​ 迭代器可以完全不用担心你使用的是什么容器。

迭代器有四种:

​ 正向iterator,只读正向const_iterator。

​ 反向reverse_iterator,只读反向const_reverse_iterator。

迭代器的主要用途为:查看和修改元素。

注意:增加和删除元素,有可能会造成迭代器的失效,需要慎重使用。

顺序容器的迭代器的主要操作:

​ 所有迭代器都支持向前迭代器的操作:自增++,比较==,取元素*/->。

​ 非forward_list容器都支持双向迭代器的操作:自减–。

​ 内存连续和deque都支持 随机访问迭代器 的操作:移动迭代器±(int)/±(int)/itr[int];迭代器距离(itr)±(itr);迭代器位置比较>/</>=/<=。

​ 其他封装的函数:advance,distance,next,prev。(向前迭代器慎用)

注意:如果迭代器有溢出操作(在begin()前或者在end()后),程序不由抛出异常也不会core,是很危险的行为。

初始化
    //顺序容器都可以如下初始化,以vector为例,array除外。
    vector<int> v1;             //无参构造
    vector<int> v2(10, 100);    //按个数构造,10个100
    vector<int> v3{1,2,3,4,5};  //列表初始化
    vector<int> v4(v2);         //拷贝构造
    vector<int> v5(v2.begin(), v2.end());   //迭代器构造

    //{}先调用列表初始化,如果无法转化类型,在退化成()的作用。
    vector<string> v6{10};          //只有一个元素10
    vector<bool> v7(10);            //有十个元素
    cout << v6.size() << endl;      //10
    cout << v7.size() << endl;      //10

    //array要特殊一些
    array<int, 10> a1;
    array<int, 10> a2{1,2,3};

除了array以外的顺序容器都可以动态分配大小。{}列表初始化可以退化成()。

修改操作

交换:swap。(除array元素外,不会引发元素的删除、拷贝等操作,效率很高)。

修改:assgin。(array除外)

注意:会导致迭代器失效。

插入操作

forward_list和array比较特殊,不予讨论。

插入:insert。

尾部插入:push_back,emplace_back。

头部插入:push_front,emplace_front。(vector和string无)

注意:push插入对象是会拷贝一份到容器中,不会对原有的对象有影响。

​ emplace则会直接调用构造函数到容器中。

​ 插入操作会使迭代器失效。

访问操作

首元素/尾元素:front,back。应防止容器为空。back不适用于forward_list。

下标查找:operator[],at。at会检查溢出,溢出会抛出out_ot_range异常。只有内存连续的容器支持此操作。

删除操作

此操作会改变容器的大小,所以array不支持删除。

删除头尾:pop_front,pop_back。避免容器为空,返回void,forward_list不支持pop_back,内存连续的容器不支持pop_back。

删除一部分:eraser。

删除所有:clear。

注意:迭代器会失效。顺序容器的size会改变,但是capacity不变。

​ 从容器中删除一个元素,会自动调用他的析构函数。

容其大小管理操作

容器中元素个数:size。

重置容器元素个数:resize。

容器以申请空间(容积):capacity,只适用于vector和string。

为容器预留空间:reserve,只适用于vector和string。

调整容器容积到适当大小:shrink_to_fit,值适用于vector,string和deque。

注意:reserve后capacity至少为shrink_to_fit后的值。reserve一般用于预留空间,不会小于当前capacity,如果想缩小容积需要用shrink_to_fit。resize>size时会调用默认构造函数,或拷贝传入的第二个参数,小于时会调用析构函数。

内存连续容器

string和vectro内存增长机制:

案例:

#include <iostream>
#include <vector>
using namespace std;

class Test {
public:
    int x;
    Test() : x(0) {cout << "Test() this = " << this << endl;}
    Test(int x) : x(x) {cout << "Test(int) this = " << this << endl;}
    Test(const Test& another) : x(another.x) {cout << "Test(const Test&) this = " << this << ", &another = " << &another << endl;}
    Test(const Test&& another) noexcept : x(another.x) {cout << "Test(const Test&&) this = " << this << ", &another = " << &another << endl;}
    Test& operator= (const Test& another) {
        cout << "operator=" << endl;
        if (this == &another)
            return *this;
        x = another.x;
        return *this;
    }
};

int main()
{
    vector<Test> v;
    for (int i = 0; i < 6; ++i) {
        v.emplace_back(i);
        cout << "size = " << v.size() << endl;
        cout << "capacity = " << v.capacity() << endl;
        cout << "==============" << endl;
    }
    return 0;
}

输出:

Test(int) this = 0x1041770
size = 1
capacity = 1
==============
Test(int) this = 0x1041794
Test(const Test&&) this = 0x1041790, &another = 0x1041770
size = 2
capacity = 2
==============
Test(int) this = 0x1041778
Test(const Test&&) this = 0x1041770, &another = 0x1041790
Test(const Test&&) this = 0x1041774, &another = 0x1041794
size = 3
capacity = 4
==============


Test(int) this = 0x104177c
size = 4
capacity = 4
==============
Test(int) this = 0x10417a0
Test(const Test&&) this = 0x1041790, &another = 0x1041770
Test(const Test&&) this = 0x1041794, &another = 0x1041774
Test(const Test&&) this = 0x1041798, &another = 0x1041778
Test(const Test&&) this = 0x104179c, &another = 0x104177c
size = 5
capacity = 8
==============

解释:体积增长是以2的倍数增加。

​ 如果size == capacity时,再插入一个会引发内存扩展。

​ 扩展时默认调用移动构造函数,如果没有移动构造函数,会调用拷贝构造函数。

​ 移动构造函数的出现,是将将亡值(可能是匿名变量也可能是被move修饰过的变量)拷贝给普通变量,存在大量内存是可以利用浅拷贝的方式,但是要注意double free。

3.3 数组

c语言中指针、数组、数组指针的区别

案例:

    int array[10];      //数组
    int *ptr = array;   //指针
    int (*ptr_array)[10] = &array;  //数组指针
    cout << "大小: 数组为 " << sizeof(array)
         << ", 指针为 " << sizeof(ptr)
         << ", 数组指针为 " << sizeof(ptr_array)
         << endl;
    cout << "指向: 数组为 " << array
         << ", 指针为 " << ptr
         << ", 数组指针为 " << ptr_array
         << endl;
    cout << "步长: 数组为 " << (uint64_t)(array + 1) - (uint64_t)(array)
         << ", 指针为 " << (uint64_t)(ptr + 1) - (uint64_t)ptr
         << ", 数组指针为 " << (uint64_t)(ptr_array + 1) - (uint64_t)(ptr_array)
         << endl;

输出:

大小: 数组为 40, 指针为 8, 数组指针为 8
指向: 数组为 0x61fde0, 指针为 0x61fde0, 数组指针为 0x61fde0
步长: 数组为 4, 指针为 4, 数组指针为 40

解释:

c语言类型区别在于大小(sizeof),和步长(地址+1)。

数组和指向首元素的指针(ptr)区别在于大小(sizeof)不同。

数组和指向数组的指针(ptr_array)区别在于步长(地址+1)不同。

上面array是数组,ptr和&array[0]是指向数组首元素的指针,ptr_array和&array是数组指针(指向数组的指针)。

注意:指针数组和数组指针并没什么关系,前者是放指针的数组,后者是指向数组的指针。

​ int *a[4]是指针数组,int (*a)[4]是数组指针,语法上带括号的是数组指针。

c++11后array只是在数组和容器中间做了适配。

3.4 string

插入

尾部插入多个:append/operator+=。

查看

查看单独元素:operator[]越界返回’\0’。

c类型字符串:data和c_str,c_str为只读。(容器操作)

查找元素:find/find_first_of/find_last_of等。未找到返回string::npos。

修改

直接全部覆盖:operator=;

与数字相互转化

字符串到正数:stoi/stol/stoll/stoul/stoull,可以选择进制,会抛出out_of_range和invalid_argument。

字符串到浮点数:stof/stod/stold,会抛出out_of_range和invalid_argument。

数字到字符串:to_string。

其他操作

比较:operator比较操作符。

字符串拼接:operator+。

字符串IO流:operator >> <<。

子字符串:substr。

字符串替换:replace。

3.5 forward_list

单向链表有自己的接口

头节点的迭代器:before_begin()/cbefore_begin()。

插入:insert_after/emplace_list。

删除:eraser。

3.6 容器的两个适配器

模板第二个参数可以改变实现他们的容器。

栈(stack)

默认用deque实现。

push/emplace/push/top

队列(queue)/优先级队列(priority_queue)

queue默认用deque实现,priority_queue默认用vector实现。

push/emplace/pop/front/back

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值