第24回
データ構造(3)~ポインタの基本

ポインタという優れた機能

ポインタは難しいとよく言われますが、コンピュータやプログラムについて正しく理解できていれば、それほど難解な機能ではありません。

ポインタと型

ポインタ変数を宣言するときには、型を明示しなければなりません。繰り返しますが、ポインタは単にアドレスだけを保持するのではなく、その型の占有するサイズ(バイト数)も保持しています。

例えば、short int型の変数xのために確保されたアドレスが1000番地だとした場合、変数xのアドレス(&xの示す値)を1増加すると、その値は当然のことながら1001になります。しかし、変数xのアドレスを保持するポインタpxを1増加すれば、その値はshort int型の変数が占有する2バイトを加算して1002になります。

char、short int、long intの3つの型の変数のアドレスとそのアドレスを保持したポインタ変数の値をそれぞれ1ずつ増加し、各変数の値を調べてみましょう。

ポインタはただのアドレスではない

リスト1はunsigned char型、リスト2はunsigned short int型、リスト3はunsigned long int型の変数を宣言し、それぞれ変数のアドレスとそのポインタの値を1ずつ増加しながら値を表示する──という処理を10回繰り返すプログラムです。

&演算子によって取り出したアドレスは定数なので、それを直接増減することはできません。以下のように、別途"addr_a"という変数にアドレスの値を代入して処理しています。
addr_a = (unsigned short int)&a;

サンプルではLSI C試用版を使用したため、コンパイルされた実行形式ファイルはデータ(変数)のアドレスを8ビット(1バイト)で扱うSmallモデル ※1 となっています。アドレスを16ビット(2バイト)で扱う処理系なら、ここは
addr_a = (unsigned long int)&a;
とすることになります。

16ビットのCPU用のOSであるMS-DOSでは、20ビットのアドレス指定(アドレシング)をセグメントとオフセットという2つの値の組み合わせで行うため、1つの領域が16ビットレジスタの限界である640KBで区切られます。そのため、コードとデータの両方が640KBで収まるTiny、コード領域とデータ領域がそれぞれ640KB以内のSmall、コードもデータも640KB超の領域を扱えるLargeなどの『メモリモデル』が存在します。無償で提供されているLSI C試食版は、生成できるメモリモデルがTinyとSmallに限定されています
リスト1:
変数のアドレスとそのポインタを1ずつ増加して値を表示するプログラム(char版)
(ex2401.cEX2401.EXE)
#include <stdio.h>

int main(void)
{
  unsigned char a, *pa;    /* 変数とポインタ */
  int i;
  unsigned short int addr_a;    /* アドレスを保持する変数 */
                                /* LSI C ではアドレスはshort int */

  /* アドレスを代入 */
  addr_a = (unsigned short int)&a;

  /* ポインタを初期化 */
  pa = &a;

  for (i=0; i < 10; i++) {
    printf("short aのアドレス : %04x, ポインタpaの値 : %04x\n", addr_a, pa);
    /* アドレスを1増加 */
    addr_a++;
    /* ポインタを1増加 */
    pa++;
  }
  return (0);
}

リスト2:
変数のアドレスとそのポインタを1ずつ増加して値を表示するプログラム(short int版)
(ex2402.cEX2402.EXE)
#include <stdio.h>

int main(void)
{
  unsigned short int a, *pa;    /* 変数とポインタ */
  int i;
  unsigned short int addr_a;    /* アドレスを保持する変数 */
                                /* LSI C ではアドレスはshort int */

  /* アドレスを代入 */
  addr_a = (unsigned short int)&a;

  /* ポインタを初期化 */
  pa = &a;

  for (i=0; i < 10; i++) {
    printf("short aのアドレス : %04x, ポインタpaの値 : %04x\n", addr_a, pa);
    /* アドレスを1増加 */
    addr_a++;
    /* ポインタを1増加 */
    pa++;
  }
  return (0);
}

リスト3:
変数のアドレスとそのポインタを1ずつ増加して値を表示するプログラム(LONG int版)
(ex2403.cEX2403.EXE)
#include <stdio.h>

int main(void)
{
  unsigned long int a, *pa;    /* 変数とポインタ */
  int i;
  unsigned short int addr_a;    /* アドレスを保持する変数 */
                                /* LSI C ではアドレスはshort int */

  /* アドレスを代入 */
  addr_a = (unsigned short int)&a;

  /* ポインタを初期化 */
  pa = &a;

  for (i=0; i < 10; i++) {
    printf("short aのアドレス : %04x, ポインタpaの値 : %04x\n", addr_a, pa);
    /* アドレスを1増加 */
    addr_a++;
    /* ポインタを1増加 */
    pa++;
  }
  return (0);
}

型によって増減値が異なる

それぞれのプログラムの実行結果は、画面1~画面3のようになります。表示されている値はプログラムの実行時にメモリ上に確保されたアドレスなので、実行環境によって異なります。

従って、みなさんがサンプルを実行した場合、必ずしも同じ値にはなりません。注目していただきたいのは、16進数で示された各値の『差』です。

1バイトのchar型(画面1)では、変数のアドレスとポインタの値が同じく1ずつ増加しています。2バイトを占有するshort int型(画面2)では、アドレスが1ずつ増加するのに対して、ポインタの値は2ずつ増加しています。

4バイトを占有するlong int(画面3)では、アドレスが1ずつ増加するのに対して、ポインタの値は4ずつ増加しています。

このようにポインタ変数は、その値を加減算すると型の占有するサイズの分だけ値を増減します。この性質が、配列の各要素に対する相対的なアクセスに威力を発揮するのです。




初期化が必須

ポインタを利用すると、変数の型に応じてアドレスを増減できます。つまりポインタを扱うには、その値の操作(増減)の対象となる変数が存在しなければならない、ということです。

ポインタは単独で機能するのではなく、元になる変数が必須です。ポインタを扱う前には、必ず変数のアドレスを代入して初期化しなければなりません。

変数は初期化しないとゴミを持っていることを説明しました。普通の変数であれば、仮に初期化しなくてもゴミを値として扱えます(プログラムとしては間違っていますが、とりあえず処理は実行されます)。しかしポインタがゴミの値を保持していれば、それはどこか『とんでもないアドレス』を示していることになります。

ポインタはそれ自体がプログラムの処理として有効な値を常に保持している訳ではありません。値を参照する前に、必ず通常の変数のアドレスを代入して初期化しなければならない──という点に注意しましょう。


このようなポインタの性質は、たくさんの要素を持つ配列を先頭から順に扱う場合に非常に有利です。数値の配列では、特定の要素を必要なときに(アトランダムに)アクセスすることが多く、先頭から順にアクセスするケースは例に示した各要素の初期化などに使える程度です。

賢明な読者の方は、もうお気付きでしょう。配列の先頭から順に1つずつ要素にアクセスするという形が最も有効なのは、文字列──char型配列です。

ポインタは、文字列を扱う際に威力を発揮します。次回は、文字列をポインタで扱うためのテクニックを紹介します。

【注意】

今回のサンプルに収録した実行形式ファイルは、拡張子.exeのMS-DOS用プログラムです。Windowsのコマンドプロンプトからも実行できます。Linuxなど他のOSで試す場合は、本文にあるようにアドレスを保持する変数の型キャストなどソースを書き直した上で、それぞれの環境でコンパイルしてください。

あとがき

hiropの『ちょっと気になる専門用語』~《ポインタという壁》

よく「ポインタはCを学ぶ上での最大の壁」などと言われます。しかし、アドレスを型と絡めて扱えるというポインタの性質を理解すれば、「壁」というほど難しいものではありません。

Cの入門書では「ここは大切なところだから、しっかり理解して欲しい」という意味で「ポインタは難しい」と書かれているのでしょう。しかし、「怖い怖い」と思っていると、風鈴の音に身震いするように、「難しい」という暗示がかえってその理解を難しくしているのかもしれません。

確かに、Visual Basicなどメモリやアドレスを意識しないで済む高級言語からCへやってきた人は、ポインタというよりアドレスや変数の型と占有するサイズについて「分かりにくいなー」「取っつきにくいなー」という印象を持つと思います。

しかし、例えば図書館の検索システムが実際の本ではなく『目指す本のある場所』を教えてくれたり、駅の案内板が実際の列車ではなく『列車の発着するフォームと出発時刻』を示しているように、我々の生活の中には『実体を間接的に示すもの』がたくさんあります。ポインタもそのような情報の仲間だと捉えれば、ごく当たり前の存在だと分かります。

メモリ上のアドレスと変数のあり方をイメージとして捉えれば、ポインタはそれほど難しいものではないでしょう。


ポインタに対する理解は、メモリとアドレスをイメージできるかどうかで大きく変わってきます。

本コラムではタイトルに「もう一度」と付いているように、一度Cを学んだ人を対象にしているため、メモリやアドレスに関する説明は割愛しました。しかし、今回の説明でもいまひとつ難しいと感じる方がいらしたら、ぜひご意見をお寄せください。ご要望が多ければ、アセンブリ言語レベルでのメモリ、アドレス、変数などについて補足し、それをポインタの理解につなげてみたいと思います。