第10章 ポインタ(2)
10-3 ポインタと配列
配列とポインタは、異なるものであると同時に、切っても切れない線にある。密接な関係にある配列とポインタについて、共通点や相違点などを学習していく。
ポインタと配列
配列に関して、必ず理解すべき規則が数多くある。まずは、次の規則である。
重要
配列名は、その配列の先頭要素へのポインタと解釈される。
すなわち、aが配列であれば、式 a の評価で得られるのは、&a[0]であるということである。もちろん、配列 a の要素型が Type であれば、得られる&a[0]の型は、配列の要素数とは無関係に Type * 型である。
Note
配列名 a が先頭要素へのポインタとみなされない文脈もある(Column 10-3:右ページ)。
配列名がポインタとみなされることが、配列とポインタとのあいだに密接な関係を生み出している。Fig.10-11 を見ながら学習していこう。
配列 a とポインタ p が宣言されている。ポインタ p に与えられた初期化子 a は &a[0] のことであるから、ポインタ p は、配列 a の先頭要素 a[0] を指すように初期化される。
Note
ポインタ p の指す先は、"先頭要素" であって、"配列全体" ではない。
さて、配列中の要素を指すポインタに対しては、次に示す規則が成立する。
重要
ポインタ p が配列中の要素 e を指すとき、
p + i は、要素 e の i 個だけ後方の要素を指すポインタとなり、
p - i は、要素 e の i 個だけ前方の要素を指すポインタとなる。
Fig.10-11 配列とポインタ①
たとえば、p + 2 は a[0] の 2 個後方の要素 a[2] を指すポインタとなり、p + 3 は a[0] の 3 個後方の要素 a[3] を指すポインタとなる。
すなわち、次のようにいえるわけである。
要素へのポインタ p + i は、&a[i] のことである。
もちろん、式 &a[i] は、要素 a[i] へのポインタであり、その値は a[i] のアドレスである。
Note
あたり前のことであるが、図に示す p は、p + 0 としても同じである。
以上のことを、実際にプログラムを作って確認しよう。右ページの List 10-9 に示すのが、そのプログラムである。
List 10-9
// chap10/list1009.c
// 配列の要素のアドレス(要素へのポインタ)を表示
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // pはa[0]を指す
for (int i = 0; i < 5; i++)
printf("&a[%d] = %p p + %d = %p\n", i, &a[i], i, p + i);
return 0;
}
実行結果一例
&a[0] = 310 p + 0 = 310
&a[1] = 312 p + 1 = 312
&a[2] = 314 p + 2 = 314
&a[3] = 316 p + 3 = 316
&a[4] = 318 p + 4 = 318
配列 a とポインタ p の関係は、先ほどの図と同じである。for文では、各要素へのポインタである、式&a[i]の値と、式 p + i の値を表示している。実行結果から、これら二つの値が同じであることが確認できる。
さて、『p + i が a[i] を指す』のは、p の指す先が a[0] であるときに限られる。
たとえば、Fig.10-12 に示すように、ポインタ p が a[2] を指しているとする。
そうすると、ポインタ p - 1 は a[1] を指して、ポインタ p + 1 は a[3] を指すことになる。
実際にプログラムで確認しよう。次のように変更する("chap10/list1009a.c")。
int *p = &a[2]; // pはa[2]を指す
for (int i = -2; i < 3; i++)
printf("&a[%d] = %p p %c %d = %p\n",
i, &a[i], i < 0 ? '-' : '+',
i < 0 ? -i : i, p + i);
Fig.10-12 配列とポインタ②
Column 10-3 配列名が先頭要素へのポインタとみなされない文脈
配列名は《先頭要素へのポインタ》と解釈されるのが原則であるが、そうならない、例外的な文脈が二つある。
-
sizeof 演算子のオペランドとして現れたとき sizeof(配列名)は、先頭要素へのポインタの大きさではなく、配列全体の大きさを生成する。
-
アドレス演算子 & のオペランドとして現れたとき & 配列名は、先頭要素へのポインタへのポインタとはならず、配列全体へのポインタとなる。
間接演算子と添字演算子
次は、配列内の要素を指すポインタ p + i に間接演算子 * を適用すると、どうなるのかを考えよう。
式 p + i は、p が指す要素の i 個後方の要素へのポインタであるから、それに間接演算子を適用した間接式 *(p + i) は、その要素をアクセスする式(その要素の別名)である。
すなわち、p が a[0] を指していれば、式 *(p + i) は、ある意味で a[i] そのものである。
ここで、次に示す規則も必ず理解しよう。
重要
ポインタ p が配列中の要素 e を指すとき、
要素 e の i 個だけ後方の要素を表す *(p + i) は、p[i] と表記でき、
要素 e の i 個だけ前方の要素を表す *(p - i) は、p[-i] と表記できる。
この規則を反映させて、p.292 の Fig.10-11 を詳細化したのが、Fig.10-13 である。ここでは、3番目の要素 a[2] に着目して理解していく。
- p + 2 が a[2] を指すため、*(p + 2) は a[2] のエイリアスである(図C)。
- その *(p + 2) は p[2] と表記できるため、p[2] も a[2] のエイリアスである(図B)。
- 配列名 a は、先頭要素 a[0] を指すポインタである。したがって、そのポインタに 2 を加えた a + 2 は、3番目の要素 a[2] を指すポインタである(図左側の矢印)。
- ポインタ a + 2 が要素 a[2] を指しているのであるから、そのポインタ a + 2 に間接演算子を適用した間接式 *(a + 2) は、a[2] のエイリアスである(図A)。
図中のA〜Cの式 (a + 2)、p[2]、(p + 2) のすべてが、配列の要素 a[2] のエイリアスであることが分かった。
ここまでは、a[2] を例に考えてきた。一般化してまとめよう。
Fig.10-13 配列の要素を指すポインタと要素のエイリアス
次に示す 4 個の式は、いずれも各要素をアクセスする式である。
- a[i] (a + i) p[i] (p + i) 先頭から i 個後ろの要素
そして、次に示す 4 個の式は、各要素を指すポインタである。
- &a[i] a + i &p[i] p + i 先頭から i 個後ろの要素へのポインタ
Note
なお、先頭要素を指すポインタ a + 0 と p + 0 は、単なる a と p で表せる。また、それらのエイリアスである (a + 0) と (p + 0) は、それぞれ a と p と表せる。
以上のことを、実際にプログラムを作って確認しよう。List 10-10 に示すのが、そのプログラムである。
List 10-10
// chap10/list1010.c
// 配列の要素の値とアドレスを表示
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int *p = a; // pはa[0]を指す
for (int i = 0; i < 5; i++)
printf("a[%d] = %d *(a+%d) = %d p[%d] = %d *(p+%d) = %d\n",
i, a[i], i, *(a + i), i, p[i], i, *(p + i));
for (int i = 0; i < 5; i++)
printf("&a[%d] = %p a+%d = %p &p[%d] = %p p+%d = %p\n",
i, &a[i], i, (a + i), i, &p[i], i, (p + i));
return 0;
}
実行結果一例
a[0] = 1 *(a+0) = 1 p[0] = 1 *(p+0) = 1
a[1] = 2 *(a+1) = 2 p[1] = 2 *(p+1) = 2
a[2] = 3 *(a+2) = 3 p[2] = 3 *(p+2) = 3
a[3] = 4 *(a+3) = 4 p[3] = 4 *(p+3) = 4
a[4] = 5 *(a+4) = 5 p[4] = 5 *(p+4) = 5
&a[0] = 310 a+0 = 310 &p[0] = 310 p+0 = 310
&a[1] = 312 a+1 = 312 &p[1] = 312 p+1 = 312
&a[2] = 314 a+2 = 314 &p[2] = 314 p+2 = 314
&a[3] = 316 a+3 = 316 &p[3] = 316 p+3 = 316
&a[4] = 318 a+4 = 318 &p[4] = 318 p+4 = 318
このプログラムは、int[5] 型配列 a の全要素の値と、要素へのポインタを表示する。
1の 4 個の式と、2の 4 個の式は、それぞれ同じ値として表示される。
Note
配列 a の要素数が n であれば、その要素は、a[0] から a[n - 1] までの n 個である。ところが、要素へのポインタとしては、『&a[0] から &a[n] までの n + 1 個が有効』という規則がある。
たとえば、配列 a は、a[0] から a[4] までの5個の要素で構成されるが、各要素を指すポインタ &a[0] から &a[4] に加えて、&a[5] も正しいポインタとして有効となる(全部で 6 個である)。
このようになっているのは、配列要素の走査における終了条件(末尾に到達したかどうか)の判定の際に、末尾要素の 1 個後方の要素へのポインタが番兵(p.166)として利用できるからである。
なお、&a[6]、&a[7]、… が、a[4] の 2 個、3 個、… 後方の要素に相当する領域を正しく指す、という保証はない。
さて、ここまでの学習から、次のことが分かる。
重要
Type 型配列 a の先頭要素 a[0] を、Type * 型ポインタ p が指すとき、ポインタ p はあたかも配列 a そのものであるかのように振る舞う。
この規則を、ポインタと配列の表記上の可換性と呼ぶことにする。
さて、式 a[i] や p + i の "i" は、ポインタ a や p が指す要素から "何要素分だけ後方に位置しているのか" を表す値である(そのため、先頭要素の添字は、必然的に 0 となる)。
他のプログラミング言語のように、添字が1から始まる、あるいは、上限や下限を自由に指定できる、といったことは、C言語では、原理的にあり得ない。
重要
配列の添字は、先頭要素から何要素分だけ後方に位置するのかというオフセットを表す値であり、必ず 0 から始まる。
さて、ここまで、ポインタと整数の加算を考えてきたが、ポインタどうしを加算することはできない。
Note
ちなみに、ポインタどうしの減算は OK である。
配列とポインタの相違点
配列とポインタの類似点を学習した。次は、相違点を学習していこう。
まずは、右に示す1を考える。int へのポインタ p に代入されているのは、a すなわち &a[0] である。
この代入の結果、ポインタ p が a[0] を指すようになるのは、ここまでに学習したとおりである。
次は2である。代入 x = a の右オペランドは、先ほどと同じ a であるが、今回の左オペランドは、配列 x である。
この代入は、エラーとなる。
配列名 a が配列の先頭要素へのポインタと解釈されるとはいえ、その値は書きかえ不可能である。
というのも、このような代入が、仮に許されるのであれば、配列のアドレスが変更されて別のアドレスに移動できることになってしまうからである。
重要
代入演算子の左オペランドを配列とすることはできない。
Note
第 5 章では、『代入演算子では配列の全要素をコピーできない』と学習した(p.130)。しかし、『代入演算子によって、配列の先頭要素へのポインタを変更することはできない』と説明したほうが正確であることが分かった。
Column 10-4 添字演算子のオペランド
ここでは、添字演算子 [] について、学習を深めていく。
まずは、ポインタ p と整数 i を加算したものに間接演算子 * を適用した間接式 *(p + i) について、考えよう。
( ) 内の p + i は、p と i の加算である。算術型の値どうしを加算する a + b が、b + a と等しいのと同理由で、p + i と i + p は等価である。
ということは、(p + i) と (i + p) は同じ、ということになる。
ここまでくると、配列要素をアクセスする式 p[i] も、i[p] と書けるような気がしてきます。実は、これも OK なのである。
添字演算子 [] は、二つのオペランドをもつ2項演算子である。オペランドの一方の型は、 * Type 型のオブジェクトへのポインタ であり、他方の型は、 * 整数型 である。生成する値の型は、次のとおりである。 * Type 型
添字演算子 [] のオペランドの順序は任意である。すなわち、a + b と b + a が同じであるように、a[3] と 3[a] は同じである。
ポインタ p が配列 a の先頭要素 a[0] を指しているとき、
a[i] (a + i) p[i] (p + i)
の 4 個の式が同じ要素を表すことを学習したが、実は、
a[i] i[a] (a + i) (i + a) p[i] i[p] (p + i) (i + p)
の 8 個の式が同じ要素を表すことが分かった。
List 10C-1 のプログラムを見ると、ほとんどの人は驚くのではないでしょう。
List 10C-1
// chap10/listC001.c
// 添字演算子と間接演算子
#include <stdio.h>
int main(void)
{
int a[4];
0[a] = a[1] = *(a + 2) = *(3 + a) = 7;
for (int i = 0; i < 4; i++)
printf("a[%d] = %d\n", i, a[i]);
return 0;
}
実行結果
配列 a の 4 個の要素すべてに 7 を代入・表示している。
注意
もっとも、i[a] などの紛らわしい表記は、使うべきではない。
配列の受渡し
ポインタと配列の表記上の可換性は、配列を受け取る関数で利用されている。そのことを、List 10-11 のプログラムで考えていこう。
Note
関数 ary_set は、受け取った配列 v の先頭 n 個の要素に val を代入する関数である。
List 10-11
// chap10/list1011.c
// 配列の受渡し
#include <stdio.h>
//--- 配列vの先頭n個の要素にvalを代入 ---//
void ary_set(int v[], int n, int val)
{
for (int i = 0; i < n; i++)
v[i] = val;
}
int main(void)
{
int a[] = {1, 2, 3, 4, 5};
ary_set(a, 5, 99);
for (int i = 0; i < 5; i++)
printf("a[%d] = %d\n", i, a[i]);
return 0;
}
実行結果
まずは、関数 ary_set の宣言の形式に着目する。Fig.10-14 aのようになっている。
実は、C言語の規則によって、図bのように要素数を与えることもできる。そればかりか、図aと図bの宣言の両方が、最終的には図cとして解釈される、という規則がある。すなわち、次のようになっているのである。
仮引数 v は、配列ではなくて、単なるポインタである。
たとえ図bのように要素数を指定しても無視される。
Note
そのため、図bのように要素数付きで宣言された関数に対して、異なる要素数の配列を渡すことができる。たとえば、要素数 12 の配列 d を、図bのように宣言された関数に渡す関数呼出し式 ary_set(d, 12, 99) がエラーになることはない。
Fig.10-14 配列を受け取る仮関数の宣言
関数 ary_set を呼び出す赤色部に着目しよう。単独で現れた配列名は、その配列の先頭要素へのポインタであるから、第 1 引数 a は、&a[0] のことである。
右ページの Fig.10-15 に示すように、関数 ary_set が呼び出される際に、int * 型の仮引数 v は、実引数 a すなわち &a[0](この図では、216 番地)で初期化される。
Fig.10-15 関数呼出しにおけるポインタの受渡し
ポインタ v が配列 a の先頭要素 a[0] を指すのであるから、ポインタと配列の表記上の可換性によって、ポインタ v は、あたかも配列 a そのものであるかのように振る舞います。
当然、ポインタ v を通して配列の要素の値を書きかえると、呼出し側の配列 a の要素の値にそのまま反映される。
重要
関数間での配列の受渡しは、先頭要素へのポインタとして行う。
呼び出された関数では、受け取ったポインタが、呼出し側が渡した配列そのものであるかのように振る舞う。
やりとりするのが、配列そのものではなく、単なるポインタであるため、要素数は別の引数として受渡しする必要がある。
これで、第 6 章で簡単に学習していた、関数間の配列の受渡し(p.160)のカラクリが、ようやく理解できた。
演習 10-5
List 10-11 の関数 ary_set を、ary_set(&a[2], 2, 99) と呼び出すとどうなるか。実行するとともに、その結果を検討せよ。
まとめ
-
アドレスは、記憶域上におけるオブジェクトの場所を示す番地である。
-
Type 型オブジェクト x にアドレス演算子 & を適用したアドレス式 &x は、オブジェクト x へのポインタを生成する。 生成されるポインタの型は Type * 型であり、値は x のアドレスである。
-
Type * 型ポインタ p の値が、Type 型オブジェクト x のアドレスであるとき『p は x を指す』と表現する。
-
Type * 型ポインタ p に、Type 型ではない型のオブジェクトを指させるようなことは、原則として避けるべきである。
-
Type * 型ポインタ p に間接演算子 * を適用した間接式 p は、ポインタ p が指す Type 型オブジェクトを表す。すなわち、p が x を指すとき、p は x のエイリアス(別名)である。
-
ポインタに間接演算子 * を適用してオブジェクトを間接的にアクセスすることを"参照外し"という。
-
関数の引数としてポインタを受け取れば、そのポインタに間接演算子 * を適用して参照外しを行うことによって、呼出し側のオブジェクトに間接的にアクセスできる。
-
一部の例外的な文脈を除き、配列名は、その配列の先頭要素へのポインタと解釈される。すなわち、a が配列であるとき、配列名 a は、&a[0] のことである。
-
配列内の要素を指すポインタ p に対して整数 i を加算/減算した式 p + i および p - i は、p が指す要素の i 個後方/前方の要素を指すポインタである。
-
配列内の要素を指すポインタ p に対して整数 i を加算/減算した式に間接演算子 * を適用した式 (p + i) および (p - i) は、p[i] および p[-i] と等価である。
-
要素型が Type である配列 a の先頭要素 a[0] を Type * 型のポインタ p が指すとき、p はあたかも配列 a そのものであるかのように振る舞う(ポインタと配列の表記上の可換性)。
-
配列名を代入演算子 = の左オペランドにすることはできない。
-
関数間での配列の受渡しは、先頭要素へのポインタとして行う。呼び出された側の関数では、受け取ったポインタが、呼出し側の配列そのものであるかのように振る舞う。やりとりするのが配列ではなくポインタであるため、要素数は別の引数として受渡しする必要がある。
-
いかなるオブジェクトも関数も指さないポインタが、空ポインタである。空ポインタを表す空ポインタ定数は、
ヘッダでオブジェクト形式マクロ NULL として定義されている。 -
算術型とポインタ型の総称がスカラ型である。
演習問題
目次
演習問題13-1:間接演算子と添字演算子の使用
問題の説明
配列の要素にアクセスする異なる4つの方法(a[i], (a+i), p[i], (p+i))を使用して、同じ値にアクセスできることを示すプログラムを作成してください。int型の配列を用意し、各要素に値を設定した後、4つの異なる表記方法でアクセスして表示してください。
期待される結果
a[0] = 5, *(a+0) = 5, p[0] = 5, *(p+0) = 5
a[1] = 10, *(a+1) = 10, p[1] = 10, *(p+1) = 10
a[2] = 15, *(a+2) = 15, p[2] = 15, *(p+2) = 15
a[3] = 20, *(a+3) = 20, p[3] = 20, *(p+3) = 20
a[4] = 25, *(a+4) = 25, p[4] = 25, *(p+4) = 25
ヒント
- 配列名は配列の先頭要素へのポインタとして解釈されます
- 添字演算子
[]
は、ポインタ演算と間接演算子を組み合わせた表現の省略形です a[i]
は*(a+i)
と同等で、p[i]
は*(p+i)
と同等です- main関数のヒント:
演習問題13-2:配列の要素を逆順に表示
問題の説明
ポインタを使用して配列の要素を逆順に表示するプログラムを作成してください。配列の最後の要素を指すポインタを初期化し、このポインタを使って配列の要素を最後から順に表示してください。
期待される結果
配列の要素を逆順に表示します:
*(p-0) = 50 (配列の4番目の要素)
*(p-1) = 40 (配列の3番目の要素)
*(p-2) = 30 (配列の2番目の要素)
*(p-3) = 20 (配列の1番目の要素)
*(p-4) = 10 (配列の0番目の要素)
ヒント
- ポインタを配列の最後の要素を指すように初期化します
- ポインタの減算(p-i)を使って、末尾から前方向に要素にアクセスできます
- main関数のヒント:
演習問題13-3:配列の要素に添字と同じ値を代入する関数
問題の説明
要素型が int 型で要素数が n の配列を受け取って、全要素に添字と同じ値を代入する関数 set_idx を作成してください。以下の関数宣言に基づいて実装してください:
この関数は、v[0] には 0、v[1] には 1、v[2] には 2、...、v[n-1] には n-1 を代入します。
期待される結果
配列の各要素に添字と同じ値を代入した結果:
a[0] = 0
a[1] = 1
a[2] = 2
a[3] = 3
a[4] = 4
a[5] = 5
a[6] = 6
a[7] = 7
a[8] = 8
a[9] = 9
別の配列でも試してみます:
b[0] = 0
b[1] = 1
b[2] = 2
b[3] = 3
b[4] = 4
ヒント
- 関数のパラメータ
int *v
は配列の先頭要素へのポインタです - ポインタを使った表記
*(v+i)
または添字表記v[i]
で各要素にアクセスできます - 代入する値は添字(インデックス)そのものです
- 関数の実装ヒント:
/* * 配列の全要素に添字と同じ値を代入する関数 * 引数: * v - int型配列の先頭要素へのポインタ * n - 配列の要素数 */ void set_idx(int *v, int n) { // ここに関数の実装を書く } int main(void) { int a[10]; // 10要素のint型配列を宣言 // 関数set_idxを呼び出して、配列aの各要素に添字と同じ値を代入 set_idx(a, 10); // 配列の内容を表示して確認 printf("配列の各要素に添字と同じ値を代入した結果:\n"); for (int i = 0; i < 10; i++) { printf("a[%d] = %d\n", i, a[i]); } // 別の配列で試してみる int b[5]; printf("\n別の配列でも試してみます:\n"); set_idx(b, 5); for (int i = 0; i < 5; i++) { printf("b[%d] = %d\n", i, b[i]); } return 0; }
問題13-4:配列の最大値と最小値を求める関数
問題の説明
整数配列の最大値と最小値を求める関数 find_max_min を実装してください。この関数は、配列、要素数、最大値を格納するポインタ、最小値を格納するポインタを引数として受け取り、配列の最大値と最小値を計算して、それぞれのポインタを通じて結果を返します。
期待される結果
ヒント
- ポインタを通じて関数から複数の値を返すことができます
- 最大値と最小値を見つけるには、配列の各要素と現在の最大値/最小値を比較します
- ポインタが指す値を変更するには、間接演算子 * を使用します
- 関数の実装ヒント:
/* * 配列の最大値と最小値を求める関数 * 引数: * a - int型配列の先頭要素へのポインタ * n - 配列の要素数 * max - 最大値を格納するためのint型変数へのポインタ * min - 最小値を格納するためのint型変数へのポインタ */ void find_max_min(const int *a, int n, int *max, int *min) { // ここに関数の実装を書く } int main(void) { // テスト用の配列を初期化 int a[] = {72, 43, 91, 28, 65, 37, 84}; int max, min; // 最大値と最小値を格納する変数 int n = sizeof(a) / sizeof(a[0]); // 配列の要素数を計算 // find_max_min関数を呼び出して最大値と最小値を求める find_max_min(a, n, &max, &min); // 配列の内容と計算結果を表示 printf("配列の要素: "); for (int i = 0; i < n; i++) { printf("%d ", a[i]); } printf("\n最大値: %d\n最小値: %d\n", max, min); // 別の配列でもテスト int b[] = {-5, -10, -3, -8, -1}; n = sizeof(b) / sizeof(b[0]); find_max_min(b, n, &max, &min); printf("\n負の数のみの配列でのテスト:\n"); printf("配列の要素: "); for (int i = 0; i < n; i++) { printf("%d ", b[i]); } printf("\n最大値: %d\n最小値: %d\n", max, min); return 0; }