【字符串探秘:手工雕刻的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

相关文章
|
11天前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
24 0
java基础(13)String类
|
8天前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①
|
1天前
|
存储 JavaScript 前端开发
JavaScript 字符串(String) 对象
JavaScript 字符串(String) 对象
10 3
|
21天前
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
20 1
Java——String类详解
|
6天前
|
索引
Sass String(字符串) 函数
Sass String(字符串) 函数用于处理字符串并获取相关信息。
14 1
|
17天前
|
安全 Java
Java StringBuffer 和 StringBuilder 类详解
在 Java 中,`StringBuffer` 和 `StringBuilder` 用于操作可变字符串,支持拼接、插入、删除等功能。两者的主要区别在于线程安全性和性能:`StringBuffer` 线程安全但较慢,适用于多线程环境;`StringBuilder` 非线程安全但更快,适合单线程环境。选择合适的类取决于具体的应用场景和性能需求。通常,在不需要线程安全的情况下,推荐使用 `StringBuilder` 以获得更好的性能。
|
17天前
|
Java 索引
Java String 类详解
Java 中的 `String` 类用于表示不可变的字符序列,是 Java 标准库 `java.lang` 包的一部分。字符串对象一旦创建,其内容不可更改,修改会生成新对象。
|
11天前
|
Java 索引
java基础扫盲-String类常用的方法
java基础扫盲-String类常用的方法
|
2月前
|
API 索引
String类下常用API
String类下常用API
38 1