C++学习笔记(十一)——String类的模拟实现(二)

简介: C++学习笔记(十一)——String类的模拟实现

迭代器相关函数


begin和end


string类中的迭代器实际就是字符指针,只是给字符指针起了个别名。

1. typedef char* iterator;
2. typedef const char* const_iterator;

按方向分: 有正向迭代器和反向迭代器(iterator和reverse_iterator)分别配合being()、end()和rbegin()、rend()使用

按属性分: 有普通迭代器和const迭代器 (iterator const_iterator | reverse_iterator const_reverse_iterator)

begin函数模拟实现

iterator begin()
{
  return _str; //返回字符串中第一个字符的地址
}
const_iterator begin()const
{
  return _str; //返回字符串中第一个字符的const地址
}

end函数模拟实现

iterator end()
{
  return _str + _size; //返回字符串中最后一个字符的后一个字符的地址
}
const_iterator end()const
{
  return _str + _size; //返回字符串中最后一个字符的后一个字符的const地址
}

在明白了string类中迭代器的底层实现,再来看看我们用迭代器遍历string的代码,其实就是用指针在遍历字符串而已。


实际上范围for并不神奇,因为在代码编译的时候,编译器会自动将范围for替换为迭代器的形式,也就是说范围for是由迭代器支持的,现在我们已经实现了string类的迭代器,自然也能用范围for对string进行遍历:

string s("hello world!!!");
//编译器将其替换为迭代器形式
for (auto e : s)
{
  cout << e << " ";
}
cout << endl;

修改字符串相关函数


push_bac


push_back函数的作用就是在当前字符串的后面尾插上一个字符,尾插之前首先需要判断是否需要增容,若需要,则调用reserve函数进行增容,然后再尾插字符,注意尾插完字符后需要在该字符的后方设置上’\0’,否则打印字符串的时候会出现非法访问,因为尾插的字符后方不一定就是’\0’。

//尾插字符
void push_back(char ch)
{
  if (_size == _capacity) //判断是否需要增容
  {
    reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
  }
  _str[_size] = ch; //将字符尾插到字符串
  _str[_size + 1] = '\0'; //字符串后面放上'\0'
  _size++; //字符串的大小加一
}

append


append函数的作用是在当前字符串的后面尾插一个字符串,尾插前需要判断当前字符串的空间能否容纳下尾插后的字符串,若不能,则需要先进行增容,然后再将待尾插的字符串尾插到对象的后方,因为待尾插的字符串后方自身带有’\0’,所以我们无需再在后方设置’\0’。

//尾插字符串
void append(const char* str)
{
  size_t len = _size + strlen(str); //尾插str后字符串的大小(不包括'\0')
  if (len > _capacity) //判断是否需要增容
  {
    reserve(len); //增容
  }
  strcpy(_str + _size, str); //将str尾插到字符串后面
  _size = len; //字符串大小改变
}

operator+=


+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。

+=运算符实现字符串与字符之间的尾插直接调用push_back函数即可。

//+=运算符重载
string& operator+=(char ch)
{
  push_back(ch); //尾插字符串
  return *this; //返回左值(支持连续+=)
}

+=运算符实现字符串与字符串之间的尾插直接调用append函数即可。

//+=运算符重载
string& operator+=(const char* str)
{
  append(str); //尾插字符串
  return *this; //返回左值(支持连续+=)
}

insert


insert函数的作用是在字符串的任意位置插入字符或是字符串。

insert函数用于插入字符时,首先需要判断pos的合法性,若不合法则无法进行操作,紧接着还需判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符的过程也是比较简单的,先将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串即可。

//在pos位置插入字符
string& insert(size_t pos, char ch)
{
  assert(pos <= _size); //检测下标的合法性
  if (_size == _capacity) //判断是否需要增容
  {
    reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
  }
  char* end = _str + _size;
  //将pos位置及其之后的字符向后挪动一位
  while (end >= _str + pos)
  {
    *(end + 1) = *(end);
    end--;
  }
  _str[pos] = ch; //pos位置放上指定字符
  _size++; //size更新
  return *this;
}

erase


erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:

1、pos位置及其之后的有效字符都需要被删除。这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。

2、pos位置及其之后的有效字符只需删除一部分。

这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’

//删除pos位置开始的len个字符
string& erase(size_t pos, size_t len = npos)
{
  assert(pos < _size); //检测下标的合法性
  size_t n = _size - pos; //pos位置及其后面的有效字符总数
  if (len >= n) //说明pos位置及其后面的字符都被删除
  {
    _size = pos; //size更新
    _str[_size] = '\0'; //字符串后面放上'\0'
  }
  else //说明pos位置及其后方的有效字符需要保留一部分
  {
    strcpy(_str + pos, _str + pos + len); //用需要保留的有效字符覆盖需要删除的有效字符
    _size -= len; //size更新
  }
  return *this;
}

clear


clear函数用于将对象中存储的字符串置空,实现时直接将对象的_size置空,然后在字符串后面放上’\0’即可。

//清空字符串
void clear()
{
  _size = 0; //size置空
  _str[_size] = '\0'; //字符串后面放上'\0'
}

swap


swap函数用于交换两个对象的数据,直接调用库里的swap模板函数将对象的各个成员变量进行交换即可。但我们若是想在这里调用库里的swap模板函数,需要在swap函数之前加上“::”(作用域限定符),告诉编译器优先在全局范围寻找swap函数,否则编译器编译时会认为你调用的是正在实现的swap函数(就近原则)。

//交换两个对象的数据
void swap(string& s)
{
  //调用库里的swap
  ::swap(_str, s._str); //交换两个对象的C字符串
  ::swap(_size, s._size); //交换两个对象的大小
  ::swap(_capacity, s._capacity); //交换两个对象的容量
}

c_str


c_str函数用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可。

//返回C类型的字符串
const char* c_str()const
{
  return _str;
}

访问字符串相关函数


operator[]


[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。

在C字符串中我们通过[ ] +下标的方式可以获取字符串对应位置的字符,并可以对其进行修改,实现[ ] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了,但需要注意在此之前检测所给下标的合法性。

//[]运算符重载(可读可写)
char& operator[](size_t i)
{
  assert(i < _size); //检测下标的合法性
  return _str[i]; //返回对应字符
}

在某些场景下,我们可能只能用[ ] +下标的方式读取字符而不能对其进行修改。例如,对一个const的string类对象进行[ ] +下标的操作,我们只能读取所得到的字符,而不能对其进行修改。所以我们需要再重载一个[ ] 运算符,用于只读操作。

//[]运算符重载(只读)
const char& operator[](size_t i)const
{
  assert(i < _size); //检测下标的合法性
  return _str[i]; //返回对应字符
}

find和rfind


find函数:

1、正向查找第一个匹配的字符。

首先判断所给pos的合法性,然后通过遍历的方式从pos位置开始向后寻找目标字符,若找到,则返回其下标;若没有找到,则返回npos。(npos是string类的一个静态成员变量,其值为整型最大值)

//正向查找第一个匹配的字符
size_t find(char ch, size_t pos = 0)
{
  assert(pos < _size); //检测下标的合法性
  for (size_t i = pos; i < _size; i++) //从pos位置开始向后寻找目标字符
  {
    if (_str[i] == ch)
    {
      return i; //找到目标字符,返回其下标
    }
  }
  return npos; //没有找到目标字符,返回npos
}

2、正向查找第一个匹配的字符串。

首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。strstr函数若是找到了目标字符串会返回字符串的起始位置,若是没有找到会返回一个空指针。若是找到了目标字符串,我们可以通过计算目标字符串的起始位置和对象C字符串的起始位置的差值,进而得到目标字符串起始位置的下标。

//正向查找第一个匹配的字符串
size_t find(const char* str, size_t pos = 0)
{
  assert(pos < _size); //检测下标的合法性
  const char* ret = strstr(_str + pos, str); //调用strstr进行查找
  if (ret) //ret不为空指针,说明找到了
  {
    return ret - _str; //返回字符串第一个字符的下标
  }
  else //没有找到
  {
    return npos; //返回npos
  }
}

rfind函数:

实现rfind函数时,我们可以考虑复用已经写好了的两个find函数,但rfind函数是从后先前找,所以我们需要将对象的C字符串逆置一下,若是查找字符串,还需将待查找的字符串逆置一下,然后调用find函数进行查找,但注意传入find函数的pos以及从find函数接收到的pos都需要镜像对称一下。

1、反向查找第一个匹配的字符。

首先我们需要用对象拷贝构造一个临时对象tmp,因为我们并不希望调用rfind函数后对象的C字符串就被逆置了。我们将tmp对象的C字符串逆置,然后将所给pos镜像对称一下再调用find函数,再将从find函数接收到的返回值镜像对称一下作为rfind函数的返回值返回即可。

//反向查找第一个匹配的字符
size_t rfind(char ch, size_t pos = npos)
{
  string tmp(*this); //拷贝构造对象tmp
  reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
  if (pos >= _size) //所给pos大于字符串有效长度
  {
    pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
  }
  pos = _size - 1 - pos; //将pos改为镜像对称后的位置
  size_t ret = tmp.find(ch, pos); //复用find函数
  if (ret != npos)
    return _size - 1 - ret; //找到了,返回ret镜像对称后的位置
  else
    return npos; //没找到,返回npos
}

注:rfind函数规定,当所给的pos大于等于字符串的有效长度时,看作所给pos为字符串最后一个字符的下标。


2、反向查找第一个匹配的字符串。

首先我们还是需要用对象拷贝构造一个临时对象tmp,然后将tmp对象的C字符串逆置,同时我们还需要拷贝一份待查找的字符串,也将其逆置。然后将所给pos镜像对称一下再调用find函数。注意:此时我们将从find函数接收到的值镜面对称后,得到的是待查找字符串的最后一个字符在对象C字符串中的位置,而我们需要返回的是待查找字符串在对象C字符串中的第一个字符的位置,所以还需做进一步调整后才能作为rfind函数的返回值返回

//反向查找第一个匹配的字符串
size_t rfind(const char* str, size_t pos = npos)
{
  string tmp(*this); //拷贝构造对象tmp
  reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
  size_t len = strlen(str); //待查找的字符串的长度
  char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串)
  strcpy(arr, str); //拷贝str给arr
  size_t left = 0, right = len - 1; //设置左右指针
  //逆置字符串arr
  while (left < right)
  {
    ::swap(arr[left], arr[right]);
    left++;
    right--;
  }
  if (pos >= _size) //所给pos大于字符串有效长度
  {
    pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
  }
  pos = _size - 1 - pos; //将pos改为镜像对称后的位置
  size_t ret = tmp.find(arr, pos); //复用find函数
  delete[] arr; //销毁arr指向的空间,避免内存泄漏
  if (ret != npos)
    return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
  else
    return npos; //没找到,返回npos
}

关系运算符重载函数


系运算符有 >、>=、<、<=、==、!= 这六个,但是对于C++中任意一个类的关系运算符重载,我们均只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。

例如,对于string类,我们可以选择只重载 > 和 == 这两个关系运算符。

//>运算符重载
bool operator>(const string& s)const
{
  return strcmp(_str, s._str) > 0;
}
//==运算符重载
bool operator==(const string& s)const
{
  return strcmp(_str, s._str) == 0;
}

剩下的四个关系运算符的重载,就可以通过复用这两个已经重载好了的关系运算符来实现了。

//>=运算符重载
bool operator>=(const string& s)const
{
  return (*this > s) || (*this == s);
}
//<运算符重载
bool operator<(const string& s)const
{
  return !(*this >= s);
}
//<=运算符重载
bool operator<=(const string& s)const
{
  return !(*this > s);
}
//!=运算符重载
bool operator!=(const string& s)const
{
  return !(*this == s);
}.

>>和<<运算符的重载以及geline函数


>>运算符的重载


重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。

//>>运算符的重载
istream& operator>>(istream& in, string& s)
{
  s.clear(); //清空字符串
  char ch = in.get(); //读取一个字符
  while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
  {
    s += ch; //将读取到的字符尾插到字符串后面
    ch = in.get(); //继续读取字符
  }
  return in; //支持连续输入
}

<<运算符的重载


重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用范围for对对象进行遍历即可。

//<<运算符的重载
ostream& operator<<(ostream& out, const string& s)
{
  //使用范围for遍历字符串并输出
  for (auto e : s)
  {
    cout << e;
  }
  return out; //支持连续输出
}

getline


getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。

//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{
  s.clear(); //清空字符串
  char ch = in.get(); //读取一个字符
  while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
  {
    s += ch; //将读取到的字符尾插到字符串后面
    ch = in.get(); //继续读取字符
  }
  return in;
}
相关文章
|
19天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
44 5
|
19天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
29 2
|
25天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
59 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
65 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
74 4
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
43 2
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
25 1
|
3月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
44 0
java基础(13)String类
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
64 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
3月前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①