こんにちは、ナナです。
C言語においてデータを管理するメモリ領域には大きく分けて次の3つがあります。
この中の「ヒープメモリ」が、今回の記事の主役です。
ヒープメモリは「動的メモリ」とも呼ばれ、プログラム実行中にメモリの確保/解放を自由にプログラマーがコントロールできるメモリです。
本記事では次の悩みを解消する内容となっています。
では、ヒープメモリの確保方法を学んでいきましょう。
ヒープメモリを確保する標準ライブラリ関数
ヒープメモリの確保は標準ライブラリ関数を呼び出すことで行います。
ヒープメモリ確保の標準ライブラリ関数一覧
ヒープメモリを確保するための標準ライブラリ関数は3つあります。
#include <stdlib.h>
void * malloc(size_t size);
void * calloc(size_t num, size_t size);
void * realloc(void * mem, size_t size);
それぞれ使い方や特徴が異なるため、違いを知っておきましょう。
malloc関数は「マロック」や「エムアロック」と呼ばれます。
ヒープメモリの解放について
確保されたヒープメモリは、使い終わったタイミングで解放する必要があります。
#include <stdlib.h>
void free(void * mem);
free関数の引数には、確保関数で取得したポインタが必要となります。
解放するための標準ライブラリ関数である「free関数」を必ず覚えておきましょう。
ヒープメモリの解放を忘れてしまうと「解放漏れ」と呼ばれる問題が発生します。「解放漏れ」は漏れる量が大きくなるにつれて、新しくヒープメモリの確保ができなくなっていきます。
ヒープメモリに関して詳しく知りたい方は『動的メモリ【ヒープメモリの使い方と獲得する方法】』を見ておくとよいでしょう。
malloc関数を使ったヒープメモリの確保方法
それでは、まずは一番代表的なmalloc関数の使い方から紹介しましょう。
malloc関数は「memory(メモリ)」と「allocation(割り当て)」を組み合わせた名称になっています。
malloc関数の仕様
引数に確保したいヒープメモリサイズをバイト単位で指定します。
includeファイル | stdlib.h |
関数仕様 | void * malloc(size_t size); |
引数1 | 確保したいメモリサイズ |
戻り値 | 確保されたヒープメモリへのポインタ |
特記事項 | ヒープメモリが確保できなかった場合はNULLポインタを返却する |
戻り値の型が「void型ポインタ」になっていますね。
void型ポインタは、「メモリ番地はわかっているが、メモリを参照するためのデータ型がわからない」というポインタです。
このmalloc関数においては、「参照したいデータ型はプログラマー側が決めてください」という意味となります。
void型ポインタについてもっと知りたい方は『void型の意味と使い方【void型ポインタの扱い方も解説】』を見ておくとよいでしょう。
malloc関数を使ったサンプルコード
具体的なmalloc関数を使ったプログラムを紹介しましょう。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i;
// ヒープメモリの確保
long * pMem = (long *)malloc(sizeof(long) * 100);
if (pMem == NULL)
{
return 0;
}
// ヒープメモリの使用
for (i=0; i < 100; i++)
{
pMem[i] = i;
}
// ヒープメモリの解放
free(pMem);
return 0;
}
malloc関数で確保したヒープメモリは、使い終わったら必ずfree関数で解放します。
malloc関数の引数の指定方法
malloc関数の引数は確保するメモリサイズとなります。次の呼び出し部分に注目してみましょう。
long * pMem = (long *)malloc(sizeof(long) * 100);
malloc関数は「とあるデータ型のメモリを100個分」といった、配列的なメモリ領域を確保したいシーンでよく利用されます。
この場合は1つ当たりのメモリサイズをsizeof演算子で求め、個数を掛け算してあげるとプログラムが読みやすくなります。
もしも、次のように「400」を指定したとしても結果は同じですが、意図がよみづらくなります。
long * pMem = (long *)malloc(400);
もしも、1つ分のデータだけでよい場合は、sizeof演算子のサイズだけ指定すればよいです。
プログラムは他の人が読むことを前提として書くことを心がけましょう。結果的に自分にも優しいプログラムになります。
malloc関数の戻り値に使われるキャスト
malloc関数の戻り値は確保されたメモリ番地であるため、番地を保存するためのポインタ変数で受け取ることになります。
戻り値を受け取る際は、次のようにキャストを行います。
long * pMem = (long *)malloc(sizeof(long) * 100);
コンパイラにもよりますが、このキャストをしないと型違いによる警告やエラーが発生することがあります。
mallocの戻り値はNULLチェック
また、malloc関数はヒープメモリの確保に失敗した場合はNULLポインタを返却する仕様となっています。そのため、取得後にNULLチェックを行うのが一般的な作法です。
long * pMem = (long *)malloc(sizeof(long) * 100);
if (pMem == NULL)
{
return 0;
}
if文の中は適切なエラー処理が必要となります。本例ではreturnしているだけですが、場合によってはメッセージを出力したりする必要があるかもしれません。
calloc関数を使ったヒープメモリの確保方法
それでは、続いてcalloc関数の使い方を紹介しましょう。
calloc関数は「キャロック」や「シーアロック」と呼ばれ、malloc関数とよく似ています。
calloc関数の仕様
calloc関数は引数が2つ存在します。
includeファイル | stdlib.h |
関数仕様 | void * calloc(size_t num, size_t size); |
引数1 | 引数2で指定したサイズの確保個数 |
引数2 | 1つ当たりのデータサイズ |
戻り値 | 確保されたヒープメモリへのポインタ |
特記事項 | 確保されたヒープメモリは「0」の値でクリアされている。 ヒープメモリが確保できなかった場合はNULLポインタを返却する |
malloc関数との引数構成の違いは後程、解説しましょう。
calloc関数を使ったサンプルコード
具体的なcalloc関数を使ったプログラムを紹介しましょう。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i;
// ヒープメモリの確保
long * pMem = (long *)calloc(100, sizeof(long));
if (pMem == NULL)
{
return 0;
}
// ヒープメモリの使用
for (i = 0; i < 100; i++)
{
pMem[i] = i;
}
free(pMem);
return 0;
}
ほとんど、malloc関数を使ったサンプルと同じですね。
calloc関数とmalloc関数の引数の違い
calloc関数とmalloc関数は引数の構成が違います。ただし、本質的にはメモリサイズを指定していることに違いはありません。
第1引数と第2引数は共にsize_t型のデータであり、この2つの引数は入れ替えても確保サイズに違いはありません。
calloc関数とmalloc関数のヒープメモリの初期値の違い
calloc関数の特徴として、確保されたヒープメモリ領域は「0」の値でクリアされていることです。
実際にmalloc関数との違いを確かめてみましょう。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
long * pMMem; // malloc用
long * pCMem; // calloc用
pMMem = (long *)malloc(sizeof(long) * 3);
pCMem = (long *)calloc(3, sizeof(long));
// 確保領域の初期値表示
printf("malloc [0]:0x%08x, [1]:0x%08x, [2]:0x%08x\n", pMMem[0], pMMem[1], pMMem[2]);
printf("calloc [0]:0x%08x, [1]:0x%08x, [2]:0x%08x\n", pCMem[0], pCMem[1], pCMem[2]);
free(pMMem);
free(pCMem);
return 0;
}
malloc [0]:0xcdcdcdcd, [1]:0xcdcdcdcd, [2]:0xcdcdcdcd
calloc [0]:0x00000000, [1]:0x00000000, [2]:0x00000000
このように、calloc関数で取得したメモリは0クリアされているのがわかりますね。
malloc関数でも同じように値を0にしたい場合は、memset関数とセットで行うとよいでしょう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
long * pMem;
// ヒープメモリの確保と領域を0クリア
pMem = (long *)malloc(sizeof(long) * 3);
memset(pMem, 0, sizeof(long) * 3);
printf("malloc [0]:0x%08x, [1]:0x%08x, [2]:0x%08x\n", pMem[0], pMem[1], pMem[2]);
free(pMem);
return 0;
}
malloc [0]:0x00000000, [1]:0x00000000, [2]:0x00000000
メモリ領域は利用する前は「0クリア」しておくということを行うことがよくあります。その場合はcalloc関数なら自動でやってもらえるのが便利なところです。
realloc関数を使ったヒープメモリの拡張方法
それでは、最後に少し特殊なrealloc関数の使い方を紹介しましょう。
realloc関数は「リアロック」と呼ばれ、malloc関数やcalloc関数とは異なる役割があります。
realloc関数の仕様
realloc関数は引数が2つ存在します。
第1引数にmalloc関数やcalloc関数で取得したポインタを指定することで、そのサイズを変更することができます。
includeファイル | stdlib.h |
関数仕様 | void * realloc(void * mem, size_t size); |
引数1 | malloc、またはcalloc関数で取得したヒープメモリへのポインタ |
引数2 | 再度割り当て直したいヒープメモリサイズ |
戻り値 | 再確保されたヒープメモリへのポインタ |
特記事項 | メモリが再確保できなかった場合はNULLポインタを返却する。 再割り当てされた際に、ポインタは変わらない場合も変わる場合もある。 |
サイズは元のサイズに対して、大きくすることも小さくすることもできます。
realloc関数はその特性上、扱いに注意しないと問題を起こしやすい関数です。使用する場合は仕様をちゃんと把握しておきましょう!
realloc関数を使ったサンプルコード
具体的なrealloc関数を使ったプログラムを紹介しましょう。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i;
long * pTmp = NULL;
// ヒープメモリの確保
long * pMem = (long *)malloc(sizeof(long) * 100);
if (pMem == NULL)
{
return 0;
}
// ヒープメモリの使用
for (i = 0; i < 100; i++)
{
pMem[i] = i;
}
// ヒープメモリを500個分へ拡張
pTmp = realloc(pMem, sizeof(long) * 500);
if (pTmp == NULL)
{ // 再割り当て失敗
free(pMem);
return 0;
}
// ポインタを新しい番地に更新
pMem = pTmp;
// 拡張したヒープメモリの使用
for (i = 100; i < 500; i++)
{
pMem[i] = i;
}
free(pMem);
return 0;
}
このように、realloc関数はヒープメモリを再度割り当て直すことができます。
realloc関数のプログラムは少し複雑なプログラムになります。注意点を解説していきますよ!
reallocの注意点①:戻り値のポインタは一時退避せよ
realloc関数は再割り当てができない場合、NULLポインタを返却します。そのため次のようなプログラムは、もとのpMemのメモリ番地を消失する可能性があります。
pMem = realloc(pMem, sizeof(long) * 500);
if (pMem == NULL)
{
return 0;
}
ヒープメモリの番地を消失すると、free関数による解放ができなくなるため解放漏れが発生することになります。
そのため、次のように一時的なポインタ変数に退避した上で、再確保できたら上書き更新します。
pTmp = realloc(pMem, sizeof(long) * 500);
if (pTmp == NULL)
{
free(pMem); // 再割り当て失敗時は元の番地は有効なまま
return 0;
}
// ポインタを新しい番地に更新
pMem = pTmp;
再割り当てに失敗した場合は、元のヒープメモリの番地は変わらずに有効です。そのため必要に応じて解放処理が必要となります。
再割り当てが失敗することは、もちろん頻度は高くありません。しかし、関数仕様に従い万が一を心がけてプログラムするとよいでしょう。
reallocの注意点②:再割り当て時に元のヒープは解放不要
再割り当てが成功した場合は、元のヒープメモリ領域は解放不要です。
pTmp = realloc(pMem, sizeof(long) * 500);
if (pTmp == NULL)
{
free(pMem); // 再割り当て失敗時は元の番地は有効なまま
return 0;
}
// 元のヒープメモリは解放不要
// free(pMem)
// ポインタを新しい番地に更新
pMem = pTmp;
free(pMem)
旧メモリ番地を捨てて、新しい番地にそのまま更新して大丈夫です。
古いメモリ番地を解放しようとすると、例外が発生するので注意してください。
reallocの注意点③:再割り当て前のデータは新しいヒープ領域に自動コピー
メモリを新しい場所に再割り当てするということは、ヒープメモリの場所が変わるということを意味します。
この場合、元のヒープメモリ場所にすでに保存してあるデータはどうなるのかというと、新しいヒープメモリ場所に自動でコピーされます。
サイズを小さくした場合は、小さくなった部分のデータは消失するため注意しましょう。