C++之string类的模拟实现(超详细)

们学习东西,先学习如果使用它,然后再学习如何实现它

文章目录

目录

[1. 命名空间以及头文件](#1. 命名空间以及头文件)

2.string类的成员变量

3.string类的成员函数

[3.1 构造函数](#3.1 构造函数)

[3.2 析构函数](#3.2 析构函数)

[3.3 拷贝构造函数](#3.3 拷贝构造函数)

[3.4 赋值运算符重载](#3.4 赋值运算符重载)

[3.5 c_str函数](#3.5 c_str函数)

[3.6 size函数](#3.6 size函数)

[3.7 clear函数](#3.7 clear函数)

[3.8 reserve函数](#3.8 reserve函数)

[3.9 尾插(push_back)](#3.9 尾插(push_back))

[3.10 尾插 (append)](#3.10 尾插 (append))

[3.11 尾插(operator +=)](#3.11 尾插(operator +=))

[3.12 插入(insert)](#3.12 插入(insert))

[3.13 删除(erase)](#3.13 删除(erase))

[3.14 查找(find)](#3.14 查找(find))

[3.15 获取子串(substr)](#3.15 获取子串(substr))

[3.16 迭代器](#3.16 迭代器)

[3.17 交换函数(swap)](#3.17 交换函数(swap))

[3.18 关系函数](#3.18 关系函数)

[3.19 流插入流提取运算符重载](#3.19 流插入流提取运算符重载)

[3.20 getline函数](#3.20 getline函数)

4.string类模拟实现源文件

string.h

string.cpp

test.cpp



前言

我们在上一节内容中,已经详细介绍了string类,这个可以是我们对目前学习的C++的一个很好的总结,以及与前面所学习的C语言很好地联系在一起了,这节我们来尝试一下如何去自己模拟实现一些string类,以及它那些丰富的接口函数。


这节我将以源代码以及注解的方式来向大家来介绍string类的模拟实现,这里我们模拟实现string类依然分多个文件进行。在最后会附上string类模拟实现的源代码。

1. 命名空间以及头文件

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;

namespace hjc
{
    //...
}

在写string类之前我们要把一些东西准备好,我们要包含一些头文件:<iostream>这是C++标准库的一个头文件,里面有C++提供的标准库函数,我们在后面的模拟实现中我们需要调用这些函数;<assert.h>这个头文件是用来后面我们写assert断言报错时所需的头文件,因为我们后面要进行一些删除操作,所以不能忘记对其删除的位置进行一个检查,因此我们要写一个assert断言报错。最后我们现在要自己来模拟实现一个string类,这个相当于一个项目了,我们可以自己写一个命名空间,后面我们要模拟实现string类中成员函数的功能,对其函数的命名就不会与标准库中的函数名发生重命名报错了,初次之外,我们在多个文件中调用了一个命名空间时编译器会将将这几个文件中的内容并在一起,这样能够更好地进行模拟实现。至于引入std命名空间,因为,我们还要使用这个库中的一些函数进行一些基本的操作(输入输出以及C语言中的一些函数等等)。

2.string类的成员变量

cpp 复制代码
class string
{
public:

//...成员函数

private:
	char* _str=nullptr;
	size_t _size=0;
	size_t _capacity=0;
}

我们在之前学习类与对象的时候就已经学习了,一个类中要有成员变量来表示这个类的一些属性,还要有一些成员函数对其属性进行操作的方法。对于那些是否对于外面公开的就要我们使用public,private等权限访问符进行修饰了,我们一般把那些成员变量是不对于外界进行公开的,我们内部知道就可以了,我们可以将其设置为private的,至于那些成员函数我们一般把它们设置为public的,因为我们定义一个类后,实例化的对象要能够使用一些操作,这也是类的一个面向对象的一个具体体现。我们在之前C语言学习中就对于一个字符串是使用一个字符串数组来进行存储的,这个我们设置一个char类型的数组变量_str并给它一个缺省值nullptr,然后我们对于字符串我们要知道它的有效数据个数即字符串长度和它这个字符串所能够容纳的空间大小,于是我们又设置了两个成员变量_size和_capacity。至于成员函数的模拟实现我们在后面一一逐个介绍。

除了上面那些私有成员变量之外,还有一个公有的成员变量------npos。

cpp 复制代码
	public:
		//static const size_t npos = -1; 
		static const size_t npos;
		//一般静态成员变量不能够在函数声明处给缺省值,因为在成员变量处给缺省值,是给初始化列表进行初始化的
		//但是const修饰的静态整型变量是一个特殊情况,可以在这里初始化

这个成员变量,我们在后面的函数参数中会多次使用,它表示的是无符号整型(size_t)的最大值。当它作为函数参数时,表示的是"直到字符串的末尾"。当它作为返回值时,它通常表示没有匹配的值。这个变量,我们不仅在类内,类外中都要进行使用,于是我们将其设置为static作为静态成员变量,并且将其的权限访问符设置为public。我们对于静态成员变量是不可以在类中给缺省值的,我们给缺省值是想让变量走初始化列表进行初始化,但是静态成员变量本身是不属于类的成员的,因此我们不可以直接给缺省值。我们一般都是在类外对其初始化,但是这里确是一个例外:对于const修饰的静态整型变量,我们可以直接在类中进行初始化。当然我们也是可以在类中进行声明,类外进行初始化,不过在类外我们要指明类域进行访问。

3.string类的成员函数

对于成员函数的模拟实现,我们将其函数的声明与定义放在两个文件中,一个string.h文件和一个string.cpp文件。

3.1 构造函数

string.h文件中的函数声明

cpp 复制代码
		string(const char* str = ""); //带缺省值的构造函数

我们在介绍这个函数之前,我先来科普一个知识点:对于一个变量或者函数,在声明的时候是不会进行开辟空间的。声明只是告诉编译器变量的类型和名称而不会分配内存空间;定义则会为变量分配内存空间,并通常赋予初始值。而且变量只能够定义一次,但是却能够多次声明。我们在函数参数传递缺省值的时候,不能够函数的声明与定义同时传缺省值,只能够在函数声明时传缺省值,在啊函数定义时就不能够传了。

现在我们来讲讲上面的构造函数,我们知道一个类的构造函数的函数名要与它的类名相同,于是我们就将string类的构造函数函数名设置为string,然后我们传递一个参数:const char* str = ""。在上一节string类的详解中我们就已经介绍了string类的构造函数有好几种函数原型,这里我们就不写那么多了,我们就模拟实现一下它那最常见的两种:使用一个字符串来初始化以及初始化一个空字符串。我们对于传递的参数一般都把它们设置为const修饰的类型,因为我们要确保我们传递过去的参数是不可以修改的,不然我们初始化的内容不就不确定的嘛。我们在这里加了一个缺省参数""。这个缺省参数就是字符串中的'\0',因为我们想要定义一个空字符串的话就要传递一个空字符串(长度为0的字符串),这里要注意一下:我们双引号里面不能有任何符号,一个空格都不行,因为我们写一个双引号表示一个字符串时,编译器就默默给我们加上一个'\0'了。这个我们是看不见的,但是它是实际存在的。

string.cpp文件中的构造函数定义

cpp 复制代码
	//虽然共用了一个命名空间,但是要指定类域才能访问其成员函数
	string::string(const char* str) //带缺省值的构造函数,我们要注意:成员函数的的缺省值不能在函数声明与定义同时给,只能在函数声明给
		:_size(strlen(str))
	{
		_capacity=_size;
		_str = new char[_size + 1];//_str是一个字符串数组,我们要new一定的空间,+1是因为strlen()函数不计算'\0'
		strcpy(_str, str);//内容也要进行初始化(使用C语言中的内容拷贝函数)
		//memcpy(_str, str, _capacity + 1);
	}

和我们最开始科普的知识点一样的,我们在函数定义的时候,函数参数的缺省值是不能够写的。另外我们在其他文件中调用时,虽然共用一个命名空间,但是我们还是要指定类域才能够访问其成员函数,毕竟这是在类外面进行定义的。

我们定义这个构造函数,首先我们对_size走初始化列表进行初始化,初始化的内容就使用我们C语言时期学习的strlen函数来求取传递的字符串长度(注意strlen函数计算的是字符串的实际长度,是不包含'\0'的)。然后我们将_capacity设置为和_size一样的大小,确保字符串能装下就行(后面我们还要实现对capacity操作的函数)。至于字符串数组_str我们需要动态申请内存空间,我们可以使用new,new的空间大小要比_size大1,因为我们初始化_size时是不包括'\0'的,但是我们申请数组空间时要加上'\0'所占的空间大小。最后我们使用C语言阶段的strcpy函数将传递的数组内容拷贝给我们自己创建的数组中。

3.2 析构函数

string.h文件的声明

cpp 复制代码
~string();//析构函数

析构函数的函数名也是和类名一样的,但是我们要在函数名的前面加上一个~,这样这就表示的是一个析构函数。对于析构函数,我们一般都不会去传递参数的,因为析构函数是进行销毁后的清理工作。

string.cpp文件中的定义

cpp 复制代码
	string::~string()//析构函数
	{
		delete[]_str;
		_str = nullptr;
		_capacity = 0;
		_size = 0;
	}

我们对于那些有资源申请的变量最后析构的时候要进行释放,由于我们在构造函数中资源申请时用的函数是new[ ],所以我们在这里为了一致我们要使用delete[ ]进行释放资源。(因为我们对于申请释放资源的函数随意匹配使用的话,就可能会造成已经释放的内存会被再次释放,这就跟我们之前使用野指针差不多,因为我们要释放的对象是已经释放过了的,严重的可能会造成内存泄露)。我们使用delete[]释放资源后,要及时将对应的数组地址指向nullptr,这是一个好的习惯,不然也会造成内存泄露的情况。至于_size和_capacity我们都将它们置为0即可。

3.3 拷贝构造函数

string.h文件中的函数声明

cpp 复制代码
string(const string& s); //拷贝构造函数

拷贝构造函数是构造函数的一个重载,但是它的第一个显示的参数必须是类类型的引用,而这个参数也是后面我们将要进行拷贝的对象。

string.cpp文件中的函数定义

cpp 复制代码
	//这个是拷贝构造,即对一个空字符串对象进行拷贝初始化
	string::string(const string& s)
	{
		_str = new char[s._capacity + 1];//重新new一个空间
		strcpy(_str, s._str); //将内容拷贝过去
		//将有效数据个数,容量大小也拷贝过去
		_size = s._size;
		_capacity = s._capacity;
	}

我们实现一个拷贝构造函数,先搞懂它的拷贝原理:编译器重新申请一个和拷贝内容相同大小的空间给我们的类对象,然后将内容也拷贝过去,size与capacity也要保持一致。如上代码,我们首先new一个和给定参数相同大小的空间,然后使用strcpy将s的内容拷贝过去,size,capacity直接赋值就行。因为拷贝构造是一个已经初始化的对象给一个未进行初始化的对象的一个拷贝,所以我们被拷贝的那个对象里面是未定义的,我们不用管,直接赋值即可。

3.4 赋值运算符重载

string.h文件中的函数声明

cpp 复制代码
string& operator=(const string s);//赋值运算符重载

对于这些运算符的重载的返回值类型,我们一般都将其设置为对应类的引用类型,因为这些运算符重载后就是为那些类对象进行使用的。运算符重载的函数参数个数由运算时我们使用的参数个数来决定。

string.cpp文件中的函数定义

cpp 复制代码
	//这个是赋值运算符重载,是对于两个已经进行初始化了的字符串对象使用的
	string& string::operator=(const string s)//赋值运算符重载
	{
		//我们不能够自己给自己进行赋值,因为我们使用赋值运算符重载时,会先将原来的那个字符串空间释放掉,然后再创建一个与要拷贝的字符串相同大小的空间
		//如果我们自己给自己进行赋值的话,我们先进行delete,这样就造成了内存泄漏,从而返回一组随机值
		if (this != &s)
		{
			delete[]_str; //因为我们不知道要进行拷贝的字符串是多大的空间,所以我们索性将其内存释放掉然后再new一个空间
			_str = new char[s._capacity + 1]; //根据要拷贝的字符串空间进行new,注意:capacity是不包括'\0'的,但是我们创建空间大小时,要将'\0'加上
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;  //由于我们返回的是引用,而且这是成员函数,我们直接返回*this(this表示的是这个字符数组的起始位置)
	}

我们这个模拟实现的原理是:将原来的那个数组的空间直接释放掉,然后再申请和拷贝对象一样大的空间,然后进行一系列的拷贝操作。我们这里就直接将_str使用delete[],因为赋值运算符是用于两个已经进行了初始化的对象使用的,我们要将右边的值赋值给左边,那样我们就要先将原来的内容擦除掉,这里我们就直接释放资源,后面的操作和我们的拷贝构造是一样的,但是我们在进行这些操作之前,我们要先进行一个条件判断,我们要确保不是自己给自己进行赋值操作,因为我们首先进行的一个操作就是释放内村,我们上来就将那个内存释放掉了,我们再申请资源,就不是原来的那个地址,这样就可能返回一组随机值。最后,我们返回一个*this。在成员函数中this指针是一个隐式的参数指的是类中的那个成员变量我们不可以显示写出来,但是在函数中我们可以直接显示写,因为这个函数的返回类型是string&是一个引用类型,是一个对象,而this是一个指针我们要对其进行一个解引用才能获取它的内容。

3.5 c_str函数

cpp 复制代码
const char* c_str() const
{
	return _str;
}

这个函数是将C++中的字符串对象内容转化为C语言风格的字符串,由于我们这里还没有实现对string类中对<<,>>的运算符重载,因此我们暂时使用这个函数进行测试。由于这个代码内容带短小,于是我们就在类里面对其进行定义,对于类中的定义的函数是内联函数。

3.6 size函数

cpp 复制代码
size_t size() const
{
	return _size;
}

这个函数是用来返回字符串的长度,不包含'\0'。

3.7 clear函数

cpp 复制代码
void clear() 
{
	_str[0] = '\0'; //对于字符数组,'\0'处表示的是结束,所以我们将'\0'放在起始位置,即将字符串清空了
	_size = 0;//有效数据个数归0,但是容量不发生改变
}

这个函数是对字符串内容进行一个清空操作,在字符串数组中,字符串结束的标志就是'\0',于是我们就将数组的第一个位置设置为'\0',那么这个字符串内容就被清空了(字符串的长度为0),我们也不要忘记将_size置为0,我们需要注意的是clear函数只是清空内容,改变的是字符串的长度,对于字符串所占的容量大小是不会改变的。

3.8 reserve函数

string.h文件中的函数声明

cpp 复制代码
	void reserve(size_t n);  //预备,预留

这个函数是对字符串的容量大小进行一个修改,我们给定一个值,它会提前为字符串开辟一个那么大的容量,传递的参数就是我们想要预留的容量大小,我们要将它的数据类型设置为无符号整型(size_t),这种类型没有负数,只有0和大于0的整数。

string.cpp文件中的函数定义

cpp 复制代码
	void string::reserve(size_t n)
	{
		//当给定预留的空间大小是大于我们本来的空间大小时,我们就需要进行扩容了,
		//我们不能够简单的使用类中的值拷贝/浅拷贝,我们需要创建一个新的存储空间,并将原来的值拷贝过去(深拷贝)
		if (n > _capacity)
		{
			/*cout << "reserve:" << n << endl;*/
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			//_str = nullptr;我们下面要将一个指针传递过去,我们就不要再将其设置为空指针了
			_str = tmp;
			_capacity = n;
		}
	}

这个函数与我们之前数据结构中的顺序表中的一个扩容操作有点像,我们在进行预留空间的时候,要对空间进行一个检查,如果原来的容量大小是小于我们所要的容量大小,于是我们就要进行扩容操作,扩容操作就是重新动态申请一个n的空间,将原来里面的内容放到新申请的空间中,最后再将原来的那个数组中的资源释放掉,最后将我们新申请的空间地址tmp赋给_str作为新的数组地址,不要忘记改下_capacity。

3.9 尾插(push_back)

string.h文件中的函数声明

cpp 复制代码
	void push_back(char ch); //尾插(字符)

这个函数进行的是尾插操作,但是这个函数只能够尾插单个字符,不能够插入字符串。它传递的参数就是它所要插入的字符。

string.cpp文件中的函数定义

cpp 复制代码
	void string::push_back(char ch) //尾插(字符)
	{
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_str[_size + 1] = '\0';
		_size++;
		//insert(_size, ch);
	}

我们对于进行任何插入操作之前,首先要做的事都是检查空间是否足够,如果不够我们就要调用上面的reserve函数进行扩容,扩容完后,我们要将字符插入到数组的最后一个位置,而_size的值在数组中表示的位置就是数组的最后一个位置'\0',因为数组是从0开始的,然后我们要在这个字符的后面的一个位置设置为新的'\0'作为新的结束标志。最后我们还要将数据中的有效个数_size++(字符串长度加1)。

3.10 尾插 (append)

string.h文件中的函数声明

cpp 复制代码
	void append(const char* str);//尾插(字符串)

这个函数的功能也是尾插,不够这个函数插入的内容不一样,上面那个函数插入的内容是字符而这个函数插入的内容是字符串。

string.cpp文件中的函数定义

cpp 复制代码
void string::append(const char* str)//尾插(字符串)
{
	//先计算出要插入的字符串的长度,然后我们在加上我们原来的字符串长度与容量大小进行比较,再进行相应的扩容操作
	size_t len =  strlen(str);
	if (_size+len > _capacity)
	{
		//我们首先2倍的扩容
		size_t newCapacity = _capacity*2;
		//如果2倍的扩容不够的话,我们就直接扩容为两个字符串相加的总长度
		if (newCapacity < len+_size)
		{
			newCapacity = _size + len;
		}
		reserve(newCapacity);
	}
	strcpy(_str + _size, str);   //在原来字符串的末尾处插入_str表示数组起始位置加上一个_size则表示的是数组的末尾位置
	_size += len;
}

这个函数上来的操作也是先进行检查空间的大小,不过这个函数由于尾插的是一个字符串,我们不能直接简单的比较_capacity与_size的大小,我们要先计算一下我们要插入的字符串长度,然后我们再将这个长度与原来字符串长度加上去与我们当前的容量大小进行比较,如果比_capacity大的话,就先暂时2倍的扩容,看容量够不够,如果不够的话,我们再根据我们刚刚所求的总长度来进行reserve。然后拷贝内容,改变字符串长度。

3.11 尾插(operator +=)

string.h文件的函数声明

cpp 复制代码
string& operator+=(char ch);
string& operator+=(const char* str);

这个函数的功能在上面的两个函数中都已经实现过了,这里不过是一个复用。

string.cpp文件中的函数定义

cpp 复制代码
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}

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

我们直接复用上面的push_back函数和append函数,最后不用忘记返回*this。

3.12 插入(insert)

string.h文件中的函数声明

cpp 复制代码
		void insert(size_t pos, char ch);//在任意位置插入字符
		void insert(size_t pos, const char* str);//在任意位置插入字符串

这个函数与上面那些尾插函数有所区别的,这个函数可以在任意位置进行插入,它有两种函数原型,可以插入字符也可以插入字符串,注意我们的第一个参数都是要插入的位置,在写位置时,我们要计算好并且要记得开始的位置是0。

string.cpp文件中函数定义

cpp 复制代码
	void string::insert(size_t pos, char ch)//在任意位置插入字符
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//我们插入字符,要将插入位置的后面所有字符都往后移动一位,给那个插入位置腾出来空间,我们设置从后往前移动,那么第一个要移动的就是'\0'(位置是_size)
		//由于end与pos都是size_t 类型的,我们一定要注意一下0处(即头插),那么循环条件就是end>0,这个循环会进入一个死循环,因为这是无符号整型,变成负数就会进行模相加
		//所以我们为了防止越界,于是从前面一位往当前位置移动的策略,那么我们要将end设置为'\0'的下一位,作为初始化位置那样才能够移动'\0'
		size_t end = _size + 1;
		while (end>pos)
		{
			_str[end] = _str[end - 1];  //最后一个数是:将pos位置的数移动到pos+1的位置
			--end;
		}
		_str[pos] = ch;//将给定的字符插入到指定的位置
		_size++;
	}

	void string::insert(size_t pos, const char* str)//在任意位置插入字符串
	{
		assert(pos <= _size); //pos=_size时即'\0'处

		size_t len =  strlen(str);
		if (len+_size> _capacity)
		{
			size_t newCapacity = _capacity * 2;
			if (_size + len > newCapacity)
				newCapacity = _size + len;

			reserve(newCapacity);
		}
		size_t end = _size + len;
		//从后往前逐个移动
		while (end>pos+len-1)//移动第一个位置的值时,所指向的位置是pos+len,所以将这个作为循环结束的条件
		{
			_str[end] = _str[end - len];  //第一个是将pos位置的值移动到pos+len位置上
			--end;
		}

		//由于我们想要插入的是一个字符串的内容,于是我们就将这个字符串遍历逐个插入到_str的位置,我们通过pos,i来找寻对应的相对位置
		for (size_t i = 0; i < len; i++)
		{
			_str[pos+i] = str[i];
		}
		_size += len;

	}

插入之前我们都要进行一下容量的检查,这个操作都是直接套用上面的push_back和append函数的。在这之前我们还要进行一下对插入位置pos的检查,我们要确保它是有效位置。后面就是插入的过程了。尾插还好直接在字符串的最后插入即可,但是如果在字符串中间插入的话,我们就需要移动插入位置后面的那些字符串了,我们要将那些字符串相对地往后移动对应位置,插入位置空出来后,我们就可以直接往里面插入字符或者字符串了。至于上面我们移动字符串是将前面的一个值赋给当前值,然后我们将初始值初始化为'\0'的下一个位置或者len位置后。

3.13 删除(erase)

string.h文件中的函数声明

cpp 复制代码
	void erase(size_t pos, size_t len= npos);  //只能够在函数申明处给缺省值

这个函数是用来删除指定位置的数据,我们可以确定删除的字符串长度。我们设定了一个缺省值npos,如果我们没有传递第二个参数的话,就直接使用这个缺省值作为参数了,由于这个参数表示的是无符号整型的最大值,其远远大于一般字符串的长度,因此我们一般认为直接将pos位置后面的字符串全部删除掉。

string.cpp文件中的函数定义

cpp 复制代码
	void string::erase(size_t pos, size_t len)
	{

		assert(pos < _size);//确保要删除的位置是属于有效位置内的
		if (len > _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;//pos的位置表示的就是第几个数字,也表示还剩下几个数字
		}
		else//剩下的子串不全部一把删除掉
		{
			//我们可以将end的初始位置放在删除完后的第一个有效数据的位置,我们从前往后逐个将其全部移动过去
			size_t end = pos + len;   //pos-pos+len这部分的数据被删除掉了
			while (end<=_size)
			{
				_str[end - len] = _str[end];
				++end;
			}
			_size -= len;
		}
	}

我们先对删除的位置进行一下检查,我们不能超过'\0'的位置(_size)也不能够等于'\0',我们插入的时候是可以等于的,我们要在那个位置放数据,但是删除数据的话我们不能在那里进行。然后我们再对所给参数------len与我们pos后面的字符串长度进行一下比较,如果len比剩余的字符串长度长的话就直接将后面那段字符串删除掉,我们直接将'\0'移到pos位置上就行,然后修改一下有效数据个数_size,由于字符串是以0开始的,以'\0'结束的,因此pos的位置恰好是剩余的字符串的实际长度。如果len比剩余的字符串长度短的话,我们就设置一个end变量用来表示删除字符串的最后一个位置,然后我们从前往后逐个将其全部移动过去。

3.14 查找(find)

string.h文件中的函数声明

cpp 复制代码
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);

这个函数是用来查找某个字符或者字符串的位置,我们的第一个参数设置为要查找的内容,第二个参数设置为开始查找的位置,如果我们不写的话就默认从起始位置开始查找,给了缺省值。

string.cpp文件中的函数定义

cpp 复制代码
	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;//这个情况是没有找到字符,返回一个npos一个特别大的数2^32,我们不能够将这个放到上面的for循环中,否则就不能是所有情况都有返回值
	}

	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);
		//strstr(s1,s2)函数是C语言中s2在s1中找子串的函数,成功找到则返回位置,未成功找到则返回空
		//const char* strstr (const char* str1, const char* str2)
		const char* ptr = strstr (_str + pos, str); //这个函数返回的是一个指针,我们设置一个指针来接收
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

不管是查找字符还是查找字符串,我们第一个要进行的操作都是进行对查找位置的一个检查。对于查找字符,我们可以直接遍历字符串,然后利用一个条件语句就可以进行查找了,如果我们没有找到,我们就返回一个npos表示没有所匹配的字符。对于查找字符串的位置,我们可以调用C语言中的strstr函数,这个函数能够查找到一个完全与给定字符串相同的子字符串,然后返回那个子字符串在字符串中的位置。这个函数返回的是一个字符指针,然后我们跟nullptr进行比较,如果不等于就说明找到了,然后我们通过这个指针减去我们原来的字符指针得到的差值就是我们查找的位置。

3.15 获取子串(substr)

string.h文件中的函数声明

cpp 复制代码
	string substr(size_t pos = 0, size_t len = npos); //获取子串

这个函数用来返回我们字符串对象的一段子串内容,第一个参数是那个子串开始的起始位置,第二个参数是那个子串的长度,我们给了它一个缺省值,如果我们没有写参数就直接将pos后面所有子串都获取。

string.cpp文件中的函数定义

cpp 复制代码
	string string::substr(size_t pos, size_t len )
	{
		assert(pos < _size);
		//如果给定的长度是大于剩余的长度,我们则将剩下的子串全部取到,即len等于剩下子串的长度
		if (len > (_size - pos))
		{
			len = _size - pos;
		}

		hjc::string sub;//我们定义一个空子串,并给它预留空间,等会我们将指定位置之后的子串逐个插入到空子串中
		sub.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}

		return sub;
	}

这个函数模拟实现的原理就是我们先来确定我们要获取字符串的具体长度,然后我们在实例化出来一个字符串对象sub,然后给这个字符串对象预留len的空间,然后我们逐个将原来字符串对象中的内容通过索引值和+=尾插到sub中,最后我们返回sub,就是我们所要获取的子字符串。

3.16 迭代器

cpp 复制代码
		//迭代器(这个要放在string类里面)
		using iterator = char*;   //使用using来进行重命名,我们要使用赋值的形式
		using const_iterator = const char*;

我们自己模拟实现迭代器的时候是没有迭代器这个概念的,但是我们可以根据我们前面的知识来模拟实现一下,我们知道迭代器和指针是十分相像的,但是两者并不等价。对于我们模拟实现就不要再考虑那么多了,我们直接通过using将char*这个指针命名为迭代器iterator。我们要实现针对于两种字符串的迭代器,一个是普通字符串一个是const修饰的字符串。

cpp 复制代码
		iterator begin()
		{
			return _str;
		}

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

这些是迭代器的一些函数,这些函数返回的类型都是迭代器,这些迭代器所指向的内容要么字符串的第一个有效数据,要么是有效数据的后面一个理论字符,于是我们就通过数组名来返回相应的位置。

3.17 交换函数(swap)

这个函数我们实现了两种形式,一种是成员函数形式的swap,另一种是非成员函数形式的swap。

cpp 复制代码
	void swap(string& s);

上面是成员函数形式的swap,别看它只有一个参数string&s,其实它还有一个隐藏的参数const string*this。这个this表示的就是调用这个函数的字符串对象。

cpp 复制代码
	//这个swap是string类的成员函数,在string类中还有有一个非成员函数的swap函数,那个函数其实就是这个函数的一个封装
	//我们实现string类的成员函数,是通过标准库中的swap函数来实现的,我们通过std::swap来交换字符串的内容,容量,数据个数
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_capacity, s._capacity);
		std::swap(_size, s._size);
	}

我们在string类中模拟实现的成员函数swap,实际上只是改变一下两个字符串指针的指向,我们这里可以直接调用标准库中的swap函数,我们就不用自己来手动进行交换了。另外两个对象的_size与_capacity都不要忘记进行交换一下。

cpp 复制代码
	void swap(string& s1, string& s2);

这个函数是string类中的非成员函数类型的swap函数,它直接传递了两个要进行交换的字符串对象。

cpp 复制代码
	void swap(string& s1, string& s2)
	{
		s1.swap(s2);
	}

我们对这个函数的模拟实现其实是对我们刚刚实现的成员函数swap的一个封装,我们知道标准库中的swap函数对于有资源申请的内容进行交换是很麻烦的,因此我们为了避免使用标准库中的那个我们就自己写一个与标准库一样参数的函数,这样编译器就会优先调用我们已经写好的这个函数了。

3.18 关系函数

cpp 复制代码
	//这个关系函数,我们不作为成员函数 ,将其放在类外面
	bool operator== (const string& lhs, const string& rhs);
	bool operator!= (const string& lhs, const string& rhs);
	bool operator> (const string& lhs, const string& rhs);
	bool operator< (const string& lhs, const string& rhs);
	bool operator>= (const string& lhs, const string& rhs);
	bool operator<= (const string& lhs, const string& rhs);

这些函数都是运算符重载,我们要显示写出来两个要进行比较的字符串对象,于是我们就不能将其作为成员函数了,我们要将它们声明在类外面。

cpp 复制代码
	//strcmp()函数是用来判断两个字符串的大小,每个字符串逐个进行比较,按照ascii码值进行比较
	//int strcmp ( const char * str1, const char * str2 );
	//当str1<str2时,返回一个小于0的值,当石str1>str2时,返回一个大于0的值,当str1==str2时,返回0
	bool operator== (const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) == 0;
	}
	bool operator!= (const string& lhs, const string& rhs)
	{
		return !(lhs == rhs);
	}
	bool operator> (const string& lhs, const string& rhs)
	{
		return !(lhs <= rhs);
	}
	bool operator< (const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) < 0;
	}
	bool operator>= (const string& lhs, const string& rhs)
	{
		return !(lhs < rhs);
	}
	bool operator<= (const string& lhs, const string& rhs)
	{
		return (lhs < rhs) || (lhs == rhs);
	}

这里模拟实现关系运算函数,我们在日期类的实现中就已经了解过了,不过当时是数字的比较,这里是字符串的比较,不过没有关系我们可以直接调用我们C语言阶段学习的用来比较两个字符串大写的函数strcmp,我们使用这个函数时也不用每一个都使用这个函数,我们实现一个函数之后剩余几个利用复用就行了,这是一个小技巧。

3.19 流插入流提取运算符重载

cpp 复制代码
	ostream& operator<< (ostream& os, const string& str);//流插入运算符重载,我们不需要调用上面类中的私有成员,所以我们不用将其设置为友元函数
	istream& operator>> (istream& is, string& str);//流提取运算符重载

对于流插入流提取运算符的重载,我们不能够将其放在类中作为成员函数,我们要将其作为非成员函数,由于我们这个函数是不需要使用类中那些私有成员变量,因此我们不用将其定义为友元函数。

cpp 复制代码
	ostream& operator<< (ostream& os, const string& str)//流插入运算符重载
	{
		for (size_t i = 0; i < str.size(); ++i)  //这里的str是const修饰的字符串对象类型,所以我们要在之前的size函数加上const修饰
		{
			os << str[i];
		}
		return os; //返回一个os值是为了能够实现连续赋值,使值能够进行保存下来
	}


	//get()函数      istream& get (char& c);
	istream& operator>> (istream& is, string& str)//流提取运算符重载
	{
		//我们在这个插入之前得先将str中的值清空,不然我们流提取插入到istram中的值会接在str后面
		//如果我们频繁地从istream流中获取字符,那样可能需要不断进行多次扩容,那样扩容所要的开销可能会有点大
		//于是我们可以设置一个数组,来将那些字符暂时的存储起来,然后一把+=到str,那样即不用进行多次扩容了
		str.clear();
		int i = 0;
		char buffer[256];  //类似于我们之前的缓冲区
		char ch = ' ';
		ch = is.get();//首先先获取一个ch字符,我们要注意,我们使用istream中的get函数时,我们要直接赋值给ch,不能够+=给ch,那样就会导致数据不能够全部传递过去了
		while (ch!=' '&& ch!='\n')   //C++中规定了空格和换行表示的是一个字符串输入结束的标志
		{
			//先将第一个获取的字符放到buffer数组中,然后,i++ 数组中到达了下一个位置,后面又进行了获取字符的操作,然后经过一个循环又放到buffer数组中
			//当数组的最后一个位置时,及时止损,在最后一个位置放上'\0',然后将数组buffer中所有的字符一把+=到目标字符串str中,并将坐标重新置为0,重新从头放字符
			buffer[i++] = ch;
			if (i == 255)
			{
				buffer[i] = '\0';  //同样地对于数组,我们直接赋值给数组即可,那样就可以改变数组里面的内容了
				str += buffer;
				i = 0;
			}
			ch = is.get();
		}

		//当我们放不满字符串时并且跳出了while循环了,就进入到了这个条件语句中,我们直接将最后一个字符('\0')放到第i个位置(结束的位置)
		if (i > 0)
		{
			buffer[i] = '\0';
			str += buffer;
		}
		return is;
	}

对于流插入运算符的重载,我们直接遍历字符串数组,然后逐个使用os变量进行输出,最后我们要返回一个os变量,我们返回一个值是为了能够连续赋值,使值能够保存下来。

对于流提取运算符的重载就有点麻烦了,我们不能够直接使用is变量来获取值了,因为在标准库中就已经规定了遇到" ''就认为是一个字符串结束的标志了,这个与我们字符串结束的标志有所不同。所以我们不能直接使用is变量,但是is变量中有一个get函数,这个函数可以逐个获取任意字符,我们就用这个函数来获取从键盘中输入的值。我们在进行输入之前,我们要确保字符串里面的内容是空的,不然输入的字符就尾插到原来那个字符串的后面去了,所以这里我们先调用一下clear函数,清空一下字符串内容。我们为了避免一个一个插入,然后频繁地进行扩容,我们可以使用一个字符数组来存储一下获取的字符,然后我们再利用一个while循环来表示字符串结束的标志。在循环体中,当我们输入到字符串数组的最后一个位置时,我们将其设置为'\0',然后将整个数组中的字符都尾插到原来的字符串中,然后循环这个操作。如果我们获取的字符装不下整个数组就直接在最后一个位置放'\0',最后+=到对应字符串对象末尾。最后同样要返回一个is对象。

3.20 getline函数

cpp 复制代码
	istream& getline(istream& is, string& str, char delim = '\n');  //取一行字符串

这个函数也是用来从键盘中获取内容的,不过这个函数不像流提取运算符重载那样遇到了'\n'和' '就结束字符串了,这个函数可以获取一整行的内容,只有'\n'才会使字符串获取结束。最后一个参数是分隔符的意思,我们可以输入这个分隔符来提前结束字符串,我们给了一个缺省值'\n',当我们什么都不写时就默认使用换行来结束。

cpp 复制代码
	istream& getline(istream& is, string& str, char delim)
	{
		str.clear();
		int i = 0;
		char buffer[256];
		char ch=' ';
		ch = is.get();
		while (ch!=delim )
		{
			buffer[i++] = ch;
			if (i == 255)
			{
				buffer[i] = '\0';
				str += buffer;
				i = 0;
			}
			ch = is.get();
		}
		if (i > 0)
		{
			buffer[i] = '\0';
			str += buffer;
		}

		return is;
	}

这个函数的大体框架都是套用上面的流提取运算符重载的,只在那while的循环条件进行了一个修改,将其改成遇到了我们自己设置的分隔符就结束了。

4.string类模拟实现源文件

string.h

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;

namespace hjc
{
	class string
	{
	public:
		//迭代器(这个要放在string类里面)
		using iterator = char*;   //使用using来进行重命名,我们要使用赋值的形式
		using const_iterator = const char*;

		string(const char* str = ""); //带缺省值的构造函数
		string(const string& s); //拷贝构造函数
		//因为我们如果想要拷贝构造一个字符串对象的话,我们仅仅调用编译器默认生成的拷贝构造的话,它只能够值拷贝
		//它不能够开辟出来一个新的空间,所以我们需要自己来写一个拷贝构造
		string& operator=(const string s);//赋值运算符重载
		~string();//析构函数


		void reserve(size_t n);  //预备,预留
		void push_back(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 erase(size_t pos, size_t len= npos);  //只能够在函数申明处给缺省值
		void swap(string& s);

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos); //获取子串


		//iterator.begin
		iterator begin()
		{
			return _str;
		}

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

		//[]
		const char& operator[](size_t i) const
		{
			assert(i < _size);
			return _str[i];
		}

		char& operator[](size_t i)
		{
			assert(i < _size);
			return _str[i];
		}


		//这里我们暂时没有写重载流插入与流提取的重载,为了后续测试所以我们写一个函数使其输出(直接输出字符串数组)
		//由于这个代码短小,我们就将其设置为内联函数
		const char* c_str() const
		{
			return _str;
		}

		//size 字符串大小
		size_t size() const
		{
			return _size;
		}

		void clear() 
		{
			_str[0] = '\0'; //对于字符数组,'\0'处表示的是结束,所以我们将'\0'放在起始位置,即将字符串清空了
			_size = 0;//有效数据个数归0,但是容量不发生改变
		}



	private:
		char* _str=nullptr;
		size_t _size=0;
		size_t _capacity=0;
	public:
		//static const size_t npos = -1; 
		static const size_t npos;
		//一般静态成员变量不能够在函数声明处给缺省值,因为在成员变量处给缺省值,是给初始化列表进行初始化的
		//但是const修饰的静态整型变量是一个特殊情况,可以在这里初始化
	};

	void swap(string& s1, string& s2);

	//这个关系函数,我们不作为成员函数 ,将其放在类外面
	bool operator== (const string& lhs, const string& rhs);
	bool operator!= (const string& lhs, const string& rhs);
	bool operator> (const string& lhs, const string& rhs);
	bool operator< (const string& lhs, const string& rhs);
	bool operator>= (const string& lhs, const string& rhs);
	bool operator<= (const string& lhs, const string& rhs);

	ostream& operator<< (ostream& os, const string& str);//流插入运算符重载,我们不需要调用上面类中的私有成员,所以我们不用将其设置为友元函数
	istream& operator>> (istream& is, string& str);//流提取运算符重载
	istream& getline(istream& is, string& str, char delim = '\n');  //取一行字符串
}

string.cpp

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS
#include"string.h"

namespace hjc
{
	//对于静态成员变量的初始化,我们要在类外面进行初始化,我们在类外面进行初始化时,外面调用这个变量时,我们不能够写staticl了,只要在变量声明处写即可,否则会无法编译报错
	const size_t string::npos=-1;

	//虽然共用了一个命名空间,但是要指定类域才能访问其成员函数
	string::string(const char* str) //带缺省值的构造函数,我们要注意:成员函数的的缺省值不能在函数声明与定义同时给,只能在函数声明给
		:_size(strlen(str))
	{
		_capacity=_size;
		_str = new char[_size + 1];//_str是一个字符串数组,我们要new一定的空间,+1是因为strlen()函数不计算'\0'
		strcpy(_str, str);//内容也要进行初始化(使用C语言中的内容拷贝函数)
		//memcpy(_str, str, _capacity + 1);
	}

	//这个是拷贝构造,即对一个空字符串对象进行拷贝初始化
	string::string(const string& s)
	{
		_str = new char[s._capacity + 1];//重新new一个空间
		strcpy(_str, s._str); //将内容拷贝过去
		//将有效数据个数,容量大小也拷贝过去
		_size = s._size;
		_capacity = s._capacity;

	}

	string::~string()//析构函数
	{
		delete[]_str;
		_str = nullptr;
		_capacity = 0;
		_size = 0;
	}


	//这个是赋值运算符重载,是对于两个已经进行初始化了的字符串对象使用的
	string& string::operator=(const string s)//赋值运算符重载
	{
		//我们不能够自己给自己进行赋值,因为我们使用赋值运算符重载时,会先将原来的那个字符串空间释放掉,然后再创建一个与要拷贝的字符串相同大小的空间
		//如果我们自己给自己进行赋值的话,我们先进行delete,这样就造成了内存泄漏,从而返回一组随机值
		if (this != &s)
		{
			delete[]_str; //因为我们不知道要进行拷贝的字符串是多大的空间,所以我们索性将其内存释放掉然后再new一个空间
			_str = new char[s._capacity + 1]; //根据要拷贝的字符串空间进行new,注意:capacity是不包括'\0'的,但是我们创建空间大小时,要将'\0'加上
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		return *this;  //由于我们返回的是引用,而且这是成员函数,我们直接返回*this(this表示的是这个字符数组的起始位置)
	}

	void string::reserve(size_t n)
	{
		//当给定预留的空间大小是大于我们本来的空间大小时,我们就需要进行扩容了,
		//我们不能够简单的使用类中的值拷贝/浅拷贝,我们需要创建一个新的存储空间,并将原来的值拷贝过去(深拷贝)
		if (n > _capacity)
		{
			/*cout << "reserve:" << n << endl;*/
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			//_str = nullptr;我们下面要将一个指针传递过去,我们就不要再将其设置为空指针了
			_str = tmp;
			_capacity = n;
		}
	}

	void string::push_back(char ch) //尾插(字符)
	{
		if (_capacity == _size)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_str[_size + 1] = '\0';
		_size++;
		//insert(_size, ch);
	}


	void string::append(const char* str)//尾插(字符串)
	{
		//先计算出要插入的字符串的长度,然后我们在加上我们原来的字符串长度与容量大小进行比较,再进行相应的扩容操作
		size_t len =  strlen(str);
		if (_size+len > _capacity)
		{
			//我们首先2倍的扩容
			size_t newCapacity = _capacity*2;
			//如果2倍的扩容不够的话,我们就直接扩容为两个字符串相加的总长度
			if (newCapacity < len+_size)
			{
				newCapacity = _size + len;
			}
			reserve(newCapacity);
		}
		strcpy(_str + _size, str);   //在原来字符串的末尾处插入_str表示数组起始位置加上一个_size则表示的是数组的末尾位置
		_size += len;
	}

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

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


	void string::insert(size_t pos, char ch)//在任意位置插入字符
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		//我们插入字符,要将插入位置的后面所有字符都往后移动一位,给那个插入位置腾出来空间,我们设置从后往前移动,那么第一个要移动的就是'\0'(位置是_size)
		//由于end与pos都是size_t 类型的,我们一定要注意一下0处(即头插),那么循环条件就是end>0,这个循环会进入一个死循环,因为这是无符号整型,变成负数就会进行模相加
		//所以我们为了防止越界,于是从前面一位往当前位置移动的策略,那么我们要将end设置为'\0'的下一位,作为初始化位置那样才能够移动'\0'
		size_t end = _size + 1;
		while (end>pos)
		{
			_str[end] = _str[end - 1];  //最后一个数是:将pos位置的数移动到pos+1的位置
			--end;
		}
		_str[pos] = ch;//将给定的字符插入到指定的位置
		_size++;
	}

	void string::insert(size_t pos, const char* str)//在任意位置插入字符串
	{
		assert(pos <= _size); //pos=_size时即'\0'处

		size_t len =  strlen(str);
		if (len+_size> _capacity)
		{
			size_t newCapacity = _capacity * 2;
			if (_size + len > newCapacity)
				newCapacity = _size + len;

			reserve(newCapacity);
		}
		size_t end = _size + len;
		//从后往前逐个移动
		while (end>pos+len-1)//移动第一个位置的值时,所指向的位置是pos+len,所以将这个作为循环结束的条件
		{
			_str[end] = _str[end - len];  //第一个是将pos位置的值移动到pos+len位置上
			--end;
		}

		//由于我们想要插入的是一个字符串的内容,于是我们就将这个字符串遍历逐个插入到_str的位置,我们通过pos,i来找寻对应的相对位置
		for (size_t i = 0; i < len; i++)
		{
			_str[pos+i] = str[i];
		}
		_size += len;

	}

	void string::erase(size_t pos, size_t len)
	{

		assert(pos < _size);//确保要删除的位置是属于有效位置内的
		if (len > _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;//pos的位置表示的就是第几个数字,也表示还剩下几个数字
		}
		else//剩下的子串不全部一把删除掉
		{
			//我们可以将end的初始位置放在删除完后的第一个有效数据的位置,我们从前往后逐个将其全部移动过去
			size_t end = pos + len;   //pos-pos+len这部分的数据被删除掉了
			while (end<=_size)
			{
				_str[end - len] = _str[end];
				++end;
			}
			_size -= len;
		}
	}

	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;//这个情况是没有找到字符,返回一个npos一个特别大的数2^32,我们不能够将这个放到上面的for循环中,否则就不能是所有情况都有返回值
	}

	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);
		//strstr(s1,s2)函数是C语言中s2在s1中找子串的函数,成功找到则返回位置,未成功找到则返回空
		//const char* strstr (const char* str1, const char* str2)
		const char* ptr = strstr (_str + pos, str); //这个函数返回的是一个指针,我们设置一个指针来接收
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

	string string::substr(size_t pos, size_t len )
	{
		assert(pos < _size);
		//如果给定的长度是大于剩余的长度,我们则将剩下的子串全部取到,即len等于剩下子串的长度
		if (len > (_size - pos))
		{
			len = _size - pos;
		}

		hjc::string sub;//我们定义一个空子串,并给它预留空间,等会我们将指定位置之后的子串逐个插入到空子串中
		sub.reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}

		return sub;
	}

	//这个swap是string类的成员函数,在string类中还有有一个非成员函数的swap函数,那个函数其实就是这个函数的一个封装
	//我们实现string类的成员函数,是通过标准库中的swap函数来实现的,我们通过std::swap来交换字符串的内容,容量,数据个数
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_capacity, s._capacity);
		std::swap(_size, s._size);
	}

//



	void swap(string& s1, string& s2)
	{
		s1.swap(s2);
	}


	//strcmp()函数是用来判断两个字符串的大小,每个字符串逐个进行比较,按照ascii码值进行比较
	//int strcmp ( const char * str1, const char * str2 );
	//当str1<str2时,返回一个小于0的值,当石str1>str2时,返回一个大于0的值,当str1==str2时,返回0
	bool operator== (const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) == 0;
	}
	bool operator!= (const string& lhs, const string& rhs)
	{
		return !(lhs == rhs);
	}
	bool operator> (const string& lhs, const string& rhs)
	{
		return !(lhs <= rhs);
	}
	bool operator< (const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) < 0;
	}
	bool operator>= (const string& lhs, const string& rhs)
	{
		return !(lhs < rhs);
	}
	bool operator<= (const string& lhs, const string& rhs)
	{
		return (lhs < rhs) || (lhs == rhs);
	}


	ostream& operator<< (ostream& os, const string& str)//流插入运算符重载
	{
		for (size_t i = 0; i < str.size(); ++i)  //这里的str是const修饰的字符串对象类型,所以我们要在之前的size函数加上const修饰
		{
			os << str[i];
		}
		return os; //返回一个os值是为了能够实现连续赋值,使值能够进行保存下来
	}


	//get()函数      istream& get (char& c);
	istream& operator>> (istream& is, string& str)//流提取运算符重载
	{
		//我们在这个插入之前得先将str中的值清空,不然我们流提取插入到istram中的值会接在str后面
		//如果我们频繁地从istream流中获取字符,那样可能需要不断进行多次扩容,那样扩容所要的开销可能会有点大
		//于是我们可以设置一个数组,来将那些字符暂时的存储起来,然后一把+=到str,那样即不用进行多次扩容了
		str.clear();
		int i = 0;
		char buffer[256];  //类似于我们之前的缓冲区
		char ch = ' ';
		ch = is.get();//首先先获取一个ch字符,我们要注意,我们使用istream中的get函数时,我们要直接赋值给ch,不能够+=给ch,那样就会导致数据不能够全部传递过去了
		while (ch!=' '&& ch!='\n')   //C++中规定了空格和换行表示的是一个字符串输入结束的标志
		{
			//先将第一个获取的字符放到buffer数组中,然后,i++ 数组中到达了下一个位置,后面又进行了获取字符的操作,然后经过一个循环又放到buffer数组中
			//当数组的最后一个位置时,及时止损,在最后一个位置放上'\0',然后将数组buffer中所有的字符一把+=到目标字符串str中,并将坐标重新置为0,重新从头放字符
			buffer[i++] = ch;
			if (i == 255)
			{
				buffer[i] = '\0';  //同样地对于数组,我们直接赋值给数组即可,那样就可以改变数组里面的内容了
				str += buffer;
				i = 0;
			}
			ch = is.get();
		}

		//当我们放不满字符串时并且跳出了while循环了,就进入到了这个条件语句中,我们直接将最后一个字符('\0')放到第i个位置(结束的位置)
		if (i > 0)
		{
			buffer[i] = '\0';
			str += buffer;
		}
		return is;
	}

	istream& getline(istream& is, string& str, char delim)
	{
		str.clear();
		int i = 0;
		char buffer[256];
		char ch=' ';
		ch = is.get();
		while (ch!=delim )
		{
			buffer[i++] = ch;
			if (i == 255)
			{
				buffer[i] = '\0';
				str += buffer;
				i = 0;
			}
			ch = is.get();
		}
		if (i > 0)
		{
			buffer[i] = '\0';
			str += buffer;
		}

		return is;
	}
}

test.cpp

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS
#include"string.h"

//初始化,[],迭代器
void test_string1()
{
	//我们包含了string.h头文件,我们直接指定命名空间域,然后通过调用这个命名空间中string的构造函数即可
	hjc::string  s1("hello world");
	cout << s1.c_str() << endl ;
	hjc::string  s2;
	cout << s2.c_str() << endl;

//我们模拟实现对字符串对象的遍历访问,我们访问的是字符串对象里面的每一个字符,故可以不使用c_str函数(也使用不了),我们输出的是访问的字符串中的字符
	for (size_t i = 0; i <s1.size(); i++)
	{
		cout << s1[i] << " ";
	}

	cout << endl;

	hjc::string::iterator it1 = s1.begin();
	while (it1!=s1.end())
	{
		cout << *it1 << " ";
		it1++;
	}
	cout << endl;

	for (auto i : s1)
	{
		cout << i << " ";
	}
	cout << endl;
}


//插入,删除
void test_string2()
{
	hjc::string s1("hello world");
	hjc::string s2;
	s2.push_back('a');
	cout << s2.c_str() << endl;
	cout << s2 << endl;
	s2.append("asdas");
	cout << s2.c_str() << endl;
	s1.insert(6, 'a');
	s1.insert(6, "asaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
	cout << s1.c_str() << endl;
	s1.erase(0,1);
	s1.erase(0,1);
	s1.erase(0,1);
	s1.erase(0);
	cout << s1.c_str() << endl;
	s2 += 'v';
	cout << s2.c_str();
	s2 += "123";
	cout << s2.c_str();
	s2.clear();
	s2 += "helloe world";
	cout << s2.c_str();
	s1 += "123456";
	s1 += '9';
	s1 += '#';
	s1 += "#helllo world";
	cout << s1 << endl;


}

void test_string3()
{
	hjc::string s1("hello world");
	hjc::string s2;
	cout << s1.find('e') << endl;
	cout << s1.find("wo") << endl;
	hjc::string s3("https://legacy.cplusplus.com/reference/clibrary/");
	size_t pos1 = s3.find(':');
	size_t pos2 = s3.find('/', pos1 + 3);  //如果我们想要指定从某个位置开始的话,我们要明确那个位置,注意要是指那个位置
	cout << pos1 << "  " << pos2 << endl;
	if (pos1 != string::npos && pos2 != string::npos)
	{
		hjc::string domain = s3.substr(pos1 + 3, pos2 - (pos1 + 3));//对于一个左闭右开的区间,直接用右边界的值减去左边界的值即为区间长度
		cout << domain << endl;

		hjc::string uri = s3.substr(pos2 + 1);
		cout << uri.c_str() << endl;
	}

}


void test_string4()
{
	hjc::string s1("hello world");
	hjc::string s2(s1);   //拷贝构造,用于一个已经初始化的对象给一个没有进行初始化的对象进行初始化
	s1[0] = 'x';
	cout << s1 << endl;
	cout << s2 << endl;
	hjc::string s3("xxxxxxxxxxxxxxxxxx");
	s1 = s3; //赋值运算符重载,用于两个已经初始化的对象
	s1[0] = 'a';
	cout << s1 << endl;
	cout << s3 << endl;


	//如果自己给自己进行赋值,那么直接返回地址解引用的内容(*this)
	s1 = s1;
	cout << s1 << endl;
}

void test_string5()
{
	hjc::string s1("hello world");
	hjc::string s2(s1);
	hjc::string s3 = s1;

	//在vs中,strcmp函数的返回值是1,0,-1
	cout << (s1 == s2) << endl;  //1
	cout << (s2 != s3) << endl; //0
	cout << ("hello world" == s1) << endl;//1
	cout << (s2 > "hello world");//0
	cout << endl;

	cout << s1 << endl;
	operator<<(cout, s1) << endl;

	hjc::string s4;
	cin >> s4;
	//getline(cin, s4,'#');
	getline(cin, s4);

	cout << s4 << endl;

}


int main()
{
	test_string5();
	return 0;
}
相关推荐
石兴稳2 小时前
SSD 固态硬盘存储密度的分区
开发语言·javascript·数据库
88号技师2 小时前
2025年2月最新SCI-中华穿山甲优化算法Chinese Pangolin Optimizer-附Matlab免费代码
开发语言·算法·matlab·优化算法
Yang-Never2 小时前
OpenGL ES -> GLSurfaceView绘制点、线、三角形、正方形、圆(索引法绘制)
android·java·开发语言·kotlin·android studio
念九_ysl2 小时前
前端排序算法完全指南:从理论到实践
开发语言·javascript·算法·ecmascript
IT猿手2 小时前
智能优化算法:雪橇犬优化算法(Sled Dog Optimizer,SDO)求解23个经典函数测试集,MATLAB
开发语言·前端·人工智能·算法·机器学习·matlab
米糕.2 小时前
【R语言】ggplot2绘图常用操作
大数据·开发语言·数据分析·r语言
m0_748251352 小时前
java进阶1——JVM
java·开发语言·jvm
Cheese%%Fate2 小时前
【C++】面试常问八股
c++·面试
叉烧钵钵鸡3 小时前
【Spring详解六】容器的功能扩展-ApplicationContext
java·开发语言·后端·spring
小彭爱学习3 小时前
bash快捷键完整版
开发语言·bash