Bootstrap

【C++笔记】string类使用详解

前言

各位读者朋友们大家好!上期我们讲完了C++的模板初阶,这一期我们开启STL的学习。STL是C++的数据结构和算法库,是我们学习C++的很重要的一部分内容,在以后的工作中也很重要。现在我们开始讲解。

一. 为什么学习string类

1. C语言中的字符串

C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但这些库函数与字符串是分开的,不太符合OOP(面向对象编程,Object-Oriented Programming是一种广泛使用的编程范式,它基于“对象”的概念来设计和实现软件。)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

二. 标准库中的string类

2.1 string类

string类的文档
在使用string类时,必须包含#include< string >头文件以及指明命名空间std;

2.2 auto和范围for

auto关键字
在这里补充两个C++11的语法,方便后续学习:

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

  • 用auto声明指针类型时,用auto和auto * 没有区别,但是auto声明引用类型的时候必须加&

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

  • auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
    在这里插入图片描述

  • auto不能用来直接声明数组
    在这里插入图片描述
    范围for

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

  • 范围for可以作用到数组和容器对象上进行遍历
    在这里插入图片描述

  • 范围for的底层很简单,容器遍历实际就是替换迭代器,这个在汇编层面可以看到


2.3 string类的常用接口说明

1. string类对象的常见构造

在这里插入图片描述

  • 1.string()
    在这里插入图片描述

这是string的无参构造,构造一个长度为0的空字符串。

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

	return 0;
}

在这里插入图片描述


  • 2. string (const string& str);
    在这里插入图片描述
    这是string类的拷贝构造
int main()
{
	string s1;
	string s2(s1);
	cout << s2;

	return 0;
}

在这里插入图片描述


  • 3. string (const string& str, size_t pos, size_t len = npos);
    在这里插入图片描述
    从字符串的指定位置(我们给的是下标)拷贝len个字符,如果字符串太短或着len是npos就拷贝到字符串结束。
int main()
{
	string s("hello world");
	string s1(s, 0, 5);
	string s2(s, 0, 15);
	string s3(s, 0);// 第三个参数使用缺省值npos
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;

	return 0;
}

在这里插入图片描述


  • 4.string (const char * s);
    在这里插入图片描述
    这是string类的带参构造
int main()
{
	string s("hello world");
	cout << s << endl;

	return 0;
}

在这里插入图片描述


  • 5. string (const char * s, size_t n);
    在这里插入图片描述
    从s指向的字符数组中,复制前n个字符
int main()
{
	string s("hello world", 5);
	cout << s;

	return 0;
}

在这里插入图片描述


  • 6. string (size_t n, char c);
    在这里插入图片描述
    用n个字符c来构造字符串
int main()
{
	string s(5, 'h');
	cout << s;
	return 0;
}

在这里插入图片描述


2. string类对象的容量操作

函数名称功能说明
size(重点)返回字符串的有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty(重点)检测字符串是否为空串,是返回True,否则返回False
clear(重点)清空有效字符
reserve(重点)为字符串预留空间
resize(重点)将有效字符的个数改成n个,多出的空间用字符c填充
2.1 size和lenth

size和lenth都是用来求字符串长度的
在这里插入图片描述

2.2 capacity

capacity是返回字符串的容量的,跟顺序表中的capacity一样,但是string中的capacity不包含\0
在这里插入图片描述
在实现顺序表的时候,我们对顺序表的扩容是二倍扩容,在Vs环境中,编译器对string类的扩容是怎样扩的呢?我们通过下面的一段代码看一下:

void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	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 << '\n';
		}
	}
}

在这里插入图片描述
Vs中,string类的实现加了一个buff数组,当字符长度小于16时,就存在buff数组中,如果大于16就去堆上开空间,看一下string的大小:
在这里插入图片描述
在32位环境下,按照上面string类的底层内存对齐计算得string类的大小也是28字节,不管使用数组还是在堆上开空间。
在这里插入图片描述
在Linux环境中使用g++编译器,是完全的2倍扩容
在这里插入图片描述

2.3 empty
2.4 clear

在这里插入图片描述
将字符串清空,空间清不清空看编译器,Vs和g++都不清

int main()
{
	string s("hello world");
	cout << s << endl;
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	return 0;
}

在这里插入图片描述

2.5 reserve

在这里插入图片描述
reserve用来预留空间,预留的空间在Vs环境下会大于等于我们给定的空间,这与Vs的内存对齐有关,在g++环境下会严格等于要求的大小。
在这里插入图片描述
那如果预留的空间过大,编译器会不会缩容呢?
在这里插入图片描述
C++标准并没有强制要求缩容,看编译器,但是有一点是确定的:这个函数不会改变字符串的长度和内容。

int main()
{
	string s("********************");
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;

	s.reserve(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;

	s.reserve(28);
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;

	s.reserve(48);
	cout << s.size() << endl;
	cout << s.capacity() << endl << endl;
	return 0;
}

在这里插入图片描述
上面可以看到,在Vs环境下,是不会缩的,但是在g++环境下编译器是会进行缩容的。

2.6 resize

在这里插入图片描述
在这里插入图片描述

int main()
{
	string s("************************");//24
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	s.resize(18);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	s.resize(28, 'x');
	cout << s << endl;
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	s.resize(36, '#');
	cout << s << endl;
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	return 0;
}

在这里插入图片描述

3. string类的访问及遍历操作

函数名称功能说明
operator[](重点)返回pos位置的字符,const string类对象调用
begin+endbegin获取开始位置字符的迭代器+end获取最后一个字符下一个位置的迭代器
rbegin+rendrbegin获取最后一个字符的迭代器+rend获取第一个字符前一个位置的迭代器
范围forC++11支持更简洁的范围for的新遍历方式
1. 迭代器(iterator)

在C++中,迭代器(Iterator)是一种用于访问容器(如数组、向量、列表、集合等)中元素的对象。迭代器提供了一种统一的方式来遍历容器中的元素,以下是迭代器的主要作用:

1.遍历容器
2.访问元素
3. 范围操作
4. 支持泛型编程:迭代器使得算法和容器可以分离,从而实现高层次的抽象和泛型编程。通过定义迭代器类型,容器可以与各种算法无缝集成。
5. 插入和删除操作
6. 统一接口:迭代器提供了一个统一的接口来访问和操作容器中的元素,使得用户可以用相同的方式处理不同类型的容器。

迭代器共有四种:
在这里插入图片描述

2.operator[]访问

string类重载了[]这一操作符,返回的是字符串的任意位置的字符的引用,这样我们就可以修改字符串某一位置的值了,并且越界访问还会报错。

int main()
{
	string s("hello world");
	cout << s[4] << endl;
	//cout << s[20];
	s[4] = 'x';
	cout << s << endl;
	return 0;
}

在这里插入图片描述
我们可以认为string类是如下结构:

class string
{
public:
	char& operator[](size_t i)
	{
		assert(i < _size);
		return _str[i];
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

operator[]封装在类里面作为成员函数默认为内联,效率也会很高。

3. [] + 下标遍历
int main()
{
	string s("hello world");
	for (int i = 0; i < s.size(); ++i)
	{
		cout << s[i] << " ";
	}

	return 0;
}

在这里插入图片描述
这种方式像数组访问一样

4. begin + end遍历
int main()
{
	string s("hello world");
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	return 0;
}

在这里插入图片描述
在这里插入图片描述
这种遍历方式所有的容器都可以使用这种方式
当字符串被const修饰时,使用string::const_interator迭代器遍历:
在这里插入图片描述

5. rbegin + rend遍历

rebegin和rend是反向迭代器(string::reverse_iterator)的函数,rbegin获取最后一个字符的迭代器+rend获取第一个字符前一个位置的迭代器。这样方式的遍历是反向遍历

int main()
{
	string s("hello world");
	string::reverse_iterator rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	return 0;
}

在这里插入图片描述

6. 范围for遍历
int main()
{
	string s("hello world");
	for (auto ch : s)
	{
		cout << ch << " ";
	}
	return 0;
}

在这里插入图片描述

范围for是自动推导出类型,自动遍历的,也就是说编译器推导出s的类型,然后将其拷贝赋值给ch。
在这里插入图片描述
在汇编层面看,范围for的底层就是迭代器。
在这里插入图片描述
我们发现,对ch进行修改并没有影响s,这是因为ch是字符串的拷贝,而对迭代器进行操作就会影响到s,我们可以将迭代器理解为指针,对指针解引用修改,想要通过范围for修改s的内容,可以给auto后面加引用,这样ch就是s里面每个变量的别名。
在这里插入图片描述

4. string类对象的修改操作

函数名称功能说明
push back在字符串后尾插字符c
append在字符串后追加一个字符串
operator+=(重点)在字符串后面追加字符串
insert在pos位置插入字符
erase在pos位置删除len长度的字符
4.1 push back

在这里插入图片描述
在字符串的后面尾插一个字符

int main()
{
	string s("hello wodld");
	s.push_back('x');
	cout << s << endl;

	return 0;
}

在这里插入图片描述

4.2 append

在字符串的末尾追加一个新的字符串

int main()
{
	string s("hello world");
	s.append(" hello Yuey");
	cout << s << endl;
	return 0;
}

在这里插入图片描述

4.3 operator+=

在这里插入图片描述
重载了+=操作符,可以在字符串的末尾追加string类、字符串以及字符

int main()
{
	string s1("hello world");
	string s2("hello world");
	string s3("hello world");

	// 追加string类
	string s4(" hello Yuey");
	s1 += s4;
	cout << s1 << endl;
	// 追加字符串
	s2 += " hello Yuey";
	cout << s2 << endl;
	// 追加字符
	s3 += "*";
	cout << s3 << endl;
}

在这里插入图片描述
这个+=在实践中使用最多

4.4 insert

在pos位置插入字符或字符串,也可以使用迭代器
在这里插入图片描述

int main()
{
	string s("hello world");
	string s1(" hello Yuey");
	//s.insert(5, "#");// 插入一个字符
	s.insert(5,1,'#');
	s.insert(12, s1);
	cout << s << endl;
	// 迭代器
	s.insert(s.begin(), '*');
	cout << s << endl;
	return 0;
}

在这里插入图片描述

4.5 erase

在这里插入图片描述
erase支持在指定位置删除len长度的字符,如果len不传的话默认删除npos也就是删除pos位置及以后的所有字符,erase也支持迭代器删除,但是用的最多的还是第一种

int main()
{
	string s1("hello world");
	// 头删
	s1.erase(0, 1);
	cout << s1 << endl;
	s1.erase(s1.begin());
	cout << s1 << endl;
	// 尾删
	s1.erase(--s1.end());
	cout << s1 << endl;
	s1.erase(s1.size() - 1, 1);
	cout << s1 << endl;
	// 指定位置删除
	string s2("hello Yuey");
	s2.erase(6, 4);
	cout << s2 << endl;
	// 指定位置不传长度
	s2.erase(1);
	cout << s2 << endl;
	return 0;
}

在这里插入图片描述

5. string类的其他相关操作

函数名功能说明
c_str(重点)返回c格式字符
find + npos(重点)从字符串pos位置开始往后找到字符c,返回该字符在字符串中的位置
rfind从字符串的pos位置开始往前找字符c,返回该字符在字符串中的位置
substr从str中从pos位置开始,截取n个字符,然后将其返回
5.1 c_str

在这里插入图片描述
这个函数的目的主要是兼容C语言,返回的是指向数组的指针,主要在文件读取时使用

5.2 find + npos

在这里插入图片描述
find支持在pos位置开始正向查找string类、字符串以及字符,返回值是第一次找到的指定字符的下标,没找到返回npos,如果pos不传,默认从第一个字符开始
将字符串中的空格替换为%:

int main()
{
	string s("hello world hello Yuey");
	cout << s << endl;
	size_t pos = s.find(' ', 0);
	while (pos != string::npos)
	{
		s.replace(pos, 1, "%");
		pos = s.find(' ', pos + 1);
	}
	cout << s << endl;
	return 0;
}

在这里插入图片描述
这里用到了replace函数
在这里插入图片描述
replace函数就是将pos及以后位置的len长度的字符替换为给定的字符

int main()
{
	string s1("hello world");
	s1.replace(0, 2, "***");
	cout << s1 << endl;
	string s2("hello Yuey");
	s2.replace(5, 3, "***");
	cout << s2;
	return 0;
}

在这里插入图片描述
但是上面的代码在空格极多的时候效率极低,我们可以用空间换时间的方法:

int main()
{
	string s("hello world hello Yuey");
	cout << s << endl;
	string tmp;
	for (auto ch : s)
	{
		if (ch == ' ')
		{
			tmp += "%";
		}
		else
		{
			tmp += ch;
		}
	}
	s = tmp;
	cout << s << endl;
	return 0;
}
5.3 substr

在这里插入图片描述
substr是返回从pos位置开始的后面len长度的子字符串,如果没传len函数使用缺省值npos,返回pos位置及以后的所有字符。

int main()
{
	string s("hello world");
	string S1;
	S1 = s.substr(5);
	cout << S1;
	return 0;
}

在这里插入图片描述

5.4 rfind

在这里插入图片描述
rfind是在pos位置反向查找string类、字符串以及字符,并返回第一次出现该字符的下标,没找到返回npos

int main()
{
	string s("Test.cpp.zip");
	// 看文件类型
	size_t pos = s.rfind('.');
	string s1 = s.substr(pos);
	cout << s1;
	return 0;
}
5.5 find_first_of 和 find_last_of

find_first_of:
在这里插入图片描述
这个函数是在字符串中找给定的字符或字符串中的所有字符,并返回第一次找到的地址

int main()
{
	string s("abcdefggfedcba");
	size_t pos = s.find_first_of("abc");
	while (pos != string::npos)
	{
		s[pos] = '*';
		pos = s.find_first_of("abc", pos + 1);
	}
	cout << s;
	return 0;
}

在这里插入图片描述
find_last_of:
在这里插入图片描述
这个可以理解为反向找

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

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

	SplitFilename(str1);
	SplitFilename(str2);

	return 0;
}

这样就能实现文件的路径和文件名分离

5.6 find_first_not_of 和 find_last_not_of

这两个函数是从字符串中找除了给定字符串里的的字符,并返回第一次找到的下标

int main()
{
	string s("abcdefggfedcba");
	size_t pos = s.find_first_not_of("abc");
	while (pos != string::npos)
	{
		s[pos] = '*';
		pos = s.find_first_not_of("abc", pos + 1);
	}
	cout << s;
	return 0;
}

在这里插入图片描述

6.getline

在讲getline之前我们先看一道题目: 字符串最后一个单词的长度
在这里插入图片描述
这种情况下就需要借助getline函数了:getline
在这里插入图片描述
getline默认读取到换行的时候读取结束,读取结束的条件也可以自己设定
在这里插入图片描述
在这里插入图片描述自定义结束条件

结语

以上就讲完了string类的基本用法,对于string类的模拟实现我们下期再讲,感谢大家的阅读,欢迎大家批评指正!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;