使用 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
太短或 len
为 string::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);
作用:用字符 c
的 n
个连续副本填充字符串。
新 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 容器比如 vector
, list
等等都有。所以需要作用域声明
注意: 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
的方便不是那些库函数可以比的,总之就多用吧