web-dev-qa-db-ja.com

ソースコードの最後に定義が書かれているのに、C言語でデータと関数の*宣言*が必要なのはなぜですか?

次の「C」コードについて考えてみます。

_#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}
_

Func_i()はソースコードの最後に定義されており、main()で使用する前の宣言はありません。コンパイラがFunc_i()main()を検出した時点で、main()から取得され、Func_i()を検出します。コンパイラーはFunc_i()によって返された値を何らかの方法で見つけ、printf()に渡します。また、コンパイラがFunc_i()戻りタイプを見つけられないことも知っています。デフォルトでは、Func_i()戻り値の型intになります(推測?)。つまり、コードにfloat Func_i()が含まれている場合、コンパイラは次のエラーを返します:Func_i()の競合する型。

上記の議論から、次のことがわかります。

  1. コンパイラは、Func_i()によって返された値を見つけることができます。

    • コンパイラがFunc_i()から出てソースコードを検索することにより、main()によって返された値を見つけることができる場合、Func_i()のタイプを見つけることができないのはなぜですか。 explicitlyが言及されています。
  2. コンパイラーは、Func_i()がfloat型であることを認識している必要があります。そのため、競合する型のエラーが発生します。

  • コンパイラが_Func_i_がfloat型であることを知っている場合、なぜそれでもFunc_i()がint型であると想定し、競合する型のエラーを表示するのはなぜですか? Func_i()を強制的にfloat型にしないでください。

変数宣言についても同じ疑問があります。次の「C」コードについて考えてみます。

_#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;
_

コンパイラはエラーを出します: 'Data_i' undeclared(この関数での最初の使用)

  • コンパイラーがFunc_i()を検出すると、ソースコードまで進んで、Func_()によって返された値を見つけます。コンパイラが変数Data_iに対して同じことを実行できないのはなぜですか?

編集:

コンパイラ、アセンブラ、プロセッサなどの内部動作の詳細はわかりません。私の質問の基本的な考え方は、使用後に、ソースコード内の関数の戻り値を最後に言った(書いた)場合です。その関数の「C」言語により、コンピュータはエラーを出さずにその値を見つけることができます。なぜコンピュータは同様にタイプを見つけることができないのですか? Func_i()の戻り値が見つかったため、Data_iのタイプが見つからないのはなぜですか。 _extern data-type identifier;_ステートメントを使用しても、その識別子(関数/変数)から返される値を指定していません。コンピュータがその値を見つけることができるなら、なぜそれはタイプを見つけることができないのですか?なぜ前方宣言が必要なのですか?

ありがとうございました。

15
user106313

Cはsingle-passstatic-typedweakly-typedcompiled言語であるためです。

  1. シングルパスは、コンパイラーが関数または変数の定義を見るために先を見ないことを意味します。コンパイラーは先を見ないので、関数の宣言は関数を使用する前に行う必要があります。そうしないと、コンパイラーはその型シグニチャーが何であるかを認識しません。ただし、関数の定義は後で同じファイルで、または別のファイルで完全に定義できます。ポイント4を参照してください。

    唯一の例外は、宣言されていない関数と変数が「int」型であると推定されるという歴史的なアーティファクトです。現代の慣例では、常に関数と変数を明示的に宣言することにより、暗黙の型指定を回避しています。

  2. Static-typedは、すべての型情報がコンパイル時に計算されることを意味します。その情報は、実行時に実行されるマシンコードを生成するために使用されます。 Cには実行時タイピングの概念はありません。一度int、常にint、一度float、常にfloat。ただし、その事実は次の点ではやや曖昧です。

  3. Weakly-typedは、プログラマーが明示的に変換操作を指定しなくても、Cコンパイラーが数値型間で変換するコードを自動的に生成することを意味します。静的型付けのため、プログラムを通じて毎回同じ変換が常に同じ方法で実行されます。コード内の特定の場所でfloat値がint値に変換される場合、float値は常にコード内のその場所でint値に変換されます。これは実行時に変更できません。もちろん、値自体はプログラムの実行ごとに変化する可能性があり、条件ステートメントはコードのどのセクションがどの順序で実行されるかを変更する可能性がありますが、関数呼び出しや条件なしのコードの特定の単一セクションは常に正確に実行されます実行されるたびに同じ操作。

  4. コンパイル済みは、人間が読み取れるソースコードを分析し、機械が読み取り可能な命令に変換するプロセスが、プログラムの実行前に完全に実行されることを意味します。コンパイラーが関数をコンパイルしているときは、特定のソースファイルでさらに下で何が発生するかはわかりません。ただし、コンパイル(およびアセンブリ、リンクなど)が完了すると、完成した実行可能ファイルの各関数には、実行時に呼び出される関数への数値ポインターが含まれます。そのため、main()はソースファイルのさらに下の方で関数を呼び出すことができます。 main()が実際に実行されるまでに、Func_i()のアドレスへのポインターが含まれます。

    マシンコードは非常に具体的です。 2つの整数を追加するコード(3 + 2)は、2つの浮動小数点数を追加するコード(3.0 + 2.0)とは異なります。これらはどちらも、floatにintを追加する(3 + 2.0)などとは異なります。コンパイラーは、関数のすべてのポイントについて、そのポイントで実行する必要がある正確な操作を判別し、その正確な操作を実行するコードを生成します。いったんそれが行われると、関数を再コンパイルせずに変更することはできません。

これらのすべての概念をまとめると、main()がFunc_i()のタイプを判別するためにさらに「確認」できないのは、タイプ分析がコンパイルプロセスの最初に行われるためです。この時点では、ソースファイルのmain()の定義までの部分のみが読み取られて分析されており、Func_i()の定義はまだコンパイラーに認識されていません。

Main()がFunc_i()がどこにあるかを「見る」ことができるcallは、コンパイルがすべての識別子のすべての名前と型をすでに解決した後で、実行時に呼び出しが行われるためです。アセンブリは既にすべての関数をマシンコードに変換しており、リンクは呼び出される各場所に各関数の正しいアドレスをすでに挿入しています。

もちろん、私は悲惨な詳細のほとんどを省略しています。実際のプロセスははるかに複雑です。私はあなたの質問に答えるのに十分な高レベルの概要を提供したことを願っています。

さらに、上で書いた内容は特にCに適用されることを覚えておいてください。

他の言語では、コンパイラーはソースコードを複数回通過する可能性があるため、コンパイラーはFunc_i()の定義を事前宣言せずに取得できます。

他の言語では、関数や変数は動的に型付けされる可能性があるため、単一の変数を保持したり、単一の関数を渡したり、整数、浮動小数点数、文字列、配列、またはオブジェクトをさまざまな時点で返したりできます。

他の言語では、タイピングの方が強く、浮動小数点から整数への変換を明示的に指定する必要があります。さらに他の言語では、タイピングが弱く、文字列「3.0」からフロート3.0から整数3への変換を自動的に実行できます。

また、他の言語では、コードは一度に1行ずつ解釈されるか、バイトコードにコンパイルされてから解釈されるか、ジャストインタイムコンパイルされるか、または他のさまざまな実行スキームを実行します。

26
Clement Cherlin

C言語の設計上の制約は、それがシングルパスコンパイラーによってコンパイルされることになっていたため、非常にメモリに制約のあるシステムに適しています。したがって、コンパイラーはいつでも以前に言及されたものについてのみ知っています。コンパイラーは、ソースで前方にスキップして関数宣言を見つけてから、その関数の呼び出しをコンパイルすることはできません。したがって、すべてのシンボルは、使用する前に宣言する必要があります。次のような関数を事前宣言できます

_int Func_i();
_

コンパイラーを支援するために、上部またはヘッダーファイル内。

あなたの例では、避けるべきC言語の2つの怪しい機能を使用しています。

  1. 関数が適切に宣言される前に使用された場合、これは「暗黙の宣言」として使用されます。コンパイラーは即時コンテキストを使用して、関数のシグニチャーを把握します。コンパイラーは、残りのコードをスキャンして実際の宣言が何であるかを理解しません。

  2. タイプなしで何かが宣言された場合、タイプはintであると解釈されます。これは、例えば静的変数または関数の戻り値の型の場合。

したがって、printf("func:%d",Func_i())には、暗黙的な宣言int Func_i()があります。コンパイラが関数定義Func_i() { ... }に到達すると、これは型と互換性があります。ただし、この時点でfloat Func_i() { ... }を記述した場合、暗黙的に宣言されたint Func_i()と明示的に宣言されたfloat Func_i()があります。 2つの宣言が一致しないため、コンパイラーはエラーを出します。

誤解を解消する

  • コンパイラは、_Func_i_によって返された値を検出しません。明示的な型がないことは、戻り値の型がデフォルトでintであることを意味します。あなたがこれをしたとしても:

    _Func_i() {
        float f = 42.3;
        return f;
    }
    _

    その後、型はint Func_i()になり、戻り値は通知なく切り捨てられます!

  • コンパイラは最終的に_Func_i_の実際の型を知るようになりますが、暗黙の宣言では実際の型はわかりません。後で実際の宣言に達したときにのみ、暗黙的に宣言された型が正しいかどうかを確認できます。しかし、その時点で、関数呼び出しのアセンブリは既に作成されている可能性があり、Cコンパイルモデルでは変更できません。

37
amon

まず、プログラムはC90標準に有効ですが、以下のプログラムには無効です。暗黙のint(戻り値の型を指定せずに関数を宣言できる)、および関数の暗黙の宣言(関数を宣言せずに関数を使用できる)は、もはや有効ではありません。

第二に、あなたが思うようにそれは機能しません。

  1. 結果タイプはC90ではオプションであり、1を指定しないとint結果を意味します。変数宣言についても同様です(ただし、ストレージクラスstaticまたはexternを指定する必要があります)。

  2. Func_iが前の宣言なしで呼び出されたときにコンパイラが行うことは、宣言があると想定しています。

    extern int Func_i();
    

    Func_iがどの程度効果的に宣言されているかを確認するために、コードをさらに詳しく調べることはしません。 Func_iが宣言または定義されていない場合、mainのコンパイル時にコンパイラーはその動作を変更しません。暗黙の宣言は関数のみであり、変数はありません。

    宣言内の空のパラメーターリストは、関数がパラメーターを取らないことを意味しないことに注意してください(そのためには(void)を指定する必要があります)。これは、コンパイラーが型をチェックする必要がないことを意味しますパラメータおよび可変関数に渡される引数に適用されるものと同じ暗黙の変換になります。

10
AProgrammer

あなたはコメントに書いた:

実行は行ごとに行われます。 Func_i()によって返された値を見つける唯一の方法は、メインからジャンプすることです

それは誤解です。実行は行ごとではありません。 Compilationは行ごとに行われ、名前解決はコンパイル中に行われ、名前のみを解決し、戻り値は解決しません。

役立つ概念モデルは次のとおりです。コンパイラが次の行を読み取る場合:

  printf("func:%d",Func_i());

次と同等のコードを出力します。

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

コンパイラーは、function #2Func_iという名前のまだ宣言されていない関数であることを内部テーブルでメモします。この関数は、不特定の数の引数を取り、int(デフォルト)を返します。

後でこれを解析するとき:

 int Func_i() { ...

コンパイラーは上記の表でFunc_iを検索し、パラメーターと戻り値の型が一致するかどうかを確認します。そうでない場合は、エラーメッセージで停止します。存在する場合は、現在のアドレスを内部関数テーブルに追加し、次の行に進みます。

そのため、コンパイラは最初の参照を解析するときにFunc_iを「検索」しませんでした。それは単にいくつかのテーブルにメモを書き、次の行を解析し続けました。ファイルの最後には、オブジェクトファイルとジャンプアドレスのリストがあります。

その後、リンカはこれらすべてを取得し、「関数#2」へのすべてのポインタを実際のジャンプアドレスに置き換えます。したがって、次のようなメッセージが出力されます。

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

ずっと後で、実行可能ファイルが実行されるとき、ジャンプアドレスはすでに解決されており、コンピュータはアドレス0x1215にジャンプすることができます。名前の検索は必要ありません。

免責事項:すでに述べたように、これは概念モデルであり、現実の世界はより複雑です。コンパイラとリンカーは、今日、あらゆる種類のクレイジーな最適化を行っています。彼らはmight "ジャンプアップダウン"してFunc_iを探しますが、疑わしいです。しかし、C言語は、あなたがcouldのような超シンプルなコンパイラを書くように定義されています。ほとんどの場合、これは非常に便利なモデルです。

7
nikie