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

相关文章
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
1月前
|
索引 Python
String(字符串)
String(字符串)。
27 3
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
65 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
47 4
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
45 2
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
25 1
|
2月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
60 4
|
2月前
|
canal 安全 索引
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
(StringBuffer和StringBuilder)以及回文串,字符串经典习题
38 5
|
3月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
44 0
java基础(13)String类
|
3月前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①