メモリの容量には限度があり、使える最大量は決まっています。
これまでは、メモリの容量のことなど考えず、プログラムを作ってきました。
小規模なプログラムばかりですから、メモリのことはあまり考えなくても良かったのです。
しかし、大規模な開発となると、メモリの割り当てを考えずにプログラムを組むと大変なことになります。
メモリの「無駄」を省き、与えられたメモリの中で動作するプログラムを考える必要が出てくるのです。
とは言え、この講座ではそのような大きなプログラムは作りません。
もっと身近な例を挙げて説明します。
以前、すごろくゲームを作りましたね。
その時には、スタートからゴールまでの長さを30マスと固定して作りました。
例えば、このゲームを繰り返しプレイできるように変更したとします。
プレイする度にマスの長さ(配列の大きさ)を変えたいと思った場合、どのようなプログラムにすれば良いでしょうか?
これまでに学習した内容で対処しようとすると↓のようになります。
このゲームで使用するマスの最大の長さを設定する(例えば100とする) 最大の大きさの配列を用意する(int masu[100]のように) ゴールを設定する位置によって、その時の長さを調整する
これなら何とかなりそうです。
しかし、この場合「常時100マスの領域が確保」されています。
ゴールが30マス目であれば、残りの70マス程度は使っていない領域になります。
この使っていない領域が「無駄」な領域となります。
プレイする度に配列の領域が変更出来れば対応出来そうですが、
int size = 20; int masu[size]; |
というプログラムはエラーになるのです。
そこで「メモリ領域の確保と解放」の出番です。
具体的に言うと、malloc という標準関数を使います。
プログラムの実行中に、必要なサイズ(バイト単位)の領域を確保し、不要になったら解放することが出来るようになります。
ちなみに、この章のタイトルは「ポインタ(メモリ領域の確保と解放1)」となっています。
ポインタが何の関係があるかと言うと、領域の確保に成功すると確保した領域の先頭アドレスが返ってきます。
それをポインタ変数で受け取って、間接的に使用するのです。
では、具体例を1つ。
まずは最低限のプログラムを作り、手順などを説明します。
int型の領域を1つ確保し、解放するプログラムを作ってみます。
<sample program 166-01>
#include <stdlib.h> int main(void) { int *p; p = (int*)malloc(sizeof(int)); free(p); return 0; } |
malloc関数を使うには、<stdlib.h> のインクルードが必要です。
※<malloc.h> でも大丈夫です。
まず、ポインタ変数を1つ用意します。
次にmalloc関数を呼び出します。
p = (int*)malloc(sizeof(int)); |
malloc関数の引数はサイズ(バイト数)です。
変数のバイト数は「4」とか「10」とか直接数値を書いてはいけないと説明しました。
そのため、sizeof命令を使って指定します。
戻り値をint型のポインタでキャストしています。
malloc関数の戻り値の型は「void*」型です。
voidポインタのままでは、何型か分からないので、int型のポインタに型変換して受け取るのです。
※詳細は「voidポインタ」で説明します。
後はfree関数を呼び出している、
free(p); |
の部分です。
これは、使い終わったメモリ領域を解放する、という意味です。
ファイルと同じで開いたファイルは閉じるということです。
これは、非常に重要なことでfree関数が無ければメモリリークという危険な事が起こります。
※詳細は「メモリリーク」で説明します。
実はmalloc関数は、常に成功するとは限りません。
当然ながら確保したい領域がメモリに残っていなければ失敗します。
失敗したかどうかどうやって判断するかというと、ファイルの時と同じです。
malloc関数は失敗時に NULL (無効なポインタ)を返してきます。
そこで、失敗したかどうか判断するために、↓のようにコードを追加します。
<sample program 166-02>
#include <stdlib.h>
int main(void)
{
int *p;
p = (int*)malloc(sizeof(int));
if (p == NULL) {
return 1;
}
free(p);
return 0;
}
|
これでエラーチェックが出来るようになりました。
が、故意にエラーを発生させて確かめることは困難です。
最近のメモリは数ギガバイトもあり、数ギガバイトの領域を確保するということは現実的では無いのです。
もし、そのようなコードを書いたとしても、確保だけで数時間かかると思います。
とは言え、エラーの可能性を放置してはなりませんので、必ずエラーチェックコードは書きましょう。
では、確保した領域を使ってみましょう。
ポインタ変数がありますから、間接的に確保した領域を使う事が出来ます。
<sample program 166-03>
#include <stdio.h> #include <stdlib.h> int main(void) { int *p; p = (int*)malloc(sizeof(int)); if (p == NULL) { return 1; } *p = 12; printf("*p = %d\n", *p); free(p); return 0; } |
<実行結果>
*p = 12 続行するには何かキーを押してください・・・
確保した後は、通常のポインタ変数と同じように使う事が出来ます。
もう1つ危険な行為について書きます。
解放後のメモリにアクセスするとどうなるか試してみます。
<sample program 166-04>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p;
p = (int*)malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 12;
printf("*p = %d\n", *p);
free(p);
*p = 15;
printf("*p = %d\n", *p);
return 0;
}
|
<実行結果>
*p = 12 *p = 15 続行するには何かキーを押してください・・・
解放したはずのメモリに数値が代入出来ました。
解放後のメモリは、誰が使っても良い状態になっています。
もしかすると、他のプログラムが使っているかも知れません。
そのメモリに対して代入を行うことが出来るのです。
これは、絶対にやってはならないことであり「ダングリングポインタ」と呼ばれる問題点なのです。
対応策は、
解放したメモリを指すポインタ変数には NULL を入れておく
です。
コードにすると、↓のようになります。
<sample program 166-05>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p;
p = (int*)malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 12;
printf("*p = %d\n", *p);
free(p);
p = NULL;
*p = 15;
printf("*p = %d\n", *p);
return 0;
}
|
<実行結果>
NULL は「無効なポインタ」です。
NULL を入れておくことで、解放後のアドレスを消し、ダングリングポインタを解消します。
↓のようなプログラムも、セーフリリースという名前でよく見かけます。
<sample program 166-06>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p;
p = (int*)malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 12;
printf("*p = %d\n", *p);
if (p) {
free(p);
p = NULL;
}
*p = 15;
printf("*p = %d\n", *p);
return 0;
}
|
malloc関数は失敗した場合、NULLを返します。
if (p) |
とは、ポインタ変数pの中身がNULL(0)で無い場合、という意味です。
NULL で無いということは、メモリの確保が成功し、何らかのアドレスが入っているということです。
メモリの確保が出来た時だけ、メモリの解放を行い、ダングリングポインタ対策を施しているのです。
これが「セーフ(安全な)リリース(解放)」と呼ばれるコードです。
今回は、malloc関数とfree関数の使い方だけを説明しました。
int型変数1つだけの領域を確保することはまずありません。
次回は配列の確保をやりましょう。