4

Visual C ++が動的配列にメモリを割り当てる方法を明確にするために、独自の小さな発見プログラムを作成しているときに、少し混乱しています。注意しなければならないのは、C++ 実装の new[]/delete[] 演算子に関するこの質問を説明している技術文書に出会ったことがないことです。

最初は、new[] と delete[] は、単純な C として解釈される場合、次のようなものだと思いました。

void fake_int_ctor(int _this) {
    printf("borns with 0x%08X in the heap\n", _this);
}

void fake_int_dtor(int _this) {
    printf("dies with %d\n", _this);
}

void *new_array(unsigned int single_item_size, unsigned int count, void (*ctor)()) {
    unsigned int i;
    unsigned int *p = malloc(sizeof(single_item_size) + sizeof(count) + single_item_size * count);
    p[0] = single_item_size; // keep single item size for delete_array
    p[1] = count; // and then keep items count for delete_array
    p += 2;
    for ( i = 0; i < count; i++ ) {
        ctor(p[i]); // simulate constructor calling
    }
    return p;
}

void delete_array(void *p, void (*dtor)()) {
    unsigned int *casted_p = p;
    unsigned int single_item_size = casted_p[-2];
    unsigned int count = casted_p[-1];
    unsigned int i;
    for ( i = 0; i < count; i++ ) {
        dtor(casted_p[i]); // simulate destructor
    }
    free(casted_p - 2);
}

void test_allocators(void) {
    unsigned int count = 10;
    unsigned int i;
    int *p = new_array(sizeof(int), count, fake_int_ctor); // allocate 10 ints and simulate constructors
    for ( i = 0; i < count; i++ ) {
        p[i] = i + i; // do something
    }
    delete_array(p, fake_int_dtor); // deletes the array printing death-agony-values from 0 to 19 stepping 2
}

このコードは、動的配列の次の構造を意味します。

-2..-1..0.....|.....|.....|.....
^   ^   ^
|   |   +-- start of user data, slots may have variable size
|   |       depending on "single item size" slot
|   +------ "items count" slot
+---------- "single item size" slot

私の VC++ コンパイラは、次の出力を生成するプログラムを生成しました。

borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
borns with 0xCDCDCDCD in the heap
dies with 0
dies with 2
dies with 4
dies with 6
dies with 8
dies with 10
dies with 12
dies with 14
dies with 16
dies with 18

明らかに、この場合はすべて問題ありません。しかし、「ネイティブ」な VC++ 動的配列アロケーターの性質を発見しようとしたとき、私は間違っていることを理解しました (少なくとも VC++ については)。そこでいくつか質問があります。動的配列サイズの値はどこに格納されますか? 動的配列アロケーターはどのように機能しますか? 動的配列に使用するバイト単位の構造はどれですか? または...または、これを明確にするリンクを提供していただけますか(VC ++が最も優先されます)。

4

4 に答える 4

3

それは実装定義です。Scott meyer は、実際には多くの C++ コンパイラが、返されたアドレスの直前の要素数を保存していることも指摘しました。VC2010 と g++ 3.4.2(mingw) で確認しました。どちらのコンパイラも、演算子 new[]/delete[] を次のように実装しています。

+--------------------+-------------------------+
| Num of elems(4byte)|  Your Object or Array   |
+--------------------+-------------------------+

#include <stdio.h>
#include <stdlib.h>
struct X {
    int i;
    ~X() {
        puts("a");
    }
};
int main()
{
    volatile int s = 3;
    printf("input a size: ");
    fflush(stdout);
    scanf("%d", &s);
    X * px = reinterpret_cast<X *>(new X[s]);
    printf("%d\n", *( (int*)px - 1));
    delete[] px;
    return 0;
}

VC2010 のアセンブリ命令に従いましたが、デバッグ シンボルを使用してコードをコンパイルすれば、それほど難しくありません。

cl /MTd /Zi array_test.cpp

fflush の目的は、実際にサイズを入力して Enter キーを押す前に、「input a size:」が画面に出力されていることを確認することです。

scanf を使用してサイズを取得する理由は 2 つあります。 1. プロセスを VS デバッガーにアタッチする機会を与える。

5 などの小さな数字を入力すると、組み立て手順を実行するときに作業が楽になります。これにより、一部の手順の結果が期待どおりかどうかを確認できるからです。

以下は、実際のアセンブリ命令に関する行ごとのコメントです。

        X * px = reinterpret_cast<X *>(new X[s]);      ; assume s = 5
00401070  mov         ecx,dword ptr [s]            ; the number of element is saved to ecx
00401073  mov         dword ptr [ebp-0Ch],ecx      ; save it to somewhere on the stack
00401076  xor         ecx,ecx  
00401078  mov         eax,dword ptr [ebp-0Ch]      ; trace it! now it's in eax
0040107B  mov         edx,4                        ; because sizeof(X) == 4
00401080  mul         eax,edx                      ; Get the total bytes needed for the whole array
00401082  seto        cl                           ; handle the scenario: big size which overflow
00401085  neg         ecx                          ; typically not, so cl = 0, and ecx = 0
00401087  or          ecx,eax                      ; now ecx = eax = 4 * 5 = 20
00401089  xor         eax,eax                      ; clear eax, now eax = 0
0040108B  add         ecx,4                        ; add 4 to ecx, why 4? for save the overhead array size
0040108E  setb        al                           ; set al to 1 if carry flag is set, typically 0
00401091  neg         eax                          ; eax = 0, neg eax also result 0
00401093  or          eax,ecx                      ; eax = ecx = 24
00401095  push        eax                          ;
00401096  call        operator new (40122Ch)       ; same as scalar new
0040109B  add         esp,4                        ; balance the stack
0040109E  mov         dword ptr [ebp-10h],eax      ; function's return value typically saved in EAX
                                                   ; [ebp-10h] is somewhere on stack, used to save the
                                                   ; returned raw memory pointer
004010A1  cmp         dword ptr [ebp-10h],0        ; check whether returned NULL pointer
004010A5  je          main+8Ah (4010BAh)  
004010A7  mov         ecx,dword ptr [ebp-10h]      ; now ECX point to 24 bytes raw memory
004010AA  mov         edx,dword ptr [ebp-0Ch]      ; Check address 00401073, edx is 5 now
004010AD  mov         dword ptr [ecx],edx          ; !!!! 5 saved to the start of the 24 bytes raw memory
004010AF  mov         eax,dword ptr [ebp-10h]      ; load start address of the 24 raw memory to EAX
004010B2  add         eax,4                        ; advance the EAX with 4 bytes, now EAX point to the
                                                   ; start address of your first object in the array
004010B5  mov         dword ptr [ebp-1Ch],eax      ; Save this address to somewhere on the stack
004010B8  jmp         main+91h (4010C1h)           ; not NULL pointer, so skip it
004010BA  mov         dword ptr [ebp-1Ch],0        ; See address 004010A5
004010C1  mov         ecx,dword ptr [ebp-1Ch]      ; Load the address to ECX
004010C4  mov         dword ptr [px],ecx           ; Load the address in ECX to px. -The End-

削除[]部分:

        delete[] px;
004010DC  mov         ecx,dword ptr [px]                         ; the address of the first object
004010DF  mov         dword ptr [ebp-18h],ecx                    ; save to somewhereon the stack
004010E2  mov         edx,dword ptr [ebp-18h]                    ; save it again to edx
004010E5  mov         dword ptr [ebp-14h],edx                    ; move around
004010E8  cmp         dword ptr [ebp-14h],0                      ; Check NULL pointer
004010EC  je          main+0CDh (4010FDh)  
004010EE  push        3                                          ; Looks silly, just because I init it to 3?
004010F0  mov         ecx,dword ptr [ebp-14h]                    ; again, ECX is just the address of first object
                                                                 ; [px] -> ecx -> [ebp-18h] -> edx -> [ebp-14h] -> ecx
004010F3  call        X::`vector deleting destructor' (401014h)  ; A good function name, lets follow it!
    X::`vector deleting destructor':
00401014  jmp         X::`vector deleting destructor' (401140h) 
X::`vector deleting destructor':
00401140  push        ebp  
00401141  mov         ebp,esp  
00401143  push        ecx                                          ; Address of the first object
00401144  mov         dword ptr [ebp-4],ecx                        ; save it to somewhere on stack
00401147  mov         eax,dword ptr [ebp+8]                        ; See address 004010EE, it's 3
0040114A  and         eax,2                                        ; ??
0040114D  je          X::`vector deleting destructor'+45h (401185h)  
0040114F  push        offset X::~X (401005h)                       ; (S1) Put address of the descructor to stack
00401154  mov         ecx,dword ptr [this]                         ; Address of first object
00401157  mov         edx,dword ptr [ecx-4]                        ; !! important, ECX - 4 to get the
                                                                   ; address of the 24-bytes raw memory
                                                                   ; The value in it is the number of the elements
                                                                   ; Save it to EDX(=5, see 004010AD)
0040115A  push        edx                                          ; (S2) Put it on stack
0040115B  push        4                                            ; (S3) Put the sizeof(X) on stack
0040115D  mov         eax,dword ptr [this]                         ; save the address of the first object to EAX
00401160  push        eax                                          ; (S4) Put it on stack
00401161  call        `vector destructor iterator' (40100Ah)       ; Good function name, follow it
`vector destructor iterator':
0040100A  jmp         `vector destructor iterator' (4011F0h) 
`vector destructor iterator':
004011F0  push        ebp  
004011F1  mov         ebp,esp  
004011F3  mov         eax,dword ptr [__s]                          ; Some tricks here, by inspecting the value and
                                                                   ; some guess work, __s = 4(S3)
004011F6  imul        eax,dword ptr [__n]                          ; __n = 5 (S2)
004011FA  add         eax,dword ptr [__t]                          ; __t = (S4), add it to EAX, now point to end
                                                                   ; of the array
004011FD  mov         dword ptr [__t],eax                          ; __t point to end of array
00401200  mov         ecx,dword ptr [__n]                          ; loop counter
00401203  sub         ecx,1  
00401206  mov         dword ptr [__n],ecx  
00401209  js          `vector destructor iterator'+2Ch (40121Ch)   ; jump out of loop if value less than 0
0040120B  mov         edx,dword ptr [__t]                          ; Load addr: 1-byte passed the end of the array
0040120E  sub         edx,dword ptr [__s]                          ; Now point to the address of last element
00401211  mov         dword ptr [__t],edx                          ; Update this address to __t
00401214  mov         ecx,dword ptr [__t]                          ; save the address to ECX
00401217  call        dword ptr [__f]                              ; __f is the address of destructor function
0040121A  jmp         `vector destructor iterator'+10h (401200h)  
0040121C  pop         ebp  
0040121D  ret         10h                                          ; Because we have S1, S2, S3, S4
                                                                   ; 4 pushes

struct X {
    int i;
    ~X() {
004011D0  push        ebp  
004011D1  mov         ebp,esp  
004011D3  push        ecx                                          ; the address of current object: this in C++
004011D4  mov         dword ptr [ebp-4],ecx                        ; save this to [ebp-4], although not used it
        puts("a");                                                 ;
004011D7  push        offset string "a" (403758h)  
004011DC  call        dword ptr [__imp__puts (406240h)]  
004011E2  add         esp,4  
    }
004011E5  mov         esp,ebp  
004011E7  pop         ebp  
004011E8  ret  
于 2012-04-18T15:46:07.433 に答える
2

ここで何を探しているのかわかりませんがfake_int_ctor(int)、割り当てられた配列に初期化されていないメモリが出力されています。代わりに次のようにしてみてください。

void fake_int_ctor(int& _this) {
    printf("born at %p\n", (void*)&_this);
}

void fake_int_dtor(int& _this) {
    printf("dies at %p\n", (void*)&_this);
}

これにより、アドレスが出力されます。これは、あなたが見たいものの線に沿っていると思います。

mallocこの小さなプログラムは、連続したストレージ ( ala ) のチャンクを割り当ててアドレスの範囲を出力しているだけなので、実際には何も表示していません。そこには本当に驚くべきことは何もありません。配列の実際のストレージは実装定義です。保証されている唯一のことは、 のようなことをしたときにC *p = new C[10]pが 10 個のオブジェクトに十分な連続したストレージを指すことCです。環境が割り当てられた要素を追跡して、delete [] p割り当てられた各要素のデストラクタを呼び出す方法は、実装によって完全に定義されます。

これを本当に掘り下げたい場合は、次のスニペットのようなものから始めてください。アセンブリ リストを有効にしてコンパイルし、生成されたアセンブリ コードを確認します。

struct C {
  C(): x(0) {}
  int x;
};

int main() {
  C *p = new C[10];
  for (int i=0; i<10; ++i) {
    p[i].x = i;
  }
  delete [] p;
  return 0;
}

すべての最適化をオフにしている限り、コンパイラが配列をどのように表現するかを理解できるはずです。

于 2009-10-21T04:46:36.393 に答える
1

GNU C / ++に関する情報ですが、いくつかの情報をあなたと共有してください。ただし、多くのC / C ++コンパイラには、互いに類似したものがあります。

動的オブジェクトまたは配列を割り当てるためにnew()実装コード(正確にはlibコード)が実行されたとき。オブジェクトと配列が配置される割り当てられたメモリは、libコードが実際に使用するメモリバッファの一部であり、構造は次の図のようになります。

+--------------------+-------------------------+
| Overhead    Area   |  Your Object or Array   |
+--------------------+-------------------------+
                     ^
                     |
  CObject *pArray ---+

もちろん、右側の領域のみを使用し、左側の領域であるオーバーヘッドを上書きすることはできません。「newCObject[]」によって返されるpArrayは(上の図に示すように)右側の領域を指しているため、一般にユーザーはオーバーヘッドに気付かないでしょう(ここでも、ユーザーはオーバーヘッド領域を使用できません)。

動的に割り当てられた配列のサイズは、オーバーヘッド領域に格納されました。delete []が呼び出されると、libコードはオーバーヘッド領域の情報から配列サイズを認識します。

割り当てられたサイズ以外の多くのものもオーバーヘッドに格納されます。

低レベルのmalloc()/ free()/ brk()などを使用してnew()とdelete()を実装している場合は、GNU Cと同様に、オーバーヘッド領域を予約して、それに続く適切な使用可能領域をユーザーに返すことができます。 /++。

于 2009-10-21T12:28:54.793 に答える
1

それは実装定義です。

したがって、実装は、コンパイラのさまざまなメーカー/バージョンで変更されます (多くの場合、変更されます) (一部のシステムでは、リリース バージョンとデバッグ バージョンで異なります)。

しかし、あなたが正しい行にいると言って、あなたは通常、ブロックの実際のサイズのようなより多くの情報があります(正確なサイズが見つからなかった場合)。 . 等...

于 2009-10-21T04:36:03.843 に答える