Learning C++ No.11【string类实现】

简介: Learning C++ No.11【string类实现】

引言:

北京时间:2023/2/19/8:48,昨天更新了有关进程状态的博客,然后在休息的时候,打开了腾讯视屏,然后看到了了一个电视剧,导致上头,从晚上6点看到了10点,把我宝贵的博客时间给搞没了,伤心,现在就让我们开始将功补过,把昨天就应该开始写的博客给补一补!加油,不怂,就是干!今天博客的内容非常简单,就是使用我们以前学过的知识,无论是类和对象、内存管理,还是string类中的函数使用,运用它们写一个较为完整的string类出来,也就是自己实现string类,写库里面的东西,想想还是挺激动的,就让我们带着这份激动,go go go!


image.png


string类的模拟实现

string类基本框架

首先开始学习之前,明白string类的底层就是一个字符数组而已,本质上我们在实现string类的过程中,也就是在补全之前学习中的一些不足,和一些重难点,当然最关键的可以复习之前的各种知识点,并且在复习的过程中更好的理解这些知识点的使用,把一小块一小块的知识给合并成一大块的知识,所以模拟实现string类是非常重要的。


步入正题,实现string类基本框架,如下代码:从代码入手,再做细节方面处理


52.png


以上就是一个string类的基本框架,大部分函数功能还没有实现,只是实现了基本的构造函数和析构函数以及一些细节方面的处理。


string类框架之后的地基

搞定了上述有关string类中的构造函数和析构函数,当然最重要的是类和对象的使用,我们可以说是有了一个string类的基本框架,也可以说是我们拥有了一个基本的类框架,所以现在就让我们走进string类,复习并使用更多的知识来实现这个STL中的经典字符类。


拷贝构造

在搞定了构造函数和析构函数,之后实现一个类最重要的莫过于是拷贝构造函数了,虽然拷贝构造函数和构造函数、析构函数一样,都是一个默认成员函数,但是懂的都懂,默认成员函数并不是万能的,准确的来说是编译器并不是万能的,这些默认成员函数大部分都只是对内置类型起作用,而我们自己实现的自定义类型就像是后娘养的,人家是爱答不理,并不能很好的把自定义类型进行相应的初始化,所以对这些后娘养的自定义类型,则需要我们自己来实现初始化,当然经典的初始化场景有在初始化列表初始化、给缺省值(但本质还是在初始化列表初始化)、调用拷贝构造函数初始化等,所以调用拷贝构造函数初始化是自定义类型初始化的一个好地方,我们现在就来复习巩固一下拷贝构造函数。(当然重点就是想要讲深浅拷贝问题)

如下图:


53.png


当然上述的前提是通过成员变量中有一个char* _str的指针,涉及指针就涉及指针指向的空间,就涉及深拷贝问题,涉及深拷贝就涉及析构问题,这些看似无关,却紧密相连,这就是自我实现string类的好处,搞清各个知识点之间的关系和熟练掌握运用,所以以后写拷贝构造函数第一点就是考虑深浅拷贝问题。


const成员函数使用场景和运算符重载


54.png


从上图中我们可以发现,我们许多函数在实现的时候,都可以去调用那些已经自己实现好了的函数,或者库函数来实现一些新的功能,并且可以发现,只要使用const来修饰成员函数,只要我们对该函数不做改变数据的操作,这种方法是很好的,可以有效的避免权利放大问题,解决不必要的麻烦,可以让我们使用const对象调用函数的时候,变得更加的放心,程序变得更加的稳定。 并且注意: 我们在进行运算符重载时,进行字符串的比较,使用的是strcmp函数,表明,此时我们比较的是该字符串的ASCII码值,而不是该字符串的大小和容量。


string类中迭代器的实现

55.png


从上图可以看出,范围for的本质就是迭代器,从迭代器可以实现语法糖来分析,足以看出迭代器身为STL六大天王之一不是浪得虚名的。并且此时注意: const修饰的迭代器,该迭代器对象是可以被改变的,只是该对象中指向的内容不可以被修改而已

扩容函数和字符、字符串插入函数

56.png


总了来说,字符和字符串插入删除函数大致上都是差不多的,细节方面处理到位就行,跟数据结构中的顺序表本质上是一样的,这里就不过多介绍。并且此时我们把字符插入这些函数实现之后,string类中的函数,也就完成了地基部分,此时我们完成了地基就可以开始盖房子了,由于时间关系,我们把盖房子部分留到下一篇博客。


具体代码如下: 包括测试部分(注释很全)

#define  _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
namespace wwx
{
  class String
  {
  public:
    typedef char* iterator;//普通类型迭代器
    typedef const char* const_iterator;//const类型迭代器(注意此时自己是可以修改的,只是解引用后的值是不可以修改的)
    iterator begin()
    {
      return _str;//此时因为String本质上就是一个字符数组,所以_str就是首元素地址,就是第一个字符
    }
    iterator end()
    {
      return _str + _size;//这个就是最后一个字符(前提是要知道第一个字符的位置)
    }
    const_iterator begin()const
    {
      return _str;
    }
    const_iterator end()const
    {
      return _str + _size;
    }
    String(const char* str = " ")//或者写成"\0",反正这个位置只要可以让strlen算出一个0来就行了(全缺省构造函数)
      : _size(strlen(str))
    {
      _capacity = _size == 0 ? 3 : _size;//_capacity为0第一种解决方法
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }
    String(const String& s)//注意:拷贝构造也是有初始化列表的(并且要回想起以前有关this指针的知识,此时*this就是s3,str就是s2)
      :_size(s._size)
      ,_capacity(s._capacity)
    {
      //深拷贝(因为使用了指针,或者说因为有自己实现析构函数)
      _str = new char[s._capacity + 1];
      strcpy(_str, s._str);
    }//并且此时要记住,此时的拷贝构造除了利用*this指针以外,还有一个是使用赋值运算符(=)
    String& operator=(const String& s)//区分赋值和拷贝构造,赋值是两个已经存在的对象,而拷贝构造是一个已经初始化的对象去初始化另一个要创建的对象
    {
      if (this != &s)//防止自己给自己赋值
      {
        //_size = s._size;
        //_capacity = s._capacity;
        //delete[]_str;//这种赋值方法,可以很好的避免被赋值空间太大或太小的问题,只是伴随着开空间的消耗而已
        //_str = new char[s._capacity + 1];
        //strcpy(_str, s._str);
        //为了防止空间开辟失败把原来的空间中的数据给破坏,下面的写法就更好
        char* tmp = new char[s._capacity + 1];//解决原理:先开空间,再销毁,再给给
        strcpy(tmp, s._str);
        delete[]_str;
        _str = tmp;//此时因为指针指向的空间,本质是内置类型,所以会自己去调用拷贝构造函数,不需要调用我们自己实现的拷贝构造函数
        _size = s._size;
        _capacity = s._capacity;
      }
      return *this;
    }
    ~String()
    {
      delete[]_str;
      _str = nullptr;
      _size = _capacity = 0;
    }
    const char* c_str()
    {
      return _str;
    }
    char& operator[](size_t pos)//普通类型使用,下面的是给特殊的const类型函数使用
    {
      assert(pos < _size);
      return _str[pos];
    }
    const char& operator[](size_t pos)const//后面位置上给了const前面就一定也要给一个const,因为此时加了const导致返回值的类型也变成了const类型
    {
      assert(pos < _size);
      return _str[pos];
    }
    size_t size()const
    {
      return _size;
    }//运算符重载
    // 所以得出结论,只要是函数内部不进行数据修改的,我们就把const给加上
    bool operator>(const String& s)const
    {
      return strcmp(_str, s._str) > 0;
    }
    bool operator==(const String& s)const
    {
      return strcmp(_str, s._str) == 0;
    }
    bool operator>=(const String& s)const
    {
      return *this > s || *this == s;
      //return *this > s || s == *this;//此时就是简单的把赋值顺序调换一下,该代码就是有问题的,因为此时的s是const类型的,妥妥的权利放大
    }                                  
    bool operator<(const String& s)const
    {
      return !(*this >= s);
    }
    bool operator<=(const String& s)const
    {
      return !(*this > s);
    }
    bool operator!=(const String& s)const
    {
      return !(*this == s);
    }
    void resize(size_t n, char ch = '\0')
    {
      int len = strlen(_str);
      if (_capacity < n)
      {
        reserve(2 * n);
      }
      if (n < _size)
      {
        _size = n;
        _str[_size] = '\0';
        return;
      }
      _size = n;
      for (int i = len; i < _size; i++)
      {
        _str[i] = ch;
      }
      _str[_size] = '\0';
    }
    void reserve(size_t n)//此时的这个n参数表示的就是n需要扩n个空间
    {
      char* tmp = new char[n + 1];//此时为了像上述一样,防止开辟失败,所以先开辟,再赋值(注意:capacity和字符个数的区别,capacity少1)
      strcpy(tmp, _str);//此时就是注意:只要是在类里面的函数都是自带一个this指针的类对象
      delete[]_str;
      _str = tmp;
      _capacity = n;//开空间跟_size是没有关系的,只有插入数据的时候才跟_size有关系
    }
    void push_back(char ch)//注意:有一个this指针,此时就是为了在这个this指针后面插入字符
    {
      if (_size == _capacity)//这种判断==的是需要多少扩多少(所以可以二倍二倍的扩)
      {
        //注意此时要配套使用,不可以使用realloc扩容
        reserve(_capacity == 0 ? 4 : _capacity * 2);
      }
      _str[_size] = ch;//string的本质就是一个字符数组
      ++_size;
      _str[_size] = '\0';//此时就是因为插入字符之后,把原来的\0给搞没了,所以要重新给一个\0,不然就会导致无法计算strlen之类的问题
    }
    void append(const char* str)//注意此时这个函数是用来插入字符串的,不是上述用来插入字符的
    {
      size_t len = strlen(str);
      if (_size + len > _capacity)//此时这个位置表示的就是插入len个字符(插入len个字符,刚好等于capacity可以,但是不可以超过)
      {
        //此时这种直接插入多个,就不可以2倍2倍的扩,需要一次性扩大一点
        reserve(_size + len);
      }
      strcpy(_str + _size, str);//该拷贝,是因为可以直接把原来字符串中的\0给覆盖掉
      //strcat(_str, str);//但是最好不要使用strcat,追加,目的是因为防止原字符串过长,\0不好找,因为strcat只有找到了\0才会进行追加
      _size += len;
      //字符串是不需要处理\0的,因为strcpy会拷贝\0
    }
    String& operator+=(char ch)
    {
      push_back(ch);
      return *this;
    }
    String& operator+=(const char* str)
    {
      append(str);
      return *this;
    }//某个位置插入、某个位置删除
    void insert(size_t pos, char ch)//某个位置插入字符
    {
      assert(pos <= _size);//防止传参的时候越界
      if (_size + 1 > _capacity)//还是因为等于的时候是刚刚好满了,所以不怕,只有大于的时候才需要扩
      {
        reserve(2 * _capacity);
      }
      size_t end = _size;
      while (end >= pos)
      {
        _str[end + 1] = _str[end];
        --end;
      }
      _str[pos] = ch;
      ++_size;
    }
    void insert(size_t pos, const char* str)//某个位置插入字符串
    {
      assert(pos <= _size);
      size_t len = strlen(str);
      if (len == 0)
      {
        return;
      }
      if (_size + len > _capacity)
      {
        reserve(_capacity + len);
      }//扩容完之后就是插入数据
      size_t end = _size + len;
      for (int i = end; i >= pos + len; --i)
      {
        _str[i] = _str[i - len - 1];
      }
      char c = _str[pos + len];
      strcpy(_str + pos, str);
      _str[pos + _size] = c;
      _size += len;
      _str[_size] = '\0';
    }
    void erase(size_t pos, size_t len = npos)
    {
      assert(pos>=0 && pos <= _size);
      size_t end = _size;
      while (end > pos)
      {
        _str[end - 1] = _str[end];
        --end;
      }
      --_size;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
    static size_t npos;//npos此时给一个静态成员变量,供给大家使用
    //有一个特例,可以不需要在全局定义static,但是只针对于整形,就是加一个const
    //static const size_t npos = -1;
    //static const size_t N = 10;
    //估计是为了可以这样使用:int _arr[N];
  };
  size_t String::npos = -1;
  void Print(const String& s)
  {
    for (size_t i = 0; i < s.size(); ++i)
    {
      cout << s[i] << " ";//此时因为这个函数就是一个const修饰的函数,所以无论在函数内存调用运算符重载,还是别的函数,此时这些函数都需要有const属性,所以导致我们需要实现两个[]运算符重载,一个给普通类型使用,一个给const类型使用
    }
    cout << endl;
    for (auto ch : s)//证明const属性的迭代器(所以需要把迭代器也给弄成两份,一份普通类型,一份const类型)
    {
      cout << ch << " ";
    }
    cout << endl;
  }
  void test_string1()
  {
    String s1;
    String s2("hello world");
    cout << s1.c_str() << endl;
    cout << s2.c_str() << endl;
    for (int i = 0; i < 10; i++)
    {
      s2[i]++;
    }
    String s3(s2);//拷贝构造(经典的指针指向同一块空间问题),涉及深拷贝
    cout << s3.c_str() << endl;
    cout << s2.c_str() << endl;
    s3 = s2;//赋值要注意有自己给自己赋值的时候 s2 = s2;
    cout << s3.c_str() << endl;
    cout << s2.c_str() << endl;
  }
  void test_string2()//验证const修饰的函数需要使用具有const属性的函数
  {
    wwx::String s1("hello world");
    for (size_t i = 0; i < s1.size(); ++i)//注意:此时访问的不是string类中的成员变量,访问的是计算size大小的公有函数
    {
      cout << ++s1[i] << " ";
    }
    cout << endl;
    Print(s1);
  }
  void test_string3()//验证正向迭代器(反向迭代器先不学)
  {
    String s1("gdkkn vnqkc");
    String::iterator it = s1.begin();//普通it类型
    String::const_iterator it2 = s1.begin();//const类型的it
    while (it != s1.end())
    {
      cout << ++(*it) << " ";//指针不仅可以读,而且可以写
      ++it;//虽然被const修饰,但是自己是可以修改的,例:++it是可以的++(*it)就是不可以的(总:自己可以修改,只是指向的内容不可以修改而已)
      ++it2;
      //cout << ++(*it2) << " ";//const迭代器指向的内容不允许被修改
    }
    cout << endl;
    for (auto ch : s1)//很好的证明了,范围for就是使用迭代器实现的(傻白甜)
    {
      cout << ch << " ";
    }
    cout << endl;
  }
  void test_string4()//验证运算符重载
  {
    string s1("hello world");
    string s2("hello world");
    string s3("xxxxxxxxxxx");
    //比大小,此时比的是ASCII码值
    cout << (s1 < s3) << endl;//涉及运算符的优先级,所以要加上()
    cout << (s1 == s3) << endl;
    cout << (s1 == s2) << endl;
    cout << (s1 >= s3) << endl;
    cout << (s1 > s3) << endl;
    cout << (s1 != s2) << endl;
  }
  void test_string5()//验证字符和字符串追加
  {
    string s1("hello world");
    s1.push_back(' ');//反正就是注意使用this指针就行(因为this指针代表的就是s1对象)
    cout << s1.c_str() << endl;
    s1.append("xxxxxxxxxxxxxxxx");
    cout << s1.c_str() << endl;
    s1 += "aaaaaaaaaaaaaaaaaa";
    cout << s1.c_str() << endl;
  }
  void test_string6()
  {
    String s1("hello world");
    s1.insert(6, 'm');
    s1.insert(7, 'y');
    s1.insert(8, ' ');//搞定了中间插入,此时要防止是在最头上插入等问题
    cout << s1.c_str() << endl;
    s1.insert(5, "bit");
    cout << s1.c_str() << endl;
    s1.erase(5, 3);
    cout << s1.c_str() << endl;
  }
}

以上就是string类地基部分代码,注释很全,注意:测试代码需要放到test.cpp文件中测试

image.jpeg

总结:还是那句话,自己实现string类,可以把以前学的知识得到很好的巩固。



相关文章
|
14天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
20天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
54 5
|
26天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
56 4
|
28天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
66 4
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
62 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
38 2
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
23 1
|
2月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
57 4