先頭へ戻る

ポインタ⑤(動的なメモリ割り当て) 解答ページ | Programming Place Plus C言語編 第35章

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

先頭へ戻る

問題①

問題① 次のプログラムの誤りを指摘してください(メモリ不足への対策を行っていない点は無視してください)

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

#define NAME_LEN          64        // 名前の最大長
#define PHONE_NUMBER_LEN  16        // 電話番号の最大長

// 個人情報
typedef struct {
    char    name[NAME_LEN];                 // 名前
    char    phone_number[PHONE_NUMBER_LEN]; // 電話番号
} PersonalInfo;

int main(void)
{
    // データ件数を受け取る
    puts( "データの件数を入力してください。" );
    char buf[40];
    int dataNum;
    fgets( buf, sizeof(buf), stdin );
    sscanf( buf, "%d", &dataNum );

    // データ件数が 0件以下なら終了
    if( dataNum <= 0 ){
        return 0;
    }

    // 件数に合わせて、領域を確保する
    PersonalInfo* infoArray = malloc( sizeof(PersonalInfo) * dataNum );
    if( infoArray == NULL ){
        exit( EXIT_FAILURE );
    }

    // データを受け取る
    for( int i = 0; i < dataNum; ++i ){
        puts( "名前を入力してください。" );
        fgets( buf, sizeof(buf), stdin );
        sscanf( buf, "%s", infoArray[i].name );

        puts( "電話番号を入力してください。" );
        fgets( buf, sizeof(buf), stdin );
        sscanf( buf, "%s", infoArray[i].phone_number );
    }

    // 特に手を加えないので、領域を解放する
    free( infoArray );

    // 結果を出力
    for( int i = 0; i < dataNum; ++i ){
        printf( "%s: %s\n", infoArray[i].name, infoArray[i].phone_number );
    }

    return 0;
}


free関数を呼び出すタイミングが問題です。

「特に手を加えないので、領域を解放する」などというコメントが書かれていますが、これは間違いです。 確かに変更を加えてはいませんが、直後に値を出力するためにアクセスしています。 free関数を呼び出した後、解放前のデータが残されている保証はありませんから、このようなプログラムには問題があります。

問題②

問題② 次のプログラムの誤りを指摘してください(メモリ不足への対策を行っていない点は無視してください)

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

void SetSequentialNumber(int* array, size_t size);
void PrintArray(int* array, size_t size);

int main(void)
{
    int* values;
    size_t size;


    size = 50;
    values = malloc( sizeof(int) * size );
    SetSequentialNumber( values, size );
    PrintArray( values, size );

    size = 100;
    values = calloc( size, sizeof(int) );
    SetSequentialNumber( values, size );
    PrintArray( values, size );

    free( values );

    return 0;
}

/*
    配列に連番をセットする。
    引数:
        array:  対象配列のメモリアドレス。
        size:   対象配列の要素数。
*/
void SetSequentialNumber(int* array, size_t size)
{
    for( size_t i = 0; i < size; ++i ){
        array[i] = i;
    }
}

/*
    配列の中身を標準出力へ出力する。
    引数:
        array:  対象配列のメモリアドレス。
        size:   対象配列の要素数。
*/
void PrintArray(int* array, size_t size)
{
    for( size_t i = 0; i < size; ++i ){
        printf( "%d\n", array[i] );
    }
}


malloc関数で領域を確保した後、calloc関数で新しい領域を確保していますが、返却されたポインタを同じポインタ変数で受け取っています。 それ自体は問題ありませんが、calloc関数の呼び出し前に free関数で解放を行っておかないと、先に確保された領域が解放できません。

この問題は正しいイメージを持つことが重要です。
calloc関数が確保したメモリ領域は、先に malloc関数で確保した領域を上書きするわけではありません。 malloc関数が確保した領域と、calloc関数が確保した領域はまったく別物です。 これは、それぞれの関数が返したポインタを、printf関数の “%p” 変換指定子で出力して確かめれば分かります。

領域が上書きされたわけではないのですから、free関数はそれぞれに対して呼び出す必要があります。 そのためには、free関数に渡すポインタを、ちゃんと残しておかないといけません。 呼び出し順としては、

  1. malloc
  2. free
  3. calloc
  4. free

か、

  1. malloc
  2. calloc
  3. free
  4. free

となります。後者の場合、2つのポインタ変数を用意して、それぞれの領域のメモリアドレスを記憶しておく必要があります。

問題③

問題③ 標準入力から、int型に収まる整数値が繰り返し入力されるとして、すべての入力を受け取った後、受け取ったすべての数値を標準出力へ出力するプログラムを作成してください。負数が入力されると終了します(これは realloc関数のところのサンプルプログラムと同様です)。

ただし、メモリ領域が倍々の大きさで確保されるようにしてください。最初の大きさは任意で決めて構いません。


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

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

#define INITIAL_SIZE    2

int main(void)
{
    // 初回確保
    size_t capacity = INITIAL_SIZE;  // 確保されている領域の大きさ
    int* array = malloc( sizeof(int) * capacity );
    size_t size = 0;      // データ総数=実際に使用されている領域の個数


    while( 1 ){
        char buf[40];
        int num;

        puts( "次のデータを整数で入力してください。負数を入力すると終了します。" );
        fgets( buf, sizeof(buf), stdin );
        sscanf( buf, "%ld", &num );

        if( num < 0 ){
            break;
        }

        size++;

        // 領域が足りなくなったら、既存の領域の大きさの倍の値で再確保する
        if( size >= capacity ){
            capacity *= 2;
            int* tmp = realloc( array, sizeof(long) * capacity );
            if( tmp == NULL ){
                // realloc関数が失敗した場合、元の領域は解放されずに残されている
                // 自分で free関数を呼び出して終了する
                free( array );
                exit( EXIT_FAILURE );
            }
            array = tmp;
            tmp = NULL;  // 安全策。確保された領域を指すポインタを array だけに限定する
        }

        // 入力されたデータを格納
        array[size-1] = num;
    }

    // 結果を出力
    for( int i = 0; i < size; ++i ){
        printf( "%d\n", array[i] );
    }

    free( array );

    return 0;
}

実行結果:

次のデータを整数で入力してください。負数を入力すると終了します。
10
次のデータを整数で入力してください。負数を入力すると終了します。
20
次のデータを整数で入力してください。負数を入力すると終了します。
5
次のデータを整数で入力してください。負数を入力すると終了します。
-1
MAX: 20

問題④

問題④ 標準入力から、long型に収まる整数値が繰り返し入力されるとして、すべての入力を受け取った後、入力された数値の平均値を出力するプログラムを書いてください。入力件数は不明ですが、最大でも 1000件とし、負数が入力されると終了します。

ただし、必要なメモリ領域はプログラムの開始直後にまとめて確保し、最後に realloc関数を使って、使われなかった領域を切り詰める方法で実装してください。


realloc関数は、領域の大きさを小さくする形での再確保もできます。ですから、必要になる可能性がある最大の大きさが分かっているのなら、最初にまとめて確保しておき、最後に切り詰めるという実装方法も考えられます。

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

#define INPUT_DATA_NUM_MAX  1000    // 入力されるデータの最大数

int main(void)
{
    // 最初に、必要になる可能性がある最大の大きさに合わせて確保しておく
    long* array = malloc( sizeof(long) * INPUT_DATA_NUM_MAX );
    size_t size = 0;      // データ総数=実際に使用されている領域の個数


    long sum = 0;

    while( 1 ){
        char buf[40];
        long num;

        puts( "次のデータを整数で入力してください。負数を入力すると終了します。" );
        fgets( buf, sizeof(buf), stdin );
        sscanf( buf, "%ld", &num );

        if( num < 0 ){
            break;
        }

        size++;
        assert( size <= INPUT_DATA_NUM_MAX );    // 想定される以上のデータ数

        // 入力されたデータを格納
        array[size-1] = num;

        // 合計を計算
        sum += num;
    }

    // 実際に使った大きさに切り詰める
    if( size > 0 ){
        long* tmp = realloc( array, sizeof(long) * size );
        if( tmp == NULL ){
            // realloc関数が失敗した場合、元の領域は解放されずに残されている
            // 自分で free関数を呼び出して終了する
            free( array );
            exit( EXIT_FAILURE );
        }
        array = tmp;
        tmp = NULL;  // 安全策。確保された領域を指すポインタを array だけに限定する
    }


    // 平均を出力
    if( size > 0 ){
        printf( "AVERAGE: %ld\n", (long)(sum / size) );
    }
    else{
        printf( "AVERAGE: %ld\n", 0L );
    }

    free( array );

    return 0;
}

実行結果:

次のデータを整数で入力してください。負数を入力すると終了します。
10
次のデータを整数で入力してください。負数を入力すると終了します。
20
次のデータを整数で入力してください。負数を入力すると終了します。
5
次のデータを整数で入力してください。負数を入力すると終了します。
-1
AVERAGE: 11

本題とは関係ありませんが、この手のプログラムでは、入力件数が 0件という場合もちゃんと考慮に入れましょう。 realloc関数の大きさの指定に 0 を渡したときに、free関数の動作になりますし、 平均値を求める除算では、0除算エラーを起こしてしまいます。

問題⑤

問題⑤ realloc関数を次の条件でラップした関数を作成してください。


標準ライブラリ関数も、そのまま使うことばかりではなく、改造を加えることを考えてみるのは良いことです。

/*
    独自の realloc関数。
    引数:
        ptr:    元の領域のメモリアドレス。
        size:   新たに確保する大きさ。 0 は許可しない。
    戻り値:
        新しい領域のメモリアドレス。
        ヌルポインタが返される可能性はなく、エラー時は exit関数で強制終了する。
*/
void* my_realloc(void* ptr, size_t size)
{
    assert( size > 0 );  // size 0 は不正とする

    void* tmp = realloc( ptr, size );
    if( tmp == NULL ){
        free( ptr );
        exit( EXIT_FAILURE );  // 失敗したら強制終了
    }
    return tmp;
}

realloc関数の第2引数に 0 を渡すと処理系定義の動作になります。意図しない状況であることが多い(と思われる)ので、これを許可しないような実装にしています。

また、第1引数に渡すポインタ変数と、戻り値を受け取るポインタ変数を同じにできない(理由は本編参照)ため、普通に realloc関数を使うと面倒ですが、ラップすると内部に隠れるので、随分楽ができます。

// ヌルポインタが返されることはあり得ない
array = my_realloc( array, sizeof(long) * size );

問題⑥

問題⑥ 次のような関数を作成しました。

char* strDuplicate(const char* s)
{
    char* dup = malloc( strlen( s ) + 1 );
    strcpy( dup, s );

    return dup;
}

この関数は何をしているか説明してください。また、この関数を実際に使用したプログラムを作成してください。


この関数は、実引数で指定された文字列の複製を作っています。実際に使用したプログラムは、たとえば次のように作成できます。

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

char* strDuplicate(const char* s);

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

    puts( str2 );

    free( str2 );

    return 0;
}

/*
    文字列の複製を作る。
    引数
        s:      複製元の文字列。
    戻り値
        複製された文字列のメモリアドレス。
        malloc関数によって領域を確保しているので、外部での解放が必要。
*/
char* strDuplicate(const char* s)
{
    char* dup = malloc( strlen( s ) + 1 );
    strcpy( dup, s );

    return dup;
}

実行結果:

abcde

malloc関数で新たなメモリ領域を作成し、そこへ strcpy関数によって、複製元の文字列の内容をコピーしています。

ローカル変数dup を返している訳ですが、この場合は、動的にメモリ割り当てを行っているので問題ありません。ローカル変数dup 自身は、strDuplicate関数を抜け出すと消えてしまいますが、これが指し示していた先にあるオブジェクトは消えません。

strDuplicate関数が返したポインタは、呼び出し側で確実に受け取り、使い終わった後、free関数を呼び出す必要があります。戻り値の受け取りを強制する手段がないため、解放忘れの危険性はあります。この辺りはC言語の限界と言えそうです。


参考リンク


更新履歴

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

≪さらに古い更新履歴を展開する≫



第35章のメインページへ

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

Programming Place Plus のトップページへ



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