GCC 下 C++ 中 new int[] 內存的額外信息在哪裡?
條件是gcc下,我有做搜索,看到據說在內存塊前面或後面存放長度信息。於是去做實驗,死活沒找到這個額外信息。
如果使用類,new A[4],則似乎可以看到一點信息,但數據並非簡單size。如果使用placement方式,則原本類情況下出現的信息也沒有了。哪位可以講解一下為什麼嗎?或者哪裡可以看到gcc下new,delete的實現源碼或文檔?謝謝!
嗯題主想像的「額外信息」不是在operator new[] (size_t sz)里記錄的,而是編譯器在外面看情況額外生成了代碼。
原因很簡單:全局的operator new (size_t sz)和operator new[] (size_t sz)並不知道要給什麼類型的對象分配空間,所以自然也不知道它們所需要記錄的額外元數據要些什麼。
而這些信息從調用者的一側是知道的,所以就由編譯器在調用者的一側生成代碼來做這些事情。===================================================================
得把回答順序調整一下免得誤導了讀者?
先看倆例子。現在在我的老MacBook Air 2011上,用的編譯器是llvm-gcc-4.2.1,平台是Mac OS X 10.7.5 x86-64。
第一個例子沒有虛析構函數:#include &
using namespace std;
class A {
int x, y;
};
// sizeof(A) == 8, x: 4, y: 4
int main() {
A* as = new A[10];
delete [] as;
return 0;
}
這個版本里main()被該版本的GCC編譯為類似下面偽代碼:
int main() {
void* _tmp = ::operator new[](sizeof(A) * 10);
A* as = reinterpret_cast&(_tmp);
::operator delete[](as);
return 0;
}
沒有留下任何C++層面上的「額外信息」。
第二個例子則使用虛析構函數:#include &
using namespace std;
class A {
int x, y;
public:
~A() { }
};
// sizeof(A) == 16, vptr: 8, x: 4, y: 4
// sizeof(size_t) == 8
int main() {
A* as = new A[10];
delete [] as;
return 0;
}
這個版本的main()則被這個GCC編譯為類似下面偽代碼:
int main() {
void* _tmp = ::operator new[](sizeof(A) * 10 + sizeof(size_t))
A* _tmpA = reinterpret_cast&(
reinterpret_cast&
{
// array length recorded at offset: -sizeof(size_t)
*(reinterpret_cast&
A* _cur = _tmpA;
for (int _i = 9; _i != -1; _cur++, i--) {
A::A(_cur); // call ctor: initialize vptr, etc
}
}
A* as = _tmpA;
if (as != nullptr) { 這個版本里,在new[]時GCC額外給數組分配了1個size_t大小的隱藏欄位來記錄數組元素的個數,並且在new[]之後會按順序調用構造函數;而在delete[]之前逆序會循環調用析構函數,然後再調用全局的operator delete[] (size_t sz)。 在GCC中,貫穿於C++的operator new[] (size_t sz)的實現代碼中的「關鍵點」是「 VEC_NEW_EXPR」。用這個詞搜索GCC的代碼就可以找到關鍵的實現代碼。
size_t _len = *(reinterpret_cast&
A* _cur = as + _len;
// call dtor from last to first element in the array
while (_cur != as) {
_cur = _cur - 1;
// call virtual dtor through vtable slot 0
auto _vptr = reinterpret_cast&
_vptr[0](_cur);
}
// call operator delete[](size_t sz) with the original location
::operator delete[](
reinterpret_cast&
}
return 0;
}
/* Generate code for a new-expression, including calling the "operator
new" function, initializing the object, and, if an exception occurs
during construction, cleaning up. The arguments are as for
build_raw_new_expr. This may change PLACEMENT and INIT. */
static tree 其中從2621行開始是global operator new的處理,它會檢測該數組是否需要「cookie」。這個「cookie」就是題主想找的C++語義層面的「額外信息」。
build_new_1 (vec&
vec&
tsubst_flags_t complain)
(TYPE_VEC_NEW_USES_COOKIE):
New macro to indicate when vec new must add a header containing the number of elements in the vector; i.e. when the elements need to be destroyed or vec delete wants to know the size.
/* Use a global operator new. */
/* See if a cookie might be required. */
if (!(array_p TYPE_VEC_NEW_USES_COOKIE (elt_type)))
這個宏的定義是:
gcc/cp-tree.h at b594087e1afe41db9d100f08644715702d6cfc1b · gcc-mirror/gcc · GitHub/* Nonzero if `new NODE[x]" should cause the allocation of extra
storage to indicate how many array elements are in use. */
#define TYPE_VEC_NEW_USES_COOKIE(NODE)
(CLASS_TYPE_P (NODE)
LANG_TYPE_CLASS_CHECK (NODE)-&>vec_new_uses_cookie)
===================================================================
關於operator new[] (size_t sz)的實現,參考某個版本的GCC對應的libstdc++ / libsupc++吧。
全局的operator new[] (size_t sz)在這裡(這個是throw版;nothrow版在對應的http://new_opvnt.cc里):gcc/new_opv.cc at master · gcc-mirror/gcc · GitHub_GLIBCXX_WEAK_DEFINITION void*
operator new[] (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
return ::operator new(sz);
}
直接轉交給operator new(size_t sz)了。多狡猾 &>_&<
然後是全局的operator new (size_t sz)gcc/new_op.cc at master · gcc-mirror/gcc · GitHub_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
while (__builtin_expect ((p = malloc (sz)) == 0, false))
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}
return p;
}
正常路徑上本質上就是調用了libc的malloc(size_t sz)而已,額外信息也不是在這裡寫入的。
同理,默認的全局oeprator delete[] (size_t sz)也是直接轉交給operator delete (size_t sz),後者則轉交給libc的free()。
至於說placement new,默認行為是什麼也不做:gcc/new at master · gcc-mirror/gcc · GitHub
// Default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
inline void* operator new[](std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
所以跟存儲相關的「額外信息」——某個指針所指向的動態申請的存儲空間到底有多大——就要看malloc()的實現了。然而底下的malloc()實現是可以變的,例如說可以動態跟jemalloc、tcmalloc鏈接上,也可以用glibc自己的malloc。大家未必會用同一種方式來記錄「額外信息」。
題主可以另外開個問題問malloc()在哪裡記錄「額外信息」?我用的這個老MacBook Air上的Mac OS X自帶的libc在libSystem里,其中的malloc叫做magazine_malloc。可以參考這裡的說明:Cocoa with Love: A look at how malloc works on the Macusing namespace std;
static int c ;
struct A {
A(){
i = c ++;
cerr &<&< __FILE__ &<&< ":" &<&< __LINE__ &<&< ": [" &<&< __FUNCTION__&<&< "] "
&<&< endl;
}
~A(){
cerr &<&< __FILE__ &<&< ":" &<&< __LINE__ &<&< ": [" &<&< __FUNCTION__&<&< "] "
&<&< endl;
}
long long i;
};
void bar(A * p)
{
delete []p;
}
int main(int argc, char *argv[])
{
A * p = new A[3];
bar(p);
return 0;
}
g++ -fno-inline O3 -ggdb main.c
開始用 gdb 調試
bash$ gdb ~/tmp/a.out
(gdb) b bar
Breakpoint 1 at 0x400ce0: file nn.cc, line 24.
(gdb) run
Starting program: /home/xxx/tmp/a.out
nn.cc:13: [A]
nn.cc:13: [A]
nn.cc:13: [A]
Breakpoint 1, bar (p=0x602018) at nn.cc:24 我加些注釋
24 delete []p;
(gdb) disassemble
Dump of assembler code for function bar(A*):
=&> 0x0000000000400c40 &<+0&>: test %rdi,%rdi
0x0000000000400c43 &<+3&>: je 0x400c80 &
0x0000000000400c45 &<+5&>: push %rbp
0x0000000000400c46 &<+6&>: push %rbx
0x0000000000400c47 &<+7&>: mov %rdi,%rbp
0x0000000000400c4a &<+10&>: sub $0x8,%rsp
0x0000000000400c4e &<+14&>: mov -0x8(%rdi),%rax 讀取數組長度到 $rax
0x0000000000400c52 &<+18&>: lea (%rdi,%rax,8),%rbx 讀取末尾指針位置到 $rbx
0x0000000000400c56 &<+22&>: cmp %rbx,%rdi 是否到達尾指針
0x0000000000400c59 &<+25&>: je 0x400c71 &
0x0000000000400c5b &<+27&>: nopl 0x0(%rax,%rax,1)
0x0000000000400c60 &<+32&>: sub $0x8,%rbx 尾指針 -- &<-
0x0000000000400c64 &<+36&>: mov %rbx,%rdi 設置 this 指針 |
0x0000000000400c67 &<+39&>: callq 0x400d00 &
0x0000000000400c6c &<+44&>: cmp %rbx,%rbp 是否到頭指針了 |
0x0000000000400c6f &<+47&>: jne 0x400c60 &
0x0000000000400c71 &<+49&>: add $0x8,%rsp
0x0000000000400c75 &<+53&>: lea -0x8(%rbp),%rdi
0x0000000000400c79 &<+57&>: pop %rbx
0x0000000000400c7a &<+58&>: pop %rbp
0x0000000000400c7b &<+59&>: jmpq 0x400900 &<_ZdaPv@plt&>
0x0000000000400c80 &<+64&>: repz retq
End of assembler dump.
(gdb) x /10xg $rdi -8
0x602010: 0x0000000000000003 0x0000000000000000
0x602020: 0x0000000000000001 0x0000000000000002
0x602030: 0x0000000000000000 0x0000000000020fd1
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
2. "-O3" 優化過的彙編看起來更加容易
3. 寄存器 `$rdi` 存儲的是第一個參數 p, 4. 用 x 命令查看內存,看到 p 前面有一個8位元組表示長度 0x602010 那裡。5. gcc 是倒著調用析構函數的。就是先析構 p[2], p[1], 最後析構 p[0]6. linux 下 X86 64 參數的傳遞是用寄存器傳遞的, $rdi, $rsi, $rcx, $rdx, $r9, $r10, windows 下的不是這樣的,沒有實驗過。總結,數組長度就在 p[-1]那裡,但是我不確定這個是標準,高度懷疑不是標準,是編譯器實現相關。大學學C++的時候做過簡單的表象研究。
似乎用GCC的話,它把new的數組長度存在了指針之前1個int的位置。所以猜想實現的方式是在new的時候多申請幾個位元組,用來放數組長度(也許還有更多,用來放單位的size?因為delete[]會逐一調用析構函數,所以不僅需要知道數組的byte length還需要知道單位個數),然後最終返回的時候就偏移一下。其他不懂,但是gcc源碼可以到gnu官網下載,http://gcc.gnu.org/http://ftp.gnu.org/gnu/gcc/http://ftp.gnu.org/gnu/gcc/gcc-5.1.0/gcc-5.1.0.tar.gz
推薦閱讀:
※如何利用C++的特性,去實現C中的可變參?
※一個關於visual studio的問題?
※2017年6月,GCC 7.1 對於 C++17 標準的支持情況如何了?
※關於VS2015的報錯問題,?