【C言語】malloc関数(メモリの動的確保)について分かりやすく解説

malloc解説ページのアイキャッチ

このページにはプロモーションが含まれています

今回はC言語の malloc 関数について解説していきたいと思います。

malloc 関数の定義

では早速 malloc 関数の定義を紹介します。

malloc関数の定義
#include <stdlib.h>
void *malloc(size_t);

malloc 関数は動的にメモリを確保する関数です。

成功時には確保したメモリのアドレスが、失敗時には NULL が返却されます。

引数には確保したいサイズをバイト単位で指定します。

また、定義されているファイルは stdlib.h なので、stdlib.hinclude してから使用してください。

動的…?確保…?

うん。そうだね。

いきなり「動的確保」って言われても意味が分からないよね…

なので、このページでは malloc 関数をもっと詳しく理解するために、動的確保、さらにはメモリについても一緒に解説していくよ!

malloc 関数をより深く理解するためには、メモリやメモリの確保について理解することが重要です。ここからは、このメモリやメモリの確保についてまず説明し、続いて malloc 関数の使い方やメリットデメリット等について解説していきたいと思います。

C言語プログラムとメモリ

前述の通り、malloc 関数のことをしっかり理解するためには、まずはメモリについて理解するのが良いと思います。

なので、まずはメモリについて解説していきます。

スポンサーリンク

メモリ

メモリとはデータを記憶するハードウェアです。

パソコンのスペック表などを見ても、このメモリのサイズが必ず明記されていると思います。

最近だと10GB を超えるサイズのメモリが内蔵されているパソコンも多く発売されていますね!

10GBのメモリでは、1バイト分のデータを 1024 * 1024 * 1024 * 10 個分記憶することができます。

メモリを図示すると ↓ のようになります。下側の数字はアドレスで、アドレスは各バイトの位置を示す数値となります。

メモリを表す図

どんなプログラミング言語で作成したプログラムにおいても、動作するにはデータの記憶が必要であり、その時にこのメモリを利用して動作しています。

例えば電卓アプリでは、ユーザーが指定した数字や演算子をメモリに記憶し、その記憶したデータを用いて計算処理が実行されます。また、ウェブブラウザで動画を再生するような場合は、ダウンロードしたデータをメモリに記憶し、その記憶したデータを利用して動画の再生が行われます。

メモリにデータを記憶する様子

メモリの割り当て

また、パソコン上では非常に多くのアプリが同時に動作しており、それぞれのアプリがメモリを利用しています(アプリだけでなくオペレーティングシステムもメモリを利用しています)。

この時、異なるアプリ同士が同じメモリを使用してしまうと、データが壊れてしまう可能性があります。

データを破壊する様子

このようなことが起こらないように、コンピュータ内では各アプリが使用しているメモリ領域や、未使用のメモリ領域が管理されています。

アプリごとに使用するメモリを割り当てる様子

また、アプリが起動するような時には、未使用のメモリ領域からアプリの起動に必要なサイズ分のメモリが確保され、そのメモリをアプリに割り当ててから起動させるようになっています。

そして、アプリはその確保されたメモリを利用して動作します。

この動作時にポイントになるのが、自身のアプリ用に確保されたメモリしか使用してはいけないという点です。

他のアプリ用に確保されたメモリを使用すると、前述したようにデータ破壊などが行われてしまいます。

なので、他のアプリ用に確保されたメモリのデータを使用されそうになったらエラーが出るようになっています。これがプログラム実行時に良く見かける Segmentation fault です。

こんな感じの制御が行われることで、アプリ同士で同じメモリを使用してデータを壊さないようになっています。

こういう話を聞くと、メモリサイズが大きいパソコンの方が同時にアプリを動作させる数が多くなることも論理的に理解できると思います。

C言語プログラムが扱えるメモリ

ここまで一般的なアプリの例で解説してきましたが、これらは私たちがC言語でプログラミングするプログラムにおいても同じです。

つまり、私たちが作成したプログラムも、起動時にプログラムの起動に必要なメモリが確保され、そのメモリを利用して動作します。

この時に確保されるメモリのサイズはプログラムの内容によって異なります。例えば下記のような情報に基づいてサイズが決まります。

  • プログラム自体のサイズ
  • グローバル変数や static 変数のサイズ
  • スタックサイズ

プログラム内で読み書きするデータという観点で考えると「グローバル変数や static 変数のサイズ」が確保されるメモリのサイズに直接関係あることになります。

これらの変数がプログラム内で使用できるように、変数宣言されている変数のサイズ分のメモリが確保され、プログラム起動時にこれらの変数が確保されたメモリ上に配置されることになります。

MEMO

ローカル変数(動的変数)はスタックに格納されるデータであり、プログラム実行時に確保されるメモリサイズには直接関係ありません

ただしローカル変数がスタックサイズに収まりきらない場合はスタックオーバーフローエラーが発生してしまいます

スタックのサイズはスレッドごとに設定できたりします

ただし、変数用のメモリは変数宣言された変数の分しか確保されませんので、変数宣言していない分のメモリは使用できないことになります。

前述のとおり、プログラムはそのプログラム用に確保されたメモリしか使用してはいけないのです。

つまり、ソースコードを書く時にはあらかじめプログラムで必要な数・サイズを決定して、漏れなく必要な分の変数を宣言してやる必要があることになります。

このサイズを決める上でよく困るのが配列です。

配列では、変数宣言時にサイズ(もしくは配列に格納するデータそのもの)を指定する必要があります。

サイズ100の配列の変数宣言
int array1[100];
char array2[] = "aiueo";

ですが、ソースコードを書いている時点ではサイズを決めるのが難しい場合があります。

例えば、ファイルから読み込んだ文字列を配列に格納するプログラムなどは、読み込むファイルが変わるとファイルのサイズが変わるため、必要になるサイズも毎回変わってしまうことになります。

こんな感じで、ソースコードを書いている時点ではサイズが定まらないようなケースは結構多いと思います。

スポンサーリンク

メモリの動的確保

こんな時に便利なのが「メモリの動的確保」です。

動的確保とは、プログラム起動時に確保されたメモリ以外のメモリを後から(プログラム起動した後から)追加で確保する手段になります。

つまり、ソースコードで変数宣言を行なって確保したメモリ “以外の” メモリを後から追加して使用することができます。

静的確保したメモリと動的確保したメモリ

例えば前述のファイル読み込みの例であれば、プログラム起動後に読み込むファイルのサイズを調べ、そのサイズが判明してから必要なサイズのメモリを追加で確保することができます。

ファイルサイズを調べてから動的にメモリを確保する様子

ですので、ソースコード記述時に必要なメモリのサイズや個数が定まらないような場合でも、動的確保を利用すれば、プログラム起動後に必要になった分のメモリだけを後から追加することができるようになります。

メモリの動的確保に対し、プログラム起動時に決まったサイズ分メモリを確保することをメモリの静的確保と呼ぶこともあります。

例えばグローバル変数や static 変数を宣言することは「メモリを静的確保すること」と捉えることができます。静的確保されたメモリはプログラム起動時から終了時まで常に確保されていますが、動的確保するメモリに関しては、プログラム起動後に好きなタイミングで確保し、不要になったらそれを手放す(解放する)ことも可能です。

malloc 関数とは

ここまで解説を行なってきた動的確保を行う関数の1つが malloc 関数です。

malloc 関数

malloc 関数の定義を下記に再掲しておきます。

malloc関数の定義
#include <stdlib.h>
void *malloc(size_t);

malloc 関数の引数には、追加で確保したいメモリのサイズを “バイト単位” で指定します。引数の型は size_t となります。

malloc 関数を実行することで、引数に指定したサイズ分のメモリを要求することができます。

スポンサーリンク

malloc 関数の成功時の動作

メモリの確保に成功した場合、malloc 関数は確保したメモリの先頭アドレスを返却します。返却値の型は void* です。

mallocの返却値

この時、返却値のアドレスから引数で指定したサイズ分のメモリだけが、プログラム動作用に追加で確保されることになります。つまり、返却値のアドレスから指定したサイズ分のメモリをプログラムが自由に利用することが可能です。

他のプログラムからは基本的にこのメモリは使用されません。ですので、他のプログラムからの影響を受けることなく使用することができます。

ただし、この範囲を超えてメモリを利用しようとするとエラー(Segmentation fault 等)が発生し、プログラムが強制終了させられることがあります。

MEMO

すぐにエラーが発生せずに、後から(解放時など)発生する場合もあります

エラーが発生しない場合もあります

エラーが発生しない場合でも、使用することが許可されていないメモリを利用することは禁止されており、他のアプリが使用しているデータを壊してしまう可能性もあります

確保したメモリ以外にはアクセスしないようにしましょう

また、この返却されるアドレスの型は void* 型になります。

下記ページでも解説していますが、void* 型のポインタ変数はただのアドレスを格納するだけの変数であり、間接演算子(*)や添字演算子([])を用いてアドレスの指す先のデータの変更や参照を行うようなことはできません。void* 型のポインタ変数への加減算もダメです。

voidとvoid*型の解説ページのアイキャッチ 【C言語】void型とvoid*型(void型ポインタ)について解説

そのため、malloc 関数で確保したメモリに対して間接演算子や添字演算子によるデータの変更や参照等を行いたい場合、malloc 関数の戻り値のアドレスは void* 型以外のポインタ変数に格納するのが一般的です。

例えば下記は malloc 関数の戻り値のアドレスを int * 型の変数に格納する例になります。

戻り値を格納するポインタ
int *addr;
addr = malloc(4);

malloc 関数の戻り値のアドレスをどのポインタの型の変数に格納すれば良いかは、malloc 関数で確保したメモリをどう扱いたいかによって変わります。

後述でも解説しますが、上記のように int* のポインタ変数に格納した場合、 malloc 関数で確保したメモリを int 型の配列同様に扱うことができます。例えば確保したメモリに文字列を格納したいのであれば、char 型の配列同様に扱えた方が便利なので、malloc 関数の戻り値のアドレスは char* 型のポインタ変数に格納するのが良いと思います。ちなみに、構造体のポインタ変数にアドレスを格納し、構造体の配列同様にメモリを扱うことも可能です。

こんな感じで、戻り値のアドレスを格納する型は確保したメモリをどう扱いたいかによって決める必要があります。

また、下記の (int*) のように、明示的に malloc 関数の戻り値をキャストする書き方がされているコードも結構見かけると思います。

明示的キャストを行う場合
int *addr;
addr = (int*)malloc(4);

C言語では、void* 型の値は明示的にキャストを行わなくても他のポインタ型の変数に格納することができるため、実は上記の (int*) のような明示的なキャストは不要です。

ただ、昔の仕様?では malloc の戻り値の型が void* 型ではなかったようで、その時は明示的キャストが必要で、その時の名残りで現在も malloc の戻り値を明示的にキャストする場合も多いようです。

ちなみに私が malloc を習った時は明示的キャストをした方が良いと習ったので、今でも明示的キャストを行う癖がついてます…。なので私の公開しているソースコードは明示的キャストをしているものが多いです…。

ちょっと話が脱線しましたが、大事なのは、特に malloc したメモリのデータに間接演算子や添字演算子を用いてアクセスする場合や、malloc が返却するアドレスに対して加減算等を行いたい場合は、malloc が返却するアドレスは void* 以外のポインタ変数に格納する必要がある点です(この時に明示的キャストはなくて良い)。

また、malloc 関数の戻り値は必ず受け取るようにしましょう。

malloc 関数で確保したメモリはアドレスを指定して使用する必要があります。このアドレスを忘れてしまうと、そのメモリを利用することはできません。

malloc 関数の失敗時の動作

malloc 関数が失敗した時には NULL が返却されます。

ですので、メモリの動的確保に成功したかどうかは、下記のように malloc 関数の戻り値と NULL を比較することで判断することができます。

mallocのエラーチェック
int *addr;
addr = (int*)malloc(4);
if (addr == NULL) {
    /* エラー処理 */
}

malloc 関数が失敗したときはメモリが確保されなかったということです。ですので、確保しようとしたメモリは使用できません。

例えば下記のように、malloc 関数に失敗したのに malloc 関数の戻り値のアドレスにアクセスしようとすると、使用が許可されていないメモリにアクセスすることになってしまいます。当然ダメな例になります。

エラー後の不正アクセス
int *addr;
addr = (int*)malloc(4);
if (addr == NULL) {
    *addr = 1024;
}

malloc に失敗したということは、そのプログラム内で使用することをアテにしていたメモリが使えなくなったことになりますので、正常にプログラムが動作できないことが多いです。ですので、malloc に失敗した場合はエラー終了するのが一般的かなぁ思います。成功するまでループで繰り返す、サイズを減らして再度 malloc を実行するようなケースもあるかもしれませんが…。

また、malloc 関数が NULL を返却するのは引数で指定したサイズのメモリが確保できなかった場合です。

例えば malloc 関数で要求したメモリサイズが大きすぎる場合などはエラーになることがあると思います。

malloc 関数の引数

前述の通り、malloc 関数の引数には追加で確保したいメモリのサイズをバイト単位で指定します。引数の型は size_t となります。

malloc関数の定義
#include <stdlib.h>
void *malloc(size_t);

バイト単位というところが1つのポイントであり、注意点でもあります。ここについて解説しておきます。

例えば配列であれば、下記のように変数宣言を行えば、int 型の変数8個分のメモリが確保されることになります。

配列の宣言によるメモリ確保
int array[8];

ご存知の通り、型にはサイズが定義されており、一般的に int 型のサイズは4バイトです。

ですので、バイト単位で考えると、上記の変数宣言により32バイト分のメモリが確保されることになります。

一方で、下記のように malloc 関数を実行したとしても、引数で指定しているサイズが8バイトですので、8バイト分のメモリしか確保されません。キャストで int* に型変換していますが、これは返却値のアドレスの型が変換されるだけであり、確保するメモリのサイズには全く関係ありません。

mallocによるメモリ確保
int *addr;
addr = (int*)malloc(8);

確保したメモリのサイズは8バイトですので、これを int 型のデータとして扱うことを考えると2つ分のメモリしか確保できていないことになります。

つまり、malloc 関数の引数は “型のサイズ” を考慮して指定する必要があります。int 型の変数8個分のメモリを確保したいのであれば、4 * 8 を指定する必要があります。

ただ、型のサイズをわざわざ指定するのは面倒です。型のサイズを全部記憶している方も少ないと思いますし、型のサイズは環境によって変わったりします。

この型のサイズを取得するのに便利なのが sizeof 演算子です。sizeof 演算子の引数に型名を指定すれば、指定した型のサイズを取得することができます。

int 型の変数8個分のメモリを確保する場合、malloc 関数の引数には sizeof 演算子を利用して下記のように指定すれば良いです。

sizeofを利用したmallocによるメモリ確保
int *addr;
addr = (int*)malloc(sizeof(int) * 8);

スポンサーリンク

malloc 関数で確保したメモリの使い方

続いて malloc 関数で確保したメモリの使い方を解説していきます。

前述の通り、malloc 関数の戻り値は確保したメモリの先頭アドレスになります。

そして、その先頭アドレスから、malloc 関数に引数で指定したサイズ分のメモリを使用することができます。

mallocの返却値

で、確保したメモリの使い方は基本的にポインタと同じになります。配列を指すポインタと考えるとより分かりやすいと思います。

ということで、まずは配列を指すポインタの使い方をおさらいしておきましょう。

配列を指すポインタ

下記は、ポインタ addr に配列 array の先頭アドレスを指させ、addr から array のデータにアクセスするプログラムになります。

配列を指すポインタ
#include <stdio.h>

/* intデータ4つ分のメモリ */
int array[4];

int main(void){
    int i;
    int x;
    int *addr;

    /* 配列の先頭を指す */
    addr = array;

    for (i = 0; i < 4; i++) {
        /* 添字演算子を用いたアクセス */
        addr[i] = i * 1024;
    }

    for (i = 0; i < 4; i++) {
        /* 間接演算子を用いたアクセス */
        x = *addr;
        printf("%d : %d\n", i, x);

        /* アドレス値を加算 */
        addr++;
    }

    return 0;
}

配列 arrayint array[4]; と変数宣言していますので、連続する int 型のデータ4つ分のメモリがプログラム起動時に確保されることになります。

配列では、配列名に対して添字演算子([])を指定することで、配列の各要素のデータにアクセスすることができます。これと同様に、配列を指すポインタも同様に、添字演算子を指定することで配列の各要素のデータにアクセスすることができます。

添字演算子によるアクセス
/* 添字演算子を用いたアクセス */
addr[i] = i * 1024;

またポインタは、間接演算子(*)を用いることで、ポインタの指す先(ポインタに格納されているアドレス)のデータにアクセスすることもできます。

間接演算子によるアクセス
/* 間接演算子を利用してアクセス */
x = *addr;

さらに、ポインタ変数に対して加算や減算を行うことで、ポインタに格納されているアドレスを増減させることができます。要はポインタの指す先を変更することができます。

アドレスの加減算
/* アドレス値を加算 */
addr++;

もっと正確に言うと、ポインタへの加減算によるアドレスの増減量やアクセスするデータのサイズはポインタの型によって異なります。

この辺りは下記ページで解説していますので、こちらも是非読んでみてください。

ポインタの型の解説ページアイキャッチ 【C言語】ポインタの「型」について解説

malloc で確保したメモリを指すポインタ

続いて malloc 関数で確保したメモリを指すポインタの使い方について確認していきましょう。

malloc 関数で確保したメモリだからといって身構える必要はなく、実は先ほどの配列を指すポインタと同じように扱うことができます。

これは、確保の仕方は異なるものの、配列も malloc 関数で確保したメモリも、どちらも結局は同じただのメモリだからです。

下記はポインタ addrmalloc 関数で確保したメモリの先頭アドレスを指させ、addr からそのメモリのデータにアクセスするプログラムになります。

mallocで確保したメモリを指すポインタ
#include <stdlib.h>
#include <stdio.h>

int main(void){
    int i;
    int x;
    int *addr, *tmp;

    /* intデータ4つ分のメモリを確保 */
    addr = (int*)malloc(sizeof(int) * 4);
    if (addr == NULL) {
        printf("malloc error\n");
        return -1;
    }

    for (i = 0; i < 4; i++) {
        /* 添字演算子を用いたアクセス */
        addr[i] = i * 1024;
    }

    tmp = addr;
    for (i = 0; i < 4; i++) {
        /* 間接演算子を用いたアクセス */
        x = *addr;
        printf("%d : %d\n", i, x);

        /* アドレス値を加算 */
        addr++;
    }

    free(tmp);

    return 0;
}

メモリを確保する方法は異なるものの、データへのアクセスの仕方やアドレスへの加算や減算を行う処理は配列を指すポインタの場合と全く同じです。

こんな感じで、malloc 関数で確保したメモリも、いつものポインタと同じように扱うことが可能です。

メモリが不要になったら free 関数で解放

ただし、静的に確保した(グローバル変数等の宣言により確保した)メモリとは異なり、動的確保したメモリは不要になったら free 関数で解放を行う必要があります。

free 関数の定義は下記のようになります。

free関数の定義
#include <stdlib.h>
void free(void*);

引数には動的確保したメモリの先頭アドレスを指定します。

解放というと具体的なイメージが付かないかもしれませんが、要は「このアドレスのメモリは不要だからお返しします」と宣言することです。

この宣言後、そのアドレスのメモリは空きメモリとして扱われます。これにより、そのメモリは他のアプリやプログラムから確保可能になります(もちろん自身プログラムで malloc した際に再び偶然同じアドレスのメモリが割りてられることもあります)。

malloc 関数の使用例

続いては malloc 関数の実際の使用例を見て malloc 関数のイメージを具体化していきましょう!

スポンサーリンク

ファイルを読み込むプログラム

ここまでの例でも挙げてきたファイルを読み込むプログラムのソースコード例は下記になります。

読み込むファイルのサイズ分のメモリを malloc 関数で動的確保しています。

mallocを利用したファイル読み込み
#include <stdio.h>
#include <sys/stat.h>
#include <stdlib.h>

int main(void) {
    char file_name[] = "test.txt";
    FILE *fi;
    struct stat stat_data;
    size_t file_size;
    char *addr;

    /* ファイルのサイズを取得 */
    if (stat(file_name, &stat_data) != 0) {
        printf("statに失敗しました\n");
        return -1;
    }

    file_size = stat_data.st_size;

    fi = fopen(file_name, "r");
    if (fi == NULL) {
        printf("ファイルオープンエラー\n");
        return -1;
    }

    /* file_size分のメモリを動的確保 */
    addr = (char*)malloc(sizeof(char) * file_size);
    if (addr == NULL) {
        printf("mallocに失敗しました\n");
        fclose(fi);
        return -1;
    }

    /* ファイルのデータの読み込み */
    fread(addr, file_size, 1, fi);
    fclose(fi);

    /* 動的確保したメモリの解放 */
    free(addr);

    return 0;

}

ファイルのサイズを取得するのに stat 関数を利用していますが、おそらく Windows 環境では使用できないと思います。Windows ではファイルサイズを取得する関数 GetFileSize が用意されていますので、そちらの関数を利用してファイルサイズを取得すると良いと思います。

ポインタ変数だけで動作するプログラム

次は配列にデータを格納し、その後に格納したデータを表示するプログラムを考えていきたいと思います。

普通に書くとソースコードは下記のようになります。

配列を利用したデータ操作
#include <stdio.h>

#define SIZE 1024

int data[1024];

int main(void) {
    int i;

    for (i = 0; i < SIZE; i++) {
        data[i] = i;
    }

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

    return 0;
}

これをポインタ変数だけを利用して記述するとソースコードは下記のようになります。

mallocを利用したデータ操作
#include <stdio.h>
#include <stdlib.h>

#define SIZE 1024

int main(void) {
    int *i;
    int *data;

    /* int変数1つ分のメモリを確保 */
    i = (int*)malloc(sizeof(int));
    if (i == NULL) {
        printf("mallocに失敗しました\n");
        return -1;
    }

    /* int変数SIZE分のメモリを確保 */
    data = (int*)malloc(sizeof(int) * SIZE);
    if (data == NULL) {
        printf("mallocに失敗しました\n");
        free(i);
        return -1;
    }


    for (*i = 0; *i < SIZE; (*i)++) {
        data[*i] = *i;
    }

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

    /* 動的確保したメモリを解放 */
    free(i);
    free(data);

    return 0;
}

両方で結果は同じになります。この2つのプログラムの1番の違いは、メモリの確保の仕方の違いです。ただし、確保の仕方が違うだけで、同じサイズのメモリを利用しているので、同じ処理を行うプログラムを実現することができています。

こんな感じで malloc 関数を使えばポインタの変数宣言だけでいろんなプログラムを作成することができます(もちろん普通はこんな使い方はしないですが、malloc 関数のイメージが湧きやすくなると思いますので参考までに紹介させていただきました)。

その他の例

その他にも私のサイトでは malloc 関数を利用する例をたくさん公開しています。例えば下記のページなどでは malloc 関数を利用したソースコードを公開していますので、よろしければこちらも是非読んでみてください。

二分探索木解説ページのアイキャッチ C言語で二分探索木(木構造・ツリー構造)をプログラミング C言語でハフマン符号化 リスト構造の解説ページアイキャッチ 【C言語】リスト構造について分かりやすく解説【図解】

スポンサーリンク

動的確保のメリット・デメリット

次はメモリの動的確保(malloc)のメリットとデメリットについて解説していきたいと思います。基本的にグローバル変数や static 変数の宣言で行われる静的なメモリ確保(プログラム起動時に決まったサイズ分メモリを確保すること)と比較してのメリット・デメリットになります。

メモリの動的確保のメリット

ではまずはメリットを見ていきましょう。

ソースコード記述時にメモリ使用量を決める必要がない

ソースコード記述時にメモリ使用量を決める必要がないところがメリットの1つ目です。

前述したように、ファイル読み込み時にファイルのサイズが分からない時など、使用したいメモリ量がソースコード記述時には決まらないときに便利です。

動的確保を利用すれば、プログラム動作時に実際に必要なサイズを確定させ、その後にメモリを追加で確保することができますからね。

ソースコード記述時に無理にサイズを決めなくても良いので、ちょっとした検証用や実験を行う時は特に動的確保は向いていると思います。

ポインタについての知識が深まる

また、malloc 関数の戻り値はアドレスですので、使用するためにはポインタの知識が必須です。なので malloc 関数を使うことで自然とポインタの知識が深まり、ポインタにもすぐ慣れることができます。

ポインタと聞くと苦手意識を持つ方もおられるかもしれませんし、むしろデメリットと捉える方もいると思います。

ただC言語とポインタは切っても切れない関係ですし、そのポインタに慣れることができるのは、私としてはメリットだと考えています。

メモリの動的確保のデメリット

次はデメリットです。

確保したメモリの情報の管理が大変

動的確保したメモリは、ポインタ変数で先頭アドレスを覚えておかないと後から解放することができません。解放できないとメモリリークになります。

また、確保したメモリのサイズを超えてアクセスすると他のアプリのメモリを壊してしまう(もしくはエラーになる)可能性があります。メモリのサイズを超えてアクセスしないように制御する必要があります。

なので、動的確保する場合は、メモリの先頭アドレスやメモリのサイズは変数で保持しておき、それらを参照しながらプログラミングする必要があります。

例えばグローバル変数の配列であれば解放をする必要はありませんので、先頭アドレスを覚えておく必要もありません。

またサイズに関しても配列のサイズはソースコード記述時にもう決まっていますので、そのサイズを使用してプログラミングしてやれば良いだけです。わざわざ変数でサイズを保持しておく必然性はありません。

こういったアドレスやサイズを管理できるように変数を用意したり解放等を行う必要があるので、静的確保の場合に比べてソースコードが複雑になります。

タイミングによってメモリ確保の成功失敗が異なる

ここが一番のデメリットだと思います。

タイミングによってメモリ確保の結果が異なる可能性があります。

malloc 関数に成功するかどうかは、未使用のメモリがどれだけあるかによって決まります。

したがって、同じプログラムであっても、プログラムを実行するタイミングによっては malloc 関数の実行結果が異なるようなこともあり得ます。

例えば未使用のメモリが少ないようなタイミングで実行してしまうと、malloc 関数が失敗する可能性が高くなります。 

また、マルチスレッドを用いて並列処理を行うようなプログラムで、いろんなスレッドから malloc 関数が実行されていたりすると、タイミングによっては同時に大量のスレッドからメモリの動的確保が行われ、その時だけ malloc 関数に失敗するようなプログラムになってしまうようなこともあり得ます。

特にプログラムが複雑な場合、こういったタイミング依存で発生するエラーの原因調査や対策は大変です。

一方で、静的なメモリ確保のみを行う場合、プログラム起動時にメモリが全て確保されることになりますので、プログラム動作中にメモリが足りなくなるようなことはありません。

したがって、動的確保を行わないことで、タイミング依存のエラー要因を減らすことができます。

スポンサーリンク

動的確保したくない場合は…

デメリットを理解すると、動的確保したくないと言う方も出てくるかもしれません。

確かにタイミング依存でエラー発生するのは嫌だなぁ…

静的確保だけでプログラミングすることってできないの?

タイミング依存を嫌う人は多いよね

実は静的確保だけでプログラミングする方法はあるよ!

次はこの方法について解説していこう

ソースコード記述時にサイズが確定しない場合でも、静的確保だけでメモリを確保するにはどうすれば良いでしょうか?最後にこの点について解説していきたいと思います。

仕様でサイズの上限を決める

ソースコード記述時にサイズが確定しない場合は、「プログラムの仕様でサイズの上限を決めてしまう」ことでメモリの静的確保だけでプログラミングすることができます。

例えばファイルを読み込むようなプログラムの場合、「このプログラムでは1KBを超えるファイルは読み込めません」というように、読み込むファイルの上限を仕様として設定してしまえば良いです。

そして、ファイルのデータを格納する配列もサイズを1KBに設定して宣言してやれば良いです。

サイズに上限を設けた配列の宣言
char array[1024];

ファイルサイズが1KBを超えるデータはプログラムの仕様として受け付けませんので、これ以上メモリを確保する必要はなく、動的確保は必要ありません。

うーん、プログラムの動作に制限かけるってこと?

なんかイマイチだなぁ…

いや、動的確保にしても制限はあるんだ

メモリも無限にあるわけじゃないからね

タイミングによってその制限が変化していつエラーになるか分からないプログラムよりも、その制限を1つに定めてそれをユーザーに明示してあげた方がよっぽど便利だと思うけどね

確かに…

メモリも有限ですので、動的確保をしたからといってどんなサイズのデータも扱えると言うわけではないです。

結局は扱えるデータサイズに上限があります。厄介なのは動的確保の場合はこの上限がタイミングによって変わるところです。

ここで紹介している方法は、その上限を自分自身で一定サイズに決めてしまう方法であり、別に邪道な方法ではありません。

上限が一定サイズなのでユーザーにとってもわかりやすいプログラムを提供することができます。

ただし、この仕様はユーザーに明示し、この仕様を超える場合は、その旨をユーザーに伝えるようにしましょう。でないと、なぜプログラムが上手く動作してくれないか?どうすればプログラムをうまく動作させられるのか?がユーザーに伝わりませんからね。

例えば下記はファイルを読み込むプログラムで、「読み込めるファイルのサイズの上限は1KB」という仕様になっています。

サイズに上限を設けたファイル読み込み
#include <stdio.h>
#include <sys/stat.h>

/* 読み込むファイルの上限サイズ */
#define MAX_SIZE 1024

/* 読み込んだファイルのデータを格納する配列 */
char array[MAX_SIZE];
int main(void) {
    char file_name[] = "test.txt";
    FILE *fi;
    struct stat stat_data;
    size_t file_size;

    /* ファイルのサイズを取得 */
    if (stat(file_name, &stat_data) != 0) {
        printf("statに失敗しました\n");
        return -1;
    }

    file_size = stat_data.st_size;

    if (file_size > MAX_SIZE) {
        printf("1KBを超えるファイルは読み込めません\n");
        return -1;
    }

    fi = fopen(file_name, "r");
    if (fi == NULL) {
        printf("ファイルオープンエラー\n");
        return -1;
    }

    fread(array, file_size, 1, fi);

    fclose(fi);

    return 0;

}

このプログラムでは、仕様を超えるサイズのファイルを読み込もうとすると「1KBを超えるファイルは読み込めません」とエラーメッセージを表示するようにしています。

このエラーメッセージを読めば、ユーザーはプログラムがエラー終了する理由をすぐに理解することができます。さらに、プログラムを正常に実行するためにはファイルサイズを1KB以下にする必要があることもすぐ理解できます。

メモリが足りない場合に「メモリが足りません」とエラー表示される時よりも、ユーザーに具体的にエラーの原因や対処法が伝えられてますよね!?

結局ユーザーに使いやすいプログラムを提供するためには仕様を明確に決めることが必要ですし、その仕様を決めることで、ソースコード記述時にサイズが確定しない場合でも、静的確保のみでプログラムを実現することができます。

まとめ

このページでは malloc 関数について、特にメモリやメモリの確保を絡めて解説しました。

メモリについても一緒に学ぶことで、動的確保・malloc 関数についての理解も深められたのではないかと思います!

プログラムが動作するためにはメモリが必要です。

そして、プログラム起動時ではなく、プログラム起動後に追加でメモリを確保するのが動的確保です。

動的確保したメモリは基本的に「配列を指しているポインタ」と同様に扱えます。

動的確保したメモリは解放する(free 関数を実行する)必要がある点には注意しましょう!

静的確保だけでプログラムを作成するか、動的確保も行ってプログラムを作成するかは、メリットやデメリットを理解した上で使い分けると良いと思います!

オススメの参考書(PR)

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示