【C++】string类的模拟实现

简介: 【C++】string类的模拟实现

准备工作:

首先我们为了和C++标准库里面的string不冲突,我们将我们自己实现的string定义在一个namespace命名空间内,并将这个命名空间放进头文件MyString.h内,我们将我们.c文件包含这个头文件。

一、string的成员变量以及类的默认成员函数

1、string的成员变量

对于string的成员变量,我们可以用顺序表来进行管理字符串,并配上顺序表容量capacity与顺序表里面的元素个数size(capacity与size都只统计有效字符,不包含’\0’)。

因此我们可以先写出下面的代码:

namespace mystring
{
  class string
  {
  public:
    string();
    ~string();
  private:
    char* _str;//指向存储的字符
    int _size;
    int _capacity;
    static const size_t npos;//静态成员变量
  };
  const size_t string::npos = -1;
}

2、string的构造函数

我们在用string类定义对象时主要有两种情况:

空串

用一个字符串初始化

因此对于string的构造函数我们可以使用缺省参数,如果给了字符串,我们就用给的字符串去初始化,如果没有给字符串我们就用空串去初始化。

class string
{
public:
  string(const char* str = "");//缺省参数,如果没有给字符串我们就用空串去初始化。
  ~string();
private:
  char* _str;
  int _size;
  int _capacity;
};
string::string(const char* str)
  :_size(strlen(str))
{
  _capacity = _size == 0 ? 3 : _size;//第一次如果是空串的话,默认开3个char类型的空间
  char* tmp = new char[_capacity + 1];//多申请一个用于存放'\0'
  _str = tmp;
  strcpy(_str, str);//strcpy拷贝时默认会将'\0'拷贝进目标位置中
}

注意:空串时不能给nullptr代替"",因为如果空串时给nullptr,那么在打印时会发生空指针解引用!同理也不能给'\0'因为会发生隐式类型转化,转换成了nullptr

3、string类的析构函数

string类的析构函数很简单,我们只需要释放申请的空间,然后将_str置空就行了。

string::~string()
{
  delete[] _str;
  _str = nullptr;
}

4、string类的拷贝构造

对于string的拷贝构造,我们实现起来并不难,我们只需要申请和传入对象一样大的空间,然后将字符串拷贝进新申请的空间,然后将capacitysize依此赋值进行了。

代码如下:

string::string(const string& s)
{
  char* tmp = new char[s._capacity + 1];//多申请一个用于存放'\0'
  _str = tmp;
  strcpy(_str, s._str);
  _size = s._size;
  _capacity = s._capacity;
}

5、赋值重载函数

由于string对象涉及资源管理,如果我们不写赋值重载函数,编译器就会帮我们生成一个按字节拷贝的赋值重载函数,如果是按字节拷贝的话,那么在内存上我们两个对象的_str指针会指向同一块空间,在我们程序运行结束时,就会导致同一个地址析构两次,从而造成程序奔溃!

那么我们应该怎么写赋值重载函数呢?

对于string对象的sizecapacity我们可以直接采取直接赋值,对于_str指针,我们在赋值时可能有以下几种情况:

如果我们按照上面的方法进行_str的赋值的话,会导致情况很多,也很复杂,同时也可能会有空间的浪费,因此上面的实现方法并不是一个好的方法,我们可以换一种思路

我们可以先直接申请一个和s2中_str指向的字符串一样的大小空间,然后直接释放掉s1中的原有的旧空间,然后让s1中_str指向新申请的空间,然后直接进行字符串拷贝就行了,这样我们就不用考虑空间不够的问题了,也不用担心空间浪费的问题了。

具体代码如下:

string& string::operator=(const string& s)
{
  if (this != &s) // 防止自己给自己赋值
  {
    char* tmp = new char[s._capacity + 1];
    delete[] _str;
    _str = tmp;
    strcpy(_str, s._str);
    _size = s._size;
    _capacity = s._capacity;
  }
  return *this;
}

二、string类常用的类的成员函数

1、获取string对象的内部信息

1.c_str函数

c_str函数的实现并不困难,我们只需要返回string成员变量中的 char *_str指针就行了。

const char* string::c_str()const //加入const为了让const对象与非const对象都能使用
{
  return _str;
}

2、size函数与capacity函数

size与capacity函数就是返回当前对象的元素个数与空间容量。

size_t string::size()const
{
  return _size;
}
size_t string::capacity() const
{
  return _capacity;
}

3、迭代器

1.普通迭代器

对于string类的迭代器,我们可以用指针去模拟,用指针指向字符的地址,解引用拿到地址里面的数据。

typedef char* iterator; //用指针去模拟迭代器
//迭代器的首地址
string::iterator string::begin()
{
  return _str;
}
//迭代器的尾地址
string::iterator string::end()
{
  return (_str + _size);
}

2.const迭代器

对于const类型的迭代器,我们可以采用同样的方法进行模拟,const迭代器只不过是指针指向的内容不能修改,但是指针可以改变指向。

typedef const char* const_iterator;
string::const_iterator string::begin() const
{
  return _str;
}
string::const_iterator string::end() const
{
  return (_str + _size);
}

3、扩容相关的函数

1.reserve函数

reserve函数的主要功能就是帮助我们的string类申请空间,但是切记,reserve函数不能进行缩容,因为缩容是有代价的,而且可能我们后续使用string时还可能因为空间不够需要扩容,因此我们一般不缩容。

我们的实现原理,也是先申请新空间,新空间申请出来后,将原有的数据拷贝进新空间,然后释放旧空间,让指针指向新空间。

实际代码

void string::reserve(size_t capacity)
{
  //防止出现缩容,如果新的空间大小小于原有的空间大小,则什么都不做
  if (capacity > _capacity)
  {
    char* tmp = new char[capacity + 1];//多申请一个用于存放'\0'
    strcpy(tmp, _str);
    delete[] _str;
    _str = tmp;
    _capacity = capacity;
  }
}

2. resize函数

resize函数也不会缩容,在新空间小于原有的有效字符串时,只会保留前面的的字符,但并不缩容,在新空间大于原有的有效字符个数时就会对后面的空间进行初始化。

void resize(size_t capacity, char ch = '\0');//半缺省
void string::resize(size_t capacity, char ch)
{
  //新空间小于原有的有效字符串时
  if (capacity <= _size)
  {
    _str[capacity] = '\0';
    _size = capacity;
  }
  else
  {
    //新空间大于原有空间时要进行扩容
    if (capacity > _capacity)
    {
      reserve(capacity);
    }
    //对有效字符串后面的空间进行初始化
    size_t begin = _size;
    while (begin < capacity)
    {
      _str[begin++] = ch;
    }
    //最后一个位置放上'\0',代表字符串结束
    _str[capacity] = '\0';
    _size = capacity;
  }
}

4、插入与删除相关的函数

1.push_back函数

push_back函数主要用来尾插一个字符,实现这个函数我们只需要先判断空间是否足够,然后在_size位置插入字符,最后再在后面补上\0就行了。

void string::push_back(char ch)
{
  //判断是否需要扩容
  if (_size + 1 > _capacity)
  {
    string::reserve(_capacity * 2);
  }
  _str[_size++] = ch;
  _str[_size] = '\0';
}

2.append函数

append函数主要是用来在尾部尾插一段字符串,对于它的实现与push_back类似。

void string::append(const char* str)
{
  size_t length = strlen(str);
  //判断是否需要扩容
  if (_size + length > _capacity)
  {
    string::reserve(_size + length);
  }
  strcpy(_str + _size, str);
  _size += length ;
}

3.insert函数

insert函数有两个版本,一个是插入字符版本,一个是插入字符串版本,这两个版本构成函数重载!

我们先来看字符版本:我们可以先判断是否需要扩容,然后根据要插入的位置,将要插入位置后面的所有元素集体向后移动,等pos位置也挪动完了,我们就插入数据。

void string::insert(size_t pos, char ch)
{
  assert(pos < _size);
  //判断空间是否足够
  if (_size + 1 > _capacity)
  {
    reserve(_capacity * 2);
  }
  //end表示要移动到的位置
  size_t end = _size + 1;
  while (end > pos)
  {
    _str[end] = _str[end - 1];
    --end;
  }
  _str[pos] = ch;
  _size++;
}

明白了字符版本后我们再来看字符串版本:字符串版本与字符版本大体思路一致,只不过我们一次移动的长度更长了。

void string::insert(size_t pos, const char* str)
{
  assert(pos <= _size);
  size_t length = strlen(str);
  //判断是否需要扩容
  if (_size + length > _capacity)
  {
    reserve(_size + length);
  }
  //end表示要移动到的位置
  size_t end = _size + length;
  while (end > pos + length - 1)
  {
    _str[end] = _str[end - length];
    --end;
  }
  strncpy(_str + pos, str, length);
  //重新设置_size
  _size += length;
}

4.erase函数

erase函数主要用来删除字符串,我们可以先判断我们要删除的起始pos位置后面有多少个字符,如果字符个数能够满足要删除的个数,那就用后面的元素覆盖前面要删除的元素就行了,如果字符个数不够,我们直接在pos位置放上\0就行了。

void erase(size_t pos, size_t len = npos);
void string::erase(size_t pos, size_t len)
{
  assert(pos < _size);
  //pos后面的元素个数
  int true_length = _size - pos;
  if (len < true_length)
  {
    true_length = len;
  }
  //要放入的位置
  int put_pos = pos;
  //要移动的位置
  int move_pos = put_pos + true_length;
  //当move_pos == _size时,搬运的是 '\0'
  while (move_pos <= _size)
  {
    _str[put_pos++] = _str[move_pos++];
  }
  //重新设置_size
  _size -= true_length;
}

5.clear函数

clear函数主要用来清空字符串,实现起来也是非常简单。

void string::clear()
{
  _str[0] = '\0';
  //重新设置_size
  _size = 0;
}

5、运算符重载函数

1. operator[]函数

对于operator[]函数,我们可以直接返回_str[]位置的引用就行了。

//非const对象调用
char& string::operator[](size_t pos)
{
  assert(pos >= 0 && pos <= _size);//防止发生数组越界
  return _str[pos];
}
//const对象调用
char string::operator[](size_t pos) const
{
  assert(pos >= 0 && pos <= _size);
  return _str[pos];
}

2.operator的比较运算符函数

对于比较类的运算符重载函数,我们可以实现>==<==的运算符重载函数然后将剩下要实现的运算符重载函数复用我们上面实现的两个运算符重载函数,这样可以提高代码的可维护性。

//加入const修饰,让const对象与非const对象都能使用
   // > 运算符
  bool string::operator>(const string& s) const
  {
    return strcmp(_str, s._str) > 0;
  }
  // == 运算符
  bool string::operator==(const string& s) const
  {
    return strcmp(_str, s._str) == 0;
  }
  // >= 运算符
  bool string::operator>=(const string& s) const
  {
    return (*this > s || *this == s);
  }
  // < 运算符
  bool string::operator<(const string& s) const
  {
    return !(*this >= s);
  }
  // <= 运算符
  bool string::operator<=(const string& s) const
  {
    return !(*this > s);
  }
  // != 运算符
  bool string::operator!=(const string& s) const
  {
    return !(*this == s);
  }

3.operator+=函数

利用上面实现的push_back函数和append函数我们能够实现operator+=函数的重载。

字符版本

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

字符串版本

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

6、其他函数

1.find 查找相关的函数

find函数主要用来匹配字符或字符串,也是两个版本。

字符版本:我们可以直接采用遍历查看是否匹配

size_t string::find(char ch, size_t pos)
{
  assert(pos <= _size);
  for (size_t i = pos; i <= _size; ++i)
  {
    if (ch == _str[i])
    {
      return i;
    }
  }
  return npos;
}

字符串版本:我们可以直接采用库函数strstr查看是否匹配

size_t string::find(const char* str, size_t pos)
{
  char* pstr = strstr(_str, str);
  if (pstr == nullptr)
  {
    return npos;
  }
  return pstr - _str;
}

2.流提取运算符

流提取运算符对于string对象就是打印对象里面的内容,不同于c_str函数的是,利用c_str函数打印的字符串是遇到\0终止,利用流提取运算符打印的字符串是根据_size的个数来打印的,即使遇到\0也不会停止。

//string的流提取
ostream& operator<<(ostream& out, const string& s)
{
  for (auto& e : s)
  {
    out << e;
  }
  return out;
}

3. 流插入运算符

流提取运算符的实现,我们可以利用ostream里面的get函数从缓冲区中拿到每一个元素,然后赋值给我们想赋值的对象就行了。

//string的流插入
istream& operator>>(istream& in, string& s)
{
  //先清空原有的字符串
  s.clear();
  char ch;
  int i = 0;
  //定义一个小的缓冲区,缓冲区满了再赋值给对象s,防止频繁扩容
  char buffer[128];
  ch = in.get();
  while (ch != ' ' && ch != '\n')
  {
    if (i == 127)
    {
      buffer[i] = '\0';
      s += buffer;
      i = 0;
    }
    buffer[i++] = ch;
    ch = in.get();
  }
  if (i != 0)
  {
    buffer[i] = '\0';
    s += buffer;
  }
  return in;
}
相关文章
|
2天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
2天前
|
C++
模拟实现c++中的string
模拟实现c++中的string
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
51 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
32 3
|
3月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
140 5
|
3月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
89 2