目录
1. vector的介绍
vector是表示可变大小数组的序列容器
就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问(支持随机访问),和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
1.1 vector构造函数的定义
这里先介绍5个重要的构造函数给大家看看,现在大家了解一下就可以了,后续会一一实现的。
构造函数声明 | 接口声明 |
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x); (重点) | 拷贝构造 |
template <class InputIterator> vector (InputIterator first, InputIterator last) |
使用迭代器进行初始化构造 |
vector(initializer_list<T> il) | 使用花括号进行初始化构造 |
1.2 vector iterator的使用
因为vector是类模板,所以iterator就是模板参数类型的指针
然后vector有三个成员变量
这三个成员变量的作用如下:
1.3 vector的空间增长问题
容量问题(这里介绍的是成员函数) | 接口说明 |
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize(重点) | 改变vector的size |
reserve | 改变vector的capacity |
-
capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
-
reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以提前将空间设置足够用于缓解vector增容的代价缺陷问题。(在插入的数据的时候避免了边插入边扩容导致效率低下的问题了)
-
resize在开空间的同时还会进行初始化,影响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';
}
}
}
1.4 vector的增删查改
vector的增删查改 | 接口说明 |
push_back(重点) | 尾插 |
pop_back(重点) | 尾删 |
find | 查找(注意这个是算法模块实现,不是vector的成员接口) |
insert | 在position之前插入val |
erase | 删除position位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] | 想数组一样访问 |
2. vector代码的实现
2.1 vector扩容
在实现扩容前,我们首先要把vector最基本的成员函数搭建好,以便于后面我们的使用
成员函数包括:begin(),end(),size(),capacity()
namespace bit
{
template <class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
const_iterator begin() const
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator end() const
{
return _finish;
}
//这里获取的是容量大小
size_t capacity()
{
return _end_of_storage - _start;
}
//指针-指针得到的是指针之间的元素个数
//这里获取的是数据个数
size_t size()
{
return _finish - _start;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
实现完最基础的成员函数后,就要实现扩容了
#pragma once
namespace bit
{
template <class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
const_iterator begin() const
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator end() const
{
return _finish;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
size_t size()
{
return _finish - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
iterator tmp = new T[n];
size_t oldsize = size();
//不为空才需要拷贝一份代码,并且释放原本的旧空间
if (_start)
{
//这种是一个字节一个字节进行拷贝
//memcpy(tmp, _start, sizeof(T) * size());
//下面这个方法也是以一个字节一个字节进行进行拷贝,但这种写法巨坑
//一共要拷贝 sizeof(T) * size()个字符个数
strncpy((char*)tmp, (char*)_start, sizeof(T) * size());
delete[] _start;
}
_start = tmp;
//此时size()里面的_start已经不是指向原先数组的起始地址
//而是指向tmp所指向的数组的起始地址,所以
//_finish = _start + _finish - _start;
//求得:_finish = _finish 因此_finish是不变的
//因此finish迭代器失效,因为指向的旧空间,并且旧空间等等会被释放
//_finish = _start + size();
//解决办法1:
//把_finish放到_start = tmp前面
//_finish = tmp + size();
//方法1不推荐哈,因为有点依赖顺序,最好方法是提前记录好size()
// 方法2
_finish = tmp + oldsize;
_end_of_storage = _start + n;
}
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
上面代码有非常多的细节需要说明
首先类里面定义的函数是没有顺序可言的,就算我们把reserve扩容函数写在size()函数上面,我们这里依旧是可以调用的!!!
这里我们需要注意的一个点就是如果_start原本指向就是为空,那就没有必要走拷贝步骤,更没必要对_start实施delete[]
重点:
我这边写了两个拷贝空间数据的函数,memcpy(tmp, _start, sizeof(T) * size())和strncpy(tmp, _start, sizeof(T) * size())。大家觉得哪个比较好,因该留哪个。
结论:
strncpy(tmp, _start, sizeof(T) * size()) 这个写法用在这里巨坑
这里我就先补充说明,就是一开始如果数组是没有空间的,那扩容一开始就会分配4个大小数组空间,所以这里一开始我们插入4个元素的时候是正常的,那我们在插入一个元素后就会产生扩容,那扩容我们本意是想把旧空间的元素拷贝到新空间中,在尾插一个5,可最终我们输出的时候不是1,2,3,4,5而是1,0,0,0,5
很明显,这是扩容出现的问题,问题就出在我们拷贝旧空间时使用错了方法
如果我们不知道strncpy的使用规则,我们可以看下面这张图
这里我们假设T是int类型,那我们就要从旧空间拷贝16个字节数据到新空间去
而strncpy有个问题就是如果我们提前遇到了‘\0’,但是我们还没有拷贝完,那就要在目的空间继续添加'\0',直到拷贝完(num等于0)
因为一开始拷贝元素是1,1在小端存储的时候是01 00 00 00,所以一开始先把01拷贝到新空间去,而接着遇到了00,00就是0,'\0'的ASCII也是0,由于提前遇到了'\0'了,而我们此时还没有拷贝完,所以就要继续追加num个'\0'
所以我说用strncpy这种拷贝方法巨坑,那为什么我一开始会用这种拷贝方法的原因是string自定义实现的时候就是用strncpy的拷贝方式,所以我把在string拷贝旧空间的方法移接到vector的时候就有问题(这个问题真的困扰我有一段时间)
解决了上面的问题,接着面临的问题就是扩容后迭代器失效