2週目:ポインタの配列と動的確保[全体PDF]

今週の目標

ユーザが指定した画像ファイルを読み込み,そのコピーを別ファイルとして出力する


図1 2週目の目標

バイナリファイル[PDF]


図2 テキストエディタで開いたPPMファイルの例

一般的には,ASCIIコード表で文字に変換できる数値(0x00から0x7F,つまり符号なし整数だと0から127)のみが 含まれたファイルはテキストファイルと呼ばれ,テキストエディタで開くと文字列が表示される. 一方,テキストとして表示する用途が想定されていない,ASCIIコード表以外の数値も持つファイルはバイナリファイルと呼ばれる. PPM画像ファイル(P6)は,ヘッダ情報はテキスト形式であるが,画素値はバイナリ形式で保存されるため, バイナリファイルの一種である. 図2にPPMファイルを無理やりテキストエディタで開いた例を示す. ヘッダ部分は意味を成す文字列として表示されるが,画素値の部分はメモリ上に1バイトずつ 画素値の値が記録されているだけで,テキストによる表示は想定されていない. よって,PPMファイルをテキストエディタで開くと,画素値の部分は意味を成す文字列にはならない. つまり,バイナリデータ(本課題ではPPMファイルの画素値)をファイルに読み書きする際は, テキストデータのための関数(fgets,fscanf,fprintfなど)が使えないことになる.

バイナリファイルの読み書き

fread関数,fwrite関数[PDF]

size_t fread(void *buffer, size_t size, size_t count, FILE *fp);

書式なしデータをストリームから読み出す関数.fp から size バイトの項目を count 数まで読み出し,buffer に格納する.戻り値は,実際に読み出した全項目の数.

size_t fwrite(const void *buffer, size_t size, size_t count, FILE *fp);

書式なしデータをストリームへ書き込む関数.size バイトの項目を count 数だけ buffer から,fp へ書き込む.戻り値は,実際に書き込んだ項目数.


図3 fread関数の使用例


図4 fwrite関数の使用例

バイナリデータ(書式なしデータ)をファイルに読み書きする関数として,freadとfwriteがある. 使用例を図3, 4に示す. いずれもバイナリファイルをオープンするため,fopenの第二引数に"b"を追加する必要がある. ファイルを開いた後,読み込みではfreadを使用し,ファイルからsizeof(char)(=1バイト)のデータを100個読み込み, bufferに格納している.また,書き込みではfwriteを使用し,bufferの1バイト×100個の要素をファイルに出力している.

演習課題4_2_1[PDF]

PPMファイルから画素値を読み込む処理をiioLoadFile関数に実装せよ. 格納先はIMAGE構造体の1次元静的配列であるpBufferとする. さらに,PPMファイルにpBufferの画素値を書き出す処理をiioSaveFile関数に実装せよ. 実装後,入力画像と同じ画像が出力されるか確認すること.以下は注意点である.

  • iioLoadFileとiioSaveFileの両方の実装
  • iioSaveFileのファイルopenはwbモードにすること
  • ipCopyにおける画素値のコピーの実装
  • 実行時,出力ファイル名は.txtではなく.ppmにすること

画素値の格納方法の改良 ①動的な一次元配列[PDF]

mallocとfree


図5 メモリの静的確保と動的確保の比較例

図5に,要素数3のunsigned charの配列を確保して値を代入するコードの例を示す. 動的配列の場合,確保した領域の先頭のアドレスを保持する必要があるため, 変数はポインタ型となる. malloc(またはcalloc)関数により,連続したメモリ領域を確保し,その先頭アドレスがaに代入される. メモリ領域を確保した後であれば,aを利用することで静的配列と同様に各要素にアクセスできる. そしてメモリ領域を保持する必要がなくなった時点で,free関数により領域を解放する.

図5の例では動的確保を行う理由は特にないが,プログラムを実行するまで配列の要素数(例では3)が確定しない場合, 静的配列を確保することは一般的には不適切である(ただし,C99以降は確保できる場合もある). 動的確保であれば,mallocの引数中の3を変数に置き換えれば対応できる. 本演習課題では,ユーザが画像を指定するまで画素数がわからないため, 画素値を格納するpBufferは静的ではなく動的に確保すべきである.

作業課題

PPMのヘッダ情報に合わせてpBufferのサイズを動的に確保できるように改良せよ. ただし,pBufferは一次元配列でよいものとする(二次元配列は演習課題4_2_2で作成する). さらに,img01.ppmとはサイズが異なる以下の画像も読み込めることを確認すること.

画素値の格納方法の改良 ②二次元配列[PDF]

ポインタの配列


図6 一次元配列による画素値の管理

図6に,一次元配列を動的に確保して先頭アドレスをpBufferに格納した状態を示す. ここで,オレンジはアドレスを格納している場所(つまりポインタ), 青は画素値を格納している場所を表している. また,説明を簡単にするため,全て十進数で表現し,ポインタの領域サイズは1バイトを想定している. これまでの作業により画素値は動的な一次元配列に格納されているため, 異なるサイズの画像ファイルが指定されても画素値は柔軟に格納できる. しかし,図6のデータ構造では, ある特定の画素(例えば,上から3行目,左から4列目)にアクセスしたい場合, 一次元配列上でのインデクスが何番目になるかは計算して求める必要がある. よって,一次元配列は画素値の管理としては直観的に扱いにくいことがわかる.


図7 二次元配列による画素値の管理

次に,図7のように,画像の各行の画素値を別々のメモリ領域で管理することを考える. 一行分,つまりsizeof(PIXEL) * xsize分の連続した領域(アドレス40から開始される領域)をmallocで確保し, その先頭アドレスである40をPIXEL*型の変数で管理する. これをysize分繰り返すと,PIXEL*の変数はysize個必要になる. つまり,アドレス24から始まる領域は,ポインタの配列として管理すると楽である. 各行の画素値領域の先頭アドレスをポインタ配列で管理することで, 二次元的なアクセスを容易に行えるようになる.

ここで,PIXEL*型の配列(図中のアドレス24から開始される配列)は, 画像を読み込むまでサイズが確定しないため, これをさらに動的な配列として確保する必要がある. PIXEL*を個々の要素とする配列を動的に確保したい場合, その先頭アドレス(24)を管理するための変数の型はPIXEL**となる.

演習課題4_2_2

動的確保によるポインタ配列を利用し,pBufferを2次元配列として確保できるようにプログラムを改良せよ. さらに,img01.ppmとimg02.ppmのどちらの画像もコピーできることを確認すること.