如何减少 vector 的内存占用
(给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 -
关于如何减少vector的内存占用,欢迎在评论中和我探讨。觉得文章不错,请点赞和在看支持我继续分享好文。谢谢!
关注『CPP开发者』
看精选C++技术文章 . 加C++开发者专属圈子
↓↓↓
点赞和在看就是最大的支持❤️