• C++ string 类实现


    写在前面

    上一个博客我们已经学习过string怎么使用了,这里就开始模拟实现,我们不是实现所有的函数,要知道string里面有106个函数,我们实现的是那些比较常用的,知道了string的底层,就可以进一步提高自己的能力.我们尽量和库里面的源代码形式写,风格保持一直.

    深浅拷贝

    在谈模拟实现的时候,我们要看看深拷贝和浅拷贝,其中编译器自动生成的就是值拷贝,也就是浅拷贝.

    • 浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规
    • 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供

    我们可以这么理解.

    我们用一个简单的类来演示一下深浅拷贝究竟是什么意思,光说的话还是有点疑惑.

    template<class T>
        class basic_string
        {
            public:
            basic_string(const T* str = "")
                :_str(new T[strlen(str)+1])
                {
                    strcpy(_str, str);
                    _size = strlen(str);
                    _capacity = _size;
                }
            private:
            T* _str;
            size_t _capacity;
            size_t _size;
        };
    
    typedef basic_string<char> string;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    浅拷贝 只是把值简单的付给新的对象,没有看它管理的空间

    image-20220715120102626

    basic_string<T>(const basic_string& a)
    {
        _str = a._str;
        _capacity = a._capacity;
        _size = a._size;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    深拷贝 我们要不它管理空间的内容也要拷贝回来.

    image-20220715121309681

    basic_string<T>(const basic_string& a)
    {
        _str = new T[strlen(a._str) + 1];
    
        strcpy(_str, a._str);
        _capacity = a._capacity;
        _size = a._size;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    模拟实现 string

    我们心里面已经对string底层原理有点把握了,类里面不就是一个数组,以及标记容量和长度的变量,这是一种想法.我们这里就按照找这个方法来实现.

    string类

    我们先来把string的基本结构给搭起来,里面存在三个成员变量.

    class string
    {
    public:  
    
    private:
          char* _str;       //   存储字符的空间
          size_t _size;     //   有效字符的长度
          size_t _capacity; //   能够存储 有效数组的长度
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    构造函数

    我们现来写一下构造函数,这里面我要说一句,我们开辟的容量总是要比实际存储的最大用量多上一个,这是因为最后存在一个’\0’,为了支持C语言.者构造函数是最基本的,后面如果还需要其他的构造函数,我们再来添加

    string(const char* str = "")
            :_size(strlen(str))
    {
    	_capacity = _size;
        _str = new char[_capacity + 1];  //多一个
        strcpy(_str,str); // 会拷贝 '\0' 的 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们先来看看什么是**“”**,可以这们理解,这是一个空字符串,里面包含了一个’\0’字符,其他的什么都没有了.

    int main()
    {
    	const char* str = "";
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    image-20220730161632199

    迭代器

    string里面的迭代器是原生指针,这里没有什么可以解说的,我们只说正向迭代器,反向的等到list在和大家解释.

    class string
    {
        public:
          typedef char* iterator;
          typedef const char* const_iterator;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    begin()

    iterator begin()
    {
        return _str;
    }
    const_iterator cbegin() const 
    {
        return _str; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    end()

    iterator end()
    {
        return _str + _size;
    }
    const_iterator cend() const 
    {
        return _str + _size; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    size() && capacity()

    size()是计算有效字符长度的函数,capacity()是计算最大容量的.这里没有什么可以说了,不过我们最好使用const修饰this,这样不能修改的string也可以调用.

    size_t size() const 
    {
        return _size;
    }
    
    size_t capacity() const 
    {
        return _capacity;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    reserve(size_t n = 0)

    这个函数可以理解为是扩容函数,前面的博客我们都已经谈过了,这里我就把简单的情况在列一下,不啰嗦了.

    • 当 n > _capacity 只改变容量,否则什么也不做
    • 不改变 size,也就是你可以理解为它是只扩容.
    void reserve(size_t n = 0)
    {
        if(n > _capacity)
        {
            char* temp = new char[n + 1];
            strcpy(temp, _str);
            delete[] _str;
            _str = temp;
            _capacity = n;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们来测试一下.

    #include "string.hpp"      
    using namespace std;      
          
    int main()      
    {      
      bit::string s;      
      s.reserve(10);  
      cout << "size:" << s.size() << endl;  
      cout << "capacity:" << s.capacity()<< endl;
        
      s.reserve(1);  
      cout << "size:" << s.size() << endl;  
      cout << "capacity:" << s.capacity()<< endl;
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    image-20220730163543291

    c_str()

    这个是返回字符串的地址,主要是为了接容C语言,我们这里先实现出来,主要是看看后面的插入和删除是不是正确,因为这还没有实现流提取和流插入.

    const char* c_str() const
    {
        return _str;
    }
    
    • 1
    • 2
    • 3
    • 4

    insert()

    插入元素分为两种情况,一种是在指定位置插入一个字符,另外一种是插入一个字符串.

    insert(size_t pos, char c)

    这个是最简单的,我们只需要仔细一点就可以了.

     string& insert(size_t pos,char c)
     {
         assert(pos >= 0 && pos <= _size);
         if(_size == _capacity)
         {
             size_t newcap = _capacity == 0 ? 4 : 2 * _capacity;  // 刚开始构造的时候  容量 为 0
             reserve(newcap); // 扩容
         }
         size_t it = _size + 1;
         while(it != pos)
         {
             _str[it] = _str[it-1];
             it--;
         }
         _str[pos] = c;
         ++_size;
         return *this;
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们来验证一下就可以了.

    int main()  
    {
      bit::string s;
      s.insert(0,'1');
      s.insert(1,'2');
      cout << s.c_str() << endl;
                                                                                                                    
      return 0;           
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image-20220730164757935

    nsert(size_t pos, const char* str)

    这个才是最难得,我们需要指定好位置,然后又要好好得移动位置,最关键得是需要精准得拷贝.

    string& insert(size_t pos,const char* str)
    {
        assert(pos <= _size);
        size_t len = strlen(str);
        // 看看是不是要扩容
        reserve(len + _size);
        
        size_t it = _size + len;
        while(it - pos > len-1)  // 这里 一定要判断好
        {
            _str[it] = _str[it - len];
            it--;
        }
        for(size_t i = 0;i < len;i++)
        {
            _str[pos++] = *(str+i);
        }
        _size = _size + len;
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    验证一下

    int main()      
    {      
      bit::string s;      
      s.insert(0,"aaaaaaaaaabbbbbbb");                                                                              
      cout << s.c_str() << endl;      
          
      return 0;      
    }      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20220730165318767

    push_back() && append()

    这个两个就可以复用上面得代码了.

    • push_back() 尾插一个 元素
    • append() 尾插一个字符串
    void push_back(const char ch)
    {
        insert(_size,ch);
    }
    
    string& append(const char* str)
    {
        insert(_size,str);
    
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    int main()  
    {
      bit::string s;
      s.push_back('a');
      s.append("bcdefg");
      cout << s.c_str() << endl;                                                                                    
      return 0;           
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20220730165949636

    operator+=

    相比较于上面得尾插,我们还是更喜欢使用+=,但是本质还是复用了原本的代码.

    string& operator+=(const char ch)
    {
        push_back(ch);
        return *this;
    }
    
    string& operator+=(const char* str)
    {
        append(str);
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    int main()               
    {                        
      bit::string s;         
      s += 'c';  
      s += "abdefr";  
      cout << s.c_str() << endl;  
      return 0;  
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    image-20220730170438968

    earse()

    删除元素这里我只实现了从指定的当前位置开始删,删除指定元素的位置,如果没有指定,那么就删除npos个,这里npos还未声明,我们这先声明和定义处出来就可以了.

    class string
    {
    public:  
    
    private:
          char* _str;       //   存储字符的空间
          size_t _size;     //   有效字符的长度
          size_t _capacity; //   能够存储 有效数组的长度
          static size_t npos;
    };
    const size_t string:: npos = -1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在我们就可以来删除数据了.这里我们给缺省值.

    string& earse(size_t pos,size_t len = npos)
    {
        assert(pos < _size);
        //  判断剩余的长度
        if(len == npos || len >= _size - pos)
        {
            _str[pos] = '\0';
            _size = pos;
        }
        else 
        {
            for(size_t i = pos+len;i<=_size;i++)
            {
                _str[i-len] = _str[i];
            }
            _size = _size - len;
        }
    
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    resize()

    这个函数是调整_size的的大小的.我们这里还是只实现一种情况.

    void resize(size_t n,char ch = '\0')
    {
        assert(n >= 0);
        reserve(n);
        //  三种情况
        if(n >= _size)
        {
            memset(_str + _size,ch,n - _size);
            _size = n;
            _str[_size] = '\0';
        }
        else 
        {
            _str[n] = '\0';
            _size = n;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    find()

    find()是查找string里面是不是有我们想要的字符或者字符串,默认是从下标0开始的,返回的是字符的下标或者是字符串的的头位置,找不的话就是返回npos.

    size_t find(char ch,size_t pos = 0) const 
    {
        for(;pos < _size;++pos)
        {
            if(_str[pos] == ch)
            {
                return pos;
            }
        }
        return npos;
    }
    
    size_t find(const char* str,size_t pos = 0) const
    {
        const char* p = strstr(_str+pos,str);
        if(p == nullptr)
        {
            return npos;
        }
        return p - _str;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    重载比较运算符

    这里的函数都是全局函数,我们在这里直接给出了.

    bool operator<(const string& s1,const string& s2)
      {
        return strcmp(s1.c_str(),s2.c_str()) < 0;
      }
    
      bool operator==(const string& s1,const string& s2)
      {
        return strcmp(s1.c_str(),s2.c_str()) == 0;
      }
    
      bool operator<=(const string& s1,const string& s2)
      {
        return s1 < s2 || s1 == s2;
      }
    
      bool operator>(const string& s1,const string& s2)
      {
        return !(s1 <= s2);
      }
    
      bool operator>=(const string& s1,const string& s2)
      {
        return !(s1 < s2);
      }
    
      bool operator!=(const string& s1,const string& s2)
      {
        return !(s1 == s2);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    析构函数

    析构函数很简单的,记住把空间给释放了就可以了.

    ~string()
    {
        if(_str)
        {
            delete[] _str;
            _size = _capacity = 0;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    operator=

    这个大家小心一点就可以了,没什么可以说的.

    string& operator=(const string& str)
    {
        if(this != &str)
        {
            char* temp = new char[str._capacity + 1];
            strcpy(temp,str._str);
            delete[] _str;
            _str = temp;       
            _size = str._size;
            _capacity = str._capacity;
        }
    
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    拷贝构造

    我们来写一个现代的写法和一个非现代写法.

    // 现代写法
    string(const string& str)
    {
       string ret;
       ret = str;
       swap(ret->_str,_str);
       swap(ret->_size,_size);
       swap(ret->_capacity,_capacity);
    }
    
    // 非现代
    string(const string& str)
    {
        char* temp = new char[str._capacity + 1];
        strcpy(temp,str._str);
        _str = temp;
        _size = str._size;
        _capacity = str._capacity;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    operator[]

    string支持随机访问.因为它底层是一个数组.

    char& operator[](size_t pos)
    {
        assert(pos >= 0 && pos < _size);
        return _str[pos];
    }
    
    const char& operator[](size_t pos) const 
    {
        assert(pos >= 0 && pos < _size);
        return _str[pos];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    流插入 和 流提取

    这个还是友元函数,

    //  流插入 <<
    std::ostream& operator<<(std::ostream& out, string& s)
    {
        //cout<< s.c_str() << endl; // 这个遇到 '\0'就停止了,但是不妨碍 string 似是 aaaaa\0\0\0\0这样的,所以用下面的
        for(const char ch : s)
        {
            out << ch;
        }
        return out;
    }
    
    //  流提取 >>
    std::istream& operator>>(std::istream& in,string& s)
    {
    
        // 来个 优化区间
        s.clear();
        char buff[128] = {'\0'};
        char ch;
        ch = in.get();
        size_t i = 0;
        while(ch != ' ' && ch != '\n')
        {
            buff[i++] = ch;
            ch = in.get();
            if(i == 127)
            {
                s += buff;
                i = 0;
                memset(buff,'\0',128);
            }
        }
    
        s += buff;
        memset(buff,'\0',128);
        return in;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    我们来测试一下流插入为何这么写的,你看下库里面的就知道了.

    int main()
    {
    	string str;
    	str += 'a';
    	str += 'a';
    	str += 'a';
    	str += 'a';
    	str += 'a';
    
    	str += '\0';
    	str += '\0';
    	str += '\0';
    	str += '\0';
    	str += '\0';
    	str += 'a';
    	str += 'a';
    	for (auto ch : str)
    	{
    		cout << ch;
    
    	}
    	cout<< endl;
    
    	cout << str.c_str()<< endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    image-20220730192858586

  • 相关阅读:
    Linux安装Hadoop超详细教程
    2024/6/5(页面静态化,熔断降级,降级处理,ES搜索实例,课程信息同步,认证授权,单点登录,Spring Security,OAuth2,授权模式)
    智慧农业建设方案中的物联网技术
    笔试刷题Day—4
    docker 知识仓库
    Linux服务器被入侵后的排查思路(应急响应思路)
    k8s集群中流水线部署微服务
    XML解析
    免费开源漏扫软件 Nessus
    java是干什么的
  • 原文地址:https://blog.csdn.net/m0_61334618/article/details/126076632