37章 バイナリの読み書き
http://www.geocities.jp/ky_webid/c/037.html
さあファイルの読み書きも佳境に近付いて参りました。
また難しそうな話がでてきましたよー。
総称ポインタ
別名、汎用ポインタとかvoidポインタとか呼ばれるポインタのことです。
総称ポインタは、どんな型のポインタとしても使えます。しかし、総称ポインタをそのまま使って間接参照を行うことはできません。なぜなら、元のデータ型が何なのかが分からないからです。例えば、
int num = 100; void *p = # /* int型変数のアドレスを保持 */ *p = 200; /* エラー */総称ポインタへのアドレスの代入は、何も考えなくてもそのまま行えます。「p = #」の部分ですね。これができるから、void*型を仮引数としている関数に、キャストなしでアドレスが渡せるのです。
一方、総称ポインタから間接参照する場合には、そのままではできません。「*p = 200;」の部分です。総称ポインタから間接参照するには、一旦、元の型にキャストしなければなりません。よって、
*(int*)p = 200;となります。
厳密な型チェックが逆に汎用性を失わせているということですかね。型に依存せずにポインタを扱えるのが総称ポインタだということですね。
ここはとっても大事だと思うので概念や書き方も含めて絶対覚えないといけないです。
問題1
バイナリデータとしてなら、どんなファイルでも読み込めるはずです。テキストファイルをfread関数で読み込んで、画面に文字データとして表示するプログラムを作って下さい。
freadを使ってテキストファイルを読み込むという問題ですね。
int main () { FILE *fp; char str[5000]; size_t size,i; fp = fopen("99.txt","r"); if ( fp == NULL ) { return 1; } size = fread(str,sizeof(char),5000,fp); for(i=0;i<size;++i){ printf("%c",str[i]); } fclose(fp); return 0; }
うまく読み込めました。
5000文字以上のデータを読み込む場合はfgetsの時と同じようにループで処理してfeofで判定すればいけると思います。
答え合わせ
解答例では配列を用意せずchar型の変数一つで処理してますね。
freadだからといって何も配列を必ず使わないといけないというのは誤りで、柔軟にやればいいことですね。盲点でした。
問題2
次の各式がどう違うのか考えてください。
char *str = "abced"; char str2[] = "abcde";という2つの宣言に対して、
sizeof( str ); sizeof( str2 ); strlen( str ); strlen( str2 );
これはわかります。ポインタと配列の違いのところで嫌というほど頭に叩き込みましたのでw
strはポインタです。なのでsizeofではポインタのサイズが得られるということになるはずです。ポインタはintで表現されるので「4」になるはずです。
str2は配列なので配列のサイズがカウントされるはずです。5文字+\0の6要素×char型(1バイト)なので「6」になるはずです。
一方strlenは文字列の長さをカウントする関数なのでこれは両者共に「5」と表示されるはずです。
どうでしょうか!?
答え合わせ
あってました。ホッと一安心ですね。
ただし、補足としてポインタのサイズはintなので環境によっては2になることもあるかもしれないということです。
問題3
指定された配列の全ての要素に0を代入して、配列をクリアする関数を作って下さい。
ある配列の全ての要素に0を代入するわけですね。やってみましょう。
void array_clear (void *array, size_t size) { size_t i; for(i=0;i<size;++i){ ((char *)array)[i] = 0; break; } } int main () { int array1[] = { 1,2,3,4,5 }; char array2[] = "abcd"; int i; array_clear(array1,sizeof(array1)); array_clear(array2,sizeof(array2)); for(i=0;i<5;++i){ printf("array1[%d] = %d\n",i,array1[i]); } for(i=0;i<5;++i){ printf("array2[%d] = %d\n",i,array2[i]); } return 0; }
$ main array1[0] = 0 array1[1] = 0 array1[2] = 0 array1[3] = 0 array1[4] = 0 array2[0] = 0 array2[1] = 0 array2[2] = 0 array2[3] = 0 array2[4] = 0
できました。総称ポインタの間接参照のところでかなり手間取りましたがなんとかなりました。
当初、int型で間接参照をしていたのですが、そうするとchar型の配列をクリアする場合に、おかしくなりました。
intは4バイトでcharは1バイトなのでおそらくintで間接参照してしまうと、charでいう4要素分のデータを上書きしていることになり、それを5文字分ループしてたので配列外のデータを上書きしてしまっていたようです。怖い。
なので1バイトであるcharにしてみたらうまくいきました。4バイトのint型を1バイトずつ0を代入するとうまくいくって言うのがちょっと実は良く分からなかったのですがとりあえずはコレでよしということで。
答え合わせ
概ねあってました。
また、任意の値で初期化できるmemset関数というのがあるようです。
実は僕も任意の値で初期化できないかと思って、試しにさっき書いたプログラムの0を代入してるところを1にしてみたんですよ。
void array_clear (void *array, size_t size) { size_t i; for(i=0;i<size;++i){ // 1で初期化 ((char *)array)[i] = 1; break; } }
コレ実行したらえらい事になりました。
$ main array1[0] = 16843009 array1[1] = 16843009 array1[2] = 16843009 array1[3] = 16843009 array1[4] = 16843009 array2[0] = 1 array2[1] = 1 array2[2] = 1 array2[3] = 1 array2[4] = 1
どっしゃあ。理屈はわかります。4バイトのint型に対して1バイトずつ1を代入してるのでおかしな値になるってことですよね。
でも何で1がこんな値にまで膨れ上がるのか意味がわかりません。理解できる日が来るんだろうか・・・。