【C++】vector 使用和实现
作者主页:lightqjx
本文专栏:C++
目录
一、vector的简介
二、vector的使用
1. vector的类型重定义
2. 构造函数
3. 迭代器的使用
4. 与空间相关的使用
5. 增删查改
三、vector的模拟实现
1. 成员变量的解释
2. 模拟实现vector的总代码
四、关于vector的常见问题
1. 迭代器失效的问题
(1)容量变化导致
(2)元素删除导致
2. 关于memcpy的拷贝问题(深浅拷贝)
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时就必须要指明类型:
//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) | 拷贝构造 |
使用实例:
#include<vector>
using namespace std;
void test1()
{vector<int> v1; // 创建一个空的 vectorvector<int> v2(4, 100); // 创建一个包含 4 个整型元素的 vector,每个元素都是 100vector<int> v3(v2.begin(), v2.end()); // 使用v2的迭代器范围 [first, last) 内的元素初始化 v3vector<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())
使用示例:
#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的遍历方法也就有三种,如下所示:
#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:范围forfor (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 更复杂。
使用示例:
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的尾插使用比较简单,
void test5()
{vector<int> v1;v1.push_back(1); // 尾插1v1.push_back(2); // 尾插2v1.push_back(3); // 尾插3v1.push_back(4); // 尾插4v1.push_back(5); // 尾插5for (auto e : v1){cout << e << ' ';}cout << endl;
}
我们需要注意在vector中是没有头插的。但我们可以通过insert实现,但通过insert进行插入有多个函数重载需要注意。如图所示:
第一个是在position位置插入val,第二个是在position位置插入n个val,第三个是在position位置插入一个由迭代器范围 [ first, last ) 定义的元素序列。要注意上面的参数都是迭代器参数。
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个位置之前插入6for (auto e : v1){cout << e << ' ';}cout << endl;v1.insert(v1.begin()+2, 6, 10); //在第2个位置之前插入6个10for (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有两个函数重载,它们都是传的迭代器参数:
使用示例:
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 对象的内容。
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和v2for (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是可以存储多种类型的,所以我们需要使用模板实现,所以成员变量定义就可以如下所示:
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的总代码
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;}//扩容完成}//改变sizevoid 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位置插入xiterator 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下的迭代器失效
#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 中的元素时,会导致后续元素向前移动,被删除元素之后的所有元素的迭代器都会失效,因为元素移动后,迭代器指向的位置可能被覆盖,从而失效,继续使用这些失效的迭代器会导致不可预测的程序行为,即未定义行为。
比如:
#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来进行拷贝,即:
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。
那么我们再执行以下代码:
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是
浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。那么我们对于上面的代码,要使自定义类型也可以使用的话就可以这样写:
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的构造函数重载的问题
我们在模拟实现构造函数时有三个函数的实现:
//构造
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>的对象时,编译器会调用最合适它的一个函数,比如这段代码:
MyTest::my_vector<int> v(4, 6); // 调用的是第三个构造函数
这里会优先调用的是第三个构造函数,但第三个函数是必须要用迭代器才能正常使用的,否则就不会正常运行:
所以在我们模拟实现vector的构造函数时,就需要注意这个情况。
在vector的库里面解决这个问题的方法就是将第二个函数进行了改进,就是将第二个函数进行重载一下,即以下代码:
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;}
}
感谢各位观看!希望能多多支持!