从零实现一个完整的vector类:深入理解C++动态数组
从零实现一个完整的vector类:深入理解C++动态数组
在C++编程中,vector是最常用的容器之一,它提供了动态数组的功能。今天我们将从实现一个相对完整的vector类来深入理解vector的封装与使用,并在过程中解决各种实际问题。
整体代码框架
首先,让我们看看vector类的整体结构:(用这份伪代码进行理解性默写手撕可以加深对vector的理解)ps:完整源码在文章最下方
cpp
namespace ddd
{template<class T>class vector{public:// 迭代器相关typedef T* iterator;typedef const T* const_iterator;// 构造函数和析构函数vector();vector(int n, const T& val = T());template<class InputIterator>vector(InputIterator first, InputIterator last);vector(const vector<T>& v);vector<T>& operator=(vector<T> v);~vector();// 容量相关size_t size() const;size_t capacity() const;void reserve(size_t n);void resize(size_t n, const T& val = T());// 元素访问T& operator[](size_t pos);const T& operator[](size_t pos) const;// 修改操作void push_back(const T& x);void pop_back();void swap(vector<T>& v);iterator insert(iterator pos, const T& x);iterator erase(iterator pos);// 迭代器方法iterator begin();iterator end();const_iterator cbegin() const;const_iterator cend() const;private:iterator _start; // 指向数据块的开始iterator _finish; // 指向有效数据的尾iterator _endOfstorage; // 指向存储容量的尾};
}构造函数:创建vector的不同方式
默认构造函数
cpp
vector() :
_start(nullptr)
, _finish(nullptr)
, _endOfstorage(nullptr)
{}最简单的构造函数,用初始化列表创建一个空的vector,所有指针都初始化为nullptr。
指定大小和初始值的构造函数
cpp
vector(int n, const T& val = T()) {reserve(n);for(size_t i = 0; i < n; i++) {push_back(val);}
}这个构造函数创建包含n个元素的vector,每个元素都初始化为val。如果val没有提供,就使用T类型的默认值(缺省参数)。
可能看不懂T():
T()表示调用模板参数T类型的默认构造。
迭代器范围构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last) {while(first != last) {push_back(*first);++first;}
}关键理解:比方说我们有一个数组arr,里面有arr[1,2,3,4,5]。
我们可以不用知道*first具体是什么,只需利用模板的威力:编译时只需传入对应指针,它就会为每种迭代器生成对应的代码,例如传入(arr,arr+5),它就自动将arr的内容用于构造一个内容基本一致的vector。
这个构造函数可以从任何迭代器范围创建vector,比如数组、list等其他容器的迭代器。
拷贝构造函数
cpp
vector(const vector<T>& v) {reserve(v.capacity());for(auto& e : v) {push_back(e);}
}创建一个与参数v相同的vector,需要分配相同大小的内存并复制所有元素。
用法示例:v1(v2);即使用v2的内容直接构造一个对象v1。
这一个构造类似于赋值
析构函数:资源清理
cpp
~vector() {delete[] _start;_start = _finish = _endOfstorage = nullptr;
}析构函数负责释放vector动态分配的内存,并将所有指针设为nullptr,防止悬空指针。
迭代器:让vector支持范围遍历
cpp
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator cbegin() const { return _start; }
const_iterator cend() const { return _finish; }end()返回的是_finish,而_finish指向最后一个元素的下一个位置(也就是说_finish和end()本质差不多)
容量操作:管理内存大小
获取大小和容量
size_t size() const { return _finish - _start; }
size_t capacity() const { return _endOfstorage - _start; }通过指针相减来计算元素个数和总容量,既简单又高效。
size()可以理解为表示当前vector当中存储的数据量,capacity()表示vector可以存储的最大量
预留空间
cpp
void reserve(size_t n) {if (n > capacity()) {T* tmp = new T[n];size_t sz = size();
for(size_t i = 0; i < sz; i++) {tmp[i] = _start[i];}
delete[] _start;_start = tmp;_finish = _start + sz;_endOfstorage = _start + n;}
}关键点:只有在请求的容量大于当前容量时才重新分配内存,避免不必要的(过多的)内存操作。
注意此操作会导致_start的位置有所改变。(此处不用纠结,后面会再次提到)
调整大小
cpp
void resize(size_t n, const T& val = T()) {if (n <= size()) {_finish = _start + n; // 缩小size}else {reserve(n); // 扩大容量while (_finish < _start + n) {*_finish = val; // 用val填充新增位置++_finish;}}
}resize可以增大或减小vector的大小,新增的元素用val初始化。
如果说要增大空间,我们就直接复用reserve()
元素访问:像数组一样使用vector
cpp
T& operator[](size_t pos) {assert(pos < size());return _start[pos];
}const T& operator[](size_t pos)const {assert(pos < size());return _start[pos];
}通过重载[]运算符,vector可以像普通数组一样使用。assert确保不会越界访问,是一种强制检查,越界就直接报错并指出报错位置,也方便调试。
修改操作:动态改变vector内容
添加和删除元素
cpp
void push_back(const T& x) {insert(end(), x); // 在末尾插入
}void pop_back() {erase(end() - 1); // 删除最后一个元素
}在类内部,我们可以直接使用end()等价于_finish,因为end()返回的就是_finish。
插入元素
cpp
iterator insert(iterator pos, const T& x) {assert(pos >= _start && pos <= _finish);if (_finish == _endOfstorage) {size_t len = pos - _start; // 记录相对位置reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len; // 重新计算位置}iterator end = _finish - 1;while (end >= pos) {*(end + 1) = *end; // 向后移动元素--end;}*pos = x; // 插入新元素++_finish;return pos;
}关键技巧:在扩容前用len记录插入位置的相对偏移量,然后再用新的_start+len来定位新的pos。原因:因为reserve会改变_start的地址,使原来的迭代器失效。
删除元素
cpp
iterator erase(iterator pos) {assert(pos >= _start && pos < _finish);iterator it = pos + 1;while (it < _finish) {*(it - 1) = *it; // 向前移动元素++it;}--_finish;return pos;
}注意:erase的断言条件与insert不同,因为不能删除end()位置(它没有元素)。
交换操作
cpp
void swap(vector<T>& v) {std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endOfstorage, v._endOfstorage);
}交换操作只需交换三个指针,非常高效,常用于实现拷贝赋值运算符。
拷贝赋值运算符
cpp
vector<T>& operator= (vector<T> v) {swap(v);return *this;
}巧妙设计:使用传值参数,让编译器自动创建临时副本,然后通过交换内容来实现赋值,既简单又安全。
测试我们的vector
cpp
void test_vector() {// 测试各种构造函数ddd::vector<int> v1;ddd::vector<int> v2(5, 10);int arr[] = {1, 3, 5, 7, 9};ddd::vector<int> v3(arr, arr + 5);// 测试修改操作v3.push_back(11);v3.insert(v3.begin() + 2, 99);v3.erase(v3.begin() + 1);// 测试容量操作v3.resize(10, 100);// 测试迭代器for(auto it = v3.begin(); it != v3.end(); ++it) {std::cout << *it << " ";}
}int main() {test_vector();return 0;
}总结
通过从头实现vector,我们深入理解了:
动态内存管理:new/delete的使用和内存分配策略
迭代器失效:插入删除操作对迭代器的影响
模板编程:如何编写通用的容器类
异常安全:通过swap技巧实现安全的赋值操作
性能优化:合理的扩容策略和内存管理
完整源码:
namespace ddd
{
template<class T>
class vector
{
public:
// Vector的迭代器是一个原生指针typedef T* iterator;typedef const T* const_iterator;iterator begin(){return _start;}iterator end(){return _finish;}const_iterator cbegin() const{return _start;}const_iterator cend() const{return _finish;}
// construct and destroyvector(){}vector(int n, const T& val = T()){reserve(n);for(size_t i=0;i<n;i++){push_back(val);}}template<class InputIterator>vector(InputIterator first, InputIterator last){while(first!=last){push_back(*first);++first;}}vector(const vector<T>& v){reserve(v.capacity());for(auto& e:v){push_back(e);}}vector<T>& operator= (vector<T> v){swap(v);return *this;}~vector(){delete[] _start;_start=_finish=_endOfstorage=nullptr;}// capacitysize_t size() const{return _finish-_start;}size_t capacity() const{return _endOfstorage-_start;}void reserve(size_t n) {if (n > capacity()) {T* tmp = new T[n];size_t sz = size();if (_start) {for (size_t i = 0; i < sz; i++) {tmp[i] = _start[i];}delete[] _start;}
_start = tmp;_finish = _start + sz;_endOfstorage = _start + n;}}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;}}}
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 push_back(const T& x) {insert(end(), x);}void pop_back() {erase(end() - 1);}void swap(vector<T>& v){std::swap(_start,v._start);std::swap(_finish,v._finish);std::swap(_endOfStoratge,v._endOfStorage);}iterator insert(iterator pos, const T& x) {assert(pos >= _start);assert(pos <= _finish);if (_finish == _endOfstorage) {size_t len = pos - _start;reserve(capacity() == 0 ? 4 : capacity() * 2);pos = _start + len;}//容量不足扩容//需要用len记录位置,是因为reserve后会改变原型_start的位置iterator end = _finish - 1;while (end >= pos) {*(end + 1) = *end;--end;}//向后挪动覆盖,腾出空间插入*pos = x;++_finish;}iterator erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);//此处没有=号了,因为insert是可以尾插,而erase必须有元素可删除iterator it = pos + 1;while (it < end()) {*(it -1 ) = *it;++it;}//向前挪动覆盖--_finish;return pos;}
private:
iterator _start; // 指向数据块的开始iterator _finish; // 指向有效数据的尾iterator _endOfStorage; // 指向存储容量的尾
};
}
