string类的模拟实现

string作为存储字符串的基本类,根据库里面的描述,字符串是用于存储连续字符的容器,并且库里面已经有相关的函数及其使用,底层也并不复杂,现在来实现一些较为常用的函数。(更多关于string的库函数见string - C++ Reference (cplusplus.com)

首先string的内部成员需要三个:一个用于存字符串的_str,一个表示有效数据大小的_size,一个表示可用空间容量的_capacity.

另外,库里面的string还有迭代器,由于string的字符都是连续的所以它在这只不过就是char*或const char*类型的指针。

namespace xxx
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
        //...
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
		const static size_t npos = -1;
	};
}

其中npos表示整形最大值。封装到命名空间的原因是防止和库混淆。

函数先看构造和析构,构造函数在库里写了比较多的版本,最常用的应该是拿另一个字符串的全部来构造。在构造函数中要完成的工作有开辟空间,并完成赋值和拷贝,在申请空间时一定要注意比有效空间多申请一个,用于存放'\0',因为_capacity和_size都是指的有效字符大小。根据不同的参数,构造函数可以构成重载。

而析构函数的主要任务就是释放空间,置容量和大小为0.

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

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

除了用构造函数赋值初始化,用‘=’符号也能完成赋值初始化,下面来重载操作符‘=’。

考虑到左操作数将会被修改,而且右操作数的大小有可能比左操作数的大,也有可能更小,不妨直接申请右操作数那么大的空间,然后把后者strcpy给前者。但在这样做之前还要考虑一件事,左操作数之前申请的空间需要被释放,同时为了防止数据丢失可以用临时变量申请空间。

用reserve申请空间亦是如此。

string& operator=(string s)
{
	if (this != &s)
	{
		char *tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[](_str);
		_str = tmp;
		_capacity = s._capacity;
		_size = s._size;
	}
	return *this;
}
void reserve(size_t capacity = 0)
{
	if (capacity > _capacity)
	{
		char *tmp = new char[capacity + 1];
		strcpy(tmp, _str);
		delete[](_str);
		_str = tmp;
		_capacity = capacity;
	}
}

如果自己给自己赋值,那就无需释放空间在申请空间,直接返回this便可.

成员变量_capacity,_size,npos都是私有的,无法随意被访问,但总会有些场景要用到这些私有成员,比如有些地方想知道该类的大小,所以通过函数返回该类的成员_capacity,_size,npos是有必要的。

size_t size()const
{
	return _size;
}

size_t capacity()const
{
	return _capacity;
}

size_t max_size()const
{
	return npos;
}

类似地,string类里面的迭代器也非常简单,直接返回指向的字符,begin()就让指针指向第一个字符,end()就让指针指向最后一个有效字符的下一个字符,也就是说所有的end()都应该指向的是‘\0’。如果不希望迭代器里面的内容被修改,则应该使用之前定义的const_iterator。

iterator begin()
{
	assert(_str != nullptr);
	return _str;
}

iterator end()
{
	assert(_str != nullptr);
	return _str + _size;
}

const_iterator begin()const
{
	assert(_str != nullptr);
	return _str;
}

const_iterator end()const
{
	assert(_str != nullptr);
	return _str + _size;
}

既然string的字符都是连续存放的,那是不是可以像数组那样能够用下标随机访问来大大提高访问效率?的确如此,不过重载'[]'运算符的任务需要自己完成。

同样,'[]'运算符重载的下标随机访问也要分为两种情况,根据内容是否可以被修改分为只读和可读可写。只读则应该加上const修饰。同时,为了确保欲访问的下标在有效空间里面,须保证下标不得超出有效字符的长度。

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

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

可以看到库里string想要尾插数据有两种基本方式,其一是调用push_back,其二是调用append,根据所给的参数可以知道,push_back用于尾插一个字符,append用于尾插多个字符。

在模拟实现时,应注意容量并据此开辟空间。其中append可以根据不同的参数来重载

void push_back(char ch)
{
	if (_capacity == _size)
	{
		//
		size_t capacity = _capacity == 0 ? 4 : (_capacity * 2);
		reserve(capacity);
	}
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}
void append(const string& s)
{
	if (_size + s._size > _capacity)
	{
		reserve(_size + s._size);
	}
	for (size_t i = 0; i < s._size; i++)
	{
		push_back(s._str[i]);
	}
	_size += s._size;
}

void append(const char* s)
{
	size_t size = strlen(s);
	if (size + _size > _capacity)
	{
		reserve(size + _size);
	}
	for (size_t i = 0; i < size; i++)
	{
		push_back(s[i]);
	}
	_size += size;
}

看起来用push_back和append进行尾插似乎很方便,但实际应用中却用的少,因为在库里面有一种更加便捷的操作符:+=。虽然‘+=’操作符的实现原理无非也就是调用push_back和append的尾插,但通常实际还是用+=。从库函数该操作符提供的接口看出,+=不仅可以尾插单个字符也能尾插多个字符或字符串,由此可以重载多个版本

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

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

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

接着来看查找,在字符串中查找子串或字符并返回它的下标,查找字符简单,直接挨个挨个去比较,相等就返回该处的下标,找不到则返回npos。如果要找子串,那就需要借助strncmp来比较。为提高查找的效率,库里面用了一种提供查找起始位置的方式来缩小范围的方式,用户需要提供字符串和查找的起始位置两个参数。

size_t find(const string& s, size_t pos = 0)const
{
	assert(pos <= _size);
	while (pos <= _size - s._size)
	{
		if (strncmp(_str + pos, s._str + pos, s._size))
			return pos;
		pos++;
	}
	return npos;
}

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

最后,再来看插入数据和擦除数据,插入的数据可以是字符可以是字符串,pos参数指明了插入数据的位置。如果插入数据后的字符长度大于该字符串容量,则应当扩容。当确保能够容纳插入后的字符串大小后,先挪动pos后面的数据,在将需要插入的数据放进来。

擦除数据也是提供两个参数,擦除的起始位置pos,默认是从0开始,还有擦除的长度,默认擦除到字符串末尾。

void insert(size_t pos, const string& s)
{
	assert(pos <= _size);
	if (s._size + _size > _capacity)
	{
		reserve(s._size + _size);
	}
	for (size_t i = s._size + _size; i > pos; i--)
	{
		_str[i] = _str[i - s._size];
	}

	strncpy(_str + pos, s._str, s._size);
	_size += s._size;
}

void insert(size_t pos, const char*s)
{
	assert(pos <= _size);
	size_t size = strlen(s);
	if (size + _size > _capacity)
	{
		reserve(size + _size);
	}
	for (size_t i = size + _size; i > pos; i--)
	{
		_str[i] = _str[i - size];
	}
	strncpy(_str + pos, s, size);
	_size += size;
}

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

在一些较大的字符串中,擦除和插入虽然确实比较方便,但由于涉及到数据挪动,导致效率低,在实际应用中应减少使用。

库里面还有更多相关的string函数 ,想了解更多见官网。

代码样例:C++的测试代码/Test_string_0704/Test_string_0704/Test_0704.cpp · Admin/C++测试代码 - 码云 - 开源中国 (gitee.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值