見出し画像

さっくりC言語_04 //演算子

 はい、というわけでさっくりC言語のお話です。今回は演算子、特に優先度のお話でございます。

 演算子の優先度はそのコンパイラやら仕様やらによって変わってきます。もっと言えば、構文規則によって、構文木を作成する際にどちらが優先されるかが決まってくるのですが、まあ、その辺については詳しくないので割愛します。

 詳しく知りりたい方は、形式文法とかBNFとか構文解析などをキーワードに調べて見てくださいな。

前回の記事


1.四則演算

 コンピューターと言う物は基本的に計算機です。ですので、当たり前ですがあらゆる言語で演算を扱うことができます。複雑で難しい計算をコンピュータはしてくれますが、突き詰めてしまえば、基本的な四則演算を高速で行ってくれる機械なのです。

で、基本的な演算は次の通りになります。

加算(足し算):+
減算(引き算):-
乗算(掛け算):*
除算(割り算):/
剰余(割ったあまり):%

 これらは、算術演算といって、まあ、数字をあれこれ計算する為に利用されます。

 また、通常の演算とは使い=は、同等の関係を示す演算子ではなく、左側に右側の情報を入れますよ、という意味になります。例えば、A=1+2では、Aというメモリに、1+2の結果を入れなさい、という意味で、この時Aのメモリに3が代入されることになります。

コード

#include<stdio.h>

int main() {
	int a = 5, b = 3;
	double c = 3.0;

	printf("a + b = %d \na - b = %d\n", a + b, a - b);
	printf("a * b = %d \na / b = %d\na / b = 余り%d\n", a * b, a / b, a % b);
	printf("a / b = %f\na / c = %f\n", (double)a / b, a / c);

	return 0;
}

結果

a + b = 8
a - b = 2
a * b = 15
a / b = 1
a / b = 余り2
a / b = 1.666667
a / c = 1.666667

 この時、一点だけ気を付けて欲しいことが有ります。それは、演算結果がどの型になるのかは、パソコンの方が勝手に決めてしまうことです。

つまり、
int / int = int
double / double = double
int / double = double
double / int = double
と、自動的に変換してしまいます。ですので、もしも整数どうしの割り算の結果を、小数点以下を含めた形で表現したい場合には、キャストを利用して型を明示的にする必要があります。

 キャストとはある型で保存されているデータを、無理やり別の形へと変換する方法です。上記のコードでは

(double) a / b

 と、表記されていますね。これは、int型のaを無理やりdouble型に変換する事を意味しています。その結果、演算は(double)  / (int)となり、結果がdouble型になるんですね。

 で、当たり前ですが、この式の結果を代入することができます。

コード

#include<stdio.h>

int main() {
	int a = 5, b = 3, c;

	c = a - b;
	printf("a - b = %d\n", c);
	c = 3;
	printf("c = 3\n");
	c = c + a;
	printf("c + a = %d\n", c);
	c = 3;
	c = c - b;
	printf("c - b = %d\n", c);


	return 0;
}

結果

a - b = 2
c = 3
c + a = 8
c - b = 0

 そして、式にメモリを用いて、その結果をメモリに格納できるということは、次々にメモリ内のデータを演算によって書き換えることもできる事になります。

コード

#include<stdio.h>

int main() {
	int a = 1, b = 2, c = 3;

	printf("a = %d b = %d c = %d\n",a , b, c);

	c = a + b;
	b = b + c;
	a = c + a;

	printf("a = %d b = %d c = %d\n", a, b, c);

	return 0;
}

結果

a = 1 b = 2 c = 3
a = 4 b = 5 c = 3

 なんかよくわかんない計算ができていますね。このように、メモリの情報をその都度書き換わっていくので注意が必要です。

 で、AとBの計算結果をAに入れる、という計算の省略した演算がC言語では用意されています。

加算:a = a + b
   a += b
減算:a = a - b
   a -= b
乗算:a = a * b
   a *= b
除算: a = a / b
   a /= b
余算:a = a % b
   a %= b

 はい、ではさっくり確認してみましょう。

ソース

#include<stdio.h>

int main() {
	int a = 2;

	a += 3;
	printf("a = %d\n", a);
	a -= 2;
	printf("a = %d\n", a);
	a *= 4;
	printf("a = %d\n", a);
	a /= 2;
	printf("a = %d\n", a);
	a %= 5;
	printf("a = %d\n", a);

	return 0;
}

結果

a = 5
a = 3
a = 12
a = 6
a = 1

 演算の内容と結果は想像通りだったでしょうか。一応確認しておくと、

a == a + 3 == 2 + 3 == 5
a == a - 2 == 5 - 2 == 3
a == a * 4 == 3 * 4 == 12
a == a / 2 == 12 / 2 == 6
a == a % 5 == 6 % 5 == 1

 と、なりますね。

 また、メモリに対して、1つだけ足す演算、一つだけ引く演算もあります。1つだけ足す演算をインクリメント演算、一つだけ引く演算をデクリメント演算と呼びます。

加算:a += 1
   a++
   ++a
減算:a -= 1
    a--
   --a

 で、このa++と++aは少し曲者でして、といいますのも、++が右につくか左につくかで、演算するタイミングが全く変わってきてしまうのです。aの前に++を付けるのを前置演算、後ろに++を付けるのを後置演算と言い、++aの前置演算で一番最初に、a++の後置演算で一番最後に計算を行います。

ソース

#include<stdio.h>

int main() {
	int a = 0;

	printf("足し算\n");
	a++;
	printf("a = %d\n", a);
	a = a++ * 3;
	printf("a = %d\n", a);
	a = 1;
	a = ++a * 3;
	printf("a = %d\n", a);

	printf("引き算\n");
	a = 2;
	a--;
	printf("a = %d\n", a);
	a = a-- * 3;
	printf("a = %d\n", a);
	a = 1;
	a = --a * 3;
	printf("a = %d\n", a);

	return 0;
}

結果

足し算
a = 1
a = 4
a = 6
引き算
a = 1
a = 2
a = 0

 計算は以下の通りです。

a == a++ * 3 == 1++ * 3 == (1*3)++ == 3++ == 4
a == ++a * 3 == ++1 * 3 == 2 * 3 == 6
a == a-- * 3 == 1-- * 3 == (1*3)-- == 3-- == 2
a == --a * 3 == --1 * 3 == 0*3 == 0

 後置では本当に、最後の最後に演算を行う事になります。それこそ、その行の演算が全て終わった後にです。この性質は、よくforループなどで使われる形式です。

2.論理演算

 論理演算は、所謂0,1の数学と呼ばれるブール代数学のお話と非常関わりが深いです。と、いっても、ここでは深くは突っ込みません。詳しいことは、調べて見てください。

 さて、論理演算では、本当に0、1、またの名はTRUE(真),FALSE(偽)しか使わない演算です。感覚的には、その演算が正しいのか、間違っているのかを判定している、とみると良いでしょうか。

 例えば、1 < 2という式があったとします。この式は勿論正しいですよね。ですのでTRUE(正しい)と、言いたい所ですね。しかし、プログラミングではこれを、数学的?、表現しなければなりません。そこで、0を偽、それ以外を真ととらえた上で、次のように考えます。

1 < 2 == 1 (正しい!!)

 かなり違和感があるかもしれませんが、まあ、その内慣れていくかと思います。因みに、なぜ0を偽、それ以外を真と呼ぶかと言いますと、コンピュータ上で何らかの操作をしたときに、その操作に失敗したのか、成功したのか、成功したのなら、その具体的な結果は何か、を表現するためです。

 例えば、文字列の長さを答えるstrlenと言う関数があるのですが、例えば、

strlen("Hello");

 と、するとこの文字列の数5を返すのですが、

strlen("");

 と、そもそも文字列が入ってないよね、みたいな異常な事態が起こったときに0を返して、おかしなことが起ってますよ、と警告する用途の為だったりします。

はい、長い前置きでしたが、実際に演算を見てみましょう。

論理積(AND): a&&b
 aとbが真の時に真
論理和(OR): a || b
 aとbどちらかが真の時に真
否定(NOT):!a
 aが真なら偽、aが偽なら真

 ちょっと、意味が分かりにくいかもしれません。ですので、みんな大好きベン図を引っ張ってくることにしましょう!


AND,OR,NOTのベン図

 ……かえって分かりにくくなった気もしますが、まあ、いいでしょう。日本語っぽく言い直すと、ANDは「かつ」、ORは「または」、NOTは「ではない」みたいな感じです。

 A and Bは「AかつB」、A or Bは「AまたはB」、not Aは「Aではない」みたいな感じです。例えば、真 and 偽は「真かつ偽」は偽になります。…かえって分かりにくかったですかね(;'∀')

  …とりあえず、コードを見てみましょう。

追記:ノットの真偽が逆なので後で訂正しますm(_ _)m

ソース

#include<stdio.h>

int main() {
	int a = 1, b = 1;

	printf("a = %d\nb = %d\n", a, b);
	printf("--------------------\n");
	printf("a && b = %d\na || b = %d\n!a = %d\n!b = %d\n", a && b, a || b, !a, !b);
	printf("--------------------\n");

	a = 1;
	b = 0;
	printf("a = %d\nb = %d\n", a, b);
	printf("--------------------\n");
	printf("a && b = %d\na || b = %d\n!a = %d\n!b = %d\n", a && b, a || b, !a, !b);
	printf("--------------------\n");

	a = 0;
	b = 1;
	printf("a = %d\nb = %d\n", a, b);
	printf("--------------------\n");
	printf("a && b = %d\na || b = %d\n!a = %d\n!b = %d\n", a && b, a || b, !a, !b);
	printf("--------------------\n");

	a = 0;
	b = 0;
	printf("a = %d\nb = %d\n", a, b);
	printf("--------------------\n");
	printf("a && b = %d\na || b = %d\n!a = %d\n!b = %d\n", a && b, a || b, !a, !b);
	printf("--------------------\n");

	return 0;
}

結果

a = 1
b = 1
--------------------
a && b = 1
a || b = 1
!a = 0
!b = 0
--------------------
a = 1
b = 0
--------------------
a && b = 0
a || b = 1
!a = 0
!b = 1
--------------------
a = 0
b = 1
--------------------
a && b = 0
a || b = 1
!a = 1
!b = 0
--------------------
a = 0
b = 0
--------------------
a && b = 0
a || b = 0
!a = 1
!b = 1
--------------------

 
 このソースコードでは、コンパイラが忠告をしてきます。と、いいますのも、普通は論理演算を整数型に使用したりはしません。bool型がちゃんと用意されていて、こちらが使用されます。しかし、後々やる条件分岐やループ文で整数型を使用することもあるので、このように説明いたしました。

 実際、このコードでは論理演算ではなく、ビット演算を使用している模様です。

3.条件演算

 条件演算は使う人は使うし、使わない人は使わない演算となります。と、いいますのも、この条件演算、やっていることは条件分岐という方法とほぼ一緒(というか下位互換)なので、コードを書いた人の特徴が出やすい演算とも言えるかもしれませんね。

 条件演算は、真偽値を扱った演算の一つです。

条件演算:
 a ? b : c
 aが真の時はbに、aが偽の時はbを示す。

 実際に見てみましょう。

ソース

#include<stdio.h>

int main() {
	int a = 0, b = 2, c = 3;

	printf("a ? b : c = %d\n", a ? b : c);
	a = 1;
	printf("a ? b : c = %d\n", a ? b : c);

	return 0;
}

結果

a ? b : c = 3
a ? b : c = 2

 aが0の時は、aは偽と認識されて、a ? b : cはcとして処理され、aが1の時は、a ? b : cはbとして処理されます。

a==0
a ? b : c == 偽 ? b : c = c = 3

a==1
a ? b : c == 真 ? b : c = b = 2

4.比較演算

 比較演算は演算と言っていいのか微妙な所ですが、一応演算として扱えます。文字通り、複数の数字の比較を行いその結果を真偽値として表現する演算方法です。

小なり:<
 A < B
AがBより小さければ真、大きければ偽(AとBが同じなら偽)
小なりイコール:<=
A <= B
AがBより小さければ真、大きければ偽(AとBが同じなら真)
大なり:>
A > B
AがBより大きければ真、小さければ偽(AとBが同じなら偽)
大なりイコール:>=
A >= B
AがBより大きければ真、小さければ偽(AとBが同じなら真)
イコール:==
A == B
AがBと同じなれば真、そうでなければ偽
ノットイコール:!=
A != B
AがBと同じなら偽、そうでなければ真

 この辺りのお話はやっぱり、自分で使ってみて確認した方が分かりやすいかと思います。ですので、コードにいってみよー。

ソース

#include<stdio.h>

int main() {
	int A = 1, B = 1, C = 1, D = 2;
	//AとBは同じ、CとDは違う

	printf("A = %d B = %d C = %d D = %d\n", A, B, C, D);
	printf("0が偽で1が真です\n");
	printf("A <  B = %d\n", A < B);
	printf("A <= B = %d\n", A <= B);
	printf("A >  B = %d\n", A > B);
	printf("A >= B = %d\n", A >= B);
	printf("A == B = %d\n", A == B);
	printf("A != B = %d\n", A != B);
	printf("----------\n");

	printf("C <  D = %d\n", C < D);
	printf("C <= D = %d\n", C <= D);
	printf("C >  D = %d\n", C > D);
	printf("C >= D = %d\n", C >= D);
	printf("C == D = %d\n", C == D);
	printf("C != D = %d\n", C != D);

	return 0;
}

結果

A = 1 B = 1 C = 1 D = 2
0が偽で1が真です
A <  B = 0
A <= B = 1
A >  B = 0
A >= B = 1
A == B = 1
A != B = 0
----------
C <  D = 1
C <= D = 1
C >  D = 0
C >= D = 0
C == D = 0
C != D = 1

 ……、説明、した方が良いですかね。
 えっと、取り合えず具体的な数字を入れて確認してみましょう。

(A < B) == (1 < 1) == 偽(0)         :おかしい式
(A <= B) == (1 <= 1) == 真(1)    :正しい式
(A > B) == (1 > 1) == 偽(0)         :おかしい式
(A >= B) == (1 >= 1) == 真(1)    :正しい式
(A == B) == (1 == 1) == 真(1)    :正しい式
(A != B) == (1 != 1) == 偽(0)       :おかしい式

(C < D) == (1 < 2) == 真(1)         :正しい式
(C <= D) == (1 <= 2) == 真(1)    :正しい式
(C > D) == (1 > 2) == 偽(0)         :おかしい式
(C >= D) == (1 >= 2) == 偽(0)    :おかしい式
(C == D) == (1 == 2) == 偽(0)    :おかしい式
(C != D) == (1 != 2) == 真(1)       :正しい式

 感覚としては、式の中身がおかしければ偽、正しければ真みたいな感じです!! 抽象的で申し訳ない…orz

4.ビット演算

 ご存じの方も多いでしょうが、コンピュータの世界では2進数を使って表されています。つまり、この二進数専用の演算もC言語では扱うことができます。

 が、そもそも2進数の説明をしなければいけません。あんまり詳しい話はしないで、さっくりとやっていこうと思います。

 2進数とは何か。そのを解明するため、、々調隊はネットの海へと飛び込んだ……。

 2進数とは、数字を0と1で表現する方法です。何故二進数と呼ぶのかと言うと、値が2になると1繰り上がるからです。

 通常、我々の使う数字は十になると位が一つ繰り上がりますよね。

1 2 3 4 5 6 7 8 9 
10 <- 繰り上がり

  二進数では、これを2でやります。

0 1
10 <- これが10進数で言う所の2

 言ってしまえばこれだけです。で、この進数制度には幾つか特色があります。

 十進数では、無理やり数式に直すとこんな感じになりますよね。

5920
= 5*1000 + 9*100 + 2*10 + 0*1
= 5*10^3+ 9*10^2 + 2*10^1 + 0*10^0

 で、これを二進数にも適用することで二進数をよむことができます。

10110
= 1*2^4 + 0*2^3 + 1*2^2 + 1*2^1 + 0*2^1
= 1*16 + 0*8 + 1*4 + 1*2 + 0
= 16 + 4 + 2
= 22

 こんな感じですね。

また、10進数では左に数字をずらすシフト)すると、桁が1つ上がります。これは、元々の数字に10をかけたと解釈することもできます。

5920を左にずらすと59200
=59200
=5920 * 10

 これもまた2進数も同じで、右にずらすと2倍になります。

10110を左にずらすと101100
101100
= 1*2^5 + 0*2^4 + 1*2^3 + 1*2^2 + 0*2^1 + 0*2^0
= 32 + 8 + 4
= 44

 同じように右にずらせば、1/10、1/2になります。

 で、本編ですが、この二進数の人桁ずつに対して論理演算を行う計算をビット演算と言います。具体的には、AND、OR、NOTを数字に対して行うことができます。

 ブール代数学では、この論理演算を普通の演算子になぞらえて表現したりもします。+をOR、*をANDで表現する、結構不思議な表現方法ですね。ただ、直観的には結構分かりやすかったりもします。

 足し算では、1+1は一桁繰り上がって、10で、上の桁を切り捨てると0になります。0+1、1+0は1に、0+0は0として考えます。
 掛け算では、1*1はそのまま1、1*0、0*1は0になり、0*0も勿論0になります。この関係が不思議とORとANDに対応するのです。

  ORは+、ANDは*と覚えると良いかもしれません。

 この論理演算をマトリックスとして表現すると、以下の図になります。

論理演算

 実際に適当な値を利用してやって見ましょう。

  1011001     1011001     
+ 0110010    *0110010     !0110010
---------    --------     --------
  1111011     0010000      1001101

 こんな感じです。10進数に直して意味のある数字になったりすることもないで、こういうもんだと思って頂ければ幸いです。デジタル回路などの分野では、このAND、OR、NOTを利用して計算機やら、メモリやらを構成する手法があったりします。

 で、このビット演算は次の演算子で表現されます。

AND:A&B
 AとBのAND演算を行います。
OR:A|B
 AとBのOR演算を行います。
左シフト:A<<B
 2進数としてAを左にB桁ずらします。
右シフト:A>>B
 二進数としてAを右にB桁ずらします。

 では、実際にプログラムを見てみましょう。

コード

#include<stdio.h>

int main() {
	int a = 12, b = 10;// a = 1100, b = 1010

	printf("%d\n", a & b); // 1100 & 1010 = 1000   (8)
	printf("%d\n", a | b); // 1100 | 1010 = 1110  (14)
	printf("%d\n",a >> 2); // 1100 >> 2   = 11     (3)
	printf("%d\n",a << 2); // 1100 << 2   = 110000(48)

	return 0;
}

ソース

8
14
3
48

 普段はあんまり使い道がないので気にする必要はないかもしれません。ただ、時たま一つの型に複数の情報を詰め込むときや、数字でフラグ管理を行うときなどこの演算を利用したりします。

 例えば、 WS_OVERLAPPEDWINDOW | WS_VISIBLEとかですかね。左右両方も文字の実体数字で、この数字の論理演算の結果を利用して、関数の動作をコントロールしたりもします。

5.演算の優先順位

 普通の計算と同じように、C言語でも演算ごとの優先順位があります。基本的な演算順序は、算術演算、比較演算、ビット演算、論理演算、代入になります。

 まずは計算を行い、その結果を比較して論理値に変換、その結果を比較してます。代入が最後に来る理由は、計算が終わっていなけらば計算が出来ないからですね。

 もしも自分の思い通りに計算が行かないときは、()で囲んでください。()の優先順位は一番強いので、優先順位を任意で決めることができます。

6.まとめ

 えーと、嫌に長くなってしまい申し訳ございませんでした。m(__)m
 
 基本的には、+、-、*、/、%、=、==,<、>、この辺の記号の意味が分かっていれば大丈夫です。

 自分でも1万文字超えるとは思わなんだ……。

次回の記事

この記事が気に入ったらサポートをしてみませんか?