1.vector的介绍
记得之前我们用C语言实现过顺序表,vector本质上也是顺序表,一个能够动态增长的数组。
vector 的底层实现机制
- 动态数组:vector 的底层实现是动态数组。它在内存中连续存储元素,就像一个可以自动调整大小的数组。
- 内存分配策略:
- 当向 vector 中添加元素导致容量不足时,vector 会重新分配一块更大的内存空间,将原有元素复制到新空间中,然后释放旧空间。这个过程可能会比较耗时,尤其是当 vector 中存储的元素数量较大时。
- 初始时,vector 通常会分配一定大小的内存空间,随着元素的不断添加,逐步扩大容量。
- 迭代器失效:在进行插入、删除等操作时,可能会导致指向 vector 中元素的迭代器失效。这是因为这些操作可能会引起内存的重新分配和元素的移动。
迭代器失效后面模拟实现会详细讲解
其实vector的常用接口和string大部分相似,但是也有不同,那有什么不同呢?
一、存储内容不同
- vector:可以存储各种类型的数据,如整数、浮点数、结构体等。例如,可以存储一组整数 vector<int> v = {1, 2, 3} 。
- string:专门用于存储字符序列,即字符串。例如 string s = "hello" 。
二、操作不同
- vector:
- 支持随机访问,可以通过下标快速访问元素。例如 v[2] 可以快速访问 vector 中的第三个元素。
- 可以动态添加和删除元素,使用 push_back 添加元素, pop_back 删除最后一个元素。
- string:
- 提供了丰富的字符串操作函数,如查找、替换、拼接等。例如 s.find("ll") 可以查找字符串中“ll”的位置。
- 可以直接使用 + 进行字符串拼接。
三、性能特点不同
- vector:
- 在内存中连续存储元素,有利于提高访问速度,但在插入和删除元素时可能需要移动大量元素,效率较低。
- 可以预先分配一定的空间,避免频繁的内存分配和释放。
- string:
- 通常会进行一些优化,如小字符串优化等,以提高性能。
- 字符串的长度可以动态变化,但在进行大量修改操作时可能会有一定的性能开销。
2.vector的使用
2.1vector的初始化
无参构造:
vector<int> v1;
构造并初始化:用n个value初始化
vector<int> v2(10, 1);//10个1
迭代器区间初始化:
vector<int> v3(v2.begin(), v2.end());
拷贝构造:
vector<int> v4(v2);
2.2vector iterator 的使用
iterator 的使用 | 接口说明 |
begin + end |
获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
|
rbegin + rend |
获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator
|
2.3 vector 空间
容量空间 | 接口说明 |
size |
获取数据个数
|
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize | 改变vector的size |
reserve | 改变vector的capacity |
2.4 vector 增删查改
vrector增删查改 | 接口说明 |
push_back | 尾插 |
pop_back | 尾删 |
find (#include <algorithm>) |
查找。(注意这个是算法模块实现,不是vector的成员接口)
|
insert | 在position之前插入value |
erase | 删除position位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] | 像数组一样访问 |
在 C++中,vector 并非没有实现 find 接口,只是没有像一些容器(如关联容器)那样有专门的成员函数 find。
原因如下:
1. 效率考虑:对于顺序容器(如 vector),线性查找的效率相对较低,通常可以使用更高效的算法如二分查找等,而不是直接调用 find 成员函数。
2. 设计理念:C++标准库的设计尽量保持不同容器的特性和用途明确,vector 主要用于存储连续的元素,更强调随机访问和高效的插入/删除尾部元素等操作,而不是查找。
可以通过标准算法中的 find 函数来在 vector 中进行查找。例如:
auto it = std::find(vector.begin(), vector.end(), target); 。
对于其他增删查改接口和string同样是类似的,就不详细介绍了,但是insert和erase这两个接口是不同的,这两个接口需要配合迭代器使用,但是配合迭代器使用这里就会有一个迭代器失效的问题
2.5 vector 迭代器失效问题。(重点)
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据,导致pos迭代器失效。
v.erase(pos);
cout << *pos << endl; // 此处会导致非法访问
return 0;
}
3.vector的模拟实现
3.1结构的定义
这里我们参考STL源码解析实现一个简易版本的vector
成员变量的定义:
#include <iostream>
#include <assert.h>
using namespace std;
namespace Ro
{
template<class T>
class vector
{
public:
typedef T* iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
这里大家可能会发现和模拟实现string的写法怎么不一样?
其实这就是参考STL的写法,虽然写法不同,但其实效果是大差不差的。
如图:
我们可以通过指针减指针的做法得到size和capacity。
这里同样和模拟实现string一样使用命名空间来和库中的vector区分开来,而且这里使用类模板是为了存储不同类型的数据,
这里顺带直接将size()和capacity()的接口实现出来,同时给成员变量加一个缺省值给初始化列表用
namespace Ro
{
template<class T>
class vector
{
public:
typedef T* iterator;
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
3.2构造函数和析构函数
构造:
vector(){}
这里初始化列表不写也会走,通过给的缺省值来初始化
析构函数:
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
3.3reverse()
由于后面大部分函数接口都需要扩容,所以为了后面方便,我们先实现reverse
void reverse(size_t n)
{
if (n > capacity())
{
if (_finish == _end_of_storage)
{
size_t old_size = size();
T* tmp = new[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * old_size);
delete[] _start;
}
_start = tmp;
_finish = tmp + old_size;
_end_of_storage = tmp + n;
}
}
}
这里我们要先将size()存下来,不然在给_finish更新时,会出现野指针。如图:
一般情况下都不会进行缩容的,所以我们在实现的时候不考虑缩容。另外,这里还会有一个坑,后面出错时我们再解决并说明
3.4push_back()和operator[]
为了让我们的vector能够跑起来,先来实现一下push_back接口
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
reverse(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = val;
_finish++;
}
先检查容量有没有满,满了就扩容,这里我们扩容就扩2倍,然后再插入数据,_finish指针++,指向下一个位置。
为了接下来方便测试我们先来实现 下标+[] 来访问数据
operator[]:
T&: T是我们不知道数据的类型,加&是为了减少拷贝
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
测试一下:
void test_vector1()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
for (int i = 0; i < v.size(); i++)
{
cout << v[i] << ' ';
}
cout << endl;
}
先测试一下扩容前的1 2 3 4,然后再测试一下扩容后的1 2 3 4 5
没有问题
3.5迭代器的实现
这里我们来实现一下普通迭代器和const迭代器
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
普通迭代器可读可写,const迭代器可读但是不可写。
测试一下迭代器遍历,同样范围for也可以使用了:
void test_vector2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
}
普通迭代器可写
void test_vector2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector<int>::iterator it = v.begin();
while (it != v.end())
{
*it *= 2;
it++;
}
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
}
const迭代器不可写
所以迭代器要正确匹配使用。
3.6深拷贝
前面我们提到过,扩容时还有一个坑没说,其实就是memcpy浅拷贝的问题。
如果我们vector存的是string这种自定义类型会发生什么?
void test_vector3()
{
vector<string> v;
v.push_back("111111111111111111111111");
v.push_back("111111111111111111111111");
v.push_back("111111111111111111111111");
v.push_back("111111111111111111111111");
//v.push_back("111111111111111111111111");
for (string& s : v)
{
cout << s << ' ';
}
cout << endl;
}
我们来看看扩容前和扩容后:
扩容前:
扩容前没有问题。那扩容后呢?
扩容后:
扩容后出问题了,运行崩溃,且打印结果错了,这是为什么?
其实就是memcpy因为浅拷贝导致的,如图:
那么整个时候就应该要深拷贝,tmp创建自己的空间存放拷贝的数据
void reverse(size_t n)
{
if (n > capacity())
{
if (_finish == _end_of_storage)
{
size_t old_size = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * old_size);
for (int i = 0; i < old_size; i++)
{
tmp[i] = _start[i];//T会调用自己的深拷贝
}
delete[] _start;
}
_start = tmp;
_finish = tmp + old_size;
_end_of_storage = tmp + n;
}
}
}
这里我们可以直接赋值,如果T是内置类型,一个一个拷贝没有问题,如果是自定义类型,就让T自己去调用它自己的深拷贝,也没有问题。
测试一下:
现在就不会出错了。
3.7resize()
如果n小于size,有效数据就会变为n个,容量不变
大于size小于capacity就会将数据扩大到n个,且会把size到n之间的数据初始化为val
大于capacity的话就会先扩容,再初始化。
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reverse(n);
while (_finish != _start + n)
{
*_finish = val;
_finish++;
}
}
}
这里缺省值我们不能直接给0或者其他,因为存储的数据类型我们不知道,这里可以用T()匿名对象作为缺省值,让T调用自己的构造去初始化。
注意:匿名对象的生命周期只在那一行,所以要使用const引用匿名对象,目的就是延长匿名对象的生命周期。
测试一下:
void test_vector4()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
v.resize(10, 1);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
}
3.8pop_back()和empty()
当没有数据时,即为空时不能尾删
所以先来实现判空:
bool empty() const
{
return _start == _finish;
}
尾删:
void pop_back()
{
assert(!empty());
_finish--;
}
比较简单就不测试了。
3.9insert()
在pos前插入val,先将pos后元素全部向后移动一格,在将val插入pos位置
void insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
if (_finish == _end_of_storage)
{
reverse(capacity() == 0 ? 4 : capacity() * 2);
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
分别测试一下扩容前和扩容后:
void test_vector5()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
//v.push_back(4);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
vector<int>::iterator pos = find(v.begin(), v.end(), 2);
v.insert(pos, 10);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
}
扩容前:
扩容后:
这里结果出错了,为什么呢?
其实就是因为我们前面提到的迭代器失效问题。
由于扩容之后,pos还是指向旧空间的2,但是我们现在要在新空间的2前面插入10,所以我们应该在扩容后更新pos指针。
void insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reverse(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
现在再测试一下:
现在就没有问题了。
但是需要注意的是,在insert中我们虽然更新了形参pos,但是外面的实参pos并没有改变,形参是实参的临时拷贝,所以形参改变不会影响形参。
那怎么办?引用形参可以吗?这里我们给出解决办法,不推荐引用,可以通过返回值来返回更新之后的pos。
不采用引用形参的原因
- 避免意外修改:
- 如果 insert 函数通过引用形参返回插入位置,这可能会导致意外的修改。因为引用本身可以被重新赋值,函数调用者可能会不小心修改这个引用,从而改变了原本应该表示插入位置的信息。
- 语义不符:
- 从语义角度看, insert 操作主要是向容器中添加元素,重点在于插入操作本身和插入后的元素位置。返回一个表示插入位置的迭代器更符合这个操作的语义,而通过引用形参返回位置不太符合这种直观的理解。引用形参更多地用于在函数内部修改外部变量的值,而 insert 的主要目的不是修改外部传入的表示位置的变量,而是告知调用者新元素的位置。
iterator insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reverse(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
return pos;
}
4.0erase()
同样erase也会有迭代器失效的问题,所以我们也可以和insert一样通过返回值来更新一下pos
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
//挪动数据
iterator end = pos + 1;
while (end < _finish)
{
*(end - 1) = *end;
end++;
}
_finish--;
return pos;
}
测试一下:
void test_vector6()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
vector<int>::iterator pos = find(v.begin(), v.end(), 2);
v.erase(pos);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
}
如果我们要删除所有的偶数呢?
void test_vector6()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0) it = v.erase(it);
else it++;
}
/*vector<int>::iterator pos = find(v.begin(), v.end(), 2);
v.erase(pos);*/
for (int e : v)
{
cout << e << ' ';
}
cout << endl;
}
4.1拷贝构造
拷贝构造可以使用传统写法,也可以使用现代写法,这里我们直接干
vector(const vector<T>& v)
{
reverse(v.size());
for (auto& e : v)
{
push_back(e);
}
}
测试一下:
void test_vector7()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (int e : v1)
{
cout << e << ' ';
}
cout << endl;
vector<int> v2 = v1;
for (int e : v1)
{
cout << e << ' ';
}
cout << endl;
}
4.2赋值重载
赋值重载我们用现代写法来写:
void Swap(vector<T>& v)
{
swap(_start, v._start);
swap(_finish, v._finish);
swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
Swap(v);
return *this;
}
测试一下:
void test_vector8()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
cout << "v1:";
for (int e : v1)
{
cout << e << ' ';
}
cout << endl;
vector<int> v2;
v2.push_back(10);
v2.push_back(20);
v2.push_back(30);
cout << "v2赋值前:";
for (int e : v2)
{
cout << e << ' ';
}
cout << endl;
v2 = v1;
cout << "v2赋值后:";
for (int e : v2)
{
cout << e << ' ';
}
cout << endl;
}
4.3迭代器区间构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
一个类模板的成员函数,还可以是一个函数模板
这里InputIterator就是函数模板,可以自动实例化出不同类型的迭代器。
来测试一下:
void test_vector9()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
cout << "v1:";
for (int e : v1)
{
cout << e << ' ';
}
cout << endl;
vector<int> v2(v1.begin(), v1.end());
cout << "v2:";
for (int e : v2)
{
cout << e << ' ';
}
cout << endl;
}
OK,这次vector的认识就到这里了。
欢迎指正和补充。