C++_string类

目录

一、string的模拟实现

1、初始化字符串

2、拷贝构造

3、赋值重载 

4、迭代器

5、比较字符串

6、尾插字符、字符串

7、resize

8、中间插入数据、删除数据

8.1 插入数据

8.2 删除数据

9、查找数据

10、打印对象(流插入、流提取)

结语:


前言:

        C++中的string类是专门用于处理字符串的,比如对字符串的增删查改、以及对字串进行各种操作,当然,上面说到的这些在c语言中一样可以使用字符数组实现,那为什么还要费尽心思的去专门实现一个类来解决字符串相关的问题呢,原因就是C++中的string类对边界访问更严格,并且用string类操作字符串相对于c语言中操作字符数组更加简便。

一、string的模拟实现

        string类是STL(标准模板库)中的八大容器之一,STL又是C++中标准库的一部分,简单来说就是库里面已经写好了一个string类,程序员只需要调用该string类就能操作字符串(使用库里的string类要包含头文件<string>),调用该类很简单,但是只有通过了解string类的底层实现是如何实现,并且自己模拟实现出一个string,才能更进一步的了解string类。

1、初始化字符串

        首先写一个类,可以实现字符串的创建和打印,初始化字符串代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:
		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		size_t size()const//私有域不可直接被外部访问,因此需要用函数返回_size
		{
			return _size;
		}

		char& operator[](size_t i)//为了让外部能用[]访问字符串的内容
		{
			assert(i < _size);
			return _str[i];
		}

		const char* c_str()//返回首元素地址(类似数组名的作用)
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("hello world");
	ZH::string st2;

	for (size_t i = 0; i < st1.size(); i++)
	{
		cout << st1[i] << " ";
	}
	cout << endl;

	for (size_t i = 0; i < st2.size(); i++)
	{
		cout << st2[i] << " ";
	}
	cout << endl;

	cout << st1.c_str() << endl;//用c语言的打印字符串方式,传首地址打印
	cout << st2.c_str() << endl;
	return 0;
}

        运行结果:

        string类的成员变量具体含义作用如下图所示:

2、拷贝构造

        我们知道拷贝构造如果不自己实现,那么系统会自动生成一个拷贝构造并且调用,但是系统自动生成的拷贝构造只能完成浅拷贝(即值拷贝)的场景,比如string类的拷贝就不能用浅拷贝完成,具体原因如下:

        因此系统自己生成的浅拷贝不能完成这类场景的拷贝,需要我们手动写一个拷贝构造函数完成深拷贝。

        演示拷贝构造代码如下(将拷贝构造代码放到上文的代码中也同样可以实现,这里省去了与拷贝构造代码无关的代码):

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:
		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		const char* c_str()//返回首元素地址(类似数组名的作用)
		{
			return _str;
		}

		string(const string& s)//深拷贝-拷贝构造
			:_size(s._size)
			, _capacity(s._capacity)
		{
			_str = new char[_capacity + 1];//开辟一块独立的空间
			strcpy(_str, s._str);
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("hello world");
	ZH::string st2(st1);//将st1的内容拷贝给st2

	cout << st1.c_str() << endl;//用c语言的打印字符串方式,传首地址打印
	cout << st2.c_str() << endl;
	return 0;
}

         运行结果:

3、赋值重载 

        赋值的时候要考虑以下几个问题:

        由于以上几个问题涉及的点太多,而且过程过于复杂或造成不必要的消耗,因此库里面的string类在面对赋值时是重新开辟一块空间然后把s1的数据拷贝到该空间中,并且让s2指向该空间即可。

        模拟实现的赋值重载代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:
		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		const char* c_str()//返回首元素地址(类似数组名的作用)
		{
			return _str;
		}

		string& operator=(const string& s)//赋值重载
		{
			if (&s != this)
			{
				char* temp = new char[s._capacity+1];//重新开辟一块空间
				strcpy(temp, s._str);//把数据拷贝到该空间中
				delete[] _str;//释放拷贝对象的原先空间内容
				_str = temp;//让拷贝对象指向该空间

				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("hello world");
	ZH::string st2("zdzdzzdzd");
	st2 = st1;

	cout << st1.c_str() << endl;//用c语言的打印字符串方式,传首地址打印
	cout << st2.c_str() << endl;
	return 0;
}

        运行结果:

4、迭代器

        在string类中,他的迭代器可以理解成是由一个指针实现的,不仅可以遍历打印字符串,还能够修改字符串中的内容,因此string类中迭代器的实现很简单,只需要一个指向首元素的指针,和一个指向尾部的指针(注意:尾部表示最后一个元素的下一个位置,因为迭代器区间是一个左闭右开的区间)即可。


        迭代器模拟实现代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:
		//用typedef来模拟迭代器的两种类型
		typedef char* iterator;
		typedef const char* const_iterator;

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		iterator begin()//返回首地址
		{
			return _str;
		}

		iterator end()//返回末尾地址
		{
			return _str + _size;
		}

		const_iterator end()const//const版本迭代器
		{
			return _str + _size;
		}

		const_iterator begin()const//const版本迭代器
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("hello world");
	ZH::string st2("hello world");
	ZH::string::iterator it = st1.begin();
	while (it!=st1.end())
	{
		(*it)++;
		cout << *it << " ";
		it++;
	}
	cout << endl;

	ZH::string::const_iterator cit = st2.begin();
	while (cit != st2.end())
	{
		//(*cit)++;//被const修饰的迭代器是不能通过该迭代器修改对象里面的内容
		cout << *cit << " ";
		cit++;
	}
	cout << endl;

	return 0;
}

        运行结果:

        总结:迭代器的模拟实现实际上就是begin()和end()两个成员函数的返回指针构成的。 

5、比较字符串

        在字符数组中,只能通过strcmp函数对比两个字符串的大小(按照元素的ASCII码值进行比较),每次对比都需要调用strcmp,写起来也麻烦。然而在string类中可以使用比较运算符进行字符串的对比,比如:‘<','>','<=','>=','==','!=',这些平时用于内置类型的比较运算符如今也可以用在string类(自定义类型)中,原因就是类里面实现了运算符重载。

        当对象支持了运算符重载,则对象与对象之间的对比写起来就更加简单了,模拟实现string运算符重载代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:
		//用typedef来模拟迭代器的两种类型
		typedef char* iterator;
		typedef const char* const_iterator;

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		bool operator>(const string& s)const
		{
			return strcmp(this->_str, s._str) > 0;
		}

		bool operator<(const string& s)const
		{
			return strcmp(this->_str, s._str) < 0;
		}

		bool operator==(const string& s)const
		{
			return strcmp(this->_str, s._str) == 0;
		}

		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);
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("jello world");
	ZH::string st2("hello world");
	cout << (st1 > st2) << endl;//1
	cout << (st1 < st2) << endl;//0
	cout << (st1 == st2) << endl;//0
	cout << (st1 != st2) << endl;//1
	cout << (st1 <= st2) << endl;//0
	cout << (st1 >= st2) << endl;//1

	return 0;
}

        运行结果:

        可以以上代码可以看到,实际上还是复用了strcmp这个函数,只不过对其进行了又一层的封装,这么做的目的就是为了在主函数中可以直接用‘<','>','<=','>=','==','!='进行对象之间的直接运算,总结就是:底层变得复杂,使用变得简单。

6、尾插字符、字符串

        尾插数据时,需要考虑容量是否足够,实现尾插字符串时可以使用函数strcpy进行复用。尾插字符时就如同在数组插入新元素一样。

        扩容函数为reserve,他的作用是单纯的开空间,增大容量,不会对空间内的数据进行修改,即只会改变_capacity的值,不会改变_size的值。(注意扩容的时候也要为’\0'单独开一块空间,但是不会把该空间记录到_capacity中

        尾插代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void reserve(size_t n)//扩容函数
		{
			if (n > _capacity)//只有n大于_capacity才需要扩容
			{
				char* temp = new char[n + 1];
				if (_str != nullptr)
				{
					strcpy(temp, _str);
					delete[] _str;
				}
				_str = temp;
				_capacity = n;
			}
			
		}

		void push_back(char ch)//尾插字符
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

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

		void append(const char* str)//尾插字符串
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			strcpy(_str + _size, str);
			_size += len;
		}
		const char* c_str()
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1;
	st1.push_back('h');
	st1.push_back('e');
	st1.push_back('l');
	st1.push_back('l');
	st1.push_back('o');
	st1.append(" world");
	cout << st1.c_str() << endl;

	return 0;
}

        运行结果:


        如果每次尾插数据时还需要调用成员函数,则就体现不出string类的优势了,因此尾插数据的方式还能再进一步的优化,可以把尾插写成运算符重载,用'+='来实现尾插功能。

        优化尾插功能代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void reserve(size_t n)//扩容函数
		{
			if (n > _capacity)//只有n大于_capacity才需要扩容
			{
				char* temp = new char[n + 1];
				if (_str != nullptr)
				{
					strcpy(temp, _str);
					delete[] _str;
				}
				_str = temp;
				_capacity = n;
			}
			
		}

		void push_back(char ch)//尾插字符
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

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

		void append(const char* str)//尾插字符串
		{
			size_t len = strlen(str);
			if (_size+len > _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			strcpy(_str + _size, str);
			_size += len;
		}

		//两个运算符重载构成函数重载
		string& operator+=(char ch)//尾插字符--运算符重载
		{
			push_back(ch);//复用尾插函数
			return *this;
		}

		string& operator+=(const char* str)//尾插字符串--运算符重载
		{
			append(str);//复用尾插函数
			return *this;
		}

		const char* c_str()
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1;
	st1 += 'h';
	st1 += 'e';
	st1 += 'l';
	st1 += 'l';
	st1 += 'o';
	st1 += " world";
	cout << st1.c_str() << endl;

	return 0;
}

         运行结果:

7、resize

void resize(int n,char ch='\0')
//n表示期望字符串的长度,如果n>_capacity则会扩容
//当n大于_size时,后面n-_size个空间赋予ch值
//n小于_size则表示字符串长度从_size缩小了n,但是空间不会缩容

        resize的作用是控制字符串的长度,可能会改变_size也可能会改变_capacity,注意的是:resize会增大空间但是不会缩小空间,原因就是缩小空间的代价太大,需要重新开辟一块空间然后拷贝数据,因此reszie缩小字符串长度时不会减小该字符串所在的空间。具体代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void reserve(size_t n)//扩容函数
		{
			if (n > _capacity)//只有n大于_capacity才需要扩容
			{
				char* temp = new char[n + 1];
				if (_str != nullptr)
				{
					strcpy(temp, _str);
					delete[] _str;
				}
				_str = temp;
				_capacity = n;
			}

		}

		void resize(int n,char ch='\0')
		{
			if (n < _size)//当n小于_size则更新_size并且直接将该位置的元素赋\0
			{
				_size = n;
				_str[_size] = '\0';
			}
			else//n大于_size时,_size后面的_size-n个值赋ch
			{
				if (n > _capacity)//n大于当前空间的容量则需要扩容
				{
					reserve(n);
				}
				while (_size!=n)
				{
					_str[_size] = ch;
					_size++;
				}

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

		const char* c_str()
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("hello world");
	ZH::string st2("hello world");
	st1.resize(20, 'x');
	st2.resize(5, 'x');
	

	cout << st1.c_str() << endl;
	cout << st2.c_str() << endl;

	return 0;
}

        运行结果:

        通过调试可以发现resize缩小字符串长度时,并没有对其空间容量进行缩小:

8、中间插入数据、删除数据

8.1 插入数据

        中间插入数据时要注意扩容问题,并且从某个位置插入时,原本该位置的数据要往后挪动,具体示意图如下:

        某个位置插入数据的代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void reserve(size_t n)//扩容函数
		{
			if (n > _capacity)//只有n大于_capacity才需要扩容
			{
				char* temp = new char[n + 1];
				if (_str != nullptr)
				{
					strcpy(temp, _str);
					delete[] _str;
				}
				_str = temp;
				_capacity = n;
			}
		}

		void Insert(size_t pos, char ch)//插入字符
		{
			assert(pos <= _size);//判断位置是否合规
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			size_t end = _size + 1;//从此处刚好可以把\0拷贝过去
			while (end > pos)
			{
				//采用的是指向位置的前一个位置的元素拷贝给指向位置
				_str[end] = _str[end - 1];
				end--;
			}

			_str[end] = ch;
			_size++;
		}

		void Insert(size_t pos, const char* str)//插入字符串
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			size_t end = _size + len;
			while (end >= pos + len)
			{
				//采用的是指向位置的前len个位置的元素拷贝给指向位置
				_str[end] = _str[end - len];
				end--;
			}

			strncpy(_str + pos, str, len);
			_size = _size + len;
		}

		const char* c_str()
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小
	};
}

int main()
{
	ZH::string st1("hello world");
	st1.Insert(3, 'z');
	st1.Insert(3, "aaa");

	cout << st1.c_str() << endl;

	return 0;
}

         运行结果:

8.2 删除数据

void erase(size_t pos, size_t n = npos)
//删除pos位置数据,n表示从pos位置起要删除n个数据
//npos表示-1,-1的正整数表示四十多亿,其实就是表示从pos位置开始后面数据全部删除

        npos可以用静态成员来定义,因为静态成员属于所有对象,属于整个类,删除的物理逻辑也和插入的物理逻辑相似,即删除一个数据后,后面的数据往前面挪动。

        删除数据的代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void reserve(size_t n)//扩容函数
		{
			if (n > _capacity)//只有n大于_capacity才需要扩容
			{
				char* temp = new char[n + 1];
				if (_str != nullptr)
				{
					strcpy(temp, _str);
					delete[] _str;
				}
				_str = temp;
				_capacity = n;
			}
		}

		void erase(size_t pos, size_t n = npos)//删除pos位置数据,n表示从pos位置起要删除n个元素
		{
			assert(pos <= _size);//判断要删除的位置是否合格

			if (n == npos || n + pos >= _size)//如果用的是缺省值nops,则表示从该位置起后面全部删除
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + n);//用拷贝的形式实现删除
				_size -= n;
			}
		}

		const char* c_str()
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小

		static size_t npos;//定义一个静态成员变量
	};
}

size_t ZH::string::npos = -1;//-1的正整数表示一个40多亿的数

int main()
{
	ZH::string st1("hello world");
	ZH::string st2("hello world");
	st1.erase(3, 7);//从位置3开始删除7个元素
	st2.erase(3);//从位置3开始后面全部删除

	cout << st1.c_str() << endl;
	cout << st2.c_str() << endl;

	return 0;
}

        运行结果:

9、查找数据

        逻辑就是遍历字符串,找到了就返回该元素的下标,没找到可以返回npos,示例代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		size_t find(char c, size_t pos = 0)//查找字符
		{
			assert(pos < _size);
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == c)
				{
					return i;//找到了返回该元素的下标
				}
			}
			return npos;//没找到返回npos
		}

		size_t find(const char* str, size_t pos = 0)//查找字符串
		{
			assert(pos < _size);
			char* poi = strstr(_str + pos, str);//复用strstr函数
			if (poi != nullptr)
				return poi - _str;//找到了通过指针-指针得到整数返回该数值
			else
				return npos;//没找到返回npos
		}

		const char* c_str()
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小

		static size_t npos;//定义一个静态成员变量
	};
}

size_t ZH::string::npos = -1;//-1的正整数表示一个40多亿的数

int main()
{
	ZH::string st1("hello world");
	cout << st1.find('l') << endl;
	cout << st1.find("wor") << endl;

	cout << st1.c_str() << endl;

	return 0;
}

        运行结果:

10、打印对象(流插入、流提取)

        以上打印字符串用的方法都是传首地址打印方式,可以对流插入、流提取符号:‘<<'和'>>'进行运算符重载,就可以直接使用‘<<'和'>>'打印对象。

        代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<assert.h>
using namespace std;

namespace ZH//把string放在命名空间中,放在与库里的string重名
{
	class string
	{
	public:

		string(const char* str = "")//构造函数初始化对象
			:_size(strlen(str))
		{
			_capacity = _size;

			//实际上会给\0开一个空间,但是capacity不记录该空间
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void reserve(size_t n)//扩容函数
		{
			if (n > _capacity)//只有n大于_capacity才需要扩容
			{
				char* temp = new char[n + 1];
				if (_str != nullptr)
				{
					strcpy(temp, _str);
					delete[] _str;
				}
				_str = temp;
				_capacity = n;
			}

		}

		void push_back(char ch)//尾插字符
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

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

		void append(const char* str)//尾插字符串
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			strcpy(_str + _size, str);
			_size += len;
		}

		//两个运算符重载构成函数重载
		string& operator+=(char ch)//尾插字符--运算符重载
		{
			push_back(ch);//复用尾插函数
			return *this;
		}

		string& operator+=(const char* str)//尾插字符串--运算符重载
		{
			append(str);//复用尾插函数
			return *this;
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		const char* c_str()const
		{
			return _str;
		}

		~string()//析构函数,释放_str申请的空间
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

	private:
		char* _str;//在堆上开辟一块空间用于存放字符串
		size_t _size;//记录字符串的总字符数
		size_t _capacity;//记录所开辟空间的大小

		static size_t npos;//定义一个静态成员变量
	};

	ostream& operator<<(ostream& out, const string& str)//流插入
	{
		cout << str.c_str()<< endl;
		return out;
	}

	istream& operator>>(istream& in, string& str)//流提取
	{
		str.clear();//每次输入数据的时候把之前数据清空

		char ch;
		ch = in.get();
		size_t i = 0;
		char buff[128];//用buff作为一个缓冲区的概念,减少每次+=字符串的消耗
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[128] = '\0';//因为是一个字符的往buff里面输入,因此buff是没有\0的,要手动添加
				str += buff;//数组满了就一次性+=到str
				i = 0;
			}
			ch = in.get();
		}

		if (i != 0)//把数组剩余的数据+=到str中
		{
			buff[i] = '\0';
			str += buff;
		}

		return in;
	}
}

size_t ZH::string::npos = -1;//-1的正整数表示一个40多亿的数

int main()
{
	ZH::string st1("ssssssssssssssssssssssssss");
	cin >> st1;
	cout << st1 << endl;

	return 0;
}

        运行结果:

        这里注意库里面的流提取是每次输入新的数据时,旧的数据会直接清空,因此每次调用流提取函数时会先调用clear()函数清空之前的数据。

结语:

        以上就是关于string类的实现和讲解,只有了解了string底层是如何实现的,才能帮助我们进一步了解和使用string类。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/291983.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实时交通标志检测和分类(代码)

交通标志检测和分类技术是一种基于计算机视觉和深度学习的先进技术&#xff0c;能够识别道路上的各种交通标志&#xff0c;并对其进行分类和识别。这项技术在智能交通系统、自动驾驶汽车和交通安全管理领域具有重要的应用前景。下面我将结合实时交通标志检测和分类的重要性、技…

天洑智能设计全系列产品完成银河麒麟操作系统适配!

近日&#xff0c;天洑软件智能设计全系列产品&#xff08;智能热流体仿真软件AICFD、智能结构仿真软件AIFEM、智能优化软件AIPOD、智能数据建模软件DTEmpower&#xff09;已成功完成银河麒麟桌面操作系统V10的适配工作。双方产品完全兼容&#xff0c;运行稳定、安全可靠、性能优…

若依项目(ruoy-vue)多模块依赖情况简要分析

主pom文件关键点分析 properties标签声明变量信息&#xff1a;版本号、编码类型、java版本spring-boot依赖形式&#xff1a;spring-boot-dependencies、pom、importdependencies中添加本项目内部模块&#xff0c;同时在modules中声明模块packaging打包选择pom设置打包maven-co…

鸿蒙4.0开发实战(ArkTS)-闹钟制作

闹钟功能要求 展示指针表盘或数字时间。添加、修改和删除闹钟。展示闹钟列表&#xff0c;并可打开和关闭单个闹钟。闹钟到设定的时间后弹出提醒。将闹钟的定时数据保存到轻量级数据库。 闹钟主界面 闹钟界面包括当前时间、闹钟列表、添加闹钟子组件&#xff0c;具体包括以下…

集合的三种遍历方式

迭代器&#xff08;Iterator&#xff09; 概述&#xff1a;Iterator 是个接口&#xff0c;迭代器是集合的专用遍历方式 使用方法&#xff0c;我们想要使用迭代器&#xff0c;必须首先得到集合对象&#xff0c;通过集合对象生成迭代器对象&#xff0c;才能进行集合的遍历 常用…

《操作系统导论》笔记

操作系统三个关键&#xff1a;虚拟化( virtualization) 并发(concurrency) 持久性&#xff08;persistence&#xff09; 1 CPU虚拟化 1.1 进程 虚拟化CPU&#xff1a;许多任务共享物理CPU&#xff0c;让它们看起来像是同时运行。 时分共享&#xff1a;运行一个进程一段时间…

vue-动态高亮效果

个人练习&#xff0c;仅供参考 实现如下效果&#xff1a;点击某块&#xff0c;某块变成其他颜色。 具体实现代码&#xff1a; 上边&#xff1a; <template><div><h3 style"color: #f69">动态高亮效果</h3><hr> <!-- 对象 -->…

FS4412系统移植及开发板启动过程

FS4412是基于samsung的arm Cortex-A9的Exynos4412的板子&#xff0c;Exynos4412采用了32nm HKMG工艺&#xff0c;是samsung的第一款四核芯片。 Windows装机过程&#xff1a; 1、准备Windows系统镜像、U盘启动盘 2、进入BIOS选择启动方式&#xff08;U盘启动&#xff09; 3、…

2024第一篇: 架构师成神之路总结,你值得拥有

大家好&#xff0c;我是冰河~~ 很多小伙伴问我进大厂到底需要怎样的技术能力&#xff0c;经过几天的思考和总结&#xff0c;终于梳理出一份相对比较完整的技能清单&#xff0c;小伙伴们可以对照清单提前准备相关的技能&#xff0c;在平时的工作中注意积累和总结。 只要在平时…

git提交操作(不包含初始化仓库)

1.进入到本地的git仓库 查看状态 git status 如果你之前有没有成功的提交&#xff0c;直接看第5步。 2.追踪文件 git add . 不要提交大于100M的文件&#xff0c;如果有&#xff0c;看第5步 3.提交评论 git commit -m "你想添加的评论" 4.push (push之前可以再…

解决Vue3 中Eharts数据更新渲染不上问题

解决办法就是让Dom节点重新渲染 定义一个变量 const postLoading ref(true); 请求数据前dom节点不渲染&#xff0c;获取完数据重新渲染

正定矩阵在格密码中的应用(知识铺垫)

目录 一. 写在前面 二. 最小值点 三. 二次型结构 四. 正定与非正定讨论 4.1 对参数a的要求 4.2 对参数c的要求 4.3 对参数b的要求 五. 最小值&#xff0c;最大值与奇异值 5.1 正定型&#xff08;positive definite&#xff09; 5.2 负定型&#xff08;negative defin…

Apache Doris (六十): Doris - 物化视图

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你学编程的个人空间-豹哥教你学编程个人主页-哔哩哔哩视频 目录

基于ssm的视康眼镜网店销售系统的设计与实现+vue论文

引 言 随着互联网应用的不断发展&#xff0c;以及受新冠病毒疫情影响&#xff0c;越来越多的零售行业将其销售方式从实体门店销售转向虚拟网店销售方向发展。中国互联网络信息中心(CNNIC)发布第48次《中国互联网络发展状况统计报告》显示&#xff0c;截至2021年6月&#xff0c…

Text-to-SQL小白入门(十一)DAIL-SQL教你刷Spider榜单第一

论文概述 学习这篇Text2SQLLLM的benchmark论文前&#xff0c;可以先学习一下基础的Text2SQL知识。 可以参考GitHub项目&#xff1a;GitHub - eosphoros-ai/Awesome-Text2SQL: Curated tutorials and resources for Large Language Models, Text2SQL, Text2DSL、Text2API、Text2…

前端开发_JavaScript基础

JavaScript介绍 JS是一种运行在客户端&#xff08;浏览器&#xff09;的编程语言&#xff0c;实现人机交互效果 作用&#xff1a; 网页特效 (监听用户的一些行为让网页作出对应的反馈) 表单验证 (针对表单数据的合法性进行判断) 数据交互 (获取后台的数据, 渲染到前端) 服…

漏洞复现-海康威视网络对讲广播系统远程命令执行漏洞(附漏洞检测脚本)

免责声明 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…

HackTheBox - Medium - Linux - Socket

Socket Socket 是一台中等难度的 Linux 机器&#xff0c;其特点是反转 Linux/Windows 桌面应用程序以获取其源代码&#xff0c;从那里发现其 Web 套接字服务中的“SQL”注入。转储数据库会显示一个哈希值&#xff0c;一旦破解&#xff0c;就会产生对该框的“SSH”访问。最后&a…

卷麻了,00后测试用例写的比我还好,简直无地自容...........

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 如何编写测试用例&#xff1f; 作为一个测试新人&#xff0c;刚开始接触测试&#xff0c;对于怎么写测试用例很是头疼&#xff0c;无法…

Spring中基于注解的IOC配置项目举例详解

文章目录 Spring中基于注解的IOC配置项目举例详解1、创建如下结构的Spring项目pom.xmldao层service层application.xmllog4j.properties 2、用于创建对象的常用注解2.1、Controller或Controller("user")声明bean,且id"user"2.2、Service或用Service("u…