1. C/C++内存分布
首先我们需要知道,在C++中的内存分为5个区。
1. 栈 又叫堆栈 -- 非静态局部变量 / 函数参数 / 返回值等等,栈是向下增长的。
2. 内存映射段 是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享共享内存,做进程间通信。
3. 堆 用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段(静态区) -- 存储全局数据和静态数据。
5. 代码段(常量区) -- 可执行的代码 / 只读常量
那么为什么要划分这么多区域呢?首先数据是需要存储的,那么不同的数据就有不同的性质,内存区域的划分就是为了满足数据存储的各种需求。
栈区可以建立栈帧,函数的一些临时变量在函数执行完之后就不需要了,可以直接销毁,函数结束了之后,栈帧会自动销毁,所以这些临时变量就会销毁。
堆区是为了满足数据动态使用的需求,在数据结构中会大量的使用动态内存,销毁的时间由自己决定,自己不需要了就可以自行销毁。
一些全局变量,静态的数据存放在静态区,整个程序在运行期间都需要这些数据,所以这些数据要一直存在,作用域是全局的。
可执行代码和不修改的数据(常量)存放在常量区。
我们先来看下面的一段代码和相关问题。
int globalVar = 1; static int staticGlobalVar = 1; void Test() { static int staticVar = 1; int localVar = 1; int num1[10] = { 1, 2, 3, 4 }; char char2[] = "abcd"; const char* pChar3 = "abcd"; int* ptr1 = (int*)malloc(sizeof(int) * 4); int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); }
前面三个肯定是存在静态区的,这个没有疑问。locaval是存在栈区的,num1也是放在栈区,因为都是临时创建出来的,存放在栈区。
char2也是临时变量,存放在栈区。*char2也是存放在栈区。pchar3也是在栈区,虽然有const修饰,但是需要注意的是这个const修饰的是这个指针指向的内容,而不是这个指针。所以*pchar3存放在常量区,因为这个指针被const修饰。ptr1是创建出来的一个临时变量,所以在栈上面,*ptr1指向的是动态开辟出来的空间,而这块空间是在堆上面。
我们再来看一下下面这些填空题。
int globalVar = 1; static int staticGlobalVar = 1; void Test() { static int staticVar = 1; int localVar = 1; int num1[10] = { 1, 2, 3, 4 }; char char2[] = "abcd"; const char* pChar3 = "abcd"; int* ptr1 = (int*)malloc(sizeof(int) * 4); int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); }
需要注意的是strlen算的是长度不算'\0',sizeof算的是大小,'\0'也开了空间的,指针的大小取决于当前编译器是32bit位还是64bit位。
2. C语言中动态内存管理方式:malloc/calloc/realloc/free
1. malloc/calloc/realloc的区别?
calloc与malloc的区别是calloc等于malloc+memset,将开辟的空间全部初始化成0.realloc是调整空间大小,如果这个空间还没有开辟,那么第一次realloc相当于一次malloc。
3. C++内存管理方式
C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此 C++ 又提出了自己的内存管理方式: 通过 new 和 delete 操作符进行动态内存管理 。
3.1 new/delete操作内置类型
C++开辟空间使用new+类型,如果需要开多个空间,在类型后加[]。
int main() { int* p1 = new int; int* p2 = new int[4]; return 0; }
对内置类型,可以看到开出来的空间并不会初始化,是随机值。
那么想初始化怎么办?
如果是单个值的话在类型后面加(),多个值则加{}。
int main() { int* p3 = new int(1); int* p4 = new int[4] {4, 6, 8}; return 0; }
如果开了10个int的空间,只初始化了前面几个,那么后面的数据会默认初始化成0.
那么怎么释放呢?C++规定new出来的空间使用delete来释放。
int main() { int* p1 = new int; int* p2 = new int[4]; int* p3 = new int(1); int* p4 = new int[4] {4, 6, 8}; delete p1; delete[] p2; delete p3; delete[] p4; return 0; }
注意:申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用
new[] 和 delete[] ,注意:匹配起来使用。
3.2 new和delete操作自定义类型
我们来看下面这段代码,如果使用malloc为自定义类型的对象开空间的话,就没办法初始化,因为自定义类型的初始化在构造函数里面,构造函数是对象实例化的时候自动调用的。
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { A* p1 = (A*)malloc(sizeof(A)); return 0; }
所以就要换成new来开空间,new会做两件事情,第一就是开空间,第二是调用此自定义类型的默认构造函数,如果没有默认构造函数则会报错。
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { A* p2 = new A(1); delete p2; return 0; }
delete也会做两件事,第一件事是调用该自定义类型的析构函数,第二件事是释放空间。就相当于先清理资源,再释放掉空间。
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { A* p2 = new A(1); delete p2; A* p3 = new A[10]; delete[] p3; return 0; }
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与 free不会。
4. operator new与operator delete函数
new 和 delete 是用户进行 动态内存申请和释放的操作符 , operator new 和 operator delete 是
系统提供的 全局函数 , new 在底层调用 operator new 全局函数来申请空间, delete 在底层通过
operator delete 全局函数来释放空间。
operator new 实际也是通过 malloc 来申请空间 ,如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施 就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的 。
5. new和delete的实现原理
5.1 内置类型
如果申请的是内置类型的空间, new 和 malloc , delete 和 free 基本类似,不同的地方是:
new/delete 申请和释放的是单个元素的空间, new[] 和 delete[] 申请的是连续空间,而且 new 在申
请空间失败时会抛异常, malloc 会返回 NULL 。
5.2 自定义类型
new 的原理
1. 调用 operator new 函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造
delete 的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用 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 来释
放空间
6. 常见面试题
6.1 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 在释放空间前会调用析构函数完成 空间中资源的清理
6.2 内存泄漏
6.2.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。
6.2.2如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。 ps : 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。
2. 采用 RAII 思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。 ps :不过很多工具都不够靠谱,或者收费昂贵。
总结一下 :
内存泄漏非常常见,解决方案分为两种: 1 、事前预防型。如智能指针等。 2 、事后查错型。如泄 漏检测工具。
今天的分享到这里就结束了,感谢大家的阅读!