【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 的版本(了解)
- 原始版本
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 AlexanderStepanov、MengLee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 H P HP HP 版本 —— 所有 S T L STL STL 实现版本的始祖。
- 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++ 采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
- 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 采用,不能公开或修改,可读性一般。
- 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 等操作,来实现大整数的加法。题目的具体做法思路如下:
- 字符串转化为数字数组:
首先,我们知道两个高精度整数是以字符串的形式存储的,因此为了方便逐位相加,我们需要将字符串转换成倒序的数组,使得个位对齐,这样可以从低位向高位依次相加。
- 逐位相加并处理进位:
对于每一位数字,两个数组中的对应位相加,记录进位。进位部分会加到下一位的相加过程中。如果当前位的和大于等于 10 10 10,则需要将该位的值取模 10 10 10,余数保留,进位值加到下一位。
- 处理进位后的结果:
在完成所有位的相加后,如果最高位有进位,则需要在结果中保留该位。否则,最高位是 0 0 0 时则忽略。
- 将结果转回字符串输出:
最后,将结果数组从高位到低位依次转换为字符串,并输出最终结果。
【代码示例】
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();
除此之外,还有一些注意事项:
- 用 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;
运行结果为:
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1, b = 2;
//error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto c = 3, d = 4.0;
- 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);
}
- 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)
- 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
}
- 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
}
- 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
}
- 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 C−string (字符数组)来构造 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 提高插入数据的效率,可以避免增容带来的开销。
总结:
-
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()。
-
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)。
-
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 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小(扩容);如果是将元素个数减少,底层空间总大小不变(不缩)。
- 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 的遍历一般是:
-
b e g i n ( ) + e n d ( ) begin()+end() begin()+end()
-
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:
-
插入(拼接)方式: 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
-
正向和反向查找: f i n d ( ) + r f i n d ( ) find() + rfind() find()+rfind()
-
截取子串: s u b s t r ( ) substr() substr()
-
删除: 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;
总结:
-
在 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 类的+=
操作用的比较多,+=
操作不仅可以连接单个字符,还可以连接字符串。 -
对 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 中字符串的存储空间:
- 当字符串长度小于 16 16 16 时,使用内部固定的字符数组( B u f Buf Buf)来存放。
- 当字符串长度大于等于 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 个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小( l e n g t h length length)
- 字符串有效长度( c a p a c i t y capacity capacity)
- 引用计数( r e f c o u n t refcount refcount)
- 指向堆空间的指针,用来存储字符串。
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 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 UTF−8 的 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 。
- 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);
}
- 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;
}
}
- 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 类的使用和底层有了更深刻的认识。