• 【C++ STL】模拟实现 string


    标题:【C++ :: STL】手撕 STL _string

    @水墨不写bug


    (图片来源于网络)


            C++标准模板库(STL)中的string是一个可变长的字符序列,它提供了一系列操作字符串的方法和功能。

            本篇文章,我们将模拟实现STL的string类的部分功能,以增强对STL的熟练度,了解STL容器的工作原理,积累项目经验,也为将来自主实现和改造容器奠定坚实的基础。

            STL的string类是一个模板,而我们为了方便实现,以达到练习的目的,我们暂时先实现一个成员变量为(下图示)的string类。    

    1. char* _str;
    2. size_t _size;//字符串长度,不加上\0
    3. size_t _capacity;

    C++ STL的string类提供了以下常用的成员函数和接口:

    1. 构造函数和赋值操作函数接口:

      • 默认构造函数:创建一个空字符串。
      • 带string参数的构造函数:将一个string对象复制到另一个string对象中。
      • 带字符数组参数的构造函数:将字符数组转换为string对象。
      • 带整数参数的构造函数:将整数转换为字符串。
      • 赋值操作符:用另一个string对象、字符数组或字符来赋值。
    2. 访问字符串内容相关函数接口:

      • at():返回指定位置的字符。
      • operator[]:返回指定位置的字符。
      • front():返回第一个字符。
      • back():返回最后一个字符。
      • c_str():返回一个以空字符结尾的字符数组。
    3. 修改字符串内容接口:

      • insert():在指定位置插入字符、字符串或字符数组。
      • erase():删除指定位置的字符。
      • replace():替换指定位置的字符串或字符。
      • append():在字符串末尾添加字符、字符串或字符数组。
      • clear():清空字符串。
    4. 字符串操作接口:

      • size() 或 length():返回字符串的长度。
      • empty():判断字符串是否为空。
      • find():查找指定字符串或字符的位置。
      • substr():返回指定位置和长度的子字符串。
      • compare():比较两个字符串

     (具体用法在上一篇讲解:【Cpp::STL】标准模板库_ string详解) 


    (一)头文件

            我们在C语言阶段实现声明和定义分离的时候,只是单一的把函数的定义放在.c(源)文件,把函数的声明,头文件的包含,宏定义等放在.h(头)文件。

            但是,在C++,不仅要遵守以上的规则,由于类的出现,需要域作用限定符(::)来限定方位;由于成员的访问权限的出现,需要考虑访问权限的问题;此外不同类型的成员的定义的位置也有讲究,比如静态成员尽量不要直接定义在头文件中,因为这会引发  多次包含多文件   在链接时的  头文件内的对象的重定义问题。

            本文根据STL标准模板库的功能,给出头文件,包括string类的定义,众多成员函数,部分非成员函数(流插入,流提取的重载),并在后半节详细讲解各个函数的实现思路。

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #pragma once
    3. #include
    4. #include
    5. #include
    6. using namespace std;
    7. namespace ddsm
    8. {
    9. class string
    10. {
    11. friend ostream& operator<<(ostream& out, const string& s1);
    12. public:
    13. //迭代器
    14. typedef char* iterator;
    15. typedef const char* const_iterator;
    16. iterator begin();
    17. const_iterator begin() const;
    18. iterator end();
    19. const_iterator end() const;
    20. //传参构造,默认构造,给默认值为空串,巧妙
    21. string(const char* str = "");
    22. string(const string& s);//copy constructor
    23. //string& operator=(const string& s);传统写法
    24. string& operator=(const char* s);
    25. string& operator=(string s);//现代写法
    26. //析构
    27. ~string();
    28. //C类型字符串
    29. const char* c_str() const;
    30. //保留
    31. void reserve(int n);
    32. string& push_back(const char ch);//尾插字符
    33. string& append(const char* str);//尾插字符串
    34. string& operator+=(char ch);
    35. string& operator+=(const char* str);
    36. string& insert(size_t pos, const char ch);
    37. string& insert(size_t pos, const char* str);
    38. //缺省值代表最一般的情况
    39. string& erase(size_t pos = 0,size_t len = npos);
    40. //找一个字符
    41. size_t find(const char ch, size_t pos = 0);
    42. //找一个子串
    43. size_t find(const char* str, size_t pos = 0);
    44. void swap(string& s);
    45. string substr(size_t pos = 0,size_t len = npos);
    46. string& clear();
    47. private:
    48. char* _str;
    49. size_t _size;//字符串长度,不加上\0
    50. size_t _capacity;
    51. //特例,const静态整形对象可声明定义和一,但是可能造成链接时的错误
    52. static size_t npos;
    53. };
    54. istream& operator>>(istream& in, string& s);
    55. };

    (二)string类的功能实现

    (1)默认成员函数

    i,构造函数

            我们知道,构造函数的作用是在对象实例化时初始化对象,对于string类对象,含有三个基本成员变量:

    1. char* _str;
    2. size_t _size;//字符串长度,不加上\0
    3. size_t _capacity;

            经过分析,我们得知在构造函数内部,需要申请动态的堆区空间给_str;需要根据_str的长度变化来动态更新_size;同时根据申请的动态空间的长度来更新_capacity。

            于是,我们理所当然的想到这样写构造函数:

    1. string::string(const char* str = "")
    2. // 缺省参数为一个空字符串,如果不传参,空字符串就是一个单独的'\0'
    3. :_size(strlen(str))
    4. ,_capacity(strlen(str))
    5. {
    6. _str = new char[_size + 1];
    7. strcpy(_str, str);
    8. }

            但是,这种简单易懂的写法也暴露出了弊端:多次无意义的重复调用strlen,这会造成额外的消耗。于是,为了减少strlen的调用次数,我们考虑这样修改:

            

    1. string::string(const char* str)
    2. :_size(strlen(str))
    3. ,_capacity(_size)
    4. {
    5. _str = new char[_size + 1];
    6. strcpy(_str, str);
    7. }

            这样修改虽然解决了strlen重复无意义调用的问题,但是也带来了新的问题:

    程序稳定性下降的问题:

    ¥¥我们知道:初始化列表的初始化顺序是成员函数在类中的声明顺序:按照此例:

    1. char* _str;
    2. size_t _size;//字符串长度,不加上\0
    3. size_t _capacity;

            先初始化_size,再初始化_capacity;在这种背景下,如果代码有一些微小的改变,或许就会造成意想不到的问题。

            如果改变成员变量的顺序,那么初始化列表就会按照不同的顺序初始化。具体来说,如果_capacity在_size之前,初始化列表就会先初始化_capacity:

    1. char* _str;
    2. size_t _capacity;
    3. size_t _size;//字符串长度,不加上\0

            这时_size还没有初始化,是随机值,那么就造成了_capacity为随机值的问题。

    解决这个问题其实很简单,将对_capacity的初始化放入函数体:

    1. string::string(const char* str)
    2. //strlen较低效,调用一次用size记录返回值
    3. //size/capacity不包含\0,但是其需要存储
    4. :_size(strlen(str))
    5. {
    6. _str = new char[_size + 1];
    7. _capacity = _size;
    8. strcpy(_str, str);
    9. }

            这样就确定了是先初始化_size,再初始化_capacity。¥¥

            (将声明和定义分离,需要将缺省参数放在声明处,同时函数名之前需要加上域作用限定符,表示这个函数在你实现的string类里面声明过。)

    ii,析构函数

             析构函数的作用是:清理资源。

    由于比较简单,这里直接给出实现:

    1. //析构
    2. string::~string()
    3. {
    4. if(_str)
    5. delete[] _str;
    6. _size = _capacity = 0;
    7. _str = nullptr;
    8. }

    (函数名之前需要加上域作用限定符,表示这个函数在你实现的string类里面声明过。)

    iii,拷贝构造

           拷贝构造,完成创建对象时的初始化。

    一般情况下,我们会这样写:

    1. //拷贝构造
    2. string::string(const string& s)
    3. {
    4. char* tem = new char[s._capacity+1];//多开一个,存储'\0'
    5. strcpy(tem, s._str);
    6. delete[] _str;//销毁原空间
    7. _str = tem;
    8. _size = s._size;
    9. _capacity = s._capacity;
    10. }

    但是,其实有更简单的写法:

    1. void string::swap(string& s)
    2. {
    3. //调用模板swap交换内置类型,损失不大
    4. std::swap(_str, s._str);
    5. std::swap(_capacity, s._capacity);
    6. std::swap(_size, s._size);
    7. }
    8. //拷贝构造的现代写法
    9. string::string(const string& s)
    10. :_str(nullptr)
    11. {
    12. string tem(s._str);
    13. swap(tem);
    14. }

    仔细分析,我们其实在无形之中让构造函数给我们“打工”了:

    string tem(s._str);

    就是用拷贝对象的字符串来构造一个tem对象,而这个tem对象就是我们需要的,所以我们实现一个swap函数,将*this与tem完全交换,同时tem在出作用域时也会自动析构,同样也达到了拷贝构造的目的。

    iv,赋值重载

    赋值重载:实现对象之间的赋值。

    我们一般会这样实现:

    1. //赋值重载
    2. string& string::operator=(const char* s)
    3. {
    4. int len = strlen(s);
    5. char* tem = new char[len + 1];
    6. strcpy(tem, s);
    7. delete[] _str;
    8. _str = tem;
    9. _size = _capacity = len;
    10. return *this;
    11. }

     但是,同样也有更简单的写法:

    1. void string::swap(string& s)
    2. {
    3. //调用模板swap交换内置类型,损失不大
    4. std::swap(_str, s._str);
    5. std::swap(_capacity, s._capacity);
    6. std::swap(_size, s._size);
    7. }
    8. //赋值重载的现代写法
    9. string& string::operator=(string tem)
    10. {
    11. //自动调用拷贝构造
    12. swap(tem);
    13. //出作用域自动完成析构
    14. return *this;
    15. }

    在无形之中,我们让拷贝构造为我们“打工”。

    我们通过传值传参,拷贝构造一个临时对象tem,这个tem就是我们需要的,所以完全交换*this就得到了构造的对象,同时tem出作用域也会自动析构。

    (2)迭代器

             对于迭代器,本质上是一个指针,也可以是一个类(对指针的封装),在这里,我们不妨用指针来作为迭代器:

    1. //声明:
    2. typedef char* iterator;
    3. typedef const char* const_iterator;
    4. iterator begin();
    5. const_iterator begin() const;
    6. iterator end();
    7. const_iterator end() const;
    1. //定义
    2. string::iterator string::begin()
    3. {
    4. return _str;
    5. }
    6. string::const_iterator string::begin() const
    7. {
    8. return _str;
    9. }
    10. string::iterator string::end()
    11. {
    12. return _str + _size;
    13. }
    14. string::const_iterator string::end() const
    15. {
    16. return _str + _size;
    17. }

            const迭代器用于const对象调用;普通迭代器用于普通迭代器调用。

    普通迭代器可读可写,const迭代器只可读不可写。

    (3)容量和长度

     i.reserve()

            改变string的容量,若要求值n大于现在的容量,则容量扩大到n;若要求值小于等于现有容量,则改变容量。

            reserve对于size没有影响,不会改变string的内容。

    实现如下:

    1. //保留指定容量,容量只增不减
    2. void string::reserve(int n)
    3. {
    4. //要求保留的大于现有容量,需要扩容
    5. if (n > _capacity)
    6. {
    7. char* tem = new char[n + 1];
    8. // 申请新空间完毕,转移数据
    9. strcpy(tem, _str);
    10. delete[] _str;
    11. _str = tem;
    12. _capacity = n;
    13. //reserve不改变size
    14. }
    15. }

    ii,resize()

    1. //resize()不改变capacity,可能改变size
    2. void string::resize(int size,int ch)
    3. //size为设定值,_size为现有值
    4. {
    5. if (size < _size)
    6. {
    7. _size = size;
    8. _str[size] = '\0';
    9. }
    10. else if (size > _size)
    11. {
    12. if (size > _capacity)
    13. {
    14. reserve(size);
    15. }
    16. int i = _size;
    17. while (i != size)
    18. {
    19. _str[i++] = '\0';
    20. }
    21. _size = size;
    22. _str[_size] = '\0';
    23. }
    24. }

            如果设定值小于现有值,减小_size,相当于截断_str;

            如果设定值等于现有值,不做处理;

            如果设定值大于现有值,有三种情况:

                    size <_capacity:        不扩容,并在[ _size,size)之间补0;

                    size == _capacity:        不扩容,并在[ _size,size)之间补0;

                    size > _capzcity:        扩容,并在[ _size,size)之间补0;

    (4)元素访问

     i,operator[]

            下标的随机访问:

    1. //声明
    2. char& operator[](size_t pos);
    3. const char& operator[](size_t pos) const;
    1. //定义
    2. char& string::operator[](size_t pos)
    3. {
    4. assert(pos >= 0 && pos < _size);
    5. return _str[pos];
    6. }
    7. const char& string::operator[](size_t pos) const
    8. {
    9. assert(pos >= 0 && pos < _size);
    10. return _str[pos];
    11. }

    对于at,front,back可以复用operator[]来实现。

    (5)修改方式

    i,push_back()

            实现尾插字符,实现如下:

    1. //尾插字符,由于是一个一个插入,扩容不能太频繁,所以采用二倍扩容
    2. string& string::push_back(const char ch)
    3. {
    4. if (_size == _capacity)
    5. //不一定需要扩容,若长度等于容量,再次插入需要扩容
    6. {
    7. int Newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
    8. reserve(Newcapacity);
    9. }
    10. //扩容完毕,尾插字符
    11. _str[_size++] = ch;
    12. _str[_size] = '\0';
    13. return *this;
    14. }

            这里使用了一个扩容技巧,就是二倍扩容。

    ii,append()

            追加,这里简化为追加一段字符串。

    1. //尾插字符串,直接reserve到指定长度字符串
    2. string& string::append(const char* str)
    3. {
    4. int len = strlen(str);
    5. if (len + _size > _capacity)
    6. {
    7. reserve(len + _size);//不改变size
    8. }
    9. //扩容完毕
    10. strcpy(_str + _size, str);
    11. _size += len;
    12. return *this;
    13. }

            首先要先保存原来的len,这样如果需要扩容,在扩容完毕之后,只需更新_size为原_size+=len即可。

            否则,如果不保存len,在需要扩容的情况下,就会出现问题了:

    ##

    ()

    ##

    iii,operator+=复用上两函数即可

            尾插一个字符

    1. string& string::operator+=(char ch)
    2. {
    3. push_back(ch);
    4. return *this;
    5. }

            尾插一个字符串

    1. string& string::operator+=(const char* str)
    2. {
    3. append(str);
    4. return *this;
    5. }

    iv,insert()

            在任意位置插入一个字符

    1. //插入一个字符
    2. //用push_back逻辑来扩容
    3. string& string::insert(size_t pos, const char ch)
    4. {
    5. assert(pos >= 0 && pos <= _size);
    6. if (_size == _capacity)
    7. {
    8. int Newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
    9. reserve(Newcapacity);//不改变size
    10. }
    11. int end = _size+1;
    12. //细节问题,int与size_t参与比较,
    13. //int隐式类型转化为size_t
    14. //size_t(-1)会变成很大的整数
    15. while(end>pos)
    16. {
    17. _str[end] = _str[end-1];
    18. --end;
    19. }
    20. _str[pos] = ch;
    21. _size += 1;
    22. return *this;
    23. }

             在任意位置插入一个字符串
    1. //插入一个字符串
    2. //用reserve逻辑扩容
    3. string& string::insert(size_t pos, const char* str)
    4. {
    5. assert(pos >= 0 && pos <= _size);
    6. int len = strlen(str);
    7. if (len + _size > _capacity)
    8. {
    9. reserve(len+_size);
    10. }
    11. int end = _size + len;
    12. while (end>pos+len-1)
    13. {
    14. _str[end] = _str[end - len];
    15. --end;
    16. }
    17. memmove(_str + pos, str, len);
    18. _size += len;
    19. return *this;
    20. }

    v,erase()

            在任意位置处删除长度为len的字符串:

    1. string& string::erase(size_t pos, size_t len)
    2. //两种情况;删除部分string,pos之后全删
    3. {
    4. assert(pos >= 0 && pos <= _size);
    5. if ((len == npos) ||(pos + len >= _size))//全删的情况
    6. {
    7. _str[pos] = '\0';
    8. _size = pos;
    9. }
    10. else
    11. //删除部分string
    12. {
    13. int end = pos + len;
    14. while (_str[end]!='\0')
    15. {
    16. _str[end - len] = _str[end];
    17. ++end;
    18. }
    19. _str[end-len] = '\0';
    20. }
    21. return *this;
    22. }

    (6)串操作

    i,find()

            找字符

    1. size_t string::find(const char ch, size_t pos)
    2. {
    3. for (size_t i = pos; i < _size; ++i)
    4. {
    5. if (_str[i] == ch)
    6. {
    7. return i;
    8. }
    9. }
    10. return npos;
    11. }

            找字符串

            用到了strstr():字符串匹配函数。

    1. size_t string::find(const char* str, size_t pos)
    2. {
    3. char* ret = strstr(_str, str);
    4. return (size_t)(ret - _str);
    5. }

    ii,c_str()

            返回C类型的字符串:

    1. const char* string::c_str() const
    2. {
    3. return _str;
    4. }

    iii,substr()

            得到字符串的子串:

    1. string string::substr(size_t pos, size_t len)
    2. {
    3. assert(pos >= 0 && pos <= _size);
    4. if ((len == npos) || (pos + len >= _size))
    5. {
    6. string sub(_str + pos);
    7. return sub;
    8. }
    9. else
    10. {
    11. string sub;
    12. sub.reserve(len);
    13. for (size_t i = 0; i < len; ++i)
    14. {
    15. sub._str[i] = _str[pos + i];
    16. }
    17. sub._str[len] = '\0';
    18. sub._size =sub._capacity = len;
    19. return sub;
    20. }
    21. }

    (7)成员常量

    1. //特例,const静态整形对象可声明定义和一,但是可能造成链接时的错误
    2. const static size_t npos = -1;

            无符号整数size_t(-1)是一个很大的整数。

    (8)流插入和流提取

    i,operator<<()

    1. ostream& operator<<(ostream& out, const string& s)
    2. {
    3. for (size_t i = 0; i < s._size; ++i)
    4. {
    5. cout << s._str[i];
    6. }
    7. cout << endl;
    8. return out;
    9. }

    ii,operator>>()

            cin的get()函数可以提取空白字符和‘\n’,这也是循环逻辑结束的条件。

    1. //流提取改进,用buf临时数组,防止string频繁扩容
    2. istream& operator>>(istream& in,string& s)
    3. {
    4. s.clear();
    5. char buff[128] = { 0 };
    6. char ch = in.get();
    7. int i = 0;
    8. while(ch != ' ' && ch != '\n')
    9. {
    10. buff[i++] = ch;
    11. ch = in.get();
    12. if (i == 127)
    13. {
    14. buff[i] = '\0';
    15. s += buff;
    16. i = 0;
    17. }
    18. }
    19. buff[i] = '\0';
    20. if (i != 0)
    21. {
    22. s += buff;
    23. }
    24. return in;
    25. }

            整体使用了用临时栈区数组的方式来减少扩容次数,提高效率。


    完~

    未经作者同意禁止转载 

  • 相关阅读:
    SpringBoot2基础篇(二)—— 基础配置
    水体中磷赋存形态
    浅谈新生代为什么要分三块区域并且比例为什么是8:1:1
    【JSP】JSTL汇总——源码解析
    嵌入式Linux裸机开发(一)基础介绍及汇编LED驱动
    k8s kubesphere 部署 harbor私服仓库
    java计算机毕业设计基于springboot人职匹配推荐系统
    IMX6Q的SD卡启动使用教程(1):uboot与kernel编译移植
    【附源码】计算机毕业设计SSM泰兴市公交信息系统
    评估指标及代码实现(NDCG)
  • 原文地址:https://blog.csdn.net/2301_79465388/article/details/139132429