通常Cでは、変数宣言でデータのタイプをコンピューターに通知する必要があります。例えば。次のプログラムでは、2つの浮動小数点数XとYの合計を出力します。
_#include<stdio.h>
main()
{
float X=5.2;
float Y=5.1;
float Z;
Z=Y+X;
printf("%f",Z);
}
_
変数Xのタイプをコンパイラーに通知する必要がありました。
- コンパイラが
X
のタイプを独自に決定できないのですか?
はい、できます。
_#define X 5.2
_
これで、コンパイラーにX
のタイプを次のように指示することなく、プログラムを作成できます。
_#include<stdio.h>
#define X 5.2
main()
{
float Y=5.1;
float Z;
Z=Y+X;
printf("%f",Z);
}
_
したがって、C言語には何らかの機能があり、それを使用してデータのタイプを独自に判別できることがわかります。私の場合、X
はfloat型であると判断しました。
- Main()で何かを宣言するときに、なぜデータのタイプについて言及する必要があるのですか? _
#define
_とは異なり、コンパイラがmain()
で変数自体のデータ型を判別できないのはなぜですか。
変数宣言を_#define
_ sと比較していますが、これは正しくありません。 _#define
_を使用して、識別子とソースコードのスニペット間のマッピングを作成します。次に、Cプリプロセッサは、その識別子の出現を文字通り、提供されたスニペットで置き換えます。書き込み
_#define FOO 40 + 2
int foos = FOO + FOO * FOO;
_
コンパイラーにとって、書き込みと同じことになります
_int foos = 40 + 2 + 40 + 2 * 40 + 2;
_
自動化されたコピー&ペーストと考えてください。
また、通常の変数は再割り当てできますが、_#define
_で作成されたマクロは再割り当てできません(ただし、_#define
_は再割り当てできます)。 「rvalues」に割り当てることができないため、式_FOO = 7
_はコンパイラエラーになります:_40 + 2 = 7
_は不正です。
では、なぜ型が必要なのでしょうか。一部の言語は明らかに型を取り除きます。これは特にスクリプト言語で一般的です。ただし、変数には通常「動的型付け」と呼ばれるものがあり、変数には固定型がありませんが、値にはあります。これははるかに柔軟ですが、パフォーマンスも低下します。 Cはパフォーマンスが好きなので、変数の概念は非常にシンプルで効率的です。
「スタック」と呼ばれる一連のメモリがあります。各ローカル変数は、スタック上の領域に対応しています。ここで問題は、この領域は何バイト長でなければならないかということです。 Cでは、各型には明確に定義されたサイズがあり、sizeof(type)
を介してクエリできます。コンパイラーは、スタック上に正しい量のスペースを予約できるように、各変数のタイプを知る必要があります。
_#define
_で作成された定数に型注釈が必要ないのはなぜですか?それらはスタックに保管されません。代わりに、_#define
_は、コピーと貼り付けよりもわずかに保守しやすい方法で、ソースコードの再利用可能なスニペットを作成します。 _"foo"
_や_42.87
_などのソースコード内のリテラルは、コンパイラによって、特別な命令としてインラインで、または結果のバイナリの個別のデータセクションに格納されます。
ただし、リテラルにはタイプがあります。文字列リテラルは_char *
_です。 _42
_はint
ですが、より短い型(ナロー変換)にも使用できます。 _42.8
_はdouble
になります。リテラルがあり、それを別のタイプにしたい場合(例えば、_42.8
_をfloat
、または_42
_を_unsigned long int
_にするため)、サフィックスを使用できます–コンパイラーがそのリテラルを処理する方法を変更するリテラルの後の文字。この場合、_42.8f
_または_42ul
_と言うことができます。
一部の言語にはCのように静的型付けがありますが、型注釈はオプションです。たとえば、ML、Haskell、Scala、C#、C++ 11、Goなどです。それはどのように機能しますか?マジック?いいえ、これは「型推論」と呼ばれます。 C#とGoでは、コンパイラーは割り当ての右側を見て、その型を推測します。右側が_42ul
_などのリテラルである場合、これはかなり簡単です。その後、変数の型がどうあるべきかは明らかです。他の言語にも、変数の使用方法を考慮したより複雑なアルゴリズムがあります。例えば。 _x/2
_を実行する場合、x
を文字列にすることはできませんが、数値型を指定する必要があります。
2番目の例のXは、floatにはなりません。これはマクロと呼ばれ、ソースで定義されたマクロ値「X」を値で置き換えます。 #defineに関する読みやすい記事は here です。
提供されたコードの場合、コンパイルの前にプリプロセッサがコードを変更します
Z=Y+X;
に
Z=Y+5.2;
そしてそれがコンパイルされます。
つまり、これらの「値」を次のようなコードで置き換えることもできます
#define X sqrt(Y)
あるいは
#define X Y
簡単な答えは、歴史/ハードウェアを表すため、Cには型が必要です。
歴史:Cは1970年代初頭に開発され、システムプログラミング用の言語として意図されていました。コードは理想的に高速で、ハードウェアの機能を最大限に活用します。
コンパイル時に型を推論することは可能でしたが、すでに遅いコンパイル時間は増加していたでしょう( XKCDの「コンパイル」漫画を参照してください。これは、Cが公開されてから少なくとも10年間は「hello world」に適用されていました) )。実行時に型を推測することは、システムプログラミングの目的に適合しなかったでしょう。ランタイム推論には、追加のランタイムライブラリが必要です。 Cは最初のPCよりずっと前に登場しました。 256 RAMがありました。ギガバイトやメガバイトではなく、キロバイト。
あなたの例では、タイプを省略した場合
X=5.2;
Y=5.1;
Z=Y+X;
次に、コンパイラーはXとYが浮動小数点であり、Zを同じにすることを喜んで解決できたはずです。実際、最近のコンパイラーは、XとYが不要で、Zを10.3に設定するだけで済むようにしています。
計算が関数内に埋め込まれていると仮定します。関数の作成者は、ハードウェアの知識、または解決中の問題を使用する場合があります。
Doubleはfloatよりも適切でしょうか?より多くのメモリを必要とし、速度は遅くなりますが、結果の精度は高くなります。
Floatからintへの変換にはコストがないわけではありませんが、小数は重要ではなかったため、関数の戻り値はint(またはlong)である可能性があります。
Float + floatがオーバーフローしないことを保証して、戻り値をdoubleにすることもできます。
これらの質問はすべて、今日書かれたコードの大多数にとっては意味がないように見えますが、Cが作成されたときには不可欠でした。
Cは古いため、型推論(コンパイラーが変数の型を推測するときに呼び出される型)はありません。それは 1970年代初頭に開発されました
新しい言語の多くには、型を指定せずに変数を使用できるシステムがあります(Ruby、javascript、pythonなど)。