vector是一个动态顺序表,可以理解为是一个数组不过该数组的大小是可变的,当数组的容量满了时,再插入数据vector会自动扩容。和string不同,string是一个类模板的实例,但vector是一个类模板,在初始化时需要我们传入模板参数。
void test1()
{
// 创建一个存储int对象的vector
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
}
除了上面的创建vector的方式,还有两种方式创建vector
void test2()
{
string str = "hello";
// 使用迭代器创建vector,注意迭代器指向的数据类型要和vecotr存储的数据类型相同
vector<char> v1(str.begin(), str.end());
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
// 初始化5个10的空间
vector<int> v2(5, 10);
for (auto e : v2)
{
cout << e << ' ';
}
cout << endl;
}
vector有三种遍历方式(和string一样这里不再赘述)
void test1()
{
// 创建一个存储int对象的vector
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
// vector的遍历:范围for
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
// 下标遍历
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << ' ';
}
cout << endl;
// 迭代器
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
}
vector的查找和删除,vector并没有实现find函数,但在algorithm中实现了一个find函数
传入要查找数据的迭代器区间,以及要查找的数据,该函数会返回一个指向查找到的数据的迭代器,如何我们可以通过vector的erase函数删除数据
void test3()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout << e << ' ';
}
cout << endl;
// 通过find查找数据,再进行删除
auto it = find(v.begin(), v.end(), 3);
v.erase(it);
for (auto e : v)
{
cout << e << ' ';
}
cout << endl;
}
vector的插入需要传入一个迭代器,将数据插入到迭代器的位置,其他数据往后移动
void test4()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
for (auto e : v)
{
cout << e << ' ';
}
cout << endl;
// 头插数据到vector中
v.insert(v.begin(), -1);
v.insert(v.begin(), -2);
v.insert(v.begin(), -3);
for (auto e : v)
{
cout << e << ' ';
}
cout << endl;
}
这次的模拟实现不再使用size,capacity来表示数组的大小和容量,而使用_start指针指向数组的第一个元素,_end指向数组最后一个存储数据的元素的下一个位置,_endofstrore指向数组最后元素的下一个位置。之所以用指针实现vector是因为STL库中也是这样实现,使用指针的方式更贴合“模拟”二字。
先给出vector的结构声明,接着一个个的实现声明中的函数
namespace myVector
{
template <class T>
class vector
{
typedef T* iterator;
typedef const T* const_iterator;
public:
void swap(const vector<T> v);
// 构造和析构
vector();
template <class InputIterator>
vector(InputIterator first, InputIterator last);
vector(int n, T val); // 用n个T构造vector
vector(const vector<T>& v);
~vector();
vector<T>& operator=(vector<T> v);
// 迭代器
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin() const { return _start; }
const_iterator end() const { return _finish; }
// 容量
size_t size() const;
size_t capacity() const;
void reserve(size_t n);
void resize(size_t n, T val = T());
// 修改
void push_back(const T& val);
void pop_back();
void insert(iterator pos, const T& val);
void erase(iterator pos);
// 下标访问
T& operator[](size_t n);
const T& operator[](size_t n) const;
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
vector的迭代器就是指针,这些函数的作用就是返回指向vector开始或结束处的指针,逻辑简单,代码就写在类里了。
文档中声明vector的构造有三种:1.无参,2.用迭代器区间的数据构造,3.用n个val值初始化。最后一个是拷贝构造。
无参的构造函数就是将vector的三个原生指针置空
template <class T>
myVector::vector<T>::vector()
{
_start = nullptr;
_finish = nullptr;
_endofstore = nullptr;
}
用迭代器first到迭代器last之间的数据构造并初始化一个vector,这里要用first迭代器遍历区间,然后调用push_back将遍历得到的数据尾插到vector中,push_back后面会实现。而迭代器类型肯定是不一样的,有指向内置类型的迭代器,指向自定义类型的迭代器,这就需要使用模板函数,再使用一个模板参数,编译器会根据实参的类型生成对应的函数
template <class T>
template <class InputIterator>
myVector::vector<T>::vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
begin++;
}
}
用n个val构造并初始化vector时,这个val值可以不给,也就是val是默认参数,当使用者不给形参时,初始化vector的val值就是默认的。但这样就有一个问题,val的默认值应该是什么?当val是自定义类型对象时,使用默认构造函数构造的对象就是val的默认值(形参中写:T val = T() ),但val是内置类型对象如int,char时,应该怎样操作?其实C++对于内置类型也支持默认构造函数,比如int i = 0,这是创建int对象并初始化成0,也能写成int i = int(),这里的意思就是创建int对象并调用int的默认构造函数初始化对象。
可以看到vs下int的默认构造是将int赋值为0。综上,得出结论:C++中的内置类型如int,char可以看成一个类,所以int类有默认构造函数,而C++这样做的原因是为了更好的支持模板
而实现就很简单了,for循环将n个val尾插到vector中
template <class T>
myVector::vector<T>::vector(int n, T val = T())
{
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
这里还要注意一个问题,int n最好不要写成size_t n,如果这样,当想用5个5初始化vector时程序会报错
大概就是非法访问内存,原因:vector< int> v(5, 5)中的两个5都是int类型,编译器在编译时不会将第一个5隐式类型转换成size_t,因为vector还有一个构造函数
由于该构造的两个参数类型相同,两个5又都是int类型,编译器会生成该模板对应的函数,再调用它,而该函数需要对指针解引用,传个5进去,对5解引用当然是非法访问了。所以最好将n的类型写为int
vector的拷贝构造,用传入的vector对象v,构造函数出tmp对象,再将tmp与this指向的vector交换。
// 两个vector的交换,只要交换它们的三个原生指针,这里的交换调用std的交换
template <class T>
myVector::vector<T>::swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstore, v.endofstore);
}
// 用v拷贝构造
template <class T>
myVector::vector<T>::vector(const vector<T>& v)
{
_start = nullptr;
_finish = nullptr;
_endofstore = nullptr;
vector tmp(v.begin(), v.end()); // 用v构造tmp对象
swap(tmp); // 此时的tmp是v的复制,将它交换
}
至于拷贝构造前为什么要将当前的vector的原生指针置空,因为如果不置空三个指针就是随机值,tmp是一个局部变量,出作用域销毁,因为交换此时tmp的三个原生指针指向随机空间,销毁tmp前要是否tmp的动态空间,而释放随机空间非法,所以要先将当前的原生指针置空。
析构函数就是将vector的动态空间释放
template <class T>
myVector::vector<T>::~vector()
{
delete[] _start; // 因为_start指向的空间是连续的,所以用delete[]
}
最后一个是vector的赋值重载,如果不写=的重载,编译器默认生成的重载只是一个浅拷贝,而vector涉及动态内存的申请,所以需要自己实现该重载,完成深拷贝。不把函数参数写成引用,在函数实例化形参时会去调用vector的拷贝构造函数,这时的形参就是一份拷贝,之后只要将当前的vector与形参交换。
template <class T>
myVector::vector<T>& myVector::vector<T>::operator=(vector<T> v)
{
swap(v);
return *this; // 返回当前的vector为了支持连续赋值
}
(vector的构造,如果无参:将三个指针置空,如果参数是两个迭代器:用迭代器初始化空间,当first迭代器不等于last迭代器,将first迭代器指向的数据push_back。push_back将数据尾插到vector,当容量满时调用reserve函数扩容,reserve:new n个T(存储的数据类型)空间,使tmp指向该空间,再将原来的数据拷贝到tmp指向的空间,注意这里的拷贝不是简单的浅拷贝,如果存储的数据是string,vector需要管理内存的类型,浅拷贝会使程序崩溃,所以不能直接用memcpy简单的拷贝数据,而是要用=,将数据一个一个的拷贝进去。而=如果不重载,也是一个浅拷贝,所以需要对=进行重载,重载=时使用现代写法,参数是vector但不是引用,这里会复用参数是vector的拷贝构造函数,参数是vector的拷贝构造函数会复用参数是两个迭代器的构造函数,又复用push_back,如果push_back的数据不是内置类型,又会调用=,直到push_back的数据类型是内置类型)
容量有四个接口,size()返回当前vector存储数据的个数,capacity()返回当前vector最大能存储数据的个数,这两个接口简单直接贴代码
template <class T>
size_t myVector::vector<T>::size() const
{
return _finish - _start; // 指针相减得到两指针之间的数据个数
}
template <class T>
size_t myVector::vector<T>::capacity() const
{
return _endofstore - _start;
}
reserve()设置vector的容量,如果参数n大于当前的capacity,进行扩容,小于或等于,什么都不做。扩容时,先申请n个T的空间,再将原来空间上的数据深拷贝进新开辟的空间中(凡是有申请资源的类,都要注意深拷贝的问题),释放原来空间。
reserve还涉及迭代器失效问题,什么是迭代器失效?两个方面:1.迭代器指向空间被释放,形成野指针但你没发现。2.迭代器指向的位置意义变了。扩容时,在拷贝完原来数据后需要释放原来的空间,这时的迭代器仍然是指向原来的空间(包括原生指针),所以需要对迭代器进行更新,_start指向新开辟的空间,_finish和_endofstore却指向原来的空间,_endofstore只要指向_start后的n个位置就行,但_finish要指向_start后size个位置,而size函数要用_finish - _start,这时的_finish已经是个失效的迭代器,所以不能调用size函数,需要先记录原来的size。
template <class T>
void myVector::vector<T>::reserve(size_t n)
{
if (n > capacity())
{
int sz = size();
T* tmp = new T[n];
if(_start) // 为空不进行拷贝
{
for (int i = 0; i < size(); i++)
{
tmp[i] = _start[i]; // 复用=的重载,如果vector存储的数据需要深拷贝,将深拷贝工作交给=的重载,前提是该类型=的重载实现了深拷贝,当然如果没有实现这里会出问题,但这不是我们的事
}
delete[] _start; // 拷贝完别忘记释放
}
_start = tmp;
_finish = _start + sz;
_endofstore = _start + n;
}
}
(记录一个在模拟实现中想到的问题,在实现reserve时,最开始没有进行vector为空并不进行拷贝的if条件判断,之后便思考是否有必要进行该判断,_start为空,说明其他两个指针也为空,在for循环的条件判断时调用size函数,以此引出一个疑问,**两个空指针是否能相减?**便敲了代码验证
可以看到,两个指针相减的前提是指针类型要统一,而nullptr作为一个空指针是没有类型的。但对于一个空的vector调用size却不会报错,并且还打印出了0,思索了一会才明白这只是C语言的语法基础。两个指针相减得到的是它们之间数据个数,这是怎么做到的?大概就是地址相减后再除以对应类型的字节数得到最后的数据个数,但nullptr没有数据类型,想要跑通这行代码只要用强制类型转换,所以不用判断vector是否为空也行,之后的delete对于空指针不释放)
题外话结束,resize,对于n大于size并且大于capacity的情况,resize就是reserve+初始化空间,开辟空间后,尾插数据,改变size。对于n只大于size小于capacity的情况,尾插数据使size到n。对于n小于size的情况,修改_finish指针达到删除数据的效果
template <class T>
void myVector::vector<T>::resize(size_t n, T val = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size())
{
while (_finish != _start + n)
{
push_back(val);
_finish++;
}
}
else
{
_finish = _start + n;
}
}
clear就是清空vector中的数据,只要将_finish指向_start的位置就行,代码简单直接写在类里。
push_back尾插数据到vector中,插入数据前要检查是否需要扩容,之后解引用_finish,将数据写入,最后++_finish。pop_back就是直接–_finish
template <class T>
void myVector::vector<T>::push_back(const T& val)
{
// 如果容量为0,直接乘2还是0,所以要判断一下
if (_finish == _endofstore)
reserve(capacity() == 0 ? 4 : capacity() * 2);
*_finish = val;
_finish++;
}
template <class T>
void myVector::vector<T>::pop_back()
{
if (size()) // 不为空了再删除
--finish;
}
insert和erase都涉及到迭代器失效问题,在pos位置插入一个数据,如果容器满了,扩容后虽然容器的三个迭代器不会失效,但用户传的迭代器pos会失效:指向原来的空间,所以在扩容要前先计算pos位置与_start的相对位置。之后就是移动数据,插入val值。但还有一个问题,有这样一个场景:在所有偶数前插入一个2,然后我们用it迭代器去遍历整个vector,随着2的插入vector进行了扩容,而it指向的还是原来的空间(虽然vector的三个迭代器没有失效,但用来遍历vector的it失效了),标准库是怎么解决这个问题的?通过返回迭代器,一个指向新插入数据的迭代器,这样我们在每次的插入后更新it,it = insert(… , …),就能保证迭代器不失效
template <class T>
typename myVector::vector<T>::iterator myVector::vector<T>::insert(iterator pos, const T& val)
{
if (_finish == _endofstore)
{
int n = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + n; // 更新pos迭代器
}
// 数据的挪动
auto end = _finish;
while (end > pos) // 挪动数据
{
*end = *(end - 1);
end--;
}
*pos = val; // 插入数据
_finish++;
return pos;
}
(insert的返回值是一个迭代器,在类外写这个函数时,需要在迭代器前加上类名::,表示该迭代器是定义在这个类下的,但这样与类名::静态变量名,这种访问静态变量的方式冲突,所以编译器不知道这里的返回类型是一个类型还是一个静态变量,编译器会报错,需要加上typenmae显式地告诉编译器这是一个类型)
再来看删除,本质上就是移动数据将被删除的数据覆盖,要注意的是erase返回值是指向被删除位置的下一个位置的迭代器,其实就是还是指向被删除的位置,因为后面的数据向前移动了。
template <class T>
typename myVector::vector<T>::iterator myVecotr::vector<T>::erase(iterator pos)
{
if (pos >= begin() && pos < end()) // 判断pos的合法性
{
auto end = pos + 1;
while (end < _finish)
{
*(end - 1) = *end;
end++;
}
_finish--;
}
return pos;
}
通过下标不仅要支持读,还要支持写,所以下标访问函数的返回值是数据的引用
template <class T>
T& myVector::vector<T>::operator[](size_t n)
{
assert(n < size()); // 断言n的合法性
return _start[n];
}
template <class T>
const T& myVector::vector<T>::operator[](size_t n) const
{
assert(n < size());
return _start[n];
}