web-dev-qa-db-ja.com

C ++の変数はどのようにその型を格納しますか?

特定のタイプの変数を定義する場合(私が知る限り、変数のコンテンツにデータを割り当てるだけです)、どのタイプの変数であるかをどのように追跡しますか?

42
Finn McClusky

変数(より一般的には、Cの意味での「オブジェクト」)は、実行時にその型を格納しません。マシンコードに関する限り、型付けされていないメモリしかありません。代わりに、このデータに対する操作は、データを特定のタイプ(フロートまたはポインターなど)として解釈します。タイプはコンパイラーによってのみ使用されます。

たとえば、構造体またはクラス_struct Foo { int x; float y; };_と変数_Foo f {}_があるとします。フィールドアクセス_auto result = f.y;_はどのようにコンパイルできますか?コンパイラは、fFooタイプのオブジェクトであること、およびFoo- objectsのレイアウトを認識しています。プラットフォーム固有の詳細に応じて、これは「fの先頭へのポインターを取得し、4バイトを追加してから、4バイトをロードしてこのデータを浮動小数点数として解釈する」としてコンパイルされる場合があります。多くのマシンコード命令セット(x86-64を含む)には、floatまたはintをロードするためのさまざまなプロセッサ命令があります。

C++型システムが型を追跡できない一例は、_union Bar { int as_int; float as_float; }_のような共用体です。ユニオンには、さまざまなタイプのオブジェクトが1つまで含まれます。ユニオンにオブジェクトを格納する場合、これはユニオンのアクティブタイプです。その型をunionから戻そうとする必要があります。それ以外は未定義の動作になります。アクティブな型が何であるかをプログラミング中に「知る」か、または型タグ(通常は列挙型)を個別に格納するタグ付き共用体を作成できます。これはCでは一般的な手法ですが、ユニオンとタイプタグを同期させる必要があるため、これはかなりエラーが発生しやすくなります。 _void*_ポインターは共用体に似ていますが、関数ポインターを除いて、ポインターオブジェクトのみを保持できます。
C++は、未知のタイプのオブジェクトを処理するための2つの優れたメカニズムを提供します。オブジェクト指向の手法を使用して、タイプの消去を実行できます(仮想メソッドを通じてのみオブジェクトと対話します)実際の型を知る必要がないようにするため)、または型安全な共用体の一種である_std::variant_を使用できます。

C++がオブジェクトのタイプを格納するケースが1つあります。オブジェクトのクラスに仮想メソッド(「ポリモーフィックタイプ」、別名インターフェイス)がある場合です。仮想メソッド呼び出しのターゲットはコンパイル時には不明であり、オブジェクトの動的タイプ(「動的ディスパッチ」)に基づいて実行時に解決されます。ほとんどのコンパイラは、オブジェクトの先頭に仮想関数テーブル(「vtable」)を格納することでこれを実装します。 vtableを使用して、実行時にオブジェクトのタイプを取得することもできます。次に、コンパイル時の既知の静的な式の型と、実行時のオブジェクトの動的な型を区別します。

C++では、_std::type_info_オブジェクトを取得するtypeid()演算子を使用して、オブジェクトの動的タイプを検査できます。コンパイラーは、コンパイル時にオブジェクトのタイプを知っているか、コンパイラーが必要なタイプ情報をオブジェクト内に格納しており、実行時にそれを取得できます。

106
amon

もう1つの答えは技術的な側面をよく説明していますが、いくつかの一般的な「マシンコードについての考え方」を追加したいと思います。

コンパイル後のマシンコードはかなり馬鹿げており、実際にはすべてが意図したとおりに機能することを前提としています。次のような単純な関数があるとします

bool isEven(int i) { return i % 2 == 0; }

Intを取り、boolを吐き出します。

コンパイルしたら、次のような自動オレンジジューサーのように考えることができます。

automatic orange juicer

オレンジを取り入れてジュースを返します。取得するオブジェクトのタイプを認識しますか?いいえ、それらはオレンジであることになっています。オレンジの代わりにApple=を取得した場合はどうなりますか?おそらく壊れるでしょう。責任ある所有者がこの方法で使用しようとしないので、問題ではありません。

上記の関数は似ています:intを取るように設計されており、他の何かを与えられたときに壊れたり、無関係なことをしたりする可能性があります。コンパイラは(通常)発生しないことを確認するため、それは(通常)重要ではありません。実際、整形式のコードでは発生しません。関数が誤った型付き値を取得する可能性をコンパイラーが検出した場合、コンパイラーはコードのコンパイルを拒否し、代わりに型エラーを返します。

警告は、コンパイラが通過する不正なコードのいくつかのケースがあることです。次に例を示します。

  • 不適切な型キャスト:明示的なキャストは正しいと見なされ、ポインターの反対側にApple=がある場合にvoid*orange*にキャストしていないことをプログラマーが確認する必要があります。 、
  • nullポインター、ダングリングポインター、use-after-scopeなどのメモリ管理の問題。コンパイラはそれらのほとんどを見つけることができません、
  • 他に何か足りないものがあると確信しています。

言ったように、コンパイルされたコードはジューサーマシンのようなものです-何を処理するのかわからず、命令を実行するだけです。そして、指示が間違っていると、壊れます。そのため、C++での上記の問題により、制御不能なクラッシュが発生します。

52
Frax

Cのような言語では、変数にはいくつかの基本的なプロパティがあります。

  1. 名前
  2. タイプ
  3. スコープ
  4. 一生
  5. 場所
  6. 価値

ソースコード内、場所(5)は概念的なものであり、この場所は名前(1)で参照されます。したがって、変数宣言を使用して値(6)の場所とスペースを作成します。ソースの他の行では、式で変数に名前を付けることにより、その場所とその値を参照します。

少し単純化すると、プログラムがコンパイラによってマシンコードに変換されると、場所(5)はメモリまたはCPUレジスタの場所であり、変数を参照するソースコード式は、そのメモリを参照するマシンコードシーケンスに変換されます。またはCPUレジスタの場所。

したがって、変換が完了し、プログラムがプロセッサで実行されている場合、変数の名前はマシンコード内で事実上忘れられ、コンパイラによって生成された命令は、変数の割り当てられた場所のみを参照します名前)。デバッグしてデバッグを要求している場合、名前に関連付けられた変数の場所がプログラムのメタデータに追加されますが、プロセッサは場所を使用したマシンコード命令(メタデータではない)を引き続き表示します。 (一部の名前はリンク、ロード、および動的検索の目的でプログラムのメタデータにあるため、これは過度に単純化されています。それでも、プロセッサーはプログラムに対して指示されたマシンコード命令を実行するだけで、このマシンコードには名前があります場所に変換されます。)

タイプ、スコープ、ライフタイムについても同様です。コンパイラが生成したマシンコード命令は、値を格納する場所のマシンバージョンを認識しています。 typeのような他のプロパティは、変数の場所にアクセスする特定の命令として翻訳されたソースコードにコンパイルされます。たとえば、問題の変数が符号付き8ビットバイトと符号なし8ビットバイトの場合、変数を参照するソースコード内の式は、たとえば、符号付きバイトロードと符号なしバイトロードに変換されます。 (C)言語のルールを満たすために必要に応じて。したがって、変数のタイプは、ソースコードから機械命令への変換にエンコードされます。これは、変数の位置を使用するたびに、CPUにメモリまたはCPUレジスタの位置を解釈する方法を指示します。

本質は、プロセッサのマシンコード命令セットの命令(およびより多くの命令)を介して何をすべきかをCPUに伝えなければならないということです。プロセッサは、実行したことや通知されたことをほとんど覚えていません。指定された命令のみを実行します。変数を適切に操作するための完全な命令シーケンスセットを提供するのは、コンパイラまたはアセンブリ言語プログラマの仕事です。

プロセッサは、バイト/ワード/整数/長い符号付き/符号なし、浮動小数点数、倍精度数など、いくつかの基本的なデータ型を直接サポートしています。たとえば、通常はプログラムの論理エラーです。変数とのすべての相互作用でプロセッサに指示するのはプログラミングの仕事です。

これらの基本的なプリミティブタイプ以外に、データ構造に物事をエンコードし、アルゴリズムを使用してそれらをプリミティブの観点から操作する必要があります。

C++では、ポリモーフィズムのクラス階層に関与するオブジェクトには、通常オブジェクトの先頭に、クラス固有のデータ構造を参照するポインターがあり、仮想ディスパッチ、キャストなどに役立ちます。

要約すると、それ以外の場合、プロセッサはストレージロケーションの使用目的を知らないか覚えていません。CPUレジスタとメインメモリのストレージを操作する方法を指示するプログラムのマシンコード命令を実行します。したがって、プログラミングはソフトウェア(およびプログラマー)の仕事であり、ストレージを有意義に使用し、プログラム全体を忠実に実行する一貫した一連の機械コード命令をプロセッサーに提示します。

3
Erik Eidt

特定のタイプの変数を定義した場合、それはどのように変数のタイプを追跡するのですか。

ここには2つの関連するフェーズがあります。

  • コンパイル時間

Cコンパイラは、Cコードを機械語にコンパイルします。コンパイラーは、ソースファイル(およびライブラリー、およびその他の作業に必要なもの)から取得できるすべての情報を持っています。 Cコンパイラは、何が何を意味するかを追跡します。 Cコンパイラは、変数をcharとして宣言すると、それがcharであることを認識します。

これは、変数の名前、型、およびその他の情報をリストする、いわゆる「シンボルテーブル」を使用して行われます。これはかなり複雑なデータ構造ですが、人間が読める名前の意味を追跡していると考えることができます。コンパイラーからのバイナリー出力では、このような変数名はもう表示されません(プログラマーが要求するオプションのデバッグ情報を無視した場合)。

  • ランタイム

コンパイラの出力-コンパイルされた実行可能ファイル-は機械語であり、OSによってRAMにロードされ、CPUによって直接実行されます。機械語では、「タイプ」の概念はありません。 "まったく-RAMの特定の場所で動作するコマンドしかありません。commandsは、実際に動作する固定タイプを持っています(つまり、これら2つの16- RAM場所0x100および0x521 ")に格納されているビット整数ですが、システムにはこれらの場所のバイトが実際に整数を表しているという情報はありませんanywhere。型エラーからの保護なしまったくここにあります。

2
AnoE

C++が実行時に型を格納するいくつかの重要な特別な場合があります。

古典的な解決策はdiscriminated union:いくつかのタイプのオブジェクトの1つを含むデータ構造と、そのオブジェクトに現在含まれているタイプを示すフィールドです。テンプレートバージョンは、C++標準ライブラリにstd::variantとして含まれています。通常、タグはenumですが、データ用のストレージのすべてのビットが必要ない場合は、ビットフィールドである可能性があります。

これの他の一般的なケースは動的型付けです。 classvirtual関数がある場合、プログラムはその関数へのポインターをvirtual function tableに格納し、作成時にclassの各インスタンスに対して初期化します。通常、これはすべてのクラスインスタンスに対して1つの仮想関数テーブルを意味し、各インスタンスは適切なテーブルへのポインタを保持しています。 (テーブルは単一のポインターよりもはるかに大きいため、これにより時間とメモリが節約されます。)ポインターまたは参照を介してvirtual関数を呼び出すと、プログラムは仮想テーブルで関数ポインターを検索します。 (コンパイル時に正確な型がわかっている場合は、この手順をスキップできます。)これにより、コードは基本クラスの代わりに派生型の実装を呼び出すことができます。

ここでこれに関連するのは、各ofstreamofstream仮想テーブルへのポインタ、各ifstreamからifstream仮想テーブルへのポインタなどが含まれることです。クラス階層の場合、仮想テーブルポインターは、クラスオブジェクトのタイプをプログラムに通知するタグとして機能します。

言語標準は、コンパイラを設計する人々に、内部でランタイムを実装する方法を教えていませんが、これは、dynamic_castおよびtypeofが機能することを期待できる方法です。

1
Davislor