【字符串探秘:手工雕刻的String类模拟实现大揭秘】(上)

简介: 【字符串探秘:手工雕刻的String类模拟实现大揭秘】

【本节目标】


  • 1. string类的模拟实现
  • 2.C++基本类型互转string类型
  • 3.编码表 :值 --- 符号对应的表
  • 4.扩展阅读


1. string类的模拟实现


1.1 经典的string类问题


上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己 来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以 下string类的实现是否有问题?为了防止和库里面的string类发生冲突,我们在这里使用命名空间来限制我们写的string类。


构造函数

namespace yu
{
  class string
  {
  public:
    string(const char* str)
      :_str(str)
    {}
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
}

上面的代码有什么问题吗?


我们之前提到权限可以缩小,可以平移,但是就是不能放大,那我们下面的写法还有错误吗?

namespace yu
{
  class string
  {
  public:
    string(const char* str)
      :_str(str)
    {}
  private:
    const char* _str;
    size_t _size;
    size_t _capacity;
  };
  void test()
  {
    string str("hello world");
  }
}


这里也是不可以的,因为常量字符串存在代码区,只能可读,不能写,那我们上面就只能完成一个打印输出的工作,不能完成扩容,修改等其他增删改操作。所以我们可以开辟一个同样的空间

namespace yu
{
  class string
  {
  public:
    string(const char* str)
      //strlen求取'\0'之前字符的个数
      :_str(new char[strlen(str)+1])
      ,_size(strlen(str))
      //capacity是存储有效字符的个数,不包括'\0'
      ,_capacity(strlen(str))
    {}
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
  void test()
  {
    string str("hello world");
  }
}


但是上面strlen这个需要计算3次,而且strlen的实践复杂度是O(N),所以我们写成下面的形式。

namespace yu
{
  class string
  {
  public:
    string(const char* str)
      :_size(strlen(str))
      //capacity是存储有效字符的个数,不包括'\0'
      ,_capacity(_size)
      //strlen求取'\0'之前字符的个数
      ,_str(new char[_capacity + 1])
    {}
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
  void test()
  {
    string str("hello world");
  }
}


随后我们写一下c_str函数,看看是否打印输出成功。

namespace yu
{
  class string
  {
  public:
    string(const char* str)
      :_size(strlen(str))
      //capacity是存储有效字符的个数,不包括'\0'
      ,_capacity(_size)
      //strlen求取'\0'之前字符的个数
      ,_str(new char[_size + 1])
    {
      strcpy(_str, str);//拷贝
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
  void test()
  {
    string str("hello world");
    cout << str.c_str() << endl;
  }
}
int main()
{
  yu::test();
  return 0;
}


此时我们的程序发生了崩溃,因为初始化的顺序和声明的顺序一致,所以程序会先执行_str(new char[_capacity + 1]),但是此时_capacity还没有初始化,此时编译器可能给了随机值或者0。


那么此时开的空间就只有1个字符的空间,开空间小导致拷贝时程序报错。


那怎么解决呢?我们可以初始化的顺序和声明的顺序一致。


但是这样的写法不好,我们这里可以不使用初始化列表,可以使用函数体内初始化。

namespace yu
{
  class string
  {
  public:
    string(const char* str)
    {
      _size = strlen(str);
      _capacity = _size;
      _str = new char[_capacity + 1];
      strcpy(_str, str);//拷贝
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
  void test()
  {
    string str("hello world");
    cout << str.c_str() << endl;
  }
}
int main()                                         
{
  yu::test();
  return 0;
}


我们再来来实现一下析构函数

~string()
{
  delete[] _str;
  _str = nullptr;
  _capacity = 0;
  _size = 0;
}


string类里面还提供了无参的构造函数

namespace yu
{
  class string
  {
  public:
    string()
      :_str(nullptr)
      ,_size(0)
      ,_capacity(0)
    {}
    string(const char* str)
    {
      _size = strlen(str);
      _capacity = _size;
      _str = new char[_capacity + 1];
      strcpy(_str, str);//拷贝
    }
    ~string()
    {
      delete[] _str;
      _str = nullptr;
      _capacity = 0;
      _size = 0;
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
  void test()
  {
    string str;
    cout << str.c_str() << endl;
  }
}
int main()                                         
{
  yu::test();
  return 0;
}


我们这里程序又崩溃了,为什么?cout在识别到char *类型的时候,会认为当前输出的是字符串,会进行解引用行为,这里报错就是空指针解引用的原因。所以我们这里可以设置一个空间存储'\0'

string()
  :_str(new char[1])
  ,_size(0)
  ,_capacity(0)
  {
    _str[0] = '\0';
  }


但是实践上我们一般写成全缺省构造函数,不分别写有参和无参两种形式。

//string(const char* str = nullptr)//error:strlen(nullptr)会报错
//string(const char* str = '\0')//error:char不能给char*
string(const char* str = "")//常量字符串默认结尾是\0
{
  _size = strlen(str);
  _capacity = _size;
  _str = new char[_capacity + 1];
  strcpy(_str, str);//拷贝
}


再来实现一下size和[ ]操作符重载,库中我们还实现了cosnt[ ]操作符重载形式,这种形式函数内部未对对象(*this)作出改变,所以可以加上const。

//不包括'\0'
size_t size() const
{
  return _size;
}
//返回pos位置值的引用
//  1.减少拷贝
//  2.修改返回值
char& operator[](size_t pos) 
{
  //这里可以=因为\0处也有空间
  //hello world\0
  //\0位置处的下标就是_size
  assert(pos <= _size);
  return _str[pos];
}
const char& operator[](size_t pos) const
{
  //这里可以=因为\0处也有空间
  //hello world\0
  //\0位置处的下标就是_size
  assert(pos <= _size);
  return _str[pos];
}


函数内部未对对象(*this)作出改变,所以可以加上const,我们可以验证一下。

void test()
{
  string str("hello world");
  for (size_t i = 0; i < str.size(); i++)
  {
    str[i]++;
  }
  cout << str.c_str() << endl;
}


运行结果:


除了上面的[ ]可以遍历和修改,迭代器也可以修改,我们来模拟实现一下。

typedef char* iterater;
iterater begin()
{
    //第一个字符位置是begin
  return _str;
}
iterater end()
{
    //\0位置就是end
  return _str + _size;
}


我们来测试一下

void test()
{
  string str("hello world");
  string::iterater it = str.begin();
  while (it != str.end())
  {
    cout << *it;
    it++;
  }
}


运行结果:


但是上面这种写法只适合底层空间连续,后面遇到不连续的我们就要修改写法,除了上面的打印工作,我们还有范围for。

for (auto ch : str)
{
  cout << ch;
}


其实范围for底层也是用的迭代器,通过反汇编我们可以看到。


如果我们上面把begin变成Begin,此时范围for就会报错,因为范围for是傻瓜式的替换成迭代器,只有我们自定义写的迭代器没有按照规则命名,范围for就不能使用。


我们再来实现一下打印输出的工作

void print_str(const string& s)
{
  for (size_t i = 0; i < s.size(); i++)
  {
    s[i]++;
  }
  cout << s.c_str() << endl;
}


但是我们发现我们的代码出现错误了,为什么?


因为我们上面的size和[ ]操作符重载传入的对象是非const类型的,而我们的打印输出是const类型的,这里会存在权限放大的方法,所里这里会报错,所以size和c_str函数内部未对对象(*this)作出改变,所以可以加上const。而[ ]操作符重载可以使用cosnt版本的。

void print_str(const string& s)
{
  for (size_t i = 0; i < s.size(); i++)
  {
    //s[i]++;//此时是const,也就不能修改
    cout << s[i];
  }
  cout << endl;
}

运行结果:


我们再将迭代器放入刚刚的输出打印函数,我们发现也出现了同样的问题。


所以这里要使用const迭代器,所以我们要实现一下。

typedef const char* const_iterater;
const_iterater begin() const
{
  return _str;
}
const_iterater end() const
{
  return _str + _size;
}


因此我们的程序就可以正常输出,但是此时指针指向的内容不可被改变。


我们再来实现一下string类的增删查改。字符串的增加操作必定都要开空间,对于字符串追加的函数,我们这里不能实现每次开2倍的空间操作,如果要追加的字符串的长度过长,开辟的空间必定不够,因此这里我们先实现reserve函数,解决空间开辟的问题。

void reserve(size_t n)
{
  if (n > _capacity)
  {
    //扩容步骤
    /*
      1.开辟空间
      2.拷贝数据
      3.释放旧空间
      4.指向新空间
    */
    char* tmp = new char[n + 1];//多开一个给'\0'的位置
    strcpy(tmp, _str);//会拷贝'\0'
    delete[] _str;
    _str = tmp;
    _capacity = n;
  }
  //不缩容
  return;
}


现在预备条件已经写好了,我就可以开始写轮子了。

void append(const char* str)
{
  size_t len = strlen(str);
  //这里都不包含'\0',因此可以不用处理
  //而且我们开空间都给\0开好了位置
  //空间永远都比capacity多一个
  if (_size + len > _capacity)
  {
    reserve(_size + len);
  }
  strcpy(_str + _size, str);
  _size += len;
  //这里插入的str字符串已经拷贝过来\0,就不需要单独处理了
}
void push_back(char ch)
{
  if (_size == _capacity)
  {
    //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
    size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
    reserve(newCapacity);
  }
  _str[_size] = ch;
  ++_size;
  _str[_size] = '\0';//处理\0
}
//这里我们就可以复用上面的接口
string& operator+=(const char* str)
{
  append(str);
  return *this;
}
string& operator+=(char ch)
{
  push_back(ch);
  return *this;
}


【字符串探秘:手工雕刻的String类模拟实现大揭秘】(中):https://developer.aliyun.com/article/1425677

相关文章
|
4天前
|
存储 安全 C语言
【C++】string类
【C++】string类
|
存储 编译器 Linux
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
标准库中的string类(中)+仅仅反转字母+字符串中的第一个唯一字符+字符串相加——“C++”“Leetcode每日一题”
|
6天前
|
编译器 C++
标准库中的string类(上)——“C++”
标准库中的string类(上)——“C++”
|
12天前
|
JavaScript
js 字符串String转对象Object
该代码示例展示了如何将一个以逗号分隔的字符串(`&#39;1.2,2,3,4,5&#39;`)转换为对象数组。通过使用`split(&#39;,&#39;)`分割字符串并`map(parseFloat)`处理每个元素,将字符串转换成浮点数数组,最终得到一个对象数组,其类型为`object`。
|
16天前
|
XML 编解码 数据格式
Python标准数据类型-String(字符串)
Python标准数据类型-String(字符串)
23 2
|
16天前
|
存储 算法 C语言
【C++初阶】8. STL初阶 + String类
【C++初阶】8. STL初阶 + String类
48 1
|
16天前
|
C语言 C++
【C++初阶】9. string类的模拟实现
【C++初阶】9. string类的模拟实现
38 1
|
13天前
|
Java API 索引
Java基础—笔记—String篇
本文介绍了Java中的`String`类、包的管理和API文档的使用。包用于分类管理Java程序,同包下类无需导包,不同包需导入。使用API时,可按类名搜索、查看包、介绍、构造器和方法。方法命名能暗示其功能,注意参数和返回值。`String`创建有两种方式:双引号创建(常量池,共享)和构造器`new`(每次新建对象)。此外,列举了`String`的常用方法,如`length()`、`charAt()`、`equals()`、`substring()`等。
15 0
|
28天前
|
Java
【Java】如果一个集合中类型是String如何使用拉姆达表达式 进行Bigdecimal类型计算?
【Java】如果一个集合中类型是String如何使用拉姆达表达式 进行Bigdecimal类型计算?
25 0