【探索C++容器:vector的使用和模拟实现】(二)

简介: 【探索C++容器:vector的使用和模拟实现】

【探索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

相关文章
|
6天前
|
存储 安全 Java
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
|
6天前
|
调度 C++ 容器
【C++】手搓 list 容器
本文我们实现了STL库中重要的list 的模拟实现,其中最重要莫过于迭代器的封装类的书写,这是前所未有的操作(对于我来说,我是第一次使用这种结构)。通过list 的模拟实现也帮我们巩固了类与对象的知识,也强化了指针操作的思路。欢迎大家讨论分析。
14 1
|
6天前
|
编译器 C++ Windows
【C++】vector问题解决(非法的间接寻址,迭代器失效 , memcpy拷贝问题)
不使用memcpy函数不就可以了,然后我们使用简单粗暴的赋值拷贝,这样就不会发生浅拷贝问题了!!!
18 1
|
6天前
|
存储 C++ 容器
【C++】vector容器初步模拟
我们初步完成了对vector 的模拟实现,但是依然有问题,比如不支持string等特殊类型。所以下一篇文章我们来一起完善一下。
15 0
【C++】vector容器初步模拟
|
6天前
|
存储 设计模式 算法
【C++/STL】stack和queue(容器适配器、优先队列、双端队列)
【C++/STL】stack和queue(容器适配器、优先队列、双端队列)
16 1
|
6天前
|
算法 C++ 容器
【C++/STL】vector(常见接口、模拟实现、迭代器失效)
【C++/STL】vector(常见接口、模拟实现、迭代器失效)
12 0
|
6天前
|
存储 算法 C++
详解C++中的STL(标准模板库)容器
【4月更文挑战第30天】C++ STL容器包括序列容器(如`vector`、`list`、`deque`、`forward_list`、`array`和`string`)、关联容器(如`set`、`multiset`、`map`和`multimap`)和容器适配器(如`stack`、`queue`和`priority_queue`)。它们为动态数组、链表、栈、队列、集合和映射等数据结构提供了高效实现。选择合适的容器类型可优化性能,满足不同编程需求。
|
6天前
|
存储 算法 程序员
C++从入门到精通:2.2.1标准库与STL容器算法深度解析
C++从入门到精通:2.2.1标准库与STL容器算法深度解析
|
6天前
|
存储 缓存 编译器
【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比
【C++进阶(五)】STL大法--list模拟实现以及list和vector的对比
|
6天前
|
编译器 C++
【C++进阶(三)】STL大法--vector迭代器失效&深浅拷贝问题剖析
【C++进阶(三)】STL大法--vector迭代器失效&深浅拷贝问题剖析

热门文章

最新文章