初級C言語Q&A(5)

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

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


 C言語で扱える型は、あまり多くはありません。大きな整数や、固定小数点、BCD 演算などは標準機能では扱えないので、別に用意されたライブラリを使うか、自 分で作るしかありません。にもかかわらず、 intfloatdouble といったいくつかの型を使うだけでも結構落とし穴があるものです。今回は、値 の処理に関連するよくある質問をまとめてみました。


整数型


Q 【intの扱える値の範囲】

UNIXで動いていたプログラムをMS-DOSに移植しようとしたら、コンパイルでき たのだが思った通りに動作しない。調べたら、ある int の変数が負になるべきでない所で負の値になっているようだ。

 int が扱える値の範囲は、 INT_MIN から INT_MAX の間です。 INT_MIN が-32767以下 であることと、 INT_MAX が32767以上であることは、ANSI Cでは保証されています。しかし、この範囲を超 えてはいけないわけではありません。 intlong int が同じ範囲の値を扱うことのできる処理系は結構あります。言い換えれば、 int の表現に32ビットが使える場合です。この場合、少なくとも-2147483647〜 2147483647の値を扱うことができます。

 プログラムが、 long int の範囲の値を扱うことを想定し、かつ int と宣言した変数を用いている場合、 int が16ビットである処理系に移植する時に問題が生じます。

 もしこの問題によっておかしな動作をしているのであれば、プログラムの該当 箇所の int で宣言されている変数を long int に修正することによって解決するでしょう。

だからといって、 #define int long のようなことはしないように。新たな問題の種を撒くことになります。


Q 【-32768】

 16ビットで表現できる符号付き整数値は-32768〜32767ではないのか。なぜANSI Cでは、 INT_MIN が-32767までの場合を許しているのか。

 ある処理系において INT_MIN が-32768であっても何の問題もありませんが、-32767までしか扱えなくても、 ANSI Cの規格には適合することになっているだけの話です。単にそういう決まり だ、ということです。

 これは、本質的には数値の表現の問題です。-32768が許される処理系では、こ の値に対応する正の値が int に入り切らないので、面白い結果になるかもしれません。例えば、次の結果がど うなるか想像してみましょう。

    int i;

    i = -32768;
    printf("i = %d, -i = %d\n", i, -i);

Q 【整数型の選択基準】

 intshort intlong int との使い分けは、どのような基準で判断すればよいか。

 まず、-32767〜32767の範囲を超える値を使う可能性がある処理なら、 long を使うべきでしょう。 int が32ビットの処理系なら、単に int と書いても同じ結果になりますが、後から int が16ビットの処理系に移植したいという人が苦労することを考えると、 long としておく方が世のためです。

 要素数の多い配列を扱う場合は、メモリの効率を重視する必要があります。こ の場合は、 short で十分に表現できる範囲の数を扱うのであれば、 short を選択しましょう。2MBの大きさになる配列を1MBで済ませることができるかもし れません。もしかすると char で十分な場合もあるかもしれません。

 ちょっとしたループカウンタや判断のための場合分けの変数には、あまり考え ずに、 int を使いましょう。

 例えば、100回処理するためのループカウンタとして用いるのであれば、理論上 は char の変数でも十分かもしれません。しかし、この場合も単純に int の型にしておくのが普通です。

(参考: comp.lang.c FAQ 1.1)


Q 【整数のオーバーフロー】

 次のコードは50000という結果を表示してくれない。
    int i, j;
    long l;

    i = 100;
    j = 500;
    l = i * j;
    printf("%ld\n", l);

 まず、 int の大きさが16ビット、すなわち-32768〜32767の範囲の数しか表現できない処理 系であると想像します。手元にある某コンパイラで実験したら、-15536と表示さ れました。

 このような結果になる理由は、掛け算を計算している行の処理を細かく考える と分かります。まず、 i * j が計算されますが、これらは両方とも int の変数なので、その結果は一旦 int の値として保存されます。16ビットの int の範囲では50000という値は保持することができず、オーバーフローが発生しま す。その結果は-(65536-50000)で、すなわち-15536です。これが l に代入されました。

 掛け算の前にどちらかの変数を long にキャストしてやれば、期待通りの結果を得ることができます。 longint の掛け算なら、 その中間結果は long の値として保持されるからです。

    l = (long) i * j;
 キャストは、掛け算の結果が出る前に行う必要があります。キャストの優先順 位は掛け算より高いので、先程の書き方は合っています。次のように書いてはい けません。これでは掛け算した結果はキャストする前に int の範囲の中間結果として保持されてしまうので、キャストしなかった時と何も変 わりません。
    l = (long) (i * j);
(参考: comp.lang.c FAQ 3.14)

Q 【char】

 漢字を処理するプログラムを書いたのだが、思った通りに動作しない。
    static char prompt[] = ">";
    int c1, c2;

    c1 = getchar();
    c2 = getchar();

    if (c1 == prompt[0] && c2 == prompt[1]) { /* ">" */
        /* ここを実行してくれない */
    }

  prompt[0]prompt[1] が漢字コードの1バイト目、2バイト目になるというアイデアはよかったのですが、 比較する時の型が intchar なのがトラブルの原因になってしまいました。 c1c2 には、読み込んだ文字に対応した0〜255の値が入ります。処理系にもよりますが、 漢字の一部分が読み込まれた場合は、128以上の値になるでしょう。これに対して、 prompt[0] は、おそらく符号付きの char 型で、表現し得る値は-128から127の値です。これらを比較しても、期待した通り に一致してくれないのです。

 解決する方法は、いくつかあります。一つは、 prompt[] の型を char ではなく unsigned char にしてしまう方法です。それでよいのですが、 prompt を標準的な関数の引数にしたい場合に、うるさい警告メッセージが出るかもしれ ません。(例えば puts の引数は unsigned char へのポインタではなく char へのポインタというようにプロトタイプ宣言されているから)

 もう一つの方法は、0xffでマスクすることにより、0〜255の範囲の整数にして から、比較する、というものです。

    if (c1 == (prompt[0] & 0xff) && c2 == (prompt[1] & 0xff)) {

Q 【getchar】

getchar は文字を返す関数なのだから、 char の変数に戻り値を受けた。なぜこれが非難の的になるのか。

 あまり難しく考えることはありません。マニュアルを見てください。どのマニ ュアルにも、 getchar() の型は int と書かれているし、ANSI Cではそのように定義されています。従って、 getchar() の戻り値は int の変数で受けるのです。

 getchar() は、実際、0〜255の値と、 EOF という値(おそらく-1)の、合計257通りの値を戻します。これは char の範囲では受け切れません。あえて char の変数で受けるなら、 EOF と255は区別できないことになります。

 「私の処理したいファイルの中には255などという文字コードは使われていない から char で受けても実害はない」という主張を見たことがありますが、 int で受ければさらに実害はないはずです。


Q 【分割コンパイル時の型の不一致】

 複数のファイルにコードを分けて分割コンパイルしている。他のモジュールで 定義した関数の戻り値がおかしいようだ。

 関数の型が適切に定義されていないのでしょう。何も指定しなければ、外部関 数は int を戻すものと想定されてリンクされるので、 longdouble を戻すつもりなら期待した結果にならないでしょう。

 分割コンパイルする時には、呼ぶ側と呼ばれる側で、関数の型が一致していな ければなりません。プロトタイプ宣言をすることによって、この問題は解決する はずです。


浮動小数点型


Q 【関数が見つからない】

 関数 sin を使ったプログラムを書いたのだが、リンクの時にsin関数が見つからないとい うエラーが出てしまう。

 伝統的に、多くの処理系で、数値演算関数のライブラリは標準のライブラリと は別に用意されています。従って、この種の関数を使うためには、数値演算のた めのライブラリをリンクすることをリンカーに知らせる必要があります。例えば、 UNIXではリンク時に-lmというオプションを付ければうまくいくと思います。

 リンク時にライブラリを指定する場合、その順序も重要な意味を持つことがあ ります。-lmというオプションは後の方に指定した方がよいと思います。

(参考: comp.lang.c FAQ 14.3)


Q 【math.h】

 -lmオプションを付けてリンクすると「sin関数が見つからない」というエラー は出なくなった。しかし、sinの値が何か変なようだ。

#include <math.h> を忘れていませんか。コードを確認してみましょう。

 これを忘れると、 double を戻すつもりで作られている関数なのに、 int を戻す関数を呼び出したようなコードが生成されてしまうことになります。呼び 出した関数と呼ばれた関数がうまくかみあわないで、変な値を使ってしまうこと になります。

(参考: comp.lang.c FAQ 14.2)


Q 【四捨五入】

 四捨五入する関数が見つからない。

 四捨五入するための特別な関数はありません。浮動小数点の変数xに対して小数 点以下を四捨五入する最も簡単な方法は、
    (int) (x + 0.5)
 を実行することです。ただし、この方法は負の数に対して期待した結果を得る ことができないかもしれません。

(参考: comp.lang.c FAQ 14.6)


Q 【誤差(1)】

 double の変数xに1.005という値を入れて、小数点以下3桁目で四捨五入する関数を呼び 出した。結果は期待した1.01ではなく、1.00になってしまう。

 浮動小数点の値には、誤差が伴います。特に注意しなければならないのは、 (1/2)のn乗の和で表現できないような値は、たとえ10進数できりのよい小数であ っても、コンピュータの内部ではぴったりの表現ではなく、近似値として値が格 納されるということです。

 1.005というのは、見た目ほどきりのよい数字ではないのです。この場合、内部 表現としては1.0049999..のような値になってしまい、四捨五入の判断によっては 4以下を切り捨てる結果になるのでしょう。

 これはC言語の問題というよりも、むしろ一般的な数値表現の問題ですから、詳 細を知りたい型は、その種の教科書を見てください。

double型を使ったままでこの問題を解決するのは困難です。BCD 演算や固定小数点の演算を行うライブラリを使うか、そのような関数を自作する ことが、本質的な解決になります。あるいは、整数で演算し、必要な時に割り算 して値を使う、という手があります。


Q 【誤差(2)】

 次のループが期待した回数実行されないのはなぜか。
    float x;

    for (x = 0.0; x < 1.0; x += 0.1) {
        /* 必要な処理 */
    }

 これも浮動小数点の誤差が原因です。0.1を十回足しても1.0になるわけではあ りません。もし0.1きざみで10回のループを実現したいのなら、次のように書くの が定石です。
    float x;
    int i;

    for (i = 0; i < 10; i++) {
        x = i / 10.0;
        /* 必要な処理 */
    }

ブール型


Q 【真偽値】

 他の言語には真偽の値を表現するための型が用意されているものがあるが、な ぜC言語には用意されていないのか。

 断言はできませんが、想像はできます。最も大きな理由は、それがなくても実 用上差し支えないからでしょう。実際、1という値を真、0という値を偽であるこ とにすれば、整数型を使って真偽を表現することが可能ですから。

 整数型は char を使った場合でもおそらく8ビット、場合によっては16ビットや32ビットの大きさ を持ちますから、1と0という二通りの状態を表現するには極めて冗長になるかも しれません。プログラムの工夫をすれば、整数の各ビットに意味を持たせて、複 数の状態を表現することも可能です。ただし、メモリを節約した結果、処理が複 雑になったり、オーバーヘッドが増えるかもしれません。C言語は、これらの選択 をプログラマーに任せているのだ、と解釈することもできます。

(参考: comp.lang.c FAQ 9.1)


Q 【真偽の値】

 真偽の値を表現する場合、どのように割り当てるのが一般的なのか。

 一言でいいますと、真が1、偽が0とするのが一般的です。その理由は、いわゆ る条件式の判断結果がこれらの値になるからです。例えば、 (2 == 3) という式の値は必ず0になり、 (4 != 5) という式の値は必ず1となります。

 中には、真の値として-1を使うことを好む人もいます。二の補数表現では、-1 は全てのビットが1となるため、分かり安いと考えるのでしょう。偽を0とした場 合に (~0) を真とする考え方です。同様な発想で、 (!0) を真とすると考えれば、真の値は1に割り当てるのが自然です。


Q 【真偽値のマクロ】

 真や偽に対する値は、次のようなマクロで定義した方が確実ではないか。
#define TRUE (1 == 1)
#define FALSE (!TRUE)

 C言語の仕様では、上の定義によってTRUEが1、FALSEが0になることは間違いあ りません。従って、次のように書いたのと全く同じことです。
#define TRUE 1
#define FALSE 0
 マクロの定義は、結局はこのように書いたのと同じ程度の意味しかありません。

Q 【偽との比較】

真の値と比較するよりも、偽と比較しろ、と言われた。どういう意味か。

 すぐに思い付く例は、次のようなものです。
    #define TRUE 1

    if (isupper(c) == TRUE) {
        /* 大文字の時の処理 */
    }
 これは失敗します。 isupper は、 c が大文字の時に「0以外の値」を戻すとしか規定されていないからです。つまり、 1か0という値しか戻らないとは決まっていないので、 TRUE という値と単純に比較することができません。

 ただし、次の処理は、 FALSE が0である限り、正しく動作します。

    if (isupper(c) == FALSE) {
        /* 大文字でない時の処理 */
    }
 なぜなら、 c が大文字でないなら、0が戻ってくることが間違いないからです。

 また、前述のように、中には-1という値を真だと定義したがるプログラマーが いるかもしれません。そのような人の書いたプログラムに手を加える時に、真が 1であると決めてかかると、もしかすると失敗するかもしれません。これに対して 偽が0であるという前提は大部分のプログラマーで異義がない定義なので、失敗す ることが少ないのです。


Q 【真の値】

C言語の条件判断は、値が0の場合は偽、それ以外の場合は真とするのだから、 1だけを真の値とするのは変ではないか。

 例えば「 if (式) 文 」という処理において、式が0以外の時には文が実行されることになっています。 式の値が1である必要はありません。0以外の値なら何でも真とみなされます。実 際にプログラムを書く場合には、これが便利なことがあります。

 前述の大文字の処理なら、次のように書くことができます。

    if (isupper(c)) {
        /* 大文字の時の処理 */
    }
 しかし、 (x == y) のような比較演算子の結果は、1か0になることが決められているので、基本はや はり1か0と考えた方がよいでしょう。

番外編


Q 【comp.lang.c FAQ】

 comp.lang.c FAQとは何か。どうすれば入手できるのか。

 インターネットのニュースグループであるcomp.lang.cは、一日に百〜数百の投 稿が飛び交う極めて活発な情報交換の場となっています。このグループは、C言語 に関する話題を扱うのですが、これだけの投稿があると、何度も同じような質問 が繰り返され、それに対する回答があるという、お決まりのパターンに陥りがち です。傍観者は、毎回同じようなやりとりを見せられ、興味を失うか、あるいは 貴重な宝石を砂の中から探す手間をかけることを強いられるでしょう。

 このような何度も繰り返し現れる類の質問はFrequently Asked Question (FAQ) と呼ばれています。FAQに対する回答を集めたものが、いくつも作られています。 comp.lang.cで頻出するFAQのリストは、Steve Summit氏が編集して、毎月1日にcomp .lang.cに投稿されています。このドキュメントは自由に配布できるため、あちこ ちに転載されているようです。

 このドキュメントのオリジナルは英文ですが、北野欽一氏が日本語に訳したも のをfj.comp.lang.c等で読むことができます。

comp.lang.c FAQは、現在WWW上で見ることができます。また、1996年2月26日に 日本語訳が公開されています。


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