web-dev-qa-db-ja.com

Cでは、ブレースはスタックフレームとして機能しますか?

新しい中括弧のセット内に変数を作成する場合、その変数は閉じ中括弧のスタックからポップされますか、それとも関数の最後までハングアウトしますか?例えば:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

dcode that takes a whileセクションでメモリを占有しますか?

153
Claudiu

いいえ、ブレースはスタックフレームとして機能しません。 Cでは、中かっこは名前付けスコープのみを示しますが、制御が渡されるときにスタックから破棄されたりポップされたりするものはありません。

コードを記述するプログラマーとして、多くの場合、あたかもそれがスタックフレームであるかのように考えることができます。中かっこ内で宣言された識別子は、中かっこ内でのみアクセスできるため、プログラマーの観点からは、宣言時にスタックにプッシュされ、スコープが終了するとポップされるようです。ただし、コンパイラーは、入り口/出口で何かをプッシュ/ポップするコードを生成する必要はありません(そして、一般的に、そうしません)。

また、ローカル変数はスタックスペースをまったく使用しない可能性があることに注意してください。CPUレジスタまたはその他の補助記憶場所に保持されるか、完全に最適化されます。

そのため、理論的には、d配列は関数全体のメモリを消費する可能性があります。ただし、コンパイラはそれを最適化するか、使用期間が重複しない他のローカル変数とメモリを共有します。

83

変数が実際にメモリを占有する時間は明らかにコンパイラに依存します(そして多くのコンパイラは、内部ブロックに入るときにスタックポインタを調整しません)関数内で終了しました)。

ただし、密接に関連しているが、おそらくより興味深い質問は、プログラムが内部スコープの外側(ただし、包含関数内)でその内部オブジェクトにアクセスできるかどうかです。

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(言い換えると、実際にはほとんど許可されていない場合でも、コンパイラはdの割り当てを解除することを許可されていますか?)。

答えは、コンパイラdの割り当てを解除し、p[0]は、コメントが未定義の動作であることを示します(プログラムはnotで、内部スコープ外の内部オブジェクトへのアクセスが許可されています)。 C標準の関連部分は6.2.4p5です。

可変長の配列型を持たないそのようなオブジェクト[自動保存期間を持つオブジェクト]の場合、その有効期間は、そのブロックの実行が何らかの方法で終了するまで、関連付けられているブロックへのエントリから延長されます =。 (囲まれたブロックに入るか、関数を呼び出すと、現在のブロックの実行が中断されますが、終了しません。)ブロックが再帰的に入力されると、オブジェクトの新しいインスタンスが毎回作成されます。オブジェクトの初期値は不定です。オブジェクトに初期化が指定されている場合、ブロックの実行で宣言に到達するたびに初期化が実行されます。そうでない場合、値は宣言に到達するたびに不確定になります。

39
caf

あなたの質問は明確に答えられるほど明確ではありません。

一方では、コンパイラは通常、ネストされたブロックスコープに対してローカルメモリの割り当てと割り当て解除を行いません。ローカルメモリは通常、関数の入り口で一度だけ割り当てられ、関数の出口で解放されます。

一方、ローカルオブジェクトのライフタイムが終了すると、そのオブジェクトが占有しているメモリは、後で別のローカルオブジェクトに再利用できます。たとえば、このコードでは

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

通常、両方の配列は同じメモリ領域を占有します。つまり、関数fooが必要とするローカルストレージの合計量は、2つの配列の両方ではなく、2つの配列の最大に必要なものです。それらを同時に。

後者がdとして適格であるかどうかは、質問のコンテキストで機能が終了するまでメモリを占有し続けるかどうかによって決まります。

20
AnT

実装に依存します。 gcc 4.3.4の機能をテストする短いプログラムを作成し、関数の開始時に一度にすべてのスタックスペースを割り当てます。 -Sフラグを使用して、gccが生成するアセンブリを調べることができます。

6

いいえ、d []はnotルーチンの残りのスタックにありません。ただし、alloca()は異なります。

編集: Kristopher Johnson(およびsimonとDaniel)はrightであり、私の最初の応答はwrong。 CYGWIN上のgcc 4.3.4。では、コードは次のようになります。

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

与える:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

生活し、学びます!また、簡単なテストでは、AndreyTが複数の割り当てについても正しいことを示しているようです。

ずっと後で追加:上記のテストは、 gccドキュメンテーション が正しくないことを示しています。何年もの間、それは次のように述べています(強調を追加):

「可変長配列のスペースはdeallocated配列名のscopeends。 "

3
Joseph Quinsey

彼らはかもしれない。そうではないかもしれません。私が本当に必要だと思う答えは:何も仮定しないでください。現代のコンパイラは、あらゆる種類のアーキテクチャと実装固有の魔法を行います。コードを人間に簡単かつ読みやすいように書き、コンパイラーに良いことをさせてください。コンパイラーの周りにコーディングしようとすると、トラブルが発生します。通常、このような状況で発生するトラブルは、恐ろしく微妙で診断が困難です。

2
user19666

通常、変数dはスタックからポップされません。中括弧は、スタックフレームを示すものではありません。そうしないと、次のようなことができなくなります。

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

(関数呼び出しのように)中括弧が真のスタックプッシュ/ポップを引き起こした場合、中括弧内のコードは中括弧の外側にある変数varにアクセスできないため、上記のコードはコンパイルされません。 (サブ関数が呼び出し元の関数の変数に直接アクセスできないように)。これは事実ではないことがわかっています。

中かっこは、単にスコープに使用されます。コンパイラーは、括弧で囲まれた外部からの「内部」変数へのアクセスを無効として扱い、そのメモリーを他の何かに再利用することがあります(これは実装に依存します)。ただし、囲む関数が戻るまで、スタックからポップされない場合があります。

更新:ここに C仕様 が言わなければならないことがあります。自動保存期間を持つオブジェクトについて(セクション6.4.2):

可変長の配列型を持たないオブジェクトの場合、その有効期間は、そのブロックの実行がとにかく終了するまで、関連付けられているブロックへのエントリから延長されます。

同じセクションでは、「ライフタイム」という用語を(emphasis mine)と定義しています。

オブジェクトのlifetimeは、ストレージが予約されるguaranteedであるプログラム実行の部分です。それ。オブジェクトは存在し、一定のアドレスを持ち、そのライフタイムを通じて最後に保存された値を保持します。オブジェクトがその有効期間外に参照される場合、動作は未定義です。

ここでのキーワードは、もちろん「保証」です。ブレースの内側のセットのスコープを離れると、アレイの寿命は終了します。ストレージはまだ割り当てられている場合と割り当てられていない場合があります(コンパイラが他の目的でスペースを再利用する場合があります)が、配列にアクセスしようとすると未定義の動作が発生し、予測できない結果が生じます。

C仕様にはスタックフレームの概念はありません。結果のプログラムがどのように動作するかについてのみ話し、実装の詳細はコンパイラーに任せます(結局、実装はスタックレスCPUとハードウェアスタックのあるCPUでの見た目とは大きく異なります)。 C仕様には、スタックフレームが終了するかしないかを指定するものはありません。 realを知る唯一の方法は、特定のコンパイラ/プラットフォームでコードをコンパイルし、結果のアセンブリを調べることです。コンパイラの現在の最適化オプションのセットも、おそらくこれで役割を果たすでしょう。

コードの実行中に配列dがメモリを消費しないようにする場合は、中括弧内のコードを別の関数に変換するか、明示的にmallocおよびfree自動ストレージを使用する代わりにメモリ。

1
bta

標準については、実際に実装固有であることを示す多くの情報が既に与えられています。

そのため、1つの実験が興味深いかもしれません。次のコードを試してみると:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Gccを使用して、同じアドレスを2回取得します。 Coliro

しかし、次のコードを試してみると:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Gccを使用して、ここで2つの異なるアドレスを取得します。 Coliro

だから、あなたは本当に何が起こっているのか確信が持てません。

0
Mi-He

私はそれが範囲外になると信じていますが、関数が戻るまでスタックからポップされません。そのため、関数が完了するまでスタック上のメモリを占有しますが、最初の閉じ中括弧の下流にはアクセスできません。

0
simon