string常用接口及模拟实现


string相当于前面学的字符串,为了更方便操作,C标准库中提供了一些str系列的库函数。那么下面就来看看这些接口。

一、string模拟实现概括

namespace s
{
	class string
	{
	public:
		string(const char* str = "");

		string(const string& s);

		void swap(string& s);

		~string();

		string& operator=(const string& s);

///

		size_t size() const;

		size_t capacity() const;

		bool empty() const;

		void clear();

		void reserve(size_t n);

		void resize(size_t n, char c = '\0');

///
		char& operator[](size_t pos);

		const char& operator[](size_t pos) const;

		typedef char* iterator;

		iterator begin();

		iterator end();

		typedef const char* const_iterator;
		const_iterator begin() const;

		const_iterator end() const;

		const char* c_str() const;

		size_t find(char c, size_t pos = 0) const;

		size_t find(const char* s, size_t pos = 0) const;

		string substr(size_t pos = 0, size_t len = npos);

///

		void push_back(char c);

		void append(const char* str);

		string& operator+=(char c);

		string& operator+=(const char* str);

		string& insert(size_t pos, const char* str);

		void erase(size_t pos, size_t len = npos);

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		static const int npos;  //静态区变量在类内声明,类外定义
	};
	const int npos = -1;   //类外定义时,不需要再写static关键词

	namespace s
{
	class string
	{
	public:
		string(const char* str = "");

		string(const string& s);

		void swap(string& s);

		~string();

		string& operator=(const string& s);

///

		size_t size() const;

		size_t capacity() const;

		bool empty() const;

		void clear();

		void reserve(size_t n);

		void resize(size_t n, char c = '\0');

///
		char& operator[](size_t pos);

		const char& operator[](size_t pos) const;

		typedef char* iterator;

		iterator begin();

		iterator end();

		typedef const char* const_iterator;
		const_iterator begin() const;

		const_iterator end() const;

		const char* c_str() const;

		size_t find(char c, size_t pos = 0) const;

		size_t find(const char* s, size_t pos = 0) const;

		string substr(size_t pos = 0, size_t len = npos);

///

		void push_back(char c);

		void append(const char* str);

		string& operator+=(char c);

		string& operator+=(const char* str);

		string& insert(size_t pos, const char* str);

		void erase(size_t pos, size_t len = npos);

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	public:
		static const int npos;  //静态区变量在类内声明,类外定义
	};
	const int npos = -1;   //类外定义时,不需要再写static关键词
	
///

	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& _cout, const string& s);

	istream& operator>>(istream& _cin, string& s);

	istream& getline(istream& _cin, string& s);
};
	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& _cout, const string& s);

	istream& operator>>(istream& _cin, string& s);

	istream& getline(istream& _cin, string& s);
};

二、模拟实现函数接口

1、(constructor)函数名称

①.构造函数

	//1.模拟实现string(const char* s)构造函数
		string(const char* str = "")
			:_size(strlen(str))
		{
			_capacity = _size;
			//开空间要多开一个存放开空间的数量,后面delete有需求
			_str = new char[_capacity + 1];
			//开出空间之后,把字符串str拷贝给我们新创建的string
			strcpy(_str,str);
		}

②.拷贝构造函数

拷贝构造可以分为浅拷贝和深拷贝。我们这里写的是深拷贝如果不写,系统自动调用的就是浅拷贝,也叫做值拷贝,即按字节进行拷贝
在这里插入图片描述
虽然浅拷贝和深拷贝都拷贝出同样的值,但指向的空间不一样!
如果是浅拷贝,在调用析构函数就会出现问题。因为ptr1和ptr2都指向同一块空间,析构会导致同一块空间被释放两次。与此同时,改变一个指针就会影响到另一个指针。

    //2.模拟实现string(const string&s)拷贝构造函数
		string(const string& s)
		{
			//这里与前面不同,前面是个字符串还不是string。所以前面还得先去构造。
			//在这里就可以直接访问string里面的私有变量了
			_str = new char[s._capacity + 1];
			_size = s._size;
			_capacity = s._capacity;
			strcpy(_str, s._str);
		}    
		
    //那么这里,我们还可以对拷贝构造函数进行一次改进
		string(const string& s)
		{
			//这里注意,我们是构造一个tmp,而不是拷贝一个tmp
			//我们传入的参数是一个char*类型的,并不是string类型的,这里要加以区分
			string tmp(s._str);
			//再把tmp和调用该函数的对象进行调换就可以了
			//但是这个swap得我们自己实现,因为在std库中swap函数形参没有两个string类型
            //因此我们得自己实现一下swap函数,也很容易实现,交换对象中的成员就可以了
			swap(tmp);
		}   //这就相当于让tmp作为一个傀儡调用构造来接收,再把接收到的数据跟调用对象交换
		
		void swap(string& s)
        {
	       std::swap(_str, s._str);
	       std::swap(_size, s._size);
	       std::swap(_capacity, s._capacity);
        }

③.析构函数

    //3.模拟实现~string 析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

④.赋值运算符重载(以及swap的实现)

这里有个点要注意,大家可以看到代码中并不是直接把s传给_str,而是通过tmp进行的。因为:_str的空间如果小于s,将s拷贝过去就会导致越界。 如果_str的空间大于s,会导致空间浪费。

   //4.模拟实现赋值运算符重载
   string& operator=(const string& s)
   {
	   //两个string无法直接传输,我们通过临时变量tmp来储存空间以及空间中的数据
	   char* tmp = new char[s._capacity + 1];
	   strcpy(tmp, s._str);

	   //我们先释放掉原来的那块空间,以免造成内存泄漏,再把 tmp指向的空间给到_str
	   delete[] _str;
	   _str = tmp;
	   _size = s._size;
       _capacity = s._capacity;

	  return *this;
   }

2、string类对象的容量操作

①.size

返回字符串中字符的个数

       //由于这里不改变内部成员变量,建议加const
		size_t size() const
		{
			return _size;
		}

②.capacity

返回字符串的容量

		//由于这里不改变内部成员变量,建议加const
		size_t capacity() const
		{
			return _capacity;
		}

③.empty

判断是否为空字符串

		//由于这里不改变内部成员变量,建议加const
		bool empty() const
		{
			if (_size == 0)
				return true;
			else
				return false;
		}

④.clear

clear用来清空有效字符

		//clear()清理的是size,即有效字符长度,而capacity并没有变。
		void clear() 
		{   
		    _size = 0;
			_str[0] = '\0';  //这里不要忘记给\0
		}

⑤.reserve

reserve用于扩容
如果n比原来的空间要小,则不发生任何变化。n比原空间大时,才去扩容

		void reserve(size_t n)
		{
			//如果n比原来的空间要小,则不发生任何变化。n比原空间大时,才去扩容
			if(n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);

				delete[] _str;
				_str = tmp;
				_size = _capacity = n;
				_str[n] = '\0';  //不要忘记给\0
			}
		}

⑥.resize

用来改变字符串的size值。如果预改变值比原size小,则把size缩小。 如果比size大,则插入字符(可传参,否则默认斜杠0)进去,如果容量都小于预改变值,则会先进行扩容。

		void resize(size_t n, char c = '\0')
		{
			if (n <= _size)
			{
				_str[n] = '\0';
				_size = n;   //这里是直接变成n,不是size-n,不要弄错了
			}
			else
			{
				reserve(n); //大于容量扩容,小于容量无影响
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = c;  //将给定的参数c依次放到_str中
				}
				_str[n] = '\0'; //依然是记得要放\0在末尾
				_size = n;  //size也随之改变了,一定不要忘记了!!
			}
		}

3、string类对象的访问及遍历操作

①.operator[]

string字符串有些像我们的数组,是可以通过下标来访问元素的
该操作符返回pos位置的字符

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		//再写一个const类型的,以便const对象也可以使用
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}
//这里不建议只写const的版本的,因为我们可能会对其进行访问修改

②迭代器

string类的迭代器是一个类似于指针的东西,但我们不能把迭代器都认作指针,我们比喻成指针可以方便我们理解!

typedef char* iterator;   //这就是迭代器
	
// begin用来返回字符串的第一个位置
iterator begin()
{
	return _str;
}
// end用来返回字符串最后一个位置的下一个位置,即'\0'
iterator end()
{
	return _str + _size;
}

//与operator[]类似,我们还需要写一个const类型的函数以便使用
typedef const char* const_iterator;   //迭代器
		
const_iterator begin() const
{
	return _str;
}
const_iterator end() const
{
	return _str + _size;
}
//这里不建议只写const的版本的,因为我们可能会对其进行访问修改

③范围for

范围for的底层是依据迭代器来的

void test1_string()
{
	s::string s1("hello world");

	//迭代器的使用
	s::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it; //hello world
		it++;
	}
	cout << endl;

//范围for:从s1的第一个字符开始往后遍历到最后一个字符,ch会自动往后走
	for (auto& ch : s1) //加上引用相当于别名,还可以对ch进行修改
	{
		cout << ch;  //hello world
	}
	cout << endl;
}

④c_str

c_str用于获取c类型的字符串,直接返回字符串即可

		const char* c_str() const  
		{
			return _str;
		}
//这里建议加上const,写一个就够了,const和非const对象都可以使用
//因为这里只是提取这个字符串,并不会对其修改

⑤find

返回c在string中第一次出现的位置

		size_t find(char c, size_t pos = 0) const
		{
		    //要记得断言pos位置
			assert(pos < _size);
			for (size_t i = 0; i < _size; i++)
			{
				if (_str[i] == c)
					return i;
			}
			return npos;
		}

返回子串s在string中第一次出现的位置

		size_t find(const char* s, size_t pos = 0) const
		{
			//要记得断言pos位置
			assert(pos < _size);

			const char* ch = strstr(_str + pos, s); //pos不要掉了
			if (ch)
			{
				return ch - _str; //两指针相减就是间隔
			}
		
			return npos;
		}

⑥substr

substr用来获取字符串的一段子串,从pos位置开始往后len个字符,注意是从pos位置开始往后数len个字符(强调)
如果len=npos,或者len > _size - pos,则从pos位置往后一直到整个字符串结尾

		string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			string sub;
			if (len == npos || len > _size - pos)
			{
				for (int i = pos; i < _size; i++)
				{
					sub += _str[i];
				}
			}
			else
			{
				for (int i = pos; i < pos + len; i++)
				{
					sub += _str[i];
				}
			}
			return sub;
		}

4、string类对象的修改操作

①push_back

即顺序表里的尾插,这里为在字符串最后一个位置插入字符

		void push_back(char c)
		{
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			//这里不能写成if-else,因为下面这几条语句是一定得执行的
			_str[_size] = c;
			_size++;
			_str[_size] = '0';	  //依旧不要忘记\0	
		}

//除此之外,我们也可以复用insert尾插入字符
		insert(_size,c);

②append

append用来在字符串的末尾再追加一串字符串

		void append(const char* str)
		{
			size_t len = strlen(str);
			/*if (_capacity < _size + len)*/  
			 //这条语句不宜写,
			//因为字符串如果很长的话,可能会出现数据溢出的情况
			if(_capacity-_size<len)   //采用移项的写法
			{
				reserve(_size + len);
			}
			// strcpy可以把选定开始拷贝的位置,并且
			//会把\0也给拷贝进来,
			//因此在结尾处我们就不用再去给\0了。
	// 这里也可以使用strcat,因为都是从末尾开始追加字符串,
	//同样strcat也会把\0也给拷贝进来。
			strcpy(_str + _size, str);  
			_size += len;
			//这里结束之后就不用像之前一样给\0了

            //这里也可以使用后文的insert追加字符串来完成
            insert(_str, str);
		}

③operator+=

该操作符用于在字符或字符串后追加字符或字符串

		string& operator+=(char c)
		{
			push_back(c);  //我们直接复用push_back就可以了
			return *this;
		}

		string& operator+=(const char* str)
		{
			append(str);   //我们直接复用append
			return *this;
		}

④insert

insert的作用是在指定的pos位置往后插入字符或字符串。

拷贝字符:

string& insert(size_t pos, char c)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	size_t end = _size + 1;  //这里还得包含\0的位置,因此+1
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = c;
	_size++;
	//这里要注意一个点,下面这个循环是走不通的!!
//假设pos==0,最后一次循环就是_str[1] = _str[0],end==0
//再之后end--变为-1,但我们可以看到pos是size_t类型的,
//属于无符号数,那么循环不会停止,会持续地进行,造成死循环
//解决方法一种是按上面写的方法来,还有一张就是强制转换,譬如下面优化后的版本
	size_t end = _size;
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}

	//优化后:
	size_t end = _size;
	while (end >= (int)pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}
}

拷贝字符串:

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			int len = strlen(str);
			if (_capacity - _size < len)
			{
				reserve(_size + len);
			}

			int end = _size + len;
			while (end > pos + len - 1) //这里-1,是为了end能够顺利走到
			{                           //我们需要的位置,大家可以结合图理解
				_str[end] = _str[end - len];
				--end;
			}                           

			strncpy(_str + pos, str, len); //这里使用strncpy,是为了
			   _size += len;               //不把str里的\0拷贝过去
		}

在这里插入图片描述
在这里插入图片描述

⑤erase

erase用来从pos位置往后删除len个字符
如果len=npos或者pos+len>_size,意味着pos后面的字符都要被删除
否则就是包含在_size里面,我们就可以通过移动数据或者strcpy的方式完成

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			if (len == npos || len > _size - pos)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
                //_str[_size] = '\0'; 这里不需要写这一步,
                //因为pos+len<_size,所以\0一直都还在
			}
		}

5、非成员函数

①关系运算符

关系运算符主要是指<,>,==,<=,>=,!=六种。

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 strcmp(s1.c_str(), s2.c_str()) > 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 || s1 == s2;  //注意这里是或符号!
}

bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

bool operator!=(const string& s1, const string& s2) 
{
	return !(s1==s2);
}

②<<流插入操作符

之所以写这个操作符是因为内置类型可以直接cout,但是我们string不可以

ostream& operator<<(ostream& _cout, const string& s)
{
	for (auto& ch : s)
	{
		_cout << s;
	}
	return _cout;
}

③>>流提取操作符

istream& operator>>(istream& _cin, string& s)
{
	//每次调用都需要通过clear来清除原字符串的数据,这样往后才能插入新的数据
	//否则就会导致新老数据叠在一起,达不到预期效果
	s.clear(); 

	char ch;

	//_cin >> ch;  //流提取不能获取空格或换行符
	ch = _cin.get(); //所以我们采用get()函数
	while (ch != ' ' && ch != '\n') //遇到空格或者换行就退出
	{
		s += ch;
		ch = _cin.get();
	}
	return _cin;
}

这个写法有缺陷,如果插入数据很大就会导致多次扩容。但这里不建议使用reserve来给定内存,因为如果插入数据很小,就会导致空间浪费。 只要string对象不销毁,那么这些不使用的空间就一直存在,堆积多了就会严重浪费!
所以我们采用一种新方法:通过开辟一个128的数组来解决:

istream& operator>>(istream& _cin, string& s)
{
//每次调用都需要通过clear来清除原字符串的数据,这样往后
//才能插入新的数据,否则就会导致新老数据叠在一起,达不到预期效果
	s.clear();

	char ch;
	//_cin >> ch;  //流提取不能获取空格或换行符
	ch = _cin.get(); //所以我们采用get()函数
	char buff[128];
	
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127) //这里相当于是buff数组的最后一个位置,
		//如果到这个位置了,就把buff数据给到s,并把i重置为0
		{
			buff[127] = '\0'; 
			s += buff;
			i = 0;
		}
		ch = _cin.get();
	}
	if (i > 0) //这里是i++后,ch不符合条件退出来的,所以
	//这个i就相当于是s最后一个位置的下一个位置了,直接插入\0
	{
		s += buff;
		s[i] = '\0';
	}

	return _cin;
}

④getline

getline与流提取不同的是,getline是获取一行的字符。
也就是说我们可以在上面流提取的代码加以修改,即把遇到空格退出循环的条件去掉,则可以达到getline的效果!

	istream& getline(istream& _cin, string& s)
	{
		s.clear();

		char ch;
		char buff[128];
		ch = _cin.get();
		size_t i = 0;
		//[0,126] 共127个字符
		
        //我们只需要把遇到空格退出循环的条件去掉就可以了
		while (ch != '\n')  
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[127] = '\0';
				s += buff;
				i = 0;
			}
			ch = _cin.get();
		}
		if (i > 0)
			s += buff;

		return _cin;
	}

制作不易,求点赞关注!❀❀

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值