第四回-02 C/C++ における演算子


インクリメントとデクリメント

四則演算については「第一回-05 変数の利用と四則演算」で既に学んだ。「+」、「-」、「*」、「/」、「%」などのことを演算子と呼ぶ。
これらは算術演算 (数学上の演算) を行う演算子なので、算術演算子と言う (他には論理演算というものがあり、いずれ学ぶ)。

四則演算の演算子に、「インクリメント (++)」および「デクリメント (--)」という新たな演算子を追加したのが下表である。

演算子 操作 備考
+ 加算
- 減算、単項のマイナス記号
* 乗算 (掛算)
/ 除算 (割算) 整数(int)型、float型、double型ともに使えるが、
int同士の除算は小数点以下は切捨てられる。(整数の割り算の商)。
% 剰余 (余り) ともにintの場合に余りを計算する。
例えば、「10÷3 = 商3 余り1」なので、10%3 は 1 と計算される。
++ インクリメント 1増やす。x++ や ++x として用いると、x = x+1 と同じ効果。
-- デクリメント 1減らす。x-- や --x として用いると、x = x-1 と同じ効果。

インクリメントとデクリメントの利用はそれほど難しくない。以下のプログラムを実行してみれば、
x に格納されていた 5 が 1 増やされて 6 になることを確認できるだろう。

int x=5;

x++;   // ++x; と書いても同じ

std::cout << "xの値は" << x  << "です\n" ; // 6と表示される

1 減らす働きをもつ演算子デクリメント (--) についても同様である。

どちらかと言えば、表の中に書かれている解説用の命令「x=x+1」や「x=x-1」の方が難しいと感じる学生が多いかも知れない。
なぜなら、「x=x+1」という式は数学では「x と x+1 が等しい」という意味なわけで、数学的にあり得ない式だからである。
すなわち、「x=x+1」や「x=x-1」は数学ではなくプログラミングでのみ意味がある式、ということである。

これらの式の意味は、「第一回-05 変数の利用と四則演算」で学んだ
右辺の計算式が先に計算され、その結果が左辺の変数に格納される」という原則に基づけば問題なく理解できる。

以下の図は、5 が格納された x に対して「x=x+1;」が実行されたときに起こることをイメージした図である。



右辺の計算と左辺への代入が別々のフェーズで行われると考えれば、自然に理解できるのではないかと思う。

なお、インクリメントやデクリメントの前置 (++x、--x) と後置 (x++、x--) と後置は、
下記のように計算のタイミングに関して微妙な (しかし重要な) 違いがある。実行して確認してみよう。
とはいえ、初心者のうちにこういう命令を書くことは少ないと思われるので、当面気にする必要はないと思う。

int x, s, t;

x = 5;
s = x++;     // s = x; x = x + 1; の順で計算されるので s = 5 となる

x = 5;
t = ++x;     // x = x + 1; t = x; の順で計算されるので t = 6 となる

std::cout << "sの値は" << s << "です\n";
std::cout << "tの値は" << t << "です\n";




演算子の優先順位

数学の計算においては乗算、除算が加算、減算よりも優先されることを皆さんご存知だろう。
これは、数学における演算子には優先順位があることを意味している。

プログラミングで用いる演算子にも優先順位がある。上で示した範囲の演算子の優先順位を示したのが下表である。

優先順位 演算子 結合性
1 ++(後置)、--(後置)
2 ++(前置)、--(前置)
3 *、/、%
4 +、-

上にある演算子ほど先に計算される。また「結合性が左」とは、同じ優先順位の演算子は左から順に計算される、ということである。
四則演算の演算に限れば、「乗算・除算が加算・減算より優先され、左から順に計算される」ということを示しており、
皆さんにとってはそれほど違和感ないルールであろう。

このルールに従えば、x と y の平均値を a という変数に格納したいとき、以下のように「a = x+y/2;」と書いてはいけないことはわかるだろう。

int x=4;
int y=10;
int a;

a = x+y/2;  // 平均値を求めるための駄目な例

std::cout << "aの値は" << a << "です\n";

もちろん、駄目な理由は、除算「/」が優先され、と解釈されてしまうためである。

平均値の計算を正しくと解釈させるためには、
数学同様、カッコ () を使って優先順位を変更すれば良い。すなわち、下記のように「a = (x+y)/2;」とするのである。

int x=4;
int y=10;
int a;

a = (x+y)/2;  // 平均値を求めるための正しい例

std::cout << "aの値は" << a << "です\n";

ただし、この用途で用いることができるのは小カッコ () のみで、 中カッコ {} や大カッコ [] は使えないので注意。

もう一つ例を見てみよう。

という式をプログラムでどう書いたら良いだろうか。
以下の例は間違った例である。

int x=2;
int y=5;
int a;

a = 10/x*y;  // 誤り例

std::cout << "aの値は" << a << "です\n";

計算式には除算「/」と乗算「*」が含まれるが、これらの優先順位は等しいので、左から順に実行されてしまい、
と解釈される。これは計算したかった式とは異なる。

正しくは、こちらの場合も 以下のように「a = 10/(x*y);」と カッコ () で優先順位を変更すれば良いのである。

int x=2;
int y=5;
int a;

a = 10/(x*y);  // 正しい例

std::cout << "aの値は" << a << "です\n";

なお、「左から順に計算される」ことに注意すれば、以下のようにカッコなしで「a = 10/x/y;」とも書ける。

int x=2;
int y=5;
int a;

a = 10/x/y;  // 正しい例

std::cout << "aの値は" << a << "です\n";

最初からこの書き方に馴染める学生は多くないと思うが、世の中には様々な記法をするプログラマがいるので、
慣れておいて損はない。


型に基づく演算と型変換

型変換とは、int や double などの「型 (type)」が変換されることを指す。
自動的に行われる場合もあれば、自分で意図的に型変換を行う場合もある。

ここでは、演算の際に自動的に型変換が行われる場合を紹介する。

まず、話をシンプルにするために以下ではint型とdouble型の数のみを例に話を進める。
なお、int型とdouble型の数とは、下記のものを指すものとする。

int型の数 ・1, 2, 3 … などのように、小数点以下のない数

・int x; などのように、int型で宣言された変数に格納されている数
double型の数 ・1.0, 2.0, 3.0, 3.14 … などのように、小数点以下を含む数

・double x; などのように、double型で宣言された変数に格納されている数

さて、上で学んだ算術演算を行う際、二つの演算が行われる数 (被演算数) は同じ型であることが前提とされる
乗算で言えば下記の通りである。

計算式 計算結果についての補足
3 * 5 (int型) * (int型) なので、結果もint型の 15
3.0 * 5.0 (double型) * (double型) なので、結果もdouble型の 15.0

では、被演算数がそれぞれ異なる型、例えば (int型) と (double型) であった場合何が起こるだろうか?
このとき、「狭い」ほうの型 (int型) が「広い」ほうの型 (double型) に変換されてから計算される。これが演算時に自動的に行われる型変換である。

具体例で言えば下記の通りである。

計算式 計算結果についての補足
3 * 5.0 (int型) * (double型) だが、int型の3がdouble型の3.0に型変換され、
3.0 * 5.0 が計算される。結果はdouble型の 15.0
3.0 * 5 (double型) * (int型) だが、int型の5がdouble型の5.0に型変換され、
3.0 * 5.0 が計算される。結果はdouble型の 15.0

結局のところ、結果は全て 15 または 15.0 となるので、何が重要なんだ?と感じたかもしれない。
この型変換に注意を払わなければならないのは演算が除算のときである。 具体例を表にまとめてみよう。

計算式 計算結果についての補足
3 / 5 (int型) / (int型) なので、計算結果は割り算の商であるint型の 0
3.0 / 5.0 (double型) / (double型) なので、小数点以下を含む割り算が行われる。結果はdouble型の 0.6
3 / 5.0 (int型) / (double型) だが、int型の3がdouble型の3.0に型変換され、
3.0 / 5.0 が計算される。結果はdouble型の 0.6
3.0 / 5 (double型) / (int型) だが、int型の5がdouble型の5.0に型変換され、
3.0 / 5.0 が計算される。結果はdouble型の 0.6

除算の場合、被演算数の型によって計算結果が 0 か 0.6 かで異なってしまう。
これに注意しないと、プログラム中、思わぬバグにつまずくことになる。


代入と型変換

自動の型変換は、変数への数値の代入においても行われる。これを解説しよう。
「狭い」型の数値を「広い」型の数値に代入する場合はなにも問題がない。例えば以下のようである。

double x = 3;   

ここでは、double型の変数にint型の 3 を格納している。
このとき、int型の 3 がdouble型の 3.0 に型変換されてから格納される。

問題は「広い」型の数値を「狭い」型の数値に代入する場合である。典型的には double 型の数値を int 型の変数に代入する場合である。
例えば以下のような場合である。

int x = 3.14;   // 何が起こる?

このとき、double型の数である 3.14 はint型に型変換されるC/C++では、double型をint型に型変換すると、小数点以下の数値は切り捨てられる
すなわち、double 型の3.14 がint型の 3 に型変換されるため、x には 3 が格納される。

実行できる形式で記すと以下のようになる。

int x = 3.14;   // 3.14 を整数に型変換された 3 が x に代入される

std::cout <<  "x は" << x << "\n";    // x は3

// 他の例も提示

x = -3.14;     // x = -3 に丸められる

std::cout <<  "x は" << x << "\n";    // x は-3

double y = 10.2;
x = y;        // x = 10 に丸められる 

std::cout <<  "x は" << x << "\n";    // x は10

なお、このようにdouble型をint型に型変換するということは、実際には意図せず行ってしまうことが多い。
すなわち、本当は 3.14 のまま数値を代入したかったのに、誤って整数の変数に格納してしまった、などのケースが多いのである。

そのため、 double 型の数を int 型の変数に代入すると、コンパイル時に警告が出るようになっている。
例えば、以下のような警告が現れる。。

warning C4244: '=' : 'double' から 'int' への変換です。データが失われる可能性があります。

このような「警告」を常にチェックして警告が出ないようプログラムする癖を付けた方がトラブルが少ない
少なくとも、上で見た「3.14 を代入したかったのに 3 が格納されてしまった」という意図せぬトラブルは、警告をチェックすることで防ぐことができる。

一方、double型からint型への変換を意図的に行いたい場合もある。意図的に行なっているのにわざわざ警告が出るのは煩わしい。
そこで、明示的に型変換を行なうことで警告を消すということも行なわれる。

int x = (int)3.14;   // 3.14 を整数に型変換された 3 が x に代入される

std::cout <<  "x は" << x << "\n";    // x は3

// 他の例も提示

x = (int)-3.14;     // x = -3 に丸められる

std::cout <<  "x は" << x << "\n";    // x は-3

これは、「わかっていて意図的にやってるんですよ」ということをコンパイラに指示していることになり、警告は消える。


int 型と double 型にまつわる注意

以上で解説した「型に基づく演算と型変換」と「代入と型変換」はセットで注意しないと引っかかることが多い。その例を示そう。

まず、下図のような命令を考えよう。右辺で何らかの計算を行ない、左辺の変数 x に代入するという命令である。

x = (何らかの計算);

何度か述べたように、上記の計算は下記の流れで行われる。
それぞれのステップで型が重要であることが記されている。
  1. 右辺の計算の実行 : 型に基づいて演算が行われ、その際に型変換が行われる場合もある
  2. 右辺の計算結果の x への代入 : 代入による型変換が行われる可能性がある
以下の具体例を見てみよう。3つの例を示すが、どれも 1 を 5 で割り、結果を変数に格納している。
一見どれも 0.2 が格納されそうに見えるが、実はどれも 0 が格納される、という例になっている。理由を考えながら読んで行こう。

まず一つ目の例である。実行して試してみると良いだろう。

double x;
x = 1 / 5;

std::cout << "xは" << x << "である\n";

x は 0 と表示されるが、それは下記の計算ステップが行われたことによる
  1. 右辺の計算の実行 : 1 / 5 はint型同士の割算なので、商である0が計算される
  2. 右辺の計算結果の x への代入 : double 型の変数 x にint 型の整数 0 が代入され、型変換されて double型の 0.0 が格納される。
次に、二つ目の例である。

int s, t;
double x;
s = 1;
t = 5;
x = s / t;

std::cout << "xは" << x << "である\n";

こちらも x は 0 と表示されるが、それは下記の計算ステップが行われたことによる。
計算内容は上の例と全く同じであることがわかるだろう。
  1. 右辺の計算の実行 : s / t はint型同士の割算なので、1 / 5 の商である0が計算される
  2. 右辺の計算結果の x への代入 : double 型の変数 x にint 型の整数 0 が代入され、型変換されて double型の 0.0 が格納される。
最後の例はこちら。

int x;
x = 1.0 / 5.0;

std::cout << "xは" << x << "である\n";

1.0 と 5.0 はどちらも double型なので、xには 0.2 が格納されるように見えるが、 やはり x には 0 が格納される。
それは以下のステップで計算が行われたことによる。
  1. 右辺の計算の実行 : 1.0 / 5.0 はdouble型同士の割算なので、double型の0.2が計算される。
  2. 右辺の計算結果の x への代入 : int 型の変数 x にdouble型の 0.2 が代入される際、
    int型に型変換されることにより、小数点以下が切捨てられて 0 が格納される。
このように、型を常に意識しないと、意図しない結果が格納されることがあることには注意しなければならない。



←第四回-01 変数の宣言と文の構造第四回課題→

非情報系学生のための C/C++ 入門に戻る