C++——动态内存分配new--delete
一、静态内存分配与动态内存分配
静态内存分配:全局或局部变量(对象),编译器在编译时都可以根据变量或对象的类型知道所需内存空间的大小。从而系统在适当的时候为他们分配内存空间
动态内存分配:有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配称为动态内存分配。动态分配都在自由存储区中进行。
- 动态内存分配 :指针变量名=new 类型名(初始化式);delete 指针名;动态分配的一般都是无名对象,自由存储区不会在分配时做初始化(包括清零)所以必须用初始化式来显示初始化。
-
1 int *pi=new int(0); 2 delete pi; 3 //释放的是pi所指向的目标的内存空间,也就是撤销了该目标。但是指针pi本身并没有撤销,指针所占用的内存空间并没有释放。
-
- 数组动态分配:
-
1 指针变量名=new 类型名[下标表达式]; 2 delete[] 指向该数组的指针变量名; 3 //两式中的方括号必须配对使用。如果delete语句中少了方括号,因编译器认为该指针指是指向数组第一个元素的指针,会产生回收不彻底的问题(只回收了数组第一个元素所占用的内存空间)
3.自由存储区对象与构造函数:
-
类对象动态建立与删除过程
//对于CGoods类,动态建立内存与释放内存 CGoods *pc; pc=new CGoods;//分配自由存储空间,并构建一个无名的CGoods对象 ...... delete CGoods;//先析构,C++自动调用商品类的析构函数,再将内存空间返回给自由存储区 - 类对象初始化:
new后面类(class)类型可以有参数这些参数即构造函数的参数。但对创建数组,则无参数,只能调用默认的构造函数。
二、类的封装:
封装的更高境界是在该类对象中的一切都是完备的,自给自足的,不仅有数据和对数据的操作,还包括资源的动态安排与释放。在需要时可以无条件地安全的使用。标准string类模板就是典型的例子。这样的类对象,作为另一个类的成员对象使用时就不会出现任何问题。这表明聚合实现了完善的封装。
有关string类会专门写一篇文章总结。
三、new/delete与malloc/free的区别
1、两者之间的区别:
- new/delete是C++中的操作符,而malloc/delete是标准库函数
- 对于非内建数据类型只使用malloc是无法完成动态对象要求的一般在创建对象时需要调用构造函数,在对象释放时需要自动调用析构函数。而malloc是库函数而不是运算符,不在编译器控制范围内,不能够自动调用构造函数与析构函数。而new与delete在为对象申请分配内存空间时,可以自动调用构造函数完成对象的初始化,delete也可以自动调用析构函数。malloc只是做一件事,只是为变量分配内存空间free只是释放变量的内存。
- new返回的是指定类型的指针,并可以自动计算所申请内存的大小。而malloc需要我们计算申请的内存的大小,并在返回时强制转换为实际类型的指针。
2、new、delete运行机制
- operator new与operatoe delete:这两个是C++标准库函数,原型分别如下:
-
1 void *operator new(size_t);//allocation an obhject 2 void *operator delete(void*;//free an object) 3 4 void *operator new[](size_t);//allocaton an array 5 void *operator delete[](void*);//free an arry
这两个函数与C语言中的malloc/free函数类似,都是用来申请和释内存的,并且operator new申请内存后不对内存进行初始化,直接返回申请内存的指针。可以直接在程序中使用这几个函数。
-
3、通过实例分析new /delete的实现机制:new/delete
-
-
//不使用C++的内置数据类型,自定义一个类A; class A { public: A(int v):var(v) { fopen_s(&file,"test","r"); } ~A() { fclose(file); } private: int var; FILE *file; } //类A有两个私有数据成员,一个构造函数和一个析构函数,构造函数中初始化私有变量var以及打开一个文件,析构函数关闭文件 class *pa=new A(10);
delete pa; - 动态内存分配的过程:
- 首先:调用上文提到的operator new 标准库函数,传入的参数为class A的大小,这里为8个字节,一个int类型,一个指针分别都是四个字节,函数的返回值是所分配内存地址的起始值。
- 上边分配的内存是未初始化的,第二步就是在这一块原始的内存上对类对象进行初始化,调用相应的构造函数这里是A::A(10);这个函数,然后对这块内存进行初始化,然后file指向打开的文件。
- 最后一步是返回分配并初始化好了的对象的指针,
- 动态内存释放的过程:delete pa;
- 首先调用pa所指向的对象的析构函数对打开的文件进行关闭
- 通过上文提到的operator delete标准库函数来释放该对象的内存,传入的函数的参数是pa的值,即内存的首地址。
-
4、动态申请和释放一个数组的机制。new[]/delete[]
-
-
1 string *psa =new string[10];//array of 10empty strings 2 int *pia=new int[10];//arry of 10 uninitialized ints
- 申请一个数组时用到了new[];第一个数组时string类型,分配的保存对象的内存空间后,调用string类型的默认构造函数依次初始化数组中的每个元素。
- 第二个申请的是具有内置类型的数组,分配了存储10个int对象的内存空间,但并没有初始化。
-
-
1 //释放内存空间: 2 delete[] psa; 3 delete[] pia; 4 //注意这里的[]不能漏掉
- 对于string类型数组:首先对10个string类型对象分别调用析构函数然后在释放掉为对象分配的所有内存对象
- 如何知道psa所指向的对象的数组的大小,怎么知道调用几次析构函数
- 这就需要在new[]一个对象时,保存数组的维度,C++的做法是在分配数组空间时多分配4个字节的大小,专门保存数组的大小,在delete[]时就可以取出这个保存的数,就知道需要调用几次析构函数了。
- 如何知道psa所指向的对象的数组的大小,怎么知道调用几次析构函数
- 对于int类型数组:因为是内置数据类型不存在析构函数,所以直接释放掉为10个int 类型所分配的内存空间。
- 对于string类型数组:首先对10个string类型对象分别调用析构函数然后在释放掉为对象分配的所有内存对象
-
class A *p=new A[3];
- 首先调用库函数operator new[]分配足够的内存大小,需要注意的是需要多出四个字节用来存放数组的大小,
- 然后在刚分配的内存上调用构造函数初始化对象
- 最后返回数组对象的指针,而不是分配内存空间的首地址,因为首地址上存放的是4个字节的数组的大小,返回的地址即在内存首地址+4的地址。
- 在delete[]时传入operator delete[]的参数便不是数组对象的指针,而是p的值减4
5、为什么new[]/delete[]要配对使用
-
class A *pAa = new class A[3]; delete pAa;
-
- delete pAa;做了两件事:
- 调用一次pAa所指向的对象的析构函数;
- 调用operator delete(pAa);释放内存;
- 第一点:这里只对数组的第一个元素调用了析构函数,后边两个对象没有调用析构函数,在销毁数组对象时少调用了析构函数,会造成内存泄漏。
- 第二点:直接释放pAa所指向的内存空间,会造成严重的段错误,因未分配的内存地址的首地址是pAa所指向的地址-4,应该传入的参数是&pAa-4。
- delete pAa;做了两件事:
-
四、new/delete运算符的重载:
1、new/delete在C++中也是运算符,可以重载:
new:
- 先开辟内存空间
- 再调用类的构造函数
开辟内存空间的部分可以被重载
delete:
- 先调用类的析构函数
- 再释放内存空间
释放内存空间的部分可以被重载
2、为什么要重载new/delete运算符
有时候在实现内存池的时候需要重载他们。频繁的使用new/delete,会造成内存碎片,内存不足等问题,影响程序的正常执行所以一次开辟一个适当大的空间,每当需要对象的时候,不再需要去开辟内存空间,只需要调用构造函数即可。
new/delete可以有多种重载方式,但是new函数的第一个参数一定是size_t类型。分别是new单个对象,new对象的数组。详见下边示例代码:
-
class String{ public: String(const char* str = ""){ cout << "Create" << endl; if(NULL == str){ data = new char[1]; data[0] = '\0'; } else{ data = new char[strlen(str) + 1]; strcpy(data, str); } } ~String(){ cout << "Free" << endl; delete []data; data = NULL; } private: char* data = NULL; }; //重载方式1 void* operator new(size_t sz){ cout << "in operator new" << endl; void* o = malloc(sz); return o; } void operator delete(void *o){ cout << "in operator delete" << endl; free(o); } //重载方式2 void* operator new[](size_t sz){ cout << "in operator new[]" << endl; void* o = malloc(sz); return o; } void operator delete[](void *o){ cout << "in operator delete[]" << endl; free(o); } //重载方式3 //第一个参数size_t即使不适用,也必须有 void* operator new(size_t sz, String* s, int pos){ return s + pos; } int main(){ String *s = new String("abc"); delete s; String *sr = new String[3]; delete []sr; //开辟内存池,但是还没有调用过池里对象的构造方法 String *ar = (String*)operator new(sizeof(String) * 2); //调用池里第一个对象的构造方法,不再开辟空间 new(ar, 0)String("first0"); //调用池里第二个对象的构造方法 ,不再开辟空间 new(ar, 1)String("first1"); //调用池里第一个对象的析构方法,注意不会释放到内存 (&ar[0])->~String(); //调用池里第二个对象的析构方法,注意不会释放到内存 (&ar[1])->~String(); //下面语句执行前,内存池里的对象可以反复利用 operator delete(ar); }