第10章 ポインタ

テキスト『新・明解C言語 入門編』第10章の内容に関する補足説明を提供します.

10-1 ポインタ

関数の引数

このセクションについての補足説明は現在のところありません.

オブジェクトとアドレス

このセクションについての補足説明は現在のところありません.

アドレス演算子

List 10-2 では,main 関数のブロックの最初に, 2つの変数 n および x と, ひとつの配列 a が宣言されています. このようなオブジェクトの宣言を行うと, それぞれの記憶領域がメモリ上に確保されます, 記憶領域にはアドレスがあります. いま,オブジェクトの「宣言」と言いましたが, オブジェクトにメモリ領域が割り当てられていますから, これはオブジェクトの「定義」にもなっています. 宣言と定義の違いについてはテキスト p.147 で学習しました. 実体を作り出す,すなわち,実際にメモリが割り当てられるとき, それを定義と呼びます.

テキストでは,List 10-2 の実行結果として, 3桁のアドレスを表示しています. 実際の環境では下図のようにもっと長いアドレスが表示されます. 先頭に 0x が付けられた表記は16進定数です(テキスト p.194).

result of List 10-2

ポインタ(pointer)とは何か, テキストでは明確に定義されていません. とりあえず,ポインタとは記憶域上のアドレスのことであると考えてください, Table 10-1 では,「a のアドレス」を「a へのポインタ」と言い換えています. しかし,ポインタとはアドレスのことであるという理解では, いろいろ疑問の点があるでしょう.たとえば, ポインタは「指し示すもの」という意味ですので, アドレスのことをなぜポインタと呼ぶのか,わかりにくいですね. とりあえず,ポインタとはアドレスのことであると理解して, 先に進んでください.疑問は次第に解決されると思います.

ポインタを生成する 単項&演算子(unary & operator), すなわち,アドレス演算子(address operator)は, scanf 関数の中でこれまでも使ってきました. たとえば,キーボードから入力された整数値を int 型の変数 x に保存するとき,


scanf("%d", &x);

と書きましたね.これは実は, 「入力された値を,変数 x のアドレスが示す場所に保存してください」という依頼だったのです. テキスト p.359 で,scanf 関数の変換指定子 d についての記述を読んでください. 「実引数は,整数へのポインタでなければならない」と書かれています. 少し言い換えると,「実引数は, 整数を保存するために確保された記憶領域を示すアドレスでなければならない」 となります.

scanf 関数でのポインタの使用については, 少し後のセクション(テキスト p.271)で説明されています.

ポインタ

List 10-3 でまず理解してもらいたいことは, int 型や char 型のように, ポインタ型(pointer type)という型があるということです. 整数は int 型の値として扱うことが普通で, int 型の値を保存しておくために int 型の変数が用いられます. 同様に,ポインタ型の値を保存するために,ポインタ型の変数を使います. ポインタ型の値とは何かというと,メモリのアドレスです.

ポインタ型の変数そのもの,あるいは, ポインタ型の変数に保存されているアドレスをポインタと呼びます. List 10-2 で見たように, これまで用いてきた int 型の変数や配列などのオブジェクトはアドレスを持っています, このアドレスはポインタ型の変数に入れることができます. もちろん,ポインタ型の変数自身もアドレスを持っていますが, そのままではこれはポインタではありません. List 10-3 でのポインタ型の変数は isako と hiroko です. これらポインタ型変数には,最初に, int 型の変数 sato と masaki のアドレスがそれぞれ保存されます. アドレス演算子を使って sato と masaki のアドレスを取得し,


isako = &sato;
hiroko = &masaki;

という式で isako と hiroko にそれぞれ代入しています.

次のセクションで説明するように,ポインタ型の変数に保存されているアドレスを使って, そのアドレスが「指し示す」場所にあるオブジェクトにアクセスすることができます. 「指し示す」というイメージを持つと,「ポインタ」という名前は適切だと思えるのではないでしょうか.

ポインタ型の変数に保存されているアドレスが指し示すオブジェクトが int 型であるとき, このポインタ型を「int へのポインタ型」と呼びます. オブジェクトが char 型ならば「char へのポインタ型」, double 型ならば「double へのポインタ型」です. わざわざ「なになにへの」と述べるのは, オブジェクトにアクセスしてそれを使うとき, アドレスだけでなく,そのオブジェクトが何であるかの情報が必要だからです. 少し比喩的に説明しましょう. メモリ上を旅してポインタが指し示す場所を訪れると, そこにあるのは連続した 0 と 1 の数値です(第7章参照). それが何を意味しているのかを知らなければ解釈できません. これは int 型の値が表現されているのだと知っていれば, 0 と 1 の系列を整数として解釈できます.

ポインタ型の変数を宣言するときには,変数名の前にアスタリスク(*)をつけます. たとえば,int へのポインタ型の変数 p を宣言するならば,


int *p;

とします.これは「int 型の変数 *p」というように見えてしまうので, 慣れるまで混乱してしまうところです. 次のセクションで扱う間接演算子との違いもまぎらわしいです. しかし,そういう文法なのでどうしようもありません. 慣れていきましょう.ポインタ型の変数をひとつだけ宣言するときには


int* p;

という書き方も可能です.しかし,これはやめた方がよいでしょう. 次に述べる,ポインタ型の変数を2つ以上宣言するときに困ります.

ポインタ型の変数を2つ以上宣言するには, それぞれの変数名の前にアスタリスクが必要になります. たとえば,List 10-3 では,int へのポインタ型の変数 isako と hiroko を,


int *isako, *hiroko;

と宣言しています.

間接演算子

これまで,変数に保存されている値を使いたいときには, その変数名を使っていました. たとえば,int 型の変数 x に整数値が代入されており, その値を表示したければ,


printf("x = %d\n", x);

のようにしてきました.簡単に述べると, これは「x の内容(整数)を表示してください」と依頼しているようなものです.

ここで,int 型の変数 x の値が保持されているメモリのアドレスが, int へのポインタ型の変数 p に保存されているとしましょう. このときには,「p に保存されているアドレスが指している変数の, 内容(整数)を表示してください」という依頼をすることも可能になります. 変数 x に直接に言及せず,アドレスを使って間接的にアクセスするわけです. 上の printf 文のかわりに,


printf("x = %d\n", *p);

と書けば,変数 x に保存されている値が表示されます. ここで使っているアスタリスクは 単項 * 演算子(unary * operator)です. 間接演算子(indirect operator)とも呼ばれます. ポインタ型の変数にこの演算子を適用すると, ポインタが指すオブジェクトが返されます. ここでの例では,*p という演算によって x が得られます. これら2つが同じものを意味しているので, *p は x のエイリアス(別名)であると言います.

アドレス演算子 & と間接演算子 * は対の関係になっています. オブジェクトにアドレス演算子を適用すると, そのオブジェクト型へのポインタが得られます, 逆に,ポインタに間接演算子を適用すると, ポインタが指しているオブジェクトが得られます.

ポインタ型の変数 p に間接演算子を適用して得られるのは, ポインタが指している変数(たとえば,int 型の x)に保存されている値というよりも, ポインタが指している変数そのものです. したがって,*p という式には値を代入できます. List 10-3 では,


*hiroko = 180;

という代入を行っています. もちろん,変数が得られるということは, 結果としてその変数に代入されている値も得られます. 2つの int 型の変数 a と b があり,b には値が代入されているとき,


a = b;

という代入ができますね.同様に,int へのポインタ型である2つの変数 px と py があるとき,


*px = *py;

という代入は可能です.List 10-6 でこうした代入を行っています. List 10-7 では,if 文の制御式において,


*n1 > *n2;

という値の比較を行っています.

ポインタ型変数の宣言で変数名の前につける記号はアスタリスク, 間接演算子もアスタリスクで,ややこしいですね. 記号としては同じですが,これらは別物です. ポインタ型変数の宣言で使用されるアスタリスクは, テキスト p.103 に一覧がある,区切り子のひとつです. 間接演算子はもちろん演算子です.

10-2 ポインタと関数

関数の引数としてのポインタ

テキストでは,ポインタ型の変数 hight に masaki のアドレスが代入されていることを, 「hight は masaki が好き」と表現していますね. ポインタを理解するための,著者の好みの比喩のようですが, よくわからなければ無視してください. 女性から男性への好意ならまだしも,これでは意味不明ですね. 単に,「int へのポインタ型の変数 hight に保存されているアドレスは masaki を指し示している」と理解すればよいでしょう.

このセクションの最後に述べられてる 参照外し(dereferencing)という語句はニュアンスがわかりにくいですね. 定義としては,テキストに書かれているように, 「ポインタに間接演算子 * を適用して,指しているオブジェクトにアクセスすること」です. テキストでは明示的に述べられていませんが, オブジェクトにアドレス演算子 & を適用してポインタを得ることを, 参照(referencing)と呼びます. この用語があてられた歴史的経緯は知りませんが, オブジェクトそのものから,それを参照するものを得るということでしょう. 間接演算子はアドレス演算子と対ですから, 「参照」と「参照外し」は対の概念となります. 参照するもの(ポインタ)からオブジェクトを得るというニュアンスで, 「参照外し」となります. ややこしいことに,間接演算子は間接参照演算子と呼ばれることがあります. こうなると,「参照」に相当するのはアドレス演算子なのか間接演算子なのか, 混乱させられます.定義に従うしかないですね.

和と差を求める関数

このセクションについての補足説明は現在のところありません.

二つの値の交換

List 10-6 で定義している swap 関数は, ポインタの説明において,いつも例に出される関数です. 他のテキストでもよく取り上げられています. List 10-1 のように一度失敗してみせるのも「お約束」です.

演習 10-2 はかなり難しいです. 作成を指示されている2つの関数と,main 関数以外に, さらに2つの関数が必要となるでしょう. ひとつは,ある年が閏年かどうかを判定する関数です. もうひとつは,月の日数を教えてくれる関数です. 閏年かどうかによって,2月の日数が異なります.

閏年は,

のいずれかです.年(西暦)を受け取り, 閏年ならば 1,閏年でなければ 0 を返す関数


int is_leap(int y)

を書けばよいでしょう.

月の日数を教えてくれる関数


int days_of_month(int y, int m)

は,年(西暦)と月を受け取り,その月の日数を返します. たとえば,2004年は閏年なので,


days_of_month(2004, 2)

は 29 を返します.

『解きながら学ぶC言語』の解答例では,decrement_date 関数と increment_date 関数の定義はとてもエレガントです. そのかわり,理解することが難しいかもしれません. エレガントではありませんがわかりやすい, decrement_date 関数の別解を以下に示します.


void decrement_date(int *y, int *m, int *d)
{
  *d = *d - 1;        /* 日から1を引く。*/

  if (*d == 0) {      /* 日が0になったら */
    if (*m == 1) {      /* 1月の場合は */
      *y = *y - 1;         /* 前年の*/
      *m = 12;             /* 12月 */
      *d = 31;             /* 31日にする。*/
    }
    else {              /* それ以外の月は */
      *m = *m -1;          /* 前月の */
      *d = days_of_month(*y, *m); /* 最終日にする。 */
    }
  }
}

二値のソート

Fig.10-10 では,変数 na のエイリアスとして *n1 および *px という式, nb のエイリアスとして *n2 および *py という式が図示されています. ややこしい関係のようですが, これはポインタ型変数によるアドレスの受け渡しが2回行われた結果です. アドレスがどのように渡されていくかを追って理解すれば, それほど難しくありません. 最初に,main 関数内の int 型変数 na および nb のアドレスが sort2 関数に渡されます. 次に,na の値が nb より大きいとき,これらアドレスが swap 関数に渡されます.

scanf 関数とポインタ

演習 10-3 は第8章で学習したバブルソート(p.218)を思い出せば簡単でしょう. 『解きながら学ぶC言語』の解答例では n1 と n2 の比較から始めていますが, List 8-5 で行ったように,n2 と n3 の比較から始めることもできます.

ポインタの型

List 10-8 をコンパイルすると警告が出ます.

warning message when compiling List 10-8

実行プログラムは作成されますが, 下図のように,実行結果は正しくありません. テキストのプログラムを少し書き換えて,sizeof(int) と sizeof(double) を最初に表示しています.

result of executing List 10-8

List 10-8 は,swap 関数の中の3か所の int を double に修正すれば, 正しいプログラムとなります.

空ポインタ

空ポインタ(null pointer)は「すべてのビットが 0 であるポインタ」です. 第9章で,「すべてのビットが 0 である文字」と定義される, ナル文字を学習したことを思い出してください(p.240), ポインタと文字という違い以外は同一の定義ですね.

空ポインタの主な使い道は,返却値としてポインタを返す関数での, エラーを示す値です.このような関数はこれまで使ってきませんでしたが, int 型や double 型の値を返す関数があるなら, ポインタ型の値(アドレス)を返す関数があってもおかしくはないと思えるでしょう. 空ポインタはどこのアドレスも指さないので, 通常のポインタのように変数へのアクセスには使えません.

空ポインタの値は NULL という記号で表されます.これは 空ポインタ定数(null pointer constant)と呼ばれ, オブジェクト形式マクロ(テキスト p.118)で具体的な値が定義されます. テキスト p.273 では,


#define NULL  0

という定義例が示されています. これは,第8章で学習した EOF の定義(p.228)と似ています.

空ポインタ定数の値がどこで定義されているかは環境に依存します. 大学の演習室の環境(Mac OS X)では, コンパイル時にインクルードされるヘッダファイル


/usr/include/stddef.h

の中では,直接には定義されていません. このヘッダファイルがさらにインクルードする,


/usr/include/sys/_types.h

および


/usr/include/sys/_types/_null.h

の中で定義されています.

第6章の補足説明で紹介した grep コマンドで NULL の定義を見たのが下図です.

#define __DARWIN_NULL ((void *)0)

#define NULL __DARWIN_NULL

スカラ型

レジスタは,コンピュータの記憶装置中のメモリではなく, CPU の一部です. 記憶装置中のメモリにはアドレスがあり, アドレス演算子を使って取得できます. CPU の一部であるレジスタもメモリですが, こちらからはアドレスを取得できません.

テキスト p.163 で説明されているように, register 記憶クラス指定子は, 変数をレジスタに記憶することをコンパイラに促します. ただし,その通りにコンパイラが振る舞うとは限りません. 最近のコンパイラなら,このような指示をするよりも, コンパイラの判断に任せた方が最適な実行ファイルが生成されるでしょう.

コンパイルの結果, 実際に変数が置かれたのがメモリなのかレジスタなのかはわかりません. いずれにせよ,register 記憶クラス指定子を付加して定義されたオブジェクトには, アドレス演算子を適用できません.

10-3 ポインタと配列

ポインタと配列

式の中では,配列はその先頭要素へのポインタに読み替えられます. テキストでは,添字演算子 [ ] を伴わずに, 単独で用いられた配列名を例に説明していますが, この規則は添え字演算子とは関係ありません. 式の中に出てくる a[i] という表記でも, 配列名 a はポインタに読み替えられます. この点は誤解しやすいにもかかわらず, 多くのテキストでは十分な説明がされていません. 以下の書籍の 1-4-3 節に詳しい解説があります.

前橋和弥 (2017) C言語ポインタ完全制覇 技術評論社

「配列全体へのポインタ」という言葉が出てきます. Column 10-2 では,「&配列名は,先頭要素へのポインタとはならず, 配列全体へのポインタとなります」と書かれています. これだけでは, 「配列の先頭要素へのポインタ」と「配列全体へのポインタ」との違いがわかりませんね.. 配列全体へのポインタはテキストでは扱いませんので, わからなくてもかまいません. しかし,違いが気になる人もいると思いますので, 以下で説明しておきます. ついでに,List 10-9 についても少し補足します.

List 10-9 を少し書き換えて,以下のようなプログラムにします.


#include <stdio.h>

int main(void)
{
  int i;
  int a[5] = {1, 2, 3, 4, 5};
  int *p = a;
  int (*array_p)[5] = &a;

  printf("sizeof(int) = %u\n", (unsigned) sizeof(int));

  for (i = 0; i < 5; i++)
    printf("&a[%d] = %p, p + %d = %p\n", i, &a[i], i, p + i);

  printf("array_p = %p\n", array_p);
  printf("array_p + 1 = %p\n", array_p + 1);

  return 0;
}

冒頭の変数定義の最後で, 配列 a 全体へのポインタ &a を変数 array_p に保存します. これは「int 型の値を要素とする大きさ5の配列へのポインタ」を保存するための変数です. この変数の定義 int (*array_p)[5] = &a; の意味がわからないと思いますが, この説明は後で行います. ポインタ型の変数 p は,配列 a の先頭要素へのポインタで初期化されています. 変数定義のすぐ後で,sizeof 演算子で int 型の大きさを取得し,それを表示しています.

List 10-9 を上のように書き換えたプログラムをコンパイルして実行した結果が下図です.

result of List 10-9

ポインタ型変数 p には,最初は, 配列 a の先頭要素へのポインタ(この要素のアドレス)が入れられています. この変数の値に1を加えると,配列 a の次の要素へのポインタ(次の要素のアドレス)となります. アドレスの値が1増えるのではないことに注意してください. 実際,値は4つ増えています. これは,このプログラムの実行環境では,int 型の大きさが4であるためです. ポインタに対して整数値を加減するこうした演算を, ポインタ演算(pointer arithmetic)と呼びます(この用語はテキストにはありません).

配列 a 全体へのポインタが保存されている array_p の値を表示してみると, これは配列 a の先頭要素へのポインタとまったく同じですね. 「配列の先頭要素へのポインタ」と「配列全体へのポインタ」は, 値としてはまったく同じということがわかります.

何が違うかと言うと,これらポインタにポインタ演算を行ったときの結果です. 「配列の先頭要素へのポインタ」に1を加えると配列の次の要素へのポインタとなるのに対して, 「配列全体へのポインタ」に1を加えると,その値は配列の外を指しています. 具体的には,配列が保存されている記憶領域の,すぐ後のアドレスです. 上図では,a[4] のアドレスよりも4大きい値になっています. これで「配列全体へのポインタ」の意味がわかったのではないでしょうか. 配列全体を丸ごと指しているポインタだと考えれば, 1を加えると次の記憶領域を指すことになります. 丸ごと指すと言っても,配列は複数のアドレスにわたって記憶されていますから, すべての値をまとめてポインタとするわけにはいきません. そこで,先頭のアドレスが使われます.

List 10-9 を書き換えたプログラムで, int[5] 型の配列全体へのポインタを保存する変数 array_p を,.


int (*array_p)[5]

と宣言しました.同じようでも,


int *array_p[5]

と宣言すると,これは「int へのポインタ型の値を格納する配列 array_p[5]」 という意味になります. ポインタを意味する * と識別子(配列名)array_p がまとまる前に, array_p[5] がひとかたまりになるわけです. そうなると,これまで int へのポインタ型の変数を, int *p と宣言してきたこととまったく同じです. 変数 p が配列 array_p[5] に変わっただけです.一方.


int (*array_p)[5]

と宣言すると,最初にかっこの中をまとめます. 英語読みすると,まずは array_p is a pointer と解釈します. 次に残りの部分を読んで, array_p is a pointer to an array int[5] となります. 英語読みすると宣言が自然に解釈できるというアイデアは, 上でも紹介した以下の書籍によるものです(3-1 節).

前橋和弥 (2017) C言語ポインタ完全制覇 技術評論社

間接演算子と添字演算子

Column 10-3 で説明されているように, 添字演算子はポインタと整数をオペランドとする2項演算子です. ポインタ p と整数 i について,p[i] は *(p + 1) を意味します. 逆に,*(p + 1) を,より簡単に p[i] と表記していると言えます.

式の中では配列名はポインタに読み替えられるので, 配列 a の要素を a[i] と書くとき, これは *(a + 1) を意味しています.

テキスト p.277 の最後に説明されているように. 大きさ n の配列 a[n] において, 配列の要素を指すポインタは &a[n] まで有効です. 配列の最後の要素を指すポインタは &a[n - 1] ですから, &a[n] は配列の記憶範囲を超えた次のアドレスです. このような仕様のおかげで,


int a[5];
int *p;

/* 省略 */

for (p = &a[0]; p < &a[5]; p++) {
 /* for 文の本体 省略 */
}

というコードが正しくなります. 配列 a の大きさは5ですから,最後の要素は a[4] です. &a[4] < &a[5] ですから, 配列の最後の要素については for 文の本体は実行されます. ポインタ p の値が更新されて &a[5] になったとき, for 文の本体は実行されず,ループを抜けます. しかし,正しくはあっても, ポインタをわざわざ使ってこのようなコードを書く必要はないでしょう. 配列の添字 i を使って,


int a[5];
int i;

/* 省略 */

for (i = 0; i < 5; i++) {
 /* a[i] を含む for 文の本体 省略 */
}

というように書けばよいからです,

このセクションの最後に書かれている.ポインタどうしの減算は, 同じ配列内の要素を指しているポインタの場合にのみ有効です. 計算の結果は, それらポインタがいくつの要素だけ離れているかを表します. 同じ要素ならば 0,となりの要素なら 1 となります. C言語のバイブルと呼ばれる書籍, B. W. カーニハンと D. M. リッチーによる『プログラミング言語C 第2版』では, 「正しいポインタ操作は,同じ型のポインタの代入,ポインタと整数との加算と減算, 同じ配列のメンバーに対する二つのポインタの引算と比較,ゼロの代入やゼロとの比較, である.他のポインタ演算はすべて不正である」(p.126)と書かれています. ここに書かれている操作のうち, 同じ配列のメンバーに対する2つのポインタの引き算と比較は, テキストの旧版にあった次のプログラム(旧版 List 10-12 を一部改変)を実行すると確かめられるでしょう. ポインタ ptr は int へのポインタ型ですが, ポインタ(アドレス)そのものは int 型ではない(たとえば,演習室の環境では long int 型)かもしれないので, ポインタどうしの引き算の結果を int 型にキャストしています.


#include <stdio.h>

int main(void)
{
  int vc[3];
  int *ptr = vc;

  printf("vc     == ptr    : %d\n", vc == ptr);
  printf("&vc[1] <= &vc[1] : %d\n", &vc[1] <= &vc[1]);
  printf("&vc[1] <  &vc[2] : %d\n", &vc[1] < &vc[2]);
  printf("&vc[2] -  &vc[0] : %d\n", (int) (&vc[2] - &vc[0]));

  return 0;
}

実行結果は次のようになります.

result of List 10-12 old

最初に vc と ptr の値を比較しています. ポインタ型変数 ptr には配列 vc の先頭要素のアドレスが保存されています. よって,当然ながら vc と ptr の値は等しく, 1 が返されています. 次は &vc[1] <= &vc[1] という関係が成立しているかどうかのテストです. この関係は成立しているので 1 が返されています. 次も同様のテストです. 最後は &vc[0] と &vc[2] が要素いくつ分離れているかを計算しています. 2つ離れていますので,2 が返されています.

配列とポインタの相違点

このセクションについての補足説明は現在のところありません.

配列の受渡し

関数の仮引数の宣言では,配列はポインタに読み替えられます. たとえば,int a[] と書くと,これは自動的に int *a と読み替えられます. この読み替えが行われるのは,関数の仮引数の宣言の場合だけです. 関数の本体,たとえば main 関数のブロック先頭で配列を宣言するときには, この読み替えは行われません.

関数に配列を直接に渡すことはできません. その代わりに,ポインタを渡します. 配列の先頭要素へのポインタを渡すことが最もよくあるケースですが, 演習 10-5 で行うように,それ以外の要素へのポインタを渡すこともできます.

まとめ

まとめのプログラムで行っているバブルソートは第8章で学習しました(p.218). このプログラムは List 8-5 とほぼ同じです. 配列の要素間で値を交換する swap 関数を定義し, ソートを行う bsort 関数で用いている点が List 8-5 との違いです. 値の交換を行う要素へのポインタを swap 関数に渡します.