1. ホーム
  2. c++

[解決済み] ポインタの「デリファレンス」とはどういう意味ですか?

2022-03-16 08:56:33

質問

説明と一緒に例を挙げてください。

どのように解決するのですか?

基本用語のおさらい

それは 通常 を想定しておけば、アセンブリのプログラミングをしていない限り、十分でしょう。 ポインタ 1はプロセスのメモリ内の2バイト目、2は3バイト目、3は4バイト目...といった具合に、メモリアドレスを数値で表したもの。

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

ポインタが指し示すメモリ内のデータ/値、つまりその数値インデックスのアドレスの内容にアクセスしたい場合、次のようになります。 ディリファレンス ポインタを指定します。

コンピュータの言語によって、指されたオブジェクトの(現在の)値に興味があることをコンパイラやインタープリタに伝える記法は異なりますが、ここではCとC++を中心に説明します。

ポインタのシナリオ

C言語で、次のようなポインタが与えられたとします。 p 以下のように

const char* p = "abc";

...文字 'a', 'b', 'c' をエンコードするための数値が入った4バイトと、テキストデータの終わりを示す0バイトは、メモリのどこかに保存され、そのデータの数値アドレスは、以下のように保存されます。 p . このようにC言語がメモリ上でテキストをエンコードする方法は、次のように知られています。 ASCIIZ .

例えば、文字列リテラルのアドレスがたまたま0x1000で、かつ p 0x2000 の 32 ビットポインタの場合、メモリの内容は次のようになります。

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

上記では、コンパイル時に「MySQL」という変数が必要なことが分かっていたはずです。 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_

マルチバイトデータ型

ポインターを使うには、コンピュータ・プログラムも、指し示すデータの種類をある程度理解する必要があります。そのデータの種類が表現するのに1バイト以上必要な場合、ポインターは通常、データの中で最も小さい番号のバイトを指し示すことになります。

では、もう少し複雑な例を見てみましょう。

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 割り当てられたオブジェクトのオーナーが一人である場合のために...

{
    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

Nullポインタ

C言語では NULL0 - で、さらに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++のみ)静的オブジェクトやその基底の直接・間接メンバ変数,あるいはゼロ初期化(例. new T();new T(x, y, z); はポインタを含むTのメンバに対してゼロ初期化を行うのに対して new T; を使用しない)。

さらに 0 , NULLnullptr ポインタのビットがすべてリセットされているとは限りません。ポインタはハードウェアレベルでは "0" を含まないかもしれないし、仮想アドレス空間ではアドレス0を参照しているかもしれません。コンパイラがそこに何か他のものを格納することは許されますが、それが何であれ、もしあなたがそのポインタと 0 , NULL , nullptr またはそのいずれかが割り当てられている他のポインタと比較した場合、期待通りに動作する必要があります。つまり、コンパイラレベルのソースコード以下では、CおよびC++言語では、"NULL"は、潜在的に少し魔法のようなものです...。

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

より厳密には、初期化されたポインターは、以下のいずれかを識別するビットパターンを格納します。 NULL または(多くの場合 仮想 ) メモリーアドレス。

より複雑なケースでは、ポインタは特定のメモリ領域に対する相対的なもので、CPU は CPU のセグメントレジスタやビットパターンにエンコードされたセグメント ID に基づいて選択し、そのアドレスを使用するマシンコード命令によって異なる場所を探すことができます。

例えば int* を指すように適切に初期化された int 変数にキャストした後に float* - がアクセスするメモリとは全く異なるquot;GPU"メモリにアクセスします。 int 変数にキャストして関数ポインタとして使用すると、プログラムのマシンオペコードを保持する、さらに別のメモリを指すかもしれません (このとき、数値は int* これらのメモリ領域内では、事実上ランダムで無効なポインタとなる)。

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

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

  • 配列の要素へのポインタを取得した場合、ポインタ演算を使用して配列内の他の場所に移動したり、配列内の要素への他のポインタ(または同様にポインタ演算によって同じ最後から1番目の値に移動したもの)と比較するのに有効な、配列の最後から1番目のアドレスを形成することさえ可能です。

  • OSの特定の機能、例えば共有メモリマッピングはポインタを与えることがあり、その場合、その機能にとって意味のあるアドレスの範囲内で "just動作"します。

  • 合法的なポインタをこれらの境界を越えて移動させようとしたり、任意の数値をポインタにキャストしたり、無関係な型にキャストしたポインタを使用しようとすると、通常、以下のような問題が発生します。 未定義の動作 しかし、OSやデバイスドライバなどのコードでは、CやC++の規格では未定義のままでも、その実装やハードウェアでは十分に定義されている動作に依存する必要がある場合があるため、このようなことは避けるべきです。