ポインターの明確化


142

私はこのチュートリアルに従って、ポインターへのポインターがどのように機能するかについて説明しました。

関連する一節を引用させてください:


    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;

これで設定できます

    int **ipp = &ip1;

and ippip1を指していiます。*ippis ip1、and、**ippis i、または5です。次のようなおなじみのボックスと矢印の表記法で状況を説明できます。

ここに画像の説明を入力してください

もしそうなら

    *ipp = ip2;

ippip1)が指すポインターを変更して、のコピーが含まれるip2ようにしip1ましたj

ここに画像の説明を入力してください


私の質問は:なぜ第二画像に、されてippまだを指してip1ではなくip2


6
ipp定義するときにのタイプを追加してください。質問が完了します;-)
zmo

1
そのようにipp指し示してip1いるの*ipp = ip2ip1 = ip2
グリジェシュチャウハン

1
先の尖ったタイプから遠く離れたアスタリスクの書き込みを停止できますか?特にが型宣言以外の完全に異なるものを意味する場合、はにint **ipp比べてはるかに直感的ではありません。int** ipp**ipp
Darkhogg

4
@Darkhogg int **ippは私にはかなり直感的に思えます。つまり、私はを作っ**ippていintます。それは本当です。
ithisa 2014

2
タイプは:@ user54609意味と離れて個人的な直感、これを考慮してippあるint**ので、ちょうど書き込みint**の代わりに、魔法のようなimp間接参照はint型である」了解
Manu343726 2014

回答:


143

ポインティングのアナロジーについては一瞬忘れてください。ポインタに実際に含まれるのはメモリアドレスです。&オペレーター「のアドレス」である-それは、オブジェクトのメモリ内のアドレスを返しますすなわち。*オペレータは、すなわちアドレスを含むポインタを与え、あなたにポインタが参照するオブジェクトを与え、それがそのメモリアドレスにオブジェクトを返します。つまり、そうするとき、*ipp = ip2あなたがしている*ippことは、に保持されippて いるアドレスでオブジェクトを取得し、ip1次ににip1格納されている値ip2(のアドレス)に割り当てることですj

単に
& ->住所
*->価値


14
&と*はそれほど簡単ではありませんでした
Ray

7
混乱の主な原因は*演算子のあいまいさによるものであると私は考えています。これは、変数宣言中に、変数が実際には特定のデータ型へのポインターであることを示すために使用されます。ただし、その一方で、ポインタ(逆参照演算子)が指す変数の内容にアクセスするためのステートメントでも使用されます。
Lucas A.

43

ippの値ではなくが指す値を変更したためipp。したがって、ippまだip1(の値ipp)を指していますが、ip1の値はの値と同じip2になっているため、どちらもを指していjます。

この:

*ipp = ip2;

と同じです:

ip1 = ip2;

11
int *ip1 = &iとの違いを指摘する価値がある*ipp = ip2;かもしれません。つまりint、最初のステートメントからを削除すると、割り当ては非常に似*ていますが、2つのケースではが非常に異なることをしています。
クローマン、2014

22

Cタグのほとんどの初心者の質問と同様に、この質問は第一原理に戻ることによって答えることができます:

  • ポインタは一種の値です。
  • 変数には値が含まれています。
  • &オペレータは、ポインタに変数を回します。
  • *オペレータは、変数にポインタを回します。

(厳密に言えば、「変数」ではなく「左辺値」と言うべきですが、可変ストレージの場所を「変数」として説明する方が明確だと思います。)

したがって、変数があります。

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

変数ip1 ポインタが含まれています。&オペレータがオンiポインタに、そのポインタ値に割り当てられますip1。したがって、へのポインタがip1 含まれていますi

変数ip2 ポインタが含まれています。&オペレータがオンjポインタに、そのポインタがに割り当てられますip2。したがって、へのポインタがip2 含まれていますj

int **ipp = &ip1;

変数ippにポインタが含まれています。&オペレータは、変数をオンip1ポインタにポインタ値が割り当てられていますipp。したがってipp、へのポインタが含まれていますip1

これまでの話をまとめてみましょう:

  • i 5を含む
  • j 6を含む
  • ip1「へのポインタi」を含む
  • ip2「へのポインタj」を含む
  • ipp「へのポインタip1」を含む

今私たちは言います

*ipp = ip2;

*オペレータは、変数にポインタバックをオンにします。の値をフェッチしますipp。これは「ポインターへのポインターでip1あり、変数に変換します。ip1もちろん、どのような変数ですか?」

したがって、これは単に別の言い方です

ip1 = ip2;

したがって、の値をフェッチしますip2。それは何ですか?「へのポインタj」。そのポインタ値をに割り当てるip1ので、ip1「ポインタj」になりました

変更したのは1つだけです:の値ip1

  • i 5を含む
  • j 6を含む
  • ip1「へのポインタj」を含む
  • ip2「へのポインタj」を含む
  • ipp「へのポインタip1」を含む

なぜippまだポイントしip1ていないのip2ですか?

変数を割り当てると、変数が変化します。割り当てを数えます。割り当てより多くの変数を変更することはできません!あなたはに割り当てることによって開始しijip1ip2ipp。次に、に割り当てます*ipp。これは、これまで見てきたように、「割り当て先ip1」と同じ意味です。ipp2回目に割り当てなかったので、変更されませんでした!

変更したい場合ippは、実際に割り当てる必要がありますipp

ipp = &ip2;

例えば。


21

このコードが役立つことを願っています。

#include <iostream>
#include <stdio.h>
using namespace std;

int main()
{
    int i = 5, j = 6, k = 7;
    int *ip1 = &i, *ip2 = &j;
    int** ipp = &ip1;
    printf("address of value i: %p\n", &i);
    printf("address of value j: %p\n", &j);
    printf("value ip1: %p\n", ip1);
    printf("value ip2: %p\n", ip2);
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
    *ipp = ip2;
    printf("value ipp: %p\n", ipp);
    printf("address value of ipp: %p\n", *ipp);
    printf("value of address value of ipp: %d\n", **ipp);
}

それは出力します:

ここに画像の説明を入力してください


12

私の非常に個人的な意見では、矢印がこの方向を指している写真や、ポインタが理解しづらくなる写真です。それはそれらをいくつかの抽象的な神秘的な実体のように見せます。ではない。

コンピューターの他のすべてと同様に、ポインターは数値です。「ポインタ」という名前は、「アドレスを含む変数」と言っただけの派手な方法です。

したがって、コンピュータが実際にどのように機能するかを説明することによって、物事をかき回してみましょう。

int名前iと値5 を持つがあります。これはメモリに保存されます。メモリに保存されているすべてのものと同様に、アドレスが必要です。そうでないと、アドレスを見つけることができません。たとえばi、アドレス0x12345678でj終わり、値6のバディがその直後にあるとします。intが4バイトでポインタが4バイトである32ビットCPUを想定すると、変数は次のように物理メモリに格納されます。

Address     Data           Meaning
0x12345678  00 00 00 05    // The variable i
0x1234567C  00 00 00 06    // The variable j

次に、これらの変数を示します。int 、、int* ip1およびへのポインタを1つ作成しますint* ip2。コンピュータ内のすべてのものと同様に、これらのポインタ変数もメモリ内のどこかに割り当てられます。それらがメモリ内の次の隣接するアドレスに到達するとすぐに考えてみましょうj。以前に割り当てられた変数のアドレスを含むようにポインターを設定しますip1=&i;:(「iのアドレスをip1にコピー」)およびip2=&j。線の間で何が起こるか:

Address     Data           Meaning
0x12345680  12 34 56 78    // The variable ip1(equal to address of i)
0x12345684  12 34 56 7C    // The variable ip2(equal to address of j)

したがって、取得されたのは、数値を含むメモリの4バイトのチャンクです。どこにも神秘的な魔法の矢はありません。

実際、メモリダンプを見ただけでは、アドレス0x12345680にintまたはが含まれているかどうかはわかりませんint*。違いは、プログラムがこのアドレスに格納されているコンテンツを使用する方法を選択することです。(私たちのプログラムのタスクは、実際には、これらの数値をどうするかをCPUに伝えることです。)

次に、さらに別のレベルの間接参照をで追加しint** ipp = &ip1;ます。ここでも、メモリのチャンクを取得します。

Address     Data           Meaning
0x12345688  12 34 56 80    // The variable ipp

パターンはおなじみのようです。数値を含むさらに4バイトのチャンク。

さて、上記の架空の小さなRAMのメモリダンプがある場合、これらのポインタが指す場所を手動で確認できます。ipp変数のアドレスに格納されているものをのぞいて、内容0x12345680を見つけます。もちろんどちらがip1格納されているアドレスです。そのアドレスに行って内容を確認し、のアドレスを見つけてi、最後にそのアドレスに行って番号5を見つけることができます。

したがって、ippの内容を*ipp取得すると、ポインタ変数のアドレスが取得されますip1。書き込むことによって*ipp=ip2、我々はIP1にIP2をコピーし、それは同等ですip1=ip2。どちらの場合でも、

Address     Data           Meaning
0x12345680  12 34 56 7C    // The variable ip1
0x12345684  12 34 56 7C    // The variable ip2

(これらの例は、ビッグエンディアンCPUに対して与えられました)


5
私はあなたの要点を取り上げますが、ポインタを抽象的な不思議な実体として考えることには価値があります。ポインタの特定の実装は単なる数値ですが、スケッチする実装戦略は実装の要件ではなく、単なる一般的な戦略です。ポインタはintと同じサイズである必要はなく、ポインタはフラット仮想メモリモデルのアドレスである必要もありません。これらは単なる実装の詳細です。
Eric Lippert、2014

@EricLippert実際のメモリアドレスやデータブロックを使用しないことで、この例をより抽象化できると思います。location, value, variable場所がどこにあるか1,2,3,4,5、値がA,1,B,C,3であるようなものを示す表である場合、ポインターの対応する概念は、本質的に混乱する矢印を使用せずに簡単に説明できます。どの実装を選択しても、値はある場所に存在します。これは、矢印でモデリングするときに難読化されるパズルのピースです。
MirroredFate 14

@EricLippert私の経験では、ポインターの理解に問題があるCプログラマーのほとんどは、抽象的な人工モデルを与えられたプログラマーです。抽象化は役に立ちません。今日のC言語の全体的な目的は、ハードウェアに近いということです。Cを学習しているが、ハードウェアに近いコードを作成するつもりがない場合は、時間を無駄にしています。コンピュータの動作を知りたくなく、高レベルのプログラミングを行う場合は、Javaなどのほうがはるかに適しています。
ランディン2014

@EricLippertそして、はい、ポインターのさまざまなあいまいな実装が存在する可能性があります。ポインターは必ずしもアドレスに対応していません。しかし、矢印を描くことは、それらがどのように機能するかを理解するのにも役立ちません。ある時点で、抽象的な思考を離れてハードウェアレベルに到達する必要があります。そうしないと、Cを使用するべきではありません。純粋に抽象的な高水準プログラミングを目的としたはるかに適切な最新の言語が数多くあります。
ランディン2014

@Lundin:私も矢印図の大ファンではありません。データとしての矢印の概念はトリッキーです。私はそれを抽象的に考えるのが好きですが、矢はありません。&変数の演算子は、その変数を表すコインを提供します。*そのコインのオペレーターが変数を返します。矢は必要ありません!
Eric Lippert、2014

8

割り当てに注意してください。

ipp = &ip1;

ippを指す結果ip1

そのためippのポイントにするためにip2、我々は同様の方法で変更する必要があり、

ipp = &ip2;

これは明らかにしていません。代わりに、が指すアドレス値を変更していますipp
以下を行うことにより

*ipp = ip2;

に格納されている値を置き換えるだけip1です。

ipp = &ip1は、を意味*ipp = ip1 = &i
ます*ipp = ip2 = &j
したがって、*ipp = ip2と本質的に同じip1 = ip2です。


5
ipp = &ip1;

後の割り当てでの値が変更されることはありませんipp。これが依然としてを指す理由ip1です。

を使って何をするか*ipp、つまりを使ってもip1、がipp指す事実は変わりませんip1


5

私の質問は次のとおりです。なぜ2番目の図で、ippはまだip1を指していますが、ip2を指していないのですか?

あなたは素敵な写真を載せました、私は素敵なアスキーアートを作ろうと思っています:

@ Robert-S-Barnesが彼の答えで言ったように:ポインターを忘れて、何が何を指しているかを忘れますが、メモリの観点から考えてください。基本的に、int*意味は、変数int**のアドレスが含まれ、変数のアドレスが含まれる変数のアドレスが含まれることを意味します。:その後、値またはアドレスにアクセスするために、ポインタの代数を使用することができる&foo手段address of foo、および*foo手段をvalue of the address contained in foo

したがって、ポインタはメモリを扱うことなので、実際にその「有形」にするための最良の方法は、ポインタ代数がメモリに対して行うことを示すことです。

だから、これがあなたのプログラムのメモリです(例の目的のために簡略化されています):

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [   |   |   |   |   ]

最初のコードを実行するとき:

int i = 5, j = 6;
int *ip1 = &i, *ip2 = &j;

これがあなたの記憶がどのように見えるかです:

name:    i   j ip1 ip2
addr:    0   1   2   3
mem : [  5|  6|  0|  1]

あなたが見ることができるip1ip2のアドレスを取得iし、jそしてippまだ存在しません。アドレスは、特別なタイプで格納された単なる整数であることを忘れないでください。

次に、次のippように宣言して定義します。

int **ipp = &ip1;

だからここにあなたの記憶があります:

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  0|  1|  2]

次に、に格納されているアドレスが指す値を変更します。ippこれは、に格納されているアドレスですip1

*ipp = ip2;

プログラムのメモリは

name:    i   j ip1 ip2 ipp
addr:    0   1   2   3   4
mem : [  5|  6|  1|  1|  2]

注意:int*特殊な型と同様に、int *x;またはのint *x, *y;表記は誤解を招く可能性があるので、同じ行で複数のポインタを宣言することは常に避けたいと思います。私は書くことを好むint* x; int* y;

HTH


あなたの例では、の初期値はip23ないはず4です。
ディプト2014

1
ああ、私はメモリを変更して、宣言の順序と一致させました。私はそうすることを修正したと思いますか?
zmo 2014

5

あなたが言うとき

*ipp = ip2

あなたが指しippているのip2は、指し示している記憶の方向を指す「オブジェクトが指し示している」ということです。

あなたがippポイントすることを言っているのではありませんip2


4

逆参照演算子*をポインターに追加すると、ポインターからポイントされたオブジェクトにリダイレクトされます。

例:

int i = 0;
int *p = &i; // <-- N.B. the pointer declaration also uses the `*`
             //     it's not the dereference operator in this context
*p;          // <-- this expression uses the pointed-to object, that is `i`
p;           // <-- this expression uses the pointer object itself, that is `p`

したがって:

*ipp = ip2; // <-- you change the pointer `ipp` points to, not `ipp` itself
            //     therefore, `ipp` still points to `ip1` afterwards.

3

ippを指し示す場合ip2は、と言う必要がありますipp = &ip2;。ただし、これはip1依然としてを指していiます。



3

次のように表される各変数を考えてみます。

type  : (name, adress, value)

だからあなたの変数はこのように表現されるべきです

int   : ( i ,  &i , 5 ); ( j ,  &j ,  6); ( k ,  &k , 5 )

int*  : (ip1, &ip1, &i); (ip1, &ip1, &j)

int** : (ipp, &ipp, &ip1)

値としてippある&ip1のでinctruction:

*ipp = ip2;

アドレス&ip1の値をの値に変更します。ip2つまりip1、変更されます。

(ip1, &ip1, &i) -> (ip1, &ip1, &j)

しかしippそれでも:

(ipp, &ipp, &ip1)

値だから、ippまだ&ip1それはまだを指す意味ip1


1

のポインタを変更しているため*ipp。その意味は

  1. ipp (可変名)----中に入る。
  2. 内部ippはのアドレスですip1
  3. *ippので(内部のアドレス)にアクセスしてくださいip1

今、私たちはにいip1ます。 *ipp(すなわちip1=)ip2.
ip2のアドレス含まj.soのip1コンテンツがIP2(Jのすなわちアドレス)を含んで置き換えることになる、私たちは変更しないippコンテンツを。それでおしまい。


1

*ipp = ip2; 意味する:

ip2指す変数に割り当てますipp。したがって、これは次と同等です。

ip1 = ip2;

にアドレスをip2保存する場合はipp、次のようにします。

ipp = &ip2;

今度はをipp指しip2ます。


0

ippポインタ型オブジェクトへのポインタの値(つまり、ポイント)を保持できます。あなたがするとき

ipp = &ip2;  

次に、変数(pointer)ippアドレスがip2含まれます。これは&ip2、タイプpointer-to-pointerの()です。これでipp、2番目の画像の矢印がを指しip2ます。

Wikiによると
*演算子は逆参照演算子であり、ポインター変数を操作し、ポインターアドレスの値と同等のl値(変数)を返します。これは、ポインターの逆参照と呼ばれます。

タイプポインタの l値にそれを逆参照するために*演算子を適用します。逆参照されたl値はへのポインタ型であり、型データのアドレスを保持できます。ステートメントの後 ippint*ippintint

ipp = &ip1;

ippアドレスを保持しているip1*ipp(指して)のアドレスを保持していますi。これ*ippはのエイリアスですip1。両方**ipp*ip1の別名がありますi
することによって

 *ipp = ip2;  

*ippそして、ip2同じ場所を指すが、両方ippまだを指していますip1

どのような*ipp = ip2;実際に行うことは、それコピーの内容ということであるip2(のアドレスjへ)ip1(として*ippの別名であるip1効果で両方のポインタを作り、)ip1ip2(同じオブジェクトを指していますj)。
だから、2番目の図では、矢印ip1ip2を指しているj一方でipp、まだを指してip1何も変更はの値を変更するために行われていないようipp

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