C++语法(9)---- 模拟string

目录

1.构造函数

1.实现传入字符数组

2.实现传入字符串

3.=的字符重载

2.析构函数

3.扩容函数

4.字符串中尾插元素

1.加入一个字符

2.加入一串字符

5.resize函数

6.c_str返回string类中数组的地址

7.迭代器

8.指定位置插入

1.插入一个字符

2.插入字符串

9.删除字符串

10.find函数

1.找一个字符

2.找一串字符

11.清空函数和判空函数

1.清空函数

2.判空函数

12.逻辑比较运算符重载

13.流提取和流插入

1.流插入

2. 流提取


模拟string类时,我们需要有些成员变量,因为string是关于字符串的类,所以存储内容使用字符的数组来存储;有存储空间需要被记录,现在记录的空间也要被记录,这两个都用无符号整型存储。

1.构造函数

这里我们只实现传入字符数组和字符串的构造函数。

1.实现传入字符数组

在传入字符时,可以进行对传入的数组指针做缺省,但是要注意,为了字符串通过缺省得到空字符,不能直接传入空指针实现,因为strlen不能通过空指针得到0,所以传入空字符串即可。注意,capacity中我在设计中没有把‘/0’算入容量中,所以扩容时我们要对数组中加一个空间存储‘\0’,但是不把其加入到capacity。

string(const char* str="")
{
	size = _capacity = strlen(str); 
    //注意_capacity存储的是有效字符,未把/0考虑进去,所以,后续扩容需要注意在扩容基础上加1
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

2.实现传入字符串

利用strcpy进行复制数组里的内容,要注意必须有该构造函数,如果没有这个构造函数会出现一个问题:浅拷贝导致的最后析构函数调用两次。

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

3.=的字符重载

需要传入的this中的内容与要等于的数组不是同一个,所以判断不等于后开辟空间strcpy每一个数组的元素。

string& operator=(const string& s)
{
	if (this != &s)
		{
			char* tmp = new char[s._capacity + 1];
			strcpy(tmp, s._str);
			delete[] _str;

			_size = s._size;
			_capacity = s._capacity;
		}
	return *this;
}

[]的运算符重载:是为了让string类也像数组一样被访问所设计的;而string类的设计是通过数组存储的,所以返回的类型使用char类型即可,访问到数组的哪个位置就传入哪个位置的指针。设计两个函数重载的函数,因为该函数调用有需要可读可写的,有点只需要读的。

那么,如何判断一个函数是否需要加const修饰呢?1.如果这个函数实现后需要对数据进行读的,那么加const即可;2.如果是需要对数据可修改的话,那么就不需要const修饰;3.那么如果该函数调用有需要可读可写的也有只需要可读的,需要写两个进行函数重载即可。

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

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

2.析构函数

注意释放new开辟的空间需要加方括号,毕竟不是开辟一个单一的空间。将指针指向空指针

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

3.扩容函数

设计容量时,我们判断传入的要求容量与目前有的容量对比,如果要求的容量小就不改变内存大小了;如果大于,则扩容到要求的大小,并且需要扩容多一位存储‘\0’。

void reserve(size_t capacity)
{
	if (capacity > _capacity)
	{
		char* tmp = new char[capacity + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = capacity;
	}
}

4.字符串中尾插元素

1.加入一个字符

因为只是插入一个元素,所以不需要考虑扩容多少的问题,同一把空间放大两倍即可。最后把已存入的大小加一,在数组后面加‘\0’作为判断结束标志。

void push_back(char c)
{
	if (_size == _capacity)
	{
		reserve(2 * _capacity);
	}
	_str[_size] = c;
	_str[_size + 1] = '\0';
	_size++;
}

+=的运算符重载实现依靠尾插函数实现,最后返回*this指针,函数的返回值为string类的引用以便于加快效率,省去拷贝构造临时变量的时间。

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

2.加入一串字符

需要注意,上面加入一个字符我们扩容直接通过放大原有空间两倍的方式进行解决;但是这里的问题是,我们不知道加入的字符串有多大,以至于如果我们盲目的扩大已有容量两倍会出现:已存大小字符串加上需要存储字符串的总和大于扩大两倍的容量空间。这样的实现是片面的

所以我们先记录下需要扩容的具体大小记为n。如果n小于已有的容量,直接加就可以了;如果大于的情况,我们要扩容,我是将空间扩大到1.5倍的n,这样既不会频繁的扩容导致时间效率降低也不会使得空间开辟过大造成严重的空间浪费。

void append(const char* str)
{
	size_t n = _size + strlen(str);
	if (n > _capacity)
	{
		reserve(n * 1.5);
	}
	strcat(_str, str);
	_size = n;
	_str[_size + 1] = '/0';
}

+=的运算符重载实现依靠append函数实现,最后返回*this指针

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

5.resize函数

需要考虑三种情况:

1.重新设置的size大于本身size

2.重新设置的size等于本身size

3.重新设置的size小于本身size

大于等于本身时,我们可以直接reverse扩容,然后把ch中的值循环尾插到最后,然后在空间最后一位加入‘\0’作为结束标志,并且把size改变为现在的大小;小于更加简单,只需要把设置缩减的位置处变为‘\0’,size改变即可。

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

6.c_str返回string类中数组的地址

设计这个函数就是为了通过指针的形势来访问string类中存储数组的成员变量。

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

7.迭代器

 不同的迭代器的本质不一定相同,但是他们归根到底是为了得到头或者尾的位置,想要通过以一种类似于数组的操作进行对类对象内容的访问。

而因为这string类存储是通过数组实现的,所以我们返回数组指针就可以实现迭代器所想要满足的要求。

所以我们将char*重定义为iterator,iterator在c++中可进行相关迭代器的操作。 

typedef char* iterator;
iterator begin()
{
	return &_str[0];
}

iterator end()
{
	return &_str[_size];
}

 for范围的实现本质就是迭代器,上下两个访问变量的本质其实是没有区别的。

void test_string2()
{
	string s1("hello world");
	string::iterator it = s1.begin();
	while (it < s1.end())
	{
		(*it)++;
		it++;
	}
	cout << s1.c_str() << endl;
	for (auto& ch : s1)
	{
		ch--;
	}
	cout << s1.c_str() << endl;
}

8.指定位置插入

1.插入一个字符

因为是在中间某个位置插入一个元素,以至于该位置后面的内容都要往后移动一位,即前一位的数给后面一位,但是如果从前往后替换内容会被干扰,所以我们是从后往前替换。

如果是插入到最后面,那么其实就是尾插的逻辑设计;如果是插入到其他位置就从后往前插入,在此会出现一个操作的问题:插入在前一位,我们要知道可能会出现越界访问的问题。

针对越界访问问题,我们需要好好讲讲。

如果我们设置从插入位置处向后复制给后一位,那么:设置的end是size大小,到达在pos处后还会判断,end+1处的值赋予end处的值,之后end--,end>=pos进行赋值,这样是可以实现部分的;但是当pos是0时,end到达pos处,赋值给pos后一位后,end变成了-1,但是size_t是无符号整型啊,所以永远大于0,循环一直停不下来。

解决方法有两个:1.把比较的两个值end和pos都变成int型,这样负数也纳入考虑了,但是设计不太符合正常思考的要求,因为我们知道size的大小不会出现负数,而我们为了这样一个特例强行转化成负数判断有点浪费;所以另一种方法是不要将等号作为边界判断的条件,因此我们end计入的是size+1,也意味着我们是把end-1的内容赋值给end处,不断end--进行循环。(那么当pos等于0,我们的end=1时,0处的数据就已经赋值到1处了,最后end--变为0跳出循环结束)

最后注意修改size的大小

string insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size+1 > _capacity)
	{
		reserve(_size + 1 * 1.5);
	}
	size_t end = _size + 1;
	while (end>pos)
	{
		_str[end] = _str[end-1];
		end--;
	}
	_str[pos] = ch;
	_size++;
	return *this;
}

2.插入字符串

插入一串字符也是,先判断是否需要扩容,如果需要就调用扩容函数;此外也需要注意插入到0处的数组越界问题;所以我们不考虑循环中判断条件出现=插入位置的判断,与插入一个字符的逻辑大差不差。

string& insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t n = _size + strlen(str);
	if (_size + 1 > _capacity)
	{
		reserve(n * 1.5);
	}
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end + strlen(str) - 1] = _str[end - 1];
		end--;
	}
	for (int i = 0; i < strlen(str); i++)
	{
		_str[end++] = str[i];
	}
	_size = n;
	return *this;
}

9.删除字符串

对比STL中的string类的删除函数,删除实现时会有一个npos,该值为-1,是为了实现不需要传入任何值便可以全部删除的缺省参数。npos便设置为全局静态变量,如果全局变量放在成员变量中,需要在类的外面进行初始化,但是C++对整型类型开放权限,其可在内部直接赋值。

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

		const static size_t npos = -1;

删除的形式有三种:

1. 没有传入删除到哪的参数,所以后面的数都要删除

2.传入删除到哪的参数足够大,后面的数都要删除

3.传入删除到哪的参数不大,后面的数还要往前补上

实现方法:

第1和第2其实都是把size大小改成pos,在最后数组加入‘\0’;第3则是把后面的补齐前面的,从前往后赋值,最后注意修改size的大小。

string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len == npos || pos + len >= _size - pos)
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		size_t begin = pos + len;
		while (begin <= _size)
		{
			_str[begin - len] = _str[begin++];

		}
		_size -= len;
	}
	return *this;
}

10.find函数

1.找一个字符

size_t find(const char ch, size_t pos = 0) const
{
	assert(pos < _size);
	while (pos < _size)
	{
		if (_str[pos] == ch)
		{
			return pos;
		}
		pos++;
	}
	return npos;
}

2.找一串字符

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

11.清空函数和判空函数

1.清空函数

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

2.判空函数

bool empty() const
{
	if (_size == 0)
		return true;
	else
		return false;
}

12.逻辑比较运算符重载

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

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

 

13.流提取和流插入

需要用全局函数实现,因为如果放到类中。this指针优先于其他,导致实现函数执行时非常别扭。不一定需要通过友元才行,实现的string类就不需要,因为我们可以通过string和[]的运算符重载找到每个元素的地址。

1.流插入

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

2. 流提取

不可以直接像流插入函数逻辑实现,in提取的不会带空格和‘\n’,他们会自动忽略,如果按照下面注释掉的代码跑,该函数会出错误。那么应该通过in的get函数,一个一个提取字符的内容,如果内容是空格或者换行则停止。

istream& operator>>(istream& in, string& s) //cin不会得到空格,直接跳过的
{
	/*char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;*/
	s.clear();
	char buff[128] = { "\0" };
	size_t i = 0;
	char ch = in.get();
	while (ch != ' ' || ch != '\n')
	{
		if (i == 127)
		{
			s += buff;
			i = 0;
		}
		buff[i++] = ch;

		ch = in.get();
	}
	if (i >= 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}

你可能感兴趣的:(c++,开发语言)