c++基础篇STLstring类的介绍及其模拟实现

Posted 东条希尔薇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++基础篇STLstring类的介绍及其模拟实现相关的知识,希望对你有一定的参考价值。

作者介绍:

关于作者:东条希尔薇,一名喜欢编程的在校大学生
主攻方向:c++和linux
码云主页点我
本系列仓库直通车
作者CSDN主页地址

从今天开始为大家介绍一下STL的相关使用及其模拟实现

笔者认为,STL是c++的灵魂,有了STL和泛型编程,让c++编程更加的方便与实用。最重要的一点就是我们不用像c语言那样自己实现一遍数据结构和算法了

而这个系列,我将采用**STL标准函数使用后面紧跟模拟实现的排版策略,**让大家的理解更加深刻

目录

STL简介

STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架

其中包括六大组件:

  • 算法
  • 容器
  • 迭代器
  • 空间适配器
  • 仿函数
  • 配接器

而我们这个系列,实现的是各种各样的容器

网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。

string类简介

c语言中我们其实没有真正的字符串类型,只有以’\\0’结尾的一些字符的集合,而为了操作方便,c语言中提供了一些字符串函数

但c语言的字符串函数使用起来相当麻烦,所以我们c++STL中实现了string类

string是c++中表示字符串的容器,其底层的字符串表示方式仍然是以’\\0’表示的字符串集合但是提供了比c语言更多的接口,使用起来也比c++方便的多

在使用string类时,必须包含string头文件以及using namespace std

string成员中包含了以下几个成员:

  • 字符串数组
  • 字符串的有效字符大小
  • 字符串容量
  • 迭代器

对以上点做出以下解释:

迭代器提供了遍历整个字符串的方式,虽然string中的遍历没有迭代器会更加方便。但因为与STL中其它难以遍历的容器统一,所以string中并没有舍弃迭代器

容量指的是类为我们的字符串数组开辟的空间,而有效字符个数指的是我们已经使用已经存储了元素的容量大小

有效字符和容量都不包括’\\0’

所以,我们模拟实现的字符串类型中,有以下成员:

char* _str;//字符串类型
size_t size;//字符串有效字符大小
size_t capacity;//字符串容量

string的构造与析构

库函数中的构造

库函数构造主要有默认构造含参构造拷贝构造

默认构造是构造出一个空字符串

string s1;
//构造出一个空字符串

含参构造中我们可以传入c语言中的字符串指针或字符串数组类型

string s2("hello world!");
//构造出"hello world!字符串"

也可以在string中构造n个相同的字符c

//string(size_t n,char c)
string s3(10,'a');
//第一个参数是个数n,第二个参数是字符c
//构造出的字符串:aaaaaaaaaa

拷贝构造使用某个其它stirng类的引用

string s4(s1);//拷贝一份s1到s4中

构造的模拟实现

默认和含参构造都比较简单,参数类型就是c语言中的const char*,缺省值给空字符串

先利用strlen算出参数的长度,size和capacity理应都等于这个大小

我们需要动态开辟空间来存放str,为了存下’\\0’,开辟的数组大小要+1

最后利用c语言字符串拷贝函数将内容放在类成员str中

string(const char* s = "")
	:_size(strlen(s)), _capacity(strlen(s))

	_str = new char[_capacity + 1];
	strcpy(_str, s);

另外一个含参构造也能轻松写出来,记得最后在末尾加上’\\0’

string(size_t n, char c)
	:_size(n), _capacity(n)

	_str = new char[_capacity + 1];
	memset(_str, c, _size);
	_str[_size] = '\\0';

拷贝构造模拟实现

重点:深浅拷贝

我们知道,拷贝构造每个类都会默认给一个,如果我们使用类默认的拷贝构造,不自己实现,会发生什么事情呢?

崩溃了?我们通过调试,发现是在析构的时候出了问题

我们再来观察一个s1,s2两个变量的详细信息

确实是成功地拷贝了,但是他们两个字符串指向的地址居然是一样的!

而我们最后在动态释放空间的时候,会因为将同一块空间释放两次而报错

原因是我们内置的拷贝构造是按字节序的简单拷贝,是浅拷贝

浅拷贝它只把指针内容进行了拷贝,而没有把指针指向的资源进行拷贝

所以我们为了解决这个问题,需要对其进行深拷贝拷贝时另外为拷贝对象开一块空间,再把内容拷贝在我们新开的空间内

这样再释放时,就不会相互影响了

string(const string& s)
	:_size(strlen(s._str)), _capacity(strlen(s._str))

	_str = new char[_capacity];//另外为拷贝对象开空间
	strcpy(_str, s._str);//再拷贝内容

我们在拷贝构造时,其实没必要自己老老实实地进行深拷贝,可以复用我们的构造函数

string(const string& s)
	:_str(nullptr),_size(0),_capacity(0)

	string tmp(s._str);
			
	swap(tmp);


我们构造字符串时,也开辟了空间,所以这也算深拷贝。最后的交换操作是把我们构造好的对象交给this

注意:这里的_str必须初始化为nullptr,不然tmp在释放时会因为释放随机空间而报错

拷贝构造之赋值操作

赋值构造我们也需要使用深拷贝另外开一块内容相同的空间给我们的赋值对象

我们需要删除原来字符串中的内容,再把我们新的字符串赋值给我们的赋值对象

string& operator=(const string& s)

	if (this != &s)//避免出现s1=s1的赋值情况
	
		delete[]_str;//删除原来的字符串
		char* tmp = new char[s._capacity];//深度拷贝操作
		strcpy(tmp, s._str);
		_str = tmp;
		_size = s._size;
		_capacity = s._capacity;
	
	return *this;

我们这个赋值操作,也可以复用构造函数

string& operator=(const string& s)

	if (this != &s)
	
		string tmp(s._str);
		swap(tmp);	
			
	
	return *this;

这个赋值操作我们还重载了利用c语言字符串赋值

string& operator=(const char* s)

	string tmp(s);
	swap(tmp);
	return *this;

析构函数的实现

非常简单,释放字符串空间,把信息全部置为0即可

~string()

			
	delete[]_str;
	_str = nullptr;
	_size = _capacity = 0;

迭代器操作

string提供了三种迭代器:

  • iterator普通迭代器,支持对字符串的遍历
  • reverse_iterator反向迭代器,支持对字符串的反向遍历
  • const_iterator常量迭代器,该迭代器只支持读而不支持修改

迭代器还指定了几个位置

  • begin()指向字符串的开始位置
  • end()指向字符串最后一个字符的后一个位置(也就是’\\0’)
  • rend(),指向字符串的开始位置的前一个位置
  • rbegin(),指向字符串最后一个字符

注意:后面两个位置仅反向迭代器才能使用

迭代器的遍历是左闭后开区间,所以要end()要指向最后一个字符的下一个位置

迭代器我们暂时可以把它理解成为指针,可以指向字符串的任意一个字符

遍历方式

string s1("hello world");
string::iterator it=s1.begin();
while(it!=s1.end())

	cout<<*it<<" ";
	it++;

cout<<endl;

迭代器的实现

其它容易的迭代器可能比较复杂,但是string迭代器十分简单,它就是一个char*类型的指针的重命名

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;

注意:函数后面的const修饰的是this不可修改

迭代器容量操作

库里面有两个容量函数:resize和reserve

reserve可以修改字符串所指空间的大小

其中只能输入比原空间大的数,如输入比原空间小的数,编译器将不做任何处理

//reserve(size_t n)
string s1;
s1.reserve(100);//将字符串空间增加到100

resize不仅在容量较小时开辟新空间,而且还会对新增的空间进行初始化,默认初始化为’\\0’

在比原size小时,会清空掉多余的字符

//resize(size_t n,char c)
s1.resize(100,'n');//将s1有效字符数增加到100并把新空间初始化为字符n

容量操作的模拟实现

reserve在底层开新空间时,先另外开一块满足大小的空间,然后把原来空间的数据拷贝到新空间

void reserve(size_t newCapacity)

	//if确保参数满足要求再进行操作
	if (newCapacity > _capacity)
	
		char* tmp = new char[newCapacity + 1];//记得+1
		strcpy(tmp, _str);
		//上面是开辟新空间
		//下面是删除原空间,并复制属性信息
		delete[]_str;
		_str = tmp;
		_capacity = newCapacity;
	

resize先检查容量够不够,不够就复用reserve函数

如果容量够,但参数大于原来的size,就设置ch信息

如果不够,直接把新size设为\\0标识字符串末尾

void resize(size_t newSize, char ch = '\\0')

	if (newSize > _size)
	
		if (newSize > _capacity)
		
			reserve(newSize);
					
		

		memset(_str + _size, ch, newSize - _size);
	

	_size = newSize;
	_str[_size] = '\\0';


字符串插入函数

插入分为尾部插入单个字符(push_back),尾部插入字符串(append),和任意位置插入(insert)

尾部插入单个字符我们用push_back

string s1;
s1.push_back('a');
s1.push_back('b');
s1.push_back('c');
s1.push_back('e');
s1.push_back('f');

尾部插入字符串我们使用append

string s2;
s2.append("hello");
s2.append("world!");

任意位置插入我们使用insert
第一个参数是插入位置,第二个参数是需要插入的字符串

//insert(size_t pos,const char* s)
string s3="hello";
s3.insert(1,"world!");

insert由于效率较低,在string类中不常用

插入函数的实现

尾部插入

push_back函数和顺序表的尾插一样,开始需要检查容量是否足够,不够则增容

直接在_size处插入即可,记得最后要加上’\\0’

void push_back(char ch)

	if (_size == _capacity)
	
		int newCapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newCapacity);
	

	_str[_size++] = ch;
	_str[_size] = '\\0';


append与push_back基本一样,只不过检查容量时需要检查当前字符串长度加上插入字符串长度够不够

void append(const char* s)

	int len = strlen(s);

	if (_size + len >= _capacity)
	
		reserve(_size + len);
	

	strcpy(_str + _size, s);
	_size += len;

append也可以重载一个string类型

void append(const string& s)

	if (_size + s._size >= _capacity)
	
		reserve(_size + s._size);
	

	strcpy(_str + _size, s._str);
	_size += s._size;

运算符+=的重载

+=的使用非常符合人们的使用习惯,所以string中重载了这个运算符

这个运算符直接在尾部进行插入操作,相当于是对push_back和append的重载

string& operator+=(char ch)

	push_back(ch);
	return *this;

string& operator+=(const char* s)

	append(s);
	return *this;


string& operator+=(const string& s)

	append(s);
	return *this;

任意位置插入

这个需要检查原来字符串长度加上新字符串长度是否符合要求

挪动时要记得为新字符串留下足够的空间

string& insert(size_t pos, const char* s)

	assert(pos <= _size);
	int len = strlen(s);

	if (_size + len >= _capacity)
	
		reserve(_size + len);
	

	size_t end = _size + len;
	while (end > pos)
	
		_str[end] = _str[end - len];
		end--;
	

	strncpy(_str + pos, s ,len);
	_size += len;
	return *this;

字符串的删除

有两个参数,第一个是删除位置,第二个是删除长度,缺省值代表将后面的字符串全部删除

删除分为两种情况,删除长度到达字符串尾和没有到达尾部

如果到达尾部,在pos位置加上\\0即可

没有到达,则需要挪动数据

		string& erase(size_t pos = 0, size_t len = -1)
		
			assert(pos <= _size);
			if (len == -1 || pos + len >= _size)
			
				_str[pos] = '\\0';
				_size = pos;
			

			else
			
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			
			return *this;
		

查找函数

string库中有查找子串的函数,第一个参数是查找的子串,第二个参数数查找开始位置

find(string substr,size_t pos)

默认从字符串开始位置查找,返回子串的开始位置

直接用strstr模拟即可

size_t find(const char* s, size_t pos = 0)const

	const char* ans = strstr(_str + pos, s);
	if (ans)
	
		return ans - _str;
	
	return -1;

访问运算符重载[]

string中支持利用[]像字符数组一样访问字符串

char& operator[](size_t index)

	assert(index >= 0 && index < _size);//此为防止越界
	return _str[index];


const char& operator[](size_t index)const

	assert(index >= 0 && index < _size);
	return _str[index];

输入输出运算符重载<<和>>

为了实现链式编程,每次<<调用都要返回cout或cin,可以实现多次调用

注意:由于类里面的成员函数第一个参数默认是this,所以需要在类外实现它们

(如果在类内实现,就只能实现成s1<<cout,及其不符合规范)

ostream& operator<<(ostream& _cout, const my::string& s)

	for (int i = 0; i < s.size(); i++)
	
		_cout << s[i];
	
	return _cout;


istream& operator>>(istream& _cin, my::string& s)

	s.clear();
	char ch = _cin.get();
	while (ch != ' ' && ch != '\\n')
	
		s += ch;
		ch = _cin.get();
	
	return _cin;

其它函数

这些函数非常简单,所以只列举出它的功能和模拟实现

		bool operator<(const string& s)
		
			return strcmp(this->_str, s._str) < 0;
		

		bool operator>=(const string& s)
		
			return !(*this < s);
		

		bool operator>(const string& s)
		
			return strcmp(this->_str, s._str) > 0;
		

		bool operator<=(const string& s)
		
			return !(*this > s);
		

		bool operator==(const string& s)
		
			return strcmp(this->_str, s._str) == 0;
		

		bool operator!=(const string& s)
		
			return !(*this == s);
		
		//以上是比较运算符重载

		//其它操作
		const char* c_str()const//返回char*字符串
		
			return _str;
		

		size_t size()const//返回有效字符个数
		
			return _size;
		

		size_t capacity()const//返回容量
		
			return _capacity;
		

		void swap(string& s)//交换两个字符串类
		
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap<

以上是关于c++基础篇STLstring类的介绍及其模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

c++基础篇STLstring类的介绍及其模拟实现

c++基础篇STLvector类的介绍及其模拟实现

c++基础篇STLvector类的介绍及其模拟实现

c++基础篇STLvector类的介绍及其模拟实现

[ C++ ] string类常见接口及其模拟实现

手撕STLstring类