Entry

プログラミングメモ - 関数に配列を渡す方法は意外と躓きの石みたい

2008年09月24日

なんか,微妙に今日話題になったこちらの話。そういうもんかと思えば大したことないし,大部分の人は問題なく使ってるもんですけど,Cをはじめたときに「自分もこれでつまづいた」って人が割といたのでメモ。

char a[10];と宣言されている時に、関数への引数として、&a[0]とする場合とa[10]とする場合でどこが違ってくるのでしょうか?

&a[0]とa[10]の違いについて - 教えて!goo

質問がちとあやふやなんですけど,あたしは,「関数の仮引数宣言で a[10] と書くのに,&a[0] と呼ぶのはどういうことなんだい?」ってな話として読みました。つか,そゆ話でつまづいた,という話を聞いたのでした。

C言語の配列って,渡し方にしても受け方にしても結構いろいろあるもんで,たしかに躓きの石になるのかも。ざっと挙げてみただけでも,呼び方も受け方もこれだけある。

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

/* argument is a pointer to char array, size of 10 */
void
func_1(const char ch[10])
{
    int i;

    srand((unsigned int)time(NULL));
    i = rand() % 10;
    putchar(ch[i]);

    return;
}

/* argument is a pointer to char array, but the size is not declared. */
void
func_2(const char ch[])
{
    func_1(ch);
    return;
}

/* argument is a char pointer. */
void
func_3(const char *ch)
{
    func_1(ch);
    return;
}

/* this func below is NOT valid. */
#if 0
void
func_ng(const char ch[0])
{
    func_1(ch);
    return;
}
#endif

int
main(int argc, char *argv[])
{
    char ch[10];
    char *str = "0123456789";

    strncpy(ch, str, sizeof(ch));

    /* call the func. */
    func_1(ch);
    func_2(ch);
    func_3(ch);

    /* you can also call the func, in this way. */
    func_1(&ch[0]);
    func_2(&ch[0]);
    func_3(&ch[0]);

    return 0;
}

仮引数宣言は,あくまでも宣言だから,宣言文で書けるような文法で書ける,ってな感じで覚えれば身に付くんでねいかな……。その一方で,関数の呼び出しの方は,C言語の場合,アドレスを渡すわけだから,アドレスを表現する形で書けば渡せますよ,みたいな感じで理解すればいいんでねいだろうか。言語仕様はともかく,覚え方として。

ここで,関数の仮引数に要素数を指定する方法,つまり a[10] とするのが微妙に分かりづらいかもしれません。でも,これは,多次元配列を渡す時に,必要になるんでした。

#include <stdio.h>

/* this declaration is valid. */
void
func_1(const char ch[2][3])
{
    putchar(ch[0][0]);
    putchar(ch[0][1]);
    putchar(ch[0][2]);

    putchar(ch[1][0]);
    putchar(ch[1][1]);
    putchar(ch[1][2]);
    return;
}

/* this declaration is ALSO valid. */
void
func_2(const char ch[][3])
{
    putchar(ch[0][0]);
    putchar(ch[0][1]);
    putchar(ch[0][2]);

    putchar(ch[1][0]);
    putchar(ch[1][1]);
    putchar(ch[1][2]);
    return;
}

/* this func below is NOT valid. */
#if 0
void
func_ng(const char ch[][])
{
    putchar(ch[0][0]);
    putchar(ch[0][1]);
    putchar(ch[0][2]);

    putchar(ch[1][0]);
    putchar(ch[1][1]);
    putchar(ch[1][2]);
    return;
}
#endif

int
main(int argc, char *argv[])
{
    char ch[2][3] = {
        {'1', '2', '3'},
        {'4', '5', '6'}
    };

    func_1(ch);
    func_2(ch);

    return 0;
}

ある意味当たり前なんですけれど,2次元配列における char 型のポインタと,char 型のポインタへのポインタはまったく別物だったりします。下のように演算すれば分かるけれども,具体的には *ch と **ch はインクリメントするときに進む大きさが違う。*ch をインクリメントしたときは,1バイト分アドレスが進むけれども,**ch はインクリメントすると4バイト進みます(32bit の処理系の場合)。おんなじ [] なんですけど,大きさが違うもんで,これが躓きの石になるのかなぁ,とも。

#include <stdio.h>

int
main(int argc, char *argv[])
{
    char *p = "abcde";
    char **ptr = &p;

    printf("%p\n", p);
    p++;
    printf("%p\n", p);

    printf("%p\n", ptr);
    ptr++;
    printf("%p\n", ptr);

    return 0;
}

(あまりやっちゃいけないプログラムなんだけど)上の結果。

aian@IBM-39B80170C23 ~
$ ./a.exe
0x402000
0x402001
0x22cce4
0x22cce8

上記例の func_ng(const char ch[][]) のような仮引数宣言が,なんで(1次元配列と同じように)宣言できないのかというと,char 型のポインタとして扱う要素数を確定できないと,どこまで char 型のポインタとして扱っていいか分からないからです(多分)。Cの場合,関数が受け取るのはアドレスだけなもんで,どれだけ進めるか(どの型のポインタなのか)についての情報は,渡された配列から分かるもんじゃありません。んなもんで,少なくとも,char 型のポインタがいくつ要素として含まれているかについては,教えてあげないといけないというわけです。「というわけです」というか,あたしゃそう理解しています。

もう,使い慣れてる人にとっては,イマサラ感が強い話で,若干恥ずかしいんですけど,意外とつまづいている人がいたのにはびっくりでした。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN