作者主页:lightqjx
本文专栏:C++
目录
[1. vector的类型重定义](#1. vector的类型重定义)
[2. 构造函数](#2. 构造函数)
[3. 迭代器的使用](#3. 迭代器的使用)
[4. 与空间相关的使用](#4. 与空间相关的使用)
[5. 增删查改](#5. 增删查改)
[1. 成员变量的解释](#1. 成员变量的解释)
[2. 模拟实现vector的总代码](#2. 模拟实现vector的总代码)
[1. 迭代器失效的问题](#1. 迭代器失效的问题)
[2. 关于memcpy的拷贝问题(深浅拷贝)](#2. 关于memcpy的拷贝问题(深浅拷贝))
[3. 模拟vector的构造函数重载的问题](#3. 模拟vector的构造函数重载的问题)
前言
vector是一个可变大小数组的序列容器,在我们学习vector时常常需要我们查阅它的文档:vector的文档链接
一、vector的简介
在C++中,std::vector 是标准模板库(STL)提供的一种动态数组容器 ,它能够自动管理内存,支持高效地随机访问、动态扩容和元素增删,换句话说,vector其实就相当于我们的数据结构中使用数组实现的动态顺序表。
其实,在STL中的各个容器的设计都是非常相似的,比如:都具有构造,析构,增删查改等基本操作等等。
如果要简单理解的话,也可以这样说:C++其实是将数据结构给封装起来的而已。但这种说法并不全面,在C++中其实对于容器还有更多的高级特性,比如:内存管理、算法适配、接口标准化等
因为vector就像一个动态的顺序表,所以它的内容是可以存储任何数据的,比如:int,char,double等内置类型;还可以存储自定义类型,比如:struct定义的类型,class定义的类型(比如string类)等等。所以我们来看它的实现都是使用模板来实现的。如图所示:

其中的 class T 就是一个模板,而class Alloc = allocator<T> 是一个空间配置器,内存池,在现阶段我们可以不用考虑这个。就只需要知道它是通过模板实现的就行了。
所以我们使用vector时就必须要指明类型:
cpp//vector v1; //错误方法 //正确方法 vector<int> v1; vector<char> v2; vector<string> v3;
v1 的类型是 std::vector<int>,即一个存储 int 的动态数组。
v2 的类型是 std::vector<char>,即一个存储 char的动态数组。
v3 的类型是 std::vector<string>,即一个存储 string的动态数组。
这时我们既然知道了vector是通过模板实现的,那么它是不是可以代替任何类型呢?其实这是不行的。比如它是不可以完全代替string类的。其原因有两个:string要求最后有\0,可以更好兼容C语言接口
string有很多他的专用接口函数
所以我们对于不同类型的数据使用vector时都必须要注意vector本身的使用。
二、vector的使用
1. vector的类型重定义
在认识vector时我们肯定会去查阅文档的,如果我们直接去看vector的各个构造函数可能是看不懂的,比如有许多看不懂的类型,这其实是因为vector类中通过typedef封装了其他类型。

其中的value_type就是第一个模板参数(T),allocator_type就是第二个模板参数 (Alloc)。这些类型定义会在vector的许多成员函数中使用到。
2. 构造函数
下图是vector的几个构造函数声明:

其中我们当下阶段可以不用考虑:const allocator_type& alloc = allocator_type(),后面会详细讲解。当然我们也可以了解一下(本文内容不在此)它的意思:调用构造函数时没有显式提供 alloc 参数,则使用默认构的 allocator_type 对象。
所以我们可以将这里的构造函数重新总结一下:
|---|-----------------------------------------------------------------------------------|--------------|
| 1 | vector() | 无参构造 |
| 2 | vector (size_type n, const value_type& val = value_type()) | 构造并用n个val初始化 |
| 3 | template <class InputIterator> vector (InputIterator first, InputIterator last) | 使用迭代器进行初始化构造 |
| 4 | vector (const vector& x) | 拷贝构造 |
使用实例:
cpp
#include<vector>
using namespace std;
void test1()
{
vector<int> v1; // 创建一个空的 vector
vector<int> v2(4, 100); // 创建一个包含 4 个整型元素的 vector,每个元素都是 100
vector<int> v3(v2.begin(), v2.end()); // 使用v2的迭代器范围 [first, last) 内的元素初始化 v3
vector<int> v4(v3); // 使用v3拷贝构造一个v4
}
这里我们可以补充一下:
当然对象会在出函数作用域时自动调用析构函数的。
3. 迭代器的使用
迭代器有两种:一种是正向的迭代器,一种是反向的迭代器。正向的迭代器通过迭代器移动可以从前往后遍历vector对象。反向的迭代器通过迭代器移动可以从后往前遍历vector对象。
在vector中,迭代器其实就是指针。
|--------|----------|-----------------------------------------------------------------------------------------------|
| 正向的迭代器 | begin() | 获取第一个数据位置。它有两个函数: iterator begin(); 可读可写 const_iterator begin() const; 只能读 |
| 正向的迭代器 | end() | 获取最后一个数据的下一个位置。它有两个函数: iterator end(); 可读可写 const_iterator end() const; 只能读 |
| 反向的迭代器 | rbegin() | 获取最后一个数据位置。它有两个函数: reverse_iterator rbegin(); 可读可写 const_reverse_iterator rbegin() const; 只能读 |
| 反向的迭代器 | rend(() | 获取第一个数据前一个位置。它有两个函数: reverse_iterator rend(); 可读可写 const_reverse_iterator rend() const; 只能读 |
所以迭代器都是前开后闭的区间。即:加入v是vector对象,则 [v.begin(), v.end()) 或 [v.rbegin(), v.rend())
使用示例:
cpp
#include<iostream>
#include<vector>
using namespace std;
void test2()
{
vector<int> v1;
// push_back是逐步向v1中尾插入数据的函数
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//正向输出
vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
//反向输出
vector<int>::reverse_iterator rit = v1.rbegin();
while (rit != v1.rend())
{
cout << *rit << ' ';
rit++;
}
cout << endl;
}
所以我们vector的遍历方法也就有三种,如下所示:
cpp
#include<iostream>
#include<vector>
using namespace std;
void test3()
{
vector<int> v1;
// push_back是逐步向v1中尾插入数据的函数
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
//遍历方法1:for循环
for (int i = 0; i < v1.size(); i++)
{
cout << v1[i] << ' ';
}
cout << endl;
//遍历方法2:迭代器输出
vector<int>::iterator it = v1.begin();
//auto it = v1.begin(); //常常是这样写的,因为比较简单
while (it != v1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
//遍历方法3:范围for
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
}
4. 与空间相关的使用
vector中有几个与空间相关的比较常用的函数:
|--------------------------------------------------------------|-------------------|
| size_type size() const; | 获取数据个数 |
| size_type capacity() const; | 获取容量大小 |
| bool empty() const; | 判断是否为空 |
| void resize(size_type n, value_type val = value_type()); | 改变vector的size |
| void reserve(size_type n); | 改变vector的capacity |
前面的size、capacity、empty都比较简单,只是获取数据即可。后面的resize、reserve需要重点了解。
需要注意的有以下几点:
- vector 的扩容策略因编译器 STL 实现而异,如 VS 中 capacity 通常按 1.5 倍增长,而 G++ 则按 2 倍增长,故不应固化扩容倍数认知。
- reserve(n) 可预先分配至少容纳 n 个元素的内存,可以避免频繁扩容带来的性能损耗,适用于我们预先知道元素数量的场景。
- resize(n) 则会调整 vector 的 size 为 n,新增元素会被初始化,多余元素会被销毁,其操作比 reserve 更复杂。
使用示例:
cpp
void test4()
{
vector<int> v1;
cout << "empty:" << v1.empty() << endl;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
cout << "empty:" << v1.empty() << endl;
cout << "size:" << v1.size() << endl;
cout << "capacity:" << v1.capacity() << endl;
v1.resize(20);
cout << "size:" << v1.size() << endl;
cout << "capacity:" << v1.capacity() << endl;
v1.reserve(100);
cout << "size:" << v1.size() << endl;
cout << "capacity:" << v1.capacity() << endl;
}
5. 增删查改
关于增删改查的常用函数有以下几种:
|---|---------------|--------------------------------|
| 增 | push_back | 尾插 |
| 增 | insert | 在position之前插入val |
| 删 | pop_back | 尾删 |
| 删 | erase | 删除position位置的数据 |
| 改 | operator[ ] | 像数组一样访问 |
| 改 | swap | 交换两个vector的数据空间 |
| 查 | find | 查找。(注意这个是算法模块实现,不是vector的成员接口) |
vector的尾插使用比较简单,
cpp
void test5()
{
vector<int> v1;
v1.push_back(1); // 尾插1
v1.push_back(2); // 尾插2
v1.push_back(3); // 尾插3
v1.push_back(4); // 尾插4
v1.push_back(5); // 尾插5
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
}
我们需要注意在vector中是没有头插的。但我们可以通过insert实现,但通过insert进行插入有多个函数重载需要注意。如图所示:

第一个是在position位置插入val,第二个是在position位置插入n个val,第三个是在position位置插入一个由迭代器范围 [ first, last ) 定义的元素序列。要注意上面的参数都是迭代器参数。
cpp
void test6()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
vector<int>::iterator it=v1.begin();
v1.insert(v1.begin()+2, 6); //在第2个位置之前插入6
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
v1.insert(v1.begin()+2, 6, 10); //在第2个位置之前插入6个10
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
int arr[] = { 11,12,13,14 };
v1.insert(v1.begin()+2, arr, arr+3); //在第2个位置之前插入arr的[arr,arr+3)的数据
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
}
进行删除操作时,尾删比较好使用,就不做解释了。
对于erase有两个函数重载,它们都是传的迭代器参数:

使用示例:
cpp
void test7()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
v1.erase(v1.begin()); //删除第一个元素
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
v1.erase(v1.begin(),v1.begin()+2); //删除当前的[v1.begin(),v1.begin())的两个元素
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
}
因为vector是使用数组实现的,,所以通过运算符重载operator[ ]可以实现对vector对象的访问及修改。这里就不多说了。
对于两个vector对象,使用swap就可以高效交换两个 vector 对象的内容。
cpp
void test8()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
vector<int> v2(3,6);
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
for (auto e : v2)
{
cout << e << ' ';
}
cout << endl;
v1.swap(v2); // 交换v1和v2
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
for (auto e : v2)
{
cout << e << ' ';
}
cout << endl;
}
对于查找,vector里面没有提供,所以我们可以使用算法模块find实现。使用算法模块需要包含头文件:
三、vector的模拟实现
1. 成员变量的解释
在STL源码底层里面,实现vector主要是靠使用三个迭代器实现的,也可以是称为指针(因为vector的迭代器就是指针)。即:
- start:指向第一个元素,标记数据起始。
- finish:指向最后一个元素的下一个位置,标记数据结束。
- end_of_storage:指向动态分配内存的末尾的下一个位置(即内存块的尾后位置),标记容量上限。
图示如下:
要注意我们模拟时需要我们自己来定义一个命名空间域,并且由于vector是可以存储多种类型的,所以我们需要使用模板实现,所以成员变量定义就可以如下所示:
cpp
namespace MyTest
{
template<class T>
class vector
{
public:
//重定义可以和库里的保持一致,便于理解
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
2. 模拟实现vector的总代码
cpp
namespace MyTest
{
template<class T>
class my_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;
}
//构造
my_vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{ }
my_vector(size_t n, const T& val = T()) //如果没有传val,则val会去调用它的默认构造
{
resize(n, val);
}
my_vector(int n, const T& val = T())
{
resize(n, val);
}
template<class InputIterator>
my_vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
//拷贝构造 - 深拷贝
my_vector(const my_vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
void swap(my_vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
my_vector<T>& operator=(my_vector<T> v)//传值,会调用拷贝构造
{
swap(v);
return *this;
}
//析构
~my_vector()
{
//清除空间资源
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
//空间相关操作
//获取数据个数
size_t size() const
{
return _finish - _start;
}
//获取容量
size_t capacity() const
{
return _end_of_storage - _start;
}
//[]运算符重载
T& operator[](size_t pos)
{
assert(pos < size()); //断言位置合理性
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size()); //断言位置合理性
return _start[pos];
}
//扩容(重要)
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();//这里需要保存一下原数据的个数
T* tmp = new T[n];
if (_start)
{
//如果vector不为空,则要拷贝数据
for (int i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
delete[] _start;//释放原空间
}
//对该对象进行赋值
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}//扩容完成
}
//改变size
void resize(size_t n, const T val = T())
{
if (n < size())
{
//如果n比当前size小,只需要移动finish即可
_finish = _start + n;
}
else
{
//如果比size大,则需要先扩容,再补充元素
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
_finish++;
}
}
}
//在pos位置插入x
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
//判满
if (_finish == _end_of_storage)
{
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++;
return pos;
}
//尾插
void push_back(const T& x)
{
insert(end(), x);
}
//删除pos位置的数据
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
_finish--;
return pos;
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
四、关于vector的常见问题
1. 迭代器失效的问题
迭代器失效是指迭代器指向的对象不再有效,继续使用该迭代器会导致未定义行为。对于vector而言,迭代器失效主要发生在以下两种情况:
- 容量变化导致的失效:当vector的容量(capacity)发生变化时,所有现有迭代器、指针和引用都会失效。
- 元素删除导致的失效:当删除某个元素时,指向该元素及其之后所有元素的迭代器都会失效。
(1)容量变化导致
原因
我们在进行插入数据时往往会执行扩容操作。由于 vector 在内存中是连续存储的,所以当空间不足时,它会分配一块更大的内存(通常是当前容量的2倍),将原有元素复制过去,然后释放旧内存。这个过程之后,如果之前使用了迭代器,则所有迭代器都会指向旧的内存地址,因此就会失效。

比如:在VS下的迭代器失效
cpp
#include<vector>
#include<iostream>
using namespace std;
int main()
{
vector<int> v = { 1, 2, 3 };
auto it = v.begin(); // 指向第一个元素
v.push_back(4); // 可能触发扩容
// 此时it可能已经失效
cout << *it << endl; //此时迭代器就失效了,在VS下就是报错
return 0;
}
运行后:
避免方法
- 预留空间:使用 reserve() 预先分配足够空间。
- 谨慎使用需要扩容的操作:在需要保持迭代器有效的操作中,避免使用可能导致扩容的操作。
(2)元素删除导致
原因
当使用 erase() 删除 vector 中的元素时,会导致后续元素向前移动,被删除元素之后的所有元素的迭代器都会失效,因为元素移动后,迭代器指向的位置可能被覆盖,从而失效,继续使用这些失效的迭代器会导致不可预测的程序行为,即未定义行为。
比如:
cpp
#include<vector>
#include<iostream>
using namespace std;
int main()
{
vector<int> v = { 1, 2, 3 };
auto it = v.begin(); // 指向第一个元素
v.erase(it); // 删除操作后,就元素移动了
cout << *it << endl; // 此时迭代器就失效了,这是未定义行为,在VS下会报错
return 0;
}
运行后:
解决方法
每次删除时都返回新的迭代器。
总结一下
在vector中,通过插入或删除迭代器对象后,就不能再访问这个迭代器了。因为我们认为此时迭代器是失效的,访问时结果是未定义的,我们不能这么使用。
同时我们需要知道,不同编译器对迭代器失效的处理也是不同的,比如VS是强制检查,比较严格;g++的处理就比较宽松。
2. 关于memcpy的拷贝问题(深浅拷贝)
使用memcpy而出现的问题常常出现在我们来模拟实现vector时出现。比如我们在模拟实现vector的拷贝构造函数时,如果使用memcpy来进行拷贝,即:
cpp
my_vector(const my_vector<T>& v)
{
_start = new T[v.capacity()];
memcpy(_start, v._start, sizeof(T) * v.size()); // 注意这样写是有问题的
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
这样写会出现的问题是:深浅拷贝的问题。可以说这样写的效果就是浅拷贝的效果。所以当我们写一个自定义的类型的vector时,就会出现问题,比如string。
那么我们再执行以下代码:
cpp
int main()
{
MyTest::my_vector<string> v;
v.push_back("11111");
v.push_back("22222");
v.push_back("33333");
v.push_back("44444");
MyTest::my_vector<string> v2(v);
return 0;
}
这段代码的执行效果如下:

其中v和v2是指向同一个空间的。
原因分析
- memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中。
- 如果拷贝的是内置类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
所以如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是
浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。那么我们对于上面的代码,要使自定义类型也可以使用的话就可以这样写:
cpp
my_vector(const my_vector<T>& v)
{
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i];
}
/*memcpy(_start, v._start, sizeof(T) * v.size());*/
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
这样就是深拷贝:

3. 模拟vector的构造函数重载的问题
我们在模拟实现构造函数时有三个函数的实现:
cpp
//构造
my_vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{ }
//第二个
my_vector(size_t n, const T& val = T()) //如果没有传val,则val会去调用它的默认构造
{
resize(n, val);
}
//第三个
template<class InputIterator>
my_vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
其中的第二个和第三个的构造函数实现是有问题的。因为它们是很像的。当我们定义vector<int>的对象时,编译器会调用最合适它的一个函数,比如这段代码:
cpp
MyTest::my_vector<int> v(4, 6); // 调用的是第三个构造函数
这里会优先调用的是第三个构造函数,但第三个函数是必须要用迭代器才能正常使用的,否则就不会正常运行:
所以在我们模拟实现vector的构造函数时,就需要注意这个情况。
在vector的库里面解决这个问题的方法就是将第二个函数进行了改进,就是将第二个函数进行重载一下,即以下代码:
cpp
my_vector(size_t n, const T& val = T()) //如果没有传val,则val会去调用它的默认构造
{
resize(n, val);
}
my_vector(int n, const T& val = T())
{
resize(n, val);
}
template<class InputIterator>
my_vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
感谢各位观看!希望能多多支持!