ポインターがC言語で保持するデータの「タイプ」とは何ですか?


30

ポインタがアドレスを保持していることを知っています。ポインターのタイプは、ポインターが指すデータの「タイプ」に基づいて「一般に」知られていることを知っています。ただし、ポインターは依然として変数であり、ポインターが保持するアドレスにはデータ「タイプ」が必要です。私の情報によると、アドレスは16進形式です。しかし、この16進数のデータの「タイプ」はまだわかりません。(16進数が何であるかはわかっていますが、10CBA20たとえば、この文字列は整数ですか?整数ですか?アドレスにアクセスしてそれ自体を操作したい場合は、そのタイプを知る必要があります。私が尋ねている理由です。)


17
ポインターは変数ではなく、です。変数は値を保持します(また、その型がポインター型の場合、その値はポインターであり、意味のあるものを含むメモリゾーンのアドレスである可能性があります)。特定のメモリゾーンを使用して、さまざまなタイプのさまざまな値を保持できます。
バジルスタリンケビッチ

29
「アドレスは16進形式」いいえ、それは単なるデバッガーまたはライブラリのフォーマットビットです。同じ引数を使用すると、バイナリまたは8進数であると言うことができます。
usr

タイプではなくフォーマットについて尋ねるほうがいいでしょう。したがって、以下のゲレンデ外の回答がいくつかあります(ただし、Kilianは正解です)。
モニカとの軽さレース

1
ここでのより深い問題は、OPのtypeの理解だと思います。結局のところ、プログラムで操作している値はメモリのほんの一部です。型は、アセンブリコードを生成するときにこれらのビットを処理する方法をコンパイラに指示するプログラマの方法です。
ジャスティンラージノア

これらすべての回答で編集するには遅すぎると思いますが、たとえば「x64 Linux」のように、ハードウェアやオペレーティングシステムを制限していれば、この質問の方がよかったでしょう。
ハイド

回答:


64

ポインター変数のタイプは.. pointerです。

Cで正式に許可されている操作は、(他のポインター、または特別なNULL /ゼロ値と)比較したり、整数を加算または減算したり、他のポインターにキャストしたりすることです。

未定義の動作を受け入れると、値が実際に何であるかを見ることができます。通常、整数と同じ種類の機械語であり、通常、整数型との間で可逆的にキャストできます。(かなり多くのWindowsコードは、DWORDまたはハンドルtypedefでポインターを非表示にすることでこれを行います)。

メモリがフラットではないため、ポインターが単純ではないアーキテクチャがいくつかあります。DOS / 8086 'near'および 'far'; PICのさまざまなメモリとコードスペース。


2
また、2つのポインターの差を取ることもできますp1-p2。結果は符号付き整数値です。特に、&(array[i])-&(array[j]) == i-j
MSalters

13
実際には、整数型への変換はまた、指定されている具体的にintptr_t及びuintptr_tポインタ値は、「十分に大きい」ことが保証されます。
マチューM.

3
変換に依存することもできますが、整数とポインター間のマッピングは実装定義です。(唯一の例外は0-> nullであり、0が定数IIRCである場合にのみ指定されます。)
cHao

7
pprintfに指定子を追加すると、cの実装依存動作の場合、人間が読めるvoidポインターの表現が定義されます。
dmckee

6
この答えは一般的に正しい考えを持っていますが、特定の主張では失敗します。整数型へのポインターの強制は未定義の動作ではなく、Windows HANDLEデータ型はポインター値ではありません(整数データ型に隠されたポインターではなく、算術を防ぐためにポインター型に隠された整数です)。
ベンフォークト

44

あなたは物事を複雑にしています。

アドレスは単なる整数、ピリオドです。理想的には、参照されるメモリセルの数です(実際には、セグメント、仮想メモリなどにより、これはより複雑になります)。

16進構文は、プログラマーの便宜のためにのみ存在する完全なフィクションです。0x1Aと26はまったく同じ型のまったく同じ数であり、どちらもコンピューターが使用するものではありません。内部では、コンピューターは常に00011010(一連のバイナリ信号)を使用します。

コンパイラーがポインターを数値として扱うことができるかどうかは、言語の定義に依存します-「システムプログラミング」言語は従来、内部で物事がどのように機能するかについてより透明です。プログラマーからですが、ポインターが数値であり、通常最も一般的なタイプの数値(プロセッサアーキテクチャと同じ数のビットを持つ数値)であるという事実については変わりません。


26
ほとんどの場合、アドレスは単なる整数ではありません。浮動小数点数が整数だけではないことは間違いありません
gnasher729

8
確かに。最もよく知られている反例は、ポインターが2つの整数であるIntel 8086 です。
–MSalters

5
@Robセグメント化されたメモリモデルでは、ポインタは、セグメントが暗黙的に指定された単一の値(セグメントの開始に相対的なアドレス、オフセット)、またはセグメント/セレクターとオフセットペアのいずれかです。(Intelは「セレクタ」という用語を使用したと思います。私はそれを調べるのが面倒です。)8086では、これらは2つの16ビット整数として表され、1つの20ビット物理アドレスを形成しました。(はい、アドレス=(セグメント<< 4 +オフセット)&0xfffffのようであれば、多くの異なる方法で同じメモリセルをアドレス指定することができます。)これは、リアルモードで実行している場合、すべてのx86互換機に引き継がれます。
CVn

4
長期的なアセンブラプログラマとして、コンピュータのメモリは整数を保持するメモリの場所に他ならないことを証明できます。ただし、それらをどのように処理し、それらの整数が何を表すかを追跡することが重要です。例えば、私のシステムでは、10進数4075876853はx'F2F0F1F5 'として保管されます。これはEBCDICのストリング' 2015 'です。x'0002015C 'はパック10進数形式の10進数2015を表しますが、10進数2015は000007DFとして格納されます。アセンブラプログラマとして、これらを追跡する必要があります。コンパイラはHL言語に対してそれを行います。
スティーブアイヴス

7
アドレスは整数と1対1で対応させることができますが、コンピューター上の他のすべても同様に対応できます:)
hobbs

15

ポインターはまさにそれです-ポインター。それは他の何かではありません。それが何か別のものだと考えようとしないでください。

C、C ++、Objective-Cなどの言語では、データポインターには次の4種類の値があります。

  1. ポインターは、オブジェクトのアドレスにすることができます。
  2. ポインターは、配列の最後の要素の直後を指すことができます。
  3. ポインターはNULLポインターにすることができます。つまり、ポインターは何も指していません。
  4. ポインターは不定の値を持つ可能性があります。つまり、ポインターはごみであり、使用しようとすると何でも起こります(悪いことを含む)。

関数を識別する関数ポインターもあります。関数ポインターは、関数を識別するか、ヌル関数ポインターであるか、不定値を持ちます。

その他のポインターは、C ++の「メンバーへのポインター」です。これらは間違いなくメモリアドレスではありません!その代わりに、彼らはのメンバー識別任意のクラスのインスタンスを。Objective-Cでは、セレクターがあります。これは、「特定のメソッド名と引数名を持つインスタンスメソッドへのポインター」のようなものです。メンバーポインターのように、同じように見える限り、すべてのクラスのすべてのメソッドを識別ます。

特定のコンパイラがどのようにポインタを実装するかを調べることができますが、それはまったく別の質問です。


4
関数へのポインターがあり、C ++ではメンバーへのポインターがあります。
-sdenham

メンバーへのC ++ポインタはメモリアドレスではありませんか?確かにそうです。class A { public: int num; int x; }; int A::*pmi = &A::num; A a; int n = a.*pmi;変数にpmiメモリアドレスが含まれていなければ、つまり、コードの最後の行で確立されているnumようにa、class のインスタンスのメンバーのアドレスであれば、変数はあまり使用されませんA。これを通常のintポインターにキャストし(コンパイラーはおそらく警告を出しますが)、正常に逆参照することができます(他のポインターの構文シュガーであることを証明します)。
-dodgethesteamroller

9

ポインターは、R​​AM内のストレージのワードをアドレス指定する(読み取りまたは書き込みの目的で一意に識別する)ビットパターンです。歴史的および従来の理由により、更新の単位は8ビットであり、英語では「バイト」として、またはフランス語ではより論理的にはオクテットとして知られています。これはどこにでもありますが、固有のものではありません。他のサイズが存在します。

私の記憶が正しければ、29ビットの単語を使用するコンピューターが1台ありました。これは2の累乗ではないだけでなく、素数でもあります。これはSILLIACだと思ったが、関連するWikipediaの記事はこれをサポートしていない。CAN BUSは29ビットアドレスを使用しますが、慣例により、ネットワークアドレスは機能的に同一であってもポインターと呼ばれません。

人々はポインターが整数であると断言し続けます。これは本質的でも本質的でもありませんが、ビットパターンを整数として解釈すると、順序の有用な品質が現れ、「文字列」や「配列」のような構造の非常に直接的な(したがって小さなハードウェアで効率的な)実装が可能になります。連続メモリの概念は順序の隣接に依存し、相対的な位置付けが可能です。整数の比較と算術演算を有意義に適用できます。このため、ストレージアドレス指定のワードサイズとALU(整数演算を行うもの)の間には、ほぼ常に強い相関関係があります。

時々、2つは一致しません。初期のPCでは、アドレスバスは24ビット幅でした。


Nitpick、最近の一般的なOSでは、ポインターは仮想メモリ内の場所を識別し、物理RAMワードとは直接関係がありません(OSがすべてゼロであることがわかっているメモリページにある場合、仮想メモリの場所は物理的に存在しないこともあります) )。
ハイド

@hyde-あなたの議論は明らかに意図したコンテキストでメリットがありますが、コンピューターの支配的な形態は組み込みコントローラーであり、仮想メモリのような驚異は限られた展開での最近の革新です。また、あなたが指摘したことは、OPがポインターを理解するのに役立ちません。いくつかの歴史的背景が、それをはるかにarbitrary意的でなくなると思った。
ピーターウォン

RAMについて話すことがOPの理解に役立つかどうかはわかりません。質問は、具体的にはポインターは何かに関するものだからです。ああ、定義によるcポインターの別のnitpickは、ワード(CPUのワードサイズがバイトサイズと同じでない限り)でなく、バイトを指します(char*たとえば、メモリのコピー/比較の目的で、sizeof char==1C標準で定義されるように安全にキャストできます)。
ハイド

基本的にポインターとは、ストレージのハッシュキーです。これは言語とプラットフォームに依存しません。
ピーターウォン

問題はc ポインターに関するものです。また、ハッシュテーブルもハッシュアルゴリズムもないため、ポインタは間違いなくハッシュキーではありません。それらは、当然のことながら(「マップ」の十分に広い定義のための)何らかの種類のマップ/辞書キーですが、ハッシュキーではありません。
ハイド

6

基本的に、最新のコンピューターはすべてビットプッシュマシンです。通常、バイト、ワード、dword、またはqwordと呼ばれるデータのクラスターでビットをプッシュします。

バイトは、8ビット、ワード2バイト(または16ビット)、dword 2ワード(または32ビット)、qword 2 dword(または64ビット)で構成されます。これらはビットを配置する唯一の方法ではありません。多くの場合、SIMD命令で128ビットと256ビットの操作も行われます。

アセンブリ命令はレジスタで動作し、メモリアドレスは通常上記の形式のいずれかで動作します。

ALU(算術論理ユニット)は、整数を表すかのようにビットのバンドル(通常は2の補数形式)で動作し、FPUは浮動小数点値(通常はIEEE 754スタイルfloatおよびdouble)を表すように動作します。その他の部分は、何らかの形式、文字、テーブルエントリ、CPU命令、またはアドレスのバンドルデータであるかのように機能します。

典型的な64ビットコンピューターでは、8バイト(64ビット)のバンドルがアドレスです。これらのアドレスは従来どおり16進形式(など0xabcd1234cdef5678)で表示されますが、これは人間がビットパターンを簡単に読み取れる方法です。各バイト(8ビット)は2つの16進文字として書き込まれます(各16進文字-0からF-は4ビットを表します)。

実際に行われているのは(実際にはある程度)、ビットがあり、通常はレジスタに格納されているか、メモリバンク内の隣接する場所に格納されており、別の人間に説明しようとしているだけです。

ポインターをたどると、メモリコントローラーにその場所のデータを提供するように要求されます。通常、メモリコントローラーに特定の場所で特定のバイト数を要求します(通常、暗黙的に一連の場所であり、通常は連続しています)。

コードは通常、フェッチするデータの宛先(レジスタ、別のメモリアドレスなど)を指定します。通常、整数を期待するレジスタに浮動小数点データをロードするか、その逆を行うのは悪い考えです。

C / C ++のデータのタイプは、コンパイラが追跡するものであり、生成されるコードを変更します。通常、実際にそれを作成するデータには本質的なものはありませんに任意の1つのタイプ。コードによって整数のような方法(またはフロートのような方法、またはアドレスのような方法)で操作されるビットの集合(バイトに配置)。

これには例外があります。特定のものが異なる種類のビットであるアーキテクチャがあります。最も一般的な例は、保護された実行ページです-CPUに行うことを指示する命令はビットですが、実行時に実行するコードを含む(メモリ)ページは特別にマークされ、変更できず、マークされていないページを実行できません実行ページとして。

また、読み取り専用データ(ROMに格納されて物理的に書き込みできない場合もあります!)、アライメントの問題(一部のプロセッサーはロードできません)もあります double特定の方法でアライメントされない限り、メモリからsを、または特定のアライメントを必要とするSIMD命令)、および無数の他のアーキテクチャの癖。

上記のレベルの詳細でさえ嘘です。コンピューターは「本当に」ビットを押しのけているのではなく、電圧と電流を押しのけています。これらの電圧と電流は、ビットの抽象化レベルで「想定」されていることを実行しないことがあります。チップは、そのようなエラーのほとんどを検出し、より高いレベルの抽象化に気付かずに修正するように設計されています。

それも嘘です。

抽象化の各レベルは以下を隠し、Feynmanダイアグラムを印刷するために気にする必要なしに問題を解決することについて考えることを可能にします "Hello World"

したがって、十分なレベルの正直さで、コンピューターはビットをプッシュし、それらのビットは、それらがどのように使用されるかによって意味を与えられます。


3

人々は、ポインターが整数であるかどうかについて多くのことをしました。これらの質問には実際に答えがあります。ただし、仕様の土地に足を踏み入れる必要がありますが、これは気弱な人向けではありません。C仕様を見てみましょう。 ISO / IEC 9899:TC2を。

6.3.2.3ポインター

  1. 整数は、任意のポインター型に変換できます。前に指定した場合を除き、結果は実装定義であり、正しく位置合わせされていない可能性があり、参照された型のエンティティを指していない可能性があり、トラップ表現である可能性があります。

  2. 任意のポインター型を整数型に変換できます。前に指定した場合を除き、結果は実装定義です。結果を整数型で表現できない場合、動作は未定義です。結果は、整数型の値の範囲内である必要はありません。

このために、いくつかの一般的な仕様用語を知る必要があります。「実装の定義」とは、コンパイラーごとに異なる定義を許可することを意味します。実際、コンパイラは、コンパイラの設定に応じて異なる方法で定義することさえあります。未定義の動作とは、コンパイル時エラーの発生から説明できない動作まで、完全に動作するまで、コンパイラーがまったく何でもできることを意味します。

これから、整数型への変換行われる可能性があることを除いて、基になるストレージ形式が指定されていないことがわかります。事実、太陽の下のほぼすべてのコンパイラーは、フードの下でポインターを整数アドレスとして表します(1つではなく2つの整数として表される可能性がある少数の特別な場合を使用)が、仕様では、 10文字の文字列としてのアドレス!

Cから早送りしてC ++仕様を見ると、でもう少しわかりやすくなりますreinterpret_castが、これは異なる言語なので、その値は異なる場合があります。

ISO / IEC N337:C ++ 11ドラフト仕様(手持ちのドラフトのみ)

5.2.10キャストの再解釈

  1. ポインターは、それを保持するのに十分な大きさの整数型に明示的に変換できます。マッピング関数は実装定義です。[注:基礎となるマシンのアドレス指定構造を知っている人にとっては当然のことです。—end note] std :: nullptr_t型の値は整数型に変換できます。変換の意味と有効性は、(void *)0から整数型への変換と同じです。[注:reinterpret_castを使用して、任意の型の値を型std :: nullptr_tに変換することはできません。—注を終了]

  2. 整数型または列挙型の値は、明示的にポインターに変換できます。十分なサイズの整数に変換されたポインター(実装上に存在する場合)は、同じポインタータイプに戻され、元の値になります。それ以外の場合、ポインターと整数間のマッピングは実装定義です。[注:3.7.4.3で説明されている場合を除き、このような変換の結果は安全に派生したポインター値ではありません。—注を終了]

ここからわかるように、C ++はさらに数年後、整数へのマッピングが存在すると想定するのが安全であることがわかったため、未定義の動作についてはもはや話題になりません(ただし、パート4と5「フレーミングが実装に存在する場合」というフレーズを付けて)


今、これから何を奪うべきですか?

  • ポインタの正確な表現は実装定義です。(実際、面倒なことをするために、いくつかの小さな組み込みコンピューターは、使用するアドレスエイリアシングトリックをサポートするために、アドレス255としてヌルポインター(void)0を表します)*
  • メモリー内のポインターの表現について尋ねる必要がある場合、おそらくプログラミングのキャリアでそれらをいじりたいと思わないでしょう。

最善策:(char *)へのキャスト。CおよびC ++の仕様には、配列と構造体のパッキングを指定する規則が多数あり、どちらも常にchar *へのポインターのキャストを許可しています。charは常に1バイトです(Cでは保証されていませんが、C ++ 11では言語の必須部分になっているため、どこでも1バイトであると想定しても比較的安全です)。これにより、ポインターの実装固有の表現を実際に知る必要なく、バイトごとのレベルでポインター演算を行うことができます。


関数ポインタを必ずにキャストできますchar *か?コードとデータ用に別々のアドレス空間を持つ仮想マシンを考えています。
フィリップケンドール

@PhilipKendall良い点。仕様にはその部分を含めませんでしたが、関数ポインターは、発生する問題のために、仕様のデータポインターとはまったく異なるものとして扱われます。メンバーポインターの扱いも異なります(ただし、動作も大きく異なります)
コートアンモン-モニカの復活

A charは常にCで1バイトです。C標準からの引用:「sizeof演算子は、オペランドのサイズ(バイト単位)を返します」および「sizeofがchar型、unsigned char型、またはsigned char型のオペランドに適用される場合、 (またはその修飾バージョン)結果は1です。 " おそらく、バイトは8ビットの長さだと考えているのでしょう。それは必ずしもそうではありません。標準に準拠するには、バイトに少なくとも8ビットが含まれている必要があります。
デビッドハメ

この仕様では、ポインター型と整数型の間の変換について説明しています。型間の「変換」は型の平等を意味するものではなく、メモリ内の2つの型のバイナリ表現も同じビットパターンを持つことを常に念頭に置く必要があります。(ASCIIはEBCDICに「変換」できます。ビッグエンディアンはリトルエンディアンに「変換」できます。など)
-user2338816

1

ほとんどのアーキテクチャでは、ポインターの種類は、マシンコードに変換されると消滅します(「脂肪ポインター」を除く)。したがって、へのポインタは、少なくともそれ自体でintdouble、へのポインタと区別できません。*

[*]ただし、適用する操作の種類に基づいて推測することもできます。


1

CおよびC ++について理解する重要なことは、実際に型が何であるかです。彼らが実際に行うことは、ビット/バイトのセットを解釈する方法をコンパイラに示すことです。次のコードから始めましょう。

int var = -1337;

アーキテクチャに応じて、整数には通常、その値を格納するための32ビットのスペースが与えられます。つまり、varが格納されているメモリ内のスペースは、「11111111 11111111 11111010 11000111」または16進数「0xFFFFFAC7」のようになります。それでおしまい。その場所に保存されるのはそれだけです。すべてのタイプは、コンパイラーにその情報を解釈する方法を伝えるだけです。ポインターも同じです。私がこのようなことをしたら:

int* var_ptr = &var;   //the ampersand is telling C "get the address where var's value is located"

次に、コンパイラはvarの場所を取得し、最初のコードスニペットが値-1337を保存するのと同じ方法でそのアドレスを保存します。保存方法に違いはなく、使用方法に違いはありません。var_ptrをintへのポインターにしたことも関係ありません。あなたがしたいなら、あなたがすることができます。

unsigned int var2 = *(unsigned int*)var_ptr;

これにより、varの上記の16進値(0xFFFFFAC7)がvar2の値を格納する場所にコピーされます。次にvar2を使用すると、値は4294965959になります。var2のバイトはvarと同じですが、数値が異なります。これらのビットは符号なしlongを表すと言ったため、コンパイラはそれらを異なって解釈しました。ポインタ値についても同じことができます。

unsigned int var3 = (unsigned int)var_ptr;

この例では、varのアドレスを表す値をunsigned intとして解釈することになります。

うまくいけば、これはあなたのために物事を明確にし、Cがどのように機能するかについてより良い洞察を与えてくれます。あなたがいることに注意してくださいすべきではない、私は実際の生産コードでは、以下の2行でやったクレイジーなもののいずれかを行います。これは単にデモ用です。


1

整数。

コンピューターのアドレス空間には、0から順に1ずつ増加する番号が付けられます。したがって、ポインターは、アドレス空間のアドレスに対応する整数を保持します。


1

タイプを組み合わせます。

特に、特定のタイプは、プレースホルダーでパラメーター化されているかのように結合します。配列とポインターのタイプはこのようなものです。それらには、そのようなプレースホルダーが1つあります。これは、それぞれ、配列の要素の型または指し示されているものです。関数タイプもこのようなものです。パラメーターの複数のプレースホルダーと戻り値の型のプレースホルダーを持つことができます。

charへのポインターを保持するように宣言された変数の型は、「charへのポインター」です。intへのポインターへのポインターを保持するように宣言されている変数の型は、「intへのポインターへのポインター」です。

(値の)型「intへのポインターへのポインター」は、逆参照操作によって「intへのポインター」に変更できます。したがって、型の概念は単なる単語ではなく、数学的に重要な構成要素であり、型の値(逆参照、パラメータとして渡す、または変数に割り当てるなど)で何ができるかを決定し、サイズ(バイトカウント)も決定しますインデックス付け、算術、インクリメント/デクリメント操作)。

PS型の詳細を知りたい場合は、このブログを試してくださいhttp : //www.goodmath.org/blog/2015/05/13/expressions-and-arity-part-1/

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