【C++】再谈 STL reserve的坑

(点击上方公众号,可快速关注)

前言

之前写过一篇文章《STL reserve函数使用误区》,主要内容是说明一些标准模板类,比如,std::vectorstd::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_backemplace_backpop_backresize等函数,更多的函数可以参考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);执行成功后,肯定会有>=2capacityassert完全没有必要,除非你怀疑标准库的实现有问题,不过这种情况基本排除。这样的代码看似严谨,实则冗余,类似的情况随处可见,要杜绝,比如:

    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::stringstd::vector这类内存连续分配的容器,虽然unordered_setunordered_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高不了多少,使用容易出现问题,所以,正常情况下尽量少用,除非有特殊需求。

喜欢我的文章,请关注我的公众号。转载请标明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值