6.17 動的なメモリ確保とポインタ
6.17.1 malloc を用いた動的なメモリ確保
malloc関数は,
#include <stdlib.h>
void *malloc(size_t size);
と定義される標準ライブラリ関数であり,size で指定されるバイト数のメモリをヒープ領域に確保して, そのメモリの先頭のアドレスをポインタとして返す. ここで,size t型とは,その処理系でのメモリ全体を 表すのに十分な符号なし整数を表す型であり, unsigned int またはunsigned long型と同じである89.
malloc関数で確保した領域が不必要になったときには,free関数でその領域を開放しなければならない.
Example 6.17.1 int型のオブジェクト100個分のメモリをヒープに確保し, 値を代入した後に, メモ リを開放する.
#include <stdio.h>
int main(int argc, char **argv) {
int *p ;
87mallocに類似の関数に関しては,mallocのオンライン・マニュアルを参照. callocのように初期化も同時に行ってくれる関 数もある.
88ヒープ領域とは,スタックセグメント内で,スタックと逆方法にメモリを利用する領域である. したがって,最悪の場合,スタッ クとヒープが重なって,メモリ不足が起きる可能性がある.
89正確には,整数型のいずれかにtypedefされている.
int i ;
if ((p = (int *)malloc(100*sizeof(int))) == NULL) { printf("Could not allocate memory!\n") ;
return -1 ; }
for(i=0;i<100;i++) *(p+i) = i ; free(p) ;
return 0 ; }
malloc関数は,メモリの確保に失敗すると NULLを返す.
Example 6.17.2 malloc関数で確保したメモリを開放しないため,メモリが確保できなくなる例.
#include <stdio.h>
void foo(void) {
int *p ;
if ((p = (int *)malloc(0x10000000)) == NULL) { printf("Could not allocate memory!\n") ; exit(-1) ;
}
printf("%p\n", p) ; return ;
}
int main(int argc, char **argv) {
int i ;
for(i=0;i<0x1000;i++) foo() ; return 0 ;
}
また,malloc関数で確保した領域が足らなくなった場合には,realloc関数で領域を追加確保もできる.
#include <stdio.h>
int main(int argc, char **argv) {
int i ; int *p ;
if ((p = (int *)malloc(100*sizeof(int))) == NULL) exit(-1) ; printf("%p\n", p) ;
if ((p = (int *)realloc(p, 200*sizeof(int))) == NULL) exit(-2) ; printf("%p\n", p) ;
free(p) ; return 0 ; }
Remark 6.17.1 なお,alloca関数はヒープ領域の代りにスタック領域で動的にメモリを確保するために 用いる. スタック領域でメモリを確保することと,ヒープ領域で確保することの違いは,ヒープ領域のメモ
リは,malloc関数(またはその類似物)を呼び出した関数の実行が終わっても,その領域は確保されたま
まであるが,alloca関数でスタック領域にメモリを確保すると,そのメモリが利用できるのは,呼び出した 関数の終了までの間に限られる. したがって,スタック領域の動的メモリの有効範囲が狭くなるが,一方で は,free によってメモリを開放する必要がないので,すなわち,関数の実行が終了すれば,スタック領域は 自動的に開放されるので,free を忘れることによる弊害を防ぐことが出来る.
動的に確保したヒープメモリを開放せずに使い続けて,メモリを大量消費している状態をメモリ・リーク
(memory leak)とよび,プログラムが正常に動作していない状態の一つである.
6.17.2 動的なメモリ確保を利用する場面
動的なメモリ確保を必要とする場面は各種考えられるが,ここでは,取りあえずいくつかの例を紹介して おこう.
6.17.2.1 標準入力からの文字列の読み込み
標準入力から文字列を読むには,fgets関数を用いるのがよい. 1行の最大の文字数が決まっているとき には,
#include <stdio.h>
#define MAX_LEN 1024
int main(int argc, char **argv) {
char str[MAX_LEN], *p ;
while((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL)) printf("%s", str) ;
return 0 ; }
とすればよい. MAX LENは, プリプロセッサ命令で1024 に置き換えられる. feof関数はファイルの終端 かどうかを判定する関数である. このプログラムを
#include <stdio.h>
#define MAX_LEN 1024
int main(int argc, char **argv) {
char str[MAX_LEN], *p ; while(!feof(stdin)) {
fgets(str, MAX_LEN, stdin) ; printf("%s", str) ;
}
return 0 ; }
とすると,最終行を2度表示することになる. (詳細はfgetsのオンライン・マニュアルを参照.)また,こ のプログラムをわかりやすくしたければ,
#include <stdio.h>
#define MAX_LEN 1024
int main(int argc, char **argv) {
char str[MAX_LEN], *p ; while(!feof(stdin)) {
if ((p = fgets(str, MAX_LEN, stdin)) != NULL) printf("%s", str) ;
}
return 0 ; }
としても良い. ここで,式
((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL))
では,&&(と||)は他の演算子と異なり, 優先順位と結合順序にしたがって, 式の評価と副作用の完了が 行われる. すなわち,feof(stdin)の返す値が0であれば,後半のfgets関数の呼出しは一切行われない.
この2つの式の順序を交換すると,標準入力がEOFに達しているにも関わらずfgets関数による読み出し が行われる. これは,実行時エラーを発生したり, 予期しない動作をする可能性がある.
このfgets関数の使い方では,MAX LENを越えた文字数を持つ行をstrに一度に格納することは出来な
い. 例えば,
#include <stdio.h>
#define MAX_LEN 10
int main(int argc, char **argv) {
char str[MAX_LEN], *p ;
while((!feof(stdin))&&((p = fgets(str, MAX_LEN, stdin)) != NULL)) printf("***%s", str) ;
return 0 ; }
として,実行すると何が起こっているのかがわかる. これでは,strに格納した1行を何かの関数に渡して 文字列処理をすることが出来ない. これを解決するために,動的なメモリ確保を行ってみよう.
#include <stdio.h>
#include <string.h>
#include <strings.h>
#define MAX_LEN 10
extern void error_jmp(void) ;
int main(int argc, char **argv) {
char *str, *temp_str, *p ;
size_t max = MAX_LEN ; /* その時点での文字列の最大の長さを格納する */
size_t len ; /* その時点での str の文字列の長さ */
if (((str = (char *)malloc(MAX_LEN+1)) == NULL)
||((temp_str = (char *)malloc(MAX_LEN+1)) == NULL)) error_jmp() ;
bzero((char *)str, MAX_LEN+1) ; /* str の内容をゼロクリアする */
len = strlen(str) ; /* 0 になっているはず */
/* 標準入力からの読み込み */
while((!feof(stdin))
&&((p = fgets(temp_str, MAX_LEN, stdin)) != NULL)) { /* 文字列の長さが足りない!
* 領域の再確保 */
if (max < len+strlen(temp_str)+1) {
if ((str = (char *)realloc(str, len+strlen(temp_str))) == NULL) error_jmp() ; max = len+strlen(temp_str)+1 ;
}
strcat(str,temp_str) ; /* 文字列の連結 */
len = strlen(str) ;
/* 改行文字が見つかったので, 文字列を表示する */
if (str[strlen(str)-1] == ’\n’) { printf("%s", str) ;
bzero(str, len+1) ; }
}
return 0 ; }
void error_jmp(void) {
printf("Could not allocate memory!\n") ; exit(-1) ;
return ; }
このプログラムでは, 改行文字を読むまでは, strに文字列を連結している. もし,strに確保した領域が 足らなくなった場合には, reallocで領域を再確保している. なお, bzeroは領域をゼロクリアする関数,
strlenは文字列の長さを返す関数である. strlenは文字列終端文字の先頭からのポインタのオフセット
を返すので, str[strlen(str)-1] は strの最後の文字を表すことになる. このプログラムでは, 一旦確 保した文字列領域は最後まで利用するので,free の呼出しの必要はない.
6.17.2.2 二重ポインタの指し示す先を確保する
Cでは,一般のサイズの多重配列を関数に渡す手段は存在しない. そのかわり,多重ポインタを渡すこと によって多重配列を渡すことに代えることが多い. 例えば,main関数のchar **argvという引数は,呼出 し側のシェルから複数の文字列を得るための手段として用いられている.
main 関数のchar **argv の場合には,それが指し示すメモリ領域はシェルからプログラムが起動され
る段階で確保されているが,プログラム内で二重ポインタを利用して複数の文字列を扱うためには,それら の文字列を格納する領域を動的に確保しなければならない.
ポインタのポインタを利用して,長さの異なる文字列を扱う例を考えてみよう. ここでは,標準入力から 入力されたテキストファイルの各行90を一つの文字列と思い, それらを二重ポインタで指し示す91.
#include <stdio.h>
#include <string.h>
#include <strings.h>
#define MAX_LEN 1024
extern void error_jmp(void) ; int main(int argc, char **argv) {
int i = 1 ; char *str, *p ; char **q ;
char temp_str[MAX_LEN] ;
/* q のための領域を取りあえず1行を指し示す分だけ確保 */
if ((q = (char **)malloc(sizeof(char *)*i)) == NULL) error_jmp() ; /* 1行分の文字列領域を確保 */
if ((str = (char *)malloc(MAX_LEN+1)) == NULL) error_jmp() ; while((!feof(stdin))
&&((p = fgets(temp_str, MAX_LEN, stdin)) != NULL)) { strcpy(str, temp_str) ;
*(q+i-1) = str ; i += 1 ;
/* q のための領域をさらに1行を指し示す分だけ確保 */
if ((q = (char **)realloc(q, sizeof(char *)*i)) == NULL) error_jmp() ; /* 次の1行分の文字列領域を確保 */
if ((str = (char *)malloc(MAX_LEN+1)) == NULL) error_jmp() ; }
/* ファイルの先頭から11行めを表示 */
printf("%s", q[10]) ; return 0 ;
}
90簡単のため,1行の最大文字数は1024としておこう.
91いささか人為的な例であることは仕方ない.
void error_jmp(void) {
printf("Could not allocate memory!\n") ; exit(-1) ;
return ; }
qは読み込んだ文字列を格納する領域を指し示すポインタの列で,1行を読み込むごとに,次の1行を格納 する領域と,それを指し示す領域を確保しながらqを構成している.
演習問題
Exercise 6.17.1 Section 6.17.2.2の例で, 1行の文字数を制限しなくても良いようにプログラムを書換 えよ.
Exercise 6.17.2 Section 6.17.2.2の例で,プログラム終了直前に,mallocで確保したすべての領域をfree で開放するようにプログラムを書換えよ.