参考博客:https://blog.csdn.net/zhong29/article/details/80930919
malloc的实现方式
malloc函数将内存空间中可用的内存块连接成一个很长的空闲链表。当调用malloc函数的时候,就会在链表中进行遍历,直到找到满足用户申请空间大小的内存块,然后就从链表中把这段内存拿出来分配给用户,剩下的接回原空闲链表。如果无法获取符合大小的内存块的时候,就会放回NULL指针,因此如果用malloc申请空间就一定要进行返回值判断。
同时用malloc返回的指针是对齐的,为了能够使得void*适用于任何对象。大多数实现所分配的空间都会比申请空间来的大一些,额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针。
当用户调用free函数的时候就把申请的内存块重新接到空闲链表上。
这里提一下realloc,realloc函数可以增减之前分配的空间的长度,如果发现当前的存储区域的高位地址上没有足够的空间了,就会分配一个新的足够大的存储区域,将现有的内容复制过去,同时释放原有存储区域的内容。这里要注意的是,因为realloc可能会移动位置,如果用指针指向该地址,就有可能使得指针无效
new的实现
先看下new的源码
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
p = (void *) malloc (sz);
while (p == 0)
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
p = (void *) malloc (sz);
}
return p;
}
从源码可以看出实际上new也只是申请了内存空间。这里涉及到一个new_handler,他的作用就是反复申请内存空间,如果这个new_handler做不了这个工作,那么就换新的new_handler,如果没有new_handler能做这个工作就把一个空指针传给handler,就可以实现异常的抛出。
总结下new_handler的工作:
- Make more memory available,使得下一次内存分配能够成功,如删除其他无用的内存,使得系统有更多可用的内存。
- Install a different new_handler,如果当前的new_handler分配不了这么多内存,或者他知道另一new_handler能完成,则可以调用set_new_handler来设置另一个new_handler
- Deinstall the new_handler,就是如果没有能分配这么大内存的handler,就会把空指针返回。这样就可以抛出异常
- Throw an exception,类型为bad_alloc的异常不会被operator new捕获,会被传播到内存请求的地方。
那么new的过程也可以理解了,首先调用operator new函数申请一片空间,然后改变指针类型从void* -> 某个指针类型,再进行构造函数的调用
pt=static_cast(operator new(sizeof(T)));
pt->T();
new和malloc的异同
- new和malloc都是在堆上开辟内存。malloc只开辟了空间,没有初始化,而new不仅开辟了空间还进行了初始化。
- malloc需要传入初始化的字节数,指定开辟空间的大小,而new就不用了,new的工作方式是就是上面代码中显示的,按照对象的大小获取字节数,然后开辟空间大小。
- malloc失败返回NULL,new失败返回的是bad_alloc的异常,因此new对象需要异常捕获来判断(其实可以不用异常捕获,但是判断是一定需要的)
- malloc用free来释放,new用delete来释放,new[]—delete[]
- malloc只有一种开辟内存的方式,而new分为普通new,nothrow的new,const new 和定位new
扩展知识1
- nothrow new
#include<new>
Manager* pManager = new (std::nothrow) Manager();
if(NULL==pManager)
{
return false;
}
//或者设置set_new_handle 自定义异常抛出函数
#include <new>
#include <iostream>
#include <stdlib.h>
using namespace std;
void __cdecl newhandler()
{
return;
}
int main()
{
set_new_handler (newhandler);
Manager * pManager = new (std::nothrow) Manager();
if(NULL == pManager)
{
//记录日志
return false;
}
}
- const new
暂不整理,
了解下常量指针和指针常量的区别
常量指针:const int *p=&a; 常量指针,这是一个指针,不过指向的内容是常量。const修饰数据类型,地址可以改,但是内容不可改int a=1,c=2; const int *b=&a; b=&c;//对的 *b=2;//报错
指针常量:int * const p=&a; 指针常量,这是一个常量,就是指针本身是常量。因此地址不可以改,但是内容可以改。
int a=1,c=2; int * const b=&a; b=&c;//错 *b=2;//对
- placement new
就是在指定的位置上来实现对象。
// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
从源码来看placement new没有申请新的空间,直接返回了第二个参数的指针。
那么placement new的过程应该是
T p=new(alreadyExistPoint) T()
//分解步骤如下
void* p1=::operator new(sizeof(T),alreadyExistPoint);
T* p=static_cast<T*> p1;
扩展知识2
存储空间的布局
C程序一直由几个部分组成
- 正文段。这里是CPU执行的机器指令部分。通常正文段是可共享的,只读的。
- 初始化数据段。 包含了程序中需明确的赋予初值的变量,例如除了函数外的声明 int maxcount=999;
- 未初始化数据段。 在程序开始执行之前,内核将此段的数据赋予初值或者空指针。
- 栈。 自动变量和每次函数调用时所需要保存的信息都存放在此段中。每次函数调用时,其返回地址以及调用者的环境信息都存放在栈中。 对于递归函数,每次自身被调用时,就会产生一个栈帧,因此函数之间的变量集就不会互相干扰。
- 堆。 通常在堆中进行动态存储的分配。
以上摘自UNIX环境高级编程
从上面的描述中我们可以知道栈是由编译器自动分配和释放的。而堆是由程序员来操作的。
如何只在堆上或者只在栈上创建对象呢。
简单说下,只在堆上就是只能通过new的方式来动态分配空间,因此就要想办法禁用在编译期间就创建对象的方式如 A a(args); 禁用的办法就是讲构造析构函数设为protected函数,那么编译器就不会在编译期间开辟栈空间给他。
class A{
protected:
A(){};
~A(){};
public:
static A* create(){
return new A();
}
void destory(){
delete this;//直接删除传入的this对象,成员函数能调用protected对象
}
};
接下来就是只在栈上生成对象,很简单就把operator new和operator delete禁用掉即可
class A{
private:
void* operator new(size_t){};
void operator delete(void* ptr){};
public:
A(){};
~A(){};
};