ToB企服应用市场:ToB评测及商务社交产业平台

标题: 【C++ STL】模拟实现 string [打印本页]

作者: 花瓣小跑    时间: 2024-6-13 19:49
标题: 【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;
  4.                
复制代码

   C++ STL的string类提供了以下常用的成员函数和接口:
     (具体用法在上一篇讲解:【Cpp::STL】标准模板库_ string详解) 

(一)头文件

        我们在C语言阶段实现声明和定义分离的时间,只是单一的把函数的定义放在.c(源)文件,把函数的声明,头文件的包罗,宏定义等放在.h(头)文件。
        但是,在C++,不光要服从以上的规则,由于类的出现,必要域作用限定符(::)来限定方位;由于成员的访问权限的出现,必要思量访问权限的问题;此外差别范例的成员的定义的位置也有讲究,比如静态成员尽量不要直接定义在头文件中,由于这会引发  多次包罗多文件   在链接时的  头文件内的对象的重定义问题。
           本文根据STL标准模板库的功能,给出头文件,包括string类的定义,众多成员函数,部分非成员函数(流插入,流提取的重载),并在后半节具体讲解各个函数的实现思绪。
  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #pragma once
  3. #include<iostream>
  4. #include<cstring>
  5. #include<cassert>
  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.                 //缺省值代表最一般的情况
  40.                 string& erase(size_t pos = 0,size_t len = npos);
  41.        
  42.                 //找一个字符
  43.                 size_t find(const char ch, size_t pos = 0);
  44.                 //找一个子串
  45.                 size_t find(const char* str, size_t pos = 0);
  46.                 void swap(string& s);
  47.                 string substr(size_t pos = 0,size_t len = npos);
  48.        
  49.                 string& clear();
  50.         private:
  51.                 char* _str;
  52.                 size_t _size;//字符串长度,不加上\0
  53.                 size_t _capacity;
  54.                
  55.                 //特例,const静态整形对象可声明定义和一,但是可能造成链接时的错误
  56.                 static size_t npos;
  57.         };
  58.         istream& operator>>(istream& in, string& s);
  59.        
  60. };
复制代码

  (二)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.    
  7.         delete[] _str;//销毁原空间
  8.         _str = tem;
  9.         _size = s._size;
  10.         _capacity = s._capacity;
  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(const string& s)
  10.         :_str(nullptr)
  11. {
  12.         string tem(s._str);
  13.         swap(tem);
  14. }
复制代码
仔细分析,我们着实在无形之中让构造函数给我们“打工”了:
  1. 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.                        
  20.                         return sub;
  21.                 }
  22.         }
复制代码

  (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. }
复制代码
        整体使用了用临时栈区数组的方式来淘汰扩容次数,进步效率。
  
完~

未经作者同意禁止转载 


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4