在学习C语言时,我们使用指针通过malloc/calloc/realloc函数去开辟堆区空间,这种方式称为动态开辟空间的方式:
- #include
- //malloc——开辟一个只有四字节大小的整型空间,里面存放一个整型数据
- int* pa = (char*)malloc(sizeof(char) * 4);
- //开辟10个char型数据的字符型空间,且都初始化为'0'
- int* pc = (int*)calloc(sizeof(int) * 10,0);
因为栈区空间很小(系统留给栈区的空间大小不多,大多都给了堆区),所以我们需要在堆区上去借用空间,既然是动态开辟,那么就需要手动去释放这块空间,否则会造成内存泄漏。而说到手动释放空间,则需要使用free函数,将这块空间的使用权限还给操作系统,于是这就不归我们管了,虽然我们可以强行访问这块空间,但属于非法行为!
- free(pa);
- free(pc);
- *pa = 10; //非法访问,但不报错
- //方法:让指针置空,永远找不到那块空间即可
- pa=NULL;
- pc=NULL;
而在C++中,诞生出了new和delete关键字,分别对应堆区空间的开辟和释放。
类型*+变量名=new +类型;
代码解析:第一句是开辟一个4字节大小的空间,里面可以存放一个整型数据;第二句是开辟一个8字节大小的空间,里面可以存放一个浮点型数据。
注:该种方式new出来的空间数据没有被初始化,所以获取的是随机值。
类型*+变量名=new +类型(为空间数据赋值);
在类型后一组括号内就可以为该空间数据赋值了。
类型*+变量名=new +类型[填写开辟个数的数字];
代码解析:动态开辟一块包含10个整型数据的堆空间;动态开辟一块包含15个浮点型数据的堆空间。这些开辟的数据仍都是随机值。
类型*+变量名=new +类型[填写数字]+{为空间内的多个数据赋值};
花括号可以为开辟的数组数据赋值。
对于内置类型来说,malloc与new的动态开辟除了语法之外没有其他区别。
2.堆区空间的开辟只能是动态进行的,没有静态开辟这一说
对于自定义类型来说,new和delete关键字不仅会开辟空间,还会调用类型的构造函数和析构函数。
- class A
- {
- public:
- A(int a = 0)
- : _a(a)
- {
- cout << "A():" << this << endl;
- }
-
- ~A()
- {
- cout << "~A():" << this << endl;
- }
- private:
- int _a;
- };
-
-
- int main()
- {
- cout << "malloc会调用构造与析构吗 ? " << endl;
- A* p1 = (A*)malloc(sizeof(A));
- cout << "new会调用构造与析构吗 ? " << endl;
- A* p2 = new A(1);
- free(p1);
- delete p2;
- }
经过验证发现:new和delete关键字会自动调用类的构造函数和析构函数。
- int main(){
- A* p3 = new A[10];
- delete p3;
- return 0;
- }
代码分析: 动态开辟了10个类型的空间,原本应该写delete[] p3;结果现在写成了delete p3;
运行后会出现错误,delete p3;只释放了一个空间。
当执行new开辟时,编译器申请了10个空间,同时也会调用10个构造函数,delete p3后,因为析构函数是自己写的,编译器接收到指令只调用一次析构,而有10个构造就得调用10次析构,所以编译器不知道该怎么办了,会报错!造成内存泄漏
若是不自己写析构函数,系统会自动生成默认的析构函数,编译器会智能判断且调用析构函数,它会将剩余开辟的空间也释放掉,结果也就不会报错了。
编译器会在释放的空间前再开辟几个字节空间,用来统计后面的对象个数,这时,编译器会根据个数去释放空间。 而delete p3会让系统认为只开辟了一个对象空间,根本不需要那个个数空间。对于delete种若是缺少了[],会出现释放不彻底问题,编译器认为指针指向的是某个数组元素,只会释放第一个元素空间,加了[ ],就能转化为指向数组的指针。
- int main(){
- A* p4 = new A;
- delete[] p4;
- return 0;
- }
delete[ ]会在p4开辟的位置前多开辟几个字节去存放开辟的类型数据个数,而p4只开辟了一个类型数据空间,根本就不需要那几个空间,所以会报错!delete关键字只释放了p4的空间,而对于前面多开辟的几个字节没有被释放,造成内存泄漏,报错!
- int main() {
- int* ptr = (int*)malloc(sizeof(int) * 1024*1024*1024*1024);
- if (ptr == nullptr) {
- perror("malloc fail");
- exit(0);
- }
- else {
- cout << ptr << endl;
- }
- return 0;
- }
malloc失败后会返回空指针!
- int main(){
- int* p = new int[100000*1024*5]{0};
- if (p) {
- cout<
- }
- else {
- cout << "申请失败" << endl;
- }
- return 0;
- }
new失败不会有返回值,会抛出异常!
四.内存泄漏
1.定义:
内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
2.如何避免内存泄漏?
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。例如:
五:malloc/free与new/delete的整体区别:
共同点是: 都是从堆上申请空间,并且需要用户手动释放。
不同的点是: 1. malloc和free是函数,new和delete是操作符。
2. malloc申请的空间不能在定义时直接初始化,new可以。
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[ ]中指定对象个数即可。
4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型。
5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常。
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理。
最后做一道练习题:
使用 char* p = new char[100]申请一段内存,然后使用语句:delete p; 将该指针释放,执行代码时会发生什么?
A.会有内存泄露
B.不会有内存泄露,但不建议用
C.编译就会报错,必须使用delete[ ]p
D.编译没问题,运行会直接崩溃
答案为B,因为题中给出的指针p的类型为char*,它是内置类型,内置类型的指向堆区空间的指针释放——delete p 等价于free(p),所以即使指针p指向的堆区空间开辟的空间个数有若干也无所谓,使用delete p;语句也是正常的;
但若是自定义类型,例如:A* p=new A[100]; 使用delete p; 语句就会发生内存泄漏,导致运行报错,系统崩溃!!!