web-dev-qa-db-ja.com

コンパイラは、コンパイル時にサイズを知らずにメモリをどのように割り当てますか?

ユーザーからの整数入力を受け入れるCプログラムを作成しました。これは整数配列のサイズとして使用され、その値を使用して指定サイズの配列を宣言し、配列のサイズを確認して確認しています。

コード:

_#include <stdio.h>
int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);
    int k[n];
    printf("%ld",sizeof(k));
    return 0;
}
_

そして驚くべきことにそれは正しいです!プログラムは、必要なサイズの配列を作成できます。
しかし、すべての静的メモリ割り当てはコンパイル時に行われ、コンパイル時にnの値は不明です。どうしてコンパイラは必要なサイズのメモリを割り当てることができますか?

そのように必要なメモリを割り当てることができる場合、malloc()calloc()を使用した動的割り当ての使用は何ですか?

66
Rahul

これは「静的メモリ割り当て」ではありません。配列kは可変長配列(VLA)です。つまり、この配列のメモリは実行時に割り当てられます。サイズは、nのランタイム値によって決定されます。

言語仕様は特定の割り当てメカニズムを規定していませんが、通常の実装では、kは通常、実行時にスタックに割り当てられる実際のメモリブロックを持つ単純なint *ポインターになります。

VLAの場合、sizeof演算子も実行時に評価されるため、実験で正しい値を取得するのはこのためです。タイプ%zuの値を出力するには、%ldsize_tではなく)を使用します。

malloc(およびその他の動的メモリ割り当て関数)の主な目的は、ローカルオブジェクトに適用されるスコープベースのライフタイムルールをオーバーライドすることです。つまりmallocで割り当てられたメモリは、「永久に」割り当てられるか、freeで明示的に割り当てを解除するまで割り当てられたままになります。 mallocで割り当てられたメモリは、ブロックの最後で自動的に割り当て解除されません。

あなたの例のように、VLAはこの「スコープを無効にする」機能を提供しません。配列kは、通常のスコープベースのライフタイムルールに従います。そのライフタイムはブロックの終わりで終了します。このため、一般的な場合、VLAはmallocおよびその他の動的メモリ割り当て関数を置き換えることはできません。

しかし、「スコープを無効にする」必要がなく、mallocを使用して実行時サイズの配列を割り当てる特定のケースでは、VLAは実際にmallocの代わりと見なされる場合があります。繰り返しになりますが、VLAは通常スタックに割り当てられ、今日までスタックに大量のメモリを割り当てることは、かなり疑わしいプログラミング手法であることに注意してください。

72
AnT

Cでは、コンパイラがVLA(可変長配列)をサポートする手段はコンパイラ次第です。malloc()を使用する必要はなく、「スタック」と呼ばれることもある(および頻繁に使用する)ことができます。 "メモリ-例標準Cの一部ではないalloca()のようなシステム固有の関数を使用します。スタックを使用する場合、通常、配列の最大サイズはmalloc()を使用した場合よりもはるかに小さくなります。スタックメモリの割り当てをはるかに小さくするプログラムを許可します。

11
Peter

可変長配列のメモリは、明らかに静的に割り当てることはできません。ただし、スタックに割り当てることはできます。一般的に、これには、「フレームポインタ」を使用して、スタックポインタに対する動的に決定された変更に直面して、関数スタックフレームの位置を追跡することが含まれます。

プログラムをコンパイルしようとすると、実際に起こるのは、可変長配列が最適化されたように見えることです。そのため、コンパイラに実際に配列を割り当てるようにコードを修正しました。

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);
    int k[n];
    printf("%s %ld",k,sizeof(k));
    return 0;
}

ゴッドボルトは、gcc 6.3を使用してアームをコンパイルします(アームASMを読み取ることができるため、アームを使用)。これを https://godbolt.org/g/5ZnHfa にコンパイルします。 (私のコメント)

main:
        Push    {fp, lr}      ; Save fp and lr on the stack
        add     fp, sp, #4    ; Create a "frame pointer" so we know where
                              ; our stack frame is even after applying a 
                              ; dynamic offset to the stack pointer.
        sub     sp, sp, #8    ; allocate 8 bytes on the stack (8 rather
                              ; than 4 due to ABI alignment
                              ; requirements)
        sub     r1, fp, #8    ; load r1 with a pointer to n
        ldr     r0, .L3       ; load pointer to format string for scanf
                              ; into r0
        bl      scanf         ; call scanf (arguments in r0 and r1)
        ldr     r2, [fp, #-8] ; load r2 with value of n
        ldr     r0, .L3+4     ; load pointer to format string for printf
                              ; into r0
        lsl     r2, r2, #2    ; multiply n by 4
        add     r3, r2, #10   ; add 10 to n*4 (not sure why it used 10,
                              ; 7 would seem sufficient)
        bic     r3, r3, #7    ; and clear the low bits so it is a
                              ; multiple of 8 (stack alignment again) 
        sub     sp, sp, r3    ; actually allocate the dynamic array on
                              ; the stack
        mov     r1, sp        ; store a pointer to the dynamic size array
                              ; in r1
        bl      printf        ; call printf (arguments in r0, r1 and r2)
        mov     r0, #0        ; set r0 to 0
        sub     sp, fp, #4    ; use the frame pointer to restore the
                              ; stack pointer
        pop     {fp, lr}      ; restore fp and lr
        bx      lr            ; return to the caller (return value in r0)
.L3:
        .Word   .LC0
        .Word   .LC1
.LC0:
        .ascii  "%d\000"
.LC1:
        .ascii  "%s %ld\000"
10
plugwash

「可変長配列」VLAと呼ばれるこの構造のメモリは、allocaと同様の方法でスタックに割り当てられます。正確にこれがどのように発生するかは、使用しているコンパイラによって異なりますが、本質的には、サイズがわかっているときにサイズを計算し、スタックポインタから合計サイズを減算する場合です。

この割り当ては、関数を終了するときに「死ぬ」ため、mallocと友人が必要です。 [そして、標準C++では無効です]

[1]「ゼロに向かって成長する」スタックを使用する一般的なプロセッサの場合。

3
Mats Petersson

コンパイラがコンパイル時で変数にメモリを割り当てると言われる場合、それらの変数の配置は、コンパイラが作成するのではなく、コンパイラが生成する実行可能コードに決定され、埋め込まれることを意味しますそれが機能している間、利用可能なスペースがあります。実際の動的メモリ割り当ては、生成されたプログラムの実行時に実行されます。

0
Linkon