<C++> 模拟实现string

目录

前言

一、模拟实现string

1. 成员变量

2. 构造函数

2.1 构造函数

2.2 重载默认构造 

2.3 合并

3. 析构函数 

4. 拷贝构造函数 

5. c_str

6. size

7. operator[ ] 

7.1 普通版

7.2 const版本 

8. 迭代器—iterator 

8.1 普通版iterator

8.2 const版本iterator

9. 尾插

10. operator+=

11. insert

11.1 插入n个字符 

11.2 插入字符串 

12. erase 

12.1 从pos位置开始删除

13. find

13.1 查找字符

13.2 查找字符串

14. substr 

15. reseve && resize

16. operator<< 流插入

17. operator>> 流提取

18. string比较

19. operator= 赋值运算符重载

 二、整体代码

总结


前言

        学习了string的用法,为了提升自身能力,我们来模拟实现string的部分功能


一、模拟实现string

初始化列表初始化顺序是声明顺序!

我们将自己实现的string放在自己的命名空间内,防止与std中的string冲突

namespace my_string
{
	class string
	{

	};
};

1. 成员变量

private:
		char* _str;
		size_t _size;
		size_t _capacity;

2. 构造函数

 2.1 构造函数

构造函数功能:为字符串开空间,并根据传入的参数进行初始化操作。

        此处会出现一些问题,当我们使用初始化列表时,每个人的初始化顺序可能不同,并且成员变量的声明顺序也可能不同,编写完代码后,发现出错,代码不能跑,这就是初始化列表的知识点——初始化列表的初始化顺序由成员变量的声明顺序决定。

        并且为了少次使用strlen这一O(N)级函数,我们可以只将一个变量在初始化列表初始化,其他成员在函数内赋值(以达到少次使用strlen函数),这是因为这些成员变量是内置类型(内置类型在列表或构造函数体内初始化区别不大),如果是自定义类型则不能在内部赋值,因为即使不写,它们仍然会被自动调用各自的构造函数。

        这里我们将全部的成员变量都放在函数体内赋值,以便代码清晰。

namespace my_string
{
	class string
	{
	public:
		string(const char* str)
		{
            _size = strlen(str);
			_capacity = _size;
            //加1,存放\0
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
};

2.2 重载默认构造 

        那么,当我们不传参构造,只实例化时,编译器找不到默认构造函数,所以我们要重载一个默认构造函数

        根据监视,可以看到C++的string在无参构造时,它是有一个‘\0'的,所以我们在初始化_str不能置空要至少开一个空间,来存储 '\0'

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

 2.3 合并

我们可以使用缺省值,将两个构造函数合并

这里的问题在于,如何设置缺省值?是 '\0' ,还是 nullptr, 还是 "\0"

  1. '\0' 不可以,问题在于左边类型为char*,而'\0'是字符,两者类型不同
  2. nullptr 也不可以,问题在于后面的strlen直接失效,并且没有 '\0'
  3. "\0" 不太合适,因为 “” 内默认有\0,所以缺省值应为“”
string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

注意:该构造函数代码之后会在operator<<处再次修改 

3. 析构函数 

  • 释放new出的空间,并将_size与_capacity置空即可
~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

4. 拷贝构造函数 

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

注意:该构造函数代码之后会在operator<<处再次修改  

5. c_str

  •  模拟string中的c_str函数功能,返回c形式的字符串,即返回 _str
  • 在函数后面加上const,可以使被const修饰的对象也可调用该函数
const char* c_str() const
{
    return _str;
}

6. size

  •  返回_size即可
size_t size() const
{
	return _size;
}

7. operator[ ] 

7.1 普通版

  • 为了可读可写,我们使用引用返回,因为返回的对象是数组内的元素,生命周期不在operator[ ]函数内,所以可以使用引用返回
char& operator[](size_t pos)
{
	assert(pos < _size);

	return _str[pos];
}

7.2 const版本 

  • const版本只能读,与普通版本形成函数重载,调用函数时,编译器会调用最匹配的函数
//const版本 只读
const char& operator[](size_t pos) const
{
	assert(pos < _size);

	return _str[pos];
}

8. 迭代器—iterator 

8.1 普通版iterator

  • 对于string类,迭代器可以使用指针实现,即迭代器就是指针(针对string类)
//迭代器,在string这里就是指针
typedef char* iterator;

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

 测试:

#include<iostream>
#include"string模拟.h"
using namespace my_string;
using std::cout;
using std::endl;

void test()
{
	my_string::string s1("hello world");
	cout << s1.c_str() << endl;

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

	for (auto ch : s1)
	{
		cout << ch << "";
	}
	cout << endl;
}
hello world
h e l l o  w o r l d
hello world

可以看到,我们在写完迭代器之后,范围for就可以直接使用了,因为它就是利用迭代器实现的,底层与使用迭代器遍历几乎一样。 

如果我们把end(),改为End(),那么范围for就不可用了。所以,范围for是傻瓜式替换,找到类中的begin(),end(),字母错一个它都实现不了。

8.2 const版本iterator

  • 当对象被const修饰,那么迭代器就要加上const,保证值不被修改
typedef const char* const_iterator;
const_iterator begin() const
{
	return _str;
}

const_iterator end() const
{
	return _str + _size;
}
string::const_iterator cit = s1.begin();
while (cit != s1.end())
{
	cout << *cit << endl;
	++cit;
}
cout << endl;

 9. 尾插

  1. 判断是否要扩容对于push_back() 函数可以进行2倍扩容对于append() 函数,要进行大小判断,如果_size + strlen(str) > _ capacity 那么至少扩容到_size + len 的容量大小。
  2. 对于扩容操作,我们实现reserve() 函数,进行扩容处理
void reserve(size_t n)
{
	if (n > _capacity)
	{
        //多开一个空间留给 '\0'
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

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

	_str[_size] = ch;
	++_size;
	_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;
}

 10. operator+=

  • 实现尾插之后,operator+=的实现就非常简单了,+=是有返回值的,返回的是string类型
  • 可以返回*this,因为*this是对象,对象的生命周期不在operator+=中,所以可以返回*this
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

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

 11. insert

  1. 先判断下标是否合法
  2. 再判断是否要扩容
  3. 移位,移位时有“坑”,因为是size_t 类型,大于等于0,-1是最大正数,如果end改为int类型也不行,因为会发生隐式类型转换。这里有多种解决方案:可以强制类型转换、可以将end从end+n开始,进行end - n 赋给 end ,这样就不会到下标为0的地方、用npos判断(npos使用静态成员函数,赋值为-1,表示最大值)
  4. 插入数据
  5. 更新_size

11.1 插入n个字符 

此处的npos定义为类的静态成员变量,并用const修饰

class string
{
public:
.
.
.
.
.
private:
    char* _str;
    size_t _size;
    size_t _capacity;
public:
    const static size_t npos;
};

const size_t string::pos = -1;

用const避免被修改,static使其称为静态成员变量
        1. 通过已经实例化的对象调用    s1.npos
        2. 通过匿名对象调用            string().npos
        3. 通过类名和域访问限定符调用    string::npos
        前提都是public,如果是private的话,就要写一个静态函数,来返回该静态变量

void insert(size_t pos, size_t n, char ch)
{
	//size_t大于0,不用判断
	assert(pos <= _size);
	if (_size + n > _capacity)
	{
		//至少扩容到 _size + n
		reserve(_size + n);
	}
	size_t end = _size;
	//size_t因为不小于0,当pos为0时,会陷入死循环
	//这里有多种方案,可以强制类型转换、可以将end从end+n开始、用npos判断
	while (end >= pos && end != npos)
	{
		_str[end + n] = _str[end];
		--end;
	}

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

	_size += n;
}

11.2 插入字符串 

  • 同理,将n替换为len即可
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;
	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;
}

12. erase 

12.1 从pos位置开始删除

  1. 先判断下标合法
  2. 再判断是不是要删到尾部,如果是,直接把pos处改为 '\0' ,并修改_size值即可
  3. 如果不是,就是在内部删除,和insert操作类似,移动
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 begin = pos + len;
		//等于size,把\0也移动过去
		while (begin <= _size)
		{
			_str[pos++] = _str[begin++];
		}
		_size -= len;
	}
}

 13. find

13.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;
	}
	//找不到,返回npos
	return npos;
}

 13.2 查找字符串

size_t find(const char* str, size_t pos = 0)
{
	assert(pos < _size);
	const char* ptr = strstr(_str, str);
	if (ptr)
	{
		return ptr - _str;
	}
	else
	{
		return npos;
	}
}

14. substr 

  • 注意for循环中 i < pos + n,别误写为 i < n
string substr(size_t pos, 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);
	//注意i < pos + n
	for (size_t i = pos; i < pos + n; i++)
	{
		tmp += _str[i];
	}

	return tmp;
}

15. reseve && resize

为什么用memcpy而不用strcpy?

        这是因为string是按照_size大小进行输出的,即使中间有\0也会打印,所以不能按照之前c语言风格的字符串进行处理,应该逐字节拷贝

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				//多开一个放'\0'
				char* tmp = new char[n + 1];
				//strcpy(tmp, _str);
				memcpy(tmp, _str, _size + 1);

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

        void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				//利用reserve进行扩容判断
				reserve(n);
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}

				_size = n;
				//由于ch不一定是\0,而且i < n没到最后的\0位置
				_str[_size] = '\0';
			}
		}

16. operator<< 流插入

流插入,有多少字符就输出多少字符,中间有'\0'也不终止

注意将该函数放在命名空间域中,还是全局函数

//流插入,有多少字符就输出多少字符,中间有'\0'也不终止
	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		//要访问私有变量才用友元
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

        直接使用 c_str 和 << 在一般情况是相同的,但是c_str是c字符串,遇到\0自动终止而string则是依靠size来判断,size有多大,operator<<就打印多长 

例如 :

void test()
{

	//与c的字符串不同,c的字符串遇到\0终止,而string是直到size终止
	string ss("hello world");
	ss += '\0';
	ss += "LLLLLLLLLL";
	cout << ss.c_str() << endl;
	cout << ss << endl;
}

int main()
{
    test();
    return 0;
}
hello world
hello wordl LLLLLLLLLL

        可以看到第二个打印有空格,空格表示了 '\0'

        所以,有了\0,之前用的str系列的函数就需要修改了,因为str类型的函数遇到\0自动结束,而string是遇到size才停止,将之前的str函数改为memcpy函数

  • 对于构造函数,可以不修改,因为本身构造时,用的就是c字符串,strlen以及strcpy都不需要修改,这里修改strcpy是为了与下面一致
        //构造
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			memcpy(_str, str, _size + 1);
		}
		//拷贝构造
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			memcpy(_str, s._str, s._size + 1);
			_size = s._size;
			_capacity = s._capacity;
		}
  • 对于reserve也需要更改
  • 对于append可以不更改,因为追加的是c字符串,为了一致性,改为memcpy,拷贝到时候都默认加一个1,因为strcpy默认拷贝\0
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

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

17. operator>> 流提取

	//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据
	std::istream& operator>>(std::istream& in, string& s)
	{
		//不能写成 in >> s.str,因为s可能没空间来存放数据
		char ch;
		in >> ch;
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			in >> ch;
		}
		return in;
	}

        上面的代码有错误,当我们输入连续的字符时,不能读到空格和换行,就会陷入死循环,这是in本身的设定,以空格和换行符来区分多个数据,所以我们要使用 get 来提取终端数据 ——in.get(),可以读空格和换行

        当我们不想以空格为分隔符时,可以将 ch != '  ' 判断条件去掉

	//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据
	std::istream& operator>>(std::istream& in, string& s)
	{
		//不能写成 in >> s.str,因为s可能没空间来存放数据
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

对于cin操作,如果多次cin,应该会覆盖原先数据 ,即我们需要实现清理工作clear()

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

    //流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据
	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		//不能写成 in >> s.str,因为s可能没空间来存放数据
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

        优化:循环里的+=效率不高,因为如果字符串过长可能经历多次扩容。我们当然可以提前reserve开一定空间,但是当字符串过短,那么如果空间开的过大就浪费了。

        在这里,我们开一个128的char数组,暂存从终端提取的数据,积累一些数据,当数据超过数组大小后,尾插 \0 后将该数组+=上去,并将下标置为0,再次提取数据。

        当数据不够128时,跳出循环,所以要再循环外判断数组内是否还存有数据,再进行+=

	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		//不能写成 in >> s.str,因为s可能没空间来存放数据
		char ch = in.get();
		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

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

        但是对于前有空格和换行,后面输入的数据就进不去,所以再进行优化,加一个while循环,将前空格和换行去除。

	//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据
	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		//不能写成 in >> s.str,因为s可能没空间来存放数据
		char ch = in.get();
		//处理前缓冲区-流-前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

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

18. string比较

  1. string按照ASCII进行比较大小
  2. 不能用strcmp,因为当遇到 例如:hello\0xxxxx 和 hello\0yyyyy 时,比较遇到 \0 就结束了,所以我们可以使用memcmp或手写
  3. 加const防止改变this值,以及可以使const对象调用该函数
        //方法一:
		bool operator<(const string& s) const
		{
			int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);

			return ret == 0 ? _size < s._size : ret < 0;
		}


		方法二:
		//bool operator<(const string& s)
		//{
		//	//return strcmp(_str, s._str) < 0;由于\0,所以不能用strcmp
		//	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;
		//		}
		//	}
		//	//在循环之外,只有一种情况是true
		//	//return i1 == _size && i2 != s._size;

		//	//或者判断size大小
		//	return _size < s._size;
		//}

 其他比较复用即可

        bool operator==(const string& s) const
		{
			//先比较size过滤各种情况
			return _size == s._size
				&& memcmp(_str, s._str, _size < s._size) == 0;
		}
		bool operator<=(const string& s) const
		{
			return (*this < s || *this == s);
		}
		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);
		}

 19. operator= 赋值运算符重载

 对于赋值,为深拷贝,则需要释放原空间,再开空间进行赋值

方法一: 开空间,赋值,再释放,赋值

		//赋值运算符重载——深拷贝
		string& operator=(const string& s)
		{
			if (this != &s)
			{
                //先开空间,赋值,再释放,赋值
				char* tmp = new char[s._capacity];
				memcpy(tmp, s._str, s._size + 1);
				delete[] _str;
				_str = tmp;

				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}

方法二:交换

        先根据s创建一个临时对象,再将临时对象和this的各个数据进行交换,这时this就指向了拷贝对象,临时对象tmp就指向了原空间,tmp出了作用域自动销毁,完美解决 

		//方法二:swap交换
		void swap(string& s)
		{
			std::swap(s._str, _str);
			std::swap(s._size, _size);
			std::swap(s._capacity, _capacity);
		}

		string& operator=(const string& s)
		{
			if (this != &s)
			{
				//不能直接交换两个对象,会发生栈溢出、无穷递归
				//std::swap(tmp, *this);  一直调用赋值运算符重载
				string tmp(s);

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

			}

			//tmp是局部对象,出函数自动销毁
			return *this;
		}

 方法三:再次优化,将形参改为string tmp,直接拷贝构造tmp,出函数作用域直接析构,所以,对于赋值运算符重载,需要先进行拷贝构造

		void swap(string& s)
		{
			std::swap(s._str, _str);
			std::swap(s._size, _size);
			std::swap(s._capacity, _capacity);
		}
		//方法三:改变行参,直接拷贝构造tmp
		string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

由此又引出拷贝构造的现代写法 (即使用交换来实现功能),但是由于\0,这个拷贝构造并不能正确实现拷贝构造功能,所以还是采用原拷贝构造。

		string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			//使用初始化列表,避免对象各成员变量为nullptr
			string tmp(s._str);
			swap(tmp);
		}

 二、整体代码

my_string.h

#pragma once
#include<cstring>
#include<assert.h>
#include<iostream>

namespace my_string
{
	class string
	{
	public:
		//迭代器,在string这里就是指针
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

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

		//构造
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			memcpy(_str, str, _size + 1);
		}
		//拷贝构造
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			memcpy(_str, s._str, s._size + 1);
			_size = s._size;
			_capacity = s._capacity;
		}

		赋值运算符重载——深拷贝
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		char* tmp = new char[s._capacity];
		//		memcpy(tmp, s._str, s._size + 1);
		//		delete[] _str;
		//		_str = tmp;

		//		_size = s._size;
		//		_capacity = s._capacity;
		//	}

		//	return *this;
		//}

		//方法二:swap交换
		void swap(string& s)
		{
			std::swap(s._str, _str);
			std::swap(s._size, _size);
			std::swap(s._capacity, _capacity);
		}

		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		//不能直接交换两个对象,会发生栈溢出、无穷递归
		//		//std::swap(tmp, *this);  一直调用赋值运算符重载
		//		string tmp(s);

		//		//this->swap(tmp);新手才写this o.0
		//		swap(tmp);

		//	}
		//	//tmp是局部对象,出函数自动销毁
		//	return *this;
		//}

		//方法三:改变行参,直接拷贝构造tmp
		string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}

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

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

		size_t size() const
		{
			return _size;
		}

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

			return _str[pos];
		}

		//const版本 只读
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				//多开一个放'\0'
				char* tmp = new char[n + 1];
				//strcpy(tmp, _str);
				memcpy(tmp, _str, _size + 1);

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

		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				//利用reserve进行扩容判断
				reserve(n);
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}

				_size = n;
				//由于ch不一定是\0,而且i < n没到最后的\0位置
				_str[_size] = '\0';
			}
		}

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

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

		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

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

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

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

		void insert(size_t pos, size_t n, char ch)
		{
			//size_t大于0,不用判断
			assert(pos <= _size);
			if (_size + n > _capacity)
			{
				//至少扩容到 _size + n
				reserve(_size + n);
			}

			size_t end = _size;
			//size_t因为不小于0,当pos为0时,会陷入死循环
			//这里有多种方案,可以强制类型转换、可以将end从end+n开始、用npos判断
			while (end >= pos && end != npos)
			{
				_str[end + n] = _str[end];
				--end;
			}

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

			_size += n;
		}

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

		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 begin = pos + len;
				//等于size,把\0也移动过去
				while (begin <= _size)
				{
					_str[pos++] = _str[begin++];
				}
				_size -= len;
			}
		}

		string substr(size_t pos, 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);
			//注意i < pos + n
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}

			return tmp;
		}

		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;
			}
			//找不到,返回npos
			return npos;
		}

		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);
			const char* ptr = strstr(_str, str);
			if (ptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

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

		//方法一:
		bool operator<(const string& s) const
		{
			int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);

			return ret == 0 ? _size < s._size : ret < 0;
		}


		方法二:
		//bool operator<(const string& s)
		//{
		//	//return strcmp(_str, s._str) < 0;由于\0,所以不能用strcmp
		//	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;
		//		}
		//	}
		//	//在循环之外,只有一种情况是true
		//	//return i1 == _size && i2 != s._size;

		//	//或者判断size大小
		//	return _size < s._size;
		//}

		bool operator==(const string& s) const
		{
			//先比较size过滤各种情况
			return _size == s._size
				&& memcmp(_str, s._str, _size < s._size) == 0;
		}
		bool operator<=(const string& s) const
		{
			return (*this < s || *this == s);
		}
		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);
		}

	private:
		size_t _size;
		size_t _capacity;
		char* _str;

		static size_t npos;
	};

	size_t string::npos = -1;

	//流插入(向终端插入数据),有多少字符就输出多少字符,中间有'\0'也不终止
	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		//要访问私有变量才用友元
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

	//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据
	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();
		//不能写成 in >> s.str,因为s可能没空间来存放数据
		char ch = in.get();
		//处理前缓冲区前面的空格或者换行
		while (ch == ' ' || ch == '\n')
		{
			ch = in.get();
		}

		char buff[128];
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

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

};

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

三、写时拷贝 

        我们再次回顾深浅拷贝问题

浅拷贝问题:

        1.析构两次

        2. 一个对象修改会影响另一个

        仔细分析,我们能发现,深拷贝效率不如浅拷贝,因为还要开辟另一块空间。此时,有了浅拷贝的改进:

  1. 对于第一个问题:进行引用计数 ,每有一个对象指向该空间,计数+1,每析构一次,计数-1,直至最后为0时,真正的析构(最后走的人关灯)
  2. 对于第二个问题,如果计数不是1,则进行深拷贝,再修改值(有博弈的成分,如果有只拷贝不修改值的情况,那么就赚了;对于需要修改值的,那么就进行深拷贝,不赚不赔)

想了解更多关于写时拷贝的内容,可以看大佬的文章

C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell

sizeof (s)是多少呢?

        并不是12,是28(32位平台),这是由于vs2019优化,给了buf数组缓存,以空间换时间

  • 当 size < 16 那么就没必要开空间,直接存在buf数组内
  • 当size >= 16 字符串存在_ptr指向的堆空间


总结

        我们模拟实现的string,具有部分功能,还有一部分没有实现,有兴趣的同学可以参照std的string再进行模拟实现,下节我们将讲解vector并进行实现。

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值