この記事では、私がポインタをまとめる次元を、四文字で要約すると、「両己三他」です!読んでいてまったく口調が良くないし、韻も踏んでいないですね。何だこれ。この「両己三他」を詳しく言うと、己址、己値、他値、他址、他型です。

私はこの5つの次元からポインタについてもう一度話せると思います。ただし、話す前に、ポインタの「両己三他」の次元をすべて含むプログラムを書きました。それから一つ一つの次元の意味を説明していきます。あなたもそうだと思いませんか?

ポインタを使うほとんどの場面では、この5つの次元で十分に理解する助けになるはずです。ただし、ポインタを使う特別な場面では、5次元法では役に立たないかもしれません。

この先は長文です。読んでいて飽きてしまったら、この記事をブックマークして、時間があるときに続きを読んでください。

一、プログラムコード

1.1. コード

例

#include <stdio.h> int main(void) { int *pInt = NULL; printf("ポインタ変数pInt自身のアドレスは: 0X%x\n", &pInt); //printf("ポインタ変数pInt自身の値は: 0X%x\n", pInt); int para = 1; printf("変数para自身のアドレスは: 0X%x\n", &para); printf("変数para自身の値は: 0X%x\n", para); pInt = &para; printf("ポインタ変数pInt自身の値は: 0X%x\n", pInt); printf("ポインタ変数pIntの値は: 0X%x\n", *pInt); int arr_int[2] = {1, 2}; pInt = arr_int; printf("arr_int第一个元素arr_int\n", pInt); printf("arr_int第二个元素arr_int\n", pInt + 1); double *pDouble = NULL; double arr_double[2] = {1.1, 2.2}; pDouble = arr_double; printf("arr_double第一个元素arr_double\n", pDouble); printf("arr_double第二个元素arr_double\n", pDouble + 1); return 0; }

実行結果は次のとおりです:

私は自分の記事で自分が理解していることを説明するとき、非常に簡単なプログラムを使って明らかにするのが好きです。だから、難解なプログラムでなければ説明にならないと考える人や、低レベルなプログラムを軽蔑する達人たちは、私を無視してください^_^。

2.2. int型変数para

プログラム中:

int para = 1;
printf("变量para自己的地址是:  0X%x\n", &para);
printf("变量para自己的值是:  0X%x\n", para);

変数paraが定義されています。それには自分自身のデータ値と、自分自身の格納アドレスがあります。これらはどれも簡単に理解できます。実行結果から見ると、変数para自身のデータ値は16進数の「0X1」で、アドレスは16進数の「0X22feb4」です。言い換えると、メモリ内でアドレス「0X22feb4」から始まる記憶領域に、4バイトでデータ値「0X1」が格納されています。私のマシンでは、int型変数は4バイトを占有します。あなたのマシンでは、私と異なるかもしれません。

int型変数paraについては、誰でも理解しやすいでしょう。次に、変数paraがメモリ内に格納されている状況を示す模式図を描きます。次のとおりです:

次に、「両己三他」の概念について説明します。

二、両己三他

「両己三他」を詳しく言うと、己址、己値、他値、他址、他型です。

2.1. 己址

2.1.1 「己址」の概念

「己址」とは、「自分のアドレス」の略です。ポインタpIntは変数として、int変数paraと同じように、メモリ内の一定の記憶領域に格納される必要があり、この記憶領域にも開始アドレスがあります。つまり、ポインタ変数pIntにも自分自身のアドレスがあるということです。先ほど、変数paraのアドレスは「0X22feb4」であると言いました。では、ポインタ変数pIntのアドレスは何でしょうか?

2.1.2 「己址」の取得

私たちは皆、「&」がアドレスを取得する演算子であることを学びました。プログラム中では:

printf("指针变量pInt自己的地址是:  0X%x\n", &pInt);

「&」を使ってポインタ変数pIntのアドレスを取得します。実行結果から見ると、ポインタ変数pIntのアドレスは「0X22feb8」です。私のマシンでは、ポインタ変数pIntも4バイトを占有します。したがって、ポインタ変数pIntは開始アドレス「0X22feb8」から始まる4バイトの領域に格納されます。

2.1.3 「己址」のコードでの書き方

コード中で、ポインタ変数pIntの「己址」を表すコードの書き方として、よくあるのは:

&pInt;

さて、先ほどの模式図を完成させましょう。図にポインタ変数pIntの「己址」を加えて、ポインタ変数pIntがメモリ内でどのような格納形式であるかを示します。

「己址」とは、こういう意味です。ほら、特に難しくないでしょう!

2.2 己値

2.2.1 「己値」の概念

「己値」とは、「自分のデータ値」の略です。ポインタpIntは変数として、変数paraと同じように、自分自身のデータ値を持っています。

2.2.2 「己値」の取得

先ほど、変数para自身のデータ値は「1」であると述べました。では、ポインタ変数pInt自身のデータ値は何でしょうか。プログラム中では:

pInt = &para;
printf("指针变量pInt自己的值是:  0X%x\n", pInt);

私は「&」演算子を使って、変数paraのアドレス値をポインタ変数pIntに代入し、printfによってポインタ変数pIntのデータ値を出力しました。実行結果から見ると、ポインタ変数pInt自身のデータ値は「0X22feb4」です。さらに、変数paraのアドレスも「0X22feb4」です。したがって、

pInt = &para;

この文の本質は、変数paraのアドレスをポインタ変数pIntの己値に与えたことであり、これによってポインタ変数pIntと変数paraが結び付けられました。

「己址」の中で述べたように、ポインタpIntのデータ値はアドレス「0X22feb8」から始まる4バイトのメモリに格納されています。つまり、アドレス「0X22feb8」から始まるメモリの後続4バイトが、データ値「0X22feb4」を格納するために使われているということです。

2.2.3 「己値」のコードでの書き方

コード中で、ポインタ変数pIntの「己値」を表すコードの書き方として、よくあるのは:

pInt;

また、次のようなコードの書き方もあります:

pInt + N;
pInt - N;

この書き方は、pIntの「己値」に数値Nを加える、または数値Nを引くという意味です。これについては「他型」という属性を説明するときに触れます。次のような書き方もあります:

pIntA - pIntB;

この書き方は、2つのポインタ変数が「己値」で減算を行うことを表します。

2.2.4 模式図

さて、引き続き上記の模式図を完成させましょう。ポインタ変数pIntの己値を加えます。

したがって、一般的に言えば、「己値」はポインタ変数pIntにとっては自分自身のデータ値であり、他のint型の変数にとってはそれらのアドレスです。

2.3 他址

2.3.1 「他址」の概念

「他址」の概念は、「他人のアドレス」という意味です。実は先ほど己値について述べたとき、すでにそれとなく「他址」の概念に触れていました。

2.3.2 「他址」の取得

整数型変数paraは、メモリアドレス「0X22feb4」から始まる4バイトに格納されています。プログラム中で、私は次のようにして

pInt = &para;

変数paraのアドレスをポインタ変数pIntに代入することで、ポインタ変数pIntと変数paraが結び付けられました。より本質的に言えば、「他人のアドレス」をポインタ変数pIntの「自身の値」に代入したのです。ここで、「他人のアドレス」の「他人」とは変数paraを指し、「他人のアドレス」の「アドレス」とは変数paraのアドレスを指します。注意してください。「他人のアドレス」と「自身の値」はデータ値として同じです。では、何か気づきましたか?

多くの教科書で言うところの「ポインタはアドレス変数であり、他の変数のアドレスを格納する」というのは、要するに「他址」という次元のデータ値が「己値」という次元のデータ値と等しいと言っているだけであり、教科書がそこまで明確に説明していないだけです。

2.3.3 模式図

もう一度、あの模式図を完成させましょう。今回は「他址」の概念を加えます。

2.4 他値

2.4.1 「他値」の概念

「他値」とは、「他人のデータ値」という意味です。

2.4.2 「他値」の取得

プログラム中で、私は次のようにして

pInt = &para;

変数paraのアドレスをポインタ変数pIntの「自分自身の値」に代入することで、ポインタ変数pIntと変数paraが結び付けられました。このとき、「他人のデータ値」の「他人」とは変数paraを指し、「他人のデータ値」の「データ値」とは変数paraのデータ値「1」を指します。プログラム中で、私は次のようにして

printf("指针变量pInt的他值是:  0X%x\n", *pInt);

:これはどうでしょう?実は *(pInt + 1) と同じです。

2.4.3 「他値」のコードの書き方

あなたがコードでよく見かけるああいったコードの書き方、例えば *pInt のような書き方は、何を意味しているのでしょうか?実は、ポインタ変数pIntの「他値」を計算しているのです!

そうした書き方とは:*(pInt + 1)、*pInt + 1、pInt

*(pInt + 1):もし pInt + 1 を別のポインタと見なすなら、例えば

int *pTemp = pInt + 1;

では *(pInt + 1) が計算しているのは、本質的にはポインタ変数pTempの「他値」です;

*pInt + 1:これは pInt の「他値」に1を加えることです;

pInt

2.4.4 模式図

上の模式図をさらに完成させます。今回は「他値」の概念を加えます:

2.5 他型

2.5.1 「他型」の概念

「他型」とは、「他人の型」の略称です。プログラムの中で、ポインタ変数pIntを宣言するとき、次のように書きます:

int *pInt = NULL;

ポインタ変数pIntの前にある「int」は、ポインタ変数pIntの「己値」がint型のデータ値であることを言っているのではありません。そうではなく、ポインタ変数pIntの「他値」がint型のデータ値であることを言っているのです。ここでのポインタ変数pIntの「他値」は変数paraのデータ値「0X1」です。したがって、ポインタ変数pIntの前にある「int」が指すのは、データ値「0X1」が「int」型であるということです。

要するに、ポインタを宣言するときの型は「他値」を修飾するものであり、「己値」を修飾するものではありません。

さらに、変数paraを宣言するときを見てください:

int para = 1;

変数paraの前にある「int」は、変数paraの型が整数型であることを指します。このときの「int」はparaにとって「自型」、つまり「自分自身の型」という意味です。ポインタの宣言時の型だけが「他型」、つまり「他人の型」なのです。

「他型」が「他値」を修飾するものなら、ポインタを宣言するときにこの「他型」を付けることにどんな意味があるのでしょうか?続けて見てください!

プログラムの中で、次のようなコード断片:

int arr_int[2] = {1, 2};    
pInt = arr_int;
printf("arr_int第一个元素arr_int[0]的地址是:  0X%x\n", pInt);
printf("arr_int第二个元素arr_int[1]的地址是:  0X%x\n", pInt + 1);

整数型配列arr_intのアドレスをポインタ変数pIntに代入しました。するとpIntの「己アドレス」は変わらず、まだ0X22feb8ですが、「己値」は変わりました。

さっきポインタ変数pIntの「己値」は「0X22feb4」、つまり変数paraのアドレスでしたが、今は「0X22feac」になりました。これは配列arr_intの最初の要素のアドレスです。つまり、ポインタ変数pIntの「己アドレス」は変わりませんが、「己値」は変更できるということです。

さて、「pInt」と「pInt + 1」の違いを見てみましょう。これはpIntの「己値」を使って演算しています。実行結果を見ると、pIntの「己値」はこのとき「0X22feac」で、pInt + 1の「己値」は「0X22feb0」です。気づきましたか?両者はちょうど4バイト違い、そして「int」型のデータもちょうど4バイトを占めます。

pInt + 1は「己値」に1を足すのだから、「0X22feac + 1」=「0X22fead」になるはずだと思うかもしれません。なぜそうならないのか?それはポインタ変数pIntの「他型」の仕業です。

「他型」の意味を、わかりやすく言うとこうです。「おい、pIntよ。お前の他値はint型のデータ値だ。これからお前が己値に+1、+2、あるいは-1、-2をするとき、馬鹿みたいに本当に1バイト、2バイト足したり、本当に1バイト、2バイト引いたりするんじゃないぞ。int型は4バイトを占めているんだから、4バイトを1単位として、1 * 4バイト、2 * 4バイトを足すか、1 * 4バイト、2 * 4バイトを引くんだ。分かったか?あ、ちなみに、pInt + NのNは正でも負でも構わない」

もちろん、お使いのマシンで「int」型が8バイトを占めるなら、pInt + 1は「己値」に8バイト加えることであり、pInt + 2は「己値」に 8 * 2 = 16バイト加えることです。そういう意味です。

プログラムの中で、この「他型」を説明するための例をもう一つ挙げました。プログラムは次のとおりです:

double *pDouble = NULL;
double arr_double[2] = {1.1, 2.2};
pDouble = arr_double;
printf("arr_double第一个元素arr_double[0]的地址是:  0X%x\n", pDouble);
printf("arr_double第二个元素arr_double[1]的地址是:  0X%x\n", pDouble + 1);

今回はポインタ変数pDoubleを宣言します。その「他型」は「double」型で、「己値」は配列arr_doubleのアドレス、「他値」は配列arr_double

3. まとめ

そろそろまとめる頃合いです。

ポインタ変数を一つ宣言します:

type *pType = NULL;

pTypeには5つの次元があります。それぞれ:

pType = (己アドレス,己値,他アドレス,他値,他型);

3.1 己址:すなわち「自分のアドレス」

ポインタ変数pTypeは変数として、自分自身のアドレスもあります。よくあるコードの書き方は『&pType』です。

己アドレスは一般的なプログラムでは頻繁には使われません。使うとすれば『ポインタのポインタ』に関わってきますが、これはまた別の話題なので、本稿では触れません;

3.2 己値:すなわち「自分のデータ値」

ポインタ変数pTypeは変数として、自分自身のデータ値もあります。コードの書き方は『pType』です。

また、己値に対して加減算を行うこともできます。よくあるコードの書き方には『pType + N』、『pType - N』、『pType2 - pType1』などがあります。

3.3 他址:すなわち「他人のアドレス」

ポインタ変数pTypeの己値は、自分自身のデータ値を表すだけでなく、pTypeに結び付けられた『type』型の変数のアドレスも表します。一般に、ポインタ変数pTypeの『己値』と『他アドレス』はデータ値としては同じです。

type型の変数をpTypeに結び付ける一般的な方法は:pType = &変数;です。

3.4 他値:すなわち「他人のデータ値」

type型の変数がpTypeに結び付けられると、ポインタ変数pTypeはいくつかのコードの書き方によって、type型変数の値、つまり『他値』を取得できます。よくあるコードの書き方には、『*pType』、『pType->』などがあります。

そして、これらのコードの書き方、『*(pType + N)』、『*(pType - N)』、『pType[N]』も『他値』を取得するものですが、特別に説明しておく必要があります:

pType + N は次のように見なせます:

type *pTemp = pType + N;

『*(pType + N)』が実際に計算しているのは、ポインタ変数pTempの『他値』です。

『*(pType - N)』は、理解しやすいでしょう;

『pType[N]』は実は『*(pType + N)』と同じです。丸暗記してください。

3.5 他型:すなわち「他人の型」

ポインタ変数pTypeを宣言するとき、前にある『type』はpTypeの『己値』を修飾するものではなく、『他値』を修飾するものです。つまり、『type』はpTypeの『己値』がtype型のデータ値だと言っているのではなく、pTypeの『他値』がtype型のデータ値だと言っているのです。

『他型』のコードにおける役割は、主に『pType + N』、『pType - N』を計算するときに、pTypeが ( N * sizeof(type) ) バイトを加算または減算することです。

ポインタはいつも人をクラクラさせますが、おそらくこの5つの次元のうちの1つまたはいくつかでクラクラさせられているのでしょう。この5つの次元をしっかり理解し尽くせば、ポインタなんてただの張り子の虎です。

4. 練習問題の解説

5つの次元を説明し終えたら、実際に手を動かす練習問題がなければいけませんね。以下に、ポインタに関連する練習問題をいくつか挙げます。どれも初心者をクラクラさせてダウンさせるものです。この5つの次元を使ってこれらの問題を解きほぐしていくので、少しは楽になるかどうか見てください!

4.1 配列要素の合計

4.1.1 プログラム

最初の例題はよくあるプログラムで、配列要素の合計を求めるものです。プログラムは次のとおりです:

例

#include <stdio.h> int main(void) { int *pArr = NULL; int sum = 0; int arr[3]= {1, 2, 3}; pArr = arr; printf("配列要素は:"); for(int index = 0; index < 3; index++) { printf("%d ", pArr[index]); } printf("\n"); for(int index = 0; index < 3; index++) { sum = sum + *(pArr + index); } printf("配列要素の合計は: %d\n", sum); return 0; }

プログラムはとても簡単で、まず配列の全要素を出力し、それから配列の全要素の合計を計算します。実行結果は次のとおりです:

4.1.2 「二己三他」による解釈

4.1.2.1 配列要素の出力

配列要素を出力するとき、コードは次のとおりです:

pArr = arr;
printf("%d ", pArr[index]);

このコードは次のものと同等です

pArr = arr;
printf("%d ", *( pArr + index));

ここでは『己値』と『他型』を使って加算演算を行い、『他値』を使って配列要素を取得しています。

『己値』:コードはまず配列名arrのデータ値をpArrの『己値』に代入します。配列名arrのデータ値は何でしょうか?arr

のアドレスです。

int *pTemp = pArr + index;

のアドレスです。

「他型」:pArrの他型は「int」型です。pArr + indexは、pArrの己値に何バイト加えるのでしょうか?pArrの他型はint型なので、pArr + indexはpArrの己値に index * sizeof(int) バイトを加えることを意味するのではないでしょうか!

pArr:pArr + 0と書くことができます。つまり 0 * 4 = 0バイトを加えることになり、このときpTempの己値はarr

pArr + 1:1 * 4 = 4バイトを加えることになり、このときpTempの己値はarr

pArr + 2:2 * 4 = 8バイトを加えることになり、このときpTempの己値はarr

このようにして、pArr + indexで配列の全要素のアドレスを走査できます。

お気づきのとおり、pTempの己値は変化し続けますが、pArrの己値と己アドレスは常に変わりません。

「他値」:pArr + indexで配列の全要素のアドレスを走査できるのであれば、*(pArr + index)、つまり*pTempを使えば、pTempの他値を取得できるのではないでしょうか。こうして配列の全要素の値も走査できるのです!

4.1.2.2 配列要素の合計

配列要素の合計を求める場合に使用するコードは次のとおりです:

sum = sum + *(pArr + index);

先ほどの配列要素の出力に関する分析から、このコードの中でpArrがどう動くか、みなさんもお分かりですよね!

pArr + indexは依然としてpArrの己値を使って加算を行い、一時ポインタpTempの己値を取得します。このpTempの己値は各配列要素のアドレスです。

さらに *(pArr + inedx)、つまり*pTempを使って、一時ポインタpTempの他値、すなわち各要素の値を取得します。

最後に、pTempの各他値をすべて合計して、配列要素の和を算出します。

4.1.2.3 まとめ

みなさん、「二己三他」の観点から、pArr、pArr + index、*(pArr + index)、pArr[index]、*pArr + indexなどの一般的なコードの書き方を理解してみてください!

4.2 ポインタ配列

ポインタ配列はポインタと配列を組み合わせたもので、初心者の方には理解しにくいかもしれません。ポインタ配列はかなり大きなテーマです。関連概念については一般的なC言語の教科書を参照してください。ここでは「二己三他」の概念を使って、プログラム中のポインタ関連の概念を説明するだけに留めます。

4.2.1 プログラム

ポインタ配列について、次に例を挙げます:

例

#include <stdio.h> int main(void) { char *arr[3] = {"abc", "def", "ghi"}; char *pArr = arr[0]; printf("文字列配列arrの各文字列要素は次のとおりです:"); for(int index = 0; index < 3; index++) { printf("%s ", arr[index]); } printf("\n"); printf("文字列配列arrの最初の文字列の各要素は次のとおりです:"); for(int index = 0; index < 3; index++) { printf("%c ", *(pArr + index) ); } printf("\n"); return 0; }

実行結果は次のとおりです:

4.2.2 「二己三他」による解釈

4.2.2.1 すべての文字列を出力する

まずポインタ配列の定義を見てください:

char *arr[3] = {"abc", "def", "ghi"};

この配列の各要素は文字列のように見えますが、実際には本質的に次のようになっています:

char *pChar1 = "abc", *pChar2 = "def", *pChar3 = "ghi";
char *arr[3] = {pChar1, pChar2, pChar3};

配列arrの各要素は、実際には「他型」が「char」であるポインタです。

arr

printf("%s ", arr[0]);

本質的には次のようになります:

printf("%s ", pChar1);

pChar1の己値または他アドレスを使用して、文字'a'のアドレスから始まり、後続の'b'と'c'を一つずつ出力します。

pChar2とpChar3についても同様に理解できます。

4.2.2.2 最初の文字列"abc"の各文字を出力する

コードは次のとおりです:

char *pArr = arr[0];
printf("%c ", *(pArr + index) );

arr

pArr + indexは、pArrの己値に index * sizeof(char) バイトを加えて、一時ポインタ変数pTempを生成します:

char *pTemp = pArr + index;

このポインタpTempの己値または他アドレスは、順に文字'a', 'b', 'c'のアドレスになります。つまり、pTempの他値も順に文字'a', 'b', 'c'になります。このようにして、ポインタpTempは文字列"abc"の各文字を順に走査できます。

4.3 連結リスト

連結リストはポインタを最も頻繁に使用します。ノードの挿入や削除などで、次のようなコードの書き方に出くわします:

p2 = p1->next;
p1->next = p3->next;
...... 

なんだこれ、もう頭がくらくらする!こっちのnextポインタ、あっちのnextポインタ、あちこち飛び回って。ああもう、頭の中はノリでいっぱいだ。ふふ、「ポインタ5次元分析法」の登場だ!ただ、連結リストについては、やっぱり別の記事で説明しようと思う。連結リストを説明し終えたら、またこれらのnextポインタを説明するからね。言っておくけど、連結リストの本質なんてそんなものだ。理解してしまえば、連結リストはポインタ以上に張子の虎だ。

著者:石家の魚

出典:https://zhuanlan.zhihu.com/p/27974028