2000 年度「計算機基礎論3B」 数値データのメモリへの格納法

コンピュータが扱える最も基本的なデータは 「0 か 1 か」です。 0, 1 の二つの状態のうちのどちらか一方だけをとる変数を 「ビット (bit)」といいます。 まずほとんどのコンピュータでは 8 ビットをひとまとめにし、 「バイト (byte)」と呼びます。 int 型や double 型は数バイトを寄せ集めたものに格納しますが、 その格納のしかたの原理をちょっとだけでも見ておくと役に立つと思います。

以下のプログラム例には、 まだ説明していないことがらが使われています。 また、 もしかしたらほかの WS やコンパイラではうまく動かないかもしれません。 データの格納の方式はコンピュータに依存するので、 出力結果もコンピュータに依存します。

※ 一応、実習用 WS(32 ビット)と自宅の 98(16 ビット)で確認済み。

== intbit.c ===================================================================
#include <stdio.h>
#include <limits.h>     /* CHAR_BIT */

void intbit(int x);

main() {
    int i;

    scanf("%d", &i);
    intbit(i);
    printf("\n");
}

void intbit(int x) {
    int i;
    char* p = (char*)&x;
    unsigned mask;

    for (i = 0; i < sizeof(int); i++) {
        putchar(' ');
        for (mask = 1 << (CHAR_BIT - 1); mask != 0; mask >>= 1) {
            putchar(*(p+i) & mask ? '1' : '0');
        }
    }
}
===============================================================================

void intbit(int x) は、 int 型変数 x のビットパターンを印字する関数です。

※ intbit というのは適当につけた名前。 「ビットパターン」とはビットが並んだパターン。 (説明になってないか!?)

関数の定義およびプロトタイプ宣言の前の 「void」は「値を返さない」ということを意味しています。 動かすとこんな感じになります。

ws47{cf7175}159% a.out
1047
 00000000 00000000 00000100 00010111
ws47{cf7175}160% a.out
-1
 11111111 11111111 11111111 11111111
ws47{cf7175}161%
この WS では int 型は 32 ビットということもわかります。 1047 は二進で 10000010111 と表現されますが、 それがそのままビットパターンとして格納されていることがわかります。 負の数は 2 の 32 乗を modulo として正の数にして表現します。 よって -1 は「2 の 32 乗 - 1」で表現され、 上のようになるわけです。

== revintbit.c ================================================================
#include <stdio.h>

char buf[1024+1];

int revintbit(char *buf);

main() {
    gets(buf);
    printf("%d\n", revintbit(buf));
}

int revintbit(char *buf) {
    int ret = 0;

    while (*buf) {
        ret <<= 1;
        if (*buf == ' ') {
            buf++;
        }
        if (*buf == '1') {
            ret++;
        }
        buf++;
    }
    return ret;
}
===============================================================================

int revintbit(char *buf) は、 配列 p に格納された 0 と 1 だけからなる文字列をビットパターンだと思って int に変換します。 間にスペースがはいっていても構いませんので、 上の intbit.c(から作った実行ファイル) が出力した結果をリダイレクトで読ませても OK です。

※ スペースが二つ以上つながっているとだめ。 結構いい加減なプログラム。:-)

== charbit.c ==================================================================
#include <stdio.h>
#include <limits.h>     /* CHAR_BIT */

void charbit (char x);

main() {
    int i;
    char c;

    scanf("%d", &i);
    c = i;
    charbit(c);
    printf("\n");
}

void charbit(char x) {
    unsigned mask;

    for (mask = 1 << (CHAR_BIT - 1); mask != 0; mask >>= 1) {
        putchar(x & mask ? '1' : '0');
    }
}
===============================================================================

int 型は WS だと 32 ビットと桁が多すぎるので、 char 型の版を書いてみました。 char 型とは、本来は文字(character)を格納するための型なのですが、 小さい整数(-128 から 127 まで)を格納するのにも使えます。 scanf() で char 型に数を入力することはできませんので、 上のようにいったん int 型に入れてからコピーするとよいでしょう。

== doublebit.c ================================================================
#include <stdio.h>
#include <limits.h>     /* CHAR_BIT */

void doublebit(double x);

main() {
    double i;

    scanf("%lf", &i);
    doublebit(i);
    printf("\n");
}

void doublebit(double x) {
    int i;
    char* p = (char*)&x;
    unsigned mask;

    for (i = 0; i < sizeof(double); i++) {
        putchar(' ');
        for (mask = 1 << (CHAR_BIT - 1); mask != 0; mask >>= 1) {
            putchar(*(p+i) & mask ? '1' : '0');
        }
    }
}
===============================================================================

void doublebit(int x) は、 double 型変数 x のビットパターンを印字する関数です。

実はこの WS の double 型は 64 ビットであると前もって調べておきましたので、 「岩波情報科学辞典」の「IEEE 規格」の項目から 64 ビットの浮動小数点数の表現方法を引用します。

※ 「浮動小数点数」というのは 6.0221 × 1023 のように表現された数のことだが、 コンピュータの中は二進なので「× 2 のなんとか乗」となる。

+-+-----+--------------+
|s|  e  |       f      |
+-+-----+--------------+
 1   11         52
64 ビットを上の図のように 1 ビット、11 ビット、52 ビットに分けます。

e=2047, f≠0非数(Nan)
e=2047, f=0(-1)s×∞
0 < e < 2047(-1)s×2e-1023×(1.f)
e=0, f≠0(-1)s×2-1022×(0.f)
e=0, f=0(-1)s×0

表の三段目が普通の数の場合です。 指数部 e は 11 ビットあるので 0 から 2047 まで表現できますが、 両端の 0 と 2047 は例外とし、1 から 2046 までとします。 それを -1022 から 1023 と見立てるため、 「e-1023 乗」と約束してあるのです。 (1.f) というのは、f の部分に並んでいる 0 と 1 からなる文字列を 「1.」の後ろにくっつけたもの、という意味です。

五段目は 0 です。 0 にも符号 s がついているのは、 2-1000 を二乗した場合は正の 0 と考え、 -2-1000 に 2-1000 を掛けたものは負の 0 と考えるためかと思われます。

四段目は e = 0 のケースを絶対値 2-1022 以下の小さな数を表わすために使うものです。

二段目は無限大、 一段目は非数です。 非数というのは負の数の平方根などを求めたときに返るのではないかと思います。

どうやらこの WS もこの説明のようになっているようですが、 あまりきちんと確かめてはいません。 また、非数などの場合を確かめるには main() を変更しなければならないでしょう。 興味のある人はやってみてください。

== revdoublebit.c ====================================================
#include <stdio.h>
#include <limits.h>

char buf[1024+1];

double revdoublebit(char *buf);

main() {
    gets(buf);
    printf("%f\n", revdoublebit(buf));
}

double revdoublebit(char *buf) {
    int i, mask;
    double ret = 0;
    char* p = (char*) &ret;

    for (i = 0; i < sizeof(double); i++) {
        for (mask = 1 << (CHAR_BIT - 1); mask != 0; mask >>= 1) {
            if (*buf == '\0') {
                goto done;
            } else if (*buf == ' ') {
                buf++;
            }
            if (*buf == '1') {
                p[i] |= mask;
            }
            buf++;
        }
    }
done:
    return ret;
}
======================================================================

double revdoublebit(char *buf) は int revintbit(char *buf) の double 版です。

さて、 次のプログラムは、a が 0 から始まって 0.1 ずつ増えてゆくので 10 回で 1.0 に達し、 ループを抜けて「無事終わりました」が出力されるように思うかもしれませんが、 そうはならずに「printf("1.0 を越えてしまいました!」が出力されて終わります。

※ 途中の「return 1;」は、 ここにきたらプログラムを終わらせるためのもの。 異常終了の時に 0 以外の値を返すのが習慣なので 1 を返してみた。

それは、0.1 がコンピュータの中では循環小数となり、 10 回足しても 1.0 ちょうどにはならないからです。 上で紹介した関数と組み合わせると、 そのあたりがよくわかるかもしれません。
== sippai.c ==========================================================
#include <stdio.h>

main() {
    double a;

    for (a = 0; a != 1.0; a += 0.1) {
        if (a > 1.0) {
            printf("1.0 を越えてしまいました!\n");
            return 1;
        }
        printf("%f\n", a);
    }
    printf("無事終わりました.\n");
    return 0;
}
======================================================================


岩瀬順一