【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 個分記憶することができます。

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

メモリを表す図

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

例えば電卓アプリではユーザーが入力した数値をメモリに記憶して計算を実行したり、ウェブブラウザではダウンロードしたデータをメモリに記憶して動画を再生したりしています。

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

メモリの割り当て

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

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

データを破壊する様子

このようなことが起こらないように、オペレーティングシステム(OS)はアプリ毎に使用中のメモリ領域や使用されていないメモリ領域を管理しています。

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

アプリ起動時には、オペレーティングシステムが他のアプリに使用されていないメモリ領域からアプリが起動するのに必要なサイズ分のメモリを確保し、そのメモリをアプリに割り当ててから起動させるようになっています。

そしてアプリはその確保されたメモリを利用して動作します。ポイントは、自身のアプリ用に確保されたメモリしか使用してはいけない点です。

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

なので、他のアプリ用に確保されたメモリのデータを使用されそうになったらエラーが出るようになっています。これがプログラム実行時に良く見かける 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 関数の成功時の動作

そしてオペレーティングシステムが、このプログラム用にメモリを確保できた場合は、確保したメモリの先頭アドレスを戻り値として返却します。返却値の型は void* です。

mallocの返却値

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

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

ただし、この範囲を超えてメモリを利用しようとするとエラー(Segmentation fault)が発生することがあります。

MEMO

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

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

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

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

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

下記ページでも解説していますが、void* 型の変数はただのアドレスを格納するだけのものであり、アドレスの指す先のデータを参照したり、この型の変数に演算することは禁じられています。

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

一般的なポインタ同様に使用するためには、 malloc 関数の返却値をキャストする必要があります。

mallocの戻り値のキャスト
int *addr;
addr = (int*)malloc(4);

よく malloc 関数の使用例で上記のようにキャストを行っているものが見かけると思いますが、理由は前述の通り、malloc 関数の戻り値の型である void* 型ではアドレスの指す先のデータを参照したり、この型の変数に演算することが禁じられているためです。

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

malloc 関数の失敗時の動作

malloc 関数が失敗した時には NULL が返却されます。ですので、メモリの動的確保に成功したかどうかは、下記のように malloc 関数の戻り値を NULL かどうかを調べることで判断することができます。

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

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

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

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

どんな時に 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つ分のメモリが確保されることになります。

配列には要素を指定することで(array[1] など)、各要素のデータにアクセスすることができます。これと同様に、配列を指すポインタも同様に要素を指定することで配列の各要素のデータにアクセスすることができます。

要素指定によるアクセス
/* 要素を指定してアクセス */
addr[i] = i * 1024;

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

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

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

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

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

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

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

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

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

先ほどの配列を指すポインタと同じように扱うことができます。

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

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

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

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

    /* 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;
    }

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

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

    free(addr);

    return 0;
}

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

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

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

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

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

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

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

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

この宣言を受けてオペレーティングシステムは、そのアドレスのメモリが不要になったことを認識し、空きメモリとして扱います。これにより、次に他のアプリやプログラムからそのメモリが使用可能になります(オペレーティングシステムからそのメモリを確保して使用)。

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 関数を利用したソースコードを公開していますので、よろしければこちらも是非読んでみてください。

二分探索木解説ページのアイキャッチC言語で二分探索木(木構造・ツリー構造)をプログラミング C言語でハフマン符号化 リスト構造をC言語プログラムの実例を用いて解説

スポンサーリンク

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

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

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

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

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

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

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

後からプログラム動作時に実際に必要なサイズが確定してからメモリを追加で確保することができますからね。

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

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

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

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

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

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

次はデメリットです。

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

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

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

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

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

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

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

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

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

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

例えば、あるタイミングでは他のアプリがあまりメモリを使用していないので 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を超えるファイルは読み込めません」とエラーメッセージを表示するようにしています。

こんな感じで仕様を決めることで、ソースコード記述時にサイズが確定しない場合でも、静的確保のみでプログラムを実現することができます。

まとめ

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

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

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

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

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

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

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

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です