栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

String类的使用与模拟实现

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

String类的使用与模拟实现

String类的使用与模拟实现 1.关于String类? 1.1问题来源:

首先我们要知道,在C语言中,字符串就是一个以’’结尾的字符集合。既然是这样,那我们为什么不尝试在C++这门面向对象的语言里,将其封装成一个类,以便大众使用呢?

这就是为啥会有这样一个类的存在——String

2.让我们瞅瞅库中对String的介绍

String文库文档

大致总结:

  • string类,是一个表示字符串的类
  • string类提供的接口和常规容器的接口差不多,但是也多加了一些特殊的接口来操作string类
  • string在底层实际是basic_string类的别名
  • string类不能操作多字节或者变长字符的序列
3.继续看看String的接口(主要的重点接口)
接口名字功能说明
string()构造函数
string(const char* s)用C语言中的string来构造string类对象
string(const string& s)拷贝构造函数
size()返回字符串的有效字符长度
empty()检测字符串是否为空
clear()清空字符串
reserver()为字符串预留空间以便于后续的写入数据
resize()将有效字符的个数改成x个,多出的空间用字符’’(缺省值)或别的字符填充
3.1稍微介绍一下这里面的某些接口吧
  • string(const char* s)

这里的构造函数很重要,也同样很容易写错。通常,我们会给这个构造函数一个缺省值,这里一定不能给nullptr指针或者’’

  • size()

其实size()和length()的作用完全一致,都是返回字符串的有效字符长度,而且实现的逻辑也是一模一样

  • empty()

大部分容器都会有的一个接口,经常用于遍历或等等情况,所以算是一个很重要的接口

  • clear()

注意这里的clear()一般是用来配合destory()来一起使用的,原因是clear()只是清空了string中的有效字符,并没有改变空间

  • reserver()

为string预留空间,不改变有效元素的个数(注:当reserver的参数小于string的底层空间总大小时,reserver不会改变容量大小)

  • resize()

resize的两个重载,都是讲字符串中的有效字符个数改变到n个,不同的是,resize(size_t n,char c)会用字符c来填充剩余的空间,而resize(size_t)则是用’’来填充

4.String类中的对象访问及遍历

作为一个容器,它应该拥有一个自己的迭代器来访问空间内的元素

operator[]返回_pos位置的字符,同时也支持const类对象的调用
begin()+end()begin(),返回一个字符的迭代器;end(),返回最后一个字符的下一个位置的迭代器
rbegin() + rend()rbegin(),返回一个字符的迭代器;rend(),返回最后一个字符的下一个位置的迭代器

当然,有了迭代器iterator,就可以很容易实现范围for

访问:

遍历:

5.String类对象的修改操作(部分重点)
push_back()字符串尾插入字符
operator+=运算符重载,追加字符串str
append()追加字符串
c_str()返回C格式的字符串
find()查找字符
5.1顺便来看看这几个接口
  • operator+= 和 append() 其实实现的内容大致相同

    (但还是使用operator+=比较多,+=不仅仅可一链接单个字符,还可以连接字符串

都是在后面追加字符串str

  • c_str() 返回C格式下的字符串,这个接口其实很重要,例如当你想用到strcmp,strcpy等等

  • find() 查找字符 返回一个pos (很常用,当你使用erase(),suffix()等,需要一个pos来确定一个范围)

6.String非成员函数(部分重点)
函数功能
operator+运算符重载,链接两string
swap()交换
operator>>输入运算符重载
operator<<输出运算符重载
getline()获取一行字符串
6.1 接着看看这些接口
  • operator+ 最好不要用,原因是因为operator+ 是值传递返回,导致最后深拷贝效率低

  • getline()和直接用cin传进去差不多

7. String类的模拟实现

到了这,我们不妨来自己设计一个string类

除了上面的接口,我们还需注意哎实现string类的构造,拷贝构造,赋值运算符重载以及析构函数

先具体定义一个string类

namespace MyString
{
  class string
  {
    public:
    ...
    private:
    	char* _str;
    	size_t _size;
    	size_t _capacity;
  }
}
7.1 构造函数
//用c格式的字符串来初始化
string(const char* str = "")//这里设置一个缺省值
{
  _size = strlen(str);
  _capacity = _size;
  _str = new char[_capacity + 1];//开辟空间,别忘了要给''留一个位置
  strcpy(_str,str);//将数据拷贝到_str
}
7.2 拷贝构造
//这里通常会有两种方法
//1.
string(const string& s)
  :_str(new char[strlen(s._str)+1])//开空间
{
  strcpy(_str,s._str);
}
//2.
string(const string& s)
  :_str(nullptr)
  ,_size(0)
  ,_capacity(0)
{
  string strTmp(s._str);//开辟一个临时类,将s中的字符串拷贝进去
	swap(strTmp);//这里使用的namespace里面的swap(),进行交换
}
void swap(string& s)
{
  ::swap(_str,s._str);
  ::swap(_size,s._size);
  ::swap(_capacity,s._capacity);//作用域前面空白,意思是调用全局的swap() -> 也就是std中的swap()
}

这里有个小问题,为啥我们要自己写一个swap(),而不是直接去调用全局的swap()呢?

其实很好理解

通过接口实现代码可以看出,如果我们使用全局的swap(),会进行三次深拷贝

我们作用域的swap(),却直接交换了类中的成员变量,对比于上面,效率会高很多

7.3 赋值运算符重载

为了使代码的可读性变高,自己写一个赋值运算符的重载是很有必要的

对于是否可以使用编译器自己合成的赋值运算符重载呢?答案是肯定不行的,对于这样一个类(涉及到资源的管理),编译器给的**浅拷贝(传值)**是一定不行的(会对同一块内存同时析构两次)。

所以对于string类,拷贝构造函数、赋值运算符重载、析构函数必须要显式给出(说白了就是要自己写)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7rE1nqqZ-1632955745309)(/Users/wuguocheng/Library/Application Support/typora-user-images/image-20210929154755106.png)]

//同样也有两种方式
//1.
string& operator=(const string& s)
{
  //确保不是同一个类对自己赋值
  if(*this != &s)
  {
  //对于这一系列操作,大致就是这样:
  //开辟新空间-->拷贝数据-->删除掉旧空间-->将新空间赋回
    char* tmpStr = new char[strlen(s._str)+1];
    strcpy(tmpStr,s._str);
    delete[] _str;
    _str = tmpStr;
  }
  return *this;
}
//2.
//第二种方法比较厉害,只需要复用刚刚我们实现的swap()就好了
string& operator=(string s)//这里用传值,让s进行拷贝构造
{
  swap(s);
  return *this;
}
7.4 析构函数

全部清空就完了,没啥好说的

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

接着我们实现一下迭代器iterator,前面了解过迭代器后,我们知道string类里面的迭代器其实就是充当着一个原生指针

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

同时也要配合着const类,设计一个const迭代器

7.6 对象访问接口 operator[ ] (size_t i)

实现功能:返回字符

注:特别还需要多设计一个const型

const char& operator[](size_t i)const
{
  assert(i < _size);//断言一下,(pos肯定不能比整个字符串都长吧
  return _str[i];
}
char& operator[](size_t i)
{
  assert(i < _size);
  return _str[i];
}
7.7 对象的容量操作 7.7.1 size()

实现功能:返回字符串的有效数字长度

size_t size()
{
  return _size;
}
7.7.2 empty()

判断字符串是否为空

bool empty()
{
  return _size == _capacity;
}
7.7.3 clear()

清空字符串内容

void clear()
{
  _size = 0;
  _str[0] = '';//只是清空数据内容,并不是将此空间从内存中删除
}
7.7.4 reserve(size_t n)

给字符串预留空间

void reserve(size_t n)
{
  if(n > _capacity)
  {
    char* tmp = new char[n + 1];
    strncpy(tmp,_str,_size+1);
    delete[] _str;
    _str = tmp;
    _capacity = n;
  }
  else
  {
    return;
  }
}
7.7.5 resize(size_t i,char c)

将有效字符的个数改成n个

顺带重载一个resize(size_t i,char c)

void resize(size_t n,char val = '')
{
  //resize()要考虑三种情况来写
  //1. 当 n < _size (修改后小于原字符串长度)
  //2. 当 _size <= n < _capacity (在预留空间内,不需要扩容)
  //3. 当 _capacity < n (在预留空间之外,需要扩容)
  
  if(n < _size)//第一种情况
  {
    _size = n;
    _str[_size + 1] = '';
  }
  else
  {
    if(n > _capacity)//第三种情况
    {
      reserve(n);//先扩容
    }
    for(size_t i = 0; i < n; i++)//第二种情况也包括在里面
    {
      _str[i] = val;//将缺省值val填入
    }
    _str[n] = '';//填入''
    _capacity = n;//顺带要改变_capacity,因为resize()是一定会补上空的
  }
}
7.8 对象修改/操作接口 7.8.1 push_back(const char ch)

字符串尾部插入字符

void push_back(const char ch)
{
  //尾插
  //需要先判断空间够不够
  if(_capacity == _size)
  {
    reserve(_capacity == 0 ? 4 : _capacity * 2);//是否为0,是的话先初始化成4,不是的话 * 2
  }
  _str[_size] = ch;
  _str[_size+1] = '';
  _size++;
}
7.8.2 append(const char* str)

在字符串后增加字符串str

void append(const char* str)
{
  size_t len = _size + strlen(s);
  if(len > _capacity)//如果空间不够
  {
    reserve(len);
  }
  strcpy(_str + _size, s);//从size位置开始往后加上字符串str
  _size = len;
}
7.8.3 operator+=(const char ch)

在字符串后增加字符串str,字符等

string& operator+=(const char ch)
{
  push_back(ch);//复用push_back()
  return *this;
}
string& operator+=(const char* str)
{
  append(str);//复用append()
  return *this;
}
7.8.4 insert(size_t pos,char ch) / insert(size_t pos,const char* str)

某个位置插入字符、字符串

string& insert(size_t pos,char ch)
{
  assert(pos < _size);
  if(_size == _capacity)//判断空间是否已经满了
  {
    reserve(_capacity == 0 ? 4 : _capacity * 2);//( 还是有可能为0的
  }
  //挪动数据
  char* end = _str + _size;
  while(end >= _str + pos)
  {
    *(end+1) = *end;
    end--;
  }
  _str[pos] = ch;//插入字符
  _size++;
  return *this;
}
string& insert(size_t pos,const char* str)
{
  assert(pos < _size);
  size_t len = _size + strlen()//这里要计算一下新串的长度
    // 和上面不同的是,上面只是插入一个字符,只需要考虑当前是否为满
	if(len > _capacity)
  {
    reserve(len);//扩容
  }
  //娜数据
  char* end = _str + _size;
  while(end >= _str + pos)
  {
    //这里要挪动pos个位置
    *(end + pos) = *end;
    end--;
  }
  strncpy(_str + pos,str,strlen(str));//将str的数据拷贝到_str中
  _size = len;
  return *this;
}

7.8.5 erase(size_t pos,size_t len = npos)

从pos位置开始删除len长度的串

可能有人会好奇npos 从哪来,这里是加在private中的一个size_t类型的静态成员变量 初始值赋值为-1

为啥设置为-1?

我们知道,int和size_t的大小都为4bit,0000 0000 0000 0001 ---- int 1

​ 1111 1111 1111 1110 ---- size_t -1

那么这个npos就是一个超级无敌大的数字,我们在这默认字符串不会达到这个长度

那么len的大小也是一个超级无敌大的数字,如果我们用缺省值

如:erase(2);

即会删除字符串第二个字符后所有的数据

可以理解吧。

这里npos还会在find()里面展示它的妙用

string& erase(size_t pos,size_t len = npos)
{
  assert(pos < _size);
  size_t lowLen = _size - pos;//计算剩余字符串长度
  if(len > lowLen)//如果要删除的数据,大于剩余字符串的大小,那么就将pos位置后面的所有数据全部删除
  {
    _str[pos] = '';//直接将pos位置记为
    _size = pos;
  }
  else//如果要删除的数据,小于剩余字符串的大小,那么就娜数据吧
  {
    strncpy(_str + pos, _str + pos + len);
    _size -= len;
  }
  return *this;
}
7.8.6 find(char ch,size_t pos = 0) / find(const char* str, size_t pos = 0)

在string字符串中找到一个字符或者一个字符串str

这里也会用到 npos,等会来解释

size_t find(char ch,size_t pos = 0)
{
  assert(pos < _size);
  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)
{
  assert(pos < _size);
  const char* ret = strstr(_str + pos, str);//strstr:查找字符串中的子串
  if(ret)//找到了
  {
    return ret - _str;//返回str在_str中的地址位置
  }
  else
  {
    return npos;//找不到,返回一个超级大的数字
  }
}

为啥要这样,返回这个npos呢?

其实很好理解,想想看我们为啥在上面很多接口都加上了这样一个断言

assert(pos < _size)

懂了吧,像find()这样专门用来找pos的接口,如果pos找不到,就直接让它终止在下一个调用find()接口上的断言里吧!

7.9 运算符重载

加强代码可读性

(在类外面实现)
思路:疯狂复用

inline bool operator<(string& s1,stinrg& s2)
{
  return strcmp(s1.c_str(),s2.c_str()) < 0;
}
inline bool operator==(string& s1,string& s2)
{
  return strcmp(s1.c_str(),s2.c_str()) == 0;
}
inline bool operator<=(string& s1,string& s2)
{
	return (s1 == s2) || (s1 < s2);
}
inline bool operator>(string& s1,string& s2)
{
  return !(s1 <= s2);
}
inline bool operator>=(string& s1,string& s2)
{
  return !(s1 < s2);
}
inline bool operator!=(string& s1,string& s2)
{
  return !(s1 == s2);
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/274524.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号