C/C++内存分布
C/C++内存有六个区域 ,分别是栈、堆、数据段,代码段,内核空间和内存映射段。
1. 内存栈区: 存放局部变量名,函数返 回值,参数列表,函数栈帧等(8M左右)
2. 内存堆区: 存放new或者malloc出来的对象;
3. 常数区: 存放局部变量或者全局变量的值;
4. 静态区: 用于存放全局变量或者静态变量;
5. 代码区:二进制代码。
[说明]
栈:栈又叫堆栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等,是向下增长的。所谓向下生长的就是,先调用的栈帧的地址比后调用的地址大,栈一般大小有几个M左右。
堆:就是那些由new/malloc分配的内存块,他们的9+89//释放编译器不去管,由我们的应用程序去控制,一般一个new/malloc就要对应一个delete/free,由程序员主动释放。堆是可以上增长的.意思是先建立的堆的地址小于后建立的堆的地址。
数据段又称为静态区:存储全局数据和静态数据。
代码段又称为常量区:可执行的代码/只读常量
注意:在堆区开辟空间,后开辟的空间地址不一定比先开辟的空间地址高。因为在堆区,后开辟的空间也有可能位于前面某一被释放的空间位置。
C语言中动态内存管理
想要具体了解的可以参考我这篇博客:动态内存管理_skeet follower的博客-CSDN博客
我这里简单概述一下:
注:思考一下realloc/malloc/calloc的区别是啥?
malloc
malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。传参时只需传入需要开辟的字节个数。
calloc
calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。calloc函数传参时需要传入开辟的内存用于存放的元素个数和每个元素的大小。calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0。
realloc
realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小。realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL。
free
free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小。
void test() { int* p1 = (int*)malloc(sizeof(int)); int* p2 = (int*)calloc(4, sizeof(int)); int* p3 = (int*)realloc(p2, sizeof(int) * 10); }
C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
new/delete操作的内置类型
int main() { //对于内置类型,malloc和new,free和delete没有本质区别,只是用法不同 int* p1 = (int*)malloc(sizeof(int)); int* p2 = (int*)malloc(sizeof(int) * 5); //动态申请一个int类型的空间 int* ptr4 = new int; //动态申请一个int类型并初始化为10 int* ptr5 = new int(5); //动态申请10个int类型 int* ptr6 = new int[10]; //int* ptr6=new int[5]={1,2,3,4,5};C++98不支持,C++11支持 free(p1); free(p2); delete ptr4; delete ptr5; delete[] ptr6; return 0; }
new/delete操作自定义类型
class A { public: A( ) :_a(10) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { //动态申请单个A对象和5个对象数组 A* p1 = (A*)malloc(sizeof(A)); A* p2 = (A*)malloc(sizeof(A) * 5); //new 在堆上申请对象空间+调用构造函数初始化对象 A* p3 = new A; A* p4 = new A[5]; free(p1); free(p2); //delete 先调用指针类型析构函数+释放空间给堆上 delete p3; delete[]p4; return 0; }
在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
也可以观察一共调用了6次构造函数,和6次析构函数
总结:
1.对于内置类型申请释放,本质上没有区别,只是用法上的区别
2.对于自定义类型,就不仅仅是用法上的区别了,new动态申请空间并调用构造函数初始化;
delete释放对象时,调用析构函数清理对象中资源,释放空间.
思考:直接释放空间就可以了,为什么要调用析构函数清理对象中资源后,在进行释放?
比如类成员中有个vector也指向了堆上的内存,就需要在析构函数中同样使用delete释放这块内存,或者说它自身处于一个容器当
中,就需要在这个容器中erase它,然后再free掉整个对象内存。delete b过后,b仍然指向该内存,即地址不变,但指针可能为悬垂指针,访问它可能带来意想不到的结果,也可能正确访问,不确定,所以建议delete后,把指针设置为NULL,后面也可根据指针是否为NULL判断是否可用。
C和C++在内存申请失败时处理方式的区别
void BuyMemory() { char* p2 = new char[1024u * 1024u * 1024u * 2u]; //p2 = new char[1024u * 1024u * 1024u * 1u]; printf("%p\n", p2); } int main() { // 面向对象的语言,处理错误的方式一般是抛异常,C++中也要求出错抛异常 -- try catch // 面向过程的语言,处理错误的方式是什么-》返回值+错误码解决 /*char* p1 = (char*)malloc(1024u*1024u*1024u*2u); if (p1 == nullptr) { printf("%d\n", errno); perror("malloc fail"); exit(-1); } else { printf("%p\n", p1); }*/ try { BuyMemory(); } catch (const exception& e) { cout << e.what() << endl; } return 0; }
面向对象的语言,处理错误的方式一般是抛异常,C++中也要求出错抛异常 -- try catch
面向过程的语言,处理错误的方式是什么-》返回值+错误码解决
这里简单了解一下,不做过多讲解,后期会详细分析.
operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new是通过malloc来开空间,相比malloc多一个抛异常处理的功能.
- operator new=malloc+抛异常处理
- new=operator new+构造函数
delete没啥区别
总结:
new和delete的实现原理
内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL.
自定义类型
1.new的原理
- 调用operator new申请空间
- 在申请的空间上调用构造函数
2.delete的原理
- 在空间上执行析构函数,完成对象中资源清理的工作
- 调用operator delete函数释放空间
- new T[N]的原理
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
2. 在申请的空间上执行N次构造函数
delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
定位new表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象.
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
class A { public: A(int a=0) :_a(a) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { int* p1 = (int*)malloc(sizeof(int)); new(p1)A(10); A* p2 = new A(10); delete p2; //等价于 A* p3 = (A*)operator new(sizeof(A)); new(p3)A(10); p3->~A(); operator delete(p3); return 0; }
malloc/free和new/delete的区别
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在释放空间前会调用析构函数完成空间中资源的清理
内存泄漏
什么是内存泄漏:
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费.
内存泄漏的危害是什么?
a、出现内存泄漏的进程正常结束,进程结束时这些内存会还给系统,不会有什么大伤害!
b、出现内存泄漏的进程非正常结束,比如僵尸进程。危害很大,系统会越来越慢,甚至卡死宕机。
c、需要长期运行的程序,出现内存泄漏。危害很大,系统会越来越慢,甚至卡死宕机。--服务器程序
一种内存申请了忘记释放是容易检查出来的
1. // 1.内存申请了忘记释放 2. int* p1 = (int*)malloc(sizeof(int)); 3. int* p2 = new int;
另一种是函数抛异常,导致下面delete未执行
1. // 2.异常安全问题 2. int* p3 = new int[10]; 3. Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放. 4. delete[] p3;
内存泄漏分类(了解)
堆内存泄漏
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定.
如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
思考:如何一次在堆上申请4G的内存?
#include <iostream> using namespace std; int main() { //0xffffffff转换为十进制就是4G void* p = malloc(0xfffffffful); cout << p << endl; return 0; }
在32位的平台下,内存大小为4G,但是堆只占了其中的2G左右,所以我们不可能在32位的平台下,一次性在堆上申请4G的内存。这时我们可以将编译器上的win32改为x64,即64位平台,这样我们便可以一次性在堆上申请4G的内存了。