C/C++ でポインターを「逆参照」するとはどういう意味ですか? 質問する

C/C++ でポインターを「逆参照」するとはどういう意味ですか? 質問する

説明には例を添えてください。

ベストアンサー1

基本的な用語の確認

通常は、アセンブリをプログラミングしているのでない限り、数値のメモリ アドレスを含むポインタを想定すれば十分です。1はプロセスのメモリの 2 番目のバイトを参照し、2 は 3 番目のバイト、3 は 4 番目のバイトを参照します。

  • 0 と最初のバイトはどうなったのでしょうか? それについては後で説明します。以下のnull ポインターを参照してください。
  • ポインターが何を格納するか、およびメモリとアドレスがどのように関連しているかについてのより正確な定義については、この回答の最後にある「メモリ アドレスの詳細と、おそらく知る必要がない理由」を参照してください。

ポインタが指すメモリ内のデータ/値 (その数値インデックスを持つアドレスの内容) にアクセスする場合は、ポインタを逆参照します。

さまざまなコンピュータ言語には、コンパイラまたはインタープリタに、現在参照されているオブジェクトの (現在の) 値に関心があることを伝えるためのさまざまな表記法があります。以下では、C と C++ に焦点を当てます。

ポインタシナリオ

C で以下のようなポインターが与えられたとしますp...

const char* p = "abc";

...文字「a」、「b」、「c」をエンコードするために使用される数値と、テキストデータの終了を示す0バイトを含む4バイトがメモリのどこかに保存され、そのデータの数値アドレスがに保存されますp。Cがテキストをメモリにエンコードするこの方法は、アスキーズ

たとえば、文字列リテラルがアドレス 0x1000 にあり、p32 ビット ポインターが 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++では、以下を使用するのがベストプラクティスです。スマートポインタポインタを保存および管理するオブジェクト。スマートポインタのデストラクタが実行されると自動的に解放されます。C++11以降、標準ライブラリは2つのunique_ptr割り当てられたオブジェクトの所有者が 1 人だけの場合...

{
    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ん。これらはすべて、変数または (C++ のみ) 静的オブジェクトまたはその基底の直接または間接メンバー変数である場合、またはゼロ初期化が行われる場合 (たとえば、 および はポインタを含む T のメンバーに対してゼロ初期化を実行しますが、 は実行しません)、0/false/NULL に設定されます。boolsfalseNULLstaticnew T();new T(x, y, z);new T;

さらに、、およびをポインターに代入する場合、ポインター0内のビットがすべてリセットされるとは限りません。ポインターはハードウェア レベルで「0」を含むことはなく、仮想アドレス空間のアドレス 0 を参照することもできません。コンパイラーは、理由があればそこに何か他のものを格納できますが、どのような場合でも、ポインターを、、、またはこれらのいずれかが割り当てられた別のポインターと比較すると、比較は期待どおりに機能する必要があります。したがって、コンパイラー レベルのソース コードの下では、「NULL」は C 言語および C++ 言語では少し「魔法」になる可能性があります...NULLnullptr0NULLnullptr

メモリアドレスについての詳細と、おそらく知る必要がない理由

より厳密に言えば、初期化されたポインタは、NULL(多くの場合、バーチャル) メモリ アドレス。

単純なケースでは、これはプロセスの仮想アドレス空間全体への数値オフセットです。より複雑なケースでは、ポインターは特定のメモリ領域に対する相対値になる可能性があり、CPU は CPU の「セグメント」レジスタまたはビットパターンでエンコードされた何らかのセグメント ID に基づいてこれを選択したり、アドレスを使用するマシン コード命令に応じて異なる場所を検索したりすることがあります。

たとえば、変数int*を指すように適切に初期化された はint、変数にキャストした後、変数float*があるメモリとはまったく異なる「GPU」メモリ内のメモリにアクセスする可能性がありますint。その後、キャストされて関数ポインターとして使用されると、プログラムのマシン オペコードを保持するさらに異なるメモリを指す可能性があります (これらの他のメモリ領域内の実質的にランダムで無効なポインターの数値を持つint*)。

C や C++ などの 3GL プログラミング言語では、次のような複雑さが隠される傾向があります。

  • コンパイラが変数や関数へのポインターを提供する場合、その変数を自由に参照解除できます(その間に変数が破壊/解放されていない限り)。たとえば、特定のCPUセグメントレジスタを事前に復元する必要があるか、または別のマシンコード命令を使用する必要があるかは、コンパイラの問題です。

  • 配列内の要素へのポインタを取得した場合、ポインタ演算を使用して配列内の他の場所に移動したり、配列の末尾の 1 つ後のアドレスを生成したりして、配列内の他の要素へのポインタ (または同様にポインタ演算によって末尾の 1 つ後の値に移動されたポインタ) と比較することができます。C および C++ では、これが「正常に機能する」かどうかはコンパイラ次第です。

  • 特定のOS機能、例えば共有メモリマッピングはポインターを提供する場合があり、それらは意味のあるアドレスの範囲内で「そのまま機能」します。

  • 正当なポインタをこれらの境界を超えて移動したり、任意の数値をポインタにキャストしたり、無関係な型にキャストされたポインタを使用したりしようとすると、通常、未定義の動作したがって、高レベルのライブラリやアプリケーションでは避けるべきですが、OS、デバイス ドライバーなどのコードでは、C または C++ 標準で定義されていない動作に依存する必要がある場合があります。ただし、その動作は、特定の実装またはハードウェアによって明確に定義されています。

おすすめ記事