C++ delete[] 是如何知道數組大小的?

Effective c++ 上面對 使用相同形式的new delete 時 提到在

A *p = new A[100];

在 p 地址到 A[100]結束的地方 是存儲A[100]數據的,在 p 地址之前有一個 n 記錄這個數組的大小,delete 的時候可以知道大小,但是書上說不是所有的編譯器都是非這麼實現不可。

然後我就是想問這個如果不記錄n的話是怎麼正確 delete 那塊內存大小呢?

如果記錄 n 的話是不是存在 p 地址前面的,是多大怎麼存的呢?

這方面有什麼書可以看下么

還有delete到底具體做了什麼呢


嗯,是這樣的,內存管理庫里總會有一個方法來記錄你分配的內存是多大,你寫的"在 p 地址之前有一個 n 記錄這個數組的大小"是一種實現方法,管理庫當然也可以自己維護一個列表,記錄所有空間分配的位置和大小以及類型,delete的時候查一下這個列表就行了,也就是所謂"不是所有的編譯器都是非這麼實現不可",目前來說,常見的C++編譯器也就是GCC/VCC/ICC/BCC等,具體實現可以查各編譯器的文檔說明,當然商業編譯器不一定肯說.


跟free知道該釋放多大空間一個道理。

要想弄清楚,看文檔(ABI)、源碼、下斷點單步跟蹤、看反彙編,都行。

在non-trivial destructor的情況下,通常的編譯器都是把n放在前面,至少佔size_t,而且按class/struct的要求對齊,因此不一定是4位元組。

(Linux/Windows的ABI要求均如此。我沒有見過別的做法。)


那就由我來給個詳細的補充吧。

==========================

申請內存時,指針所指向區塊的大小這一信息,其實就記錄在該指針的周圍

看下面這段代碼:

#include&
#include&
#include&
#include&
#include&
using namespace std;

#define size 16

int main(void)
{
void * p = NULL;
srand(time(0));
int a = 10;
while (a--)
{
int n = rand() % 10000;
p = malloc(n);
size_t w = *((size_t*)((char*)p - size));
cout &<&< "w=" &<&< w &<&< endl; cout &<&< "n=" &<&< n &<&< endl; assert(w == n); free(p); } return 0; }

(註:如果是X86的CPU,請將 size 改為 8)

你會發現 w 和 n 始終是一致的,,這樣其實不是巧合,來看 M$ 編譯器 vc include 目錄下 malloc.h這一頭文件 中 184 到 209 行的代碼:

//這兒是根據不同的硬體平台的宏定義
#if defined (_M_IX86)
#define _ALLOCA_S_MARKER_SIZE 8
#elif defined (_M_X64)
#define _ALLOCA_S_MARKER_SIZE 16
#elif defined (_M_ARM)
#define _ALLOCA_S_MARKER_SIZE 8
#elif !defined (RC_INVOKED)
#error Unsupported target platform.
#endif /* !defined (RC_INVOKED) */

_STATIC_ASSERT(sizeof(unsigned int) &<= _ALLOCA_S_MARKER_SIZE); #if !defined (__midl) !defined (RC_INVOKED) #pragma warning(push) #pragma warning(disable:6540) __inline void *_MarkAllocaS(_Out_opt_ __crt_typefix(unsigned int*) void *_Ptr, unsigned int _Marker) { if (_Ptr) { *((unsigned int*)_Ptr) = _Marker; // _Ptr = (char*)_Ptr + _ALLOCA_S_MARKER_SIZE; //最後返回給調用者的指針,是原始指針偏移了_ALLOCA_S_MARKER_SIZE的新指針,這也是剛才我將指針向後偏移,就能得到該指針所指向內存區塊的大小的原因。 } return _Ptr; }

再來看看在 M$ 編譯器中它是如何釋放的,同樣在 mallloc.h 文件249行到274行:

/* _freea must be in the header so that its allocator matches _malloca */
#if !defined (__midl) !defined (RC_INVOKED)
#if !(defined (_DEBUG) defined (_CRTDBG_MAP_ALLOC))
#undef _freea
__pragma(warning(push))
__pragma(warning(disable: 6014))
_CRTNOALIAS __inline void __CRTDECL _freea(_Pre_maybenull_ _Post_invalid_ void * _Memory)
{
unsigned int _Marker;
if (_Memory)
{
_Memory = (char*)_Memory - _ALLOCA_S_MARKER_SIZE;
//獲得原始指針
_Marker = *(unsigned int *)_Memory;//得到指針所指區塊的大小
if (_Marker == _ALLOCA_S_HEAP_MARKER)
{
free(_Memory);
}
#if defined (_ASSERTE)
else if (_Marker != _ALLOCA_S_STACK_MARKER)
{
#pragma warning(suppress: 4548) /* expression before comma has no effect */
_ASSERTE(("Corrupted pointer passed to _freea", 0));
}
#endif /* defined (_ASSERTE) */
}
}

再來看看 SGI STL標準庫源碼 stl_alloc.h 文件209 行到 246行 debug_alloc類模板的設計:

// Allocator adaptor to check size arguments for debugging.
// Reports errors using assert. Checking can be disabled with
// NDEBUG, but it"s far better to just use the underlying allocator
// instead when no checking is desired.
// There is some evidence that this can confuse Purify.
template &
class debug_alloc {

private:

enum {_S_extra = 8}; // Size of space used to store size. Note
// that this must be large enough to preserve
// alignment.

//這兒就像它所說的那樣
public:

static void* allocate(size_t __n)
{
//
這裡實際申請的內存大小要多 8 個位元組
char* __result = (char*)_Alloc::allocate(__n + (int) _S_extra);
*(size_t*)__result = __n;//前 4 個位元組用於存儲區塊大小,可以看到,它預留了4個位元組的空白區,具體原由 還望大牛能指出,==。
return __result + (int) _S_extra;//最後返回相對於原始指針偏移8個位元組的新指針
}

static void deallocate(void* __p, size_t __n)
{
char* __real_p = (char*)__p - (int) _S_extra;//獲得原始指針
assert(*(size_t*)__real_p == __n);//這裡增加了一個斷言,防止析構了被破壞的指針
_Alloc::deallocate(__real_p, __n + (int) _S_extra);
}

static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz)
{
char* __real_p = (char*)__p - (int) _S_extra;
assert(*(size_t*)__real_p == __old_sz);
char* __result = (char*)
_Alloc::reallocate(__real_p, __old_sz + (int) _S_extra,
__new_sz + (int) _S_extra);
*(size_t*)__result = __new_sz;
return __result + (int) _S_extra;
}

};

再來看看 gcc 下,其實也有類似的設計:

#if(defined(_X86_) !defined(__x86_64))
#define _ALLOCA_S_MARKER_SIZE 8
#elif defined(__ia64__) || defined(__x86_64)
#define _ALLOCA_S_MARKER_SIZE 16
#endif

#if !defined(RC_INVOKED)
static __inline void *_MarkAllocaS(void *_Ptr,unsigned int _Marker) {
if(_Ptr) {
*((unsigned int*)_Ptr) = _Marker;
_Ptr = (char*)_Ptr + _ALLOCA_S_MARKER_SIZE;
}
return _Ptr;
}
#endif

#ifndef RC_INVOKED
#undef _freea
static __inline void __cdecl _freea(void *_Memory) {
unsigned int _Marker;
if(_Memory) {
_Memory = (char*)_Memory - _ALLOCA_S_MARKER_SIZE;
_Marker = *(unsigned int *)_Memory;
if(_Marker==_ALLOCA_S_HEAP_MARKER) {
free(_Memory);
}
#ifdef _ASSERTE
else if(_Marker!=_ALLOCA_S_STACK_MARKER) {
_ASSERTE(("Corrupted pointer passed to _freea",0));
}
#endif
}
}
#endif /* RC_INVOKED */


delete[]可以有無窮種方法來實現

new/malloc是資源管理器,交給用戶的既是一個內存地址,同時也是一個資源句柄(handle)。這是雙重屬性

按照句柄來理解,自然對應的管理器內部可以額外記錄(bookkeeping)任意輔助信息。永遠不會出現拿著句柄找管理器做不成事情的情況


gcc是這樣實現的,但他在地址之前存儲的並不一定是你分配的大小,而是一個數目的倍數,這樣會使得存儲數值的最後幾位都是bit 0,他會稍微大於或者等於你分配的大小。由於末尾在沒有標識的情況下都是0,存儲的數值中末尾的幾個bit可以作為內存是否被佔用的標識(可以預防一個buffer被delete兩次,delete只需要重置對應bit就可以了,第二次delete就可以檢查這個標識然後crash掉程序)。可以寫一寫測試代碼分析gcc關於這個地方的一些做法。


看看內存分配器是怎麼實現的。

一般有兩種方式:

1 非入侵式,內存分配器自行先申請內存(和棧配合使用),用作記錄用戶層的申請記錄(地址,大小)。 用戶釋放空間時會查找該表,除了知道釋放空間大小外還能判斷該指針是合法。

2 入侵式,例如用戶要申請1byte的內存,而內存分配器會分配5byte的空間(32位),前面4byte用於申請的大小。釋放內存時會先向前偏移4個byte找到申請大小,再進行釋放。

兩種方法各有優缺點,第一種安全,但慢。第二種快但對程序員的指針控制能力要求更高,稍有不慎越界了會對空間信息做成破壞。

絕大多數的分配器會採用第一種方式實現,而操作系統級的分配器採用了虛擬等方式,可能要記錄更多信息。

還要注意的是系統級的內存分配器是需要解決多線程申請的問題。

有些分配器也會在debug版中於申請的內存塊尾端做特定的標記,如0xcccccccc,來檢測內存操作中有無越界。


《深入理解計算機系統》動態存儲分配。負荷塊之前存在頭部,記錄負荷大小與標誌(是否存儲內容),好像共計8位元組。


可以看看這篇文章;

深入探究C++的new/delete操作符 - Kelvin的胡言亂語


new []實際的內存比申請的大小多4個位元組。返回值前移4位元組就保存數字大小。delete[]時將入參前移4位元組即數組大小


@陳碩 說得很對

A *p = new A[100];

一般是內存前面保持數組的長度


百度一把不就有了?chuck size,一個sizez的大小。


size_t _msize(
void *memblock
);


知道數組的長度,如何知道每個對象的大小呢 數組中每個對象的大小保存在哪呢


推薦閱讀:

面向對象編程(oop)從誕生到現在理念上經歷了幾次怎樣大的變遷和轉化?
C++ 11為什麼引入nullptr?
為什麼判斷 std::vector 是否為空時,用 if(0==vec.size()) 提示效率低,但用 if (vec.empty()) 正常?
C/C++中char/int/long等基本內置類型為何要編譯器相關而不是固定長度?
Visual C++.NET的存在意義是什麼?

TAG:編程語言 | C | 編譯 |