- 追加された行はこの色です。
- 削除された行はこの色です。
* 第3回 関数 [#va6108c7]
* 第3回 配列 [#o5867e1f]
- ''基本課題''は授業時間中に終わらせること。
- ★印の付いている問題は,当教員に見せて確認の印(またはサイン)を得ること。
- 授業時間中に終わらなかった''応用課題''は,次回(2週間後)までの宿題とする。
- [[C言語のコンパイルと実行のしかた>../C言語環境]]
-- プログラムのファイル名は,「問題番号.c」としてください。例:a1.c, ax3.c
- [[プログラムの提出のしかた>../提出]]
-- 提出URL http://vilab.org/upload/clab-upload.html
- [[C言語おすすめ情報>../C言語参考資料]]
-- おすすめのWebページや書籍の紹介。
-- 参考になるWebページや書籍の紹介。
** 基本課題 [#a2d5bf8e]
** 基本課題 [#qaa408be]
*** 関数 [#fe72e5b1]
*** 配列 [#y5ab2a38]
''★c1)'' 半径rを引数として,円周の長さを表示する関数を作成しなさい。その関数の動作を確認するプログラムを作成しなさい。以下にプロトタイプ宣言の例を示す。
void print_circle(double r);
''c1)'' 4つの要素を持つdouble型配列にキーボードから数値列を読み込み,合計と平均を求めるプログラムを作成しなさい.
>【ポイント】 関数プロトタイプ宣言は,関数の型(使い方)をコンパイラに教えるためのもので,関数の定義(関数の中身を実際に書くこと)とは別である。関数を定義より前に使用する場合には,プロトタイプ宣言が必要である。
/* プロトタイプ宣言:行末にセミコロンが必要 */
void print_circle(double r);
int main(void)
{
/* 関数の使用例 */
print_circle(10.0);
}
/* 関数の中身の定義:{}の中に実行内容を書く */
void print_circle(double r)
{
/* 関数の実行内容 */
}
>【ポイント】 たとえばint a[N];と宣言した配列は、a[0]からa[N-1]までのN個の要素を持つ。a[N]は存在しないので注意。intでなくても同じ。
int a[10];
a[10] = 3; /* 誤り!! */
上記は誤りだが、通常Cコンパイラはこれを検出しない。しかし、実行時に意味不明の挙動を示す原因となるので決してしてはならない。
''c2)'' 関数f(x)=x+1をC言語で表し、整数nをキーボードから読み込み、f(0)+f(1)+f(2)+...+f(n)の値を表示するプログラムを作成しなさい。
''★c2)'' キーボードから10個の整数を読み込み,最後に入力とは逆の順番に表示して終了するプログラムを作成しなさい。ループ(forまたはwhile)を使うこと。
''★c3)'' 2つの整数を引数として,値の小さい方を返す関数min2を作成しなさい。それを''そのまま''呼び出して,10個の整数を読み込み,その最小値を求めるmainプログラムを作成しなさい。
int min2(int n, int m);
''★c3)'' キーボードから10個の整数を読み込んだ後、平均以上の数値と平均未満の数値を''別々に分けて''表示する(つまり,最初に平均以上の数値だけを並べて表示し,次に平均未満の数値だけを並べて表示する)プログラムを作りなさい。
*** ライブラリ関数 [#zc95c9af]
*** 文字列 [#n323204d]
- 参考 http://www9.plala.or.jp/sgwr-t/c/sec07.html
''c4)'' scanfを用いて,自分の出身地(都道府県など)をローマ字で読み込み,表示するプログラムを作成しなさい。
''★c4)'' 座標平面上の点 (x, y) から原点までの距離を計算するC言語のプログラムを作成し,コンパイル・実行させたい.キーボードからdouble型の数値xおよびyを読み込み,距離を計算して画面に表示するプログラムを作成しなさい.平方根を計算するライブラリ関数sqrtのマニュアルには、以下のように記述されている。
#include <math.h>
double sqrt(double x);
>【ポイント】 文字列の文字配列への読み込みには、%sを用いる。このとき,読み込める最大文字数(配列のサイズ−1)を指定するべきである。
char str[128];
/* 読み込める長さを指定しないやり方 */
scanf("%s", str);
/* 読み込める長さを指定したもっと好ましいやり方 */
scanf("%127s", str);
>【ポイント】 C言語の処理系(OS)によっては,数学関係の標準ライブラリを使ったプログラムをコンパイルするために数学ライブラリをリンクする必要がある。たとえば,gcc(UNIX)ではコンパイル時のオプションで-lmと指定する。
gcc -lm ファイル名.c
''★c5)'' 文字型の配列を自分の名前のローマ字で初期化し(変数の宣言時に代入し)、その配列中の文字の文字コード(文字番号)を縦に並べて1文字分づつ画面に表示するプログラムを作成しなさい。
''c5)'' 自分の名前(ローマ字)をキーボードから読み込み、標準ライブラリ関数toupperを用いて1文字ずつ大文字に変換した後、画面に表示するプログラムを作成しなさい。
#include <ctype.h>
int toupper(int c);
>【ポイント】 文字列の最後は、ヌル文字('\0': 終端文字)で終わる。よって、文字列を格納するためには、その文字列の長さ+1文字分の領域(charの配列)が必要である。
char str[] = "Tamagawa";
の場合、下記のように9文字分の領域を取る。
0 1 2 3 4 5 6 7 8
str[] ['T'|'a'|'m'|'a'|'g'|'a'|'w'|'a'|'\0']
** 応用課題 [#ifb9aed1]
>また,C言語における1文字の実態は整数の文字コード(要するに「文字番号」)である。通常使われるASCIIコードでは,A〜Z,a〜zに,それぞれ連続した番号が割り当てられている。例えば上記の例を文字コードで示すと以下のようになる。
0 1 2 3 4 5 6 7 8
str[] [ 84| 97|109| 97|103| 97|119| 97| 0 ]
''cx1)'' 西暦を引数とし,その年の1月1日の元号が平成(0)・昭和(1)・大正(2)・明治(3)・それ以外(-1)のいずれであるかを返す関数を作成しなさい。その関数の動作を確認するプログラムを作成しなさい。以下にプロトタイプ宣言の例を示す。
int gengo(int seireki);
** 応用課題 [#ke2bf6f7]
''cx2)'' キーボードから読み込みこんだ整数n (ただしn≧2)を1辺の長さとするアスタリスク(*)の四角形を描画するC言語の関数を作成し,その関数の動作を確認するプログラムを作成しなさい。たとえば、n=2,3,4のときは、それぞれ以下のような図形が出力される。
n=2 ** n=3 *** n=4 ****
** * * * *
*** * *
****
''cx1)'' キーボードから、double型の配列v[2]およびw[2]を読み込み、これらを2次元ベクトルと考えて、和と内積を表示するプログラムを作成しなさい。
- 参考(ベクトルの加算) http://shigihara.hp.infoseek.co.jp/vect12.htm
- 参考(ベクトルの内積) http://shigihara.hp.infoseek.co.jp/vect16.htm
''cx3)'' 標準ライブラリ関数rand()は,0〜RAND_MAXの範囲の擬似乱数(でたらめな数)を返す関数である。この関数を用いて,0以上10未満の乱数を100個発生させ,画面に表示するプログラムを作成しなさい。
#include <stdlib.h>
int rand(void);
- なお,randは起動するたびに同じ数列を返してしまうので,実際の使用ではさらに工夫が必要である。たとえば,発展課題 ''az1)'' のようにsrand()と時間を使って初期値をかき混ぜる。
*** 線形探索法 [#s37a5bd3]
''cx4)'' 三角形の2辺の長さとそれらの挟む角を引数とし、面積を返す関数を作成しなさい。ただし、入力する角度の単位は“度”とする。 ※関数sinを利用するが、度からラジアンへの変換が必要である。
- 参考 http://shigihara.hp.infoseek.co.jp/sin53.htm
- 参考 http://www2.ocn.ne.jp/~atel.a/emath/sugakuc.html
''cx2)'' キーボードから、10個の整数を要素とする配列aと、整数xを読み込み、xがaに含まれているかどうか,aの先頭から順に調べて結果を表示するプログラムを作成しなさい。
i→ →| iを0から9まで変化
0 1 2 3 4 5 6 7 8 9
a[] [__|__|__|__|__|__|__|__|__|__]
a[i]とxを比較
''cx5)'' 前回の線形探索のプログラム ''bx2)'' を改造して,整数配列a,その要素数n,探したい整数xを引数に取り,''最初に''見つかった場所(添字)を返す関数linear_searchを作成しなさい。ただし,見つからなかった場合には,-1を返すようにしなさい。
int linear_search(int a[], int n, int x);
- 値を「''返す''」というのは「''returnする''」ということで,画面に表示させることではありません。
''cx3)'' キーボードからアルファベットからなる文字列を読み込み,先頭文字と同じ文字の個数を数えるプログラムを作成しなさい。たとえば,「application」ならばaが2個あるので2が結果となる。
** 発展課題 [#wb83beb9]
*** ROT13暗号 [#te619baa]
- 発展課題には''提出期限を設けない''ので,前回までの発展課題でやっていないものも含めて,どれでも自分の面白いと思うものをやってよい。
''cx4)'' ROT13暗号とは,欧文に含まれる文字を1文字ずつ,下記の図に示すようにアルファベット順で13文字後ろの文字に“ずらす”ことによって,文章を暗号化するものである。
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G ...
A→→→→→→→→→→→→ N
H→→→→→→→→→→→→ U
N→→→→→→→→→→→→ A
Q→→→→→→→→→→→→ D
Aの13文字後はN,HはU,NはA,QはDへと変換され,例えば「HELLO」は「URYYB」と暗号化される。すると,「ABCDEFGHIJKLM」の各文字は「NOPQRSTUVWXYZ」の各文字と''交換''されることがわかる(小文字は小文字同士で交換)。
*** ダンジョン探索 [#ze095796]
キーボードから読み込んだ文字列を,ROT13暗号によって暗号化するプログラムを作成しなさい。また,暗号化された文字列を復号化(元に戻すこと)して,ちゃんと暗号化されていたことを確認しなさい。
''cz1)'' RPGシリーズということで,今度はダンジョン(地下迷宮)の地図を作り,プレイヤーがその中を探検できるようにしてみよう。今回はやることが多いので説明はかなり簡略されている。不明な点は自分で考えて適切に設計しながらチャレンジしてほしい。
>【ポイント】 スペースを含む文字列を1行読み込む関数にはgets()があるが,近年ではこれは使わないほうがよい。かわりに,scanf()の書式 %[^\n] を用いるとよい。
char str[128];
scanf("%127[^\n]", str);
主人公たちは村外れにたつ小さな古城にたどり着いた。言い伝えでは,この地下は昔牢獄として使われ,今では恐ろしい吸血鬼や悪霊が住み着く迷宮となっているという。村人のためにも,吸血鬼を退治しなければ…。
*** スタックの実装 [#gf047e68]
ダンジョンの地図は2次元配列(グローバル変数)として用意し,主人公の現在位置は変数(x, y)で表すとよい。この例では,初期位置は上り階段(map[1][1])のある(1,1)とする。
int map[7][7] = {
{ 9, 9, 9, 9, 9, 9, 9 },
{ 9, 1, 9, 0, 9, 5, 9 },
{ 9, 0, 9, 0, 2, 0, 9 },
{ 9, 0, 9, 0, 9, 9, 9 },
{ 9, 0, 0, 0, 2, 0, 9 },
{ 9, 0, 9, 0, 9, 7, 9 },
{ 9, 9, 9, 9, 9, 9, 9 } };
ここで,0〜9はそれぞれ次のものを表す(ここらへんは自由にアレンジしてよい)。
-「0」通路
-「1」上り階段(外に脱出できる)
-「2」扉
-「5」十字架(吸血鬼を倒すためのアイテム)
-「7」吸血鬼(ボスキャラ)
-「9」壁(進めない)
また,主人公が地図上のそれぞれの記号を訪れた場合の関数を作成する。壁は移動することができないので作らなくてよい。
void passage() /* 通路 */
{
/* 何も起きない */
}
''cx5)'' スタックとは、''先入れ後出し方式''のデータ構造である。データを上に積み重ねていくような形式のデータ構造であり、一番最後に積んだデータを一番最初に取り出すことができる。一番最初に積んだデータは、後から積んだデータを全部取り出さないと、取り出すことができない。
以下のような動作をするプログラムを作成しなさい。
+ スタックとスタックポインタを表す変数を用意する。
-- int stack[100]; 100個の整数を保存しておくための配列。
-- int sp = 0; スタックの中の''次の空き''を示すための変数(初期値はゼロ)。
+ キーボードから整数xを読み込む。
+ xの値を判定し:
-- x>0ならば、xをstackに積んで2.に戻る(push)。言い換えると、stack[sp]の位置にxを代入し、spを1増やす。
● xを積む(push)
↓
0 1 2 3 4 5 6 7 8 9
stack [●|●|●|●|●|__|__|__|__|__|....
↑
次の空き(sp)
void stairway() /* 階段 */
{
/* 地上に脱出するかどうか質問する */
/* 吸血鬼を倒してから脱出すれば,ハッピーエンド */
}
0 1 2 3 4 5 6 7 8 9
stack [●|●|●|●|●|●|__|__|__|__|....
↑
次の空き(1増えたsp)
-- x<0ならば、stackに最後に積んだ値を表示して、それをstackから取り除き、2.に戻る(pop)。言い換えると、spを1減らし、stack[sp]の位置から値を取り出す。
次の空き(sp)
↓
0 1 2 3 4 5 6 7 8 9
stack [●|●|●|●|●|●|__|__|__|__|....
↓
取り出す(pop)
void door() /* 扉 */
{
/* 何かメッセージを表示させるといいだろう */
}
void cross() /* 十字架 */
{
/* 宝箱があり,中に十字架が入っている */
/* 紙に書かれた謎々を解くと箱を開けられる */
}
void vampire() /* 吸血鬼 */
{
/* 吸血鬼に仲間にならないか誘われるが,断ると対決 */
/* 十字架を持っていれば勝つことができる */
}
次の空き(1減ったsp)
↓
0 1 2 3 4 5 6 7 8 9
stack [●|●|●|●|●|__|__|__|__|__|....
↓
●
-- x=0ならば、stackの中身を逆順にすべて表示して、プログラムを終了する。言い換えると、stack[sp-1]からstack[0]までを表示する。
+ 2.に戻って繰り返す。
プログラムは,ユーザに,主人公を東西南北(上下左右)どちらの方向に進ませるか尋ね,入力された方向に壁がなければ,xとyを適切に変化させてその場所の記号に対応する関数を呼び出す。
printf("どちらの方向に進みますか?(6:東 4:西 2:南 8:北)");
scanf("%d", &dir);
switch (dir) {
case 6:
if (map[y][x+1] != 9) x++;
break;
case 4:
if (map[y][x-1] != 9) x--;
break;
case 2:
if (map[y+1][x] != 9) y++;
break;
case 8:
if (map[y-1][x] != 9) y--;
break;
default
printf("その方向には進めません。\n");
/* エラー処理 */
なお、スタックにデータをしまう操作をpushといい、データを取り出す操作をpopという。また、スタックの中の次の空き位置(または現在の最後の位置)を保存する変数spをスタックポインタという。
** 発展課題 [#sc946e58]
*** ブラックジャック [#x8c9cdb8]
''cz1)'' トランプゲームのブラックジャック(簡略版)を作成したい。以下の手順で動作するプログラムを作成しなさい。
''準備''
+ 以下のような変数を用意しておくとよい。
int cards[52]; /* トランプのカード52枚を格納する配列 */
int player[10]; /* プレイヤーの手札 */
int dealer[10]; /* コンピュータの手札 */
int n_drawn; /* 既に束から引いたカードの枚数 */
int n_player; /* プレイヤーの手札の枚数 */
int n_dealer; /* コンピュータの手札の枚数 */
+ 配列cards[]に,スペードの「A」からクラブの「K」までの全カードを1枚ずつ格納させる。しかし,このブラックジャックではカードのスーツ(スペード等)は関係ないので,1から13までの整数を4個ずつ格納すればよい。
n = 0;
for (i = 1; i <= 13; i++) {
cards[n++] = i;
cards[n++] = i;
cards[n++] = i;
cards[n++] = i;
}
+ カードの束をよくシャッフルする。よくある方法では,配列card[]の要素からランダム2枚を選び交換する作業を適当な回数(以下の例では300回)繰り返す。なお,rand()を使う場合には''az1)''を参考にして発生前に乱数をかき混ぜておく。
/* rand()使用前に1度だけ実行 */
srand((unsigned) time(NULL))
switch (map[y][x]) {
case 0:
passage();
break;
case 1:
stairway();
break;
case 2:
door();
break;
case 5:
cross();
break;
case 7:
vampire();
break;
for (i = 0; i < 300; i++) {
n = rand() % 52;
m = rand() % 52;
temp = cards[n];
cards[n] = cards[m];
cards[m] = temp;
}
画面には,主人公の周囲の状況を表示するとよい。一例としては,最初階段にいるときに以下のよう表示させる。どんな文字や表示方法を使うか,どこまでの範囲を表示させるかは自由である。
- 文字表示の例
あなたは,いま上り階段にいます。
東は壁です。西は壁です。北は壁です。南は通路です。
どちらの方向に進みますか(東西南北)?
- マップ表示の例
■■■
■?■
■ ■ どちらの方向に進みますか(東西南北)?
吸血鬼に負けてしまった場合は,主人公も吸血鬼になってゲームオーバーである。自分のような冒険者の生き血を求めて,このダンジョンに住み続けることになる…。
''ゲーム''
+ 先頭のカードを2枚取り出し,コンピュータの手札とする。このうち1枚目だけを画面に表示する。
-- 配列cards[]の先頭の整数2つを,配列dealer[]にコピーすればよい。
-- 変数n_drawnとn_dealerも適切な値になるように。
+ 同様に,次のカードを2枚取り出し,プレイヤーの手札とする。これは両方とも画面に表示する。
-- 次の整数2つを,配列player[]にコピーすればよい。
-- 変数n_drawnとn_playerも適切な値になるように。
+ プレイヤーはカードの数字の合計が21に近くなるように,カードを1枚ずつ引くことができる。
-- 例えば「次のカードを引きますか?(Y/N)」という表示を出して,「Y」である間カードを引かせて配列player[]に追加していく。
-- 「J」「Q」「K」は,10として計算する。「A」は1または11の好きなほうで数えてよい。
-- カードの合計が21を超えてしまった場合は,その時点で負けである。
+ プレイヤーが終わったら,コンピュータの番である。
-- コンピュータは,カードの数字の合計が16以下だった場合には1枚ずつ引いて手札に加える。
-- もし,17以上になったらそのカードで引くのを終わりにする。
-- このとき「A」が含まれていたら,まず11として計算して17以上21以下ならそこで終わりとし,そうでなければ1として計算することにする。
-- プレイヤーと同様に21を超えてしまったら,その時点で負けになる。
+ 最後に,プレイヤーとコンピュータを比較して,21を超えずに21に近いほうが勝ちとなる。