実は奥が深い算術演算

こんにちは、めのんです!

前回の最後で「そろそろ関数の解説をしようかと考えています」とは書いたんですけど、先に算術演算の解説をすることにしました。
何とぞご了承ください。

Cの算術演算はPHPの算術演算と基本的にはそう変わりません。
演算子に使う記号も同じです。
じゃあ簡単に流せばいいじゃないかと思われるかもしれないのですが、そうもいかないんですよね。

今回は算術演算に関するC特有の内容を解説していきたいと思います。

単項算術演算子

単項算術演算子というのは「+」や「-」の符号演算子のことです。
こんなの取り立てて解説するまでもないと思われるかもしれませんが少しお付き合いください。
結構奥が深いと思いますよ。

基本的な考え方としては、単項の「+」を付けても値は変わりませんし、「-」を付ければプラスマイナスが反転します。
オペランドが浮動小数点型の場合はそれでいいんですけど、整数型の場合はちょっと気を付けないといけません。

整数拡張

Cでは、整数型の値がほとんどの演算子のオペランドになるとき、「整数拡張」という暗黙の型変換が起きます。
整数型が単項算術演算子のオペランドになった場合も整数拡張が行われます。
整数拡張では、その型の表現範囲がすべてint型の表現範囲におさまる場合にはint型に変換されます。

主な処理系では、int型は32ビットですのでその表現範囲は-2147483648~+2147483647です。
それに対してsigned char型の表現範囲は-128~+127、unsigned char型では0~255、char型はそのどちらかです。
また、まだあまり詳しく解説していませんが、short型は-32768~+32767、unsigned short型は0~65535になります。
つまり、signed char型、unsigned char型、char型、short型、unsigned short型はすべてint型の表現範囲におさまりますからint型に暗黙的に型変換されます。

ここで注意しないといけないのは、もとの型がunsigned char型やunsigned short型のような符号無し整数型でも(符号付きの)int型に型変換されてしまうということです。
オブジェクトの型が符号無し整数なので演算の結果がマイナスになることはないだろうと思っていると失敗するということです。

整数型の表現範囲は処理系によって異なります。
あまり細かい話をしても消化不良を起こしますから今回はこのあたりにとどめますが、いずれ別の機会に解説できたらと考えています。

符号無し整数型の符号反転

単項の「-」演算子は符号を反転します。
浮動小数点型や符号付き整数型であれば簡単にイメージできると思いますけど、符号無し整数だとどうなるのか気になりますね。

整数拡張によってint型に暗黙的に型変換されるなら以後は符号付き整数型として振る舞います。
問題はそれ以外ですよね。
たとえば、unsigned int型はint型の符号無し版で32ビットであれば0~4294967295が表現範囲になります。
これは整数拡張してもunsigned int型のままなのでマイナスの値を表現することができません。

では符号無し整数型のオペランドに「-」演算子を適用した場合はどうなるのでしょうか?
答えはもとの値の2の補数になります。
2の補数は表現範囲の最大値+1からもとの値を引いた値ですね(プログラミングの経験者であればあらためて説明は必要ないでしょう)。

unsigned int x = 123;
unsigned int y = -x;  // 2の補数、つまり 4294967295 + 1 - 123 = 4294967173

PHPでは演算結果の値によって型が変わることがありますが、Cにはそのような動的な型変換はありません。
Cでは演算結果の型はオペランドの型によってコンパイル時に決まります。

オーバフロー

整数型でも浮動小数点型でも表現範囲が有限ですからオーバフローが発生することがあります。
オーバフローが起きたときにどうなるかは型によって異なります。

  • 符号付き整数型がオーバフローした場合は未定義の動作になります。
  • 符号無し整数型は決してオーバフローしません。
  • 浮動小数点型がオーバフローした場合はHUGE_VAL等の値になります(詳細はまた別の機会に)。

Cでは符号付き整数型のオペランドに対して「-」演算子を使うとオーバフローが発生することがあります。
なんか不思議な気がしますね。

int型の表現範囲は-2147483648~+2147483647だということを思い出してください。
もし、最小値である-2147483648に「-」演算子を適用した場合はどうでしょう?
数学的には+2147483648ですが、これはint型の表現範囲を超えています(つまりオーバフローです!)。

符号付き整数型に「-」演算子を適用してオーバフローした場合ももちろん未定義の動作になります。
未定義の動作といってもほとんどのケースでは-(-2147483648)の結果は-2147483648になるようです。
マイナスの値の符号を反転したのにまたマイナスの値になってしまうんです!

増分演算子と減分演算子

「増分演算子」「減分演算子」というとわかりにくいと思いますが、要するにインクリメントとデクリメントです。
PHPにも「++」演算子と「–」演算子がありますね。
それと同じです。
前置型と後置型があることも、その意味もPHPと同じです。

注意しないといけないのは、増分演算子と減分演算子のオペランドとなるオブジェクトは、いったん整数拡張されてから1を足したり引いたりされたあと、再びもとのオブジェクトに格納されます。
1を足したり引いたりする際にもオーバフローが起きるかもしれませんし、もとのオブジェクトに格納する際に(厳密にはオーバフローではないのですが)そのオブジェクトの型の表現範囲を超えてしまうかもしれません。

符号付き整数型が型変換によって変換後の型の表現範囲を超えた場合、処理系定義の値になるか処理系定義のシグナルのシグナルが発生します。
もし処理系定義のシグナルが発生した場合は未定義の動作になります。
ですので、未定義の動作になると思っておいた方がよさそうですね。

二項算術演算子

算術演算を行うための二項演算子には加減乗除を行う演算子と剰余演算を行う演算子があります。
二項算術演算子には「+」、「-」、「*」、「/」、そして「%」があり、その意味もほぼPHPと同じです。

ひとつだけ意味が異なるのは「/」演算子です。
PHPでは両方のオペランドが整数の場合でも「/」演算子で除算を行った結果が割り切れなければ浮動小数点数になりますね。
先ほども書きましたが、Cでは演算の結果の型によって動的に型が変わることはありませんので、整数同士の除算でも結果の型は必ず整数型になります。
PHPでいえばintdiv関数と同じように振る舞うんです。

二項算術演算子を評価する際にも当然オーバフローは起こりますし、そのことには十分注意しないといけません。
なお、PHP 5.6で累乗演算子(「**」)が導入されましたがCにはありませんのでこちらもご注意ください。

通常の算術型変換

除算演算子や累乗演算子を除けばCの二項算術演算子も基本的な振る舞いはPHPと変わりません。
けれどもそれは両方のオペランドの型が同じ場合です。

PHPでもオペランドの型が異なれば暗黙の型変換が起きますが、それと同じようなことがCでも起きます。
それが「通常の算術型変換」です。
なんかパッとしない用語ですけどusual arithmetic conversionの訳語のようです。

通常の算術型変換はちょっとややこしいですけど、次の手順で暗黙の型変換が行われます。
ここは正確を期さないといけないので、標準規格から引用しますね。

まず,一方のオペランドの対応する実数型がlong doubleならば,他方のオペランドを,型領域を変えることなく,変換後の型に対応する実数型がlong doubleとなるように型変換する。

そうでない場合,一方のオペランドの対応する実数型がdoubleならば,他方のオペランドを,型領域を変えることなく,変換後の型に対応する実数型がdoubleとなるように型変換する。

そうでない場合,一方のオペランドの対応する実数型がfloatならば,他方のオペランドを,型領域を変えることなく,変換後の型に対応する実数型がfloatとなるように型変換する。

そうでない場合,整数拡張を両オペランドに対して行い,拡張後のオペランドに次の規則を適用する。 − 両方のオペランドが同じ型をもつ場合,更なる型変換は行わない。

− そうでない場合,両方のオペランドが符号付き整数型をもつ,又は両方のオペランドが符号無し整数型をもつならば,整数変換順位の低い方の型を,高い方の型に変換する。

− そうでない場合,符号無し整数型をもつオペランドが,他方のオペランドの整数変換順位より高い又は等しい順位をもつならば,符号付き整数型をもつオペランドを,符号無し整数型をもつオペランドの型に変換する。

− そうでない場合,符号付き整数型をもつオペランドの型が,符号無し整数型をもつオペランドの型のすべての値を表現できるならば,符号無し整数型をもつオペランドを,符号付き整数型をもつオペランドの型に変換する。

− そうでない場合,両方のオペランドを,符号付き整数型をもつオペランドの型に対応する符号無し整数型に変換する。

整数変換の順位についても解説が必要ですね。
これについては簡単に済ませることにします。

まず、signedやunsignedが(付く付かないも含めて)違うだけの型は同じ順位になります。
int型とunsigned型とか、signed char型とunsigned char型とchat型なんかがそうです。

あとは次のようになります。

long long > long > int > short > char
高                                 低

これは理屈では無くこういうもんだと覚えるしかありません。

ちょっと今回は難しい内容だったかもしれませんが、普通に使う分にはPHPとそんなに変わりませんのでまずは気軽に取り組んでみましょう。

こんな風にCの解説は(私に限らず)かなり厳密は話になることが多いので難しい印象を持たれるかもしれません。
けれども、PHPも厳密に解説しようとすれば同じかそれ以上に難しいんですよ。
ですので、あまりCに難しい印象は持たないでくださいね。

それでは今回の解説は以上となります。
次回は関数の解説ができればいいですね!