当前位置: 首页 > article >正文

【STL】string类

C C C++ 中, s t r i n g string string 是标准库提供的一个类,用于处理字符串。它是基于模板的容器类,提供了许多成员函数和操作符,用于对字符串进行各种操作,比如插入删除查找连接等。与 C C C 风格的字符串相比, s t r i n g string string 类更加安全和方便,它负责自动管理字符串内存,具有动态大小调整的能力。

文章目录

  • 前言 —— STL 简介
    • 1. 什么是 STL ?
    • 2. STL 的版本(了解)
    • 3. STL 的六大组件
  • 一、为什么学习 string 类?
    • 1. C语言中的字符串
    • 2. OJ 题
  • 二、如果学习 string 类?(查文档)
  • 三、补充 2 个 C++11 的知识点
    • 1. auto 关键字
    • 2. 范围 for
  • 四、迭代器(iterator)
  • 五、string 类的使用(常用接口)
    • 1. 常见构造
    • 2. 容量操作
    • 3. 访问及遍历操作
    • 4. 修改操作
    • 5. 非成员函数
  • 六、vs 和 g++ 下 string 结构的说明(了解)
    • 1. vs 下 string 的结构
    • 2. g++ 下 string 的结构
  • 七、string 类的拷贝问题
    • 1. 浅拷贝
    • 2. 深拷贝
    • 3. 写时拷贝(了解)
  • 八、string 类的模拟实现
  • 总结


前言 —— STL 简介

本篇博客作为 S T L STL STL 系列的开篇,首先要介绍一下什么是 S T L STL STL。不过要注意的是, s t r i n g string string 类不属于 S T L STL STL 的六大组件,而是在 S T L STL STL 出现之前就已经存在了。不过 s t r i n g string string 在实现和使用方式和 S T L STL STL 容器极为相似,因此把 s t r i n g string string 类暂且归为 S T L STL STL 部分学习。

1. 什么是 STL ?

S T L STL STL s t a n d a r d   t e m p l a t e   l i b a r a y standard\ template\ libaray standard template libaray - 标准模板库):是C++标准库重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。

2. STL 的版本(了解)

  1. 原始版本

A l e x a n d e r S t e p a n o v 、 M e n g L e e Alexander Stepanov、Meng Lee AlexanderStepanovMengLee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 H P HP HP 版本 —— 所有 S T L STL STL 实现版本的始祖。

  1. P . J . P. J. P.J. 版本

P .   J .   P l a u g e r P.\ J.\ Plauger P. J. Plauger 开发,继承自 H P HP HP 版本,被 W i n d o w s   V i s u a l   C + + Windows\ Visual\ C++ Windows Visual C++ 采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。

  1. R W RW RW 版本

R o u g e   W a g e Rouge\ Wage Rouge Wage 公司开发,继承自 H P HP HP 版本,被 C + +   B u i l d e r C++\ Builder C++ Builder 采用,不能公开或修改,可读性一般。

  1. S G I SGI SGI 版本

S i l i c o n   G r a p h i c s   C o m p u t e r   S y s t e m s Silicon\ Graphics\ Computer\ Systems Silicon Graphics Computer Systems I n c Inc Inc 公司开发,继承自 H P HP HP 版本。被 G C C ( L i n u x ) GCC(Linux) GCC(Linux) 采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面 S T L STL STL 要阅读部分源代码,主要参考的就是这个版本。

3. STL 的六大组件

在这里插入图片描述

可以看出, S T L STL STL 的主要内容是一个包罗数据结构与算法的软件框架。模板的出现大大提高了代码的复用性, S T L STL STL 使我们方便了非常多,不用再像 C C C 语言一样手搓算法和数据结构了。在算法竞赛中, S T L STL STL 也是一个常用且有力的工具。


一、为什么学习 string 类?

1. C语言中的字符串

C C C 语言中,字符串是以 ‘ \ 0 ’ ‘\backslash 0’ ‘\0’ 结尾的一些字符的集合。为了操作方便, C C C 标准库中提供了一些 s t r str str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合 O O P OOP OOP O b j e c t   O r i e n t e d   P r o g r a m m i n g Object\ Oriented\ Programming Object Oriented Programming)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问

因此,在 C++ 中,采用面向对象的思想,把 s t r i n g string string 这个常用的字符串封装成一个 s t r i n g string string ,这样我们用户想使用 s t r i n g string string 这个字符串类型的时候,只需要调用其接口即可。

2. OJ 题

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

以下面一道面试题为例:

【题目信息】 L e e t C o d e   415. LeetCode\ 415. LeetCode 415. 字符串相加

在这里插入图片描述

【题目解析】

这道题是高精度加法,当我们遇到很大的两个数(即使开 l o n g   l o n g long\ long long long 也存不下)做加法运算的时候,我们就要用字符串来存下数字的每一位,然后模拟竖式运算,通过各位相加、进位加 1 1 1 等操作,来实现大整数的加法。题目的具体做法思路如下:

  1. 字符串转化为数字数组:

首先,我们知道两个高精度整数是以字符串的形式存储的,因此为了方便逐位相加,我们需要将字符串转换成倒序的数组,使得个位对齐,这样可以从低位向高位依次相加。

  1. 逐位相加并处理进位:

对于每一位数字,两个数组中的对应位相加,记录进位。进位部分会加到下一位的相加过程中。如果当前位的和大于等于 10 10 10,则需要将该位的值取模 10 10 10,余数保留,进位值加到下一位。

  1. 处理进位后的结果:

在完成所有位的相加后,如果最高位有进位,则需要在结果中保留该位。否则,最高位是 0 0 0 时则忽略。

  1. 将结果转回字符串输出:

最后,将结果数组从高位到低位依次转换为字符串,并输出最终结果。

【代码示例】

class Solution {
public:
    string addStrings(string a, string b) {
        int x[10000] = {0}, y[10000] = {0}, z[10000] = {0};
    
        // 将字符串 a, b 转换为倒序数组
        for (int i = 0; i < a.size(); i++) 
            x[a.size() - 1 - i] = a[i] - '0';
        for (int i = 0; i < b.size(); i++) 
            y[b.size() - 1 - i] = b[i] - '0';
        
        // 求和并处理进位
        int len = max(a.size(), b.size());
        for (int i = 0; i < len; i++) {
            z[i] += x[i] + y[i];  // 当前位相加
            z[i + 1] += z[i] / 10;  // 处理进位
            z[i] %= 10;  // 保留个位
        }
        
        // 检查最高位是否有进位
        if (z[len] != 0) len++;
        
        // 构造返回结果字符串
        string ans;
        for (int i = len - 1; i >= 0; i--)
            ans.push_back(z[i] + '0');
        
        return ans;
    }
};

二、如果学习 string 类?(查文档)

不只是 s t r i n g string string 类,我们在学习 S T L STL STL 的时候一定要勤于查 C++ 官方文档 s t r i n g string string 类的文档介绍

在这里插入图片描述

注意:在使用 s t r i n g string string 类时,必须包含 # i n c l u d e include include < s t r i n g string string > 头文件以及 u s i n g   n a m e s p a c e   s t d ; using\ namespace\ std; using namespace std;


三、补充 2 个 C++11 的知识点

1. auto 关键字

C++11 中, a u t o auto auto 是一个类型指示符来指示编译器, a u t o auto auto 声明的变量必须由编译器在编译时期推导而得。(自动识别类型注意:只有C++11以上的版本才支持auto。

实际上, a u t o auto auto 最大的用途是为了简化代码:(但是一定程度上牺牲了可读性)

string s;

string::iterator it = s.begin();

//像迭代器这种长类型可以直接用auto代替,来简化代码
auto it = s.begin();

除此之外,还有一些注意事项:

  1. a u t o auto auto 声明指针类型时,用 a u t o auto auto a u t o ∗ auto^* auto 没有任何区别(自动识别类型),但用 a u t o auto auto 声明引用类型时则必须加 & \& & & \& & 代表可修改)。
auto x = 1;		//int

auto y = &x;	//int*
auto* z = &x;	//int*

cout << typeid(x).name() << endl;	//输出x的类型
cout << typeid(y).name() << endl;	//输出y的类型
cout << typeid(z).name() << endl;	//输出z的类型

//int
auto m = x;			
m = 2;
cout << x << endl;

//int&
auto& n = x;		
n = 2;
cout << x << endl;

运行结果为:

在这里插入图片描述

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

//error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto c = 3, d = 4.0;
  1. a u t o auto auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用。
auto add(int a, int b)
{
	return a + b;
}

//error C3533: 参数不能为包含“auto”的类型
int sub(auto b, int c)
{
	return b - c;
}

int main()
{
	add(2, 1);

	sub(4, 3);
}
  1. a u t o auto auto 不能直接用来声明数组
//error C3318 : “auto[]” : 数组不能具有其中包含“auto”的元素类型
auto a[] = { 1,2,3 };

2. 范围 for

C++11 中,引入了基于范围的 f o r for for 循环。 f o r for for 循环后的括号由冒号 : : : 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。(范围 f o r for for 的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到)

范围 f o r for for 可以作用到数组和容器对象上进行遍历:

int a[] = { 1,2,3,4,5 };

//C++98的遍历
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
	cout << a[i] << " ";
}
cout << endl;

//C++11的遍历

//a不可修改(传值)
for (auto i : a)
{
	cout << i << " ";
}
cout << endl;

//加上&代表可修改(传引用)
for (auto& i : a)
{
	i *= 2;
	cout << i << " ";
}
cout << endl;

运行结果为:

在这里插入图片描述


四、迭代器(iterator)

  1. i t e r a t o r iterator iterator
string s("hello world");

for (string::iterator it = s.begin(); it != s.end(); it++)
{
	cout << *it;	//hello world
}
  1. r e v e r s e _ i t e r a t o r reverse\_iterator reverse_iterator
string s("hello world");

for (string::reverse_iterator it = s.rbegin(); it != s.rend(); ++it)
{
	cout << *it;	//dlrow olleh
}
  1. c o n s t _ i t e r a t o r const\_iterator const_iterator
const string s = "hello world";

for (string::const_iterator it = s.cbegin(); it != s.cend(); it++)
{
	//error C3892: “it”: 不能给常量赋值
	//*it += 1;
	cout << *it;	//hello world
}
  1. c o n s t _ r e v e r s e _ i t e r a t o r const\_reverse\_iterator const_reverse_iterator
const string s = "hello world";

for (string::const_reverse_iterator it = s.crbegin(); it != s.crend(); it++)
{
	//error C3892: “it”: 不能给常量赋值
	//*it += 1;
	cout << *it;	//dlrow olleh
}

五、string 类的使用(常用接口)

下面只列举了 s t r i n g string string 最常用的几个接口,更多详细信息可以自行查官方文档: s t r i n g string string

1. 常见构造

构造( c o n s t r u c t o r constructor constructor)函数:

函数名称功能说明
s t r i n g ( ) string() string()重点构造空的 s t r i n g string string 类对象,即空字符串
s t r i n g ( c o n s t   c h a r ∗   s ) string(const\ char^*\ s) string(const char s)重点 C − s t r i n g C-string Cstring (字符数组)来构造 s t r i n g string string 类对象
s t r i n g ( s i z e _ t   n ,   c h a r   c ) string(size\_t\ n,\ char\ c) string(size_t n, char c) s t r i n g string string 类对象中包含 n n n 个字符 c c c
s t r i n g ( c o n s t   s t r i n g &   s ) string(const\ string\&\ s) string(const string& s)重点拷贝构造函数
void Member_functions()
{
	string s1; 				//1.构造空的string类对象s1
	string s2("hello bit"); //2.用C格式字符串构造string类对象s2
	string s3(s2); 			//3.拷贝构造s3
}

2. 容量操作

函数名称功能说明
s i z e size size重点返回字符串有效字符长度
l e n g t h length length重点返回字符串有效字符长度
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 填充
// 测试string容量相关的接口
// size/clear/resize
void Capacity1()
{
	string s("hello, ybc!!!");
	cout << "s.size():    " << s.size() << endl;
	cout << "s.length():  " << s.length() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << "s:           " << s << endl;
	cout << endl;
	
	// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s.clear();
	cout << "s.size():    " << s.size() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << "s:           " << s << endl;
	cout << endl;

	// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << "s.size():    " << s.size() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << "s:           " << s << endl;
	cout << endl;

	// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << "s.size():    " << s.size() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << "s:           " << s << endl;
	cout << endl;
	
	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << "s.size():    " << s.size() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << "s:           " << s << endl;
	cout << endl;
}

在这里插入图片描述

void Capacity2()
{
	string s;

	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << "s.size():    " << s.size() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << endl;
	
	// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
	s.reserve(50);
	cout << "s.size():    " << s.size() << endl;
	cout << "s.capacity():" << s.capacity() << endl;
	cout << endl;
}

在这里插入图片描述

如果 s t r i n g string string 为空,那么我们当我们插入数据的时候, s t r i n g string 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';
		}
	}
}

在这里插入图片描述

构建 s t r i n g string string 时,如果提前已经知道 s t r i n g string string 中大概要放多少个元素,可以提前将 s t r i n g string string 中空间设置好:

void TestPushBackReserve()
{
	string s;
	s.reserve(100);
	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';
		}
	}
}

在这里插入图片描述

因此,利用 r e s e r v e reserve reserve 提高插入数据的效率,可以避免增容带来的开销。

总结:

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

  2. c l e a r ( ) clear() clear() 只是将 s t r i n g string string有效字符清空,不改变底层空间大小 c a p a c i t y capacity capacity)。

  3. r e s i z e ( s i z e _ t   n ) resize(size\_t\ n) resize(size_t n) r e s i z e ( s i z e _ t   n , c h a r   c ) resize(size\_t\ n, char\ c) resize(size_t n,char c) 都是将字符串中有效字符个数改变到 n n n 个。
    不同的是:当字符个数增多时 r e s i z e ( n ) resize(n) resize(n) 0 0 0 来填充多出的元素空间, r e s i z e ( s i z e _ t   n , c h a r   c ) resize(size\_t\ n, char\ c) resize(size_t n,char c) 用字符 c c c 来填充多出的元素空间。

注意: r e s i z e resize resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小(扩容);如果是将元素个数减少,底层空间总大小不变(不缩)。

  1. r e s e r v e ( s i z e _ t   r e s _ a r g = 0 ) reserve(size\_t\ res\_arg=0) 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 r reserver reserver 不会改变容量大小。

3. 访问及遍历操作

函数名称功能说明
o p e r a t o r [   ] operator[\ ] operator[ ]重点返回 p o s pos pos 位置的字符, c o n s t   s t r i n g const\ string const string 类对象调用
b e g i n + e n d begin+ end begin+end b e g i n begin begin 获取第一个字符的迭代器 + + + e n d end end 获取最后一个字符下一个位置的迭代器
r b e g i n + r e n d rbegin + rend rbegin+rend r b e g i n rbegin rbegin 获取最后一个字符的迭代器 + + + r e n d rend rend 获取第一个字符下一个位置的迭代器
范围 f o r for for C C C++ 11 11 11 支持更简洁的范围 f o r for for 的新遍历方式

s t r i n g string string 的遍历一般是:

  1. b e g i n ( ) + e n d ( ) begin()+end() begin()+end()

  2. f o r + o p e r a t o r [   ] for+operator[\ ] for+operator[ ] / / / 范围 f o r for for

注意: s t r i n g string string 遍历时使用最多的还是 f o r for for + + + 下标,或者范围 f o r for for C C C ++ 11 11 11 后才支持); b e g i n ( ) + e n d ( ) begin()+end() begin()+end() 大多数使用在需要使用 S T L STL STL 提供的算法操作 s t r i n g string string 时,比如:采用 r e v e r s e reverse reverse 逆置 s t r i n g string string

1. 访问操作:

void Element_access1()
{
	string s1("hello ybc");
	const string s2("Hello ybc");
	cout << s1 << " " << s2 << endl;
	cout << s1[0] << " " << s2[0] << endl;
	
	s1[0] = 'H';
	cout << s1 << endl;

	// s2[0] = 'h';   代码编译失败,因为const类型对象不能修改
}

在这里插入图片描述

2. 遍历操作:

void Element_access2()
{
	string s("hello ybc");

	// 1. for + operator[]
	for (auto i = 0; i < s.size(); ++i)
		cout << s[i];
	cout << endl << endl;
	
	// 2.迭代器
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it;
		++it;
	}
	cout << endl << endl;
	
	// string::reverse_iterator rit = s.rbegin();
	// C++11之后,直接使用auto定义迭代器,让编译器推导迭代器的类型
	auto rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit;
		++rit;
	}
	cout << endl << endl;
	
	// 3.范围for
	for (auto ch : s)
		cout << ch;
	cout << endl << endl;
}

在这里插入图片描述

4. 修改操作

函数名称功能说明
p u s h _ b a c k push\_back push_back在字符串后尾插字符 c c c
a p p e n d append append在字符串后追加一个字符串
o p e r a t o r   + = operator\ += operator +=重点在字符串后追加字符串 s t r str str
c _ s t r c\_str c_str重点返回 C C C 格式字符串
f i n d + n p o s find + npos find+npos重点从字符串 p o s pos pos 位置开始往后找字符 c c c,返回该字符在字符串中的位置
r f i n d + n p o s rfind + npos rfind+npos重点从字符串 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重点从字符串 p o s pos pos 位置开始,插入 n n n 个字符 c c c / / / 字符串 s t r i n g string string
e r a s e erase erase重点从字符串 p o s pos pos 位置开始,删除长度为 n n n 的字符串
r e p l a c e replace replace重点从字符串 p o s pos pos 位置开始,将 n n n 个字符替换成字符串 s t r i n g string string

注意: n p o s npos npos s t r i n g string string 里面的一个静态成员变量:static const size_t npos = -1;

测试 s t r i n g string string

  1. 插入(拼接)方式: p u s h _ b a c k / a p p e n d / o p e r a t o r + = / i n s e r t push\_back / append / operator+= / insert push_back/append/operator+=/insert

  2. 正向和反向查找: f i n d ( ) + r f i n d ( ) find() + rfind() find()+rfind()

  3. 截取子串: s u b s t r ( ) substr() substr()

  4. 删除: e r a s e erase erase

void Modifiers()
{
	string str;
	
	str.push_back(' ');   // 在str后插入空格
	str.append("hello "); // 在str后追加一个字符"hello "
	str += 'y';           // 在str后追加一个字符'y'   
	str += "bc";          // 在str后追加一个字符串"bc"
	
	cout << str << endl;		   // 以string方式打印字符串
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串
}

在这里插入图片描述

【应用1】获取 f i l e file file 的后缀

string file("string.cpp");

//1.从file中找到.的下标pos
size_t pos = file.rfind('.');
//2.从pos开始,截取pos及pos后面的字符子串
string suffix(file.substr(pos, file.size() - pos));

cout << suffix << endl;

在这里插入图片描述

【应用2】取出 u r l url url 中的域名

string url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;

//找到域名的协议前缀
size_t start = url.find("://");
//如果找不到协议前缀(说明不是协议)
if (start == string::npos)
{
	cout << "invalid url" << endl;
	return 0;
}
//找到域名的起始位置
start += 3;
//找到域名的终止位置
size_t finish = url.find('/', start);
//获取域名子串
string address = url.substr(start, finish - start);

cout << address << endl;

在这里插入图片描述

【应用3】删除 u r l url url 的协议前缀

string url("http://www.cplusplus.com/reference/string/string/find/");

//找到域名的协议前缀
size_t pos = url.find("://");
//删除协议前缀
url.erase(0, pos + 3);

cout << url << endl;

在这里插入图片描述

string s = "o world ";

//头插
s.insert(0, "hell");
//尾插10个字符a
s.insert(s.size(), 10, 'a');、

cout << s << endl;

//删除空格后的所有元素(到npos结束)
auto pos = s.find(' ');
s.erase(pos + 1);

cout << s << endl;

//把hello替换成nihao
s.replace(0, s.size(), "nihao");

cout << s << endl;

在这里插入图片描述

总结:

  1. s t r i n g string string 尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下 s t r i n g string string 类的 += 操作用的比较多,+= 操作不仅可以连接单个字符,还可以连接字符串

  2. s t r i n g string string 操作时,如果能够大概预估到放多少字符,可以先通过 r e s e r v e reserve reserve 把空间预留好。

5. 非成员函数

这里主要介绍的是 s t r i n g string 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重点获取一行字符串,遇到 d e l i m delim delim 停止读入(默认是 ’ \0 ’ )
r e l a t i o n a l   o p e r a t o r s relational\ operators relational operators重点大小比较

在这里插入图片描述

g e t l i n e getline getline 这个函数可以获取一整行字符串,而 c i n cin cin 输入的字符串自动到空格结束
因此, g e t l i n e getline getline 的也是非常有用的,要注意其第一个参数为输入流

void Non_member_function_overloads()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s1, s2;

	getline(cin, s1);
	getline(cin, s2);
	cout << "s1:" << s1 << endl << "s2:" << s2 << endl;
	
	if (s1 < s2) cout << "s1 < s2" << endl;
	else if (s1 > s2) cout << "s1 > s2" << endl;
	else if (s1 == s2) cout << "s1 == s2" << endl;
	
	string s3 = s1 + " " + s2; cout << "s3:" << s3 << endl;
}

在这里插入图片描述

注意:这里字符串比较大小是按照字典序比较的(不是按照长度比较)。


六、vs 和 g++ 下 string 结构的说明(了解)

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

1. vs 下 string 的结构

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

  1. 当字符串长度小于 16 16 16,使用内部固定的字符数组 B u f Buf Buf)来存放。
  2. 当字符串长度大于等于 16 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;

这种设计也是有一定道理的:

1. 首先:大多数情况下字符串的长度都小于 16 16 16,那 s t r i n g string string 对象创建好之后,内部已经有了 16 16 16 个字符数组的固定空间,不需要通过堆创建,效率高。

2. 其次:还有一个 s i z e _ t size\_t size_t 字段保存字符串长度,一个 s i z e _ t size\_t size_t 字段保存从堆上开辟空间总的容量

3. 最后:还有一个指针做一些其他事情。

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

在这里插入图片描述

2. g++ 下 string 的结构

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

  1. 空间总大小 l e n g t h length length
  2. 字符串有效长度 c a p a c i t y capacity capacity
  3. 引用计数 r e f c o u n t refcount refcount
  4. 指向堆空间的指针,用来存储字符串。
struct _Rep_base
{
	size_type _M_length;
	size_type _M_capacity;
	_Atomic_word _M_refcount;
};

七、string 类的拷贝问题

1. 浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

在这里插入图片描述

2. 深拷贝

深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。如果一个类中涉及到资源的管理,其拷贝构造函数赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

1. 传统写法:

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

//赋值重载
string& operator = (const string& s)
{
	if (this != &s)
	{
		delete[] _str;
		_str = new char[s._size + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

2. 现代写法:

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

//拷贝构造
string(const string& s)
{
	string tmp(s._str);	//调用构造函数
	swap(tmp);
}

//赋值重载
string& operator = (string tmp)	//直接传拷贝
{
	swap(tmp);
	return *this;
}

在这里插入图片描述

3. 写时拷贝(了解)

写时拷贝:在浅拷贝的基础之上增加了引用计数的方式来实现的。(一种拖延症)

由于深拷贝需要开空间,消耗大;而浅拷贝会有以下两个问题(以两个对象指向同一块空间为例):

  1. 同一块空间会被析构两次

  2. 一个修改会影响另一个

因此,引入了引用计数来优化深浅拷贝:

引用计数:用来记录资源使用者的个数。(有几个对象指向这个资源)

在构造时,将资源的计数给成 1 1 1,每增加一个对象使用该资源,就给计数增加 1 1 1,当某个对象被销毁时,先给该计数减 1 1 1,然后再检查是否需要释放资源,如果计数为 1 1 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

总结:写时拷贝使其只有在被用到的时候才会进行深拷贝,避免了许多不必要的消耗。(博弈:拷贝后,如果不写就赚了)


八、string 类的模拟实现

对于 C C C++ 官方文档中,对于 s t r i n g string string 的实现是对 b a s i c _ s t r i n g basic\_string basic_string 的重命名,主要是用来处理 U T F − 8 UTF-8 UTF8 c h a r char char 类型数据的。这里模拟实现的是 s t r i n g string string 类( c h a r char char 类型顺序表)的大部分基本功能,依然采用声明与定义分离的形式,最后进行测试(一些短小且频繁调用的函数不用分离,相当于设置成内联函数)。

总共分为三个文件: s t r i n g . h string.h string.h 用来存放 s t r i n g string string 类的声明; s t r i n g . c p p string.cpp string.cpp 用来定义声明的函数; t e s t . c p p test.cpp test.cpp 用来测试。

注意:这里每个文件中的 s t r i n g string string 都是自己定义的,为了和 s t d : : s t r i n g std::string std::string 作区分,我们要单独创建一个命名空间,这里我用的是 n a m e s p a c e   y b c namespace\ ybc namespace ybc

  1. s t r i n g . h string.h string.h
#define _CRT_SECURE_NO_WARNINGS 

#pragma once

#include<iostream>

#include<assert.h>

#include<string>

using namespace std;

namespace ybc
{
	class string
	{
	public:
		typedef char* iterator;

		typedef const char* const_iterator;

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

		const_iterator cbegin() const
		{
			return _str;
		}

		const_iterator cend() const
		{
			return _str + _size;
		}

		//短小且频繁调用的函数,直接定义到类里面,默认为内联inline
	
		/*string()
			:_str(new char[1] {'\0'})
			,_size(0)
			,_capacity(0)
		{}*/

		string(const char* str = "")
		{
			_size = strlen(str);
			_str = new char[strlen(str) + 1];
			_capacity = _size;	//capacity不包含'\0'
			strcpy(_str, str);
		}

		/*string(const string& s)
		{
			_str = new char[s._size + 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 tmp(s._str);	//调用构造函数
			swap(tmp);
		}

		/*string& operator = (const string& s)
		{
			if (this != &s)
			{
				delete[] _str;
				_str = new char[s._size + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}*/
		
		string& operator = (string tmp)
		{
			swap(tmp);
			return *this;
		}

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

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

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

		size_t size() const
		{
			return _size;
		}

		size_t length() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}		

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

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

		//声明和定义分离

		void reserve(size_t n);

		void push_back(char c);

		void append(const char* s);

		string& operator += (char c);

		string& operator += (const char* s);

		void insert(size_t pos, char c);

		void insert(size_t pos, const char* s);

		void erase(size_t pos, size_t len = npos);

		size_t find(char c, size_t pos = 0);

		size_t find(const char* s, size_t pos = 0);

		string substr(size_t pos = 0, size_t len = npos);

	private:
		char* _str = nullptr;
		
		size_t _size = 0;
		
		size_t _capacity = 0;

		static const size_t npos;
	};

	bool operator < (const string& s1, const string& s2);

	bool operator <= (const string& s1, const string& s2);
	
	bool operator > (const string& s1, const string& s2);
	
	bool operator >= (const string& s1, const string& s2);
	
	bool operator == (const string& s1, const string& s2);
	
	bool operator != (const string& s1, const string& s2);

	ostream& operator << (ostream& out, const string& s);

	istream& operator >> (istream& in, string& s);

	istream& getline(istream& in, string& s);
}
  1. s t r i n g . c p p string.cpp string.cpp
#include"string.h"

namespace ybc
{
	const size_t string::npos = -1;

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

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

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

	string& string::operator += (char c)
	{
		push_back(c);
		return *this;
	}

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

	void string::insert(size_t pos, char c)
	{
		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] = c;
		_size++;
	}

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

	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			for (size_t i = pos + len; i < _size; i++)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;
		}
	}

	size_t string::find(char c, size_t pos)
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == c)
			{
				return i;
			}
		}
		return npos;
	}

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

	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		//更新一下len
		if (len > _size - pos)
		{
			len = _size - pos;
		}
		string sub;
		reserve(len);
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}
		return sub;
	}

	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 !(s1 < s2 || s1 == s2);
	}
	
	bool operator <= (const string& s1, const string& s2)
	{
		return !(s1 > s2);
	}

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

	bool operator != (const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}

	ostream& operator << (ostream& out, const string& s)
	{
		for (auto c : s)
		{
			out << c;
		}
		return out;
	}

	istream& operator >> (istream& in, string& s)
	{
		s.clear();
		//避免多次扩容的优化
		const int N = 1024;
		char buff[N];
		int i = 0;
		char c; in.get(c);
		while (c != ' ' && c != '\n')
		{
			buff[i++] = c;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			in.get(c);
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

	istream& getline(istream& in, string& s)
	{
		s.clear();
		//避免多次扩容的优化
		const int N = 1024;
		char buff[N];
		int i = 0;
		char c; in.get(c);
		while (c != '\n')
		{
			buff[i++] = c;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			in.get(c);
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
}
  1. t e s t . c p p test.cpp test.cpp
#include"string.h"

namespace ybc
{
	void test1()
	{
		string s("hello world");

		cout << s.c_str() << endl;

		for (int i = 0; i < s.size(); i++)
		{
			s[i] += 2;
		}
		cout << s.c_str() << endl;

		for (auto& i : s)
		{
			i -= 2;
			cout << i;
		}
		cout << endl;

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

	void test2()
	{
		string s("hello ");

		s.append("ybc ");

		s += "nb plus !";

		s.insert(0, "--------");
		s += "--------";

		s.erase(0, 5);
		s.erase(s.size() - 5);

		cout << s.c_str() << " " << s.find(" ", 5) << endl;
	}

	void test3()
	{
		string s = "hello world";
		string ss = s.substr(s.find("w"));
		
		cout << ss.c_str() << endl;
	
		//析构2次
		string copy(s);
		cout << copy.c_str() << endl;

		string sss;
		sss = s;
		cout << sss.c_str() << endl;
	}

	void test4()
	{
		string s1 = "hello", s2 = "world";

		cout << "s1:" << s1 << endl;
		cout << "s2:" << s2 << endl;

		if (s1 < s2) cout << "s1 < s2" << endl;
		if (s1 > s2) cout << "s1 > s2" << endl;
		if (s1 <= s2) cout << "s1 <= s2" << endl;
		if (s1 >= s2) cout << "s1 >= s2" << endl;
		if (s1 == s2) cout << "s1 == s2" << endl;
		if (s1 != s2) cout << "s1 != s2" << endl;
	}

	void test5()
	{
		string s = "hello world";

		cin >> s;

		getline(cin, s);

		cout << s << endl;
	}

	void test6()
	{
		string s = "hello world";
		string ss = s;
		string sss;

		sss = ss;

		cout << s << endl << ss << endl << sss << endl;
	}

	void test7()
	{
		string s = "hello world";
		string ss = "xxxxxxxxxxxxxxxxxxxxxxx";

		std::swap(s, ss);	//C++98:3次深拷贝(不推荐)

		cout << s << endl << ss << endl;

		s.swap(ss);

		cout << s << endl << ss << endl;
	}
}

int main()
{
	//ybc::test1();

	//ybc::test2();

	//ybc::test3();

	//ybc::test4();

	//ybc::test5();

	//ybc::test6();

	ybc::test7();

	return 0;
}

总结

以上就是对 C C C++ 标准库 S T L STL STL 的引入,以及介绍了 C C C++ 标准库中 s t r i n g string string 类的基本使用以及一些 C C C++11 的新特性,为后续学习正式的 S T L STL STL 容器做好铺垫。最后,将 s t r i n g string string 类的常用功能进行了模拟实现,对 s t r i n g string string 类的使用和底层有了更深刻的认识。


http://www.kler.cn/a/598010.html

相关文章:

  • 死锁:当程序 “卡住“ 时,发生了什么?
  • wordpress主题使用中常见错误汇总
  • OpenGL实现摄像机(根据鼠标位置放大缩小视图)
  • How to install visual studio code on Linux mint 22
  • 详解内联容器标签<span>的用法
  • 幻影星空亮相CAAPA北京展 引领文旅产业升级转型
  • uniapp从 vue2 项目迁移到 vue3流程
  • 【网络层协议】NAT技术内网穿透
  • 【实战】deepseek数据分类用户评论数据
  • ADC噪声全面分析 -04- 有效噪声带宽简介
  • Tomcat常见漏洞攻略
  • 分布式系统设计陷阱,白话CAP理论
  • 【嵌入式学习2】位运算 - 控制语句
  • 游戏引擎学习第175天
  • 前端 -- 计算机图形学基础:光与三角形面(Mesh)求交
  • gonet开源游戏服务器环境配置
  • MySQL颠覆版系列————MySQL新特性(开启数据库的新纪元)上篇
  • 1. 页面级一多开发:
  • Unity摄像机基本操作详解:移动、旋转与缩放
  • Java处理Markdown格式内容转换为Word文档