【探討】C++ delete[] 是如何知道數組大小的

該問題值得探討!


Lippman在對象模型裏提到過一些(P257-P263),拍幾張主要內容的圖片





下面是自己的測試以及知乎的一些討論和解答:


先貼自己的測試代碼、反彙編以及內存轉儲圖(測試環境:CodeBlocks、編譯器GCC、系統64位)

【分別測試了數組大小爲3和5的情況】





從內存轉儲可以看到,GCC其實並不是像網友說的那樣,通過指針偏移、額外開闢8字節空間來存儲長度。

具體是怎麼做到的,目前我也不知道。有待探討。




【下面貼出幾個在知乎上看到的我覺得值得一看的回答】


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


作者:iux001
鏈接:https://www.zhihu.com/question/25556263/answer/31114361
來源:知乎
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。



作者:夏華林
鏈接:https://www.zhihu.com/question/25556263/answer/32589012
來源:知乎
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

那就由我來給個詳細的補充吧。
==========================
申請內存時,指針所指向區塊的大小這一信息,其實就記錄在該指針的周圍
看下面這段代碼:
#include<cstdio>
#include<iostream>
#include<malloc.h>
#include<assert.h>
#include<ctime>
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 _Alloc>
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 */



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

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

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

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

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

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

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


作者:匿名用戶
鏈接:https://www.zhihu.com/question/25556263/answer/31116363
來源:知乎
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。












發佈了23 篇原創文章 · 獲贊 63 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章