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配合使用,这里不再介绍,有兴趣的自己可以查看相关内容。