C ++の変数はどのように型を保存しますか?


42

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


8
どのように追跡するのですか」の「it」で誰/何を参照してますか?コンパイラまたはCPU、または言語やプログラムのような何か
エリックエイド


8
@ErikEidt IMO OPは、明らかに「it」によって「変数自体」を意味します。もちろん、この質問に対する2語の答えは「ありません」です。
アレフゼロ

2
いい質問です!その種類を格納するすべての派手な言語を考えると、今日特に重要です。
トレバーボイドスミス

@alephzeroそれは明らかに主要な質問でした。
ルアーン

回答:


105

変数(またはより一般的には、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ます。コンパイラは、コンパイル時にオブジェクトのタイプを知っているか、コンパイラがオブジェクト内に必要なタイプ情報を保存しており、実行時にそれを取得できます。


3
非常に包括的な。
デュプリケータ

9
ポリモーフィックオブジェクトの型にアクセスするには、オブジェクトが特定の継承ファミリに属していることをコンパイラがまだ認識している必要があることに注意してください(つまり、オブジェクトへの型付き参照/ポインタではなくvoid*)。
ルスラン

5
+0最初の文が正しくないため、最後の2つの段落で修正されます。
マルチン

3
通常、ポリモーフィックオブジェクトの開始時に格納されるのは、テーブル自体ではなく、仮想メソッドテーブルへのポインタです。
ピーターグリーン

3
@ v.oddou私の段落では、いくつかの詳細を無視しました。typeid(e)式の静的型を内省しますe。静的型が多相型の場合、式が評価され、そのオブジェクトの動的型が取得されます。typeidを不明なタイプのメモリに向けて、有用な情報を取得することはできません。たとえば、共用体のtypeidは、共用体のオブジェクトではなく、共用体を表します。aのtypeid void*は単なるvoidポインターです。また、a void*を逆参照してその内容を取得することはできません。C ++では、明示的にそのようにプログラムされていない限り、ボクシングはありません。
アモン

51

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

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

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

intを受け取り、boolを吐き出します。

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

自動オレンジジューサー

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

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

警告は、コンパイラが渡す不正なコードのいくつかのケースがあるということです。例は次のとおりです。

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

前述のように、コンパイルされたコードはジューサーマシンに似ています。処理するものが分からず、命令を実行するだけです。そして、指示が間違っていると、壊れます。これが、C ++の上記の問題が制御不能なクラッシュを引き起こす理由です。


4
コンパイラー、関数に正しい型のオブジェクトが渡されることを確認しようとしますが、CとC ++の両方は、コンパイラーがすべての場合でそれを証明するには複雑すぎます。したがって、リンゴとオレンジをジューサーと比較することは非常に有益です。
カルチャ

@Calchasコメントありがとうございます!この文は確かに単純化しすぎていた。考えられる問題について少し詳しく説明しましたが、実際にはそれらは問題にかなり関連しています。
Frax

5
マシンコードのすごい比phor!あなたの比phorも写真によって10倍良くなりました!
トレバーボイドスミス

2
「私が見逃している何か他のものがあると確信しています。」- もちろん!C は、通常の算術プロモーション、型のパンニング、vs にvoid*強制しますが、悪いポインタを持っているだけでもUBなどです。そのまま。foo*unionNULLnullptr
ケビン

@Kevin質問はC ++としてのみタグ付けされているため、ここにCを追加する必要はないと思います。また、C ++ void*では暗黙的にに変換されずfoo*union型のパニングはサポートされていません(UBがあります)。
ルスラン

3

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

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

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

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

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

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

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

プロセッサは、byte / word / int / long signed / unsigned、float、doubleなどの基本的なデータ型を直接サポートします。通常、同じメモリ位置を符号付きまたは符号なしとして交互に扱う場合、プロセッサは文句を言いません。たとえば、通常はプログラムの論理エラーになりますが。変数とのすべての対話でプロセッサに指示するのはプログラミングの仕事です。

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

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

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


1
「翻訳が完了すると、名前は忘れられます」に注意してください...リンクは名前(「未定義のシンボルxy」)で行われ、動的リンクでは実行時に発生する可能性があります。blog.fesnel.com/blog/2009/08/19/…を参照してください。デバッグシンボルはなく、削除されている場合:動的リンクには関数(および、おそらくグローバル変数)名が必要です。したがって、内部オブジェクトの名前のみを忘れることができます。ところで、変数プロパティの良いリスト。
ピーター-モニカの復活

@ PeterA.Schneider、あなたは物事の全体像において、リンカーとローダーも参加し、ソースコードから来た(グローバル)関数と変数の名前を使用することは絶対に正しいです。
エリックエイド

さらに複雑な点は、一部のコンパイラは、標準に従って、書かれエイリアスを含まない場合でも、異なる型を含む操作をシーケンスなしと見なすことができるため、特定のものがエイリアスしないと仮定することを意図したルールを解釈することです。のようなものを考えるとuseT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);、clangとgccは、両方が同じから派生していても、ポインタunionArray[j].member2がアクセスできないと仮定する傾向があります。unionArray[i].member1unionArray[]
supercat

コンパイラが言語仕様を正しく解釈するかどうかにかかわらず、その仕事はプログラムを実行するマシンコード命令シーケンスを生成することです。これは、ソースコード内の各変数アクセスに対して(モジュロ最適化および他の多くの要因)プロセッサの格納場所に使用するサイズとデータ解釈を指示するマシンコード命令を生成する必要があることを意味します。プロセッサは変数について何も覚えていないため、変数にアクセスするたびに、その方法を正確に指示する必要があります。
エリックエイド

2

特定のタイプの変数を定義すると、変数のタイプをどのように追跡しますか。

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

  • コンパイル時間

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

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

  • ランタイム

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


万が一「バイトコード指向言語」でC#またはJavaを参照している場合、ポインターは決して省略されません。まったく逆です。ポインターはC#とJavaではるかに一般的です(したがって、Javaで最も一般的なエラーの1つは "NullPointerException"です)。それらが「参照」と名付けられていることは、単に用語の問題です。
ピーター-モニカの復活

@ PeterA.Schneider、確かに、NullPOINTERExceptionがありますが、私が言及した言語(Java、Ruby、おそらくC#、ある程度Perlなど)の参照とポインタの間には非常に明確な違いがあります-参照は一緒に行きます型システム、ガベージコレクション、自動メモリ管理など。通常、メモリの場所を明示的に指定することさえできません(char *ptr = 0x123Cなど)。この文脈では、「ポインタ」という言葉の使用法はかなり明確にすべきだと思います。そうでない場合は、お気軽にお知らせください。回答に文章を追加します。
AnoE

ポインタは、C ++でも「型システムと連携」します;-)。(実際、Javaの古典的なジェネリックは、C ++よりも強く型付けされていません。)ガベージコレクションは、C ++が強制しないことを決定した機能ですが、実装が1つを提供することは可能であり、ポインターに使用する単語とは関係ありません。
ピーター-モニカの復活

OK、@ PeterA.Schneider、私たちがここでレベルを上げているとは本当に思わない。私はポインターについて言及した段落を削除しましたが、とにかく答えには何もしませんでした。
AnoE

1

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

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

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

ここで関連するのは、それぞれに仮想テーブルofstreamへのポインタ、ofstream仮想テーブルへのポインタなどが含まれていることです。クラス階層の場合、仮想テーブルポインターはタグとして機能し、プログラムにクラスオブジェクトの型を伝えます。ifstreamifstream

言語の標準は、彼らはボンネットの下にランタイムを実装する必要がありますどのようにコンパイラを設計し、人々を教えてくれませんが、これはあなたが期待することができる方法であるdynamic_casttypeofの仕事に。


「言語標準はコーダーに伝えない」あなたはおそらく、問題の「コーダー」はgcc、clang、msvcなどを書いている人々であり、それらを使用してC ++をコンパイルする人々ではないことを強調すべきです。
カレス

@キャレス良い提案!
デイビスラー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.