我看了一堆相关文章,都把读者当聪明人。提到shrink_to_fit只提一嘴会收缩内存。初学的时候我天真的以为把原来持有的内存后半段不用的释放掉。但是如果经常这么释放的话难道不会造成一堆夹在中间的小内存碎片吗?还是看看源码来得实在。

随便写一个vector,ctrl一下都能导航到标准库的源码(win所以看的是MSVC)。

搜索shrink,发现shrink出现在了两个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
_CONSTEXPR20 void shrink_to_fit() { // reduce capacity to size, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
const pointer _Oldlast = _My_data._Mylast;
if (_Oldlast != _My_data._Myend) { // something to do
const pointer _Oldfirst = _My_data._Myfirst;
if (_Oldfirst == _Oldlast) {
_Tidy();
} else {
size_type _Newcapacity = static_cast<size_type>(_Oldlast - _Oldfirst);
_Reallocate<_Reallocation_policy::_Exactly>(_Newcapacity);
}
}
}
1
2
3
4
5
6
_CONSTEXPR20 void shrink_to_fit() {
if (this->_Myvec.capacity() != this->_Myvec.size()) {
this->_Orphan_all();
this->_Myvec.shrink_to_fit();
}
}

第二个shrink_to_fit很明显是在调用第一个之前去做了一些检查或者通知的操作。

让我们仔细看看第一个实际实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_CONSTEXPR20 void shrink_to_fit() { // reduce capacity to size, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
const pointer _Oldlast = _My_data._Mylast;
//这里会先检查当前大小是否已经等于容器容量,如果已经满了就不用操作
if (_Oldlast != _My_data._Myend) { // something to do
const pointer _Oldfirst = _My_data._Myfirst;
if (_Oldfirst == _Oldlast) {
//如果空了,那就不保留任何内存块了,释放所有资源(暂时没看Tidy的实现)
_Tidy();
} else {
//如果不空,且存在冗余的容量
//先计算目标容量:注意要等于当前元素个数size
//然后重新分配!去强制分配器只申请之前元素个数容量大小的内存
size_type _Newcapacity = static_cast<size_type>(_Oldlast - _Oldfirst);
_Reallocate<_Reallocation_policy::_Exactly>(_Newcapacity);
}
}
}

这里的实现细节值得注意。通常 vector 的扩容(如 push_back 触发)会采用几何级数增长(例如 1.5 倍),以减少未来的分配次数。但在 shrink_to_fit 中,通过模板参数 _Reallocation_policy::_Exactly 显式覆盖了这一默认行为。这意味着 _Reallocate 内部会执行一次标准的“分配新块 -> 移动/拷贝元素 -> 释放旧块”流程,且新块的大小被严格限制为 size()。注释中提到的 “strong guarantee” 意味着如果在移动构造元素过程中抛出异常,原 vector 的状态保持不变,不会丢失数据。

注意到了这一行:

1
_Reallocate<_Reallocation_policy::_Exactly>(_Newcapacity);

_Reallocation_policy意味着还有别的策略,进去看一眼:

会发现这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum class _Reallocation_policy { _At_least, _Exactly };

template <_Reallocation_policy _Policy>
_CONSTEXPR20 void _Reallocate(size_type& _Newcapacity) {
// set capacity to _Newcapacity (without geometric growth), provide strong guarantee
auto& _Al = _Getal();
auto& _My_data = _Mypair._Myval2;
pointer& _Myfirst = _My_data._Myfirst;
pointer& _Mylast = _My_data._Mylast;

const auto _Size = static_cast<size_type>(_Mylast - _Myfirst);

pointer _Newvec;
//这里执行判断逻辑
if constexpr (_Policy == _Reallocation_policy::_At_least) {
_Newvec = _Allocate_at_least_helper(_Al, _Newcapacity);
} else {
_STL_INTERNAL_STATIC_ASSERT(_Policy == _Reallocation_policy::_Exactly);
_Newvec = _Al.allocate(_Newcapacity);
}

_Simple_reallocation_guard _Guard{_Al, _Newvec, _Newcapacity};

if constexpr (is_nothrow_move_constructible_v<_Ty> || !is_copy_constructible_v<_Ty>) {
_Uninitialized_move(_Myfirst, _Mylast, _Newvec, _Al);
} else {
_Uninitialized_copy(_Myfirst, _Mylast, _Newvec, _Al);
}

_Guard._New_begin = nullptr;
_Change_array(_Newvec, _Size, _Newcapacity);
}

看来有两种策略:_At_least _Exactly

1
2
3
4
5
6
7
8
if constexpr (_Policy == _Reallocation_policy::_At_least) {
// 策略A:至少分配这么多
_Newvec = _Allocate_at_least_helper(_Al, _Newcapacity);
} else {
// 策略B:精确分配这么多
_STL_INTERNAL_STATIC_ASSERT(_Policy == _Reallocation_policy::_Exactly);
_Newvec = _Al.allocate(_Newcapacity);
}

这时候好奇在至少分配模式的时候,这个_Newcapacity是不是会被修改。

应该是push_back的时候就应该已经预分配好了容量了吧。验证一下:

找push_back:

1
2
3
4
5
6
7
8
_CONSTEXPR20 void push_back(const _Ty& _Val) { // insert element at end, provide strong guarantee
_Emplace_one_at_back(_Val);
}

_CONSTEXPR20 void push_back(_Ty&& _Val) {
// insert by moving into element at end, provide strong guarantee
_Emplace_one_at_back(_STD move(_Val));
}

这应该是两个实现,一个接收左值引用一个接收右值引用以启用移动构造。

看一下_Emplace_one_at_back的右值引用版本

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class... _Valty>
_CONSTEXPR20 _Ty& _Emplace_one_at_back(_Valty&&... _Val) {
// insert by perfectly forwarding into element at end, provide strong guarantee
auto& _My_data = _Mypair._Myval2;
pointer& _Mylast = _My_data._Mylast;

if (_Mylast != _My_data._Myend) {
//容量足够,直接构造
return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
}
//不够的话需要扩容了
return *_Emplace_reallocate(_Mylast, _STD forward<_Valty>(_Val)...);
}

看一下_Emplace_reallocate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template <class... _Valty>
_CONSTEXPR20 pointer _Emplace_reallocate(const pointer _Whereptr, _Valty&&... _Val) {
// reallocate and insert by perfectly forwarding _Val at _Whereptr
_Alty& _Al = _Getal();
auto& _My_data = _Mypair._Myval2;
pointer& _Myfirst = _My_data._Myfirst;
pointer& _Mylast = _My_data._Mylast;

_STL_INTERNAL_CHECK(_Mylast == _My_data._Myend); // check that we have no unused capacity

const auto _Whereoff = static_cast<size_type>(_Whereptr - _Myfirst);
const auto _Oldsize = static_cast<size_type>(_Mylast - _Myfirst);

if (_Oldsize == max_size()) {
_Xlength();
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!这里在增长
const size_type _Newsize = _Oldsize + 1;
size_type _Newcapacity = _Calculate_growth(_Newsize);

const pointer _Newvec = _STD _Allocate_at_least_helper(_Al, _Newcapacity);
const pointer _Constructed_last = _Newvec + _Whereoff + 1;
...


_Guard._New_begin = nullptr;
_Change_array(_Newvec, _Newsize, _Newcapacity);
return _Newvec + _Whereoff;
}

终于找到了

1
2
3
4
5
const size_type _Newsize = _Oldsize + 1;
size_type _Newcapacity = _Calculate_growth(_Newsize);

const pointer _Newvec = _STD _Allocate_at_least_helper(_Al, _Newcapacity);
const pointer _Constructed_last = _Newvec + _Whereoff + 1;

那么增长逻辑就在:size_type _Newcapacity = _Calculate_growth(_Newsize);里,计算完新Newcapacity直接传给刚刚已经看过的_Allocate_at_least_helper(_Al, _Newcapacity)。

继续打开看看_Calculate_growth(_Newsize):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_CONSTEXPR20 size_type _Calculate_growth(const size_type _Newsize) const {
// given _Oldcapacity and _Newsize, calculate geometric growth
const size_type _Oldcapacity = capacity();
const auto _Max = max_size();


if (_Oldcapacity > _Max - _Oldcapacity / 2) {
return _Max; // geometric growth would overflow
}

const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;

if (_Geometric < _Newsize) {
return _Newsize; // geometric growth would be insufficient
}

return _Geometric; // geometric growth is sufficient
}

着急下班吃饭懒得写注释了,交给AI老师帮忙解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
_CONSTEXPR20 size_type _Calculate_growth(const size_type _Newsize) const {
// 目标:给定当前容量和目标大小 (_Newsize),计算新的几何增长容量。
// 核心策略:MSVC STL 采用 1.5 倍增长因子 (Old + Old/2)。

// 1. 获取当前状态
const size_type _Oldcapacity = capacity(); // 当前已分配的容量
const auto _Max = max_size(); // 理论上的最大容量 (通常是 size_t 极限 / 元素大小)

// 2. 溢出保护检查 (Overflow Check)
// 问题:如果 _Oldcapacity 很大,直接计算 "_Oldcapacity + _Oldcapacity / 2" 可能会导致整数溢出(变成一个小数)。
// 解法:将加法变换为减法来判断。
// 逻辑:如果 当前容量 > (最大值 - 增量),说明加上增量后会超过最大值。
if (_Oldcapacity > _Max - _Oldcapacity / 2) {
return _Max; // 既然几何增长会溢出,那就直接返回允许的最大值,顶格处理。
}

// 3. 计算几何增长值 (Geometric Growth)
// 核心公式:NewCap = OldCap + floor(OldCap / 2)
// 这就是著名的 1.5 倍增长策略。
// 例如:10 -> 15, 100 -> 150, 1000 -> 1500.
// 为什么是 1.5 而不是 2.0?
// - 2.0 倍 (GCC/libstdc++): 扩容更激进,realloc 次数少,但内存浪费严重 (空闲内存=已用内存)。
// - 1.5 倍 (MSVC/Clang libc++): 在“减少 realloc 次数”和“内存利用率”之间取得平衡。
// 数学上,1.5 倍增长使得旧的空闲内存块有机会被后续的更大分配复用,减少堆碎片。
const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;

// 4. 需求充足性检查 (Sufficiency Check)
// 场景:用户可能一次性请求大量空间 (如 reserve(10000)),或者 insert 大量元素。
// 此时,按部就班的 1.5 倍增长可能远远不够 (例如当前 100,1.5 倍是 150,但用户需要 10000)。
// 逻辑:如果计算出的几何增长值 < 用户实际需要的最小值 (_Newsize),
// 则抛弃几何增长,直接以用户需求为准。
if (_Geometric < _Newsize) {
return _Newsize; // 几何增长不足以满足需求,直接扩容到需求大小。
}

// 5. 返回最终结果
// 如果既没有溢出,且 1.5 倍增长也足够大,那就采用这个几何增长值。
return _Geometric;
}

注意到

1
2
3
if (_Geometric < _Newsize) {
return _Newsize;
}

扩容也不是无限制的,1.5倍不够就直接按需分配了。