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

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

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


验证一下

void test()
{
  string str("hello world");
  str += '!';
  str += "!!";
  print_str(str);
}


运行结果:


我们再来实现一下插入函数

void insert(size_t pos, char ch)
{
  assert(pos <= _size);
  if (_size == _capacity)
  {
    //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
    size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
    reserve(newCapacity);
  }
  size_t end = _size;
  while (end >= pos)
  {
    //依次向后挪动
    _str[end + 1] = _str[end];
    --end;
  }
  _str[pos] = ch;
  ++_size;
  //挪动的时候挪动了'\0'
  //_str[_size] = '\0';这里不用处理
}
void test()
{
  string str("hello world");
  str += '!';
  str += "!!";
  print_str(str);
  cout << endl;
  str.insert(5, '*');
    str.insert(5, '*');
    str.insert(5, '*');
  print_str(str);
}


运行结果:


我们看一下我们的代码有没有什么问题?我们试一下我们的头插:str.insert(0, '*');


头插的时候,end减到为0,下次减减的时候减到-1,但是此时end为size_t,会变成整型的最大值,因此程序会一直运行,进入死循环,那我们将上面的end变量类型变为int呢?我们来看一下下面的程序。


上面的结果为什么是yes!当无符号整型和有符号整型比较时,有符号整型会隐式提升为无符号整型,-1此时就能转化为整型的最大值。所以将上面的end变量类型变为int也不行,因为我们的pos也是无符号整型,不过我们可以通过强制类型转换解决。

void insert(size_t pos, char ch)
{
  assert(pos <= _size);
  if (_size == _capacity)
  {
    //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
    size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
    reserve(newCapacity);
  }
  int end = _size;
  while (end >= (int)pos)
  {
    //依次向后挪动
    _str[end + 1] = _str[end];
    --end;
  }
  _str[pos] = ch;
  ++_size;
  //挪动的时候挪动了'\0'
  //_str[_size] = '\0';这里不用处理
}


或者也可以这样写,这样end变量的值只能减到0,就不会出现上面的错误。

void insert(size_t pos, char ch)
{
  assert(pos <= _size);
  if (_size == _capacity)
  {
    //当上面构造函数没有传参时,capacity值为0,这里需要单独处理一下
    size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
    reserve(newCapacity);
  }
  size_t end = _size + 1;
  while (end > pos)
  {
    //依次向后挪动
    _str[end] = _str[end - 1];
    --end;
  }
  _str[pos] = ch;
  ++_size;
  //挪动的时候挪动了'\0'
  //_str[_size] = '\0';这里不用处理
}


我们再来实现一下字符串的插入。

void insert(size_t pos, const char* str)
{
  assert(pos <= _size);
  size_t len = strlen(str);
  if (_size + len > _capacity)
  {
    reserve(_size + len);
  }
  int end = _size;
  while (end >= (int)pos)
  {
    _str[end + len] = _str[end];
    --end;
  }
  strncpy(_str+pos,str,len);
}


这里需要注意不能使用strcpy,因为它会拷贝'\0',会覆盖后面的'w'字符。接下来我们再实现一下删除,库里面接口为我们提供了缺省值npos,它属于静态成员变量,类里面定义,类外初始化。

private:
  char* _str;
  size_t _size;
  size_t _capacity;
  //不用给缺省值
  static size_t npos;//不会初始化列表,属于整个类,属于所有对象
};
size_t yu::string::npos = -1;


但是我们发现库里面是这样写的。


我们发现当我们给双精度浮点型的时候程序报错了,error C2864: yu::string::x: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”。对于上面这种写法只能支持整型变量。上面的这种写法可以算是编译器的特殊处理,此时的npos只读,不能修改,不能加加减减。

void erase(size_t pos, size_t len = npos)
{
  assert(pos < _size);
  if (len == npos || pos + len >= _size)
  {
    _str[pos] = '\0';
    _size = pos;
  }
  else
  {
    strcpy(_str + pos, _str + pos + len);
    _size -= len;
  }
}

测试一下:

void test()
{
  string str("hello world");
  print_str(str);
  cout << endl;
  str.erase(5, 1);
  print_str(str);
  cout << endl;
  str.erase(2);
  print_str(str);
}


运行结果:


我们看一下上面判断的条件,第二个条件是否可以覆盖第一个条件。这里是不能的,npos已经是最大值了,如果再加上pos就会溢出,程序就会有问题。我们再来写来写一下交换的成员函数。

void swap(string& str)
{
  //交换指向的内容即可
  std::swap(str._str, _str);
  std::swap(str._size, _size);
  std::swap(str._capacity, _capacity);
  //std::swap(str, *this);效率较低,拷贝构造消耗大
}


我们来看一下库中这几个函数的区别,如果没有第二个swap函数,就只能调用第三个swap函数,第三个函数首先要拷贝,然后赋值,这里都是深拷贝,要开空间区拷贝,代价大。                        


我们再来实现一下find函数。

size_t find(char ch, size_t pos = 0)
{
  for (size_t i = pos; i < _size; i++)
  {
    if (_str[i] == ch)
      return i;
  }
  return npos;
}
size_t find(const char* str, size_t pos = 0)
{
  //暴力匹配
  const char* ptr = strstr(_str + pos, str);
  if (ptr == nullptr)
  {
    return npos;
  }
  else
  {
    //指针相减是元素之间的个数
    return ptr - _str;
  }
}


测试一下:

void test1()
{
  string str("hello world");
  cout << str.find('h') << endl;
  cout << str.find("lo") << endl;
}


运行结果:


我们再来实现一下substr函数。

string substr(size_t pos = 0, size_t len = npos)
{
  assert(pos < _size);
  size_t end = pos + len;
  if (len == npos || pos + len >= _size)
  {
    end = _size;
  }
  string str;
  str.reserve(end - pos);
  //小于后面字符的个数
  for (size_t i = pos; i < end; i++)
  {
    str += _str[i];//会自动处理'\0'
  }
  return str;
}


我么们来看一下我们的代码有没有什么问题?


这里主要就是浅拷贝的问题,临时对象和str对象指向了同一块空间。


说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构 造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

string(const string& s)//拷贝构造 - 深拷贝
{
  _str = new char[s._capacity + 1];
  strcpy(_str, s._str);
  _size = s._size;
  _capacity = s._capacity;
}

通过深拷贝就可以解决问题指向同一块空间。


那赋值的深拷贝呢?

string& operator=(const string& s)//赋值 - 深拷贝
{
  if (this != &s)
  {
    char* tmp = new char[s._capacity + 1];
    strcpy(tmp, s._str);
    delete[] _str;
    _str = tmp;
    _size = s._size;
    _capacity = s._capacity;
  }
}


此时也能解决问题。


我们上面都是通过C语言的形式中print_str或者str.c_str()输出我们的字符串的,我们实现一下直接使用C++流插入操作符来输出字符串。

//流插入和流提取重载在全局
ostream& operator<<(ostream& out, const string& s)
{
  for (auto ch :s)
  {
    out << ch;
  }
  return out;
}


再来实现一下流提取。

// 这里会修改s,所以不用带上const
istream& operator>>(iostream& in, string& s)
{
  char ch;
  in >> ch;
  while (ch != ' ' && ch != '\n')
  {
    s += ch;
    in >> ch;
  }
}


我们来测试一下


我们发现我们的程序无论是输入空格或者换行都不能结束。我们来测试一个数据,当什么输入"12345空格6",安装我们想要的逻辑,cin应该只会读取到”123345”。我们来一下我们的调式结果。


按照常理,输入到'5'字符后应该输入' ',但是上面的调式结果却跳过了空格,而是输出字符'6',ch没有拿到空格。


我们来看一下cin的特性,当我们连续往两个字符中写入'x',' '和'y',cin会将空格当为分隔符,会忽略这个空格,从而直接读取后续的字符,也就是读取'y','\n'也是如此,我们上面的程序是将字符串分为一个个字符,然后字符依次输入,此时就是cin输入单个字符,遇到空格或者换行就会被忽略掉,所以上面的程序无论输入什么都不会被结束,因为拿不到空格或者换行,我们可以使用getchar来获取单个字符的输入,但是我们今天学习的是C++语言,最好应用C++函数。


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

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