• 【C++】手撕STL系列——string篇


    文章导读

    本章我们将参照STL源码,来模拟实现string类,但不一定非要与库中完全相同。我们将其中重要的、常用的接口进行模拟实现,旨在加深string类的学习与记忆。

    为了代码更好地复用,本篇模拟的函数接口的顺序大概为构造类——》内存类——》迭代器——》修改类——》构造类

    定义string类

    为了区别于标准库中的string类,我们这里应该使用自己的命名空间来进行定义

    string类包含以下三种成员

    • char*_str  字符数值
    • size_t _size  大小
    • size_t _capacity  容量

    注意:capacity的大小不包含\0,_size指的是\0的位置

    另外还需要一个static的size_t成员npos,值为-1,表示数组末尾

    构造函数

    string的构造函数有很多种写法,由前面类和对象的学习中了解到全缺省的构造函数是最优的写法,所以这里我们也采纳全缺省的写法

    注意:初始化列表是根据成员的定义顺序来进行初始化的,所以这里_str不能放到初始化列表进行初始化,因为放到初始化列表中会第一个初始化_str,但这是还不知道_capacity的大小

    1. /*string()
    2. :_str(new char[1]{ '\0' })
    3. ,_size(0)
    4. ,_capacity(0)
    5. {}*/
    6. //全缺省的构造函数更优
    7. string(const char* s="")
    8. :_size(strlen(s))
    9. , _capacity(_size)
    10. {
    11. _str = new char[_capacity + 1];
    12. strcpy(_str, s);
    13. }

    内存类函数接口

    size_t size()const——返回大小
    1. size_t size()
    2. {
    3. return _size;
    4. }
    size_t capacity()const——返回容量大小
    1. size_t capacity()const
    2. {
    3. return _capacity;
    4. }
    void reserve(size_t n)——扩容函数

    n大于capacity才会发生扩容,开辟另一块空间并将原来的空间拷贝过去,再销毁原来的空间;n小于等于capacity的时候不会有所作为

    不会修改_size值

    1. void reserve(size_t n)
    2. {
    3. if (n > _capacity)
    4. {
    5. char* tmp = new char[n + 1];//多出来的一个位置放\0
    6. strcpy(tmp, _str);
    7. delete[]_str;
    8. _str = tmp;
    9. _capacity = n;
    10. }
    11. }

    void resize(size_t n,char ch)——修改大小

    如果n<=size就减小_size的值,并在对应处放'\0'

    如果n>size位置,就扩容(复用reserve函数),并将size 位置到n位置的元素初始化为ch,最后一个位置放\0

    1. void resize(size_t n,char c='\0')
    2. {
    3. if (n <= _size)
    4. {
    5. _str[n] = '\0';
    6. _size = n;
    7. }
    8. else
    9. {
    10. reserve(n);
    11. while (_size < n)
    12. {
    13. _str[_size] = c;
    14. _size++;
    15. }
    16. _str[_size] = '\0';
    17. }
    18. }

    迭代器

    先实现迭代器是为了方便遍历数组(范围for)并将元素打印,方便我们后期进行调试并检验其他函数接口的正确性

    这里的迭代器可以简单地认为是原生指针(其他类可能不是原生指针)

    1. iterator begin()
    2. {
    3. return _str;
    4. }
    5. iterator end()
    6. {
    7. return _str + _size;
    8. }
    9. const_iterator begin()const
    10. {
    11. return _str;
    12. }
    13. const_iterator end()const
    14. {
    15. return _str+_size;
    16. }

    为了使string类可以像数组一样可以访问,增强代码的可读性,可以先实现一个[]的运算符重载

    实现两个,一个是只能读不能修改,一个是可读可修改

    1. char operator[](size_t i)
    2.         {
    3.             assert(i <= _size);
    4.             return *(_str + i);
    5.         }
    6.         const char operator[](size_t i)const
    7.         {
    8.             assert(i <= _size);
    9.             return *(_str + i);
    10.         }

    修改类

    注意:strcpy以\0为结束标志,会拷贝\0;strncpy自己决定拷贝个数,不会自动拷贝\0

    经过初阶数据结构与算法的学习,我们知道顺序表的优势在与尾插尾删以及随机访问,所以第一个修改类的实现当然是尾插push_back啦

    push_back
    1. void push_back(const char ch)//插入一个字符
    2. {
    3. //满了先扩容
    4. if (_size = _capacity)
    5. {
    6. reserve(_capacity==0?4:2*_capacity);
    7. }
    8. _str[_size] = ch;
    9. _size++;
    10. _str[_size] = '\0';
    11. }

    除了插入一个字符,肯定还有插入一个字符串的需求,这里就交给append

    如果插入的字符串长度+原本的长度>capacity,就需要扩容

    append
    1. void append(const char*s)
    2. {
    3. size_t len = strlen(s);
    4. if (_size + len > _capacity)
    5. {
    6. reserve(_size + len);
    7. }
    8. strcpy(_str + _size, s);
    9. _size += len;
    10. }

    用函数实现尾插当然可以,但是现实中更多人使用的是+=运算符重载,因为这样可读性很高,也十分生动形象

    有了以上两个接口,我们的+=运算符重载当然是手到擒来啦,直接复用即可

    +=运算符重载
    1. string& operator+=(const char ch)
    2. {
    3. push_back(ch);
    4. return *this;
    5. }
    6. string& operator+=(const char* s)
    7. {
    8. append(s);
    9. return *this;
    10. }

    实现了尾插,接下来就是头插(insert函数取第一个位置即可)啦

    我们可以头插一个字符,也可以头插一个字符串,也可以在任意位置插入(写完insert后尾插其实就可以复用insert了,取最后一个位置即可)

    注意插入位置pos要在合法区间,如果大于容量则要扩容

    insert
    1. void insert(size_t pos, char ch)
    2. {
    3. assert(pos <= _size);
    4. if (_size == _capacity)
    5. {
    6. reserve(_capacity == 0 ? 4 : 2 * _capacity);
    7. }
    8. //size位置指的是‘\0’,也要移动
    9. size_t end = _size + 1;
    10. while (end > pos)
    11. {
    12. _str[end] = _str[end - 1];
    13. --end;
    14. }
    15. _str[pos] = ch;
    16. _size++;
    17. }
    18. void insert(size_t pos, const char* str)
    19. {
    20. assert(pos <= _size);
    21. int len = strlen(str);
    22. if (_size + len > _capacity)
    23. {
    24. reserve(_size + len);
    25. }
    26. int end = _size;
    27. //防止发生整形提升
    28. while (end >=(int) pos)
    29. {
    30. _str[end+len] = _str[end];
    31. --end;
    32. }
    33. strncpy(_str + pos, str, sizeof(char) * len);
    34. _size += len;
    35. }

    实现完插入当然就是删除啦

    void clear()——删除所有(在开头处放\0,并不用真正地销毁)

    clear()
    1. void clear()
    2. {
    3. _str[0] = '\0';
    4. _size = 0;
    5. }

      void erase(size_t pos = 0, size_t len = npos)——删除在pos以及以后的len个元素

       如果没指定len的大小,默认将pos以后的元素全部删完

       指定了就删除len个

     void erase
    1. void erase(size_t pos = 0, size_t len = npos)
    2. {
    3. assert(pos < _size);
    4. if (pos + len >= _size)
    5. {
    6. _str[pos] = '\0';
    7. _size = pos;
    8. }
    9. else
    10. {
    11. int end = pos + len;
    12. while (end <_size)
    13. {
    14. _str[end - len] = _str[end];
    15. end++;
    16. }
    17. _size -= len;
    18. }
    19. }

    输入输出运算符重载

    为了不破坏权限以及耦合度,这里不使用友元实现,将输入输出的运算符重载放在string的类外,自己的类域里面实现

    cout重载

    就是一个简单的范围for遍历

    1. ostream& operator<< (ostream& out, const string& str)
    2. {
    3. for (auto ch : str)
    4. out << ch;
    5. return out;
    6. }
    cin重载

    为了避免输入过多字符,s不断扩容,先定义一个buff数组,用空间换取时间

    如果buff数组满了,先将数组里的内容尾插到string中,再将buff数组清空,继续往buff数组里输入值,如此反复

    1. istream& operator>>(istream& in, string& s)
    2. {
    3. s.clear();
    4. char buff[129];
    5. size_t i = 0;
    6. char ch;
    7. ch = in.get();
    8. while (ch != ' ' && ch != '\n')
    9. {
    10. buff[i++] = ch;
    11. if (i == 128)
    12. {
    13. buff[i] = '\0';
    14. s += buff;
    15. i = 0;
    16. }
    17. ch = in.get();
    18. }
    19. if (i != 0)
    20. {
    21. buff[i] = '\0';
    22. s += buff;
    23. }
    24. return in;
    25. }

    比较函数

    正如字符串可以比较一样,string类(封装的字符串)我们应该也设置成让他们可以比较

    只要实现了==和>或者(==和<),其他的函数接口都可以复用这两个函数接口

    1. bool operator==(const string& s)
    2. {
    3. return strcmp(_str, s._str)==0;
    4. }
    5. bool operator>(const string& s)
    6. {
    7. return strcmp(_str, s._str) >0;
    8. }
    9. bool operator>=(const string& s)
    10. {
    11. return (*this > s) || (*this == s);
    12. }
    13. bool operator<=(const string& s)
    14. {
    15. return !(*this > s);
    16. }
    17. bool operator<(const string& s)
    18. {
    19. return !(*this <= s);
    20. }
    21. bool operator!=(const string& s)
    22. {
    23. return !(*this == s);
    24. }

    构造类

    拷贝构造
    1. //拷贝构造函数
    2. string(const string& s, size_t pos=0, size_t len = npos)
    3. {
    4. if (len == npos)
    5. {
    6. _str = new char[s._size-pos];
    7. strcpy(_str, s._str + pos);
    8. _size = _capacity = s._size - pos;
    9. }
    10. else
    11. {
    12. _str = new char[len];
    13. strncpy(_str, s._str + pos, len);
    14. _capacity = _size = len;
    15. _str[_size] = '\0';
    16. }
    17. }
    =运算符的重载

    这里有两种写法,一种是老老实实地自己写,一种是复用拷贝构造

    1. //老实写法
    2. string& operator=(const string& tmp)
    3. {
    4. if (*this != tmp)
    5. {
    6. //char* s = new char[tmp._capacity ]——这样写下面的delete会报错,越界多拷贝了一个,释放空间的时候会出问题
    7. char* s = new char[tmp._capacity+1];
    8. strcpy(s, tmp._str);
    9. delete[]_str;
    10. _str = s;
    11. _size = tmp._size;
    12. _capacity = tmp._capacity;
    13. }
    14. return *this;
    15. }

    复用写法

    先需要一个swap函数,调用标准库里的swap函数,由函数重载自动识别是哪种类型并操作

    1. void swap(string&s)
    2.         {
    3.             std::swap(_str, s._str);
    4.             std::swap(_size, s._size);
    5.             std::swap(_capacity, s._capacity);
    6.         }

    传参的时候调用了拷贝构造函数,然后再交换,即可完成拷贝,十分简洁,但效率一样

    注意:几乎每个类的赋值重载都可以这样写,十分通用又简洁的写法

    1. string& operator=(string tmp)
    2. {
    3. swap(tmp);
    4. return *this;
    5. }

    至此,string的大概的接口就实现的差不多了,希望对大家能有所帮助

  • 相关阅读:
    10.26~10.27论文,ALP: AdaptiveLossless floating-Point Compression
    python如何将代码制作成可以pip的库,将自己的python代码打包成库,让别人pip安装调用?
    C++变量默认初始化
    Laya中的脚本与外部的Js之间相互调用
    11、【桥接模式】让将抽象和实现分离,使得它们可以独立地变化
    SpringSecurity学习笔记【授权部分待更新】
    面试题-React(十):setState为什么使用异步机制?
    【数据结构】栈的实现
    HTML使用
    安全性归约
  • 原文地址:https://blog.csdn.net/fight_for1/article/details/132367409