応用理工学情報処理 7回目

(a) マクロ, 列挙体, 再帰

前半は、テキスト第8章、「いろいろなプログラムを作ってみよう」を学習します。前半のテーマは(1)関数形式マクロ、(2)列挙体、(3)再帰関数呼び出し、の3つです。

「1」関数形式マクロ

これまでに、#のついた命令がいくつか出てきました。
#include <stdio.h>
#define NUMBER 5
などがそうです。これらの命令は、文末にセミコロンが付かないという特徴があります。これらの命令はコンパイラでC言語の文法に従った構文解釈が行われる前に実行され、必要となるファイルを読み込んで挿入したり(#include)、文字を置換したり(#define)します。この処理を行うプログラムはプリプロセッサと呼ばれるため、#の付いた命令はプリプロセッサ命令と呼ばれます。

関数形式マクロはオブジェクト形式マクロ(#define NUMBER 5 といったもの)をより高機能にしたもので、置換する文字を変数のようにプログラム中で与えることができます。

#define sqr(x) ((x) * (x))
というようにプログラムの冒頭で記述した場合、プログラム中で
sqr(a+1)のような コードがあればその部分はコンパイラによって構文解析される前に
((a+1) * (a+1))
のように置換されます。上の例で、#define sqr(x) (x * x) とした場合には、置換されたとき (a+1 * a+1) となって計算結果が違ってくるので注意が必要です。関数形式マクロはコンパイラによって構文チェックが行われないのでバグになりやすい欠点がありますが、何度も呼び出す必要があるような単純な関数の場合には関数呼び出しに伴うオーバーヘッドがなくなるため僅かにスピードの向上が期待できます。

次のプログラム(テキストp.213 List 8-2)のプリプロセッサ―処理の結果を見てみましょう。通常はプリプロセッサ―で処理されたファイルは引き続きコンパイルされるため残りませんが、コンパイルする時に/Pのオプションをせっていすれば拡張子iの付いたファイルとして残ります。確認方法をオブジェクト形式マクロを説明した4回目の授業ページで説明していますので参考にして下さい。Terminalからコンパイルする必要がありますが、MSVCコンパイラを用いている場合にはファイル名をtest.cとすると、cl /P ./test.c (Enter)と入力します。。

Visual Studioの環境であれば、プロジェクトのプロパティ―ページを開いてC/C++のプリプロセッサ―を選択し、ファイルの前処理のところを「はい(/P)」に変更してコンパイルすると、プリプロセッサ―のところで処理が止まり、拡張子iのついた処理されたファイルがDebugホルダーに出来ます。そのファイルをテキストエディターで開いて関数形式マクロがどのように展開されているかを確認してください。最初の部分にはかなり長いstdio.hのファイル部分が読み込まれています。

Borland C++であれば、 CPad開発環境のメニューから、 実行=>コマンドプロンプトの起動、を選択し、コマンドプロンプトの画面で cpp32 List8-2.c とキーボードから入力してEnterキーを押して実行してみてください(このプログラムのファイル名をList8-2.cとしています)。この命令ではプリプロセッサのみが起動し、処理されたファイルがファイル名 List8-2.i という名前で保存されます。

下のプログラム例では、sqr(nx)の部分が((dx) * (dx))のように置換されます。

/*
	整数の二乗と浮動小数点数の二乗(関数形式マクロ)
*/

#include  <stdio.h>

#define	 sqr(x)		((x)  * (x))	/* xの二乗値を求める関数形式マクロ */

int main(void)
{
	int		nx;
	double	dx;

	printf("整数を入力してください:");
	scanf("%d",	 &nx);
	printf("その数の二乗は%dです。\n", sqr(nx));

	printf("実数を入力してください:");
	scanf("%lf", &dx);
	printf("その数の二乗は%fです。\n", sqr(dx));

	return (0);
}

「2」列挙体(enumeration)

列挙体を宣言すると、整数定数のリストが定義され、それらの値を取ることが出来る型が作成されます。例えば
enum animal {Dog, Cat, Monkey, Invalid};
とすると、Dog=0, Cat=1, Monkey=2, Invalid=3という値を持った定数が作成され、enum animalという列挙型が作成されます。
enum animal selected;
とすると、enum animal型の変数selectedが定義され、変数selectedはこれらの値をとることのできる整数型変数となります。
このような変数を使うことによって、プログラム中でDogやCatのような意味のはっきりした単語を意味のはっきりしない整数値の代わりに使うことができます。C言語の場合はリストで定義していない整数を列挙型に代入してもエラーとならないので注意が必要です。

次のプログラム(テキストp.220 List 8-6)で動作を確認して下さい。

/*
	選ばれた動物の鳴き声を表示
*/

#include  <stdio.h>

enum animal  { Dog, Cat, Monkey, Invalid };

/*--- 犬が鳴く ---*/
void dog(void)
{
	puts("ワンワン!!");
}

/*--- 猫が鳴く ---*/
void cat(void)
{
	puts("ニャ~オ!!");
}

/*--- 猿が鳴く ---*/
void monkey(void)
{
	puts("キッキッ!!");
}

/*--- 動物を選ぶ ---*/
enum animal select(void)
{
	int	 tmp;

	do {
		printf("0…犬 1…猫 2…猿 3…終了:");
		scanf("%d", &tmp);
	} while (tmp < Dog	||	tmp > Invalid);
	return (tmp);
}

int main(void)
{
	enum animal	 selected;

	do {
		switch (selected = select()) {
		 case Dog	 : dog();	 break;
		 case Cat	 : cat();	 break;
		 case Monkey : monkey(); break;
		}
	} while (selected != Invalid);

	return (0);
}

「3」再帰関数呼び出し

ある関数の内部からその関数自身を呼び出すことを再帰関数呼び出しといいます。次のプログラム(テキストp.225 List 8-7)は再帰呼び出しを用いた階乗を求めるプログラムです。再帰関数呼び出しの場合は関数の内部で次の呼び出しを終了する条件を明確にしておく必要があります。次のプログラムでは、引数を1ずつ小さくして再帰呼び出しを行い、引数が0となったら終了します。

/*
	階乗を求める
*/

#include <stdio.h>

/*--- 階乗値を返す ---*/
int factorial(int n)
{
	if (n > 0)
		return (n * factorial(n - 1));
	else
		return (1);
}

int main(void)
{
	int	 num;

	printf("整数を入力してください:");
	scanf("%d", &num);

	printf("その数の階乗は%dです。\n", factorial(num));

	return (0);
}

====== 演習問題 7A (p7a.c) ==============

2つの正の整数xとyの最大公約数をユークリッドの互除法によって再帰関数呼び出しを用いて求める関数
int gcd(int x, int y) { /* */ }
を作成し、キーボードから2つの整数を読み込んでその最大公約数を以下の出力例の通りに出力するプログラムを作成せよ。ユークリッドの互除法では、x > yとしたときx÷yの余りをzとして、次にy÷zの余りを求める。この処理を繰り返し、余りが0となるときの割る数(z)が最大公約数である。
=== 出力例 ===
2つの正の整数の最大公約数を求めます。
整数1:242
整数2:33
最大公約数は11です。
====================
OKの条件:再帰関数呼び出しを用いていること、2つの正の数(x > y or x < yどちらでも)を入力したときに正しく最大公約数が求められていること。

(b) 文字列

後半はテキスト第8章 8-5 入出力と文字、から第9章の文字列の基本に関して学習します。8-4「入出力と文字」では、コンピュータの中で文字が特定の数値と対応付けて扱われていることを学びます。第9章では、文字列が文字の配列として扱われることを学びます。

「1」数字文字のカウント(テキストp.230, List 8-9) 以下のプログラムをコピーして動かして見ましょう。

/*
	標準入力から読み込まれた数字文字をカウントする
*/

#include  <stdio.h>

int main(void)
{
	int	 i, ch;
	int	 cnt[10] = {0};		/* 数字文字の出現回数 */

	while (1) {				/* 無限ループ */
		ch = getchar();
		if (ch == EOF) break;

		switch (ch) {
		 case '0' :	cnt[0]++; break;
		 case '1' :	cnt[1]++; break;
		 case '2' :	cnt[2]++; break;
		 case '3' :	cnt[3]++; break;
		 case '4' :	cnt[4]++; break;
		 case '5' :	cnt[5]++; break;
		 case '6' :	cnt[6]++; break;
		 case '7' :	cnt[7]++; break;
		 case '8' :	cnt[8]++; break;
		 case '9' :	cnt[9]++; break;
		}
	}

	puts("数字文字の出現回数");
	for (i = 0; i < 10; i++)
		printf("'%d':%d\n", i, cnt[i]);

	return (0);
}

(1) getchar( )はキーボードから1文字を読み取る関数です。キーボードからCtrl+z(コントロールキーとzキーを同時に押す)を入力するとこの関数はEOFを返します。EOFとは、stdio.hのなかで、#define EOF -1 と定義された値です。C言語として-1でなければならないという決まりは無いのですが、いま使用している処理系ではそのようになっているということです。先週には、整数の最大値がINT_MAXというマクロでlimits.hヘッダーファイル中に定義されているという話がありましたが、それと同じようにヘッダーファイル中に定義されています。

(2)これまで「キーボードから入力する」という表現を使ってきましたが、「標準入力から入力する」という表現の方が正確です。デフォルトではキーボードと標準入力が結び付けられているのでキーボードから入力する、という表現を使ってきましたが、これをファイルからに切り替えることも出来ます。切り替えには、不等号の記号(<, >)を用います。不等号の向きによって、ファイルからの入力なのかファイルへの出力なのかが判断されます。


コマンドプロンプトを起動します。Visual C++ 2008ならばメニューのツールを選択して、外部ツールにコマンドプロンプトを登録しておくと良いでしょう。コマンドとして%systemroot%\system32\cmd.exeを設定して初期ディレクトリとして$(TargetDir)を設定しておくと、実行ファイルのできたディレクトリでコマンドプロンプトが起動してくれます(ただしプロジェクトを作成するときに場所をZ:\....ではなくUNC(\\disk01\...)で指定している場合はうまくいかない)。(CPad環境ならばメニューから実行=>コマンドプロンプトを起動 (Ctrl+Q)を選択する。)上記プログラムのソースコードをList8-8.c, 実行ファイルをList8-8.exeとした場合、List8-8 < List8-8.c と入力してエンターキーを押してください。中央の不等号によって、標準入力がファイルからの入力に切り替わります。
また、これまで「画面への出力」としていたことは正確には「標準出力への出力」というのが正確な表現です。デフォルトでは標準出力とモニター画面が結び付けられています。これをファイルに切り替えることも出来ます。上記のコマンド入力で、List8-8 < List8-8.c > Output.txt とすれば画面への出力がOutput.txtというファイルへの出力に切り替わります。このとき、Output.txtというファイルがもとから存在すれば上書きされ、存在しなければ新規作成されます。このように入力や出力先を切り替えることをリダイレクトと呼びます。

(3) C言語での“文字”とは、その文字に与えられたコード、すなわち整数値です。文字とコードの対応には幾つか種類がありますが、ASCII (American Standard Code for Information Interchange) コードというものが良く使われます。テキストp.232にあるJISコード表はASCIIコードを拡張したもので、0x00~0x7FまではASCIIコードと一致します。唯一の例外は、0xC5の¥マーク(JIS)とバックスラッシュ(ASCII)です。

「2」文字列の基本

(1)文字列とは、char型の配列です。char型はここの処理系では符号付1バイト整数(-128~127)です。文字列では文字の終端を何らかの方法で認識する必要があります。この終端を識別する文字として、C言語ではナル文字というものを使います。これは 数値としては0で、文字で書くと'\0'となります。文字リテラル(定数)を書くときはシングルクォーテーションで囲むのでしたね。文字列リテラル(定数)はダブルクォーテーションマークで囲みます。"abc"と書くと文字列リテラルとなります。この場合、終端に'\0'が付加されるためメモリ上では4バイトの領域が使われます。

(2)“abc”という文字列を持った配列は以下のように宣言できます。
char ss[ ] = {‘a’, ‘b’, ‘c’, ‘\0’};
char ss[ ] = “abc”;
char *ss = “abc”; (これは次の章で説明するポインターを用いた宣言)
printf( “%s\n”, ss ); で出力すると、どれも abc と出力されます。終端は\0で判別されます。

(3)文字列をキーボードから読み込むには、まず十分な領域を持つchar型の配列を宣言して(char name[40]; )
scanf("%s", name);
とします。このときnameの前に&が付かないことに注意すること。

以上のことを踏まえて、テキストp.244, List9-4のプログラムを動かしてみましょう。

/*
	名前を尋ねて挨拶(文字列の読込み)
*/

#include  <stdio.h>

int main(void)
{
	char  name[40];

	printf("お名前は:");
	scanf("%s", name);

	printf("こんにちは、%sさん!!\n", name);

	return (0);
}

「3」文字列の応用 以上を踏まえてテキストp.251, List9-10の数字文字の出現回数をカウントするプログラムがどのように動くかを調べてみましょう。

/*
	文字列内の数字文字をカウントする
*/

#include  <stdio.h>

/*--- 文字列str内に含まれる数字文字を配列cntに格納 ---*/
void str_dcount(const char str[], int cnt[])
{
	unsigned  i = 0;
	while (str[i]) {
		if (str[i] >= '0'  &&  str[i] <= '9')
			cnt[str[i] - '0']++;
		i++;
	}
}

int main(void)
{
	int	  i;
	int	  dcnt[10] = {0};
	char  str[100];

	printf("文字列を入力してください:");
	scanf("%s", str);

	str_dcount(str, dcnt);

	puts("数字文字の出現回数");
	for (i = 0; i < 10; i++)
		printf("'%d':%d\n", i, dcnt[i]);

	return (0);
}

(1)ここで作成した関数str_dcount( )は配列を引数として持つ関数です。配列の受け渡しは配列の場所、つまりアドレスが渡されるので呼び出しもとの配列の値そのものを関数内で操作することが出来ます。ここが良くわからない人は6章 関数をもう一度復習しましょう。

(2)関数内のwhile文の条件式では、文字列の終端が'\0'であることを利用しています。ここは、while (str[i] != '\0') と同じ意味になります。整数の0がFalseと同じ意味であるというC言語特有の表現ですが、表現として str[i] != '\0'と書いたほうが理解しやすいと思えばそう書いても全く問題ありません。

====== 演習問題 7B (p7b.c) ==============

ASCII (American Standard Code for Information Interchange) コード表を下記出力例に示す通りに表示するプログラムを作成してください。C言語での“文字”とは、その文字に与えられたコード、すなわち整数値です。教科書p.232にあるJISコード表で、0x00~0x7FまではASCIIコードと一致します。唯一の例外は、0x5Cの¥マーク(JIS)とバックスラッシュ(ASCII)です。この中で、0x20から0x7Fまでを出力しますが、文字としてそのまま表示出来ない0x20に対応する空白文字と0x7Fに対応する制御文字(Delete)はそれぞれ"SP"およびDEL"と表示するものとします。

=========出力例===========

===========================
OKの条件:出力例の通りの出力が得られること。文字がずれていない、空白で区切られていること。

====== 演習問題 7C (p7c.c) ==============

3回目の演習課題3Cで作成した数当てゲームを参考にして、アルファベット大文字の一つを乱数で生成してその文字を当てるゲームを作成します。課題3Cでは乱数で0から99までの整数の中の一つを生成していましたが、アルファベットは26種類であり'A'に対応するASCIIコードは0x41です。課題3Cの問題中に示した乱数生成コードに対応するものは、

#include  <stdio.h>
#include  <stdlib.h>
#include  <time.h>
int main(void)
{
	int target; // Uppercase alphabet
	
	srand(time(NULL)); /* 乱数列を現在時刻で初期化 */
	target = 'A' + rand()%26; // 26種類
	printf("生成したアルファベット:%c\n", target);

	return (0);
}
となります。また、入力に関しては関係の無い文字が入力されたときに再度入力を繰り返すように、入力した値を格納する変数をguess, 入力回数をnとした場合、
while (1) {
    printf("%d回目の入力:", n); guess = getchar();
    while (getchar() != '\n') ;
    if (!('A' <= guess && guess <= 'Z'))
        printf("--- アルファベット大文字の1文字を入力してください ---\n");
    else
        break;
} 
といったコードを含めるようにして下さい。getchar関数は1文字を読み取る関数ですが、Enterキーを押すまで入力した文字が読み取られません。それまでバッファーに入力した文字が記憶されているわけですが、最後にはEnterキーを押したときに入力される改行文字が入ります。そこでこのコードでは、while (getchar() != '\n') ;で次回の入力の前にバッファーをクリアしています。
プログラムを実行したときの出力は以下の通りにして下さい。ここで、イタリックの太字はキーボードから入力した値を示します。また、Aに近い側を前の文字、Zに近い側を後の文字とします。

---------出力例----------
AからZまでのアルファベット1文字を当てて下さい。
何回目の入力で当たるでしょうか。

1回目の入力:N
もっと前の文字です。
2回目の入力:$
--- アルファベット大文字の1文字を入力してください ---
2回目の入力:D
もっと後の文字です。
3回目の入力:G

大正解! 3回目で正解です。
===================================
OKの条件:乱数で生成するアルファベットや入力する文字や入力回数は毎回異なるが、それらを除くと上記出力例の通りの出力が得られていること。

========