Cでの変数宣言の配置


129

Cでは、すべての変数を関数の先頭で宣言する必要があるとずっと思っていました。C99でのルールはC ++と同じであることは知っていますが、C89 / ANSI Cの変数宣言の配置ルールは何ですか?

次のコードはgcc -std=c89andで正常にコンパイルされますgcc -ansi

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

の宣言csC89 / ANSIモードでのエラーの原因になるべきではありませんか?


54
注意:ansi Cの変数は、関数の開始時に宣言する必要はなく、ブロックの開始時に宣言する必要があります。したがって、forループの先頭にあるchar c = ...は、ansi Cでは完全に合法です。ただし、char * sは正しくありません。
ジェイソンココ

回答:


149

GCC sはC89またはANSI標準の一部ではありませんが、GNU拡張としての宣言を許可しているため、正常にコンパイルされます。これらの標準に厳密に準拠する場合は、-pedanticフラグを渡す必要があります。

ブロックcの先頭の宣言は{ }C89標準の一部です。ブロックは関数である必要はありません。


41
s(C89の観点から)の宣言だけが拡張であることは、おそらく注目に値します。の宣言はcC89では完全に合法であり、拡張は必要ありません。
AnT

7
@AndreyT:ええ、Cでは、変数宣言はブロック自体の先頭であり、関数自体ではありません。しかし、ブロックは機能の主要な例であるため、ブロックと機能を混同します。
legends2k 2012年

1
+39票のコメントを回答に移動しました。
MarcH

78

C89の場合、スコープブロックの先頭ですべての変数を宣言する必要があります。

したがって、char cforループスコープブロックの先頭にあるため、宣言は有効です。ただし、char *s宣言はエラーになります。


2
正解です。変数は、任意の{...}の先頭で宣言できます。
Artelius 2008年

5
@Artelius不正解です。カーリーがブロックの一部である場合のみ(それらが構造体や共用体の宣言、またはブレース付きのイニシャライザの一部ではない場合)
Jens

見やすくするために、誤った宣言は少なくともC標準に従って通知する必要があります。したがって、それはエラーまたは警告である必要がありますgcc。つまり、プログラムがコンパイル可能であり、プログラムが準拠していることを意味するとは信じないでください。
jinawee

35

ブロックの上部で変数宣言をグループ化することは、古いプリミティブCコンパイラの制限が原因である可能性が高いレガシーです。最近のすべての言語は、ローカル変数の宣言を推奨し、場合によっては強制することさえあります。つまり、最初に初期化される場所です。これは、誤ってランダムな値を使用するリスクを取り除くためです。宣言と初期化を分離すると、可能であれば "const"(または "final")を使用できなくなります。

残念ながら、C ++はCとの下位互換性のために古いトップ宣言方法を受け入れ続けます(Cの互換性は他の多くのものか​​ら外れます...)しかし、C ++はそれから離れようとします:

  • C ++参照の設計では、このようなトップオブブロックのグループ化もできません。
  • C ++ローカルオブジェクトの宣言と初期化を分離すると、追加のコンストラクタのコストを無料で支払うことができます。引数なしのコンストラクタが存在しない場合、再び両方を分離することもできません!

C99はCを同じ方向に動かし始めます。

ローカル変数が宣言されている場所が見つからないのではないかと心配している場合は、非常に大きな問題があることを意味します。外側のブロックが長すぎて分割する必要があります。

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions



ブロックの上部で変数宣言を強制するとセキュリティホールが発生する方法も参照してください。lwn.net
Articles

「残念ながら、C ++はCとの下位互換性のために古いトップ宣言方法を受け入れ続けています」:IMHO、それはそれを行うためのクリーンな方法にすぎません。他の言語は常に0で初期化することにより、この問題を「解決」します。Bzzt、私に尋ねた場合にのみ論理エラーをマスクします。また、初期化の場所が複数ある可能性があるため、初期化なしで宣言が必要になる場合がかなりあります。そしてそれが、C ++のRAIIが本当にお尻に大きな負担をかける理由です。これらのケースを可能にするために、各オブジェクトに「有効な」初期化されていない状態を含める必要があります。
Jo So

1
@JoSo:初期化されていない変数の読み取りが任意の効果をもたらすと、一貫性のある値または決定論的エラーをもたらすよりもプログラミングの間違いを検出しやすくなるのはなぜですか?初期化されていないストレージの読み取りが、変数が保持する可能性のあるすべてのビットパターンと一致する方法で動作するという保証はなく、そのようなプログラムが通常の時間と因果律と一致する方法で動作するという保証もないことに注意してください。int y; ... if (x) { printf("X was true"); y=23;} return y;...のようなものを考えると
スーパーキャット

1
@JoSo:ポインタの場合、特にで操作をトラップする実装ではnull、すべてのビットがゼロの場合が多くの場合、トラップ値として役立ちます。さらに、変数のデフォルトがすべてビット0であることを明示的に指定する言語で、その値に依存してもエラーにはなりません。コンパイラはまだ「最適化」で過度に風変わりになる傾向はありませが、コンパイラの作成者はますます賢くしようとしています。故意の疑似ランダム変数で変数を初期化するコンパイラー・オプションは、障害を識別するのに役立つ場合がありますが、単に最後の値を保持するストレージを残すだけで、障害がマスクされることがあります。
スーパーキャット2018年

22

構文の観点からではなく、保守性の観点から、少なくとも3つの考え方があります。

  1. 関数の最初ですべての変数を宣言して、それらが1か所に配置され、包括的なリストが一目でわかるようにします。

  2. すべての変数を最初に使用した場所にできるだけ近い場所で宣言すると、それぞれが必要な理由がわかります。

  3. 最も内側のスコープブロックの先頭ですべての変数を宣言します。そうすることで、変数はできるだけ早くスコープから外れ、コンパイラーがメモリを最適化して、意図しない場所で誤ってそれらを使用したかどうかを通知できるようになります。

私は一般的に最初のオプションを好みます。他の人が宣言のコードを探すことを強いられることが多いからです。すべての変数を事前に定義すると、デバッガーから変数を初期化して監視するのも簡単になります。

小さいスコープブロック内で変数を宣言することもありますが、その理由は非常に少ないためです。1つの例はfork()、子プロセスでのみ必要な変数を宣言するためのの後である場合があります。私にとって、この視覚的インジケータは、彼らの目的を思い出させるのに役立ちます。


27
私はオプション2または3を使用しているので、変数を見つけるのは簡単です。関数が大きくなりすぎて変数の宣言が見えないようにする必要があるためです。
ジョナサンレフラー

8
70年代のコンパイラを使用しない限り、オプション3は問題ではありません。
edgar.holleis 2010年

15
まともなIDEを使用している場合は、宣言を見つけるIDEコマンドがあるはずなので、コードを探す必要はありません。(EclipseのF3)
edgar.holleis

4
オプション1で初期化を確実に行う方法がわかりません。場合によっては、ブロックの後半で、別の関数を呼び出すか、計算を実行することによってのみ初期値を取得できます。
Plumenator

4
@Plumenator:オプション1は初期化を保証しません。私はそれらを宣言時に初期化することを選択しました。それらの「正しい」値、または適切に設定されていない場合に後続のコードが壊れることを保証するものに初期化することを選択しました。これを書いてから私の好みが#2に変わったので、「Choose」と言います。おそらくCを超えてJavaを使用しているため、そしてより優れた開発ツールがあるためです。
Adam Liss、

6

他の人が指摘しているように、GCCは「C89」モードの場合でも、「ペダンティック」チェックを使用しない限り、この点(および、他のコンパイラーは、それらが呼び出される引数によっては)を許容します。正直なところ、知識を身につけない理由はそれほど多くありません。品質の高い最新のコードは常に警告なしでコンパイルする必要があります(または、間違いの可能性としてコンパイラーに疑わしい特定のことを実行していることがわかっている場合はごくわずかです)。したがって、コードを綿密なセットアップでコンパイルできない場合は、おそらく注意が必要です。

C89では、各スコープ内の他のステートメントの前に変数を宣言する必要があります。後の標準では、特に「for」ループでのループ制御変数の同時宣言と初期化の同時宣言と初期化を同時に使用できるように、宣言をより近く使用できます(これは、より直感的で効率的です)


0

前述のように、これについては2つの考え方があります。

1)年は1987年なので、関数の最上部ですべてを宣言します。

2)最初の使用に最も近く、可能な限り最小の範囲で宣言します。

これに対する私の答えは「両方」です!説明させてください:

長い関数の場合、1)リファクタリングが非常に困難になります。開発者がサブルーチンのアイデアに反対しているコードベースで作業している場合、関数の最初に50の変数宣言があり、それらの一部はforループの「i」である可能性があります。関数の下部。

したがって、私はこれからPTSD宣言を作成し、オプション2)を宗教的に実行しようとしました。

オプション1に戻ったのは、短い関数という1つの理由からでした。関数が十分に短い場合、ローカル変数はほとんどなくなります。関数が短いため、関数の先頭に配置すると、最初の使用に近くなります。

また、最初に宣言したいが、初期化に必要な計算をまだ行っていない場合の「宣言してNULLに設定」のアンチパターンは、初期化に必要なものが引数として受け取られる可能性が高いため、解決されています。

だから今私の考えは、あなたは関数の一番上で、そして最初の使用にできるだけ近いところで宣言すべきだということです。両方!そして、その方法は、よく分割されたサブルーチンを使用することです。

ただし、長い関数で作業している場合は、メソッドを抽出するのが簡単になるため、最初の使用に最も近いものを配置します。

私のレシピはこれです。すべてのローカル変数について、変数を取得してその宣言を一番下に移動し、コンパイルしてから、宣言をコンパイルエラーの直前に移動します。それが最初の使用です。これをすべてのローカル変数に対して行います。

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

次に、宣言の前に始まるスコープブロックを定義し、プログラムがコンパイルされるまで終了を移動します。

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

fooを使用するコードがいくつかあるため、これはコンパイルされません。fooを使用しないため、barを使用するコードをコンパイラが通過できたことがわかります。この時点で、2つの選択肢があります。機械的な方法は、コンパイルするまで「}」を下に移動することです。もう1つの選択肢は、コードを検査して、順序を次のように変更できるかどうかを判断することです。

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

順序を切り替えることができる場合は、一時的な値の寿命が短くなるため、おそらくこれが必要です。

もう1つ注意すべき点として、fooの値は、それを使用するコードのブロック間で保存する必要がありますか、それとも両方の異なるfooにすることができますか。例えば

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

これらの状況は私の手順以上のものを必要とします。開発者は、コードを分析して何をすべきかを決定する必要があります。

しかし、最初のステップは、最初の用途を見つけることです。視覚的に行うこともできますが、場合によっては、宣言を削除し、コンパイルして、最初の使用の前に戻すほうが簡単です。最初の使用がifステートメントの内部にある場合は、そこに配置して、コンパイルされるかどうかを確認します。その後、コンパイラーは他の用途を識別します。両方の用途を含むスコープブロックを作成してみてください。

この機械的な部分が完了すると、データの場所を分析することが容易になります。変数が大きなスコープブロックで使用されている場合は、状況を分析し、同じ変数を2つの異なるもの(2つのforループで使用される「i」など)に使用しているかどうかを確認します。用途が無関係の場合は、これらの無関係の用途ごとに新しい変数を作成します。


0

すべての変数は、関数の先頭または「ローカル」で宣言する必要があります。答えは:

使用しているシステムの種類によって異なります。

1 /組み込みシステム(特に飛行機や車などの生活に関連):動的メモリを使用できます(例:calloc、malloc、new ...)。1000人のエンジニアがいる非常に大きなプロジェクトで作業しているとします。新しい動的メモリを割り当てて、それを削除するのを忘れた場合(使用しない場合)はどうなりますか?組み込みシステムが長時間実行されると、スタックオーバーフローが発生し、ソフトウェアが破損します。品質を確認するのは簡単ではありません(動的メモリを禁止するのが最善の方法です)。

飛行機が30日間稼働しても電源が切れない場合、ソフトウェアが破損しているとどうなりますか(飛行機が空中にいる場合)。

2 / Web、PCなどのその他のシステム(メモリ容量が大きい):

使用するメモリを最適化するには、変数を「ローカルに」宣言する必要があります。これらのシステムが長時間実行され、スタックオーバーフローが発生した場合(動的メモリを削除するのを忘れたため)。PCをリセットするという簡単なことを行うだけです:P生活に影響はありません


これが正しいかどうかはわかりません。すべてのローカル変数を1か所で宣言すると、メモリリークを監査する方が簡単だと言っていると思いますか?それ本当かもしれませんが、私はそれを購入するかどうかはわかりません。ポイント(2)に関しては、変数をローカルで宣言すると「メモリ使用量が最適化される」と思いますか?これは理論的には可能です。コンパイラーは、メモリ使用量を最小限に抑えるために、関数の過程でスタックフレームのサイズを変更することを選択できますが、これを行うものは知りません。実際には、コンパイラーはすべての「ローカル」宣言を「バックグラウンドでfunction-start」に変換するだけです。
QuinnFreedman

1 /組み込みシステムでは、動的メモリが許可されない場合があるため、すべての変数を関数の上で宣言すると、ソースコードがビルドされると、プログラムを実行するためにスタックで必要なバイト数を計算できます。しかし、動的メモリでは、コンパイラは同じことを行うことができません。
Dang_Ho

2 /変数をローカルで宣言する場合、その変数は「{}」の開閉ブラケット内にのみ存在します。そのため、その変数が「スコープ外」の場合、コンパイラーは変数のスペースを解放できます。関数の上部ですべてを宣言するよりも良いかもしれません。
Dang_Ho

静的メモリと動的メモリについて混乱していると思います。静的メモリはスタックに割り当てられます。関数で宣言されているすべての変数は、どこで宣言されていても、静的に割り当てられます。動的メモリはのようなものでヒープに割り当てられますmalloc()。対応していないデバイスを見たことがありませんが、組み込みシステムでの動的割り当てを回避することをお勧めします(ここを参照)。ただし、関数内で変数を宣言する場所とは関係ありません。
QuinnFreedman

1
これは適切な運用方法であることに同意しますが、実際にはそうではありません。これは、例に非常によく似たものの実際のアセンブリです:godbolt.org/z/mLhE9a。ご覧のとおり、11行目は、ifステートメントのsub rsp, 1008配列全体にスペースを割り当てています。これがために真であると、私が試したすべてのバージョンおよび最適化レベルで。clanggcc
QuinnFreedman

-1

明確な説明のために、gccバージョン4.7.0のマニュアルからいくつかのステートメントを引用します。

「コンパイラーは、「c90」や「c ++ 98」などのいくつかの基本標準、および「gnu90」や「gnu ++ 98」などのそれらの標準のGNU方言を受け入れることができます。基本標準を指定することにより、コンパイラーは、その標準に準拠するすべてのプログラムと、GNU拡張を使用してそれらに矛盾しないプログラムを受け入れます。たとえば、「-std = c90」は、asmやtypeofキーワードなど、ISO C90と互換性のないGCCの特定の機能をオフにしますが、 ?:式の中間用語の省略など、ISO C90で意味を持たない他のGNU拡張機能。」

あなたの質問のキーポイントは、オプション "-std = c89"が使用されているのに、なぜgccがC89に準拠しないのかということです。あなたのgccのバージョンはわかりませんが、大きな違いはないと思います。gccの開発者は、オプション "-std = c89"はC89と矛盾する拡張機能がオフになっていることを意味するだけだと言っています。したがって、C89では意味を持たない一部の拡張機能とは関係ありません。また、変数宣言の配置を制限しない拡張機能は、C89と矛盾しない拡張機能に属しています。

正直に言うと、オプション「-std = c89」を一目見ただけで、C89に完全に準拠する必要があると誰もが思うでしょう。しかし、そうではありません。すべての変数を最初に宣言する問題が良いか悪いかということは、ただの習慣の問題です。


適合とは、拡張機能を受け入れないという意味ではありません。コンパイラが有効なプログラムをコンパイルし、他の必要な診断を生成する限り、適合します。
モニカを思い出してください

@Marc Lehmannはい、コンパイラを区別するために「準拠」という言葉が使用されているのは正しいです。ただし、一部の使用法を説明するために「準拠」という言葉を使用すると、「使用法は標準に準拠していません」と言うことができます。そして、初心者はみな、規格に適合しない使い方はエラーになるはずだという意見があります。
junwanghe 2012

@Marc Lehmannさん、ちなみに、gccがC89標準に準拠していない使用法を見つけた場合、診断はありません。
junwanghe 2012

「gccが準拠していない」と主張することは「一部のユーザープログラムが準拠していない」と同じではないため、あなたの答えはまだ間違っています。あなたのコンフォームの使い方は単に間違っています。それに、私が初心者だったとき、あなたが言っているような意見ではなかったので、それも間違っています。最後に、適合コンパイラが非適合コードを診断するための要件はなく、実際、これを実装することは不可能です。
モニカ覚えておいてください
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.