查看原文
其他

如何减少 vector 的内存占用

CPP开发者 2021-07-20

(给CPP开发者加星标,提升C/C++技能)

来源:邱国禄
https://blog.csdn.net/qiuguolu1108/article/details/107146466

【导读】:接上一篇《C++ vector内存分配策略浅析》,其中定义了用于测试的类A,本文中也有用到,大家可以点击链接跳转至上一篇,了解下类A的定义。本片文章从使用角度讲解如何减少vector的内存占用,内容较多,建议先码后看。


以下是正文



vector之所以会发生大量的拷贝,是因为其内存分配策略造成的。每当vector的内存不够用时,vector都会重新申请两倍的空间,并将之前的元素搬移到新空间。这才是发生拷贝的根源,既然这样,我们能不能预先给vector申请一定的空间,避免因空间不够而发生元素搬移。


1、使用reserve()函数预申请空间


reserve()函数可以给vector预先分配指定大小的空间。

vector<A> va;
va.reserve(1024);
cout<<"va size = "<<va.size()<<endl;cout<<"va capacity = "<<va.capacity()<<endl<<endl;
A a;
for(int i=0;i<1024;i++){ va.push_back(a);}
A::dis_copy_construct_count();cout<<endl;
cout<<"va size = "<<va.size()<<endl;cout<<"va capacity = "<<va.capacity()<<endl;

使用reserve()给va预先分配了1024个空间,所以再往va推入1024个元素的时候,并没有发生多余的拷贝构造。

通过reserve()给vector预分配空间,确实可以减少元素的拷贝构造,但我们在使用vector时,有时很难确定容器元素的个数。


在使用reserve()时需要自己去平衡,如果reserve()过大,会造成空间的浪费,如果过小还是会发生拷贝构造。


现在又带来一个问题,如何将vector过多的没有存放元素的空间还给系统。


2、erase()函数和clear()会减少vector的内存占用吗?


上篇文章中,我们说过vector占用空间的大小,可以通过capacity()函数来查看。

通过一个示例,来验证一下erase()、clear()会减少vector内存占用吗?

vector<int> vi;
for(int i=0;i<65;i++){ vi.push_back(i);}
cout<<"vi size = "<<vi.size()<<endl;cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;

auto itr = find(vi.begin(),vi.end(),30);
/*删除0~29元素*/vi.erase(vi.begin(),itr);cout<<"vi size = "<<vi.size()<<endl;cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;

vi.clear();cout<<"vi size = "<<vi.size()<<endl;cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;


通过测试我们发现,erase()和clear()只能将vector空间的元素给析构掉,并不能减少vector内存的占用。

这是侯捷老师《STL源码剖析》对vector的erase()、clear()函数实现介绍。这也进一步证实了erase()、clear()并不能释放vector的空间。


3、data()函数可以返回vector元素存放的位置


#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi;
for(int i=0;i<10;i++) { vi.push_back(i); }
int * p = vi.data();
for(int i=0;i<10;i++) { cout<<*p++<<endl; }
return 0;}


data()这个函数不用多说了,通过示例就可以看出这个函数好强大,直接杀入了vector的老巢。


4、swap()函数用于交换两个vector


swap()函数可以用于交换两个vector,但是交换了vector的哪些东西?

vector<int> vi0;for(int i=0;i<5;i++){ vi0.push_back(i);}cout<<"&vi0 = "<<&vi0<<endl;cout<<"vi0.data() = "<<vi0.data()<<endl<<endl;

vector<int> vi1;for(int i=0;i<5;i++){ vi1.push_back(i*100);}cout<<"&vi1 = "<<&vi1<<endl;cout<<"vi1.data() = "<<vi1.data()<<endl;
cout<<endl<<"====================="<<endl<<endl;
vi0.swap(vi1);
cout<<"&vi0 = "<<&vi0<<endl;cout<<"vi0.data() = "<<vi0.data()<<endl<<endl;
cout<<"&vi1 = "<<&vi1<<endl;cout<<"vi1.data() = "<<vi1.data()<<endl;


从示例中可以看出,vector的data()发生了交换,但vi0和vi1所在的地址并没有发生变化。swap()函数还交换了其他内部成员数据,但我们弄清了我们关心的一点,swap并没有发生空间的大量拷贝,交换的仅仅是两个空间地址。

补充一点:swap()函数,不仅仅交换两个容器的内容,同时它们的迭代器、指针和引用也被交换。在swap发生后,原先指向容器中元素的迭代器、指针和引用依然有效,并指向同样的元素----但是,这些元素已经在另一个容器中了。


5、vector的拷贝构造


使用一个vector去构造器另外一个vector,在构造新的vector时,仅会根据vector实际元素个数去构造新的vector。

vector<int> vi0;vi0.reserve(100);
//插入5个元素for(int i=0;i<5;i++){ vi0.push_back(i);}
cout<<"vi0 size = "<<vi0.size()<<endl;cout<<"vi0 capacity = "<<vi0.capacity()<<endl<<endl;

vector<int> vi1(vi0);cout<<"vi1 size = "<<vi1.size()<<endl;cout<<"vi1 capacity = "<<vi1.capacity()<<endl;


虽然vi0的内存空间可以存放100个int,但实际有效元素只有5个int。通过vi0拷贝构造vi1的时候,并不会像vi0那样占用100个int空间,而是根据实际元素的个数申请空间,并不会有多余的空间。


6、使用swap技巧移除多余的容量


铺垫了这么多,回到我们的主题上,如何减少vector的容量?


vector的构造器在构建新的容器时,会自动的去掉多余的空间,我们可以利用这个特性,结合swap函数去掉vector中多余的容量。


6.1 方法一:通过定义一个新的vector


#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); } cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
vector<int> tmp(vi); cout<<"tmp size = "<<tmp.size()<<endl; cout<<"tmp capacity = "<<tmp.capacity()<<endl<<endl;
cout<<"=================="<<endl<<endl;
tmp.swap(vi);
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
cout<<"tmp size = "<<tmp.size()<<endl; cout<<"tmp capacity = "<<tmp.capacity()<<endl<<endl;
return 0;}


vi有多余的容量,通过vi构造一个新的容器tmp,tmp没有多余的容量,通过swap函数将tmp和vi交换,则tmp变成了有多余容量的容器。tmp是一个局部变量,在离开其作用域时,会调用vector的析构器,将其自己释放。

#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); }
{ vector<int> tmp(vi); tmp.swap(vi); }//tmp在此处就会调用vector的析构器,将其自己销毁。 return 0;}

这样可以更加快速的消除多余容量。之前示例,tmp需要在main函数的最后才被销毁,此处的示例,tmp在swap之后就立即被销毁。


6.2 方法二:使用临时变量


上面的方法确实可以消除vector多余的容量,但不够优雅,略显啰嗦。使用临时变量可以更加简洁。

#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); } cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
{ vector<int>(vi).swap(vi); }//临时对象在此处离开其作用域,会被销毁。
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl;
return 0;}


没有定义多余的变量,一行代码搞定,真的很优雅了。


简单的说一下:vector<int>(vi)定义一个临时对象,这个临时对象通过拷贝构造器构造。临时对象调用swap成员函数将其自己和vi交换。临时对象在离开其作用域时被销毁。


6.3 清空vi


clear()仅会清空容器中的元素,并不能真正的释放vector占用的内存。使用swap()可以释放vector内存。

#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); } cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
{ vector<int>().swap(vi); }
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl;
return 0;}


vector<int>()会产生一个临时对象,这个对象没有名字,其size和capacity皆为零。


6.4 总结:


去除vi多余的容量:vector<int>(vi).swap(vi)


将vi的空间清空:vector<int>().swap(vi)


7、使用C++11中shrink_to_fit()函数去除多余容量


看看官方介绍


#include <iostream>#include <vector>
int main(){ std::vector<int> v; std::cout << "Default-constructed capacity is " << v.capacity() << '\n'; v.resize(100); std::cout << "Capacity of a 100-element vector is " << v.capacity() << '\n'; v.clear(); std::cout << "Capacity after clear() is " << v.capacity() << '\n'; v.shrink_to_fit(); std::cout << "Capacity after shrink_to_fit() is " << v.capacity() << '\n';}


官方给的示例,很容易理解。

#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100);
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
for(int i=0;i<10;i++) { vi.push_back(i); }
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
vi.shrink_to_fit(); cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
return 0;}

vi的容量是100,向其推入10个元素,则有90个多余的空间,调用shrink_to_fit()后,其容量变为10,释放了多余的空间。


8、减少vector容量,必要的拷贝依然存在。


不管是swap()函数还是shrink_to_fit()函数,在去除vector多余空间的时候,还是会发生必要的元素拷贝。


8.1 swap方法


vector<A> va;va.reserve(10);
A a;
for(int i=0;i<3;i++){ va.push_back(a);}
cout<<endl<<"======================"<<endl<<endl;
vector<A>(va).swap(va);
cout<<endl<<"======================"<<endl<<endl;



这里的拷贝构造主要是在构造临时对象产生的。


8.2 shrink_to_fit方法


vector<A> va;va.reserve(10);
A a;
for(int i=0;i<3;i++){ va.push_back(a);}
cout<<endl<<"======================"<<endl<<endl;
va.shrink_to_fit();
cout<<endl<<"======================"<<endl<<endl;

结果都是一样的,发生了拷贝构造。


9、resize()函数


顺便说一下resize()函数,这个函数使用也有一定的迷惑性。现在通过几个例子说明一下其背后都做了哪些事情。


现在vector有两个大小,一个是size(),vector实际元素的个数;另一个是capacity(),vector的容量。


size是小于等于capacity的。


9.1 resize()参数小于size()


将多余的元素析构掉

#include <iostream>#include <vector>
using namespace std;
class A{public: A(int data = 100) :data_(data) { cout<<"constructor : "<<this<<endl; }
A(const A& a) { data_ = a.data_; cout<<this<<" : copy constructor form : "<<&a<<endl; }
void display() { cout<<data_<<" "; }
~A() { cout<<"deconstructor : "<<this<<endl; }
private: int data_;};
int main(){ vector<A> va; va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl; va.push_back(a); va.push_back(b); va.push_back(c); va.push_back(d);
cout<<endl<<"========================"<<endl; for(auto & i : va) { i.display(); } cout<<endl;
va.resize(2);
for(auto & i : va) { i.display(); } cout<<endl;
return 0;}


9.2 resize()参数大于size(),小于capacity()。


9.2.1 情况一:使用默认构造构造新元素


vector<A> va;va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl;va.push_back(a);va.push_back(b);va.push_back(c);va.push_back(d);
cout<<endl<<"========================"<<endl;for(auto & i : va){ i.display();}cout<<endl;
va.resize(6);
for(auto & i : va){ i.display();}cout<<endl;


需要添加两个新的元素,没有指定添加的元素,则调用类A的默认构造生成新元素。

A(int data = 100) :data_(data){ cout<<"constructor : "<<this<<endl;}

这是类A的默认构造器,使用这个构造器生成的对象,其默认值为100。


9.2.2 情况二:使用指定的元素构造新元素


vector<A> va;va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl;va.push_back(a);va.push_back(b);va.push_back(c);va.push_back(d);
cout<<endl<<"========================"<<endl;for(auto & i : va){ i.display();}cout<<endl;
A aa(99);
va.resize(6,aa);
for(auto & i : va){ i.display();}cout<<endl;


和上面类似,但新元素的生成,调用了拷贝构造器。但这里多发生了一次拷贝构造,根据调用情况,猜测resize()先把元素拷贝到其内部,所有新元素的拷贝都是基于resize()内部的副本。


9.3 resize()参数大于capacity()


重新申请空间,将原数据拷贝过来,在新的位置调用默认构造器生成新的元素。

vector<A> va;va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl;va.push_back(a);va.push_back(b);va.push_back(c);va.push_back(d);
cout<<endl<<"========================"<<endl;for(auto & i : va){ i.display();}cout<<endl;
va.resize(10);
for(auto & i : va){ i.display();}cout<<endl;
cout<<va.capacity()<<endl;


10、总结


本文介绍了如何去除vector多余的容量。shrink_to_fit()是C++11提供的新方法,在C++98中可以使用swap()函数实现去除多余的容量。


参考:


《STL源码剖析》


《Effective STL》


- EOF -


推荐阅读  点击标题可跳转

1、C++语言中std::array的神奇用法总结

2、C/C++ assert()函数用法总结与注意事项

3、C++ 的 6 种内存顺序,你都知道吗?


关于如何减少vector的内存占用,欢迎在评论中和我探讨。觉得文章不错,请点赞和在看支持我继续分享好文。谢谢!


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

↓↓↓


点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存