コンピュータの中では、2進数で値を表現するが、組み込み系のような小さいコンピュータでは、たくさんの桁を必要とする情報を扱うことが苦手である。そこで、C言語で数値を扱う型と、その型で扱える数値の範囲や問題点を説明する。
補足資料:プログラミングの基礎として、C言語の基礎を示す。
数値を扱う型
C言語では、データを覚える型を大きく2つに分けると、整数型(int)と実数型(float)に分けられる。
整数型(int)
整数型も正の値しか覚えられない符号無し型(unsigned int)と、符号付き型(signed int)に分けられる。さらに、その値を8bitで覚える文字型(char)、16bitで覚える short int型、32bitで覚える int 型、64bitで覚える long int 型(※C言語では long int で宣言すると32bitの場合も多いので要注意)がある。
精度 | 符号あり | 符号なし |
8bit | char | unsigned char |
16bit | short int | unsigned short int |
32bit | int | unsigned int |
64bit | long int※ | unsigned long int※ |
符号付きのデータは、負の数は2の補数によって保存する。この場合2進数の最上位bitは、負の数であれば必ず1となる。
整数型で扱える数
例えば、2進数3桁であれば、000,001,010,011,100,101,110,111 で、10進数であれば 0~7 の8通りの値が扱える。
(例) 符号なしの1byte(8bit)であれば、いくつの数を扱えるであろうか?
一般的に N bit であれば、0~(2N-1) までの値が扱える。
bit数 | 型 | 符号なし | |
8 | unsigned char | 0~28-1 | 0~255 |
16 | unsigned short int | 0~216-1 | 0~65535 |
32 | unsigned int | 0~232-1 | 0~4294967295 |
符号付きであれば、2の補数表現で最上位bitが0であれば正の数、1であれば負の数を表す。このため、N bit の符号つき整数は、-2N-1から2N-1-1の範囲の値を覚えられる。
bit数 | 型 | 符号あり | |
8 | char | -27~27-1 | -128~127 |
16 | short int | -215~215-1 | -32768~32767 |
32 | int | -231~231-1 | -2147483648~2147483647 |
数値の範囲の問題で動かないプログラム
この話だけだと、扱える数値の上限について実感がわかないかもしれないので、以下のプログラムをみてみよう。
組み込み系のコンピュータでは、int 型でも、一度に計算できるbit数が少ない。例えば、int型が16bitコンピュータでは、以下のプログラムは期待した値が計算できない。以下の例では、16bit int型として short int で示す。
// コード1 #include <stdio.h> #include <math.h> int main() { // 原点から座標(x,y)までの距離を求める short int x = 200 ; short int y = 200 ; short int r2 = x*x + y*y ; // (x,y)までの距離の2乗 short int r = sqrt( r2 ) ; // sqrt() 平方根 printf( "%d\n" , r ) ; // 何が求まるか? return 0 ; // (例) 282ではなく、120が表示された。 }
コンピュータで一定時間かかる処理を考えてみる。
// コード2.1 // 1 [msec] かかる処理が以下のように書いてあったとする。 short int i ; for( i = 0 ; i < 1000 ; i++ ) NOP() ; // NOP() = 約1μsecかかる処理とする。 // コード2.2 // 0.5 [sec]かかる処理を以下のようにかいた。 short int i ; for( i = 0 ; i < 500000 ; i++ ) NOP() ; // でもこの処理は16bitコンピュータでは、1μsecもかからずに終了する。なぜか?
上記の例は、性能の低い16bit コンピュータの問題で、最近は32bit 整数型のコンピュータが普通だし、特に問題ないと思うかもしれない。でも、32bit でも扱える数の範囲で動かなくなるプログラムを示す。
OS(unix) では、1970年1月1日からの経過秒数で時間を扱う。ここで、以下のプログラムは、正しい値が計算できない有名な例である。(2004年1月11日にATMが動かなくなるトラブルの原因だった)
// コード3.1 int t1 = 1554735600 ; // 2019年4月09日,00:00 int t2 = 1555340400 ; // 2019年4月16日,00:00 // この2日の真ん中の日を求める。 // 以下のプログラムは、正しい 2019年4月12日12:00 が求まらない。なぜか? int t_mid = (t1 + t2) / 2; // (例) 1951年03月25日 08:45 になった。 // コード3.2 // 以下のプログラムは正しく動く。 // time_t 型(時間処理用の64bit整数) time_t t1 = 1554735600 ; // 2019年4月09日,00:00 time_t t2 = 1555340400 ; // 2019年4月16日,00:00 // たとえ32bitでも溢れない式 time_t t_mid = t1 + (t2 - t1) / 2 ;