新・闘わないプログラマ No.327

配列の先頭アドレス


また春がやってきて、新入社員が入ってきたのですが、それに伴い研修などをやる必要があるわけです。今年は仕事の都合で私は講師をやらないのですが、テキストの選定にだけは関わっています。
で、プログラミング研修としてCのテキストを選ばないといけないのですが、さてどうしようか、と。私が講師をやるのならK&Rを使うのですが…。というわけで、Cのテキストになりそうな本をいくつか眺めてみているところです。

そんな中で、
「“int a[10];”というふうに配列を宣言した場合、配列名“a”はその配列の先頭アドレスになります」
というような表現をよく目にします。
この表現、なんか気になるんですよね、私は。いや、これは「言葉のアヤじゃん」とか「初心者に厳密な説明したところで余計混乱するだけだ」とか、そういう意見も出てくることとは思いますが、せめて「配列名“a”を式の中に書くと、普通は配列の先頭要素を指すポインタになります」くらいは書けないかな、と思うわけです。いやまあ、この表現でも「式の中に書くと」とはいったいなんぞや、とか、「普通は」って「普通じゃない」場合とはなんだよ、とか、突込みどころはあるわけですが。
で、「配列名“a”はその配列の先頭アドレスになります」という表現の問題点は、大きく2つあります。

まず、「先頭アドレスになります」と断言している点。これにはよく知られているように例外があります。

    int hoge[10];
    int l;
    
    l = sizeof hoge;

とすると、もし仮に配列名hogeがアドレス(ポインタ)になるのなら、hogeの型はint *になるはずですから、変数lに代入される値は2とか4とか、そういう値になるはずです…が実際には配列hogeのサイズ(20とか40とか)になります。「sizeof演算子のオペランドが配列名の場合には、配列名はポインタにはならない」という例外です。
あと2つばかり例外があるのですが、面倒なので、ここでは省略。
まあ、この問題点は、初心者向けの説明の場合にはとりあえず目を瞑ってもいいところかな、と思わないでも無いです。私の表現でも「普通は」とか言って誤魔化してますし。

もう一点は「配列の先頭アドレス」というあたりです。「アドレス」という表現をどれだけ使う(使わない)方がいいのか、という話は置いておいたとして、配列名が式の中に現れたとき、大概の場合(上記のような例外を除く)は、「配列の先頭要素を指すポインタ」の意味になります。「配列の先頭を指すポインタ」ではありません。たとえば、

    int a[10];

という配列があったときに、配列(の先頭)を指すポインタは“&a”と表現します。他のスカラー型と同じ書き方ですね。このとき“&a”の型は“int (*)[10]”になります。
一方、配列の先頭要素(の先頭)を指すポインタは“&a[0]”ですが、これは上にある規則により“a”とも書けるわけです。この2つの表記は(上で述べた例外を除けば)同じ意味になります。ちなみにこれの型は“int *”です。
「そんなこと言ったって、『配列の先頭を指すポインタ』も『配列の先頭要素を指すポインタ』も、どちらも同じところを指しているじゃないか」と思われた方もいらっしゃると思いますが、確かにそうです。一般的には同じ場所を指します。ただし型が違います。“&a”の型は“int (*)[10]”ですし、“a”や“&a[0]”の型は“int *”です。Cでは、型が違うポインタは全く別物ですし、相互に代入等の演算も原則としてできません。
ただし、1次元配列を扱っているだけなら、「配列名“a”はその配列の先頭アドレスになります」という理解(誤解?)でも、まああまり問題になることはない(配列要素を指すポインタではなくて、配列そのものを指すポインタは必要にならないから)のですが、多次元の配列を関数に渡したりするような場合には問題になってきたりします。

多次元配列を関数に渡すプログラムとして、以前こんなのを見たことがあります。

    int hoge[5][10];                          /* 関数へ渡すべき2次元配列 */
    int temp0[10];                            /* 1次元のテンポラリ配列(0) */
    int temp1[10];                            /* 1次元のテンポラリ配列(1) */
    int temp2[10];                            /* 1次元のテンポラリ配列(2) */
    int temp3[10];                            /* 1次元のテンポラリ配列(3) */
    int temp4[10];                            /* 1次元のテンポラリ配列(4) */
    int i;
      ・
      ・
      ・
    for (i = 0; i < 10; i++) {                /* 1次元配列へコピー */
        temp0[i] = hoge[0][i];
        temp1[i] = hoge[1][i];
        temp2[i] = hoge[2][i];
        temp3[i] = hoge[3][i];
        temp4[i] = hoge[4][i];
    }
    func(temp0, temp1, temp2, temp3, temp4);  /* 関数呼び出し */
    for (i = 0; i < 10; i++) {                /* 2次元配列へ戻す */
        hoge[0][i] = temp0[i];
        hoge[1][i] = temp1[i];
        hoge[2][i] = temp2[i];
        hoge[3][i] = temp3[i];
        hoge[4][i] = temp4[i];
    }
      ・
      ・
      ・

最初見たときに「なんじゃこりゃ? こんなん2次元配列をそのまま関数に渡せばいいような…」と思い、作った本人に聞いてみました。そしたら…
「いやあ、2次元配列って関数へ渡せないみたいなんですよね〜。なもんで一旦1次元配列に入れておいて、それを渡す形にしたんですよ」
んなことあるかい? どうやって渡したんだよ。
「いやあ、関数定義の方で、

    void func(int **x)

とか、

    void func(int x[][])

とか、いろいろやってみたんですけど、どうやってもダメなんですよね」
そりゃダメだろ、そんな記述じゃ。

“int hoge[5][10]”という宣言は「hogeは、整数型の要素10個の配列の、要素5個の配列」という意味で、その配列名hogeが式の中に現れると(例のいくつかの例外を除いて)、hoge[0](という要素10個の配列)を指すポインタの意味になります。hoge[0]という表記は配列要素になっていますが、この配列要素自体がまた配列になっているわけです。そして、このhogeの型は“int (*)[10]”になります。ポインタを表す“*”の前後に括弧が付いていますけど、これの説明すると長くなるので、ここでは略 ←おい。
ですから、2次元配列を関数に渡す意図で、

    func(hoge);

としたいならば、関数の仮引数では、hogeの型である“int (*)[10]”を使って、

    void func(int (*x)[10])

と書く必要があります。ここで、xは「int型の要素10個の配列を指すポインタ」、つまり配列全体(の先頭)を指すポインタです。多次元配列を扱うと、こういうふうに配列を指すポインタというのが出てきます。なお、関数の仮引数の宣言においていは(←ここ重要)ポインタを表す“*”の替わりに“[ ]”という記号を使うこともできます。そうすると、

    void func(int x[][10])

と書くことも出来ちゃったりしますが、あくまでも、この引数の型は“int (*)[10]”でして、この型と違う引数の宣言“int **x”とか“int x[][]”とかを書くことは許されないわけです。

と、ここまで書いてから読み返してみて、なんかわかり辛いですね。図を描かなかったので「何を言っているかさっぱり分からん」とか言われそうな気もします。
さらに、前橋さんの本を引っ張り出してみたら(前橋和弥「C言語ポインタ完全制覇」技術評論社)、ほとんど似たようなこと書いてますね。
まあ、せっかく書いたのでもったいないから載せちゃいます。

[前へ] [次へ]

[Home] [戻る]


mailto:lepton@amy.hi-ho.ne.jp