【C++】_string类字符串万字详细解析

 假如没有给你生命,你连失败的机会都没有。你已经得到了最珍贵的,还需要抱怨什么!💓💓💓

目录

  ✨说在前面

🍋知识点一:什么是string?

•🌰1.string类的概念

•🌰2.string类的主要特点

•🌰3.常用接口说明

🍋知识点二:string类常用接口

•🌰1.默认成员函数

🔥构造函数(⭐)

🔥析构函数

•🌰2.string类对象的访问和遍历操作

🔥operator[ ](⭐)

 🔥迭代器(⭐)

•🌰3.string类对象的容量操作

🔥size、length、capacity

🔥reserve(⭐)

🔥clear

🔥empty

🔥resize

•🌰4.string类对象的修改操作

 🔥push_back、append

🔥operator+=

🔥assgin

🔥insert(⭐)

🔥erase

🔥replace

🔥swap

🔥pop_back

•🌰5.string类对象的其他操作

🔥c_str

🔥find

🔥rfind

🔥substr(⭐)

🔥find_first_of

🔥find_last_of

🔥find_first_not_of

🔥find_last_not_of

•🌰6.string类的非成员函数

🔥operator+

🔥relational operators

🔥getline

•🌰7.例题训练

🔥题目一:仅仅反转字母

🔥题目二:第一个唯一字符

🔥题目三:字符串相加

 • ✨SumUp结语


  ✨说在前面

亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章我给大家介绍了一下STL是什么,以及相关的一些历史和容器介绍。如果大家没有掌握好相关的知识,上一篇篇文章讲解地很详细,可以再回去看看,复习一下,再进入今天的内容。

我们今天简单给大家讲解一下C++标准库中一个非常重要的组成部分——string字符串。如果大家准备好了,那就接着往下看吧~

  👇👇👇
💘💘💘知识连线时刻(直接点击即可)

【C++】STL简介

  🎉🎉🎉复习回顾🎉🎉🎉

         

 博主主页传送门:愿天垂怜的博客

 ​​​​

 ​​​​​

🍋知识点一:什么是string?

•🌰1.string类的概念

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。 

于是 C++ 中就引入了string类它可以看做是一个管理字符串的数据结构

 C++中的string类是一个非常强大的类,它提供了对字符串的丰富操作。string类是C++标准模板库(STL)的一部分,定义在头文件<string>中。这个类封装了字符数组的功能,并且添加了许多便利的成员函数来操作字符串,如查找、替换、插入、删除等。

模拟实现如下:

class string
{
public:
	//各种接口
    //...

private:
	char* _str;
	size_t _size;
	size_t _capacity;
	static const size_t npos;
};

 ​​​​

•🌰2.string类的主要特点

🔥动态内存管理

string类自动管理内存,当你修改字符串的大小时,它会自动调整内存分配。

🔥丰富的成员函数

提供了大量的成员函数来执行各种字符串操作,如长度获取(length()或size())、子串提取(substr())、字符串比较(compare())、查找(find())、替换(replace())、插入(insert())、删除(erase())等。

🔥安全

使用string类比直接使用字符数组更安全,因为它避免了缓冲区溢出的风险。

🔥标准库支持

作为STL的一部分,string类得到了广泛的支持

 ​​​​

•🌰3.常用接口说明

🔥string类对象的常见构造:

(constructor)函数名称

功能说明

string() (重点)

构造空的string类对象,即空字符串

string(const char* s) (重点)

用C-string来构造string类对象

string(size_t n, char c)

string类对象中包含n个字符c

string(const string&s) (重点)

拷贝构造函数

🔥string类对象的容量操作:

函数名称

功能说明

size(重点)

返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty (重点)检测字符串释放为空串,是返回true,否则返回false
clear (重点)

清空有效字符

reserve (重点)

为字符串预留空间**

resize (重点)将有效字符的个数该成n个,多出的空间用字符c填充

🔥string类对象的访问及遍历操作:

函数名称功能说明

operator[] (重

点)

返回pos位置的字符,const string类对象调用
begin+ end

begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器

rbegin + rend

begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器

范围forC++11支持更简洁的范围for的新遍历方式

 🔥string类对象的修改操作:

函数名称功能说明
push_back在字符串后尾插字符c
append在字符串后追加一个字符串

operator+= (重

点)

在字符串后追加字符串str
c_str(重点)返回C格式字符串

find + npos(重

点)

从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置

rfind

从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置

substr在str中从pos位置开始,截取n个字符,然后将其返回

 🔥string类非成员函数:

函数功能说明
operator+尽量少用,因为传值返回,导致深拷贝效率低
operator>> (重点)输入运算符重载
operator<< (重点)输出运算符重载
getline (重点)获取一行字符串
relational operators (重点)大小比较

 ​​​​​

🍋知识点二:string类常用接口

•🌰1.默认成员函数

🔥构造函数(⭐)

接口如下,前面也有,大家再仔细看看:

在文档中我们可以查看它们的具体用法:

我们大家也需要学会查看英文文档,有不懂的就去查,锻炼我们查看英文文章的能力。 

#include <iostream>
using namespace std;
#include <string>

int main()
{
	string s1;
	cout << s1 << endl;

	string s2("hello world");
	cout << s2 << endl;

	string s3(s2);
	cout << s3 << endl;

	string s4(s2, 6, 15);
	cout << s4 << endl;

	string s5(s2, 6);
	cout << s5 << endl;

	string s6("hello wrold", 5);
	cout << s6 << endl;

	string s7(10, 'x');
	cout << s7 << endl;

	return 0;
}

模拟实现如下:

//构造函数
string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

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

//拷贝构造现代写法
void swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string(const string& s)
{
	string temp(s._str);
	swap(temp);
}

 ​​​​​

🔥析构函数

析构函数直接用编译器默认生成、调用的就可以了,非常简单~

模拟实现如下:

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

 它的模拟实现也很简单,和之前数据结构中的【destroy】一样。

 ​​​​​

•🌰2.string类对象的访问和遍历操作

🔥operator[ ](⭐)

operator[ ]允许你像访问数组元素一样访问字符串中的字符,operator[ ]返回的是对字符串中字符的引用(char*),你可以读取也可以修改该位置的字符。

void test_string1()
{
	string s1;
	string s2("hello world");

	cout << s1 << s2 <<endl;

	s2[0] = 'x';
	cout << s1 << s2 << endl;
}

 如上面的代码,我们可以直接像访问数组一样将s2的第一个元素修改为'x',本质是通过operator[]函数。

模拟实现如下:

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

我们可以实现两个重载,以保证const的字符串也可以使用。 

我们后面会学习【size】这个接口,可以获取字符串的有效长度,那么我们要遍历这个字符串就变得很简单方便:

for (size_t i = 0; i < s1.size(); i++)
{
	cout << s1[i] << " ";
}
cout << endl;

我们还可以用迭代器的方式遍历这个字符串:

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

那这是什么意思呢?我们可以将it想象成一个指针(但不一定是指针),begin()会返回第一个位置的迭代器,而end()是\0位置的迭代器,这样就很容易理解了。

所有的容器都可以用这种类似的方式进行遍历,例如链表:

list<int> lt = { 1,2,3,4,5,6,7 };
list<int>::iterator lit = lt.begin();
while (lit != lt.end())
{
	cout << *lit << " ";
	lit++;
}
cout << endl;

 这是第二种遍历的方式,那么我们还有第三种,这是C++11新增的:

for (auto ch : s2)
{
	cout << ch << " ";
}
cout << endl;

auto自动推导类型,范围for可以自动赋值、自动迭代、自动判断结束,非常nb,但其实也正常,因为和之前的第一种方法在底层是一样的。也就是说:

范围for的底层就是迭代器。

同时我们要注意,如果我们再第一种方法中对*it进行修改,那么会直接影响到字符串,而我们再范围for中修改,是修改不到字符串的:

void test_string1()
{
	string s2("hello world");

	string::iterator it = s2.begin();
	while (it != s2.end())
	{
		*it += 2;
		cout << *it << " ";
		it++;
	}
	cout << endl;
    //s2被修改

	for (auto ch : s2)
	{
		ch -= 2;
		cout << ch << " ";
	}
	cout << endl;
    //s2没有被修改
}

那这是为什么呢?其实是因为我们将*it一个个赋值给ch,而我们是对ch进行操作。ch只是一个局部临时变量,对其的修改并不会影响到原来的字符串。

那我们可以这么处理:

for (auto& ch : s2)
{
	ch -= 2;
	cout << ch << " ";
}
cout << endl;

我们只要修改为引用,自然就不会有临时变量,也就可以直接对原字符串进行操作了。

总结:上面三种遍历方式(下标+[ ]、迭代器、范围for)在性能上并没有什么区别,第一种和第三种用得可能相对多。

补充:auto和范围for

auto关键字

1. 在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储期的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

2. 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

3. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

4. auto不能作为函数的参数,可以做返回值,但是建议谨慎使用

5. auto不能直接用来声明数组

void test_string2()
{
	map<string, string> dict;
	//map<string, string>::iterator mid = dict.begin();
	auto mid = dict.begin();
}

看上面的代码,由于auto可以自动推导类型,我们可以减少一些代码量。

#include <iostream>
using namespace std;

int func1()
{
	return 10;
}

void func()
{
	int a = 10;
	auto b = a;
	auto d = func1();
	auto c = 'a';
	auto e;
	//编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
}

int main()
{
	func();

	return 0;
}

我们可以用【typeid(variate).name()】来打印变量的类型。 

#include <iostream>
using namespace std;

int func1()
{
	return 10;
}

auto func2()
{
	return func1();
}

auto func3()
{
	return func2();
}

int main()
{
	auto ret = func3();

	return 0;
}

auto可以做返回值,但是建议谨慎使用,如上面代码,就有种递归的感觉,func3要去查看func2的返回值,func2又要去查看func1的返回值。

范围for

1. 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。

2. 范围for可以作用到数组和容器对象上进行遍历

3. 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。

范围for适用于容器和数组:

#include <iostream>
#include <string>
#include <list>
#include <map>
using namespace std;

int main()
{
	int array[] = { 1,2,3,4,5 };
	//C++98的遍历
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
	{
		array[i] *= 2;
	}
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
	{
		cout << array[i] << endl;
	}
	//C++11的遍历
	for (auto& e : array)
	{
		e *= 2;
	}
	for (auto e : array)
	{
		cout << e << " " << endl;
	}

	return 0;
}

注意:用范围for遍历数组yyds。 

 🔥迭代器(⭐)

C++中的迭代器(Iterator)是一种允许你访问容器中元素的对象,而无需暴露容器的内部结构。迭代器提供了一种统一的方法来遍历容器中的所有元素,无论容器的具体类型如何(如数组、向量vector列表list等)。通过使用迭代器,你可以读取、写入或删除容器中的元素,而无需关心容器的具体实现细节。

 我们先简单了解前四个迭代器:

接口名称使用说明
begin()返回指向第一个元素的迭代器
end()返回指向最后一个元素的下一个位置的迭代器
rbegin()返回指向最后一个元素的反向迭代器
rend()返回指向第一个元素的前一个位置的反向迭代器

对于前两个,我们之前就介绍过:

而后面两个,就是与前两个相反。rbegin指向最后一个元素,rbegin++那么这个迭代器往回走;rend指向的是第一个元素,rend++那么这个迭代器往后走。:

但是我们需要注意,上面的四个迭代器都是可读可写的,如果这个字符串是const的,C++11还可以用下面的四个。const迭代器是只读不可写的,也就是*cit不能被修改。

//const正向迭代器
void test_string5()
{
	const string s3("hello world");
	for (auto cit = s3.begin(); cit < s3.end(); ++cit)
	{
		cout << *cit << " ";
	}
	cout << endl;
}

//const反向迭代器
void test_string6()
{
	const string s4("hello world");
	for (auto cit = s4.crbegin(); cit < s4.crend(); ++cit)
	{
		cout << *cit << " ";
	}
	cout << endl;
}

所以说,一般情况下,有四种迭代器:iterator、const_iterator、reverse_iterator、const_reverse_iterator。

模拟实现如下:

//原生指针模拟迭代器
typedef char* iterator;
typedef const char* const_iterator;

iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}
const_iterator cbegin() const
{
	return _str;
}
const_iterator cend() const
{
	return _str + _size;
}

但是注意,string类能用原生指针模拟迭代器是因为它的本质是物理数组,而像链表这样的数据结构就不行了。 

 ​​​​​

•🌰3.string类对象的容量操作

在C++中,string类是用来处理字符串的一个非常强大的类。关于string类对象的容量操作,主要包括以下几个方面:

或如下: 

函数名称

功能说明

size(重点)

返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty (重点)检测字符串释放为空串,是返回true,否则返回false
clear (重点)

清空有效字符

reserve (重点)

为字符串预留空间**

resize (重点)将有效字符的个数该成n个,多出的空间用字符c填充

🔥size、length、capacity

我们首先来看看【size】和【length】,他们都可以计算字符串的长度,但是【size】会比【length】更好些,因为如果数据结构是一棵二叉树呢?所以说后者没有前者通用。

 而【capacity】计算的是字符串的空间总大小

void test_string7()
{
	string s5("hello world");
	cout << s5.length() << endl;//11
	cout << s5.size() << endl;//11
	cout << s5.capacity() << endl;//15
	cout << s5.max_size() << endl;//2147483647
}

这里我们也可以简单了解一下【max_size】,它返回的是字符串可以达到的最大长度。通过代码我们可以发现,它直接返回了int的最大值INT_MAX。

【capacity】并不一定等于字符串的长度,它的容量变化我们可以通过下面的代码进行观察:

void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "capacity changed:" << sz << endl;
	cout << "making s grow:\n";
	for (int i = 0; i < 100; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed:" << sz << endl;
		}
	}
}

运行后我们可以发现大致情况下,第一次是二倍扩容,后面是1.5倍扩。当然,在不同的环境下是不一样的。

模拟实现如下:

//size
size_t size() const
{
	return _size;
}
//capaicty
size_t capacity() const
{
	return _capacity;
}

​​​​​

🔥reserve(⭐)

我们现在知道了字符串的扩容,那有没有什么办法能减少扩容的次数呢?答案是有的,reserve就可以解决这个问题。

reserve用于请求改变字符串的容量(即分配的内存量),确保字符串可以存储至少指定数量的字符,而不需要重新分配内存。这可以提高性能,特别是在你知道将要向字符串中添加大量字符时,因为重新分配内存(特别是当字符串变得很大时)可能会非常耗时。

void TestPushBack()
{
	string s;
    //预留100的空间
	s.reserve(100);
	size_t sz = s.capacity();
	cout << "capacity changed:" << sz << endl;
	cout << "making s grow:\n";
	for (int i = 0; i < 100; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed:" << sz << endl;
		}
	}
}

但我们可以看到VS上实际是会比100多一些,它会自动去对齐。 当然,不同平台的实现会有不同。同时注意,【reserve】大多不会造成缩容,上面文档中也有说明。

void test_string8()
{
	string s("hello world and merry christmas!");
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;;

	//n < s.size() 不会缩容
	s.reserve(20);
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;

	//s.size() < n < s.capacity() 不会缩容
	s.reserve(35);
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;

	//n > s.capacity() 增容
	s.reserve(60);
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;
}

举例说明,VS上是不会造成缩容的。

模拟实现如下:

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

 ​​​​​

🔥clear

 用于移除字符串中的所有字符,将其长度设置为0,但保留其当前分配的容量(capacity)。调用clear后,字符串将不再包含任何字符,但它在内存中的空间(即字符数组的大小)不会减小。

void test_string9()
{
	string s("hello world");
	cout << s << endl;//hello world
	cout << s.size() << endl;//11
	cout << s.capacity() << endl << endl;//15
	s.clear();
	cout << s << endl;//空
	cout << s.size() << endl;//0
	cout << s.capacity() << endl << endl;//15
}

模拟实现如下:

void string::clear()
{
	_str[0] = '\0';
	_size = 0;
}

  

🔥empty

用于检查字符串是否为空。如果字符串不包含任何字符(即其长度为0),则【empty】函数返回true;否则,返回false。

【empty】函数是检查字符串是否为空的一种高效且直观的方式,因为它直接查询字符串的长度,而不需要遍历整个字符串。

void test_string10()
{
	string s1("hello world");
	cout << s1.empty() << endl;//0

	string s2;
	cout << s2.empty() << endl;//1
}

​​​​​

🔥resize

用于改变字符串的大小。它可以增加或减少字符串中的字符数,如果增加大小,则新添加的字符默认初始化为空字符。在实践中,当增加大小时,resize通常会用指定的字符(如果有的话)填充新添加的空间,或者如果不指定,则不进行填充。

更常见的是,resize用于减少字符串的大小,此时会移除超出新大小的尾部字符。

resize函数有两种常见的重载形式:

void resize(size_t n);

这个版本将字符串的大小更改为n。如果n小于当前大小,则移除超出新大小的尾部字符。如果n大于当前大小,则添加足够数量的字符以达到新的大小。

void reserve(size_t n, char c);

这个版本还允许你指定一个字符c,用于填充在增加大小时新添加的空间如果n小于当前大小,则仍然移除超出新大小的尾部字符。如果n大于当前大小,则添加n-size()个字符c。

 ​​​​​

•🌰4.string类对象的修改操作

string也是一种数据结构,那么它就具有增删查改的各种操作。

我们一个一个来看。

 🔥push_back、append

【push_back】的功能是在字符串末尾添加一个字符。

而【append】的功能是在字符串的末尾添加字符串,它有很多的版本,但用法上我们其实都很熟悉了。

#include <iostream>
using namespace std;

void test_string()
{
	string s("hello world");
	s.push_back('s');
	s.push_back('t');

	s.append("Merry Christmas! ");

	cout << s << endl;
}

int main()
{
	test_string();

	return 0;
}

 结果如下:

模拟实现如下:

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

//append
void string::append(const char* str)
{
	size_t length = strlen(str);
	if (_size + length > _capacity)
	{
		reserve(_size + length > _capacity * 2 ? _size + length : _capacity * 2);
	}
	strcpy(_str + _size, str);
	_size += length;
}

 ​​​​​

🔥operator+=

【operator+=】用于将一个字符串追加到另一个string对象的末尾。

void test_string2()
{
	string s1("hello world");
	string s2("Happy New Year!");
	s1 += ' ';
	s1 += "Merry Christmas!";
	s1 += ' ';
	s1 += s2;

	cout << s1 << endl;
    //hello world Merry Christmas! Happy New Year!
}

模拟实现如下:

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

 ​​​​​

🔥assgin

 【assign】也是一种赋值,但是用得比较少。

void test_string3()
{
	string str;
	string base = "The quick brown fox jumps over a lazy dog.";

	str.assign(base);
	cout << str << '\n';

	str.assign(base, 10, 9);
	cout << str << '\n';         // "brown fox"

	str.assign("pangrams are cool", 7);
	cout << str << '\n';         // "pangram"

	str.assign("c-string");
	cout << str << '\n';         // "c-string"

	str.assign(10, '*');
	cout << str << '\n';         // "**********"

	str.assign(base.begin() + 16, base.end() - 12);
	std::cout << str << '\n';         // "fox jumps over"
}

 ​​​​​

🔥insert(⭐)

【insert】函数是一个非常有用的成员函数,它允许你在字符串的指定位置插入另一个字符串或字符。这个函数有几个重载版本,以适应不同的插入需求。

void test_string4()
{
	string s1("hello world");
	s1.insert(0, "Merry Christmas! ");

	//插入一个字符的写法
	char ch = 't';
	s1.insert(0, 1, ch);
	cout << s1 << endl;
	//tMerry Christmas! hello world

	s1.insert(s1.begin(), ch);
	cout << s1 << endl;
	//ttMerry Christmas! hello world
}

 但是注意,头插在顺序表中是需要将后面的所有元素都向后移动的,复杂度为O(n)。

模拟实现如下:

void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	_size++;
}
void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t length = strlen(str);
	if (_size + length >= _capacity)
	{
		reserve(_size + length > 2 * _capacity ? _size + length : 2 * _capacity);
	}
	size_t end = _size + length;
	while (end > pos + length - 1)
	{
		_str[end] = _str[end - length];
		end--;
	}
	for (size_t i = 0; i < length; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += length;
}

insert我提供两个重载,它的实现比较复杂,大家可以画图理解,不要对着代码干瞪眼,这是瞪不出个所以然的。 

 ​​​​​

🔥erase

【erase】用于从字符串中删除一个或多个字符这个函数有几个重载版本,允许你指定要删除的字符的起始位置和长度,或者通过迭代器来指定要删除的字符范围。

void test_string6()
{
	string s("hello world");
	s.erase(6, 1);
	cout << s << endl;//hello orld

	s.erase(0, 1);
	cout << s << endl;//ello orld

	s.erase(s.begin());//llo orld
	cout << s << endl;

	s.erase(--s.end());//llo orl
	cout << s << endl;
	
	s.erase(s.size() - 1, 1);
	cout << s << endl;//llo or

	s.erase(2);
	cout << s << endl;//ll
}

模拟实现如下:

//缺省值不能在定义的时候给
void string::erase(size_t pos, size_t length)
{
	assert(pos < _size);
	if (length >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		for (size_t i = pos + length; i <= _size; i++)
		{
			_str[i - length] = _str[i];
		}
		_size -= length;
	}
}

 ​​​​​

🔥replace

【replace】用于替换字符串中的一部分内容。这个函数有多个重载版本,允许你指定要替换的字符范围以及替换成的新内容。

void test_string7()
{
	string s1("hello world");
	string s2(s1);
	//少替换多-向后移动
	s1.replace(5, 1, "%%");
	cout << s1 << endl;
	//hello%%world

	//多替换少-向前移动
	s2.replace(5, 4, "%%");
	cout << s2 << endl;
	//hello%%ld
}

 ​​​​​

🔥swap

【swap】用于交换两个string对象的内部数据。在调用【swap】后,两个字符串对象将包含对方原本持有的数据。这种操作通常比手动复制和删除数据要快得多,因为它只需要交换内部指针或引用,而不需要实际复制字符串内容。

void test_string9()
{
	string str1 = "Hello";
	string str2 = "World";

	str1.swap(str2);

	std::cout << "str1: " << str1 << std::endl; // 输出: str1: World  
	std::cout << "str2: " << str2 << std::endl; // 输出: str2: Hello  
}

 ​​​​​

🔥pop_back

 【pop_back】用于移除字符串中的最后一个字符。这个函数没有返回值,并且它会修改调用它的string对象,使其长度减少一个字符。

void test_string10()
{
	string str = "Hello, World!";

	//显示原始字符串  
	cout << "Original string: " << str << std::endl;

	//移除最后一个字符  
	str.pop_back();

	//显示修改后的字符串  
	cout << "After pop_back: " << str << std::endl;
}

 ​​

•🌰5.string类对象的其他操作

string类除对象的访问遍历操作、对象的修改操作,还有一些其他的字符串操作:

下面我就注意给大家介绍。 

 ​​​​​

🔥c_str

该函数返回一个指向以NULL结尾的字符数组(C 风格字符串)的指针,该数组包含了string对象中存储的相同字符序列。这个返回的指针可以用于需要C风格字符串的C++或C的API中。

这个接口是连接C字符串与C++string类对象的桥梁,很好地起到了一个连通的效果。

void test_string11()
{
	string file;
	cin >> file;
	FILE* pf = fopen(file.c_str(), "r");
	char ch = fgetc(pf);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(pf);
	}
	fclose(pf);
}

模拟实现如下:

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

 ​​​​​

🔥find

【find】用于在字符串中搜索子字符串或字符的首次出现位置。如果找到了指定的子字符串或字符,【find】函数会返回子字符串或字符首次出现的索引(基于0的索引);如果没有找到,则返回npos,这是一个特殊的常量,表示未找到的位置,通常是一个很大的整数值。

例题:将字符串中的空格替换成"%%"

void test_string8()
{
	string s("hello world and Merry Christmas!");
	size_t pos = s.find(' ');
	while (pos != string::npos)
	{
		s.replace(pos, 1, "%%");
		pos = s.find(" ");
	}
	cout << s << endl;
}

但是,当空格较多的时候,频繁的扩容、插入、移动操作使得这个过程比较复杂,我们还可以用范围for这么来处理:

void test_string8()
{
	string s("hello world and Merry Christmas!");
	string temp;
	for (auto& ch : s)
	{
		if (ch == ' ')
			temp += "%%";
		else
			temp += ch;
	}
	cout << temp << endl;
}

模拟实现如下:

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

 ​​​​​

🔥rfind

 【rfind】的功能和【find】类似,只不过它是倒着找的。如果找到了指定的子串或字符,则返回它第一次出现的位置(从0开始计数);如果没有找到,则返回npos(这是一个常量,表示一个不可能的位置,通常是一个非常大的数)。

void test_string12()
{
	string str("The sixth sick sheik's sixth sheep's sick.");
	string key("sixth");

	size_t found = str.rfind(key);
	if (found != string::npos)
		str.replace(found, key.length(), "seventh");
        //The sixth sick sheik's seventh sheep's sick.

	cout << str << '\n';
}

 ​​​​​

🔥substr(⭐)

【substr】是string 类中的一个成员函数,它允许你从一个字符串中提取(或称为“截取”)一个子字符串。这个函数非常有用,特别是在需要处理字符串的一部分时。

举例1:打印test.cpp的后缀.cpp

void test_string13()
{
	string s1("test.cpp");
	size_t pos = s1.find(".");
	string suffix = s1.substr(pos);

	cout << suffix << endl;
}

 举例2:打印test.cpp.zip的后缀.zip

void test_string13()
{
	string s1("test.cpp.zip");
	size_t pos = s1.rfind('.');
	string suffix = s1.substr(pos);

	cout << suffix << endl;
}

模拟实现如下:

string string::substr(size_t pos, size_t length)
{
	assert(pos < _size);
	//length大于剩余字符长度,就是取到结尾
	if (length > _size - pos)
	{
		length = _size - pos;
	}
	string sub;
	sub.reserve(length);
	for (size_t i = 0; i < length; i++)
	{
		sub += _str[pos + i];
	}
	return sub;
}

 ​​​​​

🔥find_first_of

它用于在字符串中查找第一次出现与指定字符集合中任意字符相匹配的字符的位置。这个函数在你需要找分隔符、查找属于某个字符集的第一个字符时特别有用。

举例:将str中的aeiou全部屏蔽为 * 

void test_string14()
{
	string str("Please, replace the vowels in this sentence by asterisks.");
	size_t pos = str.find_first_of("aeiou");
	while (pos != string::npos)
	{
		str[pos] = '*';
		pos = str.find_first_of("aeiou", pos + 1);
	}

	cout << str << '\n';
}

 ​​​​​

🔥find_last_of

同样的道理,【find_last_of】和【find_first_of】类似,只不过是从末尾开始找的。

 举例:分割文件路径

void SplitFilename(const string& str)
{
	cout << "Splitting: " << str << '\n';
	size_t found = str.find_last_of("/\\");
	cout << " path: " << str.substr(0, found) << '\n';
	cout << " file: " << str.substr(found + 1) << '\n';
}

int main()
{
	string str1("/usr/bin/man");
	string str2("c:\\windows\\winhelp.exe");

	SplitFilename(str1);
	SplitFilename(str2);

	return 0;
}

  ​​​​​

🔥find_first_not_of

 这个就和【find_first_not_of】相反了查找不是指定字符集合中的字符并返回它的位置,是从头开始查找,如果找不到就返回string::npos。

举例:将str中的除aeiou的字符全部屏蔽为 * 

void test_string14()
{
	string str("Please, replace the vowels in this sentence by asterisks.");
	size_t pos = str.find_first_not_of("aeiou");
	while (pos != string::npos)
	{
		str[pos] = '*';
		pos = str.find_first_not_of("aeiou", pos + 1);
	}

	cout << str << '\n';
}

  ​​​​​

🔥find_last_not_of

 同理,这个和【find_last_not_of】相反查找不是指定字符集合中的字符并返回它的位置,是从尾开始查找,如果找不到就返回string::npos。

举例:删除空白字符

void test_string15()
{
	string str("Please, erase trailing white-spaces   \n");
	string whitespaces(" \n");

	size_t found = str.find_last_not_of(whitespaces);
	if (found != string::npos)
		str.erase(found + 1);
	else
		str.clear();

	std::cout << '[' << str << "]\n";
}

 ​​

•🌰6.string类的非成员函数

string类提供了丰富的成员函数来操作字符串,但也有一些与string相关的非成员函数,这些函数虽然不是string类的一部分,但经常与string 一起使用,以提供额外的功能或操作。

这些非成员函数通常定义在<string>或其他相关的头文件中,并且它们通常是为了支持更通用的操作或算法,这些操作可能不特定于string,但可以与string 一起很好地工作。

🔥operator+

【operator+】用于将两个string对象、或者一个string对象和一个C风格字符串(const char*)连接起来,生成一个新的string 对象,该对象包含了两个操作数连接后的结果。

void test_string16()
{
	string s1("hello");

	string s2 = s1 + " world";
	cout << s2 << endl;

	string s3 = "world " + s1;
	cout << s3 << endl;
}

  ​​​​​

🔥relational operators

string类提供了关系运算符(relational operators)来比较两个字符串对象这些关系运算符不是string类的成员函数,而是作为非成员函数重载的,以便它们能够接收两个string类型的操作数。这些运算符返回布尔值(true或false),表示字符串之间的比较结果,功能上类似于C语言的【strcmp】函数。

以下是string类支持的关系运算符:

等于(==):检查两个字符串是否包含相同的字符序列。

不等于(!=):检查两个字符串是否不同。

小于(<=):按字典顺序比较两个字符串。如果第一个字符串在字典上小于第二个字符串,则返回 true。

大于(>=):按字典顺序比较两个字符串。如果第一个字符串在字典上大于第二个字符串,则返回 true。

小于等于(<=):按字典顺序比较两个字符串。如果第一个字符串在字典上小于或等于第二个字符串,则返回 true。

大于等于(>=):按字典顺序比较两个字符串。如果第一个字符串在字典上大于或等于第二个字符串,则返回 true。

void test_string17()
{
	string foo = "alpha";
	string bar = "beta";

	if (foo == bar) std::cout << "foo and bar are equal\n";
	if (foo != bar) std::cout << "foo and bar are not equal\n";
	if (foo < bar) std::cout << "foo is less than bar\n";
	if (foo > bar) std::cout << "foo is greater than bar\n";
	if (foo <= bar) std::cout << "foo is less than or equal to bar\n";
	if (foo >= bar) std::cout << "foo is greater than or equal to bar\n";
}

结果如下:

foo and bar are not equal
foo is less than bar
foo is less than or equal to bar

模拟实现如下:

bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator<=(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) <= 0;
}
bool operator>(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) > 0;
}
bool operator>=(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) >= 0;
}
bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) != 0;
}

  ​​​​​

🔥getline

【getilne】是一个非成员函数,它可以从输入流中读取字符,直到遇到换行符\n然后丢弃换行符,并将读取的字符序列(不包括换行符)存储到提供的string对象中。

举例:字符串最后一个单词的长度

#include <iostream>
using namespace std;

int main() 
{
   string str;
   //cin和scanf拿不到空格
   //cin >> str;
   getline(cin, str);

   size_t pos = str.rfind(' ');
   cout << str.size() - pos - 1 << endl;

   return 0;
}

模拟实现如下:

friend istream& getline(istream& in, string& s);

istream& getline(istream& in, string& s)
{
	s.clear();
	const int N = 256;
	char buff[N];
	int i = 0;

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

			i = 0;
		}
		ch = in.get();
	}
	return in;
}

 

•🌰7.例题训练

那么学习到现在,我们已经具备了写一些简单题目的能力,那就来几道题目练练手吧!

🔥题目一:仅仅反转字母

题目链接:917. 仅仅反转字母 - 力扣(LeetCode)

思路:设置左右边界,向中间走,如果不是字母就继续往中间走,知道都是字母就交换,整个思路和快速排序的霍尔方法差不多。

代码如下:

class Solution {
public:
    string reverseOnlyLetters(string s) {
        int left = 0;
        int right = s.size() - 1;
        while(left < right)
        {
            while(!isalpha(s[right]) && left < right)
            {
                right--;
            }
            while(!isalpha(s[left]) && left < right)
            {
                left++;
            }
            swap(s[left++], s[right--]);
        }
        return s;
    }
};

大家不要学习了我们string类的相关操作就忘记了普通而本质的方法,如同不要像学习数学只顾秒杀而忽略了最基本的方法。

 ​​​​​

🔥题目二:第一个唯一字符

题目链接:387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

题目描述:

思路:直接利用计数排序的思想统计所有字母字符出现的次数,再寻找第一个为1的数组元素就可以了。 

代码如下:

class Solution {
public:
    int firstUniqChar(string s) {
        int count[26] = { 0 };
        //统计次数
        for(auto& ch : s)
        {
            count[ch - 'a']++;
        }
        for(size_t i = 0; i < s.size(); i++)
        {
            if(count[s[i] - 'a'] == 1)
                return i;
        }
        return -1;
    }
};

 ​​​​​

🔥题目三:字符串相加

题目链接:415. 字符串相加 - 力扣(LeetCode)

题目描述:

思路: 实现两个字符串表示的非负整数相加时,我们主要利用字符串从右到左(即最低位到最高位)的顺序,模拟手工加法的过程。

class Solution {
public:
    string addStrings(string num1, string num2) {
        string str;
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        //进位
        int next = 0;
        while(end1 >=0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = val1 + val2 + next;
            next = ret / 10;
            ret %= 10;
            //头插
            str.insert(str.begin(), '0' + ret);
        }
        if(next != 0)
        {
            //头插
            str.insert(str.begin(), '1');
        }
        return str;
    }
};

关键点

  • 从低位到高位相加:模拟手工加法的过程,从字符串的末尾(即最低位)开始相加。
  • 处理不同长度的字符串:通过检查索引是否越界,将不存在的位视为0,从而统一处理不同长度的字符串。
  • 进位处理:使用变量next来记录进位值,并在每次相加时更新。
  • 结果字符串的构建:使用头插法构建结果字符串,确保最终得到的字符串顺序是正确的。

但是这个方法其实不太好,原因就是不断头插构成O(n^2)的时间复杂度,因此我们可以用尾插的方法:

class Solution {
public:
    string addStrings(string num1, string num2) {
        string str;
        int end1 = num1.size() - 1, end2 = num2.size() - 1;
        //进位
        int next = 0;
        while(end1 >=0 || end2 >= 0)
        {
            int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = val1 + val2 + next;
            next = ret / 10;
            ret %= 10;
            //尾插
            str += ('0' + ret);
        }
        if(next != 0)
        {
            str += '1';
        }
        //逆置
        reverse(str.begin(), str.end());

        return str;
    }
};

此时是时间复杂度就降低至O(n)。

 • ✨SumUp结语

到这里本篇文章的内容就结束了,本节介绍了C++中string类的相关知识。这里的内容非常多,非常丰富,下一篇章还需要继续讲解。望大家能够认真学习,打好基础,迎接接下来的挑战,期待大家继续捧场~💖💖💖

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值