0
点赞
收藏
分享

微信扫一扫

C++之string的底层简单实现!(七千字长文详解)

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

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

}
举报

相关推荐

0 条评论