C言語基礎|符号付き整数の内部表現

符号ビットは同じ。でも“負の作り方”が3通りある!――ビット列を読めると、Cの整数が急に分かりやすくなる。

符号付き整数の箱の中をのぞいてみよう

符号無し整数は「2進数をそのままビットに並べる」だけでした。
でも **符号付き整数(負の数を扱える整数)**になると話がちょっと増えます。

というのも、負の数をビット列で表す方法が、昔から代表的に3種類あるからです。

  • 符号と絶対値表現(sign and magnitude)
  • 1の補数表現(one’s complement)
  • 2の補数表現(two’s complement)

そしてC言語の規格上、どれを採用するかは処理系(コンパイラやCPUなど)に委ねられています
ただ、現代の多くの環境では 2の補数が主流です。

この記事ではまず、3方式の共通点である 符号ビットから入り、
次に「同じビット列でも方式によって値が変わる」ポイントを、表と図でしっかり整理していきます。

まず共通点:最上位ビットが符号ビット

符号付き整数の3方式に共通している考え方はこれです。

  • 最上位ビット(いちばん左)で符号を表す
  • 符号ビットが 0 なら非負(0または正)
  • 符号ビットが 1 なら負

図:符号付き整数の符号ビット(イメージ)

この図の説明
箱の左端(最上位)が「プラスかマイナスか」を決めます。
ただし 残りのビット列が何を意味するかが、方式によって変わります。

3方式の違い:負の値の作り方が違う

ここでは分かりやすく 8ビットで例を出します(短いので目で追いやすいです)。

例:+5 のビット列(8ビット)

+5 は 00000101

ここから −5 を作るとき、方式によってこう変わります。

+5 から −5 を作るルール(8ビット例)

方式ルール(正のビット列から)−5 のビット列(8ビット)重要ポイント
符号と絶対値符号ビットだけ 0→1、他はそのまま10000101直感的だが 0 が2種類になりやすい
1の補数全ビット反転11111010これも 0 が2種類になりやすい
2の補数1の補数を作って 1 を足す111110110 が1種類、計算がやりやすい

この表の説明

  • 3方式とも「符号ビットは1で負」を使います。
  • でも「負の中身(残りのビット列)」の作り方が違います。
  • 特に 2の補数は 反転+1 が合言葉です。

“同じビット列の並び”を、方式ごとにどう読む?

次は「ビット列の並び(00000000 から 11111111 まで)」を方式ごとに読むとどうなるか、ざっくり掴みます。

図:8ビットのビット列は同じ順番で並ぶ

この図の説明
ビット列の“並び”は、方式が違っても同じです。
違うのは「そのビット列に割り当てられる数値」です。特に負側が変わります。

0 が何種類あるかが、方式のクセを決める

3方式を比べると、学習者がいちばん納得しやすい差はここです。

0 の扱いの違い

方式0 の種類例(8ビット)影響
符号と絶対値2種類+0 = 00000000
−0 = 10000000
比較や計算でルールが増えがち
1の補数2種類+0 = 00000000
−0 = 11111111
同上
2の補数1種類0 = 00000000 のみルールがすっきり、計算に強い

この表の説明
0 が2種類あると「0なのに違うビット列」が登場します。
2の補数はそれが起きないので、演算回路や言語処理の都合が良い、という流れです。

表現できる範囲(8ビット例でイメージ)

ビット数が同じでも、方式で表現範囲が少し変わります。

8ビット符号付き整数の範囲(方式別)

方式最小値最大値
符号と絶対値−127+127
1の補数−127+127
2の補数−128+127

この表の説明

  • 2の補数は負側が 1つ多く表現できます(−128 まで行ける)
  • これは「負側の作り方」が反転+1になっている結果です。

サンプルプログラム

ここでは +42 を例にして、
「3方式の負のビット列」を計算で作って表示します。
※これは「その処理系がどの方式か」を断定するプログラムではなく、3方式のルールを見える化するためのプログラムです。

プロジェクト名:chap7-14-1 ソースファイル名:chap7-14-1.c

// 符号付き整数の3方式(符号と絶対値 / 1の補数 / 2の補数)のビット列を作って表示する
#include <stdio.h>

void print_bits_u8(unsigned char v)
{
    for (int i = 7; i >= 0; i--) {
        putchar((v & (1u << i)) ? '1' : '0');
    }
}

int main(void)
{
    unsigned char p = 42;              // +42(0〜127を想定して例にする)
    unsigned char sign_mag = p | 0x80; // 符号と絶対値:符号ビットを1にする
    unsigned char ones = (unsigned char)~p;          // 1の補数:全ビット反転
    unsigned char twos = (unsigned char)(ones + 1u); // 2の補数:反転して +1

    puts("正の値から、負のビット列を3方式で作ってみよう(8ビット表示)");

    printf("+42                 : ");
    print_bits_u8(p);
    putchar('\n');

    printf("符号と絶対値での -42: ");
    print_bits_u8(sign_mag);
    putchar('\n');

    printf("1の補数での -42      : ");
    print_bits_u8(ones);
    putchar('\n');

    printf("2の補数での -42      : ");
    print_bits_u8(twos);
    putchar('\n');

    return 0;
}

実行結果例

正の値から、負のビット列を3方式で作ってみよう(8ビット表示)
+42                 : 00101010
符号と絶対値での -42: 10101010
1の補数での -42      : 11010101
2の補数での -42      : 11010110

この出力の説明

  • 00101010 が +42
  • 方式ごとに「負の作り方」が違うので、ビット列も全部変わります
  • 2の補数は 1の補数に 1 を足したものになっています

登場する命令(関数・演算子)の書式と役割

ここでは「この記事で使ったもの」が何をするのかを、ちゃんと整理しますね。

puts

項目内容
書式puts(文字列);
何をする?文字列を表示して、最後に改行も出す

printf

項目内容
書式printf(書式文字列, 値, ...);
何をする?書式に合わせて値を表示する

putchar

項目内容
書式putchar(文字);
何をする?1文字だけ出力する(ここでは 0 と 1 を1個ずつ表示)

ビット演算(今回の主役)

演算子書式何をする?この記事での使い方
&a & b両方1のビットだけ1指定したビットが1か確認
|a | bどちらか1なら1符号ビットを立てる(0x80 を足すイメージ)
~~a全ビット反転1の補数を作る。
<<1u << i1を左へ i ずらすi番目のビットだけ立ったマスクを作る。
+a + 1u加算2の補数の 反転+1 の 1 を足す。

ここまでのまとめ

  • 符号付き整数の内部表現は 3方式(符号と絶対値 / 1の補数 / 2の補数)
  • 共通点は「最上位ビットが符号」
  • 違いは「負のとき、残りのビット列が何を意味するか」
  • 2の補数は 0が1種類で計算が得意(現代の主流になりやすい)

次の学習では、この「2の補数の読み方」を使って、負の値をビット列から逆算したり、オーバーフローの感覚を掴んだりしていけます。