C/C++ 内存申请和释放
1. malloc & free
malloc
基本用法
-
malloc() 是程序员在堆区申请空间的函数,原型为 void *malloc(size_t size); size 是要在堆区申请的字节数,它的返回值是申请空间的首地址,它申请的空间是连续的
-
size_t 数据类型经常用到,在 32bit 编译器中是 unsigned int;在64 bit系统中是unsigned __int64
-
malloc(大于0); 如可用内存不足,返回空指针NULL,如可用内存足够,返回申请空间的首地址;malloc(0)返回一个有效的空间长度为零的内存首地址,但是没法用(只进行申请和释放可以,如申请后执行了写操作,释放时会报错);
// MSDN 推荐使用方法
// C++ 不支持 void* 向其他类型的隐式转换,需进行强制类型转换,
// C 支持 void* 向其他类型的隐式转换,可不进行强制类型转换
int* n = (int*)malloc(sizeof(int));
if (n == NULL) {
// 进行内存申请失败的错误处理
} else {
// 进行正常操作
free(n);
}
动态申请数组
申请一个有 6 个整形数组元素的一维数组,申请完不能初始化(代码如下),只能通过 memset() 或循环的方式赋值
int* p = (int*)malloc(sizeof(int) * 6);
因该操作程序运行到这条语句时才在堆区申请的数组,所以被称为动态申请内存(数组),栈区的数组在编译时就已经申请好内存了,所以不是动态申请的
动态申请数组指针
int (*p)[3] = (int(*)[3])malloc(sizeof(int) * 3); // 一维数组指针
int (*q)[2][3] = (int(*)[2][3])malloc(sizeof(int) * 6); // 二维数组指针
初始化
void* memset(void* dest, int c, size_t count);
-
此函数是按字节进行赋值的
-
dest 指向目标地址的指针;c 要赋值给内存空间的值;count 是字节数;返回值是被赋值的目标地址的指针
void* memcpy(void* dest, void* src, size_t count);
-
此函数也是按照字节进行拷贝的
-
dest 指向目标地址的指针,也就是要被赋值的空间首地址;src 指向源地址的指针,也就是要被复制的空间的首地址;count 跟 memset() 一样表示被拷贝的字节数;返回值也是被赋值的目标地址的指针
free
-
用 malloc() 申请一块内存空间,OS 会有一张表记录所申请空间的首地址和这块地址的长度
-
free(空间首地址),free 会从表中查找到这块首地址对应的内存大小,一并释放掉
int* p = (int*)malloc(4);
free(p);
p = NULL; // 释放后要置为空指针
int* q = (int*)malloc(3);
free(q); // 会报错,int 型指针一次操作 4Byte,这里只申请了 3Byte 相当去别人的地盘上拆东西,那肯定是不允许的
int* n = (int*)malloc(7); // 允许多申请,但是 intv型指针一次只能操作 4Byte 多余的空间浪费了
free(n); // 释放时,从 OS 维护的表中查找到空间长度,会一并释放掉
注意
-
free() 不能去释放栈区的空间,栈区空间是由 OS 管理的,由 OS 进行申请和释放
-
释放空间后,指针需要置空,避免成为野指针
int* p = (int*)malloc(sizeof(int));
if (p == NULL) { // p 是空指针
// 空间申请失败的错误处理
} else {
// 申请成功,假设 p == 0X00000191D34DDAB0;
free(p); // p == 0X00000191D34DDAB0;
// p有值,但是指向的内存空间已经被释放掉了,p 就成了一个野指针了
p = NULL; // 释放空间后,指针需要置空,避免成为野指针
}
int *p; //这种,定义完指针未初始化,也是野指针
2. new & delete
new
基本用法
int* p = new int; // 申请单个空间
int* q = new int[10]; // 申请连续空间
-
new 在申请基本类型空间时,主要会经历两个过程:
-
调用 operator new(size_t) 或 operator new[] (size_t) 申请空间
-
进行强制类型转换(代码如下)
// ====== 测试一:申请单个空间 ====== type* p = new type; // 执行上面这条语句实际的过程是下面的语句 void* tmp = operator new(sizeof(type)); // 调用 operator new(size_t) 申请空间 type* p = static_cast<type*>(tmp); // 进行强制类型转换 // ====== 测试二:申请数组空间 ====== type* q = new type[N]; // 执行上面这条语句实际的过程是下面的语句 void* tmp = operator[](sizeof(type) * N); // 调用 operator new[](size_t) 申请空间 type* p = static_cast<type*>(tmp); // 进行强制类型转换
-
-
new 在申请 object 空间时,主要会经历三个过程:
-
调用 operator new(size_t) 或 operator new[] (size_t)申请空间
-
进行强制类型转换
-
调用类的构造函数(代码如下)
// ====== 测试一:申请单个object ====== classname* p = new classname; // 执行上面的语句实际的过程是下面的条语句 // 调用 operator new(size_t) 申请空间 void* tmp = operator new(sizeof(classname)); // 进行强制类型转换 classname* p = static_cast<classname*>(tmp); // 调用类的构造函数,用户不允许这样调用构造函数, // 如果用户想调用可以通过 定位(placement) new 运算符 的方式调用 p->classname::classname(); // ====== 测试二:申请object数组空间 ====== classname* q = new classname[N]; // 执行上面的语句实际的过程是下面的条语句 // 调用 operator new[](size_t) 申请空间 void* tmp = operator new[](sizeof(classname) * N); // 进行强制类型转换 classname* q = static_cast<classname*>(tmp); // 调用 N 次 构造函数 q->classname::classname();
-
-
定位(placement) new 运算符
- 定位(placement) new 运算符 允许我们将 object 或者基本类型数据创建在已申请的内存中,并且 定位(placement) new 运算符 没有对应的 定位(placement) delete ,因为 定位 new 运算符 没有申请内存空间
- 定位(placement) new 实际上底层是调用了 operator new(size_t, void*); 我们也可以自定义 placement new 比如:operator new(size_t, long);
char* buf1 = new char[40]; char* buf2 = new char[sizeof(classname)]; int* q1 = new(buf1)int[5]; // ====== 底层调用 ====== // 调用 operator new(size_t size, void* start); 在给定的空间创建对象 void* tmp = operator new(sizeof(int) * 5, buf1); int* q1 = static_cast<int*>(tmp); // ====================== // 如果这样定义 buf1、q1、q2 起始地址是一样的,也是就是 q2 会覆盖掉 q1; int* q2 = new(buf1)int[5]; // 正确创建 q2 的方式 int* q2 = new(buf1 + sizeof(int) * 5)int[5]; classname* p = new(buf)classname; // 构造函数不需要传参 // ====== 底层调用 ====== // 调用 operator new(size_t size, void* start); 在给定的地址上创建对象 void* tmp = operator new(sizeof(classname), buf); // 进行强制类型转换 classname* p = static_cast<classname*>(tmp); // 调用构造函数,如果是申请object数组空间对调用多次 p->classname::classname(); // ====================== classname* p = new(buf)classname(type...); // 构造函数需要传参 // 自定义 placement new long s = 10; classname* c = new(s)classname; // ====== 底层调用 ====== // 调用 operator new(size_t size, long s); void* tmp = operator new(sizeof(classname), s); // 进行强制类型转换 classname* c = static_cast<classname*>(tmp); // 调用构造函数,如果是申请object数组空间对调用多次 c->classname::classname(); // ======================
new 的探究
-
operator new(size_t); 这个特殊的函数是允许重载的
-
operator new(size_t); 在类内重载,如在类内重载一定是 static 静态函数,因为类内的 operator new(size_t); 函数调用可能是在创建对象时,但是 C++ 中对于这个特殊函数的重载可以不加 static
-
operator new(size_t); 在类外重载,也就是全局的重载,这样做是有一定危险的,全局函数也就可以任意调用,影响比较大,不建议进行类外重载
-
重载时需要注意,函数的返回值为 void* ,第一个参数一定是 size_t
-
后面会出一篇文章专门讲一下 C++ 函数和运算符的重载,这里不再赘述
-
-
new 的底层是由 malloc 实现的
// 这是 VC\Tools\MSVC\14.16.27023\crt\src\vcruntime\new_scalar.cpp 中的代码
// 可以看到 new 的底层还是 malloc
_CRT_SECURITYCRITICAL_ATTRIBUTE
void* __CRTDECL operator new(size_t const size)
{
for (;;)
{
if (void* const block = malloc(size))
{
return block;
}
if (_callnewh(size) == 0)
{
if (size == SIZE_MAX)
{
__scrt_throw_std_bad_array_new_length();
}
else
{
__scrt_throw_std_bad_alloc();
}
}
// The new handler was successful; try to allocate again...
}
}
delete
基本用法
delete p;
delete[] q;
-
delete 的过程与 new 很相似,会调用 operator delete(void); 或 operator delete[] (void)释放内存
delete p; // 执行上面的代码实际过程是下面的语句 operator delete(p); // 调用 operator delete(void*); 释放空间 delete[] q; // 执行上面的代码实际过程是下面的语句 operator delete[](q); // 调用 operator delete[](q); 释放空间
-
delete 释放 object 空间
-
调用类的析构函数
-
调用 operator delete(void); 或 operator delete[] (void)释放内存
delete obj; // 执行上面的语句实际过程是下面的语句 obj->~classname(); // 首先调用类的析构函数 // 调用 operator delete(void*); 释放 object 内存空间 operator delete(obj); delete[] obj1; // 执行上面的语句实际过程是下面的语句 obj->~classname(); // 调用 N次 类的析构函数 // 调用 operator delete[](void*); 释放 object 内存空间 operator delete[](obj);
-
delete 的探究
-
delete 底层是由 free 实现的
// 这是 VC\Tools\MSVC\14.16.27023\crt\src\vcruntime\delete_scalar.cpp 中的代码, // 可以看到 delete 的底层还是 free _CRT_SECURITYCRITICAL_ATTRIBUTE void __CRTDECL operator delete(void* const block) noexcept { #ifdef _DEBUG _free_dbg(block, _UNKNOWN_BLOCK); #else free(block); #endif }
-
关于释放 object 数组
-
在 new 一个数组时,与 malloc 相似OS会维护一张记录数组头指针和数组长度的表
-
释放一般数据类型的指针时,数组的头指针最终会被 free(q); 释放,所以不论是 delete q; 或者 delete[] q; 最终的结果都是调用 free(q); 释放内存
-
释放 object 数组空间时,如果有空间需要在析构函数中释放,直接调用 delete obj; 只会调用一次析构函数,然后就执行 free(obj); 没有调用其他数组元素的析构函数很容易导致内存泄漏,所以在释放 object 数组时,一定要用 delete[] obj; 释放内存,总而言之,数组最好是用 delete[] 的方式释放,这里只是解释一下,为什么这么用
-
定位new 如果创建了 object ,因为没有对应的 定位delete ,所以需要程序员显式的调用类的析构函数
char* buf = new char[512]; // 定位new 运算符 在指定位置创建一个 object classname *obj = new(buf)classname; // 程序员需要显式调用类的析构函数 obj->~classname(); delete[] buf;
-
3. 其他申请内存的方式
- calloc
void* calloc(size_t num, size_t size);
申请连续的 num 块内存,每块内存的字节数为 size;并将这些字节置为初始化为 0,返回值为所申请空间的首地址,申请数组时比较方便,但是效率可能比 malloc() 会慢一点,因为多了一步初始化操作
- realloc
void* realloc(void* memblock, size_t size); // 为已分配的内存空间重新申请内存块
- memblock 指向之前已分配内存块的指针;size 新内存块大小(字节数);返回值是重新分配内存块的首地址
- 如果原来分配的内存块的地方无法再扩展到 size 要求的大小,那么会重新分配一块 size 大小的内存,原地址的内容会被拷贝过去,相应的返回值也会是新分配区域的首地址,如果可以扩展到指定大小,那返回值还会是重新分配前的返回值
- _msize
size_t _msize(void* memblock); // Windows平台下专用函数,非C语言标准函数
返回 malloc() & calloc() & realloc() 等申请内存块的大小,参数是分配内存块的首地址,也就是 malloc() & calloc() & realloc() 等的返回值
int* p = (int*)malloc(sizeof(int));
int pSize = _msize(p); // pSize == 4;
int* q = (int*)realloc(p, sizeof(int) * 2);
int qSize = _msize(q); // qSize == 8; p 和 q 可能相等也可能不相等