一、C/C++内存布局
程序运行过程中的数据存储在内存中。数据分布在内存的不同区域,不同区域的数据就有不同的性质。
【说明】
- 数据段–存储全局数据和静态数据,数据段存储的数据在程序的整个运行期间都存在。
- 代码段–可执行的代码/只读常量,代码段的数据由硬件保护不能被修改。
- 程序运行时位于数据段的全局数据,静态数据,和位于代码段的只读常量,和二进制指令最先被加载到内存。
- 栈又叫堆栈–用于存储非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 栈区数据是在程序运行过程中通过调用函数从而开辟栈帧,函数返回释放栈帧。
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 堆用于程序运行时动态内存分配,需由程序员手动申请分配,销毁释放。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
二、C语言动态内存管理方式
C语言中动态内存管理方式:malloc/calloc/realloc/free
malloc:申请指定大小的动态内存空间,返回空间首地址。
calloc:申请指定大小的动态内存空间,返回空间首地址。与malloc不同的是它会将申请好的内存每个字节初始化成0。
realloc:用于动态内存扩容,分为原地扩容和异地扩容,返回扩容后的空间首地址。
free:用于释放动态开辟的内存,以上三个函数开辟的内存最终都要free释放,否则就会造成内存泄漏。
三、C++动态内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1 new/delete操作内置类型
void Test1(){
//对比C语言申请动态内存的方法
int *p1 = (int *)malloc(sizeof(int));
//申请1个int对象
int *p2 = new int;
//申请5个int的数组
int *p3 = new int[5];
//申请1个int对象并初始化为5
int *p4 = new int(5);
//C++11支持申请数组时{}直接初始化,C++98不支持
int *p5 = new int[5]{1,2,3};//数组的前3个数被初始化做1,2,3;后两个数自动初始化成0;
free(p1);
delete p2;
delete[] p3;//[]要配对使用,否则可能导致程序崩溃
delete p4;
delete[] p5;
}
注意:
- 申请和释放单个元素的空间,使用new和delete操作符;
- 申请和释放连续的空间,建议匹配使用new[]和delete[]。
- 注意:对于内置类型,使用new[]分配,delete删除(不匹配)也不会造成程序崩溃,内存泄漏等问题,但不建议使用。
- C++没有针对realloc扩容的新语法。
结论:针对内置类型,new/delete跟malloc/free没有本质的区别,只是用法上简化了。
3.2 new和delete操作自定义类型
与malloc不同的是:针对自定义类型,new不仅申请开辟了内存空间,还会调用构造函数对对象进行初始化。
void Test2(){
//malloc:1.堆上申请空间
A *p1 = (A*)malloc(sizeof(A));
//new:1.堆上申请空间 2.调用构造函数初始化对象
A *p2 = new A; //不传参数调用默认构造函数,没有默认构造编译报错
A *p3 = new A(10); //还可以给构造函数传参
A *p4 = new A[5]; //可以连续创建多个对象,这些对象都会调用构造函数初始化
//C++11支持申请对象数组时通过{}为各自构造函数传参,C++98不支持
A *p5 = new A[5]{1,2,3}; //隐式类型转换优化成直接构造
A *p6 = new A[5]{A(1), A(2), A(3)}; //拷贝构造优化成直接构造
//多个参数这样玩
A *p5 = new A[5]{{1,10}, {2,20}, {3,30}}; //未指定参数的对象会调用默认构造,没有默认构造编译报错
A *p6 = new A[5]{A(1,10), A(2,20), A(3,30)};
//free:1.释放堆空间
free(p1);
//delete:1.调用析构函数清理对象占用的资源 2.释放堆空间
delete p2;
delete p3;
delete[] p4; //释放连续存储的多个对象,这些对象在销毁前都会调用析构函数清理资源
delete[] p4; //[]要配对使用,否则程序可能崩溃
}
注意:
- 用new/delete创建的对象也同样满足先定义的先构造,后定义的先析构的构造析构顺序。
- 对于自定义类型申请和释放连续的空间,必须匹配使用new[]和delete[],否则会导致程序崩溃。
补充:delete还可以用于删除默认函数
通过使用 delete 运算符,可以删除类的默认构造函数、拷贝构造函数、赋值重载等默认函数的默认实现。这样,当试图使用这些默认函数时,编译器会报错。
class NoDefault {
public:
NoDefault() = delete;
};
NoDefault obj; // 编译错误,无法调用默认构造函数
结论:new/delete是针对自定义类型而设计的。不仅会在堆上申请空间,还会调用构造和析构进行初始化和清理工作。
3.3 异常处理机制
void Test3(){
// malloc失败返回NULL
char *p1 = (char*)malloc(1024u*1024u*1024u*2-1);
//调用malloc申请内存空间后,需检查其返回值是否为NULL
if(p1 == NULL)
{
perror("malloc");
exit(1);
}
printf("%p\n", p1);
// new失败后抛异常,不需要判空,但需要捕获异常并进行处理
char *p2 = new char[1024u*1024u*1024u*2-1];
printf("%p\n", p2);
}
抛异常:
抛异常是面向对象语言处理错误的一种方式,相比C语言返回错误码,抛异常的效果更好。异常需用try catch等相关函数进行捕获,让程序员知道到底出了什么错误,并采取适当的措施来解决问题。
C++ 中的异常处理机制允许程序在出现异常时捕获并处理它们,以便更好地管理程序的执行流程。当程序抛出异常时,它会将异常对象作为参数传递给相应的异常处理函数,这些处理函数可能会尝试采取适当的措施来解决问题,例如尝试重新启动程序、记录错误信息、返回错误码等。
总结:new失败后抛异常,不需要判空。但是new需要捕获异常,并采取适当的措施来解决问题。
四、new和delete的实现原理
4.1 底层汇编
- new和delete是用户进行动态内存申请和释放的操作符;
- operator new 和operator delete是系统提供的运算符重载函数(全局函数);
new的原理
- 调用operator new函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造
delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用operator delete函数释放对象的空间
new T[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
- 在申请的空间上执行N次构造函数
delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放N个对象的空间
4.2 operator new与operator delete函数
operator new 源码片段:
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;
申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
operator new的作用就是在堆上申请空间。内部封装malloc进行空间申请,同时加入了C++的异常处理机制:失败抛异常。
operator delete 源码片段:
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse ); //内部封装free进行空间释放
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free是_free_dbg的宏
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator new的作用就是释放动态内存。内部封装free进行空间释放,并加入了一些内存相关的检查。
总结:通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。
4.3 malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
语法使用的区别:
- malloc和free是函数;new和delete是操作符
- malloc申请空间时,需要手动计算空间大小并传递;new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转;new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空;new不需要判空,但是new需要捕获异常
本质功能的区别:
- 申请自定义类型对象时不同:malloc/free只会开辟释放空间,不会调用构造函数与析构函数;而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
- 异常处理机制不同:malloc申请空间失败返回NULL(错误码),而new则是抛异常。
五、定位new表达式(placement-new) (了解)
定位new表达式是在已分配的原始内存空间中显示的调用构造函数初始化一个对象。
使用格式:
- new (place_address) type
- 或者 new (place_address) type(initializer-list)
- place_address必须是一个指向对象的指针,initializer-list是类型的初始化列表
注意:定位new可以初始化动态开辟的堆区对象,也可以初始化栈区和静态区对象。
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定位表达式进行显示调构造函数进行初始化。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}