C言語の基本|C言語の計算の落とし穴

正しい式なのに答えが違う。その原因を先に知って、C言語の計算バグを防ごう。

C言語では、算術演算子を使って高速で柔軟な計算処理を作れます。しかし、式の書き方が正しくても、変数の型や入力値によって、予想とは異なる結果になることがあります。

たとえば、23を4で割る計算を考えてみましょう。

数学上の結果:23 ÷ 4 = 5.75
int型同士の結果:23 / 4 = 5

どちらかの計算が間違っているわけではありません。C言語では、整数同士の除算結果も整数になるという決まりがあるためです。

ほかにも、C言語の計算では次のような問題に注意する必要があります。

  • 型の範囲を超えるオーバーフロー
  • 0を分母にするゼロ割
  • 整数同士の除算による小数部分の消失
  • 浮動小数点数の内部表現による小さな誤差

これらは、仕組みと対策を知っていれば避けやすい問題です。実際のプログラムを確認しながら、計算前に何を確認すればよいのかを身につけていきましょう。

計算で注意したい4つの落とし穴

この記事で扱う計算上の注意点を整理すると、次のようになります。

落とし穴主な原因起こりやすい現象基本的な対策
オーバーフロー型の表現範囲を超える不正な結果や未定義の動作になる大きい型を使い、計算前に範囲を確認する
ゼロ割整数を0で割る異常終了などが起こる可能性がある分母をifで確認する
整数同士の除算int型同士で割る小数部分が失われる計算前にdouble型へ変換する
浮動小数点誤差10進小数を2進数で近似する計算結果にわずかなずれが生じるdoubleや整数、許容誤差を使う

どの問題も、計算を始める前に型や値を確認することが基本的な対策になります。

オーバーフローとは

変数の型には、表現できる値の範囲があります。

計算結果がその範囲を超えることをオーバーフローといいます。反対に、表現できる最小値よりも小さくなることをアンダーフローと呼ぶこともあります。

特に注意したいのが、符号付き整数型であるintを使った大きな加算や乗算です。

一般的な32ビットのint型が表現できる範囲は、次のとおりです。

最小値:-2,147,483,648
最大値: 2,147,483,647

たとえば、次の計算結果は30億です。

1,700,000,000 + 1,300,000,000
= 3,000,000,000

30億は32ビットのint型の最大値を超えています。そのため、この計算結果をint型では正しく表せません。

整数型のおおよその範囲

整数型の大きさは処理系によって異なる場合があります。一般的な環境での目安は次のとおりです。

一般的なビット数符号付きで表現できる範囲の目安
int32ビット約-21億から約21億
long long64ビット約-922京から約922京

C言語では、int型が必ず32ビットであるとは限りません。実際の環境で表現範囲を確認するときは、limits.hで定義されているINT_MAXやINT_MINを利用できます。

  • INT_MAXはint型の最大値
  • INT_MINはint型の最小値
  • LLONG_MAXはlong long型の最大値
  • LLONG_MINはlong long型の最小値

符号付き整数のオーバーフローは未定義の動作

符号付き整数の計算結果が型の範囲を超えた場合、C言語では未定義の動作になります。

環境によっては負の値へ変化したように見えることがありますが、必ずその結果になるとは限りません。コンパイラや最適化の状態によって、異なる動作になる可能性があります。

そのため、オーバーフローする計算を実行してから結果を確認するのではなく、範囲を超えるかどうかを先に確認する方法が安全です。

図1:int型の範囲とオーバーフロー

この図から分かること

32ビットのint型では、2,147,483,647までの値を表現できます。

1,700,000,000と1,300,000,000を加算した結果は3,000,000,000です。この値はint型の最大値を超えるため、int型では正しく表現できません。

大きな値を計算するときは、計算前にlong long型へ変換し、計算結果がint型の範囲内にあるか確認することが大切です。

オーバーフローを防ぐ基本的な方法

オーバーフローを防ぐためには、次のような対策が有効です。

  • 予想される値に合わせて十分に大きな型を選ぶ
  • 加算や乗算を行う前に結果の範囲を確認する
  • 金額や個数など、入力できる値の上限を決める
  • limits.hを利用して実際の型の範囲を確認する
  • 計算を行う前にオペランドを大きい型へ変換する

特に注意したいのが、代入先だけをlong long型にしても安全とは限らない点です。

long long result = a + b;

aとbがint型の場合、a + bは先にint型で計算されます。ここでオーバーフローしたあとにlong long型へ代入しても、正しい結果には戻せません。

計算そのものをlong long型で行うには、次のように書きます。

long long result = (long long)a + b;

aを先にlong long型へ変換することで、加算全体がlong long型で行われます。

大きな加算をlong long型で計算して範囲を確認するプログラム

ファイル名:6_3_1.c

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int first = 1700000000;
    int second = 1300000000;
    long long wide_sum;

    /* 加算前にlong long型へ変換する */
    wide_sum = (long long)first + second;

    printf("first = %d\n", first);
    printf("second = %d\n", second);
    printf("long longで計算した結果 = %lld\n", wide_sum);
    printf("int型の最大値 = %d\n", INT_MAX);

    /* int型に格納できる範囲か確認する */
    if (wide_sum > INT_MAX || wide_sum < INT_MIN) {
        printf("計算結果はint型の範囲を超えています。\n");
    } else {
        int int_sum = (int)wide_sum;
        printf("int型へ格納した結果 = %d\n", int_sum);
    }

    return 0;
}
実行結果の例

一般的な32ビットのint型を使用する環境での実行例です。

first = 1700000000
second = 1300000000
long longで計算した結果 = 3000000000
int型の最大値 = 2147483647
計算結果はint型の範囲を超えています。

オーバーフロー対策プログラムの解説

変数firstとsecondはint型ですが、加算する前にfirstをlong long型へ変換しています。

wide_sum = (long long)first + second;

片方がlong long型になるため、secondもlong long型に合わせて計算されます。これにより、3,000,000,000を正しく計算できます。

続いて、計算結果がint型の範囲に収まっているかを確認しています。

if (wide_sum > INT_MAX || wide_sum < INT_MIN) {
    printf("計算結果はint型の範囲を超えています。\n");
}

wide_sumがINT_MAXより大きい場合、またはINT_MINより小さい場合は、int型へ安全に格納できません。

範囲内だった場合だけ、int型へ変換して格納します。

int int_sum = (int)wide_sum;

このプログラムでは、危険なint型の加算を実行してから結果を調べるのではなく、十分に大きな型で計算してから範囲を確認しています。

ゼロ割とは

整数の除算や剰余では、右側のオペランドを0にできません。

次の式でbが0の場合、正常な計算はできません。

a / b
a % b

整数を0で割ることや、0で割った余りを求めることは、C言語では未定義の動作です。

未定義の動作が発生すると、次のような問題につながる可能性があります。

  • プログラムが異常終了する
  • 実行時エラーが発生する
  • 想定していない処理が行われる
  • 正しくない値が表示される

入力された値を分母にする場合は、除算の前に0ではないことを確認しましょう。

ifを使って分母を確認する

ゼロ割を防ぐ基本的な形は、次のとおりです。

if (b != 0) {
    printf("%d\n", a / b);
} else {
    printf("0では割れません。\n");
}

b != 0は、bが0ではないことを確認する条件です。

判定結果bの値処理
0以外除算を実行する
0除算を実行せず、メッセージを表示する

重要なのは、0で割ったあとにエラーを処理するのではなく、危険な計算を実行しないことです。

作業量を人数で割って1人分を求めるプログラム

ファイル名:6_3_2.c

#include <stdio.h>

int main(void)
{
    int total_tasks;
    int workers;

    printf("全体の作業件数を入力してください > ");
    scanf("%d", &total_tasks);

    printf("担当者の人数を入力してください > ");
    scanf("%d", &workers);

    if (workers != 0) {
        int tasks_per_person = total_tasks / workers;
        printf("1人あたりの作業件数は%d件です。\n",
               tasks_per_person);
    } else {
        printf("人数が0なので作業件数を割り当てられません。\n");
    }

    return 0;
}
実行結果の例

担当者の人数として6を入力した場合です。

全体の作業件数を入力してください > 960
担当者の人数を入力してください > 6
1人あたりの作業件数は160件です。

担当者の人数として0を入力した場合は、次のようになります。

全体の作業件数を入力してください > 960
担当者の人数を入力してください > 0
人数が0なので作業件数を割り当てられません。

ゼロ割対策プログラムの解説

入力された担当者数は、変数workersへ格納されます。

scanf("%d", &workers);

除算を行う前に、workersが0ではないことを確認します。

if (workers != 0) {

workersが0以外なら、次の除算を実行できます。

int tasks_per_person = total_tasks / workers;

workersが0の場合はelse側へ進むため、除算は実行されません。

else {
    printf("人数が0なので作業件数を割り当てられません。\n");
}

このように、計算前に値を確認する処理は、入力を受け取るプログラムで特に重要です。

整数同士の除算で小数部分が消える

int型同士の除算では、計算結果も整数になります。

たとえば、23を4で割った数学上の結果は5.75です。

23 ÷ 4 = 5.75

しかし、次の式では両方のオペランドがint型です。

int result = 23 / 4;

この場合、小数部分は残らず、変数resultには5が格納されます。

C言語の整数除算では、小数部分が0の方向へ取り除かれます。正の整数だけを扱っている場合は、小数部分が切り捨てられたように見えます。

double型へ変換する位置が重要

小数を含む結果が必要な場合は、除算を行う前に一方をdouble型へ変換します。

double result = (double)a / b;

aが23、bが4の場合、計算は次のようになります。

(double)a / b
23.0 / 4
5.75

次の書き方では、正しい小数結果を得られません。

double result = (double)(a / b);

この場合は、a / bが先にint型同士で計算されます。

a / b
23 / 4
5

そのあと、整数の5がdouble型の5.0へ変換されます。一度失われた小数部分は、あとからdouble型へ変換しても戻りません。

除算時の型結果
a / bint / int5
(double)a / bdouble / int5.75
(double)(a / b)intで除算したあとdoubleへ変換5.0
同じ値を整数と小数の2通りで割るプログラム

ファイル名:6_3_3.c

#include <stdio.h>

int main(void)
{
    int distance = 23;
    int sections = 4;
    int integer_result;
    double decimal_result;

    integer_result = distance / sections;
    decimal_result = (double)distance / sections;

    printf("距離 = %d\n", distance);
    printf("区間数 = %d\n", sections);
    printf("整数同士で割った結果 = %d\n", integer_result);
    printf("小数として割った結果 = %.2f\n", decimal_result);

    return 0;
}
実行結果の例
距離 = 23
区間数 = 4
整数同士で割った結果 = 5
小数として割った結果 = 5.75

整数除算を確認するプログラムの解説

次の式では、distanceとsectionsがどちらもint型です。

integer_result = distance / sections;

そのため、23 / 4の小数部分は残らず、結果は5になります。

小数の結果を求める式では、distanceを先にdouble型へ変換しています。

decimal_result = (double)distance / sections;

このキャストによって、除算がdouble型を含む計算になります。結果として5.75が得られます。

小数を得たいときは、結果を格納する変数の型だけでなく、どの型で除算が行われるのかを確認しましょう。

図2:整数除算と浮動小数点誤差の違い

この図から分かること

整数同士の除算では、計算結果も整数になるため、小数部分が残りません。小数まで求めたい場合は、除算を行う前にdouble型へ変換します。

浮動小数点数の誤差は、10進小数をコンピュータ内部の2進数で正確に表せない場合があるために発生します。

どちらも小数に関係する問題ですが、原因は異なります。

浮動小数点数の誤差とは

float型やdouble型は、コンピュータ内部で値を2進数として扱います。

ところが、10進数の0.1や0.2などは、2進数では有限の桁数で正確に表せません。そのため、実際には目的の値に非常に近い値として保存されます。

次の計算結果も、内部では0.8と完全に一致しないことがあります。

double result = 0.1 + 0.7;

通常の少ない桁数で表示すると、0.8に見えます。

0.80

表示桁数を増やすと、わずかなずれが見える場合があります。

0.79999999999999993

これは、printfの計算が間違っているわけではありません。浮動小数点数の内部表現によるものです。

doubleでも誤差が完全になくなるわけではない

double型はfloat型より多くの桁を扱えるため、一般に高い精度を持っています。

ただし、double型を使えば誤差が完全になくなるわけではありません。表現できる桁数が増え、誤差が小さくなると考えるとよいでしょう。

一般的な特徴
float使用する記憶領域が比較的小さいが、精度も低い
doublefloatより多くの桁を扱え、誤差を小さくしやすい

精度が必要な小数計算では、まずdouble型を使うのが基本です。

浮動小数点数を==で直接比較するときの注意

浮動小数点数にはわずかな誤差が含まれることがあるため、計算結果を==で直接比較すると、期待どおりにならない場合があります。

double result = 0.1 + 0.7;

if (result == 0.8) {
    printf("等しい\n");
}

画面上ではどちらも0.8に見えても、内部の値が少し異なれば、条件は偽になります。

比較するときは、2つの値の差が十分に小さいかを確認する方法があります。

double difference = result - 0.8;

if (difference < 0.0) {
    difference = -difference;
}

if (difference < 0.000001) {
    printf("ほぼ等しいと判断します。\n");
}

0.000001は、どの程度の差まで同じとみなすかを表す許容誤差です。必要な精度に応じて、この値を決めます。

小数の表示桁数を増やして誤差を確認するプログラム

ファイル名:6_3_4.c

#include <stdio.h>

int main(void)
{
    double first = 0.1;
    double second = 0.7;
    double result = first + second;
    double expected = 0.8;

    printf("通常の表示 = %.1f\n", result);
    printf("17桁で表示した結果 = %.17f\n", result);
    printf("期待する0.8の内部値 = %.17f\n", expected);

    if (result == expected) {
        printf("==による比較結果:等しい\n");
    } else {
        printf("==による比較結果:等しくない\n");
    }

    return 0;
}
実行結果の例

一般的な浮動小数点環境では、次のような結果になります。

通常の表示 = 0.8
17桁で表示した結果 = 0.79999999999999993
期待する0.8の内部値 = 0.80000000000000004
==による比較結果:等しくない

浮動小数点誤差を確認するプログラムの解説

%.1fで表示すると、計算結果は小数第1位へ丸められるため、0.8と表示されます。

printf("通常の表示 = %.1f\n", result);

%.17fで小数点以下の表示桁数を増やすと、内部表現の違いが見えやすくなります。

printf("17桁で表示した結果 = %.17f\n", result);

resultとexpectedを==で比較すると、内部の値が一致していないため、条件が偽になる場合があります。

if (result == expected)

浮動小数点数を比較するときは、表示上の値だけでなく、内部にわずかな誤差が含まれる可能性を考える必要があります。

金額は整数で扱う方法もある

金額を小数で繰り返し計算すると、浮動小数点数の誤差によって、最終的な結果が1円ずれる可能性があります。

日本円のように1円未満を扱わない計算では、金額を円単位の整数で保存する方法があります。

1200円はint型の1200として保存する

小数を使わなければ、浮動小数点数の内部表現による誤差を避けられます。

小数を扱う必要がある場合でも、次の点を意識しましょう。

  • floatよりdoubleを使う
  • 表示するときに必要な桁数へ丸める
  • ==による直接比較を避ける
  • 許容誤差を使って比較する
  • 金額などは可能なら整数単位で管理する

4つの落とし穴を早見表で確認する

落とし穴起こる条件典型的な現象確認すること基本対策
オーバーフロー計算結果が型の範囲外不自然な結果や未定義の動作型の最大値と最小値long longや事前の範囲確認
ゼロ割整数の分母が0異常終了など分母が0でないかifで計算前に確認
整数同士の除算int / int小数部分が残らない除算時のオペランドの型計算前にdoubleへ変換
浮動小数点誤差floatやdoubleの計算わずかな数値のずれ必要な精度double、整数化、許容誤差

実践問題:安全にお釣りを計算しよう

商品の購入金額と預かった金額を整数で入力し、お釣りを求めるプログラムを作成してみましょう。

条件は次のとおりです。

  • 購入金額は円単位の整数で入力する
  • 預かった金額も円単位の整数で入力する
  • 預かった金額が購入金額以上なら、お釣りを計算する
  • 預かった金額が不足している場合は、計算せずにメッセージを表示する
  • お釣りは、預かった金額から購入金額を引いて求める

計算式は次のとおりです。

お釣り = 預かった金額 - 購入金額
支払い可能か確認してお釣りを計算するプログラム

ファイル名:6_3_5.c

#include <stdio.h>

int main(void)
{
    int purchase_total;
    int received_money;

    printf("購入金額(円)を入力してください > ");
    scanf("%d", &purchase_total);

    printf("預かった金額(円)を入力してください > ");
    scanf("%d", &received_money);

    if (received_money >= purchase_total) {
        int change = received_money - purchase_total;
        printf("お釣りは%d円です。\n", change);
    } else {
        printf("預かった金額が不足しています。\n");
    }

    return 0;
}
実行結果の例

預かった金額が購入金額以上の場合です。

購入金額(円)を入力してください > 2380
預かった金額(円)を入力してください > 3000
お釣りは620円です。

預かった金額が不足している場合は、次のようになります。

購入金額(円)を入力してください > 2380
預かった金額(円)を入力してください > 2000
預かった金額が不足しています。

お釣りを計算するプログラムの解説

計算前に、預かった金額が購入金額以上かを確認しています。

if (received_money >= purchase_total) {

条件が真の場合だけ、次の減算を実行します。

int change = received_money - purchase_total;

預かった金額が不足している場合は、お釣りを計算せず、else側のメッセージを表示します。

else {
    printf("預かった金額が不足しています。\n");
}

この処理ではゼロ割は行っていませんが、計算してよい状態かをifで先に確認する考え方は共通しています。

  • ゼロ割では、分母が0ではないことを確認する
  • お釣り計算では、預かった金額が不足していないことを確認する
  • 条件を満たさない場合は、危険または不適切な計算を実行しない

金額を円単位のint型で管理しているため、この範囲では浮動小数点数の誤差も避けられます。

実践問題:BMIを安全に計算しよう

BMIは、体重と身長から求めます。

BMI = 体重kg ÷ 身長mの2乗

身長mの2乗は、次の乗算で表せます。

身長m × 身長m

C言語の式では、次のように書きます。

bmi = weight_kg / (height_m * height_m);

BMIを正しく計算するために、次の点へ注意します。

  • 身長をcmからmへ変換する
  • 浮動小数点数として計算する
  • 分母全体を括弧で囲む
  • 身長が0以下でないことを先に確認する
  • 表示には小数点以下の桁数を指定する

身長をcmからmへ変換する

身長をcmからmへ変換するには、100で割ります。

height_m = height_cm / 100.0;

100.0は浮動小数点数なので、浮動小数点の除算であることが明確になります。

height_cmが168.0の場合は、次の結果になります。

168.0 / 100.0 = 1.68

height_cmがdouble型なら100と書いてもdouble型の計算になります。しかし、100.0と書くことで、浮動小数点数として計算する意図が分かりやすくなります。

分母全体を括弧で囲む

BMIでは、身長と身長を掛けた値全体を分母にします。

正しい式は次のとおりです。

bmi = weight_kg / (height_m * height_m);

次の書き方では、計算の意味が変わります。

bmi = weight_kg / height_m * height_m;

*と/は同じ優先順位であり、左側から順番に計算されます。

そのため、括弧がない式は次の順番になります。

1. weight_kg / height_mを計算する
2. その結果にheight_mを掛ける

height_mで割ったあとに同じheight_mを掛けるため、計算結果はほぼweight_kgへ戻ってしまいます。

BMIでは、分母全体を括弧で囲むことが必要です。

図3:BMIを安全に計算する流れ

この図から分かること

BMIを計算する前に、身長が0より大きいことを確認します。身長が0以下なら、計算を実行せずにメッセージを表示します。

正しい身長が入力されたら、100.0で割ってcmからmへ変換します。BMIの計算では、height_m * height_mを括弧で囲み、分母全体で体重を割ります。

最後に、printfの%.2fを使って小数第2位まで表示します。

体重と身長を入力してBMIを計算するプログラム

ファイル名:6_3_6.c

#include <stdio.h>

int main(void)
{
    double weight_kg;
    double height_cm;
    double height_m;
    double bmi;

    printf("体重(kg)を入力してください > ");
    scanf("%lf", &weight_kg);

    printf("身長(cm)を入力してください > ");
    scanf("%lf", &height_cm);

    /* 0以下の身長では計算しない */
    if (height_cm <= 0.0) {
        printf("身長は0より大きい値を入力してください。\n");
        return 0;
    }

    /* cmをmへ変換する */
    height_m = height_cm / 100.0;

    /* 身長の2乗全体を分母にする */
    bmi = weight_kg / (height_m * height_m);

    printf("身長をmへ変換した値は%.2fです。\n", height_m);
    printf("あなたのBMIは%.2fです。\n", bmi);

    return 0;
}
実行結果の例
体重(kg)を入力してください > 62
身長(cm)を入力してください > 168
身長をmへ変換した値は1.68です。
あなたのBMIは21.97です。

身長に0を入力した場合は、次のようになります。

体重(kg)を入力してください > 62
身長(cm)を入力してください > 0
身長は0より大きい値を入力してください。

BMI計算プログラムの解説

体重と身長は小数を扱えるようにdouble型で宣言しています。

double weight_kg;
double height_cm;
double height_m;
double bmi;

scanfでdouble型の変数へ入力するときは、変換指定に%lfを使います。

scanf("%lf", &weight_kg);
scanf("%lf", &height_cm);

身長が0以下の場合は、有効なBMIを計算できません。そのため、計算前に値を確認しています。

if (height_cm <= 0.0) {
    printf("身長は0より大きい値を入力してください。\n");
    return 0;
}

return 0;が実行されるとmain関数が終了するため、その後のBMI計算には進みません。

身長が正の値なら、cmからmへ変換します。

height_m = height_cm / 100.0;

BMIは、身長の2乗全体を分母にして計算します。

bmi = weight_kg / (height_m * height_m);

身長が168cmの場合、height_mは1.68です。

1.68 × 1.68 = 2.8224
62 ÷ 2.8224 = 約21.97

最後に、%.2fを使って小数第2位まで表示します。

printf("あなたのBMIは%.2fです。\n", bmi);

printfでdouble型の値を表示するときは%fを使用できます。scanfではdouble型に%lfを使うため、違いに注意しましょう。

BMI計算で起こりやすい間違い

身長をint型で宣言し、整数同士で除算すると、小数部分が失われます。

int height_cm = 168;
int height_m = height_cm / 100;

この場合、168 / 100の結果は1です。本来必要な1.68を得られません。

小数を扱う変数はdouble型にし、100.0で割ります。

double height_cm = 168.0;
double height_m = height_cm / 100.0;

計算式の括弧を省略することにも注意が必要です。

bmi = weight_kg / height_m * height_m;

この式では、除算と乗算が左から順番に行われ、BMIの式にはなりません。

正しくは、分母全体を括弧で囲みます。

bmi = weight_kg / (height_m * height_m);

計算するときは、式を書いたあとに次の点を確認すると、落とし穴を避けやすくなります。

  • 計算結果が変数の型の範囲内か
  • 除算や剰余の右側が0にならないか
  • 小数が必要な計算を整数型で行っていないか
  • 浮動小数点数を==で直接比較していないか
  • 括弧によって計算順序が明確になっているか
  • 計算してよい入力値かをifで先に確認しているか