【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 个字符的长度

<code>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

在这里插入图片描述

<code>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

t 字段保存<code>字符串长度,一个

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,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。想进一步了解可浏览下面两篇文章:写实拷贝、写实拷贝在读取时是缺陷的 ↩︎



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。