一、C/C++内存分布
1. 内存分区
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信 .
- 堆用于程序运行时动态内存分配,堆是可以上增长的
- 数据段–存储全局数据和静态数据 (也叫静态区)
- 代码段–可执行的代码/只读常量 (也叫常量区)
2. 程序运行时加载过程
首先,我们平时所写的代码(如test.cpp)存放在那?
其实是存放在磁盘上的,因为是文件形式
而运行一个程序的过程是怎么样的?
写好的代码 -> 编译链接 -> 可执行程序
也就说,我们写好代码点击运行就是运行的这个可执行程序
可执行程序(Windows下.exe、Linux下a.out) 中包括:
- 二进制指令代码(CPU读取)
- 数据
- 其他一些内容
而当程序运行时首先要加载哪些东西到内存呢?
- 二进制指令代码 -> 代码段
- 常量数据 -> 代码段
- 全局变量 -> 数据段 (因为全局变量在main函数前已经定义好)
堆栈上的数据何时创建?
-
当二进制指令代码加载到代码段之后,由CPU依次读取并执行二进制指令 -
当开始执行main函数时,开始创建函数栈帧,此时栈中开辟的变量和数据就开始定义了。 -
而堆上的数据也是在栈上,通过malloc等动态开辟内存的函数来开辟的
注意
类、函数、符号表、公共代码区等概念都是在编译链接阶段的概念
编译链接之后,类、函数等都转变成了二进制指令加载到代码了,
不存在所谓的类或者函数了
3. 常见数据类型在内存中的分布
二、动态内存管理
C/C++中,除了堆以外的内存分区中资源的申请与释放不用我们管,系统会自动处理。(所以内存泄漏等问题出现在堆上)
1. C语言中动态内存管理方式
malloc : 申请一块空间
calloc :申请一块空间并初始化
realloc :对一段空间进行扩容
使用完都需要 free ,防止内存泄漏
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
free(p3 );
}
2. C++动态内存管理
虽然C++向下兼容C,但是有些地方C的方式是无能为力的,所以C++又搞了一套自己的动态内存管理方式
即:new 和 delete操作符
注意:new和delete 不是函数,是操作符
① new和delete针对 内置类型
-
申请/释放一个int大小的空间 int* p1 = new int delete p1 -
申请/释放5个int大小的空间 int* p2 = new int[5] delete[] p2 -
申请/释放一个int大小的空间并初始化 int* p3 = new int(5) delete p3 注意:new int(5) 与 new int[5]的区别!
int* p1 = (int*)malloc(sizeof(int));
if(p1==NULL)
{
perror("malloc fail");
exit(-1);
}
int* p2 = new int;
int* p3 = new int[5];
int* p4 = new int(5);
int* p5 = new int[5]{ 1,2,3,};
free(p1);
delete p2;
delete p4;
delete[] p3;
注意
new/delete 和 new[] /delete[] 一定要匹配,否则有时候会出现崩溃。一般来说自定义类型一定会报错,自定义类型一般不会报错
结论
1. 针对内置类型,new/delete 和 malloc/free没有本质的区别,只有用法上的区别,new/delete只是用法简化了
2. malloc的需要去检查是否开辟成功,new不需要,如果失败默认会抛异常
② new和delete针对自定义类型
new和delete针对内置类型与C的malloc/free无大区别
但是对于自定义类型,区别很大!
这也是引入new/delete的原因
new/delete针对自定义类型,与malloc/free最大的区别
-
就是new的时候会自动调用 默认构造函数(如果无默认构造会报错) -
delete的时候会自动调用析构函数
单个自定义类型对象的构造/析构
A* p1 = (A*)malloc(sizeof(A));
if (p1 == nullptr)
{
perror("malloc fail");
exit(-1);
}
A* p2 = new A;
A* p3 = new A(10);
free(p1);
delete p2;
delete p3;
多个对象初始化
- 多个对象默认构造初始化
A* p4 = new A[10];
delete[] p4;
-
多个对象显示构造初始化
A* p5 = new A[10]{ 1,2,3,4,5 };
A* p6 = new A[]{ 1,2,3,4,5 };
A* p7 = new A[]{ (1),(2) };
A* p8 = new A[]{ {1},{2} };
A* p9 = new A[]{ A(1),A(2)};
最常用的是第一种和第二种 注意:支持C++11的编译器才可以显示构造初始化 VS2013就不支持,2019以上是支持的
结论
new/delete是为自定义类型准备的,不仅在堆上申请出来,还会调用构造和析构函数初始化和清理
因为如果采用malloc申请自定义类型,是无法进行初始化的,因为构造函数是在对象定义的时候自动调用
malloc只是申请了空间,无法直接调用构造函数进行初始化,也没办法通过访问成员变量进行初始化(因为一般都是私有的)
③ malloc与new失败时的区别
malloc失败时,会返回NULL
new失败时,会抛异常
测试代码:
void test3()
{
char* p1 = (char*)malloc(1024u * 1024u * 1024u * 2);
printf("%p\n", p1);
}
new失败
char* p2 = new char[1024u * 1024u * 1024u * 2 - 1];
运行结果:
所以,malloc需要检查返回值看是否malloc失败
new是不需要检查返回值的,失败会抛异常
3. operator new和 operator delete函数(底层)
我们知道,new一个对象其实做了两件事
- 申请内存
- 调用构造函数
那么new申请内存是调用了谁来申请内存呢?
事实上,new和delete是用户进行动态申请内存和释放操作符,operator new 和 operator delete是系统提供的全局函数
new在底层调用operator new全局函数来申请空间
如图可见:new操作符被解析为指令的时候,主要是 1. call operator new函数 2. call A::A(构造函数)
注意:operator new就是一个函数名,不是运算符重载,只是名字很挫
① operator new和operator delete的底层原理
operator new的源代码
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
可以看到,operator new开空间实际上他也是调用了malloc函数来开空间,只是operator new采用了如果失败就抛出bad_alloc 的异常的做法。
这也是为什么要用operator new而不直接采用malloc的原因
总结operator new的作用
- 帮助new开空间
- 封装malloc,以复合C++new的失败机制(抛异常)
operator delete
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;
}
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator delete是调用free来进行释放资源的
注意:free实际上也是宏定义的,原型是_free_dbg
free是为了方便用户使用
/其他的一些加锁和检查可以忽略/
所以,new/delete对于内置类型其实和malloc/free没有本质区别,因为从底层来看,内置类型不需要调用构造函数和析构函数
② 直接使用operator new开辟空间(了解)
用法和malloc一样,搭配抛异常的try catch使用
实际上不需要使用这个,直接用new delete即可
try
{
char* ptr = (char*)operator new(1024u * 1024u);
printf("%p\n", ptr);
operator delete(ptr);
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}
③ operator new和operator delete的重载(了解)
1) 利用重载找出内存泄漏
有时候我们不想用系统提供的operator new和operator delete
那么我们就可以进行重载我们自己的operator new和
operator delete来完成一些特殊的需要
如:我们想要检测哪里存在内存泄漏,这时候就可以自己重载一个operator new,至于operator delete则自己写一个全局的(不是重载)
-
我们在自己重载的operator new中打印文件、函数、行号、申请字节数,然后调用全局的 ::operator new -
自己写一个全局的operator delete,这样delete的时候就会先调用自己写的operator delete了
注:__FILE__ ,__FUNCTION__ ,__LINE__ 等是C语言中的宏
分别是当前文件名 ,当前函数名 ,当前行号
void* operator new(size_t size, const char* fileName, const char* funcName,
size_t lineNo)
{
void* p = ::operator new(size);
cout <<"new:" << fileName << "-" << funcName << "-" << lineNo << "-" << p << "-"
<< size << endl;
return p;
}
void operator delete(void* p)
{
cout << "delete:" << p << endl;
free(p);
}
void operator delete(void* p, const char* fileName, const char* funcName,
size_t lineNo)
{
cout << "delete:" << fileName << "-" << funcName << "-" << lineNo << "-" << p <<
endl;
::operator delete(p);
}
#ifdef _DEBUG
#define new new(__FILE__, __FUNCTION__, __LINE__)
#endif
int main()
{
A* p1 = new A;
delete p1;
A* p2 = new A[4];
A* p3 = new A;
delete p3;
A* p4 = new A;
A* p5 = new A;
delete p5;
return 0;
}
程序运行结果如图
new了5个,但是只delete了3个,并且再通过行号就可以查出具体是哪里没释放而导致的内存泄漏了
2) 重载一个类专属的operator解决频繁申请空间带来的空间碎片问题(内存池)
我们知道,如果一个类,比如ListNode类(链表),需要频繁地向内存申请空间
这样就容易造成一些内存碎片问题。这里就可以利用池化技术来减少内存碎片问题,即内存池
内存池的工作原理:
因为malloc是去向堆申请内存,但是要知道操作系统是很忙的,如果频繁的申请就会经常打断操作系统的资源的分配
而内存池就相当于中间的一个角色。内存池先申请一部分内存,当你想要开辟空间首先到内存池中开辟,当内存池中的
内存用完,才会再去向堆上申请。这样就减少了请求操作系统的次数,提高效率。
用个比喻的话就是,堆是你妈妈的钱包,内存池是你自己的钱包。你的定期生活费就是内存池提前申请的内存
我们可以再类中重载一个operator new和operator delete函数。(注意不是函数重载,也不是运算符重载,只是命名空间即域不同)在我们重载的函数中使用内存池(可以自己写,也可以调用STL或者Boost库或第三方库中的内存池)
当new一个对象和delete一个对象的时候,机制决定了会先去类中找有没有类专属的operator new 和 operator delete
找到了就直接使用,找不到才回去全局找。并且因为是类专属的,其他的类的new/delete并不受影响
所以会直接调用我们在类中重载的operator new和operator delete函数,也就是先去我们定制的内存池中申请内存而不会直接向堆上申请内存
这里演示STL中的内存池allocator(空间配置器)
allocate :申请空间
deallocate :释放空间
struct ListNode
{
int _val;
ListNode* _next;
static allocator<ListNode> _alloc;
void* operator new(size_t n)
{
cout << "void* operator new(size_t n) -> STL内存池allocator申请" << endl;
void* obj = _alloc.allocate(1);
return obj;
}
void operator delete(void* ptr)
{
cout << "void* operator delete(size_t n) -> STL内存池allocator释放" << endl;
_alloc.deallocate((ListNode*)ptr,1);
}
ListNode(int val)
:_val(val)
, _next(nullptr)
{
}
};
allocator<ListNode> ListNode::_alloc;
int main()
{
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(1);
ListNode* node3 = new ListNode(1);
delete node1;
delete node3;
return 0;
}
很容易看出存在一个内存泄漏
ps:(其实更官方一些的检查内存泄漏的并不是这样一个个数,而是用一个数据结构,在operator new的时候把地址存进来
operator delete的时候把它删除,最后进行查找,剩下的就是没被释放的)
4. new/delete的实现原理
① 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申
请空间失败时会抛异常,malloc会返回NULL
② 自定义类型
- 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来释
放空间
5. 定位new表达式(Replace new)(了解)
我们知道,构造函数的调用时机常见的就是:
- 直接创建对象的时候,自动调用构造函数初始化
- new对象的时候,自动调用构造函数
但是上面两种方式都是在开空间创建对象的时候调用构造,我们是不可以显示调用构造函数的(可以显示调用析构函数)
并且也不可以访问成员变量直接初始化
那么如何对已经分配好空间的对象调用构造函数来初始化呢? 这就是定位new的作用
**定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象 **
但是析构的话需要自己显示调用
使用格式:
new (place_address) type或者new (place_address) type(initializer-list) **place_address必须是一个指针,initializer-list是类型的初始化列表 **
int main()
{
A* p2 = (A*)malloc(sizeof(A));
if (p2 == NULL)
{
perror("malloc fail");
}
new(p2)A(10);
p2->~A();
free(p2);
return 0;
}
? 使用场景
.相比new来说,new即开了空间还自动初始化,为啥还要有定位new呢?
这里就还是因为存在 内存池 的应用场景
如果我们去内存池申请空间,那么内存池只是开空间,并不会调用构造函数初始化
所以我们要对开好的空间进行初始化就必须使用定位new
|