先頭へ戻る

ポインタ③(引数や戻り値への利用) 解答ページ | Programming Place Plus C言語編 第33章

Programming Place Plus トップページ -- C言語編 -- 第33章

先頭へ戻る

問題①

問題① 関数の仮引数において、const修飾子を以下のように使うことはよくあります。

void func(const MyStruct* s);

一方で、次のように使うことはあまりありません。

void func(MyStruct* const s);

このような、「*」の後ろ側に書くタイプの const修飾子が、関数の仮引数に使われることが少ない理由はなぜでしょう。


const を「*」の前の方に置いた場合、 「この関数の内部で、ポインタが指し示す先の値を書き換えることはない」という宣言をしていることになります。
これは、関数を呼び出す側の視点に立つと、「値が書き換えられることはない」ことが保証されるので、 関数を呼ぶ前の値を必要に応じて、コピーして取っておく等の対応を必要としていないことが分かります。

一方、const を「*」の後ろに置いた場合、「この仮引数の値は書き換えられない」という意味になります。 仮引数を書き換えるかどうかというのは、関数定義の内部での話です。

void func(MyStruct* const s)
{
    MyStruct localData;

    s = &localData;  // s は const なのでこれはできない
}

これはこれで意味のある指定ではありますが、関数の呼び出し側にとってはまったく無意味です。もし、このような const も付けるべきだというのなら、仮引数が単なる int型であっても、「const int」と書かなければならないことになります。そこまでしているプログラムはめったに見かけません。

問題②

問題② strcpy関数を自作してください。


本物の strcpy関数は、以下のように宣言されています。

char* strcpy(char* restrict s1, const char* restrict s2);

戻り値は、第1引数に指定したポインタがそのまま返されるという仕様です。

#include <stdio.h>
#include <assert.h>

char* my_strcpy(char* s1, char* s2);

int main(void)
{
    char str1[10];
    char str2[10] = "abcde";

    my_strcpy( str1, str2 );
    puts( str1 );

    my_strcpy( str1, "xyz" );
    puts( str1 );

    return 0;
}

/*
    自作 strcpy関数
    引数:
        s1: コピー先のメモリアドレス。コピーされる文字数以上の領域が必要。
        s2: コピー元のメモリアドレス。終端に '\0' が必要。
    戻り値:
        s1 と同じメモリアドレス。
*/
char* my_strcpy(char* s1, char* s2)
{
    assert( s1 != NULL );
    assert( s2 != NULL );

    int i;
    for( i = 0; s2[i] != '\0'; ++i ){
        s1[i] = s2[i];
    }
    s1[i] = '\0';

    return s1;
}

実行結果:

abcde
xyz

s2 の方を注目し、終端にある(はずの)'\0' が現れるまで、1つずつコピー作業を行います。 '\0' が現れた時点ですぐに終了してしまうと、コピー先の文字列に '\0' が付かずじまいになってしまうので注意が必要です。

また、assert も仕込んでおきました。 これは strcpy関数の仕事とは関係ありませんが、こういう注意を払うクセを付けておくと役に立ちます。


もっと短く、次のように作ることもできます。 ただしこれは技巧的すぎて、無意味に分かりづらいので、避けた方が無難です。

char* my_strcpy(char* s1, char* s2)
{
    char* p = s1;   // s1 は書き換えるので、戻り値用に保存しておく

    assert( s1 != NULL );
    assert( s2 != NULL );

    while( *s1++ = *s2++ ){
        // 空ループ
    }

    return p;
}

while文の条件式で使われている演算子は、「==」ではなく、「=」です。 条件式のところでしていることは、以下のことです。

  1. s2 が指し示す先にある値を、s1 の指し示す先へ代入。
  2. s1 と s2 をインクリメントして、ポインタを1つ先へ進める。
  3. 代入された値が偽 (つまり '\0')であれば、while文を抜け出す。真であれば続行。

この程度であればまだ読めなくはないですが、こういう技巧的なコードは可読性が損なわれます。非常に古い本でC言語を学んだ人は、こういうコードを書きがちです。昔はコンパイラが優秀でなかったため、できるだけ短く、できるだけ効率的なコードを人間が書くことを求められることがありました。現代ではコンパイラが生成するコードは非常に優秀なので、人間は分かりやすく書くことに注力するべきです。

問題③

問題③ strcmp関数を自作してください。


strcmp関数は、以下のように宣言されています。

int strcmp(const char* s1, const char* s2);

辞書順で、s1 が先に来る場合は 0未満の値、s2 が先に来る場合は 0 より大きい値、同じなら 0 が返されます。

#include <stdio.h>
#include <assert.h>

int my_strcmp(const char* s1, const char* s2);

int main(void)
{
    printf( "%d\n", my_strcmp( "abc", "abcde" ) );
    printf( "%d\n", my_strcmp( "abc", "ABC" ) );
    printf( "%d\n", my_strcmp( "", "" ) );

    return 0;
}

/*
    自作 strcmp関数
    引数:
        s1: 比較する文字列。
        s2: 比較する文字列。
    戻り値:
        辞書順で s1 の方が小さければ負数、s2 の方が小さければ 0より大きい値。
        同じであれば 0 が返される。
*/
int my_strcmp(const char* s1, const char* s2)
{
    assert( s1 != NULL );
    assert( s2 != NULL );

    while( *s1 == *s2 ){
        if( *s1 == '\0' ){
            return 0;
        }

        s1++;
        s2++;
    }

    return (*s1 < *s2) ? -1 : 1;
}

実行結果:

100
-32
0

どちらが大きいか(あるいは小さい、同じか)が決定するのは、先頭から順に調べていって、異なる文字が登場した瞬間です。 ですから、とりあえずそこまでは、s1 と s2 ともに1つずつどんどん進めていきます。

その過程の中で、一方に '\0' が現れた場合、終端まで行き着いてしまった訳です。 この場合、s1 も s2 も同じように進めてきたのですから、s1 == '\0' ですし、s2 == '\0' でもあります。 従って、s1 も s2 も同じ内容の文字列です。 このまま 0 を返して終了してしまえます。

一方、'\0' が登場することなく、s1 == s2 を満たさなくなった場合、両者は異なる文字列であることが分かります。 先ほど書いたように、このタイミングで、大きい・小さいが決定されます。 今現在、参照している文字の大小関係を調べれば良い訳です。

strcmp関数の戻り値が「-1、0、1」のいずれかを返すのではなく、「0より小さい値、0、0より大きい値」のいずれかを返すという仕様なので、アドレス計算を使って、「return (*s2 - *s1);」のようにする手もあります。この方が分岐処理が無くなって効率的な可能性もありますが、例によって分かりやすさを優先した方がいいとも言えます。

問題⑦

問題⑦ 文字列の中から特定の文字を探し出し、そのメモリアドレスを返す strchr関数という標準ライブラリ関数があります。この関数は、次のように宣言されています。

char* strchr(const char* s, int c);

文字列s の先頭から順番に文字を調べ、c と一致するものが登場したら、そのメモリアドレスを返します。もし、文字列s の末尾までの間に一致するものが登場しなかったら、ヌルポインタを返します。c は int型ですが、関数内部では char型として比較されます。また、文字列s の末尾にある '\0' を探し出すことも可能です。
strchr関数と同じことをする関数を自作してください。


たとえば、次のようになります。

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

char* my_strchr(const char* s, int c);

int main(void)
{
    char str[] = "abcde";

    char* result = my_strchr( str, 'b' );

    if( result == NULL ){
        puts( "見つかりませんでした。" );
    }
    else{
        puts( result );
    }

    return 0;
}

/*
    自作 strchr関数
    引数
        s:      探索対象の文字列。
        c:      探索する文字。
    戻り値
        見つかった場合は、その文字のメモリアドレス。
        見つからなかった場合は、ヌルポインタ。
*/
char* my_strchr(const char* s, int c)
{
    while( *s != (char)c ){
        if( *s == '\0' ){
            return NULL;
        }
        s++;
    }

    return (char*)s;  // const を外すためのキャストが必要
}

実行結果:

bcde

思ったよりも難しかったかもしれません。ポイントとなるのは、以下の3点でしょうか。

後ろの2つはキャストするだけと言えばそれまでですが、最初の1つは意外と手こずるかもしれません。

問題⑧

問題⑧ 2つの文字列を連結する strcat関数という標準ライブラリ関数があります。この関数は、次のように宣言されています。

char* strcat(char* restrict s1, const char* restrict s2);

s1 の末尾に s2 を連結させます。戻り値は s1 と同じ値をそのまま返します。この標準ライブラリ関数と同じことをする関数を自作してください。


たとえば、次のようになります。

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

char* my_strcat(char* s1, const char* s2);

int main(void)
{
    char str[10] = "abcde";

    puts( my_strcat( str, "xyz" ) );

    return 0;
}

/*
    自作 strcat関数
    引数
        s1: 連結元の文字列。
        s2: s1 の末尾へ連結する文字列。
    戻り値
        s1 と同じメモリアドレス。
*/
char* my_strcat(char* s1, const char* s2)
{
    char* ret = s1;

    // s1 の末尾まで移動
    while( *s1 != '\0' ){
        s1++;
    }

    // s2 を連結
    while( *s2 != '\0' ){
        *s1 = *s2;
        s1++;
        s2++;
    }

    // 末尾の '\0' を忘れずに
    *s1 = '\0';

    return ret;
}

実行結果:

abcdexyz

strcat関数もそうですが、この関数は大きな問題を持っています。それは、仮引数s1 に渡した文字列が、十分な要素数を持っている必要があるという点です。たとえば、次のように使ったらどうなるでしょうか。

char str[] = "abcde";
my_strcat( str, "xyz" );

配列str には '\0' を含めて 6文字分の領域しかないので、ここにさらに 3文字連結しようとして、バッファオーバーフローを起こしてしまいます。あるいは、次のように使ってしまえることにも問題があります。

char* str = "abcde";
my_strcat( str, "xyz" );

この場合、str は配列ではなく、文字列リテラルを指し示すポインタです。文字列リテラルの末尾に "xyz" を連結しようとしますが、末尾のヌル文字を上書きしてしまいますから、文字列リテラルを変更する行為であり、未定義の動作になります。

関数に配列を渡そうとすると、自動的にポインタに変換されるルールと持つC言語では、領域が十分に用意されているかどうかを調べる手段がありません。この辺りも、C言語の致命的な弱さと言えます。


参考リンク


------------------------------------------------------------------------

更新履歴

'2018/6/1 第38章から練習問題⑦⑧を移動してきて、練習問題④⑤とした。

'2018/3/9 全面的に文章を見直し、修正を行った。
第37章の練習問題①を移動してきた。 以前の問題①は削除(章の内容と若干ずれがあったので)。

'2015/7/23 練習問題②の2つ目のプログラムで、戻り値が誤っていたのを修正。

'2014/12/2 練習問題③のプログラム中に、余分なセミコロンが入っていたのを修正。

'2009/11/26 新規作成。



第33章のメインページへ

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー