您现在的位置是:首页 >技术交流 >【剖析STL】String网站首页技术交流

【剖析STL】String

学IT的小卢 2023-05-10 22:30:04
简介【剖析STL】String

在这里插入图片描述

1.什么是STL?

标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。虽说它主要出现到C++中,但在被引入C++之前该技术就已经存在了很长时间。STL的代码从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。

  • 🔥标准模板库是一个C++软件库,大量影响了C++标准程序库但并非是其的一部分。其中包含4个组件,分别为算法、容器、函数、迭代器

  • 🔥模板是C++程序设计语言中的一个重要特征,而标准模板库正是基于此特征。标准模板库使得C++编程语言在有了同Java一样强大的类库的同时,保有了更大的可扩展性

image-20230508162229893

1.1STL的缺陷:

  1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出
    来已经相隔了13年,STL才进一步更新。
  2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
  3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
  4. STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语
    法本身导致的。

2.Stirng容器

2.1为什么需要学习String容器?

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

为了优化这个问题,我们引入了String容器。

2.2初始String:

string本身是一个类模板:

image-20230508170154430

string就是char类型的数组,char类型一个字节

wstring是wchar类型的数组,wchar为两个字节

char16_t类型为2字节,char32_t为4个字节

image-20230508170222533

  • 🔥string是表示字符串的字符串类
  • 🔥该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  • 🔥string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>
    string;
  • 🔥不能操作多字节或者变长字符的序列。在使用string类时,必须包含#include头文件以及using namespace std;

image-20230508165644704

2.3String类的常用接口说明(注意下面我只讲解最常用的接口)

2.3.1string类对象的常见构造:

  • Strin在C++98下有以下几种构造函数:

image-20230508172119160

第二种: string s3 = "hello world";

这里是一个const char*类型隐式转换成string类型

第三种: string s4(s3, 6, 3);

子字符串构造函数

  • 🔥复制str中从字符位置pos开始并跨越len个字符的部分(如果str太短或len为string::npos,则复制到str的末尾)。

  • 🔥第三个参数有缺省值,npos是无符号整形,-1代表整形最大值为42亿多

image-20230411143814898

string s1;
	string s2("hello world");
	string s3 = "hello world";
	string s4(s3, 6, 3);

	string s5(s3, 6, 13);
	cout << s5 << endl;

	string s6(s3, 6);
	cout << s6 << endl;

	string s7("hello world", 5);
	cout << s7 << endl;

	string s8(10, '*');
	cout << s8 << endl;

  • 本部分会详细介绍以下几个构造函数:
(constructor)函数名称功能说明
string() (重点)构造空的string类对象,即空字符串
string(const char* s) (重点)用C-string来构造string类对象
string(size_t n, char c)string类对象中包含n个字符c
string(const string&s) (重点)拷贝构造函数
  • string() (重点):

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

这里的空字符串指的是连都没有的

string s1(""); string s2();

  • 🔥s1是只有,s2是连都没有,s1的size和length都为0,capacity为15,s2没有size和length和capacity

image-20230508195519139

  • string(const char* s) (重点):

复制以s为指向的以空结尾的字符序列(C-string)。

int main()
{
	const char* p = "aaaaa";
	string s1(p);
	cout <<s1 << endl;//aaaaa
	return 0;
}
  • string(size_t n, char c):

用字符c的n个连续副本填充字符串。

string s2(4, 'b'); cout << s2 << endl;//bbbb

image-20230508174642814

  • string(const string&s) (重点)

构造一个str的副本。

string s3(s2);

2.3.2string类对象的容量操作

  • 本部分会介绍以下几个容器操作:
函数名称功能说明
size(重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty (重点)检测字符串释放为空串,是返回true,否则返回false
clear (重点)清空有效字符
reserve (重点)为字符串预留空间**
resize (重点)将有效字符的个数该成n个,多出的空间用字符c填充
  • size和length

size和length的效果是一样的,都是返回字符串的有效长度(不包括)

注意

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

  • capacity

image-20230508191415653

  • 🔥返回当前为字符串分配的存储空间的大小,以字节表示。

这个容量不一定等于字符串长度。它可以等于或大于,额外的空间允许对象在向字符串中添加新字符时优化其操作。

  • 🔥注意,这个容量并没有假设字符串的长度有限制。当此容量耗尽并且需要更多容量时,对象会自动对其进行扩展(重新分配其存储空间)。字符串长度的理论限制由成员max_size给出。

在对象被修改的任何时候,字符串的容量都可以被改变,即使这种修改意味着减小大小或者容量没有耗尽(这与vector容器中对容量的保证相反)。

可以通过调用成员reserve(后面会介绍)显式地更改字符串的容量。

void Teststring1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, xiaolu!!!");
	cout << s.size() << endl;//13
	cout << s.length() << endl;//13
	cout << s.capacity() << endl;//15
	cout << s << endl;
}
  • 在Debug和Release版本下,编译器自动扩容情况:
void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:
";
	cout << "capacity changed: " << sz << '
';
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '
';
		}
	}
}

在debug和release版本下,我们发现编译器每次扩容1.5倍左右

image-20230509000626071

  • clear

clear()只是将string中有效字符清空,不改变底层空间大小。

这里的情况是指清空size和length,不改变capacity

image-20230508192051242

void Teststring1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, xiaolu!!!");
	cout << s.size() << endl;//13
	cout << s.length() << endl;//13
	cout << s.capacity() << endl;//15
	cout << s << endl;

	// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;//0
	cout << s.capacity() << endl;//15
}
  • resize (重点)

image-20230508192501923

  • 🔥将字符串大小调整为n个字符的长度。

如果n小于当前字符串长度,则将当前值缩短到前n个字符,删除第n个字符以外的字符。

如果n大于当前字符串长度,则通过在末尾插入尽可能多的字符来扩展当前内容,以达到n的大小。如果指定了c,则新元素被初始化为c的副本,否则,它们是值初始化的字符(空字符)。

  • 🔥这里如果缩小有效字符,只改变size和length,不改变capacity
void Teststring1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, xiaolu!!!");
	s.clear();
	cout << s.size() << endl;//0
	cout << s.capacity() << endl;//15

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

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

	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;//5
	cout << s.capacity() << endl;//15
	cout << s << endl;
}
  • reserve (重点)

image-20230508193706796

  • 🔥reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于
    string的底层空间总大小时,reserver不会改变容量大小。
  • 🔥 利用reserve提高插入数据的效率,避免增容带来的开销
void Teststring2()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;//0
	cout << s.capacity() << endl;//111

	// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;//0
	cout << s.capacity() << endl;//111
}
  • empty

image-20230508194723919

这里必须要对象存在才可以调用,像string()出来的对象不存在,根本调用不了

image-20230508200143646

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

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

我们在用字符数组的时候,我们经常用[]来访问字符,我们在stirng中同样可以这样使用

image-20230509001335100

  • 范围for

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

void Teststring4()
{
	string s("hello xiaolu");
	for (size_t i = 0; i < s.size(); ++i)
		cout << s[i] << " ";
	cout << endl;
	for (auto ch : s)
		cout << ch << " ";
}
  • 迭代器和反向迭代器

迭代器(iterable)是一个超级接口! 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦。

  • 🔥迭代器应该有4种,正向,反向,正向const,反向const

迭代器可以近似理解为指针,后面会详细讲解的

begin返回指向字符串开头的迭代器。

  • 🔥如果string对象是const限定的,该函数返回一个const_iterator。否则,它返回一个迭代器。

❓这里的const跟我们之前的const int i是不是一样的?

💡并不是,后者是一个关键字被修饰的不可以修改,前置是一个类型名称,被修饰的是这个迭代器指向的内容

end返回一个指向字符串末尾字符的迭代器。

rbegin和rend 跟begin和end完全相反

image-20230509004957501

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

	// string::reverse_iterator rit = s.rbegin();
	// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
	auto rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

2.3.4string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符c
append在字符串后追加一个字符串
operator+= (重点)在字符串后追加字符串str
c_str(重点)返回C格式字符串
find + npos(重点)从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回
  • push_back

将字符c添加到字符串的末尾,使其长度增加1。

string str; str.push_back(' '); // 在str后插入空格

  • append

image-20230509084816554

追加到字符串

通过在当前值的末尾添加额外的字符来扩展字符串:

void Teststring5()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
}
  • operator+= (重点)

在字符串后追加字符串str

void Teststring5()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'x';           // 在str后追加一个字符'x'   
	str += "iaolu";          // 在str后追加一个字符串"iaolu"
	cout << str << endl;
	}
  • c_str(重点)

image-20230509090337304

获取C字符串等价项

  • 🔥返回一个指向数组的指针,该数组包含一个以空字符结尾的字符序列(即C-string),表示string对象的当前值。
void Teststring5()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'x';           // 在str后追加一个字符'x'   
	str += "iaolu";          // 在str后追加一个字符串"iaolu"
	cout << str << endl;
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串
	}
  • find + npos(重点)

image-20230509090928480

在字符串中搜索由其参数指定的序列的第一个匹配项。

  • 🔥当指定pos时,搜索只包含pos位置或之后的字符,忽略任何可能包含pos之前字符的情况。

  • 🔥请注意,与find_first_of成员不同的是,每当搜索多个字符时,仅匹配其中一个字符是不够的,必须匹配整个序列。

  • rfind

跟find完全相反,她从pos位置开始向前走

当指定pos时,搜索只包含从pos位置开始或之前的字符序列,忽略任何从pos位置之后开始的可能匹配。

  • substr

image-20230509091625581

生成子串,返回一个新构造的string对象,其值初始化为该对象的子字符串的副本。

  • 🔥子字符串是对象的一部分,从字符位置pos开始,跨越len字符(或直到字符串末尾,以哪个先到哪个)。
void Teststring5()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'x';           // 在str后追加一个字符'x'   
	str += "iaolu";          // 在str后追加一个字符串"iaolu"
	cout << str << endl;
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串

	// 获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << suffix << endl;

	// npos是string里面的一个静态成员变量
	// static const size_t npos = -1;

	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;//http://www.cplusplus.com/reference/string/string/find/
	size_t start = url.find("://");
	if (start == string::npos)
	{
		cout << "invalid url" << endl;
		return;
	}
	start += 3;
	size_t finish = url.find('/', start);
	string address = url.substr(start, finish - start);
	cout << address << endl;//www.cplusplus.com

	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos + 3);
	cout << url << endl;
}

注意:

  • 🔥在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般
    情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  • 🔥对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

2.3.5string类非成员函数

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

连接字符串,返回一个新构造的string对象,其值由lhs中的字符和rhs中的字符拼接而成。

  • getline (重点)

image-20230509095334545

getline(istream& is, string& str, char delim);

  • 🔥这个形式的函数从输入流is中读取一行文本,并将其存储在str字符串中,直到遇到指定的分隔符delim为止。如果没有指定分隔符,则默认为换行符。读取的分隔符不包括在存储的字符串中。
int main()
{
	string line;
	char delim = ':';
	cout << "Enter a line with as delimiter :" << endl;
	getline(cin, line, delim);
	cout << "You entered:" << line << endl;
	return 0;
}

image-20230509113028819

getline(istream& is, string& str);

  • 🔥这个形式的函数从输入流is中读取一行文本,并将其存储在str字符串中,直到遇到换行符为止。读取的换行符不包括在存储的字符串中。

  • operator>> (重点)

image-20230509095046682

从流中提取字符串,从输入流中提取一个字符串,将该序列存储在str中,str被覆盖(之前的str值被替换)。

该函数重载运算符>>,使其行为与istream::operator>>中描述的一样,适用于c-string对象。

提取出的每个字符都会像调用push_back成员一样被添加到字符串中。

请注意,istream提取操作使用空格作为分隔符。因此,这个操作只会从流中提取可以认为是单词的内容。要提取整行文本,请参阅全局函数getline的字符串重载。

  • operator<< (重点)

image-20230509113440051

向流中插入字符串,将符合str的值的字符序列插入到os中。

  • relational operators (重点)

这个后面写模拟实现的时候,会详细讲解

image-20230509113755321

2.4String非常用接口说明

  • insert

在pos(或p)指定的字符之前插入额外的字符:

  • 🔥intsert不会将插入进去 的,string不推荐经常使用insert,能少用就少用

image-20230509114635482

int main()
{
	string s1("world");
	s1.insert(0, "hello");
	cout << s1 << endl;

	s1.insert(5, " ");
	cout << s1 << endl;
	return 0;
}
  • erase

从字符串中删除字符,删除字符串的一部分,减少其长度:

image-20230509115225380

  • 第一种:擦除字符串值中从字符位置pos开始并跨越len字符的部分(如果内容太短或len为string::npos,则擦除直到字符串末尾)。注意,默认实参会擦除字符串中的所有字符(类似于成员函数clear)。
  • 第二种:擦除指向p的字符。
  • 第三种:擦除[first,last)范围内的字符序列。
string s2("hello world");
	s2.erase(5, 1);
	cout << s2 << endl;

当删除的字符的个数超过了字符的个数会怎么样?

image-20230414151437990

  • replace

image-20230414151851659

int main()
{
	string s1("world");
	s1.insert(0, "hello");
	cout << s1 << endl;

	s1.insert(5, " ");
	cout << s1 << endl;
	return 0;
}

在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。