前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >string类(下)(模拟实现string类,深度剖析其底层)

string类(下)(模拟实现string类,深度剖析其底层)

作者头像
suye
发布2024-10-16 09:51:14
900
发布2024-10-16 09:51:14
举报
文章被收录于专栏:17的博客分享

string类的模拟实现

补充内容:断言

断言(Assertion)是编程中一种常用的调试辅助手段,用于在代码执行期间验证某个条件是否为真。如果条件为真(即满足预期),则程序继续执行;如果条件为假(即不满足预期),则断言失败,通常会导致程序抛出一个错误、输出一条错误信息,甚至直接终止程序。断言主要用于开发和测试阶段,以确保代码的正确性和健壮性。

  • C/C++:使用assert宏,定义在<cassert>(C++)或<assert.h>(C)头文件中。当NDEBUG宏未定义时,assert会检查其条件;如果条件为假,则输出一条错误消息并调用abort函数终止程序。如果定义了NDEBUG,则assert宏被忽略,不产生任何代码。

断言的主要目的是帮助开发者在开发和测试阶段发现潜在的问题,确保代码按照预期运行。然而,由于断言会增加额外的运行时开销,并且可能在某些配置下被禁用或忽略,因此它们通常不用于生产环境中的错误处理。相反,生产代码应该使用更健壮的错误处理机制,如异常处理、日志记录和回退策略。

在编写断言时,应该考虑以下几点:

  • 清晰性:断言的条件应该清晰明了,能够直接反映代码的预期行为。
  • 有用性:断言失败时提供的信息应该足够有用,以便开发者能够快速定位问题。
  • 可测试性:断言应该针对可测试的代码部分,避免对不可控的外部依赖进行断言。
  • 性能影响:了解断言对性能的影响,并在必要时进行优化或禁用。

最后,需要注意的是,断言是一种防御性编程技术,但它并不是万能的。开发者还应该使用其他技术和最佳实践来确保代码的质量和可靠性。

在C++中,assert 是一个宏,用于在代码中设置断言。断言是一种调试辅助工具,用于在程序运行时检查一个布尔表达式是否为真。如果表达式为假(即0或false),则assert会输出一条错误消息(通常包含失败的表达式、文件名和行号),并调用abort函数来终止程序。这有助于开发者快速定位并修复程序中的错误。

assert宏定义在<cassert>(或C风格的<assert.h>)头文件中。使用assert时,不需要包含任何错误处理代码,因为当断言失败时,程序会立即终止。然而,这意呀着assert主要用于调试阶段,而不应该用于生产环境中的错误处理。

使用示例
代码语言:javascript
复制
#include <cassert>  
#include <iostream>  
  
int main() {  
    int x = 5;  
    assert(x > 0); // 这是一个有效的断言,因为x确实大于0  
  
    // 假设这里有一些代码改变了x的值  
    x = -1;  
  
    // 下面的断言会失败,因为x不再大于0  
    assert(x > 0); // 如果x不大于0,程序将输出错误信息并终止  
  
    std::cout << "This line will not be executed if the assertion fails." << std::endl;  
  
    return 0;  
}
注意事项

调试与生产:由于assert在断言失败时会终止程序,因此它不适合用于生产环境中的错误处理。在生产代码中,应该使用更合适的错误处理机制,如异常处理。

性能影响:在编译时,如果定义了NDEBUG宏(通常在发布构建中定义),则assert宏会被忽略,不会生成任何代码。这有助于避免在发布版本中引入不必要的性能开销。

自定义错误消息assert宏允许你提供一个可选的字符串作为错误消息,这有助于在断言失败时提供更多上下文信息。

代码语言:javascript
复制
assert(x > 0 && "x should be greater than 0");

替代方案:对于需要更精细控制错误处理的情况,可以考虑使用异常处理或其他错误处理机制。

总之,assert是C++中一种有用的调试工具,但应该谨慎使用,并了解其在不同编译设置下的行为。

1. 命名空间
  • 在编写string类之前,应该先设置一个命名空间,防止与std命名空间冲突
  • 建议初学者命名空间都设置为自己的名字缩写,方便使用
代码语言:javascript
复制
namespace xny
{
	class string
    {
        // ......
	};
};
2. 成员变量
  • 刚开始时,应该先声明最基本的成员变量,后面缺什么补什么
代码语言:javascript
复制
class string 
{
private:
		size_t _size;		// 字符串的当前长度(不包括终止符 '\0')
		size_t _capacity;	// 字符串缓冲区的容量
		char* _str;			// 指向字符串数据的指针
public:
static size_t npos;
};
size_t string::npos = -1;
3. 构造函数
  • 构造函数在string类的实现中很容易写错,就比如:
代码语言:javascript
复制
string()
	:_size(0)
	, _capacity(0)
	, _str(new char[1])
{
	_str[0] = '\0';
}
  • 正确案例:
代码语言:javascript
复制
string(const char* str = "")
	:_size(strlen(str))
	, _capacity(_size)
	, _str(new char[_capacity + 1])
{
	// strcpy(_str, str);
		memcpy(_str, str, _size + 1);	// 替换strcpy的原因请见“全部代码”的"void test_string6()"部分
}
  • 成员初始化列表(Member Initializer List):_size(strlen(str)):使用成员初始化列表来初始化成员变量_size。这里,_size被设置为传入字符串str的长度(通过strlen(str)计算得到)。如果str为空字符串(即默认情况),则strlen(str)返回0,因此_size也会被初始化为0。 ,_capacity(_size):紧接着,成员变量_capacity(表示字符串的容量)被初始化为与_size相同的值。这意味着在这个实现中,字符串的初始容量等于其长度,没有预留额外的空间用于未来的增长。然而,这种设计在实际应用中可能不是最高效的,因为每次字符串增长时都可能需要重新分配内存。 ,_str(new char[_capacity + 1]):最后,为字符串数据本身分配内存。这里分配了_capacity + 1个字符的空间,其中_capacity个字符用于存储字符串内容,额外的1个字符用于存储字符串的终结符\0(C风格字符串的标准结束符)。
  • 构造函数体strcpy(_str, str);:在构造函数体内,使用strcpy函数将传入的字符串str(或其默认值空字符串)复制到之前分配的内存中(即_str指向的位置)。这里假设strcpy是安全的,即_str指向的内存足够大,可以存储str及其终结符\0。由于之前已经通过成员初始化列表确保了这一点,所以这里的使用是安全的。
4. 析构函数
  • 这里需要注意的是,添加一个判断_str是否为空的条件,避免出现释放空内存的情况。
  • 释放空间使用的是delete[]而不是delete,前者是基于new调用的次数来调用多少次,而后者固定只调用一次。
代码语言:javascript
复制
~string()
{
	if (_str)
	{
		delete[] _str;		// 释放之前为字符串数据分配的内存
		_str = nullptr;		// 将指针置为nullptr,避免野指针问题
		_size = _capacity = 0;	// 将_size和_capacity都置0,防止内存泄露
	}
}
5. 拷贝构造函数(深拷贝)
  • 深拷贝:即拷贝时不只是简单的传值拷贝,还生成了新的空间用来存储拷贝后的数据
5.1 传统写法
代码语言:javascript
复制
string(const string& s)
{
	_str = new char[s._capacity + 1];
	// strcpy(_str, s._str);
	memcpy(_str, s._str, s._size + 1);
	_size = s._size;
	_capacity = s._capacity;
}

【步骤解释】:

  1. 开空间用来存放拷贝后的数据。
  2. 拷贝 s. _str 的数据给 _str,这里设置比较字节数为 s._size + 1 的原因是包含’\0’。
  3. 修改原来的字符串长度和字符串缓冲区容量。
5.2 现代写法
代码语言:javascript
复制
void swap(string& tmp)
{
	std::swap(_str, tmp._str);
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}

string(const string& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	string tmp(s._str);
	swap(tmp);	// this->swap(tmp);
}

【步骤解释】:

  1. 先写一个swap函数实现交换两对象的成员函数。
  2. 写一个初始化列表,防止某些编译器会自动为成员变量生成随机值。
  3. 生成临时对象tmp并使用s._str进行构造。
  4. 最后交换tmp与s的成员变量。
6. 赋值运算符重载
6.1 传统写法
代码语言:javascript
复制
string& operator=(const string& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		memcpy(tmp, s._str, s._size + 1);
		delete[] _str;
		_str = tmp;

		_size = s._size;
		_capacity = s._capacity;
	}

	return *this;
}
6.2 现代写法
代码语言:javascript
复制
string& operator=(string tmp)
{
	// this->swap(tmp);
	swap(tmp);

	return *this;
}

【注意】:

  • 假如自己没有编写一个swap函数,能不能使用std::swap(tmp, *this);呢?
  • 这种写法是错误的,它会导致无穷递归,因为swap函数内部也在进行赋值运算,每次赋值都会调用swap函数,而每次swap也都会进行赋值
7. 返回C风格的字符串形式
代码语言:javascript
复制
const char* c_str() const	
{
	return _str;
}
8. 返回字符串的大小
代码语言:javascript
复制
size_t size() const		
{
	return _size;
}
9. 重载下标索引[]
  • 重载下标索引[ ],在string中的索引比迭代器更加方便
代码语言:javascript
复制
char& operator[](size_t pos)
{
	// 检查pos的合法性,设置断言
	assert(pos < _size);

	return _str[pos];
}

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

	return _str[pos];
}
10. 扩大字符串容量
代码语言:javascript
复制
void reserve(size_t n)
{
	// 如果请求的容量大于当前容量,则进行扩容  
	if (n > _capacity)
	{
		// 为新字符串分配足够的空间(包括额外的字符用于字符串终结符 '\0')  
		char* tmp = new char[n + 1];

		// 拷贝旧字符串到新分配的空间中  
		// 注意:这里假设 _str 已经是一个以 '\0' 结尾的字符串  
		// strcpy(tmp, _str);
		memcpy(tmp, _str, _size + 1);

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

		// 更新指针和容量  
		_str = tmp;
		_capacity = n;

		// 注意:这里没有更新 _size,因为 reserve 只改变容量,不改变字符串的实际长度  
		// 如果需要,可以在扩容后根据需要更新 _size(例如,如果字符串是通过某种方式动态增长的)  
	}
	// 如果 n <= _capacity,则不执行任何操作  
}
11. 改变字符串的大小,使用特定字符填充
代码语言:javascript
复制
void resize(size_t n, char ch = '\0')
{
	if (n < _size)
	{
		_size = n;
		_str[_size] = '\0';
	}
	else
	{
		reserve(n);

		for (size_t i = _size; i < n; i++)
		{
			_str[i] = ch;
		}

		_size = n;
		_str[_size] = '\0';
	}
}
12. 尾插一个字符串
代码语言:javascript
复制
void append(const char* str)
{
	size_t len = strlen(str);
	if (_size +len > _capacity)
	{
		// 至少扩容到_size +len
		reserve(_size + len);
	}

	// 将要追加的字符串复制到当前字符串的末尾 
	// strcpy(_str + _size, str);
	memcpy(_str + _size, str, len + 1);

	// 更新字符串的大小
	_size += len;

	// 注意:不需要再次设置终结符,因为 strcpy 已经做了这件事
}
13. +=操作符重载
代码语言:javascript
复制
// += (字符串)
string& operator+=(const char* str)
{
	append(str);
	return *this;
}

// += (单个字符)
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}
14. 中间插入
代码语言:javascript
复制
// 中间插入单个字符
void insert(size_t pos, size_t n, char ch)
{
	// 检查pos的合法性,设置断言
	assert(pos <= _size);

	if (_size + n > _capacity)
	{
		// 至少扩容n个单位
		reserve(_size + n);
	}
	
	// 挪动数据方法1
	/*int end = _size;
	while (end >= (int)pos)		// 将pos强转成int
	{
		_str[end + n] = _str[end];
		--end;
	}*/

	// 挪动数据方法2
	size_t end = _size;
	while (end >= pos && end != npos)	// 当end已经超过了目标位置并且不能很大的时候继续循环
	{
		_str[end + n] = _str[end];
		--end;
	}

	// 覆盖原数据
	for (size_t i = 0; i < n; i++)
	{
		_str[pos + i] = ch;
	}

	_size += n;		// 更新字符串大小
}

// 中间插入一个字符串
void insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		// 至少扩容到_size +len
		reserve(_size + len);
	}

	// 挪动数据
	size_t end = _size;
	while (end >= pos && end != npos)	// 当end已经超过了目标位置并且不能很大的时候继续循环
	{
		_str[end + len] = _str[end];
		--end;
	}
	// 覆盖原数据
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = str[i];
	}

	_size += len;
}
15. 删除
代码语言:javascript
复制
void erase(size_t pos, size_t len = npos)
{
	// 检查pos的合法性,设置断言
	assert(pos <= _size);

	// 如果 len 是 npos 或 pos + len 超出当前大小,则删除从 pos 到末尾的所有内容
	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';	// 直接使用终结符截断
		_size = pos;
	}
	else
	{
		size_t end = pos + len;
		while (end <= _size)
		{
			// 向前覆盖
			_str[pos++] = _str[end++];
		}
		_size -= len;	// 更新字符串大小
	}
}
16. 查找
代码语言:javascript
复制
// 查找一个字符
size_t find(char ch, size_t pos = 0)
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}

	return npos;
}

// 查找一个字符串
size_t find(const char* str, size_t pos = 0)
{
	// 检查pos合法性,设置断言
	assert(pos < _size);

	const char* ptr = strstr(_str + pos, str);	// 这里,strstr函数用于在_str字符串中从pos位置开始搜索子字符串str。
	if (ptr)
	{
		// 返回子字符串在原字符串中的起始索引
		return ptr - _str;
	}
	else
	{
		return npos;
	}
}
17. 清空
代码语言:javascript
复制
void clear()
{
	_str[0] = '\0';
	_size = 0;
}
18. 重载比较运算符
18.1 传统写法
代码语言:javascript
复制
bool operator<(const string& s) const
{
	size_t i1 = 0;	// 左操作符
	size_t i2 = 0;	// 右操作符
	while (i1 < _size && i2 < s._size)
	{
		if (_str[i1] < s._str[i2])
		{
			return true;
		}
		else if (_str[i1] > s._str[i2])
		{
			return false;
		}
		else
		{
			++i1;
			++i2;
		}
	}
	return _size < s._size;
}
18.2 优化写法以及复用
代码语言:javascript
复制
bool operator<(const string& s)	const
{
	int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
	return ret == 0 ? _size < s._size : ret < 0;
}

bool operator==(const string& s) const
{
	return _size == s._size && memcmp(_str, s._str,_size) == 0;
}

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

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

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

bool operator>(const string& s) const
{
	return !(*this <= s);
}
19. 流插入重载
代码语言:javascript
复制
ostream& operator<<(ostream& out, const xny::string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}

	return out;
}
20. 流提取重载
20.1 错误版
代码语言:javascript
复制
istream& operator>>(istream& in, xny::string& s)
{
	char ch;
	in >> ch;
	while (ch != ' '&& ch != '\n')
	{
		s += ch;
		in >> ch;
	}

	return in;
}
20.2 简易版
代码语言:javascript
复制
istream& operator>>(istream& in, xny::string& s)
{
	s.clear();

	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;	// 如果循环+=的话,每一次都会扩容,当传入的数据很多的时候,扩容很多次会不好
		ch = in.get();
	}

	return in;
}
20.3 优化版
代码语言:javascript
复制
istream& operator>>(istream& in, xny::string& s)
{
	// 在读取新数据之前,先清空 s 中的现有内容。
	s.clear();

	char ch = in.get();
	// 处理掉缓冲区前面的空格或者换行
	while (ch == ' ' || ch == '\n')
	{
		// 不进行操作,直接取下一个字符就相当于清理
		ch = in.get();
	}

	// 定义了一个字符数组 buff[128] 作为临时缓冲区,用于存储从输入流中读取的字符,避免出现上述的多次扩容的情况。
	char buff[128];
	int i = 0;

	while (ch != ' ' && ch != '\n')
	{
		// 将读取的字符存储到 buff 中,并更新索引 i。
		buff[i++] = ch;

		// 如果 i 达到了 127(即缓冲区即将满),则将缓冲区的内容(加上字符串终止符 '\0')添加到 s 中,并重置 i 为 0,以便继续填充缓冲区。
		if (i == 127)	// 第128位是'\0'
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}

		ch = in.get();
	}

	// 退出循环后,如果 i 不为 0,说明还有字符没有添加到 s 中,因此将 buff 中的剩余字符(加上字符串终止符 '\0')添加到 s 中。
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}
21. 全部代码
21.1 头文件
代码语言:javascript
复制
#pragma once
#include <cassert>


// *******提示:以下函数中基本都会实现普通对象的调用和常对象的调用
	
// 设置命名空间,防止与std命名空间冲突
namespace xny
{
	class string
	{
	public:
		// 声明迭代器
		typedef char* iterator;
		typedef const char* const_iterator;

		// 可读可写
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + size();
		}

		// 只能读
		const_iterator begin() const
		{
			return _str;
		}

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

		// 错误的构造函数
		/*string()
			:_size(0)
			, _capacity(0)
			, _str(new char[1])
		{
			_str[0] = '\0';
		}*/


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


		// 拷贝构造(深拷贝)
		// 即拷贝时不只是简单的传值拷贝,这里还生成了新的空间用来存储拷贝后的数据
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			// strcpy(_str, s._str);
			memcpy(_str, s._str, s._size + 1);
			_size = s._size;
			_capacity = s._capacity;
		}

		// 拷贝构造(优化)
		/*string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);
			swap(tmp);
		}*/

		// 赋值运算符重载
		// 传统写法
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				memcpy(tmp, s._str, s._size + 1);
				delete[] _str;
				_str = tmp;

				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}*/

		// 现代写法
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		// 拷贝构造
		//		string tmp(s);

		//		std::swap(_str, tmp._str);
		//		std::swap(_size, tmp._size);
		//		std::swap(_capacity, tmp._capacity);

		//		// std::swap(tmp, *this);
		//		// 这种写法是错误的,它会导致无穷递归,因为swap函数内部也在进行赋值运算,每次赋值都会调用swap函数,而每次swap也都会进行赋值
		//	}

		//	return *this;
		//}

		// 现代写法(优化)
		void swap(string& tmp)
		{
			std::swap(_str, tmp._str);
			std::swap(_size, tmp._size);
			std::swap(_capacity, tmp._capacity);
		}

		string& operator=(string tmp)
		{
			// this->swap(tmp);
			swap(tmp);

			return *this;
		}

		// 析构函数
		~string()
		{
			if (_str)
			{
				delete[] _str;		// 释放之前为字符串数据分配的内存
				_str = nullptr;		// 将指针置为nullptr,避免野指针问题
				_size = _capacity = 0;	// 将_size和_capacity都置0,防止内存泄露
			}
		}

		// 这里返回的是C风格的字符串
		const char* c_str() const	
		{
			return _str;
		}

		// 返回字符串的大小
		size_t size() const		
		{
			return _size;
		}

		// 重载下标索引[],在string中的索引比迭代器更加方便
		char& operator[](size_t pos)
		{
			// 检查pos的合法性,设置断言
			assert(pos < _size);

			return _str[pos];
		}

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

			return _str[pos];
		}

		// 扩大字符串容量
		void reserve(size_t n)
		{
			// 如果请求的容量大于当前容量,则进行扩容  
			if (n > _capacity)
			{
				// 为新字符串分配足够的空间(包括额外的字符用于字符串终结符 '\0')  
				char* tmp = new char[n + 1];

				// 拷贝旧字符串到新分配的空间中  
				// 注意:这里假设 _str 已经是一个以 '\0' 结尾的字符串  
				// strcpy(tmp, _str);
				memcpy(tmp, _str, _size + 1);

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

				// 更新指针和容量  
				_str = tmp;
				_capacity = n;

				// 注意:这里没有更新 _size,因为 reserve 只改变容量,不改变字符串的实际长度  
				// 如果需要,可以在扩容后根据需要更新 _size(例如,如果字符串是通过某种方式动态增长的)  
			}
			// 如果 n <= _capacity,则不执行任何操作  
		}

		// 改变字符串的大小,使用特定字符填充
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				reserve(n);

				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}

				_size = n;
				_str[_size] = '\0';
			}
		}



		// 尾插一个字符
		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				// 如果当前大小等于容量,则扩容  
				// 对于初始容量为0的情况,初始化为4;否则,使用二倍扩容策略  
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			// 在字符串末尾添加新字符  
			_str[_size] = ch;

			// 更新字符串大小  
			++_size;

			// 在新字符之后设置终结符  
			_str[_size] = '\0';

			// 注意:由于 _size 已经在添加字符后增加,所以上面的 _str[_size] = '\0'   
		}

		// 尾插一个字符串
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size +len > _capacity)
			{
				// 至少扩容到_size +len
				reserve(_size + len);
			}

			// 将要追加的字符串复制到当前字符串的末尾 
			// strcpy(_str + _size, str);
			memcpy(_str + _size, str, len + 1);

			// 更新字符串的大小
			_size += len;

			// 注意:不需要再次设置终结符,因为 strcpy 已经做了这件事
		}

		// += 操作符重载(字符串)
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		// += 操作符重载(单个字符)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		// 中间插入单个字符
		void insert(size_t pos, size_t n, char ch)
		{
			// 检查pos的合法性,设置断言
			assert(pos <= _size);

			if (_size + n > _capacity)
			{
				// 至少扩容n个单位
				reserve(_size + n);
			}
			
			// 挪动数据方法1
			/*int end = _size;
			while (end >= (int)pos)		// 将pos强转成int
			{
				_str[end + n] = _str[end];
				--end;
			}*/

			// 挪动数据方法2
			size_t end = _size;
			while (end >= pos && end != npos)	// 当end已经超过了目标位置并且不能很大的时候继续循环
			{
				_str[end + n] = _str[end];
				--end;
			}

			// 覆盖原数据
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}

			_size += n;		// 更新字符串大小
		}

		// 中间插入一个字符串
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);

			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				// 至少扩容到_size +len
				reserve(_size + len);
			}

			// 挪动数据
			size_t end = _size;
			while (end >= pos && end != npos)	// 当end已经超过了目标位置并且不能很大的时候继续循环
			{
				_str[end + len] = _str[end];
				--end;
			}
			// 覆盖原数据
			for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}

			_size += len;
		}

		void erase(size_t pos, size_t len = npos)
		{
			// 检查pos的合法性,设置断言
			assert(pos <= _size);

			// 如果 len 是 npos 或 pos + len 超出当前大小,则删除从 pos 到末尾的所有内容
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';	// 直接使用终结符截断
				_size = pos;
			}
			else
			{
				size_t end = pos + len;
				while (end <= _size)
				{
					// 向前覆盖
					_str[pos++] = _str[end++];
				}
				_size -= len;	// 更新字符串大小
			}
		}

		// 查找一个字符
		size_t find(char ch, size_t pos = 0)
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return npos;
		}

		// 查找一个字符串
		size_t find(const char* str, size_t pos = 0)
		{
			// 检查pos合法性,设置断言
			assert(pos < _size);

			const char* ptr = strstr(_str + pos, str);	// 这里,strstr函数用于在_str字符串中从pos位置开始搜索子字符串str。
			if (ptr)
			{
				// 返回子字符串在原字符串中的起始索引
				return ptr - _str;
			}
			else
			{
				return npos;
			}
		}

		// 返回子字符串
		string substr(size_t pos = 0, size_t len = npos)
		{
			size_t n = len;
			if (len == npos || pos + len > _size)
			{
				n = _size - pos;
			}

			string tmp;
			tmp.reserve(n);
			for (size_t i = pos; i < pos + n; i++)
			{
				tmp += _str[i];
			}

			// 发生浅拷贝,出作用域就会销毁,为避免发生这个问题,必须生成一个深拷贝构造函数
			return tmp;
		}

		// 清空字符串                       
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		// 重载'<',其余操作符可以复用
		//bool operator<(const string& s) const
		//{
		//	size_t i1 = 0;	// 左操作符
		//	size_t i2 = 0;	// 右操作符
		//	while (i1 < _size && i2 < s._size)
		//	{
		//		if (_str[i1] < s._str[i2])
		//		{
		//			return true;
		//		}
		//		else if (_str[i1] > s._str[i2])
		//		{
		//			return false;
		//		}
		//		else
		//		{
		//			++i1;
		//			++i2;
		//		}
		//	}
		//	return _size < s._size;
		//}

		// 优化后的比较运算符重载及复用
		bool operator<(const string& s)	const
		{
			int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
			return ret == 0 ? _size < s._size : ret < 0;
		}

		bool operator==(const string& s) const
		{
			return _size == s._size && memcmp(_str, s._str,_size) == 0;
		}

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

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

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

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

	private:
		size_t _size;		// 字符串的当前长度(不包括终止符 '\0')
		size_t _capacity;	// 字符串缓冲区的容量
		char* _str;			// 指向字符串数据的指针

	public:
		static size_t npos;
	};
	size_t string::npos = -1;
};

// 流插入重载
ostream& operator<<(ostream& out, const xny::string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}

	return out;
}

// 流提取重载1(错误版)
// 当读取到空格或换行符时,这些字符实际上没有被从输入流中“消耗”掉,这意味着如果你紧接着使用另一个 >> 操作符(比如再次读取一个xny::string),它将从空格或换行符开始读取,这就导致了死循环。
//istream& operator>>(istream& in, xny::string& s)
//{
//	char ch;
//	in >> ch;
//	while (ch != ' '&& ch != '\n')
//	{
//		s += ch;
//		in >> ch;
//	}
//
//	return in;
//}

// 流提取重载2(简易版)
//istream& operator>>(istream& in, xny::string& s)
//{
//	s.clear();
//
//	char ch = in.get();
//	while (ch != ' ' && ch != '\n')
//	{
//		s += ch;	// 如果循环+=的话,每一次都会扩容,当传入的数据很多的时候,扩容很多次会不好
//		ch = in.get();
//	}
//
//	return in;
//}

// 流提取重载3 (优化版)
istream& operator>>(istream& in, xny::string& s)
{
	// 在读取新数据之前,先清空 s 中的现有内容。
	s.clear();

	char ch = in.get();
	// 处理掉缓冲区前面的空格或者换行
	while (ch == ' ' || ch == '\n')
	{
		// 不进行操作,直接取下一个字符就相当于清理
		ch = in.get();
	}

	// 定义了一个字符数组 buff[128] 作为临时缓冲区,用于存储从输入流中读取的字符,避免出现上述的多次扩容的情况。
	char buff[128];
	int i = 0;

	while (ch != ' ' && ch != '\n')
	{
		// 将读取的字符存储到 buff 中,并更新索引 i。
		buff[i++] = ch;

		// 如果 i 达到了 127(即缓冲区即将满),则将缓冲区的内容(加上字符串终止符 '\0')添加到 s 中,并重置 i 为 0,以便继续填充缓冲区。
		if (i == 127)	// 第128位是'\0'
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}

		ch = in.get();
	}

	// 退出循环后,如果 i 不为 0,说明还有字符没有添加到 s 中,因此将 buff 中的剩余字符(加上字符串终止符 '\0')添加到 s 中。
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}
21.2 源文件(测试代码)
代码语言:javascript
复制
#define _CRT_SECURE_NO_WARNINGS 1

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

#include "string.h"

// 测验命名空间namespace和迭代器iterator
void test_string1()
{
	xny::string s1("hello world");
	cout << s1.c_str() << endl;

	xny::string s2;
	cout << s2.c_str() << endl;

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

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

	const xny::string s3("hello world");
	s3[0];

	// xny::string::const_iterator cit = s3.begin();
	auto cit = s3.begin();
	while (cit != s3.end())
	{
		// *cit += 1;
		cout << *cit << " ";
		++cit;
	}
	cout << endl;

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

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

// 测验push_back()和'+='重载函数
void test_string2()
{
	xny::string s1("hello world");
	cout << s1.c_str() << endl;
	s1.push_back(' ');
	s1.push_back('#');
	s1.append("hello xny");
	cout << s1.c_str() << endl;

	xny::string s2("hello world");
	cout << s2.c_str() << endl;
	s2 += ' ';
	s2 += '#';
	s2 += "hello xny";
	cout << s2.c_str() << endl;
}

// 测验insert()
void test_string3()
{
	xny::string s1("helloworld");
	cout << s1.c_str() << endl;
	
	s1.insert(5, 3, 'x');
	cout << s1.c_str() << endl;
	
	s1.insert(0, 3, 'x');
	cout << s1.c_str() << endl;

	xny::string s2("helloworld");
	s2.insert(5, "*****");
	cout << s2.c_str() << endl;
}

// 测验erase()
void test_string4()
{
	xny::string s1("helloworld");
	cout << s1.c_str() << endl;

	s1.erase(5, 3);
	cout << s1.c_str() << endl;

	s1.erase(5, 30);
	cout << s1.c_str() << endl;
}

// 测试find()和substr()
void test_string5()
{
	xny::string url = "http://legacy.cpluscplus.com/reference/string/string/";
	// 协议 域名 资源名
	size_t pos1 = url.find("://");
	xny::string protocol;	// 协议
	if (pos1 != xny::string::npos)	// 用来判断是否找到了指定的字符串
	{
		protocol = url.substr(0, pos1);
	}
	cout << protocol << endl;

	xny::string domain;	// 域名
	xny::string uri;		// 资源
	size_t pos2 = url.find('/', pos1 + 3);	// 不能从起始位置开始找,会找到://
	if (pos2 != xny::string::npos)
	{
		domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
		uri = url.substr(pos2 + 1);
	}
	cout << domain << endl;
	cout << uri << endl;
}

// 测试resize(),判断c_str()和重载 << 的区别
void test_string6()
{
	xny::string s("hello world");
	s.resize(8);
	cout << s.c_str() << endl;
	
	s.resize(13, 'x');
	cout << s.c_str() << endl;
	
	s.resize(20, 'y');
	cout << s.c_str() << endl;

	// c_str和重载后的<<打印区别
	// c_str遇到'\0'就会终止,而<<是按size打印的
	// 而strcpy也是会遇到'\0'就终止,于是为了防止出现问题,就将所有的strcpy全部换成memcpy
	xny::string ss("hello world");
	ss += '\0';
	ss += "!!!!!!!!";
	cout << ss.c_str() << endl;
	cout << ss << endl;
}

// 测试流插入 >>
void test_string7()
{
	xny::string s;
	cin >> s;
	cout << s << endl;
}

// 测试比较运算符 < , > , ==  
void test_string8()
{
	xny::string s1("hello");
	xny::string s2("hello");
	cout << (s1 < s2) << endl;
	cout << (s1 > s2) << endl;
	cout << (s1 == s2) << endl << endl;

	xny::string s3("hello");
	xny::string s4("helloxxx");
	cout << (s3 < s4) << endl;
	cout << (s3 > s4) << endl;
	cout << (s3 == s4) << endl << endl;

	xny::string s5("helloxxx");
	xny::string s6("hello");
	cout << (s5 < s6) << endl;
	cout << (s5 > s6) << endl;
	cout << (s5 == s6) << endl << endl;

}

// 测试拷贝构造和重载的赋值运算符 = 
void test_string9()
{
	xny::string s1("hello");
	xny::string s2(s1);

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

	xny::string s3("xxxxxxxxxxxxxx");
	s1 = s3;

	cout << s1 << endl;
	cout << s3 << endl;

	// 用来检测拷贝构造的优化写法的缺失
	s3 += '\0';
	s3 += "yyyyyyyyyyyyyyy";
	cout << s3 << endl;
	
	xny::string s4(s3);
	cout << s4 << endl;

	// 结果发现s4没有拷贝s3'\0'之后的部分
}


int main()
{
	test_string9();

	return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-09-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • string类的模拟实现
    • 补充内容:断言
      • 使用示例
      • 注意事项
    • 1. 命名空间
      • 2. 成员变量
        • 3. 构造函数
          • 4. 析构函数
            • 5. 拷贝构造函数(深拷贝)
              • 5.1 传统写法
              • 5.2 现代写法
            • 6. 赋值运算符重载
              • 6.1 传统写法
              • 6.2 现代写法
            • 7. 返回C风格的字符串形式
              • 8. 返回字符串的大小
                • 9. 重载下标索引[]
                  • 10. 扩大字符串容量
                    • 11. 改变字符串的大小,使用特定字符填充
                      • 12. 尾插一个字符串
                        • 13. +=操作符重载
                          • 14. 中间插入
                            • 15. 删除
                              • 16. 查找
                                • 17. 清空
                                  • 18. 重载比较运算符
                                    • 18.1 传统写法
                                    • 18.2 优化写法以及复用
                                  • 19. 流插入重载
                                    • 20. 流提取重载
                                      • 20.1 错误版
                                      • 20.2 简易版
                                      • 20.3 优化版
                                    • 21. 全部代码
                                      • 21.1 头文件
                                      • 21.2 源文件(测试代码)
                                  领券
                                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档