特定のタイプの変数(私が知る限り、変数の内容にデータを割り当てるだけ)を定義した場合、どのタイプの変数であるかをどのように追跡しますか?
特定のタイプの変数(私が知る限り、変数の内容にデータを割り当てるだけ)を定義した場合、どのタイプの変数であるかをどのように追跡しますか?
回答:
変数(またはより一般的には、Cの意味での「オブジェクト」)は、実行時にその型を格納しません。マシンコードに関する限り、型付けされていないメモリのみがあります。代わりに、このデータに対する操作は、データを特定のタイプ(フロートまたはポインターなど)として解釈します。型は、コンパイラーによってのみ使用されます。
たとえば、構造体またはクラスstruct Foo { int x; float y; };
と変数がありますFoo f {}
。フィールドアクセスはどのauto result = f.y;
ようにコンパイルできますか?コンパイラf
は、それが型のオブジェクトであるFoo
こと、およびFoo
-objects のレイアウトを知っています。プラットフォーム固有の詳細に応じて、これは「のポインタをf
取得して、4バイトを追加し、4バイトをロードして、このデータを浮動小数点数として解釈します」とコンパイルされる場合があります。 )floatまたはintをロードするためのさまざまなプロセッサ命令があります。
C ++型システムが型を追跡できない1つの例は、のような共用体union Bar { int as_int; float as_float; }
です。ユニオンには、さまざまなタイプのオブジェクトが1つまで含まれます。オブジェクトをユニオンに格納する場合、これはユニオンのアクティブな型です。その型をユニオンから取り戻そうとするだけでよく、それ以外は未定義の動作になります。アクティブな型が何であるかをプログラミング中に「知る」か、型タグ(通常は列挙型)を個別に格納するタグ付きユニオンを作成できます。これはCの一般的な手法ですが、ユニオンとtypeタグの同期を維持する必要があるため、かなりエラーが発生しやすくなります。void*
ポインタは、組合に似ていますが、唯一の関数ポインタを除き、ポインタのオブジェクトを保持することができます。
C ++申し出不明な種類のオブジェクトを扱うための2つの優れたメカニズム:我々は実行するために、オブジェクト指向技術を使用することができるタイプの消去を(私たちは実際のタイプを知る必要がないように仮想メソッドを介してオブジェクトにのみ相互作用)、または我々はできますstd::variant
タイプセーフなユニオンの一種であるuseを使用します。
C ++がオブジェクトの型を保存する場合が1つあります。オブジェクトのクラスに仮想メソッド(「ポリモーフィック型」、別名インターフェース)がある場合です。仮想メソッド呼び出しのターゲットはコンパイル時には不明であり、実行時にオブジェクトの動的タイプに基づいて解決されます(「動的ディスパッチ」)。ほとんどのコンパイラは、オブジェクトの先頭に仮想関数テーブル(「vtable」)を保存することでこれを実装します。vtableは、実行時にオブジェクトのタイプを取得するためにも使用できます。その後、コンパイル時の既知の式の静的型と、実行時のオブジェクトの動的型を区別できます。
C ++では、オブジェクトを提供するtypeid()
演算子を使用して、オブジェクトの動的な型を検査できstd::type_info
ます。コンパイラは、コンパイル時にオブジェクトのタイプを知っているか、コンパイラがオブジェクト内に必要なタイプ情報を保存しており、実行時にそれを取得できます。
void*
)。
typeid(e)
式の静的型を内省しますe
。静的型が多相型の場合、式が評価され、そのオブジェクトの動的型が取得されます。typeidを不明なタイプのメモリに向けて、有用な情報を取得することはできません。たとえば、共用体のtypeidは、共用体のオブジェクトではなく、共用体を表します。aのtypeid void*
は単なるvoidポインターです。また、a void*
を逆参照してその内容を取得することはできません。C ++では、明示的にそのようにプログラムされていない限り、ボクシングはありません。
もう1つの答えは技術的な側面をよく説明していますが、いくつかの一般的な「マシンコードについての考え方」を追加したいと思います。
コンパイル後のマシンコードはかなり馬鹿げており、実際にはすべてが意図したとおりに動作することを前提としています。次のような単純な関数があるとします
bool isEven(int i) { return i % 2 == 0; }
intを受け取り、boolを吐き出します。
コンパイルしたら、この自動オレンジジューサーのようなものと考えることができます。
オレンジを取り、ジュースを返します。入ってくるオブジェクトのタイプを認識しますか?いいえ、それらはオレンジであることになっています。オレンジではなくリンゴを受け取ったらどうなりますか?おそらく壊れるでしょう。責任のある所有者はこの方法で使用しようとしないため、問題ではありません。
上記の関数は似ています:intを取るように設計されており、他の何かを与えられたときに壊れたり、無関係なことをしたりします。(通常)コンパイラは(通常)発生しないことを確認するため、問題になりません。実際、整形式のコードでは発生しません。コンパイラーは、関数が誤った型付き値を取得する可能性を検出すると、コードのコンパイルを拒否し、代わりに型エラーを返します。
警告は、コンパイラが渡す不正なコードのいくつかのケースがあるということです。例は次のとおりです。
void*
しないことを保証するのはプログラマーですorange*
。前述のように、コンパイルされたコードはジューサーマシンに似ています。処理するものが分からず、命令を実行するだけです。そして、指示が間違っていると、壊れます。これが、C ++の上記の問題が制御不能なクラッシュを引き起こす理由です。
void*
強制しますが、悪いポインタを持っているだけでもUBなどです。そのまま。foo*
union
NULL
nullptr
void*
では暗黙的にに変換されずfoo*
、union
型のパニングはサポートされていません(UBがあります)。
Cのような言語では、変数にはいくつかの基本的なプロパティがあります。
ソースコードでは、場所(5)は概念的であり、この場所はその名前(1)で参照されます。そのため、変数宣言を使用して値の場所とスペースを作成します(6)。ソースの他の行では、式で変数に名前を付けることで、その場所と保持する値を参照します。
コンパイラーによってプログラムがマシンコードに変換されると、場所(5)はメモリまたはCPUレジスタの場所であり、変数を参照するソースコード式はそのメモリを参照するマシンコードシーケンスに変換されます。またはCPUレジスタの場所。
したがって、翻訳が完了し、プログラムがプロセッサで実行されると、変数の名前はマシンコード内で事実上忘れられ、コンパイラによって生成された命令は、変数の割り当てられた場所のみを参照します名前)。デバッグしてデバッグを要求している場合、名前に関連付けられた変数の場所がプログラムのメタデータに追加されますが、プロセッサはまだ場所を使用してマシンコード命令を表示します(そのメタデータではありません)。(これは、リンク、ロード、および動的ルックアップの目的でプログラムのメタデータに名前がいくつかあるため、過度に単純化されています。プロセッサは、プログラムに対して指示されたマシンコード命令を実行します。場所に変換されました。)
同じことは、タイプ、スコープ、およびライフタイムにも当てはまります。コンパイラが生成したマシンコード命令は、値を保存する場所のマシンバージョンを知っています。typeなどの他のプロパティは、変数の場所にアクセスする特定の命令として翻訳されたソースコードにコンパイルされます。たとえば、問題の変数が符号付き8ビットバイトと符号なし8ビットバイトの場合、変数を参照するソースコードの式は、たとえば符号付きバイトロードと符号なしバイトロードに変換されます。 (C)言語のルールを満たすために必要に応じて。したがって、変数のタイプは、ソースコードの機械語命令への変換にエンコードされます。これは、変数の位置を使用するたびにメモリまたはCPUレジスタの位置を解釈する方法をCPUに命令します。
本質は、プロセッサのマシンコード命令セットの命令(およびその他の命令)を介して、CPUに何をすべきかを伝えなければならないということです。プロセッサは、実行したことや指示したことをほとんど覚えていません。指定された命令のみを実行し、変数を適切に操作するための命令シーケンスの完全なセットを提供するのはコンパイラまたはアセンブリ言語プログラマの仕事です。
プロセッサは、byte / word / int / long signed / unsigned、float、doubleなどの基本的なデータ型を直接サポートします。通常、同じメモリ位置を符号付きまたは符号なしとして交互に扱う場合、プロセッサは文句を言いません。たとえば、通常はプログラムの論理エラーになりますが。変数とのすべての対話でプロセッサに指示するのはプログラミングの仕事です。
これらの基本的なプリミティブ型を超えて、データ構造で物事をエンコードし、それらのプリミティブに関してアルゴリズムを使用してそれらを操作する必要があります。
C ++では、ポリモーフィズムのクラス階層に関与するオブジェクトには、通常オブジェクトの先頭に、仮想ディスパッチ、キャストなどに役立つクラス固有のデータ構造を指すポインターがあります。
要約すると、それ以外の場合、プロセッサはストレージロケーションの使用目的を認識または記憶しません。CPUレジスタおよびメインメモリのストレージを操作する方法を指示するプログラムのマシンコード命令を実行します。プログラミングは、ソフトウェア(およびプログラマー)の仕事であり、ストレージを有意義に使用し、プログラム全体を忠実に実行する一貫したマシンコード命令セットをプロセッサに提示します。
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
、clangとgccは、両方が同じから派生していても、ポインタunionArray[j].member2
がアクセスできないと仮定する傾向があります。unionArray[i].member1
unionArray[]
特定のタイプの変数を定義すると、変数のタイプをどのように追跡しますか。
ここには、2つの関連するフェーズがあります。
Cコンパイラは、Cコードを機械語にコンパイルします。コンパイラには、ソースファイル(およびライブラリ、およびそのジョブを実行するために必要なその他のもの)から取得できるすべての情報が含まれています。Cコンパイラは、何が何を意味するかを追跡します。Cコンパイラは、変数をとして宣言するとchar
、それがcharであることを認識します。
これは、変数の名前、タイプ、およびその他の情報をリストする、いわゆる「シンボルテーブル」を使用してこれを行います。これはかなり複雑なデータ構造ですが、人間が読める名前の意味を追跡していると考えることができます。コンパイラからのバイナリ出力では、このような変数名はもう表示されません(プログラマが要求する可能性のあるオプションのデバッグ情報を無視した場合)。
コンパイラの出力-コンパイルされた実行可能ファイル-は機械語で、OSによってRAMにロードされ、CPUによって直接実行されます。機械語では、「タイプ」という概念はまったくありません。RAMの特定の場所で動作するコマンドしかありません。コマンドは、実際に、彼らは(すなわち、「RAMの場所は0x100と0x521に格納されているこれらの2つの16ビット整数を追加し、」機械語命令があるかもしれない)で動作し、固定タイプを持っていますが、何も情報がないどこかのシステムでは、ということこれらの場所のバイトは実際には整数を表します。型エラーからの保護がありませんすべてで、ここは。
char *ptr = 0x123
Cなど)。この文脈では、「ポインタ」という言葉の使用法はかなり明確にすべきだと思います。そうでない場合は、お気軽にお知らせください。回答に文章を追加します。
C ++が実行時に型を格納する重要な特別なケースがいくつかあります。
古典的な解決策は、差別化された共用体です。いくつかのタイプのオブジェクトの1つを含むデータ構造と、現在含まれているタイプを示すフィールドです。テンプレートバージョンは、C ++標準ライブラリにとしてありstd::variant
ます。通常、タグはになりenum
ますが、データ用にストレージのすべてのビットが必要でない場合は、ビットフィールドになります。
これの他の一般的なケースは、動的型付けです。あなたは、ときにclass
持っているvirtual
機能を、プログラムが中にその関数へのポインタを格納する仮想関数テーブル、それはのインスタンスごとに初期化されます、class
それが構築されたとき。通常、これはすべてのクラスインスタンスに対して1つの仮想関数テーブルを意味し、各インスタンスは適切なテーブルへのポインターを保持します。(これは、テーブルが単一のポインターよりもはるかに大きいため、時間とメモリを節約します。)virtual
ポインターまたは参照を介してその関数を呼び出すと、プログラムは仮想テーブルで関数ポインターを検索します。(コンパイル時に正確な型がわかっている場合は、この手順をスキップできます。)これにより、コードは基本クラスの代わりに派生型の実装を呼び出すことができます。
ここで関連するのは、それぞれに仮想テーブルofstream
へのポインタ、ofstream
仮想テーブルへのポインタなどが含まれていることです。クラス階層の場合、仮想テーブルポインターはタグとして機能し、プログラムにクラスオブジェクトの型を伝えます。ifstream
ifstream
言語の標準は、彼らはボンネットの下にランタイムを実装する必要がありますどのようにコンパイラを設計し、人々を教えてくれませんが、これはあなたが期待することができる方法であるdynamic_cast
とtypeof
の仕事に。