質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

ファイルI/O

ファイルI/Oは、コンピューターにおけるファイルの入出力です。これは生成/削除やファイルを読み込んだり、出力をファイルに書き込むようなディレクトリやファイルの運用を含みます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

5回答

2444閲覧

C言語でファイル読み込みをするときのベストプラクティスを教えてください。

maic

総合スコア8

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

ファイルI/O

ファイルI/Oは、コンピューターにおけるファイルの入出力です。これは生成/削除やファイルを読み込んだり、出力をファイルに書き込むようなディレクトリやファイルの運用を含みます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2019/06/02 06:51

編集2019/06/02 11:21

前提

BITMAPのファイルの読み書きをしようとしています。
書籍やgithubをながめていると、「ヘッダーのメンバ毎に変数を定義して読み込む方法」をよく見かけます。

c

1// メンバ毎に変数を定義し読み込む 2unsigned int bitmap_size; 3fread(&bitmap_size, 4, 1, fp); 4// 以降、メンバの数だけ似たような記述が増える

課題

上記の記述だとヘッダーの数だけ行数が増えていき、冗長に感じたので、事前に定義していた構造体のサイズ分メモリ空間を用意して一気に読み込もうとしました。

c

1BITMAPINFOHEADER bitmapInfoHeader; 2fread(&bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, fp);

ですが、構造体のメモリアラインメントのせいで~~BITMAPINFOHEADER構造体~~ BITMAPFILEHEADER構造体のサイズがメンバーサイズの合算値にならないため、期待したサイズよりも多く読み込んでいるようです。
この方法では読み込みだけでなく書き込みでも同様にパディング分ゴミが入ってしまいます。

追記:実際にはBITMAPFILEHEADERが14byteから16byteにアラインメントされてしまったため、BITMAPINFOHEADERの読み込みがずれてしまっていたようです。

上記の課題を避けるために#progma packなどでアラインメントの最適化をキャンセルすることができるとは思うのですが、いまいちこれがクリーンコーディングと言えるのか、大規模なOSS開発で是とされているのか、なにぶんC/C++での開発経験が乏しい私には判断がつきませんでした。

教えてほしいこと

今回はC言語で書いていますが、モダンで大規模なOSSのC++プロジェクトだと前提を置いたとして
BITMAPに限らず、ファイルの読み書きを行うときには多少冗長ながらも、最初に述べた「ヘッダーのメンバ毎に変数を定義して読み込む方法」が理想的(or よく使われる書き方)なのでしょうか?

他にも、C/C++で開発をしている方で美しい書き方を知っていればぜひ教えてください。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yominet

2019/06/02 10:09

›構造体のメモリアラインメントのせいでBITMAPINFOHEADER構造体のサイズがメンバーサイズの合算値にならないため、 ›期待したサイズよりも多く読み込んでいるようです。 このあたり、どういうソースを書き、どういう結果なり、 そして、その結果に対してどう調査して、この結論にいたりましたか?
maic

2019/06/02 10:45 編集

質問ありがとうございます。 INFOHEADERに関しては、回答いただいた通りアラインメントが取れていて、 実際にはその前に読み込んでいたFILEHEADERのアラインメントが悪さをしていることがわかりました。 ```c // WORD : unsigned short (2byte) // DWORD : unsigned long (4byte) typedef struct tagBITMAPFILEHEADER { WORD bfType; DWORD bfSize; WORD bfReserved1; WORD bfReserved2; DWORD bfOffBits; } BITMAPFILEHEADER, *PBITMAPFILEHEADER; ``` ```c // うまくいかない例 // printf("output: %d\n", sizeof(BITMAPFILEHEADER)) // output: 16 (メンバの合計サイズは14) BITMAPFILEHEADER bitmapFileHeader; fread(&bitmapFileHeader, 14, 1, fp); ``` `bfType`が4バイトアラインメントにより、2バイト分追加されているようです。 その結果、その後の処理もずれてしまっていました。 また、当然ではありますがFILEHEADERを以下のように各メンバーずつ読み込んでいくと、アラインメントを気にせず格納できました。INFOHEADERに関しては一気に読み込むことはできました。 FILEHEADERのようにアラインメントが取れていないものに関しては一つずつ読むしかないのでしょうか。 ``` fread(&(bitmapFileHeader.bfType), 2, 1, fp); fread(&(bitmapFileHeader.bfSize), 4, 1, fp) ```
guest

回答5

0

メモリアライメントの問題は、CPUのアーキテクチャに依存しますから、「使っているCPUに合わせる(合わせざるを得ない)[と諦める]」のがベストプラクティスだと思います。

どんなにソフトで頑張ってみても、CPUチップ内部の素子と配線で決まっているエンディアンを変えることはできませんから。
16bitのCPUが出てきたとき、32bitのCPUが出てきたとき、64bitのCPUが出てきたとき等に、多くの先達が苦労して出した現実解が冗長に見えるコードだと思います。

エンディアンが異なるCPUがあって、それらに対応しようとすれば1種類のCPUを対象とした綺麗なコードのようにはなりません。
異なるCPUで使えるようにするには、異なる部分に対応するために冗長な部分が必要になりますからね。

CPUのアーキテクチャの種類は、より高い性能を目指して今後も増えていきます。
同じコアでも高い演算能力を目指したもの、低消費電力を目指したもの、特殊機能を目指したもの(AI用エッジ等)などなど、に対応するには綺麗事では済まないと思います。

投稿2019/06/02 13:20

coco_bauer

総合スコア6915

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

maic

2019/06/04 12:40

回答ありがとうございます。 > メモリアライメントの問題は、CPUのアーキテクチャに依存しますから、「使っているCPUに合わせる(合わせざるを得ない)[と諦める]」のがベストプラクティスだと思います。 なるほど。正直Cを始めるまで、この言語ががこれ程までにCPUやメモリについて考える必要があるとは思ってもみませんでしたし、冗長に見えていたコードにも実は意味があるということを今回の体験で知れました。ありがとうございます。
guest

0

とりあえず、思いついたOSSなBMP読み込みについて調べた所
opencvは1要素ずつ読み込み
ImagaMagickも1要素ずつ

プラットフォームに縛られない必要がある場合は1要素ずつになるような気がします。

投稿2019/06/02 13:15

asm

総合スコア15147

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

maic

2019/06/04 12:45

回答ありがとうございます。 リンクまで貼っていただけて、本当に助かります。 やはりプラットフォームの問題なのですね。 今回質問したことで、なぜこのような書き方をするのか理由がわかったので、とても納得して書けそうです。
guest

0

ベストアンサー

こんにちは。

BITMAPINFOHEADERはアライメントは取れているので、通常はパディングは発生しないはずです。

事前に定義していた構造体のサイズ分メモリ空間を用意して一気に読み込もうとしました。

私が見たことがあるサンプルは大抵この方法でリード/ライトしていますし、私自身もそのようにすることがあります。

もしかして、BITMAPFILEHEADERとBITMAPINFOHEADERをもつ構造体を定義して、そこへ読み込もうとするとおっしゃる問題が発生すると思います。

BITMAPに限らず、ファイルの読み書きを行うときには多少冗長ながらも、最初に述べた「ヘッダーのメンバ毎に変数を定義して読み込む方法」が理想的(or よく使われる書き方)なのでしょうか?

個人的にはこれはバグを生みやすいので「無し」です。

C/C++では、データ交換についてはどうしてもCPUの特性を把握して処理系毎に専用のコードを書く必要がある場合が多いです。(特にエンディアン)
アライメントもその一つと思います。msvcでは #progma pack を使うのは落とし所のように感じます。

投稿2019/06/02 08:17

Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

maic

2019/06/02 11:26 編集

回答ありがとうございます。 確認してみたところ、仰る通りBITMAPINFOHEADERはアラインメントが取れていて、一度に読み込むことができました。 実際にはその前に別に定義してあるBITMAPFILEHEADER構造体で読み込む際、4byteアラインメントがかかりズレてしまっています。この場合は2バイトにアラインメントを限定しないと一つずつメンバを読み込んでいくことになると思うのですが、この方法で問題ないのでしょうか?
pepperleaf

2019/06/02 11:53

昔見た、システム寄りのソース、いやになる位の #if [cpu等] があった記憶。 仕方が無いんでしょうね。 自分も昔、合わせたつもりが、外して、自分で罠にかかった事もあります。
Chironian

2019/06/02 12:17

maicさん 回答でリンクした先のサンプル・ソースではBITMAPFILEHEADERも普通に読んでます。恐らくWindows.h側で #pragma pack しているだろうと思います。これにより「普通」に読めるはずです。 また、サンプル・ソースとご自身のプログラムとの相違を確認してみるのも手と思いますよ。
maic

2019/06/02 12:38

@pepperleaf 様々な実装のコンパイラや環境が乱立していた昔に、激しい環境依存と戦っていたプログラマたちは本当にすごいですね。 そんな彼らが築き上げてきたものを知るためにも、今になってC言語の勉強を始めました。
maic

2019/06/02 12:44

@Chironian リプライ、本当にありがとうございます。 やはり#pragma packでアライメントの設定を切り替えていたのですね。 おっしゃるように自分のコードと見比べてみてサンプルのように「普通」?に読み込むことができず調べていたら下記のような記事を見つけました。 https://www.mm2d.net/main/prog/c/image_io-05.html pragma packなどでアライメント設定をいじるか、一つずつ読み込むか、bufferに一度置くかの選択肢になるのですね。 とても勉強になりました。ありがとうございます。
guest

0

うーん、やっぱりバイナリをそのまま扱うのが間違いな気がする。JSONで扱うべきではと。(現代のPCでJSON読み込みが遅くて問題になることはない)。いやProtocol Bufferあるやろという声も聞こえそうですが個人的にはうーんという思い。

文字列ならpaddingもendianもその他諸々も関係ないですしね。

投稿2019/06/02 15:14

yumetodo

総合スコア5850

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

maic

2019/06/04 12:24

回答ありがとうございます。 > やっぱりバイナリをそのまま扱うのが間違いな気がする。JSONで扱うべきではと。 まったくその通りだと思います。ハッシュテーブルは基本的にO(1)でしょうし仰る通り、読み込みの遅さは気にならなそうです。 ただ、このプログラムは自分のCSへの理解度を上げるために書いている部分が多く、CPUの気持ちを分かりたいと思って書き始めました。こういった部分についても質問文の中で言及するべきでした。 Protocol Bufferについては全く知らなかったので新しい発見になりましたありがとうございます!
guest

0

まずはヘッダとか考えないで、ある程度の容量のバッファを用意して、そこに一括して読み込み、そこからヘッダなどのデータを読み出し、分割していけばいいです

直接ヘッダを読み出す、という考え方は捨てるほうがいいです

投稿2019/06/02 06:58

y_waiwai

総合スコア87747

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

maic

2019/06/02 11:14 編集

回答ありがとうございます。 大変恐縮ですが、回答内容で理解できていない部分があり、質問させていただきます。 > まずはヘッダとか考えないで、ある程度の容量のバッファを用意して、そこに一括して読み込み ヘッダを考慮しないというのは、たとえばとりあえず200byte分だけ読み込んでその中でヘッダーをパースしていき、ヘッダーの終端が分かった段階で実データのボディ先頭までfseekして、読み込むということでしょうか? あらかじめ拡張子などの情報によりヘッダサイズにはあたりがついていると思うのでその分を読み込むほうが事故を生みにくいような気がするのですが、どうでしょうか。 > 直接ヘッダを読み出す 直接読み出すというのは「課題」の部分で言及した `fread(&bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, fp);` のように構造体のサイズ分読み込んでしまうやり方ということでしょうか? バッファをに関しては、以下のようにバイト配列を用意して `unsigned char header_buf[HEADERSIZE];` `header_buf[0]`のように値にアクセスし、説明変数(or 構造体)に代入するような流れになるイメージで大丈夫ですか? 例となるようなコードがあればURLなど教えていただけると幸いです。
y_waiwai

2019/06/02 14:16

ヘッダ部分を直接ヘッダ構造体に読み込む、というのは、例えばエンディアンが違うCPUに変わったときに破綻します。また、アライメントの違うCPUに変わったときに修正を余儀なくされたりもします。 そんなことでコードに手を加えなければならないような作りにするというのはまずいです。 アライメントが違おうが、エンディアンが違おうがきちんと動作するコードを書くのがベストだとは思いませんか。 そのためには、読み込む場合にはバイト列として読み込み、関数なりを介してヘッダ構造体へ展開するようにしましょう で、ヘッダ分だけを読み、必要に応じてBODY分までシークして読む、というのは、コード量からしても速度の点から見ても全くメリットはありません。
maic

2019/06/04 12:50

ご返信大変ありがとうございます。 > アライメントが違おうが、エンディアンが違おうがきちんと動作するコードを書くのがベストだとは思いませんか。 本当にその通りですね。直接ヘッダを読み込むやり方がバグを生むという事もご意見いただけなければわからなかったことですし、それが環境によってどれだけ問題を起こすのかも理解できました。 不躾な質問にも丁寧にご回答くださりありがとうございました。納得しながら実装できそうです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問