当前位置: 首页 > article >正文

深入理解string:从模拟实现看本质

文章目录

  • 摘要:
  • 一、引言
  • 二、string的模拟实现
    • 2.1 string类的定义
  • 三、深拷贝和深赋值
    • 3.1 深浅拷贝构造函数
    • 3.2 深赋值运算符
  • 四、总结
  • 五、附录
    • 5.1 完整代码
    • 6.2 测试用例
  • 六、致谢

摘要:

本文将通过模拟实现一个简单的 String 类,深入探讨字符串的底层实现原理,包括内存管理、深浅拷贝、深赋值、常用操作以及性能优化等方面,帮助读者更好地理解和使用字符串。

关键词: String, 模拟实现, 内存管理, 深浅拷贝, 深赋值, 字符串操作, 性能优化

一、引言

在 C++ 中,string 是一个表示字符串的类,它提供了一系列用于字符串操作的方法。string 类的对象可以存储文本数据,支持多种操作,如赋值、连接、比较和搜索。本文将通过模拟实现string类来理解string的底层实现原理,并以此为基础,深入探讨字符串的相关知识。

二、string的模拟实现

2.1 string类的定义

我们首先定义一个 String 类,包含以下成员变量和成员函数:

  • 成员变量:

    • char* _str = nullptr; :字符串
    • size_t _size = 0; :字符串的长度
    • size_t _capacity = 0; :字符串的容量
  • 成员函数:

    • 构造函数、析构函数、拷贝构造函数、赋值运算符。
    • 常用操作:字符串连接、子串查找、字符串比较、访问元素、获取长度等。
    • 常用操作符:==、!=、>>、<<等。
namespace dza 
{
	class string {
	public:
		///*重新定义一个新的变量名*///
		//typedef char* iterator;

		using iterator = char*;
		using const_iterator = const char*;

		/*成员函数*/

		//构造函数
		string(const char* str = "");
		string(const string& s);
		string& operator=(string s);

		//析构函数
		~string();

		///*迭代器*/

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

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

		size_t size() const
		{
			return _size;
		}

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

		//*容量操作*
		
		//预留空间
		void reserve(size_t len);
		
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		*元素访问*//
        
		//[]元素符重载
		char& operator[](size_t i)
		{
			assert(i < _size);

			return _str[i];
		}

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

			return _str[i];
		}

		/*修饰符*/

		// 尾插
		void push_back(char ch);

		// 尾插
		void append(const char* str);

		// +=运算符的重载
		string& operator+=(char ch);
		string& operator+=(const char* str);

		//头插
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);

		//擦除
		void erase(size_t pos, size_t len = npos);

		//交换
		void swap(string& s);

		*字符串操作*///

		//寻找
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);

		//在一个字符串里面拷贝构造一个子字符串
		string substr(size_t pos,size_t len = npos);

		///*非成员函数重载*

	private:
		//因为是私有成员。初始化列表的时候会按照这里初始化的顺序进行初始化。为了防止初始化列表出现问题。建议直接在定义的时候初始化
		//如果这里没有初始化,初始化列表的时候可能会因为搞错初始化的顺序而导致程序报错
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

	public:
		// static const size_t npos = -1;
		static const size_t npos;// 这里的npos编译器自动初始化为0.但是在.cpp文件中又重新初始化为-1
	};

	void swap(string& s1, string& s2);
	bool operator== (const string& lhs, const string& rhs);
	bool operator!= (const string& lhs, const string& rhs);
	bool operator< (const string& lhs, const string& rhs);
	bool operator> (const string& lhs, const string& rhs);
	bool operator<= (const string& lhs, const string& rhs);
	bool operator>= (const string& lhs, const string& rhs);

	ostream& operator<<(ostream& os,const string& str);
	istream& operator>>(istream& is,string& str);
	istream& getline(istream& is,string& str,char delim = '\n');
}

三、深拷贝和深赋值

3.1 深浅拷贝构造函数

默认的拷贝构造函数和赋值运算符是浅拷贝,即只复制指针的值,而不是指针指向的内存内容。这会导致多个对象共享同一块内存,从而引发以下问题:

  • 修改一个对象会影响其他对象。
  • 析构时可能导致重复释放同一块内存,引发程序崩溃。

深拷贝是指不仅复制指针的值,还复制指针指向的内存内容。

深拷贝与浅拷贝

3.2 深赋值运算符

深赋值运算符不仅需要深拷贝新内容,还需要释放原有内存,避免内存泄漏。同时,需要检查自我赋值的情况,避免释放自身内存导致错误。

以下是深赋值的实现:

String& String::operator=(const String& other) {
    if (this != &other) {                 // 检查自我赋值
        delete[] data;                    // 释放原有内存
        length = other.length;           // 复制长度
        data = new char[length + 1];     // 动态分配内存
        strcpy(data, other.data);        // 复制内容
    }
    return *this;                        // 返回当前对象的引用
}

四、总结

通过模拟实现 String 类,我们可以更深入地理解字符串的底层实现原理,包括内存管理、深浅拷贝、深赋值、常用操作以及性能优化等方面。在实际编程中,我们应该根据具体需求选择合适的字符串实现方式,并注意避免常见的内存错误和性能问题。

五、附录

5.1 完整代码

namespace dza {
	const size_t string::npos = -1;
	/*成员函数*/

	//完全通过初始化列表这种方式来初始化。可能会有问题
	/*string::string()
		:_str(new char[1]{ '\0' })
		, _size(0)
		, _capacity(0)
	{}*/

	//构造函数
	string::string(const char* str)
		:_size(strlen(str))//只初始化size
	{
		_capacity = _size;
		_str = new char[_size + 1];

		strcpy(_str, str);
	}

	//传统写法
	string::string(const string& s)
	{
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}

	现代写法
	//string::string(const string& s)
	//{
	//	string tmp(s._str);
	//	swap(tmp);
	//}

	//传统写法
	string& string::operator=(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& string::operator=(string s)
	//{
	//	swap(s);
	//	return *this;
	//}

	//析构函数
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}

	///*迭代器*/
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1]();
			//strcpy(_str, tmp);
			strcpy(tmp, _str);
			// strcpy和swap不一样。参数不能调换
			delete[] _str;
			_str = tmp;

			_capacity = n;
		}
	}

	// 插入字符
	void string::push_back(char ch)
	{
		//两种写法
		//第一种
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_size++;

		//第二种
		//insert(_size,ch);
	}

	// 插入字符串
	void string::append(const char* str)
	{
		//两种写法
		//①
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + len)
			{
				newCapacity = _size + len;
			}
			reserve(newCapacity);
		}

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

		
		//insert(_size, str);
	}

	// 底层逻辑是push_back。所以和push_back的使用一样
	string& string::operator+=(char ch)
	{
		push_back(ch);

		return *this;
	}

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

		return *this;
	}

	// 
	void string::insert(size_t pos, char ch)
	{
		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] = ch;
		_size++;
	}

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

		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + len)
			{
				newCapacity = _size + len;
			}
			reserve(newCapacity);
		}

		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}

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

		_size += len;
	}

	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			size_t end = pos + len;
			while (end <= _size)
			{
				_str[end - len] = _str[end];
				++end;
			}
			_size -= len;
		}
	}

	size_t string::find(char ch,size_t pos)
	{
		assert(pos < _size);

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

		return npos;
	}

	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);

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

	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len > (_size - pos))
		{
			len = _size - pos;
		}

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

		return sub;
	}

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

	///
	
	// 这个swap是为了防止没有使用定义内的swap。
	// 编译器自带的swap在交换时,会调用三次构造和三次析构。大大降低程序效率
	// 自己定义的swap就只是交换两个对象所指向的资源。效率快很多
	void swap(string& s1, string& s2)
	{
		s1.swap(s2);
	}

	bool operator== (const string& lhs, const string& rhs)
	{
		// strcmp。两个字符串相等返回0。lhs大于rhs返回大于0的数,lhs小于rhs返回小于0的数
		// 相等则是真,其余情况都是返回假
		return strcmp(lhs.c_str(), rhs.c_str()) == 0;
	}

	bool operator!= (const string& lhs, const string& rhs)
	{
		// 如果lhs等于rhs,则返回假
		// 如果lhs不等于rhs,则返回真。
		return !(lhs == rhs);
	}

	bool operator< (const string& lhs, const string& rhs)
	{
		// 如果lhs小于rhs。那么strcmp返回的是小于0的数。
		// 然后判断。若是小于0,则为真;其他情况都为假
		return strcmp(lhs.c_str(), rhs.c_str()) < 0;
	}

	bool operator> (const string& lhs, const string& rhs)
	{
		// 如果lhs大于rhs,则为假,返回假说明 rhs 不大于 lhs ;等于或者大于都是真,说明 rhs 大于等于 lhs
		return !(lhs <= rhs);
	}

	bool operator<= (const string& lhs, const string& rhs)
	{
		// lhs > rhs 为假,返回假,说明 lhs 不小于等于 rhs 。其余情况都说明 lhs 大于等于 rhs
		return lhs < rhs || lhs == rhs;
	}
	
	bool operator>= (const string& lhs, const string& rhs)
	{
		// 
		return !(lhs < rhs);
	}

	ostream& operator<<(ostream& os, const string& str)
	{
		// 遍历 str 的内容
		// 这个os是什么?return os是返回什么?
		for (size_t i = 0; i < str.size(); i++)
		{
			os << str[i];
		}
		return os;
	}

	istream& operator>>(istream& is, string& str)
	{
		// 在流插入之前,把字符串的内部清空
		str.clear();

		// 使用内置类型的数组。可以避免自定义类型的频繁扩容
		// 因为内置类型的数组空间开创在栈中,空间扩容比位于堆中的自定义类型方便
		int i = 0;
		char buff[256];

		// 定义一个ch,用来记录输入的数据
		// get()函数是编译器提供输入数据的函数。这里如果使用 >> 来输入数据,字符串就无法输入' ' 和 '\0'
		// get()则是用户自己定义结束标志
		char ch;
		//cin >> ch;
		ch = is.get();

		// 这里定义的结束标志为 ' ' 和 '\0'。与编译器一致
		while (ch != ' ' && ch != '\n')
		{
			// i++。记录输入的数据的实际长度
			buff[i++] = ch;
			// 如果数据长度超过256,那么先让str的长度扩大256。再把i清0。重新记录数据的长度
			if (i == 255)
			{
				buff[i] = '\0';
				str += buff;
				i = 0;
			}

			// 把数据放进数组中
			ch = is.get();
		}

		// 这里是避免ch已经读取完。但是buff数组里面还有残余的数据没有输入进str
		if (i > 0)
		{
			buff[i] = '\0';
			str += buff;
		}

		return is;
	}

	istream& getline(istream& is, string& str, char delim)
	{
		str.clear();

		int i = 0;
		char buff[256];

		char ch;
		ch = is.get();

		while (ch != delim)
		{
			buff[i++] = ch;
			if (i == 255)
			{
				buff[i] = '\0';
				str += buff;
				i = 0;
			}

			ch = is.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			str += buff;
		}

		return is;
	}
}

6.2 测试用例

#include"string的模拟实现.h"

void test01()
{
	dza::string s1("123");
	cout << "s1.size() == " << s1.size() << endl;
	dza::string s2;
	cout << "s2.size() == " << s2.size() << endl;

	// push_back只能插入一个字符
	s2.push_back('h');
	s2.push_back('e');
	s2.push_back('l');
	s2.push_back('l');
	s2.push_back('o');

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

	s1.push_back('5');

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


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

	s1 += '6';
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	dza::string s3;
	// append可以插入一个字符串
	s3.append("12345");
	s3.append("上山打老虎");
	// 范围for不能打印汉字。因为范围for是一个一个字节的打印。但是汉字一般有两个字节以上
	/*for (auto ch : s3)
	{
		cout << ch << " ";
	}*/
	cout <<s3 <<  endl;
}

void test02()
{
	dza::string s1("123");

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

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

	s1.push_back('1');
	s1.push_back('3');
	s1.push_back('1');
	s1.push_back('4');
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	// 从位置为1开始,删除两个字符
	s1.erase(1,2);
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	// 从位置为1开始,删除一个字符
	s1.erase(1, 1);
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	// 从位置为1开始,删除后面所有字符
	s1.erase(1);
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	cout << "--------------------------------------------------------------------------------" << endl;

	dza::string s2("1234");
	for (auto ch : s2)
	{
		cout << ch << " ";
	}
	cout << endl;

	s2.insert(0, '1');
	for (auto ch : s2)
	{
		cout << ch << " ";
	}
	cout << endl;

	s2.insert(3, '1');
	for (auto ch : s2)
	{
		cout << ch << " ";
	}
	cout << endl;

	// 位置必须给,不是默认从0开始
	/*s2.insert('0');
	for (auto ch : s2)
	{
		cout << ch << " ";
	}
	cout << endl;*/

	s2.insert(s2.size(), '9');
	for (auto ch : s2)
	{
		cout << ch << " ";
	}
	cout << endl;
}

void test03()
{
	// 提取网址
	dza::string s1 = "https://legacy.cplusplus.com/reference/cstring/strstr/?kw=strstr";
	//bit::string s1 = "https://blog.csdn.net/ww753951/article/details/130427526";
	
	// pos1先找到网址中的 :
	size_t pos1 = s1.find(':');
	
	// pos2从pos1加3的位置开始找'/'。最后得到网址中的地址
	size_t pos2 = s1.find('/', pos1 + 3);
	
	if (pos1 != string::npos && pos2 != string::npos)
	{
		// substr的作用是提取字符串中的一段。来创建一个全新的子字符串
		dza::string domain = s1.substr(pos1 + 3, pos2 - (pos1 + 3));
		cout << domain.c_str() << endl;

		dza::string uri = s1.substr(pos2 + 1);
		cout << uri.c_str() << endl;
	}
}

void test04()
{
	dza::string s1("12345");
	cout << s1.c_str() << endl;
	dza::string s2("hello world");
	cout << s2.c_str() << endl;
}

int main()
{
	test04();

	return 0;
}

六、致谢

感谢您的阅读,希望本文对您有所帮助。如果您有任何疑问或建议,欢迎留言交流。


http://www.kler.cn/a/580175.html

相关文章:

  • 机器学习之KMeans算法
  • CI/CD—Jenkins配置Poll SCM触发自动构建
  • JSON.parse(JSON.stringify())深拷贝不会复制函数
  • 数据库1-2章
  • 【商城实战(18)】后台管理系统基础搭建:从0到1构建电商中枢
  • C++ 算法竞赛STL以及常见模板
  • 优选算法系列(1. 双指针_上)
  • DICOM医学影像脱敏技术应用的重要性及其实现方法详解
  • [Pytorch报错问题解决]AttributeError: ‘nn.Sequential‘ object has no attribute ‘append‘
  • Math 类的核心 API
  • BERT(Bidirectional Encoder Representations from Transformers)的序列分类模型,简单学习记录
  • 【算法】图论 —— SPFA 算法 python
  • 集合知识点
  • 《探索微观世界的钥匙:量子》
  • Manus:成为AI Agent领域的标杆
  • 大语言模型-01-语言模型发展历程-02-从神经网络到ELMo
  • go 标准库包学习笔记
  • 【数据集】社区天气资讯网络CoWIN-香港小时尺度气象数据(含MATLAB处理代码)
  • 【编程题】7-5 堆中的路径
  • 前端HTML转word文档,绝对有效!!!