【C++进阶】深入STL之string:模拟实现走进C++字符串的世界

简介: 【C++进阶】深入STL之string:模拟实现走进C++字符串的世界

前言:在C++中,string是一个极其重要且常用的类,它为我们提供了丰富的字符串操作功能。然而,了解其背后的实现原理,不仅可以帮助我们更好地使用它,还能让我们对C++的内存管理、模板编程等有更深入的理解。本文将带你走进C++字符串的世界,通过模拟实现一个简单的string类,来探索其内部机制

模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数


📒1. string类的成员变量

首先我们要先搞清楚string的成员变量,我们清楚string类在底层实际上就是一个字符指针,在模拟实现string之前,我们创建一个属于自己的命名空间来与库里面的区分

namespace pxt
{
  class string
  {
  public:
    const char* c_str() const // 为了能更好的实现,我们提前实现以下c.str
    {
      return _str;
    }
  private:
    // 成员变量
    char* _str; // 指向一段空间的指针
    size_t _size; // 有效字符串长度
    size_t _capacity; // 空间总大小
  };
}

📒2. string的构造函数

🎈无参的构造函数

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

注意:在调用无参的构造函数时,库里面并不只是开了空间,它还干了其他事情,所以我们在自己模拟现实时,一定不能用nullptr去初始化,否则就会出错,因此我们放一个'\0'进去!

std::string无参构造:


🎩带参的构造函数

string(const char* str = "")
  :_size(strlen(str))
  , _capacity(_size)
{
  _str = new char[_capacity + 1];
  strcpy(_str, str);
}

在带参的构造函数因为常量字符串最后自带了一个'\0',因此我们什么都不用带


📒3. string的析构函数

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

string的析构函数非常简单,只需要将空间用delete释放,并且将各个指针置为空,将空间大小变为0


📒4. string的拷贝构造函数

🔥 浅拷贝

首先我们来看一段拷贝构造的模拟实现:

// 拷贝构造
string(const char* str = "")
{
  if (nullptr == str)
  {
    return;
  }
  _str = new char[strlen(str) + 1];
  strcpy(_str, str);
}

// 测试
void test_string()
{
  string s1("hello world");
  string s2(s1);
}

为什么会引发异常呢?

我们发现s1和s2都指向都一块空间,在释放时同一块空间是不可以被释放多次的,从而引起了崩溃,而这就是浅拷贝

浅拷贝: 也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规,

可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享


💧 深拷贝

// 我们用s作为s1的别名
string(const string& s)
{
  _str = new char[s._capacity + 1];
  strcpy(_str, s._str);
}

深拷贝:每个对象都有一份独立的资源,不要和其他对象共享

注意: 关于浅拷贝一定要引起重视!


📒5. string类的运算符重载

operator=

operator=上,我们有两种写法

// 传统写法
string& operator=(const string& s)
{
  if (this != &s)
  {
    char* tmp = new char[s._capacity + 1]; // 存放'\0'
    strcpy(tmp, s._str);

    delete[] _str;
    _str = tmp;
    _size = s.size();
    _capacity = s.capacity();
  }

  return *this;
}

// 现代写法
string& operator=(string s)
{
  swap(_str, s._str); // swap库中存在,可以直接使用
  return *this;
}

传统写法

  • 传统写法函数的运用引用传参,通过创建中间变量,并开辟空间然后将参数拷贝进中间变量,再把这个中间变量的地址传给this,从而实现了operator=的功能

现代写法

  • 现代写法使用了库中的swap函数,从而让函数达到一个简洁的目的,该函数的参数是一个临时拷贝变量,深拷贝后,通过swap交换即可

operator<, operator==

bool operator<(const string& s) const
{
  return strcmp(_str, s._str) < 0; // 使用strcmp来比较字符串大小
}

bool operator==(const string& s) const
{
  return strcmp(_str, s._str) == 0;
}

关于比较我们就讲这两个,对于其他的都可用operator<, operator==去进行推导!


📒6. string容量相关函数

size,capacity,resize,reverse


size_t size() const
{
  return _size;
}

size_t capacity() const
{
  return _capacity;
}

size,capacity这两个函数的模拟实现相对简单,我们简单实现一下就可以


void reserve(size_t n)
{
  if (n > _capacity) // n < _capacity时,reserve不会作出回应
  {
    char* tmp = new char[n + 1];
    strcpy(tmp, _str);

    delete[]_str;
    _str = tmp;
    _capacity = n;
  }
}

reverse只会改变capacity的大小,并不会改变size的大小


void resize(size_t n, char ch = '\0')
{
  if (n < _size) // 当 n < _size时会将size变小到n
  {
    _str[n] = '\0';
    _size = n;
  }
  else // 当 n > _size时,就和reserve类似
  {
    reserve(n);
    while (n < _size)
    {
      _str[_size++] = ch;
    }
    _str[_size] = '\0';
  }
}

resize与reserve类似会改变size大小,但是也会改变capacity大小


📒7. string常用函数模拟

⭐查找

// 查找单个字符
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* sub, size_t pos = 0)
{
  // strstr为字符串匹配函数
  const char* p = strstr(_str + pos, sub);
  if (p)
  {
    return p - _str;
  }
  else
  {
    return npos;
  }
}
// 返回以pos开头,len位置结尾的字符串
string substr(size_t pos, size_t len = npos)
{
  string s;
  if (len == npos || len + pos > _size)
  {
    len = _size - pos;    
  }
  reserve(len);
  for (size_t i = pos; i < _size; i++)
  {
    s += _str[i];
  }
  return s;
}

⭐ 添加

// 尾插单个字符
void push_back(char ch)
{
  if (_size == _capacity)
  {
    reserve(_capacity == 0 ? 4 : _capacity * 2);
  }
  _str[_size] = ch;
  ++_size;
  _str[_size] = '\0';
}
// 插入字符串
void append(const char* str)
{
  size_t len = strlen(str);
  if (_size + len > _capacity)
  {
    reserve(_size + len);
  }
  strcpy(_str + _size, str);
  _size += len;
}

string& operator+=(char ch)
{
  push_back(ch);
  return *this;
}

string& operator+=(const char* str)
{
  append(str);
  return *this;
}

因为在添加中+=既可以添加字符也可以添加字符串,往往在日常中的使用频率是最高的,所以推荐大家使用+=来代替push_backappend


📒8. 总结

经过对STL中string的深入探索与模拟实现,我们仿佛揭开了一个隐藏在C++深处的奇妙世界。这个旅程不仅让我们对string这一基础数据类型有了更为深刻的理解,也让我们领略了STL背后的设计理念与精巧实现,让我们携手共进,共同走进C++字符串的奇妙世界!

谢谢大家支持本篇到这里就结束了,祝大家天天开心!


目录
相关文章
|
6天前
|
C++
C++ string中的函数和常用用法
C++ 中string中的函数和常用用法
15 4
|
12天前
|
Java 开发者 Python
Python中,字符串(String)是一种不可变的数据类型
Python中,字符串(String)是一种不可变的数据类型
|
14天前
|
设计模式 算法 Java
【c++】STL之stack和queue详解
【c++】STL之stack和queue详解
14 1
|
20天前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
29 0
|
21天前
|
存储 NoSQL Redis
Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX
Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX
遍历字符串,String line = xxx for(int i = 0;i<line.length();i++){system.out.println(line.chartAt(i)); 单个
遍历字符串,String line = xxx for(int i = 0;i<line.length();i++){system.out.println(line.chartAt(i)); 单个
String对象的特点,new创建的字符串对象地址值不同,String s3 = “abc“; s4=“abc“ sout(s1 == s2)比较地址和内容,s1.equals(s3)比较内容
String对象的特点,new创建的字符串对象地址值不同,String s3 = “abc“; s4=“abc“ sout(s1 == s2)比较地址和内容,s1.equals(s3)比较内容
new String()定义字符串为空,char[] chs = {‘a‘,‘b‘,‘c‘} String s2 = new String(chs) 输出abc,byte定99为a
new String()定义字符串为空,char[] chs = {‘a‘,‘b‘,‘c‘} String s2 = new String(chs) 输出abc,byte定99为a
TS,数据类型概述,常见的基本数据类型有number/string/boolean/undefined/null,字符串用““,let food: string = ‘糖葫芦‘,布尔类型
TS,数据类型概述,常见的基本数据类型有number/string/boolean/undefined/null,字符串用““,let food: string = ‘糖葫芦‘,布尔类型
|
1月前
|
Java UED
Java中String强转int:一种常见的错误和解决方法
在Java中将非数字字符串转换为整数会导致`NumberFormatException`。要解决这个问题,可以使用`try-catch`捕获异常,正则表达式验证数字格式,或利用异常信息提供错误提示。例如,`Integer.parseInt()`会因遇到非数字字符如`&quot;123abc&quot;`而抛出异常,但通过异常处理或正则`\\d+`可确保安全转换。记得在编程时避免直接强转,以防止程序异常中断。