【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 数组 和 编译器有关)

cpp 复制代码
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' 的

cpp 复制代码
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)

cpp 复制代码
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 提前开空间,就无需在程序运行过程中不断的扩容了,这样会影响效率

cpp 复制代码
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 的位置,直接报错

cpp 复制代码
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')

cpp 复制代码
void resize (size_t n);

也可以自己给初值

cpp 复制代码
void resize (size_t n, char c);


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

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

cpp 复制代码
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 在扩容方面,会擅作主张加一点))

c++ 复制代码
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 掉原来的那片空间

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

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


相关推荐
胡斌附体10 分钟前
微服务调试问题总结
java·微服务·架构·调试·本地·夸微服务联调
珊瑚里的鱼13 分钟前
第九讲 | 模板进阶
开发语言·c++·笔记·visualstudio·学习方法·visual studio
bing_15820 分钟前
Spring MVC HttpMessageConverter 的作用是什么?
java·spring·mvc
笨蛋不要掉眼泪29 分钟前
SpringAOP
java·数据库·spring·log4j
摄殓永恒40 分钟前
猫咪几岁
数据结构·c++·算法
oioihoii1 小时前
C++23 新增的查找算法详解:ranges::find_last 系列函数
java·算法·c++23
酷炫码神1 小时前
C#数据类型
java·服务器·c#
難釋懷1 小时前
Android开发-数据库SQLite
android·数据库·sqlite
.小墨迹1 小时前
Apollo学习——键盘控制速度
linux·开发语言·c++·python·学习·计算机外设
似水এ᭄往昔1 小时前
【数据结构】——队列
c语言·数据结构·c++·链表