続、変数に関するうるさい話

演算時の型

前に割り算のところで説明したけどCでは演算している最中の型を気にかける必要があるっていったよな。

kekka = hako1 + hako2;

前の例だと割り算だったけどこの話は割り算だけじゃなく、どんな場合でも関係あるのだ。この場合だと、"hako1 + hako2"の計算は"hako1"と"hako2"の型で、より「厳しい」型で計算されると述べた。重要なのは演算時の型は"kekka"の型とは無関係だってことだ。演算結果はたしかに"kekka"の型にあわせて代入されるだけどな。

で、「厳しい」って今までお茶を濁してきたけどこれはちゃんとCで規格で規格されているのだ。つまり演算時の型の順位ってものがある。で、それがこれだ。

long double > double > float > unsigned long int > long intとunsigned intの兼ね合いでunsigned long intかlong int(注) > long int > unsigned int > int

これの見方はこうだ。ある演算子(+, -, *, /とかのこと)の両側の数字や変数の型を見る。で、より優先順位の高い方でもって計算されるということだ。

ただ、(注)のところは場合によりけりで、もし、"[long int型] + [unsigned int]"でいずれの数字も"unsigned long int"で表せる場合は"unsigned lont ing"に、そうでない場合は"long int"になるということだ。

また、int型未満のサイズしかない型(char型とかshort int型とか)は全て、int型として扱われている。演算中にはint型未満の型は存在しないのだ。

ここまでがいわゆる一遍通りな説明ってやつだ。しかしこれだけの説明でここには大変な「罠」があることを見抜ける奴はあまりいないんじゃないかな。Fortranとかやっていた経験があれば別だけど。ということで、ここではわざと罠に嵌って「へたっぴ」って罵倒し・・・いや、ミスから学ぼう。もっともひどい書籍だとこの演算中の型すら書いていなかったりするんだけど・・・

unsignedな地雷

"(-5 / 5) * (-5)"っていくらだろうか。普通に考えると"5"である。じゃあ、次の場合はどうだろうか、黙って次のプログラムを実行してほしい。

1	#include <stdio.h>
2
3	int main()
4	{
5		int i = -5;
6		unsigned int ui = 5;
7		int result;
8
9		result = (i / ui) * i;
10		printf("(%d / %d) * %d = %d\n", i, ui, i, result);
11
12		return 0;
13	}

L5でint型でiを確保、-5で初期化する。L6でunsigned int型でuiを確保、5で初期化する。L9で"(i / ui) * i"を計算、resultに代入、L10で結果を表示させる。ただこれだけだ。僕の方ではこうなった。

(-5 / 5) * -5 = 6

あり、なんでだろう?

これがunsignedな罠だ。前に「無意味にunsignedは使うべからず」って言ったよな。それに嵌った典型的な例だ。

実はこれもちゃんと説明がつく。とりあえず、32ビットだとしよう。というか、int型が32ビットでなければこれとは違う結果になっているはずだ。

まず、Cでは算数同様、括弧内が先に計算される。で、"i / ui"がされるわけだがこれは"[int型] / [unsigned int型]"なのでunsigned int型として計算されるわけだ。だから"i = -5"、"ui = 5"なので実際の計算は(iは2の補数表現としてではなく、ただの正の数として計算されるので)・・・

111111111111111111111111111110112 / 1012
=FFFFFFFBH / 5H

FFFFFFFBHは正の数とみれば4294967291だ。で5で割るわけだから858993458.2。結果もunsigned int型として扱われるので小数以下は消えて858993458(16進数で33333332H)となる。

で、この結果をさらに-5で、つまりFFFFFFFBHを掛けるのだが前述の括弧内の結果がunsigned int型なもんだからこのFFFFFFBHも正の数として計算されてしまう。つまり、

33333332H * FFFFFFFBH = 3333333100000006H

となるが、この演算はunsigned int型で32ビットであるから下位の32バイトだけが残り、つまり00000006H、つまり結果、"6"になる。見事なまでにうまく説明が出来てしまった。

しかしこの結果は当然、プログラマーが望んでいるものではない。大体、int型が32ビットだから説明できたわけで恐らく、全く同じプログラムでもint型の長さが32ビットでないシステムに持っていくと違う結果になり、移植性もあったもんじゃない。

何故、こんなことになるのか。理由は簡単だ。前述した「厳しい」順番を見てくれ。一般に"unsigned > signed"になっているのだ。"unsigned"と"signed"の混在した式では"unsigned"で計算されてしまい、負の数の扱いはめちゃくちゃになってしまう。

で、対策は・・・ってもう言うまでもないな。前述したとおり、意味なく"unsignedは使うべからず。これは罠に嵌るというより自爆に近いぞ。"unsigned"なんて使わなければ無縁な罠だ。

floatな地雷

工事中

くわしすぎるC