STL库——string(类模拟实现)

发布于:2025-08-20 ⋅ 阅读:(13) ⋅ 点赞:(0)

ʕ • ᴥ • ʔ

づ♡ど

 🎉 欢迎点赞支持🎉

个人主页:励志不掉头发的内向程序员

专栏主页:C++语言


前言

我们上一章节讲解了有关string的函数的用法,本章节就来对string函数的重要函数进行模拟实现,让大家更好的了解我们的string函数的同时对类和对象的认识进一步加深。一起来看一下吧。


一、基本框架

我们先建立一个string.h的头文件,在里面建立string的基本框架。

#include <iostream>
#include <assert.h>

// 建立命名作用域,防止和库容器命名冲突
namespace zxl
{
	class string
	{
	public:

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

我们string中有4个成员变量,有一个是字符串数小于16个时存在栈上的字符串数组buffer,这里不模拟实现,剩下的就是一个存字符串的指针,一个记录数量的size和一个记录开辟空间的capacity。有了这些基础的成员变量,接下来我们就来实现成员函数了。

二、构造函数

我们string的构造函数主要分为两种,其一是无参构造,也就是默认构造,其二就是带参构造。

无参构造我们让我们的指针指向一个'\0'字符,size和capacity默认为0即可。

string()
	: _str(nullptr),
	  _size(0),
	  _capacity(0)
{}

而我们的带参构造用初始化列表比较麻烦,因为先用size构建出参数的数量再去开辟空间好一点,但是我们初始化列表的初始化顺序是按照我们的声明的顺序的,而更改声明顺序代码看着就很怪,所以为了方便一点就直接在函数内部初始化了。

string(const char* s)
{
	_size = strlen(s);
	_capacity = _size;
	// 多一个位置给'\0'
	_str = new char[_size + 1];
	strcpy(_str, s);
}

当然,我们可以把这两个函数用缺省参数变成一个函数

// 默认会有\0		
string(const char* s = "")
{
	_size = strlen(s);
	_capacity = _size;
	// 多一个位置给'\0'
	_str = new char[_size + 1];
	strcpy(_str, s);
}

三、析构函数

析构函数只要把我们指向的资源释放了就行,很容易就不过多赘述。

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

四、拷贝构造

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

五、运算符重载

5.1、赋值重载

先销毁在拷贝。

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

5.2、[]符重载

我们这个符号重载的作用就是为了让我们的字符串可以像数组那样去查看和修改,所以我们这个函数主要就是传字符下标后返回引用来实现的。

char& operator[](size_t pos)
{
	// 这里加断言可以防止我们访问越界
	assert(pos < _size);
	return _str[pos];
}

同时还可以实现一个const版本。

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

5.3、operator</<=/>/>=/==/!=重载

这里和库中保持一致,重载成全局函数吧。

同理,我们一般搞两个函数,后面的全部函数复用即可

<:

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

这里不用函数复用也蛮简单的,但是如果我们以后想要改一下条件时函数复用就只要该两个函数即可,所以在这里函数复用还是优于每个都写的。

5.4、operator<</>>重载

这两个函数也得是在全局域中实现才行

流输出:<<

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		cout << ch;
	}
	return out;
}

流输入:>>

我们不能这样写

istream& operator>>(istream& in, string& s)
{
	// cin和scanf默认空格和换行是分隔符,会自动跳过,
	// 如果这样写就永远不会停下来了。
	char ch;

	in >> ch;
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		in >> ch;
	}
	return in;
}

而应该调用我们cin里的get函数,它是一个字符一个字符的读的,就没有分隔符了。

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

但是由于我们原版流输入时会清空我们的字符串,所以我们也得清空一下。

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

这就是流输入和输出了。

但是如果我们一次输入太多的字符了,我们的空间不够的情况下会频繁的扩容,我们有没有什么办法去解决这一问题降低编译器损耗呢?我们可以创建一个字符串当缓冲区,先给它开辟一个空间,如果它的空间满了我们再一次性写入我们的string中,这样可以大大降低我们内存的扩容次数。

优化:

istream& operator>>(istream& in, string& s)
{
	s.clear();
	const int N = 256;
	// 缓冲字符串
	char buffer[N];
	int i = 0;
	char ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		buffer[i++] = ch;
		if (i == N - 1)
		{
			buffer[i] = '\0';
			s += buffer;
			i = 0;
		}
		ch = in.get();

	}

	if (i > 0)
	{
		buffer[i] = '\0';
		s += buffer;
	}
	return in;
}

访问私有才需要友元,这里没有访问私有,所以就不用访问。

六、增删查改

6.1、push_back函数

我们上一章节知道我们string函数的主要作用就是再字符串后面插入一个字符。这个函数实现十分简单,无非就是str[size] = 字符后size++即可,但是我们得考虑我们的开辟的空间是否足够的问题,我们可以先写一个扩容函数reserve(下面有怎么实现),先判断够不够,不够就扩容即可。

void push_back(char c)
{
	// 如果相等就是空间不足
	if (_capacity == _size)
	{
		// 扩容,防止容量初始值是0
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	// 插入
	_str[_size] = c;
	_size++;
    // 注意,不然打印就会有问题
    _str[_size] = '\0';
}

6.2、append函数

我们append函数可以插入字符串,那我们的扩容也得注意不能像push_back一样2倍扩容,不然可能会扩容后也不够用,所以得按需扩容。

void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		// 大于2倍要多少开多少,小于2倍开2倍
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	strcpy(_str + _size, str);
	_size += len;
}

6.3、operator+=函数

函数复用即可。

插入字符:

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

插入字符串:

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

6.4、insert函数

这个函数的作用就是在字符串的任意位置插入,主要涉及字符的移动

插入字符:

void insert(int pos, const char ch)
{
    // 防止pos比字符数多
	assert(pos <= _size);

	if (_capacity == _size)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

    // 挪动字符
	for (int i = _size; i >= pos; i--)
	{
		_str[i + 1] = _str[i];
	}

    // 插入
	_str[pos] = ch;
	++_size;
}

插入字符串:

void insert(int pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		// 大于2倍要多少开多少,小于2倍开2倍
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

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

	for (int i = 0; i < len; i++)
	{
		_str[i + pos] = str[i];
	}
	_size += len;

}

6.5、erase函数

这个函数就是用来删除指定位置指定长度的字符串的,如果我们不写长度就默认从指定位置后面全部删除,所以我们要创建一个缺省参数,这个参数就用npos来替换,因为我们原来的string在类外也可以调用npos,所以这里决定创建一个静态成员函数。

static const size_t npos;

静态成员要在类外进行定义,同时这里得在不同文件中定义,不然就会重定义。

const size_t zxl::string::npos = -1;

同时erase函数就得分为我们要删的长度是不是超过了字符串长度的两种情况。

void erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
			
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		for (int i = pos; i < _size - len; i++)
		{
			_str[i] = _str[i + len];
		}
		_size -= len;
		_str[_size] = '\0';
	}

}

6.6、find函数

此函数的作用就是查找字符或字符串,找到了就返回字符串下标,否则就返回npos值。

查找字符:

size_t find(const 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* str, size_t pos = 0)
{
	assert(pos < _size);
	// 这里就用比较容易的办法解决啦,等后面有时间会讲具体怎么整。
	// 这个函数就是返回第一次匹配上的指针。
	const char* ptr = strstr(_str, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}

七、其他成员函数

7.1、c_str函数

我们上一章节说过,我们的c_str函数就是返回我们str的指针的,实现起来很简单。

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

7.2、size函数

这个函数就是用来返回我们size的,实现起来也很容易。

const size_t size() const
{
	return _size;
}

7.3、capacity函数

同理

const size_t capactiy() const
{
	return _capacity;
}

7.4、reserve函数

这个函数的主要作用就是扩容,我们string插入字符时有的时候可能capacity会不够,此时就应该自动扩容,所以我们就可以先实现这个函数,然后再在插入中插入使其自动扩容。

手动扩容就是先开辟一个更大的空间后把原来空间的数据拷贝到新空间后释放原来的空间的操作

void reserve(size_t n)
{
	// 只扩容不缩容
	if (n > _capacity)
	{
		// 多开辟一个存'\0'
		char* tmp = new char[n + 1];

		// 拷贝数据到新空间
		strcpy(tmp, _str);

		// 释放旧空间
		delete[] _str;

		_str = tmp;
		_capacity = n;
	}
}

7.5、substr函数

这个函数是返回我们想要的字符串的一部分的

string substr(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	// 让我们的len变成有效长度
	if (len > _size - pos)
	{
		len = _size - pos;
	}

	string sub;
	sub.reserve(len);
	for (int i = 0; i < len; i++)
	{
		sub += _str[pos + i];
	}

	return sub;
}

我们要注意我们传值返回要调用拷贝构造,如果没有实现就是浅拷贝,会出问题。

7.6、clear函数

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

八、迭代器

这里的迭代器就简单实现一个iterator,我们实现的iterator就是将我们的char*改名一下而已。

typedef char* iterator;

8.1、begin函数

我们的iterator是char*,那我们的begin函数就是返回我们str的首地址啦。

iterator begin()
{
	return _str;
}

8.2、end函数

我们的end函数也就是我们str的最后一位地址的下一位了。

iterator end()
{
    // 首位加总数就是末尾的下一位
	return _str + _size;
}

此时我们就可以实现我们自己写的迭代器的遍历了。虽然可能还有一点小问题,但是勉强能用。

int main()
{
	zxl::string s1("hello world");
	zxl::string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
    return 0;
}

同时我们的范围for也可以使用了。

int main()
{
	zxl::string s1("hello world");
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
	return 0;
}

这也更好的说明了我们的范围for底层就是迭代器。当然我们范围for想要实现必须我们迭代器和原来的一模一样,就比如如果我们的begin变成Begin了,我们的迭代器还是正常的,但是范围for就不行了。

当然,除了普通的迭代器还有const迭代器,const迭代器就是用我们的const char*来实现的

typedef const char* const_iterator;

只要将我们刚才写的begin和end的返回值改成const_iterator在增加一个const限制即可,不是说一定要把我们的begin变成cbegin、end变成cend才可以。

const_iterator begin() const
{
	return _str;
}

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

反向迭代器比较复杂,我们后面再讲。


总结

以上便是我们string的一些重要代码的实现啦,希望大家能够仔细研究,下一章节我们继续往后学下一个容器vector,我们下一章节再见。

🎇坚持到这里已经很厉害啦,辛苦啦🎇

ʕ • ᴥ • ʔ

づ♡ど


网站公告

今日签到

点亮在社区的每一天
去签到