C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++模拟实现string

C++模拟实现string的详细过程

作者:JhonKI

在 C++ 编程中,字符串的处理是一项常见且重要的任务,标准库中的 string 类为我们提供了便捷、高效的字符串操作方法,模拟实现 string 类 的背景源于对 C++ 底层原理的探索欲望,所以本文给大家介绍了C++模拟实现string的详细过程,需要的朋友可以参考下

一、前言

在 C++ 编程中,字符串的处理是一项常见且重要的任务。标准库中的 string 类为我们提供了便捷、高效的字符串操作方法。然而,深入理解其内部实现机制对于提高编程技能和优化代码性能具有重要意义。

模拟实现 string 类 的背景源于对 C++ 底层原理的探索欲望。通过亲手模拟实现,我们能够更清晰地理解字符串的存储、管理和操作方式。

其重要性体现在多个方面。首先,有助于掌握内存管理的细节,如动态分配、扩容和释放,避免内存泄漏和提高资源利用效率。其次,能够深入理解字符串的各种操作,如拷贝构造、赋值、插入、删除等,从而在实际编程中更加准确和高效地运用这些操作。此外,模拟实现过程培养了我们解决复杂问题的能力,提升了对 C++ 语言特性的掌握程度。

总之,模拟实现 string 类 不仅是对知识的深入挖掘,更是为了在实际编程中能够更加得心应手地处理字符串相关的任务,编写更加健壮和高效的代码。

二、准备工作

2.1 涵盖必要的头文件与命名空间

在对 string 类进行模拟实现时,我们必须包含一些不可或缺的头文件,例如 <iostream> 用于执行输入输出操作,<cstring> 用于和字符串相关的函数,诸如 strlen 、 strcpy 等。与此同时,或许还需运用命名空间,例如 using namespace std; ,从而更便捷地使用标准库中的函数与对象。

2.2 简述模拟实现 string 所需的基础概念和知识

模拟实现 string 类需要熟稔一些关键的概念与知识。

首先是动态内存的管理,鉴于 string 的长度具备可变性,需要在运行期间动态地进行内存的分配与释放,以存储字符串的内容。这牵涉到 new 和 delete 操作符的使用。

其次,要领会指针的操作,凭借指针来管理动态分配的内存。

还需熟知字符串的基本操作,像是字符串的拷贝、拼接、查找、插入、删除等等。对于字符编码和字符处理方面的知识也应当有所知晓,以保障字符串的处理准确无误。

此外,掌控构造函数、析构函数、拷贝构造函数以及赋值运算符的重载也是至关重要的,以此确保对象能够正确地创建、销毁和复制。同时,对于异常处理也应当具备一定的认知,用以处理可能出现的诸如内存分配失败等异常状况。

三、成员变量

  1. char _str*
    char* _str 是一个指针,用于动态地存储字符串的数据。在模拟实现 string 类的过程中,它承担着核心的角色。通过动态分配内存,_str 能够根据字符串内容的长度灵活地调整存储空间,从而有效地管理字符串数据。在进行字符串的操作,如拷贝、修改、插入或删除时,都是基于 _str 所指向的内存区域进行的。

  2. size_t _capacity
    size_t _capacity 用于记录字符串的容量,即当前为存储字符串所分配的内存空间大小。它在字符串的扩容操作中起着关键作用。当字符串的长度即将超过当前分配的容量时,需要根据一定的策略(如倍增或按照特定比例增加)来重新分配更大的内存空间,并更新 _capacity 的值,以确保有足够的空间容纳不断增长的字符串。

  3. size_t _size
    size_t _size 表示字符串的有效长度,即实际存储的字符数量。它与 _capacity 不同,_capacity 关注的是内存空间,而 _size 关注的是字符串中实际包含的有效字符数。在进行字符串的操作,如插入、删除字符时,需要根据 _size 来确定操作的位置和范围,以保证字符串的完整性和正确性。

四、默认成员函数

4.1 构造函数

// 短小频繁调用的函数,可以直接定义到类里面,默认是inline
		string(const char* str = "")
		{
			_size = strlen(str);
			// _capacity不包含\0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

上述构造函数实现了带缺省参数的功能。若传入的字符串为空指针或空字符串,会进行相应的处理,为字符串分配适当的内存空间,并初始化_size_capacity

4.2 析构函数

析构函数用于释放构造函数中动态分配的内存,并将相关的成员变量重置为初始状态,以避免内存泄漏和资源浪费。

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

4.3 拷贝构造函数

在传统的拷贝构造函数中,首先计算源字符串的长度。然后,为新字符串分配足够的内存空间,并通过strcpy函数将源字符串的内容复制到新分配的空间中。同时,正确设置_size_capacity的值,以反映新字符串的长度和容量。

// 深拷贝问题
		
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

4.4 拷贝构造函数(临时变量)

在现代的实现方式中,首先将_str初始化为nullptr。然后创建一个临时对象tmp,其构造过程会完成对源字符串的复制。接着,通过交换函数swap将临时对象tmp的_str与当前对象的_str进行交换,从而实现深拷贝。同时,也要确保_size和_capacity的值得到正确更新。

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

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

这种现代方式避免了直接的内存分配和复制操作,通过巧妙地利用临时对象和交换,提高了代码的简洁性和效率。

五、赋值运算符重载

5.1 传统方式处理空间分配和字符串复制

在传统的赋值运算符重载中,首先检查当前对象是否与传入的对象不同。若不同,则先释放当前对象所占用的内存。然后计算传入对象字符串的长度,为新字符串分配足够的内存空间,并通过strcpy函数复制字符串内容。同时,正确设置_size和_capacity的值。

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

5.2 现代方式通过交换实现赋值

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

在现代的赋值实现方式中,首先创建一个传入对象的副本other。然后通过交换函数swap交换当前对象和副本对象的_str指针。接着,直接将副本对象的_size_capacity值赋给当前对象。这种方式避免了直接的内存分配和复制操作,提高了赋值的效率。

六、容量相关操作接口

6.1 size()

size() 函数用于返回字符串的有效长度,即实际存储的字符数量。它不包括字符串末尾的空字符 '\0' 。在使用时,直接调用 string 对象.size() 即可获取字符串的有效长度。
例如:

size_t size() const{ return _size; }

在 size_t size() const { return _size; } 这段代码中,const 关键字具有以下重要作用:

  1. 保证成员函数不修改类的成员变量:
    这意味着在这个 size() 函数内部,不能对类中的任何成员变量进行修改操作。
    例如,如果类中有其他成员变量,如 int count; ,在这个 size() 函数中就不能执行类似于 count++; 这样的修改操作。
  2. 使函数可以用于常量对象:
    当有一个常量对象时,只能调用其 const 成员函数。
    例如,如果有 const MyClass obj; ,那么只能通过 obj.size(); 来获取大小,而不能调用可能修改对象状态的非 const 成员函数。
  3. 增强程序的可读性和可维护性:
    当看到 const 修饰的成员函数,开发者能立即明白该函数不会修改对象的状态。

6.2 capacity()

capacity() 函数返回字符串当前分配的内存空间大小,即容量。它反映了为存储字符串所预留的内存空间。
例如:

size_t capacity() const{ return _capacity; }

6.3 reserve()

reserve() 函数用于调整字符串的容量。当我们预计字符串可能会增长到一定规模时,可以提前使用reserve()函数为其预留足够的内存空间,以减少后续频繁的内存重新分配操作,提高性能。

	void string::reserve(size_t n)
	{
		//	不能用realloc,必须匹配使用
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

七、修改相关操作接口

7.1 push_back()在字符串末尾追加字符。

push_back() 函数用于在字符串的末尾追加一个字符。它能够方便地扩展字符串的内容。

	void string::push_back(char ch)
	{
		if (_size == _capacity)
			reserve(_capacity == 0 ? 4 : _capacity * 2);

		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}

例如:

string str = "Hello";
str.push_back('!');
cout << str << endl;  // 输出:Hello!

在这个例子中,通过 push_back('!')操作,将感叹号字符添加到了字符串 str 的末尾。

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

例如:

string s1 = "Hello";
string s2 = " World";
s1.append(s2);
cout << s1 << endl;  // 输出:Hello World

7.3 insert()在指定位置插入字符或字符串。

insert() 函数能够在指定的位置插入字符或字符串。比如:

	void 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 - len + 1 > pos)
		{
			_str[end] = _str[end - len];
			--end;
		}
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = str[i];
		}
		_size += len;
	}

还可以插入单个字符:

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

八、非成员函数

8.1 流插入运算符重载 (<<)

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

上述重载函数使得可以使用 << 运算符将自定义的 String 对象输出到标准输出流中。它直接将 String 对象内部存储的字符串数据输出。

8.2 流提取运算符重载 (>>)

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

		const int  N = 256;
		char buff[N];

		int i = 0;

		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n') {
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;

				i = 0;
			}

			ch = in.get();// 获取下一个字符
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

此重载函数实现了从输入流中读取数据到 String 对象的功能。首先,使用一个临时缓冲区读取输入数据,然后将其转换为 String 对象并赋值给传入的 String 引用。

整体代码

string.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once

#include<iostream>
#include<string>
#include<assert.h>
using namespace std;

namespace bit
{
	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()
			:_str(new char[1]{'\0'})
			,_size(0)
			,_capacity(0)
		{}*/

		// 短小频繁调用的函数,可以直接定义到类里面,默认是inline
		string(const char* str = "")
		{
			_size = strlen(str);
			// _capacity不包含\0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// 深拷贝问题
		// 
		// s2(s1)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		// s2 = s1
		// s1 = s1
		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()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

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

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

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

		void reserve(size_t n);
		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);

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);
	private:
		//char _buff[16];
		char* _str;
		size_t _size;
		size_t _capacity;

		//static const size_t npos = -1;
		static const size_t npos;

		/*static const int N = 10;
		int buff[N];*/
	};

	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);

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

string.cpp

#include"string.h"

namespace bit
{
	const size_t string::npos = -1;


	//	保留 + 扩容
	void string::reserve(size_t n)
	{
		//	不能用realloc,必须匹配使用
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			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;
		_str[_size] = '\0';
	}

	//不能按2倍扩容
	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;
	}

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

		//	挪动数据
		//int end = _size;
		//while (end >= (int)pos)//	不强转会整形提升,导致pos为0时出错
		//{
		//	if (end == 0)
		//	{
		//		int i = 0;
		//	}
		//	_str[end + 1] = _str[end];
		//	--end;
		//}

		//	挪动数据
		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)
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		size_t end = _size + len;
		while (end - len + 1 > pos)
		{
			_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
		{
			for (size_t i = 0; i <= _size - pos - len; i++)
			{
				_str[pos + i] = _str[pos + i + len];
			}
			_size -= len;
		}
	}

	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size ;i++)
		{
			if (_str[i] == ch)
				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 ptr - _str;
		}
		else
			return npos;
	}

	//	substr 函数用于从一个字符串中提取子字符串
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		// len大于剩余字符长度,更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}

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

		return sub;
	}

	//	字符串都是按ASCLL码比较的
	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 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 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);
	}


	// 必须为全局,但不一定要为友元
	// 输出流
	ostream& operator<<(ostream& out, const string& s)	{
		for (auto ch : s)
			out << ch;
		return out;
	}
	// 输入流
	istream& operator>>(istream& in, string& s) {
		s.clear();

		const int  N = 256;
		char buff[N];

		int i = 0;

		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n') {
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;

				i = 0;
			}

			ch = in.get();// 获取下一个字符
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}


	void test_string1()
	{
		string s1;
		string s2("hello world");
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	
		for (size_t i = 0; i < s2.size(); i++)
		{
			s2[i] += 2;
		}
		cout << s2.c_str() << endl;

		//	范围for的本质就是迭代器
		for (auto e : s2)
		{
			cout << e << " ";
		}
		cout << endl;

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




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

		s1 += "  xxx";
		s1.append("  %%%");
		cout << s1.c_str() << endl;

		s1.insert(5, '^');
		cout << s1.c_str() << endl;

		s1.insert(5, " &&& ");
		cout << s1.c_str() << endl;

 		s1.erase(1);
		cout << s1.c_str() << endl;

	}


	void test_string3()
	{
		string s1("hello world");
		
		string s2 = s1.substr(0, 3);
		cout << s2.c_str() << endl;

		string copy(s1);
		cout << copy.c_str() << endl;

		string s3 = s1;
		cout << s3.c_str() << endl;

		char str[5];
		cin >> s1;
		cout << s1 << endl;
		cin >> s1 >> s2 >> str;
		cout << s1 << s2 <<str;
	}

	void test_string4()
	{
		std::string s1("11");
		std::string s2(s1);// 深拷贝
		printf("%p\n", s1.c_str());
		printf("%p\n", s2.c_str());

		cout << sizeof(s1) << endl;
		cout << sizeof(s2) << endl;
	}
}

总结

在模拟实现 string 类的过程中,我们涉及了众多的要点和需要特别注意的事项。
首先,对于成员变量的管理至关重要。char* _str 用于动态存储字符串数据,其内存的分配和释放要精确控制,以避免内存泄漏和错误访问。size_t _capacity 和 size_t _size 分别用于记录容量和有效长度,它们的正确更新和使用影响着字符串的存储和操作效率。
在函数实现方面,构造函数要处理好空字符串和正常字符串的初始化。析构函数务必释放动态分配的内存。拷贝构造函数和赋值运算符重载需要确保深拷贝,避免浅拷贝导致的问题。

容量相关操作接口,如 reserve() 和 resize() ,要理解其对内存和字符串长度的影响,以及不同实现方式的性能差异。

修改相关操作接口,如 push_back()、append()、insert() 和 erase() ,需要注意操作的边界情况和对字符串状态的更新。

遍历访问相关接口,迭代器的实现要保证操作符重载的正确性和高效性,operator[] 要进行有效的索引检查。

非成员函数中的流插入和流提取运算符重载,要保证数据的正确传输和对象的正确赋值。

总之,模拟实现 string 类需要对 C++ 的内存管理、指针操作、函数重载等知识有深入的理解和熟练的运用,同时要注重代码的效率、安全性和可维护性。只有这样,才能实现一个功能完善、性能优良的 string 类模拟。

以上就是C++模拟实现string的详细过程的详细内容,更多关于C++模拟实现string的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文