配列を自由自在に作る
配列の欠点
第13章では配列の使い方を説明しました。
この配列は、多量のデータの取り扱いに非常に有効な手段なのですが、
実はいくつかの欠点があり、いささか実用性が低いのです。
配列の最大の欠点は、
要素数をプログラム中で変更出来ないことです。
配列を宣言する時に、要素数を定数で直接指定するしかありません。
実行中にユーザーに入力してもらい、その値を利用するようなことはできません。
GCCというコンパイラでは独自の拡張により、
要素数をプログラム中で変更できるようになっています。
また、C99というより新しいC言語でも同様の機能が追加されています。
このことは、さまざまな目的で動作するプログラムを作るのに不便です。
たとえば、会社の社員の給料を管理するソフトを作る場合、
社員の給料を記憶する配列は社員人数分以上必要になります。
ところが、世の中には社員数人の会社から、数千人の会社までさまざまです。
もし、要素数を10個にすると、11人以上の会社では使えませんし、
逆に、要素数を1万にすると、10人の会社では残りの9990個がムダです。
そのムダな分にもメモリを使用するので、
巨大なメモリのムダとなります。
この様に、配列の要素数は自由に変更することが出来ないため、
メモリを有効利用するのが難しく、実用性に欠けているのです。
メモリの確保
前項では、配列は自由に要素数を変更出来ないため、不便であると説明しました。
その為、自由に配列を作る
malloc(エムアロック)関数が用意されています。
なお、malloc関数を使うには、<stdlib.h> を #include する必要があります。
malloc関数の使い方は、次の通りです。
ポインタ変数 = malloc(必要なメモリのバイトサイズ);
返されるポインタ変数には、確保された配列の先頭アドレスが代入されるので、
これに[]演算子を使用すれば、配列と同様に使うことができます。
もし、上の3行の意味がわからない人は、15章を再読してみましょう。
malloc関数で指定できるのは、バイト単位のサイズなので、
任意の要素数の配列を確保するには、sizeof演算子を使用します。
なお、malloc関数で確保したメモリを、ヒープと呼ぶことがあります。
また、ヒープに確保された配列を、
動的配列と呼ぶことがあります。
なお(深刻なメモリ不足などが原因で)メモリ確保に失敗するとNULLが返されます。
これをそのまま使用すると強制終了してしまうので、
malloc関数の戻り値は必ずチェックする必要があります。
・・ですが、深刻なメモリ不足が発生している状況で、これといった対策はありません。
アプリにもよりますが、NULLが返ってきてしまった場合は、諦めて強制終了するしかないかなとは思います。
【ヒープ】
長期的に使用される大きなサイズのメモリを格納する領域。
【動的配列】
malloc関数などを使用して、
プログラムの実行中に用意された任意のサイズの配列。
malloc関数によって確保されたメモリは、プログラムが終了するまで残りますが、
そのメモリが不要になった場合、
free(フリー)関数を使って解放します。
これを忘れると、ムダなメモリが残り続けることになるため、
malloc関数を使ったら、
必ずfree関数を呼び出します。
free関数の使い方は次の通りです。
free関数は必ず呼び出すと説明しましたが、例外もあります。
プログラムが終了する直前では、free関数を使わなくても、
プログラム終了と同時にOSがメモリを解放します。
しかし、free関数 は常に呼び出す癖をつけておいてください。
ポインタ変数には、malloc関数の戻り値を格納したポインタ変数を指定します。
次のプログラムは、int型の要素数10個の配列を動的に確保します。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i;
int* heap;
heap = (int*)malloc(sizeof(int) * 10);
if (heap == NULL) exit(0);
for (i = 0; i < 10; i++) {
heap[i] = i;
}
printf("%d\n", heap[5]);
free(heap);
return 0;
}
sizeof(int)によって、int型変数1つのバイト単位のサイズが求められるので、
それを10倍することで、int型変数10個分のメモリを確保しています。
malloc関数が返すアドレスは、void型のポインタです。
この型は、どんなポインタ変数にも代入できるという型なので、
本当は(int *)にキャストする必要はないのですが、
C++コンパイラではキャストしないとエラーがでます。
メモリ確保に失敗した場合は
exit関数を呼び出して強制終了します。
exit関数はプログラムを終了させる関数です。
なお、exit関数を使うには、<stdlib.h> を #include する必要があります。
ちなみに、エラーによる強制終了の時はabort関数を使うこともあります。
確保した配列を使い終わったら、free関数を呼び出して解放します。
malloc関数は、好きなように好きなサイズの動的配列を作ることができるので、
非常に便利ですが、実は、その仕組みは、メモリにマークをつけているだけです。
これは、冷蔵庫に入っているお菓子に名前を書いておくのと同じことで、
家族みんながその名前に従い、他人のお菓子を食べなければ問題はありませんが、
勘違いによって他の人にお菓子が食べられてしまう可能性は十分あります。
malloc関数にも似たような性質があり、うまく使うのは意外に難しいのです。
したがって、プログラムの時は、できる限り普通の配列を使うようにして、
どうしても必要な部分だけmalloc関数を使うようにした方が良いでしょう。
動的配列の要素数を拡大する
malloc関数によって、好きな要素数の動的配列を作ることができます。
しかし、これでは、最初に述べた、配列の要素数を変更出来ない、
という問題は、完全に解決されたとは言いません。
そこで、要素数を変更する、
realloc(リアロック)関数が用意されています。
realloc関数の使い方は、次の通りです。
新しいポインタ変数 = realloc(以前のポインタ変数, 必要なメモリのバイトサイズ);
以前のポインタ変数には、malloc関数で確保したメモリのアドレスを指定します。
realloc関数は、中身を維持したまま、新しいサイズのメモリを確保します。
新しいポインタ変数には、拡張されたメモリのアドレスが返されますが、
特別な理由がなければ、以前のポインタ変数と同じ変数を指定できます。
次のプログラムは、realloc関数で動的配列の要素数を変更します。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int* heap;
heap = (int*)malloc(sizeof(int) * 10);
heap = (int*)realloc(heap, sizeof(int) * 100);
free(heap);
return 0;
}
realloc関数で、要素数を10個から100個に増加しています。
realloc関数を何回も呼び出すとメモリが散らかってきます。
この様な状態をフラグメンテーションと呼び、不安定になります。
初めのmalloc関数である程度大きめに確保しておき、
realloc関数を呼び出す場合も、一回で大きめに確保するべきです。
メモリリークとの戦い
実のところ、malloc関数について、説明することはこれしかありません。
malloc関数で必要な大きさの配列を作り、不要になったらfree関数で解放する、たったこれだけです。
しかし、これこそが、C言語最大の問題でもあるのです。
動的メモリ(malloc関数で作成した配列)が不要になったときに、free関数でメモリを解放することを忘れると、
そのメモリは使われもしないままずっと残り続けることになり、いわゆる
メモリリークとなってしまいます。
皆さんも、長時間コンピュータを使っているうちに、
だんだん動作が遅くなってきて、再起動することになる、という経験があると思います。
実は、あの現象の原因が、まさにfree関数の呼び出し忘れなのです。
動的メモリの開放を忘れてしまうことをメモリリークと呼びます。
メモリリークという単語は読者の方も聞いたことがあるでしょう。
また、パソコンやスマホを再起動せずに長時間使っていると、だんだん遅くなってくる、
という現象は、皆さんも経験したことがあると思われます。
これが、動的メモリの解放忘れ、すなわち、free関数の呼び出し忘れなのです。
今回紹介したようなシンプルな例であれば、free関数の呼び出しを忘れないことは簡単です。
しかし、大規模や超大規模なプログラムでは、一転して、悪夢のように大変なことになります。
使われているメモリと、もう不要になったメモリを区別することは、極めて困難なのです。
事実上、C言語やその拡張版のC++言語ではこの問題をスマートに解決することはできておらず、
膨大かつ徹底したテストによって、なんとか対策しているのが現状です。
そのため、Java言語など、ガベージコレクションという、
自動的にメモリを解放する機能をそなえたプログラミング言語が登場し、幅広く使用されています。
使用されているメモリを自動的に監視することで、メモリリークを解決する機能。
ほとんどのメモリリークを自動的に解決してくれるため、
現代のアプリ開発では、この機能をそなえた言語を使用することが多くなりました。
ただし、メモリの利用効率上のムダが多くなってしまう弱点も抱えています
しかし、ガベージコレクションはどうしてもムダが多くなってしまうことから、
Rust言語という、メモリの自動解放と、メモリの効率利用を両立できる言語も登場しました。
Rust言語は、今もっとも期待されているプログラミング言語でもあります。
所有権システムにより、メモリリークに対して、根本的な対策を行った言語。
とても雑に言えば「メモリリークするプログラムは、コンパイルエラーになる」言語。
メモリの利用効率をまったくムダにすることなく、ほぼ完全なメモリリーク対策ができるが、
ちょっとしたミスでもコンパイルできなくなってしまうため、
プログラムを記述するのがかなり疲れるプログラミング言語です。
しかし、メモリリーク問題に対して、現時点でもっとも優れた言語には間違いありません。
今後のOSやシステムアプリは、Rust言語で開発されるものが多くなることでしょう。