(点击上方公众号,可快速关注)
前言
之前写过一篇文章《STL reserve函数使用误区》,主要内容是说明一些标准模板类,比如,std::vector
、std::string
等,提供的reserve
成员函数改变的是capacity,而不是容器的size,如果误用会出现一些匪夷所思的问题。那篇文章只介绍了其中一种较为典型的情况,本篇文章会继续深挖一些reserve
相关的误区~~因为他人的代码又给了我不少素材~~。
`reserve`跟`size`不挂钩
延续之前的结论:reserve
只能改变capacity,而不改变size。所有,跟size相关的一些操作的语义应该保持不受reserve
影响,如:
std::vector<int> numbers;
numbers.reserve(3);
numbers[0]=10;
numbers[1]=20;
std::cout << "The size of numbers: " << numbers.size() << std::endl; // 0
for (int i = 0; i < numbers.size(); ++i)
{
std::cout << numbers[i] << std::endl;
}
for (auto it = numbers.cbegin(); it != numbers.end(); ++it)
{
std::cout << *it << std::endl;
}
for (auto const& e : numbers)
{
std::cout << e << std::endl;
}
由于size为0,所以,三个循环都不会输出内容,是合乎常识的。要改变大小,就要使用push_back
、emplace_back
、pop_back
、resize
等函数,更多的函数可以参考API文档。
`reserve`容量只增不减
reserve
操作保证容量是递增的,也就是说,只有申请的容量大于当前容量时,才会分配新的内存,否则,这个函数不会改变容量大小。
在实际操作中,这会带来一些问题,比如,如果容器经过一系列操作,capacity已经很大,但size却很少,且该对象一时半会释放不了,可能需要手工释放来节省内存使用量。有人这样操作:
int small = ...
v.reserve(small); // 错误的做法
经过前面的讨论,这样是没效果的。注意:若使用resize
方法也没效果,它只是改变容器元素的个数,不会改变容量。
正确的做法:
// C++11前
vector<int>(v).swap(v);
看起来比较绕:vector<int>(v)
通过拷贝构造函数初始化一个临时对象,该临时对象是对象v
的一个拷贝,因为是新对象,所以该临时对象的capacity要比v
的要小很多,基本上跟元素个数一样;调用swap
函数会把两个对象互换,该临时对象运行完当前这行代码就会被系统自动收回,这样就完成了v
的内存收缩。这是C++11前的常用的做法。
C++11专门提供了shrink_to_fit
将不用的空间回收,与上相比,使用非常简便,现代C++推荐的方式。
// C++11
v.shrink_to_fit();
`reserve`最小保证
经常会见到一些判断很严格的代码,如下例:
std::string s;
s.reserve(2);
assert(s.capacity() == 2);
这段代码有两个问题:
reserve(new_cap)
保证新的capacity
大于等于new_cap
,而不是申请多少恰好就是多少。因为每个C++实现内存分配策略是不一样的,这样是为了给实现较大的自由度,保证运行效率。本想分配2个字节的空间,g++和clang++默认都会给15个字节的空间。所以
assert
会失败,安全的做法应为assert(v.capacity() >= 2);
无需
assert
标准已经保证
v.reserve(2);
执行成功后,肯定会有>=2
的capacity,assert
完全没有必要,除非你怀疑标准库的实现有问题,不过这种情况基本排除。这样的代码看似严谨,实则冗余,类似的情况随处可见,要杜绝,比如:std::vector<int> v; v.clear(); // 保证v是空的 -- 没必要,默认的构造函数执行后,v就是空的
`reserve`引用失效
当reserve
申请的new_cap大于当前capacity时,会重新申请内存空间,并将原有空间的元素复制到新空间,最后会释放老的内存空间,导致先前的引用或指针失效:
std::vector<int> v { 1, 2 }; //在我的机器上,capacity() == 2
int& a = v[0];
int& b = v[1];
v.reserve(100);
std::cout << a << "," << b << std::endl; // a和b的引用已失效
`reserve`特别说明
文章谈到的reserve
方法主要来自std::string
、std::vector
这类内存连续分配的容器,虽然unordered_set
和unordered_map
也提供了reserve
函数,但不在讨论之列。
细心的读者可能发现,std::string
的行为在C++20标准前后有差异,在C++20前,如果参数new_cap
小于当前capacity【C++11前】或size【>=C++11】时,该函数会执行收缩操作。我们上文讨论的是C++20里reserve
的行为,vector和string的行为保持了统一。
总结
通过以上讨论能看出来,容器的reserve
成员函数提供了相对较底层的内存操纵功能,比C语言的malloc
/realloc
/free
高不了多少,使用容易出现问题,所以,正常情况下尽量少用,除非有特殊需求。
喜欢我的文章,请关注我的公众号。转载请标明出处。