C++6:STL-模拟实现string

 string时STL中的模板库之一,类似于专门处理字符串的数据结构,在模拟实现并探讨其中构造的巧妙之处之前,我们短浅的认识一下STL是什么

目录

 什么是STL

STL的诞生

关于string

string的模拟实现

构造函数和析构函数

实现简单的string打印

 拷贝构造函数

摇人打工法实现拷贝构造

 赋值操作符重载

resize和reserve

reserve

resize

 push_back

 append追加

+=操作符重载

insert

erase

find

<<和>>

<<

>>

string的迭代器


 什么是STL

 STL的全名:standard template libaray-标准模板库

STL的诞生

出自于惠普实验室的两位大牛,Alexander Stepanov,Meng Lee,奔着开源精神,两位大牛表示STL的源码可以随意传播修改乃至商业用途,但是要求必须和源码一样开源,这就是STL的始祖版本

STL的后序版本:P. J. 版本,RW版本,SGI版本,各类版本的区别以及优缺点就不做记述了,毕竟我们作为使用者不仅是学习使用,学习其范式编程的思维也是重点。


关于string

STL其实在开发过程中更像一个非常方便的工具包,相较于什么数据结构都需要自己实现的C语言来说,STL支持了不少的数据结构,只需要包含几个头文件,我们就能非常便利的使用,比方说我们接下来需要实现的string

就平常来说,我们在C语言阶段操作字符串是比较头疼的一个过程,而使用STL,则非常的方便。

比如,我希望简单的打印一个字符串,并且将其与另一个字符串链接在一块。

 使用string,不仅不需要再写一个for循环来遍历字符串打印,甚至也能利用+=的操作实现字符串之间的相加!可以说是非常方便了,不过知其然不知其所以然就没啥意思了,毕竟单纯的使用这种事的门槛非常的低,我们还是需要了解其原理才能有所收获。


string的接口记述以及趣闻分享

string作为其中的一个模板库,由于创建的时间是有一阵子了,其中包含了许多冗余的接口,当然还有各式各样有用的接口,需要查阅的话可以跳转至这个网站:https://cplusplus.com/reference/

 当然string也是有更多有趣的事情,陈皓前辈就讲了这个问题:STL 的string类怎么啦?_haoel的博客-CSDN博客

作为一个方便使用者开发的优秀代码集大成者,STL的重要性以及其他的特点我相信其他文章的作者一定写的比我更好,我也就不多说了。

string的模拟实现

基本的成员变量

class mystring
{
   public:
    private:
		char* _str;
		size_t _size;
		size_t _capacity;
}

模拟实现string类就逃不开最基本的几个成员函数了,毕竟我们需要在堆上开辟空间,所以我们要自己实现构造函数

构造函数和析构函数

 构造函数其实和顺序表差不多,但是有了new之后整个代码也变短了不少

		
	//构造函数,字符串初始化版本,求长度,算容量,开空间
	mystring( const	char* str="")
	{
		_size = strlen(str);
		_str = new char[_size + 1];//留一个给斜杠零

			
		strcpy(_str, str);//拷贝数据到新空间
		_capacity = _size;
	}

析构函数,对应格式delete掉即可

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

实现简单的string打印

我们在使用STL的sting的时候打印非常方便,string直接适配了cout,但是目前我们只需要简单的打印功能,重载流插入和流提取操作符的具体操作后面再一块说,我们先简单的实现一个。

首先,我们需要解引用整个字符串,然后打印出来就好了,由于string的本身是一个字符串数组,其本身就支持【】解引用操作

void Print()
	{
		for (int i = 0; i < _size; ++i)
		{
				cout << _str[i];
		}
	}

然后往里面塞一个字符串看看效果

 当然我们还需要支持随机访问,也就是支持str的【】解引用功能,那么很简单,我们直接重载【】操作符

重载没什么难度,不过不要忘记多实现一个const版本。

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

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

关于const版本的成员函数,我打算在这里复盘一把,为什么要实现const版本的成员函数?

const修饰成员函数 

const修饰的成员函数,其本质上修饰的并不是这个函数,而是当前这个类对象成员函数中的那个this指针

拿上文的const版本的成员函数举例,我们看到的是

const char& operator[](size_t index)const

而实际上,真实的样子是

const char& operator[](const mystring *const this  ,size_t index)

const修饰了当前的This指针,this指针本身携带一个const,本身自带的这个const是用来保护this自己的,但是并不会保护this指针所指向的对象,而const成员函数则是保护所指向对象的。

实现const版本的成员函数,可以保证const类型的类对象能成功调用成员函数。

如果没有,const类对象就用不成了。

但是const版本的成员函数也不是都要实现,只有当满足以下条件时,实现才有必要。

1.任何不会修改成员的函数都应该声明为const类型。

2.const成员函数不可以修改对象的数据。

 拷贝构造函数

 拷贝构造函数需要传引用,为了得到原生的字符串头指针来使用strcpy,我们还需要额外实现一个c_str

mystring(const mystring& str )
{
	_size = str._size;
	_str = new char[str._capacity+1];
	_capacity = str._capacity;

	strcpy(_str, str.c_str());

}

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

 这种方法比较朴实无华,我们实现一个比较”现代“的写法,摇人打工

摇人打工法实现拷贝构造

//拷贝构造现代写法,摇人打工
mystring(mystring& str)
	:_str(nullptr), _size(0), _capacity(0)
	{
		mystring tmp(str._str);
		swap(str);

	}

我们利用构造函数创建一个临时变量tmp,然后将tmp与当前的this交换,为了防止tmp析构的时候报错,我们给一个空指针到this,delete一个nullptr不会报错。

当然swap我们也要重写一个,算法库内部的swap效率较低。

 题外话:T c(a);  这个看上去是个构造函数啊,是不是内置类型就不能用了?
 

C++在产生模板之后,为了能照顾到内置类型,为内置类型也提供了类似构造函数的初始化方法。

比如:

int i(10);
int j = int();

 所以这种调用构造函数的方法对内置类型也是生效的,不必担心

		void swap(mystring& str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);

		}

 赋值操作符重载

 赋值,传入一个字符串,把当前的sting置换成传入的字符串
与构造函数类似,但是需要注意连续赋值的问题,str1 = str2 = str3
还有刁钻情况,自己给自己赋值。

mystring& operator=(const mystring& s)
{
	if (this != &s)
	{
		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);

		delete[] _str;

		_str = tmp;

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

resize和reserve

 resize和reserve这两个接口函数在控制sting的空间上还算是挺有用的,接下来就来实现一下

reserve

reserve的功能,我们查阅原码,简单来说实现的功能就是:重置容量,但是我们还是需要注意缩容的问题。也就是传入的参数大小小于当前的容量时,我们不能执行,不然就缩容了。

	void reserve(size_t n = 0)
	{
		if (n > _capacity)
		{
            //新空间,之所以是n+1则是为了斜杠零而留出的空间
			char* tmp = new char [n + 1];

            //拷贝
			strcpy(tmp, _str);

            //更新容量
			_capacity = n;
            
            //销毁原空间
			delete[]_str;
			_str = tmp;
		}
	}

resize

阅读原码的解释,resize需要传递两个参数,一个是更新size的值,另一个则是当N大于当前size值时,我们可以选择是否往空出来的那一部分填充字符。那么在这里我们直接给个缺省值‘\0’

	void resize(size_t  n, char c = '\0')
	{
		if (n > _size)
		{
			reserve(n);
			for (size_t i = _size; i < n; ++i)
			{
				_str[i] = c;
			}
			_size = n;
			_str[_size] = '\0';

		}
		else
		{
			_size = n;
			_str[_size] = '\0';
		}
	}

 push_back

 push_back 这个函数我们都非常熟悉了,不过这里面还是有一些细节需要处理

size的位置被strcpy直接覆盖掉了,我们不仅是扩容size自增,还需要在加上一个'\0'

void push_back(char c)
	{
		//尾插,如满,扩容
		if (_size == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : _capacity * 2;

			reserve(newcapacity);
		}

		_str[_size] = c;
		++_size;

		//strcpy从\0处开始覆盖,需要在后面加上\0
		_str[_size] = '\0';

	}

 append追加

 这个函数用于在当前的string后面追加一个字符串,有多个重载·版本,在这里就实现最简单的,追加一个字符串


		//字符串追加
		void append(const char* str)
		{
			size_t  size = strlen(str);

			if (_size + size > _capacity)
			{
				reserve(_capacity + size);
			}
			strcpy(_str + _size, str);

			//只是重置了容量大小,size还没有更新。
			_size += size;


		}

+=操作符重载

实现过了append,+=就非常简单了,直接复用即可

		mystring& operator+= (const mystring& str)
		{
			append(str.c_str());

			return *this;
		}

insert

string& insert(size_t pos, const char* str)
{

	assert(pos <= _size);

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

	size_t end = _size+len ;
	while (end > pos + len-1)
	{
		_str[end] = _str[end -len];
		--end;
	}
	//用strncpy是因为直接strcpy会把\0也拷过去,读的时候就直接断层了,用strncpy的话可以规避掉\0
	strncpy(_str + pos, str,len);
	_size += len;


	return *this;
}

erase

//删除
//给个npos的缺省值,当没有传递需要删除的个数的时候全部删除
//还需要额外处理,当len<size- pos的时候正常删除,如果后面还有剩余的字符串,直接挪动过去覆盖就好,没有的话就
//在POS位置放一个\0
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len < _size - pos)
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
	else if (len == npos || len >=_size + pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	return *this;
}

find

		size_t find(const char* str, size_t pos = 0) const
		{
			assert(pos < _size);

			char* ptr = strstr(_str+pos, str);

			if (ptr != nullptr)
			{
				return ptr - _str;
			}
			else
			{
				return npos;
			}

		}

<<和>>

<<

对于流插入和流提取的问题,我们有一个前置条件,这两个重载不能称为成员函数,因为this指针会抢占第一个操作符的位置让我们使用起来非常难受。

那么我们将其写成全局函数。

这里有一个小问题,我们在类和对象的阶段所接触的<<由于访问限定符的限制原因,我们将其写成了友元成员函数,那么<<的重载函数一定要是友元么?

显然不是,不能直接访问你的成员变量,我可以写一个函数间接的去访问。

	ostream& operator<<(ostream& out,const mystring& str)
	{
		for (size_t i = 0; i < str.size(); ++i)
		{
			out << str[i];
		}
		return out;
	}

返回值使用ostream做返回值的对象,用来支持连续cout等操作

>>

提取的函数也不算困难,需要注意的是缓冲区的问题

我们先简单的实现一下,利用一下istream里面的get函数来获取当前缓冲区内部的字符,get的行为很像gets,不过我们还是在C和C++之间做出区别较好。

	istream& operator>>(istream& in, mystring& str)
	{
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			str += ch;
			ch = in.get();
		}

		return in;
	}

目前的版本仅能实现拿取空格和换行符之间的字符

 当然要实现一次拿完也可以,修改条件即可

 不过库里的实现就是这样的,我们跟着库来实现。不过这里有一些效率上的问题,应对短小的字符串+=的操作还算可行,可一旦字符串变长了,效率就下来了。

那我们可以分批次处理。

	istream& operator>>(istream& in, mystring& str)
	{
		str.clear();

		char ch = in.get();
		char buff[128] = { '\0' };
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			//已满,+=数据
			if (i == 127)
			{
				str += buff;			
				//i置零
				i = 0;
			}

			buff[i++] = ch;
			ch = in.get();

		}

		//已跳出循环,遇到'\n'或' '已取完当前缓冲区分割区域内的数据,
		//由于上逻辑为满127才+=数据,那么一定会有剩余情况发生,在这里额外处理

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

		return in;
	}

string的迭代器

 我们知道迭代器的使用方法类似指针,那么我们可以简单的实现一下。

	//迭代器:
	
	typedef char* iterator;

	iterator begin()
	{
		return _str;
	}
		
	iterator end()
	{
		return _str + _size;
	}

 


到这,一个具有基本功能的stirng就模拟实现完成了

感谢阅读,希望对你有点帮助

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值