c++内存管理
看得多不如自己动手写着试试,先看代码
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_;
Layer(){
opa_ = new OpA;
cout<<"Layer被调用"<<endl;
}
~Layer(){
cout<<"析构函数Layer被调用"<<endl;
}
};
int main(){
{
Layer* layer =new Layer;
}
cout<<"finished"<<endl;
}
在main函数中,我们new了一个Layer,但没有delete这个指针,析构函数不会被调用,内存势必会泄露。运行结果如下:
OpA被调用
Layer被调用
finished
现在加入main函数中加入delete;
int main(){
{
Layer* layer =new Layer;
delete layer;
}
cout<<"finished"<<endl;
}
运行如下:
OpA被调用
Layer被调用
析构函数Layer被调用
finished
OpA并没有被释放,也就是Layer对象里的OpA指针指向的OpA对象没有被释放,我们可以在Layer的析构函数中释放这个对象,代码如下:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_; //这个指针只能是堆指针,因为析构函数中有delete
Layer(){
opa_ = new OpA;
cout<<"Layer被调用"<<endl;
}
~Layer(){
cout<<"析构函数Layer被调用"<<endl;
delete opa_; //注意,这里只能delete堆指针
}
};
int main(){
{
Layer* layer =new Layer;
delete layer;
}
cout<<"finished"<<endl;
}
运行如下:(注意,此时class Layer中的指针只能是堆指针)
OpA被调用
Layer被调用
析构函数Layer被调用
析构函数OpA被调用
finished
OK,目前为止泄露问题得到了解决。
那看看这个例子:
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_; //这个指针只能是堆指针,因为析构函数中有delete
Layer(){
opa_ = new OpA;
cout<<"Layer被调用"<<endl;
}
~Layer(){
cout<<"析构函数Layer被调用"<<endl;
delete opa_; //注意,这里只能delete堆指针
}
};
int main(){
vector<Layer>list;
{
Layer layer0;
Layer layer1;
list.push_back(layer0);
list.push_back(layer1);
}
}
vector中存放layer实例,猜一猜这个代码能不能正常运行,不能的话,错在哪?
运行结果如下:
OpA被调用
Layer被调用
OpA被调用
Layer被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
free(): double free detected in tcache 2
Aborted (core dumped)
double free!!
so why?
错误原因发生在copy构造函数!当vector扩容时,copy构造函数被调用,生成layer的副本,这样就有两个指针同时指向同一块资源!每个副本在析构时都会去释放那块内存。
class Layer是一个资源管理类,啥是资源管理类,就是把资源放入类中。我把代码改一下,再看看:
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_; //这个指针只能是堆指针,因为析构函数中有delete
Layer(OpA* opa):opa_(opa){
cout<<"Layer被调用"<<endl;
}
~Layer(){
cout<<"析构函数Layer被调用"<<endl;
delete opa_; //注意,这里只能delete堆指针
}
};
int main(){
vector<Layer>list;
{
OpA* p1 =new OpA;
Layer layer0(p1);
//shared_ptr<OpA>p_share_(p1);
OpA* p2 =new OpA;
Layer layer1(p2);
list.push_back(layer0);
list.push_back(layer1);
}
}
运行结果是一样的:
OpA被调用
Layer被调用
OpA被调用
Layer被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
free(): double free detected in tcache 2
Aborted (core dumped)
Layer不就是一个管理OpA资源的类嘛,这就是一个智能指针!!!
怎么解决这个double free 问题
小心在资源管理内中的copy行为!!!
四个方法:
1、禁止复制
禁止复制的意思就是,一块内存(资源)始终只允许一个指针指向它。实现很简单,就是把copy 构造函数设为私有的!
显然vector扩容时必须要调用copy构造函数,这种简单粗暴的方式不太好,因为这个类不能再作为vector的元素了。
2.深度复制(复制底部资源)
实现如下:
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include<cstring>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_; //这个指针只能是堆指针,因为析构函数中有delete
Layer(OpA* opa):opa_(opa){
cout<<"Layer被调用"<<endl;
}
Layer(const Layer& layer){
cout<<"copy构造函数被调用"<<endl;
OpA* p =new OpA;
opa_ =p;
memcpy(p,layer.opa_,sizeof(OpA));
}
~Layer(){
cout<<"析构函数Layer被调用"<<endl;
delete opa_; //注意,这里只能delete堆指针
}
};
int main(){
vector<Layer>list;
{
OpA* p1 =new OpA;
Layer layer0(p1);
//shared_ptr<OpA>p_share_(p1);
OpA* p2 =new OpA;
Layer layer1(p2);
list.push_back(layer0);
list.push_back(layer1);
}
}
运行结果如下:
OpA被调用
Layer被调用
OpA被调用
Layer被调用
copy构造函数被调用
OpA被调用
copy构造函数被调用
OpA被调用
copy构造函数被调用
OpA被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
析构函数Layer被调用
析构函数OpA被调用
构造函数被调用5次,析构5次,没有内存泄漏。(顺便想一想为啥是5次)
3、对底层资源祭出“引用计数法”
我们希望保有资源,直到最后一个使用者被销毁。
#include <iostream>
#include <string>
#include <memory>
#include <vector>
#include<cstring>
#include<atomic>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_ =nullptr; //这个指针只能是堆指针,因为析构函数中有delete
std::atomic_uint* cnt_ =nullptr;//引用计数指针
//int* cnt_ =nullptr;//引用计数指针;
Layer(OpA* opa):opa_(opa){
cnt_ =new atomic_uint(1);
cout<<"Layer被调用"<<endl;
}
Layer(const Layer& layer){
cout<<"copy 构造函数被调用"<<endl;
opa_ = layer.opa_;
cnt_ = layer.cnt_;
(*cnt_) += 1;
}
~Layer(){
//cout<<"析构函数Layer被调用"<<endl;
//cout<<"*cnt_="<<*cnt_<<endl;
(*cnt_) -= 1;
if(*cnt_ == 0)
delete opa_; //注意,这里只能delete堆指针
}
};
int main(){
vector<Layer>list;
{
OpA* p1 =new OpA;
Layer layer0(p1);
OpA* p2 =new OpA;
Layer layer1(p2);
list.push_back(layer0);
list.push_back(layer1);
}
}
运行结果如下:
OpA被调用
Layer被调用
OpA被调用
Layer被调用
copy 构造函数被调用
copy 构造函数被调用
copy 构造函数被调用
析构函数OpA被调用
析构函数OpA被调用
资源构造两次,析构两次!perfect!
这就是智能指针share_ptr的思想!所以share_ptr能够用于STL的容器元素。
上面的代码相当于自己实现了一个share_ptr的智能指针,如果你有兴趣的话,可以直接用智能指针改写上面的代码,效果是一样的。
4.转移底部资源的控制权
任何一个时刻,只允许一个指针指向一块资源(内存)
unique_ptr和auto_ptr就是这种思想。
auto_ptr可以进行赋值和拷贝运算,但是他虽然名义上做的是赋值和拷贝,但是背后做的却是move语义做的事情,拷贝和赋值之后,源对象被置位空,这看起来十分反常。而unique_ptr只支持移动语义,使用起来更加清晰。
auto_ptr无法作为容器元素。因为想作为STL的容器元素需要“拷贝和赋值操作之后,有两个独立的相等的对象”,显然auto_ptr的拷贝和赋值不满足这个条件。但为什么unique_ptr就可以呢?网上查到的是因为它支持移动语义,具体的原因还没有搞清楚。
现在考虑令一种应用场景,class不变,这个在main函数中做些变化,看代码:
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
class OpA{
public:
int a_;
int b_;
OpA(int a=0,int b=0):a_(a),b_(b){
cout<<"OpA被调用"<<endl;
}
~OpA(){
cout<<"析构函数OpA被调用"<<endl;
}
};
class Layer{
public:
OpA* opa_;
Layer(){
opa_ = new OpA;
cout<<"Layer被调用"<<endl;
}
~Layer(){
delete opa_;
cout<<"析构函数Layer被调用"<<endl;
}
};
int main(){
vector<Layer*>list;
{
Layer* layer0 = new Layer;
list.push_back(layer0);
Layer* layer1 = new Layer;
list.push_back(layer1);
delete layer0;
delete layer1;
}
for(int i=0;i<list.size();i++)
delete list[i];
cout<<"finished"<<endl;
}
这里有个vector<Layer*>list,我们现在不玩资源管理类的实例了,改玩资源管理类的指针了,上面代码对不对?
不对,正确的姿势是这样:
int main(){
vector<Layer*>list;
{
Layer* layer0 = new Layer;
list.push_back(layer0);
Layer* layer1 = new Layer;
list.push_back(layer1);
}
for(int i=0;i<list.size();i++)
delete list[i];
cout<<"finished"<<endl;
}
最后只需要把list中资源管理类的指针都delete一次就行了,不再需要设计copy构造函数了,为资源管理类中的copy行为而忧心忡忡了,想想为什么?
最后,释放内存到底是一个什么样的动作呢?
很多帖子说释放内存时,那块内存就会被清空,然后指针变为野指针或者空指针。也有的说指针还是那个指针,只是那块内存被清空。
我们自己写个简单的例子试一试:
class A{
public:
int a_;
A(int a=1):a_(a){};
};
int main(){
{//作用域1
A* p_A=new A;
cout<<"p_A="<<p_A<<endl;
cout<<p_A->a_<<endl;
delete p_A;
cout<<"p_A="<<p_A<<endl;
cout<<p_A->a_<<endl;
}
cout<<"finished"<<endl;
}
运行结果:
p_A=0x5624459dbeb0
1
p_A=0x5624459dbeb0
0
finished
可以看出,这里的行为是:指针还是那个指针,但是指向的内存被清零。
但不同的编译器行为不同,有的编译器都懒得去清空那块内存,那delete到底做了什么动作呢?
可以这么理解,new一块内存之后,那块内存被标记,再次new时,不会分配到被标记的内存,所谓的内存泄漏就是,你new了内存,那个内存被标记了,但你没有释放,那块内存始终处于被标记状态,如果长期这样,甚至导致所有可用内存都被标记了,最后new时,分配不到内存了,程序无法继续运行。
还有一个特殊操作,正常的new是不会分配到以被标记的内存,但可以强制要求分配那块内存,虽然它已经被标记,那就是placemen new
int main(){
{//作用域1
char* p=new char[8]{'h','e','l','l','o','!'};
cout<<p<<endl;
printf("0x%x \n",p);
int* p2=new(p) int[2]{1,2};
cout<<*p2<<endl;
printf("0x%x \n",p2);
delete p2;
}
cout<<"finished"<<endl;
}
运行结果:
hello!
0xeba74eb0
1
0xeba74eb0
finished
这就是placement new的作用,单独使用placement new是必须禁止的,必须和operator new配合使用,这里不再介绍,有兴趣的自己可以查看相关内容。