(日変研+α同報)
昔のファイルを整理していたらアホなドキュメントが出てきましたので参考まで
に送ります。タイムスタンプは 1990/01/11 でした。例によって歴史を感じさせ
る内容ですね・・
C言語ノート
Cマガジン・Cジャーナルが発行されたように、現在はC言語のブームであるそ
うだ(あのくだらないCマガ3号が大学生協で売り切れて「追加発注しています」
と書かれるぐらいだから)。
ちょっと知っていると入門者にお節介な手ほどきを始めるのもCユーザーの悪い
癖である。筆者もその一人であるが、約1年前にDBVSのマニュアルに書いたC
言語の落し穴シリーズが某Ki氏に「多少は役に立つ」と評価されたので、マニュア
ルを離れてちょっとまとまった解説を書いてみようかと思う。対象としては全くの
入門者ではなく、一応の文法は理解している段階の人を想定している。まあそうで
なくても「聞いておけばいつかは役に立つ」ことがあるのもパソコンの世界の特徴
だし、よく知っている人はいろいろとケチを付けて楽しむのもCユーザーの健全な
姿である。
C言語はさまざまな記述スタイルが可能な言語なので、これからの内容は筆者の
独断による部分が多い。あくまで中規模のプログラムを個人で開発する時の手助け
程度のもので、これでなくてはいけないという理由はない。またDBVS開発の時
に比べ、筆者の記述能力(?)が進歩したので、DBVSでは使っていない機能もた
くさんある(そのためDBVSの改造には手間がかかってしまう−新しいプロジェ
クトを考えている人は保守性向上のためにも以下の記載を参考にされるようお願い
したい)
例によって最初は系統的解説を目指しながら、次第に思い付いたことを適当に書
くようになってしまっているが、これも例によってKi氏が並べなおしてくれるもの
と期待している。系統的に記述したわけではないので、重要な部分でももれている
ところが多いと思うが、こんな内容でもよければリクエストをいただけると幸いで
ある。
なお、例にあげてあるプログラムは表記の通りを実際に試したわけではないので、
ミスがあるかもしれない。
コンパイラは何を使えばよいか?
特に断わりがなければ以下MS−DOSに限定する。(OS/2で使えるコンパ
イラは事実上MS−Cだけだし、他のマシンでも事情はそう変わらない)
ユーザーの立場から言えば、コンパイルが速くてコストが安く、バグが少なく、
ANSIを満たしており、8086の多くのメモリモデルをサポートしているもの、そして
アセンブラ出力をサポートしているものであれば良い。マルチウインドウ統合環境・
強力なデバッガー・最高の最適化などというのは宣伝文句であって、実際にはそう
いうものは必要としないことが多い。とにかくスピードが命であり、瞬時にコンパ
イルができればほとんどの不満は解消されるものである。
このような点から現状ではTurbo-C(V1.5は安くてよかった),Quick-C+MS-Cが候補
となる。どちらも(雑誌の記事をみる限り)文句の付け難いコンパイラである
(Quick-C単独ではアセンブラ出力ができないなど制限があるので、あくまでMS-C
とペアにして使うことを念頭においている)
統合環境を使うべきか?
Turbo-C,Quick-Cおよび最近の多くのコンパイラは、統合環境をサポートしてい
るが、ある程度の規模のプログラムを書くならば、コマンドライン版を使い、高速
高機能のエディタを使った方が便利である。その場合もコンパイル過程とリンク過
程は別にしておく(バッチファイルを作る)こと。RAM Diskは必須である。
MAKEは必要か?
統合環境とも関係があるが、MAKEは開発段階では使わない。これはコメントだけ
では記述できないdocumentationの代わりになるもので、できあがったプログラム
のコンパイル方法を記述するものだと思った方が良い。どうせMAKEは処理系によっ
て大きく違うので、これならばバッチ処理でも良いわけデス。(MAKEMAKEでも作り
ませんか?) 中規模ソフトの場合ソースファイルが100個程度になるが、共通
のheader fileをちょっと書き換えた(新しいマクロや新しい関数のプロトタイプ
を追加するなど)だけで100個のファイルをコンパイルするのを待てますか?
(もちろんこれには逃げ道があるわけだが)。その意味で、GREPはよく使う。構造
体のメンバーを追加した時など、その構造体を含むファイルだけを抜き出して、修
正・コンパイルするのに非常に便利なツールといえる。
なお、100個のファイルをまとめてリンクするのではなく、完成した部品から
順にlibrary file(標準ライブラリでなく、そのproject専用のもの)に追加して
ゆくのである。その意味でライブラリアンは高速なものが望ましい。
メモリモデルの選択
8086系のCPUの場合、メモリモデルの選択は避けられない。8086は「最もC言
語に向いていないCPUのひとつ」だそうだが、筆者が推薦するのはlarge model
(Turbo-C/MS-C)である。これはプログラム・データともにメモリ上限まで使えるも
のであり、small modelを使っていて後々メモリ不足に悩む心配がない。huge model
もあるが、実行速度がかなり落ちる(サポートしていない処理系もある)ので、ボ
ケないためにもlarge modelでメモリ管理の方法を習得すべきである。(64KBを越
える配列の動的確保やポインタの扱いなど)。small modelに比べて致命的バグが
発覚しやすいという利点もある。特に自前のライブラリが蓄積してくるとメモリモ
デルの変更が大変なので、小さいプログラムを書く時からlarge model一本で行く
のが良い。small modelではバグなしに動くのにlarge modelでコンパイルするとエ
ラーになるケースは多いが、逆は少ない。(small modelで使う far,huge pointer
は一種の方言であることに注意)。また処理系のバグもlarge modelではよく見つ
かる(??)との噂である。
アセンブリ出力
某I氏のように機械語16進で直接書ける人はともかく、せっかく高級言語を使う
のだからコンパイラにアセンブラのソースファイルを出力させることは労力削減に
重要な意味がある。ちょっと難しいコードを書いてみて、どのように展開されるの
かを見るのは日頃バグに悩まされるマニアの楽しみである。またアセンブラソース
に手を入れることによって高速化を計るのも最終手段として有効である。特に現状
では80386,80387をMS−DOS、OS/2で32ビットマシンとして使うためには
この機能が必要である。
ここまでの全般的な話はどこにでも書いてあることが多いので、言語そのものに移
ろう。といいつつも例によって寄り道が非常に多い。
ポインタ
ポインタはCプログラミングには必須のものであり、これを多用するマニアはプ
ログラムをわかりにくくする原因となることが多い。そこでポインタを使用するの
は以下の場合に限定するようにすすめる。
1)型宣言・プロトタイプ宣言
2)文字列操作
3)アドレスを直接要求する関数へ (scanfなど)
4)抽象的ポインタ:構造体を関数に渡す目的など
4)についてはpascalのポインタと同様に考えていただいてよい。この用途に用い
る構造体はtypedefされているものとする。FILE型などが典型的である。FILE *fp
として当然のごとく使っている(しかも内容は参照しないであろう)はずであるが、
抽象化された変数であるため、ポインタとして意識することは少ない。
2重ポインタ(char **s)は、型宣言などを除き、直接に操作することは避けた方
がよい。1レベル下の一時変数を作っても、そちらを使うべきである。
(例)
void main(int argc,char **argv)
{
int i;
for (i=1; i はシステムファイル
#include "..." はユーザーファイル
というのは常識であるが、フロッピー時代の名残か、間違いではないがいまだに
#include "stdio.h" という参考書をみかける。
CPU速度・記憶媒体にもよるが、筆者の場合、普段使われるinclude fileをすべ
て読み込む many.h というヘッダーを作っている。(all.hでないのはassert.hな
どを含まないため)
この利点は
1)記述の手間が省ける(これが1番)どの関数がどのヘッダーを必要とするか覚
える必要がない。
2)コンパイラ依存性を軽減できる。現在のコンパイラではメーカーによってどの
関数をどのinclude fileに入れてあるかが多少なりとも異なるが、全部入れておけ
ば少なくとも宣言もれはなくなる。またコンパイラによって、includeする順序が
決まっているものがあり、逆にするとエラーになるケースがある。そういうところは
#if defined MSC
#include ...
...
#endif
#if defined TurboC
#include ...
...
#endif
(双方で定義が違うマクロなどの調整もここで行う)
として避けることができる。(もちろんコンパイルオプションでコンパイラ名を
defineする) これを使うことによってTurbo-C <-> MS-C の移行はノーエラーで
済ますことができた。
余談であるが、MS-CからTurbo-Cに移行した時のこと、Turbo-Cのたち上げをする
システムディスクをつくり、必要なファイルをRAM Diskに転送するようにした。リ
セットをしてTurbo-Cをたち上げ、これまでに作ってあるいくつかのプログラムを
コンパイルしてみた。MS-Cに比べて相当早くなって喜んだのだが、どうもRAM Disk
容量が少ない。実は最初にシステムディスクを作る際にたちあげたディスクがMS-C
のもので、自動的にRAM-Diskにファイル転送をしていたのだった。そのRAM Diskは
外付のものだったので、resetしても内容は消えずMS-Cのファイルがそのまま残っ
ていたのである、Turbo-Cのたち上げバッチファイルの if not exist c:¥stdio.h
copy a:¥stdio.h c:¥ などのため、Turbo-Cのheaderのコピーがなされなかったの
である。つまりMS-CのheaderでTurbo-Cでコンパイルしてしまったのである。両者
のANSI準拠度の高さ、many.hの威力がうかがえるエピソードであった。
3)既に存在する関数・定数・マクロとの衝突を避けることができる。Cの全ての
関数などを覚えているわけはないので、誤って同じ関数名を作ってしまうと発見困
難なエラーを起こすことがある。また内部で使われている関数などとの衝突を避け
るため、"_"で始まる識別子は避けるようにする。
ユーザー定義include fileについての注意
1)いつも使うマクロなどを収めたもの:一種のC言語の拡張と考えられるので、
many.hに入れて常時includeするようにしてもよい。
typedef unsigned char uchar;
typedef unsigned int uint;
#if !defined max
#define max(a,b) ((a)>(b))?((a):(b))
#endif
のようなもの。
2)関数型宣言を伴うinclude file
中規模ソフトの場合はよく必要になる。enum,構造体のtypedefなどを含むもので、
原則的にそれらの新しい型に対する演算を行う関数群のプロトタイプ宣言を含む。
この場合、include fileの間の依存性が問題となる。例えばファイル出力を伴う関
数でFILE型を使っていれば、それ以前にstdio.hをincludeしておく必要がある。そ
のため、これらのinclude fileは最後にincludeすべきである。
また、graph.hなどにおいて、Point型を定義して、それを別の系統のinclude file
で使う場合(例えばマウス操作)もユーザー定義型の間でも依存性が生じる。その
ため、include fileの最後に、そのファイル名をdefineすべきである。
いうまでもないが、header fileには変数や関数本体を定義してはならない。
(例)
File: graph.h
#if !defined GRAPH_H /* すでに他のファイルからincludeされている */
...
#define GRAPH_H
#endif /* #if !defined GRAPH_H に対応する */
File: mouse.h
#if !defined MOUSE_H
#if !defined GRAPH_H
#include "graph.h"
#endif
...
#define MOUSE_H
#endif /* #if !defined MOUSE_H */
のようにすれば、
#include "graph.h"
#include "mouse.h"
と書いても
#include "mouse.h"
と書いても
#include "mouse.h"
#include "graph.h"
と書いても、同じ動作を行い、未定義エラーや多重includeを防ぐことができる。実
際には多くのコンパイラは同じ内容であれば多重定義をしても文句を言わないこと
が多いが、念のためこのようにしておくべきである。Cコンパイラに付属している
header file に不必要なほど多くの条件コンパイルが含まれているのはこのためで
ある。もちろんあまりに多くの定義を行うとコンパイルの際にメモリ不足になるこ
とがあるが...
include fileにおけるメモリモデルやデータサイズのチェックの方法
多くのMS−DOSのCコンパイラでは、メモリモデルを示すシステム定数を
defineしているが、これは互換性に乏しい。実際のプログラムではポインタサイズ
に依存することがあったり(データを内部表現のままファイル出力する時など)、
混合メモリモデルになることを避けるため、header fileにチェックの機能を設け
たいことがある。ANSIでは認められていないが一部のコンパイラでは
#if sizeof(char *) != 4
#error use large model
#endif
のようにsizeofを使い、エラーを発生させることができる。これが使えない場合は
チェックプログラムを作成するか(あるいはmainの最初に組み込み、エラーを発生
する)必要がある。
数値型の範囲をチェックするには limits.h に定義されており、それを使うことが
できるので、ある値を収めるのに必要な型をコンパイル時に定義することができる。
ここでは条件コンパイルとtypedefが活躍する。
(例)
#define MYMAXINT 10000000 /* 必要とする最大の整数 */
#if MYMAXINT < INT_MAX
typedef int INT;
#else if MYMAXINT < LONG_MAX
typedef long int INT;
#else ...
...
#endif
移植性を高度に保つ必要があるときは、これらの再定義された型を使い、オリジナ
ルの型であるint,long等は使わないようにすればよい。
なお、これと同じ機能をsizeofで実現するのは(そのコンパイラがプリプロセス
でsizeofを使うことを認めていても)不可能である。sizeofはあくまでメモリ上占
めるサイズであって、それが全部使われている保証はない。INT_MAX < LONG_MAX
であっても sizeof(int) < sizeof(long) とは限らない。
標準BASICなどでこのような抽象化をすることは難しいし、複雑でトリッキーな
技を使うと(多くの場合DEF FNがからむ)驚くほど遅くなる。Cの場合はtypedef
が言語の一部であり、コンパイル時に標準型に戻されるため、スピード低下が起こ
る心配はない。
構造体のサイズを知る必要がある時(特にbyte allignmentか、word allignmentか
はコンパイラスイッチで制御できるので重要である)もsizeofを必要とするので最
初と同様の対処を必要とする。bit fieldやunionを使う場合(このような用途はほ
とんどOSとのインターフェースに関連するか、記憶域の節約のためなので)必要
メモリを予測し難いことがあり、特に注意を要する。
関数ポインタを使う場面
Cの関数ポインタは言語仕様のなかでも難しい部類に入るだろう。できれば使わ
ずに済ませたいところだが、絶対必要になるケースが存在する。qsort(),signal()
などに渡すものがそうであるが、ユーザープログラムでも独自の関数ポインタを必
要とする場面がある。例を示そう。
まず、簡単な話だが、ある内容を画面、ファイル、プリンターに出力する必要が
ある時には当然のごとく次のような関数を作成するだろう。
int listdata(FILE *output,DATA *data etc);
そして、場面に応じて
listdata(stdout,data);
listdata(stdprn,data);
listdata(fpout,data);
のようにすればよいことは明白である。これならばBASICでPRINT #1,...として使
うのと同じである(某BASICではOPEN "SCRN:" FOR OUTPUT AS #1とすると漢字が出
せないことがあったが)
これを使えばlistdataの汎用性が非常に高まる。そこでこれをさらに拡張するの
が関数ポインタである。例えばグラフを作成する関数を作成したとしよう。
int writegraph(char *starname,JD jdstrat,JD jdend,MAG magmax,MAG magmin,etc..);
ところが、ここでXYプロッターを買ってしまったとしよう。XYプロッターのために
新たなXYwritegraph()などを作っていては多くの場合大変なむだが生じる。なぜな
らばグラフィック画面へのプロットとXYプロッターへのプロットは、プログラマの
側からみるとごく下位のレベルの関数の違いでしかなく、writegraphが本来持って
いるdatabaseからの高レベルのデータの抜き出し、座標計算や観測者パターンの呼
び出しなどの大部分は共通だからである。そのため相違のある関数だけをまとめて
関数ポインタ(からなる構造体)とし、それをwritegraphに渡すようにするほうが
よほど有効である。ディスプレイのノーマルモード、高解像度モードの差を吸収す
るのにも適している(CPU速度が気になるが、渡す座標は論理座標とする)。
int writegraph(PLOTFUNC *plot,..other data..);
writegraph(normalgraph,data..);
writegraph(highresograph,data..);
writegraph(XYplotter,data..);
typedef int (*GR_PSET)(Point point,Color color,Mode mode);
typedef int (*GR_LINE)(Point start,Point end,Template templ,Mode mode);
etc..(ウィンドウシステムなどのためクリッピングが必要ならばさらに引数が増える)
typedef struct {
Rectangle gr_range;
GR_PSET gr_pset;
GR_LINE gr_line;
GR_CIRCLE gr_circle;
...
} PLOTFUNC;
/* 関数本体の例:ソースファイルのどこかにある */
int XYplot_pset(Point p,Color c,Mode mode)
{
...
/* サポートできないか、いらない引数は無視すればよい */
}
PLOTFUNC FuncXYplotter={XYplot_pset,XYplot_line,etc.};
とすればよい。これをどのような記憶クラスにおくかはユーザーに任せるが、あまり
globalにはしないほうがよいであろう。例えば
file:graph.h
typedef enum {NormalGraph,HighGraph,XYPlotter} PlotDevice;
extern PLOTFUNC *GetPlotfuncPtr(PlotDevice device);
file:grplot.c
static PLOTFUNC FuncXYplotter={XYplot_pset,XYplot_line,etc.};
static PLOTFUNC FuncNormalGraph={XYplot_pset,XYplot_line,etc.};
...
static int XYplot_pset(Point p,Color c,Mode m)
{
...
}
/* and other functions */
/*global*/ PLOTFUNC *GetPlotfuncPtr(PlotDevice device)
{
switch (device) {
case NormalGraph:
return &FuncNormalGraph;
case HighGraph:
return &FuncHighGraph;
case XYPlotter:
return &FuncXYplotter;
...
default:
return NULL;
}
}
として、ユーザーが呼び出すときには
writegraph(GetPlotfuncPtr(XYPlotter),data..);
と用いれば、プロットのための関数を見かけ上隠すことができるし、大域変数を使
う必要がなくなる。もちろん欠点もある。この方法で作ったプログラムには、実際
に必要な以上の多くの関数がリンクされてしまう。(プロッターを使うことのない
プログラムでもグラフィックを使えばプロッター用の関数がくっついてしまうので
ある)これはprintfなどでも同じである(%dしか使っていなくても、%s,%ld,%cな
どのためのルーチンは必然的についてくる)もっとも、プログラムが大きくなると
この違いは大きな問題ではなくなる。もしメモリが足りなくなったらMS−DOS
の限界を叫び、OS/2導入の口実にすればよい。OS/2であればこのような低
レベル汎用関数はdynamic link libraryに入れるのが普通なので、メモリ効率はず
っと良くなる。
実行速度の低下も問題になる可能性があると想像されるであろうが、実際に行っ
てみるとまったく問題にならない(graphic routineのできばえの方がよほど影響
が大きい)
さらに、writegraph以外の高レベル関数に対して同じ手順で呼び出しが可能であ
ることのメリットは大きい。ちょうど普段のプログラミングでデバイスドライバを
意識しないように、上位関数は実際の動作を考える必要がなくなる。
なお、関数ポインタを介した関数の呼び出しは、普通のポインタとやや趣が異な
り、一見して関数を呼んでいることがわかりにくい可能性があるので、記述を明確
にするため、マクロを使うとよい。
#define call(func) (*(func))
call(plotfunc->gr_pset)(point,White,PSET);
次のような例も考えられるが、凝りすぎの感が強い。
call(GetPlotfunPtr(NormalGraph)->gr_pset)(point,color,mode);
ここで述べた程度の例であれば、関数ポインタを使わない解決法もある。例えば、
psetなどの関数のパラメータにdeviceも渡すようにすることである。
int pset(Point p,Color c,Mode m,PlotDevice device)
{
switch (device) {
case NormalGraph:
return Normal_pset(p,c,m);
case HighGraph:
return High_pset(p,c,m);
...
などであるが、あまりエレガントでない。新しいデバイスをユーザーが利用する場
合には、多くの関数を書き直す必要がある。それに比べ、関数ポインタを使う方法
であれば、ユーザーは「まったく新しい」関数を数個作成して、それへのポインタ
を渡せばよいので、既製のルーチンに手を加える必要がない。
もうひとつ利点としてあげられるのは、実際に出力を行うモジュールでの最適化
がやりやすいことである。通常のグラフィック画面への書き込みの場合はどこに描
いても速度に差はないが、プロッターではペンの移動を最小にするような動きが望
ましい。そのような場合、XYplot_lineなどの関数で作図順序を高度に最適化する
ことが、他から見えない形で可能である。知らない間にバージョンアップをするこ
とができる。
プリンターを使う場合のstdprnとfopen("prn","w")
どちらを使っても動作はほぼ同じであるが、処理系によっては、MS−DOSの
テキスト・バイナリのモードに違いが生じることがある。どちらかと言えばstdprn
をすすめる。両者を混在させてはならない。というのは、fopenで開けた場合は(処
理系によっては必ずしもそうではない可能性があるが)stdprnとは別のファイルバ
ッファが割当られるからである。両者が別々に出力バッファリングがされる。
ゆえに
FILE *prn;
prn=fopen("prn","w");
fprintf(stdprn,"abc¥n");
fprintf(prn,"def¥n");
では、abc,defのいずれが先に出力されるかは不定である。但しfflush()を使えば
強制的に出力バッファを空にすることができる。
fprintf(stdprn,"abc¥n");
fflush(stdprn);
fprintf(prn,"def¥n");
fflush(prn);
特に、ユーザーがファイル名を入力してそのファイルに書き出すと同時にプリン
タ(他の標準デバイスでも同様)に出力を行うような場合は注意が必要である。入
力された文字列がPRN,CONなどと解釈できないかチェックする必要がある。
望ましいことではないが、BIOSに直接データを渡してプリンタを制御することが
あるが、この場合はMS−DOSのバッファflushのタイミングをさらによく検討
する必要がある。
エラー処理
Cプログラミングに慣れてくると(特にボトムアップ式を取っている場合)、関
数の入口や、要所要所で値のチェックを行うようになる(はずである)。
int func(FILE *fp,int number)
{
if (fp == NULL)
return FAIL;
if (number <= 0 || number > 10000)
return FAIL;
etc.
もっと親切な人ならば、
if (fp == NULL) {
fprintf(stderr,"ファイルポインタがNULLになっています。"
"使い方が間違っていると思われます。¥n");
return FAIL;
}
if (number <= 0 || number > 10000) {
fprintf(stderr,"与えられた数値が定義域に入っていません。"
"使い方が間違っています。¥n");
return FAIL;
}
etc.
あるいは、回復不可能なエラーが発生した場合など、その場でexit()することも多い。
if ((p=malloc(1000)) == NULL) {
fprintf(stderr,"Not enough memory .. execution terminated¥n");
exit(1);
}
などは最もよく見かけるエラー処理である。日本語環境に慣れている人は、おそら
く日本語でメッセージを書くであろう。
fprintf(stderr,"メモリが足りません。実行を中断します。¥n");
多くの分割コンパイルを行うと、全てのファイルに同じ様なエラーメッセージが入
ることがあり、他のデータと合わせれば64KBを越えることも有り得る(huge model
でなければ64KBを越えるstaticなデータは使えない)。エラーメッセージだけでメ
モリを浪費するのもバカな話だが、他人が誤った関数呼び出しをすることを考慮す
る必要もあるので少なくともエラーメッセージは残しておきたい。このような場合
には、エラーメッセージだけを集めたファイルをつくり、エラー番号でそのエラー
を表示する関数を作ればよい。特にmalloc, fopen などといった標準関数の場合は
多くのプログラムに共通のエラーメッセージファイルを作成するのも容易であろう。
可能性のある全てのエラーのマニュアルを抜き書きしておくのもおもしろい。
例えば
0001 malloc():メモリが足りません。メモリを増設する・不要なデバイスドライバ
や常駐プログラムを削除するなどして、もう一度試みて下さい。あるいは過去に取
得したメモリが解放されていないのかもしれません。プログラムをチェックして下
さい。
0002 fopen():指定されたファイルがオープンできません。ファイル名が不適当・同
時にオープンできるファイルの数が少なすぎる(config.sysを調べて下さい)ディ
スク容量が不足している・メモリを使いきっているなどの原因が考えられます。
というような、ほとんどhelpのようなエラーメッセージを出すことさえ可能である。
そしてこのファイルを例えば errmsg.err といったファイル名で、適当なディレク
トリに格納し、そのディレクトリ名を環境変数に設定するようにしておけばよい。
もし環境変数が設定できていないときや、errmsg.err が存在しない場合には、単に
error 0001
とでも表示するようにしておけばよい。この方法が便利なのは、errmsg.errファイ
ルを取り替えれば日本語でも英語でもメッセージが出せるということである。外国
にもソフトを売り込もうと考えている人は、今からでも作り始めて損はない。(全
くOS/2の発想だ!)
もちろんエラーメッセージに限らず、一般のメッセージ(入力要求など、あるい
は場合によってはアイコンメニューでさえ)についても同じ事が言える。その場合
はプログラム毎に別のメッセージファイルを用意したり、文字列の長さに依存しな
いプログラムにしたり、といった配慮が必要になる。ただしこの方法はハードディ
スクを使うか、起動時に全部を読み込んでしまうか(そうすればstaticデータでな
いので64KB以上を扱える)でもしないと、「反応が遅い」とか、Ki氏あたりにボロ
クソにけなされる可能性があるので注意を要する。またこのようなプログラムにな
るとinstallが難しくなるので、セットアッププログラムを準備するのもよい。ファ
イルは普通の方法で書換えができないようにする必要もある。
エラー出力を要求する側のプログラムは、例えば
if ((p=malloc(1000)) == NULL) {
syserrout(stderr,1);
exit(1);
}
とか、適当な定数を用いて
syserrout(stderr,MallocFail);
のようになる。そのほか出力デバイスへのファイルポインタを送るのではなく、出
力関数へのポインタをわたす・fatal error/non-fatal errorを区別するなど、いろ
いろと工夫されたい。
また、プログラムの保守の点からは、このようなerrorが発生した時の原因究明
のため、error発生時の状況をファイルに出力し、それをerror reportとしてもら
うなどの手法が考えられる。
高度なエラー処理の例
上のような例ではエラーが関数の戻り値から判定できるので分かりやすい。しか
しまれには信じられないようなエラーが起こることがある。
while ((c=fgetc(fp)) != EOF) {
putchar(c);
}
といったループは、ファイルエンドに達すればいずれ脱出条件が満たされるもので
ある。しかし実際には、ディスクのFATテーブルがおかしくなって無限ループに陥
ることがないとはいえない。特に厳密なエラー処理を希望する場合は、さらにルー
プ回数のカウントを行い、非常識な値(例えばディスク容量以上)になったらエラ
ーを発生して強制的にループを抜けるやり方をすすめる。
実際に遭遇する可能性があるのは、出力専用デバイスから入力を実行した場合な
どである。mifes prn などを実行してみるとわかるが、mifesはある所で入力を打
ち切る。これが別のエディタでは無限ループとなってしまうのである。あなたの愛
用のエディタの性能を試すため、clock aux nul などのデバイスをファイル名とし
て与えてみることをすすめる。ユーザー組み込みデバイスの場合は前もってデバイ
ス名をチェックできないので、さらに厄介である。(当然であるが、ATOK,MOUSE,
RAMDISKなどにもデバイス名は存在するのである) 筆者の場合はとりあえずopen()
でオープンしてみてそれをisatty()で調べる(キャラクターデバイスの場合は区別
可能)方法でisdevice()関数を作っている。
Return to the Daisaku Nogami, Daisaku Nogami