vector的介绍
- vector是一个可以改变大小的序列容器。
- 和数组一样,vector使用连续的存储位置来存储其元素,这也就意味着可以通过使用下标对vector进行访问,并且效率与数组相同。但与数组不同的是,它们的大小可以动态变化,其存储由容器自动处理。
- 本质上,vecto使用动态分配的数组来存储其元素。当插入新元素时,这个数组可能需要重新分配以增加大小,这意味着分配一个新的数组并将所有元素移动到其中。从时间复杂度来看,这是一项代价很高的任务,因此,vector不会在每次向容器中添加元素时重新分配。
- vector的分配空间的方式:vector会分配一些额外的存储空间来适应可能的增长,因此容器的实际容量会比实际需要存储的空间更大。不同的库可以实施不同的增长策略来平衡内存使用和重新分配,但在任何情况下,重新分配都应该只在大小呈对数增长的区间内发生,以便在向量末尾插入单个元素时是在常数时间的复杂度内完成的。
- 因此,与数组相比,vector 消耗更多的内存,以换取管理存储和以高效方式动态增长的能力。
- 与其他动态序列容器 (deque、list 和 forward_lists) 相比,vector 访问其元素非常高效 (就像数组一样), 并且相对高效地在其末端添加或删除元素。对于涉及在末端以外位置插入或删除元素的操作,它们的性能比其他容器差。
记下来,我们根据https://legacy.cplusplus.com/文档中接口的介绍,进而实现一个自己编写的vector容器。
vector的定义:
template<class T>
class vector
{
private:
T* _start;
T* _finish;
T* _end_of_storage;
};
这就是vector的定义,_start表示第一个元素的位置,_finish表示最后一个元素的下一个位置,_end_of_storage代表开辟空间的下一个位置 ,就是为了尽可能去减少开辟空间的资源消耗,所以所开辟的空间会比实际的空间会大一点。
vector的构造:
allocator_type()
是 C++ STL 中容器类的一个成员函数。分配器负责管理容器的内存分配和释放操作。默认的分配器在大多数情况下已经够用了,所以我们不需要自己动手写一个,用库默认提供的就好。
vector()
:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
{}
vector(const vector<T>& x)
{
int len = x._finish - x._start;
int capacity = x._end_of_storage - x._start;
_start = new T[len];
//memcpy(_start, x._start, size() * sizeof(T));
for (int i = 0; i < len; i++)
{
_start[i] = x._start[i];
}
_finish = _start + len;
_end_of_storage = _start + capacity;
}
vector(size_t n, const T& val = T())
{
_start = new T[n];
for (size_t i = 0; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
_end_of_storage = _start + n;
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
InputIterator cur = first;
while (cur != last)
{
push_back(*cur);
cur++;
}
}
vector的iterator
begin是第一个元素的位置,而end是最后一个位置的下一个位置。 形成左闭右开的结构,STL中的序列容器大多都是左闭右开的结构。
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;
}
vector的容量函数:
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
bool empty()
{
return _start == _finish;
}
void resize(size_t n, T val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
size_t len = _finish - _start;
for (size_t i = len; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
}
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t len = size();
T* temp = new T[n];
if (_start)
{
//memcpy(temp, _start, size() * sizeof(T));
for (int i = 0; i < len; i++)
{
temp[i] = _start[i];
}
delete[] _start;
}
_start = temp;
_finish = _start + len;
_end_of_storage = _start + n;
}
}
vector的增删查改:
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
reserve(size() == 0 ? 4 : capacity() * 2);
}
*_finish = val;
_finish++;
}
void pop_back()
{
_finish--;
}
iterator insert(iterator position, const T& val)
{
if (_finish == _end_of_storage)
{
int len = position - _start;
reserve(capacity() * 2);
position = _start + len;
}
size_t end = size();
while (_start + end > position)
{
_start[end] = _start[end - 1];
end--;
}
_start[end] = val;
_finish++;
return position;
}
iterator erase(iterator position)
{
iterator cur = position;
for (int i = 0; cur < _finish; cur++)
{
*cur = *(cur + 1);
}
_finish--;
return position;
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
T& operator[](size_t n)
{
assert(n < size());
return _start[n];
}
vector空间增长:
capacity的代码在vs和g++下分别运行就可以发现,vs下capacity基本是按照1.5倍增长的,g++是按2倍增长的。vs是PJ版本下的STL,g++是SGI版本下的STL。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v;
size_t sz = v.capacity();
for (int i = 0; i < 100; i++)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
std::cout << "capacity change:" << sz << std::endl;
}
}
//buluo::test();
return 0;
}
VS下:
g++下:
vector迭代器失效的问题。
迭代器的主要作用就是让算法可以不用关心底层的数据结构,其底层就是一个指针或者对指针进行了封装,vector的迭代器就是原生指针T*。因此迭代器失效,实际就是迭代器底层对应的指针所指向的空间被销毁了,而使用一块已经被释放的空间,,如果继续使用已经失效的迭代器,程序就很有可能会崩溃。现在我们通过测试代码,来看看迭代器失效的问题。
- 扩容导致底层空间改变,造成迭代器失效。
void test()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int>::iterator it = v.begin() + 2;
v.insert(it, 30);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v.insert(v.begin(), 30);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
大家可以看到这段代码貌似没有问题,我让它插入的位置都是正确的,那么我现在就增加一个东西。
void test()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
v.push_back(7);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
vector<int>::iterator it = v.begin() + 2;
v.insert(it, 30);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v.insert(v.begin(), 30);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
仅仅是在刚开始插入的时候多插入了一个7
明显可以看到,程序崩溃了,正常退出时返回值应该为0的,但是这次崩溃了,这就是迭代器失效的问题,我们通过调试窗口看一看。
通过两张图对比,我们可以发现,当我们进行插入的时候,空间不够了,需要扩容,扩容之后,明显可以看到position位置已经不是之前的位置了,而此时我们仍然在一块不合法的位置进行插入,这就导致程序崩溃了。
那我们应该怎么解决这个问题,当扩容后进行pos的重新定位就可以了,现在我们进行改变insert函数。
void insert(iterator position, const T& val)
{
if (_finish == _end_of_storage)
{
int len = position - _start;
reserve(capacity() * 2);
position = _start + len;
}
size_t end = size();
while (_start + end > position)
{
_start[end] = _start[end - 1];
end--;
}
_start[end] = val;
_finish++;
}
- 指定位置进行删除时
void test()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
auto pos = std::find(v.begin(), v.end(), 3);
v.erase(pos);
cout << *pos << endl;
}
可以明显看到上面那段代码的行为是越界的,erase在删除position位置之后元素是会往前移动的,并不会导致底层空间的改变,理论上讲迭代器是不会失效的,但是上面这段代码中,删除的是最后一个元素,那么迭代器就失效了,这时候如果继续访问,就会造成越界的问题。而在VS下对迭代器的检查是很严格的,一般都会直接报错。我们可以换成STL库中的vector看一看。
在Linux中,g++编译器对迭代器的失效检测的并不严格,处理方式没有vs下那么极端,VS只要失效就报错了,而g++可以运行,但是结果可能会出错,我们进行同一段代码的测试:
int main()
{
vector<int> v{1, 2, 3, 4, 5};
for (size_t i = 0; i < v.size(); ++i)
cout << v[i] << " ";
cout << endl;
auto it = v.begin();
cout << "扩容之前,vector的容量为: " << v.capacity() << endl;
v.reserve(100);
cout << "扩容之后,vector的容量为: " << v.capacity() << endl;
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
int main()
{
vector<int> v{1, 2, 3, 4, 5};
auto it = find(v.begin(), v.end(), 3);
v.erase(it);
cout << *it << endl;
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
而解决这个迭代器失效的问题就是在再次使用之前,对迭代器进行重新赋值即可。我们就拿上面的代码举例。
可以看到在接收返回值后,VS下也可以成功运行了。
使用memcpy拷贝的问题
vector(const vector<T>& x)
{
int len = x._finish - x._start;
int capacity = x._end_of_storage - x._start;
_start = new T[len];
memcpy(_start, x._start, size() * sizeof(T));
//for (int i = 0; i < len; i++)
//{
// _start[i] = x._start[i];
//}
_finish = _start + len;
_end_of_storage = _start + capacity;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t len = size();
T* temp = new T[n];
if (_start)
{
memcpy(temp, _start, size() * sizeof(T));
//for (int i = 0; i < len; i++)
//{
// temp[i] = _start[i];
//}
delete[] _start;
}
_start = temp;
_finish = _start + len;
_end_of_storage = _start + n;
}
}
大家自己写这两个功能模块时,大家可能会想,为什么还要一个一个赋值那么麻烦,C语言的库中不是提供了memcpy的函数么,我们直接拷贝一下不久可以了么,简单高效。那么我们来测试一下下面这段代码,看看memcpy函数会对这两个函数会有什么影响。
void test_vector6()
{
vector<string> v;
v.push_back("111111111111111111");
v.push_back("222222222222222222");
v.push_back("333333333333333333");
v.push_back("444444444444444444");
v.push_back("555555555555555555");
for (auto& e : v)
{
cout << e << " ";
}
cout << endl;
vector<string> v1(v);
for (auto& e : v1)
{
cout << e << " ";
}
cout << endl;
}
可以明显看到,程序崩溃了,这是为什么呢
memcpy是内存的二进制格式的拷贝,将一段内存空间原封不动的拷贝到另一块空间中,如果是自定义类型的话,memcpy即高效又不会出错,但是如果是自定义类型的元素,且自定义类型的元素涉及资源管理时,这时候就会出错,因为memcpy是浅拷贝。
这样就可以宏观的看到,当插入“555555555555”时,我们需要进行空间的扩增,这个时候将它们按值拷贝之后,之前的空间会被释放,delete[] _start,这时对于自定义类型会去调用自己的析构函数,这样就会导致新创建的空间指向了一片已经被析构的空间,这时候导致程序错误,所以我们需要进行深拷贝,这样才是正解。
结论:如果对象涉及资源管理时 ,我们需要进行深拷贝,浅拷贝容易造成内存泄漏甚至程序崩溃。
模板参数匹配的问题
vector(size_t n, const T& val = T())
{
_start = new T[n];
for (size_t i = 0; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
_end_of_storage = _start + n;
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
InputIterator cur = first;
while (cur != last)
{
push_back(*cur);
cur++;
}
}
int test()
{
vector<int> v2(10, 1);
return 0;
}
就这么一个简单的代码,为什么会报错呢,这不是调用构造函数就可以了吗?问题的原因就是因为 10和1都是int类型的整数,而我们的vector(size_t n, const T& val = T())第一个参数是size_t,如果要使用该函数的话,第一个参数类型必须是size_t 类型的,这就意味着必须从int转为size_t,但是编译器在编译的时候发现了两个都是int,在匹配时他会匹配与自己最相适应的,所以它就直接匹配到模板的那个构造函数去了,所以导致编译错误,所以这时候我们需要重载一个第一个参数为int的vector(int n, const T& val = T())就可以了。
vector(int n, const T& val = T())
{
_start = new T[n];
for (int i = 0; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
_end_of_storage = _start + n;
}
vector的模拟实现
template<class T>
class vector
{
public:
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;
}
vector()
:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
{}
vector(const vector<T>& x)
{
int len = x._finish - x._start;
int capacity = x._end_of_storage - x._start;
_start = new T[len];
//memcpy(_start, x._start, size() * sizeof(T));
for (int i = 0; i < len; i++)
{
_start[i] = x._start[i];
}
_finish = _start + len;
_end_of_storage = _start + capacity;
}
vector(size_t n, const T& val = T())
{
_start = new T[n];
for (size_t i = 0; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
_end_of_storage = _start + n;
}
vector(int n, const T& val = T())
{
_start = new T[n];
for (int i = 0; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
_end_of_storage = _start + n;
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
InputIterator cur = first;
while (cur != last)
{
push_back(*cur);
cur++;
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
bool empty()
{
return _start == _finish;
}
void resize(size_t n, T val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
size_t len = _finish - _start;
for (size_t i = len; i < n; i++)
{
_start[i] = val;
}
_finish = _start + n;
}
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t len = size();
T* temp = new T[n];
if (_start)
{
//memcpy(temp, _start, size() * sizeof(T));
for (int i = 0; i < len; i++)
{
temp[i] = _start[i];
}
delete[] _start;
}
_start = temp;
_finish = _start + len;
_end_of_storage = _start + n;
}
}
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
reserve(size() == 0 ? 4 : capacity() * 2);
}
*_finish = val;
_finish++;
}
void pop_back()
{
_finish--;
}
iterator insert(iterator position, const T& val)
{
if (_finish == _end_of_storage)
{
int len = position - _start;
reserve(capacity() * 2);
position = _start + len;
}
size_t end = size();
while (_start + end > position)
{
_start[end] = _start[end - 1];
end--;
}
_start[end] = val;
_finish++;
return position;
}
iterator erase(iterator position)
{
iterator cur = position;
for (int i = 0; cur < _finish; cur++)
{
*cur = *(cur + 1);
//*(position + i) = *(position + i + 1);
//_start[position + i] = _start[position + i + 1];
}
_finish--;
return position;
}
T& operator[](size_t n)
{
assert(n < size());
return _start[n];
}
private:
T* _start;
T* _finish;
T* _end_of_storage;
};