【C++】string类的模拟实现

一. 简单string类设计

主要是实现string类的构造拷贝构造赋值运算符重载以及析构函数等资源管理功能。

1. private成员

就是一个C语言中的字符串指针

class string
{
public:

private:
	char* _str;
};

2. 构造函数

我们设计一个全缺省的默认构造函数,如果不传参时就默认存储\0,就是空串

class string
{
public:
	//构造函数(错误写法)
	string(char* str = '\0')
	{
		_str = str;
	}

private:
	char* _str;
};

上面的那个写法其实是错误的,当我们显示地给string对象初始值(字符串)时,这个字符串是存储在代码段的常量只读不可写。不能进行修改操作的话这个string对象也就没意义了。

在这里插入图片描述

既然不能传存在代码段的常量字符串,那么我们传存储在栈上的字符串行不行?也不行,栈上的字符串是存储在字符数组里的,我们虽然可以修改但是不能扩容,因为数组定义出来时空间就是定死的,这样不方便我们对字符串进行资源管理。

在这里插入图片描述

既想要修改字符串内容又想随时扩容,那么把string对象的值放在堆上是最合适的。此时存储在堆空间上的字符串既可以像栈上的字符串一样修改,又可以随时通过new[ ]来开辟你想要大小的空间。

class string
	{
	public:
		//构造函数(正确写法)
		string(const char* str = '\0')//const char* str=""  两种写法一样
		{
			// 1.让_str指向我们在堆上开辟的空间(多开一个为了存储\0)
			_str = new char [strlen(str) + 1];

			// 2.把 str 内容拷贝到 _str(也就是把代码段的内容(str)拷贝到堆空间上(_str))
			strcpy(_str, str);
		}

	private:
		char* _str;
	};

3. 析构函数

使用delete[ ]释放我们开辟的空间,再把_str置为nullptr(防止野指针)

class string
	{
	public:
	    //析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

	private:
		char* _str;
	};

4. 拷贝构造和赋值重载

这里一定要显示定义拷贝构造和赋值重载,如果用默认的会造成浅拷贝问题(多次释放同一块空间)

4.1 什么是浅拷贝?

默认的拷贝构造和赋值重载都是通过浅拷贝实现的。浅拷贝就是一个字节一个字节的拷,浅拷贝也叫值拷贝

在这里插入图片描述

4.2 浅拷贝带来的问题

在这里插入图片描述

4.3 深拷贝完成拷贝构造和赋值重载

既然是指向同一块空间带来的问题,那我们就重新开辟一块同样大小的空间,利用strcpy把另一块空间的内容拷贝到新开辟空间上,这就是深拷贝。

  • 浅拷贝:空间相同,内容相同
  • 深拷贝:空间不同,内容相同

拷贝构造

传统写法:

class string
	{
	public:
		//拷贝构造
		string(const string& s)
		{
			// 1.在_str指向一块新开辟的同样大小的空间(加一个是为了存储\0)
			_str = new char[strlen(_str) + 1];
			// 2.拷贝str空间的内容到_str指向的空间里
			strcpy(_str, s._str);
		}

	private:
		char* _str;
	};

现代写法(更加简洁):

class string
	{
	public:
		string(const string& s)
		{
			string tmp(s._str);
			//这里的swap是c++提供的
			//交换_str和tmp.str指向的空间
			//出了函数tmp生命周期结束,自动调用析构函数释放tmp的空间(也就是原来s的空间)
			swap(_str, tmp._str);
		}

	private:
		char* _str;
	};

赋值重载
赋值重载的两个对象都已经初始化过了,所以在把右值拷贝给左值前要先把左值的旧空间释放,在让它指向新空间

传统写法:

class string
	{
	public:
		//赋值重载
		string& operator=(const string& s)
		{
		    if(this!=&s)
		    {
			   // 1.在_str指向一块新开辟的同样大小的空间(加一个是为了存储\0)
			   char* newstr = new char[strlen(s._str) + 1];
			   // 2.拷贝str空间的内容到newstr指向的空间里
			   strcpy(newstr, s._str);
			   // 3.释放旧的空间
			   delete[] _str;
			   // 4.让_str指向新开辟并且已经拷贝了值的空间
			   _str = newstr;
			   //返回
			   return *this
			}
		}

	private:
		char* _str;
	};

现代写法:

class string
	{
	public:
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s);//拷贝构造s
				swap(_str, tmp._str);
			}
			return *this;
		}

	private:
		char* _str;
	};

赋值重载的几点说明

  • 返回值:为了支持连等,返回值为string又因为出了这个函数之后*this(也就是左值)依然存在所以返回左值的引用(少一次拷贝构造)。
  • 参数值:对于右值我们只是读它的值用来拷贝给左值,并不修改它的内容,所以加上const修饰

二. string类的模拟实现

private成员

除了字符数组(_str)外还加了 _size(记录当前有效字符个数),_capacity(记录可以存储多少个有效字符)和static常量npos(npos就是size_t类型的-1)

class string
{
public:

private:
	char* _str;
	size_t _size;
	size_t _capacity;
	static const size_t npos;
};

接下来我们介绍几个较复杂的接口

1. string类对象容量操作接口

1.1 reserve

原型:void reserve (size_t n = 0);
作用:给字符串对象扩容,若n小于等于当前容量(_capacity)啥事没有;n大于当前容量就扩容

void reserve(size_t n=0)
		{
			if (n > _capacity)
			{
				char* newstr = new char[n+1];// 1.开新空间
				strcpy(newstr, _str);        // 2.拷贝旧空间
				delete[] _str;               // 3.释放旧空间
				_str = newstr;               // 4.指向新空间
				_capacity = n;               // 5.更新容量
			}
		}
1.2 resize

原型:void resize (size_t n, char c=’\0’);
作用:将有效字符的个数改成n个,如果大于原size多出的有效空间用字符c填充,如果没有传字符c,那就默认是字符’ \0 ';如果小于原size就会截断多出来的有效字符。

void resize(size_t n, char c = '\0')
		{
			if (n > _size)
			{
				//检查是否需要扩容
				if (n > _capacity)
				{
					reserve(n);
				}
				memset(_str + _size, c, n - _size);
				_size = n;
				_str[_size] = '\0';
			}
			else if(n<_size)
			{
				_size = n;
				_str[_size] = '\0';
			}
		}

		//简化后可以这样写
		void resize(size_t n, char c = '\0')
		{
			if (n>_size)
			{
				if (n > _capacity)
				{
					reserve(n);
				}
				memset(_str + _size, c, n - _size);
			}
			_size = n;
			_str[_size] = '\0';
		}

如果要求的有效字符个数大于原来的size那么我们用memset来设置后面多出的有效空间,要注意的是memset是一个字节一个字节地拷贝,一般只在设置字符的时候才会用这个。

下面举个例子来说明这个问题:我们要把10容量的整形数组arr内容用memset设置为3
在这里插入图片描述
通过计算器也可以佐证我们的结果
在这里插入图片描述
按照一个字节一个字节来设置的就只适用于给字符数组来设置字符,因为一个字符的大小就是一个字节
在这里插入图片描述

2. string类对象字符串操作接口

2.1 c_str

原型:const char* c_str() const;
作用:返回C格式字符串,只可读不可写

就是直接返回成员变量_str,它的类型是char*

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

C格式字符串和string对象还是不同的,C格式字符串看’ \0 ‘,用cout输出时遇到’ \0 ‘就结束了;而string对象看的是它的有效字符个数(也就是_size),不管中间有没有’ \0 ’

2.2 substr

原型:string substr (size_t pos = 0, size_t len = npos) const;
作用:在str中从pos下标开始,截取n个字符,然后将其返回

string substr(size_t pos = 0, size_t len = npos) const
		{
			//既然是子串,那下标必须合法
			assert(pos < _size);
			if (len > _size)
			{
				len = _size-pos;
			}
			char* tmp = new char[len + 1];// 1.开新空间(多开一个为了存储\0)
			strncpy(tmp, _str + pos, len);// 2.拷贝子串到新空间
			tmp[len] = '\0';              // 3.处理末尾的\0
			string s_tmp(tmp);            // 4.利用前面开的子串空间拷贝构造一个string对象
			delete[] tmp;                 // 5.释放前面开的新空间
			return s_tmp;                 // 6.返回拷贝构造的string类对象
		}

3. string类对象修改操作接口

3.1 insert

原型:string& insert (size_t pos, const char* s);
作用:在pos位置插入一个字符串

string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			// 1.判断容量是否足够,不够的话需要增容
			int len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			// 2.保证空间足够了,就开始挪动数据(一个字符一个字符的挪)
			size_t end = _size;
			while ((int)pos <= (int)end)
			{
				_str[end + len] = _str[end];
				end--;
			}
			// 3.挪好之后,开始放数据
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}

关于insert的几点说明
在这里插入图片描述

3.2 erase

原型:string& erase (size_t pos = 0, size_t len = npos);
作用:删除 pos 下标后的 len 长度字符串

string& erase(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			// 1.如果要求的长度大于等于后面的有效字符的长度,就是删除pos后面的所有有效字符
			if (len >= _size - pos)
			{
				len = _size - pos;
				resize(pos);
			}
			else// 2.删除的是中间一段的话,那就直接把前后拼接起来
			{
				strncpy(_str + pos, _str + pos + len, _size - pos - len + 1);
				_size -= len;
			}
			return *this;
		}

关于erase的几点说明
在这里插入图片描述

4. string类的非成员函数

4.1 operator<<

原型:ostream& operator<< (ostream& out, const string& s);
作用:string类的<<运算符重载

ostream& operator<<(ostream& out, const string& s)
	{
		int len = s.size();
		// 把字符串的字符一个一个的输出,共输出size个
		for (size_t i = 0; i < s.size(); i++)
		{
			out << s[i]; 
		}
		// 最后还要返回out,为了支持连续的<<操作
		return out;
	}

在这里插入图片描述

4.2 operator>>

原型:istream& operator>> (istream& , string& s);
作用:重载string类的<<运算符

该运算符读取和C语言里的scanf一样,在读取字符串时不能读取到空格和回车,都是输入回车时算输入完毕。

istream& operator>>(istream& in, string& s)
	{
		while (1)
		{
			char c = in.get();// 从缓冲区接收数据,一个字符一个字符的接收

			//如果遇到空格或者回车算接收完毕
			if (c == ' ' || c == '\n')
			{
				break;
			}
			else// 否则把字符尾插到对象
			{
				s += c;
			}
		}
		return in;
	}
4.3 getline

原型:istream& getline (istream& is, string& str);
作用:string类对象接收一行的数据(空格也可以接收)

相当于C语言的gets(),可以接收一行的数据

istream& getline(istream& in, string& s)
	{
		while (1)
		{
			char c = in.get();
			if (c == '\n')// 从缓冲区接收数据,遇到回车才停止
			{
				break;
			}
			else
			{
				s += c;
			}
		}
		return in;
	}

5. string类的iterator

对于string类而言,它的typedef就是char*(iterator要在类内的pubulic内声明),既然是char*那么就可以像C语言里面的指针一样使用了。

class string
{
public:
    //string类的iterator
	typedef char* iterator;

private:
	char* _str;
	size_t _size;
	size_t _capacity;
	static const size_t npos;
};

我们可以用iterator来遍历string类对象

void test_string()
	{
		string s("hello");
		my_string::string::iterator it = s.begin();
		while (it != s.end())
		{
			cout << *it << " ";
			it++;
		}
		cout << endl;
	}

其实auto也是的底层也是迭代器,auto最终会被编译器转化成迭代器
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值