第63回 プログラミングについて

C言語でプログラムを書いていると、ときどきすごく複雑な宣言や定義を行なわなければならないときがあります。
まず誰もが最初に頭を悩ますのが、構造体のメンバーが自分自身と同じ型のものを指し示す場合です。決してこれは複雑なものではなく、ポインタを使える言語では当たり前なことです。
struct data_strct{
int x,y;
struct data_strct *next;
};
このような宣言はごく普通に使用されるものです。構造体 data_strct の next が構造体を指し示す構造で、私がごく初心者の頃は理解に苦しんだものです。理解してしまえばどうということでもないのですが、構造が入れ子になっているようで無限に大きい構造体なのかと思い込んでしまうのです。多分、次のような宣言をしたように思い込んだのだと思います。
struct data_strct{
int x,y;
struct data_strct next;
};
構造体のメンバー next にポインタを示す * があるか否かで意味が全然違ってしまいます。* を指定しないと、構造体が無限にループになってしまうことになるので、コンパイラはエラーを発生させます。
構造体の中に構造体がある場合も珍しいものではありません。
struct data_strct{
struct xy_strct{
int x,y;
}xy;
struct data_strct *next;
};
これは、構造体 data_strct の中で構造体 xy_strct を宣言しているのでなんだか難しいように感じられるのですが、コンパイラは、
struct xy_strct{
int x,y;
};
struct data_strct{
struct xy_strct xy;
struct data_strct *next;
};
と構造体 data_strct の外で構造体 xy_strct を宣言したことと全く同じに解釈されます。
struct data_strct{
int x,y;
};
struct data_strct *data[10]; ー(1)
この定義の場合の(1)はどのような解釈になるのでしょうか。この定義の解釈は、
struct data_strct *(data[5]);
と同一で、data のそれぞれの要素は構造体 data_strct 型のものを指し示し、配列の要素数は5個ということになり下の図のようになります。
data[0] → struct data_strct
data[1] → struct data_strct
data[2] → struct data_strct
data[3] → struct data_strct
data[4] → struct data_strct
ところが、(1)を、
struct data_strct (*data)[5];
とすると、解釈が全く違ってきます。この場合は data が5個の要素数を持つ構造体struct data_strct 型の配列を指し示すことになり、次の図のようになります。
data → struct data_strct [0]
struct data_strct [1]
struct data_strct [2]
struct data_strct [3]
struct data_strct [4]
構造体の配列や構造体のポインタの配列は頻度的にはそれほど高くはないけれども、プログラムを書くときには必要に迫られることがあります。例えば、次のようなリストをソートしたいときなどには上の例の前者の定義を使用します。
struct data_strct{
int x,y;
struct data_strct *next;
};
struct data_strct *data;
struct data_strct *(array[MAX_ARRAY]); -(2)
:
:
array に data のアドレスを代入し、間接的な参照を行なってソートを行います。この例では array の要素数は MAX_ARRAY 定数に依存して固定ですが、 data の数が不定のときには、(2)を
struct data_strct **array;
と定義し、malloc 関数などで動的にメモリーを確保してソートを行ないます。この場合を図にすると、
array → struct data_strct *[0] → struct data_strct
struct data_strct *[1] → struct data_strct
struct data_strct *[2] → struct data_strct
struct data_strct *[3] → struct data_strct
struct data_strct *[4] → struct data_strct
:
:
のような感じになります。
struct data_strct{
int x,y;
};
struct data_strct (*(data[3]))[5]; ー(3)
この例は、ちょっと意地悪ですが頭の体操になりますので考えてみましょう。
(3)は、data 要素数が3の配列で、それぞれの要素が struct data_strct 型の5つの要素をもつ配列を示しているという定義です。
data[0] ---------------------------------------------→struct data_strct [0]
data[1] -----------------------→struct data_strct [0] struct data_strct [1]
data[2] → struct data_strct [0] struct data_strct [1] struct data_strct [2]
struct data_strct [1] struct data_strct [2] struct data_strct [3]
struct data_strct [2] struct data_strct [3] struct data_strct [4]
struct data_strct [3] struct data_strct [4]
struct data_strct [4]
という感じになります。ですので、data が指し示す構造体のメンバーは、
(*data[0])[1].x
というように表現します。
関数の宣言や定義のときにも、複雑になってしまうことがあります。
int abc();
と宣言したときには関数 abc は int 型を返す関数です。この宣言は多分誰もが簡単に理解できるはずです。ところが、
int *abc();
と宣言するとちょっと頭を使わなければなりません。この宣言は int 型のポインタを返す関数であると abc を宣言しているのですが、一体こんな関数は何に使うのかと思われる方がいると思います。例えば、int 型の配列を参照したいときに、その配列は別の関数が管理(この場合は関数 abc)しているとすると、その配列のアドレスを何らかの方法で取得しなければなりません。このようなときに、ポインタを返す関数を使用することがあるのです。ちょっとした例ですが、次のものを参照すれば理解していただけると思います。
int *abc();
main()
{
int *array,i;
array=abc();
for(i=0;i<10;i++)array[i]=i;
for(i=0;i<10;i++)printf("%d\n",array[i]);
}
int *abc()
{
static int array[10];
return array;
}
この例では関数 abc から取得したポインタの値を int *array に代入して使用していますが、abc() が int 型のポインタですので、
for(i=0;i<10;i++)abc()[i]=i;
と記述することも関数の呼び出しが多くなり、オーバーヘッドが大きくなりますが可能です。
int (*abc())();
という宣言も理解し辛いと思いますが、この宣言も時々使います。これは、関数 abc がint 型の関数のポインタを返すという宣言です。まず例を見てみましょう。
int (*abc())();
int def();
main()
{
(*abc())(); ー(4)
}
int (*abc())()
{
return def; ー(5)
}
def()
{
printf("Function def!\n");
}
このプログラムを実行すると、Function def! と表示されます。(5)は def の値を返すのですが、この場合 def は関数名ですので関数 def のポインタを返すことになります。(4)は関数 abc の戻り値を関数として呼び出しているので、関数 def を呼び出すことになります。このような方法を使った例は26回目に紹介してありますので参考にして下さい。補足ですが、(4)は次のようにも記述できます。
abc()();
同じ意味ですが不思議な感じがします。どちらを使うかは好みの問題だとは思いますが、直感的に理解できる表現の方がいいでしょう。
ついでですので、頭の体操をしましょう。
struct data_strct *(*abc())();
は、関数 abc は struct data_strct 型のポインタを返す関数のポインタを返す関数という宣言。
int (*(array[3]))();
は、array は int 型を返す関数のポインタの配列。
int (*(array[3]))(int x,int y);
は関数のプロトタイプも同時に宣言したもので、array は int 型の引数を2つ持つ関数のポインタの配列。
この辺でやめて置きましょう。
プログラムが複雑になってくると、変数や関数の宣言や定義が複雑なものになってしまうことがあります。しかしながら、どうしても仕方のないときは別として、できる限りは簡単なもので構成するようにするべきです。これまでにもお話したように、プログラムを難しく書けることイコール腕の良いソフト屋さんではないのです(自分もまだまだですが)。まあそれはそれとして、複雑な宣言や定義を行なうときには後々のことを考えて、理解しやすい表現をするように心がけてください。
それではまた次回。