プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第9回 プログラミングについて 『ポインタの話し』
コラム
2022/07/15

第9回 プログラミングについて 『ポインタの話し』

アポロレポート

『ポインタの話し』


 前回は配列でしたので、今回はポインタの話をしましょう。C言語等を使い始めて最も理解に苦労するのがこのポインタです。FORTRAN77ではポインタなどといったものがないのでアドレスの操作が全く必要ではないかというと、やはり必要になることが多いのです。そのようなときFORTRAN77を使っている人はただ涙を流して我慢しているわけにもいかないので、ちょっとした工夫をするのです。VAX FORTRANでの例ですが、

 INTEGER*4 ADDR      !ARRAY のアドレスを代入するための変数。
 REAL*4  ARRAY(100)
 :
 :
 ADDR=%REF(ARRAY(0))    !ARRAY のアドレスを ADDR に代入。
              !%REF() は変数のアドレスを参照するための組み込み
              !関数。

 CALL SUB(%VAL(ADDR),100) !サブルーチンに ADDR の値を渡す。
 :
 :
 END

 SUBROUTINE SUB(ARRAY,LAST)
 REAL*4  ARRAY(*)    !渡されたのは ARRAY のアドレスになるので通常の配列
              !の使い方と同じになる。
 INTEGER*4 LAST
 :
 :
 RETURN
 END

といった方法で実現するのです。FORTRANは通常ではサブルーチンの引数は、値も変数も全てアドレス渡しですが、値渡しのできる組み込み関数が用意されていればこのように別のサブルーチンに処理をゆだねることで可能になります。ただ自分自身の中ではできないので、まどろっこしいプログラムにはなってしまいますが...

 そのポインタですが、ポインタとは一体何者かといえば名の通り指し示すものです。
変数でも関数でも基本的にはなんでも指し示すのです。上の例では ADDR がそれにあたる訳ですが、FORTRANの場合では(FORTRAN90では可能です)、 ADDRが直接そのアドレスにあるものをアクセスができません。C言語等のポインタをサポートしている言語ではそれが可能になっていますので、その仕組を見てみましょう。

 これまでにも色々な例でポインタを使っていますが、C言語ではポインタをアスタリスクを使って表現します。

 char *sp;

このように記述すれば変数 sp は文字型を指し示す変数となります。ようするに文字型のポインタです。コンピュータ上では実際にこれがどのようになっているかと言えば、変数 sp のアドレスの位置にアドレスを保存する領域が確保されるだけです。すなわち2バイトか4バイトのメモリーが確保されるのです。自動変数のときには、この確保された領域に何が入っているのかは不定で全くでたらめな値です。

 char sp[10];

としたときには、また事情が違っていてコンピュータ上では sp のアドレスの位置にデータを入れるための10バイトの領域が確保されます。このように sp のアドレスの位置に何が確保されているかが違っていても、

 for(i=0;i<=9;i++)sp[i]=' ';

などと同じ記述ができるので混乱してしまうのです。

 main()
 {
  char *sp;

  *sp=10;
 }

としたとき、一体どうなるでしょうか。これは運の良し悪しにもよりますが、プログラムは異常停止してしまいます。先ほども説明しましたが、 sp のアドレスの位置にはアドレスを入れるための領域が確保されているのみで、更に自動変数のためその領域の値は不定となっています。その値をアドレスとして指し示す位置に代入しようとしているので、メモリーの管理がしっかりされているOSではアクセス違反となりますが、そうではないOSの場合は、OSは暴走したりすることもあります。

 ポインタ変数には使用する前に何を指し示すのかを教えてあげなければなりません。

 main()
 {
  char *sp;
  char data[10];

  sp=data;

  *sp=10;
 }

とすると、 sp には data のアドレスが代入され、*sp ではそのアドレスの指し示す位置すなわち data の最初の要素に 10 を代入することになります。

 char sp1[]="abcdefg";
 char *sp2 ="abcdefg";

としたとき、

 printf("%s\n",sp1);
 printf("%s\n",sp2);

とすると結果は同じものが表示されますが、

 strcpy(sp1,"abcdefg");
 strcpy(sp2,"abcdefg");

とすると事情が違ってきます。sp1 は sp1 のアドレスの位置に abcdefg という文字列を保存するメモリーが確保され、abcdefg という文字が代入されているのですが、sp2は sp2 のアドレスの位置に abcdefg という文字列のアドレスを代入するメモリーが確保され、そこに abcdefg のアドレスが代入されているだけなのです。それでは sp2 が指し示す文字列はというと全く別の場所(例えばスタック)にあるのです。基本的にはこの文字列は読み込みのみのアクセスが可能なので、 strcpy 関数で文字列を代入すると sp1 には正常に代入ができますが、sp2 には基本的には不可能です。 基本的というのはこれもOSの違いで、メモリー管理がしっかりされているものはアクセス違反を発生させ、そうでないものでは代入できてしまうからなのです。

構造体を指し示すポインタで、

 struct data_strct{
  int x,y;
  struct data_strct *next;
 };

 struct data_strct *data;

などと記述することはよくあります。この構造体の *next は自分自身と同じ型を指し示すポインタで、data_strct の構造体には int 型の x と y を保管するためのメモリーと data_strct を指し示すアドレスを保管するメモリーが確保されることになります。*next を next と記述すると無限の入れ子になりますので、構造体の宣言が完成しなくなりコンパイル時にエラーとなってしまいます。 ちなみに構造体を指し示すポインタではその要素を指定するときには上の例では、

 (*data).x
 (*data).y
 (*data).next

と記述します。 *data.x と記述すると data 自体は構造体ではないので誤りです。
またC言語は構造体の要素を指し示すときに使用できる便利な演算子があって、

 data->x
 data->y
 data->next

と記述でき、全く同じ意味となります。 data が指し示す構造体が更に指し示している要素は、

 data->next->x
 data->next->y
 data->next->next

と表現できるので、(*((*data).next)).x と記述するよりはわかりやすいでしょう。

 ポインタの配列を作ることも当然のことですが可能です。

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

とすると、 sp のアドレスの位置には3つの連続したアドレスを保管する領域が確保され、それぞれに abc、def、ghi の文字列のアドレスが代入されますので、

 printf("%s %s %s\n",sp[0],sp[1],sp[2]);

とすると3つの文字列が表示されます。ここで大切なことはこの sp[0] と配列の形式で記述したものをポインタの形式で記述すると、

 printf("%s %s %s\n",*(sp+0),*(sp+1),*(sp+2));

となることで、

 printf("%s %s %s\n",sp+0,sp+1,sp+2);

とはならないことです。配列自体がポインタの形式で表現できるので、いわゆるポインタのポインタのとして表現しなければなりません。つまり文字列は **sp のアドレスの
位置にあるわけです。

 ポインタを返す関数は、

 int *func();

とするのですが、これは func のアドレスの位置にポインタを返す関数ができるのではなく、 func はポインタを返す関数であるという宣言になります。C言語は特に型を宣言しなければ整数値を返す関数と解釈されますので、この場合はコンパイラに func は整数の変数へのポインタを返す関数なのだよと教えるにとどまります。

 #include <stdio.h>

 int *func()
 {
  static int i=0;

  i++;

  return &i;
 }

 main()
 {
  int i;

  for(i=0;i<10;i++){
   printf("%d\n",*func());
  }
 }

この例では整数の変数へのポインタを返す関数 func を作っているのですが、C言語の場合、int *func(); と int *func(){} ではやや解釈のされかたが違っていて、前者のは単に func の型の宣言のみを行なっているのに対し、後者は宣言と定義の両方を兼ねています。もし上の例で、関数 func と main の順序を逆にして記述すると、 main の中で呼び出されている func は整数値を返す関数と解釈されますので、*func() とすると func はポインタを返す関数ではないと叱られます。こういうときには次のようにあらかじめ宣言をしておかなければなりません。

 #include <stdio.h>

 int *func();  /* ここで宣言しておく */

 main()
 {
  int i;

  for(i=0;i<10;i++){
   printf("%d\n",*func());
  }
 }

 int *func()
 {
  static int i=0;

  i++;

  return &i;
 }

 この main 関数を先に書くか、後に書くかという記述の順序が時々論争の種になっているようです。ちなみに私は main は先に書きます。

 さて、

 int (*func)();

と書いたらこれはどういう意味になるでしょうか。これは int *func(); とは全く別な解釈になります。これは func が指し示しているものが int型を返す関数ということになります。この場合は func のアドレスの位置に関数のアドレスを保存するためのメモリーが確保されます。

 #include <stdio.h>

 int abc()
 {
  static int i;

  i++;

  return i;
 }

 main()
 {
  int (*func)();
  int i;

  func=abc;           /* func に関数 abc のアドレスを代入 */

  for(i=0;i<10;i++){
   printf("%d\n",(*func)());  /* (*func)() とすると abc() が呼び出された */
                 /* ことと同じになる */
  }
 }

 (*func)() を func() と記述してもいいのですが、プログラムを読むときに理解しやすい方で記述すべきでしょう。これも個人の好みの差がありますので、どちらがいいかは分りません。

 C言語はこのポインタが使いやすいので様々な使い方をしますが、乱用するとプログラムが理解しずらくなってしまうことがあります。他人の書いたプログラムはいつでも理解しずらいものですが、最悪の場合には書いた本人までもが理解できないプログラムになってしまうことがありますので注意して下さい。


そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?