C++ string(2)实现精讲

个人主页:Jason_from_China-优快云博客

所属栏目:C++系统性学习_Jason_from_China的博客-优快云博客

所属栏目:C++知识点的补充_Jason_from_China的博客-优快云博客

​​​​​​​码云Gitee:贾杰森 (jiajiesen) - Gitee.com

前言

  1. 提示:C++ string的使用我们已经讲解了,接下来我们会讲解string的实现,但是并不是每一个我都会实现,比如string删除有好几个,但是我会删除只是实现一个erase,首先是使用的次数频繁,其次是都大差不差,没有必要实现那么多,我这里只是把string整体的逻辑给实现,对string有一个更深入的了解
     
  2. 目的:提升代码能力和思维能力。
  3. 注意事项1:对于string不是很理解的可以直接看这个博客,有一定关联性,但是关联不是特别大,但是对于文档的阅读如果没有经验,还是建议去阅读string的使用,开始的部分就详细讲解了文档阅读。
    C++ string(1)使用精讲-优快云博客icon-default.png?t=O83Ahttps://blog.youkuaiyun.com/Jason_from_China/article/details/142909086
  4. 注意事项2:对于数据结构学习不精通的同学会有一定的难度,其实建议学一下数据结构,当然直接学习也是没有问题的。
  5. 代码的实现:下面我们讲解代码的时候,在构造函数和析构函数,我会把这个整体的文件代码直接上来,方便大家适应。
    但是构造函数和析构函数讲解完毕之后,除非头文件有缺省参数参数,上代码的时候,我会只是会实现这个函数接口的实现,但是需要头文件的时候,我会进行讲解这个头文件,这样文章不会显得很冗余
  6. 测试:我的测试代码会放到最后,实现代码是都有的

创建文件

  

这里我们和之前一样,创建三个文件

创建类并且进行封装

创建类

  1. 注意事项1:string这里我们是模拟实现的,也就是,这里是自定义类型的,所以我们需要自己创建类
  2. 注意事:2:这里采取的是Visual Studio 2022编译器,有的C语言的语法在C++上编译是需要输入一点东西才能不报错,下面会讲解
//string.h
//头文件
	class string
	{
	public:
	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str;
		size_t _size;
		size_t _capacity;
	};

进行封装

string本身就是一个类,我们直接直接使用string命名就会导致冲突的问题,所以我们采取namespace的命名空间进行封装,C++ namespace(域)-优快云博客icon-default.png?t=O83Ahttps://blog.youkuaiyun.com/Jason_from_China/article/details/142183304 这里我们顺便展开std域,因为这就是一个很小的模拟实现,所以展开的话,打印和输入的使用还有其他的一些点,我们不需要进入str::cout里面去寻找,直接cout就可以


using namespace std;//突破域名
//这里我们采取namespace封装一下
namespace Test
{
	class string
	{
	public:

	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

string模拟实现构造

方案1(初始化列表的实现):

这一种方案是一种不完整方案,是不合适的方案,我是用来对比讲解使用的,所以可以看,可以不看,这里实现的无参数构造

//.h头文件
using namespace std;//突破域名
//这里我们采取namespace封装一下
namespace Test
{
	class string
	{
	public:
		//构造函数
		string();
        string(const char* str);
	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

初始化列表格式进行初始化:
  1. 其实对于构造函数我们之前学过初始化列表和函数体的两种方式,所以我们到底在实际操作的时候,使用哪一种方式?这里采取初始化列表讲解,分析利弊。
  2. 初始化列表进行初始化,带参数构造的使用会存在一点问题,就是,我们需要先计算出字符串长度,然后才能开辟空间
  3. 初始化列表的构造,是按照私有成员变量的顺序进行初始化的,所以在后期代码维护,你的代码别人不注意就会很容易更改从而导致错误


初始化列表两种方式的代码:
  1. 不带参数

    这里创建是有一点小心机的,这里我们是_str(new char[]({'\0})->数组的形式创建空间,这里其实我们完全可以_str(new char{'\0}->不是数组形式创建空间,但是我们析构的时候,总不能再因为这个写两个析构函数吧,所以我这里直接就是使用创建多个空间的逻辑创建空间,也就是数组的形式创建空间
  2. 带参数


 

方案2(函数体和初始化列表的综合实现):

  1. 上面我们已经发现,纯粹采取初始化列表是可以实现的,但是是存在一些问题的,也就是我们需要改变私有变量的顺序
  2. 初始化列表的构造,是按照私有成员变量的顺序进行初始化的,所以在后期代码维护,你的代码别人不注意就会很容易更改从而导致错误
  3. 所以我们可以采取更加符合常规的一种写法

这里解释一下namespace,命名空间不仅可以单独给,还可以直接大规模的给,这样我们就可以不用在string.cpp实现的文件里面每次实现接口都需要 Test::string::接口

//.h头文件
using namespace std;//突破域名
//这里我们采取namespace封装一下
namespace Test
{
	class string
	{
	public:
		//构造函数
        string(const char* str="");
	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}
//实现文件
#include"string.h"
namespace Test
{
	//构造函数(传参)
	string::string(const char* str)
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
		//   目的地,来源
	}
}

代码的解释:

  1. 在头文件我们可以看见,string(const char* str="");,这里我们是不需要string(const char* str="\0");因为本身创建空间的时候就是会自带/0,没有必要继续加上/0
  2. 在实现上面,我们创建空间需要多创建一个空间,因为我们strlen是不计算\0的,所以我们需要_str = new char[_size + 1];从而在开辟空间的时候,多创建一个空间
  3. 最后我们只需要把字符拷贝到开辟好的空间,最后就可以
  4. 我们的测试我们会在实现析构之后一起进行测试,这里就不单独测试了

注意事项:

  1. 之前我们说过,在vs编译器下,cpp编译下,一些C语言的语法结构是需要写一行代码的,不然会导致报错
  2. #define  _CRT_SECURE_NO_WARNINGS 1//这一行代码,这是编译器的行为,编译器也会提醒你加上,这里我说明一下,不是语法结构的问题,是编译器认为这里有危险,加上这一行强制使用就可以。

string模拟实现析构

析构函数的实现是比较简单的,这里只需要直接析构就可以,因为我们创建空间的时候我们都是采取数组的形式创建的空间,所以我们析构的时候,我们直接数组的形式析构就可以

//头文件
#define  _CRT_SECURE_NO_WARNINGS 1
#pragma once
using namespace std;//突破域名
//这里我们采取namespace封装一下
namespace Test
{
	class string
	{
	public:
		//构造函数
		string(const char* str="");

		//析构函数
		~string();

	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}
//实现文件
#include"string.h"
 
namespace Test
{
	////构造函数(不传参)
	//string::string()
	//	:_str(new char[1] {'\0'})
	//	, _size(0)
	//	, _capacity(0)
	//{}
	// _size指的是实际的个数
	// _capacity指的是空间,空间的使用
	//构造函数(传参)
	string::string(const char* str)
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
		//   目的地,来源
	}
	//析构函数
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}
}

代码讲解:

  1. 首先我们析构_str字符串,这里我们采取析构数组的方式进行析构
  2. 让字符串指向空,C++的空和C语言的空是不一样的
  3. 最后把_size,_capacity,都归0
  4. 比较简单,这里不做过多赘述,这里的核心是提高测试调试的使用能力

代码测试:

  1. 在构造函数里面我们没有测试,因为一方面我们知道我们写的代码很简单不会报错,一方面我们的析构函数没有实现,其实不实现也可以测试,只是作者有点懒,想起来需要测试的时候,已经写到这里了。
  2. 构造函数的测试
  3. 析构函数的测试

注意事项:

  1. 下面我们讲解代码的时候,除非头文件有缺省参数参数,上代码的时候,我会只是实现这个函数接口的实现,但是需要头文件的时候,我会进行讲解这个头文件

string模拟实现【】

这里分为const修饰和不用const修饰,所以我们实现两种:

所以我们实现也得实现两种方式

代码实现:

	//修改字符的实现
	char& string::operator[](size_t i)
	{
		assert(i < _size && i >= 0);
		return _str[i];
	}
	const char& string::operator[](size_t i)const
	{
		assert(i < _size && i >= 0);
		return _str[i];
	}

代码解释:

  1. 首先我们采取一个断言,因为我们改变的数值是在数组规定范围里面,所以我们需要采取一个断言,不能越界
  2. 我们返回数值,返回的数值是可以修改的字符这里是私域成员变量,类似于一个get函数。

代码测试

测试之后,我们发现没有问题

string模拟实现迭代器

迭代器的实现

主要实现的两种迭代器

  1. 这里我们实现迭代器我们主要实现的是begin(),end(),其他都差不多
  2. 注意实现的时候,const和非const是有区别的
    iterator end();
    const_iterator end();//const采取const_iterator的方式来区分非const
//.h文件,放到类里面

//迭代器的实现
//iterator通常是一个类型别名或者一个具体的类类型的名称,它被用来表示具有迭代器功能的对象。在 C++ 标准库中,迭代器是一种用于遍历容器中元素的对象,它提供了一些特定的操作,如解引用、递增、递减、比较等。
//iterator通常是一个类型别名或类类型,用于表示可以遍历特定容器中元素的迭代器对象。它定义了迭代器的行为和操作,如解引用(获取指向的元素)、递增(移动到下一个元素)、比较等。
// 所以在我们还没有学到容器的情况下我们可以直接对iterator进行封装,从而变相实现迭代器


//typedef char* iterator;//iterator的第一种封装方式
using iterator = char*;//iterator第二种封装方式是
using const_iterator =const char*;
iterator begin();
const_iterator begin()const;//这里后面的const也是需要加上的,因为在istream类里面,是加上的,这里不加上会报错
iterator end();
const_iterator end()const;//这里后面的const也是需要加上的,因为在istream类里面,是加上的,这里不加上会报错
//.cpp文件,string的实现文件

//这里是类里面定义的,所以是需要突破类域的
string::iterator string::begin()
{
	return _str;
}
string::iterator string::end()
{
	//end指向的是\0
	return _str + _size;
}
string::const_iterator string::begin()const
{
	return _str;
}
string::const_iterator string::end()const
{
	//end指向的是\0
	return _str + _size;
}

头文件代码解释

  1. 重命名的方式有两种,一种是typedef,一种是using,我们采取using的方式
  2. using iterator = char*;,首先我们把char*类型重命名为iterator
  3. 我们把const char*;,类型重命名为const_iterator 
  4. end采取相同的逻辑

实现文件的解释

  1. 这里实现文件比较好理解,begin(),返回的首元素地址
  2. end(),返回的是最后一个元素的后一个位置的地址
  3. const就是修饰后,只能读不能写的元素,目的是防止产生权限放大和缩小的问题
  4. 也就是,当创建一个const修饰的string的类的时候,如果我们调用迭代器,并且此时没有const类型进行匹配,那么我创建的类是不可修改的,只是可读的,如果我调用迭代器,迭代器是可以修改,可以读的,此时就产生了权限放大的问题
    C++ const成员函数-优快云博客

注意事项:

  1. 实现迭代器之后我们不需要实现语法糖范围for了,因为范围for的底层是迭代器
  2. 这一点了解就可以

 string模拟实现reserve

这里实现的是扩容

扩容这里是可以实现缩容,可以实现扩容,这里主要实现的就是扩容的实现,这里实现缩容的实现

//扩容(reserve扩容是不更新_size的,因为你只是扩容,_size==_capacity)
void string::reserve(size_t n)
{
	assert(n >= 0);
	//扩容
	//扩容这里是需要拷贝空间的
	//不能直接new加空间,new存在的意义是开辟空间,不能像realloc一样扩容,但是realloc底层其实也是malloc,然后和这个逻辑一样
	if (n > _size)
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

代码解释

  1. 首先我们断言一下,因为size_t是无符号整数类型,所以肯定的大于等于0
  2. 这里我们判断是不是需要扩容,如果是需要扩容我们继续进行逻辑的实现
  3. 扩容的时候我们是需要创建一个新的空间的,然后析构旧空间,让_str指向新的空间
  4. 更新_capacity,注意这里是_size不做更新的,因为这里只是扩容,不是输入什么字符

string模拟实现尾插push_back

尾插的实现是很简单的:

	//尾插的实现
	void string::push_back(char ch)
	{
		if (_capacity == _size)
		{
			//这里是不能使用_size的,_size是空间里面包含的个数
			//_size == 0 ? 4 : _capacity * 2;
			//reserve(_size);
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;

		_size++;
		_str[_size] = '\0';
	}

代码解释:

  1. (_capacity == _size)首先判断是不是需要扩容
  2. reserve(_capacity == 0 ? 4 : _capacity * 2);如果空间是0的情况下,我们需要给初始空间
  3. 插入数值,更新_size

注意事项:

  1. 这里我们可以看到:_str[_size] = '\0';,我们在尾部插入字符0,这里是很关键的一步骤,因为如果你的优化开的比较大,那么有的编译器会直接自己给你加上字符\0,但是按照实际书写来说的话,其实这里是需要我们自己加上的
  2. 如果我们不加上字符\0,就会导致打印的时候把后面没有初始化的空间打印出来

  3. 我们加上之后,就不会产生这样的问题

  4. 1,原因解释,因为我们在尾插的时候,首先字符\0就是占据一个空间的,但是这个空间是不计入个数的。
    2,其次,这个空间就在字符计数的下一个,所以我们尾插,包括append的实现,都是会直接把这个\0的位置给直接替换,所以需要追加字符\0。
    3,除非我们再实现一个向后移动,但是没有必要。
    4,或者我们实现运算符重载+=,我们利用+=来实现,但是我还是觉得没有必要,因为的+=我们是复用append,而且是直接string这个类来接受,如果再实现一个字符串的+=会导致代码的冗余,所以此时是最优解
    5,这里我们可以看见,这里我们的+=是直接返回整个类的,如果只是改变字符串不改变整个类,那么是没有必要的

string模拟实现append

  1. append我们主要实现的是插入字符和字符串,这两个核心功能,并且也都是实现尾插
  2. 对于指定位置插入字符串,我们会在insert这个接口实现,
  3. 我们的目的是在实现的过程里面更加区分不同接口的作用
	//随机插入的实现,插入字符,插入字符串
	void string::append(char ch)
	{
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}		
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';//这里relase会进行优化,但是debug不会进行优化,所以是需要加上字符\0的
	}
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (len + _size > _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
			if (len + _size > _capacity)
				reserve(len + _size);
		}
		//参数
		//目的地
		//指向要复制内容的目标数组的指针。
		//来源
		//要复制的 C 字符串
		strcpy(_str + _size, str);
		_size += len;
	}

字符插入的代码解释:

  1. 这里字符的插入的实现逻辑和尾插的实现逻辑差不多,所以不做过多赘述

字符串插入的代码解释:

  1. 首先我们需要判断,插入的字符串的长度和现有_size的长度,会不会超过存储空间,超过空间了,我们一般是采取二倍扩容,如果二倍扩容还是不够的情况下,此时我们需要再次扩容
  2. strcpy
  3. 我们实现扩容之后,我们只需要了解strcmp的特性就可以,我们直接把需要插入的字符串拷贝到_str里面可以,这里有一点就是,我们是从\0开始拷贝的,我们把\0给覆盖了。因为拷贝过来的字符串是包含\0的

string模拟实现+=

这里其实就是复用append,比较简单,直接上代码

	//尾插的实现
	string& string::operator+=(char ch)
	{
		append(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

注意事项:

  1. 我们只需要返回的时候返回这个对象,也就是*this,因为+=是对象本身是需要发生改变的

string模拟实现insert

这里是有一点难度的,难度不大,主要是边界问题的处理,这里的图解会涉及的多一点

插入字符:

	//插入+添加字符串
	void string::insert(size_t pos, char ch)
	{
		//这里需要断言一下,无符号整形如果传递是负值,就会导致传递一个非常大的数值
		//但是我们不需要断言插入的数值是否小于_size,因为当大于_size的时候,会把空格当做字符,进行移动,当然前提是\0在空间结束之前,调试的时候可以看出来
		assert(pos >= 0);
		//判断需不需要扩容
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//移动的两种方式
		//1,end=_size,往后移动,进行插入->弊端,会产生越界的行为,我们需要进行修正
		//2,end=_size+1,往后移动,进行插入
		int end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			end--;
		}
		_str[pos] = ch;
		_size++;
	}

代码解释

  1. 首先我们进行断言,这里断言直接大于0就可以,如果按照下图\0是会往后移动的
  2. 我们判断插入此时空间是不是满了,满了是需要扩容的

核心代码讲解(不推荐的方式)

  1. 第一种方式:
    1,如果我希望在pos==0这个位置插入一个字符2,那么此时我就需要把所有的数值往后移动,那么我们就涉及到一点,我们可以指向已知的最后一个字符,设置为end
    2,但是这样是存在弊端的,我们看代码是可以发现的,我们的循环条件是end>=pos
    我们的条件不能是end>pos,当pos==0的时候,这样就会导致end在pos==1的位置停下来
    3,当我们end>=pos,当end==0的时候,依旧会继续循环,然后end---,最后越界,最后我们才能在pos的位置进行插入
    4,但是需要清楚一点的是,pos是size_t类型的,是无符号整形,所以我们需要转换为int类型,从而进行对比
    5,所以这一种方式是不推荐的
  2. 第二种方式(比较推荐的方式):
    这里实现的关键是要把插入的字符计入到总的空间里面,此时不会产生越界的情况
    此时我们的循环条件只是end>pos
    当等于pos的时候,就会停止循环


 

插入字符串:

这里我们直接上代码,并且实现方式我们依旧是采取第二种实现方式进行实现

	void string::insert(size_t pos, const char* str)
	{
		assert(pos >= 0);
		//判断是不是需要扩容
		int len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
			if (_size + len > _capacity)
				reserve(_size + len);
		}
		//留出来插入字符的空间
		int end = _size + len;
		//end>
		while (end > pos + len - 1)//这里是需要等于的,因为需要把数值赋值给_str[end] = _str[end - len];
		{
			_str[end] = _str[end - len];
			end--;
		}
		//进行插入
		for (size_t i = 0; i <len; i++)
		{
			_str[pos + i] = str[i];
		}
		//更新长度
		_size += len;
	}

代码解释:

  1. 我们依旧是需要判断是不是需要扩容

核心代码讲解:

  1. 留出充足的移动的空间
  2. 进行移动
    移动的时候我们是不能直接移动到pos这个位置的,这样会导致越界的行为
  3. 进行插入

string模拟实现earse

earse的实现,我们主要是实现指定位置删除指定长度

不传递参数会有缺省参数

//头文件	
	//删除字符+删除字符串
		void earse(size_t pos, size_t len = npos);


	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str;
		size_t _size;
		size_t _capacity;
		//C++静态成员变量,规定静态成员变量必须是类里面声明,类外面定义,但是C++还规定,int类型是可以类里面声明,类里面定义的
		static const int npos = -1;
//实现文件

//删除字符+删除字符串
//这里是pos指的是下标
void string::earse(size_t pos, size_t len)
{
	assert(pos >= 0);
	//这种情况下,就是从pos位置开始往后全部删除
	// || len == npos,这里不需要再这样,因为这里是无符号整形,也就是我们传递是npos==-1,但是我们接受的是一个很大的数值,所以已经确定了是直接全部删除的
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		size_t end = pos + len;
		while (end <= _size)
		{
			_str[end - len] = _str[end];
			end++;
		}
		_size -= len;
	}
}

注意事项:

  1. 首先我们看npos,因为npos是一个默认的缺省参数,因为npos在很多地方都会用到,所以我们给到一个静态成员变量
  2. 我们给npos是一个-1的数值,因为npos是一个无符号整形,-1就会直接给到一个最大值。
  3. 关于静态成员变量,类里面定义,类外面初始化的问题。这里刚好有一个点就是,C++给整形,也就是int类型开了一个后门,就是只有int类型可以类里面定义,类里面初始化

代码解释:

  1. 首先我们得知道我们删除的字符的长度是多少,如果pos所在位置到尾部最后一个位置的字符只有三个,你需要删除的有四个,其实就没有必要了,直接在pos这里插入字符\0就可以,并且更新_size
  2. 如果不是这样的情况,也就是从中间删除一段字符,此时我们只需要把后面的字符移动到中间那一段字符就可以,进行覆盖最后在后面插入字符串
    移动的时候我们是需要移动到_str[_size]这个位置的,这个位置是\0,所以最后我们是不需要插入字符\0的
  3. 最后更新_szie

string模拟实现find

查找分为查找字符和查找字符

查找字符的实现:

	//find查找字符+find查找字符串(strstr暴力查找)
	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size - pos; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos;
	}

代码解释:

  1. 这里查找字符我们采取的是暴力查找,找到了,直接返回找到的下标,没找到直接返回npos,也就是一个很大是数值


 

查找字符串的实现:

查找字符串我们可以使用一个库函数来实现,暴力实现的讲解有点复杂,所以我们直接使用strstr的函数实现

size_t string::find(const char* str, size_t pos)
{
	assert(pos >= 0 && pos <= _size);
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		//正确计算位置索引:ptr是在_str + pos所指向的内存区域中找到子字符串str时的指针。通过ptr - _str,可以计算出子字符串在整个_str字符串中的起始索引位置。
		//将指针ptr强制转换为size_t类型并返回,并没有实现find函数预期的功能。find函数的目的是返回子字符串在原字符串中的位置索引(一个基于 0 开始的偏移量),而不是指针值的数值表示。
		
		//简单的说就是,这个ptr是指针类型,但是我们这个函数的接口是size_t类型
		//所以我们需要转化为size_t类型的,但是还不能直接强制类型转化
		//因为指针强制类型转换计算偏移量是从0开始是,所以就会导致报一个很大的数值
		//我们需要计算偏移量,那么我们知道的偏移量就是_str首元素的地址也就是0
		//此时任何数值-0都等于那个数值本身,所以我们计算出结果。
		return ptr-_str;
	}
}

代码解释:

  1. 这里的关键在于,strstr的数值的接收,这里返回的是一个指针,但是我们的返回值一个是一个size_t类型的
  2. 此时就会产生无法返回的情况,那么我们就有两种解决办法,要么是强制类型转换,要么是减去相对地址
  3. 但是强制类型转化的行不通的,因为我们强制类型转换的时候,我们拿到的是绝对地址,但是实际上我们需要的是相对地址
  4. 所以我们需要ptr-_str这个是ptr-0,也就是得出相对地址位置

string模拟实现拷贝构造operator=

拷贝构造的实现

  1. 这是需要实现拷贝构造的
  2. 什么时候需要实现拷贝构造:凡是存在资源的情况下,都是需要实现拷贝构造的,简单的可以理解为只要是需要手动实现析构函数的,都是需要实现拷贝构造的
  3. 这里本来是需要先实现substr取出字符串
  4. 但是,substr取出字符串是需要实现深拷贝的,如果我们不实现拷贝构造那么就会导致只是实现浅拷贝,资源是没有办法拷贝成功的,所以我们是需要实现拷贝构造的
	//拷贝构造
	string& string::operator=(const string& s)
	{
		//这里是不能自己给自己拷贝构造的
		//因为我们首先就是进行函数的析构,这样就会导致我们无法找到数值
		if (this != &s)
		{
			delete[] _str;
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
			return *this;
		}
	}

代码解释:

  1. 拷贝构造我们实现的是把传递过来的对象拷贝到this指向的对象里面,注意这里是对象,不是字符串
  2. 首先销毁原来的空间,然后创建一个和当前传递过来对象一样大小的空间
  3. 把字符串拷贝到里面,然后同步_size,_capacity
  4. 最后返回这一个this指向的对象

注意事项:

  1. 这里我们可以看出来我们是有实现条件的,因为我们不能自己给赋值
  2. 原因:delete[] _str;首先我们就是销毁对象
  3. 我们销毁对象之后我们是没办法继续进行赋值的,所以我们是不能给自己赋值
  4. 除非我们改变代码逻辑,但是没什么必要,还会变得麻烦

string模拟实现substr

取出字符串,这里还是比较简单的,但是这里是有问题的,我们看得出来在C++文档里面取出字符串实际上是取出整个对象,不是取出的只是字符串,所以是需要实现拷贝构造的

//取出字符串实现
string string::substr(size_t pos, size_t len)
{
	//取出字符串的问题设计深浅拷贝的问题,这里我们需要优先实现拷贝构造函数
	//关于拷贝构造函数的实现,我们需要注意的是,拷贝构造实现的是深拷贝、
	//因为我们创建的空间赋值后指向的是同一个空间,所以我们需要实现拷贝构造
	//因为存在资源的话,就需要我们手动实现拷贝构造。
	//否则就会指向一个空间,从而导致析构析构两次
	assert(pos >= 0);

	////首先我们创建一个空间,然后对空间进行扩容
	//Test::string tmp;
	//tmp.reserve(len);
	//这里可以不用优先扩容,我们可以优先计算出来空间需要开辟的大小

	if (len >= _size - pos)
	{
		len = _size - pos;
	}
	//首先我们创建一个空间,然后对空间进行扩容
	Test::string tmp;
	tmp.reserve(len);
	for (size_t i = 0; i < len; i++)
	{
		//tmp[i] = _str[pos + i];//这里插入会依旧导致需要继续添加\0,所以我们采取+=的复用,更加方便一点
		tmp+= _str[pos + i];
	}
	tmp._size = len;

	//此时完成了资源的拷贝也就是深拷贝,那么此时返回就不会报错
	//注意完成浅拷贝的时候,返回报错不是因为空间销毁,而是因为析构函数析构两次,所以才会导致报错
	return tmp;
}
	////取出字符串的测试
	Test::string s7(" hello word hello word hello word hello word ");
	Test::string ret1 = s7.substr(0, 10);
	cout << "substr查找字符的测试:" << ret1.c_str() << endl;

代码解释:

  1. 断言
  2. 我们需要判断取出的字符串的实际长度是多少
  3. 创建一个空间,扩容到相应的长度
  4. tmp+= _str[pos + i];,这里我们把从pos位置开始的字符串追加到tmp里面
  5. 更新字符串的长度

注意事项:

  1. 这里可能会有小伙伴说了,这里也没有遇见需要实现拷贝构造的情况啊,这个时候我们查看测试用例
  2. 首先我们的函数实现返回参数返回的是一个临时对象,这个临时对象是需要接收值来延长生命周期的Test::string ret1 = s7.substr(0, 10);,所以这个时候拷贝构造就使用上了
  3. 如果不实现拷贝构造,对象的生命周期没有延长,就会导致对象出去函数就会直接被销毁,因为我们是在函数里面创建的对象

string模拟实现clear

模拟实现clear的目的是在流提取的时候我们清空之前的数据,然后重新输入使用的,不然就会导致

	//clear的实现,和流提取综合使用实现
	void string::clear()
	{
		_str[0] = '\0';
		_size = 0;
	}

代码解释:

  1. 直接在下标为0的位置插入字符\0
  2. 更新_size

string模拟实现比较大小

模拟实现比较我们主要依赖的是strcmp函数

	//比较大小
    //比较大小不需要重载为成员函数
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}
	bool operator>(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) > 0;
	}
	bool operator<(const string& s1, const string& s2)
	{
		return !(s1 > s2 && s1 == s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return s1 == s2 || s1 > s2;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 == s2 || s1 < s2;
	}

注意事项:

  1. 这里的代码没有什么好解释的,主要就是实现大于和等于,其他的都是复用
  2. 复用的好处就在于如果我们要改变所有的代码,我们只需要改变核心的大于和等于就可以
  3. 这里返回的是bool值

string模拟实现流插入(输出)

流插入和流提取我们直接重载为友元函数,在日期类的流插入和流提取里面什么表述的比较清楚,不明白为什么需要重载为友元函数的,可以看一下

日期类的实现(C++)_c++如何用whatday-优快云博客

这里我们实现的目的就是可以直接打印出类

C++文档里面的参数

	//重载为友元函数
	//流插入(输出)
	ostream& operator<<(ostream& os, const string& str)
	{
		cout << "_str:" << str.c_str() << "/_size:" << str._size << "/_capacity:" << str._capacity << endl;

		//cout << "当前字符串:" << str.c_str() << endl;
		return os;
	}

注意事项:

  1. 这里是参数的接收

ostream&是引用:

  1. 避免不必要的复制:当函数的参数是对象时,如果不使用引用,在函数调用时会创建对象的副本。对于像ostream这样的复杂流对象,复制它们的代价是很高的。ostream类可能包含大量的内部状态信息,如缓冲区、格式标志等,复制这些信息会消耗大量的时间和内存。通过使用引用,函数直接操作原始的ostream对象,避免了这种不必要的复制开销。
  2. 保持对象的一致性:如果ostream对象被复制,那么在函数中对副本所做的任何操作(如设置格式标志、写入数据到缓冲区等)都不会影响到原始的ostream对象。而使用引用可以确保函数对ostream对象的操作反映在调用该函数的原始环境中。例如,在operator<<函数中对输出格式的修改(如设置精度、宽度等)会在后续使用该ostream对象的代码中生效。

ostream& os,不使用const引用:

 
  • 需要修改流状态ostream对象在输出过程中可能会改变其内部状态,比如更新缓冲区指针、设置错误标志等。如果将参数声明为const引用,就意味着在函数内部不能对ostream对象进行任何修改,但实际上在输出操作中是需要修改这些状态信息的。例如,当向流中写入数据时,如果缓冲区已满,ostream对象可能会自动刷新缓冲区,这涉及到对ostream内部状态的修改,与const的语义相冲突。因此,不能将ostream&参数声明为const引用。

string模拟实现流提取(输入)

	//流提取(输入),提取不能使用cin和scanf进行封装,需要get或者getline
	istream& operator>>(istream& is, string& str)
	{
		str.clear();
		cout << "请输入字符串创建类:";
		//字符串输入不能使用cin,cin遇见空格和换行会导致直接停止读取,所以我们采取get()
		char ch;
		ch = is.get();//读取
		while (ch != '\n')
		{
			str += ch;//追加字符
			ch = is.get();//继续读取
		}
		return is;
	}

注意事项:

  1. 我们读取不能使用cin,因为cin读取的时候会遇见空格和换行都会停止读取,我们是需要读取空格的,所以是不能使用cin,所以我们需要到iostream库里面找找看看什么合适
  2. 我们需要创建一个字符,然后循环读取,这里的判断条件是遇见换行就会跳出循环,我们也可以自己设置判断条件,比如可以设置为遇见///

精讲合集:

string模拟实现的拓展讲解

swap的拓展讲解

在C++库里面,是有一个swap的,在string里面是有两个swap的,所以我们就会很纳闷,为什么会出现三个交换,接下来我们会进行讲解

算法库里面的swap

  1. 算法库里面的swap是可以完成交换的
  2. 弊端就是,代价太大
  3. 原因,算法库里面的swap是一个模版,泛函数编程,如果实现深拷贝就需要创建三次空间,销毁三次空间,所以这里虽然使用起来很爽,但是会使用代价很大

string类里面的swap

  1. 这里就需要我们了解到,我们是否真的需要交换的时候必须创建空间。显然不是的,其实只需要交换字符串就可以实现,而不是反复的创建空间,销毁空间
  2. 交换
  3. 所以字符串可以交换,那么我们完全可以调用C++库里面的交换函数,实现交换
    注意:这里是swap这里一定要加上std::swap,这里我们调用的是C++库里面的函数,不然会调用string库里面的函数
	//类里面的交换
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

string类里面的全局swap

  1. 这里我们重载为友元函数
  2. 有人就疑问了,既然string里面已经有swap了,为什么还需要一个string的全局swap函数
    1,首先我们可能会使用swap函数直接交换string
    2,其次对于函数模版来说,写一个重载好的全局友元函数这个就像点外卖,使用函数模版就像去做饭,就像你忙的时候,有一个已经点好的外卖和需要自己做饭你选择哪一个,肯定的直接吃外卖。效果一样,又省时间,点外卖就是类似于写一个全局的函数。
    3,所以我们为了使用时候不自己使用函数模版,往往是自己实现一个全局swap函数,并且重载为友元函数
  3. 这里我们实现代码的时候可以直接使用复用,从而实现s1,s2的交换
	//string全局的交换
	void swap(string& s1, string& s2)
	{
		s1.swap(s2);
	}

拷贝构造的拓展讲解

实现拷贝构造是有点麻烦的,所以我们还有一种拷贝构造的写法,那就是可以直接实现构造再拷贝,这里是基于swap的逻辑进一步实现的

代码

	//拷贝构造的实现,新版本的实现
	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}

代码解释:

  1. 拷贝构造也是属于构造的一种
    1. 首先我们传参过来的是引用并且是利用const修施过的,首先保证了不会被更改
    2. 我们使用构造函数,构造一个传参过来的string里面的字符
    3. 然后因为创建的对象是临时对象,所以出去作用域会进行销毁,这里我们不需要担心销毁的问题,所以我们可以放心进行交换
    4. 我们交换的时候是this和tmp进行交换,调用的是刚才我们写的函数

赋值的拓展讲解

赋值的实现我们可以基于swap和拷贝构造进行实现,但是和拷贝构造还有点不一样,可以进一步进行简化

	//赋值,新版本的实现
	//string& string::operator=(const string& s)
	//{
	//	string tmp(s._str);
	//	swap(tmp);
	//	return *this;
	//}
	
	//这个是使用是需要在初始化的时候去_str=nullptr;不然交换之后会导致s指向的是空的空间,会导致析构失败
	string& string::operator=(string s)
	{
		swap(s);
		return *this;
	}

代码的讲解

  1. 如果我们实现第一种方式还好说,在私有成员变量可以不进行赋值,但是第二个我们必须在初始化成员变量的时候进行赋值
  2. 首先我们在拷贝构造的另类实现方式里面使用的是引用传参,这里我们直接使用传值传参
  3. 传值传参会调用拷贝构造,也就是    string& string::operator=(string s)在传递过来的时候,(string s)会调用拷贝构造,那么此时拷贝构造会再拷贝出来一个临时对象作为等一下交换的对象
  4. 此时问题出现了,这个拷贝构造出现的对象指向的空间是没有的,是不指向空间的,和swap交换空间地址之后,此时(string s)创建的又是一个临时对象,那么出去作用域会进行销毁,所以就会导致销毁的空间是一个指向什么都没有的空间,是不指向地址的
  5. 解决办法,我们需要对私有变量进行初始化,不然在这里会报错

ASCII码值的讲解

前言

  1.  ASCII码值的讲解其实也没什么深入讲解的,但是在string篇章,不提一句也是少点什么东西,所以还是需要简单介绍一下

什么是ASCII码值

  1. 利用编码存储一些文字符号
  2. 举例
    从这里可以清楚的看出来,每一个字符其实本质上存储的都是编码
  3. 这个字符可以是英文,可以是中文但是我们也知道,这个计算机是美国人先搞出来的,加上英文字符表示的比较简单,所以往往开始的0-127的就被英文字母和美国那边的字符占据了

编码有几种

  1. 很多种,但是我们常用的往往是UTF-8,不过UTF-8不是最先出来的

时代的发展

  1. 随着时代的发展为了让计算机走向国际,出现了万国码,也就是表示各国语言字符的
  2. 这个时候就不能使用ASCII码值进行表示,因为英文的表示比较简单,算上大小写字母数字和字符可能也就一百多个,但是来到中亚这边,包括中国周边的国家,他们受中国的影响比较深,所以这些国家的文字他们国家的语言啊也不是单单的一百多个能够表示的。
    尤其是中国,汉字更是多

  3. 所以此时就逐步出现了什么UTF-16 UTF-16​​​​​32,UTF-8

UTF-16(首先出现的)

  1. 最先出现的是UTF-16,它的特点就是一个字符表示两个字节
  2. 但是UTF-16的弊端就是它的表述方式和组合方式很少,如果以中国举例的话UTF-16并不能把中国的汉字完全的映射出来

UTF-32(UTF-16之后出现的)

  1. UTF-32的出现缓解了UTF-16的尴尬,但是也不是经常使用,因为它是一个字符对应4个字节,这样就会导致比较浪费空间

UTF-8(使用最多的)

  1. 这里使用是最多的是因为它首先兼容ASCII码值,其次它是可变长编码,也就是两个字节不行的情况下我们可以三个字节甚至四个字节甚至更多
  2. 混编的情况下如果是0开头那就是ASCII码值

string里面关于ASCII码值的讲解

  1. string里面关于ASCII码值
  2. 这里是一个编码两个字符

  3. 一个编码四个字符

  4. 这里是一个编码两个字符

  5. Utf-8编码的作用和兼容ASCII码值
    这里我们加加和减减的时候我们会发现字符会发生变化
    这里的变化加加和减减是从第二个字符开始的,学到vector会知道什么意思,这里主要讲解的是字符的变化并且都是同音字,方便大规模删除

代码合集

//头文件(string.h)

#define  _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<cstring>
#include<assert.h>
#include<string>
using namespace std;//突破域名
//这里我们采取namespace封装一下
namespace Test
{
	class string
	{
		//重载为友元函数
		friend void swap(string& s1, string& s2);
		//流插入(输出)
		friend ostream& operator<<(ostream& os, const string& str);
		//流提取(输入),提取不能使用cin和scanf进行封装,需要get或者getline
		friend istream& operator>>(istream& is, string& str);
		//getline的实现
		friend istream& getline(istream& is, string& str, char delim);
	public:
		//构造函数
		string(const char* str="");

		//析构函数
		~string();

		//修改字符的实现,分为const修饰和不修饰
		char& operator[](size_t i);
		const char& operator[](size_t i)const;

		//迭代器的实现
		//iterator通常是一个类型别名或者一个具体的类类型的名称,它被用来表示具有迭代器功能的对象。在 C++ 标准库中,迭代器是一种用于遍历容器中元素的对象,它提供了一些特定的操作,如解引用、递增、递减、比较等。
		//iterator通常是一个类型别名或类类型,用于表示可以遍历特定容器中元素的迭代器对象。它定义了迭代器的行为和操作,如解引用(获取指向的元素)、递增(移动到下一个元素)、比较等。
		// 所以在我们还没有学到容器的情况下我们可以直接对iterator进行封装,从而变相实现迭代器
		//typedef char* iterator;//iterator的第一种封装方式
		using iterator = char*;//iterator第二种封装方式是
		using const_iterator =const char*;
		iterator begin();
		const_iterator begin()const;//这里后面的const也是需要加上的,因为在istream类里面,是加上的,这里不加上会报错
		iterator end();
		const_iterator end()const;//这里后面的const也是需要加上的,因为在istream类里面,是加上的,这里不加上会报错

		//扩容(reserve扩容是不更新_size的)
		void reserve(size_t n);

		//尾插的实现
		void push_back(char ch);

		//随机插入的实现,插入字符,插入字符串,这里我们只实现尾插
		void append(char ch);
		void append(const char* str);

		//尾插的实现
		string& operator+=(char ch);
		string& operator+=(const char* str);

		//插入+添加字符串
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);

		//删除字符+删除字符串
		void earse(size_t pos, size_t len = npos);

		//find查找字符+find查找字符串(strstr暴力查找)
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);

		//取出字符串实现
		string substr(size_t pos, size_t len = npos);

		//拷贝构造的实现,老版本的实现
		//string(const string& s);
		//拷贝构造的实现,新版本的实现
		string(const string& s);

		//赋值,老版本的实现
		//string& operator=(const string& s);
		//赋值,新版本的实现
		string& operator=(string s);


		//clear的实现,和流提取综合使用实现
		void clear();

		//支持返回C语言类型的字符串,w我们测试使用
		//这里尽量用const进行修饰
		//这里做出解释,前面的const修饰的是返回值,也就是修饰的是内容
		//后面修饰的是隐藏的this指针,确保不会因为权限放大问题导致报错
		const char* c_str()const
		{
			return _str;
		}

		//交换的实现
		void swap(string& s);
	private:
		//这里本质上就是字符串的增删查改,所以和数据结构是有点像的
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
		//C++静态成员变量,规定静态成员变量必须是类里面声明,类外面定义,但是C++还规定,int类型是可以类里面声明,类里面定义的
		static const int npos = -1;
	};

	//比较大小
	//比较大小不需要重载为成员函数
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator<(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);

	//重载为友元函数
    //流插入(输出)
	ostream& operator<<(ostream& os, const string& str);
	//流提取(输入),提取不能使用cin和scanf进行封装,需要get或者getline
	istream& operator>>(istream& is, string& str);
	//getline的实现
	istream& getline(istream& is, string& str, char delim = '\n');
	void swap(string& s1, string& s2);
}

//实现文件(string.cpp)

#include"string.h"
 
namespace Test
{
	////构造函数(不传参)
	//string::string()
	//	:_str(new char[1] {'\0'})
	//	, _size(0)
	//	, _capacity(0)
	//{}
	// _size指的是实际的个数
	// _capacity指的是空间,空间的使用
	//构造函数(传参)
	string::string(const char* str)
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1];//这里多开一个空间本身就是留给\0的
		strcpy(_str, str);
		//   目的地,来源
	}
	//析构函数
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}
	//修改字符的实现
	char& string::operator[](size_t i)
	{
		assert(i < _size && i >= 0);
		return _str[i];
	}
	const char& string::operator[](size_t i)const
	{
		assert(i < _size && i >= 0);
		return _str[i];
	}
	//这里是类里面定义的,所以是需要突破类域的
	string::iterator string::begin()
	{
		return _str;
	}
	string::iterator string::end()
	{
		//end指向的是\0
		return _str + _size;
	}
	string::const_iterator string::begin()const
	{
		return _str;
	}
	string::const_iterator string::end()const
	{
		//end指向的是\0
		return _str + _size;
	}
	//扩容(reserve扩容是不更新_size的,因为你只是扩容,_size==_capacity)
	void string::reserve(size_t n)
	{
		assert(n >= 0);
		//扩容
		//扩容这里是需要拷贝空间的
		//不能直接new加空间,new存在的意义是开辟空间,不能像realloc一样扩容,但是realloc底层其实也是malloc,然后和这个逻辑一样
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];//这里多开一个空间本身就是留给\0的
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	//尾插的实现
	void string::push_back(char ch)
	{
		if (_capacity == _size)
		{
			//这里是不能使用_size的,_size是空间里面包含的个数
			//_size == 0 ? 4 : _capacity * 2;
			//reserve(_size);
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;

		_size++;
		_str[_size] = '\0';


	}
	//随机插入的实现,插入字符,插入字符串
	void string::append(char ch)
	{
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';//这里relase会进行优化,但是debug不会进行优化,所以是需要加上字符\0的
	}
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (len + _size > _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
			if (len + _size > _capacity)
				reserve(len + _size);
		}
		//参数
		//目的地
		//指向要复制内容的目标数组的指针。
		//来源
		//要复制的 C 字符串
		strcpy(_str + _size, str);
		_size += len;
	}
	//尾插的实现
	string& string::operator+=(char ch)
	{
		append(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	//插入+添加字符串
	void string::insert(size_t pos, char ch)
	{
		//这里需要断言一下,无符号整形如果传递是负值,就会导致传递一个非常大的数值
		//但是我们不需要断言插入的数值是否小于_size,因为当大于_size的时候,会把空格当做字符,进行移动,当然前提是\0在空间结束之前,调试的时候可以看出来
		assert(pos >= 0);
		//判断需不需要扩容
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//移动的两种方式
		//1,end=_size,往后移动,进行插入->弊端,会产生越界的行为,我们需要进行修正
		//2,end=_size+1,往后移动,进行插入
		int 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 >= 0);
		//判断是不是需要扩容
		int len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
			if (_size + len > _capacity)
				reserve(_size + len);
		}
		//留出来插入字符的空间
		int end = _size + len;
		//end>
		while (end > pos + len - 1)//这里是需要等于的,因为需要把数值赋值给_str[end] = _str[end - len];
		{
			_str[end] = _str[end - len];
			end--;
		}
		//进行插入
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = str[i];
		}
		//更新长度
		_size += len;
	}
	//删除字符+删除字符串
	//这里是pos指的是下标
	void string::earse(size_t pos, size_t len)
	{
		assert(pos >= 0);
		//这种情况下,就是从pos位置开始往后全部删除
		// || len == npos,这里不需要再这样,因为这里是无符号整形,也就是我们传递是npos==-1,但是我们接受的是一个很大的数值,所以已经确定了是直接全部删除的
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			size_t end = pos + len;
			while (end <= _size)
			{
				_str[end - len] = _str[end];
				end++;
			}
			_size -= len;
		}
	}
	//find查找字符+find查找字符串(strstr暴力查找)
	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size - pos; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos;
	}
	size_t string::find(const char* str, size_t pos)
	{
		assert(pos >= 0 && pos <= _size);
		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			//正确计算位置索引:ptr是在_str + pos所指向的内存区域中找到子字符串str时的指针。通过ptr - _str,可以计算出子字符串在整个_str字符串中的起始索引位置。
			//将指针ptr强制转换为size_t类型并返回,并没有实现find函数预期的功能。find函数的目的是返回子字符串在原字符串中的位置索引(一个基于 0 开始的偏移量),而不是指针值的数值表示。

			//简单的说就是,这个ptr是指针类型,但是我们这个函数的接口是size_t类型
			//所以我们需要转化为size_t类型的,但是还不能直接强制类型转化
			//因为指针强制类型转换计算偏移量是从0开始是,所以就会导致报一个很大的数值
			//我们需要计算偏移量,那么我们知道的偏移量就是_str首元素的地址也就是0
			//此时任何数值-0都等于那个数值本身,所以我们计算出结果。
			return ptr - _str;
		}
	}
	//取出字符串实现
	string string::substr(size_t pos, size_t len)
	{
		//取出字符串的问题设计深浅拷贝的问题,这里我们需要优先实现拷贝构造函数
		//关于拷贝构造函数的实现,我们需要注意的是,拷贝构造实现的是深拷贝、
		//因为我们创建的空间赋值后指向的是同一个空间,所以我们需要实现拷贝构造
		//因为存在资源的话,就需要我们手动实现拷贝构造。
		//否则就会指向一个空间,从而导致析构析构两次
		assert(pos >= 0);

		////首先我们创建一个空间,然后对空间进行扩容
		//Test::string tmp;
		//tmp.reserve(len);
		//这里可以不用优先扩容,我们可以优先计算出来空间需要开辟的大小

		if (len >= _size - pos)
		{
			len = _size - pos;
		}
		//首先我们创建一个空间,然后对空间进行扩容
		Test::string tmp;
		tmp.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			//tmp[i] = _str[pos + i];//这里插入会依旧导致需要继续添加\0,所以我们采取+=的复用
			tmp += _str[pos + i];
		}
		tmp._size = len;

		//此时完成了资源的拷贝也就是深拷贝,那么此时返回就不会报错
		//注意完成浅拷贝的时候,返回报错不是因为空间销毁,而是因为析构函数析构两次,所以才会导致报错
		return tmp;
	}

	//clear的实现,和流提取综合使用实现
	void string::clear()
	{
		_str[0] = '\0';
		_size = 0;
	}
	//比较大小
	//比较大小不需要重载为成员函数
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}
	bool operator>(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) > 0;
	}
	bool operator<(const string& s1, const string& s2)
	{
		return !(s1 > s2 && s1 == s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return s1 == s2 || s1 > s2;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 == s2 || s1 < s2;
	}
	//重载为友元函数
	//流插入(输出)
	ostream& operator<<(ostream& os, const string& str)
	{
		cout << "_str:" << str.c_str() << "/_size:" << str._size << "/_capacity:" << str._capacity << endl;

		//cout << "当前字符串:" << str.c_str() << endl;
		return os;
	}
	//流提取(输入),提取不能使用cin和scanf进行封装,需要get或者getline
	istream& operator>>(istream& is, string& str)
	{
		str.clear();
		//创建一个数组,输入的时候减少扩容
		char buff[256];
		int i = 0;
		cout << "请输入字符串创建类:";
		//字符串输入不能使用cin,cin遇见空格和换行会导致直接停止读取,所以我们采取get()
		char ch;
		ch = is.get();//读取
		while (ch != '\n')
		{
			buff[i++] = ch;
			if (i == 255)
			{
				buff[i] = '\0';
				str += buff;//追加字符
				i = 0;
			}

			//继续读取
			ch = is.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			str += buff;//追加字符
		}
		return is;
	}
	//getline的实现
	istream& getline(istream& is, string& str, char delim)
	{
		str.clear();
		//创建一个数组,输入的时候减少扩容
		char buff[256];
		int i = 0;
		cout << "请输入字符串创建类:";
		//字符串输入不能使用cin,cin遇见空格和换行会导致直接停止读取,所以我们采取get()
		char ch;
		ch = is.get();//读取
		while (ch != delim)
		{
			buff[i++] = ch;
			//我们开辟256个空间 这里需要在size==255的时候,加上\0,当加满了,直接进行追加,减少扩容次数
			if (i == 255)
			{
				buff[i] = '\0';
				str += buff;//追加字符
				i = 0;
			}

			//继续读取
			ch = is.get();
		}
		//这里是一个收尾,如果追加之后的字符串
		if (i > 0)
		{
			buff[i] = '\0'; 
			str += buff;//追加字符
		}
		return is;
	}
	//类里面的交换
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}
	//string全局的交换
	void swap(string& s1, string& s2)
	{
		s1.swap(s2);
	}
#if 0
	//拷贝构造的实现,老版本的实现
	string::string(const string& s)
	{
		delete[] _str;
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	//赋值运算符重载,老版本的实现
	string& string::operator=(const string& s)
	{
		//这里是不能自己给自己拷贝构造的
		//因为我们首先就是进行函数的析构,这样就会导致我们无法找到数值
		if (this != &s)
		{
			delete[] _str;
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
			return *this;
		}
	}
#endif

	//拷贝构造的实现,新版本的实现
	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}
	//赋值,新版本的实现
	//string& string::operator=(const string& s)
	//{
	//	string tmp(s._str);
	//	swap(tmp);
	//	return *this;
	//}
	
	//这个是使用是需要在初始化的时候去_str=nullptr;不然交换之后会导致s指向的是空的空间,会导致析构失败
	string& string::operator=(string s)
	{
		swap(s);
		return *this;
	}
}

// 测试文件(test.cpp)

#include"string.h"
//string原生函数,对照组
void test01()
{
	cout << "对照组string原生:" << endl;
	string s1("hello word");
	string::iterator it1 = s1.begin();
	string::iterator it2 = s1.end();
	//这里我们可以看出来end指向的是最后一个字符的下一个字符
	cout << "s1.end():" << *(it2 - 1) << endl;
	while (it1 != s1.end())
	{
		cout << *it1;
		it1++;
	}
	cout << endl << endl << endl;
}
//构造函数析构函数调用//修改字符函数测试//迭代器的测试实现
void test02()
{
	cout << "模拟实现组string:" << endl;
	Test::string s1("hello word");
	Test::string s2;
	Test::string s3("   ");
	const Test::string s4("hello word");
	s1[0] = '0';
	s1[9] = '0';
	s1[5] = '0';
	s3[0] = '0';
	s3[1] = '0';
	s3[2] = '0';
	/*s4[1] = 'x';*///这里报错,所以是不可修改的
	char _s4 = s4[1];
	cout << "这里返回字符串:" << s1.c_str() << endl;
	cout << "这里返回字符串是空:____" << s2.c_str() << endl;
	cout << "这里返回字符串:" << s3.c_str() << endl;
	cout << "这里返回字符:" << _s4 << endl;//e,这里读取成功,调用的是const char& string::operator[](size_t i)const,这个函数
	
	//迭代器的测试实现
	Test::string s5("hello word");
	Test::string::iterator it1 = s5.end() - 1;
	cout << "s5.begin()测试:" << s5.begin() << endl 
		<< "s5.end()测试:" << s5.end() - 1 << endl;


	cout << endl << endl << endl;
}
//插入的实现
void test03()
{
	cout << "模拟实现string插入字符串的测试:" << endl;
	Test::string s1("hello word ");
	//尾插的测试
	s1.push_back('1');
	cout <<"尾插的测试push_back:" << s1.c_str() << endl;

	//append的测试
	s1.append('1');//追加字符
	s1.append("  hello word ");//追加字符串
	s1.append("  hello word ");//追加字符串
	s1.append("  hello word ");//追加字符串
	cout << "append的测试:" << s1.c_str() << endl;

	//+=的测试
	Test::string s2("hello word ");
	s2 += 'c';
	s2 += " weishenm";
	cout << "+=的测试:" << s2.c_str() << endl;

	//insert的测试
	//字符插入的测试
	Test::string s3("hello word ");
	s3.insert(0, 'x');
	s3.insert(13, 'x');
	cout << "insert字符插入的测试:" << s3.c_str() << endl;
	//字符串插入的测试
	Test::string s4("hello word ");
	s4.insert(0, "hello word ");
	s4.insert(22, "hello word ");
	cout << "insert字符串插入的测试:" << s4.c_str() << endl;

	//删除的测试
	Test::string s5(" hello word hello word hello word hello word ");
	s5.earse(0,5);
	cout << "earse字符串删除的测试:" << s5.c_str() << endl;
	s5.earse(1);
	cout << "earse字符串删除的测试:" << s5.c_str() << endl;

	//查找字符的测试
	Test::string s6(" hello word hello word hello word hello word ");
	size_t pos1 = s6.find('o');
	cout << "find查找字符的测试:" << pos1 << endl;
	size_t pos2 = s6.find("word");
	cout << "find查找字符的测试:" << pos2 << endl;
	//find综合实现查找

	////取出字符串的测试
	Test::string s7(" hello word hello word hello word hello word ");
	Test::string ret1 = s7.substr(0, 10);
	cout << "substr查找字符的测试:" << ret1.c_str() << endl;

	//拷贝构造的测试	
	
	//清空的测试

	//比较的测试

	cout << endl << endl << endl;
}
//比较大小的测试
void test04()
{
	cout << "模拟实现string比较大小的测试:" << endl;
	Test::string s1("hello word ");
	Test::string s2("aello word ");
	Test::string s3("hello word ");
	bool ret1 = s2 > s1;
	bool ret2 = s2 == s1;
	bool ret3 = s1 > s2;
	cout << ret1 << endl;
	cout << ret2 << endl;
	cout << ret3 << endl;

	//流插入流提取的测试
	cout << s1 << endl;
	cin >> s1;
	//getline(cin, s1, '=');
	cout << s1 << endl;

	cout << endl << endl << endl;

}
//测试交换
void test05()
{
	cout << "测试交换:" << endl;
	Test::string s1("xxxxxxxxx xxxxx xxxxxx");
	Test::string s2("hello word ");
	cout << "交换前s1:" << s1 << endl 
		<<"交换前s2:" << s2 << endl;
	swap(s1, s2);
	cout << "一次交换后s1:" << s1 << endl
		<< "一次交换后s2:" << s2 << endl;
	s1.swap(s2);
	cout << "二次交换后s1:" << s1 << endl
		<< "二次交换后s2:" << s2 << endl;
	cout << endl << endl << endl;

}
void test06()
{
	cout << "拷贝构造和构造函数的特殊实现:" << endl;
	Test::string s1("xxxxxxxxx xxxxx xxxxxx");
	Test::string s2("hello word ");

	Test::string s3(s2);
	Test::string s4("hello word ");
	s4 = s1;
	cout << "拷贝构造的特殊实现:" << s3 << endl;//hello
	cout << "构造函数的特殊实现:" << s4 << endl;//xxxxxxxxxxxxxxxxxxx

}
int main()
{
	//string原生函数,对照组
	test01();
	//构造函数析构函数调用//修改字符函数测试//迭代器的测试实现
	test02();
	//插入字符串的测试
	test03();
	//比较大小的测试
	test04();
	//测试交换的特殊实现
	test05();
	//拷贝构造和构造函数的特殊实现
	test06();
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值