【C++】—— string 类的了解与使用

1、 为什么学习string 类

1.1、 C语言中的字符串

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

1.2、 面试题中更多以 string 类出现

  在 OJ 中,有关字符串的题目基本以 string类 的形式出现,而且在常规工作中,为了简单、方便快捷,基本都使用 string类,很少有人去使用 C语言 库中的字符串操作函数。
  

下面是两个面试题(暂不做讲解)

  

2、 标准库中的 string 类

学习 string类,是离不开查阅文档

在使用 string类 时,必须包含 #include<string> 头文件。

   s t r i n g string string 底层类似一个顺序表:是由一个指向一个开辟出的空间的指针,和两个记录字符串长度和空间大小的变量 _size_capacity 组成的
  
简单结构如下:

class string
{
private:
	char* str;//指向存储字符串的空间
	size_t _size;//记录当前字符串长度
	size_t capacity;//记录当前空间大小
};

  
  

3、 string 的默认成员函数

3.1、 string 的构造与拷贝构造

在这里插入图片描述
  :因 s t r i n g string string 产生的比较早,有些部分考虑的并没有那么成熟,所以 s t r i n g string string 设计的有些冗余。只需重点学习标记出来的 3 个构造函数即可

  

  • e m p t y empty empty s t r i n g string string c o n s t r u c t o r ( d e f a u l t c o n s t r u c t o r ) constructor (default constructor) constructor(defaultconstructor)
    • 构造一个空字符串,长度为零个字符
  • c o p y copy copy c o n s t r u c t o r constructor constructor
    • 构造 s t r str str 的副本。
  • s u b s t r i n g substring substring c o n s t r u c t o r constructor constructor
  • f r o m from from c c c- s t r i n g string string
    • 复制 s s s 指向的以 n u l l null null 结尾的字符序列(C 字符串)。
  • f r o m from from b u f f e r buffer buffer
    • s s s 指向的字符数组中复制前 n n n 个字符。
  • f i l l fill fill c o n s t r u c t o r constructor constructor
    • 用字符 c c c n n n 个连续副本填充字符串。
  • r a n g e range range c o n s t r u c t o r constructor constructor
    • 以相同的顺序复制 [ f i r s t first first l a s t last last] 范围内的字符序列。

  
  第三个默认构造函数的缺省值 n o p s nops nops 是什么呢?
   n o p s nops nops s t r i n g string string 类中的一个静态成员变量 n o p s nops nops的值是 -1,但其为 s i z e size size_ t t t 类型,所以实际值为整型的最大值
  编译器认为你的字符串不可能有这么长 (42亿9千万字节),所以 n o p s nops nops 的意思是有多长取多长

在这里插入图片描述

  
我们来一起来实践一下

void Test1()
{
	//使用默认构造函数,不需要传参。
	string s1;
	//带参构造,使用指定字符数组初始化。
	string s2("hello world");
	//使用拷贝构造初始化
	string s3 = s2;
	//使用拷贝构造初始化
	//字符串会隐式类型转换。生成一个临时对象,再用临时对象进行拷贝(实际执行编译器会进行优化)
	string s4 = "你好";

	//string库中重载了流插入与流提取,我们可以直接使用
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;

}

运行结果:

在这里插入图片描述

  
剩下 4 个我们也来看下

void Test2()
{
	string s1("hello world");
	
	//3. 使用一个string的某段区间初始化,其中pos是字符串下标,npos是指无符号整数的最大值。
	string s3(s1, 3, 8);
	//5. 使用的是某个字符数组前n个字符来初始化
	string s5("hello world!", 5);
	//6. 使用的是n个c字符初始化。
	string s6(8, 'a');
	//7. 使用的是某段迭代器区间初始化。(现在看不懂没关系,后面会介绍)
	string s7(s1.begin() + 2, s1.end() - 3);

	cout << s3 << endl;
	cout << s5 << endl;
	cout << s6 << endl;
	cout << s7 << endl;
}

运行结果:

在这里插入图片描述

  
  

3.2、 string 的赋值重载

在这里插入图片描述

  
s t r i n g string string 重载了 3 个赋值运算符重载函数:

  1. 使用 s t r i n g string string对象进行赋值
  2. 使用字符串进行赋值
  3. 使用单个字符进行赋值
void Test3()
{
	//使用拷贝构造进行初始化
	string s1 = "hello world";

	//使用string对象进行赋值
	string s2;
	s2 = s1;

	//使用指定字符串进行赋值
	string s3;
	s3 = "你好";

	//使用指定字符进行赋值
	string s4;
	s4 = 'a';

	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
}

运行结果:

在这里插入图片描述

  这里,其实第二个赋值重载函数可以省略,因为字符串会生成一个临时对象,再用临时对象进行赋值拷贝。

  
  

3.3、 string 的析构函数

在这里插入图片描述

  析构函数很简单,因为会自动调用,这里不做介绍。
  
  

4、 operator[ ]

   s t r i n g string string 类重载了[],使用户可以像数组一样访问 string类 中的字符

在这里插入图片描述

   s t r i n g string string 类重载了两个operator[],一个是普通版的,一个是 c o n s t const const 不可修改版
  
  

4.1、 访问

  有了operator[],我们就可以像数组一个对 s t r i n g string string 进行访问

void Test5()
{
	string s1 = "hello world";

	cout << s1[1] << endl;
	cout << s1[3] << endl;
	cout << s1[6] << endl;
}

在这里插入图片描述

  

4.2、 修改

  同时,因为 operator[] 返回的是字符的引用,这意味着普通版的operator[]不仅仅可以获取相应位置的字符,还能对其进行修改

void Test5()
{
	string s1 = "hello world";
	s1[0] = 'a';
	s1[1] = 'a';
	s1[2] = 'a';
	cout << s1 << endl;
}

在这里插入图片描述

  
  

4.3、 检查越界

  不仅如此,operator[]还能检查是否越界,一旦越界直接报错。这样就能解决我们平时不小心越界却无法检查出来的困扰啦。

void Test5()
{
	string s1 = "hello world";
	s1[20];
}

在这里插入图片描述

  
  

5、 string类的三种遍历方式

5.1、 下标访问遍历

  我们先来看两个函数接口:sizelength
  

5.1.1、 size和length函数

在这里插入图片描述

size函数接口

在这里插入图片描述

length函数接口
  • sizelength两函数的功能是一样的:返回字符串的长度
    :计算出的长度不包含字符串中 ‘\0’

  size()length()方法底层实现原理完全相同,引入 size() 的原因是为了与其他容器的接口保持一致,一般情况下基本都是用 s i z e ( ) size() size()
  
  

5.1.2、 下标访问

  下标访问遍历的方式和数组的访问类似,我们直接上代码

void Test6()
{
	string s1 = "hello world";

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

运行结果:

在这里插入图片描述

  
  

5.2、 迭代器访问遍历

5.2.1、 初识迭代器访问

  首先,我们先看一下迭代器访问的写法:

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

运行结果:

在这里插入图片描述

  

看不懂没关系,我们现在来讲解。

  • 迭代器属于其对应容器的类域。比如说 s t r i n g string string,就是 s t r i n g : : i t e r a t o r string::iterator string::iterator;后面还会学顺序表 v e c t o r vector vector;就是 v e c t o r < i n t > : : i t e r a t o r vector <int>::iterator vector<int>::iterator
  • string::iterator it:我们用 s t r i n g string string 的迭代器定义了一个对象 i t it it。我们可以将 i t it it 想象成一个指针(但它底层不一定是指针),它的用法完全是跟指针类似的
  • s1.begin();begin()是规定返回这块空间开始位置的迭代器;s1.end()是最后一个有效字符的下一个位置(这里是 ‘\0’ 位置)
  • 上述代码逻辑是:当 it 不等于end()时,对其进行解引用。 i t it it 不是指针怎么进行解引用呢?可以进行运算符重载 operator*。再接着 ++it i t it it 往后移一位,不是原生指针同样进行运算符重载。

  

在这里插入图片描述

  

在这里插入图片描述

begin函数接口

在这里插入图片描述

end函数接口

  

  迭代器提供了一种 通用的 访问容器的方式所有的容器都可以用这种方式访问,而不需要关心容器的具体实现细节。掌握了 s t r i n g string string 的迭代器访问方式,就掌握了其他所有容器的访问方式,他们都会提供统一的接口

  我们可以通过迭代器进行修改

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

运行结果:

在这里插入图片描述

  

  当然,如果对象本身是被 c o n s t const const 修饰,那就不能用普通迭代器了,因为普通迭代器可读可写,这时就要用 const迭代器 了。

void Test12()
{
	const string s1 = "hello world";
	string::const_iterator it = s1.begin();  //const迭代器
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

  const迭代器的特点是只读,其不能修改指向的内容,但其自身是可以修改的(它自己还要++呢)
  
  

5.2.2、 反向迭代器

  上述使用迭代器string::iterator来遍历,我们将其称为正向迭代器。此外,还有反向迭代器string::reverse_iterator
  反向迭代器是用来倒着遍历的,获取反向迭代器的起始位置用 r b e g i n ( ) rbegin() rbegin() 函数,获取结束位置用 r e n d ( ) rend() rend() 函数

void Test11()
{
	string s1 = "hello world";

	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit;
		
		//这里是++而不是--,它的++是倒着走的,因为是反向迭代器
		++rit;
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

  
  我们可以这样来理解:rbegin指向最后一个有效位置rend指向第一个字符的前一个位置。当然,其实际底层并不一定是这样,只是为了方便我们理解
  

在这里插入图片描述

  

在这里插入图片描述

rbegin函数接口

  

在这里插入图片描述

rend函数接口

  而同样,反向迭代器也是有 c o n s t const const版本 的:const_reverse_iterator。这里就不再演示了

  
  

5.3、 范围 for 访问遍历

5.3.1、 初识范围for访问

  • 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 f o r for for循环 f o r for for 循环后的括号由冒号 “:” 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
  • 范围 f o r for for 可以作用到数组容器对象上进行遍历
  • 范围 f o r for for 的底层很简单,其实就是迭代器,这个从汇编层也可以看到

我们先来看 范围 f o r for for 是怎么写的

void Test8()
{
	string s1 = "hello world";
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

  
  范围for是一种自动赋值、自动迭代、自动判断结束的访问方式。

  • for (auto ch : s1):自动从容器 ( s 1 s1 s1) 中取其每一个值(字符),给 c h ch ch 变量。该变量的类型是 a u t o auto auto(自动推导),这里不写 a u t o auto auto 也可以写 c h a r char char,但一般都写 auto
  • {}中的内容,就是用户需要对容器中每个值的具体操作

  范围 f o r for for 看起来非常厉害,不用自动来迭代和判断,但其本质上这段代码编译以后,会替换成迭代器其底层就是迭代器,就像引用底层时指针一样。所以所有的容器,只要支持迭代器,就支持 范围 f o r for for
  
  当然,范围 f o r for for 也可以用来遍历数组:

void Test10()
{
	int array[] = { 1,2,3,4,5,6,7,8,9,10 };

	for (auto e : array)
	{
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

  
  

5.3.2、 范围for访问的注意事项

  范围 f o r for for 访问也是可以进行修改

void Test8()
{
	string s1 = "hello world";
	for (auto ch : s1)
	{
		ch += 1;
		cout << ch << " ";
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

  可是我们直接打印 s 1 s1 s1 会发现 s 1 s1 s1 的值并没有改变

void Test8()
{
	string s1 = "hello world";
	for (auto ch : s1)
	{
		ch += 1;
		cout << ch << " ";
	}
	cout << endl;

	cout << s1 << endl;
}

运行结果:

在这里插入图片描述

  这是为什么呢?

  for (auto ch : s1):可以简单理解在底层转换成迭代器后 *it 取出 s 1 s1 s1 中的值,拷贝 c h ch ch。既然是拷贝,对 c h ch ch 的修改自然无法改变 s 1 s1 s1 中的值啦。迭代器能够修改,是因为 it 相当于是指针一样,通过指针来修改当然可以修改啦

  范围 f o r for for 想修改应用 引用

void Test8()
{
	string s1 = "hello world";
	for (auto& ch : s1)
	{
		ch += 1;
		cout << ch << " ";
	}
	cout << endl;

	cout << s1 << endl;
}

运行结果:

在这里插入图片描述

  这样, c h ch ch 相当于 s 1 s1 s1 中每个值的别名,就可以对 s 1 s1 s1 的值进行修改啦

  
  

5.3.3、 auto关键字

5.3.3.1、 auto关键字的简单认识

在这里补充 2 个 C++11 的小语法

  • 在早期 C/C++ 中 a u t o auto auto 的含义是:使用 a u t o auto auto 修饰的变量,是具有自动存储器的局部变量,后来这个功能没什么用,没废除了
  • C++11 中,标准委员会变废为宝赋予了 a u t o auto auto 全新的含义,即: a u t o auto auto 不再是存储类型指示符,而是作为一个新的类型指示符来指示编译器 a u t o auto auto 声明的变量必须由编译器在编译时期推导而得
int func1()
{
	return 10;
}
void Test9()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = func1();
	
	//typeid().name()可以帮助我们看变量的类型
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	
}

typeid().name()可以帮助我们看变量的类型。
  
运行结果:

在这里插入图片描述

  
但是 a u t o auto auto 不能直接这样定义:

auto e;

   a u t o auto auto 的具体类型是 根据右边的表达式或者返回值来推导 的,上述定义方式无法推导出 e e e 的类型,这样就不知道给 e e e开多大的空间
  
  auto的价值主要是简化代码,如果类型太长,我们可以使用 a u t o auto auto 让编译器自己来推导。

string::iterator it = s1.begin();
auto it = s1.begin();

map<string, string>::iterator mit = dict.brgin();
auto mit = dict.begin();

  但是 a u t o auto auto 也有一些缺陷,某种程度上 a u t o auto auto 减小了代码的可读性。比如上述代码我们不能一眼看出 i t it it 的类型就是string::iterator
  
  

5.3.3.1、 auto关键字的注意事项
  • auto 声明指针类型时,用 a u t o auto auto a u t o auto auto* 没有任何区别,只是 a u t o auto auto* 必须是指针。但是用 a u t o auto auto 声明引用类型时,必须加 &
void Test9()
{
	int x = 10;
	auto y = &x;
	auto* z = &x;
	auto& m = x;

	cout << typeid(x).name() << endl;
	cout << typeid(y).name() << endl;
	cout << typeid(z).name() << endl;
	cout << typeid(m).name() << endl;
}

在这里插入图片描述

  

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

// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto cc = 3, dd = 4.0;

  

  • a u t o auto auto 不能直接用来声明数组
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
auto array[] = { 4, 5, 6 };

  

  • a u t o auto auto 不能做参数,但是 a u t o auto auto 可以做返回值,但一定要谨慎使用
auto func1()
{
	auto a = 1;
	return a;
}
auto func2()
{
	return func1();
}
auto func3()
{
	return func2;
}
int main()
{
	auto ret = func3();
	return 0;
}

  像这样, r e t ret ret 的类型是什么?看 f u n c 3 func3 func3 的返回值, f u n c 3 func3 func3 去看 f u n c 2 func2 func2 f u n c 2 func2 func2 去看 f u n c 1 func1 func1。如果每个函数又有一大堆逻辑,那么代码效率将会很低,可读性也很低

  
  

6、 string类的容量操作

函数名称功能说明
s i z e size size (重点)返回字符串有效字符长度
l e n g t h length length返回字符串有效字符长度
m a x max max_ s i z e size size返回字符串的最大长度
c a p a c i t y capacity capacity返回空间总大小
e m p t y empty empty (重点)检查字符是否为空串,是返回 t r u e true true,否则返回 f a l s e false false
c l e a r clear clear (重点)清空有效字符
r e s e r v e reserve reserve (重点)为字符串预留空间
r e s i z e resize resize (重点)将有效字符的个数改成 n n n, 多出的空间用字符 c c c 填充

  
  

6.1、 reserve

   r e s e r v e reserve reserve 函数的作用是为字符串预留空间

在这里插入图片描述

void test10()
{
	string s1;
	s1.reserve(50);
	cout << s1.capacity() << endl;
}

在这里插入图片描述

  程序实际上开辟的空间往往是大于等于程序员所要求的空间的,这是为了遵循对齐原则。上述就是对齐到 64,因为 ‘\0’ 是不计入空间的,所以为 63

  这样,我们就能通过 r e s e r v e reserve reserve 函数提前在 s t r i n g string string 中开辟空间,以减少扩容的次数了。
  

但是,当预留空间小于原空间,甚至小于字符个数呢?

  • s i z e size size < n n n < c a p a c i t y capacity capacity :C++ 并没有做出明确规定,编译器可自行选择缩容还是不做处理
  • n n n < s i z e size size:C++ 也没有做出明确规定,编译器也是自行选择是否缩容,但有一点:即使缩容也不改变字符的长度

  在VS编译器中,两种情况都是不缩容

void test11()
{
	string s1 = "hello world hello world hello world";
	cout << "字符个数:" << s1.size() << endl;
	cout << "当前空间" << s1.capacity() << endl;

	s1.reserve(40);
	cout << "当前空间" << s1.capacity() << endl;

	s1.reserve(20);
	cout << "当前空间" << s1.capacity() << endl;
}

在这里插入图片描述

  

在这里插入图片描述

  
  

6.1、 resize

在这里插入图片描述

   r e s i z e resize resize调整字符串的大小,即将字符串调整为 n n n 个字符的长度

  • n 小于当前字符串长度将当前值将缩短为其前 n n n 个字符,并 删除 超出第 n n n 个字符的字符。

  • n 大于当前字符串长度:则通过在末尾插入所需数量的字符来扩展当前内容,以达到 n n n 的大小 (若空间不够,则进行扩容)。如果指定了 c c c,则新元素将初始化为 c c c 的副本,否则,它们是值初始化字符(空字符)。

  

  • n n n < s i z e size size
void test12()
{
	string s1 = "hello world";
	cout << " 字符个数:" << s1.size() << endl;
	cout << " 空间大小:" << s1.capacity() << endl << endl;

	s1.resize(5);
	cout << " 修改后字符个数:" << s1.size() << endl;
	cout << " 修改后空间大小:" << s1.capacity() << endl;
}

在这里插入图片描述

  

  • s i z e size size < n n n < c a p a c i t y capacity capacity
void test12()
{
	string s1 = "hello world";
	cout << " 字符个数:" << s1.size() << endl;
	cout << " 空间大小:" << s1.capacity() << endl << endl;

	s1.resize(13);
	cout << " 修改后字符个数:" << s1.size() << endl;
	cout << " 修改后空间大小:" << s1.capacity() << endl;
}

在这里插入图片描述

  

  • n n n > c a p a c i t y capacity capacity
void test12()
{
	string s1 = "hello world";
	cout << " 字符个数:" << s1.size() << endl;
	cout << " 空间大小:" << s1.capacity() << endl << endl;

	s1.resize(20);
	cout << " 修改后字符个数:" << s1.size() << endl;
	cout << " 修改后空间大小:" << s1.capacity() << endl;
}

在这里插入图片描述

  

在这里插入图片描述

  
  

6.2、 注意事项

  1. size()length() 方法底层实现原理完全相同,引入size() 的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()
  2. clear() 只是将 s t r i n g string string有效字符清空不改变底层空间的大小
  3. resize(size_t n)resize(size_t n, char c)都是将字符串中有效字符个数改变到 n n n。不同的是当字符个数增多时:resize(n)0 来填充多出的元素空间,resize(size_t n,charc)用字符 c 来填充多出的元素空间。注意: r e s i z e resize resize 在改变元素个数时,如果是将元素个数增多可能改变底层容量的大小,如果将元素个数减少底层空间总大小不变
  4. reserve(size_t res_arg=0):为 s t r i n g string string 预留空间,不改变有效元素个数,当 r e s e r v e reserve reserve 的参数小于 s t r i n g string string 的底层空间总大小时, r e s e r v e reserve reserve 的行为是不确定的 。

  
  

7、 string类对象的修改操作

函数名称功能说明
p u s h push push_ b a c k back back在字符串后尾插字符 c c c
a p p e n d append append在字符串后追加一个字符串
o p e r a t o r operator operator+= (重点)在字符串后追加字符或字符串
c c c_ s t r str str (重点)返回 C 格式字符串
f i n d find find从字符串 p o s pos pos 位置开始往后找字符 c c c,返回该字符在字符串中的位置
r f i n d rfind rfind从字符串 p o s pos pos 位置开始往前找字符 c c c,返回该字符在字符串中的位置
s u b s t r substr substr s t r str str 中从 p o s pos pos 位置开始,截取 n n n 个字符,然后将其返回
i n s e r t insert insert在指定位置追加字符或者字符串
e r a s e erase erase删除字符串指定部分
s w a p swap swap (重点)交换两个 s t r i n g string string 对象
f i n d find find _ f i r s t first first _ o f of of在字符串中搜索与其参数中指定的任何字符匹配的第一个字符

  
  

7.1、 operator+=

在这里插入图片描述

  

  • o p e r a t o r operator operator+= 是在后面追加字符或字符串,甚至是 s t r i n g string string 对象
void test14()
{
	string s1 = "hello";
	cout << s1 << endl;

	s1 += 'a';
	cout << s1 << endl;

	s1 += "你好";
	cout << s1 << endl;

	string s2 = "haha";
	s1 += s2;
	cout << s1 << endl;
}

在这里插入图片描述

  通常,我们对 string 对象进行尾插都是用 o p e r a t o r operator operator+=函数,因为 ‘+=’ 生动形象。当然尾插还有 push_backappend 等函数
  
  

7.2、 c_str

在这里插入图片描述

   c c c_ s t r str str 函数的功能是获取存储字符串空间的地址

   s t r i n g string string底层 简单来看如下:

class string
{
private:
	char* _str;//指向存储字符串的空间
	size_t _size;//记录当前字符串长度
	size_t _capacity;//记录当前空间大小
};

  
  而 c c c_ s t r str str 就是获取 _ s t r str str

void test16()
{
	string s1 = "hello world";
	printf("%p\n", s1.c_str());
	printf("%s\n", s1.c_str());
}

在这里插入图片描述

  
  

7.3、 swap

在这里插入图片描述

  swap函数交换两个 s t r i n g string string 对象。

  可能有小伙伴很疑惑,C++ 库中不是有 s w a p swap swap模板 吗?为什么 s t r i n g string string 库中又要写一个 s w a p swap swap函数 呢?
  这肯定是因为 s w a p swap swap模板 生成的 s w a p swap swap函数 有缺点
  
  我们一起来看下 s w a p swap swap模板 的缺点。

  • 下面是 s w a p swap swap模板 的内部实现
      在这里插入图片描述

  
  当交换两个 s t r i n g string string 对象时,生成的函数是这样的:

void swap(string& a, string& b)
{
	string c(a);
	a = b;
	b = c;
}

  

  • 如果是传统的交换,会进行 3次 深拷贝
    • 首先是用 a a a 拷贝构造 c c c
    • b b b 拷贝给 a a a
    • 最后是 c c c 拷贝给 b b b
  • 3次 拷贝都是深拷贝
      

  对自定义类型来说,深拷贝的代价是很大的,每次深拷贝都要开空间拷贝数据。而你现在还是深拷贝 3 次,效率无疑是大大降低

  
  那有没有办法提升效率呢?
  首先,我们知道, s t r i n g string string 底层类似一个顺序表:

class string
{
private:
	char* _str;//指向存储字符串的空间
	size_t _size;//记录当前字符串长度
	size_t _capacity;//记录当前空间大小
};

  

  那么我们可不可以就两个对象指针所指向的空间进行交换。这样,仅仅是内置类型进行交换效率会大大提高
  在这里插入图片描述
  
  再把两个对象中的 _size_capacity 各自交换一下,就完成啦

  
  实际上, s t r i n g string string 库中就是这么实现的

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

  而我们知道,当模板和函数命名重合时,若函数更适合,则优先调用函数。因此是不会调用库中的函数模板的。
  
  

8、 string类非成员函数

函数名称功能说明
o p e r a t o r operator operator+尽量少用,因为传值返回,导致深拷贝效率低
o p e r a t o r operator operator>> (重点)输入运算符重载
o p e r a t o r operator operator<< (重点)输出运算符重载
g e t l i n e getline getline (重点)获取一行字符串
r e l a t i o n a l relational relational o p e r a t o r s operators operators(重点)大小比较
s w a p swap swap (重点)交换两个对象

  
  

8.1、 getline

在这里插入图片描述

  getline函数是从输入流中获取一串字符串到 s t r i n g string string 对象中,以 d e l i m delim delim 为结束标志,默认是 ‘\n’

  那 g e t l i n e getline getline 有什么用呢?库中不是重载了 o p e r a t o r operator operator>> 吗?
  我们知道,用 c i n cin cin 输入字符串默认是以 “ ”“\n” 为分隔符的,因此即使读到了 “ ”“\n” c i n cin cin 也会忽略他们,将他们跳过。
  当我们想输入的字符串中有 “ ”“\n” 时,就可以用 g e t l i n e getline getline 函数
  

void test15()
{
	string s1;
	cout << "请输入字符串:";
	getline(cin, s1);
	cout << "s1内容:" << s1 << endl;
}

在这里插入图片描述

  

void test15()
{
	string s1;
	cout << "请输入字符串:";
	getline(cin, s1, '#');
	cout << endl;
	cout << "s1内容:" << endl << s1 << endl;
}

在这里插入图片描述

  
  

8.2、 swap

  非成员的 swap函数 的实际行为与成员函数中 swap 的行为是一样的,这里就不再过多介绍
  
  

9、 不同平台下string类的结构

  注:下述结构是在 32 位平台下进行验证,32位平台下指针占 4 字节

9.1、 VS 下string类的结构

  VS 下 s t r i n g string string 总共占 28 个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义 s t r i n g string string 中字符串的存储空间

  • 当字符串长度小于 16 时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于16 时,使用从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
	value_type _Buf[_BUF_SIZE];
	pointer _Ptr;
	char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
class String
{
private:
	//vs下string类里面的成员变量大概是这样
	char _buff[16];
	char* str;
	size_t _size;
	size_t capacity;
};
int main()
{
	cout << sizeof(String) << endl;
	return 0;
}

在这里插入图片描述

  这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16,那 s t r i n g string string 对象创建好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。

  其次:还有一个 s i z e size size _ t t t 字段保存字符串长度,一个 s i z e size size_ t t t 字段保存从堆开辟空间的容量
  最后:还有一个指针做一下其他事情

  故总共占 16 + 4 + 4 = 28 个字节

在这里插入图片描述

  
VS 下 s t r i n g string string 的扩容

void test17()
{
	string s;
	size_t sz = s.capacity();
	cout << "原始大小:" << sz << endl;
	cout << "making s grow:" << endl;
	for (int i = 0; i < 1000; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity change:" << sz << "\n";
		}
	}
}

在这里插入图片描述

  capacity函数所获得的空间是容纳有效字符的最大空间,是不包括 ‘\0’ 的,实际空间还要再 +1

  我们可以看到,在 VS 下,从数组转到堆开辟的空间时,是 2 倍扩容,即16 -> 32;之后在堆上的扩容都是 1.5倍 扩容

  
  

9.2、 g++ 下string类的结构

  g++ 下, s t r i n g string string 是通过写实拷贝实现的, s t r i n g string string 对象总共占4个字节,内部只含有一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符串有效长度
  • 引用计数1
  • 指向堆空间的指针,用来存储字符串

  

在这里插入图片描述

struct _Rep_base
{
	size_type _M_length;
	size_type _M_capacity;
	_Atomic_word _M_refcount;
};

  
  g++ 下, s t r i n g string string 的扩容

在这里插入图片描述

  
运行结果:

在这里插入图片描述

  可以看到,g++ 下是标准的 2 倍扩容


  1. 引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源, 如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。想进一步了解可浏览下面两篇文章:写实拷贝写实拷贝在读取时是缺陷的 ↩︎

  • 65
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 87
    评论
评论 87
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值