【C++】—— string 类的了解与使用
9毫米的幻想 2024-09-06 09:05:02 阅读 60
【CPP】—— string类的了解与使用
1、 为什么学习string 类1.1、 C语言中的字符串1.2、 面试题中更多以 string 类出现
2、 标准库中的 string 类3、 string 的默认成员函数3.1、 string 的构造与拷贝构造3.2、 string 的赋值重载3.3、 string 的析构函数
4、 operator[ ]4.1、 访问4.2、 修改4.3、 检查越界
5、 string类的三种遍历方式5.1、 下标访问遍历5.1.1、 size和length函数5.1.2、 下标访问
5.2、 迭代器访问遍历5.2.1、 初识迭代器访问5.2.2、 反向迭代器
5.3、 范围 for 访问遍历5.3.1、 初识范围for访问5.3.2、 范围for访问的注意事项5.3.3、 auto关键字5.3.3.1、 auto关键字的简单认识5.3.3.1、 auto关键字的注意事项
6、 string类的容量操作6.1、 reserve6.1、 resize6.2、 注意事项
7、 string类对象的修改操作7.1、 operator+=7.2、 c_str7.3、 swap
8、 string类非成员函数8.1、 getline8.2、 swap
9、 不同平台下string类的结构9.1、 VS 下string类的结构9.2、 g++ 下string类的结构
1、 为什么学习string 类
1.1、 C语言中的字符串
C语言中,字符串是以 <code>'\0' 结尾的一些字符的集合,为了操作方便,C标准库 提供了一些str系列
的库函数,但是这些库函数与字符串是分离开的,不太符合 OP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C语言中的字符串有诸多弊端。
1.2、 面试题中更多以 string 类出现
在 OJ 中,有关字符串的题目基本以 string类
的形式出现,而且在常规工作中,为了简单、方便快捷,基本都使用 string类
,很少有人去使用 C语言 库中的字符串操作函数。
下面是两个面试题(暂不做讲解)
把字符串转换成整数字符串相加
2、 标准库中的 string 类
学习 string类
,是离不开查阅文档的
s
t
r
i
n
g
string
string 的文档
在使用 string类
时,必须包含 #include<string>
头文件。
s
t
r
i
n
g
string
string 底层类似一个顺序表:是由一个指向一个开辟出的空间的指针
,和两个记录字符串长度和空间大小的变量 _size
和 _capacity
组成的
简单结构如下:
class string
{
private:
char* str;//指向存储字符串的空间
size_t _size;//记录当前字符串长度
size_t capacity;//记录当前空间大小
};
3、 string 的默认成员函数
3.1、 string 的构造与拷贝构造
注:因
s
t
r
i
n
g
string
string 产生的比较早,有些部分考虑的并没有那么成熟,所以
s
t
r
i
n
g
string
string 设计的有些冗余。只需重点学习标记出来的 3 个构造函数即可
e
m
p
t
y
empty
empty
s
t
r
i
n
g
string
string
c
o
n
s
t
r
u
c
t
o
r
(
d
e
f
a
u
l
t
c
o
n
s
t
r
u
c
t
o
r
)
constructor (default constructor)
constructor(defaultconstructor)
构造一个空字符串,长度为零个字符
c
o
p
y
copy
copy
c
o
n
s
t
r
u
c
t
o
r
constructor
constructor
构造
s
t
r
str
str 的副本。
s
u
b
s
t
r
i
n
g
substring
substring
c
o
n
s
t
r
u
c
t
o
r
constructor
constructor
复制
s
t
r
str
str 中从字符位置
p
o
s
pos
pos 开始并跨越
l
e
n
len
len 字符的部分(如果任一
s
t
r
str
str 太短或
l
e
n
len
len 为
s
t
r
i
n
g
:
:
n
p
o
s
string :: npos
string::npos,则复制到
s
t
r
str
str 末尾的部分)。
f
r
o
m
from
from
c
c
c-
s
t
r
i
n
g
string
string
复制
s
s
s 指向的以
n
u
l
l
null
null 结尾的字符序列(C 字符串)。
f
r
o
m
from
from
b
u
f
f
e
r
buffer
buffer
从
s
s
s 指向的字符数组中复制前
n
n
n 个字符。
f
i
l
l
fill
fill
c
o
n
s
t
r
u
c
t
o
r
constructor
constructor
用字符
c
c
c 的
n
n
n 个连续副本填充字符串。
r
a
n
g
e
range
range
c
o
n
s
t
r
u
c
t
o
r
constructor
constructor
以相同的顺序复制 [
f
i
r
s
t
first
first,
l
a
s
t
last
last] 范围内的字符序列。
第三个默认构造函数的缺省值
n
o
p
s
nops
nops 是什么呢?
n
o
p
s
nops
nops 是
s
t
r
i
n
g
string
string 类中的一个<code>静态成员变量,
n
o
p
s
nops
nops的值是 -1,但其为
s
i
z
e
size
size_
t
t
t 类型,所以实际值为整型的最大值
。
编译器认为你的字符串不可能有这么长 (42亿9千万字节),所以
n
o
p
s
nops
nops 的意思是有多长取多长。
我们来一起来实践一下
<code>void Test1()
{
//使用默认构造函数,不需要传参。
string s1;
//带参构造,使用指定字符数组初始化。
string s2("hello world");
//使用拷贝构造初始化
string s3 = s2;
//使用拷贝构造初始化
//字符串会隐式类型转换。生成一个临时对象,再用临时对象进行拷贝(实际执行编译器会进行优化)
string s4 = "你好";
//string库中重载了流插入与流提取,我们可以直接使用
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
运行结果:
剩下 4 个我们也来看下
<code>void Test2()
{
string s1("hello world");
//3. 使用一个string的某段区间初始化,其中pos是字符串下标,npos是指无符号整数的最大值。
string s3(s1, 3, 8);
//5. 使用的是某个字符数组前n个字符来初始化
string s5("hello world!", 5);
//6. 使用的是n个c字符初始化。
string s6(8, 'a');
//7. 使用的是某段迭代器区间初始化。(现在看不懂没关系,后面会介绍)
string s7(s1.begin() + 2, s1.end() - 3);
cout << s3 << endl;
cout << s5 << endl;
cout << s6 << endl;
cout << s7 << endl;
}
运行结果:
3.2、 string 的赋值重载
s
t
r
i
n
g
string
string 重载了 3 个赋值运算符重载函数:
使用
s
t
r
i
n
g
string
string对象进行赋值使用字符串进行赋值使用单个字符进行赋值
<code>void Test3()
{
//使用拷贝构造进行初始化
string s1 = "hello world";
//使用string对象进行赋值
string s2;
s2 = s1;
//使用指定字符串进行赋值
string s3;
s3 = "你好";
//使用指定字符进行赋值
string s4;
s4 = 'a';
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
运行结果:
这里,其实第二个赋值重载函数可以省略,因为字符串会生成一个临时对象,再用临时对象进行赋值拷贝。
3.3、 string 的析构函数
析构函数很简单,因为会自动调用,这里不做介绍。
4、 operator[ ]
s
t
r
i
n
g
string
string 类重载了<code>[],使用户可以像数组一样访问 string类 中的字符
s
t
r
i
n
g
string
string 类重载了两个<code>operator[],一个是普通版的,一个是
c
o
n
s
t
const
const 不可修改版 的
4.1、 访问
有了operator[]
,我们就可以像数组一个对
s
t
r
i
n
g
string
string 进行访问
void Test5()
{
string s1 = "hello world";
cout << s1[1] << endl;
cout << s1[3] << endl;
cout << s1[6] << endl;
}
4.2、 修改
同时,因为 <code>operator[] 返回的是字符的引用,这意味着普通版的operator[]
不仅仅可以获取相应位置的字符,还能对其进行修改,
void Test5()
{
string s1 = "hello world";
s1[0] = 'a';
s1[1] = 'a';
s1[2] = 'a';
cout << s1 << endl;
}
4.3、 检查越界
不仅如此,<code>operator[]还能检查是否越界,一旦越界直接报错。这样就能解决我们平时不小心越界却无法检查出来的困扰啦。
void Test5()
{
string s1 = "hello world";
s1[20];
}
5、 string类的三种遍历方式
5.1、 下标访问遍历
我们先来看两个函数接口:<code>size 和 length
5.1.1、 size和length函数
size函数接口
length函数接口
<code>size与length
两函数的功能是一样的:返回字符串的长度
注:计算出的长度不包含字符串中 ‘\0’
size()
与length()
方法底层实现原理完全相同,引入 size()
的原因是为了与其他容器的接口保持一致,一般情况下基本都是用
s
i
z
e
(
)
size()
size()。
5.1.2、 下标访问
下标访问遍历的方式和数组的访问类似,我们直接上代码
void Test6()
{
string s1 = "hello world";
int end = s1.size();
for (int i = 0; i < end; ++i)
{
cout << s1[i] << " ";
}
cout << endl;
}
运行结果:
5.2、 迭代器访问遍历
5.2.1、 初识迭代器访问
首先,我们先看一下迭代器访问的写法:
<code>void Test7()
{
string s1 = "hello world";
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
看不懂没关系,我们现在来讲解。
迭代器属于其对应容器的类域。比如说
s
t
r
i
n
g
string
string,就是
s
t
r
i
n
g
:
:
i
t
e
r
a
t
o
r
string::iterator
string::iterator;后面还会学顺序表
v
e
c
t
o
r
vector
vector;就是
v
e
c
t
o
r
<
i
n
t
>
:
:
i
t
e
r
a
t
o
r
vector <int>::iterator
vector<int>::iterator<code>string::iterator it:我们用
s
t
r
i
n
g
string
string 的迭代器定义了一个对象
i
t
it
it。我们可以将
i
t
it
it 想象成一个
指针
(但它底层不一定是指针),它的用法完全是跟指针类似的s1.begin();
:begin()
是规定返回这块空间开始位置的迭代器;s1.end()
是最后一个有效字符的下一个位置(这里是 ‘\0’ 位置)上述代码逻辑是:当it
不等于end()
时,对其进行解引用。
i
t
it
it 不是指针怎么进行解引用呢?可以进行运算符重载
operator*
。再接着++it
,
i
t
it
it 往后移一位,不是原生指针同样进行运算符重载。
begin函数接口
end函数接口
迭代器提供了一种 通用的 访问容器的方式,<code>所有的容器都可以用这种方式访问,而不需要关心容器的具体实现细节。掌握了
s
t
r
i
n
g
string
string 的迭代器访问方式,就掌握了其他所有容器的访问方式,他们都会提供统一的接口
我们可以通过迭代器进行修改
void Test7()
{
string s1 = "hello world";
string::iterator it = s1.begin();
while (it != s1.end())
{
*it += 1;
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
当然,如果对象本身是被
c
o
n
s
t
const
const 修饰,那就<code>不能用普通迭代器了,因为普通迭代器可读可写,这时就要用 const迭代器 了。
void Test12()
{
const string s1 = "hello world";
string::const_iterator it = s1.begin(); //const迭代器
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
const迭代器
的特点是只读,其不能修改指向的内容
,但其自身是可以修改的
(它自己还要++呢)
5.2.2、 反向迭代器
上述使用迭代器string::iterator
来遍历,我们将其称为正向迭代器。此外,还有反向迭代器:string::reverse_iterator
。
反向迭代器是用来倒着遍历的,获取反向迭代器的起始位置用
r
b
e
g
i
n
(
)
rbegin()
rbegin() 函数,获取结束位置用
r
e
n
d
(
)
rend()
rend() 函数
void Test11()
{
string s1 = "hello world";
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit;
//这里是++而不是--,它的++是倒着走的,因为是反向迭代器
++rit;
}
cout << endl;
}
运行结果:
我们可以这样来理解:<code>rbegin指向最后一个有效位置,rend
指向第一个字符的前一个位置。当然,其实际底层并不一定是这样,只是为了方便我们理解
rbegin函数接口
rend函数接口
而同样,反向迭代器也是有
c
o
n
s
t
const
const版本 的:<code>const_reverse_iterator。这里就不再演示了
5.3、 范围 for 访问遍历
5.3.1、 初识范围for访问
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此
C++11
中引入了基于范围的
f
o
r
for
for循环。
f
o
r
for
for 循环后的括号由冒号 “:” 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。范围
f
o
r
for
for 可以作用到
数组
和容器对象
上进行遍历范围
f
o
r
for
for 的底层很简单,其实就是迭代器,这个从汇编层也可以看到
我们先来看 范围
f
o
r
for
for 是怎么写的
void Test8()
{
string s1 = "hello world";
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
运行结果:
范围for是一种自动赋值、自动迭代、自动判断结束的访问方式。
<code>for (auto ch : s1):自动从容器 (
s
1
s1
s1) 中取其每一个值(字符),给
c
h
ch
ch 变量。该变量的类型是
a
u
t
o
auto
auto(自动推导),这里不写
a
u
t
o
auto
auto 也可以写
c
h
a
r
char
char,但一般都写
auto
{}
中的内容,就是用户需要对容器中每个值的具体操作
范围
f
o
r
for
for 看起来非常厉害,不用自动来迭代和判断,但其本质上这段代码编译以后,会替换成迭代器。其底层就是迭代器,就像引用底层时指针一样。所以所有的容器,只要支持迭代器,就支持 范围
f
o
r
for
for。
当然,范围
f
o
r
for
for 也可以用来遍历数组:
void Test10()
{
int array[] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto e : array)
{
e *= 2;
cout << e << " ";
}
cout << endl;
}
运行结果:
5.3.2、 范围for访问的注意事项
范围
f
o
r
for
for 访问也是可以进行修改的
<code>void Test8()
{
string s1 = "hello world";
for (auto ch : s1)
{
ch += 1;
cout << ch << " ";
}
cout << endl;
}
运行结果:
可是我们直接打印
s
1
s1
s1 会发现
s
1
s1
s1 的值并没有改变
<code>void Test8()
{
string s1 = "hello world";
for (auto ch : s1)
{
ch += 1;
cout << ch << " ";
}
cout << endl;
cout << s1 << endl;
}
运行结果:
这是为什么呢?
<code>for (auto ch : s1):可以简单理解在底层转换成迭代器后 *it
取出
s
1
s1
s1 中的值,拷贝
给
c
h
ch
ch。既然是拷贝,对
c
h
ch
ch 的修改自然无法改变
s
1
s1
s1 中的值啦。迭代器能够修改,是因为 it
相当于是指针一样,通过指针来修改当然可以修改啦
范围
f
o
r
for
for 想修改应用 引用:
void Test8()
{
string s1 = "hello world";
for (auto& ch : s1)
{
ch += 1;
cout << ch << " ";
}
cout << endl;
cout << s1 << endl;
}
运行结果:
这样,
c
h
ch
ch 相当于
s
1
s1
s1 中每个值的别名,就可以对
s
1
s1
s1 的值进行修改啦
5.3.3、 auto关键字
5.3.3.1、 auto关键字的简单认识
在这里补充 2 个 <code>C++11 的小语法
在早期 C/C++ 中
a
u
t
o
auto
auto 的含义是:使用
a
u
t
o
auto
auto 修饰的变量,是具有自动存储器的局部变量,后来这个功能没什么用,没废除了C++11 中,标准委员会变废为宝赋予了
a
u
t
o
auto
auto 全新的含义,即:
a
u
t
o
auto
auto 不再是存储类型指示符,而是作为一个
新的类型指示符来指示编译器
,
a
u
t
o
auto
auto 声明的变量必须
由编译器在编译时期推导而得
。
int func1()
{
return 10;
}
void Test9()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
//typeid().name()可以帮助我们看变量的类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
}
注:typeid().name()
可以帮助我们看变量的类型。
运行结果:
但是
a
u
t
o
auto
auto 不能直接这样定义:
<code>auto e;
a
u
t
o
auto
auto 的具体类型是 根据右边的表达式或者返回值来推导 的,上述定义方式无法推导出
e
e
e 的类型,这样就不知道给
e
e
e 该开多大的空间
auto
的价值主要是简化代码,如果类型太长,我们可以使用
a
u
t
o
auto
auto 让编译器自己来推导。
string::iterator it = s1.begin();
auto it = s1.begin();
map<string, string>::iterator mit = dict.brgin();
auto mit = dict.begin();
但是
a
u
t
o
auto
auto 也有一些缺陷,某种程度上
a
u
t
o
auto
auto 减小了代码的可读性
。比如上述代码我们不能一眼看出
i
t
it
it 的类型就是string::iterator
5.3.3.1、 auto关键字的注意事项
用
auto
声明指针类型时,用
a
u
t
o
auto
auto 和
a
u
t
o
auto
auto* 没有任何区别,只是
a
u
t
o
auto
auto*
必须是指针
。但是用
a
u
t
o
auto
auto 声明引用类型时,必须加 &
void Test9()
{
int x = 10;
auto y = &x;
auto* z = &x;
auto& m = x;
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(z).name() << endl;
cout << typeid(m).name() << endl;
}
当在同一行声明多个变量时,这些变量必须是相同类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出的类型定义其他变量
<code>auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto cc = 3, dd = 4.0;
a
u
t
o
auto
auto 不能直接用来声明数组
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
auto array[] = { 4, 5, 6 };
a
u
t
o
auto
auto 不能做参数,但是
a
u
t
o
auto
auto 可以做返回值,但一定要谨慎使用
auto func1()
{
auto a = 1;
return a;
}
auto func2()
{
return func1();
}
auto func3()
{
return func2;
}
int main()
{
auto ret = func3();
return 0;
}
像这样,
r
e
t
ret
ret 的类型是什么?看
f
u
n
c
3
func3
func3 的返回值,
f
u
n
c
3
func3
func3 去看
f
u
n
c
2
func2
func2,
f
u
n
c
2
func2
func2 去看
f
u
n
c
1
func1
func1。如果每个函数又有一大堆逻辑,那么代码效率将会很低,可读性也很低
6、 string类的容量操作
函数名称 | 功能说明 |
---|---|
s
i
z
e
size
size (重点) | 返回字符串有效字符长度 |
l
e
n
g
t
h
length
length | 返回字符串有效字符长度 |
m
a
x
max
max_
s
i
z
e
size
size | 返回字符串的最大长度 |
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 填充 |
6.1、 reserve
r
e
s
e
r
v
e
reserve
reserve 函数的作用是为字符串预留空间
<code>void test10()
{
string s1;
s1.reserve(50);
cout << s1.capacity() << endl;
}
程序实际上开辟的空间往往是大于等于程序员所要求的空间的,这是为了遵循对<code>齐原则。上述就是对齐到 64
,因为 ‘\0’ 是不计入空间的,所以为 63
这样,我们就能通过
r
e
s
e
r
v
e
reserve
reserve 函数提前在
s
t
r
i
n
g
string
string 中开辟空间,以减少扩容的次数
了。
但是,当预留空间小于原空间,甚至小于字符个数呢?
s
i
z
e
size
size <
n
n
n <
c
a
p
a
c
i
t
y
capacity
capacity :C++ 并没有做出明确规定,
编译器可自行选择缩容还是不做处理
n
n
n <
s
i
z
e
size
size:C++ 也没有做出明确规定,编译器
也是自行选择是否缩容
,但有一点:即使缩容也不改变字符的长度
在VS编译器中,两种情况都是不缩容
void test11()
{
string s1 = "hello world hello world hello world";
cout << "字符个数:" << s1.size() << endl;
cout << "当前空间" << s1.capacity() << endl;
s1.reserve(40);
cout << "当前空间" << s1.capacity() << endl;
s1.reserve(20);
cout << "当前空间" << s1.capacity() << endl;
}
6.1、 resize
r
e
s
i
z
e
resize
resize 是 调整字符串的大小,即将字符串调整为
n
n
n 个字符的长度
n
n
n 个字符,并 删除 超出第
n
n
n 个字符的字符。
n 大于当前字符串长度
:则通过在末尾插入所需数量的字符来扩展当前内容,以达到
n
n
n 的大小 (若空间不够,则进行扩容)。如果指定了
c
c
c,则新元素将初始化为
c
c
c 的副本,否则,它们是值初始化字符(空字符)。
n
n
n <
s
i
z
e
size
size
void test12()
{
string s1 = "hello world";
cout << " 字符个数:" << s1.size() << endl;
cout << " 空间大小:" << s1.capacity() << endl << endl;
s1.resize(5);
cout << " 修改后字符个数:" << s1.size() << endl;
cout << " 修改后空间大小:" << s1.capacity() << endl;
}
s
i
z
e
size
size <
n
n
n <
c
a
p
a
c
i
t
y
capacity
capacity
<code>void test12()
{
string s1 = "hello world";
cout << " 字符个数:" << s1.size() << endl;
cout << " 空间大小:" << s1.capacity() << endl << endl;
s1.resize(13);
cout << " 修改后字符个数:" << s1.size() << endl;
cout << " 修改后空间大小:" << s1.capacity() << endl;
}
n
n
n >
c
a
p
a
c
i
t
y
capacity
capacity
<code>void test12()
{
string s1 = "hello world";
cout << " 字符个数:" << s1.size() << endl;
cout << " 空间大小:" << s1.capacity() << endl << endl;
s1.resize(20);
cout << " 修改后字符个数:" << s1.size() << endl;
cout << " 修改后空间大小:" << s1.capacity() << endl;
}
6.2、 注意事项
<code>size() 与
length()
方法底层实现原理完全相同,引入size()
的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()
clear()
只是将
s
t
r
i
n
g
string
string 中有效字符清空,不改变底层空间的大小
resize(size_t n)
与resize(size_t n, char c)
都是将字符串中有效字符个数改变到
n
n
n 个。不同的是当字符个数增多时:
resize(n)
用 0 来填充多出的元素空间,resize(size_t n,charc)
用字符 c 来填充多出的元素空间。注意:
r
e
s
i
z
e
resize
resize 在改变元素个数时,如果是将元素个数
增多
,可能改变底层容量的大小,如果将元素个数减少
,底层空间总大小不变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
reserve
reserve 的行为是不确定的 。
7、 string类对象的修改操作
函数名称 | 功能说明 |
---|---|
p
u
s
h
push
push_
b
a
c
k
back
back | 在字符串后尾插字符
c
c
c |
a
p
p
e
n
d
append
append | 在字符串后追加一个字符串 |
o
p
e
r
a
t
o
r
operator
operator+= (重点) | 在字符串后追加字符或字符串 |
c
c
c_
s
t
r
str
str (重点) | 返回 C 格式字符串 |
f
i
n
d
find
find | 从字符串
p
o
s
pos
pos 位置开始往后找字符
c
c
c,返回该字符在字符串中的位置 |
r
f
i
n
d
rfind
rfind | 从字符串
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 | 在指定位置追加字符或者字符串 |
e
r
a
s
e
erase
erase | 删除字符串指定部分 |
s
w
a
p
swap
swap (重点) | 交换两个
s
t
r
i
n
g
string
string 对象 |
f
i
n
d
find
find _
f
i
r
s
t
first
first _
o
f
of
of | 在字符串中搜索与其参数中指定的任何字符匹配的第一个字符 |
7.1、 operator+=
o
p
e
r
a
t
o
r
operator
operator+= 是在后面追加字符或字符串,甚至是
s
t
r
i
n
g
string
string 对象
<code>void test14()
{
string s1 = "hello";
cout << s1 << endl;
s1 += 'a';
cout << s1 << endl;
s1 += "你好";
cout << s1 << endl;
string s2 = "haha";
s1 += s2;
cout << s1 << endl;
}
通常,我们对 string 对象进行尾插都是用
o
p
e
r
a
t
o
r
operator
operator+=函数,因为 ‘+=’ 生动形象。当然尾插还有 <code>push_back、append
等函数
7.2、 c_str
c
c
c_
s
t
r
str
str 函数的功能是获取存储字符串空间的地址
s
t
r
i
n
g
string
string 的 底层 简单来看如下:
<code>class string
{
private:
char* _str;//指向存储字符串的空间
size_t _size;//记录当前字符串长度
size_t _capacity;//记录当前空间大小
};
而
c
c
c_
s
t
r
str
str 就是获取 _
s
t
r
str
str。
void test16()
{
string s1 = "hello world";
printf("%p\n", s1.c_str());
printf("%s\n", s1.c_str());
}
7.3、 swap
<code>swap函数 是交换两个
s
t
r
i
n
g
string
string 对象。
可能有小伙伴很疑惑,C++ 库中不是有
s
w
a
p
swap
swap模板 吗?为什么
s
t
r
i
n
g
string
string 库中又要写一个
s
w
a
p
swap
swap函数 呢?
这肯定是因为
s
w
a
p
swap
swap模板 生成的
s
w
a
p
swap
swap函数 有缺点
我们一起来看下
s
w
a
p
swap
swap模板 的缺点。
下面是
s
w
a
p
swap
swap模板 的内部实现
当交换两个
s
t
r
i
n
g
string
string 对象时,生成的函数是这样的:
<code>void swap(string& a, string& b)
{
string c(a);
a = b;
b = c;
}
如果是传统的交换,会进行 3次
深拷贝
:
首先是用
a
a
a 拷贝构造
c
c
c后
b
b
b 拷贝给
a
a
a最后是
c
c
c 拷贝给
b
b
b 3次 拷贝都是
深拷贝
。
对自定义类型来说,深拷贝的代价是很大的,每次深拷贝都要开空间拷贝数据。而你现在还是深拷贝 3 次,
效率无疑是大大降低
。
那有没有办法提升效率呢?
首先,我们知道,
s
t
r
i
n
g
string
string 底层类似一个顺序表:
class string
{
private:
char* _str;//指向存储字符串的空间
size_t _size;//记录当前字符串长度
size_t _capacity;//记录当前空间大小
};
那么我们可不可以就两个对象指针所指向的空间进行交换。这样,仅仅是内置类型进行
交换效率会大大提高
再把两个对象中的 <code>_size 和
_capacity
各自交换一下,就完成啦
实际上,
s
t
r
i
n
g
string
string 库中就是这么实现的
void swap(string& s1)
{
std::swap(_str, s1._str);
std::swap(_size, s1._size);
std::swap(_capacity, s1._capacity);
}
而我们知道,当模板和函数命名重合时,若函数更适合,则优先调用函数
。因此是不会调用库中的函数模板的。
8、 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 (重点) | 获取一行字符串 |
r
e
l
a
t
i
o
n
a
l
relational
relational
o
p
e
r
a
t
o
r
s
operators
operators(重点) | 大小比较 |
s
w
a
p
swap
swap (重点) | 交换两个对象 |
8.1、 getline
s
t
r
i
n
g
string
string 对象中,以
d
e
l
i
m
delim
delim 为结束标志,默认是 ‘\n’
那
g
e
t
l
i
n
e
getline
getline 有什么用呢?库中不是重载了
o
p
e
r
a
t
o
r
operator
operator>> 吗?
我们知道,用
c
i
n
cin
cin 输入字符串默认是以 “ ”
和 “\n”
为分隔符的,因此即使读到了 “ ”
和 “\n”
,
c
i
n
cin
cin 也会忽略他们,将他们跳过。
当我们想输入的字符串中有 “ ”
或 “\n”
时,就可以用
g
e
t
l
i
n
e
getline
getline 函数
void test15()
{
string s1;
cout << "请输入字符串:";
getline(cin, s1);
cout << "s1内容:" << s1 << endl;
}
<code>void test15()
{
string s1;
cout << "请输入字符串:";
getline(cin, s1, '#');
cout << endl;
cout << "s1内容:" << endl << s1 << endl;
}
8.2、 swap
非成员的 <code>swap函数 的实际行为与成员函数中 swap
的行为是一样的,这里就不再过多介绍
9、 不同平台下string类的结构
注:下述结构是在 32 位平台下进行验证,32位平台下指针占 4 字节
9.1、 VS 下string类的结构
VS 下
s
t
r
i
n
g
string
string 总共占 28 个字节,内部结构稍微复杂一点,先是有一个联合体
,联合体用来定义
s
t
r
i
n
g
string
string 中字符串的存储空间
当字符串长度小于 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;
class String
{
private:
//vs下string类里面的成员变量大概是这样
char _buff[16];
char* str;
size_t _size;
size_t capacity;
};
int main()
{
cout << sizeof(String) << endl;
return 0;
}
这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16,那
s
t
r
i
n
g
string
string 对象创建好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个
s
i
z
e
size
size _
t
t
s
i
z
e
size
size_
t
t
t 字段保存从堆开辟空间的容量
最后:还有一个指针做一下其他事情
故总共占 16 + 4 + 4 = 28 个字节
VS 下
s
t
r
i
n
g
string
string 的扩容
<code>void test17()
{
string s;
size_t sz = s.capacity();
cout << "原始大小:" << sz << endl;
cout << "making s grow:" << endl;
for (int i = 0; i < 1000; i++)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity change:" << sz << "\n";
}
}
}
<code>capacity函数所获得的空间是容纳有效字符的最大空间,是不包括 ‘\0’ 的,实际空间还要再 +1
我们可以看到,在 VS 下,从数组转到堆开辟的空间时
,是 2 倍扩容,即16 -> 32;之后在堆上的扩容都是 1.5倍 扩容
9.2、 g++ 下string类的结构
g++ 下,
s
t
r
i
n
g
string
string 是通过写实拷贝实现的,
s
t
r
i
n
g
string
string 对象总共占4个字节,内部只含有一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小
字符串有效长度
引用计数
1指向堆空间的指针,用来存储字符串
<code>struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
g++ 下,
s
t
r
i
n
g
string
string 的扩容
运行结果:
可以看到,g++ 下是标准的 2 倍扩容。
引用计数:<code>用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源, 如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。想进一步了解可浏览下面两篇文章:写实拷贝、写实拷贝在读取时是缺陷的 ↩︎
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。