Bootstrap

【C++修炼之路 第五章】string 类(补充):string 扩展空间规律 + reserve 的缩容 + resize 的使用

在这里插入图片描述

在这里插入图片描述



1、深入探寻 string 扩展空间的规律 与 capacity 的变化


1.1 string 内部的 _Buf 数组


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传



打开监视窗口,可以看到 string 类中有个成员变量:数组 _Buf

该数组空间大小为 16字节

上面存入不同长度的字符串时,可以发现两者存储的位置不同

  • 当存入字符串的长度小于 16字节时,就会存入自己的成员:数组中

  • 当大于16字节,会到内存中开辟空间,使用成员变量:指针 _Ptr 指向字符串


  类中 _Buf 数组的设计:小字符串无需存储在内存中,而是存在自己的数组中,以空间换取时间,提高不少时间效率,同时减少了内存碎片的产生

注意:实际上最多只能存入 15个有效字符,第16个字符是用于存字符串结尾的 ‘\0’

  没扩容前,capacity 就是固定为 15 字节大小:如下面字符串本身大小为 6(包括尾部的 ‘0’) ,而capacity 固定为 15,不是 6

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传




1.2 观察 不同平台和不同编译器之间 的不同扩容表现



⭐🐔 写一段程序:观察一下字符串底层的扩容

  其中,若扩容的容量 capacity 大小为单数是因为扩容 capacity 时,capacity 会比真正开的空间少一个,多的一个是留给 ‘\0’ 的



同时,不同平台下不同编译器的扩容表现不同


⭐🐔 (1)Window环境下:VS2022:微软的 msvc


扩容规律:第一次扩容是 2倍扩容,其他次扩容都是 1.5 倍扩容

(先了解,为什么这样扩容的原理不用理解,大致和其本身的设计、 string 的 _Buf 数组 和 编译器有关)

在这里插入图片描述


void TestPushBack()
{
	string s;

	size_t sz = s.capacity();
	cout << "capacity changed: " << sz << '\n';

	cout << "making s grow:\n"; // 下面用一个循环不断往 string 中 push 字符,观察其扩容
	for (int i = 0; i < 200; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity()) // 用 sz 时刻检查其有无进行扩容
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}


⭐🐔 (2)Linux环境下:g++ 4.8

同一段程序 void TestPushBack() 放入 Linux 环境下观察

这明显是标准的 2 倍扩容

在这里插入图片描述

可以将下面这段代码放入 Linux 环境下观察:可知 capacity 是不计算 ‘\0’ 的

string s;
cout << sizeof(s) << '\n'; // sizeof 本身不计算一个字符串的 '\0'
cout << s.capacity() << '\n';
return 0;


⭐🐔 (3) Mac 环境下: Clang 编译器

在这里插入图片描述



⭐🐔 (4)结论

可以得出结论:
1、无论是 VS ,还是 g++,还是 Clang ,string 的 capacity 都是不计算 ‘\0’ 的

2、根据对比可以知道,string 如何扩容,C++标准并没有明确规定,取决于编译器自己的实现




2、关于 reserve



2.1 reserve 的缩容性质 与 使用方法


⭐🐔 reserve 是可以改变 capacity 的

注意分辨两个相似的单词

  • reverse:反转 逆置
  • reserve:保留,存储



⭐🐔 使用 reserve 开空间:观察 capacity 的变化,谈一谈 reserve 的缩容


下面开 100 字节的空间,可以发现:capacity = 111

结论:在目前这个编译器下( 即 VS ),reserve 开空间,会比你申请的数额要大 (VS 比较叛逆doge)

int main()
{
	string s;
	s.reserve(100);
	cout << s.capacity() << '\n';
	return 0;
}

在这里插入图片描述




⭐🐔 对上面同一个 string ,再次让 reserve 开 10个字节空间,有的编译器会缩容成 reserve 指定的大小,有的不会


⭐🐔 (1)在 VS编译器下,capacity 缩水成 15

在这里插入图片描述




⭐🐔 (2)在 Linux下,capacity 会缩成 10:即遵循你的 reserve,不会自己擅作主张(老听话了doge)

即:有些编译器不会主动缩水



⭐🐔实际上,在 VS 下:若 reserve 给的值 小于 当前 capacity ,同时 大于 16 ,默认不缩容

小于 16 缩容是因为:前面介绍过,string 类内部有个 空间大小为 16字节的数组 _Buf ,小于 16 的就放入自己的数组,而不是外面的内存,提高效率

在这里插入图片描述



⭐🐔小结:虽然 reserve 有缩容功能,但是一般不使用缩容,因为不同的平台 reserve 缩容规则不同,会导致代码的可迁移性降低





2.2 reserve 的应用



⭐🐔 应用:使用 reserve 提前开好空间固定的 capacity ,可 减少运行过程的扩容操作

  上面的讨论可以得出结论:reserve 可以控制 capacity 的大小,同时可以固定住 capacity的大小,使空间大小不变化

  结合之前那段【void TestPushBack】的代码:使用 reserve 提前开空间,就无需在程序运行过程中不断的扩容了,这样会影响效率

void TestPushBack()
{
	string s;
    ///
	s.reserve(200);  // 若我们提前知道会用到多大的空间,就直接提前扩容
    ///
	size_t sz = s.capacity();
	cout << "capacity changed: " << sz << '\n';

	cout << "making s grow:\n"; // 下面用一个循环不断往 string 中 push 字符,观察其扩容
	for (int i = 0; i < 200; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity()) // 用 sz 时刻检查其有无进行扩容
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

注意:我们 reserve 200,内部开了 207(为什么不是 200 :前面讲过 VS 比较叛逆doge)

在这里插入图片描述




2.3 reserve 改变空间(capacity),但不改变可访问范围(size)


⭐🐔改变 capacity ,就等于你可以直接访问某块空间了吗?

比如:使用方括号访问下标 110 的位置,直接报错

string s;
s.reserve(200);
s[110] = 'x';

在这里插入图片描述



⭐🐔结论:注意 capacity 是给字符串开辟空间,供字符串使用,而并不代表此时可访问范围有这么大

string 内部还有一个 size 成员,代表字符串的有效长度

capacity 的改变并不会影响 string 的 size (或者说一个容器的可访问范围是当前容器内的数据个数)

在这里插入图片描述




谈一谈 string 内部的 size 和 capacity 各自有什么用?

​ 整个 string 可以访问的范围本来就是 capacity(用这个表示当前 string 拥有的总空间),但是不能全部将 capacity 的空间暴露出来(否则 string 会展示出来 空间后段未被使用的空间,都是随机值,没什么意义),而是用一个变量 size 来控制住 string 的有效范围,不管是 打印,还是什么操作,string 对外展示的就只是 size 的范围,即 string 的真正有效字符的范围

在这里插入图片描述




3、关于 resize


resize 默认初始化为 ASCII码值为 0 的数值:‘\0’(文档中的 空字符就是 ‘\0’)

void resize (size_t n);

也可以自己给初值

void resize (size_t n, char c);


在这里插入图片描述




3.1 resize 扩容,直接改变 size 和 capacity (不像 reserve 只能改变 capacity)


⭐🐔如下图:默认初始化为 ‘\0’ 且 改变 size 和 capacity

void resize (size_t n);  

在这里插入图片描述


⭐🐔给定初始化,同时你空间中原有的值,不会改变,只会影响剩余未被初始化的空间

在这里插入图片描述


⭐🐔resize 不会缩容(即调整空间大小时,当resize的空间小于原来的 capacity 时,也不会改变 capacity)

⭐🐔resize 会 直接调整 字符串内部 size 的大小,直接调小了,会删除原有数据,即 截断后半段

在这里插入图片描述




4、关于 shrink_to_fit (专门用于缩容)

​ 进行某些操作时, capacity 开的空间过多了,想要释放一下,就可以用这个函数:将 capacity 调整成 和 size

一般用这个缩容,不会用 reserve 缩容

在这里插入图片描述




⭐🐔演示使用:

  这里 capacity 被缩容到与 size 差不多的大小(注意:这里的 capacity = 111 ,实际上是 capacity = 100,只是 VS编译器自动加了那 11 (前面讲过 VS 在扩容方面,会擅作主张加一点))

在这里插入图片描述

int main()
{
	string s;
	s.resize(100);  // resize 改变 size:使 size 有基础大小
	s.reserve(1000);    // reserve 改变 capacity,不改变 size:模拟 capacity 很大,需要释放的场景
	cout << s.size() << '\n';
	cout << s.capacity() << "\n\n";

	s.shrink_to_fit();
	cout << s.size() << '\n';
	cout << s.capacity() << '\n';
	return 0;
}


⭐🐔注意:不要经常使用这个函数

⭐🐔你以为他底层的缩容机制:是直接将后面不要的部分直接释放掉吗?(是直接截断后面的部分吗?)

实际上:是先找一块适当大小的空间,将原有空间的数据拷贝到新空间中,再 free 掉原来的那片空间

为什么不能直接截断?答:内存空间是不能分段释放的

因此:这里缩容的本质是拷贝加释放(在某些时候,拷贝的代价较大)


;