C++において剰余演算は、数値計算、アルゴリズムの実装、データの周期的な処理など、プログラミングのあらゆる局面で頻繁に利用される基本的な操作です。
しかし、単純に % 演算子を使用するだけでは、負の数が関わる計算や浮動小数点数の扱いにおいて、予期せぬ挙動やバグに直面することが少なくありません。
本記事では、C++における剰余演算の仕様を深く掘り下げ、実務で役立つ正しい使い方と注意点を詳しく解説します。
C++における剰余演算の基本仕様
C++の剰余演算子 % は、2つの整数値の間で除算を行い、その「あまり」を求めるために使用されます。
この演算子は、整数型(int, long, short など)および文字型(char)に対して適用可能ですが、浮動小数点数(float や double)に対して直接使用することはできないという制約があります。
整数同士の剰余演算
最も基本的な使い方は、正の整数同士の計算です。
C++における除算と剰余の関係は、数学的な定義に基づいて以下の式を維持するように規定されています。
a = (a / b) * b + (a % b)
この式において、a / b は商(整数除算による切り捨てられた値)であり、a % b がその余りとなります。
以下のプログラムで、基本的な動作を確認してみましょう。
#include <iostream>
int main() {
int a = 10;
int b = 3;
// 10を3で割った余りを計算
int result = a % b;
std::cout << "10 % 3 = " << result << std::endl;
// 除算と剰余の関係を確認
int quotient = a / b;
std::cout << "商: " << quotient << ", 余り: " << result << std::endl;
std::cout << "再現計算: (" << quotient << " * " << b << ") + " << result
<< " = " << (quotient * b + result) << std::endl;
return 0;
}
10 % 3 = 1
商: 3, 余り: 1
再現計算: (3 * 3) + 1 = 10
このように、正の整数同士であれば直感通りの結果が得られます。
しかし、剰余演算の本質的な難しさは、オペランドに負の数が含まれる場合に現れます。
負の数が含まれる場合の剰余演算
負の数に対する剰余演算の結果は、プログラミング言語によって挙動が異なる場合があるため、非常に注意が必要です。
C++においては、C++11規格以降、挙動が明確に統一されました。
C++11以降の規格:ゼロ方向への丸め
C++11より前の古い規格では、負の数を含む除算の切り捨て方向が実装依存(処理系定義)となっていました。
しかし、現代のC++(C++11, C++14, C++17, C++20, C++23以降)では、「商は常にゼロの方向に丸められる(truncate toward zero)」と定められています。
これに伴い、剰余の符号についても以下のルールが適用されます。
剰余 a % b の結果の符号は、常に被除数(左項)a の符号と一致する。
具体的な例を見てみましょう。
| 式 | 商 (a / b) | 剰余 (a % b) | 理由 |
|---|---|---|---|
| 7 % 3 | 2 | 1 | 7 = (2 * 3) + 1 |
| -7 % 3 | -2 | -1 | -7 = (-2 * 3) + (-1) |
| 7 % -3 | -2 | 1 | 7 = (-2 * -3) + 1 |
| -7 % -3 | 2 | -1 | -7 = (2 * -3) + (-1) |
以下のサンプルコードで、この挙動を検証します。
#include <iostream>
int main() {
// 負の数が含まれる場合の剰余
std::cout << "-7 % 3 = " << (-7 % 3) << std::endl;
std::cout << " 7 % -3 = " << (7 % -3) << std::endl;
std::cout << "-7 % -3 = " << (-7 % -3) << std::endl;
return 0;
}
-7 % 3 = -1
7 % -3 = 1
-7 % -3 = -1
数学的な剰余(常に正の剰余)を求める方法
多くのアルゴリズム(特にハッシュ計算やカレンダー計算)では、結果を常に 0 から b - 1 の範囲に収めたい、つまり「常に正の剰余」を求めたい場合があります。
C++の標準の % 演算子では、左項が負の場合に結果も負になってしまうため、工夫が必要です。
もっとも一般的で効率的な実装は、以下の計算式を用いる方法です。
int mod = (a % b + b) % b;
この式により、a が負であっても、一度 b を足すことで正の範囲に持ち込み、再度剰余を取ることで正しい巡回範囲に収めることができます(ただし b が正であることを前提とします)。
浮動小数点数における剰余演算
前述の通り、C++において % 演算子は整数専用です。
浮動小数点数(float, double, long double)に対して余りを求めたい場合は、標準ライブラリの <cmath> ヘッダーに含まれる関数を使用する必要があります。
主に利用されるのは std::fmod と std::remainder の2種類です。
これらは挙動が異なるため、用途に応じた使い分けが不可欠です。
std::fmod の使い方
std::fmod は、整数演算子の % と同様に、商をゼロの方向に丸めたときの余りを返します。
#include <iostream>
#include <cmath>
int main() {
double x = 5.3;
double y = 2.0;
double res = std::fmod(x, y); // 5.3 / 2.0 = 2.65 -> 商2、余り1.3
std::cout << "std::fmod(5.3, 2.0) = " << res << std::endl;
// 負の数の場合
std::cout << "std::fmod(-5.3, 2.0) = " << std::fmod(-5.3, 2.0) << std::endl;
return 0;
}
std::fmod(5.3, 2.0) = 1.3
std::fmod(-5.3, 2.0) = -1.3
物理シミュレーションやグラフィックスなどで、一定の範囲内に値をラップさせたい場合には、この std::fmod が適しています。
std::remainder の使い方(C++11以降)
std::remainder は、IEEE 754 規格に基づいた演算を行います。
これは、商を「最も近い整数」に丸めたときの余りを返します。
もし商がちょうど中間(0.5など)であれば、偶数の方に近い整数が選ばれます。
#include <iostream>
#include <cmath>
int main() {
// fmodとの違い
// 5.1 / 3.0 = 1.7 -> 商を2として計算 (2 * 3 = 6)
// 余りは 5.1 - 6 = -0.9
std::cout << "std::remainder(5.1, 3.0) = " << std::remainder(5.1, 3.0) << std::endl;
std::cout << "std::fmod(5.1, 3.0) = " << std::fmod(5.1, 3.0) << std::endl;
return 0;
}
std::remainder(5.1, 3.0) = -0.9
std::fmod(5.1, 3.0) = 2.1
std::remainder は、計算誤差の累積を最小限に抑えたい学術計算や特定の信号処理などで利用されます。
一般的な「あまり」のイメージに近いのは std::fmod です。
剰余演算のパフォーマンスと最適化
剰余演算は、加算や乗算に比べて計算コストが高い処理です。
特にループの中で大量に剰余演算を行う場合、パフォーマンスに影響を与える可能性があります。
CPUレベルでは、剰余演算は除算命令の一部として実行されることが多く、これは非常にサイクル数を消費する命令です。
2の累乗による最適化
除数(割る数)が「2の累乗(2, 4, 8, 16, …)」であることがコンパイル時に分かっている場合、コンパイラは剰余演算を高速なビット演算(AND演算)に置き換えます。
たとえば、x % 8 は x & 7 と等価です。
#include <iostream>
int main() {
unsigned int x = 25;
unsigned int res1 = x % 8;
unsigned int res2 = x & 7; // (8 - 1)
std::cout << "Result 1: " << res1 << ", Result 2: " << res2 << std::endl;
return 0;
}
ビット演算は1サイクルで完了するため、非常に高速です。
ただし、この最適化は符号なし整数(unsigned)または正の整数であることが保証されている場合にのみ安全に適用されます。
負の数が関わる場合はビットの並びが異なるため、単純な置き換えはできません。
std::div による商と剰余の同時取得
プログラム内で、同じ値に対して商(/)と剰余(%)の両方を必要とする場面があります。
この場合、別々に演算を行うよりも、std::div 関数(または std::ldiv など)を使用する方が効率的です。
多くのCPUアーキテクチャでは、除算命令を実行した際に商と余りが同時に計算されます。
std::div を使うことで、コンパイラに対して「一度の命令で両方の値を取得したい」という意図を明確に伝えることができます。
#include <iostream>
#include <cstdlib> // std::div
int main() {
int numerator = 17;
int denominator = 5;
std::div_t result = std::div(numerator, denominator);
std::cout << "商: " << result.quot << ", 余り: " << result.rem << std::endl;
return 0;
}
実践的な活用シーンとテクニック
剰余演算は単なる計算以上に、プログラムの構造を制御するために多用されます。
ここでは代表的な3つのパターンを紹介します。
1. 周期性とインデックスのラッピング
配列やバッファを循環して使いたい場合、剰余演算は必須です。
例えば、サイズ N の配列に対してインデックスをインクリメントし続け、最後まで到達したら 0 に戻したい場合です。
#include <iostream>
#include <vector>
int main() {
std::vector<std::string> days = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
for (int i = 0; i < 10; ++i) {
// iが7を超えても0〜6の範囲に収まる
std::cout << days[i % 7] << " ";
}
std::cout << std::endl;
return 0;
}
2. 偶数・奇数および倍数の判定
剰余が 0 であるかどうかを確認することで、特定の数の倍数であることを判定できます。
if (n % 2 == 0) { /* 偶数 */ }
if (n % 3 == 0) { /* 3の倍数 */ }
3. 時間の単位変換
秒単位のデータを「分:秒」の形式に変換する際にも剰余が活躍します。
int total_seconds = 125;
int minutes = total_seconds / 60; // 2
int seconds = total_seconds % 60; // 5
剰余演算における注意点とエラー回避
剰余演算を安全に使用するためには、いくつかの致命的なエラーを避ける必要があります。
ゼロ除算(Division by Zero)
剰余演算において、除数(右項)が 0 の場合は、未定義動作(Undefined Behavior)となります。
多くの場合、実行時にプログラムがクラッシュ(Floating point exceptionなどのシグナルが発生)します。
int a = 10;
int b = 0;
int c = a % b; // ここでランタイムエラーが発生する可能性が高い
ユーザー入力や動的な計算結果を除数にする場合は、必ず事前に 0 でないことをチェックするガード節を入れるのが鉄則です。
符号の混在(unsigned と signed)
C++では、符号付き整数(int)と符号なし整数(unsigned int)を混ぜて演算を行うと、暗黙の型変換(型昇格)が行われます。
これにより、負の数が非常に大きな正の数として扱われ、剰余演算の結果が予想外の大きな値になることがあります。
剰余演算を行う際は、両方の項の型を一致させるか、意図的にキャストを行うことで、安全性を確保してください。
まとめ
C++の剰余演算子 % は非常にシンプルに見えますが、その背景には厳密な規格と計算機科学の仕組みが存在します。
本記事で解説した重要なポイントを振り返ります。
| 項目 | 内容 |
|---|---|
| 基本動作 | a % b は整数除算の余りを返す。 |
| 負の数の扱い | C++11以降、商はゼロ方向に丸められ、余りの符号は被除数と同じになる。 |
| 浮動小数点数 | % は使用不可。std::fmod や std::remainder を使用する。 |
| パフォーマンス | 2の累乗の場合はビット演算が有効。商と余りの同時取得には std::div が推奨される。 |
| 安全性 | 除数が 0 にならないよう、必ずチェックを行う。 |
剰余演算を正しく理解し、適切に使いこなすことは、堅牢で効率的なプログラムを書くための第一歩です。
特に負の数が関わるアルゴリズムを実装する際には、本記事で紹介した (a % b + b) % b のようなテクニックを思い出し、バグのないコード作成に役立ててください。






