30章 ポインタによる関数への受け渡し

http://www.geocities.jp/ky_webid/c/030.html

さて、ポインタの話が本格化してきましたね。

今度は関数への引数、及び戻り値にポインタを使用するというお話です。

仮引数の宣言を見てください。「int array[]」となっています。先程言ったように、配列を引数にすることはできません。では、この一見配列のように見える宣言は何でしょう。これは配列ではなく、ポインタの宣言です。つまり「int *array」と宣言しているのと同じです。ここが、配列とポインタを混同させる原因の1つだと思うのですが、 仮引数の宣言に限っては配列とポインタは同じように宣言できます。あくまでも配列とポインタは別物ですので間違えないで下さい。

ここ超重要。試験にでるよー。

あくまでポインタなので間違えないようしないといけない。

まとめると、「ローカル変数のアドレスを返してはならない」ということです。この間違いは非常に多いようです。気をつけて下さい・・・というのは簡単ですが、ではどうやって解決すればいいのでしょうか? 問題なのは関数を終了したときにメモリから消えてしまうという点にあります。よって、関数を終了しても消えない変数を使えばいいということです。すると方法としては、「グローバル変数のアドレスを返すようにする」か「static変数のアドレスを返すようにする」ということになります。第22章で書いたように、スコープはできるだけ狭くするべきですから「static変数のアドレスを返すようにする」方が望ましいでしょう。

これも注意が必要ですね。

ただし一つ気になったことがあります。

char *hoge() {
    char *str = "abcd";
    return str;
}

int main () {
    printf("%s",hoge());
    return 0;
}

この場合はうまく表示されました。

strが指している文字列は存在しているので有効ということなのでしょうか?

この辺はもう少し調べる必要がありそうですね。



さぁ練習問題に取り掛かりますかってうおおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお

今回はなかなかヘビーな練習問題ですね・・・。これはかなり気合入ります。

問題1

strlen関数を自作して下さい。

まずはstrlenです。\0までのカウントを返すだけなので比較的楽に実装できそうです。

int mystrlen(char *str);

int mystrlen(char *str) {
    char *start;
    
    for(start=str; *str; ++str);
    
    return str - start;
}

int main () {
    char str[] = "1234567890";
    
    printf("%d\n",mystrlen(""));
    printf("%d\n",mystrlen("abc"));
    printf("%d\n",mystrlen("abcdef"));
    printf("%d\n",mystrlen(str));
    return 0;
}

コレは比較的簡単でした。すでに存在している関数を自作するというのもなかなか面白いですね。

また動作が正しいかどうかも検証しやすいので良いです。

答え合わせ

まったく同じでした。

また、解答例の方ではfor文による解答もありました。単純に\0までループしてカウントするだけのものです。

問題2

strcpy関数を自作して下さい。

さて二問目はstrcpyです。これはなかなか手ごわそうです。

void mystrcpy(char *str1, char *str2) {
    while(*str2){
        *str1 = *str2;
        str1++;
        str2++;
    }
    *str1 = '\0';
}

int main () {
    char str1[] = "1234567890";
    char str2[] = "asdfasdf";
    
    mystrcpy(str1,str2);
    
    printf("%s\n",str1);
    return 0;
}

上書きしつつ、お互いのアドレスを進めていきます。

str2が\0まで来たらループが終了するので終了した時点で*str1に\0を代入してあげます。この代入がないとstr1の文字列がどこで終わりなのかわからなくなるので注意が必要です。

答え合わせ
char *mystrcpy(char *s, char *t){
    char* p = s;  /* 戻り値用に先頭アドレスを保存 */

    while( *s = *t )  /* tをsに代入。'\0'なら終了 */
    {
        s++;
        t++;
    }
    
    return p;  /* 先頭アドレスを返す */
}

なるほどー。whileの中で代入式を書いちゃえば一石二鳥なわけですか。

あと戻り値のことをすっかり忘れてました。先頭アドレスを返すんですね。しまった。

そして解答例にはもう一つさらに短い例が。

char *mystrcpy(char *s, char *t){
    char* p = s;  /* 戻り値用に先頭アドレスを保存 */
    
    while( *s++ = *t++ )  /* tをsに代入し、両方のポインタを+1。'\0'を代入したら終了 */
        ;
        
    return p;
}

こ、これはすごい。この発想は無かった。

後置インクリメントは最後に評価されるので、まず最初に値の代入が行われ、\0かどうかが評価され、最後にインクリメントが走るという感じでしょうか。

そりゃそうですね。\0の評価よりも先にインクリメントが走ってしまうと、次のアドレスの評価になってしまうのでコピーが一つ少なくなるという事態(つまり\0がコピーされない!)になってしまうので。

Cってかなり柔軟な言語ですよね。本当に感心しました。

問題3

strcat関数を自作して下さい。

お次はstrcatですね。これも結構難しそうです。

char *mystrcat(char *str1,char *str2) {
    char *p = str1;
    
    while(*str1++) {}
    str1--;
    
    while(*str1++ = *str2++) {}
    
    return p;
}

int main () {
    char str1[21] = "1234567890";
    char str2[11] = "asdfasdfas";
    
    mystrcat(str1,str2);
    printf("%s\n",str1);
    return 0;
}

さっそくさっきのやり方をマネしてみました(笑)

まずはstr1のアドレスを終端まで進めます。

\0までいった後も最後にインクリメントが走るので、アドレスを一つ戻します。

そしてstrcpyの時に見たあの一行whileでコピーするという感じです。

あと今回はちゃんと戻り値も返すようにしました。流石に同じ轍は二度踏みません。

答え合わせ

解答例ではmystrlenを利用してポインタの移動を行ってますね。

char *p = s + mystrlen(s);  /* sの末尾を指すポインタ */

作った物を再利用する。うまいやり方ですね。

問題4

strcmp関数を自作して下さい。

さて最後の問題です。

これは難しそうだ・・・。

まずstrcmpの仕様を再確認する必要がありますね。

strcmp

とりあえず実装してみましょう。間違ってたっていいじゃないか。

int mystrcmp(char *str1,char *str2) {
    while(*str1 == *str2) {
        if ( *str1 == '\0' ) {
            return 0;
        }
        str1++;
        str2++;
    }
    
    if ( *str1 > *str2 ) {
        return 1;
    }
    return -1;
}

int main () {
    printf("%d\n",mystrcmp("asdf","asdf"));
    printf("%d\n",mystrcmp("0129","0120"));
    printf("%d\n",mystrcmp("0120","0129"));
    return 0;
}

仕様通りに作ってみました。

whileで常に一致しているかを調べていき、もしstr1が\0だったならstr2も\0のはずなので完全一致ということで0を返します。

str1が大きければ正の値、str2が大きければ負の値を返す仕様ということでそれぞれ1と-1を返しています。この辺は正と負であれば何を返してもいいのだろうか?

答え合わせ
int mystrcmp(char *s, char *t){
    while( *s == *t )  /* 比較している両者の文字が同じ間ループ */
    {
        if( *s == '\0' )  /* 文字列の末尾に到達 */
        {
            return 0;  /* 両者は同じ文字列 */
        }
        s++;
        t++;
    }
    
    return (*s - *t);  /* sの方が大きければ正数、小さければ負数になる */
}

なななるほどおおお。

アドレスの引き算をすれば自動的にstr1が大きければ正だし、小さければ負が返るということなんですね。

うまいやり方だなぁ。もっとこういう応用が利くように精進しないといけないですね。