閉じる

【C言語】符号あり整数と符号なし整数の違い(signed, unsigned)

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/unsignedshort/longなどとも組み合わせられます(例: unsigned long)。
  • charの符号は処理系依存なので、負の値を扱うか否かが重要ならsigned charunsigned 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.hint32_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,6482,147,483,647
unsigned int04,294,967,295

実際の環境の範囲はlimits.hで確認できます。

次のサンプルは、実行環境に依存しない方法でサイズと範囲を表示します。

C言語
// 範囲を確認するプログラム(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に変換されます

負の値は巨大な正の値に化けるため、比較結果が直観と逆になることがあります。

C言語
// 比較での自動変換の例(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 になります。

C言語
// オーバーフロー/アンダーフローの挙動(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だと無限ループになります。

C言語
// 危険な例: 無限ループになる恐れ(コンパイルはできますが実行しないでください)
/*
for (unsigned i = n - 1; i >= 0; --i) {
    // i は常に 0 以上なので条件 i >= 0 は常に真
    // 0 から 1 を引くと折り返して非常に大きな値になり、永遠に終わりません
}
*/

安全な書き方はいくつかあります。

size_tで昇順、または降順なら「i– > 0」パターンが定番です。

C言語
// 安全なループ例(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なら-14294967295(= UINT_MAX)になります。

C言語
// 負の値を 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;
}
実行結果32ビットunsigned想定
(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を採用しています。

C言語
// 個数(サイズ)には 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を使います。

C言語
// フラグ操作の基本(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」を表します。

代表例は以下のとおりです。

リテラル型の候補(環境により決定)
10int など通常はint
10uunsigned int など非負の計算を明示
100ULunsigned longサイズ・ビットマスクに便利
1ULLunsigned long long64ビットマスクなど

unsignedのリテラルを混ぜると式全体の型推論にも影響します。

次の差を見てください。

C言語
// リテラル接尾辞の挙動(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;
}
実行結果32ビットunsigned想定
1 - 2  = -1
1u - 2 = 4294967295
mask = 255

「非負であること」を表明したいだけでunsignedを乱用しないことも大切です。

APIや標準関数に合わせて、サイズはsize_t、一般の数値はintという使い分けが無難です。

まとめ

本記事では、C言語における符号あり整数(signed)と符号なし整数(unsigned)の違いを、宣言、範囲、計算・比較での挙動、そして実践的な使い分けの観点から解説しました。

要点は次のとおりです。

文章で再確認すると、理解が定着しやすくなります。

負の値が必要ならsigned、個数やサイズにはsize_t、ビット演算はunsignedという軸をまず持ち、型が混ざる比較やsignedオーバーフローには常に注意を払ってください。

リテラルのu接尾辞やlimits.hによる範囲確認を活用すると、移植性と安全性が高まります。

慣れないうちは、サンプルをそのまま写経して挙動を確かめるのが近道です。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!