yo!这里是STL::string类简单模拟实现

目录

前言

常见接口模拟实现

默认成员函数

1.构造函数

2.析构函数

3.拷贝构造函数

4.赋值运算符重载

迭代器

简单接口

1.size()

2.c_str()

3.clear()

操作符、运算符重载

1.操作符[]

2.运算符==

3.运算符>

扩容接口

1.reserve()

2.resize()

增删查改接口

1.push_back()

2.append()

3.运算符+=

4.insert()

5.erase()

6.find()

7.substr()

流插入&流提取

后记


前言

        我们知道,熟练使用STL是c++程序员的必备技能之一,今天我们来了解STL中的string类,即字符串类型,与c语言中的字符串类似,c++中只是对此做了封装,以及一些接口,基础的知识点这里不再赘述,全部接口功能包括但不限于默认成员函数、运算符操作符重载、reserve、push、pop、insert等等,更多接口介绍参考cplusplus.com/reference/string/string/ ,下面介绍一些常见或者主要接口的实现。

常见接口模拟实现

  • 默认成员函数

1.构造函数

        如下图可知,库中string构造函数很多,适合接受不同参数列表的初始化方式,我们实现一种参数列表全缺省的构造函数,可以满足构造基本要求:

①传c类字符串就是用此字符串给类型初始化;

②不传就是用空字符串初始化对象,

注意:初始化时_size和_capacity都是初始化为字符串的长度,而new申请空间时多申请一个用来存放'\0'。

yo!这里是STL::string类简单模拟实现_第1张图片

代码:

String(const char* str = "")
	{
		_size = strlen(str);  //初始化列表有依赖关系时建议不用初始化列表,在大括号内初始化
		_capacity = _size;
		_str = new char[_capacity + 1];

		strcpy(_str, str);
	}

2.析构函数

        析构函数在库中只有下面一种,无参数无返回值,符合特性,我们知道,此类涉及申请内存,所以在析构函数中一定要释放内存,将变量置0,代码如下:

代码:

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

3.拷贝构造函数

        我们知道,拷贝构造函数是构造函数的一种,与普通构造函数写法类型,这是一种传统写法,代码如下,但是,这里讲一种现代写法,也是一种的复用的方法,就是定义一个临时对象,调用其自己的构造函数初始化,之后将初始化的成员交换给目标对象,而临时对象会自动调用析构函数释放,完美地将目标对象初始化完成。

代码:

//现代写法(老板思维)
String(const String& s)
	:_str(nullptr)
	,_size(0)
	,_capacity(0)
{
	String tmp(s._str);
	swap(_str, tmp._str);
	swap(_size, tmp._size);
	swap(_capacity, tmp._capacity);
}

//传统写法
//String(const String& s)
//{
//	_size = s._size;
//	_capacity = _size;
//	_str = new char[_capacity + 1];
//	strcpy(_str, s._str);
//}

4.赋值运算符重载

        库中的赋值运算重载如图,这里实现第一种,传统写法就是赋值_sieze、_capacity之后开空间传元素,考虑还用一下现代写法,去定一个临时对象调用拷贝构造初始化,再将其成员交换给目标对象,目标对象完美完成其初始化。

注意:考虑自己给自己赋值的情况

代码:

	//现代写法(老板思维)
	String& operator=(const String& s)
	{
		if (&s != this)
		{
			String tmp(s);
			/*delete[] _str;
			_str = nullptr;*/  //无需在此释放,交换给tmp后,出了作用域自动调用析构清理
			swap(_str, tmp._str);
			swap(_size, tmp._size);
			swap(_capacity, tmp._capacity);
		}
		return *this;
	}

    //传统写法
	//String& operator=(const String& s)
	//{
	//	if (&s == this)   //注意:自己给自己赋值的情况
	//		return *this;
	//	_size = s._size;
	//	_capacity = _size;

	//	/*delete[] _str;  //先释放的话万一new失败了就导致释放但未开辟空间成功
	//	_str = new char[_capacity + 1];*/
	//	
	//	char* tmp = new char[_capacity + 1];  //所以先开空间,开成功了再释放原空间
	//	delete[] _str;
	//	_str = tmp;
	//	strcpy(_str, s._str);
	//	return *this;
	//}
  • 迭代器

        迭代器有四种,这里实现两种以及其const型迭代器,string的迭代器就是原生指针(其他类型不一定),所以实现很容易,代码如下。 

yo!这里是STL::string类简单模拟实现_第2张图片

代码:

	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;
	}
  • 简单接口

1.size()

        size()就是获取字符串字符个数,很简单不过多赘述,设置成const成员函数,是因为可以接受普通对象,也可以接受const对象。 

代码: 

	size_t size() const
	{
		return _size;
	}

2.c_str()

        c_str()是获取c类型字符串,有一些场合可能需要传递指针型字符串,但是其是私有成员不能直接访问,此接口可以实现访问。

代码: 

	char* c_str() const
	{
		return _str;
	}

3.clear()

        clear()是清除字符串中字符,使其成为空串。 

代码: 

	void clear()
	{
		_str[0] = '\0';
		_size = 0;
	}
  • 操作符、运算符重载

1.操作符[]

        操作符[]重载是很重要的重载,实现字符串对象可以像数组一样使用[]加下标访问,代码实现也很简单,注意普通对象和const对象权限不同,需要分开写。

代码: 

	char& operator[](size_t pos)  //普通对象,[]访问元素可读可写
	{
		assert(pos < _size);

		return _str[pos];
	}
	const char& operator[](size_t pos) const  //const对象,[]访问元素只可读
	{
		assert(pos < _size);

		return _str[pos];
	}

2.运算符==

        看过笔者上篇文章(http://t.csdn.cn/xT4nq)可以知道,先重载==、>,其他关系运算符可以复用它们方便的实现出来,不多赘述,不理解的可以翻翻看。

代码: 

    bool operator==(const String& s) const
	{
		return strcmp(_str, s._str) == 0;
	}

3.运算符>

        参考==运算符介绍。

代码: 

	bool operator>(const String& s) const
	{
		return strcmp(_str, s._str) > 0;
	}
  • 扩容接口

1.reserve()

        reverse()是预留空间,但不初始化,即只改变_capacity,传进想要预留的字符个数n,比_capacity大就会释放空间重新申请并拷贝元素。

注意:为string申请空间一定要多申请一个空间放'\0'

代码: 

	void reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}

2.resize()

        resize()也是预留空间,并且初始化,即改变_capacity和_size,不传需要初始化的字符则默认是'\0',实现过程比reserve()多考虑一个赋值情况即可。

代码: 

    void resize(size_t n, char ch = '\0')
	{
		if (n > _size)
		{
			reserve(n + 1);
			for (size_t i = _size; i < n; i++)
			{
				_str[i] = ch;
			}
			_str[n] = '\0';
			_size = n;
		}
		else
		{
			_str[n] = '\0';
			_size = n;   //只需要改变size,不需要改变capacity
		}
	}
  • 增删查改接口

1.push_back()

        push_back()是在字符串结尾插入指定字符,首先检查是否需要扩容,需要扩容则要看是不是刚初始化的空字符串,空字符串默认先给4个地址空间,不是空字符串则扩容空间至二倍。

注意:

        ①莫忘_size加一;

        ②莫忘加'\0'在结尾。        

代码: 

	void push_back(char ch)
	{
		if (_size == _capacity)
			reserve(_capacity ? _capacity * 2 : 4);
		_str[_size] = ch;
		_str[_size + 1] = '\0';
		_size++;
	}

2.append()

        append()是在字符串尾部插入一个字符串,实现逻辑很简单,可以直接复用push_back,但避免重复向操作系统申请空间,提前预留好空间用来插入字符,代码如下。

yo!这里是STL::string类简单模拟实现_第3张图片

代码: 

	void append(const char* str)
	{
		size_t i = 0;
		size_t n = strlen(str);
		if (_size + n > _capacity)
			reserve(_size + n);   //要尽量少的重复申请空间
		for (i = 0; i < n; i++)
		{
			push_back(str[i]);
		}
	}

3.运算符+=

        +=运算符重载则是在字符串尾部既可以插入字符,也可以插入字符串,调用上面俩函数即可。

代码: 

	String& operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	String& operator+=(const char* str)
	{
		append(str);
		return *this;
	}

4.insert()

        上面的插入操作只能作用在字符串尾部,而insert()函数是指定位置插入字符或者字符串,极端位置可以复用上面函数,普通位置需要移动元素,经历过c语言的学习,实现此功能并不难。

注意:代码中的标记点处是向后移动元素,注意移动过程中对下标的控制,不要越界访问。

yo!这里是STL::string类简单模拟实现_第4张图片

代码: 

	String& insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
			reserve(_capacity ? _capacity * 2 : 4);
		if (pos == _size)
			push_back(ch);
		else
		{
			size_t end = _size + 1;
			while (end > pos)   //标记点
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[pos] = ch;
			_size++;
		}
		return *this;
	}
	String& insert(size_t pos, const char* str)
	{
		size_t i = 0;
		size_t n = strlen(str);
		if (_size + n > _capacity)
			reserve(_size + n);
		if (pos == _size)
			append(str);
		else
		{
			size_t end = _size + n;
			while (end >= pos + n)   //标记点
			{
				_str[end] = _str[end - n];
				end--;
			}
			for (i = 0; i < n; i++)
			{
				_str[i + pos] = str[i];
			}
			_size += n;
		}
		return *this;
	}

5.erase()

        erase()则是删除指定位置指定长度的字符串,比插入的实现要简单,其中可以调用strcpy函数,npos是size_t类型的最大值,作为长度缺省值,实现不指定长度时默认是删除至结尾,注意删除完以后莫忘改变_size变量,其他并无难度。

代码: 

    void erase(size_t pos, size_t len = npos)
	{
		assert(pos < _size);
		if (_size - pos <= len)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			strcpy(_str + pos, _str + pos + len);
			_size -= len;
		}
	}

6.find()

        find()是查找指定字符或字符串第一次出现的位置下标,找不到返回npos,即无符号整型最大值,查找字符从头向后遍历查找即可,查找字符串调用strstr函数,注意此函数找到了是返回字符串地址,返回空是没找到,而find()是需要返回下标,注意控制。

yo!这里是STL::string类简单模拟实现_第5张图片

代码: 

	size_t find(char ch, size_t pos = 0) const
	{
		assert(pos < _size);
		size_t i = 0;
		for (i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos;
	}
	size_t find(const char* substr, size_t pos = 0) const
	{
		assert(substr);
		assert(pos < _size);
		char* ptr = strstr(_str + pos, substr);
		if (ptr == nullptr)
			return npos;
		return ptr - _str;
	}

7.substr()

        substr()是提取指定位置指定长度的子串,形成返回string类对象,复用+=运算符简化了实现逻辑,大体实现逻辑与其他函数并无不同,不多赘述。

代码: 

	String substr(size_t pos, size_t len = npos)
	{
		assert(pos < _size);
		String tmp;
		if (_size - pos <= len)
		{
			tmp += (_str + pos);
		}
		else
		{
			for (size_t i = 0; i < len; i++)
			{
				tmp += _str[pos + i];
			}
		}
		return tmp;
	}
  • 流插入&流提取

        在上一篇文章Date类型的简单实现中,使用友元的方式将流插入、流提取编写成了全局函数,因为当时是因为需要访问私有成员变量,那么这里我们发现,可以不需要访问私有成员变量,所以这里不在需要成为友元。

        对于流插入,定义一个临时数组,大小固定为32个元素的地址空间,循环从键盘获取字符插入,当临时数组满了之后,就整体插入到对象中,当获取到空格或者回车就停止,符合流插入的特性。

注意:

        ①临时数组没满时就遇到空格或回车的情况;

        ②这里使用istream类的get函数获取字符,不仅可以接收到普通字符也可以收到空格和回车,用以判断,而不直接使用cin<<,是因为cin输入过程中遇到空格或回车就停止了,接收不到它们。

代码: 

istream& operator>>(istream& in, String& s)   //无需成为友元函数,因为不需要访问私有成员
{
	s.clear();

	const size_t N = 32;
	char tmp[N];
	size_t i = 0;

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

	return in;
}

ostream& operator<<(ostream& out, const String& s)   //无需成为友元函数,因为不需要访问私有成员
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}

后记

        今天介绍了一些string常见常用接口的使用及底层实现,希望对于以上接口的使用可以熟练掌握,对于不常见的可以了解,知道有这样一个函数,在后面需要使用的时候,也可以查阅文档即时了解一下(cplusplus.com/reference/string/string/),对于以上接口的使用或者实现不懂的地方,欢迎可以在评论区评论或者私我,一起学习进步哦!


你可能感兴趣的:(c++,开发语言,职场和发展,c语言,git,后端)