栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

详析new/delete内存分配与释放的内部工作流程

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

详析new/delete内存分配与释放的内部工作流程

1. 示例类

提供一个精简版的复数类和一个精简版的字符串类用来演示。
示例类中函数均取自C++标准库。

复数类:

#include 
#include 
#include 
using namespace std;
//复数类
class Complex
{
public:
    
    Complex(double re = 0, double im = 0) :m_re(re), m_im(im) {};
    Complex& operator+=(const Complex& r) 
    {
        m_re += r.m_re;
        m_im += r.m_im;
        return *this;
    }
    
    double real() const { return m_re; }
    double imag() const { return m_im; }
//对于所有的数据,最好采用private保护起来,如果外部需要访问数据提供外部接口即可。
private:
    double m_re;
    double m_im;
};


Complex operator+(const Complex& x, const Complex& y)//复数+复数
{
    return Complex(x.real() + y.real(), x.imag() + y.imag());
}
Complex operator+(const Complex& x, const double y)//复数+实数
{
    return Complex(x.real() + y, x.imag());
}
Complex operator+(const double x, const Complex& y)//实数+复数
{
    return Complex(x + y.real(), y.imag());
}
ostream& operator<<(ostream& os, const Complex& l)
{
    os << '(' << l.real() << "," << l.imag() << ")";
    return os;
}

字符串类:

#include 
#include 
using namespace std;

class String
{
public:                                 
   String(const char* cstr=0);                     
   String(const String& str);                    
   String& operator=(const String& str);         
   ~String();                                    
   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};

//inline只是对编译器的一个建议,希望编译器可以将这个函数设置为内联函数,实际上会不会设置取决于编译器。
inline String::String(const char* cstr)
{
   if (cstr) {
      m_data = new char[strlen(cstr)+1];
      strcpy(m_data, cstr);
   }
   else {   
      m_data = new char[1];
      *m_data = '';
   }
}
inline String::~String()
{
   delete[] m_data;
}
inline String& String::operator=(const String& str)
{
   if (this == &str)
      return *this;

   delete[] m_data;
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
   return *this;
}
inline String::String(const String& str)
{
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
}
ostream& operator<<(ostream& os, const String& str)
{
   os << str.get_c_str();
   return os;
}
2. new/delete

定义一个对象时,一般会将内存分配在栈区、堆区,用来控制对象存在的生命周期。
还可以使用static关键字或者将对象定义为全局变量来控制生命周期。

  • static关键字:变量生命周期在作用域结束之后仍然存在,直到整个程序结束。
  • 全局对象:变量声明周期在整个程序结束之后才结束,作用域是整个程序。其生命周期比main()更早。
2.1 new内存泄漏

下面这块代码就会造成内存泄漏。指针变量p离开作用域后,指针p的生命结束,作用域外再也看不到p,也没有机会delete p,此时其所指向的堆区空间无法使用delete进行释放,这块内存将会泄漏。

{
	Complex* p = new Complex;
}

正确实例:

{
	Complex* p = new Complex;
	...
	delete p;
}
2.2 new

使用new关键字时,会先分配内存,然后调用类的构造函数。

Complex* p = new Complex(1,2);

编译器会将上行代码转化为:

void *mem = operator new( sizeof(Complex) ); //分配内存
Complex* pc = static_cast(mem);//类型转换
pc->Complex::Complex(1,2);//调用Complex类的构造函数

operator new()是C++内部一个名字特殊的函数,其内部调用的就是malloc(n)函数,来分配内存的。

Complex::Complex(1,2)函数的参数列表中实际上隐藏着this指针,谁调用Complex::Complex()函数谁就是this指针,所以实际调用方式为Complex::Complex(pc,1,2);,此时pc就是this指针。

2.3 delete

使用delete关键字时,会先释放内存,然后调用类的析构函数。

String* ps = new String("123");
...
delete ps;

编译器会将上面代码转化为:

String::~String(ps);//析构函数
operator delete(ps);//释放内存

operator delete()是C++内部一个名字特殊的函数,其内部调用的就是free(ps)函数,来释放内存的。

对于String类来说,其大小本身只是一个指针m_data大小,指向一个字符串。

2.4 内存分配过程分析

使用Complex* p = new Complex(1,2);创建一个复数,会分配一个内存空间,大小为8个字节(两个double,按原来double为4个字节时分析)。

在调试模式下,并不是真正得到8个字节,得到的字节数如下图左侧所示,每一格为4个字节,灰色的上下两部分为Debug的头和尾,砖红色的为cookie,用来标记这块内存的分配范围一级是否已经分配出去。在VC下,每次编译器所分配的内存区块都会是16的倍数,所以new一个复数实际上分配的内存大小为:8+(32+4)+(4*2)+12=64个字节。

  • 8为Complex大小;
  • (32+4)为Debug头尾大小;
  • (4*2)为cookie大小
  • 12(图中pad部分)为补充16倍数添加的大小。

所以一个复数在Debug模式下实际上会分配64个字节大小的内存区块。

在release模式下,分配的内存区块如下图右侧所示。当然,对于分配的内存区块来说,一定是16倍数的,只是release模式下内存大小刚好已经是16的倍数了,所以不再需要pad部分。

为什么分配的内存区块一定要是16的倍数呢?
操作系统利于16进制的最后一个16进制位来标记这块内存是给出去了还是还回来了。16的倍数转换为16进制后,最后一个16进制位就一定是0,才可以用来作为标记。
cookie值为0x00000041,如果没有给出去值为0x00000040,给出后值为0x00000041。


同理,String* ps = new String("123");也是如此。


上面只是讨论了分配一个复数类或一个字符串类的内存模型,那么当分配多个的情况呢,如下所示。例如分配一个复数数组的内存空间。

我们都知道分配内存时,使用Complex* p = new Complex[3]来分配3个复数对象的内存空间,释放内存时需要使用delete[] p来释放这块内存,为什么需要加上[]呢。

除去Debug头尾、补充16倍数的pad块、cookie块,已经此时内存模型比上面多了4个字节,是来记录数组个数的。3个复数,这四个字节里就存放的是3。(VC编译器是这样做的,其他编译器不保证)。

所以在Debug模式下,实际分配的内存大小为(83)+(32+4)+(42)+4+8=80个字节。

  • (8*3)为数据大小,1一个复数有两个double为8个字节,一个3个复数;
  • (32+4)为Debug头尾大小;
  • (4*2)为上下cookie大小;
  • 4为存放数组个数的大小;
  • 8为pad块大小。

在release模式下,实际分配内存大小除去Debug头尾后为(83)+(42)+4=36个字节。

2.5 array new内存泄漏

通过上面的例子,已经知道了动态分配数组的内存模型。上面也谈到需要使用delete[] p来释放数组,那么假如忘记加[]会发生什么情况呢。此处使用String类来做演示,如图所示。

实际上没有添加[]造成了内存泄漏,但泄漏的并不是String类的内存空间,因为已经有cookie来标记了操作系统给出去的整个内存区块范围,所以仍然能够正常回收。但是因为少加了[],会少调用两次析构函数,而析构函数中会去释放指针指向的字符串空间,所以此时实际上是指向的字符串空间没有被释放造成的内存泄漏,即图中?!标记的地方。

总结:

Complex类与String类的区别是一个类内部没有指针;另外一个类内部有指针,并且指针指向其他的内存空间。

所以,当使用Complex* p = new Complex[3]分配一个复数数组时,因为Complex类内并没有指针指向另外的内存,所以其实不使用delete[] p也不会造成内存泄漏,但是为了养成一个良好的编程习惯,array new还是一定要搭配array delete来使用。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/920388.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号