目录
3.1 std::vector::iterator 没有重载下面哪个运算符( )
1. vector的介绍和使用
vector和string不同,string后有 \0,可以更好的兼容C,string有很多它专用的接口,vector的比大小没什么用,总的来说,vector和string各有其使用价值。
1.1 vector的介绍
std::vector
是 C++ 标准模板库 (STL) 提供的一个动态数组容器,位于头文件 <vector>
中。它是一个能够动态调整大小的数组,提供了非常方便的动态存储机制和丰富的接口。相比传统的 C 风格数组,std::vector
更加安全、灵活且功能强大。
特性:
-
动态大小:
vector
的大小是动态的,可以随着元素的插入或删除自动扩展或收缩。- 内部实现利用动态内存分配,会根据需要重新分配内存,但代价是拷贝已有数据。
-
随机访问:
- 提供与数组类似的随机访问功能,可以通过下标访问元素,时间复杂度为 O(1)。
-
连续存储:
vector
内部存储的元素是连续的,意味着可以直接传递给需要数组指针的 C 函数。
-
丰富的成员函数:
- 支持插入、删除、排序、查找等操作,使用方便。
-
类型安全:
vector
是模板类,支持强类型约束,不同类型的vector
之间不会混淆。
使用场景:
- 动态大小数组的需求。
- 需要频繁随机访问。
- 数据存储需要保持连续性(如传递给 C 风格函数)。
1.2 vector的使用
1.2.1 初始化
vector
支持多种初始化方法:
vector<int> v1; // 空vector
vector<int> v2(5); // 大小为5的vector,默认初始化为0
vector<string> v3(5, "***"); // 大小为5,所有元素初始化为10
vector<int> v4 = {1, 2, 3, 4, 5}; // 使用列表初始化
vector<int> v5(v4); // 拷贝构造
1.2.2 vector iterator
std::vector
提供了以下几种迭代器:
1.2.3 vector 空间增长
容量空间 | 接口说明 |
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize | 改变vector的size |
reserve | 改变vector的capacity |
当添加元素导致 size > capacity
时,vector
会分配更大的内存空间,将旧元素复制到新空间,并释放旧空间。每次分配的容量通常是当前容量的 2 倍。但是vs下是按照1.5倍增长的,g++是按2倍增长的,所以不要固化的认为都是2倍。
在某些情况下,频繁的内存重新分配会影响性能。如果你能预估到容器的最终大小,可以通过以下两种方法提前分配空间:
reserve
会预先分配内存,但不会改变size()
的值。
优点:
- 避免频繁的内存分配,提高性能。
capacity
会被设置为指定值,但size
不会改变
2. resize
会改变 size()
,并填充默认值。
两种方法的区别:
resize
会真正改变size
,并初始化新元素。reserve
只改变容量,不改变大小。
1.2.4 vector增删查改
push_back | 胃插 |
pop_back | 尾删 |
find | 查找 |
insert | 在pos之前插入 |
erase | 删除某位置的数据 |
swap | 交换两个空间 |
operator[] | 像数组一样访问 |
clear | 清空整个vector |
效率:
- 尾部插入/删除最快,时间复杂度为 O(1)O(1)O(1)。
- 中间插入/删除较慢,时间复杂度为 O(n)O(n)O(n)。
- 查找元素时间复杂度为 O(n)O(n)O(n)
使用下标运算符 []
或 at()
方法访问元素时,在vs系列编译器中,debug模式下,at() 和 operator[] 都是根据下标获取任意位置元素的,在debug模式下两者都会去做边界检查。当发生越界行为时,at 是抛异常,operator[] 内部的assert会触发。std::vector::operator[]
本身不会进行边界检查,越界访问会导致未定义行为。
如果我们像删除一个数组中的3,但是不知道在哪个位置,怎么办?
vector<int> v;
auto pos = find(v.begin(), v.end(), 3);
if(pos != v.end())
{
v.erase(pos);
}
for(auto e : v)
{
cout << e << " ";
}
cout << endl;
但如果有好几个3呢,要求删除所有的3,这就涉及迭代器失效的问题了:
1.2.5 vector迭代器失效问题
当我们对 std::vector
进行某些修改操作时,比如插入、删除或者重新分配内存,可能会导致迭代器失效,进而引发未定义行为。以下是有关 std::vector
迭代器失效的详细说明:
迭代器失效是指一个原本指向容器元素的迭代器,在容器被修改后,可能变得无效。使用失效的迭代器会导致程序运行错误甚至崩溃。
对于 std::vector
来说,以下操作可能导致迭代器失效:
-
内存重新分配:
- 当
std::vector
扩容时,底层存储数据的内存地址会重新分配。 - 所有现有的迭代器、指针和引用都会失效。
- 当
int main()
{
vector<int> v{1,2,3,4,5,6};
auto it = v.begin();
v.resize(100,8);
v.reserve(100);
v.insert(v.begin(), 0);
v.push_back(6);
v.assign(100,8);
while(it != v.end())
{
cout << *it << endl;
++it;
}
cout << endl;
return 0;
}
以上操作都有可能会导致vector扩容,也就是说vector底层旧空间被释放掉,而在打印时,it还使用的是释放前的旧空间,在对it进行操作时,实际操作的是一块已经被释放的空间,从而引起崩溃。
解决方式:在以上操作后,如果想继续通过迭代器操作vector中的元素,需要给it重新赋值。
2.元素插入或删除:
- 插入或删除元素可能导致部分迭代器失效。
- 特别是插入或删除位置后的迭代器可能受到影响。
int main()
{
vector<int> v{1,2,2,3,4};
auto it = v.begin();
while(it != v.end())
{
if(*it % 2 == 0)
{
v.erase(it);
}
++it;
}
return 0;
}
比如我们想删除其中的偶数,当删掉第一个2以后,后面的2、3、4向前移动,此时it已经指向第二个2了,但是还是走了++it,导致忽略了第二个2。
erase删除某位置的元素后,pos位置之后的元素会向前搬移,没有导致底层空间的改变,理论上迭代器应该不会失效,但是如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效。
要注意在linux下,g++编译器对上面的代码不会失效,g++下不检查,对迭代器失效的检测并不是非常严格,处理也没有vs极端。建议使用如下可移植的代码:
int main()
{
vector<int> v{1,2,2,3,4};
auto it = v.begin();
while(it != v.end())
{
if(*it % 2 == 0)
{
it = v.erase(it);
}
else
{
++it;
}
}
return 0;
}
其中接收了erase的返回值,也就是下一个位置。
在g++下:
- 扩容之后,迭代器已经失效了,程序虽然可以运行,但是运行结果已经不对了,虽然可以运行,但是结果肯定是错误的。
- erase删除的不是最后一个元素,迭代器不会失效,空间还是原来的空间
- erase删除最后一个元素,删除之后it超过了end,此时迭代器无效,++it导致程序崩溃。
3.string下的失效
与vector类似,string在插入+扩容操作+erase后,迭代器也会失效,不能再访问。
void teststring()
{
string s("hello");
auto it = s.begin();
//s.resize(20,'*');
while (it != s.end())
{
cout << *it;
++it;
}
cout << endl;
it = s.begin();
while (it != s.end())
{
it = s.erase(it);
//s.erase(it);
++it;
}
}
如果将 //s.resize(20,'*'); 取消注释,代码会崩溃,因为resize到20的空间会扩容,扩容之后it指向之前的旧空间被释放,迭代器失效,后序打印时,再访问it指向的空间程序就会崩溃。
如果只删除数据,而不接收它返回的新位置,程序就会崩溃。
2. vector模拟实现
因为vector是个模板类,可以存int,char,string等,所以要用类模板
2.1 构造函数、size、capacity
namespace zzy
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
size_t capacity() const
{
return _endofstorage - _start;
}
size_t size() const
{
return _finish - _start;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
2.2 reserve
void reserve(size_t n)
{
if(n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if(_start)
{
memcpy(tmp, _start, sizeof(T)*sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
为什么不可以直接写size(),而是要把size的值保存成sz?
因为在最后_finish = _start + sz 如果我们的sz是size(),会调用我们上面实现的size函数,其中的_finish指向的还是原来的空间,而_start已经被 _start = tmp 改变了,指向的是新空间,会出错。
如果我们使用memcpy,运行下面的代码会出现什么问题呢?
int main()
{
zzy::vector<std::string> v;
v.push_back("11111");
v.push_back("22222");
v.push_back("33333");
return 0;
}
由于memcpy是浅拷贝,会将空间中内容原封不动的拷贝到另一段空间中。
比如:在插入11111后,_start会指向空间“1”,插入22222期间需要扩容,假如开辟的新空间为“2、3”,如果采用memcpy,空间“2”将会被“1”覆盖,空间地址就被替换了,现有的空间变成了“1、3”中间的2号空间就丢失了。
而在delete后,旧空间被释放,_start就变成了野指针,因为它指向了指向了一块被释放的空间,再往里插入时,再扩容就拷贝不到了。
结论:如果对象中涉及到资源管理时,不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,不然会引起内存泄露甚至程序崩溃。
void reserve(size_t n)
{
if(n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if(_start)
{
//memcpy(tmp, _start, sizeof(T)*sz);
for (size_t i=0; i<sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endofstorage = _start + n;
}
}
2.3 push_back
void push_back(const T& x)
{
if(_finish == _endofstorage)
{
size_t newcapacity = capacity() == 0? 4:capacity*2;
reserve(newcapacity);
}
*_finish = x;
++_finish;
}
2.4 begin、end
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;
}
2.5 析构函数
~vector()
{
if(_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
2.6 [ ]重载
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.7 insert
void insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if(_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator end = _finish - 1;
while(end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
}
当扩容时,开辟了新空间,旧空间被释放,pos如果不更新就是野指针,start 和 finish 都指向新空间。所以在代码中使用了相对位置,保留了位置差,扩容后更新pos到新空间。
记住:insert以后迭代器可能会失效(扩容)
所以 insert 以后就不要使用这个形参迭代器了,因为它可能失效了。
v.insert(p, 300);
高危行为:
*p += 10;
2.8 erase
iterator erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
iterator it = pos + 1;
while(it != _finish)
{
*(it -1) = *it;
++it;
}
--_finish;
return pos;
}
2.9 resize
void resize(size_t n, const T& val = T())
{
if(n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}
}
为什么val不直接给0?
因为T不一定是int型,可能是各种类型,所以T()的本质是匿名对象。
由于resize的实现,引申出两种额外的构造函数:
explicit vector(size_t n, const T& val = T())
{
resize(n, val);
}
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
此处可以发现:类模板里的成员函数仍可以是函数模板。
在使用时我发现
vector<int> v(10, 1);
想调用下面的构造函数,因为10默认是int,而我们的第一个实现是size_t ,类型不匹配。解决办法:
vector<int> v(10u, 1);
u代表unsigned int
2.10 拷贝构造
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(v.capacity());
for(auto e : v)
{
push_back(e);
}
}
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
3.练习
3.1 std::vector::iterator 没有重载下面哪个运算符( )
A.==
B.++
C.*
D.>>
vector底层是以当前类型的指针作为迭代器,对于指针而言,能够进行操作的方法都支持,如==,++,*,而>>运算符并没有重载。