C プログラミング入門 基幹 7 ( 水 5) 1 11: 動的メモリ確保 Linux にログインし 以下の講義ページを開いておくこと http://www-it.sci.waseda.ac.jp/teachers/w48369 2/CPR1/ 2017-06-28
まとめ : ポインタを使った処理 2 内容呼び出し元の変数を書き換える文字列を渡す 配列を渡すファイルポインタ複数の値を返す大きな領域を確保する 説明第 9 回第 10 回第 10 回今回今回
ポインタの扱い 3
複数の値を返す 4 return は 1 つの値しか返せない 複数返すには 書きこんでもらいたい変数へのポインタを渡す 返り値を使って計算 例 :int 配列の最大 最小値を計算する int maximum(const int *a, int n); int minimum(const int *a, int n); 整数の配列の先頭ポインタとその個数 void minmax(const int *a, int n, int *m, int *M); 返り値はなし 最小値を書き込むアドレス ( 変数などへのポインタ ) 書き換えるので const がつかない 最大値を書き込むアドレス ( 変数などへのポインタ )
標準ライブラリ関数の例 5 整数部と小数部を求める関数 double modf(double value, double *iptr); 戻り値 :value の小数部 ( 符号付き ) iptr の指すメモリ領域 : 整数部 ( 符号付き ) #include <stdio.h> #include <math.h> double ipart double fpart int main(void) double ipart, fpart; fpart = modf(32.5, &ipart); 小数部 0.5 は戻り値として返る 32 0.5 変数 ipart のアドレスを渡すことで 関数 modf は ipart の中身を書き換えることができる
NULL ポインタ 6
ポインタ変数の初期値の注意 7 通常の変数同様 ポインタ変数も初期化されない しかし どこかは指している 初期化しないポインタ変数でアクセスすると危険 int *p; // 初期化なし *p = 100; 暗黙の初期化はされないので どこを指すアドレスが入っているかは不明 p
NULL ポインタによる安全策 8 以下の場合 null ポインタを入れるとよい 初期化ではアドレスを決定しない 今まで使っていたアドレス無効になった null ポインタを通したアクセスは発見しやすい ポインタ変数の値 ( アドレス ) が null ポインタの場合 斜線 int *p = NULL; を引いて どこも指示していないことを表現することがおおい *p = 100; p null ポインタが指す領域にアクセスすると システムが検知して 例外を発生する
null ポインタ 9 どのアドレスでもないことを示す特別な値 空ポインタともいう null ポインタに対してデリファレンス演算子 * を使うと例外 (null pointer exception) が発生する null ポインタ定数マクロ NULL を使って判定する 数値リテラル 0 は null ポインタに変換される <stddef.h> で定義されているが <stdio.h> などで自動的にインクルードされる
null ポインタ判定 10 以下のような書き方のバリエーションがある p が null であるか p が null でないか if(p == NULL) if(p!= NULL) if(p == 0) if(p!= 0) if(!p) if(p) if は常に式の評価値が 0 でないことを判定するので 上段の式と同じ意味になる
メモリ領域を作成する 11
基本型を返す関数 12... // 行列のトレースを返す関数 double trace3(const double Mat[3][3]) double tr = Mat[0][0]+Mat[1][1]+Mat[2][2]; return tr; } int main(void) double Mat[3][3] = 1, 2, 3 }, 4, 5, 6 }, 7, 8, 9 } }; trace3(mat); // => 15 double 型などの基本型を戻り値とする関数は簡単に作れる このような 配列サイズが固定の場合は 大きさを仮引数に書くことができる ただし 単に先頭アドレスが渡されてくることには変わらない ここで 配列のアドレス計算は Mat[i][j] = *(Mat + i*3 + j) であり 2 番目のサイズのみが使われるので Mat[3][3] という宣言は Mat[][3] と書いてもよく 1 番目の値は単に無視される 値そのものがコピーをされて伝搬していく
... // a から始まる 3 要素の連続値を返す int* seq3(int a) int s[] = a, a+1, a+2 }; return s; } 配列を返す関数? 13 配列のコピーはできないので 関数の中で用意した配列 配列の先頭へのポインタを返す? int main(void) int *seq = seq3(10); seq[0]; // => 10? seq[1]; // => 11? seq[2]; // => 12? 配列の先頭へのポインタを受け取る? この方法ではうまくいかない
関数内の配列のアドレスを返してはいけない 14 関数内で定義された配列変数は 通常の変数と同様に関数の実行が終了する際に消滅する... unsigned char *createnewimage(void) unsigned char img[512*512] = 0 }; return img; } int main(void) unsigned char *pimage;... pimage = createnewimage(); // この後ポインタ pimage を使って // アクセスしてはいけない... 配列の型を戻り値とする関数は作れないその代わり ポインタを返す createnewimage の領域この領域は関数の実行後 img 消滅してしまう pimage このポインタ ( アドレス ) の指す領域は値が返った時点ですでに無い
自動的に消滅しないメモリ領域を作る 15 動的メモリ確保 (dynamic memory allocation) 変数のような言語機能ではなく 標準ライブラリ関数を使って 直接 OS にメモリ領域を要求する 確保された領域は自由に使える 変数名はつかないので ポインタでアクセスする 自動的には消去されないので 自分で解放する
動的メモリの確保 16 malloc() 関数によって指定したバイト数のメモリ領域を確保する メモリ領域は初期化されない メモリ不足などで確保が失敗した場合 NULL ポインタが返る 確保された領域が置かれているところをヒープ (heap) という malloc() のプロトタイプ #include <stdlib.h> void *malloc(size_t size); 次のスライドで説明
確保されたメモリ領域の値 17 malloc は void * 型を返す 特定の型を表さない 任意のポインタ型に自動変換できる C++ では自動変換されないため static_cast が必要 int *pint; double *pdouble; pint = malloc(16); pint[0] = 100; // double の場合 // pdouble = malloc(16); // pdouble[0] = 3.14; sizeof(int) == 4, sizeof(double) == 8 の場合 ヒープ領域 100??????????? 3.14 int *pint 指し示すアドレスは同じだが 扱われ方が型によって異なる?? main のスタック領域 double *pdouble
サイズの指定 18 例 : int 型の 100 個の領域を確保 int *mem = malloc(sizeof(int) * 100); mem[0] ~ mem[99] としてアクセス
サイズの指定 19 例 : 画素値を unsigned char 型として w h のサイズのメモリ領域を確保 unsigned char *image = malloc(sizeof(unsigned char) * w * h); 座標 x, y に対して image[x+y*w] としてアクセス 0 x < w, 0 y < h w image[0+0*w] sizeof(unsigned char) は必ず 1 なので 書かなくてもよい h image[x+y*w]
動的メモリの確保 ( その他 ) 20 calloc(): malloc() と同じだが さらにゼロで初期化を行う size num バイトの領域を確保する メモリの全てのビットが 0 となるからと言って double などの型の値として 0 となるとは限らない realloc(): 確保された領域のサイズを変更 calloc(), realloc() のプロトタイプ #include <stdlib.h> void *calloc(size_t num, size_t size); void *realloc(void *ptr, size_t new_size);
動的メモリの解放 21 確保したメモリへのポインタを指定して その領域を解放する 解放した領域は使用してはならない NULL を入れておくと安全 既に解放済みのポインタに対して実行してはいけない (2 重解放エラー ) NULL ポインタを与えた場合は何もしない free() のプロトタイプ #include <stdlib.h> void free(void* ptr); どんな型のポインタも void * に自動的に変換される
例題 :PGM 画像 22 画像のためのメモリを自動的に確保する関数... // 画素値 0 で初期化された画像を動的に作成する unsigned char *createimage(int width, int height) int i; unsigned char *img = malloc(width * height); if(img == NULL) sizeof(unsigned char) == 1 な ので 掛けるのを省略 return NULL; } メモリ不足の場合は NULL を返す for(i = 0; i < width*height; ++i) img[i] = 0; return img; } すべてゼロで初期化する
例題 :PGM 画像 23 使い終わったら自分で free() する... int main(void) unsigned char *Image = NULL;... Image = createimage(640, 480);... free(image), Image = NULL; メモリ領域を解放する ポインタは無効になるので NULL ポインタを代入しておくとよい
用語 : メモリリーク (memory leak) 24 malloc() で動的確保した領域を free() で解放し忘れて メモリが圧迫されること 単純なプログラムではこの手のバグは見つけやすいが 秋期 C プログラミング で扱う複雑なデータ構造では発生しやすく 見つけづらいバグとなる
変数とポインタと動的メモリ確保の整理 25 メモリに領域を確保して値を読み書きする方法には 4 つある 分類生存期間スコープメモリ領域初期化 自動変数 ( ローカル変数 ) 大域変数 ( グローバル変数 ) 静的変数 (static 変数 ) 動的メモリ 定義位置からブロック終端まで プログラムの実行開始から終了まで プログラムの実行開始から終了まで 確保から解放まで 定義位置からブロック終端まで 定義位置からプログラム終了まで 定義位置からブロック終端まで 変数名で参照しない スタック 静的領域 静的領域 ヒープ 初期化が指定されている場合のみ ブロックに入るたびに初期化される プログラム開始時に 1 度だけ 初期化が指定されない場合 0 で初期化される 同上 malloc() はされない calloc() は 0 を書きこむ
配列変数と動的メモリ確保の比較 26 機能配列動的メモリ確保 サイズ 多次元 コピー 解放 自動変数の場合は あまり大きなサイズは確保できない 可能 ただし 関数などに次元やサイズを伝えられない 不可能 標準ライブラリ関数を利用 自動変数の場合 ブロックを抜けると自動的に消滅する ほとんど制限がない 単なるポインタなので不可能 不可能 標準ライブラリ関数を利用 free() によって自分で解放する アクセス 変数名が先頭へのポインタになる malloc() によって得られるアドレ スをポインタ変数に入れて扱う