閉じる

【C言語】なぜ signed と unsigned があるの?初心者がつまずきやすいポイントをやさしく整理

C言語を学び始めると、必ず出てくるのに最初はよく分からないものの1つがsignedunsignedです。

同じintなのに結果が変わったり、警告が出たり、ループが終わらなくなったりします。

本記事では、C言語初心者の方がつまずきやすい「符号あり整数」と「符号なし整数」の違いを、ビット表現や具体例のコードとともに、やさしく丁寧に整理していきます。

符号あり整数(signed)と符号なし整数(unsigned)とは

まずは言葉の意味と、ざっくりとしたイメージを押さえます。

整数型には「プラスとマイナスの両方を表せる型」と「0以上だけを表せる型」があります

前者がsigned(符号あり)、後者がunsigned(符号なし)です。

符号あり整数(signed)の基本と表せる範囲

符号あり整数は負の数も含めて表せる整数型です。

C言語でよく使うintは、何も指定しなければ符号あり(signed)になります。

32ビット環境の典型的な範囲

多くの環境でintは32ビット(4バイト)です。

この場合、signed intの範囲は次のようになります。

ビット数最小値最大値
signed int32-2,147,483,6482,147,483,647

負の値まで表せる代わりに、正の最大値は半分程度に小さくなります

ビットの一部を「符号(プラスかマイナスか)の情報」に使うためです。

直感的なイメージ

32ビットのsigned intでは、先頭の1ビット(MSB)を符号ビットとして使います。

  • 0 … プラス(または0)
  • 1 … マイナス

残りのビットで「大きさ」を表現します。

実際には後で説明する2の補数表現を使うため、単純な「符号 + 絶対値」ではありませんが、まずはこのイメージで十分です。

符号なし整数(unsigned)の基本と表せる範囲

符号なし整数は0以上の値だけを表す整数型です。

unsigned intunsigned longなどがこれに当たります。

32ビット環境の典型的な範囲

同じ32ビットでも、unsigned intでは符号ビットを使わないため、すべてのビットを大きさの表現に使えます

ビット数最小値最大値
unsigned int3204,294,967,295

同じビット数でも、unsigned のほうが2倍近く大きな正の値まで表せることが分かります。

直感的なイメージ

unsigned intでは、すべてのビットが「大きさ」を表します。

  • ..0000 → 0
  • ..0001 → 1
  • ..1111 → 最大値

というシンプルな構造になっています。

C言語でよく使う整数型

C言語の整数型には、ビット幅や符号の有無によってさまざまな種類があります。

型名符号あり/なし典型的なビット数(32bit環境)備考
char実装依存8signed か unsigned かは処理系依存
signed char符号あり8明示的に符号ありの8ビット
unsigned char符号なし8バイト列操作によく使う
short符号あり16= signed short
unsigned short符号なし16
int符号あり32= signed int
unsigned int符号なし32
long符号あり32 or 64実装依存
unsigned long符号なし32 or 64
long long符号あり64大きな整数用
unsigned long long符号なし64

初心者のうちは、まずintunsigned intsize_tをしっかり区別できるようになることを目標にすると理解しやすいです。

C言語での書き方と動き方の基本

ここからは、具体的な書き方と、内部でどう動いているかを見ていきます。

signed と unsigned の宣言方法と省略ルール

基本的な宣言方法

C言語では、次のようにsignedunsignedを付けて宣言します。

C言語
#include <stdio.h>

int main(void) {
    signed int a = -10;      // 符号ありのint
    unsigned int b = 10u;    // 符号なしのint。'u'はunsignedリテラル

    printf("a = %d\n", a);   // signedは%dで出力
    printf("b = %u\n", b);   // unsignedは%uで出力

    return 0;
}
実行結果
a = -10
b = 10

省略ルール

C言語では、何も書かなければ「符号あり」です

  • intsigned int と同じ
  • shortsigned short と同じ
  • longsigned long と同じ

一方、符号なしにしたい場合は必ずunsignedを付ける必要があります

C言語
#include <stdio.h>

int main(void) {
    int x = -5;           // int は signed int と同じ
    signed y = -6;        // signed 単体でも signed int
    unsigned z = 5u;      // unsigned 単体でも unsigned int

    printf("x = %d, y = %d, z = %u\n", x, y, z);

    return 0;
}
実行結果
x = -5, y = -6, z = 5

ビット表現と最上位ビット(MSB)の役割

整数は内部的には2進数のビット列として扱われます。

その中で最上位ビット(MSB: Most Significant Bit)が特に重要です。

MSBの役割の違い

  • signed int
    MSBが符号を表す一部として使われる(2の補数表現の一部)
  • unsigned int
    MSBも単なる値の一部として扱われる

32ビットの例で見てみます。

2進数ビット列(例)解釈される値
unsigned int11111111 11111111 11111111 111111114,294,967,295
signed int11111111 11111111 11111111 11111111-1

同じビット列でも、signed と unsigned では意味がまったく変わることが分かります。

負の数の表現方法(2の補数)をイメージで理解

C言語では、整数の負の数は2の補数(にのほすう)表現で表されます。

これは少し難しく聞こえますが、「反転+1」と覚えるとイメージしやすいです。

2の補数の基本ルール

あるビット幅(ここでは8ビット)での-nの表現は、nのビット列に対して次の操作を行います。

  1. 全ビットを反転する(0→1、1→0)
  2. 1を足す

例として-1-2を8ビットで見てみます。

すべてが1のビット列(1111 1111…)は、signed では -1 として扱われる理由が分かります。

なぜ2の補数を使うのか

2の補数を使うと、加算回路だけで減算も実現できるというハードウェア上のメリットがあります。

たとえば、a - bは「a + (-b)」と同じです。

負の数も2の補数で表されているため、+だけで計算できます。

これにより、CPUの設計がシンプルになります。

ビット幅と型ごとの範囲

ビット幅が変わると、表現できる範囲も変わります。

ここでは、典型的な32ビット環境を例に、よく使う型の範囲をまとめます。

ビット数最小値最大値
signed char8-128127
unsigned char80255
signed short16-32,76832,767
unsigned short16065,535
signed int32-2,147,483,6482,147,483,647
unsigned int3204,294,967,295
signed long long64-9,223,372,036,854,775,8089,223,372,036,854,775,807
unsigned long long64018,446,744,073,709,551,615

ビット数が1増えると表現できる数は2倍になります

符号ありでは「マイナス側」と「プラス側」に分かれ、符号なしでは全部を0以上に使います。

signed / unsigned の使い分けポイント

ここからは、実際にプログラムを書くときにどちらを選べばよいかを整理します。

負の値を扱うなら signed を使う場面

負の値を扱う可能性があるなら、必ずsignedを使います

代表的な例としては次のようなものがあります。

  • 温度や速度変化量など、プラス・マイナスがある物理量
  • 差分、増減量、偏差など
  • エラーコードで-1を返す場合など
C言語
#include <stdio.h>

int main(void) {
    signed int diff = -3;      // 差分(増減量)は負になることがある
    int temperature = -10;     // 気温なども負の値があり得る

    printf("diff = %d\n", diff);
    printf("temperature = %d\n", temperature);

    return 0;
}
実行結果
diff = -3
temperature = -10

少しでも負の値になる可能性があるなら signed を選ぶのが安全です。

サイズや個数など0以上だけなら unsigned を使う場面

「数え上げる」系の値は、基本的に0以上です

こうした値にはunsignedがよく使われます。

  • 配列の要素数
  • バイト数、サイズ
  • ループ回数
  • IDやインデックスなど、負にならない識別子
C言語
#include <stdio.h>

int main(void) {
    unsigned int count = 0;     // 個数
    unsigned int length = 100;  // 長さやサイズ

    printf("count = %u, length = %u\n", count, length);

    return 0;
}
実行結果
count = 0, length = 100

ただし、「サイズだから必ず unsigned」と決めつけると、後で説明する比較の落とし穴にはまりやすくなります

ライブラリ関数がどの型を使っているかも確認することが大切です。

ポインタやサイズ関連での unsigned(size_t など)の扱い

C言語では、標準ライブラリでサイズや要素数を表すときにsize_tという型をよく使います。

これは符号なし整数(通常はunsigned longunsigned long longなど)です。

代表的な関数と size_t

関数戻り値の型説明
strlensize_t文字列の長さ(終端の’\0’を除く)
sizeofsize_t型やオブジェクトの大きさ(バイト数)
mallocvoid*引数は size_t 型のサイズ

例としてstrlenを見てみます。

C言語
#include <stdio.h>
#include <string.h>  // strlen

int main(void) {
    const char *str = "hello";
    size_t len = strlen(str);  // 戻り値は size_t

    printf("len = %zu\n", len); // size_t用の書式指定子は %zu

    return 0;
}
実行結果
len = 5

size_t は「サイズや個数」を表すための標準的な型です。

ライブラリ関数と値をやり取りするときは、基本的に size_t を使う習慣をつけると、型の不一致によるバグを減らせます。

オーバーフロー時の挙動の違いに注意する

signed と unsigned では、オーバーフローしたときのルールが違います

これは非常に重要なポイントです。

signed 整数のオーバーフロー

signed 整数のオーバーフローは「未定義動作」です。

つまり、何が起きてもおかしくないことになります。

C言語
#include <stdio.h>
#include <limits.h>  // INT_MAX

int main(void) {
    int x = INT_MAX;
    int y = x + 1;   // オーバーフロー(未定義動作)

    printf("x = %d, y = %d\n", x, y);

    return 0;
}

このプログラムはコンパイル・実行できるかもしれませんが、結果は処理系によって異なり、信頼できません

unsigned 整数のオーバーフロー

一方、unsigned 整数のオーバーフローは「2の補数での剰余演算」として定義されています

つまり、「2^Nで割った余り」になります(Nはビット数)。

C言語
#include <stdio.h>

int main(void) {
    unsigned int x = 0xffffffffu; // 32ビットの最大値想定
    unsigned int y = x + 1;       // 0 に戻る(ラップアラウンド)

    printf("x = %u, y = %u\n", x, y);

    return 0;
}
実行結果
x = 4294967295, y = 0

unsigned ではオーバーフロー時に0に戻る、という動きが仕様として決まっているため、値を「ぐるぐる回す」用途(ハッシュやリングバッファのインデックスなど)でよく使われます。

バグを防ぐための比較・代入の注意点

ここからは、実際によく起きるバグのパターンを具体例で確認します。

signed と unsigned を比較するときの落とし穴

最も危険で、初心者がよくハマるポイントがこれです。

signed と unsigned を比較すると、signed 側が「unsigned に変換されてから」比較されるため、予想とまったく違う結果になります。

典型的なバグ例

C言語
#include <stdio.h>

int main(void) {
    int s = -1;              // signed
    unsigned int u = 1u;     // unsigned

    if (s < u) {
        printf("s < u です\n");
    } else {
        printf("s < u ではありません\n");
    }

    return 0;
}

一見すると、s-1なのでs < uは真に思えます。

しかし、実際の結果は多くの処理系で次のようになります。

実行結果
s < u ではありません

なぜこうなるのか

この比較では、型変換のルールにより、signed のsが unsigned に変換されます

  • s = -1 (signed int)
  • これを unsigned int に変換 → 非常に大きな値(例: 4,294,967,295)
  • 比較は「4,294,967,295 < 1」になり、偽になる

このように、負の signed 値を unsigned と比較すると、ほぼ必ず「ものすごく大きな正の値」になってしまうため、非常に危険です。

対策のポイント

  • 比較する値は、できるだけ同じ型にそろえる
  • 特に、負の値の可能性があるものを unsigned へ暗黙変換させない

たとえば、整数リテラル側を signed に合わせるなどの工夫が必要です。

代入時の型変換(暗黙の型変換)で起きる意図しない値

代入のときも、signed から unsigned、unsigned から signed の変換で値が変わる場合があります。

signed → unsigned の例

C言語
#include <stdio.h>

int main(void) {
    int s = -1;                 // signed
    unsigned int u = s;         // 暗黙の型変換

    printf("s = %d\n", s);
    printf("u = %u\n", u);      // 非常に大きな値になる

    return 0;
}
実行結果
s = -1
u = 4294967295  (32ビット環境の例)

ビット列はそのままで「解釈だけが変わる」イメージです。

signed での-1は、ビット列としては1111...1111なので、unsigned では最大値になります。

unsigned → signed の例

逆方向では、値がその型で表せる範囲に収まらないと、結果は処理系定義(処理系ごとに異なる)になります。

C言語
#include <stdio.h>

int main(void) {
    unsigned int u = 4000000000u; // 32ビットなら signed int の範囲外
    int s = (int)u;               // キャストして代入

    printf("u = %u\n", u);
    printf("s = %d\n", s);        // 何になるかは処理系次第

    return 0;
}

このようなコードは非常に危険であり、極力避けるべきです。

for文のカウンタでの signed / unsigned の選び方

for文のカウンタ変数の型選びも、初心者がよく迷うポイントです。

単純なカウンタは signed でもよい

配列のインデックスなど、単純なループではint で書いても問題ない場合が多いです。

C言語
#include <stdio.h>

int main(void) {
    int i;
    for (i = 0; i < 10; i++) {
        printf("i = %d\n", i);
    }
    return 0;
}
実行結果
i = 0
i = 1
...
i = 9

初心者のうちは、まずは int で慣れるのも1つのやり方です。

size_t を使うケース

一方、配列の要素数や strlen の結果など、size_t と比較するループでは、カウンタも size_t に合わせるのが安全です。

C言語
#include <stdio.h>
#include <string.h>

int main(void) {
    const char *str = "hello";
    size_t len = strlen(str);

    // i を size_t にすると、len との比較で型がそろう
    for (size_t i = 0; i < len; i++) {
        printf("str[%zu] = %c\n", i, str[i]);
    }

    return 0;
}
実行結果
str[0] = h
str[1] = e
str[2] = l
str[3] = l
str[4] = o

負の値を使うループでは unsigned は危険

次のようなループは典型的なバグです。

C言語
#include <stdio.h>

int main(void) {
    unsigned int i;

    // i が 0 から減っていくループは危険
    for (i = 10; i >= 0; i--) {
        printf("i = %u\n", i);
    }

    return 0;
}

このコードは、一見すると10から0までカウントダウンして終わりそうですが、実際には無限ループになります。

  • i = 0 でループ内を実行
  • その後 i– → オーバーフローして非常に大きな値に
  • 条件i >= 0は常に真になり、終わらない

減っていく for ループで 0 判定を使うときは、signed を使うか条件式を工夫する必要があります。

ライブラリ関数(size_t など)との型を合わせるコツ

最後に、標準ライブラリ関数と型をそろえるコツをまとめます。

代表的なパターン

  • strlenの戻り値 → size_t
  • sizeofの結果 → size_t
  • freadfwriteの戻り値 → size_t

これらと比較・計算するときは、自分の変数も size_t にするのが基本です。

C言語
#include <stdio.h>
#include <string.h>

int main(void) {
    const char *str = "example";
    size_t len = strlen(str);   // len は size_t

    // i も size_t で宣言して、型をそろえる
    for (size_t i = 0; i < len; i++) {
        printf("%c\n", str[i]);
    }

    return 0;
}

キャストより「型を合わせる」

安易にキャストで型を合わせるのは危険です。

たとえば、(int)strlen(str)のような書き方は、長い文字列でオーバーフローする可能性があります。

できる限り、変数側を size_t にしてライブラリ関数の型に合わせるようにすると、バグを防ぎやすくなります。

まとめ

本記事では、C言語の「符号あり整数(signed)」と「符号なし整数(unsigned)」の違いについて、初心者の方がつまずきやすいポイントを中心に整理しました。

  • signed は負の値も表せる代わりに、正の最大値が小さくなること
  • unsigned は0以上だけだが、同じビット数で2倍近く大きい値まで表せること
  • 整数は内部的にビット列で表現され、signed では2の補数で負の数を表すこと
  • signed と unsigned を混ぜて比較・代入すると、暗黙の型変換で予想外の挙動になること
  • サイズや要素数には size_t などの unsigned 型が使われること
  • for文のカウンタやライブラリ関数の戻り値では、型をそろえることが重要であること

特に「signed と unsigned の比較」「オーバーフローの挙動」は、バグの原因になりやすい重要ポイントです。

まずは「負になる可能性があるなら signed」「サイズや個数はライブラリに合わせて size_t」という大きな方針を押さえ、実際に小さなプログラムを書いて挙動を確かめながら、少しずつ感覚を身につけていくと理解が深まります。

C言語では、型とビットの世界を正しくイメージできるかどうかが、バグの少ないプログラムを書くための大きな鍵になります。

今回の内容を足がかりに、整数型や型変換の仕組みを少しずつ広げて学んでいってください。

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

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

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

URLをコピーしました!