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 掉原来的那片空间
为什么不能直接截断?答:内存空间是不能分段释放的
因此:这里缩容的本质是拷贝加释放(在某些时候,拷贝的代价较大)