【C++】STL | string 详解及重要函数的实现

目录

前言

总代码

string结构框架搭建

三个成员

构造

析构

拷贝构造、赋值重载 和 swap

size、c_str、operator[ ]

string迭代器的简单实现

扩容 reserve

insert(插入字符和字符串)

单字符

字符串

push_back、append、+=

erase 删除

find查找

运算符 >、<、>=、<=、==、!=

substr(重点)

operator<<

operator>>

结语


前言

string的实现较为简单,属于奖励课的知识点,但是string的知识点比较冗余,没有后面STL的其他结构那样简洁,很多是日常中不会用到的,所以我们今天就实现一些日常中特别常见且重要的

如果有需求要看所有的函数的话,可以上C++官方的网站上去学习  网址如下:

https://legacy.cplusplus.com/reference/string/string/?kw=string

总代码

如果有仅为复习需要的友友,可以直接点开下方gitee链接,里面是该博客的string实现

C++ string实现 gitee

string结构框架搭建

三个成员

我们先来将string的大体框架搭建一下吧

首先,string这个类我们可以拿一个命名空间包起来,以免我们实现后与库里命名的冲突

我们的string一共有三个成员组成:

  1. char* 类型的指针
  2. size(有效数据个数)
  3. capacity(空间总大小)

第一个指针用来指向string的头,size和capacity这两个可以用来分辨string是否满了,是否需要扩容,如下:

namespace hjx
{
	class string
	{
    private:
	    char* _str;
	    size_t _size;
	    size_t _capacity;
    };
}

构造

值得注意的是,构造函数在这里有一个坑:我们写函数参数的时候,需要给一个缺省值“”

该符号的意思是:因为是双引号,所以系统会认为这是一个字符串(只不过没有内容)

但是会默认往里面放一个 \0,我们需要这个 \0 因为不加的话,后面如果要插入,string会因为找不到\0而报错

另外,我们初始的对象有三个,一个指针,一个size,一个capacity,我们需要传一个char* 的参数来初步构建string,所以我们在初始化时还需要将空间开出来,这里直接new就好

但是我们要new多大的空间呢?

这时,我们可以使用strlen来计算传过来的参数有多大,但是如果三个参数的初始化都用strlen,那么代价就会略大

我们可以只对size使用strlen在初始化列表初始化,后面的两个都在函数体里复用size的大小即可

代码如下:

string(const char* str = "")
	:_size(strlen(str))
{
	_str = new char[_size + 1];
	strcpy(_str, str);
	_capacity = _size;
}

析构

析构倒没有构造那么多弯弯绕绕,我们只需要删除空间,指针置空,size和capacity都变为0即可

代码如下:

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

拷贝构造、赋值重载 和 swap

提及这两个,就不得不使用一下资本家的思想了(滑稽

对于资本家而言,要的就是榨干劳动者的所有价值,并且自己还很轻松

对于拷贝构造,我们可以先创建一个string类,拿参数做初始化,最后将两个类成员里面的指针互换即可

这种方法是很妙的,我们创建了一个类,生命周期是在该函数范围内的,出了函数就调用析构销毁了

但是创建出来的空间是在堆上的,如果不调用析构就不会销毁,我们只需要将一个临时的string构造出来,再让我们要构造的对象的指针指向这块空间,不让他调用析构即可

图示如下:

再举一个形象的例子,有两张银行卡,你和你老板各一张

你赚完了钱之后,你老板如何才能快速获得你的钱?

只需要让你的银行卡变成他的,他的没钱,但是那张卡变成你的,这样你老板就可以获得你的钱了

string(const string& s1)
{
	string tmp(s1._str);
	swap(tmp);
}

赋值重载同理

甚至,我们可以在参数部分就不使用引用,就让编译器复制一份,直接和参数的空间交换即可,如下:

string& operator=(string s1)
{
	swap(s1);
	return *this;
}

这里我们再将我们的swap实现一下

因为库里的swap默认会将空间一个一个交换,我们只需要交换指针即可,如下:

void swap(string& s1)
{
	std::swap(_str, s1._str);
	std::swap(_size, s1._size);
	std::swap(_capacity, s1._capacity);
}

另外,为了防止有人不使用对象调用该函数(不使用的话还是会调用到库里的swap)

所以我们还需要在类外面再实现一个版本的swap,如下:

void swap(string& s1, string& s2)
{
	s1.swap(s2);
}

size、c_str、operator[ ]

size:

由于我们的成员中有_size,所以我们直接返回即可,size如下:

size_t size()const
{
	return _size;
}

c_str:

c_str的效果是使用的时候我们能将整个打印出来,所以我们直接返回成员中的指针即可,如下:

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

operator[ ]:

使用这个重载的目的就是为了返回某个位置的值,所以参数就是一个整形代表位置

返回值就用char&,因为[]使用完要返回一个字符类型,如下:

char& operator[](int pos)
{
	assert(pos < _size && pos >= 0);
	return _str[pos];
}
const char& operator[](int pos)const
{
	assert(pos < _size && pos >= 0);
	return _str[pos];
}

此处实现了加const与否的两个版本

string迭代器的简单实现

由于string的本质就是一个字符数组,所以我们不需要另外实现一个类,直接使用typedef即可

由于迭代器的作用就是模拟指针,我们这里的访问直接使用原生指针恰好可以,所以我们可以直接将char*  typedef  为 iterator,const_iterator 同理

接着就是 begin 和 end,我们将这两个实现了之后,底层才会支持范围for(范围for的底层就是迭代器)

begin其实就是将指向头部的指针返回,也就是将我们成员中的指针返回即可

end就是指向尾部的下一个节点的指针,只需要让成员中的指针+size(有效个数)再返回即可

代码如下:

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;
}

扩容 reserve

我们的扩容逻辑相对简单,如果空间满了或者不够,我们就扩容

但是考虑到有一次插入一个字符的,也有一次插入一个字符串的,所以我们需要一个长度作为参数

如果这个参数的大小比我们的capacity都要大,我们就扩容,反之就不需要处理

而到了扩容逻辑里,我们需要先判断一下这个string本来的大小是否就为0

如果为0,就扩为4(这个大小随意,只不过我喜欢扩成4而已),但如果不为0,那么我们就二倍扩容

但是C++中的扩容不想C语言,这里涉及到一个迭代器失效的问题,所以C++的扩容需要自己开新空间,拷贝数据,释放久空间

代码如下:

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

insert(插入字符和字符串)

单字符

insert需要两个参数,pos位置,和要插入的数据,意味pos位置后插入数据

insert的逻辑其实不难,单字符就是将pos位置的数据全部向后移动一格,如果大小会超就扩容

在数据全都向后移动了之后,将数据插入在pos位置(因为数组下标是从0开始的,所以pos位置就是pos的下一个位置)

代码如下:

void insert(size_t pos, const char s)
{
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		reserve(newcapacity);
	}

	for (size_t i = _size + 1; i > pos; i--)
		_str[i] = _str[i - 1];
	_str[pos] = s;
	_size++;
}

这里有一个大坑就是

我们for循环的 i 需要是_size+1,因为如果是_size的话,循环条件就需要是 >= pos

但是如果 >= pos 的话,如果我的pos就等于 _size,那就不会进入循环,在后期写istream的时候会发现这样会死循环,所以还是需要注意一下的

字符串

大体逻辑和插入单字符的一样,但是这里不一样的点在于,我们需要先求出参数(字符串的大小)(假设长度为len),然后再将这个数据和size相加看看是否会超过capacity,如果会超,再扩容

剩下的就是将pos位置的数据全都往后移len个大小,再用memcpy将数据拷贝过去

void insert(size_t pos, const char* s)
{
	size_t len = strlen(s);
	if (len + _size > _capacity)
		reserve(len + _size);

	for (size_t i = _size + len; i > pos + len - 1; i--)
		_str[i] = _str[i - len];

	memcpy(_str + pos, s, len);
	_size += len;
}

push_back、append、+=

由于我们前面实现了insert,所以我们这里的这些函数都可以直接复用我们的insert

push_back就是尾插一个数据

append就是尾插一个字符串

+=就是某位置插入一个字符或一个字符串

+=单加一个字符其实效果就是push_back,加一段字符串就是append,都可以相互复用

代码如下:

void push_back(const char s)
{
	insert(_size, s);
}

void append(const char* s)
{
	insert(_size, s);
}

string& operator+=(const char s)
{
	push_back(s);
	return *this;
}

string& operator+=(const char* s)
{
	append(s);
	return *this;
}

erase 删除

删除的主逻辑就是,用后面的数据覆盖掉要删除的部分,再在尾部放上一个 \0(\0 为字符串结尾,后面即使有字符也不会查看到)

但是我们此时还需要使用for循环折磨自己吗(滑稽)

我们在这里可以直接使用 strcpy !!!

因为strcpy会将后面的所有数据都进行拷贝到前面,而memcpy则是指定大小

所以我们可以直接用strcpy将后面的数据拷贝到前面

图示如下:

代码如下:

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

ps:代码中的 len > _size - 1 - pos 代表的是包括pos位置在内的数据都要删除,而下文中有一个substr,那里的是不包括pos位置

find查找

查找单字符的话,我们就直接用一个for循环,从到到尾找一遍,这没什么好说的,代码如下:

size_t find(char s, size_t pos)
{
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == s)
		{
			return i;
		}
	}

	return npos;
}

这里返回的npos指代的是整形最大值,意味你这字符串肯定没有这么长

查找字符串的话,我们可以直接使用库里面的strstr函数,我们就不需要自己写了(但其实我们自己写的话使用一个双指针,或者叫滑动窗口就解决了,本质就是整个string找一遍而已,并不是特别重点的知识)

strstr函数是找到字符数组中的相同的字符串,并将相同部分的起始位置返回

但由于库里面的find返回值是一个size_t,所以我们需要将strstr返回的结果(一个指针),与起始位置的指针相减得出的结构返回,意味从起始位置到要找到部分之间隔了多远

代码如下:

size_t find(char* s, size_t pos)
{
	char* p = strstr(_str + pos, s);
	return p - _str;
}

运算符 >、<、>=、<=、==、!=

 这些部分其实就是将 < 和 ==实现出来,然后 != 就是 == 的结果取反,>= 就是 < 的结果取反,其他都类似,由于较为简单这里就不作过多讲解,本人前面也有写过一篇有详细讲解过该内容的博客,如果有需要的可以浏览一下,链接如下:

C++ | 日期类详解

代码如下:

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

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

bool operator<=(const string& s) const
{
	return *this < s || *this == s;
}

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

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

bool operator!=(const string& s) const
{
	return !(*this == s);
}

substr(重点)

substr的主要作用就是:将string里面的一部分内容单独抽出来构造成一个string

返回值也是一个string,参数就是从哪个位置开始(pos),往后多长一段距离的字符串(len)

而我们可以先做一个小判断,如果substr后面的长度比_size - pos(意味不包括pos位置)要长,就意味着pos位置后面的所有数据都要拿来构造成一个新的string并返回,如果不是的话,就走else的情况

如果是走else了,那就只能乖乖的开空间,拷贝数据,注意 \0 不能忘,调整新string的成员size

最后返回(注意,这里使用的拷贝数据的函数是memcpy,因为比起strcpy他有一个长度的参数,可以控制想要拷贝多长的数据)

代码如下:

string substr(size_t pos, size_t len = npos)
{
	if (len > _size - pos)
	{
		string tmp(_str + pos);
		return tmp;
	}
	else
	{
		string tmp;
		tmp.reserve(len + 1);
		memcpy(tmp._str, _str + pos, len);
		tmp._str[len + 1] = '\0';
		tmp._size = len;
		return tmp;
	}
}

operator<<

试想一下,我们打印string类的时候,一般情况下还是会用范围for,但是能不能这样呢:

string s1("hello world");
cout << s1 << endl;
hello world //结果

如果要实现这种效果的话,我们就需要用到我们的ostream

首先我们这个函数是不能在string类里面实现的

因为我们在类里面实现的话,类里面的函数都会默认有一个隐含的this指针,这个this指针会将第一个参数的位置给占掉

但是我们需要的是 <<s1 而不是 s1<<

所以我们只能在类外面实现

而我们这个的大体逻辑就是让一个ostream类型的参数不断地使用<<,里面其实主要的逻辑就是一层for循环,代码如下:

ostream& operator<<(ostream& out, const string& s1)
{
	for (int i = 0; i < s1.size(); i++)
		out << s1[i];
	return out;
}

注意,这里面的返回值是由于我们现实中可能会出现这种情况:cout << s1 << s2 << s3

如果是这种情况的话,我们在调用完后还需要返回一个ostream类型的对象,这样下一个才能打印

operator>>

和operator<<相反,这个代表的作用是cin

而我们这个重载的主要逻辑就是一个一个输入(用istream参数的函数)

当遇到输入的是空格或者换行的时候,就停止

和上面一样的是,我们也需要一个返回值以防止出现 cin >> s1 >> s2 >> s3 这种情况

代码如下:

istream& operator>>(istream& in, string& s1)
{
	s1.clean();
	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s1 += ch;
		ch = in.get();
	}
	return in;
}

结语

这篇文章到这里,就结束啦 ╰(*°▽°*)╯

如果觉得对你有帮助的话,希望可以多多支持作者喔(〃 ̄︶ ̄)人( ̄︶ ̄〃)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值