目录
基础构造
vector 是一种动态数组,它可以根据需要自动调整大小,可以理解为顺序表
#include <vector>
using namespace std;
// 存放int类型的数组
vector<int> v;
// 存放string类型的数组
vector<string> vv;
同样使用时必须包含#include头文件以及using namespace std;(根据具体情况而定)
vs下的 vector 结构:
底层有三个迭代器(像指针,但不是指针!)
① start:指向数据块的开始
② finish:指向有效数据的结尾(finish - start = 有效元素个数)
③ end_of_storage:指向存储容量的结尾(end_of_storage - start = 容量大小)
常用接口
下面我就介绍一些常用的接口,大家可以配合C++官网的介绍来看下面的内容
1>常见构造
函数名称 | 功能说明 |
---|---|
vector() | 无参构造 |
vector(const vector& x) | 拷贝构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector(Inputlterator first, Inputlterator last) | 使用迭代器进行初始化构造 |
2>容量操作
函数名称 | 功能说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize | 改变vector的size |
reserve | 改变vector的capacity |
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v 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';
}
}
}
在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的(不同平台不同版本的增容倍数一般都不相同,具体增长多少是根据具体的需要定义的
reserve只负责开辟空间,如果知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题
3>遍历操作
函数名称 | 功能说明 |
---|---|
begin + end | 获取第一个数据位置的iterator/const_iterator,获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
4>修改操作
函数名称 | 功能说明 |
---|---|
push_back | 尾插 |
pop_back | 尾删 |
find | 查找(算法模拟实现,不是成员接口!) |
insert | 在position之前插入val |
erase | 删除position位置的数据 |
swap | 交换两个vector的数据空间 |
operator | 类似数组访问 |
知识点
在修改操作中有一个重点,那就是vector迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其实底层实际就是一个指针,或者是对指针进行了封装,比如 vector的迭代器就是原生态指针T*
因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)
a.示例一
下面这段代码是我模拟实现vector的接口,如果使用库里面的vector会直接报错
iterator insert(iterator pos, const T& n)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endOfStorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;
}
iterator i = _finish - 1;
while (i >= pos)
{
*(i + 1) = *i;
--i;
}
*pos = n;
++_finish;
return pos;
}
void test1()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
int x;
cin >> x;
auto it = find(v.begin(), v.end(), x);
if (it != v.end())
{
v.insert(it, x * 10);
cout << "*it的值:" << *it << endl;
}
for (auto& e : v)
{
cout << e << " ";
}
}
通过运行结果可以知道it此时失效了,那又为什么会失效呢?
插入操作可能会导致vector扩容,也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,所以会引起代码运行时崩溃(还有一个点就是在传值传参中,形参的改变不会影响实参)
if (it != v.end())
{
// v.insert(it, x * 10);
it = v.insert(it, x * 10);
cout << "*it的值:" << *it << endl;
}
如果需要继续通过迭代器操作vector中的元素,只需给it重新赋值就可以了
b.示例二
iterator erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
iterator i = pos + 1;
while (i < _finish)
{
*(i - 1) = *i;
++i;
}
--_finish;
return pos;
}
void test2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
int x;
cin >> x;
auto it = find(v.begin(), v.end(), x);
if (it != v.end())
{
v.erase(it);
cout << "*it的值:" << *it << endl;
}
for (auto& e : v)
{
cout << e << " ";
}
}
通过运行结果可以知道it此时也失效了,那原因是什么呢?
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了
总结:
① 第一种失效:扩容引起的野指针
② 第二种失效:删除数据,导致数据挪动,it已经不是指向之前位置了,it也失效了,因为it已经不是指向之前的位置,可能会导致逻辑问题
c.Linux角度
但是在Linux下,g++编译器对迭代器失效的检测相对vs来说没有那么严格、极端
通过运行结果可以知道,扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了
erase删除任意位置代码后,Linux下迭代器并没有失效,因为空间还是原来的空间,后序元素往前搬移了,it的位置还是有效的
当删除最后一个偶数后,it 和 end() 就此错过,导致陷入死循环使程序崩溃
根据以上三个例子可以看出,迭代器失效后,代码并不一定会崩溃,但是运行结果肯定不对,如果it不在begin和end范围内,肯定会崩溃的
与vector类似,string在插入+扩容操作+erase之后,迭代器也会失效
模拟实现
vector.h
namespace tianci
{
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;
}
// construct
vector(){}
vector(initializer_list<T> il)
:_start(nullptr)
, _finish(nullptr)
, _endOfStorage(nullptr)
{
reserve(il.size());
for (auto& e : il)
{
push_back(e);
}
}
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
void swap(vector<T>& tmp)
{
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endOfStorage, tmp._endOfStorage);
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
// destroy
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endOfStorage = nullptr;
}
}
//access
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
// capacity
size_t size()const
{
return _finish - _start;
}
size_t capacity()const
{
return _endOfStorage - _start;
}
bool empty()
{
return _start == _finish;
}
void resize(size_t n, T value = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n)
{
*_finish = value;
++_finish;
}
}
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * oldsize);
for (int i = 0; i < oldsize; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = oldsize + _start;
_endOfStorage = _start + n;
}
}
//modify
void push_back(const T& n)
{
//if (_finish == _endOfStorage)
//{
// reserve(capacity() == 0 ? 4 : 2 * capacity());
//}
//*_finish = n;
//++_finish;
insert(_finish, n);
}
void pop_back()
{
assert(!empty());
--_finish;
}
// 注意迭代器失效问题
// 扩容可能会导致 pos 的指向有问题
iterator insert(iterator pos, const T& n)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _endOfStorage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;
}
iterator i = _finish - 1;
while (i >= pos)
{
*(i + 1) = *i;
--i;
}
*pos = n;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
iterator i = pos + 1;
while (i < _finish)
{
*(i - 1) = *i;
++i;
}
--_finish;
return pos;
}
private:
iterator _start = nullptr; // 指向数据块的开始
iterator _finish = nullptr; // 指向有效数据的结尾
iterator _endOfStorage = nullptr; // 指向存储容量的结尾
};
}
深浅拷贝
模拟实现中,reserve成员接口如果使用了memcpy会有问题,对于int类型程序可能可以正常运行,而string类型就一定会导致程序运行失败
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * oldsize);
delete[] _start;
}
_start = tmp;
_finish = oldsize + _start;
_endOfStorage = _start + n;
}
}
void test3()
{
vector<string> v;
v.push_back("1111111111111111111111");
v.push_back("1111111111111111111111");
v.push_back("1111111111111111111111");
v.push_back("1111111111111111111111");
v.push_back("1111111111111111111111");
for (auto& e : v)
{
cout << e << " ";
}
cout << endl;
}
通过运行结果可以知道程序是有问题的,原因就在于深浅拷贝
① memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
② 如果拷贝的是内置类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型的元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * oldsize);
for (int i = 0; i < oldsize; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = oldsize + _start;
_endOfStorage = _start + n;
}
}
可以通过赋值重载实现深拷贝
总结:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则会引起内存泄漏甚至程序崩溃
Leecode
a.杨辉三角
b.数组中出现次数超过一半的数字
c.电话号码的字母组合
本篇文章到这里就结束啦,希望这些内容对大家有所帮助!
下篇文章见,希望大家多多来支持一下!
感谢大家的三连支持!