C++之string的底层简单实现!

string之私有成员变量

namespace MySTL
{
    class string
	{
    private:
		char* _str;
		size_t _size;
		size_t _capacity;
		//这里capacity表示的是实际可以存的内存的大小!
        //也可以表示真实的内存大小!
        const static size_t npos = -1;
	};
}

因为_size和 _capacity都不存在负数所以都是使用 size _t类型

size_t 是 unsigned long long 类的重命名!

本来c++的静态成员变量是不可以使用缺省值的!

但是c++对于整形类型开了一个后门!可以允许在整形静态成员变量的情况下使用缺省值!

string构造函数

string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

错误的写法在初始化列表初始化数组!

string(const char* str = "")
    :_str(str),
	_size(strlen(str)),
	_capacity(strlen(str))
{
}//写法1

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

写法1这样的写法有什么问题呢?

那就是_str这个指针它指向了的是一个常量字符串!而常量字符串位于常量区是不可修改的!所以我们就不可以对这个字符串进行增删查改!

而且这样的写法不太好看,可读性不强!

写法2 都会这样写会导致直接蹦,因为strlen遇到了空指针会直接崩溃!

使用’\0'和空指针也是一样的!,因为’\0‘ 的ASCII值为 0 也相当于空指针!

""  ------这是一个字符串,里面只有一个\0
'\0' ------这个一个字符
"\0" -------这也是一个字符串,里面有两个\0
//这三者要区分清楚!

string的拷贝构造

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

= 重载!

 string& operator=(const string& str)
{
	if (this == &str)
	{
		char* temp = new char[str._capacity + 1];
		strcpy(temp, str._str);
        
		delete[] _str;//比起拷贝构造要多一步先把原来自己的空间释放掉!
		_str = temp;
        
		_capacity = str._capacity;
		_size = str._size;
	}
	return *this;
}

string的析构函数

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

string的迭代器

typedef char* iterator;

string类型的迭代器底层就是char*!

begin

iterator begin()
{
	return _str;
}

end

iterator end()//迭代器的最后一个指向有效数据的下一位!
{
	return _str + _size;
}

string的const迭代器

typedef const char* const_iterator;

静态迭代器

begin

const_iterator begin()const
{
	return _str;
}

end

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

size

size_t size()const
{
	return _size;
}

对于这种不需要对string类里面进行修改的函数接口我们都在后面加上const保证其更好的泛用性!

capacity

size_t capacity()const
{
	return _capacity;
}

[]重载!

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

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

[]重载要两个,因为[]既可以用来访问string类里面的值,也可以用来修改string类里面的值!

reserve

void reserve(size_t n)
{
	char* temp = new char[n + 1];
	strcpy(temp, _str);
	delete[] _str;
	_str = temp;
	_capacity = n;
}

new 要开n+1个空间!是为了最后要装一个\0

然后将原来的数据拷贝到新的空间里面!释放旧空间!

然后将_str 指向新空间!

resize

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

三种情况! 第一种 n < _ size 直接删除数据! 第二种情况!_ size< n < capacity 填充数据! 第三种情况! _ capacity < n 扩容 + 填充数据!

插入类接口!

Push_Back

void Push_Back(char c)
{
	if (_size == _capacity)
	{
	    size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
		_capacity = newcapacity;
	}
	_str[_size] = c;//这是\0的位置!
	_size++;
	_str[_size] = '\0';//最后记得加上\0
}

插入首先就要判断是否要扩容!一般都是选择二倍扩容!

Push_Back接口的错误写法!

void Push_Back(char c)
{
	if (_size == _capacity)
	{
		reserve(capacity*2);
		_capacity *= 2;
	}
	_str[_size] = c;//这是\0的位置!
	_size++;
	_str[_size] = '\0';//最后记得加上\0
}

这样的写法有什么问题呢?那就是一旦capacity为0的话,那就无法扩容!

append

void append(char c)
{
	Push_Back(c);
}
//插入单个的字符,逻辑是根Push_Back是一样的!
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	//strcat(_str, str);
	strcpy(_str + _size,str);
    //上面两种实现方式都可以!
    //strcpy已经吧\0都拷贝到_str里面了
    _size += len;
}
//插入一个字符串,也要进行扩容!
//但是不能二倍扩容!因为不能保证二倍扩容的空间大小是足够的!

+=重载

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

insert

插入一个字符

insert是一个坑很多的接口接下来先给读者看一个错误的写法!

string& insert(size_t pos, char c)
{
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
		_capacity = newcapacity;
	}
	size_t end = _size;
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}
	_str[pos] = c;
	_size++;
	return *this;
}

这个写法看上去是没有什么问题的!但是其实,在第0位插入的时候会陷入死循环!

为什么?因为size_t是一个无符号位!

一旦小于 0 就会变成一个极大的值!也就是说 永远无法小于 pos!

image-20221108221438334.png

那如果我们把end的类型换做int这个有符号位是否能解决这个问题呢?

==答案是不行!==因为发生了==整形提升==!当左右运算符左右的两个类型相似的时候!为了确保精度!编译器会自动的将低精度的类型提升为高精度的类型!

image-20221108223247908.png

所以其实实际上这个表面上是int类型的end变量,实际上是size_t类型的!

就看到end好像就是不断的减下去不停止!

如果要使用int类型的end变量来解决这个问题!还必须将pos强转为int类型防止发生整形提升!

string& insert(size_t pos, char c)
{
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
		_capacity = newcapacity;
	}
	int end = _size;
	while (end >= (int)pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}
	_str[pos] = c;
	_size++;
	return *this;
}

image-20221108223133913.png

结果成功的插入了!

还有一种解决方法就是不让end 小于 0

就是让end的开始位置从 '\0'的下一位开始!

string& insert(size_t pos, char c)
{
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
		_capacity = newcapacity;
	}
	size_t end = _size + 1;
	while (end > pos)//不可以 >= 因为一旦 == 就会导致 end == 0 随后end-- 变成-1 其实是一个极大的值!
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = c;
	_size++;
	return *this;
}

image-20221108224020969.png

插入一个字符串

用insert插入一个字符串的难点在于循环的范围!

下面就是一个经典的错误案例

string& insert(size_t pos, const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	size_t end = _size;

	/*while (end > pos)
	{
		_str[end] = _str[end - len];
		end--;
	}*///循环1
 /*while (end >= pos)
	{
		_str[end+len] = _str[end];
		end--;
	}*///循环2

 while (end > pos)
	{
		_str[end] = _str[end - len];
		end--;
	}//循环3
	strcpy(_str + pos, str);
	return *this;
}
int main()
{
	MySTL::string s1 = "hello";
	s1.insert(0, "hhh");
	cout << s1.c_str() << endl;
	return 0;
}
  1. 首先是循环范围这个循环其实其实已经发生了越界访问!

    当end < len的时候 就会出现一个极大值!此时已经出现了越界的访问!

image-20221108225844336.png

2 . 第二种循环的问题就是和上面的一样一旦遇到pos = 0 的位置的时候就会进入死循环!解决的方法也是一样的!就是将end类型换成int 将pos强转为int

int end = _size;
while (end > (int)pos)
{
	_str[end] = _str[end - len];
	end--;
}

3.第三种循环会出现!会出现越界访问!当end < len的时候!end -len就会变成一个极大的值!

4 . 然后是strcpy,因为拷贝的过程中将\0也一起拷贝进去了,所以一旦打印出结果就只有插入的值,而插入后面的值都无法显示!

image-20221108230044881.png

==insert的正确写法==

string& insert(size_t pos, const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	size_t end = _size + len
        ;//从最后的\0的位置开始!
	while (end > pos + len - 1)
        //如果不pos +len-1 会导致少移动一个字符!
	{
		_str[end] = _str[end - len];
		end--;
	}
	strncpy(_str + pos, str, len);//不可以把str的\0也拷贝进去!
	_size += len;
	return *this;
}
while (end > pos + len - 1);
//如果改成
while (end >= pos + len);
//当pos == 0 ,len == 0 ,end == 0的时候!就会进入死循环!
//因为end-- 变成一个极大的值!一直死循环!

image-20221109203840841.png

while (end > pos + len - 1);
//这个条件循环在end == 0 ,pos == 0,len == 0 的条件下!
//因为 pos + len — 1 也是被整形提升了!所以结果是一个无符号数!导致了小于 0 之后变成了一个极大值!这个情况下无法进入循环刚刚好避免了上面的问题!

返回值一定要是string& 而不是 string!这样可以防止没有写拷贝构造的时候发生调用了两次析构用来释放同一块空间!

默认拷贝构造是浅拷贝!使用string作为返回值就会让临时产生变量中的_str指向同一块的空间!

image-20221109215325168.png

==一旦空间被释放,我们插入的值和原先的值都会清空!==

image-20221110095446705.png

不仅打印错误还出现了崩溃!

erase

string& erase(size_t pos, size_t len = npos)
{
	size_t end = _size;
	if (len == npos || len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}//当len不传值的时候,和len大于剩下空间的时候!
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
	return *this;
}

返回值一定要是string& 而不是 string!理由和上面一样都是为了防止二次释放!

find

size_t find(char ch, size_t pos = 0)const
{
	assert(pos < _size);
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}
size_t find(const char* str, size_t pos = 0)const
{
	const char* ptr = strstr(_str + pos, str);//要加pos是因为从pos的位置开始找的!
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}

}

clear

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

>>的重载!

ostream& operator<< (ostream& out, const string& str)
{
	for (size_t i = 0; i < str.size(); i++)
	{
		out << str[i];
	}
	return out;
}

流插入的重载相比以前的对于c的字符串的打印有个特点就是不以\0作为结尾!

image-20221110110958246.png

<< 重载!

istream& operator >>(istream& in, string& str)
{
    str.clear();
	char ch;
	in >> ch;
	while (ch != '\n' && ch != ' ')
	{
		in >> ch;
		str += ch;
	}
	str += '\0';
	return in;
}

这样写看上去没有问题!但是其实这样写会进入死循环!

这是因为cin将空格和换行当做是多个字符串之间的间隔!所以cin 是拿不到 空格和 \0

所这样写是错误的!

正确写法!

istream& operator >>(istream& in, string& str)
{
    str.clear();
	char ch;
	ch = in.get();
	while (ch != '\n' && ch != ' ')
	{
		str += ch;
		ch = in.get();
	}
	return in;
}

in.get(),就相当于c语言中的getchar!可以提取空格和换行!

但是这个代码有一个不好的一点!

一旦我们输入一串及其长的字符!那么就会频繁扩容!这样会导致不必要的性能损失!

所以我们可以继续改进一些!

==最终优化版!==

istream& operator >>(istream& in, string& str)
{
    str.clear();//用来清空str保留的数据!
	char buff[128] = { '\0' };
	char ch;
	ch = in.get();
	int i = 0;
	while (ch != '\n' && ch != ' ')
	{
        if (i == 127)//留一个用来存放\0    如果i == 128的话!会导致乱码!因为+=的底层是append,append是调用strcpy来实现的!strcpy是以\0作为结尾的!
		{
			i = 0;
			str += buff;
		}
		buff[i++] = ch;
		ch = in.get();
	}
	if (i > 0)
	{
		buff[i] = '\0';//如果不加上这个的话会把后面原本的值都拷进去!
		str += buff;
	}
	return in;
}
	//getline的实现原理就是把条件换成while (ch != '\n')

将要输入的长字符串分割成一个个小段!减少扩容的次数!

最终代码

namespace MySTL
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		const_iterator begin()const
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		const_iterator end()const
		{
			return _str + _size;
		}

		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		~string()
		{
			_size = _capacity = 0;
			delete[] _str;
			_str = nullptr;
		}
		string(const string& str)
		{
			_str = new char[str._capacity + 1];
			strcpy(_str, str._str);
			_capacity = str._capacity;
			_size = str._size;
		}
		string& operator=(const string& str)
		{
			if (this == &str)
			{
				char* temp = new char[str._capacity + 1];
				strcpy(temp, str._str);
				delete[] _str;
				_str = temp;
				_capacity = str._capacity;
				_size = str._size;
			}
			return *this;
		}
		const char* c_str()const
		{
			return _str;
		}

		size_t size()const
		{
			return _size;
		}

		size_t capacity()const
		{
			return _capacity;
		}

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

		void reserve(size_t n)
		{
			char* temp = new char[n + 1];
			strcpy(temp, _str);
			delete[] _str;
			_str = temp;
			_capacity = n;
		}
		void resize(size_t n ,char ch = '\0')
		{
			assert(n >= 0);
			if (n <= _size)
			{
				_str[n] = '\0';
				_size = n;
			}
			else if (n > _size && n <= _capacity)
			{
				while (_size < n)
				{
					_str[_size] = ch;
					_size++;
				}
				_str[_size] = '\0';
			}
			else
			{
			
				reserve(n);
				while (_size < n)
				{
					_str[_size] = ch;
					_size++;
				}
				_str[_size] = '\0';
				_capacity = n;
			}
		}



		void Push_Back(char c)
		{
			if (_size == _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
				_capacity = newcapacity;
			}
			_str[_size] = c;
			_size++;
			_str[_size] = '\0';
		}

		void append(char c)
		{
			Push_Back(c);
		}


		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			//strcat(_str, str);
			strcpy(_str + _size, str);
			_size += len;
		}
		string& operator+=(char c)
		{
			Push_Back(c);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

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

			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[pos] = c;
			_size++;
			return *this;
		}

		string& insert(size_t pos, const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			size_t end = _size + len;
			while (end >= pos + len)
			{
				_str[end] = _str[end - len];
				end--;
			}
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}
		string& erase(size_t pos, size_t len = npos)
		{
			size_t end = _size;
			if (len == npos || len >= _size - pos)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
			return *this;
		}
		size_t find(char ch, size_t pos = 0)const
		{
			assert(pos < _size);
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}
			return npos;
		}
		size_t find(const char* str, size_t pos = 0)const
		{
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr)
			{
				return npos;
			}
			else
			{
				return ptr - _str;
			}

		}
		void clear()
		{
			_str[0] - '\0';
			_size = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
		const static size_t npos = -1;
	
	};
	ostream& operator<< (ostream& out, const string& str)
	{
		for (size_t i = 0; i < str.size(); i++)
		{
			out << str[i];
		}
		return out;
	}

	istream& operator >>(istream& in, string& str)
	{

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

}