C言語の整数型には、符号あり(signed)と符号なし(unsigned)があり、見た目は似ていても扱える範囲や計算・比較の挙動が大きく異なります。
本記事では、C言語初心者が最初に押さえるべき基礎から、実際に起きやすい落とし穴、そして安全に使い分けるためのベストプラクティスまでを、サンプルコードと出力例を交えて丁寧に解説します。
符号あり整数(signed)と符号なし整数(unsigned)の基礎
signed/unsignedの意味と違い
signedは負の数を含む整数を表現でき、unsignedは0以上の非負の整数のみを表現します。
C言語では、内部表現(ビット列)が同じビット幅でも、値の解釈規則が異なります。
現代的な多くの処理系では2の補数表現が使われますが、初学者はまず次の直観を持つと理解しやすいです。
- signed(例:
int
)は範囲がマイナス側とプラス側に分かれます。 - unsigned(例:
unsigned int
)は0から大きな正の値まで連続します。
同じビット数なら、unsignedのほうが最大値が大きいというのが重要なポイントです。
宣言の書き方(int, unsigned int)
C言語では、以下のように宣言します。
無印のint
は「符号あり(signed)」です。
int a;
はsigned int a;
と同じ意味です。unsigned int u;
または短くunsigned u;
でもOKです。signed
/unsigned
はshort
/long
などとも組み合わせられます(例:unsigned long
)。- charの符号は処理系依存なので、負の値を扱うか否かが重要なら
signed char
やunsigned char
を明示します。
次の表は、よく使う宣言と意味の対応を簡単にまとめたものです。
宣言例 | 意味 | 典型的な使いどころ |
---|---|---|
int x; | 符号ありのint | 一般的な整数(カウンタ、差分など) |
unsigned int u; | 符号なしのint | 個数、サイズ、ビットフラグ |
signed int s; | 符号ありのint(明示) | 「符号あり」を強調したい時 |
unsigned long ul; | 符号なしのlong | ビット演算、大きいサイズ値 |
signed char sc; | 符号ありのchar | バイト単位の負値を扱う場合 |
unsigned char uc; | 符号なしのchar | 生データのバイト、ビット列 |
幅を明示したい場合はstdint.h
のint32_t
/uint32_t
なども有用です。
値の範囲の違いと考え方
マイナスを扱えるのはsignedのみ
負の値が必要なら必ずsigned系を使います。
unsigned変数に負の値を代入すると、後述のとおり「大きな正の値」に変換されます。
同じビット数ならunsignedは上限が大きい
ビット数をNとすると、おおよそ次のように考えられます。
- signedの範囲: 約
-2^(N-1)
から2^(N-1)-1
- unsignedの範囲:
0
から2^N-1
そのため同じNビットなら、unsignedの最大値はsignedの約2倍になります。
範囲の例(32bit想定)
多くの環境でint
は32ビットですが、C標準ではビット幅は処理系依存です。
参考として32ビットの例を示します。
型(32bit想定) | 最小値 | 最大値 |
---|---|---|
int | -2,147,483,648 | 2,147,483,647 |
unsigned int | 0 | 4,294,967,295 |
実際の環境の範囲はlimits.h
で確認できます。
次のサンプルは、実行環境に依存しない方法でサイズと範囲を表示します。
// 範囲を確認するプログラム(range_check.c)
#include <stdio.h>
#include <limits.h>
int main(void) {
// sizeofで実際のバイト数を確認できます
printf("sizeof(int) = %zu bytes\n", sizeof(int));
printf("INT_MIN = %d\n", INT_MIN);
printf("INT_MAX = %d\n", INT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
return 0;
}
実行例(32ビットintの典型的な環境):
sizeof(int) = 4 bytes
INT_MIN = -2147483648
INT_MAX = 2147483647
UINT_MAX = 4294967295
計算・比較での注意点(C言語)
型が混ざると自動変換が起きる
Cでは式の中で型が混在すると「整数拡張」「通常の算術変換」が行われ、比較や計算の前に型が揃えられます。
このとき、signedとunsignedが混ざると、しばしばsignedがunsignedに変換されます。
負の値は巨大な正の値に化けるため、比較結果が直観と逆になることがあります。
// 比較での自動変換の例(signed_vs_unsigned_compare.c)
#include <stdio.h>
int main(void) {
int si = -1; // 符号あり: -1
unsigned int ui = 1u; // 符号なし: 1
printf("si = %d, ui = %u\n", si, ui);
// ここで si は unsigned に変換される
printf("si < ui ? %s\n", (si < ui) ? "true" : "false");
// リテラルと混在した場合 (-1 は int、0u は unsigned int)
printf("-1 < 0u ? %s\n", ((-1) < 0u) ? "true" : "false");
return 0;
}
si = -1, ui = 1
si < ui ? false
-1 < 0u ? false
負のintがunsignedへ変換されると巨大値になり、比較が意図と逆転する点は必ず覚えておきましょう。
オーバーフロー/アンダーフローに注意
signed整数のオーバーフローは未定義動作(UB)です。結果が決まっていないため、最適化で消える、想定外の値になるなど何でも起こり得ます。
unsigned整数の桁あふれ(オーバーフロー/アンダーフロー)は2^Nを法とする剰余演算として定義されています。つまり、UINT_MAX + 1 == 0
になります。
// オーバーフロー/アンダーフローの挙動(overflow_underflow.c)
#include <stdio.h>
#include <limits.h>
int main(void) {
unsigned int um = UINT_MAX;
printf("UINT_MAX = %u\n", um);
// unsigned の加算は 2^N で折り返す(定義済みの挙動)
unsigned int wrap_add = um + 1u; // 0 に戻る
unsigned int wrap_sub = 0u - 1u; // UINT_MAX になる
printf("UINT_MAX + 1 => %u\n", wrap_add);
printf("0u - 1u => %u\n", wrap_sub);
// signed のオーバーフローは未定義なので、実演しない
// 代わりに安全な加算のガード例を示す
int a = INT_MAX, b = 1;
if (b > 0 && a > INT_MAX - b) {
printf("安全: これ以上加算すると signed のオーバーフローになります\n");
} else {
printf("安全: a + b = %d\n", a + b);
}
return 0;
}
UINT_MAX = 4294967295
UINT_MAX + 1 => 0
0u - 1u => 4294967295
安全: これ以上加算すると signed のオーバーフローになります
ループの罠(i >= 0はunsignedで常に真)
unsigned変数は常に0以上なので、i >= 0
は常に真になります。
次のような「後ろから前へ」反復は、unsignedだと無限ループになります。
// 危険な例: 無限ループになる恐れ(コンパイルはできますが実行しないでください)
/*
for (unsigned i = n - 1; i >= 0; --i) {
// i は常に 0 以上なので条件 i >= 0 は常に真
// 0 から 1 を引くと折り返して非常に大きな値になり、永遠に終わりません
}
*/
安全な書き方はいくつかあります。
size_tで昇順、または降順なら「i– > 0」パターンが定番です。
// 安全なループ例(loop_patterns.c)
#include <stdio.h>
#include <stddef.h>
int main(void) {
size_t n = 5;
// 昇順: 最も安全で読みやすい
printf("Ascending: ");
for (size_t i = 0; i < n; ++i) {
printf("%zu ", i);
}
printf("\n");
// 降順: i-- > 0 パターン (i は 0 で止まる)
printf("Descending: ");
for (size_t i = n; i-- > 0; ) {
printf("%zu ", i);
}
printf("\n");
return 0;
}
Ascending: 0 1 2 3 4
Descending: 4 3 2 1 0
負の値をunsignedに代入すると大きな値になる
負の値をunsignedへ代入(またはキャスト)すると、ビット幅2^Nの剰余に変換されます。
32ビットunsignedなら-1
は4294967295
(= UINT_MAX
)になります。
// 負の値を unsigned に変換(neg_to_unsigned.c)
#include <stdio.h>
#include <limits.h>
int main(void) {
unsigned int u1 = (unsigned int)-1;
unsigned int u2 = (unsigned int)-123;
printf("(unsigned)-1 = %u (期待: UINT_MAX)\n", u1);
printf("(unsigned)-123 = %u (期待: UINT_MAX - 122)\n", u2);
printf("UINT_MAX = %u\n", UINT_MAX);
printf("UINT_MAX - 122 = %u\n", UINT_MAX - 122u);
return 0;
}
(unsigned)-1 = 4294967295 (期待: UINT_MAX)
(unsigned)-123 = 4294967173 (期待: UINT_MAX - 122)
UINT_MAX = 4294967295
UINT_MAX - 122 = 4294967173
printfの書式指定子を間違えると未定義動作になります。
unsignedは%u
、signed intは%d
を使います。
使い分けの目安とベストプラクティス
基本はint(signed)を使う
迷ったらint
(signed)を使うのが原則です。
負の値も扱え、算術や比較での予期せぬ型変換による落とし穴が減ります。
処理系依存のcharの符号などと異なり、int
は「符号あり」で安定した選択です。
個数やサイズはunsigned(size_t)を使う
配列の長さやメモリサイズなど非負で自然な数量にはsize_t
を使います。
標準ライブラリもサイズや個数にsize_t
を採用しています。
// 個数(サイズ)には size_t を使う(size_usage.c)
#include <stdio.h>
#include <stddef.h>
int main(void) {
int arr[3] = {10, 20, 30};
size_t n = sizeof(arr) / sizeof(arr[0]);
for (size_t i = 0; i < n; ++i) {
printf("arr[%zu] = %d\n", i, arr[i]);
}
return 0;
}
arr[0] = 10
arr[1] = 20
arr[2] = 30
ビット演算やフラグはunsignedが便利
ビットシフトやマスクはunsignedを使うと安全です。
signedの右シフトは処理系依存な場合があり、ゼロ埋めを期待するならunsigned
を使います。
// フラグ操作の基本(bit_flags.c)
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint32_t flags = 0u; // 32ビットの符号なし(明確で安全)
const uint32_t FLAG_A = 1u << 0; // ビット0
const uint32_t FLAG_B = 1u << 1; // ビット1
// 立てる(OR)
flags |= FLAG_A;
flags |= FLAG_B;
// 調べる(AND)
printf("FLAG_A? %s\n", (flags & FLAG_A) ? "ON" : "OFF");
printf("FLAG_B? %s\n", (flags & FLAG_B) ? "ON" : "OFF");
// 落とす(AND NOT)
flags &= ~FLAG_A;
printf("FLAG_A after clear? %s\n", (flags & FLAG_A) ? "ON" : "OFF");
return 0;
}
FLAG_A? ON
FLAG_B? ON
FLAG_A after clear? OFF
数値リテラルの接尾辞uの使い方(例: 10u)
整数リテラルには型を決めるための接尾辞があり、u/Uは「unsigned」、l/Lは「long」、ll/LLは「long long」を表します。
代表例は以下のとおりです。
リテラル | 型の候補(環境により決定) | 例 |
---|---|---|
10 | int など | 通常はint |
10u | unsigned int など | 非負の計算を明示 |
100UL | unsigned long | サイズ・ビットマスクに便利 |
1ULL | unsigned long long | 64ビットマスクなど |
unsignedのリテラルを混ぜると式全体の型推論にも影響します。
次の差を見てください。
// リテラル接尾辞の挙動(literal_suffix.c)
#include <stdio.h>
int main(void) {
printf("1 - 2 = %d\n", 1 - 2); // signed の計算 => -1
printf("1u - 2 = %u\n", 1u - 2u); // unsigned の計算 => 折り返して巨大値
// 16進のマスクにも u を付けると安全
unsigned int mask = 0xFFu; // 下位8ビット
printf("mask = %u\n", mask);
return 0;
}
1 - 2 = -1
1u - 2 = 4294967295
mask = 255
「非負であること」を表明したいだけでunsignedを乱用しないことも大切です。
APIや標準関数に合わせて、サイズはsize_t
、一般の数値はint
という使い分けが無難です。
まとめ
本記事では、C言語における符号あり整数(signed)と符号なし整数(unsigned)の違いを、宣言、範囲、計算・比較での挙動、そして実践的な使い分けの観点から解説しました。
要点は次のとおりです。
文章で再確認すると、理解が定着しやすくなります。
負の値が必要ならsigned、個数やサイズにはsize_t、ビット演算はunsignedという軸をまず持ち、型が混ざる比較やsignedオーバーフローには常に注意を払ってください。
リテラルのu
接尾辞やlimits.h
による範囲確認を活用すると、移植性と安全性が高まります。
慣れないうちは、サンプルをそのまま写経して挙動を確かめるのが近道です。