Cプログラムの実行中、データはヒープまたはスタックに格納されます。値はRAMアドレスに格納されます。しかし、タイプインジケーター(たとえば、int
またはchar
)はどうですか?それらも格納されますか?
次のコードを検討してください。
char a = 'A';
int x = 4;
Aと4はRAMアドレスにここに格納されています。しかし、a
とx
についてはどうですか?最も混乱しますが、a
がcharであり、x
がintであると実行はどうやって知るのですか? 、int
とchar
はRAMのどこかで言及されていますか?
値がRAMとして10011001に格納されているとします。コードを実行するプログラムである場合、この10011001がchar
であるかint
であるかをどのように知ることができますか?
私が理解していないのは、コンピューターが変数の値を10001などのアドレスから読み取るときに、int
であるかchar
であるかをコンピューターがどのように認識するかです。 anyprog.exe
というプログラムをクリックするとします。すぐにコードが実行を開始します。この実行可能ファイルには、格納されている変数のタイプがint
であるかchar
であるかに関する情報が含まれていますか?
あなたがいくつかのコメントで投稿した質問に対処するには(私はあなたの投稿を編集するべきだと思います):
私が理解していないのは、コンピューターが変数の値を読み取るときに、コンピューターがどのようにして10001などのアドレスを読み取るかを知る方法です。 anyprog.exeというプログラムをクリックしたとします。すぐにコードが実行を開始します。このexeファイルには、変数がinまたはcharとして格納されているかどうかに関する情報が含まれていますか?
int x = 4;
そして、それがRAMに格納されると仮定しましょう:
0x00010004: 0x00000004
最初の部分はアドレス、2番目の部分は値です。プログラム(マシンコードとして実行される)が実行されると、0x00010004
で確認できるのは値0x000000004
だけです。このデータのタイプを「知っている」わけではなく、どのように使用されると「想定されている」のかわかりません。
では、プログラムはどのようにして正しいことを理解するのでしょうか?このコードを考えてみましょう:
int x = 4;
x = x + 5;
ここには、読み取りと書き込みがあります。プログラムがメモリからx
を読み取ると、そこに0x00000004
が見つかります。そして、あなたのプログラムはそれに0x00000005
を追加することを知っています。そして、あなたのプログラムがこれが有効な操作であることを「知っている」理由は、コンパイラがその操作が型安全を通じて有効であることを保証するからです。コンパイラは、4
と5
を一緒に追加できることをすでに確認しています。そのため、バイナリコードが実行されるとき(exe)、その検証を行う必要はありません。すべてがOKであると仮定して、各ステップを盲目的に実行するだけです(悪いことは、実際にはOKでなく、OKであるときに起こります)。
0x00000004: 0x12345678
以前と同じフォーマット-左側にアドレス、右側に値。値はどのタイプですか?この時点で、コンピューターがコードを実行しているときと同じくらい、その値についての情報がわかります。その値に12743を追加するように言った場合、それを行うことができます。システム全体でその操作の影響がどのようになるかはわかりませんが、2つの数値を追加することはあなたが本当に得意なことなので、couldを実行します。それは値をint
にしますか?必ずしもそうとは限らない-表示されるのは、2つの32ビット値と加算演算子だけです。
char A = 'a';
コンピューターは、コンソールにa
を表示することをどのようにして知っていますか?まあ、それには多くのステップがあります。 1つ目は、メモリ内のA
sの場所に移動してそれを読み取ることです。
0x00000004: 0x00000061
ASCIIのa
の16進値は0x61なので、上記はメモリに表示されるものとなる可能性があります。そのため、マシンコードは整数値を認識しています。整数を回転させる方法文字に値を入力して表示しますか?簡単に言うと、コンパイラーは、その遷移を行うために必要なすべてのステップを確実に入力しました。しかし、コンピューター自体(またはプログラム/ exe)は、そのデータの種類がわかりません。その32ビット値は、int
、char
、double
の半分、ポインタ、配列の一部、string
の一部、命令の一部など、何でもかまいません。
ここでは、プログラム(exe)がコンピュータ/オペレーティングシステムと行う可能性のある簡単な相互作用を示します。
プログラム:立ち上げたい。 20 MBのメモリが必要です。
オペレーティングシステム:使用されていない20 MBの空きメモリを見つけて、それらを引き渡します
(重要なメモは、これによりany 20空きMBのメモリが返される可能性があるということです。これらは連続している必要もありません。この時点で、プログラムは通信せずに、メモリ内で動作できます。 OS)
プログラム:メモリの最初のスポットは、32ビット整数変数x
であると想定します。
(コンパイラーは、他の変数へのアクセスがメモリー内のこのスポットに決して触れないことを確認します。最初のバイトが変数x
である、または変数x
が整数であると言うシステムには何もありません。アナロジー:バッグがあります。このバッグには黄色のボールだけを入れるという人がいます。誰かが後でバッグから何かを引っ張ったとき、何かが青や立方体を引き抜くのは衝撃的です。何かがひどく間違っているのです。コンピューターでも同じです。 :プログラムは、最初のメモリスポットが変数xであり、整数であると想定しています。メモリのこのバイトに何か他のものが書き込まれたり、何か別のものであると想定されたりした場合-何かひどいことが起こりました。コンパイラはこれらの種類の事は起こらない)
プログラム:2
を、x
が存在すると想定している最初の4バイトに書き込みます。
プログラム:x
に5を追加します。
Xの値を一時レジスタに読み込みます
一時レジスタに5を追加します
一時レジスタの値を最初のバイトに格納します。これは、x
であると見なされます。
プログラム:次に使用可能なバイトは、char変数y
であると想定します。
プログラム:a
を変数y
に書き込みます。
ライブラリは、a
のバイト値を見つけるために使用されます
バイトは、プログラムがy
であると想定しているアドレスに書き込まれます。
プログラム:y
の内容を表示したい
2番目のメモリスポットの値を読み取ります
ライブラリを使用してバイトから文字に変換します
グラフィックライブラリを使用してコンソール画面を変更します(ピクセルを黒から白に設定、1行スクロールなど)
(そしてここから続く)
おそらくハングアップしているのは、メモリ内の最初のスポットがx
でなくなったときに何が起こるかです。または秒はもはやy
ではありませんか?誰かがx
をchar
として、またはy
をポインターとして読み取るとどうなりますか?つまり、悪いことが起こります。これらのいくつかには明確に定義された動作があり、いくつかには未定義の動作があります。未定義の動作とは、まったく同じです。まったく発生しないものから、プログラムやオペレーティングシステムのクラッシュまで、何でも発生する可能性があります。明確に定義された動作でさえ、悪意がある可能性があります。 x
を自分のプログラムへのポインターに変更して、プログラムにポインターとして使用させることができれば、プログラムに私のプログラムの実行を開始させることができます。これがまさにハッカーの仕事です。コンパイラーは、int x
をstring
として使用しないようにするためのもので、その性質のものです。マシンコード自体はタイプを認識せず、命令が指示することを実行するだけです。実行時に発見される大量の情報もあります。プログラムが使用を許可されているメモリのバイト数はどれですか。 x
は最初のバイトから始まりますか、それとも12番目のバイトから始まりますか?
しかし、このようなプログラムを実際に(そして、アセンブリ言語で)書くのがどれほど恐ろしいか想像できます。まず、変数を「宣言」することから始めます。バイト1がx
、バイト2がy
であることを自分自身に伝え、コードの各行を記述し、レジスタをロードおよび格納するとき、(人間として)どれがどれかを覚えておく必要があります。 x
とy
のどちらであるかは、システムに認識がないためです。そして、あなたは(人間として)x
とy
の型を覚えておく必要があります。
私の主な質問は次のように思われます:「型がコンパイル時に消去され、実行時に保持されない場合、コンピューターはコードを実行してint
として解釈するか、コードを実行するかをどのように知るのですか?これはchar
として解釈されますか?」
そして、答えは…コンピュータにはありません。ただし、コンパイラはdoesであり、正しいコードをバイナリの最初の場所に配置するだけです。変数がchar
として型指定されている場合、コンパイラーはそれをint
として処理するためのコードをプログラムに配置せず、char
。
ある実行時に型を保持する理由:
+
演算子など)を除きます)ので、そのためのランタイム型は必要ありません。理由。ただし、繰り返しになりますが、ランタイムタイプは静的タイプとは少し異なります。 Javaでは、理論的には静的型を削除しても、ポリモーフィズムの実行時型を保持できます。また、型ルックアップコードを分散して特殊化し、それをオブジェクト(またはクラス)内に配置する場合は、必ずしもランタイム型も必要ないことにも注意してください。 C++ vtables。Cで実行時に型を保持する唯一の理由はデバッグのためですが、デバッグは通常、使用可能なソースを使用して行われ、ソースファイルで型を単純に検索できます。
タイプ消去はごく普通のことです。型の安全性には影響しません。型はコンパイル時にチェックされます。プログラムが型セーフであるとコンパイラーが判断すると、型は不要になります(そのため)。静的ポリモーフィズム(別名オーバーロード)には影響しません。オーバーロードの解決が完了し、コンパイラーが適切なオーバーロードを選択すると、型は不要になります。型も最適化の指針となりますが、再度、オプティマイザが型に基づいて最適化を選択すると、それらは不要になります。
実行時に型を保持する必要があるのは、実行時に型を操作する場合のみです。
Haskellは、最も厳密で最も厳密な、タイプセーフな静的型付き言語の1つであり、Haskellコンパイラは通常、すべての型を消去します。 (例外は、型クラスのメソッドディクショナリの引き渡しです。)
コンピュータはどのアドレスが何であるかを「知る」ことはできませんが、プログラムの命令に組み込まれているものは何であるかについての知識です。
Char変数の書き込みと読み取りを行うCプログラムを作成すると、コンパイラはそのデータの一部をcharとして書き込むアセンブリコードを作成します。また、メモリアドレスを読み取り、charとして解釈する他のコードがどこかにあります。これら2つの操作を結び付ける唯一のことは、そのメモリアドレスの場所です。
読む時間になると、指示には「そこにあるデータタイプを確認する」とは表示されず、「そのメモリをフロートとしてロードする」などと表示されます。読み込まれるアドレスが変更された場合、またはメモリがフロート以外の何かでそのメモリを上書きした場合、CPUはとにかくそのメモリをフロートとして喜んでロードし、結果としてあらゆる種類の奇妙なことが発生する可能性があります。
悪いアナロジータイム:倉庫がメモリであり、物を選ぶ人がCPUである複雑な配送倉庫を想像してください。倉庫の「プログラム」の一部では、さまざまなアイテムが棚に置かれます。別のプログラムでは、アイテムを倉庫から取り出して箱に入れます。彼らが引き出されるとき、彼らはチェックされません、彼らはただビンに行きます。倉庫全体は、すべてが同期して機能することによって機能します。適切なアイテムは常に適切な場所に適切なタイミングで存在します。それ以外の場合は、実際のプログラムのようにすべてがクラッシュします。
そうではありません。 Cがマシンコードにコンパイルされると、マシンはたくさんのビットを見るだけです。これらのビットがどのように解釈されるかは、いくつかの追加のメタデータではなく、それらのビットで実行されている操作によって異なります。
ソースコードに入力するタイプは、コンパイラー専用です。それはあなたがデータがそうであるはずであると言うタイプを取り、その能力の及ぶ限りでは、そのデータが意味のある方法でのみ使用されることを確認しようとします。コンパイラーは、ソースコードのロジックをチェックするのに可能な限り良い仕事をすると、それをマシンコードに変換し、型データを破棄します。これは、マシンコードにはそれを表す方法がないためです(少なくともほとんどのマシン) 。
ほとんどのプロセッサは、さまざまなタイプのデータを操作するためのさまざまな命令を提供するため、タイプ情報は通常、生成されたマシンコードに「組み込まれ」ます。追加のタイプメタデータを保存する必要はありません。
いくつかの具体的な例が役立つかもしれません。以下のマシンコードは、SuSE Linux Enterprise Server(SLES)10を実行しているx86_64システムでgcc 4.1.2を使用して生成されました。
次のソースコードを想定します。
_int main( void )
{
int x, y, z;
x = 1;
y = 2;
z = x + y;
return 0;
}
_
上記のソース(_gcc -S
_を使用)に対応する生成されたアセンブリコードの要点を以下に示します。
_main:
.LFB2:
pushq %rbp ;; save the current frame pointer value
.LCFI0:
movq %rsp, %rbp ;; make the current stack pointer value the new frame pointer value
.LCFI1:
movl $1, -12(%rbp) ;; x = 1
movl $2, -8(%rbp) ;; y = 2
movl -8(%rbp), %eax ;; copy the value of y to the eax register
addl -12(%rbp), %eax ;; add the value of x to the eax register
movl %eax, -4(%rbp) ;; copy the value in eax to z
movl $0, %eax ;; eax gets the return value of the function
leave ;; exit and restore the stack
ret
_
ret
の後にいくつか追加の要素がありますが、それはディスカッションには関係ありません。
_%eax
_は32ビットの汎用データレジスタです。 _%rsp
_はスタックポインタを保存するために予約されている64ビットレジスタで、スタックに最後にプッシュされたもののアドレスが含まれています。 _%rbp
_は、現在のスタックフレームのアドレスを含むフレームポインターを保存するために予約されている64ビットレジスタです。関数を入力すると、スタックフレームがスタック上に作成され、関数の引数とローカル変数用にスペースが予約されます。引数と変数には、フレームポインターからのオフセットを使用してアクセスします。この場合、変数x
のメモリは、_%rbp
_に格納されているアドレスの「下」の12バイトです。
上のコードでは、x
の整数値を[movl
命令を使用してレジスタ_%eax
_にレジスター[-12(%rbp)
に保存)にコピーします。 32ビットの単語をある場所から別の場所にコピーします。次に、addl
を呼び出し、y
(-8(%rbp)
に格納)の整数値を、_%eax
_に既にある値に追加します。次に、結果を-4(%rbp)
(z
)に保存します。
次に、これを変更して、double
値ではなくint
値を処理するようにします。
_int main( void )
{
double x, y, z;
x = 1;
y = 2;
z = x + y;
return 0;
}
_
_gcc -S
_を再度実行すると、次のようになります。
_main:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movabsq $4607182418800017408, %rax ;; copy literal 64-bit floating-point representation of 1.00 to rax
movq %rax, -24(%rbp) ;; save rax to x
movabsq $4611686018427387904, %rax ;; copy literal 64-bit floating-point representation of 2.00 to rax
movq %rax, -16(%rbp) ;; save rax to y
movsd -24(%rbp), %xmm0 ;; copy value of x to xmm0 register
addsd -16(%rbp), %xmm0 ;; add value of y to xmm0 register
movsd %xmm0, -8(%rbp) ;; save result to z
movl $0, %eax ;; eax gets return value of function
leave ;; exit and restore the stack
ret
_
いくつかの違い。 movl
とaddl
の代わりに、movsd
とaddsd
を使用します(倍精度浮動小数点数を割り当てて追加します)。暫定値を_%eax
_に格納する代わりに、_%xmm0
_を使用します。
これは、型がマシンコードに「組み込まれている」と私が言っているときの意味です。コンパイラは、特定のタイプを処理するための適切なマシンコードを生成するだけです。
歴史的に、Cはメモリをタイプunsigned char
の番号付きスロットのグループの数で構成されると見なしました(「バイト」とも呼ばれますが、常に8ビットである必要はありません)。メモリに格納されているものを使用するコードは、情報が格納されているスロットを認識し、そこにある情報を使用して何を実行する必要があるかを知る必要があります。 "アドレス123:456で始まる4バイトを32ビット浮動小数点値として解釈する"または "最新の計算量の下位16ビットをアドレス345:678で始まる2バイトに格納する]。メモリ自体もまたメモリスロットに格納されている値が「意味のある」ものであることを知りません。コードが1つのタイプを使用してメモリに書き込み、別のタイプとして読み取ろうとした場合、書き込みによって格納されたビットパターンは、2番目のタイプのルールに従って解釈されます。結果が生じる可能性があるものは何でも。
たとえば、コードが0x12345678
を32ビットのunsigned int
に格納し、そのアドレスと上記のアドレスから2つの連続する16ビットのunsigned int
値を読み取ろうとした場合、 unsigned int
の半分が格納された場所で、コードは値0x1234と0x5678、または0x5678と0x1234を読み取る可能性があります。
ただし、C99標準メモリが、ビットパターンが何を表すかについて何も知らない番号付きスロットの束として動作する必要はなくなりました。コンパイラーは、メモリースロットがそれらに格納されているデータのタイプを認識しているかのように動作することができ、unsigned char
以外のタイプを使用して書き込まれたデータは、どちらかのタイプを使用して読み取ることができますunsigned char
またはそれが書かれたのと同じタイプ。さらに、コンパイラーは、メモリスロットがそれらのルールに反する方法でメモリにアクセスしようとするプログラムの動作を任意に破壊する能力と傾向を持っているかのように動作することができます。
与えられた:
unsigned int a = 0x12345678;
unsigned short p = (unsigned short *)&a;
printf("0x%04X",*p);
実装によっては0x1234を出力するものもあれば、0x5678を出力するものもありますが、C99標準では、実装が「FRINK RULES!」を出力することは合法です。または、a
を保持するメモリ位置に、それらを書き込むために使用されたタイプを記録するハードウェアを含めること、およびそのようなハードウェアがいずれかの無効な読み取り試行に応答することは正当であるという理論に基づいて何かを行います「FRINK RULES!」を引き起こすことを含め、あらゆるファッション出力されます。
そのようなハードウェアが実際に存在するかどうかは問題ではないことに注意してください。そのようなハードウェアが合法的に存在する可能性があるという事実は、コンパイラがそのようなシステムで実行されているかのように動作するコードを生成することを合法にします。特定のメモリ位置が1つのタイプとして書き込まれ、別のタイプとして読み取られるとコンパイラが判断できる場合、ハードウェアがそのような判断を行うシステム上で実行されているように見せかけて、コンパイラ作成者が適切と考える気まぐれさの程度で応答する可能性があります。 。
このルールの目的は、あるタイプの値を保持するバイトのグループが特定の時点で特定の値を保持し、それ以降同じタイプの値が書き込まれていないことを知っているコンパイラがそのグループを推測できるようにすることですのバイトはまだその値を保持します。たとえば、プロセッサがバイトのグループをレジスタに読み込んだ後、同じ情報をまだレジスタにある間にもう一度使用したい場合、コンパイラはメモリから値を再読み込みすることなくレジスタの内容を使用できます。便利な最適化。ルールの最初の約10年間、これに違反すると、一般に、変数がそれを読み取るために使用されるタイプ以外のタイプで書き込まれる場合、書き込みは読み取られる値に影響する場合と影響しない場合があります。このような動作は、一部のケースでは悲惨な場合がありますが、他のケースでは、特に値を読み取っているコードが書き込まれた値または書き込み前に保持された値に等しく満足する場合、または値が書き込まれた場合にさらに害を及ぼす場合もあります。すでに保持されている値と偶然一致しました。
ただし、2009年頃、CLANGのような一部のコンパイラの作成者は、メモリが1つのタイプを使用して書き込まれ、別のタイプとして読み取られる場合、コンパイラは好きなことを実行できるため、コンパイラはプログラムがそのようなことを引き起こします。標準では、このような無効な入力を受け取ったときにコンパイラが好きなことを行うことができると規定しているため、標準が要件を課さない場合にのみ影響するコードは省略できます(一部のコンパイラ作成者の見解では、そうする必要があります)。無関係として。これにより、エイリアス違反の動作が、読み取り要求が与えられたときに、読み取り要求と同じタイプを使用して書き込まれた最後の値または他のタイプを使用して書き込まれた最新の値を任意に返すようなメモリのようなものから、メモリのようなものに変更されます。標準で許可されている場合はいつでも、プログラムの動作を気まぐれに変更します。
Cではありません。他の言語(LISP、Pythonなど)には動的型がありますが、Cは静的型です。つまり、プログラムは、データが文字、整数などとして適切に解釈するためのタイプを認識している必要があります。
通常、コンパイラーがこれを処理します。何かが間違っていると、コンパイル時エラー(または警告)が表示されます。
一方でcompiletime
とruntime
を区別し、もう一方でcode
とdata
を区別する必要があります。
マシンの観点からは、code
またはinstructions
と呼ぶものとdata
と呼ぶものに違いはありません。それはすべて数字に帰着します。しかし、いくつかのシーケンス-code
と呼ぶもの-便利だと思うことを実行するものもあれば、単純にcrash
マシンを実行するものもあります。
CPUによって実行される作業は、単純な4ステップのループです。
instruction
として「解釈」します)これは 命令サイクル と呼ばれます。
Aと4はRAMここのアドレスに格納されています。しかし、aとxはどうですか?
a
とx
は変数であり、プログラムは変数の「内容」を見つけることができるアドレスのプレースホルダーです。したがって、変数a
が使用されるときは常に、[実質的に使用されるa
のコンテンツのアドレスが存在します。
最も紛らわしいことに、実行はaがcharであり、xがintであることをどのようにして知るのでしょうか。
実行は何も知らない。はじめに言ったことから、CPUはデータをフェッチするだけで、このデータを命令として解釈します。
printf -関数は、どのような入力を入力するかを「知る」ように設計されています。つまり、結果のコードは、特別なメモリセグメントを処理する方法を適切に指示します。もちろん、意味のない出力を生成することも可能です。printf()
に「%s」とともに文字列が格納されていないアドレスを使用すると、ランダムなメモリ位置によってのみ意味のない出力が停止します。 (\0
)です。
同じことが、プログラムのエントリポイントにも当てはまります。 C64では、プログラムを(ほぼ)すべての既知のアドレスに置くことができました。アセンブリプログラムは、sys
という命令の後にアドレスが続く形で開始されました。sys 49152
は、アセンブラコードを配置するための一般的な場所です。しかし、ロードを妨げるものは何もありません。グラフィカルデータを49152
に変換すると、この時点から「起動」した後にマシンがクラッシュします。この場合、命令サイクルは、「グラフィックデータ」を読み取り、それを「コード」として解釈しようとすることから始まりました(もちろん、意味がありません)。効果は驚異的なものでした;)
値がRAMとして10011001に格納されているとしましょう。もし私がコードを実行するプログラムである場合、この10011001がcharかintかをどうやって知るのでしょうか?
前述のように、「コンテキスト」、つまり前の命令と次の命令は、データを希望どおりに処理するのに役立ちます。マシンの観点からは、メモリの場所に違いはありません。 int
とchar
は単なる語彙であり、compiletime
では意味があります。 runtime
(アセンブリレベル)では、char
またはint
はありません。
私が理解していないのは、コンピューターが変数の値を10001などのアドレスから読み取るときに、それがintであるかcharであるかをどのように認識するかです。
コンピュータ知っている何もない。 programmerはそうです。コンパイルされたコードはcontextを生成します。これは、人間にとって意味のある結果を生成するために必要です。
この実行可能ファイルには、格納されている変数のタイプがintかcharかに関する情報が含まれていますか
はいおよびいいえ。 int
であるかchar
であるかの情報は失われます。しかし、その一方で、コンテキスト(データが格納されているメモリの場所を処理する方法を指示する命令)は保持されます。だからimplicitelyはい、「情報」はimplicitelyで利用できます。
この議論を[〜#〜] c [〜#〜]言語のみに留めましょう。
あなたが参照しているプログラムはCのような高級言語で書かれています。コンピューターは機械語しか理解しません。高級言語は、プログラマーにロジックをより人間にやさしい方法で表現する能力を与えます。それは、マイクロプロセッサーがデコードして実行できるマシンコードに変換されます。次に、あなたが言及したコードについて説明します。
char a = 'A';
int x = 4;
各部分を分析してみましょう:
char/intはデータ型と呼ばれます。これらは、メモリを割り当てるようコンパイラーに指示します。
char
の場合は1バイト、int
は2バイトになります。 (このメモリサイズもマイクロプロセッサに依存することに注意してください)。a/xは識別子と呼ばれます。これで、RAMのメモリ位置に付けられた「ユーザーフレンドリー」な名前を言うことができます。
=は、 'A'を
a
のメモリ位置に、4をメモリ位置x
に格納するようコンパイラに指示します。
そのため、int/charデータ型識別子は、コンパイラによってのみ使用され、プログラムの実行中にマイクロプロセッサでは使用されません。したがって、それらはメモリに保存されません。
ここでの私の答えは多少簡略化されており、Cのみを参照します。
int
またはchar
は、CPUに対するタイプインジケータではありません。コンパイラにのみ。
変数がint
として宣言されている場合、コンパイラーによって作成されたexeには、int
sを操作するための命令があります。同様に、変数がchar
として宣言されている場合、exeにはchar
を操作するための命令が含まれます。
C:
int main()
{
int a = 65;
char b = 'A';
if(a == b)
{
printf("Well, what do you know. A char can equal an int.\n");
}
return 0;
}
char
とint
のRAMにはと同じvaluesがあるため、このプログラムはメッセージを出力します。
ここで、printf
が65
はint
の場合、A
の場合はchar
です。これは、「フォーマット文字列」でprintf
がvalueを処理する方法を指定する必要があるためです。
(例えば、 %c
は、値をchar
として扱うことを意味し、%d
は、値を整数として扱うことを意味します。どちらにしても同じ値です。)
簡単に言えば、型はコンパイラが生成するCPU命令にエンコードされます。
情報のタイプまたはサイズに関する情報は直接格納されませんが、これらの変数の値にアクセス、変更、および格納するときに、コンパイラーはこの情報を追跡します。
実行は、aがcharであり、xがintであることをどのようにして知るのですか?
それはしませんが、コンパイラが知っているマシンコードを生成するとき。 int
とchar
は異なるサイズにすることができます。 charが1バイトのサイズで、intが4バイトのアーキテクチャでは、変数x
はアドレス10001にはなく、10002、10003、10004にもあります。コードが値をロードする必要がある場合x
のCPUレジスタに、4バイトをロードするための命令を使用します。 charをロードするとき、1バイトをロードする命令を使用します。
2つの指示のどちらを選択するか?コンパイラーはコンパイル時に決定しますが、メモリー内の値を検査した後は実行時に行われません。
また、レジスタのサイズが異なる場合があることに注意してください。 Intel x86 CPUでは、EAXは32ビット幅で、その半分はAX(16)であり、AXはAHとALの両方に分割されており、どちらも8ビットです。
したがって、(x86 CPUで)整数をロードする場合は、整数のMOV命令を使用して、charのMOV命令を使用してcharをロードします。どちらもMOVと呼ばれますが、オペコードが異なります。事実上、2つの異なる指示である。変数のタイプは、使用する命令にエンコードされています。
他の操作でも同じことが起こります。オペランドのサイズに応じて、またそれらが符号付きまたは符号なしであっても、加算を実行するための多くの命令があります。 https://en.wikipedia.org/wiki/ADD_(x86_instruction) を参照してください。さまざまな追加がリストされています。
値がRAMとして10011001に格納されているとしましょう。もし私がコードを実行するプログラムである場合、この10011001がcharかintかをどうやって知るのでしょうか。
最初に、charは10011001ですが、intは00000000 00000000 00000000 10011001になります。これは、それらが異なるサイズであるためです(上記と同じサイズのコンピューター上)。しかし、signed char
対unsigned char
。
メモリの場所に保存されているものは、とにかく解釈することができます。 Cコンパイラの責任の一部は、変数に格納され、変数から読み取られるものが一貫した方法で行われるようにすることです。つまり、プログラムがメモリの場所に何が格納されているかを知っているのではなく、常に同じ種類のものを読み書きすることを事前に認めているということです。 (キャストタイプなどは数えません)。
最下位レベルでは、実際の物理CPUにはタイプはまったくありません(浮動小数点ユニットは無視されます)。ビットの単なるパターン。コンピュータは、ビットのパターンを非常に高速に操作することによって機能します。
CPUでできることはこれだけです。 intやcharなどはありません。
x = 4 + 5
次のように実行されます:
Iadd命令は、レジスター1および2が整数であるかのように動作するハードウェアをトリガーします。それらが実際に整数を表さない場合、あらゆる種類のものが後でうまくいかない可能性があります。通常、最良の結果はクラッシュします。
ソースで指定されたタイプに基づいて正しい命令を選択するのはコンパイラーですが、CPUによって実行される実際のマシンコードでは、どこにもタイプはありません。
編集:実際のマシンコードは実際には4、5、整数については言及していません。それはビットの2つのパターンだけであり、2つのビットパターンを受け取り、それらがintであると想定してそれらを加算する命令です。
しかし、なぜC#のケースでここで言うのか、それは物語ではありませんか?他のコメントをいくつか読んだところ、C#とC++ではストーリー(データ型に関する情報)が異なり、CPUでさえ計算ができないとのことです。それについてのアイデアは?
C#などの型チェック言語では、型チェックはコンパイラーによって行われます。コードベンジは書きました:
int main()
{
int a = 65;
char b = 'A';
if(a == b)
{
printf("Well, what do you know. A char can equal an int.\n");
}
return 0;
}
単にコンパイルを拒否します。同様に、文字列と整数を乗算しようとした場合(私はaddと言っていましたが、演算子 '+'は文字列連結でオーバーロードされており、うまくいくかもしれません)。
int a = 42;
string b = "Compilers are awesome.";
double[] c = a * b;
コンパイラーは、文字列がどれだけキスしたかに関係なく、このC#からのマシンコードの生成を拒否します。