分配 | 释放 | 类别 | 是否可以重载 |
malloc | free | C | 否 |
new | delete | C++ 表达式(expressions) | 否 |
operator new() | operator delete() | c++ 函数 | 是 |
operator new[] | operator delete[] | c++ 函数(用于数组) | 是 |
allocator |
allocator |
c++ 标准库 | 可以自由设计,并应用于各容器 |
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 是类中可以重载的两个函数,两个函数是 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 分别发生了什么。
现在有这样一个 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;
}
输出结果如下:
总结一下:
2.1.1 调用 operator new 函数分配目标类型大小的内存空间,而 operator new 函数内存实际上调用的是 malloc 函数分配的内存
2.1.2 将申请到的内存块,由 void* 指针类型 强制转换为目标类型的指针
2.1.3 通过指针调用目标类的构造函数(只有编译器可以只有直接调用构造函数)
2.2.1 通过指针调用目标类的析构函数
2.2.2 调用 operator delete 释放对象内存,而 operator delete 内部实际是通过 free 函数释放分配的内存
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 分别发生了什么。
那么在执行 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;
}
输出:
从输出结果的构造函数的地址来看,地址是依次递增的,而执行析构函数时,地址正好是反回来的,说明是构造对象执行构造函数的顺序,与执行对象的析构函数的顺序是反过来的。
验证程序中在事先分配好的内存上,调用 placement new ,在已有的内存上构造对象。
总结:
1.1 调用 operator new[] 函数分配目标类型大小的内存空间,而 operator new[] 函数内存实际上调用的是 malloc 函数分配的内存, 如:想要分配 size 个 Demo 类对象大小的内存,那么内存大小最终为 size * sizeof(Demo)。
1.2 将申请到的内存块,由 void* 指针类型 强制转换为目标类型的指针
1.3 通过指针依次调用 size 个目标类的构造函数(只有编译器可以只有直接调用构造函数)
调用顺序下标:从 0 , 1, ..., size - 1
2.1 通过指针依次调用 size 个目标类的析构函数,与构造函数的调用顺序正好相反,调用顺序下标:从 size - 1 , ..., 1 , 0
2.2 调用 operator delete 释放对象内存,而 operator delete 内部实际是通过 free 函数释放分配的内存
若是 c++ 中的基本类型分配的话,这样是没问题的,比如new 一个 int 数组,其内存分配情况如下:
若是用户自定义的类型,new[] 分配的内存,若是用 delete 可能在运行时会发生不可预知的错误,即便不发生错误,也会出现内存泄漏的问题。
下图中用户自定义的 Demo class ,定义如下:
class Demo
{
public:
Demo(){ }
~Demo(){ }
private:
int a;
int b;
int c;
};
下面用程序来证实一下:
// 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;
}
输出如下:
通过输出可以看出,new[] 返回地址与delete[]接收的地址是不一致的,delete[] 接收的地址比new[] 返回的地址要小, 把delete[] 接收地址上存储的值打印出来,发现是 3 与数组大小是一致的,也就是说用delete[] free 的地址是从存储数组大小的地方开始的。这个也比较容易理解,毕竟free内存的时候,还是需要知道需要内存的大小,不然对于 delete[] 来说,就给我个地址,如何知道之前给多少个元素分配的内存空间?
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;
}
输出:
::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
当 operator new 没有能力分配出我们所要求的内存大小时,一般的编译器会抛出 std::bad_alloc_exception 的异常出来。
然而,再抛出异常之前,会先调用 一个可以由我们用户指定的handler,做一下最后的挣扎,相当于把这个问题抛给用户去解决。
以下是 new handler 的形式与设定方法:
typedef void (*new_handler)(); // 定义函数指针
new_handler set_new_handler(new_handler p) throw;
良好的 new handler 的设计有两种:
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;
}
输出如下: