1. 内存分布
在对操作系统有更加深入的了解之前,在写代码的层面我们需要对下面的几个内存区域有所了解:
1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 堆--用于程序运行时动态内存分配,堆是可以上增长的。
3. 数据段--存储全局数据和静态数据。
4. 代码段--可执行的代码/只读常量。
简单来说,直接定义的变量存放在栈中,动态申请的空间存放在堆中,全局或静态变量存放在数据段,字面量存放在代码段。
值得注意的是以下这段代码:
const char* pChar = "abcd";
int* ptr = (int*)malloc(sizeof(int) * 4);
这段代码中,“pChar”和“ptr”作为直接定义的变量,都是存放在栈中的。
而“abcd”作为只可读不可写的字面量存放在代码段,“pChar”中存放的是这个字符串的首元素地址。
malloc动态申请的空间存放在堆中,“ptr”只是存放其地址。
需要与上面的“pChar”形成区分的是:
char charArr[] = "abcd";
这段代码并非将“abcd”这一字面量字符串定义为了一个数组,而是以该字符串的内容为蓝本,在栈上定义了一个数组。
2. C++内存管理方式(new/delete)
在C语言中,我们使用malloc/calloc/realloc/free四个函数来进行内存管理。
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
申请/释放单个对象:
Type* pName = new Type(value);
delete pName;
// 动态申请一个int类型的空间
int* p1 = new int;
// 动态申请一个int类型的空间,并将其初始化为10
int* p2 = new int(10);
delete p1;
delete p2;
申请/释放数组:
Type* pName = new Type[num];
delete[] pName;
// 动态申请10个int类型的空间
int* p3 = new int[10];
delete[] p3;
3. new/delete与malloc/free的区别
最主要的区别就是,new/delete在申请和释放自定义类型的对象时,会调用对应类的构造和析构函数,而malloc/free则不会。
究其本质,new/delete是按照类型来处理空间,malloc/free是按照大小来处理空间。
换句话来说,malloc并不知道自己申请的这块空间是用作何用的,准确来说是这块空间根本没有类型,只不过我们将malloc返回的指针进行了强转,并通过这个指针来对这块空间进行操作,使其具有了对应类型的性质。
由于空间本身不存在任何类型,自然在被申请和被释放时都不会有某个类的构造函数和析构函数参与。
用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在释放空间前会调用析构函数完成空间中资源的清理释放。
可以通过下面的方式来捕获异常:
try
{
int* p = new int;
}
catch(const exception& e)
{
cout << e.what() << endl;
}
exception是C++的一种内置类型,what()函数会返回异常信息。
4. operator new与operator delete函数
new/delete在工作时,会经历“申请/释放空间”和“调用构造/析构函数”两步。
operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。
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 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 );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
上面代码中的"_free_dbg"实际上就是free,头文件中free的实现如下:
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
用operator new和operator delete对malloc和free进行包装的目的是为了适应C++的异常抛出与捕获机制。
5. new和delete的实现原理
new的原理:
1. 调用operator new函数申请空间;
2. 在申请的空间上执行构造函数,完成对象的构造。
delete的原理:
1. 在空间上执行析构函数,完成对象中资源的清理工作;
2. 调用operator delete函数释放对象的空间。
new Type[num]的原理:
1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请;
2. 在申请的空间上执行N次构造函数。
delete[]的原理:
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理;
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间。
6. 定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
由于析构函数可以显式调用,所以不存在“定位delete表达式”。
使用格式:
new (place_address) Type或者new (place_address) Type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
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));
new(p1)A(1);// 有默认构造时也可不要初始化列表
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
return 0;
}
使用场景:
定位new表达式在实际中一般是配合内存池使用。
因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显式调构造函数进行初始化。
7. 申请释放方式不匹配
前面讲到,单个对象和申请数组有自己对应的申请释放方式。
自然,我们不建议混用,但是如果混用会出现什么问题呢?
由于new/delete的底层是malloc/free,所以内置类型进行混用不会出现问题,但自定义类型会。
1. new + free
析构函数不会被调用,如果对象中有动态资源的话,这些动态资源就没有被释放,会导致内存泄漏。
2. malloc + delete
构造函数不会被调用,对象不会进行初始化,未进行初始化的对象在调用成员函数时可能出现野指针或空引用等错误。
3. new [] + free
无析构函数时,正常;有析构函数时,程序会直接崩溃。
new []还存在一个隐藏特性:如果类中有显式实现的析构函数,new []在开辟数组空间时,会在数组前额外开辟四个字节的空间,用于存储数组的元素个数,该数据存在的目的是告诉delete[]有多少个元素需要被释放。
而返回的地址依然是数组首元素的地址。
由于这整个数组及数组前的空间是new利用malloc一并申请的连续的空间,所以当我们给free传入数组首元素的地址时,就会发生报错(free不能释放连续空间的某部分)。
4. malloc + delete[]以及new + delete[]
无析构函数时,正常;有析构函数时,由于缺少数组前的数据,会一直重复调用析构函数,具体机理不清楚(vs2022)。
5. new [] + delete
delete单独释放首元素,导致与3相似的错误。
由于是先调用析构函数再释放,所以此处调用了一次析构函数之后才发生报错。