3日目 5日目

4日目

目標

宿題を完成させ、今まで学習したことの理解を深める

課題 3 の続き

解答を見ていて気付いたこと

以下に解答を見ていて気付いた点をあげる。 たまたま今回、自分の解答が該当していなくても、C 言語を習得する上で大切なものも含まれるので気をつけること。

前回の宿題の解答例

あくまで解答「例」であり、この通りである必要はない。

問題 3-1

年が変数 year に入っているものとして、 2月の日数を求めるような関数 february を考えてみよ。

授業の際に、言っていたように、閏年かどうかを leap という変数に記憶することにする。

とすることにすれば、目標の2月の日数は 28 + leap で表されることになる。

日数は整数なので、関数の型は int であり、 引数(year)の型も int であることから 関数宣言はつぎのように書ける。

int february(int);

次に、条件文を順番に C 言語で表現してみる。 西暦年数は引数 year として与えられるものとする。

  1. 西暦年数が4で割りきれなければ平年

    「4で割りきれない」というのが、「4で割った余りが0に等しくない」事に気付けば、簡単である。 year を 4で割った余りは、剰余演算子 % を用いれば year % 4 と書ける。 「等しくない」は演算子 != であった。 またこの条件が成立すれば、平年すなわち変数leap を 0 にするのであるから、 上の文章は C 言語で表すと

         if ( (year % 4) != 0 ) {
              leap = 0;
         }
     

    と書けることになる。

  2. 西暦年数が400で割りきれると閏年

    今度は割りきれる場合である。 したがって、先ほど使用した演算子 != ではなく、== を使う("="が2つである事に注意!)必要がある以外は、それほど第1の条件と変わらない。

    問題文には「そうではなくて」と明記されていないが、日本語として、条件 1 が 成立しない場合に追加する条件が提示されているのであるから、else ifを使用する。 また、実際に、4で割りきれず、400で割りきれる数は存在しない。

         else if ( (year % 400) == 0 ) {
              leap = 1;
         }
      
  3. 西暦年数が400では割りきれず、100で割りきれると平年

    さらに条件が続くが、これまた、1 と 2 が成立する場合以外であるから、 引き続き、else ifを用いる。また、「西暦年数が400では割りきれず」は 2番目の条件に成立しない場合のみしか、この else ifには該当しないので あえて表現する必要はない。(表現しても間違いではないが冗長である。)

       else if ( (year % 100) == 0) {
             leap = 0;
       }
      
  4. 残りは閏年

    「残りは」という事なので、上の3条件に合致しなかったものは無条件に うるう年と言うことであるから、elseを用いて表現する。

        else {
          leap = 1;
        }
      

以上と、最初に書いた2月の日数は 28 + leap と表されることを一緒にすれば、 与えられた年の 2月の日数を返す関数 february は次のように書くことができる。

int february(int year) {
     int leap = 0;

     if ((year %  4) != 0) {
         leap = 0;
     } else if ((year % 400) == 0) {
         leap = 1;
     } else if ((year % 100) == 0) {
         leap = 0;
     } else {
         leap = 1;
     }

     return (28 + leap);
}

条件をまとめたいという欲求にかられた人は、こんな風に書いているかも知れない。

int feburary(int year) {
    int leap = 0;

    if ((year % 4) != 0 || ((year % 400) != 0 && (year % 100) == 0)) {
        leap = 0;
    } else {
        leap = 1;
    }

    return (28 + leap);
}

問題 3-2

曜日を計算するという行為は、ある1つの関数にまとめた方がよいと考えられるので、 最初に曜日を計算する過程を関数として表現することを考えよう。

この関数を youbi_keisan と名付けることにすると、この関数の引数は「年」、「月」という整数が2つである。 曜日も、整数で返すので、関数宣言は次のようになる。

int youbi_keisan(int, int);

次に関数の本体部分である。 最初に関数の引数や、内部で使用する変数の宣言を行う。 文章に出てくる、ym をそのまま関数の引数としても良いが、 ここでは別に用意することとし、引数は、year, month として受けることにした。 内部で使用する変数は、文章を良く読むと3つ (y, m, d) あるので、つぎのように書ける。

  int youbi_keisan(int year, int month) {
      int y, m, d;

同じ型の変数を複数宣言するときには、, (カンマ)で区切って並べて宣言することができる。

では、順に、書かれた文章を C 言語に翻訳してみよう。

  1. 整数変数 y に年を代入する
  2. 整数変数 m に月を代入する

    年は変数 year に、月は month に入っているのであった。 両辺とも変数だが、どちらの値をどちらの変数にいれるのかに気をつければ、次のように表現できる。

       y = year;    /* (1) */
       m = month;   /* (2) */
      
  3. y から 1600を引く

    細かに日本語を補えば、「y から 1600 を引いた値を y に代入する」という事である。 (y に 2000 が入っていたとして、この文章を行ったあと、y には何が入っていると考える?)

    したがって、この文章は、つぎのように書ける。

       y = y - 1600;
      
  4. 次のような条件に基づく処理を行う。
  5. 条件処理であるから、if を使うのはすぐ想像がつくであろう。 「m が 1 または 2の場合」とは、「m が 1の場合 または m が 2の場合」であることや、 1つ前と同様、「m に 9 を足し」とは、「m に9 を足したものを m に代入し」の意味である事が判れば 文章をそのまま C 言語に直すと次のようになる。

       if (m == 1 || m == 2) {
           m = m + 9;
           y = y - 1;
       } else {
           m = m - 3;
       }
     

    等しいかどうかを判定するのは、== と "=" を2つ並べるのであるが、1つしか書かないという間違いは良くしてしまうので注意。

  6. 整数変数 d に(365.25*y)を代入する
  7. d から (y/100) を引く
  8. d に (y/400) を加える
  9. さらに d に 30.6*m + 3.5を加える

    そのまま。順番にひとつづつ書いていくと次のようになる。

        d = 365.25 * y;
        d = d - (y/100);
        d = d + (y/400);
        d = d + 30.6*m + 3.5;
     
  10. 得られた d を7で割った余りは、上で決めたルールにしたがった曜日を表す数字である。

    d を 7 で割った余りは d % 7 であるから、その値を return 文に渡してやり、完成。

         return (d % 7);
     }
     

全部まとめると、youbi_keisan は次のようになる。

int youbi_keisan(int year, int month) {
    int y, m, d;

    y = year;
    m = month;

    y = y - 1600;

    if (m == 1 || m == 2) {
        m = m + 9;
	y = y - 1;
    } else {
        m = m - 3;
    }

    d = 365.25 * y;
    d = d - (y/100);
    d = d + (y/400);
    d = d + 30.6*m + 3.5;

    return (d % 7);
}

さて、3-1 の feburary と、youbi_keisan を合わせると、calendar 関数を年、月 を引数に呼べるようになる。 mycal5.cmycal6.cという名前でコピーして書き直し作業を行おう。 最初に calendar 関数の前の部分のコメントを修正しよう。 第1引数は、西暦で表した年、第2引数は月とする。

もとの引数 youbi, nissu は、内部で使う変数として宣言しなおし、引数を year, month とすることにしよう。 そして、kaigyo = (7 - youbi) % 7; の文より前で、 year と month から、youbi と nissu を計算してやれば、修正は終りである。

  1. nissu

    nissuは、月に含まれる日の数であった。 したがって、2月以外は「決まっている」ので配列から読み出せばよいし、2月の場合は関数 feburary を使えば良い。 ただし、配列名と変数名は同じにはできないので、前回作成したコードの配列名を少し変更して利用することにする。

        int nissu_hyo[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
    
        if (month != 2) {
            nissu = nissu_hyo[month];
        } else {
            nissu = feburary(year);
        }
      
  2. youbi

    とっても簡単である。上で作った youbi_keisan関数を呼び出すだけで良い。

        youbi = youbi_keisan(year, month);
      

結局、mycal6.c としての解答例は次のようなものになる。 存在しない月はエラーとしたり、先頭部分に何月のカレンダか表示したり、曜日を表すヘッダをつけたりしてみた。 また、mainでの calendar の呼び出し部分もきちんと修正すること。

/* mycal6.c */
#include <stdio.h>

int feburary(int);
int youbi_keisan(int, int);
int calendar(int, int);

int main()
{
	calendar(2006, 11);
	printf("\n");
	calendar(2006, 12);
	printf("\n");
	calendar(2007, 1);
	printf("\n");
}

/*
 * 与えられた年の2月の日数を返す。
 */

int feburary(int year) {
    int leap = 0;

    if ((year % 4) != 0 || ((year % 400) != 0 && (year % 100) == 0)) {
        leap = 0;
    } else {
        leap = 1;
    }

    return (28 + leap);
}

/*
 * 与えられた年月の最初の日の曜日を計算する
 */
int youbi_keisan(int year, int month) {
    int y, m, d;

    y = year;
    m = month;

    y = y - 1600;

    if (m == 1 || m == 2) {
        m = m + 9;
	y = y - 1;
    } else {
        m = m - 3;
    }

    d = 365.25 * y;
    d = d - (y/100);
    d = d + (y/400);
    d = d + 30.6*m + 3.5;

    return (d % 7);
}

/*
 * カレンダを表示する。
 * 第1引数は西暦で表した年、第2引数は月

 * 曜日は次のルールで数字に置き換える
 * 日曜日 → 0
 * 月曜日 → 1
 * ...
 * 土曜日 → 6
 */

int calendar(int year, int month)
{
	int i;
	int kaigyo; /* 最初の土曜日を7で割った余 == 改行条件*/
	int youbi, nissu;  /* youbi: 1日の曜日、
			    * nissu: その月の最後の日 */

	int nissu_hyo[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};

	if (month != 2) {
		nissu = nissu_hyo[month];
	} else {
		nissu = feburary(year);
	}

	youbi = youbi_keisan(year, month);

	kaigyo = (7 - youbi) % 7;

	if (month > 12 || month < 1) {
		/* そんな月は存在しない */
		printf("Illegal month %d\n", month);
		return (1);  /* カレンダを書けなかった (エラー) */
	}

	/* いつのカレンダなのかを表示 */
	printf("        %4d/%02d\n", year, month);

	/* 簡単に何曜日かを判るようにヘッダを表示 */
	printf("  S  M Tu  W Th  F  S\n");


	/* 月始めの「空白」 */
	for (i = 0; i < youbi; i = i + 1) {
		printf("   ");
	}

	/* 日付の表示 */
	for (i = 1; i <= nissu; i = i + 1) {
		printf(" %2d", i);

		/* 「土曜日」ならば、改行する */
		if ((i % 7) == kaigyo) {
			printf("\n");
		}
	}

	/* 最後には改行 */
	printf("\n");

	return (0);  /* カレンダ表示が成功したことを意味する */

}


問題 3-3

変数に型があったように、 プログラム中に出現するデータにも型が存在する。 同じ型の計算 (int と int など) では問題がないが、異なる型の場合にはルールがなければ計算結果が異なってしまう。 C では、演算を行う場合には、精度の高い方に揃えて演算が行われる。

例えば、次のようなプログラムを考えよう。

   int i, j, k;
   
   i = 5;
   j = 2;
   k = i/j + i/j

   printf("%d\n", k);

このプログラムでは i, jの両変数とも int 型であるから 変数 k への代入文はすべて整数で演算され printf は 4 と表示する。 (i/j = 5/2 = 2 である)

もし、j が double 型であったらどうなるだろうか。 この場合、 i/j の演算が、int型/double型 であるため、自動的に double型/double型 となるように読替え(cast, 型変換)が行われる。 その結果、i/j は double 型で計算されるため 2.5 となる。 k への代入の左辺は double 型同士の足し算であるため、 2.5 + 2.5 = 5.0 となり、その結果が int 型の変数 k に代入される。 結果的に、j を double 型にすると、printf は 5 と表示することになる。

   d = (365.25*y) - (y/100) + (y/400) + (30.6*m + 3.5);

とまとめて書くと、第1項(365.25*y)は実数で計算される。 第2項(y/100), 第3項(y/400) の計算は整数で行われるが、 第4項(30.6*m+3.5) は実数で計算される。 各項が計算されたあと、最後の加減は実数で行われることになる。

このように、5 から 8 をまとめて行うと、一部の計算が浮動小数点で行われるため 繰り上がりが発生する可能性があることになる。

なお、あらわに

 (型名) 変換したいもの

という形式で書くことで型変換を行うことができる。

たとえば、上記の計算をまとめて行いたい場合には次のような形式を用いればよい。

   d = (int)(365.25*y) - (y/100) + (y/400) + (int)(30.6*m + 3.5);

このようにすることで、各項の計算が終わった時点ですべて int 型に変換されるので、 まとめて行っても繰り上がりが発生することはない。


新しい演算子

今日書いてきたプログラムでも良く出てきたのが、「ある変数に XX という操作を行った結果を、元の変数に代入する」という操作である。 (例: y = y - 1600; m = m - 3; 等) 今日の例題が特別なわけではなく、プログラムを書く場合に、このような操作は、よく必要となる。 そのため、C 言語ではこのような場合に利用できる特別な演算子「代入演算子」が存在する。 今までに出てきた算術演算子 「+-*/%」に対して、それぞれ次のような代入演算子がある

演算子 名称 使用例 意味
  *=   乗法代入演算子 a *= 5 a = a * 5
/= 商法代入演算子 a /= 6 a = a / 6
%= 剰余代入演算子 a %= 6 a = a % 6
+ 加法代入演算子 a += 400 a = a + 400
-= 減法代入演算子 y -= 1600 y = y - 1600

また、ある変数の値を1だけ増やす(減らす)という操作は、さらに良く行われるため これらにもインクリメント(デクリメント)演算子とよばれる特別な演算子が存在する。 変数 a の値を 1 増やす場合には a++ 等と利用する。 for文で

for ( i = 0; i < 3; ++i) {
...

等のように良く使われているのを目にすることができる。


3日目 表紙 5日目

tacha@tack.fukui-med.ac.jp
$Id: 04.html,v 1.1 2006/11/16 16:52:38 tacha Exp $