【探索C++容器:vector的使用和模拟实现】(一):https://developer.aliyun.com/article/1425779
那我们的vector可行吗?可行。
void test8() { vector<string> vstr; string s1("张三"); vstr.push_back(s1); vstr.push_back(string("李四")); vstr.push_back("王五"); for (auto e : vstr) { cout << e << endl; } cout << endl; }
我们上面的程序vstr将每个string赋值个e,这里就会存在深拷贝的问题,消耗较大,所以我们可以使用引用,并且这里我们不希望值会被更改,所以加上const。
void test8() { vector<string> vstr; string s1("张三"); vstr.push_back(s1); vstr.push_back(string("李四")); vstr.push_back("王五"); for (const auto& e : vstr) { cout << e << endl; } cout << endl; }
运行结果:
如果我们想访问第一个元素呢?此时e就是一个string,访问第一个字符就可以通过[ ]访问。
void test8() { vector<string> vstr; string s1("leetcode"); vstr.push_back(s1); vstr.push_back(string("our")); vstr.push_back("vector"); for (const auto& e : vstr) { cout << e[0]; } //还可以这样访问 //第一个[]调用vector的[],找到string //的二个[]调用string的[],找到char cout << vstr[0][1]; }
运行结果:
这里就存在一个问题了,能否用vector代替string类呢?
这里是不行的,string 类在 C++ 中是一个标准库提供的字符串类型,它自动管理字符串的长度,包括在末尾添加 null 终止符 '\0'。这使得 string 对象可以方便地与 C 风格的字符串互操作。而 vector 是一个通用的动态数组,它只是存储一系列的字符,不会自动在末尾添加 null 终止符。因此,如果你试图用 vector 来代替 string,你可能会遇到一些问题,尤其是在与 C 风格字符串函数或其他字符串处理函数的交互时,所以不能使用vector代替string。
2.vector深度剖析及模拟实现
我们先来看一下vector实现的源码,看一个容器的源码,我么首先提出几点:
- 1.不要一行一行看
- 2.不要看太多细节,这个阶段我们的水平有一点不匹配,待我们的代码经验更丰富再去看细节
- 3.学会看框架
我们首先要看类的成员变量
然后再看构造函数,我们去看看vector是怎么初始化的。
最后我再看看插入逻辑 - push_back
此时我们就能大概猜到上面成员变量的大概意思,start是指向数组开始的位置,finish是指向有效元素个数的下一个位置,end_of_storage是指向空间结束位置。
现在我们就开始手撕vector的实现。首先为了防止和库里面的vector容器冲突,我们直接使用命名空间解决这个问题。
然后我们再来写一下成员变量。
namespace yu { template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; private: iterator _start; iterator _finish; iterator _end_of_storage; }; }
我们先来实现一下构造函数,这里直接无参时直接初始化为空指针即可。
vector() :_start(nullptr) ,_finish(nullptr) ,_end_of_storage(nullptr) {}
除了无参的构造函数,库里面还存在其他的构造函数。
我们先来实现一下迭代器区间构造函数。
//迭代器区间构造 //单独添加模板作为参数 template <class InputIterator> vector(InputIterator first, InputIterator last) { while (first != last) { push_back(*first); ++first; } }
运行结果:
这里为什么不直接给迭代器,而要给一个模板参数呢?这里要支持多种形式,比如数组或者链表,数组这里可以是因为数组底层物理空间是连续的,属于天然迭代器。
接着实现一下n个val初始化
vector(size_t n, const T val = T()) { resize(n, val); } void test_vector11() { vector<string> v1(5, "123"); for (auto e : v1) { cout << e << " "; } cout << endl; vector<int> v2(5, 1); for (auto e : v2) { cout << e << " "; } cout << endl; }
运行我们的程序发现报错了,我们发现v2调用了迭代器区间构造,因为它更匹配v2。
怎么修改呢?参数不使用size_t,使用重载int就可以解决。
// 使用int重载函数 vector(int n, const T val = T()) { resize(n, val); } void test_vector11() { vector<string> v1(5, "123"); for (auto e : v1) { cout << e << " "; } cout << endl; vector<int> v2(5, 1); for (auto e : v2) { cout << e << " "; } cout << endl; }
再来实现一下析构函数
~vector() { if(_start) { delete[] _start; _start = nullptr; _finish = nullptr; _end_of_storage = nullptr; } }
vector的本质就是顺序表,在我们之前写的顺序表中,除了指向这个顺序表的指针,同时还存在了size和capacity这两个重要的变量,因此在这里我们也要有能获取size和capacity的函数。在这之前我们先复习一下,还记得我们的strlen是干嘛的吗?还记得是怎么实现的吗?
//模拟实现strlen int mystrlen(char* str) { assert(str != nullptr); char* begin = str; while (*str != '\0') { ++str; } return str - begin; } int main() { //strlen是获取字符串的个数,不包括'\0' char str[] = "hello!"; printf("strlen(str):%d\n", strlen(str)); printf("mystrlen(str):%d\n", mystrlen(str)); return 0; }
运行结果:
通过上面的模拟实现我们想证明什么呢?我想说的是指针相减是之间元素的个数,begin指向'h'的位置,而str指向'\0'的位置,它们之间相减就是元素个数,而我们上面的vector的成员变量都是指针,因此我们也可以通过指针相减获取size和capacity。
//指针相减是之间的元素个数。 size_t size() const { return _finish - _start; } size_t capacity() const { return _end_of_storage - _start; }
由于在获取size和capacity的时候,我们没有改变对象的属性,因此此时可以加上const修饰。现在我们再想实现尾插,尾插必定要有扩容的动作,所以这里我们先实现reserve函数(不缩容版本)。我们在模拟实现string类的时候已经实现过reserve函数,reserve函数的实现四个步骤:
- 1.开辟新空间
- 2.拷贝数据
- 3.释放旧空间
- 4.指向新空间
void reserve(size_t n) { if(n > capacity()) { T* temp = new T[n]; if(_start) memcpy(temp,_start,n*sizeof(T)); delete[] _start; _start = temp; _finish = _start + size(); _end_of_storage = _start + capacity(); } }
此时我们的开辟空间的函数就已经写成了,然后我们在来实现我们的尾插push_back函数,注意我们这里实现的是二倍扩容。
void push_back(const T& x) { if(_finish == _end_of_storage) { //开辟空间 size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2; reserve(newcapacity); } *_finish = x; ++_finish; }
这里就有一个问题了,我们在push_back函数已经判断了newcapacity必定是大于capacity(),可是在reserve函数我们又再次判断了一下,这是不是有一点多余呀!不多余,因为reserve函数除了push_back函数会使用,也可以单独使用,此时就需要另做判断。为了方便观察到我们是否成功插入函数,这里我们需要写一下迭代器begin和end。
iterator begin() { return _start; } iterator end() { return _finish; }
现在就可以测试我们的vector的尾插功能了。
void test_vector1() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); vector<int>::iterator it = v.begin(); while(it != v.end()) { cout << *it << " "; ++it; } cout << endl; } int main() { yu::test_vector1(); return 0; }
此时我们运行我们的程序,发现程序崩溃了。为什么呢?我们调试一下。
很明显我们发现我们开空间没有开辟成功,_finish为空指针,在尾插的时候进行了解引用操作,所以程序就会崩溃,所以就是eserve函数出现了问题。
【探索C++容器:vector的使用和模拟实现】(三):https://developer.aliyun.com/article/1425783