提供一个精简版的复数类和一个精简版的字符串类用来演示。
示例类中函数均取自C++标准库。
复数类:
#include#include #include
字符串类:
#include2. new/delete#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; }
定义一个对象时,一般会将内存分配在栈区、堆区,用来控制对象存在的生命周期。
还可以使用static关键字或者将对象定义为全局变量来控制生命周期。
- static关键字:变量生命周期在作用域结束之后仍然存在,直到整个程序结束。
- 全局对象:变量声明周期在整个程序结束之后才结束,作用域是整个程序。其生命周期比main()更早。
下面这块代码就会造成内存泄漏。指针变量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个字节。
通过上面的例子,已经知道了动态分配数组的内存模型。上面也谈到需要使用delete[] p来释放数组,那么假如忘记加[]会发生什么情况呢。此处使用String类来做演示,如图所示。
实际上没有添加[]造成了内存泄漏,但泄漏的并不是String类的内存空间,因为已经有cookie来标记了操作系统给出去的整个内存区块范围,所以仍然能够正常回收。但是因为少加了[],会少调用两次析构函数,而析构函数中会去释放指针指向的字符串空间,所以此时实际上是指向的字符串空间没有被释放造成的内存泄漏,即图中?!标记的地方。
总结:
Complex类与String类的区别是一个类内部没有指针;另外一个类内部有指针,并且指针指向其他的内存空间。
所以,当使用Complex* p = new Complex[3]分配一个复数数组时,因为Complex类内并没有指针指向另外的内存,所以其实不使用delete[] p也不会造成内存泄漏,但是为了养成一个良好的编程习惯,array new还是一定要搭配array delete来使用。



