初阶C++——STL——string类、vector类和list类(使用方法+模拟实现+测试+思路分析)

简介: Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。

目录


STL简介


STL版本


STL的六大组件:


STL的缺陷:(了解)


string类


介绍


string类的常用结构说明


1、常见构造类


2、容量操作类


3、string类对象的访问及遍历操作


4、string类对象的修改操作


5、string类非成员函数



string类的模拟实现


vector的使用


vector常用结构说明


1、vector定义(构造)类


2、vector与string相类似的部分


3、vector 迭代器失效问题。


vector的模拟实现


list类


list 的用法简介


list的构造函数


list的迭代器


其他成员函数


list的模拟实现(重头戏)


在介绍之前,我们可以先来了解一下,何为STL?它有多少个版本?以及它的优势和缺陷。


STL简介

image.png


STL版本

截至今天,目前认为:STL有四大版本:


原始版本(HP版本)

Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。


P. J. 版本

由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。


RW版本

由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。


SGI版本

由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。


STL的六大组件:

仿函数;算法;迭代器;空间配置器;容器;配接器。


STL的缺陷:(了解)

1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。

2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。

3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。

4. STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的


今天,我们将学习其中的三个类——string类、vector类、list类。


string类

介绍

我们先来说一说它的用法,然后再来讲解其模拟实现。


首先我们来简单地介绍一下string类:(了解)


1. 字符串是表示字符序列的类

2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。

3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。

4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参basic_string)。

5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。


string类的常用结构说明

1、常见构造类

我们可以访问相关的网站来看一下:

image.png



框~~可以看到,有这么多函数都可以作为构造函数。


我们介绍几个常用的:

image.png



重点来看这么几个:


微信图片_20221209143122.png


像上面几种写法,都是可以的。


注意,其隐藏了'\0',用户即便在调式的时候也是看不见的。


实际上,还可以直接用赋值操作,编译器底层会将其优化为先调用一次构造函数,再调用一次拷贝构造。


2、容量操作类

image.png


可以看到,又有不少。


我们介绍一下下面这几个,并举出例子(一块说了)


注意:


1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。

2. clear()只是将string中有效字符清空,不改变底层空间大小。

3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。

4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于

string的底层空间总大小时,reserver不会改变容量大小。


3、string类对象的访问及遍历操作


我们主要来介绍上面这么几个:


上面8个分别是返回正向、逆向、常量的迭代器。


下面的是运用的操作符重载函数完成的。


说到这个访问,于是我们就引出了三种常见的遍历访问方式:

image.png微信图片_20221209143400.png

#include<iostream>
#include<string>
using namespace std;
int main()
{
  string s = "hello jxwd"; //其会转换成temp("hello jxwd"),然后拷贝构造到s中.
  //1、迭代器
  string::iterator it = s.begin();//也可以去定义反向迭代器,反向遍历。
  while (it != s.end())
  {
  cout << *it;
  it++;
  }
  cout << endl;
  //2、for+operator[]
  for (size_t i = 0; i < s.size(); i++)
  {
  cout << s[i];
  }
  cout << endl;
  //3、范围for
  for (char e : s)
  {
  cout << e;
  }
  cout << endl;
  return 0;
}


4、string类对象的修改操作


image.png

 image.png


至于功能是什么,上面已经写得很清楚了。


我们现在用这些函数来举几个例子:

微信图片_20221209143549.png


#include<iostream>
#include<string>
using namespace std;
int main()
{
  string s1 = "hello ";
  cout << s1 << endl;
  s1 += "jxwd";//其相当于s1.append("jxwd");
  cout << s1 << endl;
  s1.push_back(' ');
  s1.push_back('i');
  s1.push_back(' ');
  s1.push_back('l');
  s1.push_back('o');
  s1.push_back('v');
  s1.push_back('e');
  s1.push_back(' ');
  s1.push_back('y');
  s1.push_back('o');
  s1.push_back('u');
  cout << s1 << endl;
  cout << s1.c_str() << endl;
  return 0;
}


我们接着来说一下substr接口:

image.png



通过上述文献的索引,我们可以直到,substr是从pos的为止开始,向后len个字符为止,返回值为这中间的字符串。


注意到这里的npos是size_t类型的-1,就是说,其是最大的那个数。


我们再来说一下find接口:

image.png



可以看到,其有4种重载形式,每一种也分别代表了不同的含义。


而rfind和find的意思差不多,其主要是从后面去找。


我们把substr和find接口联系在一块来举个例子:


#include<iostream>
#include<string>
using namespace std;
int main()
{
  //获得file的后缀
  string file("string.cpp");
  size_t pos = file.rfind('.');
  cout << pos << endl;
  string suffix(file.substr(pos, file.size() - pos));//这里就相当于是suffix(file.substr(6,4));
  cout << suffix << endl;
  return 0;
}


我们再来举一个find 和substr结合的例子:



注意到这样一个接口,和上面的find的四种重载形式,


就是说,我们这里的find可以指定从某一位置开始找,也可以指定在某一范围开始找。(注:如果找不到,则返回-1)


可以找string,可以找C类型的字符串,还可找单个字符。

#include<iostream>
#include<string>
using namespace std;
int main()
{
    //打印出下面链接的域名
  string url("http://www.cplusplus.com/reference/string/string/find/");
  cout << url << endl;
  size_t start = url.find("://");   //先找 :// ,找到,返回:位置的下标,找不到,返回npos
  if (start == string::npos)
  {
  cout << "invalid url" << endl;
  return -1;
  }
  start += 3;                            //让其向后偏移三个位置
  size_t finish = url.find('/', start);  //从start的位置开始向后找,找'/' 
  string address = url.substr(start, finish - start); //截取finish到start位置中间的字串
  cout << address << endl;
  return 0;
}


删除和插入:

image.png


image.png

5、string类非成员函数


实际上,这些我们已经不需要再举过多的例子了,相信各位已经或多或少地会用了。我们在后面的模拟实现中会具体地说到其原理。


string类的模拟实现

实际上,用博客的形式来介绍还是比较生硬的,但是不妨碍能够将其说清楚。


好长一大段。


下面,笔者将耐心地为大家一点一点解释。

#include<iostream>
#include<string.h>
#include<assert.h>
namespace jxwd
{
  class string
  {
  /*string(const string& s)
    :_str(new char[strlen(s._str+1)])
  {
    strcpy(_str, s._str);
  }*///传统写法
  //string& operator=(const string& s)
  //{
    /*if (this != &s)
    {
    delete[] _str;
    _str = new char[strlen(s._str) + 1];
    strcpy(_str, s._str);
    }
    return *this;*///传统写法
    //}
public:
  typedef char* iterator;
  typedef const char* const_iterator;
  iterator begin()                                        //定义迭代器
  {
    return _str;                                        //可读可写
  }
  iterator end()                                         //同理,定义end()函数返回的迭代器,这里,我们用指针来去实现
  {                                                      //注意其返回的是最后一个元素的下一个位置
    return _str + _size;
  }
  const_iterator begin() const
  {
    return _str;                                      //仅可读版本
  }
  const_iterator end() const
  {
    return _str + _size;
  }
  string(const char* str = "", size_t capacity = 1,size_t size = 0)   //构造函数 
  { 
    _str = new char[strlen(str) + 1];                             //先创建这么大的空间
    if (capacity > strlen(str))_capacity = capacity;              //如果现有容量比其小,则现有容量扩大至新的容量大小
    else _capacity = strlen(str);                                 //(这取决于你传过来的capacity有多大)
    _size = _capacity;                            
    strcpy(_str, str);                                            //在自己开辟的_str里,将str的内容拷贝过来。
  }
  ~string()                                                         //析构函数
  {
    delete[] _str;
    _str = nullptr;
    _size = _capacity = 0;
  }
  void swap(string& s)                           //自己创建的swap函数
  {
    std::swap(_str, s._str);
    std::swap(_size,s._size);
    std::swap(_capacity, s._capacity);
  }
  string(const string& s)                         //拷贝构造函数  在创建类的时候调用
    :_str(nullptr)                               //初始化三个成员变量  即this指针指向的对象
    ,_size(0)         
    ,_capacity(0)
  {
    string tmp(s._str);                         //调用其构造函数
    std::swap(_str, tmp._str);                  //可以直接swap(tmp);
    std::swap(_size,tmp._size);                 //将其三个量全部交换
    std::swap(_capacity,tmp._capacity);
  }
  string& operator=(string s)                   
  {
    std::swap(_str, s._str);                 //直接调用库中的swap 函数,将变量的内容交换
    return *this;                           //可以直接swap(s),这样的 话调用的是jxwd::swap
  }//现代写法  地址会变                      //返回*this
  char& operator[](size_t i)                     //操作符重载,重载[]操作符//可读可写
  { 
    assert(i < _size);
    return _str[i];
  }                                        
  const char& operator[](size_t i) const        //操作符重载,重载[]操作符//仅读
  {
    assert(i < _size);
    return _str[i];
  }
  const char* c_str() const                     //c_str函数,用于返回其数组
  {
    return _str;
  }
  size_t size() const                          //返回数组元素的大小
  {
    return _size;
  }
  void reserve(size_t n)                       //reserve函数,用于重置空间
  {
    if (n > _capacity)  //开空间,扩展capacity,如果传过来的n>_capacity才开辟,否则就当视而不见
    {
    char* tmp = new char[n + 1]; //动态开辟n+1个(最后一个用于存放'\0')
    strncpy(tmp, _str,_size);    //拷贝,将原先_str里面的_size个数据全部拷贝到tmp里面。
    delete[] _str;              //销毁原有的_str
    _str = tmp;                   //让_str指向tmp
    _capacity = n;                //_capacity赋值成n
    }
  }
  void resize(size_t n , char val = '\0')
  {                                  //开空间+初始化,并且可能拓展capacity
    if (n < _size)
    {
    _size = n;
    _str[_size] = '\0';        //要注意需要在最后位置放上'\0'
    }
    else
    {
    if (n > _capacity)
    {
      reserve(n);
    }
    for (size_t i = _size; i < n; i++)
    {
      _str[i] = val;
    }
    _str[n] = '\0';
    _size = n;
    }
  }
  void push_back(char ch)             //尾插
  {
    if (_size == _capacity)        //如果空间不够,则自动扩容
    {
    reserve(_capacity == 0 ? 4 : _capacity*2);
    }
    _str[_size] = ch;              //将最后原先最后一个位置赋值成ch
    _str[_size + 1] = '\0';        //然后将下一个位置赋值成'\0'
    _size++;                       //让_size自增。因为push_back后元素多了一个
  }
  void append(const char* str)      //这就是在后面增加了一个字符串
  {
    size_t len = _size + strlen(str);   
    if (len > _capacity)          //先计算是否需要扩容
    {
    reserve(len);
    }
    strcpy(_str + _size, str);   //再将str拷贝至_str+_size的位置
    _size = len;                 //重新赋值_size的值
  }
  string& insert(size_t pos, char ch)   //在pos的位置插入ch   很像顺序表的插入
  {
    assert(pos <= _size);             //先断言,判断能否去插入
    if (_size == _capacity)     
    {
    reserve(_capacity * 2);       //计算是否需要扩容
    }
    size_t end = _size + 1;           //计算新的容量大小
    while (end > pos)
    {
    _str[end] = _str[end - 1];   //模拟顺序表的插入
    end--;
    }
    _str[pos] = ch;
    _size++;
    return *this;
  }
  string& insert(size_t pos, const char* str) //与上面的构成重载,插入一个字串
  {
    assert(pos <= _size);
    size_t len = strlen(str);
    if (_size + len > _capacity)
    {
    reserve(_size + len);            //和上面一样,该断言断言,该开空间就开空间
    }
    char* end = _str + _size + len;           //标注出尾部的地址
    while (end >= _str + pos)           //同样地道理向后移动,腾出来len个空间
    {
    *(end + 1) = *end;
    end--;
    }
    strncpy(_str + pos, str, len);     //同样道理去拷贝
    _size += len;                      //_size重新计算
  }
  string& erase(size_t pos,size_t len = npos)  //同理,相当于顺序表的尾删
  {
    size_t leftLen = _size - pos;            //如果len缺省,就是从pos的位置,向后全删了
    if (len > leftLen)                      //如果len > leftLen还要大,那就全删了
    {
    _str[pos] = '\0';               //注意'\0'
    _size = pos;
    }
    else                                
    {                                 //否则
    strcpy(_str + pos, _str + pos + len); //直接将后面的元素连着'\0'一块,拷到前面的位置
    _size -= len;                 //重新计算_size
    }
  }
  string& operator+=(char ch)          //重载+=  ,如果是加等一个字符,主要是调用push_back
  { 
    push_back(ch);
    return *this;
  }
  string& operator+=(const char* str) //如果是+=一个字串,就调用append
  {
    append(str);
    return *this;
  }
  size_t find(char ch, size_t pos = 0) //find函数,主要就是一个一个去遍历寻找,找到了就返回下标,否则,返回npos
  {                                     //这是找字符
    for (size_t i = pos; i < _size; i++)
    {
    if (_str[i] == ch)
    {
      return i;
    }
    }
    return npos;
  }
  size_t find(const char* str, size_t pos = 0)
  {
    const char* ret = strstr(_str, str);  //这是找字串,直接调用strstr函数
    if (ret)
    {
    return ret - _str;
    }
    else
    {
    return npos;
    }
  }
  void clear()               //清除所有数据
  {
    _size = 0;
    _str[_size] = '\0';
  }
  std::istream& getline(std::istream& cin, string& s)  //getline函数,主要获取一行的信息
  {
    s.clear();                   //首先是清楚所有数据,
    char ch;               
    ch = cin.get();
    while (ch != '\n')          //然后一个一个字符获取,直到获取到'\n'为止
    {
    s += ch;
    ch = cin.get();
    }
    return cin;               //定义要求返回输入流
  }
  private:            //这里定义其私有类  
  char* _str;     //由于是字串,我们定义一个数组
  size_t _size;    //并且定义出其容量和元素个数的大小
  size_t _capacity;
  static const size_t npos;  //定义一个静态变量npos,我们等会会看到在外部去定义,定义其为-1.
  };
  //接下来的都是运算符重载
  inline bool operator<(const string& s1, const string& s2)  //关系运算符的重载
  {
  return strcmp(s1.c_str(), s2.c_str()) < 0;   //直接调用strcmp函数
  }
  inline bool operator>(const string& s1, const string& s2)  //下面的直接复用上面的
  {
  return !(s1 <= s2);
  }
  inline bool operator==(const string& s1, const string& s2)  //同上
  {
  return strcmp(s1.c_str(), s2.c_str()) == 0;
  }
  inline bool operator<=(const string& s1, const string& s2)
  {
  return s1 < s2 || s1 == s2;
  }
  inline bool operator>=(const string& s1, const string& s2)
  {
  return!(s1 < s2);
  }
  inline bool operator!=(const string& s1, const string& s2)
  {
  return !(s1 == s2);
  }
  std::ostream& operator<<(std::ostream& cout, string& s)   //输出一个字串
  {
  for (auto ch : s)                             //一个一个输出直到将s内容全部输出(遇到空格 Tab和回车都会停止输出)
  {
    cout << ch;
  }
  return cout;
  }
  std::istream& operator>>(std::istream& cin, string& s)  //输入一个字串
  {
  s.clear();
  char ch;
  ch = cin.get();                                   //调用cin.get()函数
  while (ch != ' ' && ch != '\n')                   //一个一个输入,直到遇到空格、换行符或者Tab 
  {
    s += ch;
    ch = cin.get();
  }
  return cin;
  }
  const size_t string::npos = -1;                           //定义静态变量为-1(由于其是size_t,所以其为很大很大的数)
}
各位可以复制到本地的ide中去看。
我们可以测试测试。
比如:(测试用例:)
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string.h>
#include<assert.h>
namespace jxwd
{
    void test_string1()
  {
  string s1("hello world");
  string s2(s1);
  std::cout << s1.c_str() << std::endl;
  std::cout << s2.c_str() << std::endl;
  string s3("hello bit");
  s1 = s3;
  std::swap(s1, s3);//效率低
  }
  void test_string2()
  {
  string s1("hello world");
  s1[0] = 'x';
  std::cout << s1[0] << std::endl;
  std::cout << s1.c_str() << std::endl;
  }
  void test_string3()
  {
  string s1("hello bit");
  string::iterator it = s1.begin();
  while (it != s1.end())
  {
    std::cout << *it << " ";
    it++;
  }
  for (auto ch : s1)//会被替代成迭代器,是有迭代器支持的
  {
    std::cout << ch << " ";
  }
  std::cout << std::endl;
  }
  void test_string4()
  {
  string s1("hello");
  s1 += '!';
  s1.resize(8, 'z');
  std::cout << s1.c_str() << std::endl;
  s1.resize(1, 's');
  std::cout << s1.c_str() << std::endl;
  s1.resize(3);
  std::cout << s1.c_str() << std::endl;
  }
  void test_string5()
  {
  string s1("hello ljx");
  std::cout << s1.find("hel") << std::endl;
  }
}
using namespace std;
int main()
{
  jxwd::test_string1();
  jxwd::test_string2();
  jxwd::test_string3();
  jxwd::test_string4();
  jxwd::test_string5();
  return 0;
}



我们得出:


我们下面来看vector.


vector的使用

与string同理,vector实际上与string有许许多多的地方都是一样的。


有关vector的一些说明:


1. vector是表示可变大小数组的序列容器。

2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。

3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。

4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。

5. 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好


相比于string而言,vector甚至更简单。因为其不需要再末尾考虑'\0'的问题了。


对于vector的学习,我们同样的道理,还是借助于文档来完成。


vector常用结构说明

1、vector定义(构造)类

image.png


可以看出,密密麻麻的,我们根据具体的用法简化一下:


我们可以这样构造

image.png



第一个:无参构造;


第二个:构造并初始化n个val;


第三个:拷贝构造                    //也就是文档中的copy


第四个:用迭代器进行构造。


还需要注意的是:

image.png

我们可以看到,其定义出来的时候,需要一个模板参数,所以,我们在定义vector类的时候就需要加上这么个模板参数,形成显式模板转换,要不然就会出错。


我们来举几个例子:

如上图所示。各种方法已经标出。


2、vector与string相类似的部分

我们之前说过,vector和string有许许多多类似的地方。


接下来,我们将对比学习。


首先,是二者的迭代器。一样的用法:

image.png

再次,是vector的增删查改。

image.png


image.png



看不出来什么不一样。



并且,vector的空间增容等问题与string基本也是一样的。

image.png



这些东西,笔者就不再赘述了,如果以后遇到了问题,可以先于string类进行比较,然后如果有具体问题我们再做补充。


3、vector 迭代器失效问题。

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装。


比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃


(即如果继续使用已经失效的迭代器,程序可能会崩溃)


对于vector可能会导致其迭代器失效的操作有:


1. 会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。


它们实际上都是由于底层可能会增容导致。我们在string类的模拟实现中就提到过,如果增容用的是calloc,也就是说vector底层原理旧空间被释放掉,而在打印时,it还使用的是释放之间的旧空间,在对it迭代器操作时,实际操作的是一块已经被释放的空间,而引起代码运行时崩溃那么其迭代器所指向的那部分空间,就很有可能失效了。


那有什么解决办法呢?


解决方式:在以上操作完成之后,如果想要继续通过迭代器操作vector中的元素,只需给it重新

赋值即可。


2、指定位置删除元素——erase

#include <iostream>
using namespace std;
#include <vector>
int main()
{
    int a[] = { 1, 2, 3, 4 };
    vector<int> v(a, a + sizeof(a) / sizeof(int));
    // 使用find查找3所在位置的iterator
    vector<int>::iterator pos = find(v.begin(), v.end(), 3);
    // 删除pos位置的数据,导致pos迭代器失效。
    v.erase(pos);
    cout << *pos << endl; // 此处会导致非法访问    
    return 0;
}


erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效,但是:如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。


因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。


迭代器失效解决办法:在使用前,对迭代器重新赋值即可


关于底层:

image.png

我们给个图来理解一下:


注意:其不一定直接在原位置上开辟新空间。其可能是另外开辟,然后其拷贝,再释放旧空间的过程。


vector的模拟实现

#include<iostream>
#include<assert.h>
using namespace std;
namespace ljx
{
  template <class T>
  class vector
  {
  public:
  typedef T* iterator;             //定义出迭代器,可以看出,这里底层是用指针来实现的
  typedef const T* const_iterator;
  vector()                        //构造函数,实际上,构造函数应当有重载的形式
    :_start(nullptr)
    , _finish(nullptr)
    , _endofstorage(nullptr)
  {}
  vector(int a, int b)           //构造函数的重载形式  fill 的形式
    :_start(nullptr)
    ,_finish(nullptr)
    ,_endofstorage(nullptr)
  {
    for (int i = 0; i < a; i++)
    this->push_back(b);
  }
  vector(const vector<T>& v)    //拷贝构造    也可以用交换的方法
  {
    _start = new T[v.capacity()];    //首先开辟空间,首地址返回给_start
    _finish = _start;                //_finish 给_start
    _endofstorage = _start + v.capacity(); //计算尾部的容量 
    for (size_t i = 0; i < v.size(); i++)
    {
    *_finish = v[i];
    ++_finish;
    }
  }
  /*
    vector(const vector<T>& v)
    :_start(nullptr)
    ,_finish(nullptr)
    ,_endofstorage(nullptr)
  {
    reserve(v.capacity());
    for(const auto e : v)
    push_back(e);//可以直接调?!
  }
  */
  vector<T>& operator=(const vector<T>& v)  //赋值构造
  {
    if (this != &v)
    {
    delete[] _start;
    _start = new T[v.capacity()];
    memcpy(_start, v._start, sizeof(T) * v.size);
    }
    return *this;
  }
  //还可以这样写:
  /*vector<T>& operator=(vector<T> v)
  {
    ::swap(_start, v._start);
    ::swap(_finish, v._finish);
    ::swap(_endofstorage, v._endofstorage);//可以把它们合成一个Swap函数
    return *this;
  }*/
  //如果要交换,不建议直接swap.因为会有三个深拷贝
  //可以用v1.swap(v2),这样只是三个指针的拷贝
  void print_vector() const                    //打印
  {
    vector v = *this;
    vector<int>::const_iterator it = v.begin();
    while (it != v.end())
    {
    cout << *it << " ";
    it++;
    }
    cout << endl;
  }
  size_t size() const                //返回size,即元素个数
  {
    return _finish - _start;
  }
  size_t capacity() const            //返回capacity,即容量大小
  {
    return _endofstorage - _start;
  }
  void reserve(size_t n)            //重新定义空间大小
  {
    if (n > capacity())
    {
    size_t sz = size();
    T* tmp = new T[n];         //重新在新的为止上开辟空间
    if (_start)
    {
      //memcpy(tmp, _start, sizeof(T) * sz);
      for (size_t i = 0; i < sz; i++)
      {
      tmp[i] = _start[i];//如果T是string等自定义类型,那么这样就会按照string的深拷贝来进行
      }        //注意,这里不能用memcpy.因为memcpy所对应的是浅拷贝,只是按照二进制,将对应的地址拷贝过去
      delete[] _start;
    }
    _start = tmp;
    _finish = tmp + sz;
    _endofstorage = tmp + n;
    }
  }
  const_iterator begin() const           //定义返回头部第一个元素的迭代器
  {
    return _start;              //仅可读
  }
  const_iterator end() const            //定义返回尾部的迭代器
  {
    return _finish;            //仅可读
  }
  iterator begin()
  {
    return _start;
  }
  iterator end()
  {
    return _finish;
  }                            //略
  void push_back(const T& x)              //尾插
  {
    if (_finish == _endofstorage)        //问:是否要增容?
    {
    size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newcapacity);
    }
    *_finish = x;
    _finish++;
  }
  T& operator[](size_t i)                 //重载[]运算符
  {
    assert(i < size());
    return _start[i];
  }
  T& operator[](size_t i) const            //第二个版本
  {
    assert(i < size());
    return _start[i];
  }
  void pop_back()                       //尾删
  {
    assert(_start < _finish);
    --_finish;
  }
  void insert(iterator pos, const T& x)  //插入
  {
    assert(pos <= _finish);
    if (_finish == _endofstorage)          //判断是否要增容
    {
    size_t n = pos - _start;
    size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    reserve(newcapacity);
    pos = _start + n;
    }
    iterator end = _finish - 1;
    while (end >= pos)     //向后移动
    {
    *(end + 1) = *end;
    --end;
    }
    *pos = x;            //在pos位置插入
    ++_finish;
  }
  iterator erase(iterator pos)          //删除
  {
    assert(pos < _finish);
    iterator it = pos;        //把后面的位置往前移动
    while (it < _finish)
    {
    *it = *(it + 1);
    ++it;
    }
    --_finish;               //减减最后元素的门神
    return pos;
  }
  void resize(size_t n,const T& val = T())  //重新开辟元素空间大小
  {
    if (n < this->size())
    {
    _finish = _start + n;            //问:是否比我原有的size要大?
    }
    else
    {
    if (n > capacity())               //问:是否要增容
    {
      reserve(n);
    }
    while (_finish < _start + n)      //接下来,就将剩下的新增元素初始化为val
    {
      *_finish = val;
      ++_finish;
    }
    }
  }
  private:
  iterator _start;           //左指针
  iterator _finish;          //左闭右开,右指针
  iterator _endofstorage;    //容量的尾部
  };
}
我们可以来测试一下:
(让主函数去调用这两个函数)
void test_vector1()
  {
  vector<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  v.push_back(4);
  v.push_back(5);
  vector<int>::iterator it = v.begin();
  while (it != v.end())
  {
    cout << *it << " ";
    ++it;
  }
  cout << endl;
  }
  void test_vector2()
  {
  vector<int> v;
  v.push_back(1);
  v.push_back(2);
  v.push_back(3);
  v.push_back(4);
  v.push_back(5);
  vector<int>::iterator it = v.begin();
  while (it != v.end())
  {
    if (*it % 2 == 0)
    {
    it = v.erase(it);
    }
    else
    {
    it++;
    }
  }
  v.print_vector();
  }


我们得到:

ok。有关vector的内容暂且介绍到这里,后期我们刷题时还会重新见面的。


list类

对于list类,我们重点说其迭代器的模拟实现。


如果说vector是一个数组,是一个顺序表,那么list就是一个链表。


1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。

3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。

4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。

5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息


如果要更准确地说,我们的list 是双向循环带头链表。


list 的用法简介

关于list的用法,实际上,我们通过查看文档就是可以理解的。因为我们已经有了上面string和vector的基础了。


list的构造函数

image.png


我们可以看到,其构造的方式有4种。


简而言之,我们可以用迭代器区间去进行构造,可以直接去填充,也可以进行拷贝。


举几个例子:

int main(){
    std::list<int> l1; // 构造空的l1
    std::list<int> l2 (4,100); // l2中放4个值为100的元素
    std::list<int> l3 (l2.begin(), l2.end()); // 用l2的[begin(), end())左闭右开的区间构
                                                    造l3
    std::list<int> l4 (l3); // 用l3拷贝构造l4
                            // 以数组为迭代器区间构造l5
    int array[] = {16,2,77,29};
    std::list<int> l5 (array, array + sizeof(array) / sizeof(int) );
    // 用迭代器方式打印l5中的元素
    for(std::list<int>::iterator it = l5.begin(); it != l5.end(); it++)
    std::cout << *it << " ";
    std::cout<<endl;
    // C++11范围for的方式遍历
    for(auto& e : l5)
    std::cout<< e << " ";
    std::cout<<endl;
    return 0;
}



那么链表有两种遍历方式。


一种是用显示的迭代器去遍历;


还有一种是用范围for(本质上还是迭代器)(如上述代码)


list的迭代器

image.png


1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动

2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动


这些东西和string、vector几乎都是一样的,就是要注意的是其是循环的。这个链表的思路我们在数据结构的时候就曾说过。


其他成员函数

image.png


我们这里就说一个:assign(其余的我们将在模拟实现的时候一笔带过)


image.png


它的意思就是说,先把原有的链表内容清空,然后将n个val的值拷贝入新的链表中。


我们再说一个:

image.png


用于拼接,就是说,我能将list& x拼接到list中(就是调用该函数的类)


后面的二者是迭代器,即拼接的范围。  


注意:在list中,使用insert后,原本的迭代器不会失效。但是在使用erase时,使用完的迭代器便会失效。


list的模拟实现(重头戏)

在此之前,我们需要将一个思想:


由于list的底层在存储的时候地址不是连续的,但是我们有需要实现++;--;*等运算操作,我们需要自己构建一个迭代器类,将其封装起来然后使用。这也是我们模拟实现list的重点。

image.png




我们接下来进行模拟实现:

#include<iostream>
#include<assert.h>
namespace jxwd
{
  template<class T>
  struct _list_node                  //该结构体是为了要构建结点(即链表的结点)
  {
  T val;
  _list_node<T>* _next;
  _list_node<T>* _prev;
  _list_node(const T& x = T())
    :val(x)
    ,_next(nullptr)
    ,_prev(nullptr)
  {}
  };
  template <class T ,class Ref ,class Cal>  //封装着迭代器的类
  struct _list_iterator
  {
  typedef _list_node<T> node;        //而在这里,我就是想要进行一个浅拷贝
  typedef _list_iterator<T,Ref,Cal> self;    //所以这里拷贝构造、operator、析构我们不写,编译器默认生成的就可以用
  node* _pnode;                      //定义一个_pnode结点,即用于迭代器的底层实现——实际上就是一个链表结点的指针
  _list_iterator(node* pnode)         //迭代器类的构造函数
    :_pnode(pnode)
  { }
  Ref operator*()                 //解引用,返回T&类型
  {
    return _pnode->val;
  }
  Cal operator->()                //重载->操作符,返回T*
  {
    return &_pnode->val;
  }
  bool operator!=(const self& s) const  //操作符重载,并且提供const与否的两种类型
  {
    return _pnode != s._pnode;
  }
  bool operator==(const self& s) const
  {
    return _pnode == s._pnode;
  }
  self& operator++()                  //重载前置++,返回一个迭代器类型
  {
    _pnode = _pnode->_next;
    return *this;
  }
  //it++ -> it.operator++(&it,0)
  //++it ->it.operator++(&it)
  self operator++(int)              //重载后置++,返回未++的迭代器,并将迭代器++
  {
    self tmp(*this);
    _pnode = _pnode->_next;
    return tmp;
  }
  self& operator--()            //同++理
  {
    _pnode = _pnode->_prev;
    return *this;
  }
  self operator--(int)
  {
    self tmp(*this);
    _pnode = _pnode->_prev;
    return tmp;
  }
  };
  template<class T>
  class list
  {
  typedef _list_node<T> node;
  public:
  typedef _list_iterator<T , T&, T*> iterator;                 //将封装的迭代器类重命名未iterator
  typedef _list_iterator<T, const T&,const T*> const_iterator;//同上,只不过定义要求了另外一种const 类型
  iterator begin()                        //返回第一个结点的迭代器
  {
    return iterator(_head->_next);
  }
  iterator end()                    //返回最后一个结点的下一个位置的迭代器(即头节点)
  {
    return iterator(_head);
  }
  const_iterator begin() const       //第二种类型
  {
    return const_iterator(_head->_next);
  }
  const_iterator end() const
  {
    return const_iterator(_head);
  }
  list()                            //构造函数
  {
    _head = new node;
    _head-> _next = _head;
    _head-> _prev = _head;
  }
  ~list()                        //析构函数
  {
    clear();
    delete _head;
    _head = nullptr;
  }
  list(const list<T>& x)           //拷贝构造
  {
    _head = new node;
    _head->_next = _head;
    _head->_prev = _head;
    for (const auto& e : x)
    {
    push_back(e);
    }
  }
  //list<int>& operator=(const list<T>& lt)
  //{
  //  if (this != &lt)
  //  {
  //  clear();
  //  for (const auto& e : lt)
  //  {
  //    push_back(e);
  //  }
  //  }
  //  return *this;
  //}//传统写法
  list<T>& operator=(list<T>& lt) //赋值运算符重载      
  {
    ::swap(_head, lt._head);
    return *this;
  }
  void clear()
  {
    iterator it = begin();
    while (it != end())
    {
    it = erase(it);
    }
  }
  void push_back(const T& x)
  {
    node* newnode = new node(x);
    node* tail = _head->_prev;
    tail->_next = newnode;
    newnode->_prev = tail;
    newnode->_next = _head;
    _head->_prev = newnode;
    //insert(end(),x);                //尾插,一种思路时复用insert()
  }
  void push_front(const T& x)
  {
    insert(begin(), x);
  }
  void pop_back()
  {
    erase(--end());
  }
  void pop_front()
  {
    erase(begin());
  }
  void insert(iterator pos, const T& x) //在pos前面插入
  {
    assert(pos._pnode);
    node* cur = pos._pnode;       //三结点之间的关系
    node* prev = cur->_prev;
    node* newnode = new node(x);
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
  }
  iterator erase(iterator pos)     //删除pos结点
  {
    assert(pos._pnode);
    assert(pos != end());
    node* prev = pos._pnode->_prev;
    node* next = pos._pnode->_next;
    delete pos._pnode;
    prev->_next = next;
    next->_prev = prev;
    return iterator(next);
  }
  bool empty()                   //判断是否为空
  {
    return begin() == end();
  }
  size_t size()               //计算结点数
  {
    size_t sz = 0;
    iterator it = begin();
    while (it != end())
    {
    sz++;
    it++;
    }
    return sz;
  }
  private:
  node* _head;                   //定义一个哨兵位——头节点
  };
}


我们这里的模拟实现,主要是针对迭代器封装的模拟,其他的链表成员我们都简略或者省略了。


我们可以测试看看:

namespace jxwd{
void func()
  {
  list<int> lt;
  lt.push_back(1);
  lt.push_back(2);
  lt.push_back(3);
  lt.push_back(4);
  lt.push_back(5);
  list<int> llt(lt);
  list<int>::iterator it = llt.begin(); //这里是将返回的迭代器 进行拷贝构造
  while (it != llt.end())
  {
    std::cout << *it << " ";
    ++it;
  }
  std::cout << std::endl;
  }
  void test_list2()
  {
  list<int> lt;
  lt.push_back(10);
  lt.push_back(20);
  list<int> lt2(lt);
  list<int>::iterator it = lt2.begin();
  while (it!=lt2.end())
  {
    std::cout << *it << " ";
    it++;
  }
  }
}


我们将上面的函数放在main函数中测试一下看看,发现程序成功运行了起来。下面是运行截图:

这就说明我们模拟成功了。


对于迭代器的封装模拟使用也是正确的。


好耶!!!


至于vector和list 的对比,实际上就是链表和顺序表的对比,如果想看的话,我们下节再啰嗦一遍吧


好啦,我们本节的内容就先到这里吧,我们下节再见。



目录
相关文章
|
6天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
22 7
|
25天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
41 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
88 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1
|
5天前
|
监控 JavaScript 测试技术
postman接口测试工具详解
Postman是一个功能强大且易于使用的API测试工具。通过详细的介绍和实际示例,本文展示了Postman在API测试中的各种应用。无论是简单的请求发送,还是复杂的自动化测试和持续集成,Postman都提供了丰富的功能来满足用户的需求。希望本文能帮助您更好地理解和使用Postman,提高API测试的效率和质量。
34 11
|
1月前
|
JSON Java 测试技术
SpringCloud2023实战之接口服务测试工具SpringBootTest
SpringBootTest同时集成了JUnit Jupiter、AssertJ、Hamcrest测试辅助库,使得更容易编写但愿测试代码。
61 3
|
2月前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
77 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
|
3月前
|
移动开发 JSON Java
Jmeter实现WebSocket协议的接口测试方法
WebSocket协议是HTML5的一种新协议,实现了浏览器与服务器之间的全双工通信。通过简单的握手动作,双方可直接传输数据。其优势包括极小的头部开销和服务器推送功能。使用JMeter进行WebSocket接口和性能测试时,需安装特定插件并配置相关参数,如服务器地址、端口号等,还可通过CSV文件实现参数化,以满足不同测试需求。
263 7
Jmeter实现WebSocket协议的接口测试方法