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


153

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

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

ウィルd中にメモリを占有するcode that takes a whileセクション?


8
(1)標準に従って、(2)実装間の普遍的な実践、または(3)実装間の共通の実践を意味しますか?
David Thornley、

回答:


83

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

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

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

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


9
それは実装固有ではありませんか?
avakar

54
C ++では、オブジェクトのデストラクタはスコープの最後で呼び出されます。メモリが解放されるかどうかは、実装固有の問題です。
クリストファージョンソン

8
@ pm100:デストラクタが呼び出されます。それは、それらのオブジェクトが占有していたメモリについては何も述べていません。
ドナルフェロー

9
C標準では、ブロックで宣言された自動変数の有効期間は、ブロックの実行が終了するまでのみ延長されると規定されています。したがって、基本的にこれらの自動変数ブロックの最後で「破棄」されます。
カフェ

3
@KristopherJohnson:メソッドに2つの個別のブロックがあり、それぞれが1Kバイトの配列を宣言し、3番目のブロックがネストされたメソッドを呼び出した場合、コンパイラは両方の配列に同じメモリを自由に使用したり、配列を配置したりできますスタックの最も浅い部分で、スタックポインターをその上に移動して、ネストされたメソッドを呼び出します。このような動作により、関数呼び出しに必要なスタックの深さが2K減少する可能性があります。
スーパーキャット2014年

39

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

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

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

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

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

   return;
}

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

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

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


高レベルの言語を何年も使用してきた後、CとC ++でスコープとメモリがどのように機能するかを学ぶ人として、私はこの答えが受け入れられたものよりも正確で有用であることに気づきました。
クリス

20

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

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

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

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

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

後者dがあなたの質問の文脈で機能の終わりまでメモリを占有し続けるとみなされるかどうかはあなたが決めることです。


6

実装に依存します。私はgcc 4.3.4が何をするかをテストする短いプログラムを書きました、そしてそれは関数の開始時にすべてのスタック空間を一度に割り当てます。-Sフラグを使用して、gccが生成するアセンブリを調べることができます。


3

いいえ、Dは[]うないルーチンの残りのスタック上にあります。しかし、alloca()は異なります。

編集:クリストファージョンソン(およびサイモンとダニエル)は正しいです、そして私の最初の応答は間違っていました。gcc 4.3.4。のCYGWINでは、コードは次のとおりです。

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のドキュメントが正しくないことを示しています。何年もの間それは言った(強調は加えられた):

「可変長配列のスペースは、配列名のスコープが終了するとすぐに割り当て解除さます。」


最適化を無効にしてコンパイルしても、最適化されたコードで何が得られるかは必ずしもわかりません。この場合、動作は同じです(関数の最初に割り当て、関数を終了するときにのみ解放されます): godbolt.org/g/M112AQ。しかし、cygwin以外のgccはalloca関数を呼び出しません。cygwin gccがそれを行うことに本当に驚いています。それは可変長配列でさえないので、IDKがそれを提示する理由です。
Peter Cordes 2017

2

彼らはかもしれない。彼らはそうしないかもしれません。私が本当に必要と思う答えは、何も仮定しないことです。最新のコンパイラーは、あらゆる種類のアーキテクチャーと実装固有の魔法を実行します。人間に簡単かつ読みやすいコードを記述し、コンパイラーに優れた機能を実行させます。コンパイラーを中心にコーディングしようとすると、問題が発生します。これらの状況で通常発生する問題は、恐ろしく微妙であり、診断が困難です。


1

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

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

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

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

更新:C仕様 の内容は次のとおりです。自動保存期間を持つオブジェクトについて(セクション6.4.2):

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

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

オブジェクトの存続期間は、プログラム実行の一部であり、その間にストレージが確保されることが保証されています。オブジェクトは存在し、一定のアドレスを持ち、その存続期間を通して最後に格納された値を保持します。オブジェクトがその存続期間外で参照された場合の動作は未定義です。

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

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

あなたは配列があることを確認しないようにしたい場合はd、もはやあなたのコードの実行中にメモリを食べている、あなたはどちらか別の関数または明示的に中括弧内のコードを変換することができますmallocし、freeメモリの代わりに、自動ストレージを使用しました。


1
「中括弧がスタックのプッシュ/ポップを引き起こした場合、中括弧内のコードは中括弧の外にある変数varにアクセスできないため、上記のコードはコンパイルされません。 -これは単に正しくありません。コンパイラーは常にスタック/フレームポインターからの距離を記憶し、それを使用して外部変数を参照できます。また、中括弧の例については、ヨセフの答えを参照くださいスタックプッシュ/ポップ原因。
ジョージ2012

@ george-記述した動作とジョセフの例は、使用しているコンパイラとプラットフォームに依存します。たとえば、MIPSターゲット用に同じコードをコンパイルすると、まったく異なる結果が得られます。私は純粋にC仕様の観点から話していました(OPがコンパイラまたはターゲットを指定しなかったため)。回答を編集して詳細を追加します。
bta

0

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


3
保証なし。スコープが閉じると、コンパイラはそのメモリを追跡しなくなり(または少なくとも...する必要はありません)、メモリを再利用できます。これが、以前はスコープ外の変数によって占有されていたメモリに触れることが未定義の動作である理由です。鼻の悪魔と同様の警告に注意してください。
dmckee ---元モデレーターの子猫2010

0

この規格については、実装に固有であることを示す多くの情報がすでに提供されています

したがって、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);
}

我々はここで2つの異なるアドレスを取得gccの使用:Coliroを

だから何が起こっているのか本当にわからない。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.