■万年カレンダーを作ろう
みなさん、こんにちは。C言語講座も発展編に突入しました。
発展編ではコンピューターの内部について触れる事も多くなります。
データ型と食うメモリの関係、演算子、ポインタのポインタ等。
ムダ知識(ト○ビア)も紹介するかも知れません。
それはさておき、今回は復習と言う事で、「基礎編」で学んだ知識を活かし、
「万年カレンダー」を作ってみましょう。
こんな仕様です。
○現在の年、月を入力すると、一ヶ月分のカレンダーを表示する。 ○閏年(うるうどし)もきちんと数える。 (現在の年が、 4で割れるなら閏年で、2月は29日となる。 しかし100で割れる年は、閏年ではない。 ただし例外として、400で割れる年は、閏年である。) ○2、4、6、9、11(西向く侍)以外の月は、31日。 4、6、9、11の月は30日。 2月は閏年でないなら28日。 |
まずは自力で作ってみて下さい。
どーしても分からなかったら解答を見てください。
■解答編
さて、解答編です。
まずは、現在の年、月を入力させるプログラムを書くとしましょう。
今回は「発展編」最初と言う事で、文法講座のおさらいも兼ねています。
なので、文法の説明も加えながらの解説となります。
#include <stdio.h>
int main(){
long month,year;
printf("万年カレンダー ver 0.01\n\n\n");
printf("現在の年を入力して下さい\n");
scanf("%d",&year);
printf("\n\n現在の月を入力して下さい\n");
scanf("%d",&month);
printf("今は%d年%d月です。", year, month);
return 0;
}
|
これだけ作っても役に立たないのですが...
ステップバイステップ。一歩一歩作りましょう。
#include <stdio.h>
インクルード。括弧で囲まれたファイルを読み込みます。
stdio.h というファイルにはprintfとかscanfとかいう、
「標準入出力」の関数が含まれている事はかなり前に話したと思います。
printf("現在の年を入力して下さい\n");
scanf("%d",&year);
現在の年をキーボードから入力させます。
scanfで文字を書く事はできないので、はじめにprintfで入力させる事を書いておきます。
\nは改行の印。scanfで代入したい変数の頭に「&」をつける事を忘れないで下さい。
つまりここではメモリ上の変数yearの位置(つまり変数yearへのポインタ)を示す訳です。
printf("今は%d年%d月です。", year, month);
入力された値を表示します。
%dは、その後の引き数の値を持ってこさせる事を意味し、ここでは数をあらわします。
つまり、最初の%dにはyearの値、次の%dにはmonthの値が入る事になります。
//前のソースコードから続けて
long days,nowdays;
long uruu;
long week;
//暫定日数計算
days = 365*(year-1);
//閏年
uruu = year/4;
days += uruu;
//100で割れる年
uruu = year/100;
days -= uruu;
//400で割れる年
uruu = year/400;
days += uruu;
//現在の月までの日数計算
for (int x=1 ; x <= month-1 ; x++){
days += getdays(year, x);
}
nowdays = getdays(year, month);
//その月の始まりの曜日。1年1月1日を日曜日とする。
week = (days + 1) % 7;
printf("今%d年%d月で、初めの曜日は%dで、%d日あります。", year, month, week, nowdays);
return 0;
}
|
long days,nowdays;
long uruu;
long week;
//暫定日数計算
days = 365*(year-1);
//閏年
uruu = year/4;
days += uruu;
//100で割れる年
uruu = year/100;
days -= uruu;
//400で割れる年
uruu = year/400;
days += uruu;
|
//現在の月までの日数計算
for (int x=1 ; x <= month-1 ; x++){
days += getdays(year, x);
}
nowdays = getdays(year, month);
//その月の始まりの曜日。1年1月1日を月曜日とする。
week = (days + 1) % 7;
printf("今%d年%d月で、初めの曜日は%dで、%d日あります。", year, month, week, nowdays);
return 0;
|
int getdays (int year, int month){
if (month == 2){
//順番重要!
if (year % 400) return 29;
if (year % 100) return 28;
if (year % 4) return 29;
} else if (month == 4 || month == 6 || month == 9 || month == 11) {
return 30:
} else {
return 31;
}
}
|
■開発環境を手に入れよう
○Borland C .... Windowsで有名なコンパイラ。ただで入手可能。
○Visual C++ .... 市販のコンパイラ。Win対応。
○MPW .... フリーのMac対応コンパイラ。
Borland Cの場合、テキストエディタがないので、有名なものを紹介します。
○CPad .... コンパイラのパスを渡すとと自動でコンパイルしてくれる
○BCC developer .... 普通はこちらを使いましょう。多機能で便利。
○ボーランド帳 .... CRenさん作のエディタ。ホームページから入手可。(リンクページからどうぞ)
■メモリを節約
では、それぞれのデータ型がメモリ上でどのぐらいの位置を占めるか?
| 型名 | 使える範囲 | 食うメモリ | char | -128〜+127 | 1バイト | short | -32768〜+32767 | 2バイト | int | -2147483648〜+2147483647 | 4バイト | long | -2147483648〜+2147483647 | 4バイト |
|---|
ちなみに、floatは4バイト、doubleは8バイトです。
doubleの方がfloatよりも、小数部分にかけるビットが多いので、
doubleの方がより高精度な小数の計算ができます。
■符号付けるの付けないの
いままで変数を宣言する時、
int x; |
signed int x; |
unsigned int x; |
| 型名(unsigned) | 使える範囲 | 食うメモリ | char | 0〜255 | 1バイト | short | 0〜65,535 | 2バイト | int | 0〜4,294,967,295 | 4バイト | long | 0〜4,294,967,295 | 4バイト |
|---|
■算術演算子
今回はC言語で使う演算子を解説。
今回は表ばっか出てきます。
○2項演算子
| 演算子 | 意味 | 用法 | + | 足し算 | a = b + 5; | - | 引き算 | a = 8 - 4; | * | 掛け算 | a = 6 * b; | / | 割り算 | a = b / 3; | % | 余り | a = 7 % 4; |
|---|
○単項演算子
| 演算子 | 意味 | 用法 | ++ | インクリメント、+1 | a++; または ++a; | -- | デクリメント、-1 | b--; または --b; | - | 符号反転 | a = -b; |
|---|
○代入演算子
| 演算子 | 意味 | 用法 | = | 右辺から左辺に代入 | a = b; | += | 左辺に右辺の値を加えたものを代入 | a += 3; | -= | 左辺から右辺の値を減算したものを代入 | b -= a; | *= | 左辺に右辺の値を掛けたものを代入 | a *= 3; | /= | 左辺を右辺の値で割ったものを代入 | a /= 3; | %= | 左辺を右辺の値で割ったときの余りを代入 | b %= 7; |
|---|
■論理演算子
論理演算子は、真(1)または偽(0)を論理判断の結果を返します。
if文でも使えますし、()や代入演算子と併用すれば論理判断の結果を変数に代入したりできます。
○関係演算子
| 演算子 | 意味 | 用法 | 用法の意味 | > | 〜より大きい | a = (b > 4) | bが4より大きいならば1が、そうでなければ0がaに代入される。 | < | 〜より小さい | a = (b < 4) | bが4より小さいならば1が、そうでなければ0がaに代入される。 | >= | 〜以上 | a = (b >= 4) | bが4以上ならば1が、そうでなければ0がaに代入される。 | <= | 〜以下 | a = (b <= 4) | bが4以下ならば1が、そうでなければ0がaに代入される。 | == | 等しい | a = (b == 4) | bが4ならば1が、そうでなければ0がaに代入される。 | != | 等しくない | a = (b != 4) | bが4でないなら1が、4ならば0がaに代入される。 |
|---|
○論理演算子
| 演算子 | 意味 | 用法 | 用法の意味 | && | 且つ(AND) | a = (b == 4 && c == 5) | bが4、且つcが5の場合、aに1が代入される | || | または(OR) | a = (b < 4) | bが4、またはcが5の場合、aに1が代入される | ! | 反転(NOT) | a = !(c >= 4) | cが4以上でないなら1がaに代入される。(つまり a = (c < 4) と同じ) | <= | 〜以下 | a = (b <= 4) | bが4以下ならば1が、そうでなければ0がaに代入される。 | == | 等しい | a = (b == 4) | bが4ならば1が、そうでなければ0がaに代入される。 | != | 等しくない | a = (b != 4) | bが4でないなら1が、4ならば0がaに代入される。 |
|---|
論理演算子において、偽なら0、真なら1を示しますが、
実際は0以外なら真とみなされます。 なので、
if (a % b) {
printf ("割り切れませーん!");
}
|
こんな使い方ができてしまいます。
a % b 、例えば、a = 5、b = 3とすると、a % b = 2です。
これは0でないので、真とみなされ、printf ("割り切れませーん!");が実行されます。
■その他...
○三項演算子(条件演算子)
| 演算子 | 意味 | 用法 | 用法の意味 | ? : | 条件式 | a = ( b ? 3 : 5) | bの値が真の時、aに3が代入され、偽の時、aに5が代入される |
|---|
つまり、上の例でいうと、a = (b ? 3 : 5) は
if (b != 0){
a = 3;
}else{
a = 5;
}
? : ... 簡単な条件式の役目をはたす。 条件 ? 真の時実行 : 偽の時実行 |
○カンマ演算子
| 演算子 | 意味 | 用法 | 用法の意味 | , | 区切られた順に左から計算を実行する | a = (b = 5 , c *= 7 , d %= 3) | b = 5 , c *= 7 , d %= c の順に計算され、その結果、dの値(則ち2)がaに代入される。 |
|---|
■ビット演算子
さてここからが重要です。第1回に話したビットの話にしばらく戻ります。
では、6を2進数で表してみて下さい。(1バイトで。01010101のように8桁で。)
では、その2倍の12を2進数で表してみて下さい。
そして2つの数を縦に並べてみよう。何か気付く事はありませんか?
00000110 00001100 |
1ビット分だけ左にずらす(左にシフトするといいます)ことは、2倍する事を意味します。
nビット左にシフトすると、2のn乗倍になります。
反対にnビット右にシフトすると、2のn乗分の1倍になります。
そこで、1ビット左や右にシフトする演算子があります。
| 演算子 | 意味 | 用法 | 用法の意味 | << | ビットを左にシフト。 | a = b << 3 | bを3ビット左にずらし、その値をaに代入する。 (a = b * 8) | >> | ビットを右にシフト。 | a = b >> 3 | bを3ビット右にずらし、その値をaに代入する。 (a = b / 8) |
|---|
ビットシフトは数が大きい時、符号ありの時は要注意です。なのでビットシフトの例をもう一つ。
今度は、unsigned 204を2進数で表します。「11001100」です。
これを、signedとunsignedの場合に分けて、左と右に2ビットずつずらすという4パターンをやってみます。
ちょっと問題。11001100はsignedだといくつ?8桁目が1なので負の数。
64+8+4で、-76です。...と言いたいところですが、実は違って、
頭の符号の1は、マイナスと考えずに、「2の(ビットの数-1)乗を引く」と考える方をしているらしいです。
つまり負の場合は、数を表すビットが1が多いほど、数が大きい事になります。
だから、「どっちにしろ数を表すビットが1が多いほど、数が大きい」と覚えた方がいいです。
よって正解は、76 - 128 = -48 です。
まず、符号無しで2ビット左にシフトの場合。
一見、2つずれると「00110000」となり、48に見えますが、実際の変数で考えると、
204 * 4 = 816 で余裕でオーバーフロー(使える範囲の限界をこえる事)しているのでダメです。
符号無しで2ビット右にシフトの場合。
2つずれると「00110011」となり、51です。これは問題ナシ。
符号有りで2ビット右にシフトの場合。
2つずれると「11110011」となり、-13となってしまいます。
実は、符号ありで右にシフトすると、左に新しく入るデータは、符号ビットの値と同じものになります。
2つ右にシフトする場合、符号ビットの値が1なら「11」が、0なら「00」が入ります。これは少し特殊ですね。
○ビット代入演算子
普通の計算に代入演算子があったと同様に、ビット演算にも代入演算子が用意されています。
| 演算子 | 意味 | 用法 | 用法の意味 | <<= | ビットを左にシフト。 | a <<= 3 | aの値を3だけ左にシフトした値をaに代入する。 | >>= | ビットを右にシフト。 | a >>= b | aの値をbの値だけ右にシフトした値をaに代入する。 |
|---|
& ビットAND | ビットOR ^ ビットXOR ~ ビット反転 |
「ビットをマスク」して、ビット列の特定のビットが0か1かを調べたり、
特定のビットを1にしたりできます。
これはハード的に周辺機器コントロール用のフラグを調べたり、操作したりする場合に使われます。
具体的には、ビット演算のANDは、1ビットごとに両方1なら1、それ以外なら0にします。
ビット演算のORは、1ビットごとにどちらかが1なら1、それ以外なら0にします。
ひとつ例を挙げます。
「10010101」の下位4ビットを調べたい場合。
「00001111」とAND演算をして、上位4ビットをマスクして、「00000101」と
下位4ビットの値がそのまま出てきました。このようにして特定の場所のビットの値を取り出す事ができます。
また「10010101」の上位4ビットを1にしたい場合。
「11110000」とOR演算をして、上位4ビットが必ず1111になるようにします。
すると結果は「11110101」と見事に上位4ビットを1111にする事ができました。
AND、ORの他に、XORというものも存在します。「排他的論理和」ってやつです。
どうなるかというと、どちらか一方が1なら1になります。
両方1だったり、両方0だったりすると0になります。
○ビット代入演算子(その2)
ビットごとの論理演算にも代入演算子が有ります。
| 演算子 | 意味 | 用法 | 用法の意味 | &= | ビットごとのAND | a &= b | aとbのビットごとのANDをaに代入 | |= | ビットごとのOR | a |= b | aとbのビットごとのORをaに代入 | ^= | ビットごとのXOR | a ^= b | aとbのビットごとのXORをaに代入 |
|---|
■sizeof演算子
いよいよラスト!sizeof演算子を紹介して今回は終わりにします。
sizeof演算子は、一言で言うと変数に割り当てるメモリの大きさを返す関数みたいなものです。
sizeof (long) .... 4バイトが返されます。 sizeof (123 * 45) ....計算結果はintなので4バイトが返されます。 |
さて、様々な演算子を紹介してきました。得にビット演算子の所は重要です。
次回は演算子の優先順位、データ型の変換について解説します。
■演算子の優先順位
前回演算子をずらずらと紹介してきましたが、それには優先順位があります。
まぁ、不安な場合は先に処理したい部分を括弧でくくってしまえばいいのですが。
式 () [] -> . 単項演算子 ! ~ ++ -- + - (型) * & sizeof (*と&はポインタ) 二項演算子 乗除 * / 加減 + - シフト << >> 比較 < <= &rt; &rt;= 等価 == != ビット & | ^ (左から、AND、OR、EXORの順で低くなる) 論理 && || (左から、AND、ORの順で低くなる) 条件演算子 ? : 代入演算子 = += -= *= /= %= >>= <<= &= |= ^= カンマ演算子 , |
上に行く程優先順位が高いです。
また普通同じ順位が並んだ場合左から右に実行しますが、
単項演算子、条件演算子、代入演算子は、右から左に実行するという法則があります。
例えば、
if (a == b && c){
|
a = b = c = d = 777; |
a = b << 1 + 2 || c == 3 && d == 4; |
■型変換
まず結論から話しちゃいますw
原則1、その式中の最も大きいデータ型に合わせて型変換を行う。 原則2、数値を代入する場合、左辺のデータ型にあわせられる。 |
原則1では、例えばchar型とlong型だとlong型に合わせられるので、別に数値は変わったりはしません。
ですが、原則2で小さなデータ型に数値を代入する場合、
char型にlong型の変数を代入する場合は無理矢理丸められてしまうので要注意です。
(その丸められ方はコンパイラによって違います。)
また、C言語では明示的に型変換を行う事ができます。こんな風に。
(データ型)変数・式など。 |
具体例を示すと、
void thefunction(long n){
・・・
}
int main(){
int a;
thefunction ((long)a);
・・・
・・・
}
|
これはint型のaがlong型に変換されています。
このように変数と違う型を持つ関数にその値の引き数を渡したい時に使ったりします。
■ソートとは?
今回は文法はお休みして、ソート処理のアルゴリズムを組んでみます。
ソートとは、一定のルールに従って並び変える事です。例えば、大きい順に並べるとか。
今回はソートするデータは配列。以下のものとします。
int Num[10] = { 50 , 24 , -9 , -7 , 4 , 24 , 8 , -17 , -7 , 0 };
|
■解答例1・単純ソート
int main(){
int Num[10] = { 50 , 24 , -9 , -7 , 4 , 24 , 8 , -17 , -7 , 0 };
int Max,Maxn;
int check[10];
for (int n = 0 ; n <= 9 ; n ++){
Max = Num [0];
for (int m = 0 ; m <= 9 ; m ++){
if (Max < Num [m] && check[m] == 0) {
Max = Num [m];
Maxn = m;
}
check[Maxn] == 1;
printf ("%d ",Max);
}
}
|
■解答例2・バブルソート
5つの数があります。 1、3、4、2、5 順番がバラバラですね。大きい順にバブルソートしてみます。 まず、1番目と2番目の数をくらべます。1番目の方が小さいので、その2数を入れ替えます。 3、1、4、2、5 次に、2番目と3番目の数をくらべます。2番目の方が小さいので、その2数を入れ替えます。 3、4、1、2、5 ...同様に、3番目と4番目、4番目と5番目 とやっていくと、 3、4、2、5、1 一番小さい数である1が、最も右側に寄ったではありませんか!これで第1段階完成。 次にもう一回同じ事をします。1番目と2番目で、1番目の方が小さいので、その2数を入れ替えます。 4、3、2、5、1 2番目と3番目では3番目の方が小さいので、そのままです。 3番目と4番目では3番目の方が小さいので、入れ替えます。 5番目には最小値の1が入っているので、ここでは操作しません。すると、 4、3、5、2、1 次に小さい数の2が、1のとなりに並びました。 同様に同じ事をあと2回繰り返すと、みごとに5、4、3、2、1と並びます。 |
int main(){
int Num[10] = { 50 , 24 , -9 , -7 , 4 , 24 , 8 , -17 , -7 , 0 };
int s;
int f;
for (int n = 0 ; n <= 9 - f ; f ++){
for (int m = 0 ; m <= 8 ; m ++){
if (Num[m] < Num[m+1]) {
s = Num[m+1];
Num[m+1] = Num[m];
Num[m] = s;
}
}
//出力は省略
}
|
■結構重要
・基本的に宣言したブロックの範囲内。 ・関数の外に宣言した場合は、「グローバル変数」となり、どこでも使える。 |
・ある関数で宣言した変数は他の関数では使えない。 ・for文の中で宣言した変数は外では使えない。 ・逆に、for文の外で宣言した変数はfor文の中でも使える。 |
突然ですが、実験をしてみましょう(何。
変数を違うブロック内で宣言して、その値を調べるという実験です。
int main(){
int i;
for (i = 0 ; i < 2 ; i ++ ){
int i;
for ( i = 0 ; i < 3 ; i ++){
int i = 0;
i++;
print ("i (3) .... %d\n", i);
}
printf ("i(2) .... %d\n", i);
}
printf ("i(1) ..... %d\n", i);
}
|
1 1 1 3 1 1 1 3 2 |
また、4行目、7行目の値に御注目。外側のループのiの値に関係なく、値はいつも3です。
つまり同名の変数がぶつかった場合はそのブロックの中でのみ使える事になります。
ここでは内側ループのiのループを抜ける時の値、3が表示されています。要は違うブロックなら同名の変数でも区別されている訳ですね。
実はこの変数は、ブロックの外に出ると使えなくなってしまいます。
変数iはどのブロックでも区別されています。ブロックを抜けるとそのブロック内で使われていた変数は忘れられてしまいます。
なので入る度に初期化されるのです。
ちょっとややこしい話でした...(笑
説明が雑で申し訳ありません。分からなかったら掲示板などで御報告お願いします。
■前回の実験
いままでずーーーーーーーっと使ってきた変数は「auto」変数です。
auto 変数は「スタック領域」に保存される変数です。
「スタック領域」というのは、メモリの領域の一種で、
記憶した順序と逆にデータを取り出す事が出来ます。
img
↑よく書かれる図ですが(笑
前回の実験を覚えているでしょうか?
「ブロックを抜けるとそのブロック内で使われていた変数は忘れられてしまいます」
これが実験の結果でした。これは、ブロックを抜けた瞬間に、そのブロックで使われていた変数が「pop」されてしまったということです。
外側のループは内側よりも前に変数が「push」されているので、内側のループを抜けた時に、
スタック領域の一番上にあるものは外側のループのものになります。
これがスタックの原理です。
■じゃあ、「静的変数」って何な訳?
では、早速前回やった実験を「static」変数を使ってやってみましょう。
int main(){
int i;
for (i = 0 ; i < 2 ; i ++ ){
int i;
for ( i = 0 ; i < 3 ; i ++){
static int i = 0;
i++;
print ("i (3) .... %d\n", i);
}
printf ("i(2) .... %d\n", i);
}
printf ("i(1) ..... %d\n", i);
}
|
1 2 3 3 4 5 6 3 2 |