学到string终于就不用像c语言一样造轮子了,接下来我们就模拟一下string方便我们更好理解string,首先我们都知道库里有个string,所以为了避免我们的string和库里的冲突,要用命名空间my_string将我们写的string包含在内。string的成员变量和我之前写的顺序表差不多,首先是一个指针指向一块存放字符串的空间,还有_size记录该空间内的有效字符个数,不包括\0,然后就是_capcity记录该空间能存放有效字符的个数,当然,实际上由于我们末尾要放一个\0,所以我们在实现的时候实际开辟空间比容量稍微大一点。库里的容量则更加充裕。
     
一:构造函数,析构函数和赋值运算符重载
1.构造函数,析构函数实现:
#pragma once
#include<iostream>
#include<string>
using namespace std;
namespace my_string
{
	class string
	{
	public:
		
		//构造函数
		string(const char*str="")//默认用空串初始化
		{
          都是内置类型,在函数体内初始化也挺方便,免得初始化列表的
            书写顺序和声明顺序不同导致出错
			int len = strlen(str);计算要开辟的空间
			_str = new char[len + 1];
			_capcity = len;
			_size = len;
			strcpy(_str, str);
			_str[_size] = '\0';末尾补\0
		}
		~string()
		{
			delete[]_str;要注意的是new[]对应delete[],new对应delete,原因不好解释。
			_size = 0;
			_capcity = 0;
			_str = nullptr;
		}
		const char* c_str()const将string以c语言的字符串形式返回
		{
			return _str;
		}
	private:
		size_t _capcity;
		size_t _size;
		char* _str;
	};
};2.拷贝构造函数
string(const string& Src)
{
	_str = new char[Src._size+1];
	_size = Src._size;
	_capacity = _size;
	memcpy(_str, Src._str, Src._size+1);//将\0也一同拷贝
}
3.测试构造函数
void TestString1()
{
	my_string::string s1("hello world");
    cout << s1.c_str() <<endl;
}首先c._str()函数将string s1以字符串的形式返回,也就是返回指针,并且cout内部有内置类型的输出重载,所以可以将其打印出来。
const修饰对象只会限制成员变量不被修改,却并不限制修改其成员指向的空间,也就是说如果c_str()返回指针不加const修饰,我们可以在外部用该指针修改const对象,这是不合理的,又为了保证const对象和普通对象都可以调用这个函数,所以对c_str()这个函数用了两个const修饰
4.赋值运算符重载
string.h
string& operator=(string tmp)
{
	swap(tmp);
	return (*this);//支持连续赋值
}
main.c
string s1;
string s2;
s1=s2;	此时s1=s3被编译器转为s1.operator(s2), s2是传值传参,要调用拷贝构造,原因我在我的博客类的六大成员函数中曾提及,tmp是s2的深拷贝对象,我们交换了s1和tmp的成员,这样s1指向的空间就是s3深拷贝后的了,也就完成了赋值,最妙的是我们把s1要析构的空间给了局部对象tmp,让其在函数调用结束时销毁,不用我们手动delete[]。
void swap(string s)
{
	//复用库的swap函数
	std::swap(_str, s._str);//直接交换两个对象成员
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}要注意的是我们不是直接调用库里的swap函数交换两个string对象,而是自己写个swap,内部调用库里的swap函数来交换string对象的成员来达到交换对象的目的。原因是库里的swap函数是如下的:
class T
swap(T a,T b)
T c(a);
a=b;
b=c;当我们调用赋值的时候,用了swap函数,swap内部又是赋值,赋值内部又用swap,无穷递归调用,死循环,千万注意。
二:string的遍历
1:实现迭代器和范围for
在第一次使用迭代器时是std::iterator it,现在我才意识到原来iterator实质上是char*的typedef,下面的begin()函数如果被const修饰,const对象可以调用,非const对象也可以调用,但是这样的话返回类型就只能是const char*了,那对于普通对象来说就无法修改了,所以要再实现一个const修饰的begin()函数,下面还重定义了const char*就是为了服务const对象。
public:
		typedef char* iterator;
		typedef const char* const_iterator;
        
        char& operator[](int i)
		{
			return _str[i];
		}
		const char& operator[](int i)const
		{
            //_size,_str,_cap不可修改,因为this指针此时是const string*const
			 //若是const不修饰char&,_str指向的字符串可修改
			return _str[i];
		}
		iterator begin()  对const对象不适用,所以要再写一个
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
        
	不会修改成员变量的成员函数都可以用const修饰,这样就方便const对象和普通对象的调用了。
		size_t size()const//获取string中的有效字符个数
		{
			return _size;
		}
		char* c_str()const//将string以c语言的字符串形式返回
		{
			return _str;
		}如下为const对象的begin()和end()函数,返回类型是const_char*,为了和char*的重命名作区分,命名为const_iterator,注意:const对象的迭代器不可修改,所以返回的指针要用const修饰,否则该指针同样在外部会被用来修改const对象。
const_iterator begin()const
		{
			return _str;
		}
		const_iterator end()const
		{
			return _str + _size;
		}2.测试遍历功能
在准备好了成员函数后,就可以测试遍历功能了,下面提供三种遍历方式。
   for (size_t i = 0; i < s1.size(); i++)//测试size()和[]重载
	{
		cout << s1[i];//测试[]重载
	}
	cout << endl;
	//迭代器
	//my_string::string::iterator it = s1.begin();
      auto it = s1.begin();
 是不是感觉it的类型名特别长,这个时候auto自动推导类型名的作用就很舒服了
	while (it != s1.end())
	{
		cout << *it;
		it++;
	}
	cout << endl;
	范围for,范围for在底层被替换成上述的迭代器,而且是按照既定格式来替换的,
如果我们模拟的begin函数名改为Begin,范围for就无法运行,因为范围for被替换后是用begin函数名访问。
	for (auto ch : s1)
	{
		cout << ch;
	}
	cout << endl;三:string的添加字符
1.push.back尾插一个字符
void Reserve(size_t n=0)//扩容
{
	char*tmp = new char[n + 1];//预留一个位置给\0
	memcpy(tmp, _str, _size);
	delete[]_str;
	_str = tmp;
	_capcity = n;
}上面的扩容函数中我们用memcpy来把原先存在的字符串复制到tmp中,之所以不用strcpy,是因为我们考虑string中可能会存入"hello \0world'。
void push_back(char c)//尾插一个字符
{
	if (_size == _capcity)//扩容
   {
		Reserve(_capcity==0?4:2*_capcity);
                                    当string对象为空串时,给个初始容量
				                     其余情况扩大二倍   
   }
	   _str[_size++] = c;
	   _str[_size] = '\0';//末尾补充\0
}
		
string& operator+=(char s)
{
	push_back(s);
	return (*this);
}2.append尾插一个字符串
void append(const char* s)//尾插一个字符串
{
	int len = strlen(s);
	if (_size + len > _capcity)先判断size+len是否会超出容量
	{
		Reserve(_size+len);  扩容到_size+len而不是2*_capcity,
                           2*_capcity不一定大于_size+len 
	}
	else
	{
		for (int i = 0; i < len; i++)
	    {  
			_str[_size++] = s[i];//插入字符
		}
	}
}
+=运算符重载,便于调用,且代码看起来更加规整
string& operator+=(const char* s)
{
	append(s);
	return (*this);
}
代码中使用+=运算符重载比直接append和push_back函数更加美观,如下。
s += 'w';
s += 'o';
s += "hello ";
s += "world";
对比
s.push_back('w');
s.push_back('o');
s.append("hello");
s.push_back("world");3.在pos位置添加字符或者字符串
 string& insert(size_t pos,char ch,size_t n)
{
	assert(pos <= _size);
	//判断容量
	if (_size + n > _capacity)
   {
	   Reserve(_size + n);
   }
	//挪动数据
	size_t end = _size;//从\0开始移动
	while (end>=pos && end!=npos)    end为size_t,当pos=0时,end无法跳出循环
                                            ,所以添加一个判断条件
   {
	  _str[end+n] = _str[end];
	  end--;
   }
	     //插入数据
	 _size += n;
	while (n)
	{
	    _str[pos++] = ch;
		n--;
	}
	return (*this);
}
 string& insert(size_t pos,const char*src)
{
	assert(pos <= _size);
	int len = strlen(src);
	//判断容量
	if (_size + len> _capacity)
   {
	  Reserve(_size + len);
   }
	   //挪动数据
	size_t end = _size;//从\0开始移动
	while (end >= pos && end != npos)
   {
		str[end + len] = _str[end];
		end--;
   }
		//插入数据
	 _size += len;
	for (int i = 0; i < len; i++)
	{
		_str[pos++] = src[i];
	}
		return (*this);
}四:删除string对象的字符
len的缺省值为npos,由于npos为-1,对于无符号数len来说是四十几亿,而实际上字符串都不会这么大,也就表示删除pos位置后全部的字符。
      void erase(size_t pos=0,size_t len=npos)   删除pos位置以及之后len个字符
                                          
  		{
			if (len==npos||pos + len>= _size)//删到末尾
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
               挪动覆盖数据
				size_t begin = pos + len;
				while (begin <= _size)    将后面未删除字符以pos为起点往后放
				{                         包括了\0
					_str[pos++] = _str[begin++];
				}
				_size -= len;
			}
		}五:查找字符
1.从指定位置查找字符串,返回字符串第一次出现位置的首字符地址
size_t find(const char*goal,size_t pos=0)
		{
			if (pos >= _size)
				return npos;//下标非法,返回-1
			char*begin = strstr(_str+pos, goal);//复用库函数
			if (begin == NULL)
				return npos;//找不到,返回-1
			return begin - _str;
		}
	2.从指定位置查找字符,返回字符下标
	size_t find(char goal, size_t pos = 0)
		{
			if (pos >= _size)
				return npos;//下标非法,返回-1
			for (size_t i =pos; i < _size; i++)//遍历判断
			{
				if (_str[i] == goal)
					return i;
			}
				return npos;//找不到,返回-1
		}六:截取字符串
string substr(size_t pos=0,size_t len=npos)
{
    assert(pos <= _size);
    string ret;
	if (len==npos||pos + len > _size)//截取到末尾
   {
        此处有个细节是先判断len是否等于npos,可以免得当len=npos时,len+pos出现栈溢出
        erase函数处同理
			
		len = _size - pos;//处理len
   }
   for (size_t i = pos; i < len+pos; i++)
  {
		ret += _str[i];//ret无需处理\0,+=会处理
  }
	return ret;
}七:流插入流提取
1.流插入
可以选择将流提取和流插入函数放在全局域或者命名空间内,最好直接放在命名空间内,免得冲突,但是放在命名空间的时候,若在命名空间内声明,外写定义,定义处要指定命名空间域,否则函数调用时编译器看到命名空间内有个声明,会认为有个定义,虽然没找到,而全局处的函数定义又没有指定命名空间,就不会认为是命名空间中那段声明对应的函数定义,这样函数调用时就会出现调用不明确。
ostream& my_string::operator<<(ostream& out, const my_string::string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i]; s[i]调用的是类的公有函数,
                        用公有函数访问string对象的字符不受访问限定符限制
	}
	out << endl;
	return out;
}
2 流提取
istream& my_string::operator>>(istream& in, my_string::string& s)
{
	s.clear();
	char ch = 0;
	ch = in.get();
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();  处理有效字符前的分隔符
	}
	int i = 0;
	char arr[128] = { 0 };  模拟缓冲区,防止s频繁扩容
	while (ch != ' ' && ch != '\n')//当ch读到分隔符,一个string对象读取结束
	{
		arr[i++] = ch;//先存到数组中去
		ch = in.get();
		if (i == 127)//数组满了后,一次性填入s中
		{
			arr[i] = '\0';
			i = 0;
			s += arr;
		}
	}           可能i小于127,此时又读到了分隔符,要在外面判断是否要处理数组中剩下的字符
	if (i != 0)
		s += arr;
	return in;
}字数有点多,但是string是我们学习c++的关键,对于理解vector和list有着非常大的作用,个人的一些理解希望对大家有帮助。


















