C++ 中的内存分配 -- new 与 delete

c++ 常用的内存分配
分配 释放 类别 是否可以重载
malloc free C
new delete C++ 表达式(expressions)
operator new() operator delete() c++ 函数
operator new[] operator delete[] c++ 函数(用于数组)
allocator::allocate allocator::deallocate c++ 标准库 可以自由设计,并应用于各容器

一   malloc 与 free

     malloc 与 free 是 C语言中用于分配内存与释放内存的两个函数,两者配对使用。

malloc 函数的函数原型为:void* malloc(unsigned int size),它根据参数指定的尺寸来分配内存块,并且返回一个void型指针,指向新分配的内存块的初始位置。如果内存分配失败(内存不足),则函数返回NULL。

free 函数原型为:void free (void* ptr),用于将 malloc 分配的内存释放掉

#include
#include

int main()
{
     int SIZE = 10;
    // 分配三个 int 内存的空间
    int* ptr = (int*)malloc(sizeof (int) * SIZE);

    if(ptr == NULL)
    {
        printf("failed allocate. \n");
        exit(1);
    }

    // 赋值
    for(int i = 0; i < SIZE; i++)
    {
        ptr[i] = i;
    }

    // 打印
    for(int i = 0; i < SIZE; i++)
    {
        printf("%d  ", ptr[i]);
    }

    // 释放内存
    free(ptr);

    return 0;
}

这里有一个问题:

      我们调用 malloc 后,只返回了一个指针,那么 free 函数如何知道 malloc 分配了多大的内存, free 以指针为起点释放掉多大的内存呢?

      为了解决这个问题,内存管理提供了 cookie 机制。实际上,malloc分配的内存会在内存的起始与结尾带上有 cookie。

参考:

动态内存分配(malloc)详解-CSDN博客

C++ 中malloc函数详解(转载)_c++中void* __cdecl-CSDN博客

浅谈malloc()与free() - 知乎 (zhihu.com)

malloc和free的实现原理解析 - 知乎 (zhihu.com)

C++内存管理(malloc和free中的cookie) - 知乎 (zhihu.com)

二  operator  new 与 operator  delete

1.  operator new 与 operator delete 成员函数介绍

operator new 与 operator delete 是类中可以重载的两个函数,两个函数是 static 函数,但是重载时并不需要我们显示的指定 static,因为operator new 与 operator delete 是 static 在c++中是一个共识,即便不加 static 修饰, 编译器也会将其视为 static函数。

  当我们使用 这样的表达式

A* ptr = new A; // new expression
delete ptr; // delete expression

 new expression 会分别先去调用类的 operator new 成员函数申请内存,delete expression 会去调用类的 operator delete 成员函数释放内存,两个函数均可以被重载,下面我们看看 new expression 与 delete expression 分别发生了什么。

2. operator new 与 operator delete 成员函数重载测试

现在有这样一个 class Complex:

#include

class Complex
{
public:
    Complex():x(0),y(0)
    {
        std::cout << "Complex consturctor." << std::endl;
    }

    ~Complex()
    {
        std::cout << "Complex desturctor." << std::endl;
    }

    // 用于给单个对象分配内存
    void* operator new(std::size_t size)
    {
        std::cout << "Foo operator new size:  " << size << std::endl;
        return malloc(size);
    }
    void operator  delete(void* ptr)
    {
        std::cout << "Foo operator delete " << std::endl;
        return free(ptr);
    }

    // 用于给一组对象分配内存
    void* operator new[](std::size_t size)
    {
        std::cout << "Foo operator new[] size:  " << size << std::endl;
        return malloc(size);
    }
    void  operator delete[](void* ptr)
    {
        std::cout << "Foo operator delete[] " << std::endl;
        return free(ptr);
    }

    // 标准库提供的 placement new 的重载形式
    void* operator new(std::size_t size, void* ptr)
    {
        std::cout << "Foo operator operator new(std::size_t size, void* ptr), size: " << size << std::endl;
        return ptr;
    }
    void  operator delete(void* ptr1, void* ptr2)
    {
        std::cout << "Foo::operator delete(void*, void*)."<< std::endl;
    }

private:
    int x;
    int y;
};

那么在执行 new 与 delete 两个 expression 时,内部发生了什么呢?(面试常问)

// new expression
Complex* ptr = new Complex;

// 在编译器中等同于下面大括号内
{

Complex* ptr;

try {
  void*  mem =  operator new(size);  // 1. 分配内存
  ptr = static_case(mem);  // 2. 指针类型转换
  ptr->Complex::Complex();           // 3. 执行构造函数,只有编译器可以这样使用
} 
cache(std::bad_alloc)
  // 若是分配内存阶段出错,则不再执行 构造函数
}

// delete expression
delete ptr;
// 在编译器中等同于下面大括号内
{
   ptr->~Complex();    // 1. 执行析构函数
   operator delete(ptr);        // 2. 释放内存
}

程序验证如下:

#include"complex.h"

int main()
{
    // 输出 class 大小
    std::cout << "sizeof(Complex): " << sizeof(Complex) << std::endl;
    // 1. 单个对象
    Complex* comPtr = new Complex;

    delete comPtr;
    return 0;
}

输出结果如下:

C++ 中的内存分配 -- new 与 delete_第1张图片

总结一下:

  2.1 new expression 申请内存的步骤

   2.1.1  调用 operator new 函数分配目标类型大小的内存空间,而 operator new 函数内存实际上调用的是 malloc 函数分配的内存

   2.1.2  将申请到的内存块,由 void* 指针类型 强制转换为目标类型的指针

   2.1.3  通过指针调用目标类的构造函数(只有编译器可以只有直接调用构造函数) 

  2.2. delete expression 释放内存的步骤

   2.2.1 通过指针调用目标类的析构函数

   2.2.2  调用 operator delete 释放对象内存,而 operator delete 内部实际是通过 free 函数释放分配的内存

三  operator new[] 与 operator  delete[]

1.  operator new[] 与 operator delete[] 成员函数介绍

operator new[] 与 operator delete[] 是类中可以重载的两个函数,与前面的类似,这两个函数也是 static 函数,重载时,同样不需要显示指定 static。

当我们使用 这样的表达式

A* ptr = new A[3]; // new expression
delete ptr[]; // delete expression

new[] expression 会分别先去调用类的 operator new[] 成员函数申请内存,delete[] expression 会去调用类的 operator delete[] 成员函数释放内存,两个函数均可以被重载,下面我们看看 new[] expression 与 delete[] expression 分别发生了什么。

2. operator new[] 与 operator delete[] 成员函数重载测试

那么在执行 new[ ] 与 delete 两个 expression[ ] 时,内部发生了什么呢?(面试常问)

// new[ ] expression
Complex* ptr = new Complex[3];

// 等同于下面大括号内
{
Complex* ptr;

try {
  void*  mem =  operator new[](size * sizeof(Complex)); // 1. 分配内存
  ptr = (Complex*)mem;                                  // 2. 指针类型转换
  ptr->Complex::Complex();                              // 3. 执行 3 次构造函数,从下标 i = 0, 1, 2 依次执行构造函数,只有编译器可以这样使用                
} 
cache(std::bad_alloc)
  // 若是分配内存阶段出错,则不再执行 构造函数
}

// delete[ ] expression
delete ptr[];
// 等同于下面大括号内
{  
   ptr->~Complex();               // 1. 执行 3 次析构函数,从下标 i = 2, 1, 0 依次执行析构函数
   operator delete[](ptr);        // 2. 释放内存
}

下面来验证一下内部发生的过程,先定义一下 class Complex

#include

class Complex
{
public:
    Complex():x(0),y(0)
    {
        std::cout << "Complex default consturctor. this = " << this << std::endl;
    }

    Complex(int x, int y):x(x),y(y)
    {
        std::cout << "Complex consturctor. this = "<< this <<" x: "<< x <<", y: "<< y << std::endl;
    }

    ~Complex()
    {
        std::cout << "Complex desturctor. this = "<< this <<" x: "<< x <<", y: "<< y << std::endl;
    }

    // 用于给单个对象分配内存
    void* operator new(std::size_t size)
    {
        std::cout << "Foo operator new size:  " << size << std::endl;
        return malloc(size);
    }
    void operator  delete(void* ptr)
    {
        std::cout << "Foo operator delete " << std::endl;
        return free(ptr);
    }

    // 用于给一组对象分配内存
    void* operator new[](std::size_t size)
    {
        std::cout << "Foo operator new[] size:  " << size << std::endl;
        return malloc(size);
    }
    void  operator delete[](void* ptr)
    {
        std::cout << "Foo operator delete[] " << std::endl;
        return free(ptr);
    }

    // 标准库提供的 placement new 的重载形式
    void* operator new(std::size_t size, void* ptr)
    {
        std::cout << "Foo operator operator new(std::size_t size, void* ptr), size: " << size << std::endl;
        return ptr;
    }
    void  operator delete(void* ptr1, void* ptr2)
    {
        std::cout << "Foo::operator delete(void*, void*)."<< std::endl;
    }

private:
    int x;
    int y;
};

验证程序:

#include"complex.h"

int main()
{
    // 输出 class 大小
    std::cout << "sizeof(Complex): " << sizeof(Complex) << std::endl;
    // 1. 单个对象
    std::cout << "---new[] expression---" << std::endl;
    Complex* comPtr = new Complex[3];

    Complex* tmpPtr = comPtr;
    // placement new
    for(int i = 0; i < 3; i++)
    {
        new(tmpPtr++)Complex(i, i);
    }

    std::cout << "---delete[] expression---" << std::endl;
    delete[] comPtr;
    
    return 0;
}

输出:

C++ 中的内存分配 -- new 与 delete_第2张图片

从输出结果的构造函数的地址来看,地址是依次递增的,而执行析构函数时,地址正好是反回来的,说明是构造对象执行构造函数的顺序,与执行对象的析构函数的顺序是反过来的。

验证程序中在事先分配好的内存上,调用 placement new ,在已有的内存上构造对象。

总结

1. new[] expression 申请内存的步骤

   1.1  调用 operator new[] 函数分配目标类型大小的内存空间,而 operator new[] 函数内存实际上调用的是 malloc 函数分配的内存, 如:想要分配 size 个 Demo 类对象大小的内存,那么内存大小最终为 size * sizeof(Demo)。

   1.2  将申请到的内存块,由 void* 指针类型 强制转换为目标类型的指针

   1.3  通过指针依次调用 size 个目标类的构造函数(只有编译器可以只有直接调用构造函数) 

          调用顺序下标:从 0 , 1, ..., size - 1

  2. delete[] expression 释放内存的步骤

   2.1 通过指针依次调用 size 个目标类的析构函数,与构造函数的调用顺序正好相反,调用顺序下标:从 size - 1 , ..., 1 , 0

   2.2  调用 operator delete 释放对象内存,而 operator delete 内部实际是通过 free 函数释放分配的内存

3. 常见问题

   3.1    new[] 分配的内存如果用   delete 回收会出现什么问题?

     若是 c++ 中的基本类型分配的话,这样是没问题的,比如new 一个 int 数组,其内存分配情况如下:

C++ 中的内存分配 -- new 与 delete_第3张图片

 若是用户自定义的类型,new[] 分配的内存,若是用 delete 可能在运行时会发生不可预知的错误,即便不发生错误,也会出现内存泄漏的问题。

下图中用户自定义的 Demo class ,定义如下:

class Demo
{
public:
   Demo(){ }
  ~Demo(){ }
private:
   int a;
   int b;
   int c;
};

C++ 中的内存分配 -- new 与 delete_第4张图片

下面用程序来证实一下:

// complex.h
#include

class Complex
{
public:
    Complex():x(0),y(0)
    {
        std::cout << "Complex default consturctor. this = " << this << std::endl;
    }

    Complex(int x, int y):x(x),y(y)
    {
        std::cout << "Complex consturctor. this = "<< this <<" x: "<< x <<", y: "<< y << std::endl;
    }

    ~Complex()
    {
        std::cout << "Complex desturctor. this = "<< this <<" x: "<< x <<", y: "<< y << std::endl;
    }

    // 用于给一组对象分配内存
    void* operator new[](std::size_t size)
    {
        std::cout << "Foo operator new[] size:  " << size << std::endl;
        return malloc(size);
    }
    void  operator delete[](void* ptr)
    {
        std::cout << "Foo operator delete[] ptr: " << ptr << std::endl;
        std::cout << "Foo operator delete[] *ptr: " << *(long long*)ptr << std::endl;

        return free(ptr);
    }
   
private:
    int x;
    int y;
};

// main.cpp
#include"complex.h"

int main()
{
      // 输出 class 大小
    std::cout << "sizeof(Complex): " << sizeof(Complex) << std::endl;
    // 1. 单个对象
    std::cout << "---new[] expression---" << std::endl;
    Complex* comPtr = new Complex[3];
    std::cout << "comPtr: " << comPtr << std::endl;

    std::cout << "---delete[] expression---" << std::endl;
    delete[] comPtr;
    return 0;
}

输出如下:

C++ 中的内存分配 -- new 与 delete_第5张图片

      通过输出可以看出,new[] 返回地址与delete[]接收的地址是不一致的,delete[] 接收的地址比new[] 返回的地址要小, 把delete[] 接收地址上存储的值打印出来,发现是 3 与数组大小是一致的,也就是说用delete[] free 的地址是从存储数组大小的地方开始的。这个也比较容易理解,毕竟free内存的时候,还是需要知道需要内存的大小,不然对于 delete[] 来说,就给我个地址,如何知道之前给多少个元素分配的内存空间?

四  placement new

placement new 允许我们将对象构建与已分配好的内存上,不需要开辟新的内存。没有所谓的 placment delete ,因为压根也没有专门为 placement 分配过内存。

那 placement new 一般用在什么场合呢?

答:需要频繁创建销毁对象的场合,比如server端处理来自用户得请求,处理完以后,已经申请得内存就可以留着,给下次发起请求的用户使用

char* buf = new char[sizeof (Complex)]; // 1. 分配内存

Complex* ptr = new(buf)Complex(0,0);    // 2. 在已分配的内存上构造对象 

delete [] buf;                          // 3. 释放内存

// 上面的 2 步在编译器中等同于下面
Complex* ptr;
try
{
   void*  ptr = operator new(sizeof(Complex), buf); // 1. 实际上就是将char* 指针转为 void*
   ptr = static_cast(ptr);                // 2. 将 void* 指针强转为 Complex*
   ptr->Complex::Complex();                         // 3. 利用 Complex* 指针调用构造函数
}
cache(std::bad_alloc)
{
   // 若 allocate 分配失败,则 不再执行构造函数
}

 验证程序如下:

#include"complex.h"

int main()
{
    char* buf = new char[sizeof (Complex)];

    Complex* ptr = new(buf)Complex(0,0);

    delete [] buf;

    // 手动 malloc 分配内存空间,利用 placement new 函数在已分配空间上构造新对象
    Complex* ptr2 = (Complex*)malloc(sizeof (Complex));
    new(ptr2)Complex(1,2);

    ptr2->~Complex();
    free(ptr2);

    return 0;
}

输出:

C++ 中的内存分配 -- new 与 delete_第6张图片

五  ::operator  new 与 ::operator delete

::operator  new 与 ::operator delete 是 global 函数,在用 new expression 生成对象与delete expression 销毁对象时,调用顺序是:一般先调用 类的成员函数 前面 2 中介绍的 operator new 与 operator delete ,若是没有就会再去调用 global 的函数 ::operator  new 与 ::operator delete。

2 中的成员函数重载比较常见,global 的  ::operator  new 与 ::operator delete 重载不常见。

下面代码强制调用 global 的 

#include"complex.h"

int main()
{
  Complex* p1 = ::new Complex(1, 2); // 调用 ::operator new

  ::delete p1; // ::operator delete
  return 0;
}

输出可以看到,只有 constructor 与 destructor 的打印,未调用类的成员函数 operator new 与 operator delete

六  new handler

当 operator new 没有能力分配出我们所要求的内存大小时,一般的编译器会抛出 std::bad_alloc_exception 的异常出来。

然而,再抛出异常之前,会先调用 一个可以由我们用户指定的handler,做一下最后的挣扎,相当于把这个问题抛给用户去解决。

以下是 new handler 的形式与设定方法:

typedef void (*new_handler)(); // 定义函数指针
new_handler  set_new_handler(new_handler p) throw;

良好的 new handler 的设计有两种:

  • 让更多的 memory可用
  • 调用 abort() 或 exit()

new_handler 例子如下:

#include
#include

using namespace std;

void noMemoryMore()
{
    cout << "My new handler out of memory. " << endl;
    abort();
}

int main()
{
    set_new_handler(noMemoryMore);

    int* p = new int[100000000000000000L];
    return 0;
}

输出如下:

你可能感兴趣的:(c++,开发语言)