今回は《メモリの動的確保について学ぼう》というテーマで記事をまとめます。
今までは、使用する変数の数や配列の要素数が固定だった為、組めるプログラムに制限がありました。「メモリの動的確保」を習得すれば、プログラムの制限も大きく緩和されます。しっかり学んでいきましょう!
尚、「メモリの動的確保」はポインタと密接に関係してきます。ポインタの基本を習得していない方は、まずは下記の記事を参考に学習をしてみてください。
尚、現在執筆中のC言語入門記事は以下の人を対象としています。
- プログラミング未経験の人
- C言語プログラミングの学習をこれから始めたい人
- C言語プログラミングの学習を始めたばかりの人
メモリの動的確保について学ぼう
メモリの確保【静的確保と動的確保】
コンピュータ上のメモリは、使用する前にあらかじめ使うことを宣言する必要があります。これを「メモリの確保」と呼びます。
プログラム実行前から使用するメモリが決定されているものを「メモリの静的確保」と呼び、プログラム実行時に使用するメモリが決定されるものを「メモリの動的確保」と呼びます。
今までの使い方は前者の「メモリの静的確保」でした。わかりやすい例としては、配列の要素数などがそれに該当しますね。
/* プログラム実行前から要素数が決定している為、*/ /* 5個しか使用できない */ int array[5];
「メモリの動的確保」と「メモリの解放」
動的確保されたメモリは静的確保されたメモリとは異なり、使用後に必ず「メモリの解放」を行う必要があります。
「メモリの解放」とは、動的確保されたメモリを手放し、再利用できるようにすることです。「メモリの解放」を行わずにメモリを動的確保し続けると使用可能メモリの上限に達してしまい、動的確保が行えなくなります。
メモリの動的確保【malloc/calloc/realloc】
それでは、早速、「メモリの動的確保」の方法を解説していきたいと思います。
「メモリの動的確保」には専用の関数を呼び出す必要があります。C言語では次の3種類が実装されています。
- malloc関数
- calloc関数
- realloc関数
また、いずれのメモリ確保関数も引数にバイト数を指定する必要があります。
変数の型ごとにバイト数が異なることはご存じかと思いますが、実は変数のバイト数は『sizeof演算子』で取得することができます。
なので、まずは『sizeof演算子』を解説し、そのあとに3つのメモリ確保関数について解説をしたいと思います。
それでは早速、各関数の仕様を1つずつ解説していきます。但し、解説を読むだけではよくわからないと思いますので、解説を一度さらっと読んで役割を把握したら、サンプルコードの実行確認をしてみた後にもう一度解説を読んでみることをオススメします。
sizeof演算子
まずは、変数の型サイズを取得する「sizeof演算子」について解説します。メモリ確保関数を呼び出す際に必須となるものなので、しっかりと習得しておいてください。
【構文】
sizeof ( 変数の型、または変数名 )
【使用例①】
int size = 0;
size = sizeof( int ); /* int型の変数サイズ=4バイトを取得 */
【使用例②】
int size = 0;
double value = 0.0;
size = sizeof( value ); /* value変数のサイズ=double型の変数サイズ=8バイトを取得 */
malloc関数
次に、最も使用率が高いと思われる「malloc関数」について解説します。
【構文】
#include <stdlib.h>
void *malloc( size_t size );
【引数】
size:確保するバイト数
【戻り値】
成功時:確保されたメモリ領域の先頭ポインタ
失敗時:NULL
【解説】
引数sizeで指定したバイト数分のメモリを確保します。確保されたメモリは初期化されません。
malloc関数で確保したメモリは、使用後にfree関数で解放する必要があります。
【使用例】
int* pnum = NULL;
pnum = (int*)malloc( sizeof( int ) ); /* int型の変数サイズ=4バイトのメモリを確保 */
calloc関数
メモリの確保と同時に”ゼロ初期化”を行ってくれる「calloc関数」について解説します。
【構文】
#include <stdlib.h>
void *calloc( size_t number, size_t size );
【引数】
number:要素の数
size:各要素の長さ(バイト単位)
【戻り値】
成功時:確保されたメモリ領域の先頭ポインタ
失敗時:NULL
【解説】
引数sizeで指定したバイト数×引数numberで指定した数のメモリを確保します。確保されたメモリは0で初期化されます。
calloc関数で確保したメモリは、使用後にfree関数で解放する必要があります。
【使用例】
int* pnum = NULL;
pnum = (int*)calloc( 3, sizeof( int ) ); /* int型の変数サイズ×3=12バイトのメモリを確保(同時に0で初期化) */
realloc関数
「メモリを一度確保したはいいものの、次のプログラムを実行してみるとメモリが足りなくなった!」なんてことはザラにあります。
そんな時にメモリの再確保を行ってくれる「realloc関数」について解説します。
【構文】
#include <stdlib.h>
void *realloc( void *memblock, size_t size );
【引数】
memblock:再確保したいメモリのポインタ(malloc、calloc、reallocで確保されたメモリ領域のポインタ)
※NULL(0)を指定した場合、mallocと同様の動作
size:再確保するバイト数
【戻り値】
成功時:確保されたメモリ領域の先頭ポインタ
失敗時:NULL
【解説】
確保済みのメモリ領域を、引数sizeで指定したバイト数分のメモリで再確保します(メモリ領域のサイズを変更します)。引数memblockにNULL(0)を指定した場合、malloc関数と同様の動作を行います。
また、元のメモリ領域にあったデータは、可能な限り新しいメモリ領域にコピーされます(元のメモリ領域より大きいサイズに変更した場合は全てのデータがコピーされ、元のメモリ領域より小さいサイズに変更した場合は新しいメモリ領域の分だけコピーされます)。
realloc関数で確保したメモリは、使用後にfree関数で解放する必要があります。
また、メモリの再確保が成功した場合、引数memblockの指すメモリ領域は解放されます。失敗した場合、引数memblockの指すメモリ領域は解放されません。
【使用例】
int* pnum = NULL;
int* pnum2 = NULL;
pnum = (int*)calloc( 3, sizeof( int ) ); /* 12バイトのメモリを確保 */
pnum2 = (int*)realloc( pnum, sizeof( int ) * 10 ); /* 40バイトのメモリを再確保 */
if( pnum2 != NULL ){
/* 再確保できた場合はpnumにNULLを代入(解放済み扱いとする) */
pnum = NULL;
}
【補足】戻り値「void*」について
上記で紹介した3つのメモリ確保関数は、いずれも「void*」を戻り値として返す関数でしたね。よって、いずれの関数もキャストする必要があります。
「void*」やキャストについての詳細は<【独学C言語入門⑰】型変換について学ぼう【暗黙の型変換/明示的型変換(キャスト)】>をご参照ください。
メモリの解放【free】
ここでは、メモリの確保を行ってくれる「free関数」について解説します。
malloc関数、calloc関数、realloc関数で確保したメモリは必ず、この「free関数」で解放する必要があります。
【構文】
#include <stdlib.h>
void free( void *memblock );
【引数】
memblock:解放するメモリのポインタ
【戻り値】
なし
【解説】
malloc関数、calloc関数、realloc関数で確保したメモリを解放します。また、多くの場合でメモリ解放後にポインタにはNULLを代入します。
【使用例】
int* pnum = NULL;
pnum = (int*)calloc( 3, sizeof( int ) ); /* 12バイトのメモリを確保 */
/* (何かしらの処理) */
if( pnum != NULL ){
/* メモリ確保されている場合のみ解放する */
free( pnum );
pnum = NULL; /* 解放後はNULLを代入(解放済み扱いとする) */
}
サンプル
今回、malloc関数、calloc関数、realloc関数、free関数を使用して、『ユーザ入力に応じて、動的にメモリ確保を行う』プログラムのサンプルを用意しました。
【補足】サンプル実行の前に
サンプルを実行する前に、今回のサンプルで使用するユーザ入力用の関数『scanf関数』について解説します。
【構文】
#include <stdio.h>
int scanf( const char *format [, argument]... );
【引数】
format:書式指定文字列
argument:引数(省略可能)
【戻り値】
変換および代入された変数の数
※読み取られても代入されなかった変数は含まない
※戻り値が 0 の場合、代入された変数がなかったことを示す
【解説】
scanf関数は、標準入力ストリームからデータを読み取り、引数で指定された場所にデータを書き込みます。各引数は、型指定子に対応する型の変数へのポインタである必要があります。
【使用例】
int num = 0;
/* ユーザ入力が要求され、数値を入力してエンターキーを押すと */
/* 入力した数値がnumに代入される */
scanf( "%d", &num );
使い方は概ねprintf関数と同じです(考え方は逆ですが)。気をつけるポイントとしては『引数は必ずポインタ(変数のアドレス)であること』です。
サンプル実行
それでは、サンプルを用意したので実行してみてください。
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> void UpdateAndPrintArray( int* pArray, int num ); void main() { int i = 0; int* pArray_m = NULL; int* pArray_c = NULL; int* pArray_r = NULL; int num = 0; /* malloc関数 */ printf( "【malloc】確保する要素数を入力(入力後エンターキー)\n" ); scanf( "%d", &num ); pArray_m = (int*)malloc( sizeof(int) * num ); UpdateAndPrintArray( pArray_m, num ); /* calloc関数 */ printf( "【calloc】確保する要素数を入力(入力後エンターキー)\n" ); scanf( "%d", &num ); pArray_c = (int*)calloc( num, sizeof(int) ); UpdateAndPrintArray( pArray_c, num ); /* realloc関数(calloc関数で確保したメモリ領域を対象) */ printf( "【realloc】再確保する要素数を入力(入力後エンターキー)\n" ); printf( " ※calloc関数で確保したメモリ領域を対象\n" ); scanf( "%d", &num ); pArray_r = (int*)realloc( pArray_c, sizeof(int) * num ); if( pArray_r != NULL ){ /* 再確保でた場合はpArray_cにNULLを代入(解放されている為) */ pArray_c= NULL; } UpdateAndPrintArray( pArray_r, num ); /* 確保したメモリの解放 */ /* ※解放してNULLを代入(NULL代入は解放済み扱いとする為) */ if( pArray_m != NULL ){ free( pArray_m ); pArray_m = NULL; } if( pArray_c != NULL ){ free( pArray_c ); pArray_c= NULL; } if( pArray_r != NULL ){ free( pArray_r); pArray_c= NULL; } } void UpdateAndPrintArray( int* pArray, int num ) { int i = 0; /* データ入力前 表示 */ printf( "データ入力前 表示\n" ); for( i = 0; i < num; i++ ){ printf( "pArray[%d] = %d\n", i, pArray[i] ); } printf( "\n" ); /* 配列データ入力 */ for( i = 0; i < num; i++ ){ printf( "pArray[%d]に代入する値を入力(入力後エンターキー)\n", i ); scanf( "%d", &pArray[i] ); } printf( "\n" ); /* 結果表示 */ printf( "結果表示\n" ); for( i = 0; i < num; i++ ){ printf( "pArray[%d] = %d\n", i, pArray[i] ); } printf( "\n" ); }
次回は「複数 Cソースのコンパイル」
今回は「メモリの動的確保」について学んでもらいました。
次回は、「複数Cソースのコンパイル」について解説します。
今までは1つのCソースファイルに全てのプログラムコードを記述していました。しかし、ある程度大きい規模のプログラムを作成する場合、複数のファイルにプログラムコードを分割したほうが管理しやすくなりますね。その方法について解説します。
ご期待ください。