文章目录
- 1 vector介绍
- 2 实现vector
- 2.1 类的定义
- 2.2 默认成员函数
- 2.2.1 构造函数
- 2.2.2 析构函数
- 2.2.3 拷贝构造
- 2.2.4 赋值重载
- 2.3访问接口
- 2.4 容量接口
- 2.5 修改接口
- 2.5.1 尾插尾删
- 2.5.2 任意位置插入
- 2.5.3 任意位置删除
- 2.6 其他接口
1 vector介绍
1 vector是表示可变大小数组的序列容器。
2 就像数组一样,vector也采用连续存储空间的方式来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组为了增加存储空间需要被重新分配大小。其做法是,分配一个新的数组,然后将全部元素移到这个数组。
2 实现vector
可以看出,vector使用start指向数组的起始位置,finsh指向数组有效元素的下一个位置,end_of_storage指向区间的容量
2.1 类的定义
namespace zbt
{
template <class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;//指向数组的起始位置
iterator _finsh;//指向数组有效元素的下一个位置
iterator _end_of_storage;//指向区间的容量
};
和之前实现string不同的是,把指向元素个数和区间容量的值改为用相应的迭代器实现,但本质是一样的。我们可以看到,vector的迭代器底层实现依旧是指针。
之所以定义为类模板,是因为vector里面不仅可以存int,char,double这样的内置类型,同样也可以存储string,甚至vector这样的自定义类型
2.2 默认成员函数
2.2.1 构造函数
vector()
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
}
无参的构造函数,直接把三个指针初始化为nullptr。
除了使用无参的构造函数,我们看到C++的官方文库还支持使用迭代器区间进行构造的方式
//函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)//给定一段迭代器区间
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);//从头开始将元素一个接一个尾插到vector里面
first++;
}
}
从中可以看出,在类模板里面同样也可以套用函数模板,将此函数实现为模板函数,这样任意类型的迭代器都可以用来构造vector
当然,实现迭代器区间构造的方式前提还需要实现push_back函数
example:
std::string s("hello");
//使用迭代器区间构造v
vector<char>v(s.begin(), s.end());
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
用string的迭代器去构造vector
2.2.2 析构函数
~vector()
{
delete[]_start;//将动态开辟的空间释放掉
_start = _finsh = _end_of_storage = nullptr;
}
2.2.3 拷贝构造
//现代写法
void swap(vector<T>& tmp)
{
::swap(_start, tmp._start);
::swap(_finsh, tmp._finsh);
::swap(_end_of_storage, tmp._end_of_storage);
}
//v2(v3)
vector(const vector<T>& v)
:_start(nullptr)
, _finsh(nullptr)
, _end_of_storage(nullptr)
{
vector<T>tmp(v.begin(), v.end())//利用迭代器区间构造一个tmp,里面存储的值就是对应v的值;
swap(tmp);//将v2和tmp进行交换
}
2.2.4 赋值重载
//v2=v3;
vector<T>& operator=(vector<T> v)
{
//v就是v3的拷贝,然后将v和v2进行交换
swap(v);
return *this;
}
2.3访问接口
访问方式可以是operator[ ]或者迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finsh;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finsh;
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
2.4 容量接口
resize
void resize(size_t n,const T& val = T())
//匿名对象调用默认构造,
{
if (n > capacity())//需要的空间比原有的空间大,先扩容
{
reserve(n);
}
if (n > size())//需要的空间大于有效数据的个数,在原有有效数据的后面插入val
{
while (_finsh != _start + n)
{
*(_finsh) = val;
_finsh++;
}
}
else//需要的空间比原有的空间小,需要删除数据
{
_finsh = _start + n;
}
}
缺省值采用T()的形式,T()调用的是T类型的默认构造函数,初始化出一个匿名对象,得到的是T类型的空值
reserve
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T [n];//动态开辟一块新的空间,大小为n
if (_start)//如果原来的vector有数据,将数据拷贝到新的空间去
{
//memcpy(tmp, _start,sizeof(T)* sz);
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[]_start;
}
//更新成员变量的值
_start = tmp;
_finsh = _start + sz;
_end_of_storage = _start + n;
}
}
需要注意的是,在拷贝数据的时候,我们并没有使用memcpy直接将数据拷贝过去,而是采用一个一个的赋值,这是为什么呢?这里便涉及更深层次的深拷贝问题
如果vector里面存储的是int,double等内置类型,用memcpy进行浅拷贝是完全没有问题的。但是vector里面也可以存储自定义类型的数据,例如string,vector等,这时如果粗暴的将数据进行浅拷贝,那么原来数组中的数据和和此时新拷贝的数据便会指向同一块空间,析构函数对同一块空间释放两次,程序便会崩溃
所以在拷贝数据的时候,应使用深拷贝。这里我们采用的是赋值重载,即一个一个赋值。
2.5 修改接口
2.5.1 尾插尾删
void push_back(const T& x)//尾插
{
if (_finsh == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);//满了就扩容
}
*(_finsh) = x;
_finsh++;
}
void pop_back()//尾删
{
assert(_finsh > _start);
_finsh--;
}
2.5.2 任意位置插入
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finsh);
if (_finsh == _end_of_storage)//满了扩容
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
//扩容完之后,_start的地址会发生变化,所以要更新pos地址
pos = _start + len;
}
iterator end = _finsh - 1;
while (end >= pos)//[pos~_finsh)位置的元素后移一位
{
*(end+1) =*end ;
end--;
}
*pos = x;
_finsh++;
return pos;
}
注意:
① 因为扩容会重新开辟一块空间,_start会指向一块新的空间,但pos还是指向之前的位置,没有更新,这时迭代器就会失效,所以在扩容之后要更新pos的值,指向新的位置。
② insert函数内的iterator失效问题解决了,但是函数外的pos并没有改变,仍然指向之前的位置,所以最后要将更新后的pos值作为返回值返回。
2.5.3 任意位置删除
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finsh);
iterator begin = pos + 1;
while (begin < _finsh)//pos位置之后的元素统一向前移动
{
*(begin - 1) = *begin;
begin++;
}
_finsh--;
return pos;
}
任意位置删除是否有迭代器失效的问题呢?
答案是有的,举个栗子
删除数组元素中的偶数
第一种写法
vector<int>v3;
v3.push_back(1);
v3.push_back(2);
v3.push_back(4);
v3.push_back(3);
v3.push_back(4);
v3.push_back(5);
vector<int> ::iterator it = v3.begin();
while (it != v3.end())
{
if (*it % 2 == 0)
{
v3.erase(it);
}
it++;
}
for (auto e : v3)
{
cout << e << " ";
}
cout << endl;
很显然,答案错误,偶数并没有删除完,这种写法是有bug的
在删除元素2之后,由于2后面的元素前移,所以此时pos指向的是4,pos++刚好越过了4,所以没有删除掉4这个元素。此时迭代器便失效
第二种写法
vector<int>v3;
v3.push_back(1);
v3.push_back(2);
v3.push_back(4);
v3.push_back(3);
v3.push_back(4);
v3.push_back(5);
vector<int> ::iterator it = v3.begin();
while (it != v3.end())
{
if (*it% 2 == 0)
{
it = v3.erase(it);
}
else
{
it++;
}
}
for (auto e : v3)
{
cout << e << " ";
}
cout << endl;
答案正确,在删除完元素后返回当前的pos,保证pos的位置正确(有些STL版本可能会缩容,导致pos为野指针),并且删除之后不能++pos的值。
综上所述,insert/erase pos位置的值后不要直接访问pos,因为此时迭代器已经失效。
2.6 其他接口
size_t capacity()const//返回容量大小
{
return _end_of_storage - _start;
}
size_t size()const//返回有效元素的个数
{
return _finsh - _start;
}