目录
[四:vector 增删查改接口](#四:vector 增删查改接口)
vector其实就是我们数据结构中的顺序表。
一:vector的构造函数
(1)无参构造:即默认构造。vector构造一个没有元素的空容器。
cpp
// 走默认构造
vector<char> vc;
(2)填充构造函数:构造一个包含 n 个元素的容器。每个元素都是 val 的副本。
cpp
// 默认初始化为0
vector<int> v1(10);
// 初始化为1
vector<int> v2(10, 1);
(3)拷贝构造函数:构造一个容器,其中包含与范围 [first,last] 一样多的元素,每个元素都按相同的顺序从该区域中的相应元素构造。
cpp
// 拷贝构造
vector<int> v3(v2);
(4)使用迭代器进行初始化的构造:构造一个容器,其中包含与范围 [first,last] 一样多的元素,每个元素都按相同的顺序从该区域中的相应元素构造。
cpp
vector<int> v1(10);
// 使用迭代器进行初始化
vector<int> v3(v1.begin(), v1.end()-3);
二:vector的迭代器
同其他的string容器一样,begin和end分别是容器的第一个位置和最后一个位置,反向迭代器则相反。const迭代器和非const迭代器和string的也是一样的C++_STL_string使用和模拟实现-CSDN博客
这里可以看一下。
cpp
// 初始化为一个定值
vector<int> v2(10, 1);
// 迭代器使用
//vector<int>::iterator it;
//vector<int>::const_iterator it;
// 反向迭代器
vector<int>::reverse_iterator it;
//it = v2.begin();
it = v2.rbegin();
//while (it != v2.end())
while (it != v2.rend())
{
//cout << ++(*it) << " ";
cout << *it << " ";
it++;
}
三vector的空间增长问题
(1)size():返回元素的实际个数
(2)capacity():容器容量的大小
(3)resize() :修改实际容器元素个数
(4)reserve():改变capacity的大小
(5)empty():判断容器是否为空
注意:在g++和vs中capacity在扩容的时候可能不一样,有的是1.5倍扩容,有的是2倍扩容
具体我们来看下这几个接口的情况:
cpp
// 测试vector 空间增长问题接口
void test02()
{
vector<int> v2(10, 1);
cout << v2.size() << endl;
cout << v2.capacity() << endl;
cout << v2.empty() << endl;
// resize如果n大于size的话会将size的元素进行初始化
v2.resize(40);
// 这里改变capacity的为13并不会缩容
v2.reserve(13);
cout << v2.capacity() << endl;
for (auto& x : v2)
{
cout << x << " ";
}
}
四:vector 增删查改接口
(1)push_back:在容器末尾插入一个值
cpp
void push_back (const value_type& val);
(2)pop_back:删除容器末尾的一个值
cpp
void pop_back();
(3)find:在范围内查找值
返回范围中比较等于 的第一个元素的迭代器。如果未找到此类元素,则函数返回 。
cpp
template <class InputIterator, class T> InputIterator find (InputIterator first, InputIterator last, const T& val);
(4)insert:通过在指定位置的 元素之前插入新元素来扩展向量,从而有效地增加容器大小(增加插入的元素数)。
cpp
single element (1)
iterator insert (iterator position, const value_type& val);
fill (2)
void insert (iterator position, size_type n, const value_type& val);
range (3)
template <class InputIterator> void insert (iterator position, InputIterator first, InputIterator last);
(5)erase:从向量中删除单个元素 (位置) 或一系列元素 ([first,last))
cpp
iterator erase (iterator position);iterator erase (iterator first, iterator last);
(6)swap:交换向量的数据空间
cpp
void swap (vector& x);
(7)operator[]:返回对向量容器中位置 n 处的元素的引用。
cpp
reference operator[] (size_type n);const_reference operator[] (size_type n) const;
1~7接口代码:
cpp
void test03()
{
//find
//push_back
///
vector<int> v2;
for (int i = 0; i < 100; i++)
{
v2.push_back(i);
}
v2.pop_back();
//for (auto& e : v2)
//{
// cout << e << " ";
//}
cout << endl;
// 在一个范围内查找数据val,并且返回第一个等于val的迭代器,如果没有找到就返回区间的末尾迭代器
vector<int>::iterator it = find(v2.begin(),v2.end(), 31);
//while (it != v2.end())
//{
// //cout << ++(*it) << " ";
// cout << *it << " ";
// it++;
//}
/
//insert
//在pos迭代器位置插入val,不是下标
v2.insert(v2.begin() + 10, 10025);
it = v2.begin();
//while (it != v2.end())
//{
// //cout << ++(*it) << " ";
// cout << *it << " ";
// it++;
//}
//erase
//删除迭代区间的数据或者迭代pos位置的数据
// iterator erase (iterator first, iterator last);
v2.erase(v2.begin() + 3, v2.end() - 10);
it = v2.begin();
while (it != v2.end())
{
//cout << ++(*it) << " ";
cout << *it << " ";
it++;
}
// iterator erase (iterator position);
v2.erase(v2.begin() + 20);
it = v2.begin();
while (it != v2.end())
{
//cout << ++(*it) << " ";
cout << *it << " ";
it++;
}
}
// 测试swap
void test04()
{
// template <class T, class Alloc>
// void swap(vector<T, Alloc>&x, vector<T, Alloc>&y);
vector<int> v2(10, 1);
vector<int> v3(10, 3);
// 交换两个向量的空间
v2.swap(v3);
vector<int>::iterator it;
it = v2.begin();
//while (it != v2.end())
//{
// //cout << ++(*it) << " ";
// cout << *it << " ";
// it++;
//}
//
//reference operator[] (size_type n);const_reference operator[] (size_type n) const;
for (int i = 0; i < v2.size(); i++)
{
cout << ++v2[i]<< ' ';
}
}
五:vector的模拟实现
在实现vector的时候我们采用模板来实现,方便vector存取任意的类型,可以是自定义类型,也可以是内置类型,例如:vector<vector<int>>(二维数组)。
因为vector的数据结构本质是顺序表,所以我们可以设置一个顺序表来模拟实现它。
然后设置三个标志位:
cpp
private:
iterator _start = nullptr;
iterator _finsh = nullptr;
iterator _end_of_storage = nullptr;
_start 表示容器的起始位置,_finsh 表示有效数据个数的最后一个位置,_end_of_storage表示容器容量的大小。
iterator 使我们使用typedef将模板类型重命名的。
cpp
namespace cp
{
template<class T>
class vector {
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start = nullptr;
iterator _finsh = nullptr;
iterator _end_of_storage = nullptr;
};
}
我们将我们的vector使用一个单独的命名空间CP包起来,避免跟库里面的vector冲突。
关于模板请看:C++模板初阶-CSDN博客
(一)一些简单接口的实现:
一些比较简单的接口我们就将他们放到类里面
关于容量相关的接口:
cpp
//清除数据
void clear()
{
_finsh = _start;
}
//判断容器是否为空
bool empty()const
{
return _finsh == _start;
}
//获取容器容量的大小
size_t size() const
{
//指针相减得到元素个数
return _finsh - _start;
}
//获取容器容量的大小
size_t capacity() const
{
return _end_of_storage - _start;
}
迭代器的实现:const迭代器和非const迭代器,反向迭代器就不实现了
cpp
几种迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finsh;
}
const_iterator cbegin() const
{
return _start;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finsh;
}
const_iterator cend() const
{
return _finsh;
}
函数后边加const表示成员变量不可以修改。
方括号下标**[ ]**的重载实现:
cpp
//返回普通引用,可以修改
T& operator[](size_t pos)
{
return _start[pos];
}
//返回const引用,不可以修改
const T& operator[](size_t pos)const
{
return _start[pos];
}
尾删除函数
cpp
void pop_back()
{
assert(!empty());
--_finsh;
}
(二)一些复杂接口的实现
这些复杂的接口我们将他们放到类外面。
(1)reserve:开辟空间并检查。
cpp
template<class T>
void vector<T>::reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* tmp = new T[n];
for (int i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
if (_start)
{
delete[] _start;
}
_start = tmp;
_finsh = _start + old_size;
_end_of_storage = _start + n;
}
}
注意:在更新成员变量的时候要先预存一份没交换之前的size,因为在_start 和tmp交换的时候也会影响到size的改变。另外,记得释放旧空间,当然如果原来的容器是空的就不用释放了。
(2)push_back:在容器的末尾位置尾插一个数据
cpp
template<class T>
void vector<T>::push_back(const T& x)
{
if (_finsh == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finsh = x;
++_finsh;
}
只要是往容器里面插入数据都要检查容器是否满了,如果满了就需要先扩容再插入数据,这和数据结构的顺序表是一样的。
迭代器失效问题:
通常我们在插入和删除的时候使用的是迭代器,所以在一些情况下会导致迭代器失效,string的迭代器版本也会有这样的情况。
(1)insert:在pos迭代器位置插入数据,并且返回当前插入数据位置的迭代器。
cpp
template<class T>
typename vector<T>::iterator vector<T>::insert(iterator pos, T val)
{
assert(pos >= _start);
assert(pos < _finsh);
if (_finsh == _end_of_storage)
{
//防止pos位置因为扩容而失效
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;
}
iterator end = _finsh;
while (end >= pos)
{
*end = *(end - 1);
end--;
}
*pos = val;
_finsh++;
pos++;
return pos;
}
**知识点补充:**在这里我们使用typename的原因,因为在实例化的时候编译器不知道迭代器(iterator)到底是类型还是成员变量,所以我们使用typename。(规定使用typename指定的是类型)。
在实现insert的时候我们要先扩容,但是扩容的时候原来_start指向的空间和tmp交换了
但是我们的pos位置迭代器还是原来空间的pos,原来空间交换给tmp之后随着函数结束tmp变量指向的空间销毁,pos所指向的也就是一个随机空间。当我们在挪动数据的时候,因为pos不是当前对象指向的空间位置,所以会发生一些错误,插入数据也就会导致一些问题。
解决方案:
如上述代码,我们先记录pos的相对位置,在更新_start指向的空间的之后我们也更新pos的迭代器,这样就不会发生问题了。
这是第一种迭代器失效的情况:迭代器指向的空间被释放,类似于野指针。
(2)erase:挪动覆盖删除迭代器位置的数据,返回pos位置的迭代器。
我们先实现一下这个成员函数,同样,我们将他们放到类外面。
cpp
template<class T>
typename vector<T>::iterator vector<T>::erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finsh);
iterator start = pos;
while (start < _finsh)
{
*start = *(start + 1);
start++;
}
_finsh--;
return pos;
}
当我们删除数据之后,pos还是指向原来的位置,但是如果我们以下标的形式看待它。
例如:我们来测试一下,我们写一个vector,往里面放一些数据,删除之后使用它的迭代器来操作。
因为方便测试我们实现一个打印容器的函数模板:
cpp
template<class Container>
void print_container(const Container& con)
{
/*auto it = con.cbegin();
while (it != con.cend())
{
cout << *it << " ";
++it;
}
cout << endl;*/
for (auto& e : con)
{
cout << e << " ";
}
cout << endl;
}
cpp
//测试迭代器失效问题
void test_vaid_iterator()
{
cp::vector<int> v;
v.push_back(0);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//删除5位置的内容
cp::vector<int>::iterator it = v.erase(v.begin()+3);
cp::print_container(v);
*it *= 10;
cp::print_container(v);
}
可以看到原来位置的意义已经改变了,我们原本的位置在删除数据之后已经改变了。
(3)库里面的迭代器失效问题:Windows平台下。
我们来测试一下库里面的insert和erase。
测试函数:
(1)测试insert:
cpp
//测试迭代器失效问题
void test_vaid_iterator()
{
std::vector<int> v;
v.push_back(0);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//删除5位置的内容
std::vector<int>::iterator pos = v.begin() + 3;
v.insert(pos, 3);//删除pos位置的内容
cp::print_container(v);
//访问pos位置
*pos*=10;
cp::print_container(v);
}
(2)的是erase:
cpp
//测试迭代器失效问题
void test_vaid_iterator()
{
std::vector<int> v;
v.push_back(0);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
//删除5位置的内容
std::vector<int>::iterator pos = v.begin() + 3;
cp::print_container(v);
v.erase(pos);//删除pos位置的内容
*pos *= 10;
cp::print_container(v);
}
在vs2020中编译器会强制检查报错。在insert和erase之后是不允许访问pos位置的迭代器的。
总结:不管是自己实现的还是标准库内部实现的,在容器中(string,vector..)为了安全考虑,在insert和erase之后就不要访问pos位置的迭代器了。如果要访问就要更新迭代器。
(三)五个构造函数的模拟实现
接下来我们来实现他们的成员函数。
(1)默认构造
因为构造函数默认走初始化列表,所以我们在实现默认构造函数的时候可以这样写。
cpp
vector() = default;
因为我们在成员变量哪里给了缺省值,所以使用default关键字可以强制编译器生成默认构造。
当然,如果们没在成员变量给缺省参数的话可以自己走初始化列表:
cpp
vector()
:_start(nullptr)
,_finsh(nullptr)
,_end_of_storage(nullptr)
{
// 啥也不干,因为会自动走舒适化列表用缺省值完成初始化
}
我们来测试一下:
可以看到v对象的三个成员已经被初始化完成了,都是空指针。
(2)拷贝构造函数
用一个已经存在的对象去初始化另一个刚创建的对象
cpp
template<class T>
vector<T>::vector(const vector<T>& v)
{
//防止自己给自己赋值
if (this != &v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
//_finsh和_endd_of_storage是指针,不是整型,不能直接赋值,而且在reserve和push_back的时候已经更新过了,不用更新
//_finsh = v._finsh;
//_end_of_storage = v._end_of_storage;
}
}
(3)使用n个val去初始化容器的构造
cpp
template<class T>
vector<T>::vector(size_t n, vector<T>& val)
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
(4)迭代器区间初始化构造:
cpp
template<class input_iterator>
vector(input_iterator first, input_iterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
测试:
cpp
void Test03()
{
cp::vector<int> v;
v.push_back(0);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
cp::print_container(v);
cp::vector<int> v4(v.begin()+2, v.begin() + 5);
cp::print_container(v4);
}
(5)赋值构造
因为都是自定义类型,且需要开辟空间,所以和拷贝构造一样都是深拷贝。
cpp
//this = v
vector<T>& operator=(const vector<T>& v)
{
if (this != &v)
{
clear();
reserve(v.size());
/*size_t size = v.size();
T* tmp = new T[v.capacity()];
for (size_t i = 0; i < size; i++)
{
tmp[i] = v._start[i];
}
if (_start)
delete[] _start;
_start = tmp;
_finsh = _start + size;
_end_of_storage = _start + v.capacity();
*/
for (auto& e : v)
{
push_back(e);
}
}
return *this;
}
赋值构造的现代写法:
cpp
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finsh, v._finsh);
std::swap(_end_of_storage, v._end_of_storage);
}
//现代写法
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
(6)析构函数
cpp
~vector()
{
if (_start)//如果_start是空(为假),则说明已经析构了,不需要析构
{
delete[] _start;
_start = _finsh = _end_of_storage = nullptr;
}
}