HOWTO · C
C 言語での Unsigned Int と SignedInt の違い
次の記事では、C プログラミング言語での signed int と unsigned int の違いについて説明します。
このページの内容
signed int は負の値と正の値を表せますが、unsigned int が表すのは 0 以上の値です。C では int はデフォルトで符号付きなので、符号付き整数には int、符号なし整数が必要な場合には unsigned int と記述します。どちらを使うかは、プログラムで扱う値と必要な演算によって決まります。違いは小さく見えます — 一方の型はマイナス記号を認め、他方は認めない — というだけではありません。値の範囲、その範囲の限界で何が起こるか、そして 2 つの型を混ぜた式の振る舞いも変わります。
C言語における signed int と unsigned int の違い
どちらも整数型ですが、値の範囲が異なります。C規格では、int は少なくとも -32767 から 32767 までを表せること、unsigned int は少なくとも 0 から 65535 までを表せることが保証されています。これは最小限の保証であり、すべてのシステムでの正確な上限ではありません。limits.h をインクルードし、INT_MIN、INT_MAX、UINT_MAX を確認すると、コンパイルに使う実装での範囲がわかります。
| 型 | 表せる値 | 移植可能な最小範囲 |
|---|---|---|
int |
負の値と 0 以上の値 | -32767 から 32767 |
unsigned int |
0 以上の値 | 0 から 65535 |
2 つの型は通常同じ記憶サイズを占めるため、どちらを選んでも全体の容量が増えるわけではありません。容量の使い道が変わるのです。符号付き型は値を負の数と正の数に分け、符号なし型は範囲全体を 0 以上の値に使うため、同じ実装では最大値がおよそ 2 倍になります。この記事の例に使った 64 ビット Linux システムでは、どちらの型も 4 バイトを占め、限界値を出力するとおなじみの 32 ビット範囲が得られます。
#include <limits.h>
#include <stdio.h>
int main(void) {
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(unsigned int) = %zu\n", sizeof(unsigned int));
printf("int range: %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int range: 0 to %u\n", UINT_MAX);
return 0;
}
sizeof(int) = 4
sizeof(unsigned int) = 4
int range: -2147483648 to 2147483647
unsigned int range: 0 to 4294967295
これらの正確な数値は言語の性質ではなく、この実装の性質として扱ってください。どこでも動作しなければならないコードは、32 ビットの限界値をハードコードする代わりに、INT_MIN、INT_MAX、UINT_MAX と比較すべきです。
値の範囲と型変換
実際の値の範囲と記憶サイズは実装によって異なります。sizeof(int) と sizeof(unsigned int) が4バイトであるとは限らず、C規格は int が32ビットであることも保証していません。符号付き整数について、左端のビットを符号用に使うと一律に説明することもできません。表現の詳細は実装によって異なります。
負の値を符号なし型に変換すると、結果は UINT_MAX + 1 を法として、その値と合同になります。そのため、符号なしオブジェクトに格納されるのは 0 以上の値であり、負の値を保持して後から読み出せるわけではありません。符号なし演算も UINT_MAX + 1 を法として折り返します。次の例はこの規則を 3 通りに適用します — -1 の変換、ゼロ未満への減算、最大値を超える加算です。
#include <limits.h>
#include <stdio.h>
int main(void) {
unsigned int converted = -1;
unsigned int wrapped_down = 0u - 1u;
unsigned int wrapped_up = UINT_MAX + 1u;
printf("converted -1: %u\n", converted);
printf("0u - 1u: %u\n", wrapped_down);
printf("UINT_MAX + 1u: %u\n", wrapped_up);
return 0;
}
converted -1: 4294967295
0u - 1u: 4294967295
UINT_MAX + 1u: 0
この実装では -1 は 4294967295、つまりちょうど UINT_MAX になります。どの実装でも結果は UINT_MAX そのものです。法とする演算では -1 は常にゼロの 1 つ下だからです。ゼロから 1 を引くと -1 にはならず、同じ最大値へ折り返し、最大値に 1 を足すとゼロに戻ります。これらの演算はどれもエラーではありません。折り返しこそが符号なし演算の定義された振る舞いです。
以下の完全な例では、int に %d、unsigned int に %u を使います。例と出力は変更しておらず、出力中の表示ラベルは英語のままです。例は正常に実行されており、下の出力はその実行結果です。
#include <stdio.h>
int main(void) {
int temperature_change = -7;
unsigned int item_count = 42;
printf("Temperature change: %d\n", temperature_change);
printf("Item count: %u\n", item_count);
return 0;
}
Temperature change: -7
Item count: 42
上記の変換規則を使うと、負の値を符号なし型に代入したときの結果を考えられます。具体的な値は UINT_MAX によって異なります。unsigned int を %u ではなく %d で printf に渡すのは書式指定の不一致であり、未定義動作を引き起こすため、表示結果を当てにすることはできません。
オーバーフロー:符号なしでは定義済み、符号付きでは未定義
上記の折り返しには符号付きの対応物がありません。符号付き整数のオーバーフロー — INT_MAX を超える、または INT_MIN を下回る結果を計算すること — は C では未定義動作であり、折り返しが保証されるわけではありません。コンパイラは「それは決して起こらない」という前提で最適化するため、x + 1 が x より小さいかどうかを調べるようなガードでは、符号付きオーバーフローを事後に確実に検出できません。計算が限界に近づく可能性がある場合は、計算する前に確認してください。たとえば y を x に足す前に x > INT_MAX - y をテストしたり、より広い型を使ったりします。この非対称性は、通常の演算に符号付き型を好む実践的な理由です。符号なし値の誤りは、間違っているのに正しく見える数へ静かに折り返しますが、符号付きコードは限界を実際に超えるまでは期待どおりの値を保ちます。
1 つの式で符号付きと符号なしを混ぜる
最も驚くべき違いは、2 つの型が 1 つの式で出会うときに現れます。通常の算術変換 (usual arithmetic conversions) では、同じランクの int と unsigned int を組み合わせると、演算の前に int オペランドが unsigned int に変換されます。そのため、一方が負の値で始まっていても、比較や演算は符号なしとして行われます。
#include <stdio.h>
int main(void) {
printf("-1 < 1: %d\n", -1 < 1);
printf("-1 < 1u: %d\n", -1 < 1u);
printf("-1 == 4294967295u: %d\n", -1 == 4294967295u);
return 0;
}
-1 < 1: 1
-1 < 1u: 0
-1 == 4294967295u: 1
数学として読めば -1 < 1 は真であり、1 を出力します。サフィックス u を付けると答えは 0 に変わります。-1 がまず 4294967295 に変換され、その値は 1 より小さくないからです。同じ理由で、-1 は最大の符号なし値と等しくなります。GCC はまさにこのパターンに対して -Wsign-compare で警告します。この警告は恒常的に有効化する価値があります。演算も同じ規則に従うため、負の調整量が小さな負の結果ではなく、巨大な正の結果になることがあります。
#include <stdio.h>
int main(void) {
unsigned int count = 10u;
int delta = -20;
int signed_total = 10 + delta;
printf("unsigned total: %u\n", count + delta);
printf("signed total: %d\n", signed_total);
return 0;
}
unsigned total: 4294967286
signed total: -10
ここでは符号なし型は何もオーバーフローしていません。変換だけで 4294967286 が生まれました。値が負になりうる場合 — 2 つのカウントの差、残高、-1 のようなエラー用センチネル — は、0 以上の値が保証される地点までは符号付き型に保持してください。
sizeof の落とし穴と符号なしループカウンタ
キャストもサフィックスも見えないところでも混在は起こります。sizeof 演算子は符号なし型である size_t を生成するため、符号付き変数を sizeof の結果と比較すると同じ変換が引き起こされます。
#include <stdio.h>
int main(void) {
int index = -1;
printf("sizeof(int): %zu\n", sizeof(int));
printf("index < (int)sizeof(int): %d\n", index < (int)sizeof(int));
printf("index < sizeof(int): %d\n", index < sizeof(int));
return 0;
}
sizeof(int): 4
index < (int)sizeof(int): 1
index < sizeof(int): 0
最後の比較は真であるはずです — -1 は本当に 4 より小さい — のに、符号なし比較は逆を言います。値が収まることが分かっている場合に sizeof の結果を int に変換すると、期待どおりの答えに戻ります。関連する落とし穴が、for (unsigned int i = count - 1; i >= 0; --i) と書く下降ループです。符号なし値は常時 0 以上であるため、条件は決して偽になりません。カウンタがゼロに達してデクリメントされると、UINT_MAX へ折り返し — 変換の例が 0u - 1u に対して出力したのと同じ折り返しです —、ループは巨大なインデックスで続行します。符号付き型でカウントダウンするか、デクリメントの前に古い値をテストする for (unsigned int i = count; i-- > 0; ) というイディオムを使ってください。
signed int と unsigned int の選び方
ゼロを上回る場合も下回る場合もある変化量のように、負の値が意味を持つ場合は int を選びます。値が 0 以上で、型の最大値を超えたときに折り返す演算が適切な場合(そのような動作が必要なカウントなど)は unsigned int を選びます。unsigned は、より大きな正の値を安全に格納するための汎用的な方法ではありません。値の範囲は実装によって異なり、符号付きと符号なしの値を変換したり混在させたりすると、比較や演算の結果が変わることがあります。実際に扱う値の範囲と演算に合った型を使ってください。
実際には、これによりいくつかの信頼できる既定の選び方が得られます。一般の演算、差分、ループカウンタには符号付き型を使ってください。0 以上で始まる量であっても同じです。中間結果はしばしばゼロ未満になる必要があるからです。符号なし型は、ビットパターンとマスク、ハッシュ値やハードウェアレジスタのように折り返しが意図的な値、そしてすでに符号なし型や size_t を使っているインタフェースへの適合のために取っておきます。「値は決して負になるべきではない」という文書化のためだけに符号なしを選ばないでください。減算によってその値が生まれることを止められず、非常に大きな正の数へ名前を変えるだけです。最後に、本当の問題が範囲である場合は、符号なし型が提供するように見える余分な容量に頼るのではなく、long long のようなより広い型へ移ってください。
これらの型をめぐる混乱のほとんどは、3 つの誤解から生まれます。1 つ目は、unsigned int は単に正の範囲が大きい int だという考えです。実際には、2 つの型は範囲だけでなく、オーバーフローの振る舞い、変換、比較でも異なります。2 つ目は、負の値を符号なし変数に格納すると負の値が何らかの形で記憶されるという考えです。上の例が示すように、変換は即座に置き換えます。3 つ目は、符号なし演算は負にならないため安全だという考えです。負の結果を大きな正の結果に静かに置き換えるため、防ごうとした問題よりもデバッグが難しいことがよくあります。