【C言語】計算結果がおかしい時の対処法まとめ

このページではC言語で計算結果がおかしい場合の原因とその対処法について解説していきたいと思います。

何回確認しても上手くプログラミングできてるはずなんだけどなぁ…

なぜか計算結果がおかしい

よくある話だよ…

でも計算結果がおかしいのには必ず理由があるはずだよ

私も結構C言語歴は長いですが、いまだに計算結果が意図したものにならなくて悩むことが多いです。

計算結果がおかしいのには必ず原因があります。

その計算結果がおかしくなる原因と、その原因に対する私が実践している対処法についてこのページにまとめてみました!

何回試行錯誤しても計算結果が意図したものにならない場合に是非参考にしていただければと思います。

MEMO

本ページに公開しているソースコードでは下記を省略していますので、動作確認時は必要に応じて追加してください

#include <stdio.h>

演算子の優先順位

まずは演算子の優先順位の問題で計算結果がおかしくなっていないかを確認しましょう!

計算結果がおかしくなる例

BMI は下記の式で計算することができます(”^” は冪乗を表しています)。

BMI = 体重(kg) / 身長(m) ^ 2

下記のようにソースコードを書いてしまうと、演算子の優先順位の問題で計算結果がおかしくなってしまいます。

演算子の優先順位
int main(void) {
    float height = 1.75;
    float weight = 60.5;
    float bmi;

    bmi = weight / height * height;

    printf("bmi = %f\n", bmi);

    return 0;
}

計算結果はいくらになるでしょう?

お?まさに僕の身長・体重と一緒じゃん

確か 20 くらいだったはず!

実行結果は下記のようになります。

bmi = 60.500004

僕はそんなに太ってない!

スポンサーリンク

スポンサーリンク

原因

数学等と同様に、プログラミングで扱う演算子にも優先順位があります。

この優先順位によって、どの演算が先に行われるかの順序が決まります。

優先順位の考慮もれで計算結果がおかしくなることがあります。

先ほどの例であれば、身長 * 身長 を先に計算する必要があるところが、先に 体重 / 身長 の計算が行われているところに計算結果がおかしくなる原因があります。

スポンサーリンク

スポンサーリンク

対処法

優先順位を考慮してプログラミングしましょう!

と言ってしまうと一言で終わってしまいますね….。

またC言語で扱える演算子は多いので、全ての演算子の優先順位関係を覚えるのは大変です。

私がはっきり覚えているのは下記くらいですね…。

  • +- よりも */ の方が優先される
  • = の優先度は低い
  • 同じ優先度の場合は左側の計算が優先される
  • 括弧内の計算が優先される

個人的には、最低限下記さえ覚えておけばこの問題による計算結果がおかしい現象は対処できると思います。

  • 括弧内の演算の優先度が一番高い

演算子の優先度に迷ったら、まずは演算を先に行いたい部分を括弧で括ってしまいましょう!

例えば計算結果がおかしくなる例で示した例だと下記のように括弧を使用すれば計算結果が意図したものに直すことができます。

演算子の優先度による問題の対処
int main(void) {
    float height = 1.75;
    float weight = 60.5;
    float bmi;

    bmi = weight / (height * height);

    printf("bmi = %f\n", bmi);

    return 0;
}

ただし、括弧をつけすぎるとソースコードが読みにくくなってしまうので、そのような場合は式を複数に分けてやれば良いと思います。

代入時の桁あふれ

ここからは「型」が大きく関係してきます。まずは基本的なところから紹介していきます。

スポンサーリンク

計算がおかしくなる例

代入時に桁あふれが発生して計算結果がおかしくなる例は下記のようなプログラムです。

代入時の桁あふれ
int main(void) {
    char a;

    a = 300;

    printf("a = %d\n", a);
    return 0;
}

では a の表示結果はいくつになるでしょう?

ん? 300 でしょ?

計算結果は下記の通りです。どう考えても結果がおかしいですね。

a = 44

パソコン壊れた…?

スポンサーリンク

スポンサーリンク

原因

計算結果がおかしくなる原因は「代入時の桁あふれ(オーバーフロー)」です。

C言語で計算結果がおかしくなる主な原因の1つがこの桁あふれです。

最初なので「桁あふれ」とはどのようなものであるかを解説しておきます。

まず前提として、C言語ではデータの格納先として変数を用意(変数宣言)する必要があります。

データを格納する変数の説明図

そして、この変数にはサイズが決められています。変数には、このサイズよりも大きなデータは格納(代入)することができません。

このサイズの単位としてはビットやバイトが用いられます。

さらに、C言語の場合、このサイズは型によって決まります。

様々なサイズの型を表した様子

例えば int 型のサイズは一般的に 32 ビット(4 バイト)になります(環境によってはビット数が異なる場合がありますが、このページでは 32 ビットとして扱っていきたいと思います)。

他の整数型の型の具体的なサイズは下記のようになります(環境によって異なる可能性があります)。

サイズ
char8 ビット
short16 ビット
int32 ビット
long32 ビット
long long64 ビット

各型では unsigned を指定して符号なしの型にすることができますが、その場合でもサイズは一緒です。ただし、最上位の1桁の扱いが異なります(符号ありの場合は最上位の1桁が符号を表す)。

では、変数に値を代入する場合、この値のサイズはどうやって決まるでしょうか?

MEMO

値のサイズとは、より正確にいうと、その値を表すために必要な最小のサイズです

整数の値のサイズは、その値を「2進数に変換した時の桁数」によって決まります。桁数がそのままビット数となります。

例えば 300 を2進数に変換すると 100101100 ですので、この 300 のサイズは 9 ビットということになります。

で、ここから本題の桁あふれの説明です。

桁あふれとは、変数のサイズを超えるサイズの値を代入した時に、「変数に入りきらない桁があふれて捨てられてしまうこと」を言います。

桁あふれは「オーバーフロー」とも呼ばれます。

計算がおかしくなる例では char 型の変数 a に対して 300 を代入しています。

サイズで考えると、変数 achar 型なので 8 ビットであるのに対し、300 は前述の通り 9 ビットです。つまり変数 a300 は入りきりません。

したがって、変数 a300 を代入する際に、桁あふれが発生して入りきらない上位の 1 桁分が捨てられてしまいます。

桁あふれが発生する様子

その結果、300 を2進数に変換した結果である 100101100 の上位 1 桁を無くした 00101100 のみが変数 a に格納されることになります。

00101100 は10進数で考えると 44 になるので、明らかに結果がおかしくなるというわけです。

こんな感じで、期待している結果よりも極端に値が小さい場合や正の値を代入しているのに負の値になっている場合などは、この桁あふれを疑うと良いと思います。

スポンサーリンク

スポンサーリンク

対処法

原因は代入時に桁あふれが発生していることですので、その桁あふれが発生しないようにすれば良いです。

サイズの大きな型を利用する

対処法は変数の型を「代入する値のサイズよりも大きなサイズの型にする」ことです。

要は、そのプログラムで入力される値をきちんと考慮し、適切に型を選びましょうということです。

例えば計算がおかしくなる例ではサイズ 9 ビットの値を格納しようとしているので、変数 a の型を short 型や int 型にしてやれば問題は解決します。

例えば下記は変数 a の型を int 型にして代入時の桁あふれを防いでいる例になります。

代入時の桁あふれの対処
int main(void) {
    int a;

    a = 300;

    printf("a = %d\n", a);
    return 0;
}

サイズが大きい変数を利用するデメリットってないの?

サイズが大きい変数を利用することで、プログラムで使用するメモリ量が増えます。これがデメリットです。

特にこのメモリの使用量を意識する必要があるのは、組み込み製品開発時など少量のメモリ上で動作するプログラムを開発するときくらいです。

最近だとサーバーやパソコンには大量のメモリが搭載されていることが多いので、そこまでメモリに対してシビアに気を遣ってプログラミングする必要もないのが現実です。

ただ、C言語は省メモリで動作できる点がメリットでもありますので、そのメリットを活かせるプログラムを作るためには、省メモリも意識した方が良いとは思います。

スポンサーリンク

計算時の桁あふれ

次も先ほど同様に桁あふれの問題について扱います。

スポンサーリンク

計算がおかしくなる例

先ほどは代入時の桁あふれについて取り扱いましたが、次は代入時ではなく計算時に桁あふれが発生して計算結果がおかしくなる例を紹介します。

計算時の桁あふれ
int main(void) {
    int a, b, c;

    a = 10000;
    b = 20000000;

    c = a * b / a;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    return 0;
}

1000020000000int 型の変数に十分に収まるサイズです。

では c の表示結果はいくつになるでしょう?

もしかしてバカにされてる…?

10000 と 20000000 を掛けて、その後また 10000 で割ってるんだから 20000000 でしょ

計算結果は下記の通りです。

c = -186346

なんじゃこりゃー!

スポンサーリンク

スポンサーリンク

原因

原因は代入時の桁あふれと同様に「桁あふれ」が発生していることです。

ですが、今回は代入時に桁あふれが発生しているわけではなく、a * b の計算時に桁あふれが発生しています。

C言語では「計算時にも桁あふれ」が発生するのです。

今回は abint 型ですので、計算結果も int 型として扱われることになります。つまり、計算結果の値のサイズが int 型のサイズである 32 ビットを超えてしまうと桁あふれが発生します。

MEMO

計算結果は基本的に、計算時に使用した型のサイズのデータとして扱われます

ただし、これも環境によって異なるかもしれませんが、サイズは 32 ビットの倍数に切り上げられるようです

私の環境では、例えば char 型同士の掛け算でも計算結果は 32 ビットとして扱われました

10000 と 20000000 の掛け算結果は 200000000000 であり、サイズは 38 ビットですので、上位の 6 ビット分が捨てられます。したがって、掛け算結果は 200000000000 ではなく、-1863462912 になってしまいます(最上位の桁が 1 なのでマイナスの値になる)。

さらに、この -1863462912 を変数 a、つまり 10000 で割り算しているので、最終的に c に代入される値は -186346 になります。

こんな感じで、代入時の桁あふれと同様に、期待している結果よりも極端に値が小さい場合や、計算結果が必ず正の値になるはずなのに負の値になっている場合などは、この桁あふれを疑うと良いと思います。

スポンサーリンク

スポンサーリンク

対処法

では対処法を紹介していきます。

サイズの大きな型を利用する

代入時の桁あふれと同様にサイズの大きな型を利用することで問題を解決することができます。

ただし、今回は計算途中で桁あふれが発生しているわけですので、「計算途中に取りうる値」を考慮して「計算時に使用する変数の型」を決める必要があります。

特に掛け算を行うと一気に計算結果の値のサイズが大きくなることが多いですので、掛け算を行う場合は注意が必要だと思います(もちろん足し算でも起こりうる問題です)。

下記は変数 a と変数 b の型を long long にすることで計算結果がおかしくなる現象を対処する例になります。

計算時の桁あふれの対処1
int main(void) {
    long long a;
    long long b;
    int c;

    a = 10000;
    b = 20000000;

    c = a * b / a;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    return 0;
}

前述の通り、int 型と int 型の変数の演算結果は int 型として扱われます。

一方、long long 型と long long 型の変数の演算結果は long long 型として扱われます。

long long 型はサイズが 64 ビットですので、計算結果も 64 ビットになり、a * b の結果も十分代入できるサイズになっています。

サイズの大きな型にキャストする

後は変数の型を「サイズの大きな型にキャストする」ことで対処することもできます。

キャストについては下記で解説していますので、詳しく知りたい人はこちらをご覧ください。

キャスト解説ページのアイキャッチC言語のキャストについて解説!「符号あり」と「符号なし」の比較・計算は特に危険!

要はキャストを利用することで「型を一時的に変換する」ことができます。

ですので、計算時のみ変数の型をサイズの大きい型に変換してやることで、計算途中の桁あふれを防ぐことができます。

今回の例であれば、掛け算時に変数 a や変数 blong long 型にキャストしてやれば桁あふれが防げます(long long 型でのサイズは 64 ビットです)。

計算時の桁あふれの対処2
int main(void) {
    int a, b, c;

    a = 10000;
    b = 20000000;

    c = (long long)a * (long long)b / a;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    return 0;
}

詳細は異なる型の比較で後述しますが、実は ab 両方をキャストしなくても一方さえキャストしてやれば計算結果は正しく得ることができるようになります。

ただし、下記のように掛け算の後にキャストをするのはダメです。

計算時の桁あふれの間違った対処
int main(void) {
    int a, b, c;

    a = 10000;
    b = 20000000;

    c = a * b / (long long)a;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    return 0;
}

この場合、a * b の計算結果は int 型として扱われるので、掛け算した時点で上位の桁が捨て去られてしまっています。桁あふれが発生した後にキャストをしても、その失われた桁は戻ってきません。

今回の例のように単純なプログラムであれば変数そのものの型を変えてやれば良いですが、プログラムが大規模になると変数の型を変えると影響範囲が広くなる可能性があります。

影響範囲を局所的にしたい場合は、このキャストによる対処がオススメです。

計算途中の小数点以下の切り捨て

次は整数ではなく小数点以下の値を取り上げた問題です。が、結局この問題も「型」が関連してきます。

スポンサーリンク

計算がおかしくなる例

整数同士の割り算は特に注意が必要です。

式としては成立していそうだけど、計算途中の小数点が意図せずに切り捨てられて計算結果がおかしくなる場合があります。

計算途中の小数点以下の切り捨て
int main(void) {
    int a, b, c;

    a = 100;
    b = 200;

    c = a / b * b;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    return 0;
}

では c の表示結果はいくつになるでしょう?

式だけ見ると 100 だけど…

ここまでの話を聞いた感じだと int 型と int 型の計算結果も int で扱われるから…

答えは 0…?

計算結果は下記の通りです。

c = 0

やったぜ!

スポンサーリンク

スポンサーリンク

原因

現象は全然違うように思えますが、根本的な原因は計算時の桁あふれと同じです。

計算結果が int 型で扱われてしまうのが原因です。

前述の通り int 型と int 型の演算結果は int 型として扱われます。要は、整数型同士の演算では結果も整数型で扱われてしまうところが問題です。

計算途中の値を小数点以下も含めて扱いたい場合は、計算結果が小数点以下も使用できる型として扱えるように、計算に使用する変数の型も変更する必要があります。

特に整数型と整数型の割り算を行うときはこの問題で計算結果がおかしくなる場合がありますので、このような演算を行う時は注意が必要です。

スポンサーリンク

スポンサーリンク

対処法

C言語では小数点以下も扱える型として、浮動小数点数型の floatdouble があります。

ですので、計算途中の値を小数点以下も含めて扱いたい場合は計算時に用いる変数の型も float もしくは double にする or 計算時に float もしくは double にキャストすれば良いです。

計算時に double にキャストして計算結果がおかしくならないように変更したプログラムは下記のようになります。

計算途中の小数点以下の切り捨ての対処
int main(void) {
    int a, b, c;

    a = 100;
    b = 200;

    c = (double)a / (double)b * b;

    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("c = %d\n", c);
    return 0;
}

ただし、整数型同士の割り算時に毎回浮動小数点数型に変換する必要があるわけではありません。

意図して小数点以下を切り捨てるような計算もあり得ますので、その場合は整数型同士の割り算を行えば良いです。

例えば下記は、変数 x の値を 4 の倍数に切り上げる関数になります。この例では、変に浮動小数点数型を用いると演算結果が 4 の倍数にならなくなってしまいます。

4の倍数に切り上げる関数
int fourAlign(int x) {
    return (x + 3) / 4 * 4;
}

このように意図した切り捨てもあり得ますので、その割り算では小数点以下を切り捨てるべきかどうかをしっかり考え、必要に応じて型を浮動小数点数型に変更するようにしましょう。

異なる型の比較

次は異なる型を用いた時に陥りやすい問題を紹介します。

比較の話なので直接計算式に関わるわけではないですが、比較結果に応じて計算を切り替える場合などは計算結果がおかしくなる原因になり得ます。

スポンサーリンク

計算結果がおかしくなる例

下記は変数 a と変数 b の値を比較し、比較結果に応じて表示を切り替えるプログラムのソースコードになります。

異なる型の比較
int main(void) {
    int a;
    unsigned int b;

    a = -100;
    b = 100;

    if (a < b) {
        printf("a < b\n");
    } else {
        printf("a >= b\n");
    }
    return 0;
}

ではプログラムを実行すると何が表示されるでしょうか?

100 の方が絶対 -100 よりも大きいよね

なので a < b でしょ!

表示結果は下記の通りです。

a >= b

流石にこれは納得できない…

スポンサーリンク

スポンサーリンク

原因

これも型が関係しています。

異なる型の比較では型が自動的に揃えられる

2つの変数に対して演算や比較が行われる際、2つの変数で型が異なる場合は2つの変数の「型が自動的に揃えられて」から演算や比較が行われます。

この時、基本的には2つの変数のうち「サイズが大きい方の変数」に型が合わせられることになります。

MEMO

例外もあります

これも環境によって異なるかもしれませんが、サイズが 32 ビットの型の変数は演算や比較時に int 型として扱われるようです

例えば char 型 “同士” の比較でも int 型に揃えられてから比較が行われるようです(少なくとも私の使用している環境は)

とにかく演算時や比較時には型の変換が行われる可能性がある、そしてそれにより演算や比較結果が意図しないものになることを覚えておくと良いと思います

では、サイズが一緒で符号の “あり”・”なし” のみが異なる型での演算や比較では、型はどちらに合わせられるでしょうか?

具体例としては int 型の変数と unsigned int 型の比較などが挙げられます。

このような場合、(もしかしたら環境によって異なるかもしれませんが)「符号なし」の方に型が揃えられてから演算や比較が行われます。

型が揃えられたとしても、どちらの変数にも正の整数が入っていれば特に問題ありません。

しかし、符号ありの方の変数に負の整数が格納されていると問題が発生することがあります。特に比較時に。

負の値の符号なし型への変換

C言語における符号ありの型では(おそらく他のプログラミング言語もそうだとは思いますが)、変数に格納されている値が正の値と負の値のどちらであるかを、その変数の型の最上位ビットで見分けるようになっています。

最上位ビットが 0 の場合は正の値、1 の場合は負の値として扱います。

そして、他のビットには実際の値を表すデータが格納されています。

MEMO

負の値の場合は2の補数表現で格納されています

符号なしの場合は全ビットを値を表すデータとして扱います。

例えばサイズ 32 ビットの int 型では、第 31 ビット目の値が正負を表すビットになります。

-10int 型の変数 x に格納された場合、x の各ビットのデータは下記のようになります(負の値なので2の補数表現で格納されています)。

11111111111111111111111111110110

では、もしこの変数 x を符号なしの型の変数として扱うとどうなるでしょうか?

符号なしの型では、最上位ビットも値を表すデータとして扱います。

このとき、変数 x の値は下記として扱われます(上記の2進数を10進数に変換した値)。

4294967286

めちゃめちゃ大きい値になっていますね….。

負の値の場合、最上位ビットが 1 になりますので、値を2進数で考えた時の最上位の桁が 1 になることを意味します。

なので、符号ありの型に格納された負の値を符号なしの方として扱うと大きな数に変換されて扱われてしまいます。

つまり、小さな値である負の値が、比較時に大きな値として扱われてしまうことになります。

これにより、特に比較時の結果が意図しないものになることがありますので注意しましょう。

スポンサーリンク

スポンサーリンク

対処法

これも型による問題ですので、型を変更することで解決することが可能です。

演算や比較時に用いる変数の型を合わせる

まず検討すべきは「演算や比較時に用いる変数の型を合わせる」ことです。

両方を同じ型にすることで、この型が自動的に揃えられることによる問題は解決できます。

例えば計算結果がおかしくなる例で紹介したソースコードは下記のように比較時に用いる変数の型を int 型に統一すれば意図した通りの比較結果を得ることができるようになります。

異なる型の比較の対処1
int main(void) {
    int a;
    int b;

    a = -100;
    b = 100;

    if (a < b) {
        printf("a < b\n");
    } else {
        printf("a >= b\n");
    }
    return 0;
}

演算や比較時にキャストする

後は、これも他の原因の時にも紹介した方法ですがキャストを用いることで対処することができます。

今回は符号なしの型に揃えられることが問題ですので、キャストにより符号なしの型を符号ありの型に変換してやることで対処できます。

例えば計算結果がおかしくなる例で紹介したソースコードは下記のように比較時に unsigned int 型の変数を int 型にキャストするようにすれば、意図した通りの比較結果を得ることができるようになります。

異なる型の比較の対処2
int main(void) {
    int a;
    unsigned int b;

    a = -100;
    b = 100;

    if (a < (int)b) {
        printf("a < b\n");
    } else {
        printf("a >= b\n");
    }
    return 0;
}

ただし、このようにキャストしたとしても、unsigned int 型の変数名 b の値が大きすぎるとまだ比較が上手くいかないことがあります。

具体的には変数 b の最上位ビットが 1 の場合、int 型にキャストすると負の値として扱われることになります。

これは前述の通り、符号ありの型の場合は最上位ビットが 1 だと負の値として扱われるからです。

このような時は変数 b の符号が逆転するために、同様に比較結果が意図しないものになってしまいます。

例えば下記のような場合は比較結果がおかしくなります。

キャストしても結果がおかしくなる例
int main(void) {
    int a;
    unsigned int b;

    a = -100;
    b = 2484144651;

    if (a < (int)b) {
        printf("a < b\n");
    } else {
        printf("a >= b\n");
    }
    return 0;
}

このように最上位ビットが 1 になるような大きな値を扱う場合は、より大きな型にキャストする様に検討する必要があります。

下記は long long 型へのキャストで問題を解決する例になります。

より大きな型にキャストする例
int main(void) {
    int a;
    unsigned int b;

    a = -100;
    b = 2484144651;

    if (a < (long long)b) {
        printf("a < b\n");
    } else {
        printf("a >= b\n");
    }
    return 0;
}

スポンサーリンク

浮動小数点数型の誤差

次は小数点数型に特化した問題になります。

計算がおかしくなる例

floatdouble といった浮動小数点数型は小数点以下の値も扱えて便利ですが、小数点以下の値には誤差が含まれる可能性があることに注意が必要です。

下記は浮動小数点数型の誤差によって計算結果がおかしくなる例になります。

浮動小数点数型の誤差
int main(void) {
    float a;
    float b;
    int i;

    a = 0.001;
    b = 0;
    for (i = 0; i < 1000; i++) {
        b = b + a;
    }
    printf("%f\n", b);

    return 0;
}

では c の表示結果はいくつになるでしょう?

0.001 を 1000 回足してるんだから…

1 かな!

計算結果は下記の通りです。

0.999991

ひっかけだとは思ったんだけどね…

スポンサーリンク

スポンサーリンク

原因

前述の通り浮動小数点数型が扱う値において、小数点以下の値には誤差が含まれる可能性があります。

浮動小数点数型の float 型には実際には下図のようなフォーマットで 0 or 1 の値が格納されています。

floatのフォーマット

符号部・指数部・仮数部を用いて下記のように計算することで float 型の変数に格納されたデータを10進数に変換することができます。

floatの値の計算式

1.仮数部 は2進数表現です。ですので、2進数から10進数に変換して考える必要があります。この10進数への変換は2の冪乗の和で計算することができます。

さらに、それに対して2の冪乗がかけられることになるので、結局表現できる数は「2の冪乗の和で表現できるもの」のみということになります。

MEMO

例外的に 0 などのように「2の冪乗の和」でなくても表せる値もあります

で、困ったことに特に小数点以下の値においては「2の冪乗の和で表現できない値」が多く存在します。例えば 0.30.001 などは表現できません。

MEMO

桁数を無限に考えると近似的に表現できるかもしれませんが、float は 32 bit、double は 64 bit と扱える桁数に限りがあるため、桁数を無限にすることはできません

そういった「2の冪乗の和で表現できない値」は「2の冪乗の和で表現できる値」に丸められて扱われてしまいます。強制的に「2の冪乗の和で表現できる値」として扱われるということです。

本来とは異なった値に丸められるわけですから、「誤差」が発生してしまうというわけです。そしてその誤差が発生した状態の値が変数に格納されることになります。

「誤差」が含まれた変数を計算に用いると、当然計算結果にも誤差が含まれ、意図しない結果になる場合があります。

スポンサーリンク

スポンサーリンク

対処法

誤差を完全に無くすのは正直難しいです。

なので、対処法としては誤差を小さくするための方法を紹介します。

double 型を使う

1つは float ではなく double 型を使うことです。

double の方が float よりも発生する誤差が小さくなります。これは double の方が型のサイズが大きく、float より多くの種類の値を扱えるためです。

例えば計算結果がおかしくなる例で紹介したソースコードは下記のように変更すれば誤差の小さい計算結果を得ることができるようになります。

浮動小数点数型の誤差の対処法1
int main(void) {
    double a;
    double b;
    int i;

    a = 0.001;
    b = 0;
    for (i = 0; i < 1000; i++) {
        b = b + a;
    }
    printf("%f\n", b);

    return 0;
}

整数型で小数点以下も扱う

ちょっとマニアックなテクニックになりますが、浮動小数点数型を使わずに charintlong long 等の整数型で小数点以下も擬似的に扱う方法もあります。

と言っても当然整数型で小数点以下の値は扱えませんので、あくまでも「擬似的に」です。

整数型は小数点以下を扱えませんが、(扱える範囲の値であれば)誤差なしに扱えるというメリットがあります(基本的に整数は全て2の冪乗の和で表現できます)。

やり方はおそらくソースコードを見た方が早いと思いますので先に載せておきます。

浮動小数点数型の誤差の対処法2
int main(void) {
    float a;
    int int_a;
    int int_b;
    int i;

    /* 値を格納(誤差発生の可能性あり) */
    a = (float)0.001;

    /* 10000をかけて小数点以下をなくしてint変数に格納 */
    int_a = a * 10000;

    /* int型として計算 */
    int_b = 0;
    for (i = 0; i < 1000; i++) {
        /* ここで誤差は発生しない */
        int_b = int_b + int_a;
    }

    /* 表示する時に最初にかけた10000で割る */
    /* 誤差発生の可能性あり */
    printf("%f\n", (float)int_b / 10000);

    return 0;
}

扱いたい値を格納した a に 10000 を掛けて整数型に変換し、整数型のまま計算を行うようにしています。

a には 0.001 が格納されているので、掛け算結果は 10 になり、小数点以下がなくなるため整数として扱って差し支えありません。

さらに結果を表示するときは、最初に 10000 を掛けているので辻褄を合わせるために 10000 で割っています。

要は扱いたい値の小数点の位置を自身でずらしながら、計算時は値を整数として扱うようにしています。

計算時は整数同士の計算になりますので誤差は発生しません。

ただし、float 型の a に値を代入するときや最後の割り算時といった、浮動小数点数型を扱う際に誤差が発生する可能性があるのであくまでも誤差を小さくするための対処になります。

今回は 10000 を掛けていますが、小数点以下が無くなれば良いので、10000 である必要はありません。

1000 でも良いですし、100000 でも良いです。

ただし、100 とかだと小数点以下が無くならないのでダメです。

本当はコンピュータ内部では値が2進数で扱われているので、2の冪乗をかける方が良いと思います。

整数に変換する時に桁あふれが発生する可能性があったり、小数点の位置を自分で制御する必要がある点が大変な点は注意しましょう。

例えば掛け算など行うと小数点の位置も変わってしまうので、その小数点の位置が変わったことも考慮して最後に割り算する値を決定する必要があります。

小数点の位置を考慮した割り算
int main(void) {
    float a;
    float b;
    float c;
    int int_a;
    int int_b;
    int int_c;
    /* 値を格納(誤差発生の可能性あり) */
    a = (float)0.001;
    b = (float)0.01;

    /* 10000をかけて小数点以下をなくしてint変数に格納 */
    int_a = a * 10000;
    int_b = b * 10000;

    /* int型として計算 */
    /* 本当は小数点以下を含む掛け算なので小数点の位置がここで変わる */
    int_c = int_a * int_b;

    /* 小数点の位置を考慮して割り算 */
    /* 誤差発生の可能性あり */
    c = (float)int_c / (10000 * 10000);
    
    printf("%f\n", c);

    return 0;
}

まとめ

このページではC言語で計算結果がおかしくなる例とその原因、そしてその対処法の紹介を行いました。

どうだった?

うーん、型ってもしかして超大事?

その通りだね!

型を制するものはC言語を制す!

え…?(いきなりどうした?)

今回紹介した問題の原因の多くは「型」が関連しています。

もし演算式などが合っていそうなのに計算結果がどうしてもおかしい場合は、型に注目してみると良いと思います。

今回紹介したようなケースに当てはまった場合は、そこを修正することで計算結果が意図したものに直る可能性は高いと思います。

C言語では(C言語だけではないですが)、変数を使うためには型を指定する必要があり、その型にしたがってプログラムが動作することになります。

ですので、その型が不適切であるとプログラムも上手く動作してくれません。

型をてきとうに選んでしまうと、今回紹介したように計算結果がおかしくなる場合も多いですので、そのプログラムで扱う値等を考慮して適切な型を選択するようにしましょう!

逆に型をしっかり理解してプログラムを作れば、プログラムはあなたが意図した通りにしっかり動作してくれるはずです!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です