説明に例を含めてください。
6 に答える
基本用語の復習
アセンブリをプログラミングしていない限り、通常は、1 がプロセスのメモリの 2 番目のバイトを参照し、2 が 3 番目、3 が 4 番目などの数値メモリ アドレスを含むポインタを想定するだけで十分です。
- 0 と最初のバイトはどうなりましたか? それについては後で説明します。以下のnull ポインターを参照してください。
- ポインタが格納するもの、およびメモリとアドレスがどのように関係するかについてのより正確な定義については、この回答の最後にある「メモリ アドレスの詳細と、おそらく知る必要がない理由」を参照してください。
ポインタが指すメモリ内のデータ/値(その数値インデックスを持つアドレスの内容)にアクセスする場合は、ポインタを逆参照します。
コンピューター言語が異なれば、ポインター先のオブジェクトの (現在の) 値に関心があることをコンパイラーまたはインタープリターに伝えるための表記法も異なります。以下では、C および C++ に焦点を当てます。
ポインターのシナリオ
以下のようなポインターが与えられた場合、Cで検討してくださいp
...
const char* p = "abc";
...文字「a」、「b」、「c」をエンコードするために使用される数値を含む4バイト、およびテキストデータの終わりを示す0バイトは、メモリのどこかに保存され、その数値アドレスにデータが格納されp
ます。このように C がメモリ内のテキストをエンコードする方法は、ASCIIZとして知られています。
たとえば、文字列リテラルがたまたまアドレス 0x1000 にありp
、32 ビット ポインターが 0x2000 にある場合、メモリの内容は次のようになります。
Memory Address (hex) Variable name Contents
1000 'a' == 97 (ASCII)
1001 'b' == 98
1002 'c' == 99
1003 0
...
2000-2003 p 1000 hex
アドレス 0x1000 の変数名/識別子はありませんが、アドレスを格納するポインターを使用して文字列リテラルを間接的に参照できることに注意してください: p
.
ポインターの逆参照
指す文字を参照するには、次のいずれかの表記法を使用しp
て逆参照p
します (これも C の場合です)。
assert(*p == 'a'); // The first character at address p will be 'a'
assert(p[1] == 'b'); // p[1] actually dereferences a pointer created by adding
// p and 1 times the size of the things to which p points:
// In this case they're char which are 1 byte in C...
assert(*(p + 1) == 'b'); // Another notation for p[1]
また、ポインタを参照先のデータに移動して、ポインタを逆参照することもできます。
++p; // Increment p so it's now 0x1001
assert(*p == 'b'); // p == 0x1001 which is where the 'b' is...
書き込み可能なデータがある場合は、次のようなことができます。
int x = 2;
int* p_x = &x; // Put the address of the x variable into the pointer p_x
*p_x = 4; // Change the memory at the address in p_x to be 4
assert(x == 4); // Check x is now 4
上記では、コンパイル時に と呼ばれる変数が必要であることを知っている必要がありx
、コードはコンパイラに変数を格納する場所を調整するように要求し、アドレスが を介して確実に利用できるようにします&x
。
構造体データ メンバーの逆参照とアクセス
->
C では、データ メンバーを持つ構造体へのポインターである変数がある場合、逆参照演算子を使用してそれらのメンバーにアクセスできます。
typedef struct X { int i_; double d_; } X;
X x;
X* p = &x;
p->d_ = 3.14159; // Dereference and access data member x.d_
(*p).d_ *= -1; // Another equivalent notation for accessing x.d_
マルチバイト データ型
ポインターを使用するには、コンピューター プログラムは、ポイントされているデータの型をある程度把握する必要があります。そのデータ型を表すために複数のバイトが必要な場合、ポインターは通常、データ内の最小番号のバイトを指します。
もう少し複雑な例を見てみましょう。
double sizes[] = { 10.3, 13.4, 11.2, 19.4 };
double* p = sizes;
assert(p[0] == 10.3); // Knows to look at all the bytes in the first double value
assert(p[1] == 13.4); // Actually looks at bytes from address p + 1 * sizeof(double)
// (sizeof(double) is almost always eight bytes)
++p; // Advance p by sizeof(double)
assert(*p == 13.4); // The double at memory beginning at address p has value 13.4
*(p + 2) = 29.8; // Change sizes[3] from 19.4 to 29.8
// Note earlier ++p and + 2 here => sizes[3]
動的に割り当てられたメモリへのポインタ
プログラムが実行され、どのデータがスローされるかを確認するまで、必要なメモリの量がわからない場合があります...その後、を使用してメモリを動的に割り当てることができますmalloc
。アドレスをポインタに格納するのが一般的です...
int* p = (int*)malloc(sizeof(int)); // Get some memory somewhere...
*p = 10; // Dereference the pointer to the memory, then write a value in
fn(*p); // Call a function, passing it the value at address p
(*p) += 3; // Change the value, adding 3 to it
free(p); // Release the memory back to the heap allocation library
C++ では、通常、メモリの割り当てはnew
演算子で行われ、解放は次のように行われますdelete
。
int* p = new int(10); // Memory for one int with initial value 10
delete p;
p = new int[10]; // Memory for ten ints with unspecified initial value
delete[] p;
p = new int[10](); // Memory for ten ints that are value initialised (to 0)
delete[] p;
以下のC++ スマート ポインターも参照してください。
アドレスの紛失と漏えい
多くの場合、ポインタは、データまたはバッファがメモリ内のどこに存在するかを示す唯一の指標である場合があります。そのデータ/バッファの継続的な使用が必要な場合、またはメモリの呼び出しfree()
またはdelete
リークを回避する機能が必要な場合、プログラマはポインタのコピーを操作する必要があります...
const char* p = asprintf("name: %s", name); // Common but non-Standard printf-on-heap
// Replace non-printable characters with underscores....
for (const char* q = p; *q; ++q)
if (!isprint(*q))
*q = '_';
printf("%s\n", p); // Only q was modified
free(p);
...または変更の取り消しを慎重に調整します...
const size_t n = ...;
p += n;
...
p -= n; // Restore earlier value...
free(p);
C++ スマート ポインター
C++ では、スマート ポインターオブジェクトを使用してポインターを格納および管理し、スマート ポインターのデストラクタの実行時にそれらの割り当てを自動的に解除することをお勧めします。unique_ptr
C++11 以降、割り当てられたオブジェクトの所有者が 1 人である場合のために、標準ライブラリは 2 つを提供しています...
{
std::unique_ptr<T> p{new T(42, "meaning")};
call_a_function(p);
// The function above might throw, so delete here is unreliable, but...
} // p's destructor's guaranteed to run "here", calling delete
...そしてshared_ptr
共有所有権の場合 (参照カウントを使用)...
{
auto p = std::make_shared<T>(3.14, "pi");
number_storage1.may_add(p); // Might copy p into its container
number_storage2.may_add(p); // Might copy p into its container } // p's destructor will only delete the T if neither may_add copied it
ヌル ポインター
C ではNULL
、0
さらに C++ ではnullptr
、ポインタが現在変数のメモリ アドレスを保持しておらず、逆参照したり、ポインタ演算で使用したりしてはならないことを示すために使用できます。例えば:
const char* p_filename = NULL; // Or "= 0", or "= nullptr" in C++
int c;
while ((c = getopt(argc, argv, "f:")) != -1)
switch (c) {
case f: p_filename = optarg; break;
}
if (p_filename) // Only NULL converts to false
... // Only get here if -f flag specified
C および C++ では、組み込みの数値型のデフォルトが0
や であるとは限らないのと同様bools
にfalse
、ポインターが常に に設定されるとは限りませんNULL
。これらはすべて、static
変数または (C++ のみ) 静的オブジェクトまたはそのベースの直接的または間接的なメンバー変数である場合、0/false/NULL に設定されるか、ゼロ初期化されます (たとえば、ポインターを含む T のメンバーに対してゼロ初期化new T();
を実行しますが、ではない)。new T(x, y, z);
new T;
さらに、 と をポインターに割り当てる0
とNULL
、nullptr
ポインターのビットがすべてリセットされるとは限りません。ポインターは、ハードウェア レベルで「0」を含んでいないか、仮想アドレス空間のアドレス 0 を参照していない可能性があります。コンパイラは、理由があればそこに何か他のものを格納することができますが、それが何であれ、ポインタを0
、NULL
、nullptr
またはそれらのいずれかに割り当てられた別のポインタと比較すると、比較は期待どおりに機能する必要があります。そのため、コンパイラ レベルのソース コードの下では、「NULL」は、C および C++ 言語では少し「魔法」になる可能性があります...
メモリアドレスの詳細と、おそらく知る必要がない理由
NULL
より厳密には、初期化されたポインターは、または (多くの場合、virtual ) メモリ アドレスを識別するビット パターンを格納します。
単純なケースは、これがプロセスの仮想アドレス空間全体への数値オフセットである場合です。より複雑なケースでは、ポインターは特定のメモリ領域に関連している可能性があり、CPU は、CPU の「セグメント」レジスタまたはビットパターンでエンコードされた何らかの方法のセグメント ID に基づいて選択したり、アドレスを使用した機械語命令。
たとえば、変数int*
を指すように適切に初期化された は、キャストした後、変数が存在するメモリとはまったく異なる「GPU」メモリ内のメモリにアクセスし、キャストして関数ポインタとして使用すると、さらに先を指す可能性がありますプログラムのマシンオペコードを保持する個別のメモリ(これらの他のメモリ領域内の事実上ランダムで無効なポインタの数値を持つ)。int
float*
int
int*
C や C++ などの 3GL プログラミング言語は、この複雑さを隠す傾向があります。
コンパイラーが変数または関数へのポインターを提供する場合、それを自由に逆参照できます (その間に変数が破棄/割り当て解除されない限り)。たとえば、特定の CPU セグメントレジスターを事前に復元する必要があるかどうかはコンパイラーの問題です。個別の機械語命令が使用される
配列内の要素へのポインターを取得した場合、ポインター演算を使用して配列内の他の場所に移動したり、要素への他のポインターと比較するのに適した配列の末尾を 1 つ過ぎたアドレスを形成したりすることもできます。配列内 (または同様にポインター演算によって同じ 1 つ後の値に移動されたもの)。再び C および C++ では、これが「正常に機能する」ことを保証するのはコンパイラ次第です。
共有メモリ マッピングなどの特定の OS 機能は、ポインタを提供する場合があり、それらは意味のあるアドレスの範囲内で「動作する」だけです。
正当なポインターをこれらの境界を超えて移動しようとしたり、任意の数値をポインターにキャストしたり、関連のない型にキャストされたポインターを使用したりする試みは、通常、未定義の動作をするため、高レベルのライブラリやアプリケーションでは避ける必要がありますが、OS やデバイス ドライバーなどのコード. は、C または C++ 標準で定義されていない動作に依存する必要がある場合がありますが、それにもかかわらず、特定の実装またはハードウェアで明確に定義されています。
ポインターの逆参照とは、ポインターが指すメモリ位置に格納されている値を取得することを意味します。演算子 * はこれを行うために使用され、逆参照演算子と呼ばれます。
int a = 10;
int* ptr = &a;
printf("%d", *ptr); // With *ptr I'm dereferencing the pointer.
// Which means, I am asking the value pointed at by the pointer.
// ptr is pointing to the location in memory of the variable a.
// In a's location, we have 10. So, dereferencing gives this value.
// Since we have indirect control over a's location, we can modify its content using the pointer. This is an indirect way to access a.
*ptr = 20; // Now a's content is no longer 10, and has been modified to 20.
ポインターは、値への「参照」です。図書館の請求番号が書籍への参照であるのと同じように。請求番号の「逆参照」は、物理的に通過してその本を取得します。
int a=4 ;
int *pA = &a ;
printf( "The REFERENCE/call number for the variable `a` is %p\n", pA ) ;
// The * causes pA to DEREFERENCE... `a` via "callnumber" `pA`.
printf( "%d\n", *pA ) ; // prints 4..
本がそこにない場合、司書は叫び始め、図書館を閉鎖し、数人の人々が人がそこにない本を見つけようとする原因を調査するように設定されています.
Pointer Basicsからのコードと説明:
逆参照操作はポインターから始まり、その矢印をたどってそのポインターにアクセスします。目標は、指示先の状態を確認すること、または指示先の状態を変更することです。ポインターの逆参照操作は、ポインターにポインティーがある場合にのみ機能します。つまり、ポインティーが割り当てられ、ポインターがそれを指すように設定されている必要があります。ポインター コードで最も一般的なエラーは、pointee の設定を忘れることです。コード内のそのエラーが原因で発生する最も一般的なランタイム クラッシュは、逆参照操作の失敗です。Java では、不適切な逆参照は、ランタイム システムによって丁寧にフラグが付けられます。C、C++、Pascal などのコンパイル済み言語では、不適切な逆参照によってクラッシュする場合があり、微妙なランダムな方法でメモリが破損する場合もあります。
void main() {
int* x; // Allocate the pointer x
x = malloc(sizeof(int)); // Allocate an int pointee,
// and set x to point to it
*x = 42; // Dereference x to store 42 in its pointee
}
逆参照は実際の値にアクセスすることを意味すると述べているため、以前の回答はすべて間違っていると思います。ウィキペディアは代わりに正しい定義を提供します: https://en.wikipedia.org/wiki/Dereference_operator
ポインター変数を操作し、ポインター アドレスの値に相当する左辺値を返します。これは、ポインターの「逆参照」と呼ばれます。
つまり、ポインターが指す値にアクセスすることなく、ポインターを逆参照できます。例えば:
char *p = NULL;
*p;
値にアクセスせずに NULL ポインターを逆参照しました。または、次のようにすることもできます。
p1 = &(*p);
sz = sizeof(*p);
繰り返しますが、逆参照しますが、値にアクセスすることはありません。このようなコードはクラッシュしません: クラッシュは、実際に無効なポインターによってデータにアクセスしたときに発生します。ただし、残念ながら、標準によれば、無効なポインターの逆参照は、実際のデータに触れようとしなくても (いくつかの例外を除いて) 未定義の動作です。
要するに、ポインターの逆参照とは、ポインターに逆参照演算子を適用することを意味します。その演算子は、将来の使用のために左辺値を返すだけです。