string 类以及模拟实现

𝙉𝙞𝙘𝙚!!👏🏻‧✧̣̥̇‧✦👏🏻‧✧̣̥̇‧✦ 👏🏻‧✧̣̥̇:Solitary_walk

      ⸝⋆   ━━━┓
     - 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━  ➴ ⷯ

本人座右铭 :   欲达高峰,必忍其痛;欲戴王冠,必承其重。

👑💎💎👑💎💎👑 
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑    希望在看完我的此篇博客后可以对你有帮助哟

👑👑💎💎💎👑👑   此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!
👑👑👑💎👑👑👑 

目录:

一:C语言里面的字符串

二:标准库里面的string

三:string常用的接口

四:对string 这个类的模拟实现


1:C语言里面的字符串
C 语言中,字符串是以 '\0'结尾 的一些字符的集合,为了操作方便, C 标准库中提供了一些 str 系列的库函数, 但是这些库函数与字符串是分离开的,不太符合OOP(面向对象编程,核心:封装,继承,多态) 的思想。
但是有很多局限:很容易出现越界访问
2:标准库里面的string
1.) string 是表示字符串的字符串类
2. ) 该类的接口与常规容器的接口基本相同,无非就是添加了一些专门用来操作 string 的常规操作。

3. )string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>  string;

4. ) 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
3:string常用的接口
3.1对string 对象常用到的构造函数

常用到的构造函数:多为这4个

 使用示范:

3.2string 对象的容量的操作

1)size () 使用

 简言之就是:size () 这个函数只统计字符串的有效个数(\0之前的字符)

2)resize() 和 reserve () 使用以及对比

 通俗的理解就是:resize ( )这个函数可以进行扩容(并默认支持指定 /0 来进行初始化初始化)也可以进行缩容

 运行结果:

reserve () :这个函数也可以进行空间的开辟,但是不支持初始化(一般当提前大概知道当前对象的大小的时候可以进行空间开辟,避免底层扩容的消耗,代价过大) 

3)capacity()

 capacity():这个函数是以字节作为单位返回当前指向对象被开辟的空间大小,注意capacity有效空间的大小并不受当前size()的影响

4)clear () 

简言之就是:把当前的有效的字符进行删除

 

3.3 对string 对象的访问以及遍历操作
1) operator [ ]

operator [ ] : 返回下标为pos 对应位置的字符

 

2) begin ()+ end( )

注意:begin 指向当前字符的第一个位置,end 指向最后一个有效字符的下一个位置,范围是左闭右开

 使用示范:

3) rebegin( ) reend( )

使用示范:

4) 范围for

3.4 对string  对象的修改操作
3.4.1) push_back()  append( )

push_back: 在原来字符串的后面追加一个字符

 使用示范:

3.4.2) operator +=

对于以上追加一个字符和一个字符串的过于复杂了,有没有一个函数就可以搞定的???

3.4.3) find() 和  pos() substr()   getline( )

find () 这个函数 的返回值:若是存在返回第一个字符出现的对应下标位置否则返回npos

使用示范:

 对于getline( )这个函数结合具体的例题更有助于理解

对于这个题其实并不难理解,关键就是如何处理输入中的空格或者是 \0 ,这个时候使用getline () 即可,默认遇到缓冲区里面的空格或者是 \0并不结束读取

#include <iostream>
#include <string>
using namespace std;

int main()
{
	string str;
	//cin >> str;
	getline(cin, str);
	//对于scnaf ()和cin 进行输入的时候:当有多个数据进行输入的时候,编译器会自动把空格或者是换行作为数据分割开的依据
	int len = 0;
	size_t pos = str.rfind(' ');
	if (pos != string::npos)//已经找到
    {  
		len = str.size() - (pos + 1);
        cout << len << endl;
    }
	else
	 cout<< str.size()<<endl;//就是只有整个单词
	
	
}
 3.4.5) vs下的string深度理解

在VS下:一个string 类的对象 大小是28个字节,包括默认一个16个字节的数组,一个维护_size 的变量,一个维护_capacity的变量,和维护堆上的一个指针变量(默认32平台)总大小 = 16 + 3*4 = 28

注意: 对于VS而言,\0不算做有效字符,所以_capacity的大小是15个字节

3.5对string 非类的成员函数
4:对string 这个类的模拟实现
4.1 有参以及无参的构造函数模拟

首先需要自己定义一个类:string (我为了避免与库里面的string 冲突,把自己定义的这个string 放在 Y 这个命名空间里面)

string 这个类的定义

namespace Y
{
	class string
	{
	public:
		
	private:
		size_t _size;
		size_t _capacity;
		const char* _str;
	};
}

 构造函数的实现

namespace Y
{
	class string
	{
	public:
		//无参的构造函数
		string()
			:_size(0)
			, _capacity(_size)
			, _str(new char[1]{ '\0' })  // 开一个空间并进行初始化
		{}
		//有参的构造函数
		string(const char* s = "")
			:_size(strlen(s))
			,_capacity(_size)
		{
			_str = new char[_size+1];
			strcpy(_str, s);
		}
	private:
		size_t _size;
		size_t _capacity;
		 char* _str;
	};
}

注意有参构造函数 的参数类型必须的const char * 类型,所以说是不能传 '\0'

在对_str开空间的时候要多开一个是为了存放 \0

4.2 析构函数模拟
        ~string()//析构函数
		{
			delete[] _str;
			_size = _capacity = 0;
		}
4.3 push_back()  append()  operator+=() 模拟

push_back() append()  operator+=()  这三个函数都是在原来字符串的基础上进行尾插,只不过是尾插一个字符还是尾插一个字符串的区别(对这三个函数的具体理解见前面的分析)

push_back() 模拟

进行尾插的时候:需要考虑是否进行扩容以及对_size要进行改变

void reserve(size_t n)
		{
			if (n > _capacity) // 扩容条件:扩容之后的空间必须是大于当前的空间容量
			{
				char* tmp = new char[n + 1];
				//对原来数据进行拷贝
				strcpy(tmp, _str);
				delete []_str;//_str 是一个数组
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(const char ch = '\0')
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
append() 模拟
	void append(const char* str)
		{
			size_t len = strlen(str);
			if (len + _size > _capacity)
			{
				reserve(len + _size);//注意这里不能单纯的以2倍扩容(当size= 4,len= 80,有问题)
			}
			strcpy(_str + _size, str);
			_size += len;
			//_str[_size] = '\0'; //err 因为strcpy拷贝的时候会自动拷贝\0过去
		}
operator +=()模拟

其实就是对push_back() 以及 append() 函数的封装

string& operator+=(const char ch)
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
4.4 insert ()模拟

错误使用:当对字符串下标为0 的位置插入字符或者是字符串的时候,程序就有问题了:

分析:这是因为发生了整形提升,因为end 类型是size_t 当end = -1是进入死循环导致end 下标的越界最终程序出现问题

 解决:

1) 对pos 进行类型强转

2)改变 end 所指向的位置

 

4.5 erase 模拟

erase介绍见前序

指定删除序列的n个字符从pos位置开始,(注意默认pos是从下标为0的时候,len默认是整个字符串)

 代码:
void erase(size_t pos , size_t len = npos)
		{
			//1:特殊情况处理 (直接对_size,以及\0改变)2:正常情况(数据覆盖)
			if (len >= _size || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			//正常删除
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos] = _str[end];
					end++;
					pos++;
				}
				_size -= len;
			}
		}
4.6  流插入的重载(<<)

为了更好的对类成员的访问(有的类成员可能设置成private,protected)这里把流插入函数写成友元函数:注意并不是所有 的 流插入函数都写成友元函数,写成友元函数只是更好的对类成员的访问(这个小小的知识点可能是面试官的灵魂拷问哟)

 

ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s._size; i++)// i< _size 错误:
		{
			cout << s._str[i];
		}
		return out;
	}
4.7 流提取的重载(>>)
istream& operator>>(istream& in, string& s)
	 {
		 s.reserve(128);//避免频繁开空间
		 char ch = in.get();
		 //char buff[129];//多开一个为\0存储
		 size_t i = 0;

		 while (ch !='\n' && ch!= ' ')//从缓冲区读取数据的时候,默认遇到\n 或者空格读取结束
		 {
			
			 s += ch;
			 ch = in.get();
		 }
		 
		 cout << s.size() << endl;
		 cout << s.capacity() << endl;
		 //问题:当输入字符个数过少的时候,会造成空间 空间的浪费,而且是不可回收的
		 //解决:提前整个数组,这样即使输入字符个数很少程序结束的时候也会归还系统
		 return in;
	 }

运行结果:

 

自习分析:发现我们的代码有很大的优化空间,当我们输入的字符个数过少 的时候会造成所开空间的浪费,而且是不可回收的,那岂不是有待优化对于咱们的代码?

代码:

 istream& operator>>(istream& in, string& s)
	 {
		 //s.reserve(128);//避免频繁开空间
		 char ch = in.get();
		 char buff[129];//多开一个为\0存储
		 size_t i = 0;

		 while (ch !='\n' && ch!= ' ')//从缓冲区读取数据的时候,默认遇到\n 或者空格读取结束
		 {
			 //s += ch;
			 if (i == 128)
			 {
				 buff[i] = '\0';//这一段读取结束,开始下一段读取
				 s += buff;
				 i = 0;
			 }
			 buff[i++] = ch;//读取的每一个字符放到buff这个数组,并让下标++
			 //s += ch;
			 ch = in.get();
		 }
		 if (i != 0)  //可能存在当前buff里面只有一部分的数据
		 {
			 buff[i] = '\0';//一定要置空,否则出现随机值
			 s += buff;
		 }
		 cout << s.size() << endl;
		 cout << s.capacity() << endl;
		 //问题:当输入字符个数过少的时候,会造成空间 空间的浪费,而且是不可回收的
		 //解决:提前整个数组,这样即使输入字符个数很少程序结束的时候也会归还系统
		 return in;
	 }

运行结果:

4.8 find ()模拟
对一个字符的查找

		size_t find(char ch, size_t pos = 0)const
		{
			size_t find = 0;
			while (find <_size)
			{
				if (_str[find] == ch)
				{
					return find;
				}
				find++;
			}
			return npos;
		}
对一个字符串的查找
size_t find(const char* str, size_t pos = 0)const
		{
			//找一个子字符串,借助strstr函数
			 char* find = strstr(_str + pos, str);
			if (find != nullptr)
			{
				return find - _str;// 返回找到的第一个字符所对应下标(指针相减 = 个数)
			}
			else
				return  npos;//未找到
		}

 在这里有一个关键的知识点:就是如何把指针转换成对应 的元素个数

指针与指针相减即可

4.9 substr() 模拟

 代码:
string substr(size_t pos = 0, size_t len = 0)
		{
			size_t end = len+pos;//结束的下标
			if (pos + len >= _size || pos == end) //特殊情况处理:要取的字符串可能超出当前的有效字符串 的长度,需要进行修改
			{
				len = _size - pos;//实际取到的有效字符
				end = _size;
			}
			string sub("hello");
			sub.reserve(15);//提前进行空间的开辟
			while (pos < end)
			{
				sub += _str[pos];
				pos++;
			}
			return sub;
		}
4.10  resize()模拟

分析:

resize() 这个函数有双重功能:一个是缩容;另一个是扩容(默认\0进行初始化)

void resize(size_t n,char ch = '\0')
		{
			if (n < _size)//缩容
			{
				_str[n] = ch;
				_size = n;
			}
			else //扩容,默认\0
			{
				reserve(n);
				_size += n;
				size_t i = 0;
				while (i < n)
				{
					_str[i++] = ch;
				}
			}
			//不要忘了最后置空
			_str[_size] = '\0';
		}
4.11 reserve()模拟
void reserve(size_t n)
		{
			if (n > _capacity) // 扩容条件:扩容之后的空间必须是大于当前的空间容量
			{
				char* tmp = new char[n + 1];
				//对原来数据进行拷贝
				strcpy(tmp, _str);
				delete []_str;//_str 是一个数组
				_str = tmp;
				_capacity = n;
			}
		}

 本质上讲:用realloc()函数和用new进行扩容其实没有质的区别,只不过用new,delete等操作符可以支持自定义类型

注意这里是进行异地扩容,不要忘了进行数据 对比以及旧空间的释放

4.12 size() 模拟
size_t size() const
		{
			return _size;
		}
4.13capacity() 模拟
ize_t capacity()
		{
			return _capacity;
		}
4.14  迭代器相关的模拟

暂时可以把begin end理解成指针(单本质上并不是指针)

begin:指向数据的起始位置

end:指向最后一个数据的下一个位置

注意:迭代器的范围是左闭右开的

//暂时可以把迭代器理解成指针
		iterator begin()
		{
			return _str;//返回第一个数据的位置
		}
		iterator end()
		{
			return _str + _size;// 返回最后一个数据的下一个位置
		}
		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}
4.15  比较关系的运算符的重载

在这里简单的实现以下几个比较关系操作符的书写,其余进行复用,还是挺香的

bool operator>(const string& s2) const
		{
			//借助strcmp()函数一次进行比较返回int 类型的

			//return strcmp(*this, s2._str) > 0; //err  指代不明确
			return strcmp(_str, s2._str) > 0;//只要是在类里面都可以进行对成员变量的访问,不分私有还是公有
		}
		bool operator==(const string& s2) const
		{
			return strcmp(_str, s2._str) == 0;
		}

CV一下

//接下来比较关系运算符的虫重载直接进行复用即可
		bool operator>=(const string& s2) const
		{
			//return (operator>(s2) || operator==(s2));
			return (*this > s2 || *this == s2);
		}
		bool operator<(const string& s2) const
		{
			return (!operator>=(s2));
		}
		bool operator<=(const string& s2) const
		{
			return (!operator>(s2));
		}
		bool operator !=(const string& s2)
		{
			return !(operator==(s2));
		}
 4.16赋值运算符的重载

当我们想让已经实例化好的对象对未实例化的对象进行初始化的时候相信不少老铁是这样写的:

 

 分析:

此时对象s2是对s1这个对象进行浅拷贝的(值拷贝),在未经任何处理的时候,编译器默认对自定义类型的对象进行浅拷贝的,当s2这个对象调用析构函数的时候,_str所指向的空间就归还了系统;当s1调用析构函数的时候,s1这个对象_str所指向的空间就已经是随机的了,再进行释放的时候就出现了 野指针的问题,所以此时程序崩溃(注意:先构造的对象后析构

解决:s2这个对象拷贝构造s1这个对象的本质只是进行数据的拷贝,并不是让s2对象的_str和s1这个对象里面 _str都指向相同的空间,所以就需要自己进行空间的申请

传统写法
void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);

		}
//传统写法
		string operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._size + 1];
				delete _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
 现代写法:
void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);

		}
		
		//现代写法
		//  s2 = s1
		string operator=(const string& s) 
		{
			if (this != &s)
			{
				string tmp(s);
				swap(tmp);
			}
			return *this;
		}

 其实本质上没有什么不同,最终编译器还是进行传统写法的调用只不过用现代的写法我们自己是省去了不少事(简言之,就是自己不用搬太多的砖了,有个小弟帮自己干点,就可以减轻自己的负担

4.17拷贝构造函数的重载
传统写法:
 //传统拷贝构造函数的写法
		 //s2(s1)
		 string(const string& s)
		 {
			 _str = new char[_capacity + 1];
			 //数据拷贝
			 strcpy(_str, s._str);
			 _size = s._size;
			 _capacity = s._capacity;
		 }
		 
现代写法:
// 现代写法的拷贝构造函数
		 // s2(s1)
		 string(const string& s) 
			 : _size(0)
			 ,_str(nullptr)//为什么要对_str置空(不置空的话,此时_str指向的数据是随机值,也就是说此时的_str是野指针):方便后面交换之后,tmp指向的空间为空,这时在调用析构函数的时候不会崩溃,因为只有动态开辟出来的空间才可以进行释放
			 , _capacity(0)
		 {
			 //我在实现的过程中面临的问题:程序出现野指针(没有对tmp 进行初始化)
			 //string tmp(s);//err
			 string tmp(s._str);//将s1里面的字符串初始化tmp 
			 swap(tmp);
		 }

 在这里有个小小的问题:为什么那个现代写法里面的_str要进行置空???

分析:对于delete 而言,当释放的空间为空是没有问题的,若不及时置空,那么此时的_str就会指向随机的数据(空间不是明确的),那么在调用swap()函数的时候,tmp 所指向的_str就会指向随机数据,在进行资源释放的时候,就出现野指针的问题,导致程序崩溃

结语:

以上就是关于string相关函数应用以及模拟实现,相信不少老铁们看到这里,已经实属不易了,毕竟大大浪淘沙,最后终能淘的自己的“金银珠宝”。关于前期阶段的理解是很重要的,只要是前期自己掌握了七八成,相信到这里你也是可以看懂滴,希望各位老铁们看到这里都能有所收获,当然我的此篇博客也存在多多的不足之处,欢迎各位大佬们随时指正,当然后期也会不断更新,希望各位大佬们阔以多多支持!

  • 46
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值