文章目录
一、vector的介绍
1. 文档链接
2. 简要介绍
- vector是表示可变大小数组的序列容器。
- 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
- 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
- vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
- 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
- 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list和forward_list统一的迭代器和引用更好。
二、vector的使用
1.vector的定义
(constructor)构造函数声明 | 接口说明 |
---|---|
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x); (重点) | 拷贝构造 |
vector (InputIterator first, InputIterator last); | 使用迭代器进行初始化构造 |
vector& operator= (const vector& x); | 赋值重载 |
(1)构造函数
- vector的构造函数主要有三种
- 无参构造
- 放入n个相同的数据
- 使用迭代器区间进行构造
其中的allocator是空间配置器,只是用来分配空间的,加快空间申请和释放的速度。
vector是一个模板类,在实例化的时候要指明其内置类型。
template < class T, class Alloc = allocator<T> > class vector;
void test1()
{
vector<int> v1;//无参构造
vector<int> v2(3, 2);//构造一个有3个2的vector
string s1("abcd");
vector<int> v3(s1.begin(), s1.end());//迭代器构造
}
在迭代器构造的示例中我们可以看到v3的size是4,但是里面存的却不是字符abcd,那是因为我们实例化的时候类型写的是int,他这里发生了隐式类型转换。
如果想要看到正确的字符的话改成下面这样就好了
vector<char> v3;
vector<char>和string的区别:vector存的是一个一个的字符,结尾是没有'\0'的,而是string是字符串所以结尾会有'\0'
vector中不仅可以存自定义类型,它还可以放自定义类型,你创建的结构体什么的都可以放。因为【vector】是一个模版类,其会根据所传入的类型去做一个自动类型的推导,例如在vector中放入string对象,我们就可以直接这样写
vector<string> s1;
(2)拷贝构造函数
我们可以简单来看一下
void test2()
{
vector<int> v1(3, 2);
vector<int> v2(v1);//拷贝构造
vector<int> v3 = v1;//这也是拷贝构造,不是赋值重载
}
(2)赋值重载
void test3()
{
vector<int> v1(3, 2);
vector<int> v2;
v2 = v1;//赋值重载
}
2. vector 增删查改
vector增删查改 | 接口说明 |
---|---|
push_back(重点) | 尾插 |
pop_back (重点) | 尾删 |
find | 查找。(注意这个是算法模块实现,不是vector的成员接口) |
insert | 在position之前插入val |
erase | 删除position位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] (重点) | 像数组一样访问 |
(1)operator []
在vector中对[]进行了运算符重载,使得我们可以像通过下标访问数组元素一样来访问vector,同时支持修改
- 下面是官方文档中的形式,虽然看起来很复杂,但是读者完全不用理会,会用就可以了
reference operator[] (size_type n);const_reference operator[] (size_type n) const;
void test4()
{
string s1("hello,world");
vector<char> v1(s1.begin(), s1.end());
for (int i = 0; i < s1.size(); i++)
{
cout << v1[i] << " ";
}//支持访问
cout << endl;
v1[0] = 'x';
v1[1] = 'y';//支持修改
for (int i = 0; i < s1.size(); i++)
{
cout << v1[i] << " ";
}
}
(2)push_back和pop_back
在数组尾部插入一个元素和删除最后一个元素。
void test4()
{
string s1("hello,");
vector<char> v1(s1.begin(), s1.end());
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
v1.push_back('L');
v1.push_back('i');
v1.push_back('n');
v1.push_back('u');
v1.push_back('x');
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
v1.pop_back();
v1.pop_back();
v1.pop_back();
v1.pop_back();
v1.pop_back();
v1.pop_back();
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
}
如果我们这里采取string类作为【vector】的内置类型,然后通过三种形式往里面插入数据:
- 第一种是构造出具体的对象
- 第二种采取的是匿名对象
- 第三种采取的则是单参数的构造函数所引发的 隐式类型转换
void test5()
{
vector<string> v;
string name1("张三");
v.push_back(name1);
v.push_back(string("李四"));
v.push_back("王五"); // 单参数的构造函数支持隐式类型转换
}
(3)insert和erase
insert有很多,这里仅仅展示部分常用的两参数的,第一个参数是要插入位置的迭代器,第二个是要插入的元素。
void test7()
{
vector<string> v;
v.push_back("张三");
v.push_back("王五");
v.insert(v.begin() + 1, "李四");//在begin的下一个位置插入一个元素
for (int i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
v.erase(v.begin());//删除begin位置的元素
for (int i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
}
如果想要删除指定元素,我们可以find和erase搭配使用
(4)find查找
find是在范围内查找,观察函数的参数我们可以知道如果要使用这个函数的话就需要先传入一个迭代器区间,然后传入一个值,在指定的区间内查找这个值。如果找到则返回指向改元素位置的迭代器,如果找不到那就返回最后的迭代器也就是end()
void test6()
{
string s1("hello,world");
vector<char> v1(s1.begin(), s1.end());
std::vector<char>::iterator it = find(v1.begin(), v1.end(), 'w');
if (it != v1.end())
{
cout << *it << endl;
}
}
我们可以搭配erase加循环使用,删除vector中所有的 ‘l’ 字符
void test6()
{
string s1("hello,world");
vector<char> v1(s1.begin(), s1.end());
std::vector<char>::iterator it = find(v1.begin(), v1.end(), 'l');
while (it != v1.end())
{
it = v1.erase(it);
it = find(it, v1.end(), 'l');
}
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
3. vector 空间增长问题
容量空间 | 接口说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize(重点) | 改变vector的size |
reserve (重点) | 改变vector的capacity |
(1)size和empty及capacity
size就是获取容器中有几个元素,capacity就是容器的容量
capacity和size是不一样的,你可以开10个空间但只放5个数据,此时size就是5,而capacity就是10.
void test10()
{
vector<int> v(5, 10);
cout << v.size();
}
可以看到我们插入了5个10,所以size是5,capacity也是5.
empty就是判断容器是否有元素,没有元素返回1,有元素返回0
void test11()
{
vector<int> v;
cout << v.empty() << endl;//没元素所以为真,输出1
v.push_back(1);
cout << v.empty();//插入一个元素后不为空,输出0
}
(2)resize和reserve
vector是可以自动扩容的,但频繁扩容是浪费时间的,所以我们可以提前开足够的空间,提高效率。
我们可以探索一下vector的扩容机制
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
可见在vs中,vector是按1.5倍的规则扩容的,我们去Linux平台下再去试试。
可以看到在Linux平台下是按照2倍的扩容逻辑走的。可见不同地方vector的实现方法略有区别。
(3 reserve
reserve是开好空间但不填充元素,所以size是不改变的,只有capacity会改变。因为size没改变,所以不能通过[]来访问没元素的位置。
void TestVectorExpandOP()
{
vector<int> v;
size_t sz = v.capacity();
v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容
cout << "making bar grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
可以看到避免了频繁扩容
(4 resize
resize是开好指定的空间并填充默认值,capacity和size都会改变
可以看到size和capacity都是3,填充的默认值我们没有指定,所以默认填充的是0。
当然我们可以指定填充的内容,比如这里我们指定填充数字10
接下来我们说一下常见的错误
大家可以看看下面的代码有什么问题,是不是乍一看好像每什么问题,但是一运行直接寄了。
void test13()
{
vector<int> v1;
v1.reserve(10);
for (size_t i = 0; i < 10; i++)
{
v1[i] = i;
}
}
- 大家要关注前面的reserve(10),我们在上面说到对于【reserve】而言只是做的扩容而已,即只变化capacity,而不会变化size
- 另一点,对于v1[i]我们上面在讲元素访问的时候有说到过,这是下标 + []的访问形式,在出现问题的时候会直接给出断言错误。因为这里我们在【reserve】的时候只是开出了指定的空间,但size还是为0,此时去访问的时候肯定就出错了
改正方法就是将reserve改成resize即可
可以看到成功运行
4. vector iterator 的使用
iterator的使用 | 接口说明 |
---|---|
begin + end(重点) | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
(1)begin和end
- 和【string】中一样,每个迭代器也是具有两种形式,第一个呢是具有读写的,第二个则是只读的const迭代器
- begin获取一个字符的迭代器
- end获取最后一个字符下一个位置的迭代器
迭代器的理解,迭代器呢可以说是STL中很重要的一部分。简单来说迭代器就是用来遍历或访问容器中的数据的,我们暂时可以把迭代器想象成指针,通过指针的++或者–加解引用的方式,我们就可以遍历一个数组,或者访问数组中的元素。当然指针只是迭代器中的一种,迭代器要实现的目的就是通过++或者–能够遍历容器中所有的元素,我们数组是一段连续的空间,可以通过指针加一的方式遍历整个数组,但是如果是链表呢?这种情况下通过对每个指针++的操作就无法实现目的了,因此指针就不适合当迭代器了,我们就得封装新的迭代器。
void test8()
{
string s1("hello,world");
vector<char> v1(s1.begin(), s1.end());
vector<char>::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it << " ";//通过解引用迭代器获取容器中的元素
++it;//迭代器++,指向下一个位置
}
}
(2)rbegin和rend
rbegin和rend是反向迭代器
反向迭代器呢顾名思义就是从反方向进行遍历
void test8()
{
string s1("hello,world");
vector<char> v1(s1.begin(), s1.end());
vector<char>::iterator it1 = v1.begin();
while (it1 != v1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
auto it2 = v1.rbegin();
while (it2 != v1.rend())
{
cout << *it2 << " ";
++it2;
}
}
(3)范围for
既然支持迭代器的话,那肯定支持范围for的,我们可以来试试。
void test9()
{
string s1("hello,world");
vector<char> v1(s1.begin(), s1.end());
for (auto e : v1)
{
cout << e << " ";
}
}
可以看到没有任何问题。