C++关键字new的原理

一、问题

有了 malloc/free 为什么还要 new/delete?

二、解析

malloc 与 free 是 C++/C 语言的标准库函数, new/delete 是 C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,光用 maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。

因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符 new,以及一个能完成清理与释放内存工作的运算符 delete。注意 new/delete 不是库函数。

三、new的底层实现

阅读STL源码,空间配置器(allocate)的实现

template <class T>
inline T* allocate(ptrdiff_t size, T*) {
    set_new_handler(0); // 设置内存申请失败的回调函数
    T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
    if (tmp == 0) { // 申请内存失败
        cerr < "out of memory" << endl;
        exit(1):
    }
    return tmp;
}
template <class T>
inline void deallocate(T*buffer) {
    ::operator delete(buffer);
}

或许你没有听过 operator new(),只听过 new()。其实 new() 底层是调用 operator new() 来实现的。而operator new() 其实还是调用 malloc()。下面展示的是 VC 下的 operator new() 的源码,可以很清楚的看到它其实也是调用 malloc() 进行内存分配的。

void *operator new(size_t size, const std::nothrow_t&)
_THROW0()
{   // try to allocate size bytes
    void *p;
    while ((p = malloc(size)) == 0)
    {
        // buy more memory or return null pointer
        _TRY_BEGIN
            if (_callnewh(size) == 0) break;
        _CATCH(std:bad_alloc) return (0);
        _CATCH_END
    }
    return (p);
}

 分析:调用malloc失败后会调用_callnewh。如果_callnewh返回0则抛出bac_alloc异常,返回非零则继续分配内存。这个_callnewh是什么呢?它是一个new handler,通俗来讲就是new失败的时候调用的回调函数,可以通过_set_new_handler来设置。 

下面是一个简单的示例程序,new操作分配内存失败时将调用no_memory函数:

Typedef void  (*new_handler)(); // 定义函数指针

new_handler  set_new_handler(new_handler  new_p) throw();//C++98

new_handler  set_new_handler (new_handler  new_p) noexcept;//C++11

// new_handler example
#include <iostream>     // std::cout
#include <cstdlib>      // std::exit
#include <new>          // std::set_new_handler
 
// 自定义处理函数
void no_memory () {
  std::cout << "Failed to allocate memory!\n";
  std::exit (1);
}
 
int main () {
  std::set_new_handler(no_memory); // new失败后,进行相应的处理
  std::cout << "Attempting to allocate 1 GiB...";
  char* p = new char [1024*1024*1024];
  std::cout << "Ok\n";
  delete[] p;
  return 0;
}

既然allocator 最终的都是调用 malloc() 来分配大小,为什么又弄个operator new? alloc 最主要的作用就是要尽量减少 malloc() 的次数,而 malloc() 的次数越多所带来的额外开销就越大,会形成更多的内存碎片。 

四、new和operator new之间的关系

其实, 当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:

1、分配一块内存空间

2、调用构造函数生成类对象

3、返回正确的指针。

当然,如果我们创建的是简单类型的变量,那么第二步会被省略。

事实上,分配内存这一操作就是由operator new(size_t)来完成的,如果类A重载了operator new,那么将调用A::operator new(size_t ),如果没有重载,就调用::operator new(size_t ),全局new操作符由C++默认提供。因此前面的两步也就是:

1.调用operator new

2.调用构造函数。

举个例子:

class A
{
private:int m_temp;
public:
   A(int  temp) :m_temp(temp){}
   void Say()  { printf("i=%d/n", i); }
};

A* pa = new A(3);//调用new:
那么上述动态创建一个对象的过程大致相当于以下三句话:

A* pa = (A*)malloc(sizeof(A));

pa->A::A(3);

return pa;


虽然从效果上看,这三句话也得到了一个有效的指向堆上的A对象的指针pa,但区别在于,当malloc失败时,它不会调用分配内存失败处理程序new_handler,而使用new的话会的。因此我们还是要尽可能的使用new,除非有一些特殊的需求。

同理,delete操作也是两个步骤

1、调用析构函数~A();

2、::operator delete 将内存释放

五、重载operator new

因为new是关键字,我们本应该无法修改new分配内存的方式。由于new在分配内存时,调用operator new。所以重载operator new就可以修改分配内存的方式了。

class Foo {
public:
    void* operator new(std::size_t size)
    {
        std::cout << "operator new" << std::endl;
        return std::malloc(size);
    }
};

int main()
{
    Foo* m = new Foo;
    std::cout << sizeof(m) << std::endl;
    delete m;
    return 0;
}

operator new返回值必须是void*。第一个参数必须是size_t,还可加其它参数,下文讨论。

operator new重载可以放在全局中,也可以放到类内部。当编译器发现有new关键字,就会现在类和其基类中寻找operator new,找不到就在全局中找,再找不到就用默认的。

在类中的operator new默认就是static。所以加static可以,不加也是全局,可以正常使用。

六、operator new 加入其它形参

上文说到operator new第一个参数必须是size_t并且可以重载加入其它形参。

这就引出placement new。原型是这样:

class Foo {
public:
    void* operator new(std::size_t size)
    {
        std::cout << "operator new" << std::endl;
        return std::malloc(size);
    }
    void* operator new(std::size_t size, void* ptr)
    {
        std::cout << "placement new" << std::endl;
        return ptr;
    }
};

int main()
{
    Foo* m = new Foo;
    Foo* m2 = new(m) Foo;
    std::cout << sizeof(m) << std::endl;
    // delete m2;
    delete m;
    return 0;
}

 

 placement new就是operator new重载的一种形式。上文说到,operator new的主要作用就是分配空间,初始化对象的工作是new关键字的。

经过这样重载,placement new完全没有分配新的空间,而是把ptr的地址传了出去。由于调用构造函数的工作是new关键字,所以placement new不影响对象初始化。

placement new由于不需要申请新内存和释放旧内存。所以在内存池中,意外的便捷。

七、重载operator delete

void operator delete(void* ptr)
{
     std::cout << "operator delete" << std::endl;
     std::free(ptr);
}

deletenew类似,只不过返回值必须为void。 

参考:

C++ 内存分配(new,operator new)详解 - 王小北 - 博客园

C++ 内存管理之重载operator new 和operator delete_FBI-PC的博客-CSDN博客_operator delete

详解C++重载new, delete - 知乎

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值