【C言語】浮動小数点数に対して剰余演算を行う方法(fmodや自力で演算など)

浮動小数点数に対して剰余演算を行う方法の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、double 型や float 型などで扱う浮動小数点数に対して剰余演算を行う方法について解説していきます。

具体的には、浮動小数点数に対して剰余演算を行う方法として、下記の3つについて解説していきます。

皆さんもご存知だと思いますが、整数同士の剰余演算に関しては剰余演算子 % を使うだけで簡単に実行することができます。

整数%整数
int x, y;
int ans;

x = 10;
y = 3;

ans = x % y; /* ansに1が格納される */

その一方で、剰余演算子では被除数(% 演算子の前側の数)もしくは除数(% 演算子の後ろ側の数)のどちらか一方が浮動小数点数である場合、コンパイルエラーが発生してしまいます。つまり浮動小数点数に対する剰余演算は % 演算子では実行することができません。

整数%浮動小数点数
int x;
double y;
double ans;

x = 10;
y = 3.3;

ans = x % y; /* ここでコンパイルエラー */

ちなみに私の環境だと、上記のソースコードをコンパイルすると次のようなエラーが発生しました。

error: invalid operands to binary expression ('int' and 'double')
ans = x % y; /* ここでコンパイルエラー */

fmod 関数を利用する

このように、浮動小数点数に対しては剰余演算子 % を利用することができません。

C言語 では、浮動小数点数に対して剰余演算を行う演算子は存在しませんが、その代わりに浮動小数点数に対して剰余演算を行う関数が用意されています。

fmod 関数

その関数は、下記の fmod 関数になります。

fmod
#include <math.h>

double fmod(double x, double y);

上記の fmod 関数は引数・返却値ともに double 型になりますが、他にも long double 型や float 型に対応した fmodl 関数と fmodf 関数も存在します。

#include <math.h>

long double fmodl(long double x, long double y);

float fmodf(float x, float y);

このページでは fmod 関数について解説していきますが、ご自身のソースコードで使用する型に応じて fmodl 関数や fmodf 関数を使い分けていただければと思います。基本的な使い方は同じです。

MEMO

ちなみに fmod によく似た名前の関数に modf 関数が存在します

この modf 関数は浮動小数点数の整数部と小数部を分離する関数であり、全く異なる関数なので注意してください

スポンサーリンク

fmod 関数の使い方

fmod 関数はヘッダーファイル math.h でプロトタイプ宣言されていますので、fmod を使用する際には math.h を事前にインクルードしておく必要があります。

第1引数 x には被除数を、第2引数 y には除数を指定して実行すると、fmod 関数の戻り値として xy で割った時の余りを取得することができます。

fmod 関数の使用例は下記のようになります。

fmodの使用例
#include <stdio.h>
#include <math.h>

int main(void) {
    double x;
    double y;
    double ans;

    x = 0.07;
    y = 0.03;

    ans = fmod(x, y);
    
    printf("%f\n", ans);

    x = 0.3;
    y = 0.07;

    ans = fmod(x, y);
    
    printf("%f\n", ans);

    return 0;
}

上記を実行すると、結果として次のように表示されます。

0.010000
0.020000

fmod 関数の返却値の詳細

扱う値が浮動小数点数になるものの、基本的に返却値の考え方は % 演算子と同様です。

fmod 関数の返却する値の符号(正負)は被除数である第1引数 x と一致します。

この剰余演算結果の符号については下記ページで解説していますので、詳しく知りたい方は下記ページをご参照ください。

負の値に対する剰余演算の結果まとめページのアイキャッチ 【C言語】負の値に対する剰余演算の結果まとめ

さらに、 fmod 関数の返却する値の絶対値は、必ず除数である第2引数 y の絶対値よりも小さくなります。

また、第2引数 y0 を指定した場合や、剰余演算結果が無限大になるような場合に fmod 関数が返却する値は処理系依存になるようです。

ちなみに私の環境(MacOSX BigSur、gcc 11.2.0)では、上記のような場合は両者ともに fmod 関数は NAN を返却するようでした。

環境によって異なる可能性はありますので、ぜひご自身の環境でも、上記のような場合に fmod 関数がどんな値を返却するかを確認しておくと良いと思います。

fmod 関数使用時の注意点

前述の通り fmod 関数の引数や返却値は浮動小数点数です。

実数を浮動小数点数で扱う場合は誤差が発生することがあり、その誤差によって fmod 関数の結果が意図しないものになることがあるので注意してください。

その fmod 関数の結果が意図しないものになる例が下記になります。

結果が意図通りにならない例
#include <stdio.h>
#include <math.h>

int main(void) {
    double x;
    double y;
    double ans;

    x = 5.05;
    y = 0.05;

    ans = fmod(x, y);
    
    printf("%f\n", ans);

    return 0;
}

上記のプログラムを実行した場合、最後の printf で表示される値はいくつになるでしょうか?

この場合、意図する結果は 0 だと思います。その一方で、上記を実行したときに実際に得られる結果は次のようになります。

0.050000

明らかにおかしいですよね…。

そもそも fmod 関数の返却値の詳細 で下記のように説明したのに、これに反しているようにも思えます。

さらに、 fmod 関数の返却する値の絶対値は、必ず除数である第2引数 y の絶対値よりも小さくなります。

このような結果になるのは、浮動小数点数を扱ったときに発生する誤差が原因です。

上記のソースコードでは、x = 5.05y = 0.05 を実行していますが、実際に xy に格納される値は次のようになります。

x:5.0499999999999998
y:0.050000000000000003

ちょっと大雑把に x = 5.0499y = 0.05 として考えれば、x / y の余りは 0.0499 になるので、それが printf での表示時に値が丸められて 0.05 になると考えると、まぁ納得かなぁいう気もしますね。

こんな感じで浮動小数点数を扱うと誤差が発生し、その誤差によって思いもよらぬ結果になることがあるので注意してください。

もし上記のように誤差が出たとしても、足し算・引き算・掛け算・割り算に関しては、演算結果に大きな影響を及ぼすようなケースは少ないと思います。ですが、剰余演算の場合、すごく小さな誤差であっても演算結果に大きな差が出る(結果が 0 になるはずなのに結果がy になってしまう)ことがあるので、特に注意が必要だと思います。

ちなみにですが、浮動小数点数を扱った場合に発生する誤差は、単に printfを実行するだけでは確認しづらいです。

例えば下記のように x = 5.05 実行後の x の値を printf で表示しても、

誤差がうまく確認できない
double x = 5.05;

printf("%f\n", x);

下記のように表示され誤差がないように感じてしまいます(実際は上で示しているように、x には 5.0499... が格納されている)。

5.050000

これは printfで表示する値にも誤差が含まれているからです(5.05 を浮動小数点数を扱うことによる誤差が printf で表示する際の誤差で打ち消されている)。

どうすれば誤差を正確に表示することができるかは下記ページで解説していますので、興味のある方は是非読んでみてください。浮動小数点数を扱っているプログラムが思ったように動作しない場合のデバッグの助けになると思います!

printfで小数点以下の桁をたくさん表示する方法の解説ページアイキャッチ 【C言語】printfで小数点以下の桁をたくさん表示する方法

スポンサーリンク

自力で演算する

次は、浮動小数点数に対して剰余演算を行う方法の2つ目として、自力で演算を行う方法について解説していきます。

整数に対する剰余演算結果の求め方

この方法は、剰余演算がどのような演算であるかを考えることで導き出せる方法になります。

ということで、まずは剰余演算がどのような演算であるかを整数同士の剰余演算でおさらいしていきましょう。

被除数を整数の x、除数を整数の y とすれば、x % y の剰余演算は xy で割った時の余りです。

さらに C言語 においては、整数同士の割り算結果は整数に丸められます。ですので x / y の結果は必ず整数になります。

MEMO

整数同士の割り算結果は、丸めた後の結果が 0 に近づくように整数に丸められます

例えば x / y3.05 である場合は 3 に丸められますし、x / y-3.05 の場合は -3 に丸められます

ここまでをおさらいした上で、下記の式について考えてみましょう(xyz ともに int 型で、xy には 0 以外の整数が格納されているとします)。

割り算結果を掛けた結果
z = x / y * y;

この式を実行した後の z の値は x の値と一致するでしょうか?

数学的に考えると zx は一致するはずですが、C言語 プログラムでは x / yxy で割った時の結果を整数に丸めたものですので、zx とで一致する場合と一致しない場合があります。

一致する場合は、xy で割り切れる場合です。つまり余りがない場合です。その一方で、一致しない場合は、xy で割り切れない場合です。つまり xy で割ると余り出る場合です。この時、余りが出た分 zx よりも小さくなります。

逆に考えると、zxy で割った時の余りを足してやれば、その結果は x に一致することになります。

したがって、z を下記のように求めれば x と一致することになります。

被除数を元の値に戻すための式
z = x / y * y + x % y;

この zx に置き換えても成立します。さらに、その置き換えを行なった上で x % y が左辺に来るように式を整理すれば、下記の式に変形することができます。

剰余演算結果の求め方
x % y = x - x / y * y

つまり、x に対する y での剰余演算結果、つまり xy で割った時の余りは上式により求めることができることになります。したがって、x - x / y * y は剰余演算と同等の処理であると考えることができます。

浮動小数点数に対する剰余演算結果の求め方

ここまでは xy が整数の場合の話です。次は、本題の xy が浮動小数点数であるときについて考えていきましょう!

といっても、余りの求め方の考え方は整数の時と同様で、ベースは先ほど導き出した下記の式になります。

剰余演算結果の求め方
剰余演算結果 = x - x / y * y

ただ、この式を成立させるのには、x / y の結果が整数である必要があります。その一方で 整数 / 整数 の時とは異なり、浮動小数点数 / 浮動小数点数 の結果は浮動小数点数となります。

この場合、x / y の結果は小数点以下も含めて算出されるため、x / y * yx と一致することになり、上式の右辺の結果は必ず 0 になることになります(より詳細にいうと、浮動小数点数で扱うために誤差が発生する可能性があり、上式の結果はほぼ 0 となる)。

これだと剰余演算結果を求めることができないので、x / y の結果を整数に丸める必要があります。この整数への丸めは、x / y の結果を整数型の変数に格納する or  (int) 等のキャスト演算子を用いて割り算結果を整数型にキャストすることで実現することができます。

例えば後者の方法を採用すれば、下記の式により浮動小数点数に対する剰余演算結果を求めることができることになります。

浮動小数点数に対する剰余演算
剰余演算結果 = x - (int)(x / y) * y

負の値も扱うためには、符号ありの型にキャストする必要がある点に注意してください(例えば (int)(x / y) ではなく (unsigned int)(x / y) にすると負の値の演算結果がおかしくなる)。

ちなみに、上記の式で剰余演算結果を求めた場合、剰余演算結果は被除数の符号(正負)に一致しますので、符号に関しては整数に対して % 演算子で剰余演算を行なった時と同等の結果を得ることができます。

また、上記では (int) によりキャストを行なっていますが、x / yint 型で扱える値を超えた場合に桁あふれが発生してしまうので注意してください。特に y の絶対値が 1 よりも小さい値の場合は簡単に桁あふれが発生してしまいます。

これを防ぐためには、(int) よりも扱える値の範囲が大きい型の (long long int) などでキャストを行う方が良いです(次に紹介する関数でも (long long int) でキャストを行なっています。

スポンサーリンク

浮動小数点数に対して剰余演算を行う関数

ここまでの解説内容を踏まえて作成した浮動小数点数に対する剰余演算を行う関数は下記の my_fmod になります。

my_fmod
double my_fmod(double x, double y) {

    double ans;

    /* long doubleで扱うことで誤差を極力減らす */
    long double lx = x;
    long double ly = y;

    if (ly != 0) {
        ans = lx - (long long int)(lx / ly) * ly;
    } else {
        /* 0除算時は0を返却 */
        ans = 0;
    }

    return ans;
}

この my_fmod 関数の使い方に関しては、fmod 関数を利用する で紹介した fmod 関数と同様なので説明は省略します。

また、fmod 関数を利用する で説明したように、fmod 関数では環境によっては第2引数 y0 であったり y / x が無限大になるような場合に NAN を返却することもありますが、この my_fmod 関数に関しては第2引数 y0 の場合のみ 0 を返却し、それ以外の場合は全て剰余演算を求める式を実行した結果を返却するようになっています。

こういった特殊ケースを除けば、my_fmod 関数でも大体 fmod 関数と同様の結果は得られるようになっていると思います。ただ my_fmod 関数と fmod 関数では引数が同じでも結果に若干誤差が出ますね…。

この誤差を減らしたい& double 型よりも long double 型の方が精度が高くなる環境なのであれば、my_fmod 関数を下記のように変更すれば、多少は誤差は減ると思います。

my_fmod
double my_fmod(double x, double y) {

    double ans;

    /* より精度の高い浮動小数点数で扱う */
    long double lx = x;
    long double ly = y;

    if (ly != 0) {
        ans = lx - (long long int)(lx / ly) * ly;
    } else {
        /* 0除算時は0を返却 */
        ans = 0;
    }

    return ans;
}

これで my_fmod 関数と fmod 関数の誤差が減り、全く同じ値になることもありますが、それでも引数によって多少の誤差が発生することがあるようでした。

自力で剰余演算を行う時の注意点

上記により自力で剰余演算が行えますが、fmod 関数と全く同じ値にならないことがある点にはご注意ください。

また fmod 関数使用時の注意点 で紹介した浮動小数点数による誤差が本方法により解決できるというわけではない点に注意してください。あくまでも my_fmod 関数は fmod 関数と同様の結果を得るための関数であり、fmod 関数で生じる問題は同様に my_fmod 関数でも生じます。

桁上げしてから整数同士の剰余演算を行う

最後の方法として、浮動小数点数を桁上げしてから整数同士の剰余演算を行う方法を紹介していきます。

スポンサーリンク

剰余演算前に桁上げを行う

浮動小数点数に対しては剰余演算子 % を利用することはできませんが、整数であれば剰余演算子 % を用いて剰余演算を行うことができます。

ですので、浮動小数点数を、例えば int 型等の整数型の変数に格納して整数化してから剰余演算を行えば、当然剰余演算を行うことができることになります。

ただ、与えられた浮動小数点数を単に整数化してしまうと小数点以下が無くなってしまうため、必要な小数点以下の値を整数部に移動してから整数化を行う必要があります。

この整数部への移動をするために行うのが桁上げです。この桁上げは、1001000 などの 10 の累乗の値を剰余演算を行いたい浮動小数点数に掛けることで実現することができます(その冪数の数だけ値を桁上げすることができます)。

桁上げしてから剰余演算を行う様子

例えば double 型の変数 x と変数 y に対して剰余演算を行いたいのであれば、下記のように処理を行います。

剰余演算前の桁上げ
double x, y;
long long int ix, iy;

x = 5.05;
y = 0.05;

/* 3桁桁上げしてから整数化 */
ix = x * 1000; /* 5050 */
iy = y * 1000; /* 50 */

/* ix % iy は実行可能 */

誤差を減らすために四捨五入する

上記の場合はうまくいくのですが、浮動小数点数を扱う以上はやっぱり誤差が出てしまいます。

例えば下記を実行した場合、ix2 になってしまいます(本当であれば 3 になって欲しい)。

整数変換後に誤差が含まれる例
double x;
long long int ix;

x = 0.0003;

/* 4桁桁上げしてから整数化 */
ix = x * 10000;

なぜ 2 になるかというと、x = 0.0003 によって格納される値が下記のように誤差を含んだものになっているからです。

0.00029999999999999997

こういった誤差があると整数変換後の値にも誤差が発生することがあります。そして整数変換後の値に誤差があると、当然剰余演算結果にも誤差が出ることになります。

ただ、この誤差はよっぽど大きくない限り、桁上げ後の値に対して小数第1位で四捨五入してから整数変換することで、大体の誤差を無くすことができると思います。

四捨五入で誤差を減らす
double x;
double shift_x;
long long int ix;

x = 0.0003;

/* 4桁桁上げ */
shift_x = x * 10000;

/* 四捨五入してから整数化 */
ix = shift_x + 0.5;

なぜ ix = shift_x + 0.5 で四捨五入ができるかについては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。

切り捨て四捨五入切り上げの解説ページのアイキャッチ C言語で割り算結果を「切り捨て」「四捨五入」「切り上げ」する方法

shift_x の値は下記のように誤差を含むことになるので、そのまま int 型の変数に格納すると 2 になってしまいますが、四捨五入してから int 型の変数に格納すれば 3 になります(浮動小数点数を int 型の変数に格納する際には、小数点以下の値が切り捨てられる)。

shift_x      :2.9999999999999996
shift_x + 0.5:3.4999999999999996

ただ、上記の四捨五入処理は x が正の値であることを前提にした処理になっており、負の値を四捨五入する場合は x = shift_x - 0.5 の処理を実行する必要があります。

剰余演算後に桁下げを行う

上記のように被除数と除数を桁上げしてから剰余演算を行なった場合、剰余演算の結果もその分桁上げした状態の値として算出されることになります。

したがって、もともとの浮動小数点数で剰余演算を行った結果に戻すため、剰余演算結果に対して桁下げする必要があります。

この桁下げは、被除数と除数を桁上げする時に掛けた値で割ることで実現することができます。

剰余演算結果を桁下げする様子

例えば被除数と除数を 3 桁桁上げするために 1000 を掛けた状態で剰余演算を行なった場合、本来の剰余演算結果の 1000 倍の値が求まることになるので、剰余演算結果を 1000 で割ってやれば良いです。

スポンサーリンク

桁上げしてから剰余演算を行う関数

以上の解説を踏まえて作成した関数が下記の shift_fmod になります。

shift_fmod
double shift_fmod(double x, double y) {

    double shift_ans, ans;
    long long int ix, iy;
    unsigned int shift;
    double shift_x, shift_y;

    /* 桁上げ時に掛ける値を設定 */
    shift = 1000;

    /* shiftを掛けてxとyを桁上げする */
    shift_x = x * shift;
    shift_y = y * shift;

    /* 四捨五入してから整数化 */
    if (shift_x < 0) {
        ix = shift_x - 0.5;
    } else {
        ix = shift_x + 0.5;
    }

    if (shift_y < 0) {
        iy = shift_y - 0.5;
    } else {
        iy = shift_y + 0.5;
    }

    /* 整数同士で剰余演算 */
    if (iy != 0) {
        shift_ans = (double)(ix % iy);
    } else {
        shift_ans = 0;
    }

    /* 剰余演算結果を桁下げ */
    ans = (double)shift_ans / (double)shift;

    return ans;
}

剰余演算前の桁上げは shift を掛けることで、剰余演算後の桁下げは shift で割ることで実現しています。

上記では、この shift1000 に設定しているので 3 桁の桁上げと桁下げが行われることになります。

さらに桁上げ後には整数化を行うため、基本的には小数第 4 位以下の桁の情報が捨てられることになります。

もっと下位の桁も含めて剰余演算を行いたいような場合は shift に格納する値を大きくしてください。

桁上げしてから剰余演算を行う時の注意点

おそらくこの方法が一番誤差が発生しにくいと思います(おそらくですが…)。

ただ、剰余演算時にどれだけ桁上げするか(桁上げするときに掛ける値 shift をいくつにするか)を上手く決めるのが難しいというのがこの方法の注意点になると思います。

上記の shift_fmod 関数は、1000 を掛けることで引数で与えられる浮動小数点数を 3 桁桁上げ後に整数化しているので、小数第 4 位以下の値は捨てられることになります。

ですので、上記の shift_fmod 関数で扱えるのは小数第 3 位までのみとなります。

小数第 4 位の値も含めて剰余演算を行いたいのであれば、桁上げするときに掛ける値 shift10000 にする必要がありますし、小数第 5 位の値も含めて剰余演算を行いたいのであれば shift100000 にする必要があります。

つまり、桁上げするときに掛ける値 shift をうまく設定することで、小数点以下の桁をどこまで扱うかを決めることができます。

逆にいうと、桁上げ時に掛ける値 shift をうまく設定しなければ、本当は扱いたい小数点以下の桁が捨てられることになり、剰余演算をうまく行うことができなくなってしまいます。

したがって、どの桁までを扱いたいのかをしっかり決めてから、桁上げ時に掛ける値 shift を設定する必要があります。

もちろん桁上げ時に掛ける値 shift が大きいほど、扱える小数点以下の桁数も増えますが、shift での掛け算時に桁あふれが発生する可能性も高くなるので、その点にも注意が必要です。

ただ、誤差は減りますし、扱いにくい浮動小数点数ではなく整数で演算できるため、どれだけ桁上げすれば良いかをしっかり決められるのであれば、今回紹介した方法の中では一番オススメの方法だと思います。

まとめ

このページでは浮動小数点数に対する剰余演算を行う方法について解説しました。

方法としては大きく下記の3つがあります。

1. と 2. と浮動小数点数に対して剰余演算を直接行う方法であり、3. に対しては浮動小数点数を整数に変換してから整数に対して剰余演算を行う方法になります。

いずれの方法も浮動小数点数を扱うので誤差に注意が必要です。

おそらく一番その誤差の影響を受けにくいのが 3. の方法だと思いますので、扱う小数点以下の桁数が決められるのであればこの方法が一番オススメです。

この記事を書いてて改めて思ったのが「浮動小数点数の扱いの難しさ」ですね…。

fmod 関数使用時の注意点 で解説したように、その誤差により演算結果が思わぬ結果になることがあるのが浮動小数点数を扱う時の難しさだと思います。

この記事の内容から、浮動小数点数に対する剰余演算のやり方を理解するだけでなく、浮動小数点数の誤差や扱いの難しさについて学んでいただけたのであれば幸いです!

オススメの参考書(PR)

C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!

まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。

  • 参考書によって、解説の仕方は異なる
  • 読み手によって、理解しやすい解説の仕方は異なる

ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?

それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。

なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。

特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。

もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!

入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示