内存申请和释放

10k words

malloc&free

malloc(0)返回一个有效的空间长度为0的内存首地址,但是没法用(只能进行申请和释放).

动态申请数组指针

1
2
int (*P)[3]=(int(*)[3])=malloc(sizeof(int)*3);
int (*q)[2][3]=(int(*)[2][3])malloc(sizeof(int)*6);

初始化

malloc函数分配得到的内存是未初始化的.一般在使用该内存空间时,要调用memset来初始化为0.

1
void* memset(void *dest,int c,size_t count);

该函数可以将指定的内存空间按字节单位置为指定的字符c。其中,dest为要清零的内存空间的首地址,c为要设定的值,count为被操作的内存空间的字节长度。

1
void* memcpy(void* dest, void* src, size_t count);

此函数也是按照字节进行拷贝的,dest指向目标地址的指针,也就是要被赋值的空间首地址;src指向源地址的指针,也就是要被复制的空间的首地址;count跟memset()一样表示被拷贝的字节数;返回值也是被赋值的目标地址的指针。

其他申请方式

calloc

1
void* calloc(size_t num, size_t size); 

realloc

1
void* realloc(void* memblock, size_t size);  // 为已分配的内存空间重新申请内存块

_msize

1
size_t _msize(void* memblock);  // Windows平台下专用函数,非C语言标准函数

返回malloc() & calloc() & realloc()等申请内存块的大小,参数是分配内存块的首地址,也就是malloc() & calloc() & realloc()等的返回值。

free

malloc()申请一块内存空间,OS会有一张表记录所申请空间的首地址和这块地址的长度,free(空间首地址),free会从表中查找到这块首地址对应的内存大小,一并释放掉。

  1. free()不能去释放栈区的空间,栈区空间是由OS管理的,由OS进行申请和释放。
  2. 释放空间后,指针需要置空,避免成为野指针。
1
2
3
4
int* q = (int*)malloc(3);
free(q); // 会报错,int型指针一次操作 4Byte,这里只申请了 3Byte 相当去别人的地盘上拆东西,那肯定是不允许的
int* n = (int*)malloc(7); // 允许多申请,但是 int型指针一次只能操作 4Byte 多余的空间浪费了
free(n); // 释放时,从OS维护的表中查找到空间长度,会一并释放掉

new & delete

new

new 在申请基本类型空间时,主要会经历两个过程:

  1. 调用 operator new(size_t)operator new[] (size_t) 申请空间
  2. 进行强制类型转换
1
2
3
4
5
6
7
8
9
10
11
// ====== 申请单个空间 ======
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 new[](sizeof(type) * N); // 调用 operator new[](size_t) 申请空间
type* p = static_cast<type*>(tmp); // 进行强制类型转换

static_cast

是一种用于显示类型转换的强制转换运算符。是一个编译时运算符,用于在兼容类型之间执行转换.

1
2
3
static_cast<new_type>(expression)
int integerNumber = 42;
double doubleNumber = static_cast<double>(integerNumber);

new 在申请 object 空间时,主要会经历三个过程:

​ 1.调用 operator new(size_t)operator new[] (size_t) 申请空间

​ 2.进行强制类型转换

​ 3.调用类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// ====== 申请单个object ======
classname* p = new classname;
// 执行上面的语句实际的过程是下面的条语句
void* tmp = operator new(sizeof(classname)); // 调用 operator new(size_t) 申请空间
classname* p = static_cast<classname*>(tmp); // 进行强制类型转换
p->classname::classname(); // 调用类的构造函数,用户不允许这样调用构造函数,如果用户想调用可以通过 定位(placement)new 运算符 的方式调用

// ====== 申请object数组空间 ======
classname* q = new classname[N];
// 执行上面的语句实际的过程是下面的条语句
void* tmp = operator new[](sizeof(classname) * N + 4); // 调用 operator new[](size_t) 申请空间
classname* q = static_cast<classname*>(tmp + 4); // 进行强制类型转换
q->classname::classname(); // 调用 N次 构造函数

注意

申请的空间大小为

1
sizeof(classname)*N+4;

前4个字节写入数组大小,最后调用N次构造函数。

这里为什么要写入数组大小呢?释放内存之前会调用每个对象的析构函数。但是编译器并不知道 q 实际所指对象的大小。如果没有储存数组大小,编译器如何知道该把p所指的内存分为几次来调用析构函数呢?

new[] 调用的是operator new[],计算出数组总大小之后调用operator new。值得一提的是,可以通过()初始化数组为零值,实例:

1
char* p = new char[32]();

等同于:

1
2
char *p = new char[32];
memset(p, 32, 0);

new的底层

operator new(size_t) 是系统提供的全局函数,其 底层是由 malloc 实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试
执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)//是否注册了新的处理程序,没有则继续循环试图分配空间.
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}

delete

delete 的过程与 new 很相似,会调用 operator delete(void*)operator delete[] (void*) 释放内存。

1
2
3
4
5
6
7
delete p;
// 执行上面的代码实际过程是下面的语句
operator delete(p); // 调用 operator delete(void*); 释放空间

delete[] q;
// 执行上面的代码实际过程是下面的语句
operator delete[](q); // 调用 operator delete[](q); 释放空间

delete 释放 object 空间

  1. 调用类的析构函数
  2. 调用 operator delete(void*)operator delete[] (void*) 释放内存
1
2
3
4
5
6
7
8
9
delete obj;
// 执行上面的语句实际过程是下面的语句
obj->~classname(); // 首先调用类的析构函数
operator delete(obj); // 调用 operator delete(void*); 释放 object 内存空间

delete[] obj1;
// 执行上面的语句实际过程是下面的语句
obj->~classname(); // 调用 N次 类的析构函数
operator delete[](obj1); // 调用 operator delete[](void*); 释放 object 内存空间

new[]分配的内存只能由delete[]释放。如果由delete释放会崩溃,假设指针obj1指向new[]分配的内存,因为要4字节存储数组大小,实际分配的内存地址为obj1-4,系统记录的也是这个地址。delete[] 实际释放的就是obj1-4指向的内存。而delete会直接释放obj1指向的内存,这个内存根本没有被系统记录,所以会崩溃。

delete底层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;

RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));

if (pUserData == NULL)
return;

_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);

/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));

_free_dbg( pUserData, pHead->nBlockUse );

__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY

return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

总结

new一个数组时,与malloc相似,os会维护一张记录数组头指针和数组长度的表.

释放基本数据类型的指针时,数组的头指针最终会被free(q)释放,所以调用delete q或者delete[] q,最终的结果都是调用free(q);

释放自定义类型的数组时,如果类中有需要在析构函数中释放,直接调用delete obj只会调用一次析构函数,后执行free(),就没有调用其他的析构函数,会造成内存泄漏.一定调用delete[] obj释放内存

new 和 delete的重写和重载

这两个函数在使用时,其实执行的是全局的::operator new::operator delete,如果我们在类中重载了这两个函数,那么就会调用我们实现的,没有则使用全局的. 对于基本数据类型则使用全局的.

1
2
3
4
5
6
7
8
9
10
//全局运算符定义格式
void* operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);

//类内运算符定义格式
class CA
{
void* operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);
};

标准库提供的全局的 operator new( 函数 ) 有六种重载形式,operator delete也有六种重载形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// =======================new=========================//
void *operator new(std::size_t count)
throw(std::bad_alloc); // 一般的版本
void *operator new[](std::size_t count)
throw(std::bad_alloc);

void *operator new(std::size_t count, // 兼容早版本的 new
const std::nothrow_t const&) throw(); // 内存分配失败不会抛出异常
void *operator new[](std::size_t count,
const std::nothrow_t const&) throw();

void *operator new(std::size_t count, void *ptr) throw(); // placement 版本
void *operator new[](std::size_t count, void *ptr) throw();

// ======================delete========================//
void * operator delete(void *) noexcept;
void * operator delete[](void *) noexcept;

void * operator delete(void *, std::nothrow_t const&) noexcept;
void * operator delete[](void *, std::nothrow_t const&) noexcept;

void * operator delete(void *, void *) noexcept;
void * operator delete[](void *, void *) noexcept;

// VS C++下还有两个,GCC下不确定
void * operator delete(void *, size_t) noexcept;
void * operator delete[](void *, size_t) noexcept;

new/delete运算符重载规则:

  1. new和delete运算符重载必须成对出现。
  2. new运算符的第一个参数必须是size_t类型的,也就是指定分配内存的size尺寸;delete运算符的第一个参数必须是要销毁释放的内存对象。其他参数可以任意定义。
  3. 系统默认实现了new/delete、new[]/delete[]、 placement new 5个运算符(expression)。它们都有特定的意义。使用它们在底层调用对应的函数,可以是系统提供的,也可能是被重写或重载的。
  4. 你可以重写默认实现的全局运算符,比如你想对内存的分配策略进行自定义管理或者你想监测堆内存的分配情况或者你想做堆内存的内存泄露监控等。但是你重写的全局运算符一定要满足默认的规则定义。也可以重载全局运算符,但也必须符合默认的规则,即第一个参数不能变。
  5. 如果你想对某个类的堆内存分配的对象做特殊处理,那么你可以重载这个类的new/delete运算符。当重载这两个运算符时虽然没有带static属性,但是不管如何对类的new/delete运算符的重载总是被认为是静态成员函数。
  6. 当delete运算符的参数>=2个时,就需要自己负责对象析构函数的调用,并且以运算符函数的形式来调用delete运算符。
  7. 这里的重载遵循作用域覆盖原则,即在里向外寻找operator new的重载时,只要找到operator new()函数就不再向外查找,如果参数符合则通过,如果参数不符合则报错,而不管全局是否还有相匹配的函数原型。比如如果这里只将Foo中operator new(size_t, const std::nothrow_t&)删除掉,就会在61行报错:

对象的自动删除

一般来说系统对new/delete的默认实现就能满足我们的需求,我们不需要再去重载这两个运算符。那为什么C++还提供对这两个运算符的重载支持呢?答案还是在运算符本身具有的缺陷所致。我们知道用new关键字来创建堆内存对象是分为了2步:1.是堆内存分配,2.是对象构造函数的调用。而这两步中的任何一步都有可能会产生异常。如果说是在第一步出现了问题导致内存分配失败则不会调用构造函数,这是没有问题的。如果说是在第二步构造函数执行过程中出现了异常而导致无法正常构造完成,那么就应该要将第一步中所分配的堆内存进行销毁。C++中规定如果一个对象无法完全构造那么这个对象将是一个无效对象,也不会调用析构函数。为了保证对象的完整性,当通过new分配的堆内存对象在构造函数执行过程中出现异常时就会停止构造函数的执行并且自动调用对应的delete运算符来对已经分配的堆内存执行销毁处理,这就是所谓的对象的自动删除技术。正是因为有了对象的自动删除技术才能解决对象构造不完整时会造成内存泄露的问题。

全局delete运算符函数所支持的对象的自动删除技术虽然能解决对象本身的内存泄露问题,但是却不能解决对象构造函数内部的数据成员的内存分配泄露问题,此时我们需要将析构函数中需要释放的数据成员的内存在重载的operator delete函数中进行释放。

operator new运用技巧

  • 用于调试
  • 内存池优化
  • STL中的new

new和malloc的区别

内存的申请所在位置

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

特别的,new甚至可以不为对象分配内存!定位new的功能可以办到这一点:

1
new(place_address) type

place_address为一个指针,代表一块内存的地址。

返回类型安全性

new返回对象类型的指针,类型严格匹配

而malloc返回void *,需要通过强制类型转换将void *转换成我们需要的类型.

内存失败时的返回值

new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。

是否需要指定内存大小

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

是否调用构造函数/析构函数

使用new操作符来分配对象内存时会经历三个步骤:

  1. 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  2. 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
  3. 第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

  1. 第一步:调用对象的析构函数。
  2. 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

对数组的处理

C++提供了new[]与delete[]来专门处理数组类型,new[]分配的内存必须使用delete[]进行释放。

new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。

至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

1
int * ptr = (int *) malloc( sizeof(int)* 10 ); //分配一个10个int元素的数组

new与malloc是否可以相互调用

operator new 的实现可以基于malloc,而malloc的实现不可以去调用new。

是否可以被重载

opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本:

// 这些版本可能抛出异常

1
2
3
4
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete(void *) noexcept;
void * operator delete[](void *) noexcept;
1
2
3
4
5
// 这些版本承诺不抛出异常
void * operator new(size_t, nothrow_t const&) noexcept;
void * operator new[](size_t, nothrow_t const&) noexcept;
void * operator delete(void *, nothrow_t const&) noexcept;
void * operator delete[](void *, nothrow_t const&) noexcept
1
2
3
// VS C++下还有两个,GCC下不确定
void * operator delete(void *, size_t) noexcept;
void * operator delete[](void *, size_t) noexcept;

我们可以重载上面函数版本中的任意一个,前提是自定义版本必须位于全局作用域或者类作用域中。总之,我们有足够的自由去重载operator new /operator delete ,以决定我们的new与delete如何为对象分配内存,如何回收对象。

malloc/free并不允许重载。

能够直观地重新分配内存

使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

new没有这样直观的配套设施来扩充内存。

客户处理内存分配不足

在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new_handler。new_handler是一个指针类型:

1
2
3
4
namespace std
{
typedef void (*new_handler)();
}

指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:

1
2
3
4
namespace std
{
new_handler set_new_handler(new_handler p ) throw();
}

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。

对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。