【C++】string

我们正式进入到C++的STL的第一课:string的学习

目录

一、为什么要学习string类?

二、标准库中的string类

2.1 string类

2.2 string类的常用接口说明

2.2.1 string类构造函数接口

2.2.2 string类对象的容量函数接口

2.2.3 string类对象的修改函数接口

2.2.4 string类的功能函数接口

2.3 三种访问string类创建的对象的元素的方式

2.3.1 使用operator[ ]

2.3.2 使用迭代器 

2.3.3 使用范围for


一、为什么要学习string类?

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

对于这一系列问题,C++使用了string类来补缺了这些不足。

二、标准库中的string类

2.1 string类

在我们学习string类时,先来查阅其文档:

我们通过文档可以知道string是一个基于basic_string模板类重定义的类,其中因为编码情况的不同含有四种string类的实例化:分别是string/u16string/u32string/wstring

对于我们一般情况(ASCLL、UTF-8、GBK等)使用string的string类就足够了:

string是表示字符序列的类

标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。

string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。

string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits 和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。

注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作

总结:

1. string是表示字符串的字符串类

2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作

3. string在底层实际是:basic_string模板类的别名,typedef basic_string string

4. 不能操作多字节或者变长字符的序列

2.2 string类的常用接口说明

2.2.1 string类构造函数接口

我们先来看到string类的构造函数:

函数名称功能说明
string();创建一个空的string类
string (const string& str);拷贝构造传入的srt对象
string (const string& str, size_t pos, size_t len = npos);复制从pos位置开始向后len字节的str部分(如果len没有传值一直复制到str的末尾,如果传入的len值大于str的长度就一直复制到str的末尾)
string (const char* s);复制s所指向的以空结尾的字符序列(C-字符串)
string (const char* s, size_t n);从s指向的字符数组复制前n个字符
string (size_t n, char c);连续使用n个字符c来填充字符串

​​​一共有六种常用函数接口,还有一种要涉及到迭代器,我们现在不进行讲解。 

下面是各种函数接口使用的举例:

●  我们先来看到string()

int main()
{
	string S1;//无参构造
	cout << S1 << endl;
	return 0;
}

在这里可以使用<<(流插入)是因为string类支持了<<的重载

●  再来看到string (const char* s)

    string S2("Hello world");
    cout << S2 << endl;

当然我们可以使用=来进行隐式类型转换来传值:

	string S3 = "Hello world";
	cout << S3 << endl;

 ●  接着我们来看到string (const string& str)

	string S3 = "Hello world";
	string S4(S3);
	cout << S4 << endl;

●  后面我们来看到string (const string& str, size_t pos, size_t len = npos)

	string S3 = "Hello world";
	string S4(S3, 6, 2);
	cout << S4 << endl;
	string S5(S3, 6, 20);//len大于S3的pos位置后面的长度
	cout << S5 << endl;
	string S6(S3, 6);//第三个参数不传
	cout << S6 << endl;

注意:这里的缺省值npos是一个无符号整型

无符号整型实际值并不是-1,而是一个非常大的数(42亿多)

●  我们来看到string (const char* s, size_t n)

	string S7("Hello world", 6);
	cout << S7 << endl;

●  最后再来看到string (size_t n, char c)

	string S8(6,'*');
	cout << S8 << endl;

2.2.2 string类对象的容量函数接口

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

在这里有两个功能相同的函数length和size,那为什么会有两个功能相同的函数呢?这是因为C++的历史原因,在STL出来之前标准库中就有了string类,在那段时间是用length来描述字符串的长度,在后期STL出来了之后常用size来描述容器的大小,所以在string类中加上了size接口。在现在size作为标准,用的比较多。

下面是一些接口的举例:

●   size

	string S = "Hello world";
	cout << S.size() << endl;

注意:在这里size函数接口并不统计字符串结尾的‘/0’,所以字符串实际大小是12字节

●  length

	string S = "Hello world";
	cout << S.length() << endl;

注意:在这里length函数接口并不统计字符串结尾的‘/0’,所以字符串实际大小是12字节

●  capacity

	string S = "Hello world";
	cout << S.capacity() << endl;

注意:在这里capacity函数接口并不统计字符串结尾的‘/0’,所以字符串实际容量大小是16字节

在这里我们深入讲解一下string类扩容的过程:

//观察扩容过程
void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "init capacity:" << sz << endl;
	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');//每次向s中加入一个字符
		if (sz != s.capacity())//当s扩容时打印扩容后的容量
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

上面这段代码可以让我们观察到string所创建的对象扩容的过程:

我们可以看到在最初时字符串的实际容量是16字节,再一下子扩容2倍到32字节,接下来以1.5倍的大小再进行扩容,这是为什么呢?为什么不按1.5倍匀速扩容呢?

这是因为在字符串数据大小小于等于16字节时,string会将数据存在一个叫_Buf的字符数组中,该数组大小为16字节:

当字符串数据大小大于16字节时,string会将数据存在一个叫_Ptr指向的动态开辟的空间中,之后扩容时以1.5倍进行:

 但是上述结论仅在VS环境中适用!在Linux的g++中测试结果是一个经典的二倍扩容:

●  reserve

对于上面不断的扩容开辟空间的情况会造成效率上的低下,如果我们知道我们想要的空间大小可以使用reserve函数来预留一些空间:

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

这样子在程序运行时,下面就没有继续开辟空间了。

注意:在VS环境下由于底层实现原因,可能会将预留的空间放大一点点

●  resize

该函数有两种重载形式:

只传参数n:

int main()
{
	string S = "Hello";
	cout << S.size() << endl;
	cout << S.capacity() << endl;
	S.resize(100);//将字符串有效数据个数改为100字节
	cout << S.size() << endl;
	cout << S.capacity() << endl;
	return 0;
}

我们可以看到只传参数n时,如果n大于字符串的容量,该函数会自动扩容,并且会将未初始化的字符修改为‘/0’,并不会去修改已经被初始化的字符。

传入参数n和c:

int main()
{
	string S = "Hello";
	cout << S.size() << endl;
	cout << S.capacity() << endl;
	S.resize(100,'c');//将字符串有效数据个数改为100字节,并且将未被初始化的字符初始化为字符'c'
	cout << S.size() << endl;
	cout << S.capacity() << endl;
	return 0;
}

我们可以看到传入参数n和c时,如果n大于字符串的容量,该函数会自动扩容,并且会将未初始化的字符修改为字符c,并不会去修改已经被初始化的字符。

那如果n小于字符串的有效字符的个数呢?

int main()
{
	string S = "Hello";
	cout << S.size() << endl;
	cout << S.capacity() << endl;
	cout << S << endl;
	S.resize(1,'c');//传入1小于有效字符个数
	cout << S.size() << endl;
	cout << S.capacity() << endl;
	cout << S << endl;
	return 0;
}

我们可以看到如果传入的n小于字符串的有效字符的个数,函数会删除n个字符之后的数据(将第n个的后面一个字符改为'/0')

2.2.3 string类对象的修改函数接口

函数名称功能说明
push_back在字符串后尾插字符c
append在字符串后追加一个字符串
 operator+=(重点)在字符串后追加字符串
insert(不推荐使用)在字符串某个位置插入字符(字符串)
erase(不推荐使用)在字符串某个位置向后删除字符
swap交换两个字符串的值

下面是一些接口的举例讲解:

●  push_back

	string S = "Hello";
	S.push_back(' ');
	S.push_back('!');
	cout << S << endl;

●  append

	string S = "Hello";
	S.append(" world !");
	cout << S<< endl;

 另外append函数还有很多重载,这里不再一一举例

●  operator+=

	string S = "Hello";
	S += " ";
	S += "world !";
	cout << S<< endl;

注意:在这里operator+=只是一个封装,里面实现还是会调用push_back等接口的,但是我们只需要使用一个+=就可以向字符串添加字符比push_back、append函数更加方便,推荐使用!

●  insert

insert函数有以下重载:

 这里举几个常用的:

int main()
{
	string S = "wrld";
	S.insert(1, 1, 'o');
	cout << S << endl;
	S.insert(0, "Hello");
	cout << S << endl;
	S.insert(S.begin() + 5, ' ');
	cout << S << endl;
	return 0;
}

 注意:频繁使用insert函数会导致程序效率下降(插入字符可能需要整体移动字符串)

●  erase

该函数有三种重载方式: 

int main()
{
	string S = "Hello world !";
	S.erase(5, 1);
	cout << S << endl;
	S.erase(S.begin() + 5);
	cout << S << endl;
	S.erase(5);
	cout << S << endl;
	return 0;
}

  注意:频繁使用erase函数会导致程序效率下降(删除字符可能需要整体移动字符串)

●  swap

int main()
{
	string s1 = "Hello";
	string s2 = "world";
	s1.swap(s2);
	cout << s1 << endl;
	cout << s2 << endl;
	return 0;
}

string类的swap函数可以交换两个字符串的值,那库函数中也有swap函数,它们到底有什么区别呢?

我们来看看库函数中的swap:

我们看看到对于任意类型的a,b参数,库中的swap函数会另外开辟一个空间c来拷贝a,再将a修改为b,b修改为c。在这个过程中需要扩容,效率不高。

而string类型内置的函数swap只需要更改两个string类对象的内部指针指向的存储空间即可,不需要另外开辟空间,效率较高。

2.2.4 string类的功能函数接口

函数名称函数功能
c_str返回一个指向字符串的指针,该字符串包含一个以空结尾的字符序列(即一个C-字符串)
find顺序在string中查找内容(字符或字符串),找到返回字符返回字符在string中的位置,没找到返回npos
rfind逆序在string中查找内容(字符或字符串),找到返回字符返回字符在string中的位置,没找到返回npos
find_first_of顺序查找传入要查找字符串中的任何一个字符,找到返回字符返回字符在string中的位置,没找到返回npos
find_last_of逆序查找传入要查找字符串中的任何一个字符,找到返回字符返回字符在string中的位置,没找到返回npos
find_first_not_of顺序在string中搜索与其参数中指定的字符串中任何字符不匹配的第一个字符,不匹配返回该字符位置,匹配继续向后找,没找到不匹配的返回npos
find_last_not_of

逆序在string中搜索与其参数中指定的字符串中任何字符不匹配的第一个字符,不匹配返回该字符位置,匹配继续向前找,没找到不匹配的返回npos

 下面是一些接口的举例讲解:

●  c_str

int main()
{
	string s = "Hello world";
	cout << s.c_str() << endl;
	cout << s << endl;
	return 0;
}

我们可以看到使用c_str函数和用string创建的对象进行流输出的结果是一样的,那下面这个例子呢?

int main()
{
	string s = "Hello world";
	cout << s.c_str() << endl;
	cout << s << endl;
	s.push_back('\0');
	s.push_back('\0');
	s += "cccccccc";
	cout << s.c_str() << endl;
	cout << s << endl;
	return 0;
}

我们可以清楚的看到使用c_str函数进行流输出时会受到\0字符的影响,而string类的流输出重载不会(根据size来进行输出)

●  find

int main()
{
	string s = "https://legacy.cplusplus.com/reference/string/string/find/";
	size_t st = s.find("t");
	while (st != string::npos)
	{

		s[st] = '*';
		st = s.find('t', st + 1);
	}
	cout << s << endl;
	return 0;
}

上面的例子是寻找字符‘t’并替换为‘*’的过程

●  find_frist_of

int main()
{
	string s = "https://legacy.cplusplus.com/reference/string/string/find/";
	size_t st = s.find_first_of("abcd");
	while (st != string::npos)
	{

		s[st] = '*';
		st = s.find_first_of("abcd", st + 1);
	}
	cout << s << endl;
	return 0;
}

 上面的例子是寻找字符串“abcd”并将字符串中任何一个找到的字符替换为‘*’的过程

●  find_frist_not_of

int main()
{
	string s = "https://legacy.cplusplus.com/reference/string/string/find/";
	size_t st = s.find_first_not_of("/");
	while (st != string::npos)
	{

		s[st] = '*';
		st = s.find_first_not_of("/", st + 1);
	}
	cout << s << endl;
	return 0;
}

 上面的例子是将非“/”字符替换为‘*’的过程

2.3 三种访问string类创建的对象的元素的方式

2.3.1 使用operator[ ]

string支持[]重载,我们可以在创建的string类的对象名后面使用[ ]来直接访问其内部有效元素:

int main()
{
	string S = "Hello world !";
	for (int i = 0; i < S.size(); ++i)
	{
		cout << S[i];//访问S的第i个元素
	}
	cout << endl;
	return 0;
}

2.3.2 使用迭代器 

由于我们现在还未深入学习迭代器,可以暂且将其理解成一种类似于指针的东西

对于迭代器有四种类型,分别为正向迭代器(iterator)正向常量迭代器(const_iterator)反向迭代器(reverse_iterator)反向常量迭代器(const_reverse_iterator)

●  正向迭代器演示:

int main()
{
	string S = "Hello world !";
	string::iterator it = S.begin();//创建一个正向迭代器it,并将S对象的首元素地址传给it
	while (it != S.end())//当it未指向S对象的最后元素后一位地址时进入循环
	{
		cout << *it;//打印it所指向的元素信息
		++it;//将it向右移动指向下一个元素
	}
	cout << endl;
	return 0;
}

我们来理解一下上面的代码:一开始调用begin函数(string类的begin函数可以返回一个首元素地址的正向迭代器)将首元素地址交给迭代器it,it拿到地址后每次向右移动一个元素的单位,并将指向的元素信息打印出来,一直到it指向S对象的最后元素后一位地址时停止打印(string类的end函数可以返回一个最后一个元素的后一位地址的正向迭代器):

 ●  反向迭代器演示:

正向迭代器++向右移动,反向迭代器反之,++向左移动:

int main()
{
	string S = "Hello world !";
	string::reverse_iterator rit = S.rbegin();//创建一个反向迭代器rit,并将S对象的最后一个元素地址传给rit
	while (rit != S.rend())//当rit未指向S对象的首元素前一位地址时进入循环
	{
		cout << *rit;//打印rit所指向的元素信息
		++rit;//将it向左移动指向下一个元素
	}
	cout << endl;
	return 0;
}

我们来理解一下上面的代码:一开始调用rbegin函数(string类的rbegin函数可以返回一个最后元素地址的反向迭代器)将末尾元素地址交给迭代器rit,rit拿到地址后每次向左移动一个元素的单位,并将指向的元素信息打印出来,一直到rit指向S对象的首元素前一位地址时停止打印(string类的rend函数可以返回一个首元素前一位地址的反向迭代器):

 ●  正向常量迭代器和反向常量迭代器演示:

当我们创建了一个const的string类的对象时,并不能正常使用正向迭代器和反向迭代器了:

这时就只能使用正向常量迭代器和反向常量迭代器搭配cbegin/cend(begin/end)crbegin/crend(rbegin/rend)函数了:

void Func(const string& s)
{
	string::const_iterator cit = s.cbegin();//创建一个常量反向迭代器cit,并将S对象的首元素地址传给cit
	while (cit != s.cend())//当cit未指向S对象的最后元素后一位地址时进入循环
	{
		cout << *cit;//打印cit所指向的元素信息
		++cit;//将cit向右移动指向下一个元素
	}
	cout << endl;
	string::const_reverse_iterator crit = s.crbegin();//创建一个常量反向迭代器crit,并将S对象的最后一个元素地址传给crit
	while (crit != s.crend())//当crit未指向S对象的首元素前一位地址时进入循环
	{
		cout << *crit;//打印crit所指向的元素信息
		++crit;//将crit向左移动指向下一个元素
	}
	cout << endl;
}

普通迭代器和常量迭代器唯一区别就是:普通迭代器可以修改数据而常量迭代器不可以

当然如果嫌迭代器的命名太烦杂我们也可以使用auto来自动推导(不过会降低程序的可读性):

//string::iterator it = S.begin();
auto it = S.begin();
//string::reverse_iterator rit = S.rbegin();
auto rit = S.rbegin();
//string::const_iterator cit = s.cbegin();
auto cit = S.cbegin();
//string::const_reverse_iterator crit = s.crbegin();
auto crit = S.crbegin();

2.3.3 使用范围for

int main()
{
	string S = "Hello world !";
	for (auto ch : S)
	{
		cout << ch;
	}
	cout << endl;
	return 0;
}

在这里我们使用了一下基于范围for循环,也可以很好的访问string类的内部元素(但是底层原理依然会使用迭代器),对于范围for不熟悉的同学可以看下面这篇文章:

【C++】入门(下)


本期博客到这又要结束了,欢迎大家在评论区指正

下期见啦~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1e-12

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值