使用 string(类对象/容器)

了解 STL 后,就可以利用 string 来学习容器。但是从某种角度来说,string 并不是 STL 里内置的容器,而是 C++ 早就实现了的容器,但是并不影响 STL 的学习

认识 string

我们之前学习过 字符串 ,那是以 '\0' 结尾的 字符序列 。相信大家也一定做过相应的编程题,就类似给字符数组赋值、切割字符串、求字符串长度等等的操作字符串的题目,就是实际开发过程中实实在在的需求。虽然可以使用使用 C 库中的字符串操作函数完成相应需求,但对于 C++ 来说,这些库函数与字符串是分离开的,不太符合 OOP(面向对象) 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。所以,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数

Strings are objects that represent sequences of characters.

翻译过来就是: string 是表示字符序列的对象,是一个 类对象 !!!

类对象 就势必会牵扯相应的 成员函数 ,而我们的需求均可在成员函数里来实现(在库里人家已经帮忙实现好了,现在你总该要会用)

string 的使用

注意:需要包头文件 #include <string>

初始化 string

上例子:

void TestString1()
{
	const char* str = "Hello C++";
	string s0;                    // 无参构造
	string s1("Hello World!");    // 有参(c-string -> C语言字符串或字符数组)构造
	string s2(s1);                // 拷贝构造
	string s3(s1, 5, 3);          // 子字符串(string类)构造
	string s4(s1, 5, 10);
	string s5(s1, 5);
	string s6(str, 7);            // from sequence
	string s7(10, '#');           // 填充构造
	string s8(10, '*');

	cout << s0 << endl;
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;zizi
	cout << s5 << endl;
	cout << s6 << endl;
	cout << s7 << endl;
	cout << s8 << endl;
}

我们知道 string 是一个类对象后,上述相关代码应当不难理解,只是功能上有所不理解,解释相关难懂代码:

子字符串(string类)构造

string (const string& str, size_t pos, size_t len = npos);

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

说白了,将 str 里的字符串从 0 开始进行编号,我们只要编号为 pos 开始的 len 个字符来初始化新 string 对象。
npos 是缺省参数,在这里可以理解为 无穷大 (但实际上不是)

from sequence

string (const char* s, size_t n);

作用:从 s 指向的字符数组中复制前 n 个字符。

填充构造

string (size_t n, char c);

作用:用字符 cn 个连续副本填充字符串。
string 对象里就是 n 个字符 c

常用遍历 string 操作

上例子:

void TestString2()
{
	string s1("Hello World!");

	// 传统意义上 C 风格字符串使用
	for (size_t i = 0; i < s1.size(); ++i)   // string 类型对象包含 '\0',但 size() 成员函数不将 '\0' 计入 string 长度
	{
		cout << s1[i] << " ";    // 底层重载 [] 运算符
		++s1[i];                 // 返回引用,可读可写
	}
	cout << endl;
	for (size_t i = 0; i < s1.size(); ++i)
	{
		cout << s1.operator[](i) << " ";   // 就是 [] 的重载函数
	}
	cout << endl;

	// 迭代器使用 (行为像指针一样的对象类型,实际上不是指针,而是对象)
	string::iterator it1 = s1.begin();  // 左闭右开区间 [ )
	while (it1 != s1.end())
	{
		cout << *it1 << " ";
		(*it1)--;   // 来看到这里,迭代器可读可写
		++it1;
	}
	cout << endl;
	it1 = s1.begin();
	while (it1 != s1.end())
	{
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;

	// 范围 for 遍历 string 容器(不易修改,一般只用做遍历)但底层就是迭代器
	for (auto e : s1)
		cout << e << " ";
	cout << endl;
}

这里结合注释对于 传统意义上 C 风格字符串使用 应该不难理解,核心要素就是 string 类对象无法直接使用 [] 运算符,没关系,直接 重载 它(不理解的小伙伴需要恶补 类与对象运算符重载

至于 迭代器 嘛,可以先记住怎么用,这里 string::iterator 是对象类型,之所以要使用 string:: 是因为迭代器不止 string 有,其他 STL 容器比如 vectorlist 等等都有。所以需要作用域声明
注意: begin()s1 的开头位置(即下标为 0 的字符), end() 是字符串结束标志 '\0' 的下一个位置,所以是 左闭右开区间 [ ),如图:
在这里插入图片描述

最后:范围 for 自己会用就行了

迭代器操作

以上三种方法即可对 string 容器里的元素进行访问、遍历甚至是修改,但迭代器内容不足,需补充,例子:

void TestString3()
{
	// 反向迭代器
	string s1("Hello World!");
	string::reverse_iterator rit1 = s1.rbegin();
	while (rit1 != s1.rend())
	{
		cout << *rit1 << " ";
		++rit1;  // 尤其需要注意:反向迭代器本身就是反向状态,所以是++而不是--
	}
	cout << endl;

	// 只读迭代器
	const string s2("Hello World!");
	string::const_iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// 反向只读迭代器
	const string s3("Hello World!");
	string::const_reverse_iterator rit2 = s3.rbegin();
	while (rit2 != s3.rend())
	{
		cout << *rit2 << " ";
		++rit2;
	}
	cout << endl;
}

结合上述代码和注释 ,可以看到算上最开始的一种常规迭代器,按照 是否只读正反向 共有 4 种迭代器,这里的 反向迭代器 如图:
在这里插入图片描述

所以 反向迭代器 本身就是 反向状态 ,不论是正向迭代还是反向迭代,都应该使用 ++ 而不是 --

当然啦,只读迭代器 里的内容是不能修改的,只能访问;
对于有 只读 需求时,可以使用 cbegin()cend()crbegin()crend() ,但也没必要,像上面示例代码一样即可

有关容量的函数操作

老样子,先上案例:

void capacityExpansion()  // 扩容机制查看
{
	// 首先要知道,STL 是一个规范,只规定功能,不规定的细节
	// 例如:linus 下 g++ 的 capacity 一直是2倍扩容,且初始值为0
	// 以下为使用 vs2019 测试
	cout << "***************** CapacityExpansion *********************" << endl;
	string s;
	size_t sz = s.capacity();
	cout << "Initial value: " << sz << endl;
	cout << "making s grow..." << endl;
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "Capacity changed: " << sz << endl;
		}
	}
	cout << "*********************** End ******************************" << endl;
}

void TestString4()
{
	// 以下均为公共成员函数
	string s("Hello World!!!");
	cout << s.size() << endl;       // 返回字符串长度(推荐)
	cout << s.length() << endl;     // 返回字符串长度
	cout << s.max_size() << endl;   // 返回字符串的最大长度(因为环境问题会导致不确定)
	cout << s.capacity() << endl;   // 返回已分配存储的大小(扩容机制)

	// s.resize(100);               // 调整字符串大小,针对字符串的
	s.reserve(100);                 // 请求更改string容量,比当前capacity大才有作用,不然无用
	cout << s.capacity() << endl;
	cout << s.empty() << endl;      // 测试字符串是否为空,是为1,否为0

	s.clear();                      // 只是清空字符串,容量不变
	cout << s.empty() << endl;
	cout << s.capacity() << endl;

	s.shrink_to_fit();              // 缩容,vs2019 缩不到 0,其他的我没验证
	cout << s.capacity() << endl;
}

结合代码和注释并不难,但还是有地方需要进行解释:

size() 和 length()

这哥俩并无区别 ,虽说使用二者均可,但推荐使用 size() ,因为对于其他容器大小的函数均为 size() ,如此一来不会混杂

max_size()

此函数是用来计算当前平台下字符串的最大长度,区别在于 x86 和 x64 平台,大家可以试试,此函数具有相应参考价值

capacity()

此函数是计算 string字符串 开辟的空间大小;不是 字符串的大小(这是函数 size() 的功能)

虽然对于使用者来说不必担心容量问题,因为 string 会自动扩容,但如果你频繁不断扩容,效率上一定会有所损失
可以使用上面的 capacityExpansion() 函数查看扩容机制:使用 vs2019 的初始值是 15, 第一次扩容结果是2倍,接着往后都是1.5倍扩容(vs2019 验证时的结果里是不算上 '\0' 的,所以都是单数)

在这里插入图片描述

所以如果有事先知道要插入多少数据,那么提前开好空间,就可以避免频繁扩容,提高效率,这就需要函数 reserve()

reserve()

首先来看函数原型:

void reserve (size_t n = 0);

注释已经说明 作用 是:请求更改 string 容量,且比当前 capacity 大才有作用,不然无用

  • 请求字符串容量适应最大长度为 n 个字符的计划大小更改
  • 如果 n 大于当前字符串容量,则该函数使容器将其容量增加到 n 个字符(或更大)
  • 在所有其他情况下,收缩字符串容量被视为非绑定请求 :容器实现可以自由地进行优化,并使字符串的容量大于 n
  • 此函数对字符串长度没有影响,也不能改变其内容

可以看到函数 reserve() 针对的是 string 为字符串分配的容量 ,且只能变大扩容(就是为字符串预留出足够的空间),所以不论传递的参数有多小,对原字符串长度都没有影响,且它不具备缩容条件,所以不要写出什么奇奇怪怪的错误代码,例如 reserve(-1)

那么还是那句话,如果事先知道要插入多少数据,请提前开好空间,避免频繁扩容,提高效率

resize()

函数原型:

void resize (size_t n);
void resize (size_t n, char c);

注意作用 :调整字符串大小,是针对字符串的,和 reserve() 区分开

  • 字符串大小 调整为 n 个字符的长度
  • 如果 n 小于 当前字符串长度( size() ),则将当前字符串缩短到前 n 个字符,删除第 n 个字符以外的字符
  • 如果 n 大于 当前字符串长度( size() ),则通过在末尾插入尽可能多的字符来扩展当前字符串内容,以达到 n 的大小
  • 如果指定了字符 c ,则新元素被初始化为 c 的副本,否则初始化为空字符 '\0'

要注意了, resize() 是针对 字符串 的,影响的是 size() ;但 reserve() 针对 为字符串分配的容量 ,影响的是 capacity()
但我们知道 size() 值又能间接影响 capacity() 值,所以请自己总结吧

可以手动自己下去验证

string 访问操作

用例代码:

void TestString5()
{
	string s("Hello World!!!");
	cout << s[6] << endl;
	cout << s.at(6) << endl;
	
	// 越界检查不同
	// cout << s[20] << endl;        // 断言崩溃
	// cout << s.at(20) << endl;     // 抛异常,可被捕获

	cout << s.front() << endl;       // 访问首字符
	cout << s.back() << endl;        // 访问尾字符
}

结合代码和注释,这里几乎没什么难点,要会用就行

注意:要访问 string 里的字符, []at() 几乎没什么区别,就是当你越界的时候会有所不同,注释已标明

string 修改操作

上用例代码:

void TestString6()
{
	string s1("Hello World!!!");
	s1.push_back('*');       // 尾插(追加)字符
	cout << s1 << endl;
	s1.append(" Hello C++!");  // 尾插(追加)字符串,用法贼像 string 自己的构造函数,自行查阅文档解释说明
	cout << s1 << endl << endl;

	string s2("Hello W");
	cout << s2 << endl;      // 尾插(追加)字符或字符串,对 += 的重载,函数 operator+=()
	s2 += 'o';
	cout << s2 << endl;
	s2 += "rld!!!";
	cout << s2 << endl << endl;

	string s3("Hello World!!!*************************");
	cout << s3 << endl;
	s3.assign("###");         // 为字符串赋一个新值,替换其当前内容,用法和 append 一模一样
	cout << s3 << endl;
	cout << s3.capacity() << endl << endl; // 此代码替换后容量不变

	string s4("Hello World!");
	cout << s4 << endl;
	s4.insert(0, "###");     // 在 pos 位置前插入,可以插入 string 对象(一部分)或字符串(一部分),但如果要插入单个字符,要使用迭代器定位,或者插入len=1个字符
	cout << s4 << endl;
	s4.erase(4, 10);         // 在 pos 位置进行删除,如果指定删除的字符个数len,则删除算上pos位置的len个字符;否则算上pos位置往后全删
	cout << s4 << endl << endl;

	string s5("Hello World");
	cout << s5 << endl;
	s5.replace(0, 5, "hELLO");   // 即:将pos位置开始的len个字符(或某迭代器区间)替换为 string 对象(一部分)或字符串(一部分)
	cout << s5 << endl << endl;

	string s6("Hello World!!");
	string s7("Hello C++!!");
	cout << s6 << endl;
	cout << s7 << endl;
	s6.swap(s7);         // 交换两 string 里的内容
	cout << s6 << endl;
	cout << s7 << endl;
	s6.pop_back();       // 删除最后一个字符
	s7.pop_back();
	cout << s6 << endl;
	cout << s7 << endl;
}

个人觉得上述代码和注释已将相关使用讲明白,翻阅相关文档即可深入了解

字符串操作

这个就比较杂了,捡几个重要一点的:

void TestString7()
{
	string s1("Hello World!!!");
	const char* ps1 = s1.c_str();  // 返回一个C风格的字符串
	
	char buffer[20];
	size_t len = s1.copy(buffer, 7, 5);  // 返回值是你复制过去的长度
	buffer[len] = '\0';     // 这一步不能少
	cout << buffer << endl;

	size_t pos1 = s1.find('o');  // 查找字符 'o'
	cout << pos1 << endl;
	size_t pos2 = s1.rfind('o');
	cout << pos2 << endl;

	string s2(s1.substr(3, 7)); //返回一个新构造的子字符串对象
	cout << s2 << endl;
}

find() 和 rfind()

先看函数原型:

size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
size_t find (const char* s, size_t pos, size_t n) const;
size_t find (char c, size_t pos = 0) const;

在这里应该不难看出来我们要查找的可以是 string 对象 、字符串和字符

  • 在字符串中搜索由其参数指定的序列第一次出现的位置
  • 当指定 pos 时,搜索只包括位置 pos 或位置 pos 之后的字符,忽略任何可能出现的包含位置 pos 之前字符的字符
  • 搜索多个字符时,仅匹配其中一个字符是不够的,整个序列必须匹配

好像很明了了,而 rfind() 只是倒着查找而已

substr()

string substr (size_t pos = 0, size_t len = npos) const;
  • 返回一个新构造的字符串对象,其值初始化为此对象的子字符串的副本。
  • 子字符串是对象的一部分,从字符位置 pos 开始,跨越 len 个字符(或直到字符串的末尾,以先到者为准)

总结

我也没什么好方法可以快速记住这些用法,只有多用 string ,当然 string 的方便不是那些库函数可以比的,总之就多用吧

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值