前言
本节继续C++的学习,让我们来看看C++的动态内存管理吧!
推荐阅读
《深入理解计算机系统》- 虚拟内存 —深入理解计算机系统pdf
引子
动态内存管理我们在C语言中就是重要的部分,我们应该不会对其陌生。 在C语言中有关动态内存管理的函数有malloc()、calloc()、realloc()、free() ; 其中malloc、calloc、realloc 是向堆区申请内存的函数,free 是释放在堆区申请的内存空间的函数;
malloc 函数 向堆申请以字节为单位的内存空间,并且申请的空间中初始值是随机值;
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p1 = (int*)malloc(sizeof(int) * 4);
if (!p1) {
perror("malloc fail");
return -1;
}
free(p1);
p1 = NULL;
return 0;
}
calloc 函数 向堆申请以字节为单位的内存空间,并且对申请的内存空间每一个位bit 都初始化为0;
int main() {
int* p2 = (int*)calloc(4, sizeof(int));
if (!p2) {
perror("malloc fail");
return -1;
}
free(p2);
p2 = NULL;
return 0;
}
realloc 函数 对已经开辟的内存空间进行扩容,扩容成功返回扩容后内存起始地址,扩容失败返回空指针; 扩容又分为原地扩和异地扩: 当原来开辟空间之后有足够的的空闲空间,进行原地扩容; 当原来开辟的空间之后没有足够的空间,进行异地扩容,在堆中随机寻找一块足够的空间并把原来空间内容拷贝到新空间,释放旧空间,函数返回新空间的起始地址;
int main() {
int* p1 = (int*)malloc(sizeof(int) * 4);
if (!p1) {
perror("malloc fail");
return -1;
}
int* tmp = (int*)realloc(p1, sizeof(int) * 8);
if (!tmp) {
perror("realloc fail");
return -1;
}
p1 = tmp;
free(p1);
p1 = NULL;
return 0;
}
C++由C而来,C++兼容C语言,C++中也可以直接使用C语言中有关动态内存开辟空间的函数;不过C++中一般不会直接使用原生的C语言中的malloc/calloc/realloc/free 函数,C++中为了更好地支持面向对象类 ,引入了有关动态内存的新概念:new和delete ;
C/C++进程内存的分布
在我们写的C/C++程序运行起来时,操作系统会为我们的程序建立一个进程,而每一个进程都有自己的虚拟地址空间,这里要介绍的就是C/C++程序对应进程中虚拟地址空间的划分。
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等,栈是向下增长的;
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口
创建共享共享内存,做进程间通信; - 堆用于程序运行时动态内存分配,堆是上增长的;
- 数据段–存储全局数据和静态数据;
- 代码段–可执行的代码/只读常量;
C++的动态内存管理
new
new 和delete 是C++中新引入的关键字,同时也是运算符,这一点与C语言中malloc等 函数不同; new 格式
类型* 指针变量名 = new 类型
申请空间
使用new 申请1个整型内存空间
int* p1 = new int;
cout << "*p1= " << * p1 << endl;
*p1 = 10;
cout << "*p1= " << *p1 << endl;
使用new 申请多个个整型内存空间
int* p1 = new int[5];
初始化
int* p1 = new int(20);
cout << "*p1= " << *p1 << endl;
int* p1 = new int[5]{ 1,2 };
int* p2 = new int[5]{ 1,2,3,4,5 };
delete 释放申请的空间
格式:
delete 指向动态申请空间的指针
int* p1 = new int(20);
cout << "*p1= " << *p1 << endl;
delete p1;
int* p1 = new int[5]{ 1,2 };
delete[] p1;
p1 = nullptr;
new和delete对于自定义类型
对于内置类型,new申请内存空间和delete释放空间相对于malloc申请空间和free释放空间基本没有区别,只是new和delete用法更加简洁和方便; new和delete的真正不同的用处是相对于自定义类型来说的;
new
- 完成内存空间的申请;
- 调用类的构造函数进行初始化
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
int main() {
A* p1 = new A;
A* p2 = (A*)malloc(sizeof(A));
return 0;
}
delete
- 调用类的析构函数完成类对象资源清理工作;
- 释放申请的空间。
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
int main() {
A* p1 = new A;
A* p2 = (A*)malloc(sizeof(A));
delete p1;
free(p2);
return 0;
}
new和delete的注意事项
new申请的空间和delete释放的空间要严格匹配,否则可能会出现意想不到的错误;
错误举例:
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
new了一个对象,delete多个对象
int* p1 = new int;
delete[] p1;
A* p1 = new A;
delete[] p1;
new了多个对象,delete一个对象
A* p2 = new A[10];
delete p2;
new出来的对象,使用free()释放
A* p1 = new A;
free(p1);
A* p1 = new A[10];
free(p1);
以上这些行为到底会发生什么报错、异常、正常运行 ,不同编译器的处理方式不一定相同; 我们在使用new和delete时应该匹配使用,这样才能避免可能的错误。
new的底层
我们知道new操作符包含的操作:
对内存空间的申请; 如果是自定义类型,则还会调用自定义类型的构造函数进行初始化;
C++引入了new,但也不是凭空而来,new实际上是对malloc()的封装;
operatoe new 函数
operator new介绍
new的底层申请空间是通过调用operator new函数实现的;
operator new函数一览 函数接受一个无符号整型,作为待申请空间的大小 申请成功返回起始空间的地址; 申请失败返回nullptr;
void * operator new(size_t size){
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0){
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
operator new() 通过malloc()申请空间,如果malloc()申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常; 对于自定义类型,operator new()不会调用构造函数,而是由其他函数调用;
显式使用operator new
operator new的使用与malloc相似
内置类型
int main() {
int* p1 = new int;
int* p2 = (int*)operator new(sizeof(int));
int* p3 = (int*)malloc(sizeof(int));
return 0;
}
自定义类型
- 调用operator new函数申请空间;
- 在申请的空间上执行构造函数,完成对象的构造;
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
int main() {
A* p1 = new A;
A* p2 = (A*)operator new(sizeof(A));
A* p3 = (A*)malloc(sizeof(A));
return 0;
}
operator new[]函数
operator new[]函数介绍
operator new[] 申请多个对象时调用; operator new[] 函数底层会调用operator new 函数,operator new 函数又调用malloc 函数;
void* operator new[](size_t size){
return operator new(size);
}
显式使用operator new[]
内置类型
int main() {
int* p1 = new int[4];
int* p2 = (int*)operator new[](sizeof(int)*4);
int* p3 = (int*)malloc(sizeof(int) * 4);
return 0;
}
自定义类型
- 调用
operator new[] 函数,在operator new[] 中实际调用operator new 函数完成N个对 象空间的申请; - 在申请的空间上执行N次构造函数 ;
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
int main() {
A* p1 = new A[4];
A* p2 = (A*)operator new[](sizeof(A) * 4);
A* p3 = (A*)malloc(sizeof(A) * 4);
return 0;
}
delete的底层
delete底层可以分为两部分
- 如果是自定义类型,先调用自定义类型的析构函数,清理资源;
- 释放申请的空间;
operator delete函数
operator delete介绍
delete调用operator delete函数,operator delete函数又调用free函数; 如果是自定义类型,delete将先调用自定义类型的析构函数,再调用operator delete函数;
为什么说operator delete函数调用了free函数呢? 因为operator delete函数调用了_free_dbg()函数 ,而free()函数实际是由_free_dbg()函数 宏实现的;
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
void operator delete(void *pUserData){
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK);
__TRY
pHead = pHdr(pUserData);
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK);
__END_TRY_FINALLY
return;
}
显式使用operator delete
内置类型
int main() {
int* p1 = (int*)operator new(sizeof(int));
operator delete(p1);
p1 = nullptr;
return 0;
}
这里直接使用delete运算符也可以对operator new函数申请的空间进行释放; 这是因为,delete的底层会调用operator delete,而operator delete再调用free,这与operator delete直接调用free相比只是多了一层转换;
int main() {
int* p1 = (int*)operator new(sizeof(int));
delete p1;
p1 = nullptr;
return 0;
}
自定义类型
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
_a = 0;
}
private:
int _a;
};
int main() {
A* p1 = (A*)operator new(sizeof(A));
operator delete(p1);
p1 = nullptr;
return 0;
}
这里也可以直接使用delete运算符
int main() {
A* p1 = (A*)operator new(sizeof(A));
delete p1;
p1 = nullptr;
return 0;
}
operator delete[]函数
operator delete[]函数介绍
operator delete[]将调用operator delete函数
void operator delete (void* ptr) noexcept;
显式使用operator delete[]
内置类型
int main() {
int* p1 = (int*)operator new[](sizeof(int)*4);
operator delete[](p1);
p1 = nullptr;
return 0;
}
自定义类型
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
int main() {
A* p1 = (A*)operator new[](sizeof(A) * 4);
operator delete[](p1);
return 0;
}
关于delete底层实现的一些简单分析
为什么说申请内存和释放内存的方式要严格匹配呢? 我们知道如果不匹配可能会引发意想不到的情况,这与编译器有关; new 是创建一个新对象,delete 也释放一个对象(如果是自定义类型还会调用析构函数); new[] 是创建一个对象数组;我们当然知道我们自己创建的对象数组的大小,对于delete[] 并不知道对象数组的大小,只知道对象数组的起始地址; 那么编译器如何知道delete[] 要释放的对象个数呢? 一种方式是,再开始创建对象数组时new [] 并不是创建了我们指定的大小,而是在对象数组前且紧邻对象数组又额外开辟了一小块空间用于记录对象数组的大小; 这样,在delete [] 时,我们释放表面上的内存空间,实际上编译器依据表面上对象数组起始地址再往前偏移一个确定大小的空间,实际从偏移后的位置释放申请的对象数组空间。 对于有显式析构函数的自定义类型来说,这也是其调用析构函数次数的依据;
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
}
private:
int _a;
};
int main() {
A* p = new A[7];
delete[] p;
return 0;
}
解释delete p异常的可能原因
int main() {
A* p = new A[7];
delete p;
return 0;
}
内存泄漏,对象数组起始地址之前还有额外的空间未被释放;
把类A的显式析构函数去掉就不报错了: delete不需要调用显式的析构函数,在申请对象数组时就没有开辟额外的空间记录对象数组的元素个数,释放对象数组也不需要再往前偏移了,使用delete和delete[]没有区别了;
class A {
public:
A(int a = 1) :_a(a) {
cout << "构造函数: A(int a)" << endl;
}
private:
int _a;
};
int main() {
A* p = new A[4];
delete p;
return 0;
}
C++异常
我们知道new运算符向堆申请一块内存空间,但是申请空间是有失败的情况的,new失败了会发生什么呢? 与malloc/calloc/realloc 失败返回空指针不同,new失败了是抛出一个异常,而非返回空指针;
int main() {
try {
while (1) {
char* p1 = new char[1024 * 1024 * 1024];
cout << (void*)p1 << endl;
}
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}
内存泄漏
概念
内存泄漏指因为疏忽或错误造成程序没有释放已经不再使用的内存的情况。 所以说内存泄漏不是内存在物理上的消失内存还在那里 ,而是因为设计错误,失去了对分配给应用程序的内存的控制指针丢了 ,造成了内存的浪费。
分类
堆内存泄漏(Heap leak)
堆内存指的是程序执行中通过malloc / calloc / realloc / new 等从堆中分配的一 块内存,用完后必须通过调用相应的 free 或者delete 释放。 如果申请的内存使用完后没有被释放,那么这部分内存就无法再次被申请使用,将导致堆内存泄露;
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定;
内存泄漏危害
对于我们写的短时间运行的程序,内存泄露影响一般比较小,因为每次程序重启内存会被强制回收; 而对于长时间运行的程序或设备:操作系统/服务器等,内存泄露危害很大;
如果内存泄漏比较明显,短时间内我们就可以察觉到,这样的内存泄漏一般不会造成大的影响,我们能够及时排查; 而对于轻微的内存泄漏,就是头疼的事情,我们一般很难在初期发现这样的内存泄漏,往往等到发现时时间已经过去想当久了,这可能导致运行了很长时间的系统或设备卡顿甚至突然死机,这对于多人使用的服务器来说影响巨大,损失也往往是巨大的;
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死
规避内存泄漏
事先预防
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间匹配的去释放,但是如果碰上异常时,就算注意释放了,还是可能会出问题;
- 采用RAII思想或者智能指针来管理资源;
事后查错
使用内存泄漏工具检测;
内存泄露检测推荐
_CrtDumpMemoryLeaks() 函数
windows操作系统 提供的_CrtDumpMemoryLeaks() 函数 进行简单检测,该 函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息;
int main() {
int* p1 = new int[4];
_CrtDumpMemoryLeaks();
return 0;
}
第三方工具检测
Windows
VLD工具 - (Visual LeakDetector)内存泄露库
VLD官网 https://kinddragon.github.io/vld/ VLD教程推荐 http://t.csdn.cn/6AKkI
得到内存泄漏点的调用堆栈和泄露内存的完整数据;
Linux
valgrind - 功能非常强大
valgrind官网 https://valgrind.org/ 教程推荐 http://t.csdn.cn/ix463
定位new表达式(placement-new)
概念
在已分配原始内存空间中调用构造函数初始化一个对象; 也就是已经申请的内存空间malloc/calloc/realloc/operator new/operator new[] ,但是还未调用构造函数,可以使用定位new 表达式来调用构造函数。
语法
new (place_address) type 或者new (place_address) type(initializer-list) place_address 是一个指针,initializer-list 是类型的初始化列表
用法
class A {
public:
A(int a = 1)
:_a(a) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
_a = 0;
}
private:
int _a;
};
int main() {
A* p1 = (A*)malloc(sizeof(A));
new(p1)A(2);
p1->~A();
free(p1);
return 0;
}
int main() {
A* p1 = (A*)operator new(sizeof(A));
new(p1)A(2);
p1->~A();
operator delete(p1);
p1 = nullptr;
return 0;
}
构造函数有多个参数时,可以传多个实参;
class A {
public:
A(int a = 1,int b = 1, int c = 1) :_a(a),_b(b),_c(c) {
cout << "构造函数: A(int a)" << endl;
}
~A() {
cout << "析构函数: ~A()" << endl;
_a = _b = _c = 0;
}
private:
int _a;
int _b;
int _c;
};
int main() {
A* p1 = (A*)operator new(sizeof(A));
new(p1)A(2, 2, 2);
p1->~A();
operator delete(p1);
p1 = nullptr;
return 0;
}
new/delete与malloc/free
相同点
都从堆上申请空间,需要用户手动释放
不同
malloc 和free 是函数;new 和delete 是操作符(也是C++新增的关键字)malloc 申请的空间不会初始化;new 可以初始化malloc 申请空间时,需要手动计算空间大小并传递;new 只需在其后跟上空间的类型即可,如果是多个对象,[] 中指定对象个数malloc 的返回值为void* , 使用时必须强转;new 后跟的空间的类型可以直接得到空间类型,不强转malloc 申请空间失败时,返回的是NULL ,使用前必须判空;new 失败则是抛出异常,可以由另一部分捕获- 对于自定义类型对象空间的申请,
malloc/free 只开辟空间和释放空间,不会调用构造函数与析构函数(没有初始化);new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理
后记
本节主要介绍了C++中的动态内存管理方式:new/delete 、new[]/delete[] 的使用和底层的原理;同时内存泄漏是动态内存经常会遇到的问题,我们也不需要过多担心,小心使用动态内存+内存泄漏检测或以后的智能指针可以解决绝大部分问题。 下次再见!
E
N
D
END
END
|