C言語 動的メモリ【ヒープメモリの使い方と獲得する方法】

C言語
この記事は約13分で読めます。

こんにちは、ナナです。

4つ目のメモリの種類、最後のトリは「動的メモリ」です。

「動的メモリ」を自由に扱えるようになると、プログラム実行中に大きなメモリを一時的に確保できます。

実践でも比較的利用する機会が多く、扱いを間違えやすいメモリでもあるため注意が必要です。

本記事では次の疑問点を解消する内容となっています。

本記事で学習できること
  • 動的メモリの特徴とコンセプトとは?
  • malloc関数を使った動的メモリの確保方法とは?
  • malloc関数を扱う時に持つべきイメージとは?
  • free関数を使った動的メモリの返却方法とは?
  • 動的メモリを使う時の注意すべきこととは?

では、動的メモリの使い方を学んでいきましょう。

スポンサー

動的メモリ(ヒープメモリ)の特徴を知ろう

はい、はーい。今日のテーマは「動的メモリ」ですね!

えっ、動的?メモリって動くんですか?歩いてるところ見てみたいでーすっ!

ナナ
ナナ

「動的」っていうのは動くって意味じゃないんだよ。そのあたりも含めて解説しようね。

「動的メモリ」は、別名「ヒープメモリ」とも呼ばれるメモリのことです。

まずは、「動的」という意味を学びましょう。

動的メモリの特徴

動的メモリは、他のメモリと比べて次の特徴を持ちます。

種類配置データRead/Write容量説明
プログラムメモリ関数・定数R大サイズ関数や定数が配置
スタックメモリ変数R/W小サイズ関数内で定義した変数が配置
静的メモリ変数R/W中サイズ関数外で定義した変数が配置
動的メモリ変数R/W大サイズメモリ確保関数で取得

「動的メモリ」はメモリを確保する方法として、メモリ確保関数を利用することが大きな特徴です。

「静的」と「動的」の違い

グローバル変数が配置されるのが静的メモリでしたが、「静的」「動的」という言葉はプログラミングの世界でよく使用される言葉です。

静的プログラム実行前にすでに決まっていること。
動的プログラム実行中に決まること。

つまり、動的メモリとは

プログラム実行中に、使用サイズを変えられるメモリ

のことです。

ナナ
ナナ

「ヒープメモリ」という言葉はよく使われます。それは「動的メモリ」と同じ意味なんです。覚えておくとよいでしょう。

動的メモリのコンセプトとは

例えば、音楽データAと音楽データBがあるとします。

再生プレーヤーでは、音楽データをメモリに読み込んでから再生する必要があります。ただ、音楽といっても長い曲もあれば短い曲もあります。

音楽データ

音楽プレーヤーのプログラムではメモリをどの程度確保しておけばよいでしょうか?

大きなサイズ側に合わせて、5Mバイトのメモリをグローバル変数に確保しておくのはひとつの手でしょう。

しかし、音楽データAを再生する時には4Mバイトは使わないメモリなのにもったいないですね。他にも10Mバイトの音楽データもあるかもしれません…。

メモリとは完全予約制の有限の資源なのです。使わないのに無駄に確保してあると、メモリを使いたい人が使えないのです。

このような場面で使われるのが「ヒープメモリ」です。

ヒープメモリのコンセプト

「使いたい時に使いたい分だけメモリを確保できる!」

ヒープメモリを利用することで、いざ使いたい時に必要な分だけメモリを使うことができるのです。

ナナ
ナナ

ヒープメモリは、プログラムの実行中に確保するメモリサイズを変化させることができるんです!

スポンサー

malloc関数を使った動的メモリの確保方法とは

ほい、ほーい。プログラムを動かしている時にメモリを利用するって「変数定義」でいつもやってますよー。同じじゃないですか?

ナナ
ナナ

動的メモリは、今までの変数定義とは全く違うアプローチでメモリを確保するんだよ。これが大きな違いだね。

皆さんはここまで「変数定義」をすることで、利用するメモリを確保してきました。

しかし、ヒープメモリの確保は全く異なる方法で行います。ヒープメモリはライブラリ関数を使用して確保を行います。

ヒープメモリを利用する上で、代表的なライブラリ関数が次の2つです。

ヒープメモリの確保・解放関数説明
void * malloc(size_t size);確保したいヒープメモリのサイズを引数で指定する。
戻り値は確保されたヒープメモリの番地が返却。
「マロック」「エムアロック」と呼ばれる。
void free(void * ptr);malloc関数で確保したメモリを解放する。
引数にはmalloc関数で取得したメモリ番地を指定する。

これらの関数を呼び出すためには「stdlib.h」のヘッダインクルードが必要になります。

malloc関数を使ったヒープメモリの獲得方法

では、プログラムで具体的にヒープメモリの獲得方法を示しましょう。

#include <stdio.h>

// malloc,free関数を呼ぶためにインクルード
#include <stdlib.h>

enum
{
    E_MUSIC_A, //  音楽A
    E_MUSIC_B, //  音楽B
};

void music(int musicKind)
{
    // 確保したメモリへのポインタ
    char *  pMusic = NULL;
    size_t  musicSize = 0;

    if (musicKind == E_MUSIC_A)
    {
        // 1MByte
        musicSize = 1024 * 1024;
    }
    else
    {
        // 5MByte
        musicSize = 5 * 1024 * 1024;
    }

    // ヒープメモリ
    // 指定サイズ分のメモリ確保
    pMusic = (char *)malloc(musicSize);

    // 音楽の読み込みと再生処理・・・
    // readAndPlay(pMusic);

    // 使用後は不要なメモリを解放
    free(pMusic);
    pMusic = NULL;

    return;
}

int main(void)
{
    // 音楽Aを再生
    music(E_MUSIC_A);

    return 0;
}

一部、音楽データの読み込みと再生は省略してありますが、このプログラムはビルド可能なプログラムです。

確保されたヒープメモリは、malloc関数の戻り値によりメモリ番地が返却されます。そのため、ポインタ変数で戻り値を受け取ることになります。

ナナ
ナナ

このように「ヒープメモリ」は変数定義で確保するのではなく、malloc関数に対してメモリ申請を行う形で確保を行います。

malloc関数のサービスイメージ

本サイトでは、関数とはサービスであると解説しています。

malloc関数とは

ヒープメモリを貸し付けるサービスを提供している

といえます。

イメージとしては、次のようなものです。

メモリの貸付

malloc関数にて確保されたヒープメモリは、プログラマーの明示的な解放処理がない限り、確保された状態が維持されます。

ローカル変数のように、関数の呼び出しが終わったからといって使えなくなるということはありません。

ナナ
ナナ

獲得したヒープメモリは関数処理が終わっても、確保された状態が継続されます。これがローカル変数とは根本的に使い方が異なる点です。

スポンサー

free関数を使った動的メモリの返却方法

ほーい。「動的メモリ」ってお金を借りるのと似てるんですね。

僕も奨学金をもらって学校に通ってるんです。動的メモリが身近なヤツに見えてきましたー。

ナナ
ナナ

奨学金はあくまでも借りているだけだからね。返さなきゃいけないことに気を付けようね。

「動的メモリ」も使い終わったら、ちゃんと返さないといけないんだよ。

malloc関数で確保したメモリは、使い終わったら次のようにfree関数で解放処理を行う必要があります。

pMusic = (char *)malloc(musicSize);

// 使用後は不要なメモリを解放
free(pMusic);

free関数の引数に渡すのは、malloc関数で取得したメモリ番地です。

malloc関数で取得した番地こそが、「メモリを返却するためのキー」となっているのです。そのため、この番地情報は返却するまで決して紛失してはいけません。

ヒープメモリの返却処理をすることで、そのメモリはリサイクルされます。つまり、次にmalloc関数が呼ばれたときに、再度貸し出しできるようになるのです。

ナナ
ナナ

この番地情報をメモリ返却前に紛失した場合、二度とそのメモリを返却することはできなくなります。ちゃんと管理しましょうね!

動的メモリを使う時に一番注意すべきこと

動的メモリを利用する際の一番注意することは、メモリが自動的に返却されないことです。つまり、メモリの返却忘れが発生しうるということです。

動的メモリの返却忘れのことを「メモリリーク(メモリの解放漏れ)」と呼びます。

返済取り立て

メモリリークはシステムにボディーブローのようにじわじわ効いてきます。

どこかのプログラムで返却漏れが何度も発生するとメモリの在庫がなくなっていき、しだいにmalloc関数はヒープメモリの貸し付けができなくなります。

そうするとシステムが徐々に重くなる、動かなくなるといった現象が発生します。

ナナ
ナナ

実際にメモリリークを起こしたために苦労した開発者を、私はたくさん見てきました。

意図せずとも借りたメモリを返さないことは、開発者としてやってはならないのです。

スポンサー

Q&A:動的メモリに関するよくある質問

ナナ
ナナ

動的メモリの質問、こいこい!

Q: malloc関数の戻り値の型が「voidポインタ型」になっているけどなぜ?

ほーーーい。malloc関数って戻り値の型が変わった型になっています。これはなんでしょうか?

ナナ
ナナ

これは「voidポインタ型」と呼ばれるポインタの一種だね。この型は明確なデータ型を持たない、番地のみを表すポインタ型のことだね。

malloc関数のプロトタイプ宣言は、次のものになっています。確かに、戻り値の型が「voidポインタ型」になっていますね。

void * malloc(size_t size);

malloc関数は動的メモリの確保をお願いするサービスを提供します。

引数には確保したいメモリのサイズを指定しますが、よく見るとデータ型に関する情報が一切含まれていないことに気づきます。

voidポインタ型とは、参照先のデータ型を持たない「場所を示すだけのためのポインタ」です。

つまり、malloc関数が提供するのは確保したメモリの範囲であり「データ型に関しては利用する側で自由に決定してください」というスタンスを取っているわけです。

そのため、malloc関数の戻り値であるメモリ番地を受け取る際には、対象メモリを扱うためのポインタ型へ明示的キャストが必要になります。

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

int main(void)
{
    short  * p1 = NULL;
    double * p2 = NULL;

    // short 型[100]分の配列
    p1 = (short *)malloc(sizeof(short) * 100);

    // double型[100]分の配列
    p2 = (double *)malloc(sizeof(double) * 100);

    return 0;
}

malloc関数で配列用のメモリを確保する際の引数には、確保したいデータ型サイズに配列要素数を掛け算する方法はよく利用されます。

この書き方は覚えておくとよいでしょう。

Q:malloc関数で確保したメモリをfree関数で返却せずにプログラムを終了しちゃいました…

ほ~い。あのー、確保したメモリを返すのついつい忘れてしまいました…。出来心だったんですっ!でも、もうプログラムが終了しちゃったんです~っ。

どどど、どうしたらいいんですか?もう番地がなくなっちゃって、メモリを返せないじゃないですかー。取り立て屋が僕を脅しにくるんですか???ギャー、イヤー、誰か助けて-!

ナナ
ナナ

あちゃー、取り立て屋の人が君を後ろからのぞいているよー。

っていうのは冗談で、大丈夫だよ。WindowsといったOSの上で動いているアプリケーションならという前提はあるけどね。

次のようにmalloc関数で確保したメモリの返却を忘れたケースですね。

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

int main(void)
{
    short * pData = NULL;

    pData = (short *)malloc(sizeof(short) * 10);

    // freeを忘れてmain関数を終了しちゃった
    return 0;
}

安心してください。

Windowsプログラムにおいて、malloc関数で確保したメモリを返却せずに終了した場合、自動的にメモリは返却されたことになります。

ナナ
ナナ

組み込み開発のアプリケーションは、製品の電源が切られるまで終了しないため、ヒープメモリが自動で返却されることがありません。

そのため「メモリリーク」に対して、非常にシビアに捉えています。

スポンサー

課題:動的メモリを学べたかを確認しよう

課題1

課題内容

動的メモリ上にlong型の領域を1000個分確保し、先頭から順に0~999の値を格納せよ。

正しく格納されたことを確認するために、990~999番目の領域のメモリ内容を出力期待結果に従い表示させよ。

プログラムは次のものをベースに作成するものとする。

main.c

#include <stdio.h>

int main(void)
{


    //  long型の1000個分の動的メモリを確保する


    //  [0]~[999]の動的メモリに0~999の値を順に設定する


    //  [990]~[999]のメモリ内容を画面に表示する


    //  動的メモリを解放する


    return 0;
}

出力期待結果

[990] = 990
[991] = 991
[992] = 992
[993] = 993
[994] = 994
[995] = 995
[996] = 996
[997] = 997
[998] = 998
[999] = 999

main.c

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

#define     D_ARRAY_NUM     (1000)

int main(void)
{
    long * pArray = NULL;
    int i;

    //  long型の1000個分の動的メモリを確保する
    pArray = (long *)malloc(sizeof(long) * D_ARRAY_NUM);
    if (pArray == NULL)
    {
        printf("ERROR");
        return 0;
    }

    //  [0]~[999]の動的メモリに0~999の値を順に設定する
    for (i = 0 ; i < D_ARRAY_NUM ; i++)
    {
        pArray[i] = i;
    }

    //  [990]~[999]のメモリ内容を画面に表示する
    for (i = D_ARRAY_NUM - 10; i < D_ARRAY_NUM ; i++)
    {
        printf("[%d] = %d\n", i, pArray[i]);
    }

    //  動的メモリを解放する
    free(pArray);
    pArray = NULL;

    return 0;
}
ナナ
ナナ

malloc関数ではsizeofを使ってメモリを確保していますね。最後は忘れずにfree関数による返却処理をしましょう。

ポインタを使ってヒープメモリは使うことになるため、ポインタ機能はしっかりと使えるようになっておきましょう!