関数/ブロックに出入りするときにスタック上で何が起こるかという点で、C99 のブロック スコープと関数スコープの違いは何ですか?
3 に答える
理論的には、コンパイラは、ローカル変数を含む任意のブロックへのエントリでスタック フレームを割り当てるコードを生成できます。そのような場合、ほとんど違いはありません。
実際には、ほとんどのコンパイラは、関数を介して任意のパスで使用できるローカル変数の最大サイズを計算し、そのサイズのスタック フレームをエントリに割り当てます。関数内の任意のブロック内の変数は、スタック ポインターからのオフセットが異なるだけです。このような場合、2 つ (またはそれ以上) のブロックが同じアドレスを使用する可能性があることに注意してください。たとえば、次のようなソース コードを使用します。
void f(int x) {
if (x) {
long y;
}
else {
float z;
}
}
...可能性は非常に高く、y
最終z
的に同じ住所になります。
制御が関数またはブロックのスコープに入ったときに実装が行う必要がある唯一のことは、そのスコープ内のすべてのデータ オブジェクトの新しいインスタンスが「自動ストレージ期間」で直接作成されたかのように動作することです。あたかも振る舞うということは、コンパイルされているプログラムが違いを見分けられない限り (または、振る舞いが定義されていない何かをすることによってしか違いを見分けられない限り)、何か違うことをすることができるということを意味します。たとえば、変数が関数スコープで宣言されているが、1 つのサブブロック内でのみ使用されている場合、コンパイラはそのライブ範囲をそのサブブロックに折りたたむことができます。これにより、レジスタの割り当てが容易になるため、おそらくそうするでしょう。
制御が関数またはブロックのスコープを終了するとき、実装は何もする必要はありません。そのスコープ内のすべての自動ストレージ期間オブジェクトの有効期間は直接終了しますが、未定義の動作をトリガーせずにこれが発生したことをプログラムは通知できません。
C 実装にスタックが必要という要件はありません。また、上記の要件を実装する方法はスタックだけではありません。たとえば、「Cheney on the MTA」およびc2:SpaghettiStackを参照してください。
スタックを持つC 実装は、通常、関数の途中でスタック ポインターを調整することを避けようとしますが、その理由は複雑すぎてここでは説明できません。これは、ブロック スコープを持つ値が、宣言された有効期間よりも長くスタック上で存続することを意味する可能性がありますが、それにアクセスするための動作はまだ定義されていません。コンパイラは、スコープ内にない値のストレージをリサイクルできますが、まだスコープ内にあるがもうアクセスされない値 (コンパイラ用語では「デッド」) のストレージをリサイクルすることもできます。歴史的に、コンパイラは、スタック スロットの値よりもレジスタの値に対してより積極的にそれを行ってきましたが、これも実装に必ずしも存在しない違いです。
このような:
void foo(int n) // <-- beginning of function scope
{ // <-- beginning of function body scope
int x = n;
for (;;)
{ // <-- beginning of block scope
int q = n;
x *= q;
} // <-- end of block scope
foo(x);
{ // <-- another block scope
int w = x;
}
} // <-- end of function body scope
// and of function scope
スコープが終了しても何も「起こりません」が、変数はそれが宣言されているスコープ内にのみ存在します(いくつかの難解な例外があります)。終了した以前のネストされたスコープの変数のスペースを再利用するのは、実装次第です。