【C++】string模拟实现

各位读者老爷好,俺最近在学习string的一些知识。为了更好的了解string的结构,俺模拟实现了一个丐版string,有兴趣的老爷不妨垂阅!!!

目录

1.string类的定义

2.模拟实现成员函数接口 

2.1.constructor(构造函数)

2.2.destructor(析构函数)

2.3.c_str 

 2.4.size

 2.5.operator[]

2.6.begin、end

2.7.capacity

2.8.reserve 

2.9.push_back 

2.10.append 

2.11.operator+=

2.12.insert 

2.13.erase 

2.14.find

 2.15.substr

2.16.constructor

2.17.operator=

 2.18.clear

 2.19.swap

 3.模拟实现非成员函数接口

3.1.relational operators

3.2.operator<< 

3.3.operator>> 

3.4.getline

4.个别接口的不同写法

 4.1.constructor的不同写法

4.2.operator=的不同写法

4.3.operator>>的不同写法

5. string模拟实现完整代码

5.1.string.h

5.2.string.cpp 

5.3.test.cpp 


我们要明白,代码的实现方法有一万种,只要能实现特定功能就行。例如string类的实现,不同的平台下实现的方法也会有所区别,但是实现的string类一定要满足string文档的要求。俺下面模拟实现的string亦是如此。

俺在上篇博客介绍过,string是一个由类模板实例化而来的类,是用来管理字符串的。其底层实现大致就是字符顺序表。

那么俺模拟实现string,是不是要从类模板开始搞起呢?当然不是,俺可没有这个本事。俺直接定义string类就好。并且string的接口众多,俺只模拟实现部分常用接口噢噢!!

1.string类的定义

namespace HD
{
	class string
	{
		char* _str = nullptr;
		size_t _capacity = 0;
		size_t _size = 0;
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		static const size_t npos;

		//string()
		//	:_str(new char[1]{'\0'})
		//	,_capacity(0)
		//	,_size(0)
		//{
		//}

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

		string(const char* s = "")//以上两构造合二为一
		{
			_size = strlen(s);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, s);
		}

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

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

		size_t size()const
		{
			return _size;
		}

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

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

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin()const
		{
			return _str;
		}

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

		size_t capacity()const
		{
			return _capacity;
		}

		string(const string& str)//拷贝构造函数
		{
			/////////////////写法一/////////////
			/*_str = new char[str._capacity + 1];
			strcpy(_str, str._str);
			_size = str._size;
			_capacity = str._capacity;*/

			/////////////////写法二/////////////
			string tmp(str.c_str());
			swap(tmp);
		}
		void reserve(size_t n = 0);
		void push_back(char c);
		string& append(const char* s);
		string& operator+= (const char* s);
		string& operator+=(char c);
		string& insert(size_t pos, size_t n, char c);
		string& insert(size_t pos, const char* s);
		string& erase(size_t pos = 0, size_t len = npos);
		size_t find(char c, size_t pos = 0) const;
		size_t find(const char* s, size_t pos = 0) const;
		string substr(size_t pos = 0, size_t len = npos) const;
		string& operator=(const string& str);
		//string& operator=(const string str);
		void clear();
		void swap(string& str);
	};
}

对于这个string类的定义如上。

1.俺们知道string类底层是字符顺序表,所以俺定义3个private成员变量_str、_capacity、_size来实现,并且给这3个成员变量缺省值,这3个缺省值是很有意义的,意义的体现会在下面的讲解可见。

2.俺将一些成员函数(接口)的声明和定义全部放到string类当中,则默认当成内联函数处理;还有一些成员函数(接口)的声明和定义分离,只将这些成员函数的声明放到string类当中。

3.为了防止与命名空间std的string(也就是STL的string)产生冲突,俺实现的string使用自己命名空间(如HD)封装起来。

PS:以下所说的string若没有特殊声明,都默认指俺模拟实现的string。若指STL的string,俺会用红色字体标注string,如string

2.模拟实现成员函数接口 

2.1.constructor(构造函数)

对于这个接口,string内重载了很多个函数。俺暂时就模拟实现两个,如下:


1.string()

这个函数的模拟实现如下,俺将其声明和定义都放到string类中:

		string()
			:_str(new char[1]{'\0'})
			,_capacity(0)
			,_size(0)
		{
		}

这里俺使用初始化列表来初始化。易知这是一个构造空的string对象的,那么当然将_capacity和_size初始化成0;但是对于_str,俺将其指向堆区动态开辟的一个字节的空间,这块空间存储字符'\0'。

为什么_str不用nullptr来初始化呢?其实大有意义,让子弹再飞一会,原因俺下面介绍。


3.string (const char* s)

这个函数的模拟实现如下,俺将其声明和定义都放到string类中:

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

这里俺在函数体内初始化。俺们知道这个构造函数的功能是用C-string来构造string类对象。那么我们开好空间(记得多开一个空间存放'\0'),将C_string(也就是s)拷贝到空间上,_size和_capacity 初始化为C_string的字节数(长度)就好。


2.2.destructor(析构函数)

这个接口就是析构函数,string类中申请了资源,俺们必须显示写析构函数,不然会造成资源的泄露。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:

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

若不显示写析构函数,_str指向的空间就泄露了。

2.3.c_str 

 这个接口功能就是返回C形式的字符串,也就是返回底层的_str。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:

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

这是一个对this指针指向对象的成员变量只进行读访问的函数,所以我们将其实现成const成员函数,且这样子普通对象和const对象都可调用这个函数。


俺们来使用以下自己模拟实现的接口。事先要先知道的是本工程包含3个文件

……

string.h:存放string类的定义和非成员函数的声明;

……

string.cpp:存放string类部分成员函数的定义、静态成员变量的定义和非成员函数的定义;

……

test.cpp:存放测试代码,来测试咱们模拟实现的string的功能;

……

俺们到现在介绍了几个接口的模拟实现了,咱们在test.cpp中试试:

#include"string.h"
namespace HD
{
	void test1()
	{
		string S1;
		string S2("hello world");
		cout << S1.c_str() << endl << S2.c_str() << endl;
	}
}
int main()
{
	HD::test1();
	return 0;
}

运行结果:

 有几个点需要明确:

……

1.为什么test1函数需要用命名空间HD封装起来?

因为若是不用命名空间HD封装起来,string类实例化对象S1、S2会调用string的构造函数接口,且c_str接口的调用亦是调用string的接口,这样子就违背了我们测试模拟实现的接口的初衷。有关命名空间和编译器访问规则的介绍可以看[C++]C++入门1.0

……

2.主函数中调用test1函数为什么要指定命名空间HD域?

因为若是不指定命名空间HD域,编译器会访问不到test1函数。编译器的访问规则介绍可以看[C++]C++入门1.0

……

3.string()函数的定义中,_str不用nullptr来初始化呢?

若是用nullptr来初始化_str,当空string对象调用c_str接口时就返回nullptr,当我们对这个返回值进行操作时就有可能访问到空指针,程序就会崩溃。


 2.4.size

 这个接口这个接口返回字符串的长度。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:

		size_t size()const
		{
			return _size;
		}

 这是一个对this指针指向对象的成员变量只进行读访问的函数,所以我们将其实现成const成员函数,且这样子普通对象和const对象都可调用这个函数。

在test.cpp中测试一下:

#include"string.h"
namespace HD
{
	void test2()
	{
		string S1;
	    string S2("hello world");
		cout << S1.size() << endl << S2.size() << endl;
	}
}
int main()
{
	HD::test2();
	return 0;
}

运行结果:

 2.5.operator[]

本接口返回对字符串中位置pos处的字符的引用。且string中本接口重载了2个函数分别供普通对象和const对象使用。俺也模拟实现2个函数,并将其声明和定义都放到string类中:

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

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

在test.cpp中测试一下:

#include"string.h"
namespace HD
{
	void test3()
	{
		string S1("nbo");
		for (int i = 0; i < S1.size(); i++)
		{
			S1[i] -= 1;
			cout << S1[i];
		}
		cout << endl;
		const string S2("hello world");
		for (int i = 0; i < S2.size(); i++)
		{
			cout << S2[i];
		}
	}
}
int main()
{
	HD::test3();
	return 0;
}

对于char& operator[](size_t pos)这个函数,这个函数是一个对this指针指向对象的成员变量只进行读访问的函数,可是我们不能将其实现成const成员函数,为什么呢?

因为俺们实现了const char& operator[](size_t pos)const这个函数供const对象调用,若是还将char& operator[](size_t pos)实现成const成员函数的话,无法与const char& operator[](size_t pos)构成函数重载!

2.6.begin、end


有一个问题:基于范围的for循环的方式,对所有容器都适用。那么到现在为止俺们模拟实现的string适用于基于范围的for循环吗?

俺们在test.cpp试试:

#include"string.h"
namespace HD
{
	void test4()
	{
		string S1("hello world");
		for (auto ch : S1)
		{
			cout << ch;
		}
		cout << endl;
	}
}
int main()
{
	HD::test4();
	return 0;
}

发现行不通的,错误列表有一堆报错:

到现在为止模拟实现的string不适用基于范围的for循环的原因也很简单,因为范围for的底层很简单,容器遍历实际就是替换为迭代器。咱们模拟实现的string还没有模拟实现迭代器,也没有模拟实现迭代器相关接口,当然不适用了。

那么如何模拟实现迭代器呢?


 迭代器的实现很复杂,其实迭代器本质就是模拟指针的行为啊,不管迭代器是用什么方法实现的,都希望迭代器能像指针般使用达到访问容器的目的。

那么针对string的模拟实现来说,不用搞那么复杂,我们正好可以返璞归真,利用string类的底层是字符顺序表这个特点,俺们就可以用指针模拟实现迭代器,因为本身就可以用指针来访问顺序表啊!

正向迭代器的模拟实现如下,对应的迭代器属于对应容器的类域,所以俺将其声明放在string类中:

		typedef char* iterator;
		typedef const char* const_iterator;

 俺就不模拟实现反向迭代器了,反向迭代器就不是用指针可以模拟实现的了,复杂的很!


模拟实现了正向迭代器,俺们就可以模拟实现正向迭代器相关的接口了,以下接口的声明和定义都放到string类中:

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin()const
		{
			return _str;
		}

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

这些接口的模拟实现好像不用解释什么,根据接口的作用很容易就能写出来啊。 

模拟实现了正向迭代器和正向迭代器相关的接口,俺们在test.cpp中测试一下:

#include"string.h"
namespace HD
{
	void test4()
	{
		string S1("hello world");
		for (auto ch : S1)
		{
			cout << ch;
		}
		cout << endl;
		const string S2("God is a girl");
		string::const_iterator cit = S2.begin();
		while (cit != S2.end())
		{
			cout << *cit;
			cit++;
		}
		cout << endl;
	}
}
int main()
{
	HD::test4();
	return 0;
}

运行结果没问题的,基于范围的for循环也能正常使用了:

2.7.capacity

 这个接口这个接口返回字符串的容量。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:

		size_t capacity()const
		{
			return _capacity;
		}

 这是一个对this指针指向对象的成员变量只进行读访问的函数,所以我们将其实现成const成员函数,且这样子普通对象和const对象都可调用这个函数(这点以下不再赘述)。

在test.cpp中测试一下:

#include"string.h"
namespace HD
{
	void test5()
	{
		string S1;
		string S2("hello world");
		cout << S1.capacity() << endl << S2.capacity() << endl;
	}
}
int main()
{
	HD::test5();
	return 0;
}

运行结果:

2.8.reserve 

 本接口功能是为string预留空间,不改变有效元素个数。俺模拟实现这个接口不搞那么复杂,当扩容才使其生效。模拟实现本成员函数采用声明和定义分离的写法:

string类(再次说明string的定义放在了string.h中)中声明:

void reserve(size_t n = 0);

 string.cpp中定义:

#include"string.h"
namespace HD
{
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* p = new char[n + 1];
			strcpy(p, _str);
			delete[] _str;
			_str = p;
			_capacity = n;
		}
	}
}

1.缺省参数不能在函数声明和定义中同时出现,当函数声明和定义分离时在声明给缺省参数 。 

2.别忘记使用命名空间HD封装起来。

3.该函数声明和定义

### 实现C++自定义String类 为了创建一个具备基本功能的`String`类,可以考虑以下几个方面: #### 类成员变量 首先声明私有成员来存储字符数组以及字符串长度。 ```cpp class Mystring { private: char* str; size_t length; }; ``` 这允许内部管理动态分配的内存用于保存实际字符串数据[^1]。 #### 构造函数与析构函数 提供默认构造函数、带参数构造函数和拷贝构造函数,并实现析构函数以释放资源。 ```cpp public: // 默认构造函数 Mystring() : str(nullptr), length(0) {} // 带参构造函数 explicit Mystring(const char* s) { if (s != nullptr) { length = strlen(s); str = new char[length + 1]; strcpy(str, s); } else { str = nullptr; length = 0; } } // 拷贝构造函数 Mystring(const Mystring& source) { length = source.length; if (source.str != nullptr) { str = new char[source.length + 1]; strcpy(str, source.str); } else { str = nullptr; } } // 析构函数 ~Mystring() { delete[] str; } } ``` 上述代码展示了如何初始化对象并处理深浅复制问题。 #### 运算符重载 通过运算符重载使`Mystring`能够像内置类型一样操作。这里展示赋值运算符(`=`)的例子。 ```cpp // 赋值运算符 Mystring& operator=(const Mystring& rhs) { if (this == &rhs) return *this; delete[] str; length = rhs.length; if (rhs.str != nullptr) { str = new char[rhs.length + 1]; strcpy(str, rhs.str); } else { str = nullptr; } return *this; } ``` 此部分实现了安全的对象间赋值逻辑。 #### 成员方法 增加一些实用的方法如获取字符串大小、访问单个字符等。 ```cpp size_t get_length() const {return length;} char at(size_t pos) const {if(pos<length)return str[pos];else throw std::out_of_range("Out of range");} void append(const Mystring& other){ char* temp = new char[this->length + other.length + 1]; strcpy(temp, this->str); strcat(temp, other.str); delete[] this->str; this->str = temp; this->length += other.length; } ``` 这些辅助函数增强了类的功能性和易用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X_chengonly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值