【CPP】—— string类的了解与使用
1、 为什么学习string 类
1.1、 C语言中的字符串
C语言中,字符串是以 '\0'
结尾的一些字符的集合,为了操作方便,C标准库 提供了一些str系列
的库函数,但是这些库函数与字符串是分离开的,不太符合 OP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
C语言中的字符串有诸多弊端。
1.2、 面试题中更多以 string 类出现
在 OJ 中,有关字符串的题目基本以 string类
的形式出现,而且在常规工作中,为了简单、方便快捷,基本都使用 string类
,很少有人去使用 C语言 库中的字符串操作函数。
下面是两个面试题(暂不做讲解)
2、 标准库中的 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 类中的一个静态成员变量
,
n
o
p
s
nops
nops的值是 -1,但其为
s
i
z
e
size
size_
t
t
t 类型,所以实际值为整型的最大值
。
编译器认为你的字符串不可能有这么长 (42亿9千万字节),所以
n
o
p
s
nops
nops 的意思是有多长取多长。
我们来一起来实践一下
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 个我们也来看下
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对象进行赋值
- 使用字符串进行赋值
- 使用单个字符进行赋值
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 类重载了[]
,使用户可以像数组一样访问 string类 中的字符
s
t
r
i
n
g
string
string 类重载了两个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、 修改
同时,因为 operator[]
返回的是字符的引用,这意味着普通版的operator[]
不仅仅可以获取相应位置的字符,还能对其进行修改,
void Test5()
{
string s1 = "hello world";
s1[0] = 'a';
s1[1] = 'a';
s1[2] = 'a';
cout << s1 << endl;
}
4.3、 检查越界
不仅如此,operator[]
还能检查是否越界,一旦越界直接报错。这样就能解决我们平时不小心越界却无法检查出来的困扰啦。
void Test5()
{
string s1 = "hello world";
s1[20];
}
5、 string类的三种遍历方式
5.1、 下标访问遍历
我们先来看两个函数接口:size
和 length
5.1.1、 size和length函数
size函数接口
length函数接口
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、 初识迭代器访问
首先,我们先看一下迭代器访问的写法:
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
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函数接口
迭代器提供了一种 通用的 访问容器的方式,所有的容器都可以用这种方式访问,而不需要关心容器的具体实现细节
。掌握了
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 修饰,那就不能用普通迭代器了
,因为普通迭代器可读可写,这时就要用 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;
}
运行结果:
我们可以这样来理解:rbegin
指向最后一个有效位置,rend
指向第一个字符的前一个位置。当然,其实际底层并不一定是这样,只是为了方便我们理解
rbegin函数接口
rend函数接口
而同样,反向迭代器也是有
c
o
n
s
t
const
const版本 的: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是一种自动赋值、自动迭代、自动判断结束的访问方式。
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 访问也是可以进行修改的
void Test8()
{
string s1 = "hello world";
for (auto ch : s1)
{
ch += 1;
cout << ch << " ";
}
cout << endl;
}
运行结果:
可是我们直接打印 s 1 s1 s1 会发现 s 1 s1 s1 的值并没有改变
void Test8()
{
string s1 = "hello world";
for (auto ch : s1)
{
ch += 1;
cout << ch << " ";
}
cout << endl;
cout << s1 << endl;
}
运行结果:
这是为什么呢?
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 个
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 不能直接这样定义:
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;
}
- 当在同一行声明多个变量时,这些变量必须是相同类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出的类型定义其他变量
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 函数的作用是为字符串预留空间
void test10()
{
string s1;
s1.reserve(50);
cout << s1.capacity() << endl;
}
程序实际上开辟的空间往往是大于等于程序员所要求的空间的,这是为了遵循对齐原则
。上述就是对齐到 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 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
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
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、 注意事项
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 对象
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+=函数,因为 ‘+=’ 生动形象。当然尾插还有 push_back
、append
等函数
7.2、 c_str
c c c_ s t r str str 函数的功能是获取存储字符串空间的地址
s t r i n g string string 的 底层 简单来看如下:
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
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 对象时,生成的函数是这样的:
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;//记录当前空间大小
};
那么我们可不可以就两个对象指针所指向的空间进行交换。这样,仅仅是内置类型进行
交换效率会大大提高
再把两个对象中的_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
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;
}
void test15()
{
string s1;
cout << "请输入字符串:";
getline(cin, s1, '#');
cout << endl;
cout << "s1内容:" << endl << s1 << endl;
}
8.2、 swap
非成员的 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
t 字段保存字符串长度
,一个
s
i
z
e
size
size_
t
t
t 字段保存从堆开辟空间的容量
最后:还有一个指针做一下其他事情
故总共占 16 + 4 + 4 = 28 个字节
VS 下
s
t
r
i
n
g
string
string 的扩容
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";
}
}
}
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- 指向堆空间的指针,用来
存储字符串
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 倍扩容。
引用计数:
用来记录资源使用者的个数
。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源, 如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。想进一步了解可浏览下面两篇文章:写实拷贝、写实拷贝在读取时是缺陷的 ↩︎