【C++初阶】C++STL详解(二)—— string类的模拟实现

news2024/11/26 22:29:30

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:C++初阶
🎯长路漫漫浩浩,万事皆有期待

上一篇博客:【C++初阶】C++STL详解(一)—— string类

文章目录

  • string各函数接口总览
  • 默认成员函数
    • 构造函数
    • 拷贝构造函数
    • 赋值运算符重载函数
    • 析构函数
  • 迭代器相关函数
    • begin和end
  • 容量和大小相关函数
    • size和capacity
    • reserve和resize
    • empty
  • 修改字符串相关函数
    • push_back
    • append
    • operator+=
    • insert
    • erase
    • clear
    • swap
    • c_str
  • 访问字符串相关函数
    • operator[ ]
    • find和rfind
  • 关系运算符重载函数
  • >>和<<运算符的重载以及getline函数
    • >>运算符的重载
    • <<运算符的重载
    • getline
    • substr
  • vs下string的特殊处理
  • 浅拷贝问题的解决方案
  • 总结:

string各函数接口总览

namespace sherry
{
	//模拟实现string类
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		//默认成员函数
		string(const char* str = "");         //构造函数
		string(const string& s);              //拷贝构造函数
		string& operator=(const string& s);   //赋值运算符重载函数
		~string();                            //析构函数

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;

		//容量和大小相关函数
		size_t size();
		size_t capacity();
		void reserve(size_t n);
		void resize(size_t n, char ch = '\0');
		bool empty()const;

		//修改字符串相关函数
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string& insert(size_t pos, char ch);
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos, size_t len);
		void clear();
		void swap(string& s);
		const char* c_str()const;

		//访问字符串相关函数
		char& operator[](size_t i);
		const char& operator[](size_t i)const;
		size_t find(char ch, size_t pos = 0)const;
		size_t find(const char* str, size_t pos = 0)const;
		size_t rfind(char ch, size_t pos = npos)const;
		size_t rfind(const char* str, size_t pos = 0)const;

		//关系运算符重载函数
		bool operator>(const string& s)const;
		bool operator>=(const string& s)const;
		bool operator<(const string& s)const;
		bool operator<=(const string& s)const;
		bool operator==(const string& s)const;
		bool operator!=(const string& s)const;

	private:
		// < 16字符串存在buff数组中
		//>= 16存在)_str指向堆空间上
		// char _buff[16];
		char* _str;       //存储字符串
		size_t _size;     //记录字符串当前的有效长度
		size_t _capacity; //记录字符串当前的容量
		
		static const size_t npos; //静态成员变量(整型最大值)
	};
	const size_t string::npos = -1;

	//<<和>>运算符重载函数
	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
	istream& getline(istream& in, string& s);
}

:为了防止与标准库当中的string类产生命名冲突,模拟实现时需放在自己的命名空间当中。

默认成员函数

构造函数

构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入字符串的长度(不包括’\0’)

//构造函数
string(const char* str = "")
{
	_size = strlen(str); //初始时,字符串大小设置为字符串长度
	_capacity = _size; //初始时,字符串容量设置为字符串长度
	_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
	strcpy(_str, str); //将字符串拷贝到已开好的空间
}

直接构造;

string str;

传字符串构造;

string str("hello world");

对于传字符串构造的情况,我们可以实现用const char* str 来接收它;
对于直接构造的情况,我们可以用缺省值来解决;

//构造函数
string(const char* str = "")  //使用缺省值
	:_size(strlen(str)) //初始时,字符串大小设置为字符串长度
{
	_capacity = _size == 0 ? 3 : _size;  //_capacity初始值不能为0
	_str = new char[_capacity + 1];//为存储字符串开辟空间(多开一个用于存放'\0')
	strcpy(_str, str); //将字符串拷贝到已开好的空间
}

拷贝构造函数

在模拟实现拷贝构造函数前,我们应该首先了解深浅拷贝:
在这里插入图片描述

浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。

很明显,我们并不希望拷贝出来的两个对象之间存在相互影响,因此,我们这里需要用到深拷贝。下面提供深拷贝的两种写法:

写法一:传统写法
先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的。

//传统写法
string(const string& s)
	:_str(new char[strlen(s._str) + 1]) //_str申请一块刚好可以容纳s._str的空间
	, _size(0)
	, _capacity(0)
{
	strcpy(_str, s._str);    //将s._str拷贝一份到_str
	_size = s._size;         //_size赋值
	_capacity = s._capacity; //_capacity赋值
}

写法二:现代写法

现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。
在这里插入图片描述

//现代写法
string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象
	swap(tmp); //交换这两个对象
	//this->swap(tmp):
}
void swap(string &tmp)
{
	::swap(_str,tmp.str);
	::swap(_size,tmp.size):
	::swap(_capacity,tmp.capacity):
}

:swap成员函数的模拟实现在文章的后面。

赋值运算符重载函数

与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。下面也提供深拷贝的两种写法:

写法一:传统写法
赋值运算符重载函数的传统写法与拷贝构造函数的传统写法几乎相同,只是左值的_str在开辟新空间之前需要先将原来的空间释放掉,并且在进行操作之前还需判断是否是自己给自己赋值,若是自己给自己赋值,则无需进行任何操作。否则,自己把自己释放了

//传统写法
string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		delete[] _str; //将原来_str指向的空间释放
		_str = new char[strlen(s._str) + 1]; //重新申请一块刚好可以容纳s._str的空间
		
		strcpy(_str, s._str);    //将s._str拷贝一份到_str
		_size = s._size;         //_size赋值
		_capacity = s._capacity; //_capacity赋值
	}
	return *this; //返回左值(支持连续赋值)
}

写法二:现代写法
赋值运算符重载函数的现代写法与拷贝构造函数的现代写法也是非常类似,但拷贝构造函数的现代写法是通过代码语句调用构造函数构造出一个对象,然后将该对象与拷贝对象交换;而赋值运算符重载函数的现代写法是通过采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。

//现代写法1
string& operator=(string s) //编译器接收右值的时候自动调用拷贝构造函数
{
	swap(s); //交换这两个对象
	return *this; //返回左值(支持连续赋值)
}

但这种写法无法避免自己给自己赋值,就算是自己给自己赋值这些操作也会进行,虽然操作之后对象中_str指向的字符串的内容不变,但是字符串存储的地址发生了改变,为了避免这种操作我们可以采用下面这种写法:
在这里插入图片描述

//现代写法2
string& operator=(const string& s)
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s); //用s拷贝构造出对象tmp
		swap(tmp); //交换这两个对象
	}
	return *this; //返回左值(支持连续赋值)
}

但实际中很少出现自己给自己赋值的情况,所以采用“现代写法1”就行了。

析构函数

string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。

//析构函数
~string()
{
	delete[] _str;  //释放_str指向的空间
	_str = nullptr; //及时置空,防止非法访问
	_size = 0;      //大小置0
	_capacity = 0;  //容量置0
}

迭代器相关函数

string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。

typedef char* iterator;
typedef const char* const_iterator;

:不是所有的迭代器都是指针。

begin和end

string类中的begin和end函数的实现简单的可怕,begin函数的作用就是返回字符串中第一个字符的地址:

iterator begin()
{
	return _str; //返回字符串中第一个字符的地址
}
const_iterator begin()const
{
	return _str; //返回字符串中第一个字符的const地址
}

end函数的作用就是返回字符串中最后一个字符的后一个字符的地址(即’\0’的地址):

iterator end()
{
	return _str + _size; //返回字符串中最后一个字符的后一个字符的地址
}
const_iterator end()const
{
	return _str + _size; //返回字符串中最后一个字符的后一个字符的const地址
}

在明白了string类中迭代器的底层实现,再来看看我们用迭代器遍历string的代码,其实就是用指针在遍历字符串而已。

string s("hello world!!!");
string::iterator it = s.begin();
while (it != s.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;

在string的介绍中我们还说到,可以用范围for来遍历string,可能很多初学者都会觉得范围for是个很神奇的东西,只需要一点点代码就能实现string的遍历。
实际上范围for并不神奇,因为在代码编译的时候,编译器会自动将范围for替换为迭代器的形式,也就是说范围for是由迭代器支持的,现在我们已经实现了string类的迭代器,自然也能用范围for对string进行遍历:此时e=*it

string s("hello world!!!");
//编译器将其替换为迭代器形式 
for (auto e : s)
{
	cout << e << " ";
}
cout << endl;

容量和大小相关函数

size和capacity

因为string类的成员变量是私有的,我们并不能直接对其进行访问,所以string类设置了size和capacity这两个成员函数,用于获取string对象的大小和容量。
size函数用于获取字符串当前的有效长度(不包括’\0’)。

//大小
size_t size()const
{
	return _size; //返回字符串当前的有效长度
}

capacity函数用于获取字符串当前的容量。

//容量
size_t capacity()const
{
	return _capacity; //返回字符串当前的容量
}

reserve和resize

reserve和resize这两个函数的执行规则一定要区分清楚。
reserve规则:
 1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
 2、当n小于对象当前的capacity时,什么也不做。

//改变容量,大小不变
void reserve(size_t n)
{
	if (n > _capacity) //当n大于对象当前容量时才需执行操作
	{
		char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'
		strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
		delete[] _str; //释放对象原本的空间
		_str = tmp; //将新开辟的空间交给_str
		_capacity = n; //容量跟着改变
	}
}

注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。

resize规则:
 1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
 2、当n小于当前的size时,将size缩小到n。

//改变大小
void resize(size_t n, char ch = '\0')
{
	if (n <= _size) //n小于当前size
	{
		_size = n; //将size调整为n
		_str[_size] = '\0'; //在size个字符后放上'\0'
	}
	else //n大于当前的size
	{
		if (n > _capacity) //判断是否需要扩容
		{
			reserve(n); //扩容
		}
		for (size_t i = _size; i < n; i++) //将size扩大到n,扩大的字符为ch
		{
			_str[i] = ch;
		}
		_size = n; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0'
	}
}

empty

empty是string的判空函数,我们可以调用strcmp函数来实现,strcmp函数是用于比较两个字符串大小的函数,当两个字符串相等时返回0。

//判空
bool empty()
{
	return strcmp(_str, "") == 0;
}

注意:两个字符串相比较千万不能用 == 。

修改字符串相关函数

push_back

push_back函数的作用就是在当前字符串的后面尾插上一个字符,尾插之前首先需要判断是否需要增容,若需要,则调用reserve函数进行增容,然后再尾插字符,注意尾插完字符后需要在该字符的后方设置上’\0’,否则打印字符串的时候会出现非法访问,因为尾插的字符后方不一定就是’\0’。

//尾插字符
void push_back(char ch)
{
	if (_size == _capacity) //判断是否需要增容
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
	}
	_str[_size] = ch; //将字符尾插到字符串
	_str[_size + 1] = '\0'; //字符串后面放上'\0'
	_size++; //字符串的大小加一
}

:增容时以二倍的形式进行增容,避免多次调用push_back函数时每次都需要调用reserve函数。

实现push_back还可以直接复用下面即将实现的insert函数

//尾插字符
void push_back(char ch)
{
	insert(_size, ch); //在字符串末尾插入字符ch
}

append

append函数的作用是在当前字符串的后面尾插一个字符串,尾插前需要判断当前字符串的空间能否容纳下尾插后的字符串,若不能,则需要先进行增容,然后再将待尾插的字符串尾插到对象的后方,因为待尾插的字符串后方自身带有’\0’,所以我们无需再在后方设置’\0’。

//尾插字符串
void append(const char* str)
{
	size_t len = _size + strlen(str); //尾插str后字符串的大小(不包括'\0')
	if (len > _capacity) //判断是否需要增容
	{
		reserve(len); //增容
	}
	strcpy(_str + _size, str); //将str尾插到字符串后面
	//strcat(_str,str); 需要找\0,效率低
	_size = len; //字符串大小改变
}

实现append函数也可以直接复用下面即将实现的insert函数。

//尾插字符串
void append(const char* str)
{
	insert(_size, str); //在字符串末尾插入字符串str
}

或者

void append( const string& s)
{
	append(s._str) ;
}
void append(size_t n, char ch)
{
	reserve(_size + n);
	for (size_t i = 0; i < n; ++i)
	{
		push_back( ch);
	}
}

operator+=

+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。
+=运算符实现字符串与字符之间的尾插直接调用push_back函数即可。

//+=运算符重载
string& operator+=(char ch)
{
	push_back(ch); //尾插字符串
	return *this; //返回左值(支持连续+=)
}

+=运算符实现字符串与字符串之间的尾插直接调用append函数即可。

//+=运算符重载
string& operator+=(const char* str)
{
	append(str); //尾插字符串
	return *this; //返回左值(支持连续+=)
}

insert

insert函数的作用是在字符串的任意位置插入字符或是字符串。
insert函数用于插入字符时,首先需要判断pos的合法性,若不合法则无法进行操作,紧接着还需判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符的过程也是比较简单的,先将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串即可。

//在pos位置插入字符
string& insert(size_t pos, char ch)
{
	assert(pos <= _size); //检测下标的合法性
	size_t len = strlen(str); //计算需要插入的字符串的长度(不含'\0')
	if (_size == _capacity) //判断是否需要增容
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2); //将容量扩大为原来的两倍
	}
	char* end = len + _size;
	//将pos位置及其之后的字符向后挪动一位
	while (end > pos)
	{
		_str[end] = _str[end-1];
		end--;
	}
	_str[pos] = ch; //pos位置放上指定字符
	_size++; //size更新
	return *this;
}

在这里插入图片描述

insert函数用于插入字符串时,首先也是判断pos的合法性,若不合法则无法进行操作,再判断当前对象能否容纳插入该字符串后的字符串,若不能则还需调用reserve函数进行扩容。插入字符串时,先将pos位置及其后面的字符统一向后挪动len位(len为待插入字符串的长度),给待插入的字符串留出位置,然后将其插入字符串即可。

//在pos位置插入字符串
string& insert(size_t pos, const char* str)
{
	assert(pos <= _size); //检测下标的合法性
	size_t len = strlen(str); //计算需要插入的字符串的长度(不含'\0')
	if (len + _size > _capacity) //判断是否需要增容
	{
		reserve(len + _size); //增容
	}
	char* end = len + _size;
	//将pos位置及其之后的字符向后挪动len位
	while (end >= len + pos)
	{
		 _str[end] = _str[end-len];
		end--;
	}
	strncpy(_str + pos, str, len); //pos位置开始放上指定字符串
	_size += len; //size更新
	return *this;
}

在这里插入图片描述

注意:插入字符串的时候使用strncpy,不能使用strcpy,否则会将待插入的字符串后面的’\0’也插入到字符串中。

erase

erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:
1、pos位置及其之后的有效字符都需要被删除。
这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。

2、pos位置及其之后的有效字符只需删除一部分。
这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了。

//删除pos位置开始的len个字符
string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size); //检测下标的合法性
	size_t n = _size - pos; //pos位置及其后面的有效字符总数
	if (len = npos||len >= n) //说明pos位置及其后面的字符都被删除
	{
		_size = pos; //size更新
		_str[_size] = '\0'; //字符串后面放上'\0'
	}
	else //说明pos位置及其后方的有效字符需要保留一部分
	{
		strcpy(_str + pos, _str + pos + len); //用需要保留的有效字符覆盖需要删除的有效字符
		_size -= len; //size更新
	}
	return *this;
}

clear

clear函数用于将对象中存储的字符串置空,实现时直接将对象的_size置空,然后在字符串后面放上’\0’即可。

//清空字符串
void clear()
{
	_size = 0; //size置空
	_str[_size] = '\0'; //字符串后面放上'\0'
}

swap

swap函数用于交换两个对象的数据,直接调用库里的swap模板函数将对象的各个成员变量进行交换即可。但我们若是想在这里调用库里的swap模板函数,需要在swap函数之前加上“::”(作用域限定符),告诉编译器优先在全局范围寻找swap函数,否则编译器编译时会认为你调用的是正在实现的swap函数(就近原则)。

//交换两个对象的数据
void swap(string& s)
{
	//调用库里的swap
	::swap(_str, s._str); //交换两个对象的C字符串
	::swap(_size, s._size); //交换两个对象的大小
	::swap(_capacity, s._capacity); //交换两个对象的容量
}

重点:若想让编译器优先在全局范围寻找某函数,则需要在该函数前面加上“::”(作用域限定符)。

c_str

c_str函数用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可。

//返回C类型的字符串
const char* c_str()const
{
	return _str;
}

访问字符串相关函数

operator[ ]

[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。
在C字符串中我们通过[ ] +下标的方式可以获取字符串对应位置的字符,并可以对其进行修改,实现[ ] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了,但需要注意在此之前检测所给下标的合法性。

//[]运算符重载(可读可写)
char& operator[](size_t i)
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}

在某些场景下,我们可能只能用[ ] +下标的方式读取字符而不能对其进行修改。例如,对一个const的string类对象进行[ ] +下标的操作,我们只能读取所得到的字符,而不能对其进行修改。所以我们需要再重载一个[ ] 运算符,用于只读操作。

//[]运算符重载(只读)
const char& operator[](size_t i)const
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}

find和rfind

find函数和rfind函数都是用于在字符串中查找一个字符或是字符串,find函数和rfind函数分别用于正向查找和反向查找,即从字符串开头开始向后查找和从字符串末尾开始向前查找。
find函数:
1、正向查找第一个匹配的字符。
首先判断所给pos的合法性,然后通过遍历的方式从pos位置开始向后寻找目标字符,若找到,则返回其下标;若没有找到,则返回npos。(npos是string类的一个静态成员变量,其值为整型最大值)

//正向查找第一个匹配的字符
size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size); //检测下标的合法性
	for (size_t i = pos; i < _size; i++) //从pos位置开始向后寻找目标字符
	{
		if (_str[i] == ch)
		{
			return i; //找到目标字符,返回其下标
		}
	}
	return npos; //没有找到目标字符,返回npos
}

2、正向查找第一个匹配的字符串。
首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。strstr函数若是找到了目标字符串会返回字符串的起始位置,若是没有找到会返回一个空指针。若是找到了目标字符串,我们可以通过计算目标字符串的起始位置和对象C字符串的起始位置的差值,进而得到目标字符串起始位置的下标。

//正向查找第一个匹配的字符串
size_t find(const char* str, size_t pos = 0)
{
	assert(pos < _size); //检测下标的合法性
	const char* ret = strstr(_str + pos, str); //调用strstr进行查找
	if (ret) //ret不为空指针,说明找到了
	{
		return ret - _str; //返回字符串第一个字符的下标
	}
	else //没有找到
	{
		return npos; //返回npos
	}
}

rfind函数:
实现rfind函数时,我们可以考虑复用已经写好了的两个find函数,但rfind函数是从后先前找,所以我们需要将对象的C字符串逆置一下,若是查找字符串,还需将待查找的字符串逆置一下,然后调用find函数进行查找,但注意传入find函数的pos以及从find函数接收到的pos都需要镜像对称一下。

1、反向查找第一个匹配的字符。
首先我们需要用对象拷贝构造一个临时对象tmp,因为我们并不希望调用rfind函数后对象的C字符串就被逆置了。我们将tmp对象的C字符串逆置,然后将所给pos镜像对称一下再调用find函数,再将从find函数接收到的返回值镜像对称一下作为rfind函数的返回值返回即可。

//反向查找第一个匹配的字符
size_t rfind(char ch, size_t pos = npos)
{
	string tmp(*this); //拷贝构造对象tmp
	reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
	if (pos >= _size) //所给pos大于字符串有效长度
	{
		pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
	}
	pos = _size - 1 - pos; //将pos改为镜像对称后的位置
	size_t ret = tmp.find(ch, pos); //复用find函数
	if (ret != npos)
		return _size - 1 - ret; //找到了,返回ret镜像对称后的位置
	else
		return npos; //没找到,返回npos
}

:rfind函数规定,当所给的pos大于等于字符串的有效长度时,看作所给pos为字符串最后一个字符的下标。

2、反向查找第一个匹配的字符串。
首先我们还是需要用对象拷贝构造一个临时对象tmp,然后将tmp对象的C字符串逆置,同时我们还需要拷贝一份待查找的字符串,也将其逆置。然后将所给pos镜像对称一下再调用find函数。注意:此时我们将从find函数接收到的值镜面对称后,得到的是待查找字符串的最后一个字符在对象C字符串中的位置,而我们需要返回的是待查找字符串在对象C字符串中的第一个字符的位置,所以还需做进一步调整后才能作为rfind函数的返回值返回。

//反向查找第一个匹配的字符串
size_t rfind(const char* str, size_t pos = npos)
{
	string tmp(*this); //拷贝构造对象tmp
	reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
	size_t len = strlen(str); //待查找的字符串的长度
	char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串)
	strcpy(arr, str); //拷贝str给arr
	size_t left = 0, right = len - 1; //设置左右指针
	//逆置字符串arr
	while (left < right)
	{
		::swap(arr[left], arr[right]);
		left++;
		right--;
	}
	if (pos >= _size) //所给pos大于字符串有效长度
	{
		pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
	}
	pos = _size - 1 - pos; //将pos改为镜像对称后的位置
	size_t ret = tmp.find(arr, pos); //复用find函数
	delete[] arr; //销毁arr指向的空间,避免内存泄漏
	if (ret != npos)
		return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
	else
		return npos; //没找到,返回npos
}

关系运算符重载函数

关系运算符有 >、>=、<、<=、==、!= 这六个,但是对于C++中任意一个类的关系运算符重载,我们均只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。

例如,对于string类,我们可以选择只重载 > 和 == 这两个关系运算符。

//>运算符重载
bool operator>(const string& s)const
{
	return strcmp(_str, s._str) > 0;
}
//==运算符重载
bool operator==(const string& s)const
{
	return strcmp(_str, s._str) == 0;
}

剩下的四个关系运算符的重载,就可以通过复用这两个已经重载好了的关系运算符来实现了。

//>=运算符重载
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 !(*this > s);
}
//!=运算符重载
bool operator!=(const string& s)const
{
	return !(*this == s);
}

>>和<<运算符的重载以及getline函数

>>运算符的重载

重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。

//>>运算符的重载
istream& operator>>(istream& in, string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in; //支持连续输入
}

若输入字符串很长.不断+=.频繁扩容,效率很低,.可以优化一下:

istream& operator>>(istream& in, string& s)
{
	s.clear( );
	char ch;
	ch = in.get();
	const size_t N = 32;
	char buff[N];
	size_t i = 0;
	
	while (ch != '' 8& ch != '\n ')
	{
		buff[i++] = ch;
		if ( i == N-1)
		{
			buff[i] = '\0 ';s += buff;
			i = 0;
		}
	}
	ch = in.get();
}
	buff[i] = '\0 ' ;
	s += buff;
	
	return in;
}

<<运算符的重载

重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用范围for对对象进行遍历即可。

//<<运算符的重载
ostream& operator<<(ostream& out, const string& s)
{
	//使用范围for遍历字符串并输出
	for (auto e : s)
	{
		out << e;
	}
	return out; //支持连续输出
}

getline

getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。

//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{
	s.clear(); //清空字符串
	char ch = in.get(); //读取一个字符
	while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
	{
		s += ch; //将读取到的字符尾插到字符串后面
		ch = in.get(); //继续读取字符
	}
	return in;
}

substr

string substr(size_t pos, size_t len = npos) const
{
	assert(pos < _size);size_t realLen = len;
	if ( len == npos ll pos+len > _size)
	{
	realLen = _size - pos;
	}

	string sub;
	for (size_t i = 0; i < realLen; ++i)
	{
		sub += _str[pos + i];
	}
	return sub;
}

vs下string的特殊处理

缺点:本身对象变大了
优点:小对象的效率会变高一些,有了buff数组,不需要去堆上动态申请开辟空间->空间换时间
在这里插入图片描述

浅拷贝问题的解决方案

在这里插入图片描述
这里是陈皓大佬关于写时拷贝的文章,想进一步了解的小伙伴可以看看:
C++ STL STRING的COPY-ON-WRITE技术

总结:

今天我们比较详细地完成了string类的模拟实现,了解了一些有关的底层原理。接下来,我们将进行STL中vector类的学习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/665396.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Internet Relay Chat:mIRC 7.73 Crack

mIRC是一个流行的互联网中继聊天客户端&#xff0c;个人和组织使用它在世界各地的IRC网络上相互交流、共享、玩耍和工作。为互联网社区服务了20多年&#xff0c;mIRC已经发展成为一种强大、可靠和有趣的技术。 Latest News mIRC 7.73 has been released! (June 18th 2023) This…

Linux常用命令——fuser命令

在线Linux命令查询工具 fuser 使用文件或文件结构识别进程 补充说明 fuser命令用于报告进程使用的文件和网络套接字。fuser命令列出了本地进程的进程号&#xff0c;那些本地进程使用file&#xff0c;参数指定的本地或远程文件。对于阻塞特别设备&#xff0c;此命令列出了使…

【C语言复习】第六篇、关于C语言操作符的知识

目录 第一部分、常见操作符 第二部分、每个操作符的具体使用 1、算术操作符 1.1、除法运算符 / 1.2、取模运算符 % 2、移位操作符 2.1、左移 << 2.2、右移 >> 3、位操作符 3.1、按位异或的妙用 3.2、按位与的妙用 4、单目操作符 4.1、逻辑反操…

18-1降维与特征选择——偏最小二乘方法(附matlab程序)

1.简述 降维&#xff1a; 比如现在有100维的变量来表征一个东西&#xff0c;我们觉得太冗余复杂了&#xff0c;想降低到10维。但是我们没有确定的筛选依据&#xff0c;直接使用数学工具来实现降维&#xff0c;就好像丢进了一个黑箱&#xff0c;经过抽象、提炼&#xff0c;得到了…

【深度学习】3-3 神经网络的学习- 导数梯度

导数 导数就是表示某个瞬间的变化量&#xff0c;式子如下&#xff1a; 式子的左边&#xff0c;表示f(x)关于x的导数&#xff0c;即f(x)相对于x的变化程度。式子表示的导数的含义是&#xff0c;x的“微小变化”将导致函数f(x)的值在多大程度上发生变化。其中&#xff0c;表示…

AI绘画是什么?怎样提高AI绘画技巧

大家好&#xff0c;我是权知星球&#xff0c;今天跟大家探讨一下AI绘画是什么&#xff1f;怎样才能提高AI绘画技巧的问题。 随着人工智能技术的迅速发展&#xff0c;AI绘画已成为一项具有前瞻性的技术。在过去几年中&#xff0c;涌现出了许多功能强大的人工智能绘画工具&#x…

荣耀加冕!数据猿斩获三项大奖,彰显技术媒体硬实力!

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 6月15日及6月16日&#xff0c;由数央网、数央公益联合国内众多媒体共同举办的2023国际绿色零碳节暨ESG领袖峰会、2023国际智造节暨硬科技峰会在北京举行。 该峰会旨在倡导全社会关注气候变化问题&#xff0c;积极采取行动&a…

TBarCode SDK:条码生成:11.15.1 Crack

TBarCode SDK&#xff1a;条码生成软件 TBarCode SDK 提供给 Microsoft Office 用户 和软件开发人员 条码打印。用这种 条码生成软件 您可以优良的品质的创建和打印所有用于工业和商业的 条码符号 。 最好的条码生成软件 TBarCode SDK 表示一个公知的集条形码创建组件集. 优秀…

Python爬虫需要那些步骤 ?

Python爬虫是一种自动化程序&#xff0c;可以通过网络爬取网页上的数据。Python爬虫可以用于各种用途&#xff0c;例如数据挖掘、搜索引擎优化、市场研究等。Python爬虫通常使用第三方库&#xff0c;例如BeautifulSoup、Scrapy、Requests等&#xff0c;这些库可以帮助开发者轻松…

Python面向对象编程2-面向过程的银行账号模拟程序 项目2.1 创建账号与存款

项目总目标&#xff1a;用面向过程思想设计一个简单的银行账号模拟程序。本次将迭代多个程序版本&#xff0c;每个版本都将添加更多功能。虽然这些程序没有达到发布的质量标准&#xff0c;但整个项目的目的是关注于代码如何与一个或多个银行账户的数据进行交互。 分析项目的必要…

<Linux> 基础IO

文章目录 文件操作基本概念当前路径文件打开方式"a""w""r" 文件描述符文件描述符fd是啥1. 为什么fd是从3开始&#xff0c;0&#xff0c;1&#xff0c;2呢&#xff1f;2. fd为什么是0&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;4&#…

Mockito单元测试基本使用

文章目录 1.为什么需要Mock2.Mockito 中常用方法2.1 Mock 方法2.2 对 Mock 出来的对象进行行为验证和Junit结果断言2.3 测试桩stub2.4 参数匹配器2.5 mock()与spy()2.6 InjectMocks 本文参考&#xff1a; 【码农教程】手把手教你Mockito的使用 - 掘金 (juejin.cn) java - doRet…

利用SD的插件从文本或者图片创建3D模型

利用SD的插件从文本或者图片创建3D模型 一个用于自动从文本或图像生成3D模型的稳定扩散WebUI的自定义扩展。 利用 OpenAl Shap-E 从文本或图像生成 3D 模型&#xff0c; GitHub项日地址: https://github.com/jtydhr88/sd-webui-txt-img-to-3d-model 安装步骤: 在扩展中心选…

Linux之LVM模式下LV和VG扩容

一、LVM简介 LVM (Logical Volume Manager) 是一个逻辑卷管理器&#xff0c;它允许用户将多个硬盘分区或者整个硬盘组成一个或多个逻辑卷。LVM 可以在运行时动态地改变逻辑卷的大小&#xff0c;而不需要关机或重新启动系统。它也可以将多个硬盘的存储空间组合在一起&#xff0c…

OpenGL视口学习

VC6新建一个openGL类型项目&#xff1b; 出现一些选项&#xff0c;默认后生成一个项目&#xff1b; VC6自带GL支持&#xff1b; 需要添加附加包含路径&#xff1b; 而后构建工程&#xff1b;运行如下&#xff1b; 是一个旋转的立方体&#xff1b; 生成的代码不是MFC的&#xff…

Python入门教程+项目实战-13.4节-程序实战-二分查找算法

目录 13.4.1 何为二分查找&#xff1f; 13.4.2 算法实现 13.4.3 系统学习python 13.4.1 何为二分查找&#xff1f; 我们已经学完了Python中的字典和集合&#xff0c;利用字典和集合就可以实现快速查找&#xff0c;非常方便。字典与集合使用了哈希表的索引结构来加快查找&am…

C++ 设计模式----“对象创建“模式

“对象创建”模式  通过“对象创建” 模式绕开new&#xff0c;来避免对象创建&#xff08;new&#xff09;过程中所导致的紧耦合&#xff08;依赖具体类&#xff09;&#xff0c;从而支持对象创建的稳定。它是接口抽象之后的第一步工作。  典型模式 • Factory Method •…

【Java】Java核心要点总结 68

文章目录 1. 为什么重写 equals() 时候必须重写 hashCode()2. 字符串常量池3. 字符串中的 intern() 方法4. try-catch-finally5. finally 中的代码不一定会执行 1. 为什么重写 equals() 时候必须重写 hashCode() 因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equ…

三、动画 -变形transform

目录&#xff1a; 1.变形transform定义 2.具体描述 3.用途 4.练习 一、变形transform定义 变形就是指通过CSS来改变元素的形状或位置 变形不会影响到页面的布局transform 用来设置元素的变形效果设置具体的值用translate()函数 二、具体描述 - 平移&#xff1a;translateX() 沿…

2023 3de实时仿真环境下资源不可见或没有了(只有floor)

F3退出实时仿真环境&#xff0c;双击结构树父节点 之后再进入实时仿真&#xff0c;3d仿真&#xff0c;再打开资源就可以看到了。