string的模拟实现

上篇,我们已经了解了string的基本内容功能了,今天我们来具体模拟实现一下来助力为我们更加深刻地理解string的知识。由于string的接口很多,这里只是模拟常见的几个。

本篇使用到了多次多个字符串的函数,可以先复习复习:

字符串与内存函数的知识点总结-优快云博客

ps:为什么接下来我们使用的是memcpy/memcmp开头的字符串函数,而不使用str开头的字符串函数?

因为strcpy/strcmp开头的是以‘\0’为中止结束字符,而我们的字符串并不是以字符串'\0'结束的,而是根据_size()的大小而决定的。所以我们接下来使用的基本mem开头,除非对'\0'没有影响的地方会使用一下str开头的

我们在C++第一篇(入门基础)中讲到了namespace的命名空间定义,这里我们就使用到了,避免与string里面的命名 冲突。

基本框架:

namespace bai
{
    //创建类
    class string
    {
     private:
        char*_str;
        size_t _size;
        size_t _capacity;        
    };
}

讲解:为什么会使用到上面的几个成员变量呢?

1._str是毫无疑问的,string嘛,字符串肯定需要的.

2.另外,这个框架是不是有点熟悉,它好像顺序表的定义?是的,确实跟顺序表很相似,只不过顺序表是整形,这里是字符串而已。

3.我们在模拟过程中是不是要知道字符串的大小长度,所以有个_size

4.也要判断空间是否满的情况,需要用到_capacity(最大容量)。

我们类是有6个默认构造的

C++——类与对象2-优快云博客

四个默认构造:

构造函数:

namespace bai
{
    //创建类
    class string
    {
    public:
         string(const char* str)
	        :_size(strlen(str))
	        ,_capacity(_size)
	        , _str(new char[_capacity + 1])
        {
	        strcpy(_str, str);
        }
        
     private:
        char*_str;
        size_t _size;
        size_t _capacity;        
    };
}

当我们写出了这个初始化时,这是有问题的。为什么?

因为我们的初始化列表声明的顺序不是按照代码的顺序执行下来的,而是根据类的成员变量的声明顺序来执行的:也就是说,如下图:先执行3->1->2.

那么,我们再来看,执行3的时候,由于_capacity还没有初始化,还是个随机值,所以开出来的空间也是随机值,1和2是正常的。

所以,我们为了解决这个问题:

方法一:

namespace bai
{
    //创建类
    class string
    {
    public:
         string(const char* str)
	        :_str(new char[strlen(_str)+ 1])
            ,_size(strlen(str))
	        ,_capacity(_size)
	       
        {
	        strcpy(_str, str);
        }
        
     private:
        char*_str;
        size_t _size;
        size_t _capacity;        
    };
}

方法二:改变成员变量声明的顺序

namespace bai
{
    //创建类
    class string
    {
    public:
         string(const char* str)
	        :_size(strlen(str))
	        ,_capacity(_size)
	        , _str(new char[_capacity + 1])
        {
	        strcpy(_str, str);
        }
        
     private: 
        size_t _size;
        size_t _capacity;  
        char* _str;
            
    };
}

通常我们会使用第二种,(第一种看这别扭)。

当我初始化一个无参的string时

我们的构造函数写法:

string()
  :_size(0)
  ,_capacity(0)
  ,_str(new char[1])
{
	_str[0] = '\0';
}

int main()
{
    bai::string s1;
    return 0;
}

注意:这里可不能写成

string()
  :_size(0)
  ,_capacity(0)
  ,_str(nullptr)
{
	
}


1. 内存管理问题:如果将  _str  初始化为  nullptr  ,后续在对这个  string  对象进行操作(比如尝试访问  _str  指向的内容)时,会导致未定义行为。因为  nullptr (或  NULL )表示不指向任何有效的内存地址,当代码中出现类似  cout << _str;  这样的语句时,程序就会崩溃。而一个合理的默认构造的  string  对象,即使是空字符串,也应该有一个合法的内存空间来存放字符串结束标志  '\0' ,所以需要动态分配至少一个字节的内存来存储  '\0' ,像原来正确的写法  _str(new char[1])  那样。


2. 后续操作的一致性:在  string  类的其他成员函数(如析构函数、拷贝构造函数、赋值运算符重载等)的实现中,通常会假设  _str  指向一个合法的内存空间(即使是长度为 0 的空字符串情况)。如果初始化为  nullptr ,那么在这些函数中就需要额外添加大量的特殊处理逻辑来判断  _str  是否为  nullptr ,这会使代码变得复杂且容易出错,破坏了代码的一致性和简洁性。
3. 符合语义:一个空的  string  对象从语义上来说应该是有一个表示空字符串的合法存储,而不是没有指向任何内存的  nullptr  指针。将  _str  初始化为指向包含  '\0'  的内存空间,更符合  string  类表示字符串的语义。
 
在 C++ 中,当使用  nullptr ,一般是在指针可能不指向任何有效对象的情况下,而对于表示字符串存储的指针  _str ,在默认构造时应该确保它指向一个合法的内存(哪怕只是存放  '\0' ) 。

此外,我们还可以将上面两部分内容合并成一起:

       1.//string(const char* str = '\0')
	    2.//string(const char* str = nullptr)
	    3.//string(const char* str = "\0")
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

分析为啥1-3不可以:

1.'0'是一个字符,而左边那里是字符串,类型不匹配。

2.如上面讲到的

3."\0",这里会重复了,因为这里"" 里面就会隐藏有一个\0了,如果再次另外添加,就是有两个\0。

 拷贝构造:

这里的拷贝构造就不能直接赋值拷贝(浅拷贝),

而是需要深拷贝。

深拷贝:

1.动态开辟一个新的空间

2.复制拷贝原来类对象动态开辟空间的内容到新的空间

3.拷贝原来类对象其他内置类型成员的变量。

string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

析构函数:

	~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

赋值重载函数 

s1=s2

这里跟拷贝构造差不多。 

这里有两种方法:

一:传统的方法:

string& operator=(const string& s)
{
	if (this != &s)
	{
        开一个临时字符串数组
		char* tmp = new char[s._capacity + 1];
		拷贝数据到新的空间
        memcpy(tmp, s._str, s._size+1);
		释放原来的空间
        delete[] _str;
        指向新空间
		_str = tmp;
        更新_size大小
		_size = s._size;
        更新_capacity大小
		_capacity = s._capacity;
	}

		return *this;
}

 二:新方法:使用swap交换函数

string& operator=(const string& s)
{
	if (this != &s)
    {
    	string tmp(s);

		std::swap(tmp._str);
        std::swap(tmp._size);
	    std::swap(tmp._capacity);
	
	}

	return *this;
}
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
	if (this != &s)
    {
    	string tmp(s);

		//this->swap(tmp);
		swap(tmp);
	}

	return *this;
}

或者:

	

    	string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}

需要注意的是:

不能下图那样,因为:重载参数传递是引用传参,我们使用=赋值,往往是不希望修改给之前的值的,如果这里传引用,就要构造一个temp来当这个工具人

string& operator=(const string& s)
{
	if (this != &s)
    {
		std::swap(s._str);
        std::swap(s._size);
	    std::swap(s._capacity);
	
	}

	return *this;
}

string内的函数

返回_size个数大小

size_t size() const
{
	return _size;
}

因为_size的个数大小出了作用域也是固定不变的,所以加了个const。

c_str函数

因为我们再之后的过程中会调用到库里面的东西,且为了更好地跟c语言的一些接口配合

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

出了作用域不会发生改变,所以加了const。防止权限放大。

reserve

void reserve(size_t n)
{
    //判断空间是否满
	if (n > _capacity)
	{
		
		char* tmp = new char[n + 1];
		//strcpy(tmp, _str);
		memcpy(tmp, _str, _size+1);

		delete[] _str;
		_str = tmp;
		_capacity = n;
    }
}

这里跟赋值重载函数的传统方法几乎一样的,就不多解释了。

push_back尾插

	void push_back(char ch)
		{
			if (_size == _capacity)
			{
				// 2倍扩容
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			_str[_size] = ch;

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

append追加

	void append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			// 至少扩容到_size + len
			reserve(_size+len);
		}

		  //strcpy(_str + _size, str);
		  memcpy(_str + _size, str, len+1);

		  _size += len;
	}

operator+= 

1.+字符

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

		

2.+字符串 

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

insert插入

1.插入一个字符

 

思路:

1.断言,pos下标位置是否合理合法

2.判断空间是否满?

3.定义一个pos来定位它的位置

4.然后把pos位置后面的数据依次挪动后n位

5.挪动完之后,依次把数据插到pos位置

讲解:

void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);

			if (_size +n > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size + n);
			}
            

           
			// 添加注释最好
			size_t end = _size;
			while (end >= pos )    //解释1
			{
				_str[end + n] = _str[end];
				--end;
			}

			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}

			_size += n;
		}

当我们有了上面的思路时,模拟出了已经没有太大问题了,但是还是有些细节需要注意的:

即当我们把pos=0时,会出现问题,程序会崩溃?

这是因为,我们这里所定义的是size_t无符号整形,当pos=0时,循环end移动到0位置,--后end不会变成-1,而是无符号的整形的最大值,所以end会永远大于pos,循环无法停止,会造成死循环。

那么,我们又怎么取解决这种问题呢?

1.类型改变(强制类型转化)

  写法一:
	 挪动数据
	int end = _size;
	while (end >= (int)pos)  //解释1:如果没有强制转变成int,会造成整形提升,也是会出现问题的
                                    整形的-1是比无符号整形size_t的0要大的

	{
		_str[end + n] = _str[end];
		--end;
	}

写法二:

   写法二:
	// 添加注释最好
	size_t end = _size;
	while (end >= pos && end != npos)     //解释1:nops是静态成员变量,赋值为-1
	{
		_str[end + n] = _str[end];
		--end;
	}

 2.插入一个字符串

有了上面的讲解后,插入一个字符串的思路也是大概一致的。(就不多讲解了)

void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
            //求插入字符串的长度
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				// 至少扩容到_size + len
				reserve(_size + len);
			}

		
			size_t end = _size;
			while (end >= pos && end != npos)
			{
				_str[end + len] = _str[end];
				--end;
			}

			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}

			_size += len;
		}

erase删除

思路:

1.断言(判断pos位置是否合理合法)

2.删除的话分为两种情况:

1)从pos的位置一直删到结尾

i)直接把’\0‘赋值给pos位置。

j)更新_size大小

2)从pos的位置没有删到结尾,即小于size大小

i)找到从pos位置删到最后的那个位置为end。

j)挪动数据

k)更新_size大小

void erase(size_t pos, size_t len = npos)
		{
			assert(pos <= _size);

			if (len == npos || pos + len >= _size)
			{
				//_str[pos] = '\0';
				_size = pos;

				_str[_size] = '\0';
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					_str[pos++] = _str[end++];
				}
				_size -= len;
			}
		}

find查找

查找一个字符:

查找的思路很简单

思路:

1.断言(判断pos位置是否合法合理)

2.依次查找,如果查找字符与字符串的字符相等即找到了。否则没有

3.我们一般没有找到的话返回nops(即-1);

size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size);

	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}

	return npos;
}

查找一个字符串

1.这里使用到了strstr(这是寻找相同的字符,并从相同的那里开始往下打印)

size_t find(const char* str , size_t pos = 0)
{
	assert(pos < _size);

	const char* ptr = strstr(_str + pos, str);
	if (ptr)
	{
		return ptr - _str;
	}
	else
	{
		return npos;
	}
}

 substr

思路:

1.断言(判断pos的位置合理性合法性)

2.获取有效长度:

举个例子:

pos=0,len=10,而你的字符串长度size一个才9,所以有效长度就是9,不可能是10.

3.创建string变量来存储字符串。

4.即通过遍历的形势一一存储到temp

5.最后返回temp

string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
            
            //获取有效长度
			size_t n = len;
			if (len == npos || pos + len > _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}

			return tmp;
		}

resize

思路:分三种情况:

1.n<个数 ,

2.个数<n<最大容量,

3.个数>最大容量

而2和3是可以合并的,都可以先reserve,虽然第二种情况达不到需要扩容,但是你看看我们实现的reserve,它设置了条件的(_size==_capacity)才进入,所以,第二种实际只是进入函数,并没有真正实现,所以2和3可以成一种情况

 情况一:我们知道这个函数的定义是如果小于size,就直接将后面是删去

所以,我们的思路:

直接将_size变为n,并将这个位置变为\0。

情况2:

我们知道这个函数的定义是如果大于size,会把_size后面的数据初始化为某个字符。

有了这个了解,我们需要做的就是遍历,把字符填进去。

最后更新_size即完成。

void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				reserve(n);

				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}

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

clear清除

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

运算符重载总规!!

operator[](可修改)

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

operator[](不可修改)

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

operator+=(加一个字符)

ps:因为+=运算符的返回值是+=后的结果类对象,所以+=运算符重载返回值是string&

思路:

1.直接用上面我们写过的push_back插入

2.最后返回this指针

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

 operator+=(加一个字符串)

思路:

1.这相当于插入一个字符串,用我们之前写过的append函数即可。

2.最后返回this指针。

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

运算符的比较

ps:接下来的都用了const

原因:我们返回的this指针是不能被修改的,所以用了const

 operator<

思路:

1.我们比较字符串时比较的是它们的ACALL码,并不是比较的是它们的长度

2.那么,我们就需要一遍一遍的遍历字符串比较

3.现在,来看正常的情况比较:遍历比较,一旦发现遍历到的字符与另一个字符不一样,就返回得出结果。

 

但是呢?上面的情况,两个字符串前面部分都是一样的话(如下),这里会有以下的三种情况:

    第一种:"hello" "hello"   false----这种直接=返回错误没有太大问题
     第二种:"helloxx" "hello" false
     第三种:"hello" "helloxx" true

所以,看到上面的情况,我们是不是遍历完最小的字符串长度后,是不是还要检查它们两个的长度是否一样?才能真正判断正确?

注意:这里不能够使用比较它的_size+1,因为’\0‘不一定算小字符,有一些汉族比'\0'还要小

 

所以,得出了我们的第一种写法: 

bool operator<(const string& s)
{
	size_t i1 = 0;
	size_t i2 = 0;
	while (i1 < _size && i2 < s._size)
	{
	     if (_str[i1] < s._str[i2])
	     {
			return true;
		 }
		 else if (_str[i1] > s._str[i2])
		 {
			return false;
		 }
		 else
		 {
			++i1;
			++i2;
	   	 }
	}
	 
		if (i1 == _size && i2 != s._size)
		{
			return true;
		}
	    else
		{
			return false;
		}

			//return i1 == _size && i2 != s._size;
			return _size < s._size;
		}

第二种写法:我们使用字符串函数帮忙解决:memcmp(比较字符串大小)

bool operator<(const string& s) const
{
    如果==,就返回0,不是就返回1.
	int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
			
	// "hello" "hello"   false
	// "helloxx" "hello" false
	// "hello" "helloxx" true

    解决返回0的情况:
	return ret == 0 ? _size < s._size : ret < 0;
}

operator== 

思路:

我们判断的是字符串是否相等,所以我们先比较一下它们的长度是否相等,如果相等了,在取比较两个字符串大小即可

bool operator==(const string& s) const
{
	return _size == s._size 
	&& memcmp(_str, s._str, _size) == 0;
}

operator<= 

思路:

运算符重载的比较,只要先写出<或者==这两种情况的话。其他的情况使用复用就可很快得出了。

bool operator<=(const string& s) const
{
	return *this < s || *this == s;
}

 operator>

bool operator>(const string& s) const
{
	return !(*this <= s);
}

operator>= 

bool operator>=(const string& s) const
{
	return !(*this < s);
}

operator!= 

bool operator!=(const string& s) const
{
	return !(*this == s);
}

operator<<流插入

类与对象4-优快云博客

我们在这篇文章中已经写过了关于日期类的流插入和流提取了。

那么,现在对于string类,又该怎么进行流提取呢?

思路:我们只需要把string类中的字符一一插入到ostream流插入中即可。最后将流插入对象为返回值返回。

1.我们之前写过的知道,如果我们的流插入放到类对象里面,使用习惯很不符合我们正常的做法。所以移到类外面。(若需要使用到内置(私有)成员即使用有元函数即可)

2.需要注意的是:

1)ostream必须使用&返回,因为如果不使用&,就会是变成形参,会进行拷贝(ostream是不能进行拷贝操作的!)

ostream& operator<<(ostream& out, const string& s)
{
		第一种:写法
        for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i];
		}

        第二种:写法
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
}

c_str和ostream流插入的区别: 

    c的字符数组,以\0为终止算长度
    string不看\0,以size为终止算长度

	bai::string s("hello C++");
	s += '\0';
	s += "!!!!!";
	cout << s.c_str() << endl;
	cout << s << endl;

//结果是:
hello C++
hello C++!!!!!

 

流提取operator>>

思路:

1.函数返回一个 istream 的引用,接收两个参数:一个输入流对象 in 和一个 string 对象 s 的引用。

2.清空 string 对象 s 的内容,确保读取新字符串前它是空的。

3.使用 in.get() 读取一个字符,然后循环检查该字符是否为空格或换行符。如果是,则继续读取下一个字符,直到遇到非空格和非换行符的字符。

4.定义一个字符数组 buff 作为缓冲区,用于存储读取的字符。在循环中,只要读取的字符不是空格和换行符,就将其存入缓冲区 buff 。当缓冲区快满( i == 127 )时,将缓冲区内容添加到 string 对象 s 中,并重置缓冲区索引 i 为0。

5.循环结束后,如果缓冲区中还有未处理的字符( i != 0 ),则将其添加到 string 对象 s 中。

6.返回输入流对象 in ,以便支持链式调用,例如 cin >> s1 >> s2; 。


istream& operator>>(istream& in, string& s)
	{
		s.clear();

		char ch = in.get();
		// 处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		//in >> ch;
		char buff[128];
		int i = 0;

		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			//in >> ch;
			ch = in.get();
		}

		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}
};

好了,string模拟到此结束了。

最后到了我们的鸡汤环节:

没有所谓失败,除非你不再尝试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值