Part 1.変数の定義域(1)

やねうプログラ民 道場


最近、他人のソースをコードレビューする機会が増えて、もう同じことを言うのがいい加減疲れてきたので、ひとまとめにして公開していくことにしたい。


今回は整数型変数についてだ。話を簡単にするためint型は32bit符号あり,uint型は32bit符号なし、shortは16ビット符号あり、ushortは16ビット符号なし。sbyteは8ビット符号あり、byteは8ビット符号なしとしよう。あとはポインタも32bitと限定しよう。あまりC/C++言語の特有の仕様にまで立ち入るつもりはないが、一応C/C++言語を対象とする。


まずn回まわるループを考えてみよう。とりあえずは


for( int i=0 ; i<n ; ++i){


と書くかも知れない。このループ変数であるiはint型を用いている。ループカウンタは非負(0か、正)の整数であって、定義域が非負である以上、ここでintを持ち出すと、intの上限までしか回ることが出来ない。つまり、2,147,483,647まで。nがこれを超えていると、このループは正しく回らない。ループ回数であるnがuintだと i<n は、符号型と無符号型とを比較することになるし、また、配列の要素すべてに対して同じ処理をしたいときに、vector::size回数だけ回ろうにもsize()の返し値は無符号型なので、i<(int)v.size() なんて型キャストが必要になる。細かいことを言えば、ループのなかで a[i] = 0; のようにアクセスするときも、配列のindexは非負であって、ここに符号付のindexを指定するのも少しおかしい。


言うなれば、ループ回数をカウントするためだけのループ変数にわざわざ符号型を使う意味がどこにもない。強いてメリットをあげるとすれば、uintをtypedefせずに済むとか、n-1回まわるつもりで


for( uint i=0 ; i<n-1 ; ++i){


と書いたらn==0のときに意図している動作と異なるだとか、その程度の理由だ。まあしかし、C言語では伝統的にintを使うという慣習もあるので、ループ変数のことにはあまり突っ込まないことにする。


しかし、このへんを意識していない人のソースを見ると、オーバーフローやアンダーフローについて何も考慮されていないことが多い。たとえば、次のようなコードだ。


c = (a + b)/2;


平均をとるだけの平凡なコードに見える。しかし、仮にa,b,cがbyte*だとすると、このコードは正しくない。*1 (バイナリサーチなどでこういうコードを書いていないだろうか?)


byte*は a + b の時点でオーバーフローしかねないからだ。実際はこういうへたれコード救済のため32bit Windowsでは 0x80000000以上のメモリアドレスがallocで返されることはないのだが、64bit Windowsでこういう問題が表面化しかねない。*2


もちろん、32bit Windowsであっても、


d = (a + b + c)/3;


こういうコードではオーバーフローしかねない。つまり、ポインタにポインタを加算するのは一般的には正しくない。オーバーフローしかねないからだ。(つづく)

*1:ポインタ同士の加算はC/C++では許されないので、実際はいったんuintにキャストする必要がある。

*2:参考→http://www.radiumsoftware.com/0408.html#040831