深入计算机语言之C++:String的模拟实现

news2024/11/14 6:51:05

 🔑🔑博客主页:阿客不是客

🍓🍓系列专栏:从C语言到C++语言的渐深学习

欢迎来到泊舟小课堂

😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注

一、模拟实现 string 库 

1.1 string 的成员变量

string 简单来说就是一个被封装可动态增长的字符数组,这与我们在数据结构中学的串非常类似,所以我们可以借助实现串的思路来大致模拟 string 的结构。

下面是string的成员变量:

namespace betty
{
	class string 
    {
	public:
		//...
	private:
		size_t _size;//当前有效字符的个数
		size_t _capacity;//当前容量的大小,方便扩容
		char* _str;//存储的字符串
	};
}

值得注意的是\0既不占据有效长度的大小,也不占据容量的大小。

img

1.2 string的构造和销毁(析构)

💬 string 构造函数的实现:

我们先来模拟实现一个不带参的构造函数,也被称为默认构造:

string()//无参初始化
	:_str(new char[1]{'\0'})
	,_size(0)
	,_capacity(0)
{}

这里我们开一个空间给 \0,既然都这么写了,我们不如直接加入缺省值实现带参的构造:

string(const char* str = "")
{
	_size = strlen(str);
	// capacity不包括 \0
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

一般的类都是提供全缺省的,值得注意的是,这里缺省值给的是 " "

有人看到指针 char* 在缺省值的位置就忍不住想给个空 nullptr:

string(const char* str = nullptr)

  不能给!给了就崩。因为 strlen 是不会去检查空的,它是去找 \0 ,也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。所以我们这里给的是一个空的字符串 " ",常量字符串默认就带有 \0,这样就不会出问题。

我们有了构造,那么同时也要有销毁,销毁需要一个析构函数。

💬 ~string 析构函数的实现:

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

1.3 返回字符指针c_str

📚 c_str() 在库中是返回的是C语言字符串的指针常量,是可读不写的。

实现方式很简单:

/* 返回C格式字符串:c_str */
const char* c_str() const
{
	return _str;
}

const char*,因为是可读不可写的,所以我们需要用 const 修饰。c_str 返回的是当前字符串的首字符地址,这里我们直接 return _str 即可实现。

我们来测试一下:

void TestString1()
{
	string s1("hello world");
	string s2;
 
	cout << s1.c_str() << endl;
}

🚩 运行结果如下: 

1.4 size() 和 operator[] 的实现

💬 size() 的实现:

size_t size() const
{
	return _size;
}

size() 只需要返回成员函数 _size 即可,考虑到不需要修改,我们加上 const,capacity() 同理。

💬 capacity() 的实现:

size_t capacity() const
{
	return _capacity;
}

💬 operator[] 的实现:

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

直接返回字符串对应下标位置的元素,因为返回的是一个字符,所以我们这里引用返回 char。

我们来测试一下,遍历整个字符串:

void TestString2()
{
	string s1("hello world");

	for (size_t i = 0; i < s1.size(); i++)
    {
		cout << s1[i] << " ";
	}
	cout << endl;
}

🚩 运行结果如下: 

 我们再来测试一下 operator[] 的 "写" 功能:

void TestString1()
{
	string s1("123456");

	cout << s1.c_str() << endl;
	for (size_t i = 0; i < s1.size(); i++)
	{
		s1[i] += 2;
	}
	cout << s1.c_str() << endl;
}

🚩 运行结果如下:

要注意的是:

  1. 普通对象可以调用,但是 const 对象呢?所以我们还需要写一个 const 对象的重载版本
  2. 我们需要考虑一下越界的问题,这里我们使用断言暴力处理一下

二、实现迭代器

2.1 迭代器的实现

 在上一章中,我们首次讲解迭代器,为了方便理解,我们当时解释其为像指针一样的类型。实际上,迭代器并非指针,而是类模板。 只是它表现地像指针,模拟了指针的部分功能。

对于 string 类型的迭代器的实现非常简单,它就是一个 char* 的指针罢了,后面我们讲解 list 的时候它不是指针了,又是自定义类型了。所以迭代器其实只是一个类模板,根据要实现的功能来决定。

💬 实现迭代器的 begin() 和 end() :

typedef char* iterator;
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}

💬 我们来测试一下:

void TestString1()
{
	string s1;
	string s2("123456");

	cout << s2.c_str() << endl;

	string::iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

🚩 运行结果如下:

const 迭代器就是可以读但是不可以写的迭代器。 

💬 const 迭代器:

typedef char* const_iterator;
const const_iterator begin() const
{
	return _str;
}
const const_iterator end() const
{
	return _str + _size;
}

迭代器的底层是连续地物理空间,给原生指针++解引用能正好贴合迭代器的行为,就能做到遍历。但是对于链表和树型结构来说,迭代器的实现就没有这么简单了。但是,强大的迭代器通过统一的封装,无论是树、链表还是数组……它都能用统一的方式遍历,这就是迭代器的优势,也是它的强大之处。

2.2 范围for

上一章讲 string 类对象的遍历时,我们讲的第三种方式就是范围 for,我们现在就来演示一下范围 for 的实现:

for (auto e : s1)
{
	cout << e << " ";
}
cout << endl;

你会发现根本就不需要自己实现,你只要把迭代器实现好,范围 for 直接就可以用。范围 for 的本质是由迭代器支持的,编译时范围 for 会被替换成迭代器。

三、string 的增删查改

3.1 reserve 的实现

💬 我们先实现一下 reserve 增容:

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

这里可以检查一下是否真的需要增容,万一接收的 n 比 _capacity 小,就不动。

这里我们之前讲数据结构用的是 realloc,现在我们熟悉熟悉用 new,还是用申请新空间、原空间数据拷贝到新空间,再释放空间地方式去扩容。我们的 _capacity 存储的是有效字符,没算 \0,所以这里还要 +1 为 \0 开一个空间。

3.2 push_back 的实现

💬 push_back:

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

首先检查是否需要增容,如果需要就调用我们上面实现的 reserve 函数,参数传递可以用三目操作符,防止容量是0的情况,0乘任何数都是0从而引发问题的情况。然后在 \0 处插入要追加的字符 append_ch,然后 _size++ 并手动添加一个新的 \0 即可。

我们来测试一下效果如何:

void TestString2()
{
	string s1("hello world");
	cout << s1.c_str() << endl;
 
	s1.push_back('!');
	cout << s1.c_str() << endl;
 
	s1.push_back('A');
	cout << s1.c_str() << endl;
}

 🚩 运行结果如下:

3.3 append 的实现

💬 append:

void string::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;
}

append 是追加字符串的,首先我们把要追加的字符串长度计算出来,然后看容量够不够,不够我们就交给 reserve 去扩容,大于容量2倍要多少扩多少,小于2倍扩2倍。

3.4 operator+= 的实现

这就是我们一章说的 "用起来爽到飞起" 的 += ,因为字符和字符串都可以用 += 去操作,所以我们需要两个重载版本,一个是字符的,一个是字符串的。我们不需要自己实现了,直接复用 push_back 和 append 就好了。

💬 operator+= 

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

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

3.5 insert 的实现

💬 insert:字符

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

	return *this;
}

💬 insert:字符串

string& string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}
	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		end--;
	}
	strncpy(_str + pos, str, len);
	_size += len;

	return *this;
}

测试一下:

void TestString3()
{
	string s1 = "hello world";

	s1.insert(1, '#');
	cout << s1.c_str() << endl;
	s1.insert(0, "*******");
	cout << s1.c_str() << endl;
}

 🚩 运行结果如下:

insert 都实现了,那 push_back 和 append 直接复用,岂不美哉?

⚡ 修改 push_back 和 append:

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

		insert(_size, c);
	}

	void string::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;

		insert(_size, str);
	}

3.6 erase 的实现

删除指定位置之后长度为 len 的字符,如果超过字符串末尾,则删除到最后一个字符为止

💬 erase:

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

	return *this;
}

测试一下:

void TestString4()
{
	string s1 = "hello world";

    cout << s1.c_str() << endl;
	s1.erase(0, 1);
	cout << s1.c_str() << endl;
	s1.erase(1, 2);
	cout << s1.c_str() << endl;
	s1.erase(1, 6);
	cout << s1.c_str() << endl;
	s1.erase(0, 2);
	cout << s1.c_str() << endl;
}

🚩 运行结果如下:

3.7 resize 的实现

我们为了扩容,先实现了 reverse,现在我们再顺便实现一下 resize。这里再提一下 reverse 和 resize 的区别:resize 分给初始值和不给初始值的情况,所以有两种:

但是我们上面讲构造函数的时候说过,我们可以使用全缺省的方式,这样就可以二合一了resize 实现的难点是要考虑种种情况,我们来举个例子分析一下:

如果欲增容量比 _size 小的情况:

因为标准库是没有缩容的,所以我们实现的时候也不考虑去缩容。我们可以加一个 \0 去截断。

如果预增容量比 _size 大的情况:

resize 是开空间 + 初始化,开空间的工作我们就可以交给已经实现好的 reserve,然后再写 resize 的初始化的功能,我们这里可以使用 memset 函数。

💬 resize:

void string::resize(size_t n, char c)
{
	if (n < _size)
	{
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		if (n > _capacity)
		{
			reserve(n);
		}
		// 起始位置   初始化字符   初始化个数
		memset(_str + _size, c, n - _size);
		_size = n;
		_str[_size] = '\0';
	}
}

3.8 find 的实现

find 是用于查找字符或者字符串,返回对应下标,我们先来实现较为简单的查找字符的功能。

💬 find:查找字符

size_t string::find(char c, size_t pos) const
{
	if (pos >= _size)
		return npos;

	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == c)
		return i;
	}

	return npos;
}

遍历整个字符串,找到了目标字符 ch 就返回对应的下标。如果遍历完整个字符串都没找到,就返回 npos(找到库的来)。

💬 这个 npos 我们可以在成员变量中定义:

...
    private:
		char* _str;
		size_t _size;
		size_t _capacity;
    public:
		static const size_t npos;
	};
 
	const size_t string::npos = -1;   // 无符号整型的-1,即整型的最大值。
 
...
}

💬 find:查找字符串

size_t string::find(const char* s, size_t pos) const
{
	if (pos >= _size)
		return npos;

	size_t len = strlen(s);
	for (size_t i = pos; i <= _size - len; ++i)
	{
		if (!strncmp(&_str[i], s, len))
			return i;
	}

	return npos;
}

strncmp是用来比较字符串大小的,小于返回<0,大于返回>0,相等返回 0。

3.9 clear 的实现

💬 clear:清理string

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

3.10 empty 的实现

💬 empty:判断字符串是否为空,为空返回 true,不为空返回 false。

bool string::empty() const
{
	return _size == 0;
}

四、运算符重载

4.1 operator< 的实现

💬 我们在全局实现:

bool string::operator<(const string& s)
{
	if (_size < s._size)
		return true;
	if (_size > s._size)
		return false;
	for (size_t i = 0; i < _size; i++)
	{
		if (_str[i] < s._str[i])
			return true;
		if (_str[i] > s._str[i])
			return false;
	}
	return false;
}

当然,我们还可以实现的更简单些,直接用 strcmp 偷个懒:

bool string::operator<(const string& s)
{
	return strcmp(_str, s.c_str()) < 0;
}

4.2 operator> 的实现

bool string::operator>(const string& s)
{
	//if (_size > s._size)
	//	return true;
	//if (_size < s._size)
	//	return false;
	//for (size_t i = 0; i < _size; i++)
	//{
	//	if (_str[i] > s._str[i])
	//		return true;
	//	if (_str[i] < s._str[i])
	//		return false;
	//}
	//return false;

	return strcmp(_str, s.c_str()) > 0;
}

4.3 复用

有了上面两种的基础,剩下的都可以直接进行复用:

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

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

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

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

五、流插入与流提取

我们当时实现日期类的流插入和流提取时,也详细讲过这些,当时讲解了友元。在友元那一章我们说过 "占参问题" ,这里就不再多做解释了。

如果我们重载成成员函数,第一个位置就会被隐含的 this 指针占据。这样实现出来的流插入必然会不符合我们的使用习惯,所以我们选择在全局实现。在全局里不存在隐含的 this 指针了。

💬 operator<<

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

	return out;
}

💬 operator>>

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}

	return in;
}

六、深浅拷贝

6.1 引入

我们前面完成了string类型的构造和析构,但是这里的写法其实是有一个很大的问题的,不知道同学们发现了没有,让我们看看下列的测试案例:

void TestString9()
{
	string s1 = "hello world";

	cout << s1 << endl;
}

 🚩 顺利编译通过:

再看另一种情况: 

void TestString9()
{
	string s1 = "hello world";
	string s2(s1);

	cout << s1 << endl << s2 << endl;
}

 🚩 代码发生崩溃(提示语句中的返回值):

🔑 详细解析:

打印的时候还没有上什么问题,但当出作用域的时候需要调用析构:

❓ 如何解决这样的问题呢?

我们 s2 拷贝构造你 s1,本意并不是想跟你指向一块空间!我们的本意是想让 s2 有一块自己的空间,并且能内容是 s1 里的 hello world

所以这里就涉及到了深浅拷贝的问题,我们下面就来探讨一下深浅拷贝的问题。

6.2 深浅拷贝问题

举个最简单的例子 —— 拷贝就像是在抄作业!

浅拷贝:直接无脑照抄,连名字都不改。

            (直接把内存无脑指过去)

深拷贝:聪明地抄,抄的像是我自己写的一样。

            (开一块一样大的空间,再把数据拷贝下来,指向我自己开的空间)

  • 浅拷贝就是 原封不动 地把成员变量按字节依次拷贝过去。
  • 深拷贝就是进行深一个层次的拷贝,不是直接拷贝,而是 拷贝你指向的空间

6.3 拷贝构造的实现

 我们之前实现日期类的时候,用自动生成的拷贝构造(浅拷贝)是可以的,所以当时我们不用自己实现拷贝构造,让它默认生成就足够了。但是像 string 这样的类,它的拷贝构造我们不得不亲自写。

💬 string 的拷贝构造:

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

6.4 赋值的深拷贝

💬 现在有一个 s3,如果我们想把 s3 赋值给 s1:

void TestString8()
{
	string s1 = "hello world";
	string s2(s1);   // 拷贝构造
	
	string s3 = "bac";
	s1 = s3;  // 赋值

	cout << s1 << endl << s2 << endl << s3 << endl;
}

如果你不自己实现赋值,就和之前一样,会是浅拷贝,也会造成崩溃:

所以,我们仍然需要自己实现一个 operator= ,实现思路如下:

💬 代码实现 operator=  

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

🔑 代码解析: 

  • 根据我们的实现思路,首先释放原有空间,然后开辟新的空间,最后把 s3 的值赋值给 s1。
  • 为了防止自己给自己赋值,我们可以判断一下。
  • 这时我们还要考虑一个难以发现的问题,如果 new 失败了怎么办?
  • 抛异常!抛异常!抛异常!失败了没问题,也不会走到 strcpy,但问题是我们已经把原有的空间释放掉了,神不知鬼不觉地,走到析构那里二次释放可能会炸,所以我们得解决这个问题!

七、传统写法和现代写法

7.1 拷贝构造的现代写法

现在我们来介绍一种现代写法,它和传统写法本质工作是一样的,即完成深拷贝。现代写法的方式不是本本分分地去按着 Step 一步步干活,而是 "投机取巧" 地去完成深拷贝。

string(const string& s)
    : _str(nullptr) 
{
	string tmp(s._str);
	swap(tmp);
}

为此,我们还要为 string 类设计一个 swap :

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

现代写法的本质就是复用了构造函数。我想拷贝,但我又不想自己干,我把活交给工具人 swap 来帮我干,交换了指向空间的指针。

❓ 我们为什么要在初始化列表中,给 _str 个空指针:

我们可以设想一下,如果我们不对他进行处理,那么它的默认指向会是个随机值。

这样交换看上去没啥问题,确实能完成深拷贝,但是会引发一个隐患!

tmp 是一个局部对象,我们把 s2 原来的指针和 tmp 交换了,那么 tmp 就成了个随机值了。tmp 出了作用域要调用析构函数,对随机值指向的空间进行释放,怎么释放?都不是你自己的 new / malloc 出来的,你还硬要对它释放,就可能会引发崩溃。

我们这里初始化列表中把 nullptr 给 _str,是为了交换完之后, nullptr 能交到 tmp 手中,这样 tmp 出了作用域调用析构函数就不会翻车了。

7.2 赋值重载的现代写法

💬 现代写法:复用拷贝构造:

string& operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s._str);
		swap(tmp);
	}
	return *this;
}

我们先通过 s3 拷贝构造出 tmp,这样 tmp 就是 _str 的工具人了。交换完之后,正好让 tmp 出作用域调用析构函数,属实是一石二鸟的美事。把 tmp 压榨的干干净净,还让 tmp 帮忙把屁股擦干净(释放空间)。

⚡ 还有更简洁的写法:将拷贝构造放在缺省值里面

string& operator=(string tmp)
{
	if (this != &s)
	{
		swap(tmp);
	}
	return *this; 
}

和上面的写法本质是一样的。这种写法不用引用传参,它利用了拷贝构造。

总结:

传统写法和现代写法就是交换 内容 和交换 指向内容的指针 的区别

现代写法在 string 中体现的优势还不够大,因为好像和传统写法差不多,那是因为string 在底层实现只是一个 char* 的类型,可以使用 strcpy 进行交换,只是多了两个大小和容量的成员变量。

但是到后面我们实现 vector、list 的时候,你会发现现代写法的优势真的是太大了。现代写法写起来会更简单些,比如如果是个链表,传统写法就不是 strcpy 这么简单的了,你还要一个一个结点使用 strcpy或者其他的办法 将 next 结点拷贝过去,但是现代写法只需要调用 swap 交换一下首元素的结点就可以了。

现代写法更加简洁,只是在 string 这里优势体现的不明显罢了,我们后面可以慢慢体会。

八、完整代码

💬 string.h:

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<string>
#include<cassert>
using namespace std;

namespace Bit
{
	class string
	{
	public:
		typedef char* iterator;
		typedef char* const_iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		const const_iterator begin() const
		{
			return _str;
		}
		const const_iterator end() const
		{
			return _str + _size;
		}

		// 默认构造
		//string()//无参初始化
		//	:_str(new char[1]{'\0'})
		//	,_size(0)
		//	,_capacity(0)
		//{}
		string(const char* str = "")
		{
			_size = strlen(str);
			// capacity不包括 \0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// 深拷贝问题
		// 拷贝构造
		/*string(const string& s)
		{
			_str = new char[s._capacity];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}*/
		//现代写法:
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
		}

		// 赋值
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;
				_str = new char[s._capacity + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
			}
			
			return *this;
		}*/
		// 现代写法
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		string tmp(s._str);
		//		swap(tmp);
		//	}
		//	return *this;
		//}
		// 最终简化版
		// 拷贝构造一定要引用,不然会产生自己调用自己
		// 赋值可以调用拷贝构造,可以不用引用
		string& operator=(string tmp)
		{
			swap(tmp);
			return *this; 
		}


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

		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];
		}
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}

		// string的增删查改
		void reserve(size_t n);
		void push_back(char c);
		void append(const char* str);
		string& operator+=(char c);
		string& operator+=(const char* str);

		// 在pos位置上插入 字符c/字符串str,并返回该字符的位置
		string& insert(size_t pos, char c);
		string& insert(size_t pos, const char* str);

		// 从pos位置上删除len个字符
		string& erase(size_t pos, size_t len = npos);

		void resize(size_t n, char c = '\0');

		// 返回 字符c/子串s 在string中第一次出现的位置
		size_t find(char c, size_t pos = 0) const;
		size_t find(const char* s, size_t pos = 0) const;

		void clear();
		void swap(string& s);
		bool empty()const;

		bool operator<(const string& s);
		bool operator<=(const string& s);
		bool operator>(const string& s);
		bool operator>=(const string& s);
		bool operator==(const string& s);
		bool operator!=(const string& s);

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

		static const size_t npos = -1;
	};

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& out, string& s);
}

💬 string.cpp:

#include"string.h"

namespace Bit
{
	// static const size_t npos = -1;

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

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

		insert(_size, c);
	}

	void string::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;

		insert(_size, str);
	}

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

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

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

		return *this;
	}

	string& string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			end--;
		}
		strncpy(_str + pos, str, len);
		_size += len;

		return *this;
	}

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

		return *this;
	}

	void string::resize(size_t n, char c)
	{
		if (n < _size)
		{
			_str[n] = '\0';
			_size = n;
		}
		else
		{
			if (n > _capacity)
			{
				reserve(n);
			}
			// 起始位置   初始化字符   初始化个数
			memset(_str + _size, c, n - _size);
			_size = n;
			_str[_size] = '\0';
		}
	}

	size_t string::find(char c, size_t pos) const
	{
		if (pos >= _size)
			return npos;

		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == c)
				return i;
		}

		return npos;
	}

	size_t string::find(const char* s, size_t pos) const
	{
		if (pos >= _size)
			return npos;

		size_t len = strlen(s);
		for (size_t i = pos; i <= _size - len; ++i)
		{
			if (!strncmp(&_str[i], s, len))
				return i;
		}

		return npos;
	}

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

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

	bool string::empty() const
	{
		return _size == 0;
	}

	bool string::operator<(const string& s)
	{
		//if (_size < s._size)
		//	return true;
		//if (_size > s._size)
		//	return false;
		//for (size_t i = 0; i < _size; i++)
		//{
		//	if (_str[i] < s._str[i])
		//		return true;
		//	if (_str[i] > s._str[i])
		//		return false;
		//}
		//return false;

		return strcmp(_str, s.c_str()) < 0;
	}

	bool string::operator>(const string& s)
	{
		//if (_size > s._size)
		//	return true;
		//if (_size < s._size)
		//	return false;
		//for (size_t i = 0; i < _size; i++)
		//{
		//	if (_str[i] > s._str[i])
		//		return true;
		//	if (_str[i] < s._str[i])
		//		return false;
		//}
		//return false;

		return strcmp(_str, s.c_str()) > 0;
	}

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

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

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

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

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

		return out;
	}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}

		return in;
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2239998.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

植物大战僵尸杂交版v2.6.1最新版本(附下载链接)

B站游戏作者潜艇伟伟迷于11月3日更新了植物大战僵尸杂交版2.6.1版本&#xff01;&#xff01;&#xff01;&#xff0c;有b站账户的记得要给作者三连关注一下呀&#xff01; 不多废话下载链接放上&#xff1a; 夸克网盘链接&#xff1a;https://pan.quark.cn/s/279e7ed9f878 新…

【Pikachu】目录遍历实战

既然已经决定做一件事&#xff0c;那么除了当初决定做这件事的我之外&#xff0c;没人可以叫我傻瓜。 1.目录遍历漏洞概述 目录遍历漏洞概述 在Web功能的设计过程中&#xff0c;开发者经常会将需要访问的文件作为变量进行定义&#xff0c;以实现前端功能的灵活性。当用户发起…

[ 网络安全介绍 5 ] 为什么要学习网络安全?

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

项目技术栈-解决方案-注册中心

项目技术栈-解决方案-注册中心 Zookeeper、Eureka、Nacos、Consul和Etcd参考文章 服务注册中心&#xff08;Registry&#xff09;&#xff1a;用于保存 RPC Server 的注册信息&#xff0c;当 RPC Server 节点发生变更时&#xff0c;Registry 会同步变更&#xff0c;RPC Client …

除了 Postman,还有什么好用的 API 测试工具吗

尽管 Postman 提供了团队协作的功能&#xff0c;但是免费版本的功能较为基础&#xff0c;付费版的价格对于小型团队或个人开发者来说可能较高。而且访问 Postman 有时会非常慢&#xff0c;太影响体验了。 鉴于上述局限性&#xff0c;Apifox 成为了一个很好的替代选择。Apifox …

WPF学习之路,控件的只读、是否可以、是否可见属性控制

C#的控件学习之控件属性操作 控件的只读、是否可以、是否可见&#xff0c;是三个重要的参数&#xff0c;在很多表单、列表中都有用到&#xff0c;正常表单控制可以在父层主键控制参数是否可以编辑和可见&#xff0c;但是遇到个别字段需要单独控制时&#xff0c;可以在初始化wi…

LabVIEW开发相机与显微镜自动对焦功能

自动对焦是显微成像系统中的关键功能&#xff0c;通常由显微镜的电动调焦模块或特定的镜头系统提供&#xff0c;而工业相机则主要用于高分辨率图像的采集&#xff0c;不具备独立的自动对焦功能。以下是自动对焦的工作原理、实现方式及实际应用案例。 1. 自动对焦的工作原理 &a…

IBM 开源的文档转化利器「GitHub 热点速览」

上周的热门开源项目&#xff0c;Star 数增长犹如坐上了火箭&#xff0c;一飞冲天。短短一周就飙升了 6k Star 的多格式文档解析和导出神器 Docling&#xff0c;支持库和命令行的使用方式。全新的可视化爬虫平台 Maxun&#xff0c;则在刚开源时便轻松斩获了 4k Star。而本地优先…

STM32完全学习——点亮LED灯

一、寄存器描述 首先我们知道STM32对外设的操作&#xff0c;是靠对寄存器的设置来完成的。因此我们想要点亮LED灯&#xff0c;就需要知道端口的控制寄存器&#xff0c;然后给寄存器设置不同的值就可以让端口来输出0或1&#xff0c;首先我这里使用的是GPIOA这个端口的0-8位来做…

【MongoDB】MongoDB的核心-索引原理及索引优化、及查询聚合优化实战案例(超详细)

文章目录 一、数据库查询效率问题引出索引需求二、索引的基本原理及作用&#xff08;一&#xff09;索引的创建及数据组织&#xff08;二&#xff09;不同类型的索引&#xff08;三&#xff09;索引的额外属性 三、索引的优化与查询计划分析&#xff08;一&#xff09;通过prof…

约束(MYSQL)

not null&#xff08;非空&#xff09; unique&#xff08;唯一&#xff09; default&#xff08;默认约束&#xff0c;规定值&#xff09; 主键约束primary key&#xff08;非空且唯一&#xff09; auto_increment&#xff08;自增类型&#xff09; 复合主键 check&#xff08…

如何保证RabbitMQ的可靠性传输

文章目录 producer到broke生产者到交换机&#xff1a;confirm交换机到队列&#xff1a;returns模式队列溢出&#xff1a;可以采用死信等方式①ConfirmCallback接口②ReturnCallback接口 Broke内部Broke到达消费者 producer到broke 发送方确认 生产者到交换机&#xff1a;conf…

数据库参数备份

MySQL #!/bin/bash # 获取当前日期和时间的时间戳 TIMESTAMP$(date "%Y%m%d-%H%M%S")# 0、创建目录 mkdir /tmp/parameter_$TIMESTAMP/# 1、获取所有命名空间 echo "1、获取所有命名空间" NAMESPACES$(kubectl get ns | grep qfusion- | grep -v qfusion-…

拦截器实现http请求访问本地图片

本文来记录下拦截器实现http请求访问本地图片 文章目录 概述代码实现本文小结 概述 如下图&#xff0c;本机(服务器)存储的图片想要在浏览器上通过Url地址访问&#xff1a; 浏览器直接访问 代码实现 烂机器实现文件真实地址和物理地址之间的映射 Slf4j Configuration public cl…

【数据结构】快排之三路划分

目录 一、前言 二、 快排性能的关键点分析 三、 三路划分基本思想 四、 思路分析 五、提醒 六、代码实现 一、前言 继续对快速排序的深入优化进行探讨 二、 快排性能的关键点分析 决定快排性能的关键点是每次单趟排序后&#xff0c;key对数组的分割。 如果每次选key都能…

Web安全之SQL注入---基础

文章目录 SQL注入简介SQL注入基础SQL注入分类SQL注入流程 SQL注入简介 什么是SQL注入&#xff1f; SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严&#xff0c;攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句&#xff0c;在管理…

iOS 18.1,未公开的新功能

童锦程祖师爷曾说过&#xff1a;“发誓可以&#xff0c;发朋友圈不行。”表面上看是渣男语录&#xff0c;实际上也说明了人们对隐私的看重。 在当今生活中&#xff0c;智能手机可能是最私密的电子产品&#xff0c;没有之一。不管是照片、联系人、短信、APP数据&#xff0c;甚至…

网页版五子棋——对战模块(服务器端开发②)

前一篇文章&#xff1a;网页版五子棋——对战模块&#xff08;服务器端开发①&#xff09;-CSDN博客 项目源代码&#xff1a;Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com 目录 前言 一、创建并注册 GameAPI 类 1.创建 GameAPI 类 2.注册 GameAPI 类 …

★ C++进阶篇 ★ 异常

Ciallo&#xff5e;(∠・ω< )⌒☆ ~ 今天&#xff0c;我将和大家一起学习C中的异常 ~ ​❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️ 澄岚主页&#xff1a;椎名澄嵐-CSDN博客 C基础篇专栏&#xff1a;★ C基础篇 ★_椎名澄嵐的博客-CSDN博客 C进阶篇专栏&am…

MFC图形函数学习08——绘图函数的重载介绍

在《MFC图形函数学习06——画椭圆弧线函数》中介绍了CPoint类、POINT结构体&#xff1b;在《MFC图形函数学习07——画扇形函数》中介绍了CRect类、RECT结构体。在介绍完后&#xff0c;没有介绍它们怎样使用。实际上&#xff0c;这些类和结构体对象或指针也是我们学习过的绘图函…