C言語編 第34章 ポインタ④(動的なメモリ割り当て)

先頭へ戻る

この章の概要

この章の概要です。

動的記憶域期間

これまでに、記憶域期間の種類として、自動記憶域期間と静的記憶域期間の2つが登場しました。 記憶域期間とは、あるオブジェクトがいつからいつまでメモリ上に存在しているかのルールです。 これは言い換えると、そのオブジェクトのメモリアドレスが有効である期間を表しているとも言えます。

オブジェクトとは、ある型のある値を持つ、メモリ上の一部分のことを言います。 これまでの章では、これはつまり変数のことでした。 しかし、この章で説明するように、変数を宣言することなく、メモリ上に値を置く方法があるため、 正確には、オブジェクトと表現するのが適切です。

あるオブジェクトの記憶域期間の範囲外に、そのオブジェクトへアクセスした場合は未定義の動作となります。 このことは当然、ポインタを経由した場合にもいえることです。 すでに記憶域期間を終えているオブジェクトを、ポインタを経由してアクセスした場合も未定義の動作です。

さて、本章では、3つ目の記憶域期間として、動的記憶域期間(割付け記憶域期間)を取り上げます。

規格上、「allocated」という単語で表現されるため、訳語としては「動的」ではなく「割付け」の方が適切です。 しかし、「静的」という言葉に対応して、「動的」という言葉を使う機会が多いため、ここでは、動的記憶域期間の方で統一します。

動的記憶域期間を持つオブジェクトは、任意のタイミングでメモリ上に置かれ、任意のタイミングで消すことができます。 非常に自由度が高い反面、慎重なプログラミングが要求されます。 また、絶対にポインタを使わなければならず、ポインタの正しい理解が必須と言えます。

メモリの使われ方

オブジェクトを記憶しておくとき、メモリをどのように使うかという観点でも、記憶域期間による違いが見られます。ただし、以下の説明はあくまで、「多くの環境で」の話であるということに注意して下さい。規格上は、記憶域期間ごとのメモリの使い方に関して、具体的な規定はありません。

自動記憶域期間を持つオブジェクトは、メモリ内のスタック領域に配置されます。スタック領域は、メモリの中の比較的狭い範囲に固定的に用意されており、ここを使いまわすように利用されます。
使いまわすというのが1つのポイントで、ある関数の中で定義されたローカル変数のメモリアドレスと、異なる関数の中で定義されたローカル変数のメモリアドレスが一致する可能性があります。また、同じローカル変数であっても、その関数を呼び出すたびに、異なるメモリアドレスに配置される可能性もあります。

静的記憶域期間を持つオブジェクトは、メモリ内の専用の領域(静的領域グローバル領域)に配置されます。静的記憶域期間を持つオブジェクトは、プログラムの実行が開始されたときから、実行が終了するまで消えることがないので、メモリアドレスは常に固定されています。
この領域は、ビルドを行い、実行ファイルを生成する過程の中で、必要な大きさが分かるため、無駄な領域を取らないように割り当てられます。

動的記憶域期間を持つオブジェクトは、メモリ内のヒープ領域に配置されます。
ヒープ領域を使うためには、OS に「これだけの大きさの領域が欲しい」という要求を出さなくてはなりません。OS は要求に応じて必要な領域を確保し、そのメモリアドレスを返してくれます。
また、その領域を使い終えて必要なくなったときには、OS に「この領域はもういらない」と伝えます。すると OS は、その領域を誰にも使われていない状態に戻します。
このように、プログラムの実行中にメモリを確保することを、動的メモリ割り当てとか、ダイナミックアロケーションなどと呼びます。また、使い終わったときにメモリを返却する過程を、メモリを解放(デアロケート)すると表現します。

また、細かいことですが、「確保」という言葉が、「メモリ領域を確保」と「オブジェクトを確保」の両面で区別せず使われていることが多いです。メモリ領域がなければ、オブジェクトを置く場所がない訳ですから、両方を含めて言っている訳ですが、それぞれを別の過程としてイメージする癖を付けておくと、理解が深まります。まず、メモリ領域が確保され、そこにオブジェクトが置かれるという流れです。

malloc関数 free関数

では実際に、動的メモリ割り当てを行い、動的記憶域期間を持つオブジェクトを作ることを体験してみましょう。まずは、malloc関数を紹介します。

malloc関数は、stdlib.h に次のように宣言されています。

void* malloc(size_t size);

malloc関数は、動的にメモリ領域を割り当て、そのメモリアドレスを返す関数です。 確保されたメモリ領域には、不定値を持ったオブジェクトが置かれた状態です。 いつものように、きちんと正しい値を入れてから使うようにして下さい。 このようにして確保されたオブジェクトは、動的記憶域期間を持ちます。

引数には、確保したい領域の大きさを指定します。 少し感覚的にずれを感じる人もいるかも知れませんが、ここで指定するのはオブジェクトの「型」のような情報ではなく、 オブジェクトを置くために必要十分な「大きさ」です。 そのため、例えば double型のオブジェクトを置くための領域が欲しいのなら、「sizeof(double)」を渡します。

戻り値は、確保されたメモリ領域の先頭のメモリアドレスが返されます。もし何らかの要因で(普通は、メモリ不足です)確保に失敗した場合には、ヌルポインタが返されます。

戻り値の型が void* になっています。 ポインタ型は、指し示す先にある型+「*」で表現されますから「void*」ではおかしいようですが、これは特別なポインタ型を表現しています。 「void*」で表されるポインタは、汎用ポインタと呼ばれ、どんな型でも指し示すことができるポインタです。 malloc関数は、いかなる型のための領域であっても確保できなければならないため、汎用ポインタが使われます。

汎用ポインタなどというものがあるのなら、いつもそれを使えばいいのではないかと思われるかも知れません。 しかし、指し示す先にあるものが何型であるかという情報が欠如しているため、間接参照ができません。 そのため、汎用ポインタを使って間接参照を行うためには、結局、適切なポインタ型に型変換する必要があります。

汎用ポインタから、通常のポインタへは暗黙的に型変換できます。 そのため、malloc関数の戻り値は、通常のポインタ型を使って受け取ることができます。

int* p1 = malloc( sizeof(int) );         /* int 1個分 */
int* p2 = malloc( sizeof(int) * 100 );   /* int 100個分 */
char* p3 = malloc( sizeof(char) * 101 ); /* 100文字分 */

malloc関数の実引数は、確保するメモリ領域の大きさなので、単独のオブジェクトのための領域なら sizeof が返す値をそのまま渡せばよいです。 配列の場合は要素数を掛け合わせて合計の大きさを渡します。
文字列の場合には特に注意が必要です。 末尾のヌル文字('\0') も格納できるだけの領域がなければなりませんから、必ず 1文字分多く確保することを忘れないようにして下さい。

C++ では、汎用ポインタから通常のポインタ型への暗黙的な型変換はできなくなりました。 必要があれば、明示的にキャストを行う必要があります(Modern C++編【言語解説】第2章)。 ただし、C++ では malloc関数を使う必要性自体が無くなっているため(Modern C++編【言語解説】第15章)、この場面で面倒が増えることはありません。

malloc関数は失敗する可能性があることを考慮しなければなりません。 動的なメモリ割り当てに失敗するということは、ほとんどの場合、メモリが足りておらず、プログラムの実行を続けることができない状況のはずです。 そのため、原則的には、プログラムを正しく終わらせるようにします。

int* array = malloc( sizeof(int) * 10000 );
if( array == NULL ){
    exit( EXIT_FAILURE );
}

exit関数は、プログラムを強制終了させる標準ライブラリ関数で、stdlib.h にあります。実引数に、EXIT_FAILURE というオブジェクト形式マクロを使うと、何らかの問題が起きてプログラムを終わらせようとしていることを意味します。

もちろん、可能な限りのことはしたいところではあります。 例えば、プログラムを実行しているユーザーに対して、何らかの問題が起きたことを伝えるだとか、 開発者が対応できるような情報をファイルなどに書き出しておくだとかといったことが考えられます。 とはいえ、メモリが足りない以上、できることも限られており、限界があると言わざるを得ません。

どうせ exit関数で終了させるのなら、失敗の確認なんてしなくてもいいと思う人もいるかも知れません。しかし、失敗を確認しなかったら、正常時のコードを実行してしまう訳なので、恐らく、ヌルポインタを経由した間接参照を行うコードを実行するはずです。これは未定義の動作ですから、「何が起こるか分かりません」。exit関数は「何が起きているか分かったうえでの終了」なので、まったく意味が違います。いかなる場面でも、未定義の動作は引き起こしてはなりません。

malloc関数の失敗を調べるにしても、malloc関数を呼ぶたびにチェックするのは面倒なので、独自の関数を用意することもあります。 例えば、次のような関数を用意し、常にこれを使うようにしていれば、後から失敗時の対応の方針を変えることも容易になります。

void* xmalloc(size_t size)
{
    void* p = malloc( size );
    if( p == NULL ){
        exit( EXIT_FAILURE );
    }
    return p;
}


さて、動的に確保されたメモリ領域は、使い終わったら解放しなければならないのでした。 解放には、free関数を使います。

void free(void* ptr);

引数には、malloc関数が返したポインタを渡します。動的に確保した領域以外を指すポインタを渡してはいけません。ただし、ヌルポインタを渡した場合には、何も起こらないことが保証されています

free関数によって、オブジェクトは動的記憶域期間を外れることになります。 記憶域期間の範囲外にオブジェクトへアクセスする行為は未定義の動作ですから、 free関数で解放されたオブジェクトへのアクセスは行ってはなりません

free関数で解放することを忘れても、OS がきちんと管理している多くの環境では、プログラム終了時に自動的に解放されます。 ただし、C言語の立場としては、これは必ずそうであるとは言えません。 プログラム内で解放することを勧めます。


さて、それでは実際のプログラムを見てみましょう。

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

int main(void)
{
    int* p;

    p = malloc( sizeof(int) );
    if( p == NULL ){
        exit( EXIT_FAILURE );
    }

    *p = 123;
    printf( "%d\n", *p );

    free( p );

    return 0;
}

実行結果:

123

以下の点を確認してください。

プログラム例を示しておいて何ですが、通常、int型変数 1個のために、動的なメモリ割り当てを使うことはあり得ません。 動的なメモリ割り当ては、実行速度の面でも、メモリ使用量の面でも不利に働きます。

動的なメモリ割り当てを使う価値があるのは、どれだけの大きさがあれば十分なのか、プログラムを実行してみないと分らないようなときです。 例えば、標準入力からデータを受け取る際、そのデータ総数が何個あるか不明な場合などです。 こういう場合は、配列のために動的なメモリ割り当てを行うことになります。

配列を動的に確保する例を挙げます。

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

int main(void)
{
    char buf[40];
    int dataNum;
    int* dataArray;
    int i;


    /* データ件数を受け取る */
    puts( "データの件数を入力して下さい。" );
    fgets( buf, sizeof(buf), stdin );
    sscanf( buf, "%d", &dataNum );

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

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

    /* データを受け取る */
    for( i = 0; i < dataNum; ++i ){
        puts( "データを入力して下さい。" );
        fgets( buf, sizeof(buf), stdin );
        sscanf( buf, "%d", &dataArray[i] );
    }

    /* 結果を出力 */
    for( i = 0; i < dataNum; ++i ){
        printf( "%d: %d\n", i+1, dataArray[i] );
    }

    free( dataArray );

    return 0;
}

実行結果:

データの件数を入力して下さい。
5
データを入力して下さい。
35
データを入力して下さい。
-50
データを入力して下さい。
95
データを入力して下さい。
20
データを入力して下さい。
-45
1: 35
2: -50
3: 95
4: 20
5: -45

このプログラムの場合、入力されるデータの総数が毎回変わりうるという状況を想定しています。 このように、プログラム作成時点ではデータ数が不明な場合、静的に領域を用意することは難しいといえます。 もし用意する領域が少な過ぎれば、膨大な量の入力に対応できないことになってしまいます。 逆に、膨大な量の入力に備えて巨大な配列を作ると、データ数が非常に少ない場合、ほとんどのメモリが無駄になってしまいます。

動的に確保された配列を使う場合、ポインタを通して扱うという事情から、 動的な配列は、sizeof演算子を使って配列全体の大きさを知ることができない点に注意が必要です。

char static_array[100];
char* dynamic_array = malloc( sizeof(char) * 100 );

printf( "%u\n", sizeof(static_array) );   /* 100 */
printf( "%u\n", sizeof(dynamic_array) );  /* 4 */

ポインタ経由で扱う以上、sizeof演算子が返す値は、常にポインタ変数の大きさにしかならない訳です。 このため、配列の要素数を調べる SIZE_OF_ARRAYマクロのようなマクロを用意していても、動的な配列には使用できません。 どうしても後から要素数を知る必要がある場合には、管理が煩雑になりますが、 malloc関数を呼び出したついでに、別の変数に要素数を保存しておくしかありません。

memset関数

malloc関数で確保されたメモリ領域は、不定値が入った状態でした。 例えば配列であれば、次のように for文で初期化することになるでしょう。

const int size = 10;
int i;
int* array;

array  = malloc( sizeof(int) * size );
if( array == NULL ){
    exit( EXIT_FAILURE );
}
for( i = 0; i < size; ++i ){
    array[i] = 0;
}

メモリ領域にまとめて特定の値を格納する場合には、memset関数を使う方法があります。memset関数は、string.h に次のように宣言されています(若干、想定外の標準ヘッダに含まれています)。

void* memset(void* s, int c, size_t size);

s が指し示しているメモリ領域を先頭として、そこから sizeバイト分の範囲の各バイトに、c の値を埋めます。 引数 c が int型ですが、「1バイト単位」で値を埋めていくのだということに注意して下さい。 つまり実際に行われていることは、c の値を unsigned char型にキャストした値を、各バイトに入れていくことです。
戻り値は、s と同じものが返されます。

memset関数を使うと、次のように初期化できます。

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

int main(void)
{
    const int size = 10;
    const size_t alloc_size = sizeof(int) * size;
    int i;
    int* array;

    array = malloc( alloc_size );
    if( array == NULL ){
        exit( EXIT_FAILURE );
    }
    memset( array, 0, alloc_size );
    
    for( i = 0; i < size; ++i ) {
        printf( "%d\n", array[i] );
    }
    
    free( array );
    
    return 0;
}

実行結果:

0
0
0
0
0
0
0
0
0
0

memset関数の第2引数が、unsigned char型で収まる値 (0~255) でなければならないことと、 第3引数が要素数ではなく、領域の大きさであることに注意して下さい。

calloc関数

配列の動的な確保に関しては、calloc関数を使う方法もあります。calloc関数も stdlib.h にあります。

void* calloc(size_t n, size_t size);

第1引数に要素数を、第2引数に要素1つ分の大きさを指定します。 戻り値は、malloc関数と同様に、動的に確保されたメモリ領域のアドレスが返されます。 失敗した場合の戻り値はヌルポインタです。

malloc関数と異なり、確保された領域の全ビットが自動的に 0 で埋められます整数型であれば 0 で初期化されていると考えて良いですが、他の型の場合は想定と異なる意味を持つかも知れません。例えば、ポインタの場合、「全ビットが 0」という状態が、ヌルポインタを表すとは限りませんし、浮動小数点型の場合、「全ビットが 0」=「0.0」とはならないかも知れません。

なお、calloc関数で確保した領域も、free関数で解放します

calloc関数は、malloc関数で確保して、memset関数で 0 を埋めるのと同じ結果を生みます。

大抵は気にする必要はありませんが、様々な要因によって、行っていることが異なることがあり、効率面においても差が開くことがあります。

memset関数のところで取り上げたサンプルプログラムを、calloc関数で書き換えると次のようになります。

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

int main(void)
{
    const int size = 10;
    int i;
    int* array;

    array = calloc( size, sizeof(int) );
    if( array == NULL ){
        exit( EXIT_FAILURE );
    }
    
    for( i = 0; i < size; ++i ) {
        printf( "%d\n", array[i] );
    }
    
    free( array );
    
    return 0;
}

実行結果:

0
0
0
0
0
0
0
0
0
0

注意事項のまとめ

malloc関数、calloc関数、free関数を正しく使うには、色々と注意すべき点があります。 慣れるまで大変なので、ここにまとめておきます。

malloc関数、calloc関数に関して、

free関数に関して、

これだけのことを考慮するのは、結構、骨が折れます。 労力を減らすために有効な手法の1つとして、解放のための関数形式マクロを定義しておくというものがあります。

#define SAFE_FREE(ptr)		if(ptr != NULL ){ free(ptr); ptr = NULL; }

free関数を直接呼び出す代わりに、このマクロを使って解放するようにします。

int* array = malloc( sizeof(int) );
SAFE_FREE( array );

こうすると、解放後にポインタ変数が NULL で上書きされます。ちなみに、free関数にヌルポインタを渡しても何も起こらないので、ヌルポインタかどうかのチェックは無くても構いませんが、効率面を考えてチェックすることが多いです。

このようなマクロを使うことで、以下のような利点が生まれます。

ただし、同じメモリ領域を指し示すポインタが 2つ以上存在するような状況を作ってしまっていると、これだけで確実とはいえません。


また、動的なメモリ割り当てを覚えると、やたらと使いたがる人もいるようですが、それはお勧めできません。 ここまでの内容を見ても明らかなように、バグを作ってしまう可能性が一気に増えます。 実際、プログラムのバグの原因として、動的なメモリ割り当ての扱いを失敗しているケースは非常に多いです。

また、動的なメモリ割り当ては、処理速度の面でも不利です。 確保も解放も、それなり時間のかかる処理です。

さらに、メモリの利用効率の面でも不利です。 例えば、8バイトの領域を確保したつもりでも、実際には OS側の都合などで、管理情報(数十バイト程度)が付加されるため、 普通はもっと多くの領域を消費します。 ある程度まとまった大きさの確保であれば、管理情報の大きさは、想定的にいって小さくなるので、無視できる程度になりますが、 細々した領域を大量に確保するような使い方はやめた方が良いです。

このような理由から、動的なメモリ割り当ては、明確な必要性が無い限りは避けるべきです。


練習問題

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

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

int main(void)
{
    long* value;

    value = malloc( sizeof(long) );

    value = 100;
    printf( "%d\n", *value );

    free( value );

    return 0;
}

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

#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)
{
    char buf[40];
    int dataNum;
    PersonalInfo* infoArray;
    int i;


    /* データ件数を受け取る */
    puts( "データの件数を入力して下さい。" );
    fgets( buf, sizeof(buf), stdin );
    sscanf( buf, "%d", &dataNum );

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

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

    /* データを受け取る */
    for( 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( i = 0; i < dataNum; ++i ){
        printf( "%s: %s\n", infoArray[i].name, infoArray[i].phone_number );
    }

    return 0;
}

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

#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)
{
    size_t i;

    for( i = 0; i < size; ++i ){
        array[i] = i;
    }
}

/*
    配列の中身を標準出力へ出力する。
    引数:
        array:	対象配列のアドレス。
        size:	対象配列の要素数。
*/
void PrintArray(int* array, size_t size)
{
    size_t i;

    for( i = 0; i < size; ++i ){
        printf( "%d\n", array[i] );
    }
}


解答ページはこちら

参考リンク

更新履歴

'2018/5/14 章のタイトルを変更(「ポインタ④ 動的なメモリ割り当て①」->「ポインタ④ 動的なメモリ割り当て」)

'2018/4/20 「NULL」よりも「ヌルポインタ」が適切な箇所について、「ヌルポインタ」に修正。

'2018/3/10 全面的に文章を見直し、修正を行った。
章のタイトルを変更(「動的なメモリ」->「動的なメモリ割り当て」)
memset関数」の項を追加。

'2018/2/28 「C99 (可変長配列)」の項を、第25章へ移動。

'2018/2/22 「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。

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





前の章へ(第33章 ポインタ③(引数や戻り値への利用))

次の章へ(第35章 ポインタ⑤(動的なメモリの再割り当て))

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS