初級C言語Q&A(9)

初出: C MAGAZINE 1996年2月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


処理系依存の問題

 初回に、いくつかの典型的な処理系依存の問題を紹介しましたが、C言語の場合、 実際に使うプログラムを作ろうとすると、なぜか処理系依存の処理が必要になる というおかしな傾向があります。


文字コード


Q 【ASCIIコード】

 ASCIIコードとは何か。

 ASCII(American National Standard Code for Information Interchange)とい うのは、アメリカで用いられている標準規格の文字コードです。コンピュータ技 術は常にアメリカが先行して他を引っ張るという状態で進歩してきましたから、 アメリカの規格はコンピュータ全般に対して大きな影響力を持っています。例え ば、C言語の世界においては、まずANSI(American National Standards Institute) というアメリカの規格が決まり、それを用いる形式でISO(International Organaization for Standardization)が国際規格を制定し、さらに追従するとい うことでJIS(Japanese Industrial Standard)が決まったという推移があります。

 ただし、C言語で、特に日本で使われている「ASCII文字」という表現は、それ ほど厳密な意味ではなく、単に1バイト文字の中の0〜0x7Fに割り当てられている ものを指すことが殆どです。日本で使われているパソコンの大部分は、実際はASCII 文字ではなくJISの文字もどきを表示するように設計されています。例えば0x5cに 該当する文字はASCIIではバックスラッシュ(\)が割り当てられていますが、JIS では円記号(¥)になっています。俗にASCII文字と呼ぶ場合は、このような差異は 無視して、漢字ではない1バイト文字の意味で使われることが多いようです。


Q 【改行コード】

 改行コードとは何か。

 C言語では '¥n' で表現される文字のことです。意味は「現表示位置を次の行の最初の位置に移動 する」(JISによる定義)です。

 さて、これがどのような値のコードのデータとして表現されるかという問題が あります。実はこれは簡単な話ではありません。結論からいえば、0x0a、0x0d、 またはこの二つの組み合わせとして使われることが多いようです。

 0x0aはしばしばLF(line feed)と略記されます。これを単純に解釈すると改行と いう意味になるのですが、場合によっては、'¥n'と同じ働きを持つコードとして 扱われることもあるし、単に行を改めるだめ、すなわち現表示位置を現在の桁の 次の行の位置に移動する特殊コードとして扱われることもあります。

0x0dはCR(Carriage Return)と略記されます。これはC言語の表現では'¥r'に相 当するコードになることが多く、意味としては「現表示位置を現在の行の最初の 位置に移動する」ということになります。

 そこで、MS-DOSでよくある解釈としては、各行の最後が0x0a、0x0dの2バイトに なっている、というものです。これは0x0aにより次の行に移動し、0x0dにより今 いる行の先頭に移動する、と考えることができます。すると、 '¥n' というのは一体どんなコードなのか?という疑問が発生します。

 C言語は、最初UNIXの世界で発達しました。UNIXの環境下では、改行コードは0x0a だけというのが一般的で、0x0dは不要です。従って、UNIXで作成したドキュメン トを文字コード変換だけ行ってMS-DOSに持ってきたりすると、改行コードの変換 を忘れてしまい、見た目がどうもおかしい、という事件が発生することがありま す。


Q 【EOF】

 NIFTY-Serveのログを後で読もうとしたのだが、発言は200個程あるはずなのに、 125番目から後を見ることができなかった。なぜか質問したら、発言にEOFが混ざ っているのだろうと言われた。何のことか?

 これはよくある話です。いくつかの処理系においては、EOFと呼ばれているコー ドがテキスト中に現れると、それでファイルが終了したとみなし、それ以降に入 っているデータは一切関知しないということになります。これは具体的には Control-Zに割り当てられているコードです。

 C言語では、単に EOF といえば、ファイルの終了を意味する値であり、大抵の場合 <stdio.h> で定義された-1という値のことを意味します。この質問でいうところのEOFは、 この定義とは関係なく、Control-Zに相当する文字コードとして使われているの で、紛らわしいですが混同しないよう注意が必要です。

 一部のワープロ、エディタを使って文章を作成した場合に、文章の最後に Control-Zが勝手に付加されてしまうので、それをそのままネットで発言した場合 に、さらに、そのコードがそのまま掲示されてしまうということがあります。そ して、読む側はそのコードをログに取り込んでしまうと、テキストモードを使っ てファイルを処理しているようなソフトウェアを使ってこれを見ようとした時、 この位置から後に書かれている内容が消えたように見えてしまうわけです。

 このようなファイルからControl-Zを取り除くだけのプログラムを作るのは難し くはありません。バイナリモードで問題のファイルをオープンして、順次文字を 読みだし、Control-Z以外の文字をそのまま書き出せばよいだけです。

#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "[使い方] cutz 元ファイル名 新ファイル名¥n");
        exit(1);
    }

    fp_in = fopen(argv[1], "rb");
    if (fp_in == NULL) {
        fprintf(stderr, "ファイルを読むことができません¥n");
        exit(1);
    }

    fp_out = fopen(argv[2], "wb");
    if (fp_out == NULL) {
        fprintf(stderr, "ファイルを書くことができません¥n");
        exit(1);
    }

    while ((c = getc(fp_in)) != EOF) {
        if (c != CTRLZ) {
            putc(c, fp_out);
        }
    }

    fclose(fp_out);
    fclose(fp_in);
    return 0;
}

入出力


Q 【fopenのバイナリモード】

fopen のモードにテキストとバイナリがあるが、これらの違いは何か。

 バイナリの場合の方が考え方は単純で、書き込んだデータに含まれているあら ゆるコードがそっくりそのまま処理される、ということになります。

 テキストの場合、考え方としては「行」という単位が処理の単位となります。 各行は改行文字を付加した0バイト以上の文字の並びです。そこで、前述の質問に あったような、改行文字とは何か、という問題が発生することになります。また、 処理系によっては、ファイルの最後という概念が導入されていることがあります。

 テキストモードでファイルをオープンした場合、読み取った行の改行文字の直 前に書かれている空白文字の並びが、データが読み込まれる時に現れるかどうか は、処理系定義になっています。つまり、もしかすると行末の空白は読み飛ばさ れたように見えるかもしれません。


Q 【ファイルを縮める】

 あるファイルの最後をカットする処理が欲しいのだが、どの関数を使えばよい のか分からない。

 不思議なことに、ファイルのサイズを縮小するための標準関数は用意されてい ません。つまり、この処理を一つの関数呼び出しで行うには、処理系依存の特別 に用意された関数を使うしかありません。

 この処理を標準関数だけで実現するためには、最後をカットしたいファイルを 最初から必要な所まで読み、別のファイルに書き出し、その後、書き出したファ イルと元のファイルを置き換えるという処理が必要になります。

(参考: c.l.c FAQ 19.13)


Q 【ファイルの途中を編集する】

 ファイルの途中にある一行だけを変更したい。ファイル全体の長さが変わって しまうのだが、何かうまい方法はあるか。

 標準関数の中に、ファイルの途中の一部を書き換えて、長さまで変更するよう な機能を持ったものはありません。

 もし長さを変えずに済むのなら、ファイルを更新モードでオープンすれば、途 中の内容を変更することは可能です。

(参考: c.l.c FAQ 19.14)


Q 【文字の修飾表示】

 画面上の文字を反転させたり、色を変えて表示させたいが、どうすればよいか。

 これも、処理系に完全に依存する問題です。そもそも、その画面にどんな色が 何種類から選択できて、それらがどのような色コードとして割り当てられている か、ということに対する標準的な解決方法がありません。

 printf を使って文字を表示する場合には、俗にエスケープシーケンスと呼ばれている一 連の特殊文字列を使ってこの目的を達成できることがあります。または、textmode のような処理系依存の関数を使って表示モードを変更できる場合もあります。

特にUNIXの環境の場合、画面表示に用いられるエスケープシーケンスは、いろ いろな端末によって異なるため、termcapやterminfoというデータベースエントリ にあらかじめ属性変更用のシーケンスを登録しておき、それを参照して文字属性 を変更するようなライブラリを使うことになります。


Q 【キーボードからリアルタイム入力】

 画面上にメニューを表示して、ユーザーに何かキーを押させて、それに応じて 必要な処理に分岐するというプログラムを作ろうとした。しかし、キーを押した 時には何の反応もなく、リターンを押すと始めて処理が始まるようだ。リターン キーを押さずに分岐させる方法はあるか。

 この現象は、標準入力がバッファリングされているために発生します。すなわ ち、入力はリターンまでの一連のキー操作の結果として、初めてプログラムに渡 るように設計されているのでしょう。このような仕組みを用意しておけば、途中 でキーを間違えたことに気付いた時にバックスペースで編集したりすれば、プロ グラムには訂正後の正しい入力だけが渡るようになり、途中の編集のような複雑 な処理のことはプログラムで意識しなくても済むようになります。

 ですから、単純な解決方法としては、ユーザーにリターンキーまで押させて、 その後、リターンキーが入力されるまで標準入力を読み飛ばす処理を追加すると いう手があります。

    c = getchar();
    while ((dummy = getchar()) != '¥n')
        ;
 こうすることによって、メニューの1を実行したいのに2を押してしまったよう な場合に、バックスペースで訂正して1にしてからリターンを押す、という逆戻り が可能になります。ただし、それだけユーザーの操作が面倒になるという考え方 もあるでしょう。

 どうしてもキーを押した瞬間に処理させたいのなら、各処理系に依存した入力 関数を使うしかありません。MS-DOSなら getch() 、UNIXの環境なら ioctl 関数で端末の状態を変更したり、cursesライブラリを使って目的が達成できるで しょう。そもそも、最近のGUI環境であれば、これらの概念から全く独立したGUI ライブラリが用意されていて、メニューの処理を書くことができるはずですが、 その具体的な方法は、ますますC言語とは無関係な処理系依存の話になってしまい ます。


Q 【キーボードバッファの状態】

 キーボードの先行入力バッファに文字が入っているかどうかを、あらかじめ知 るにはどうすればよいか。すなわち、ある時点で入力文字が既にタイプされてい ることを知る方法はあるか。

 しばしば必要になるにもかかわらず、この処理は完全に処理系に依存していま す。標準的に解決する方法はありません。

 一般に、何も読み込む文字がない場合にも即座に処理を終えて戻ってくるよう な入出力のことを「ノンブロッキングモードのI/O」「ブロックしないI/O」のよ うに表現します。関数 open には O_NDELAY というオプションが使えることがあります。MS-DOSのキーボードの状態を知るた めには、biosを直接呼び出す関数を使うか、あるいは kbhit() のような関数が非標準関数として用意されていることを利用します。

 標準入力、パイプからの入力、端末やシリアルポートからの入力をリアルタイ ムで処理したい場合に、現在バッファにデータがあるかどうかの判断が重要です。 例えば通信ソフトを作る場合に、シリアルポートに何かデータがあればそれを受 け取って処理して、何もデータがなければそれ以外の処理をする、というような 設計が考えられますが、これを実現するためにはシリアルポートを読む前に、そ こにデータが入っているかどうかを知らなければなりません。

 ノンブロッキングモードでオープンしたファイルから、 fread 等の関数でデータを読む場合には、必ずしも用意したバッファサイズと等しいだ けデータが読まれているとは限らないことに注意が必要です。場合によっては、 バッファの途中までデータが入った状態で戻ってくるかもしれません。何バイト のデータが得られたかどうかを関数の戻り値によって確認して、必要なだけ処理 するようなコードを書くことになります。


その他


Q 【make】

 makeとは何か。

 C言語の一つの特徴に、プログラムをモジュール化して分割コンパイルし、最後 にリンクという処理で一つのプログラムを完成できるということがあります。モ ジュールに分割することによって、独立した処理を分離し、局所的な見通しをよ くしてバグの入り込む余地を減らす効果が見込めます。

 プログラムをこのようにして部分に分けると、ある一部を変更した場合に全て のファイルを再コンパイルしなくても、変更のあったファイルだけをコンパイル し直せばよいということが頻繁に起きます。この処理を手作業で行うのはばかげ ています。基本的な考え方としては、ソースファイルの変更時刻をチェックし、 前回コンパイルした時より後に更新されたファイルだけをコンパイルして、全体 をリンクし直せば、必要な作業は完了するはずです。この作業を自動的に行うプ ログラムがmakeと呼ばれているものです。

 実際は、それほど話は単純ではありません。例えば、いくつかのファイルが共 通して読み込んでいる "header.h" を更新した場合には、関連したファイルを再コンパイルする必要があります。こ のように、どれを更新したら何を再処理しなければならない、というルールをあ らかじめ記述しておき、それに従ってmakeが処理する、というのが一般的です。 このルールを記述するファイルは、歴史的にmakefile、あるいはMakefile、 MAKEFILEのような名前にすることが標準的です。

 ところが、make自体はPDSやフリーソフトのものや、市販のコンパイラや処理系 に付属しているものなど、いろんな種類のものがあって、大筋ではかなり似てい るのですが、細部の機能は違っている、という困った状況になっています。例え ばBorland C++で処理できるmakefileをそのままMSCで使おうとしたら動かない、 というようなトラブルがしばしばありますから、注意が必要です。


Q 【スタックの大きさ】

 大きな配列を使ったプログラムを動かそうとしたら、実行中に何の反応もなく なったようだ。何が悪かったのだろうか。

 配列をメモリのどこに確保しようとしたのでしょうか。自動変数の配列は、ス タックと呼ばれる、使い回しの効くエリアに確保されることがあります。この種 のメモリ獲得は、関数の呼び出し毎に行われるため、その関数が終了した時点で メモリは開放され、同じ領域を何度も別の用途に使うことができるというメリッ トがあります。ただ、この場合、スタックのサイズは意外と小さいことがあって、 しかも、あまりに大きな配列を獲得しようとしてスタックを全て使い尽くしてし まった場合に、一体何が起こるか分からないという欠点があります。スタックは 通常は十分な大きさを持っていますが、巨大な配列をスタック上に置くことはあ まり想定されていません。特に、多重の配列は、直感的にイメージするよりも大 量のメモリをあっという間に必要とすることを頭に入れておいてください。例え ば char a[100][100][100] を扱うために必要なメモリは、100×100×100バイトで、約0.95MBになります。

 処理系によっては、コンパイル時や、あるいは処理系依存のグローバル変数を 指定することによって、スタックを大きさを変更することができます。ただし、 特にx86系のCPUの場合、上限が64KBに限られてしまうといったこともあるので、 油断はできません。

 大きな配列を使うなら、それを static で宣言するか(おそらく、static変数はスタック以外の領域に配置されます)、あ るいは malloc のようなメモリ獲得関数を使って、空きメモリの容量を監視しながら、メモリ不 足時に適切な処理を行うことを想定したコードを書く方が安全です。


Q 【自動配列の初期化】

 次のプログラムはなぜコンパイルできないのか?
#include <stdio.h>

int main(void)
{
    char s[14] = "hello, world!";

    printf("%s¥n", s);
    return 0;
}

 といっても、多分現時点では殆どの処理系で何の問題もなくコンパイルできる でしょう。ANSI Cは自動変数の配列を初期化できる仕様を定めていますが、それ 以前のコンパイラは、このような配列を初期化することはできませんでした。特 に、ANSI Cへの移行が進んだ頃に発売されたコンパイラの中には、ANSI C準拠と 宣伝しながら、この配列が初期化できないものがありました。

 配列を static で宣言するか、 strcpy を使ってプログラムの中で明示的に初期化するように変更すれば、コンパイルで きるようになるでしょう。


(C) 1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。