因为我们要实现一个 模板,则不建议将 声明和定义分离至两个文件(如上几期实现string那样)
否则容易引起编译链接等错误
1、vector 的核心框架实现(vector 类模板)
#include<iostream> #include<vector> #include<string> #include<assert.h> using namespace std; namespace my { template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; private: iterator _start; //指向有效数据区域的开始(相当于 begin()) iterator _finish; //指向有效数据的最后一个有效元素的下一个位置(相当于 end()) iterator _end_of_storage; //指向总存储空间的最后 }; }
关于私有成员
先看 vector 的源码
三个私有成员都是使用 迭代器实现(不像之前的 string 类中的写法)
2. vector 元素 增加删除的相关函数
- reserve:扩容,增加总空间
- size :获取当前 vector 中有效数据的空间大小
- capacity:获取 总空间大小
- push_back / pop_back:尾插 和 尾删
- insert / erase : 指定 pos 位置,删除该位置的元素
- find:给定迭代器区间,查找值为 val 的元素,返回下标迭代器
(1)size 和 capacity
size_t size() {
return _finish - _start;
}
size_t capacity() {
return _end_of_storage - _start;
}
(2)reserve:扩容,增加总空间
void reserve(size_t n)
{
if (n > capacity) {
T* tmp = new T[n];
//strcpy(tmp, _start); // 这是字符串的拷贝函数,不能使用在这里
memcpy(tmp, _start, sizeof(T) * n); // 使用memcpy拷贝数据
delete[] _start;
_start = tmp;
_finish = _start + size();
_end_of_storage = _start + n; // n 就是 新的空间的 capacity
}
}
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
关于需要设置 oldSize(旧空间大小)
上面那段代码会程序崩溃
问题出在这
_finish = _start + size();
size() == _finish - _start; //代入后,相当于 _finish 原地不动 _finish = _start + _finish - _start = _finish;
我们更新 _finish 的原理是,使用新的 _start 间隔 一块已有有效数据空间的大小 size 的距离
而当前面的 _start 更新成新空间的位置时,
_start = tmp;
函数 size() 里面的 return 值也发生改变,其中的 _start 已经不是原有空间的 _start 了,变成新的了
所以我们需要的是 原有空间的 size ,而直接使用 size函数当然计算不出
因此,我们需要使用一个临时值,来存储旧 size 的大小
代码修改为
void reserve(size_t n) { if (n > capacity) { size_t oldsize = size(); / T* tmp = new T[n]; if (_start) { memcpy(tmp, _start, sizeof(T) * n); delete[] _start; } _start = tmp; _finish = _start + oldsize; / _end_of_storage = _start + n; // n 就是 新的空间的 capacity } }
关于拷贝数据的方式
memcpy 可以逐字节的拷贝数据
但是因为 memcpy 是浅拷贝,对一些自定义类型有影响(一些自定义类型需要深拷贝)
因此,这里换成循环,每次拷贝一个元素类型大小的数据
for (size_t i = 0; i < oldSize; ++i) { tmp[i] = _start[i]; }
最终代码
void reserve(size_t n) { if (n > capacity()) { size_t oldSize = size(); T* tmp = new T[n]; //memcpy(tmp, _start, sizeof(T) * oldSize); // 因为 memcpy 是浅拷贝,对一些自定义类型有影响,换成循环 for (size_t i = 0; i < oldSize; ++i) { tmp[i] = _start[i]; } delete[] _start; // 注意:这里 delete 如果会报错,好像是因为没写析构 _start = tmp; _finish = _start + oldSize; _end_of_storage = _start + n; } }
(3)push_back / pop_back:尾插 和 尾删
void push_back(const T& val) { //空间不够就扩容 if (_finish == _end_of_storage) { size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2; reserve(newCapacity); } *_finish = val; // 尾巴赋值 _finish++; // 尾巴后移 } void pop_back() { assert(capacity() > 0); _finish--; }
(4)insert / erase : 指定 pos 位置,删除该位置的元素
我们模拟库中的 vector ,使用迭代器,会比直接使用 size_t 类型的数字下标更加安全
// 插入 void insert(iterator pos, const T& val) { assert(pos <= _finish); assert(pos >= _start); // 扩容 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--; } ++_finish; // 如果扩容了,pos 还指向旧空间:迭代器失效 *pos = val; } // 删除 iterator erase(iterator pos) { assert(pos < _finish); assert(pos >= _start); // 元素向前 移 // 因为可能会缩容,导致 pos 指向已释放的空间 // 库中的 erase 会返回刚刚删除的元素位置的下一个位置 iterator end = pos + 1; while (end < _finish) { *(end - 1) = *(end); end++; } _finish--; return pos + 1; }
关于 insert 扩容处 添加了 len
这是扩容产生的 迭代器失效问题,需要将 pos 重新移动位置(即现在 pos 指向已释放的空间,造成野指针形成的 迭代器失效问题 ,需要将 pos 指向新空间)
关于 erase 返回 iterator
因为删除数据,可能会导致缩容,也会使 pos 指向已释放的空间,产生 迭代器失效 的情况
如何解决?
库中的 erase 会返回刚刚删除的元素位置的下一个位置,我们模拟库中的写法,返回一个 iterator
(5)find:给定迭代器区间,查找值为 val 的元素,返回下标迭代器
在本文中,使用 find 配合 erase 删除某个元素
看文档中,find 是一种函数模板
template<class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& val) { while (first != last) { if (*first == val) return first; ++first; } return last; }
配合 erase 使用
如在 v1 中,先找到值为 3 的数据,然后删除
void Test() { vector<int>v1; v1.push_back(1); v1.push_back(2); v1.push_back(3); v1.push_back(4); for (auto e : v1) { cout << e << ' '; } cout << '\n'; // 删除 + find vector<int>::iterator pos = find(v1.begin(), v1.end(), 3); v1.erase(pos); for (auto e : v1) { cout << e << ' '; } cout << '\n'; }
3. 构造 + 拷贝构造 + 析构
vector() = default; // 拷贝构造 vector(vector<T>&v) { reserve(v.capacity()); for (auto e : v) { push_back(e); } } // 析构 ~vector() { if (_start) { delete[] _start; _start = _finish = _end_of_storage = nullptr; } }
关于 vector() = default
default 关键字:使编译器强制生成默认函数
因为在 vector 的实现中,直接使用默认生成的构造函数已经足够用,无需自己写
但是当我们显式实现 拷贝构造函数时,编译器不会默认生成构造函数
因此要使用关键字 default 强制生成
4. 迭代器 begin() / end()
这里实现 iterator 和 const_iterator 两种
iterator begin() { return _start; } iterator end() { return _finish; } const_iterator begin() const { return _start; } const_iterator end() const { return _finish; }
5. 迭代器区间拷贝
这是一种函数模板:可以将传递过来的迭代器区间内的 数据 拷贝
template<class inputiterator> vector(inputiterator first, inputiterator last) { while (first != last) { push_back(*first); first++; } }
6. 模拟库中的 拷贝构造
产生数量 n 个元素 ,初始化为 val,否则使用缺省值初始化
size_type 就是 size_t
value_type 就是 T
// 模拟库的构造函数:vector (size_type n, const value_type& val = value_type() vector(size_t n, const T& val = T()) { reserve(n); // 和拷贝构造那里一样,先使用 reserve for (size_t i = 0; i < n; ++i) { push_back(val); } }
关于缺省值
这里的缺省值能不能直接给 零?
vector(size_t n, const T& val = 0);
若 T 为 int 的整型类型,给 0 确实可以
但是 T 为 string 呢?T 为 set 呢?
因此:一个 整型类型的 0 不能初始化所有种类的类型
这里修改
vector(size_t n, const T& val = T());
这个意思是:给自定义类型创建一个临时的匿名对象,初始化为这个
int 这种 内置类型可以这样写吗?如 int()
可以,C语言不支持,C++可以
就是因为出现 模板 这个语法,有了泛型编程的概念
T 可以是任意类型,初始化时会调用构造函数,T 可以是 自定义类型时如此,T 也可以是 内置类型呀
可是内置类型不是没有构造函数吗
因此为了兼容模板, C++ 对内置类型进行升级,使内置类型也有构造函数,也可以像下面这样初始化
int i = 10; int j(10); int k = int(); int x = int(2);
关于函数匹配冲突问题
从上面的文章写下来的代码中
如果你这样写,会报错:非法的间接寻址
vector<int>v3(3, 5);
这个函数会自动匹配上面讲解过的 迭代器区间拷贝函数
// 迭代器区间拷贝 template<class inputiterator> vector(inputiterator first, inputiterator last) { while (first != last) { push_back(*first); first++; } }
为什么不匹配 拷贝构造函数,而是上面这个?
vector(size_t n, const T& val = T()) { reserve(sizeof(T) * n); // 一次开好空间,避免频繁扩容 for (size_t i = 0; i < n; ++i) { push_back(val); } }
因为在编译器的眼中,参数类型更加匹配的函数,会优先被选择
前者参数类型都是统一的 inputiterator
后者不一样:一个是 size_t,一个是 T(这里的 T = int)
因此优先匹配前者
但是使用一个 int 类型放到 一个关于迭代器的函数,就会出现一些语法错误:非法的间接寻址
在函数里,就是将 int 解引用使用了
如何避免冲突?
既然是类型惹的祸,可以从类型方面思考
我们可以添加一个新的重载函数(将第一个参数的 size_t 类型换成 int),这样编译器就会优先选择这个新的函数
vector(int n, const T& val = T()) { reserve(sizeof(T) * n); // 一次开好空间,避免频繁扩容 for (size_t i = 0; i < n; ++i) { push_back(val); } }
7. 赋值重载 + 重载方括号
赋值重载
这个是一种 STL 的现代写法,不知道的可以了解一下
vector<T>& operator=(const vector<T>& v) { vector<T> tmp(v); swap(tmp); return *this; } void swap(vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_end_of_storage, v._end_of_storage); }
重载方括号
这个重载函数使 vector 可以像数组一样直接使用下标访问
一种是访问了需要修改:传引用
一种只需访问无需修改:const 修饰
// 重载方括号 T& operator[](size_t pos) { assert(pos < size()); assert(pos >= 0); return _start[pos]; } const T& operator[](size_t pos) const { assert(pos < size()); assert(pos >= 0); return _start[pos]; }
8. (重点)关于花括号的隐式类型转换 与 initializer_list< T >
观察下面代码:关于对象的直接构造 / 隐式类型转换 / 花括号的隐式类型转换
class A { public: A(int n) :_a1(n) , _a2(0) {} A(int x, int y) :_a1(x) , _a2(y) {} private: int _a1; int _a2; }; void Test7() { // 直接构造 A a1(1); A a2(1, 2); // 单参数 和 多参数对象隐式类型转换 A a3 = 1; A a4 = { 1, 2 }; // C++11:提出一切皆可用花括号初始化(可以说,遇到花括号基本是 隐式类型转换) // 这几个是单参数的隐式类型转换(不建议单参数使用花括号操作,但看到别人代码这样写要懂) A a5 = { 1 }; A a6{ 1 }; // 省略掉等号 A a7{ 1, 2 }; // 省略掉等号 const A& a8 = { 1 }; // 引用的是临时对象(常性) }
我们使用本章学的 vector 试试:看看花括号能不能初始化 vector
//my::vector<int> v1 = { 1, 2, 3 }; // 报错 std::vector<int> v1 = { 1, 2, 3 }; // 用库里面的 vector 却不报错
甚至,花括号里面给多少值都正确,难道是库里面的 vector 构造函数 有好多参数(甚至无穷个)供你一一匹配吗?
std::vector<int> v1 = { 1, 2, 3 }; std::vector<int> v1 = { 1, 2, 3, 4}; std::vector<int> v1 = { 1, 2, 3, 4, 5}; std::vector<int> v1 = { 1, 2, 3, 4, 5, .....};
解释:
这种也是一种隐式类型转换,但是这里的参数匹配不是按花括号里面的几个数字一一匹配的,花括号括起来的所有值一起算作一个整体匹配到一个 参数中(而数字之间并非独立的参数个体)
这个“强大的”参数是 C++11新增的语法:initializer_list< T > 初始值设定项列表
大致基础功能,就和直接传一个 数组过去差不多(“该数组”里面存储着你自定义的几个值)
使用示范
auto in1 = { 1, 2, 3, 4 }; initializer_list<int> in2 = { 1, 2, 3, 4, 5 };
该函数模板 内部实际有两个指针,一个指向头,一个指向尾
前面为什么说 这个也算作隐式类型转换?
因为这也是一种类型,可以直接隐式类型转换
库中的vector:
vector (initializer_list<value_type> il)
std::vector<int> v1 = { 1, 2, 3 }; // 上面这样写,就相当于传一个参数过去,如下面 std::vector<int> v1({ 1, 2, 3 });
因此,仿照这个,我们自己模拟实现一个支持initializer_list 的 vector 构造函数
// 支持 initializer_list 的构造 vector(initializer_list<T> il) { reserve(il.size()); for (auto e : il) { push_back(e); } }
9. 总代码
vector.h
#pragma once #include<iostream> #include<vector> #include<string> #include<assert.h> using namespace std; /* * 开发日志 * 、基础模板框架 * 、迭代器 * 、空间增长相关函数析构(push_back / pop_back + 插入删除(同时实现 find)) + 析构 * 、拷贝构造:模拟库的构造函数(+int型的重载,为了不错误匹配)+ 原始的拷贝构造 * 、一般构造 + 支持 initializer_list 的构造 * 、赋值重载 + 重载方括号 * 、迭代器区间 */ namespace bit { template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; // 析构 ~vector(){ if (_start) { delete[] _start; _start = _finish = _end_of_storage = nullptr; } } // 支持 initializer_list 的构造 vector(initializer_list<T> il) { reserve(il.size()); for (auto e : il) { push_back(e); } } // 1、迭代器 iterator begin() { return _start; } iterator end() { return _finish; } const_iterator begin() const { return _start; } const_iterator end() const { return _finish; } // 2、空间增长相关函数 size_t size(){ return _finish - _start; } size_t capacity(){ return _end_of_storage - _start; } void reserve(size_t n) { if (n > capacity()) { size_t oldSize = size(); T* tmp = new T[n]; //memcpy(tmp, _start, sizeof(T) * oldSize); // 因为 memcpy 是浅拷贝,对一些自定义类型有影响,换成循环 for (size_t i = 0; i < oldSize; ++i) { tmp[i] = _start[i]; } delete[] _start; // 注意:这里 delete 会报错,好像是因为没写析构 _start = tmp; _finish = _start + oldSize; _end_of_storage = _start + n; } } void push_back(const T& val) { //空间不够就扩容 if (_finish == _end_of_storage) { size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2; reserve(newCapacity); } *_finish = val; _finish++; } void pop_back() { assert(capacity() > 0); _finish--; } // 插入 void insert(iterator pos, const T& val) { assert(pos <= _finish); assert(pos >= _start); // 扩容 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--; } ++_finish; // 如果扩容了,pos 还指向旧空间:迭代器失效 *pos = val; } // 删除 iterator erase(iterator pos) { assert(pos < _finish); assert(pos >= _start); // 元素向前 移 // 因为可能会缩容,导致 pos 指向已释放的空间 // 库中的 erase 会返回刚刚删除的元素位置的下一个位置 iterator end = pos + 1; while (end < _finish) { *(end - 1) = *(end); end++; } _finish--; return pos + 1; } // 看文档中,find 是一种函数模板 template<class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& val) { while (first != last) { if (*first == val) return first; ++first; } return last; } // 3、拷贝构造 // 3.1 一般拷贝构造 vector() = default; vector(vector<T>& v) { reserve(v.capacity()); for (auto e : v) { push_back(e); } } void swap(vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_end_of_storage, v._end_of_storage); } // 2.2 模拟库的拷贝构造:给数量,初始化 vector(size_t n, const T& val = T()) { reserve(sizeof(T) * n); // 一次开好空间,避免频繁扩容 for (size_t i = 0; i < n; ++i) { push_back(val); } } // 2.3 避免冲突 vector(int n, const T& val = T()) { reserve(sizeof(T) * n); // 一次开好空间,避免频繁扩容 for (size_t i = 0; i < n; ++i) { push_back(val); } } // 4. 迭代器区间拷贝 template<class inputiterator> vector(inputiterator first, inputiterator last) { while (first != last) { push_back(*first); first++; } } // 5、赋值重载 + 重载方括号 // 赋值重载 vector<T>& operator=(const vector<T>& v) { vector<T> tmp(v); swap(tmp); return *this; } // 重载方括号 T& operator[](size_t pos) { assert(pos < size()); assert(pos >= 0); return _start[pos]; } const T& operator[](size_t pos) const { assert(pos < size()); assert(pos >= 0); return _start[pos]; } private: iterator _start; iterator _finish; iterator _end_of_storage; }; }