このページでは、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; /* ここでコンパイルエラー */
Contents
fmod
関数を利用する
このように、浮動小数点数に対しては剰余演算子 %
を利用することができません。
C言語
では、浮動小数点数に対して剰余演算を行う演算子は存在しませんが、その代わりに浮動小数点数に対して剰余演算を行う関数が用意されています。
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
関数を使い分けていただければと思います。基本的な使い方は同じです。
ちなみに fmod
によく似た名前の関数に modf
関数が存在します
この modf
関数は浮動小数点数の整数部と小数部を分離する関数であり、全く異なる関数なので注意してください
スポンサーリンク
fmod
関数の使い方
fmod
関数はヘッダーファイル math.h
でプロトタイプ宣言されていますので、fmod
を使用する際には math.h
を事前にインクルードしておく必要があります。
第1引数 x
には被除数を、第2引数 y
には除数を指定して実行すると、fmod
関数の戻り値として x
を y
で割った時の余りを取得することができます。
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引数 y
に 0
を指定した場合や、剰余演算結果が無限大になるような場合に 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.05
と y = 0.05
を実行していますが、実際に x
と y
に格納される値は次のようになります。
x:5.0499999999999998 y:0.050000000000000003
ちょっと大雑把に x = 5.0499
、y = 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
で表示する際の誤差で打ち消されている)。
どうすれば誤差を正確に表示することができるかは下記ページで解説していますので、興味のある方は是非読んでみてください。浮動小数点数を扱っているプログラムが思ったように動作しない場合のデバッグの助けになると思います!
【C言語】printfで小数点以下の桁をたくさん表示する方法スポンサーリンク
自力で演算する
次は、浮動小数点数に対して剰余演算を行う方法の2つ目として、自力で演算を行う方法について解説していきます。
整数に対する剰余演算結果の求め方
この方法は、剰余演算がどのような演算であるかを考えることで導き出せる方法になります。
ということで、まずは剰余演算がどのような演算であるかを整数同士の剰余演算でおさらいしていきましょう。
被除数を整数の x
、除数を整数の y
とすれば、x % y
の剰余演算は x
を y
で割った時の余りです。
さらに C言語
においては、整数同士の割り算結果は整数に丸められます。ですので x / y
の結果は必ず整数になります。
整数同士の割り算結果は、丸めた後の結果が 0
に近づくように整数に丸められます
例えば x / y
が 3.05
である場合は 3
に丸められますし、x / y
が -3.05
の場合は -3
に丸められます
ここまでをおさらいした上で、下記の式について考えてみましょう(x
、y
、z
ともに int
型で、x
と y
には 0
以外の整数が格納されているとします)。
z = x / y * y;
この式を実行した後の z
の値は x
の値と一致するでしょうか?
数学的に考えると z
と x
は一致するはずですが、C言語
プログラムでは x / y
は x
を y
で割った時の結果を整数に丸めたものですので、z
と x
とで一致する場合と一致しない場合があります。
一致する場合は、x
が y
で割り切れる場合です。つまり余りがない場合です。その一方で、一致しない場合は、x
が y
で割り切れない場合です。つまり x
を y
で割ると余り出る場合です。この時、余りが出た分 z
は x
よりも小さくなります。
逆に考えると、z
に x
を y
で割った時の余りを足してやれば、その結果は x
に一致することになります。
したがって、z
を下記のように求めれば x
と一致することになります。
z = x / y * y + x % y;
この z
は x
に置き換えても成立します。さらに、その置き換えを行なった上で x % y
が左辺に来るように式を整理すれば、下記の式に変形することができます。
x % y = x - x / y * y
つまり、x
に対する y
での剰余演算結果、つまり x
を y
で割った時の余りは上式により求めることができることになります。したがって、x - x / y * y
は剰余演算と同等の処理であると考えることができます。
浮動小数点数に対する剰余演算結果の求め方
ここまでは x
と y
が整数の場合の話です。次は、本題の x
と y
が浮動小数点数であるときについて考えていきましょう!
といっても、余りの求め方の考え方は整数の時と同様で、ベースは先ほど導き出した下記の式になります。
剰余演算結果 = x - x / y * y
ただ、この式を成立させるのには、x / y
の結果が整数である必要があります。その一方で 整数 / 整数
の時とは異なり、浮動小数点数 / 浮動小数点数
の結果は浮動小数点数となります。
この場合、x / y
の結果は小数点以下も含めて算出されるため、x / y * y
は x
と一致することになり、上式の右辺の結果は必ず 0
になることになります(より詳細にいうと、浮動小数点数で扱うために誤差が発生する可能性があり、上式の結果はほぼ 0
となる)。
これだと剰余演算結果を求めることができないので、x / y
の結果を整数に丸める必要があります。この整数への丸めは、x / y
の結果を整数型の変数に格納する or (int)
等のキャスト演算子を用いて割り算結果を整数型にキャストすることで実現することができます。
例えば後者の方法を採用すれば、下記の式により浮動小数点数に対する剰余演算結果を求めることができることになります。
剰余演算結果 = x - (int)(x / y) * y
負の値も扱うためには、符号ありの型にキャストする必要がある点に注意してください(例えば (int)(x / y)
ではなく (unsigned int)(x / y)
にすると負の値の演算結果がおかしくなる)。
ちなみに、上記の式で剰余演算結果を求めた場合、剰余演算結果は被除数の符号(正負)に一致しますので、符号に関しては整数に対して %
演算子で剰余演算を行なった時と同等の結果を得ることができます。
また、上記では (int)
によりキャストを行なっていますが、x / y
が int
型で扱える値を超えた場合に桁あふれが発生してしまうので注意してください。特に y
の絶対値が 1
よりも小さい値の場合は簡単に桁あふれが発生してしまいます。
これを防ぐためには、(int)
よりも扱える値の範囲が大きい型の (long long int)
などでキャストを行う方が良いです(次に紹介する関数でも (long long int)
でキャストを行なっています。
スポンサーリンク
浮動小数点数に対して剰余演算を行う関数
ここまでの解説内容を踏まえて作成した浮動小数点数に対する剰余演算を行う関数は下記の 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引数 y
が 0
であったり y / x
が無限大になるような場合に NAN
を返却することもありますが、この my_fmod
関数に関しては第2引数 y
が 0
の場合のみ 0
を返却し、それ以外の場合は全て剰余演算を求める式を実行した結果を返却するようになっています。
こういった特殊ケースを除けば、my_fmod
関数でも大体 fmod
関数と同様の結果は得られるようになっていると思います。ただ my_fmod
関数と fmod
関数では引数が同じでも結果に若干誤差が出ますね…。
この誤差を減らしたい& double
型よりも long double
型の方が精度が高くなる環境なのであれば、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
型等の整数型の変数に格納して整数化してから剰余演算を行えば、当然剰余演算を行うことができることになります。
ただ、与えられた浮動小数点数を単に整数化してしまうと小数点以下が無くなってしまうため、必要な小数点以下の値を整数部に移動してから整数化を行う必要があります。
この整数部への移動をするために行うのが桁上げです。この桁上げは、100
や 1000
などの 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 は実行可能 */
誤差を減らすために四捨五入する
上記の場合はうまくいくのですが、浮動小数点数を扱う以上はやっぱり誤差が出てしまいます。
例えば下記を実行した場合、ix
は 2
になってしまいます(本当であれば 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
で四捨五入ができるかについては下記ページで解説していますので、詳しく知りたい方は下記ページを参照していただければと思います。
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
になります。
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
で割ることで実現しています。
上記では、この shift
を 1000
に設定しているので 3
桁の桁上げと桁下げが行われることになります。
さらに桁上げ後には整数化を行うため、基本的には小数第 4
位以下の桁の情報が捨てられることになります。
もっと下位の桁も含めて剰余演算を行いたいような場合は shift
に格納する値を大きくしてください。
桁上げしてから剰余演算を行う時の注意点
おそらくこの方法が一番誤差が発生しにくいと思います(おそらくですが…)。
ただ、剰余演算時にどれだけ桁上げするか(桁上げするときに掛ける値 shift
をいくつにするか)を上手く決めるのが難しいというのがこの方法の注意点になると思います。
上記の shift_fmod
関数は、1000
を掛けることで引数で与えられる浮動小数点数を 3
桁桁上げ後に整数化しているので、小数第 4
位以下の値は捨てられることになります。
ですので、上記の shift_fmod
関数で扱えるのは小数第 3
位までのみとなります。
小数第 4
位の値も含めて剰余演算を行いたいのであれば、桁上げするときに掛ける値 shift
を 10000
にする必要がありますし、小数第 5
位の値も含めて剰余演算を行いたいのであれば shift
を 100000
にする必要があります。
つまり、桁上げするときに掛ける値 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/