C++中的vector详解
vector是C++标准模板库(STL)中的一个非常重要的容器,它提供了动态数组的功能。下面我将详细介绍vector的各个方面。
1.基本概念
vector是一个能够存储任意类型数据的动态数组,它具有以下特点:
- 可以动态改变大小(不像普通数组大小固定)
- 在内存中是连续存储的
- 支持随机访问(可以通过下标直接访问元素)
- 在尾部插入和删除元素效率高,在中间或头部插入删除效率较低
2.基本用法
头文件和声明
#include <vector> // 必须包含的头文件
// 声明vector
std::vector<int> v1; // 空vector,存储int类型
std::vector<double> v2(10); // 有10个元素的vector,初始值为0.0
std::vector<std::string> v3(5, "hello"); // 5个元素,每个都是"hello"
初始化方式
std::vector<int> v1; // 空vector
std::vector<int> v2(5); // 5个元素,默认值为0
std::vector<int> v3(5, 10); // 5个元素,每个都是10
std::vector<int> v4{1, 2, 3, 4}; // 初始化列表
std::vector<int> v5(v4); // 拷贝构造
std::vector<int> v6(v5.begin(), v5.end()); // 通过迭代器范围构造
常用操作
访问元素
std::vector<int> v = {1, 2, 3, 4, 5};
// 使用下标访问(不检查边界)
int a = v[2]; // a = 3
// 使用at()访问(会检查边界,越界抛出异常)
int b = v.at(2); // b = 3
// 访问第一个和最后一个元素
int first = v.front(); // first = 1
int last = v.back(); // last = 5
// 使用迭代器访问
for(auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
修改元素
v[2] = 10; // 修改第3个元素为10
v.at(3) = 20; // 修改第4个元素为20
添加元素
v.push_back(6); // 在末尾添加元素6
v.emplace_back(7); // C++11新方法,效率更高
v.insert(v.begin() + 2, 8); // 在第3个位置插入8
删除元素
v.pop_back(); // 删除最后一个元素
v.erase(v.begin() + 1); // 删除第2个元素
v.erase(v.begin(), v.begin() + 2); // 删除前2个元素
v.clear(); // 清空vector
大小和容量
int size = v.size(); // 当前元素个数
bool empty = v.empty(); // 是否为空
int cap = v.capacity(); // 当前分配的存储空间大小
v.resize(10); // 改变大小
v.reserve(100); // 预留空间
3.重要特性
1.空间增长问题
- capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。VS是PJ版本STL,g++是SGI版本STL。
- reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
- resize在开空间的同时还会进行初始化,影响size。
// 测试vector的默认扩容机制
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:运行结果:vs下使用的STL基本是按照1.5倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141
g++运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128
vector<int> v;
v.reserve(100); // 预分配空间,避免多次扩容
for(int i=0; i<100; i++) {
v.push_back(i);
}
2. 迭代器失效问题
导致迭代器失效的主要操作
空间重新配置操作
push_back
:当vector容量不足时会导致扩容insert
:插入元素可能导致扩容reserve
:显式改变容量resize
:增加size可能导致扩容assign
:重新分配内容可能导致空间变化
元素删除操作
erase
:删除元素会使被删除位置及其后的迭代器失效pop_back
:删除尾部元素会使指向尾部的迭代器失效clear
:清空vector使所有迭代器失效
迭代器失效的具体表现
扩容导致的失效
扩容底层其实是开辟新内存,然后把原内存中的数据拷贝到新内存中再释放原内存,如果扩容后没有更新迭代器则会访问到已经释放的内存,从而失效
vector<int> v = {1,2,3};
auto it = v.begin();
v.push_back(4); // 可能导致扩容
// it已失效,不能再使用
插入删除导致的失效
erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置选代器失效了。
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据,导致pos迭代器失效。
v.erase(pos);
cout << *pos << endl; // 此处会导致非法访问
不同编译器的处理差异
VS (Visual Studio)
- 对迭代器失效检查严格
- 一旦失效立即崩溃(调试模式下)
- 包括所有可能导致失效的操作
g++ (Linux)
- 检查相对宽松
- 部分失效情况可能不会立即崩溃
- 但使用失效迭代器仍会导致未定义行为
正确的处理方式
插入操作后
auto it = v.begin() + 2;
v.insert(it, 10); // insert返回新的有效迭代器
it = v.insert(it, 10); // 正确做法:接收返回值
删除操作后
- 删除vector中所有偶数
auto it = v.begin();
while (it != v.end()) {
if (*it % 2 == 0) {
it = v.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
扩容操作后
auto it = v.begin();
size_t distance = it - v.begin(); // 保存距离
v.reserve(100); // 可能导致扩容
it = v.begin() + distance; // 重新计算迭代器
迭代器失效的根本原因
- 内存重新分配:扩容操作会导致vector使用新的内存空间,原有迭代器仍指向旧内存
- 元素位置移动:插入/删除操作会改变元素位置,使原有迭代器指向错误位置
- 尾后迭代器失效:任何改变vector大小的操作都会使end()迭代器失效
模拟实现
假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?
int main()
{
vector<string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}
memcpy 的基本特性
memcpy
是 C 标准库函数,其原型为:
void* memcpy(void* dest, const void* src, size_t count);
特点:
-
二进制拷贝:逐字节复制内存内容
-
高效:通常由编译器优化为最有效的拷贝方式
-
浅拷贝:
只复制指针或值本身,不处理资源所有权
-
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
7. 总结
memcpy
在 vector 实现中的主要问题是:
- 无法正确处理管理资源的类
- 破坏了C++对象的生命周期规则
- 导致资源管理混乱(内存泄漏或重复释放)
正确的做法是:
- 使用 placement new 和拷贝构造/移动构造
- 对每个元素单独处理
- 保证异常安全
- 遵循C++对象模型规则
理解这一点对于实现符合标准的容器类至关重要,也是C++资源管理核心思想的体现。
#pragma once
#include <iostream>
#include <string>
#include <assert.h>
using namespace std;
namespace sty
{
/**
* @brief 模拟实现的vector类模板
* @tparam T 存储的元素类型
*/
template<class T>
class vector
{
public:
// 迭代器类型定义
typedef T* iterator; // 普通迭代器
typedef const T* const_iterator; // const迭代器
/******************* 构造函数和析构函数 *******************/
/**
* @brief 默认构造函数(C++11风格)
*/
vector() = default;
/**
* @brief 使用迭代器范围构造vector
* @tparam InputIterator 输入迭代器类型
* @param first 范围起始迭代器
* @param last 范围结束迭代器
*/
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first); // 逐个插入元素
++first;
}
}
/**
* @brief 构造包含n个val的vector
* @param n 元素数量
* @param val 初始值
*/
vector(size_t n, const T& val = T())
{
reserve(n); // 预分配空间
for (size_t i = 0; i < n; i++)
{
push_back(val); // 插入n个val
}
}
/**
* @brief 拷贝构造函数
* @param v 要拷贝的vector
*/
vector(const vector<T>& v)
{
reserve(v.size()); // 预分配空间
for (auto& e : v)
{
push_back(e); // 逐个拷贝元素
}
}
/**
* @brief 析构函数
*/
~vector()
{
if (_start) // 如果内存已分配
{
delete[] _start; // 释放内存
_start = _finish = _end_of_storage = nullptr; // 重置指针
}
}
/******************* 容量相关操作 *******************/
/**
* @brief 清空vector
*/
void clear()
{
_finish = _start; // 简单地将_finish指针移回_start
}
/**
* @brief 交换两个vector的内容
* @param v 要交换的vector
*/
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
/**
* @brief 赋值重载,现代写法
* @param v 要赋值的vector
* @return vector& 返回当前对象的引用
*/
vector<T>& operator=(vector<T> v)
{
swap(v); // 交换内容
return *this;
}
/**
* @brief 预留空间(不影响size)
* @param n 要预留的空间大小
*/
void reserve(size_t n)
{
if (n > capacity()) // 只有当n大于当前容量时才需要处理
{
size_t old_size = size();
T* tmp = new T[n]; // 分配新空间
// 拷贝元素到新空间
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i]; // 使用赋值操作
}
delete[] _start; // 释放旧空间
_start = tmp; // 更新指针
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
/******************* 迭代器相关操作 *******************/
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin() const { return _start; }
const_iterator end() const { return _finish; }
/******************* 容量查询操作 *******************/
size_t size() const { return _finish - _start; }
size_t capacity() const { return _end_of_storage - _start; }
bool empty() { return _start == _finish; }
/******************* 元素访问操作 *******************/
T& operator[](size_t i)
{
assert(i < size() && i >= 0); // 越界检查
return _start[i];
}
const T& operator[](size_t i) const
{
assert(i < size() && i >= 0);
return _start[i];
}
/******************* 修改操作 *******************/
/**
* @brief 在尾部添加元素
* @param x 要添加的元素
*/
void push_back(const T& x)
{
if (_finish == _end_of_storage) // 检查是否需要扩容
{
reserve(capacity() == 0 ? 4 : 2 * capacity()); // 扩容策略
}
*_finish = x; // 在尾部构造元素
++_finish;
}
/**
* @brief 删除尾部元素
*/
void pop_back()
{
assert(!empty()); // 不能对空vector执行pop_back
--_finish;
}
/**
* @brief 在指定位置插入元素
* @param pos 插入位置迭代器
* @param x 要插入的元素
* @return iterator 返回指向新插入元素的迭代器
*/
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish); // 检查位置有效性
// 处理可能发生的扩容导致的迭代器失效
if (_finish == _end_of_storage)
{
size_t len = pos - _start; // 保存相对位置
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len; // 重新计算pos位置
}
// 向后移动元素
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x; // 插入新元素
++_finish;
return pos;
}
/**
* @brief 删除指定位置的元素
* @param pos 要删除元素的位置
* @return iterator 返回指向被删除元素后面元素的迭代器
*/
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish); // 检查位置有效性
// 向前移动元素覆盖要删除的元素
iterator it = pos + 1;
while (it != end())
{
*(it - 1) = *it;
it++;
}
--_finish; // 调整_finish指针
return pos;
}
/**
* @brief 调整vector的大小
* @param n 新的大小
* @param val 新增元素的初始值(默认为T())
*/
void resize(size_t n, T val = T())
{
if (n < size()) // 缩小size
{
_finish = _start + n;
}
else // 增大size
{
reserve(n); // 预分配空间
while (_finish < _start + n)
{
*_finish = val; // 初始化新增元素
++_finish;
}
}
}
private:
iterator _start = nullptr; // 指向数据块的起始位置
iterator _finish = nullptr; // 指向最后一个元素的下一个位置
iterator _end_of_storage = nullptr; // 指向存储空间末尾的下一个位置
};
/******************* 辅助函数 *******************/
template<class Container>
void print_container(const Container& v)
{
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}