演算を行う


コンピュータのことを「計算機」と呼ぶことがあります。計算機といわれると私なんかは電卓を思い浮かべてしまいますが、たしかに俗にいう重いソフトなんかを使っていると「なんか計算しとるんやろう」という気分にさせてくれます。今回はこの「コンピュータが計算する」ことについて考えてみます。


計算機の計算

 現在、私たちの使っているコンピュータを電子計算機と考えると、直接の先祖にあたるのが1946に完成した「ENIAC」だと言われています。第2次世界大戦中に開発の始まったこの電子計算機は、砲弾の弾道計算を行うために、高速に微分方程式の数値解を求める事が直接の開発目的でした。当時すでに同様の目的で開発された機械式デジタル計算機が数台存在していましたが、ENIACは動作の論理構造を同じくするこれらの従兄弟達よりもはるかに高速に演算を行えたため、これ以降は電子式デジタル計算機の開発が主流になりました。
 分かりやすい簡単なイメージでいうと「30トンの関数電卓」なENIACは、様々な方程式を数値演算によって解くことができました。解を求める為の計算の過程で、加減乗除として知られる四則演算を多く用いる必要があります。このためENIACは、我々が普段「計算」というイメージで捉えている算術演算を行う能力を有していました。例えば、
(3 - 1)×(4 / 3)
といったような計算を行えたわけです。
 まさに「計算機」として出発したコンピュータですが、算術演算を行えるだけではなく、条件判断を演算として定式化した「論理演算」という計算も行うことができます。例えば、

のように、いわゆる「場合分け」を含む式に関して計算を行おうとするときに、算術演算と論理演算を組み合わせて処理できます。
 C言語は関数型言語なので、あらゆる処理を式の計算として扱うのが基本になります。ただ、計算といっても四則(算術)演算だけではなく、普段の生活ではあまり馴染みのない論理演算も含めて計算と考えます。この概念を上手く扱えるようになると、プログラミングの幅が大きく広がります。今回はこうした各種演算について考えてみます。

Table.1:算術演算子

-A 符号 反転
A+B
A-B
A*B
A/B
A%B

List.1:list1.c


#include <stdio.h>

int main()
{
	int a[8];
	double b[8];

	a[0] = 1 + 1;
	a[1] = 4 - 3;
	a[2] = 2 * 3;
	a[3] = 6 / 3;
	a[4] = 7 % 4;
	a[5] = -a[0];
	a[6] = (3 - 1) * (4 / 3);
	a[7] = 4 / 3;

	b[0] = 1.0 + 1.0;
	b[1] = 4.0 - 3.0;
	b[2] = 2.0 * 3.0;
	b[3] = 6.0 / 3.0;
	b[4] = 7 % 4;
	b[5] = -b[0];
	b[6] = (3.0 - 1.0) * (4.0 / 3.0);
	b[7] = 4.0 / 3.0;

	printf("1 + 1 is %d\n", a[0]);
	printf("4 - 3 is %d\n", a[1]);
	printf("2 * 3 is %d\n", a[2]);
	printf("6 / 3 is %d\n", a[3]);
	printf("7 %% 4 is %d\n", a[4]);
	printf("-(1 + 1) is %d\n", a[5]);
	printf("(3 - 1) * (4 / 3) is %d\n", a[6]);
	printf("4 / 3 is %d\n", a[7]);

	printf("1.0 + 1.0 is %f\n", b[0]);
	printf("4.0 - 3.0 is %f\n", b[1]);
	printf("2.0 * 3.0 is %f\n", b[2]);
	printf("6.0 / 3.0 is %f\n", b[3]);
	printf("7.0 %% 4.0 is %f\n", b[4]);
	printf("-(1.0 + 1.0) is %f\n", b[5]);
	printf("(3.0 - 1.0) * (4.0 / 3.0) is %f\n", b[6]);
	printf("4.0 / 3.0 is %f\n", b[7]);

	return 0;
}

算術演算

 算術演算については、既にみなさんに十分に馴染み深い計算方法ですので、言葉を重ねるより、実際のC言語による表現を見てもらった方がよいでしょう。まず、list.1をコンパイルし実行してから、table.1を参考にlist.1を見て下さい。各種算術演算を行って、計算結果を変数に代入しています。C言語では、掛け算を表す演算子が「×」ではなく「*」、割り算を表す演算子が「÷」ではなく「/」、割り算の余りを求める演算子が「%」であること以外は通常の数式と同じように式が表現できます。カッコ付きの式も書くことができます。
 list.1で注意して見て欲しいのは、「4 / 3」と「4.0 / 3.0」の計算結果が異なる点です。これは、変数に型があるように、定数値にも型が存在することに起因しています。数値4を表現するとき、「4」と書くと整数型になり、「4.0」と書くと浮動小数点型になります。浮動小数点型の数値を演算するプログラムを作成するときは、この点を忘れると計算結果が変わってきますので、注意が必要です。

演習:list.1を参考にlist.2に書き足して、
width * height * (5 + 5 + 5 + 1) / 8
を計算して、mem_sizeに代入するプログラムを完成させよ。

List.2:list2.c


#include <stdio.h>

int main()
{
	int width;
	int height;
	int mem_size;

	width = 640;
	height = 480;

/* ここから */

/* ここまでに書く */

	printf("width * height * (5 + 5 + 5 + 1) / 8 is %d\n", mem_size);

	return 0;
}

Table.2:真偽値計算

!A 真偽 反転
A==B AとBが しければ
A!=B AとBが しくなければ
A<B,A>B,A<=B,A>=B AとBの 大小 比較 して、 条件 たしていれば
A&&B 論理
A||B 論理

論理演算

 論理演算と言われると、はじめて耳にされる方には、まったくイメージが湧かないと思います。論理演算は、数学における集合の概念を応用して、真偽値を評価しようとする演算です。例えば式に関して考えてみると
1 + 1
という式は、2という値(演算結果)を持ちますが、これと同様に、
1 < 2
という式は正しいから真、
1 > 2
という式は間違っているから偽、それぞれの値を持つというふうに考え、こうした式の真偽値計算を行うのが論理演算です。
 式の真偽値評価に用いる主な演算子をtable.2にまとめていますので、順に見ていきましょう。
 単項演算子「!」は算術単項演算子「-」と同じようなイメージで、真偽値を反転します。いわば「〜ではない」といった意味を持ちます。
 「==」は比較演算子で「AとBは同じである」という意味です。数学の教科書の中に出てくる「=」と同じ意味なのですが、C言語では代入操作を表す記号(演算子)として「=」が使われているので「==」になっています。C言語の文法上、「=」と「==」を間違えて用いていてもコンパイルできてしまうので、慣れないうちは比較演算子の意味で「=」と書いてしまうことが多いので、注意して下さい。
 「!=」は「AとBは同じでない」という意味で、数学では「≠」と表記されます。
 「<」、「>」、「<=」(数学では「≦」)、「>=」(数学では「≧」)、といった比較演算子群は、AとBの大小関係を評価します。

演習:次の式の真偽値を求めよ。
1. 30 > 15
2. 15 == (7 + 8)
3. !(40 != (4 * 10))

 また標準的なC言語の処理系では、真偽値を整数型の値として扱えるようになっています。真が1、偽が0という値をとるのが一般的です。list.3では、真偽値をint型変数に代入しています。逆に、数値を真偽値として(無理矢理)評価することも可能で、ほとんどのC言語処理系において、0以外を真、0を偽として評価が行われます。

List.3:list3.c


#include <stdio.h>

int main()
{
	int a[7];

	a[0] = 1 < 2;
	a[1] = 1 > 2;
	a[2] = 2 == (1 + 1);
	a[3] = !a[2];

	a[4] = a[0] && a[1];
	a[5] = a[0] && a[2];
	a[6] = a[0] || a[1];

	printf("1 < 2 is %d\n", a[0]);
	printf("1 > 2 is %d\n", a[1]);
	printf("2 == (1 + 1) is %d\n", a[2]);
	printf("!(2 == (1 + 1)) is %d\n", a[3]);

	printf("(1 < 2) && (1 > 2) is %d\n", a[4]);
	printf("(1 < 2) && (2 == (1 + 1)) is %d\n", a[5]);
	printf("(1 < 2) || (1 > 2) is %d\n", a[6]);

	return 0;
}

論理積
 真偽値の評価に関する演算で特に重要視されるものの1つが「論理積(and)」と呼ばれる演算です。論理積は2つの真偽値(もしくは真偽値評価される式)A、Bがあった場合、その両方が真の時のみ、自身の評価値を真にします。例えば、
(1 > 0) && (3 > 2)
の式は、「&&」で結ばれた2つの項が共に真であるので、式全体の評価も真になりますが、
(1 > 0) && (3 < 2)
(1 < 0) && (3 > 2)
(1 < 0) && (3 < 2)
といった式の式全体の評価は、すべて偽になります。
 論理積「&&」は、数学の集合の概念における「AかつB」という意味を表現しています。

演習:次の式の真偽値を求めよ。
4. (10 > 15) && (1 == (2 - 1))
5. (15 == (30 / 2)) && (!(4 != (44 % 8)))

論理和
 論理積と同様に、真偽値の評価に関する演算で重要視されているのが「論理和(or)」と呼ばれる演算です。論理和は2つの真偽値(もしくは真偽値評価される式)A、Bがあった場合、その何れか一方が真であれば、自身の評価値を真にします。例えば、
(1 > 0) || (3 > 2)
(1 > 0) || (3 < 2)
(1 < 0) || (3 > 2)
といった式は、「||」で結ばれた2つの項の何れか一方(もしくは両方)が真であるので、式全体の評価も真になりますが、
(1 < 0) && (3 < 2)
の式は、2つの項が両方とも偽なので、式全体の評価も偽になります。
 論理和「||」は、数学の集合の概念における「AもしくはB」という意味を表現しています。

演習:次の式の真偽値を求めよ。
6. (!(30 > 15)) || (1 == (2 - 1))
また、list.3を参考に、1〜6の式の真偽値計算を行うプログラムを作成せよ。

Table.3:ビット演算

~A ビット 反転
A>>B シフト
A<<B シフト
A&B 論理
A|B 論理
A^B 排他的 論理

List.4:list4.c


#include <stdio.h>

int main()
{
	unsigned char a[5];

	a[0] = ~(0xf0);
	a[1] = 0x3c >> 2;
	a[2] = 0x7c & 0xa8;
	a[3] = 0xf0 | 0x0f;
	a[4] = 0xff ^ 0xf1;

	printf("~(0xf0) is 0x%02x\n", ~(0x96));
	printf("(unsigned char)~(0xf0) is 0x%02x\n", (unsigned char)~(0x96));
	printf("a[0] = ~(0xf0) is 0x%02x\n", a[0]);
	printf("a[1] = 0x3c >> 2 is 0x%02x\n", a[1]);
	printf("a[2] = 0x7c & 0xa8 is 0x%02x\n", a[2]);
	printf("a[3] = 0xf0 | 0x0f is 0x%02x\n", a[3]);
	printf("a[4] = 0xff ^ 0xf1 is 0x%02x\n", a[4]);

	return 0;
}

ビット演算

 C言語では、式の真偽値計算に関して論理積や論理和が使えるだけでなく、データをビット単位で扱う場合にも論理積や論理和が使えます。1ビットで表現できるのは1か0なので、これを真偽値に見立てると、1バイトは8個の真偽値の集合体と考えることができます。そう考えると、コンピュータ雑誌をよく読まれる方なんかには「ビット論理演算は真偽値計算のパイプライン処理だ」と例えると、イメージが伝わりやすいかも知れませんね。
 ビット演算に用いる主な演算子をtable.3にまとめていますので、list.4とその実行結果と一緒に、順に見ていきましょう。
 単項演算子「~」は単項演算子「-」や「!」と同じようなイメージで、対象となるデータの0、1の状態を反転します。2進数の各桁に対して「!」を適用します。つまり、
10010110 (0x96)

01101001 (0x69)
となるわけです。この時、気をつけなければならないのが、データのビット長です。例えば、ほとんどの処理系で
~(0x96)
は、0x69 になりません。これは、何も指定しなければ、定数値のデータ長がint型のデータ長になってしまい、ほとんどの処理系でint型のデータ長は8ビットではないためです。したがって、int型のデータ長が32ビットの場合、0xffffff69になってしまいます。演算結果として 0x69 を得たい場合は、キャストと呼ばれる仕組みを使って、
(unsigned char)~(0x96)
と表記します。
 「>>」、「<<」はシフト演算子と呼ばれ、強制的に桁(当然、2進数における桁です)を上げたり、下げたりします。例えば、「0x96 >> 2」は強制的に2桁下げるので、
10010110 (0x96) → 00100101 (0x25)
となります。シフトでは、失われていく桁は消えてなくなってしまいます。

演習:2の累乗で表現される数で掛けたり割ったりする場合、シフト演算子を用いた式に置き換えることができる。どのようにすればよいか考察せよ。

 ビット演算によって得られる値は、真偽値ではないことに注意して下さい。あくまでも真偽値の集合体とみなすことのできる整数値です。

論理積
 ビット演算における論理積は、対応する各桁毎に論理積を求めます。例えば、「0x7f & 0xba」を計算すると、

01111111 (0x7f)
& 10111010 (0xba)
----------
00111010 (0x3a)

となります。実際に手で計算する場合は、上記のように2進数の筆算の形で行うとよいでしょう。
 ビット論理積の用法としてよく知られているのが「マスク処理」への応用です。マスク処理とは、ビット列上の特定の部分だけを取り出したい場合に、フィルターで濾すようなイメージの処理を行うというものです。例えば、1バイト長のデータの下4ビットのみを取り出したい場合は、0x0fというマスク(フィルター)を用いてビット論理積を行います。

stuvwxyz ( s 〜 z は、任意の0もしくは1)
& 00001111 (0x0f)
----------
0000wxyz

すると、どのようなデータでも、下4桁のみを取り出す事ができます。逆に考えれば、特定のビットを0にしたい場合は、0にしたいビット以外を1にしたデータでマスクをかければよいことになります。

論理和
 ビット演算における論理和は、対応する各桁毎に論理和を求めます。例えば、「0x31 | 0x65」を計算すると、

00110001 (0x31)
| 01100101 (0x65)
----------
01110101 (0x75)

となります。
 ビット論理和を用いれば、特定のビットを1にすることができます。例えば、8ビット長のビット列上の上4桁をすべて1にし、下4桁はそのままの値とする場合、0xf0で論理和をとります。

stuvwxyz ( s 〜 z は、任意の0もしくは1)
| 11110000 (0x0f)
----------
1111wxyz

Table.4:真偽値表

論理積:A&&B、A&B

A\B (1) (0)
(1) (1) (0)
(0) (0) (0)

論理和:A||B、A|B

A\B (1) (0)
(1) (1) (1)
(0) (1) (0)

排他的論理和:A^B

A\B (1) (0)
(1) (0) (1)
(0) (1) (0)

排他的論理和
 排他的論理和とは、条件付きの論理和のことで、C言語では式の真偽値評価を行う演算子はありませんが、ビット演算を行う「^」演算子が用意されています。この条件とは「2項が共に真であった場合は、真にならない」というものです。table.4に論理演算についてまとめてありますので、論理和と排他的論理和を比べれば、違いは一目瞭然だと思います。実際に計算してみると、例えば「0x31 ^ 0x65」は、

00110001 (0x31)
^ 01100101 (0x65)
----------
01010101 (0x55)

となります。

演習:次のビット演算を計算せよ。
42 & ((0x3f >> 3) | (0x3e ^ 0xf1))

まとめ

 今回はC言語上での演算について、算術演算、真偽値評価、ビット演算を軸に、特に論理演算を中心に見てきました。
 算術演算以外の演算に関しては、馴染みのある人はそんなにおられないでしょうし、今回の話だけでは、イマイチ何に使えるのか分からなかったと思います。算術演算以外の演算の具体的な使用例については、様々なアルゴリズムの各論に踏み込むことになり、アルゴリズムを理解する為の知識なども必要になってきますので、今回は避けることにしました。
 しかし、本誌は何といってもCマガジンですから、そういった具体例は、他のページに見つけることができるはずです。興味のある分野の記事を読むときに、そこにCのソースが提示されていれば、様々な演算がどのように使われているかを注意して見てみましょう。演算の使い方は、いわばパズルの解き方みたいなものです。自分が考えもしなかった使い方を見つけることがあるかも知れません。
 次回は「手続きの流れを制御する」です。今回扱った真偽値評価に基づく、条件による手順の制御など、C言語の制御構文について解説します。

 それでは。

ソースファイル一式


コラム

ブール代数
 1854年、イギリスの George Boole は、その後の数学に一分野を築き、コンピュータの発明とは切っても切り離せない、一つの提案を行います。それは論理学と代数学を結び付け、論理を数式で扱える体系、ブール代数の提案でした。
 それまで数値を扱うための計算理論であった代数学を拡張し、真と偽の2値を持つ命題を扱う論理学を数学の世界に取り込んだのです。ブール代数によって、論理学で扱われる推論を数式で表現できるようになり、論理の演算が行えるようになりました。現在、インテリジェントに振舞うソフトウェアがたくさんありますが、最初に科学的に知性にアプローチしようとしたのがブール代数だったといえるかも知れません。
 日本の数学教育の中では、集合論として代数学教育に組み込まれています。コンピュータを支えている数学理論としては大変重要な分野の一つなのですが、我が国では(受験)数学の主流から外れていることもあり、あまり熱心に教えられてはいないようです。プログラミングを志す人には、是非とも押さえておいて欲しい知識なのですが……

魔法の呪文「1248」
 最近のC言語では、特にハードウェアに近い部分(画像や音を扱うプログラム)でビット単位でデータを扱うことが多くなっています。ビット単位でデータを扱うとなると、頭の中では2進数で考え、記述上は16進数で表記しなければなりません。この2進数⇔16進数相互変換が初心者にとっては悩みのタネになることがあるようです。
 実際にそれなりのプログラマが2進数⇔16進数相互変換をどのように行っているのかというと、可能な人は暗算なのですが、暗算があやしい人なんかは「124812481248……」と呪文を唱えたりします。電卓に頼っている人は、作業効率が悪くなるので少数派ではないでしょうか?
 「1248」の呪文は、2進数4桁が16進数1桁に完全に対応することに由来する呪文で、

2進数 0110
「1248」8421
(右から対応するビットが立ってたらその数字を足していく。この場合、2 + 4 = 6。)
16進数 6

という2進数→16進数変換処理を頭の中で行う為に、呪文を補助に使います。
逆に、16進数→2進数変換処理を行う場合は、

16進数 A
「1248」8421
(左側の数字から順に、引けるか試して、引けたら対応ビットを1にする。その次の試行では、引いた余りから引けるか試す。この場合、10 - 8 = 2、2 - 4 は引けない、2 - 2 = 0、0 - 1 は引けない。)
2進数 1010

という具合に考えます。
 仮に丸暗記しても16パターンしかないので、大した量じゃないんですが、暗算も暗記も苦手な方にオススメです。

前回の演習問題の答え
 前回、実際にプログラムを書く演習問題を出題しました。各演習問題の答えとなるソースファイルを、list2.cの答えはlist2a.cといった具合に、今月号のCD-ROMに収録してあります。分からなかったところがあった場合は確認しておいて下さい。また、list13.cの既に書いてある部分に、変数名のタイプミスが入っていました。list13a.cでは直してあるので、合わせて確認して下さい。
 さて、「両手の10本の指を使って0〜1023を数える方法」ですが、分かったでしょうか?指1本で表現できる情報容量は、指を伸ばしている場合を0、指を折ったときを1と考えると、ちょうど1ビットになります。指が10本あることを考えると、10ビット分、つまり2進数10桁で0000000000〜1111111111の数値を表現可能だと見ることができます。これは10進数に直すと0〜1023になりますので、両手の10本の指を使って0〜1023を数えることが可能になるわけです。手のひらを上に向けて、右手の親指を1番下の桁、左手の親指を1番上の桁とすると、例えば204は両手それぞれの中指、薬指を折り曲げた状態で表現できます。

前回の演習問題の解答例一式