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

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