より良いプログラムを書くために
良いプログラムとは,単に誤りなく目的の機能を実行できることだけではなく,次のような特徴を持ったプログラムである.
- プログラム構造が簡明で解り易いこと.
- 他のマシンやOSへの移植性が良いこと.
- コメントが適切に記述されており,コードの解釈が容易であること.
これらにより,プログラムの改良や保守が容易になるとともに,プログラムの再利用を促進することができる.
- データ構造定義
- データ構造の定義をincludeファイルにまとめ,インタフェース情報の 一元管理を図る.
- 定数定義
- #define機能を利用し,固有な定数値をプログラム中に散在させない.
プリプロセッサ機能の#defineは定数値をニーモニック表現にして,プログラムの可読性を向上させるとともに,将来の定数値の変更を容易にする.
移植性に関する問題
C言語の移植性に関する主要な問題は以下のものである.
- オぺレーティング・システム(OS)の差
- プロセッサの差
- 処理系の差
- ライブラリの差
- オぺレーティング・システム(OS)の差
- ファイルシステムの違い
Windowsのファイルシステムは,ディレクトリ以外にもドライブという形で区別されているため,UNIXでは必要のないドライブの識別が必要になる.
- トランスレートモード
UNIX上では,テキストファイルとバイナリファイルの区別はないが,Windows上ではテキストモードとバイナリモードが存在し,必要に応じて使い分ける必要がある.例えば,fopen関数でバイナリファイルをオープンする場合,アクセスモードに"b"を指定する必要がある.
- 文字セット
文字セットの代表的なものにASCIIとEBCDICがあるが,IBM系の大型機で採用しているEBCDICでは,'A'から'Z'までが連続しておらず,'0' <
'A'といった関係が成り立たない.コードが連続していることを前提にしたプログラムを異なる文字セットに移植する場合は問題となる.
- プロセッサの差
- 基本データ型
処理系の基本データ型(char, int,
floatなど)の大きさは,プロセッサのレジスタ幅に依存して処理系ごとに異なっている.データ型に課せられた規定は,shortとintは少なくとも2バイト,longは少なくとも4バイト,shortはintより長くてはならず,intはlongより長くてはならないことだけである.
したがって,データ型の大きさを固定的に扱ったコーディングをすると,移植先で異なった動作をする可能性がある.
また,データ型の大きさに従って格納できる値の範囲も異なってくる.格納できる値の範囲が変わるとオーバフローやアンダフローの起き方が変わり,プログラムの動作が変わる可能性がある.
- エンディアン
int型やlong型,float型などの複数バイトのデータをメモリに格納する場合,上位バイトから格納するか(ビッグエンディアン),下位バイトから格納するか(リトルエンディアン)はプロセッサによって異なる.このようなバイト並びを仮定したプログラムは移植性がない.
基本的にはエンディアンによらないコーディングをする.例えば,オブジェクト内におけるビットの左右への方向付けに依存したコードや、2つの変数を連結するようなことをしてはいけない.
但し、外部との入出力(通信,交換媒体,システムコールパラメータ)を行う場合,その内容はエンディアンに依存することになるため,どちらのエンディアンで動作するかを明確にし,エンディアンに依存した処理の部分を局所化する.
- データアライメント
データのアライメントはプロセッサにより異なる.例えば,4バイトデータは偶数アドレスからのみ,4の倍数アドレスからのみ,どのアドレスで始まってもよいのいずれかはプロセッサにより異なる.このため,構造体の要素はプロセッサにより異なるオフセットを持ち得る.
ポインタを介して整数をメモリに格納するような場合,その処理系では可能であっても奇数アドレスや2の倍数アドレスを指定せず,4の倍数アドレスを指定するなどデータ配置の制約に関して安全側のコーディングをする.
また、連続して宣言される2つの変数はメモリ内に一緒に配置されるとか,ある型の変数は別の型としても用いられるように適切にアラインされているとか仮定してはいけない.
- 処理系の差
- オペレーティング・システムや処理系に依存するインクルードファイルが存在する.
- 処理系によって各種のシンボルや定数が定義されている.このような暗黙の内に定義されるシンボルや参照可能な変数は,その種類や内容が処理系によって異なるため移植の障害となる.
-
構造体には,高速化やハードウェアの制約から各要素が特定のバイト数の整数倍に配置されることによる要素間の未使用領域が存在し得る.このときの配置ルールは,処理系やその起動オプションに依存している.そのため,同じ構造体でもその大きさや要素の相対アドレス位置が異なる場合がある.その結果,構造体で定義される通信メッセージや媒体上の情報の内容に差異が生じる可能性がある.
- ライブラリの差
ANSI C標準ライブラリ以外にも処理系により各種のライブラリが提供されている.
プロセス関係のライブラリやシステムコールを直接呼び出す関数など,オペレーティング・システムやプロセッサに依存するこれらの部分は移植性がない.
また,関数名が同じでも引数が異なったり,機能は同じでも関数名が異なる場合があるので注意が必要である.
移植性を良くするために
- 多くのコンパイラはANSI C標準にはない機能拡張をしている.他のコンパイラにプログラムを移植する場合はこのような機能拡張部分を使わないように注意する.
- マシン独立なコードとマシン依存のコードとは別々のファイルに入れるようソースファイルを組織化すべきである.
- #ifdef機能によりマシン依存部の記述を条件コンパイルにする.
- sizeof演算子を用いて,マシン依存の定数値を隠す.
- 2の補数表現の特徴を利用したコードを用いてはいけない.算術演算を等価なシフト演算に置き換える最適化には特に注意が必要である.
- 語長はシフトやマスク操作に影響する.例えば,整数xの下6ビットを0にするには
x = x & ~077
とし,語長には無関係なようにする.語長を16ビットと仮定して x & 0177700 と書いてはいけない.
- 文字列定数を変更してはいけない.
システム依存性
以下に示すようなマシンやOSに依存した機能は移植性を持たない.これらの機能を使う場合は,システムに依存している部分を明確にするとともに,移植を考慮してコードを注意深く書かねばならない.
- キーボードからの1文字入力
キーボードから入力された文字をCプログラムに渡すのはOSの役割であり,C言語では標準化されていない.
cursesライブラリはcbreak()関数を持つ.UNIXでは,端末モードをioctlにより制御する.また,Windowsではgetch()関数が利用できる.
- 読み出し可能な文字数
読み出し可能な文字が幾つあるかを知ること,および読み出す文字がない場合に読み出しがブロッッキングされないようにするのは,完全にOSに依存した機能である.
cursesライブラリにはnodelay()関数を持つものがある. システムによって,select, FIONREAD ioctl, kbhit(), rdchk(), O_NDELAYオプションを指定した
open()または fcntl()などがある.
- 端末画面の操作
画面操作は,使用する端末タイプに依存している.画面操作を行なうには,termcap, cursesあるいはシステムが提供するライブラリを利用する.
- マウス
マウスの取り扱いは,システムごとに全く異なっている.通常はシステムが提供するライブラリを利用する.
- 実行プログラムのパス名
起動された実行プログラムの完全なファイルパス名を知る互換性のある一般的な方法はない. argv[0]がどのような形式のファイルパス名を含むかはシステム依存である (何も含まない場合もある).
- 環境変数
プロセスは呼び出し側の環境変数を変えることは一般にはできない.実行プログラムによる環境の変更の可否およびその方法はシステム依存である.
UNIXでは,プロセスは自身の環境変数を変えることができsetenv(),putenv()関数),子プロセスに変更された環境変数を渡すことができる.しかし,その親プロセスには影響を与えない.
- 子プロセスの起動
プログラム内より子プロセスを起動する方法はシステム依存である.
- ディレクトリ情報の読み込み
ディレクトリ情報を読み込む方法はシステム依存であるが,多くのUNIXシステムではopendir(), readdir()が利用できる.また,Windowsではfindfirst(),
findnext()がライブラリとして用意されている.
- ファイルを読む前にそのサイズを知る方法
UNIXにおいてはstat()関数によりファイルの大きさを知ることができる.
また,fseek()によりファイルの終端に移動した後ftell()によりファイル位置を求めてファイルサイズを知ることができる.しかし,これは移植性のある方法ではない.
Cプログラムにおいてファイルのサイズを知る最も正確な方法は,ファイルをオープンし実際に読み出すことである.
- ファイルサイズの変更
ファイルを完全に書き換えることなくサイズを変えるには,BSDシステムではftruncate()が,他のシステムではchsize()あるいはfcntlオプションのF_FREESPがある.これらには移植性はない.
- ユーザからの応答待ち
秒以下の精度で遅延やユーザからの応答待ちを行なう移植性のある方法はない.
- 大部分のプログラムにおけるコードの多くは時間的にクリティカルではない. コードが時間的にクリティカルでないときには,実行効率よりは可読性や移植性に重点を置くべきである.
計算機の速度は向上しており,多少非効率なコードであっても実用上問題になることは少ない.
- 関数呼び出しは,インラインコードよりも実行効率は落ちるが,コードのモジュラリティと可読性はすぐれている.これらの特長を犠牲にしてもインラインコードを用いなければならないケースは少ない.
-
プログラムにおけるオーバヘッドを解析するには,profのようなプロファイラによりプログラムのどの部分がどれくらい時間を使っているか,各関数が何回呼び出されたかといった情報を収集する必要がある.これを解析することで効率を向上させるためのプログラムの改良部分が把握できる.
- 時間的にクリティカルな部分のコードに対しては,より良いアルゴリズムを選択するようにする.コーディングの詳細を個別に最適化するのは重要で
はない.効率的なコーディング手法の多くはコンパイラによる最適化により自動的に取り入れられている.また,最適化を追求しすぎるとかえってコードが 大きくなり,効率が低下することもある.
-
実行効率とコーディングの関係は,最終的には使用しているプロセッサと処理系に依存している.そのため,この関係を正確に知るためにはプログラムの実行時間を測定してみる必要がある.また,異なるコーディングが同一のコードを生成していないか,処理系が生成するアセンブラコードを調べてみる必要がある.
- 一般的な処理系は i++, i += 1 および i = i + 1 のいずれも同じコードを生成する.これらのいずれを用いるかはスタイルの問題であり,最終的な実行効率には関係しない.
コーディングスタイルの選択の多くは任意なものである.重要な点は少なくとも1つのプログラムシステム内では統一的なスタイルを用いることである.
コーディングスタイルの中で,インデントや空白の数などはソースプログラムの整形ツール(indentなど)を用いることにより自動的に統一することができる.
しかし,整形ツールは構文的に正しいプログラムに対してのみ適用可能であり,プログラムの作成中におけるスタイルの統一による構文エラーの発見や修整のし易さの向上などには役に立たない.
整形ツールはプログラムが完成した段階で最終的なコーディングスタイルの統一を図るために適用し,規模の測定や保守対象用のソースコードを作成するのには利用できる.
なお,emacsなどの自動的にインデンテーションを行なえるエディタを使ってコーディングするのも有効な方法である.
ここでは,コーディングスタイルの内,整形ツールなどでは対応できない部分を中心にコーディングスタイルの指針を示す.
- コメント
コメントは,コードが何をするものか,それがどのように行なわれるか,変数やパラメータは何を意味するかなどを記述し,コードの保守性を高めるものである. コメントの一般的な記述方針は以下のようなものである.
- コメントはコードのブロックに対してそれが全体として何をするものかを説明する.行ごとに細かい論理を説明するよりは分かり易いことが多い.また,コードから自明なコメントは避ける.
- 変数やデータ構造の宣言には,その使用目的を記述する.
- 各関数の先頭には,その関数の機能と使用方法を簡潔に記すコメントを付ける.
- トリッキーなコードに対しては,それが何をするものかとその理由を説明すべきである.
- コードを修整した場合,その理由と修整日付を記入する.
なお,ANSI Cではコメントのネストは許されていない.但し,処理系によってはコメントのネストをオプションで認めているものもある.コメントを含む大量のコードをコメントアウトするには,#ifdefまたは,#if
0を用いる.
- 宣言
- 初期値が重要な変数は明示的に初期化されるべきである.Cにおけるデフォルトの0への初期化に頼っている場合は,その旨をコメントする.
- 複数のファイルから構成されるプログラムの各ファイルでは,関数や変数がそのファイルにローカルであればstatic宣言すべきである.
- 数値定数は#defineにより意味のある名前を付けるようにし,直接用いてはいけない.
- ある一定の離散値のみを取る変数は列挙型で宣言する.
- 名前付け
名前付けの一般的な規則を以下に示す.
- #define定数は全体を大文字とする.
- enum定数はその先頭文字のみが大文字か,その全体を大文字とする.
- 構造体/共用体/enumのタグ名および関数名/型宣言名/変数名は小文字とする.
- マクロ関数は原則として全体を大文字とする.但し,関数としても存在し得るマクロ(getchar, putcharなど)は小文字とする.
- 大文字と小文字の違いのみ異なる名前(fooとFOOなど)は避ける.また,似かよった名前も避ける.
- 'l', '1', 'I'や'O', '0'のように同じように見えて紛らわしい文字の使用には注意する.
- 大規模なプログラムの場合など,名前のぶつかりが起こり易いときには各モジュールを示す1ないし2文字のプレフィックスを先頭に付ける.
- typedefされた名前の後ろに"_t"を付ける.