C++ —— 模拟实现string类

目录

1.类的声明

2.成员变量的确定

3.构造函数与析构函数

4.赋值运算符重载

5.流插入运算符重载

6.reserve()的实现

7.push_back()的实现

8.append()的实现

9.+=运算符重载

10.[]运算符重载

11.迭代器的实现

12.insert()的实现

13.erase()的实现

14.find()的实现

15.拷贝构造与赋值运算符重载的现代写法

16.完整代码

1.类的声明

与C语言模拟实现<string.h>中的库函数不同,C++中有命名空间的存在,我们只需把我们的代码封到自定义的命名空间即可。为了方便,成员函数的声明和定义不分离,统一放在类内中。

那么我们的代码可以这么写:

//string.h 头文件
namespace lzw		//使用命名空间与标准库隔离
{
	class string
	{
	public:
		//成员函数
	private:
		//成员变量
	};
}

2.成员变量的确定

我们知道string类是其原生模板被char类型实例化的结果,也就是说我们需要char类型的数组或者指针(指向堆的某块空间)用来存储字符串,像顺序表一样,我们需要可以表示有效字符个数的变量以及表示当前除'\0'以外的容量的变量。

那么成员变量就确定了:

		//成员变量
		char* _str;		//指向堆的某块空间的指针
		size_t _size;		//表有效字符的个数
		size_t _capacity;		//除'\0'外的可用容量

3.构造函数与析构函数

我们在使用标准库的string类时,最常用的初始化的方式有三种:不使用任何参数构造、使用字符串构造、使用对象构造

那么构造函数我们便可以这样实现:

        string(const char* str = "")
	    {
			_size = _capacity = strlen(str);
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		string(const string& s)		//使用对象初始化就变成拷贝构造了
		{
			_size = _capacity = s._size;
			_str = new char[_capacity + 1];		//一定是深拷贝
			strcpy(_str, s._str);
		}

第一个构造函数我们使用了缺省参数,也就是给了一个空字符串。空字符串实际上并不为空,还有一个隐藏的'\0',后来用 strcpy 拷贝至我们自己设计的成员变量指向的空间中(包括'\0'一起拷贝),所以我们看似没有给定'\0'实际上我们已经悄悄的给了。

那么析构函数的设计就是与构造函数的逻辑相反了:

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

4.赋值运算符重载

虽然我们可以使用 = 来给给stirng对象赋值一个字符串,这是因为隐式类型转换的缘故,如果构造函数前面加上了 explicit 关键字的话,隐式类型转换就不能使用了。

即使我们可以使用隐式类型转换来给string对象赋值一个字符串,但我们依然要提供一个 =运算符重载来让对象与对象之间赋值。我么不写不依然可以使用赋值运算符重载吗?记住了,我们不显示定义赋值运算符重载,编译器会自动生成一个进行浅拷贝的赋值运算符重载

		string& operator=(const string& s)
		{
			if (this != &s)		//确保不是自我赋值
			{
				char* tmp = new char[s._capacity + 1];		//开辟空间
				strcpy(tmp, s._str);		//拷贝字符串

				delete[] _str;		//改变_str,指向新的空间
				_str = tmp;

				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

5.流插入运算符重载

这个运算符重载就可以明显感受到C++相比C语言的优越性了,我们不需要像在C语言中输出结构体的成员时那么辛苦了,我们只需要简单的使用一个运算符重载。

		friend std::ostream& operator<<(std::ostream& out, const string& s)
		{
			out << s._str;
			return out;
		}

6.reserve()的实现

这个函数可谓是极其重要的函数,我们在模拟实现的时候许多地方会用到这个函数。原生reserve()作用是改变对象的容量大小,我们在模拟实现的时候也要实现这个功能。因为很多地方,例如push_back、insert、甚至+=运算符重载时,都会涉及到对象的元素个数变化,一旦变化容量就要随之变化,否则就会出现元素个数多于容量的尴尬场面。

首先设计这个函数是一个合理并且明智的选择:

		void reserve(size_t n)
		{
			char* tmp = new char[n + 1];		//C++没有类似realloc的函数
			strcpy(tmp, _str);		//所以统一使用异地扩容办法
			delete[] _str;
			_str = tmp;
			_capacity = n;		//注意我们的_capacity描述的是不包括'\0'的容量
		}

7.push_back()的实现

有了reserve()的支持,那么尾插字符的工作就轻松多了:

		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);;
			}
			_str[_size++] = ch;
			_str[_size] = '\0';		//对于尾插字符,我们需要手动放置'\0'
		}

现在让我们复习一下顺序表或者说学一个新东西:为什么要定义 newcapacity 这个变量?

如果我们实例对象时没有给定任何参数,那么成员变量 _capacity 的值就为0。我们的容量变化理念是呈二倍增长。如果贸然使用值为0的 _capacity 作为 reserve() 函数的实参,那么尾插的工作就无法完成。

	string s1;		//如果直接用 _capacity*2 作为 reserve() 的参数
	s1.push_back('x');		//那么尾插必定是不行的

8.append()的实现

有了尾插字符,我们也要想办法尾插字符串。实际上过程太过于简单,这里直接放代码了:

		void append(const char* str)
		{
			if (_size + strlen(str) > _capacity)
			{
				reserve(_size + strlen(str));		//开辟足够的空间
			}
			strcat(_str, str);		//巧用函数
			_size += strlen(str);		//不忘改变有效字符个数
		}

9.+=运算符重载

+= 才是最常用的武器。我们已经实现了push_back和append了,剩下的任务就是调用它们了:

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		string& operator+=(char ch)		//重载
		{
			push_back(ch);
			return *this;
		}

10.[]运算符重载

[]运算符也是常用的武器。

		char& operator[](size_t pos)		
		{
			return _str[pos];
		}
		char operator[](size_t pos) const		//需要兼顾const 修饰的对象
		{
			return _str[pos];
		}

11.迭代器的实现

我们一定要知道:迭代器的很多行为与指针类似。或者说,我们完全可以把迭代器当成指针来用。因为迭代器可以代表指向某个元素,指针也可以;迭代器可以更新到下一个元素的位置,指针也可以;迭代器可以通过解引用来找到指向的元素,指针也可以。

        typedef char* iterator;
		
        iterator begin()
        {
	        return _str;		//数组名就是首元素地址
        }
        iterator end()
        {
	        return _str + _size;		//指向最后一个元素的下一个位置,即'\0'
        }
        iterator begin() const
		{
			return _str;		
		}
		iterator end() const
		{
			return _str + _size;		
		}

12.insert()的实现

兜兜转转又到了顺序表的位置挪动,插入的类型有两个:一是字符,而是字符串。至于代码的算法,还希望读者亲自体会

string& insert(size_t pos, char ch)
		{
			if (_size == _capacity)
			{
				int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}		//检查容量是否足够

			int end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}		//数据的挪动
			_str[pos] = ch;
			_size += 1;
			return *this;
		}
		
		string& insert(size_t pos, const char* str)
		{
			if (_size + strlen(str) > _capacity)
			{
				reserve(_size + strlen(str));
			}

			int end = _size + strlen(str);
			while (end > pos)
			{
				_str[end] = _str[end - strlen(str)];
				--end;
			}
			strncpy(_str + pos, str,strlen(str));
			_size += strlen(str);
			return *this;
		}

13.erase()的实现

这个函数的功能与insert()相反。不过这个函数涉及到一个概念——npos。npos是一个无符号的数,是size_t类型的最大值。也就是 size_t 的比特位全为1,所以赋值可以直接给它赋-1(原、反、补规则)。

如果不加任何修饰地作为成员变量,虽然可以,但还有优化的空间。首先 npos 属于我们的string类,而且它的值不会改变,所以我们可以用const修饰;其次我们完全可以像函数那样把 npos 放在公共空间,也就是使用 static 修饰。不过有人又要问了,static 修饰的成员变量要在类外定义, 我懒得去定义。这里我告诉大家,C++11中,const修饰的static成员变量(只能是整型),可以直接给定缺省值

那么我们的erase就可以出来了:

        string& erase(size_t pos =0, size_t len = npos)		//如果不传参,默认清空字符串
		{
			assert(pos < _size);		//删除的起始位置必定在有效字符里
			if (len >= _size - pos)
			{
				_str[pos] = '\0';		//起始位置之后的都要删除
			}
			else
			{
				size_t end = pos + len;
				strcpy(_str + pos, _str + end);
				_size -= len;
			}
			return *this;
		}


		const static size_t npos = -1;        //成员变量

14.find()的实现

上一篇我们讲过这是个非常实用的接口。原生的string类的find()函数可以查找字符、字符串、对象并返回第一次出现的位置,如果没有就返回npos。我们也要实现这个功能。

其中的算法是比较简单的,我认为各位读者花三秒钟就能理清楚其中的思路:

        size_t find(char ch, size_t pos = 0) const		//pos是查找的起始位置
		{
		    assert(pos < _size);
			while (pos < _size)
			{
				if (_str[pos] == ch)
				{
					return pos;
				}
				pos++;
			}
			return npos;
		}
		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);
			char* ret = strstr(_str+pos, str);		//注意开始查找的位置
			if (ret == nullptr)
			{
				return npos;
			}
			else
			{
				return ret - _str;		//指针相减等于元素个数
			}
		}
		size_t find(const string& s, size_t pos = 0) const
		{
			assert(pos < _size);
			char* tmp = strstr(_str+pos, s._str);		//注意开始查找的位置
			if (tmp == nullptr)
			{
				return npos;
			}
			else
			{
				return tmp - _str;
			}
		}

15.拷贝构造与赋值运算符重载的现代写法

即使上面的实现的拷贝构造和赋值运算符能够完成任务,但是这样的写法是非常保守的,体现不出我们作为C++程序员的"功力"。所以现在对拷贝构造和赋值运算符进行整改和升级,代码非常简单,大家仔细分析即可:

		//拷贝构造的现代写法
		void swap(string& s)		//模拟实现了string类提供的swap接口
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);		//先使用一个临时对象构造
			swap(tmp);		//然后交换内容
			//this->swap(tmp);		//与上面等同的swap调用
		}
		//赋值运算符重载的现代写法
		string& operator=(string s)		//使用传值调用,s就是一个临时对象
		{
			swap(s);		//交换,这里的swap等同与拷贝构造的swap
			return *this;
		}

16.完整代码

因为string类的接口设计的实在是太多了,我们就只挑几个常用来模拟实现一下。如果大家有兴趣,大家可以参照我给的这个头文件,继续往后实现吧!

//string.h 头文件

#include <iostream>
#include <assert.h>
namespace lzw		//使用命名空间与标准库隔离
{
	class string
	{
		

	public:
		//成员函数

		typedef char* iterator;
		
		iterator begin()
		{
			return _str;		//数组名就是首元素地址
		}
		iterator end()
		{
			return _str + _size;		//指向最后一个元素的下一个位置,即'\0'
		}
		iterator begin() const
		{
			return _str;		
		}
		iterator end() const
		{
			return _str + _size;		
		}

		string(const char* str = "")
		{
			_size = _capacity = strlen(str);
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		//string(const string& s)		//使用对象初始化就变成拷贝构造了
		//{
		//	_size = _capacity = s._size;
		//	_str = new char[_capacity + 1];		//一定是深拷贝
		//	strcpy(_str, s._str);
		//}

		//拷贝构造的现代写法
		void swap(string& s)		//模拟实现了string类提供的swap接口
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		string(const string& s)
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{
			string tmp(s._str);		//先使用一个临时对象构造
			swap(tmp);		//然后交换内容
			//this->swap(tmp);		//与上面等同的swap调用
		}


		~string()
		{
			delete[] _str;
			_size = _capacity = 0;
		}
		friend std::ostream& operator<<(std::ostream& out, const string& s)
		{
			out << s._str;
			return out;
		}

		//string& operator=(const string& s)
		//{
		//	if (this != &s)		//确保不是自我赋值
		//	{
		//		char* tmp = new char[s._capacity + 1];		//开辟空间
		//		strcpy(tmp, s._str);		//拷贝字符串

		//		delete[] _str;		//改变_str,指向新的空间
		//		_str = tmp;

		//		_size = s._size;
		//		_capacity = s._capacity;
		//	}
		//	return *this;
		//}

		//赋值运算符重载的现代写法
		string& operator=(string s)		//使用传值调用,s就是一个临时对象
		{
			swap(s);		//交换,这里的swap等同与拷贝构造的swap
			return *this;
		}

		~string()
		{
			delete[] _str;
			_size = _capacity = 0;
		}
		friend std::ostream& operator<<(std::ostream& out, const string& s)
		{
			out << s._str;
			return out;
		}

		void reserve(size_t n)
		{
			char* tmp = new char[n + 1];		//C++没有类似realloc的函数
			strcpy(tmp, _str);		//所以统一使用异地扩容办法
			delete[] _str;
			_str = tmp;
			_capacity = n;		//注意我们的_capacity描述的是不包括'\0'的容量
		}

		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);;
			}
			_str[_size++] = ch;
			_str[_size] = '\0';		//对于尾插字符,我们需要手动放置'\0'
		}

		void append(const char* str)
		{
			if (_size + strlen(str) > _capacity)
			{
				reserve(_size + strlen(str));		//开辟足够的空间
			}
			strcat(_str, str);		//巧用函数
			_size += strlen(str);		//不忘改变有效字符个数
		}
		
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		string& operator+=(char ch)		//重载
		{
			push_back(ch);
			return *this;
		}

		char& operator[](size_t pos)		
		{
			return _str[pos];
		}
		char& operator[](size_t pos) const		//需要兼顾const 修饰的对象
		{
			return _str[pos];
		}

		string& insert(size_t pos, char ch)
		{
			if (_size == _capacity)
			{
				int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}		//检查容量是否足够

			int end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}		//数据的挪动
			_str[pos] = ch;
			_size += 1;
			return *this;
		}
		
		string& insert(size_t pos, const char* str)
		{
			if (_size + strlen(str) > _capacity)
			{
				reserve(_size + strlen(str));
			}

			int end = _size + strlen(str);
			while (end > pos)
			{
				_str[end] = _str[end - strlen(str)];
				--end;
			}
			strncpy(_str + pos, str,strlen(str));
			_size += strlen(str);
			return *this;
		}

		string& erase(size_t pos =0, size_t len = npos)		//如果不传参,默认清空字符串
		{
			assert(pos < _size);		//删除的起始位置必定在有效字符里
			if (len >= _size - pos)
			{
				_str[pos] = '\0';		//起始位置之后的都要删除
			}
			else
			{
				size_t end = pos + len;
				strcpy(_str + pos, _str + end);
				_size -= len;
			}
			return *this;
		}

		size_t find(char ch, size_t pos = 0) const		//pos是查找的起始位置
		{
			assert(pos < _size);
			while (pos < _size)
			{
				if (_str[pos] == ch)
				{
					return pos;
				}
				pos++;
			}
			return npos;
		}
		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);
			char* ret = strstr(_str+pos, str);		//注意开始查找的位置
			if (ret == nullptr)
			{
				return npos;
			}
			else
			{
				return ret - _str;		//指针相减等于元素个数
			}
		}
		size_t find(const string& s, size_t pos = 0) const
		{
			assert(pos < _size);
			char* tmp = strstr(_str+pos, s._str);		//注意开始查找的位置
			if (tmp == nullptr)
			{
				return npos;
			}
			else
			{
				return tmp - _str;
			}
		}

	private:
		//成员变量
		char* _str;		//指向堆的某块空间的指针
		size_t _size;		//表有效字符的个数
		size_t _capacity;		//除'\0'外的可用容量
		const static size_t npos = -1;
	};
}

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小龙向钱进

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

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

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

打赏作者

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

抵扣说明:

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

余额充值