C言語を学び始めると、必ず出てくるのに最初はよく分からないものの1つがsignedとunsignedです。
同じintなのに結果が変わったり、警告が出たり、ループが終わらなくなったりします。
本記事では、C言語初心者の方がつまずきやすい「符号あり整数」と「符号なし整数」の違いを、ビット表現や具体例のコードとともに、やさしく丁寧に整理していきます。
符号あり整数(signed)と符号なし整数(unsigned)とは
まずは言葉の意味と、ざっくりとしたイメージを押さえます。
整数型には「プラスとマイナスの両方を表せる型」と「0以上だけを表せる型」があります。
前者がsigned(符号あり)、後者がunsigned(符号なし)です。
符号あり整数(signed)の基本と表せる範囲
符号あり整数は負の数も含めて表せる整数型です。
C言語でよく使うintは、何も指定しなければ符号あり(signed)になります。
32ビット環境の典型的な範囲
多くの環境でintは32ビット(4バイト)です。
この場合、signed intの範囲は次のようになります。
| 型 | ビット数 | 最小値 | 最大値 |
|---|---|---|---|
| signed int | 32 | -2,147,483,648 | 2,147,483,647 |
負の値まで表せる代わりに、正の最大値は半分程度に小さくなります。
ビットの一部を「符号(プラスかマイナスか)の情報」に使うためです。
直感的なイメージ
32ビットのsigned intでは、先頭の1ビット(MSB)を符号ビットとして使います。
- 0 … プラス(または0)
- 1 … マイナス
残りのビットで「大きさ」を表現します。
実際には後で説明する2の補数表現を使うため、単純な「符号 + 絶対値」ではありませんが、まずはこのイメージで十分です。
符号なし整数(unsigned)の基本と表せる範囲
符号なし整数は0以上の値だけを表す整数型です。
unsigned intやunsigned longなどがこれに当たります。
32ビット環境の典型的な範囲
同じ32ビットでも、unsigned intでは符号ビットを使わないため、すべてのビットを大きさの表現に使えます。
| 型 | ビット数 | 最小値 | 最大値 |
|---|---|---|---|
| unsigned int | 32 | 0 | 4,294,967,295 |
同じビット数でも、unsigned のほうが2倍近く大きな正の値まで表せることが分かります。
直感的なイメージ
unsigned intでは、すべてのビットが「大きさ」を表します。
- ..0000 → 0
- ..0001 → 1
- …
- ..1111 → 最大値
というシンプルな構造になっています。
C言語でよく使う整数型
C言語の整数型には、ビット幅や符号の有無によってさまざまな種類があります。
| 型名 | 符号あり/なし | 典型的なビット数(32bit環境) | 備考 |
|---|---|---|---|
| char | 実装依存 | 8 | signed か 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 |
初心者のうちは、まずintとunsigned int、size_tをしっかり区別できるようになることを目標にすると理解しやすいです。
C言語での書き方と動き方の基本
ここからは、具体的な書き方と、内部でどう動いているかを見ていきます。
signed と unsigned の宣言方法と省略ルール
基本的な宣言方法
C言語では、次のようにsignedやunsignedを付けて宣言します。
#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言語では、何も書かなければ「符号あり」です。
int→signed intと同じshort→signed shortと同じlong→signed longと同じ
一方、符号なしにしたい場合は必ずunsignedを付ける必要があります。
#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 int | 11111111 11111111 11111111 11111111 | 4,294,967,295 |
| signed int | 11111111 11111111 11111111 11111111 | -1 |
同じビット列でも、signed と unsigned では意味がまったく変わることが分かります。
負の数の表現方法(2の補数)をイメージで理解
C言語では、整数の負の数は2の補数(にのほすう)表現で表されます。
これは少し難しく聞こえますが、「反転+1」と覚えるとイメージしやすいです。
2の補数の基本ルール
あるビット幅(ここでは8ビット)での-nの表現は、nのビット列に対して次の操作を行います。
- 全ビットを反転する(0→1、1→0)
- 1を足す
例として-1と-2を8ビットで見てみます。

すべてが1のビット列(1111 1111…)は、signed では -1 として扱われる理由が分かります。
なぜ2の補数を使うのか
2の補数を使うと、加算回路だけで減算も実現できるというハードウェア上のメリットがあります。
たとえば、a - bは「a + (-b)」と同じです。
負の数も2の補数で表されているため、+だけで計算できます。
これにより、CPUの設計がシンプルになります。
ビット幅と型ごとの範囲
ビット幅が変わると、表現できる範囲も変わります。
ここでは、典型的な32ビット環境を例に、よく使う型の範囲をまとめます。
| 型 | ビット数 | 最小値 | 最大値 |
|---|---|---|---|
| signed char | 8 | -128 | 127 |
| unsigned char | 8 | 0 | 255 |
| signed short | 16 | -32,768 | 32,767 |
| unsigned short | 16 | 0 | 65,535 |
| signed int | 32 | -2,147,483,648 | 2,147,483,647 |
| unsigned int | 32 | 0 | 4,294,967,295 |
| signed long long | 64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
| unsigned long long | 64 | 0 | 18,446,744,073,709,551,615 |
ビット数が1増えると表現できる数は2倍になります。
符号ありでは「マイナス側」と「プラス側」に分かれ、符号なしでは全部を0以上に使います。
signed / unsigned の使い分けポイント
ここからは、実際にプログラムを書くときにどちらを選べばよいかを整理します。
負の値を扱うなら signed を使う場面
負の値を扱う可能性があるなら、必ずsignedを使います。
代表的な例としては次のようなものがあります。
- 温度や速度変化量など、プラス・マイナスがある物理量
- 差分、増減量、偏差など
- エラーコードで
-1を返す場合など
#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やインデックスなど、負にならない識別子
#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 longかunsigned long longなど)です。
代表的な関数と size_t
| 関数 | 戻り値の型 | 説明 |
|---|---|---|
| strlen | size_t | 文字列の長さ(終端の’\0’を除く) |
| sizeof | size_t | 型やオブジェクトの大きさ(バイト数) |
| malloc | void* | 引数は size_t 型のサイズ |
例としてstrlenを見てみます。
#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 整数のオーバーフローは「未定義動作」です。
つまり、何が起きてもおかしくないことになります。
#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はビット数)。
#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 に変換されてから」比較されるため、予想とまったく違う結果になります。
典型的なバグ例
#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 の例
#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 の例
逆方向では、値がその型で表せる範囲に収まらないと、結果は処理系定義(処理系ごとに異なる)になります。
#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 で書いても問題ない場合が多いです。
#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 に合わせるのが安全です。
#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 は危険
次のようなループは典型的なバグです。
#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_tsizeofの結果 → size_tfreadやfwriteの戻り値 → size_t
これらと比較・計算するときは、自分の変数も size_t にするのが基本です。
#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言語では、型とビットの世界を正しくイメージできるかどうかが、バグの少ないプログラムを書くための大きな鍵になります。
今回の内容を足がかりに、整数型や型変換の仕組みを少しずつ広げて学んでいってください。
