初級C言語Q&A(17)

初出: C MAGAZINE 1996年10月号
Updated: 2002-01-09

[1つ前] [質問一覧] [記事一覧] [ホームページ]


 今回は、多重配列についてよくある質問からいくつかピックアップしてみました。

多重配列


Q 【二次元の配列を引数に与える】

 二次元の配列を定義して関数を呼び出したい。次のコードをコンパイルしたが、 うまくいかない。何がおかしいのか。
void foo(int **array)
{
    ...
}

void bar(void)
{
    int array[10][10];

    foo(array);
    ...
}

 配列はポインタとは根本的に違います。関数fooを次のように修 正すればOKです。
void foo(int array[10][10])
{
   ...
}
 実際は、このような呼び出しにおいては、最初の要素の個数を省略することが 許されています。従って、次のようにしてもOKです。
void foo(int array[][10])
{
   ...
}
 「ポインタと配列は同じ」とよく言われますが、この表現は大雑把すぎるので 数々の誤解を生んでいるようです。簡単に理解するには、 a[i]*(a + i)と同じ意味である  と覚えておけばよいと思います。a[i]と書ける所であれば、 それを*(a + i)と書くことができるし、逆に*(a + i) と書ける所であれば、a[i]と書いてもよいのです。この意味で、 ポインタと配列はかなり親密な関係にあるわけです。  しかし、ポインタ変数と配列変数の実態は異なっています。最も大きな差は、 ポインタ変数はアドレスを格納できるサイズを持つ変数であり、配列変数は配列 の要素として値を入れることのできる変数だが配列そのものを表現する値は定数 であるということです。言いかえれば、int *p;と定義した場合、 pには代入することができますが、int a[10];と定義 した場合、a[0]a[1]には代入することができますが、 aには代入することはできません。これは、paの内部表現が全く異なっているからです。  また、おそらくここが混乱の種なのですが、関数の引数に配列を与えた場合に は、実際に渡される値はそのアドレスである、という規則になっています。従っ て、関数の引数として与える最初の要素に関してはポインタと解釈しても構わな いことになり、次のように書くことができます。
void foo(int (*array)[10])
{
    ...
}
(参考: c.l.c FAQ 6.18)

Q 【二次元の配列を引数に与える】

 二次元配列を関数の引数として与える場合に、受け取る側で、最初の要素のサ イズを省略できるのはなぜか。

 まず、一次元の配列で考えてみましょう。List 1のような引数で呼び出される 関数があるとします。
---- List 1 ----

void foo1(int array[4])
{
    ...
}

void bar1(void)
{
    int a[4];

    foo1(a);
    ...
}
 この関数foo1は、intの配列で要素が4個あるもの が渡されることを期待していることになりますが、実際は、関数の引数として配 列が渡される場合には、配列の先頭のアドレスが重要なのであって、配列のサイ ズは中の処理としては使われません。bar1では、配列aはFig. 1の ような実体を持つように定義されます。
---- Fig. 1 ----

    +-------+ <-- ここのアドレスが a
    | a[0]  |
    +-------+
    | a[1]  |
    +-------+
    | a[2]  |
    +-------+
    | a[3]  |
    +-------+
bar1は、関数foo1を、foo1(a);のよ うにして呼び出しています。この時与えられるaというのは配列の 先頭の要素を指すアドレスであって、この時点では配列要素の個数のような情報 は全く渡らないことになります。 関数foo1は、この情報を受け取って処理しようとしますが、一次 元の配列の場合は、先頭のアドレスと、各要素のサイズが分かれば、それぞれの 要素を参照することができます。この場合は要素がintであり、先 頭の要素のアドレスがarrayであることが分かっていれば、 n番目の要素はarrayからintをn個格納できるスペー スを飛ばした先にあることが分かるのです。この場合、元の配列で定義されてい た要素の個数は必要ありません。従って、関数foo1は、List 1' のようにすることもできます。
---- List 1' ----

void foo1(int array[])
{
    ...
}
 ただし、いずれの場合も、定義された個数を超えて読み書きしようとすると、 何が起こるか分からないので注意が必要です。許された範囲内で処理することは、 プログラム側で保証してやらなければなりません。  次に、二次元の配列を考えてみます。で考えてみましょう。List 2のような引 数で呼び出される関数を想定します。
---- List 2 ----

void foo2(int array[3][4])
{
    ...
}

void bar2(void)
{
    int a[3][4];

    foo2(a);
    ...
}
 二次元配列の場合は、実体はFig.2のように取られることになっています。
---- Fig.2 ----

    +==========+ <-- ここのアドレスが a
    | a[0][0]  |
    +----------+
    | a[0][1]  |
    +----------+
    | a[0][2]  |
    +----------+
    | a[0][3]  |
    +==========+
    | a[1][0]  |
    +----------+
    | a[1][1]  |
    +----------+
    | a[1][2]  |
    +----------+
    | a[1][3]  |
    +==========+
    | a[2][0]  |
    +----------+
    | a[2][1]  |
    +----------+
    | a[2][2]  |
    +----------+
    | a[2][3]  |
    +==========+
a[2][1]の値を参照する場合、まず、a[2]という 情報から、データが先頭から3番目(添字が0から始まるのでa[0]が 1番目、a[1]が2番目と考える)のブロックにあることがわかります。 1つのブロックは、int a[3][4];と定義された配列ですから、4つの int型の変数が格納できるサイズであることが分かります。  結局、a[2][0]の値がある場所は、a[0][0]の位置 に比べて「4つのint型の変数が格納できるサイズ」を2つ飛ばした 先にあることが分かります。  a[2][0]の値がある場所が分かれば、a[2][1]の場 所は、そこからint型の変数が格納できるサイズ1つを飛ばした先 にあることが分かります。こうやって、プログラムの内部で二重配列を参照する 仕組みになっているのです。  さて、関数foo2を呼び出す時には、先ほどの例と同じで、やはり foo2(a)のように、配列の先頭アドレスしか渡されていません。配 列の要素や、それが二重配列であるというような情報は、この時点では一切知る よしもない状態です。このような情報は、foo2が受け取った時に、 foo2側で考えることになります。すなわち、関数foo2 は、引数として書かれている箇所のint array[3][4]という情報を 見て、それを判断します。 このように定義されたfoo2は、与えられた引数が3×4の intの配列だと想定して解釈しますが、呼び出した側で本当にその ような形式のデータを渡したかどうかは関知しません。変な型を与えて呼び出し ても、あくまで3×4のintの配列とみなして処理します。その結果 どうなるかは分かりませんので、プログラマーはそのようなことのないように、 関数を呼び出す側と呼び出される側で、引数の型が一致しているようにプログラ ムを書く必要があります。幸い、ANSI Cの場合は、プロトタイプ宣言を行うこと により、引数の型の不一致を事前にチェックすることができますから、実行して 破綻するまで失敗に気付かないということは少なくなりました。ただし、先に述 べたように、int a[3]のような配列を引数に与えた場合に、処理す る関数がa[3]以降(a[3]も含む)を使うことのないよ うに注意する必要はあります。  ところで、関数foo2の処理をもう一度考えてみると、 int array[3][4]という情報のうち、[4]という情報 は、二重配列における一連のブロックの大きさを計算する時に使っていますが、 [3]という情報はどこにも使われていません。これは一次元の配列 の場合と同様です。そこで、実は関数foo2を書く時には、List 2' のように、先頭要素のサイズを省略することもできます。この情報がなくても、 関数foo2の内部処理には何の差し支えもないからです。
---- List 2' ----

void foo2(int array[][4])
{
    ...
}
 これは、一般に「多重配列を関数の引数に与える場合には、呼び出される関数 の仮引数に書く配列の先頭要素サイズを省略することができる」と拡張すること ができます。三重配列の場合は、List 3のようになります。
---- List 3 ----

void foo3(int array[][3][4])
{
    ...
}

void bar3(void)
{
    int a[2][3][4];

    foo3(a);
    ...
}

Q 【二次元配列の領域をmallocで獲得する】

 二次元の配列を静的変数ではなくプログラム実行中でmallocで確 保するにはどのようにすればよいか。

 配列のサイズが分かっている場合には、配列へのポインターを使うことにより、 mallocで動的に確保したメモリを割り当てることができます。  例えば、M×N個のintの要素を持つ配列を扱うことを考えます。 すなわち、int a[M][N]のような配列を考えます。この配列を扱う コードの例は、Listのようになります。
---- List ----

#include <stdio.h>
#include <stdlib.h>

#define M 5
#define N 10

void foo(int a[M][N]) /* int a[][N] としてもよい */
{
    int x, y;

    for (x = 0; x < M; x++) {
        for (y = 0; y < N; y++) {
            printf("a[%d][%d] = %d\n", x, y, a[x][y]);
        }
    }
}

int main(void)
{
    int (*a)[N];
    int i, j;

    a = malloc(sizeof(*a) * M);
    if (a == NULL) {
        return -1;
    }

    for (i = 0; i < M; i++) {
        for (j = 0; j < N; j++) {
            a[i][j] = i * 3 + j * 2;
        }
    }

    foo(a);
    return 0;
}
(参考: c.l.c FAQ 6.16)

Q 【サイズが動的に変化する二次元配列】

 二次元の配列、a[M][N]の領域をmallocを使って動 的に獲得したい。ただし、M、Nは実行時まで分からないとする。どのようにすれ ば実現できるか?

 先に述べたような方法では、配列の要素数が動的に変化する多重配列を mallocを使って領域確保することはできません。従って、C言語の 配列をそのまま使うのではなく、工夫が必要になります。  一番単純な方法としては、M×N個の要素が格納できる一次元配列を割り当てて おき、要素を参照する時にはその都度計算するというものがあります。
---- List ----

    int *p;
    int i, j, m, n;

    ...

    p = malloc(sizeof(int) * M * N);

    /* a[m][n]への代入 */
    *(p + (m * N + n)) = i;

    /* a[m][n]の値の参照 */
    j = *(p + (m * N + n));

 この他に、各ブロック毎にmallocしてポインタを割り当てる方 法や、無駄を承知であらかじめ十分大きな配列を確保しておく方法などがあります。

ネットで見かけた質問


Q 【文字列定数の変更】

 文字列定数は変更してはいけないと言われているが、私の使っているコンパイ ラでは文字列定数を変更する次のようなコードはちゃんとコンパイルできるし、 期待通りに動作する。
   char *str = "sample string";

   *str = 'S';

 ANSI規格では、文字列定数(文字列リテラル)を変更した場合の結果は未定義で す。すなわち、どのような結果になっても規格に合致することになります。たま たま文字列定数を変更しても動作するようなコンパイラがあっても、不思議では ありません。問題は、そのプログラムをANSI規格に合致したコンパイラを用いて コンパイルした場合にも動作するという保証が全くないということです。  本当に内容を変更する必要があるコードを書くのなら、文字列定数ではなく配 列を使うように書き直すことは極めて簡単ですから、そのように修正して他のコ ンパイラでも動作が保証されたコードにする方が得策です。

Q 【if (p)】

 ポインタがNULLであるかどうかのテストを、 if (p != NULL)ではなくif (p)の ように書いても構わないのか。

pがポインタであるかどうかにかかわらず、if (p) と書いた場合には、これがif (p != 0)と等しい結果となるように 動作します。これが大前提となります。  すると、pがポインタである場合には、p != 0とい う比較の式は、型を一致させるために、右辺に現れる式「0」をNULL とみなすことになります。なぜなら、0をポインタ型にキャストした (void *)0は、NULLと一致するからです。  従って、結局if (p)と書いた結果とif (p != NULL) と書いた結果は常に一致するはずです。これはNULLの内部表現が0 という値であるかどうかには無関係です。  同様に、if (!p)と書けば、if (p == NULL)と同 じ結果が得られます。  これらの書き方のどちらが望ましいかということについては、賛否両論で、決 定的な意見はないようです。私見としては、どちらでも構わないと思います。

Q 【エンディアン】

 リトルエンディアン、ビッグエンディアンというのは何か。

 第1回にも説明があります。整数をメモリに格納する場合に、Figのような二種 類の方法が考えられます。それぞれをビッグエンディアン、リトルエンディアン と呼んでいます。使っているマシンがどちらであるかを判別するには、例えばList のようなプログラムを実行させると分かります。  エンディアン(endian)という言葉は、卵を食べる時に、卵のとがった方を割る のが正しいか、あるいは丸い方を割るのが正しいか、というガリバー旅行記の中 に出てくる争いの逸話が由来になっているそうです。
---- Fig ----

     short int i = 0x1234;

 アドレス               $+0    $+1
                       +------+------+
  ビッグエンディアン   | 0x12 | 0x34 |
                       +------+------+
                       +------+------+
  リトルエンディアン   | 0x34 | 0x12 |
                       +------+------+

---- List ----

#include <stdio.h>

int main(void)
{
    int i = 1;

    printf("%s-endian\n", *((char *) &i) == 1 ? "little" : "big");
    return 0;
}
(参考: c.l.c FAQ 20.9)

Q 【関数の配列】

 次のような関数を、関数への配列を使って実現するにはどうすればよいか。
int foo(int i)
{
    if(0 == i)
        return func0();
    if(1 == i)
        return func1();
    if(2 == i)
        return func2();
    if(3 == i)
        return func3();
    return -1;
}

 まず、関数へのポインタ型をtypedefで定義します。例えば int型の関数であれば、typedef int (*int_fp)(); のように決めておきます。こうすれば、プログラムを分かりやすく書くことがで きます。  あとは、この型の配列を用意して、呼び出す時に添字を使ってやればよいだけ です。Listでは、function_array[i]が関数型int_fp を持ちますので、関数呼び出しを意味するように、()を付けてやればOKです。
---- List ----

typedef int (*int_fp)(); /* int型の関数へのポインタ型 */

int func0(void);
int func1(void);
int func2(void);
int func3(void);

int_fp function_array[4] = {func0, func1, func2, func3};

int foo(int i)
{
    if (i >= 0 && i <= 3) {
        return function_array[i]();
    }
    return -1;
}

Q 【コンマ演算子】

 次のコードを実行すると、jの値は上から順に0、1となった。
    i = 0;
    j = i++; /* jは0 */
    i = 0;
    j = ++i; /* jは1 */
 では、次のコードを実行すると、jの値はどうなるのか。
    i = 0;
    i++, j = i;

j = i++;という代入を詳しく考えてみると、j = 式; という形式 になっていることが分かります。ここで「式」に該当するのは、「i++」です。  C言語の特徴として「式が値を持つ」という性質があることを思い出してくださ い。インクリメント演算子「++」は、前置しても後置しても、それを作用させる 変数を1増加するというふるまいに関しては同じ結果になります。異なるのは、式 の値です。i++と書いた場合には、式の値はiに1を加える前の最初 の値に等しくなりますが、++iとすると、式の値はiに1を加えた後 の値となります。  これに対して、コンマ演算子を使った、i++, j=1; というコード を考えてみます。これは「式, 式;」という形式になっています。コンマ演算子 の場合、コンマの左辺の式の値は無視されることになり、右辺には影響しません。 従って、右辺に影響するのは++演算子によって変更された iの値そのものということになります。これがjに代 入されるのですから、iの値、すなわち++演算子によ って1を加えた後の「1」という値がjに入ることになります。  コンマ演算子は、コンマの所で評価が完了することになっています。副作用は、 この時点で完了します。その後にコンマの右が評価されるという順序が保証され ています。従って、次のように書いたコードも結果は同じことになります。
    i = 0;
    ++i, j = i;

Q 【可搬性】

 可搬性(ポータビリティ)とは何か?

 プログラムを他の処理系で動作させる場合に、どの程度楽にできるか、という 目安になるものです。コンパイルしたプログラムをそのまま実行できたり、ある いはコードを変更しなくてもコンパイルするだけで期待通りの動作をするものは、 可搬性が高いといいます。コードを大きく変更しなければ動作しないものは、可 搬性が低いといいます。  あるプログラムを現在動作している処理系から別の処理系に動作するように手 を加えることを「移植する」といいます。移植する時の手間を表す目安が可搬性 ということになります。  可搬性を高くするのは簡単なこともあり難しいこともあります。ANSI Cに決め られた関数だけを使い、動作が確実なコードだけを含むようなプログラムは、他 の処理系においてもANSI C準拠のコンパイラで再コンパイルすれば動作する確率 が高いと思われます。ANSIに定義されていない関数を使っている場合は、移植の 際に、別の関数を使うように処理を変更したり、場合によっては動作が保証でき ないため妥協を強いられることもあるかもしれません。しかし、このようなケー スは、処理系に依存した関数を置き換える程度で何とかなる場合もあります。  ANSI Cの仕様で処理系依存になっていたり、あるいは未定義とされているコー ドは可搬性が低いことになります。他の処理系で動く保証が全くないからです。 特に、未定義のコードは現在動いているものが明日も動くという保証すらないの ですから、プログラムとして書くべきではありません。
※ c.l.c FAQ : comp.lang.c FAQ list

http://www.eskimo.com/~scs/C-faq.top.html
文中の項目番号は新しい版に対応しています。旧版とは異なります。

(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です
内容は予告なく変更することがあります。