変数に関するうるさい話

各型の薀蓄

さて、変数に関して初歩中の初歩は脱却した。そういうわけで少しだけ高等、ってほどでもない話をしよう。とはいうものの、この辺りの知識がしっかりしているとバグに嵌る率が格段に下がる。というわけで各型をもう少しだけ見てみる。

int型

int型の大きさ

整数が入るのは前述したとおり。では、どんな整数が入るか。

int型の大きさは使用しているコンピュータの自然長である。

どういうことかというとコンピュータの頭脳であり、実際の計算をしているCPUと関係がある。CPUの中にはレジスタという一時的にデータを蓄えておくための場所がある。特に計算を高速に行うことを目的としたレジスタをアキュムレータレジスタと言うんだ。自然長とは一般に(例外も多々あるが)このレジスタの大きさのことをいうんだ。

今、あなたがPentiumIIIや4とかAthronXPを使っているならこれは32bitのデータが入る。32bitでどれだけの数値が表現できるかは前書きで2進数の話を散々したからわかると思うけど念のため、書いておくと232で4,294,967,296個(0も含めて)である。ただ、単にintと書いた場合は正負両方を表現することになるので(2の補数表現ってのやったよな)実際に扱える数字は-2,147,483,648から2,147,483,647ということになる。

・・・ちょっと待って。上の文を良く読んでほしい。「今、あなたがPentiumIIIや4とかAthronXPを使っているなら・・・」と書いてある。この前書き、かなり重要だ。正確にいうと上で書いたことは32bitCPUを使っていれば当てはまるという話でしかない。

少し昔のコンピュータ、例えばi8086やi80286と言ったCPUは16bitCPUだ。これらのCPUを使っている場合、int型は65,536個、つまり-32,768から32,767の数しか表現できない。これらのCPUは古めのNECのPC-98シリーズなどで普通に使われていたものだ。かつての日本を独占していたコンピュータ達である。20年ほど前の話だが。

「そんな古いコンピュータ達は知らん。」・・・とは残念ながら言っていられない。もしあなたがC言語を高価なワークステーションで動かす場合、64bitCPUを使っている可能性がある。近い将来、パソコンも64bitCPUが使われることが決定的だ。

何が言いたいのかというともしあながた「int型は絶対に32bitだ」としてプログラムを組むと確実に移植性が悪くなる。つまり、32bitCPUのシステムでしか動作しないプログラムになってしまうということなのだ。というのはCでint型が32bitだとは誰も言っていないのだ。64bitなシステムが普及してもint型だけは32bitとして据え置かれる可能性もあるにはあるが決定したわけではない。

とどのつまり、何が言いたかったのかというとint型の大きさというのは実に曖昧極まりないものなのだ。もし、あなたが自分の作ったプログラムを他のコンピュータで動かすことを前提とするなら非常に注意が必要だ。

さしあたってはこう、思っとけばいいんじゃないかしら。「int型は-32,768から32,767までは表現できる。それ以外はとりあえず、扱わない。」

長くしたり短くしたり

long int型, short int型みたいのを考えてみよう。

  1. long int nagai;
  2. short int mijikai;

longとshortを付けてみた。実はintを略して

  1. long nagai;
  2. short mijikai;

と書いても良い・・・というかそう書くほうが多いんだけど。これはint型を拡張したものだ。で、サイズだが32bitCPUを使っている場合、大抵はshort int型は16bit(-32768から32767)、long int型はint型と同じで32bit(-2147483648から2147483647)である。が、これもCの規格ではない。例によって使っているCPUによるわけだ。

Cの規格としてはこうなっている。

・・・つまり、曖昧だ。

また最近のCの拡張ではlong long int型というのがあり、これは64bitとか。

まとめるとこのように厳しく見ておけばint型で地雷を踏む率は下がる。

char型

前に述べた通り、本来は文字を表すための型である。が、コンピュータでは文字を文字としては扱っていないわけである。今、あなたが読んでいるこの文章もデータとしてはただの数字の羅列だ。それをブラウザが読める形に変換して表示しているだけだ。この変換を誤ると文字化けとなる、というわけだ。

で、char型は具体的にどういう文字を扱うための型かというとASCII文字である。ASCII文字というのは簡単に言ってしまうと半角文字(半角カナ以外)である。つまりアルファベットとか数字とかである。ちなみに改行も文字として扱っている。

ASCIIは8bitの文字コードなのである。というわけでchar型も8bitである。当然、値としては-128から127まで扱える。

「ねえ、日本語は扱えないの?」その変の話は後回し。

double型

倍精度浮動小数点型である。浮動小数点には2つの情報がある。3.141582 * 100の場合、3141592の部分を仮数部、0の部分を指数部という、というのはもう、述べたよな。

で、当然、サイズってのは仮数部、指数部それぞれにあるのだ。で、実際にそれぞれがいくらのサイズかは・・・実は処理系依存。実はC言語としては規定されていない。

ただ、IEEE(Institute of Electrical and Electronic Engineers)っていう学会が実は定めており、大方のシステムではこの規格に沿っているようだ。で、こうなっている。内部的には2進数なのでこういう感じだ。

で、aの部分(仮数部)が52ビット、bの部分(指数部)が11ビット、正負を示すビットが1ビットの計64ビットである。というわけで(考えてみよう)精度は約16桁、絶対値としては309桁の数字を表すことが出来る。ちなみにaの部分は普通、固定小数を使って書かれているんだけど・・・ま、これが罠になることも実はあったりする。まあ、いいや、今は。

・・・って書いたけどてっちゃんもヘッポコプログラマーだからこんなことを意識したことはないなあ。

ちなみにdouble型よりもさらにでかいサイズのlong double型ってのもある・・・が、サイズに関しては例の如くシステムによるらしい。

float型

float型は単精度浮動小数型。要はdouble型よりも精度の劣る小数を扱う場合。サイズはdouble型同様Cの規格では厳密に決められていないがaの部分が23ビット、bの部分が8ビット、正負を示すビットが1ビットの計32ビットであることが多い。・・・が、やはりヘッポコプログラマのてっちゃんは意識したことない。・・・が、実はfloat型は「罠」が存在するので使うことはまずない。

俺は負の数なんか使わないぞ

・・・そういう方もいるのでCにはsignedunsignedっていう形容詞があるぞ。例えば

unsigned int plus_number;

ってやると正のint型となる。で、仮にだよ、仮にint型が32bitだとすると(←くどい)単純にintだと-2147483648から2147483647までだったのが0から4294967295までになるのだ。つまり、負の数で使っていた部分まで正の数として扱うわけだ。

ちなみにsignedってのは「これは正負、両方扱うぞ」って意味なんだけど普通、「"signed int"なんて書かなくとも"int"だけで正負扱うんじゃなかったの?」ってその通りなのであんまり使わないなあ。というか、使ったことない。何に使うんだろう、教えてくれ。

ただ、意味なく"unsigned"を使うのはやめた方がいい。正の数が多く扱えるようになるとはいっても倍でしかない。特にint型と使うメリットなんてない。それどころか、後で述べるように「罠」が存在する。

"unsigned"はよく、char型と組み合わせて使うことも多い。これは実は処理系依存な話で文字を扱うときに"unsigned char"じゃないと警告を返すコンパイラがあったりするからだ。また、直接文字列とかの値を見る場合にunsignedの方が便利なことはある。けど、そういう事情がない限り、"unsigned"は実は使うべきじゃない。

質問コーナー

・・・つまりうるさい小言を語るコーナー。

int型って32ビットだよね。じゃあ、32ビットを超えるようなことをしたらどうなるの?

まず、前提としてint型は32ビットとは限らないので・・・(小一時間)←もういい。

では、あなたが32ビットなシステムを扱っていると仮定して話そう。次のプログラムはどうなると思う?

1	#include <stdio.h>
2
3	int main(void)
4	{
5		unsigned int dekai_kazu = 4294967295;
6
7		dekai_kazu = dekai_kazu + 1;
8		printf("dekai_kazu = %u \n", dekai_kazu);
9
10		return 0;
11	}

L5でunsigned int型のdekai_kazuを確保、で4294967295で初期化。L7は"dekai_kazu"に1を足してその結果を"dekai_kazu"に再び、代入。普通に考えると"dekai_kazu"は4294967295になるはずだよな。ただ、これは32ビットで表せる数の限界を超えているわけだ。L8で"dekai_kazu"の値を表示しようとしているけど"%u"ってのはunsigned intを受けるときに使うのだ。

さあ、L8で何が表示されるのでしょう?実行結果は僕のところではこうなった。

dekai_kazu = 0

??????なんで。

理由は単純、規格に反することをやろうとしたから。まあ、ただ、この場合、実は説明がちゃんとできる。

4294967295は2進数だと11111111111111111111111111111111(2)だけど長ったらしいので16進数で書くとFFFFFFFFHだ。これに1を足すと100000000000000000000000000000000(2)、つまり100000000Hになる。そう、33ビットになったのだ。この場合、通常は下位の32ビットだけが値となってしまい、それ以上のビットはなくなってしまうのだ。もしint型が64ビットであれば上位のビットも残り、ちゃんと4294967296と表示されるはずであろう。これが前に述べた「int型は32ビットとは限らない」のよからぬ作用なのだ。

ちなみにこの手の話はint型に限らず、どんな型でも起こる。というわけで各型の範囲を超えてしまうような演算はするべからず

でもさあ、じゃあ単純に(signedな)int型は2147483647までだよね。で次のようなプログラムを組んでみた。
1	#include <stdio.h>
2
3	int main(void)
4	{
5		int dekai_kazu = 2147483647;
6
7		dekai_kazu = dekai_kazu + 1;
8		printf("dekai_kazu = %d \n", dekai_kazu);
9		printf("dekai_kazu = %u \n", dekai_kazu);
10
11		return 0;
12	}
L8では-217483648となってたしかに範囲を超えたからだろうかおかしな値になった。でもL9ではちゃんと217483648になったよ。おかしいね。

217483647は7FFFFFFFHだ。これに1を足すと80000000Hになる。32ビット目が1になったということは負の数になったことになるわけだ。実際、これは2の補数表現を考えると-217483648なのだ。それがL8の結果なのだ。

一方、L9では何故、ちゃんと217483648と表示されたかというと"printf"で「"dekai_kazu"はunsigned"だと解釈してちょうだい。」って指定しているからなのだ。"%u"だから。80000000Hは"unsigned"で解釈すれば217483648だ。例えば次のプログラムを考えてみよう。

1	#include <stdio.h>
2
3	int main(void)
4	{
5		int kazu = -1;
6
7		printf("kazu = %d \n", kazu);
8		printf("kazu = %u \n", kazu);
9
10		return 0;
11	}

結果はこうだ。

kazu = -1
kazu = 4294967295

-1はFFFFFFFFHだ。これは2の補数表現とみれた-1だし、ただの正の数とみれば4294967295なのだ。"printf"自身は変数の型自体は把握していないので、プログラマーが変数の型を"%d"とか"%u"って指定しているのだ。だからどちらも正解なのだ。ただし、もうわかるだろうけどもし、int型が64ビットなら2行目は18446744073709551615になるはずだ(考えてみよう)。

ただ、今回のように足し算だけなら負の数であろうが正の数であろうが計算の仕方自体は同じなのであとは"printf"だけの問題だったが、実際には正負が効いてくる演算もあるのでこういう紛らわしいことをするのはやめよう。

なんでfloat型の扱いが軽いの?

滅多に使わないから。

なんでfloat型は滅多に使わないの?

意味がないから。double型で充分だから。float型とdouble型の違いって仮数部と指数部の長さだけだ。

だって不必要に高い精度を使うなんてメモリの無駄だし計算時間も長くなりそうだ。float型で事足りるんならfloat型がいいんじゃないの?

そんなこたーない。というか、float型の方がデメリットが多い。実は計算時間だが今のシステムだとfloat型もdouble型も変わらない、もしくはfloat型の方が長いことさえある。サイズも実は変わらない場合が多い。

確かにずっと昔、まだCPUに浮動小数演算機能がない時代の話、つまり浮動少数演算に専用のチップとかを使っていたときはfloat型の方が速かった。しかも当時はメモリーが今からすれば馬鹿高かった時代である。しかし、今のCPUは普通、倍精度浮動少数演算にハードレベルで対応しているのだ。それどころか、float型の演算も実は内部的にdouble型に変換して行っている場合もあるらしいのだ。そうなるとfloat型は変換をしている分、double型よりも遅いことだってある。しかもCにある標準関数(用はよく使う機能をまとめたやつら)もdouble型の利用を前提としているのだ。

そういうわけでよほど、何かの理由がない限り浮動少数を扱う場合、迷わずdouble型を使うべし。

くわしすぎるC