C++中vector<T>().swap(x)的底层行为与实现方法
vector<T>().swap(x) 实现原理与手写方法 先把这句代码拆成两步看,逻辑非常直白:
std::vector<T>()是调用vector默认构造函数生成的临时匿名空对象,标准实现下这个对象的size为0、capacity为0,不会预先分配任何堆内存。- 这个临时对象立刻调用成员函数
swap(src),和你定义的src对象交换内部持有的资源。
swap操作的本质
vector的内部实现一般只维护三个核心指针,没有多余的复杂状态:
_start:指向分配的堆内存块起始地址_finish:指向最后一个有效元素的下一个位置,_finish - _start就是当前vector的size_end_of_storage:指向分配的堆内存块的末尾位置,_end_of_storage - _start就是当前vector的capacity
成员函数swap是绝对的O(1)操作,不会拷贝、移动任何容器内的元素,也不会分配或释放内存,它做的事只有一件:直接把两个vector对象的三个内部指针互相交换,如果使用了带状态的分配器,顺带交换分配器实例(默认的std::allocator是无状态的,这步没有额外开销)。
交换完成后的自动析构逻辑
swap执行完的瞬间,两个对象的状态就完全对调了:
- 原来的临时空对象现在持有src之前的那块堆内存,上面存着所有已插入的元素,size和capacity也和原来的src完全一致
- 原来的src现在持有临时对象原本的空状态,三个指针都是空,size=0、capacity=0
而临时匿名对象的生命周期只到当前这行语句结束,语句执行完它会立刻被析构。临时对象析构时,会先逐个销毁自己持有的(也就是原来src的)所有元素,再释放对应的堆内存——原来src占用的所有资源就被彻底清理干净了,没有任何残留。
这也是为什么这个写法比直接调用src.clear()效果更彻底:clear()只会销毁所有有效元素、把size设为0,不会释放已经申请的堆内存,调用完capacity还是保持原来的值,做不到内存回收。
手写vector时的实现要点
只要把三个核心模块写正确,这个逻辑不需要额外写特殊处理就能自动支持:
- 默认构造函数:不要做多余的内存预分配,直接把三个内部指针初始化为空即可
// 极简默认构造实现 vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}
- 成员swap函数:只做内部指针的交换,不要加任何元素拷贝、内存操作逻辑
void swap(vector& other) { std::swap(_start, other._start); std::swap(_finish, other._finish); std::swap(_end_of_storage, other._end_of_storage); // 若使用带状态的分配器,在此处补充分配器交换逻辑即可 }
- 析构函数:先逐个析构有效区间内的元素,再释放持有的堆内存
~vector() { if (_start) { // 逐个析构[_start, _finish)区间的有效元素 for (T* p = _start; p != _finish; ++p) { p->~T(); } // 释放堆内存,这里以默认分配器为例 std::allocator<T>().deallocate(_start, _end_of_storage - _start); _start = _finish = _end_of_storage = nullptr; } }
你之前理解的swap1和swap2的核心区别,就在于交换的对象生命周期不同:swap1是和具名局部变量tmp交换,tmp会等到所在作用域结束才会析构释放内存;swap2是和临时对象交换,临时对象在当前行执行完就立刻析构,马上完成旧内存的释放,所以src会直接变成容量为0的空容器。
顺便提一句,这个写法是C++11标准推出shrink_to_fit()方法之前,业界通用的vector强制缩容技巧,现在部分标准库的shrink_to_fit()内部也是用这个逻辑实现的,只是标准不强制要求shrink_to_fit()必须把容量压缩到和size相等,而swap的写法是100%能达到清空+容量置0效果的。
内容的提问来源于stack exchange,提问作者UnDesSix

