web-dev-qa-db-ja.com

C ++ポインタをメモリアドレスと考えることはどの程度許容できますか?

C++を学ぶとき、または少なくともC++ Primerを通して学んだとき、ポインターは、それらが指す要素の「メモリーアドレス」と呼ばれていました。これはどの程度真実なのだろうか。

たとえば、2つの要素を実行します*p1および*p2プロパティを持っているp2 = p1 + 1またはp1 = p2 + 1その場合のみ物理メモリ内で隣接していますか?

41
user5648283

ポインタは仮想メモリのアドレスであると考える必要があります。最新のコンシューマーオペレーティングシステムとランタイム環境では、物理メモリとポインタ値として表示されるものの間に少なくとも1層の抽象化レイヤーが配置されます。

最終的なステートメントについては、仮想メモリのアドレス空間であっても、そのような仮定を立てることはできません。ポインタ演算は、配列などの連続するメモリのブロック内でのみ有効です。また、配列(またはスカラー)を過ぎた1つのポイントにポインターを割り当てることは(CとC++の両方で)許可されていますが、deferencingのようなポインターの動作は定義されていません。 CおよびC++のコンテキストでの物理メモリの隣接性についての仮説は無意味です。

32
Bathsheba

まったくありません。

C++は、コンピューターが実行するコードを抽象化したものです。この抽象化のリークはいくつかの場所で見られますが(たとえば、ストレージを必要とするクラスメンバーの参照)、一般に、抽象化にコーディングするだけで他に何もしない方がよいでしょう。

ポインターはポインターです。彼らは物事を指しています。それらは実際にはメモリアドレスとして実装されますか?多分。それらは最適化することもできますし、(たとえば、メンバーへのポインタの場合)単純な数値アドレスよりもいくらか複雑にすることもできます。

ポインタをメモリ内のアドレスにマップする整数として考え始めると、たとえば、オブジェクトへのポインタを保持するのはundefinedであることを忘れ始めます。存在しません(ポインタを好きなメモリアドレスに簡単にインクリメントおよびデクリメントすることはできません)。

多くの回答がすでに述べているように、それらはメモリアドレスとして考えられるべきではありません。それらの答えをチェックしてください そしてここで それらの理解を得るために。最後のステートメントに対処する

* p1と* p2は、物理メモリ内で隣接している場合に限り、プロパティp2 = p1 +1またはp1 = p2 +1を持ちます。

p1p2が同じタイプであるか、同じサイズのタイプを指している場合にのみ正しいです。

11
Aiden Deom

ポインタをメモリアドレスと考えるのは絶対に正しいことです。これは、私が使用したすべてのコンパイラーに含まれているものです。さまざまなコンパイラー・プロデューサーによって製造された、さまざまなプロセッサー・アーキテクチャー向けです。

ただし、コンパイラはいくつかの興味深い魔法を実行し、通常のメモリアドレス(少なくとも最近のすべての主流プロセッサでは)がバイトアドレスであり、ポインタが参照するオブジェクトが正確に1バイトではない可能性があるという事実に加えて役立ちます。したがって、T* ptr;がある場合、ptr++((char*)ptr) + sizeof(T);を実行するか、ptr + n((char*)ptr) + n*sizeof(T)です。これは、p1 == p2 + 1が実際には+sizeof(T)*1であるため、p1ではp2+1が同じタイプのTである必要があることも意味します。 。

上記の「ポインタはメモリアドレスです」には1つの例外があり、それはメンバー関数ポインタです。これらは「特別」であり、今のところ、実際にどのように実装されているかは無視してください。「単なるメモリアドレス」ではないと言えます。

5
Mats Petersson

オペレーティングシステムは、物理マシンの抽象化をプログラムに提供します(つまり、プログラムは仮想マシンで実行されます)。したがって、プログラムは、CPU時間、メモリなど、コンピュータの物理リソースにアクセスできません。これらのリソースをOSに要求するだけです。

メモリの場合、プログラムはオペレーティングシステムによって定義された仮想アドレス空間で動作します。このアドレス空間には、スタック、ヒープ、コードなどの複数の領域があります。ポインターの値は、この仮想アドレス空間内のアドレスを表します。実際、連続するアドレスへの2つのポインターは、このアドレス空間内の連続する場所を指します。

ただし、このアドレス空間はオペレーティングシステムによってページとセグメントに分割され、必要に応じてメモリからスワップインおよびスワップアウトされるため、ポインタが連続する物理メモリの場所を指している場合とそうでない場合があり、実行時にそれが本当かどうか。これは、オペレーティングシステムがページングとセグメンテーションに使用するポリシーにも依存します。

要するに、ポインタはメモリアドレスであるということです。ただし、これらは仮想メモリ空​​間内のアドレスであり、これを物理メモリ空間にどのようにマッピングするかはオペレーティングシステムが決定します。

プログラムに関する限り、これは問題ではありません。この抽象化の理由の1つは、プログラムがマシンの唯一のユーザーであるとプログラムに信じ込ませるためです。プログラムを作成するときに他のプロセスによって割り当てられたメモリを考慮する必要がある場合に経験しなければならない悪夢を想像してみてください。どのプロセスが自分のプロセスと同時に実行されるかさえわかりません。また、これはセキュリティを強化するための優れた手法です。プロセスは2つの異なる(仮想)メモリ空間で実行されるため、別のプロセスのメモリ空間に悪意を持ってアクセスすることはできません(少なくともアクセスできないはずです)。

5
Paul92

他の変数と同様に、ポインタは他のデータが格納されているメモリのアドレスになることができるデータを格納します。

したがって、ポインタはアドレスを持ち、アドレスを保持できる変数です。

ポインタが常にアドレスを保持している必要はありません であることに注意してください。非アドレスID /ハンドルなどを保持している可能性があります。したがって、ポインタをアドレスとして言うのは賢明なことではありません。


2番目の質問について:

ポインタ演算 メモリの連続したチャンクに対して有効です。 p2 = p1 + 1と両方のポインタが同じタイプの場合、p1p2は連続したメモリチャンクを指します。したがって、p1p2が保持するアドレスは互いに隣接しています。

4
haccks

この答え は正しい考えですが、用語が貧弱だと思います。 Cポインターが提供するのは、抽象化の正反対です。

抽象化は、ハードウェアがより複雑で理解しにくい、または推論するのが難しい場合でも、比較的理解しやすく推論しやすいメンタルモデルを提供します。

Cポインタはその反対です。実際のハードウェアの方が単純で推論しやすい場合でも、ハードウェアの可能性のある問題を考慮に入れています。それらは、手元のハードウェアが実際にどれほど単純であるかに関係なく、最も複雑なハードウェアの最も複雑な部分の結合によって許可されるものにあなたの推論を制限します。

C++ポインターは、Cに含まれていないものを1つ追加します。同じ配列にない場合でも、同じタイプのすべてのポインターを順序で比較できます。これにより、ハードウェアと完全に一致していなくても、もう少しメンタルモデルが可能になります。

4
Jerry Coffin

ポインタがコンパイラによって最適化されていない限り、ポインタはメモリアドレスを格納する整数です。それらの長さは、コードがコンパイルされているマシンによって異なりますが、通常intとして扱うことができます。

実際、printf()を使用して、それらに格納されている実際の数を出力することで、それを確認できます。

ただし、_type *_ポインタのインクリメント/デクリメント操作はsizeof(type)によって実行されることに注意してください。このコードで自分の目で確かめてください(Repl.itでオンラインでテスト済み):

_#include <stdio.h>

int main() {
    volatile int i1 = 1337;
    volatile int i2 = 31337;
    volatile double d1 = 1.337;
    volatile double d2 = 31.337;
    volatile int* pi = &i1;
    volatile double* pd = &d1;
    printf("ints: %d, %d\ndoubles: %f, %f\n", i1, i2, d1, d2);
    printf("0x%X = %d\n", pi, *pi);
    printf("0x%X = %d\n", pi-1, *(pi-1));
    printf("Difference: %d\n",(long)(pi)-(long)(pi-1));
    printf("0x%X = %f\n", pd, *pd);
    printf("0x%X = %f\n", pd-1, *(pd-1));
    printf("Difference: %d\n",(long)(pd)-(long)(pd-1));
}
_

コンパイラがそれらを最適化しないように、すべての変数とポインタは揮発性であると宣言されました。また、変数は関数スタックに配置されるため、デクリメントを使用したことにも注意してください。

出力は次のとおりです。

_ints: 1337, 31337
doubles: 1.337000, 31.337000
0xFAFF465C = 1337
0xFAFF4658 = 31337
Difference: 4
0xFAFF4650 = 1.337000
0xFAFF4648 = 31.337000
Difference: 8
_

このコードは、特に変数を同じ順序で格納しない場合、すべてのコンパイラで機能するとは限らないことに注意してください。ただし、重要なのは、ポインター値を実際に読み取って出力できることと、ポインターが参照する変数のサイズに基づいて、1つのデクリメントがデクリメントされる場合とデクリメントされることです。

また、_&_および_*_は、reference( "この変数のメモリアドレスを取得")およびの実際の演算子であることに注意してください。 )dereference( "このメモリアドレスの内容を取得")。

これは、_float*_を_int*_としてキャストすることにより、フロートのIEEE754バイナリ値を取得するなどのクールなトリックにも使用できます。

_#include <iostream>

int main() {
    float f = -9.5;
    int* p = (int*)&f;

    std::cout << "Binary contents:\n";
    int i = sizeof(f)*8;
    while(i) {
        i--;
        std::cout << ((*p & (1 << i))?1:0);
   } 
}
_

結果は次のとおりです。

_Binary contents:
11000001000110000000000000000000 
_

https://pt.wikipedia.org/wiki/IEEE_754 からの例。コンバーターをチェックしてください。

1
Ronan Paixão

どういうわけか、ここでの回答は、特定のポインターのファミリー、つまり、メンバーへのポインターについて言及していません。それらは確かにそうではありませんメモリアドレスです。

1
SergeyA

ポインタはメモリアドレスですが、物理アドレスを反映していると思い込まないでください。 0x00ffb500のようなアドレスが表示された場合、それらはMMUが対応する物理アドレスに変換される論理アドレスです。仮想メモリが最も拡張されたメモリ管理であるため、これが最も可能性の高いシナリオです。システムですが、物理アドレスを直接管理するシステムが存在する可能性があります

0
Mr. E

C++ 14標準によると、[expr.unary.op]/3:

単項&演算子の結果は、そのオペランドへのポインターです。オペランドは左辺値または修飾IDでなければなりません。オペランドが、タイプmのあるクラスCの非静的メンバーTに名前を付ける修飾IDである場合、結果のタイプは「タイプCのクラスTのメンバーへのポインター」であり、C::mを指定する優先値です。それ以外の場合、式のタイプがTの場合、結果のタイプは「Tへのポインター」であり、prvalue 指定されたオブジェクトのアドレスまたは指定された関数へのポインターになります。 [注:特にタイプ「cvT」のオブジェクトのアドレスは「cvTへのポインター」です、同じcv-qualificationを使用します。 —エンドノート]

したがって、これは、オブジェクトタイプ(つまり、T *Tは関数タイプではない)へのポインタがアドレスを保持していることを明確かつ明確に示しています。


「アドレス」は[intro.memory] ​​/ 1で定義されています。

C++プログラムで使用可能なメモリは、連続するバイトの1つ以上のシーケンスで構成されます。すべてのバイトには一意のアドレスがあります。

したがって、アドレスは、メモリの特定のバイトを一意に識別するのに役立つものであれば何でもかまいません。

注:C++の標準用語では、memoryは次のようなスペースのみを指します。使用中です。物理メモリや仮想メモリなどを意味するものではありません。メモリは互いに素な割り当てのセットです。


メモリ内の各バイトを一意に識別する1つの可能な方法は、物理メモリまたは仮想メモリの各バイトに一意の整数を割り当てることですが、それが唯一の可能な方法ではないことを覚えておくことが重要です。

移植性のないコードを書くことを避けるために、アドレスが整数と同一であると仮定することを避けるのは良いことです。とにかく、ポインタの算術規則は整数の算術規則とは異なります。同様に、5.0f1084227584と同じであるとは言えませんが、メモリ内のビット表現は同じです(IEEE754の下で)。

0
M.M

あなたが与える特定の例:

たとえば、2つの要素* p1と* p2は、物理メモリ内で隣接している場合に限り、プロパティp2 = p1 +1またはp1 = p2 + 1を持ちますか?

[〜#〜] pic [〜#〜] のように、フラットなアドレス空間を持たないプラットフォームでは失敗します。 PICの物理メモリにアクセスするには、アドレスと銀行番号の両方が必要ですが、後者は特定のソースファイルなどの外部情報から取得される場合があります。したがって、異なるバンクからのポインターに対して算術演算を実行すると、予期しない結果が得られます。

0
Owen