string类的模拟实现

string类初步了解

目录

一.   成员变量

二.   构造、析构、赋值

1.构造

2.析构

3.赋值

三.   容量

1.size

2.clear

3.reserve

4.resize

四.   遍历

1.[ ]操作符重载

2.迭代器

五.   增删查改、字符串操作

1.增(push_pack、append、operator+=、insert)

2.删(erase)

3.查(find)

4.改

5.字符串操作(c_str)

六.   比较操作符、流操作符重载

1.比较操作符

2.流提取操作符

3.流插入操作符


为了让我们进一步加深对string类的了解,我们可以试着模拟实现一下string类

实际上,string是basic_string类模板的一个实例化,因此,我们在这里就只是模拟一下这个实例化的string类,我们也只是模拟一下常见的接口


一.   成员变量

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

 除此之外,还要注意npos这个静态变量

public:
    static const size_t npos=-1;

二.   构造、析构、赋值

1.构造

我们就还是模拟一下那几个常用的方式、不传参、传对象、传字符串

其中,我们可以使用缺省值来将不传参和传字符串的方式结合起来

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

传对象也是类似的操作

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

2.析构

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

和我们学的顺序表类似,释放、置空

3.赋值

赋值,就涉及到深浅拷贝的问题了

为了减少拷贝,我们采用的是传引用,而若是直接浅拷贝,将s中的_str赋给该对象,会导致两个_str指向的是同一块地址,引发一系列的问题(之前在引用中讲过了)

因此,我们可以先拷贝一下s对象中的_str,之后将所拷贝到的指针赋给对象。

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

我们还可以换成现代写法

void Swap(string& s)
{
	swap(_str, s._str);
	swap(_size, s._size);
	swap(_capacity, s._capacity);
}

string& operator=(string& s)
{
    string tmp(s);
	Swap(tmp);
	return *this;
}

这种写法其实就是将整个s进行拷贝构造,之后将tmp与对象进行交换

当然我们还可以简写一下,将拷贝构造这一过程放在传参中

void Swap(string& s)
{
	swap(_str, s._str);
	swap(_size, s._size);
	swap(_capacity, s._capacity);
}

string& operator=(string s)
{
	Swap(s);
	return *this;
}

三.   容量

1.size

没啥好说的,返回_size就完了,其他的例如length、capacity同理

size_t size() const
{
	return _size;
}

2.clear

在清除时,对_size置0是肯定的,而我们没有必要去清除_str中的内容,只需要将_str[0]置为'\0'就可以了。

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

3.reserve

首先要考虑的就是参数n与_capacity的大小关系,当n<=_capacity时,不会进行任何操作,因此,我们只需要考虑大于的增容情况

增容的话最好是重新开辟大小为n的空间,之后将原对象的_str拷贝进来,最后重新将这个新_str赋给原对象,并改变_capacity的大小就好了。

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

4.resize

与reserve不同,在n<_size时,会缩小_size,因此我们需要考虑两种情况

小于_size时,只需要减容,不需要进行赋值,而大于时,我们还需要考虑是否大于_capacity,若大于,还需要进行增容(可以使用reserve),之后就可以进行赋值了

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

当然,我们也可以不进行第二个判断,直接reserve,让reserve函数去判断是否大于_capacity


四.   遍历

主要就是[ ]操作符重载、迭代器(范围for本质上也是一个迭代器)

1.[ ]操作符重载

直接转换为下标访问_str,为了避免越界,还要考虑一下pos的范围

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

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

2.迭代器

在string类中,迭代器本质上就是一个指针,因此我们可以直接模拟为指针

typedef char* iterator;
typedef const char* const_iterator;

const_iterator begin() const
{
	return _str;
}

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

iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

反向的不好弄,那就别弄了!(


五.   增删查改、字符串操作

1.增(push_pack、append、operator+=、insert)

push_pack

就是往后插个字符

记得扩容就好,而由于插的是单个字符,我们可以直接指数扩增

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

append

插字符串的话,也和上面类似,不一样的是并不适合指数扩增,因为指数扩增后不一定符合要求,可能空间还是不够,直接扩增到能放下字符串就好了

当然,原本的string类中的扩增是根据字符串长度找到合理的指数扩增次数,挺麻烦的,没必要我们自己去实现

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

当然,若是想要传string对象,将str替换成s._str就行了,不写了

operator+=

上面两个的结合体,直接复用就行了

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

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

insert 

在pos位置插入,首先要判断pos是否合法,之后判断是否需要扩增,之后就可以往后挪了,挪完将字符或字符串插入进去就行了

string& insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	_size++;
	for (size_t i = _size; i > pos; i--)
	{
		_str[i] = _str[i - 1];
	}
	_str[pos] = ch;
	return *this;
}

string& insert(size_t pos, const char* s)
{
	assert(pos <= _size);
	int len = strlen(s);
	if (_size + len > _capacity)
		reserve(_size + len);
	_size += len;
	for (size_t i = _size; i >= pos+len; i--)
	{
		_str[i] = _str[i - len];
	}
	strncpy(_str + pos, s, len);
	return *this;
}

要注意的是,例如插入字符时,往前挪的时候不能这样

for (size_t i = _size-1; i >= pos; i--)
{
	_str[i+1] = _str[i];
}

如果这样,在头插的时候,当i=0时i--还会执行一次,而由于i是无符号的,会变成一个很大的数,从而导致死循环,而我们也不能将i的类型改为int,因为pos的类型时size_t,隐式类型转换会默认为size_t更大。

2.删(erase)

也要分为两种情况,当删不完后面的字符时,要往前挪,而删的完的话,直接将pos位置置为'\0'

要注意的是,erase的参数len有缺省值,因此当len采用缺省值时,也是固定删完的

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

3.查(find)

首先还是确定pos是否合法,之后只需要从pos位置开始遍历来找对应的字符或字符串,在找字符串时,我们可以利用库函数strstr来完成,而strstr返回的是指针,我们只需要用指针减指针来得到之间的元素个数,即对应的下标

size_t find(char ch, size_t pos = 0)
{
	for (int i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
			return i;
	}
	return npos;
}

size_t find(const char* s, size_t pos = 0)
{
	char* ptr=strstr(_str+pos, s);
	if (ptr == nullptr)
		return npos;
	return ptr - _str;
}

4.改

想改的话直接用find返回的下标进行下标访问来修改就行了

5.字符串操作(c_str)

直接返回_str就行了

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

六.   比较操作符、流操作符重载

1.比较操作符

简简单单地比较一下字符串(strcmp),复用一下代码

bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}

bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}

bool operator<=(const string& s1, const string& s2)
{
	return s1 < s2 || s1 == s2;
}

bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}

bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

bool operator!=(const string& s1, const string& s2)
{
	return !(s1 == s2);
}

2.流提取操作符

和我们日期类中实现的类似,只是将年月日替换成遍历的字符串

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

3.流插入操作符

istream其实是一个类,我们可以使用其中的函数get()来完成重载

string类的模拟实现_第1张图片

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

而若是仅仅这样写,会出现一些问题。

在流插入时,是对原对象进行重新赋值,而若是像上面这样写,当对象有初始值时,会在初始值后面进行插入,不符合要求,因此我们可以在开始加入clear()

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

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