* 第6回 ポインタ [#e9e24e95]

- ''基本課題''は授業時間中に終わらせること。
- ★印の付いている問題は,当教員に見せて確認の印(またはサイン)を得ること。
- 授業時間中に終わらなかった''応用課題''は,次回(2週間後)までの宿題とする。
- ''基本課題''は,必ず授業時間中に終わらせること。

- [[C言語のコンパイルと実行のしかた>../C言語環境]]
-- プログラムのファイル名は,「問題番号.c」としてください。例:a1.c, ax3.c
- [[プログラムの提出のしかた>../提出]]
- ''★印の付いている問題''は,担当教員に見せて確認の印(またはサイン)を得ること。

- ''応用課題''は,終わらなかったら次週(1週間後の午前11:00)までの宿題とする。

- ''他人のコピーと判断されたものは正当な提出と認めない。''その場合,2人のコピーなら2分の1の点数,3人のコピーなら3分の1の点数(以下同様)とする。

- プログラムのファイル名は,「問題番号.c」とすること。
-- 例: a1.c, ax3.c

- [[プログラムの提出のしかた>../提出]]をよく読むこと。
-- 提出URL http://vilab.org/upload/clab-upload.html
- [[C言語おすすめ情報>../C言語参考資料]]
-- おすすめのWebページや書籍の紹介。

** 基本課題 [#i341647d]

*** ポインタ [#v1805475]

''★f1)'' int型の変数aとint型へのポインタpを定義し、pを用いてaに好きな値(キーボードから入力した整数)を代入するプログラムを作成しなさい。※直接aに値を代入せず、pを使ってaの値を変える。
 int a;
''f1)'' char型の配列 str[9] = "TAMAGAWA" を定義し,その各要素a[0], a[1], ..., a[8]の値と''アドレス''を表示するプログラムを作成しなさい。''※この問題ではポインタ変数は用いない。''

> 【ポイント】 変数の''アドレス(メモリ内での格納番地)''を求めるには「&」演算子を用い,それをprintfで表示するには%pを用いる。ポインタはこのような変数のアドレスを記憶するための変数である。※以下は整数の例である。
 int n;
 printf("%p\n", &n);

''★f2)'' まず,int型の変数a,bに適当な整数を代入しておく。次に,apをaへのポインタ,bpをbへのポインタにする。そして,apとbpを使って変数aとbの内容を交換するプログラムを作成しなさい。''交換のときaやbへの代入(「a=...」や「b=...」)を使ってはいけない。'' 結果を確認するため,交換前後のa,bの値を表示させなさい。

> 【ポイント】 ポインタ(アドレス値)の頭に「*」演算子をつけると,そのアドレスにあるデータを普通の変数と同じように操作することができる。
 int n, m;
 int *p;
 p = &n;
 *p = 3; /* n = 3 と書くのと同じ意味になる */
 m = *p; /* m = n と書くのと同じ意味になる */

> 【ポイント】 ポインタは必ず変数などのある有効なメモリ領域を指していなければならない。初期化していないポインタは非常に危険である。※下記は課題とは関係ない。
 char str[20];
 char *ptr;
 ptr = &str[3];
''★f3)'' int型の配列array[10]とint型へのポインタptrを定義し,ptrを用いてarrayの各要素にキーボードから整数を入力するプログラムを作成しなさい(最後に全要素を表示して確認する)。※直接array[i]に値を入力せず,ptrを使ってarrayの値を変えること。
 int array[10];  /* int型の配列 */
 int *ptr; /* int型へのポインタ */
以下は''ダメな例''である。
 scanf("%d", &array[i]);  /* ← ダメ! */
 array[i] = ...なんか...; /* ← ダメ! */

''f2)'' まず,int型の変数a,bに適当な整数を代入しておく。次に,pをaへのポインタ,qをbへのポインタとする。そして,pとqを使って(a,bを使わずに)変数a,bの内容を入れ替えなさい。結果を確認するため,入れ替える前と後の変数a,bの値を表示しなさい。
> 【ポイント】 ポインタは必ず変数などのある有効なメモリ領域を指していなければならない。初期化していないポインタは非常に危険である。

''★f3)'' 整数型の配列a[10]と,整数型へのポインタpを定義し,配列の要素a[3]に適当な値を代入し,pにa[3]のアドレスを代入する。このときのa[3],&a[3], *p, pを表示すること。その後,*pに別の値を代入するとどうなるかも同様に表示しなさい。
''f4)'' まず,char型の配列strとポインタptrを定義し,strにキーボードから文字列を読み込む。次に,文字列の先頭から末尾まで順に str[i] と *(str + i) を表示する。最後に,ptrにstrを代入(ptr = str)してから,同様に ptr[i] と *(ptr + i) を表示するプログラムを作成しなさい。

> 【ポイント】 ポインタの値(アドレス)を表示するには%pを用いる。
 printf("%p\n", ptr);
> 【ポイント】 C言語ではほとんどの場合,配列の名前はその先頭要素のアドレスとして扱われる。つまり,配列名は値を変更できないポインタだと考えてよい((ただし,sizeofの結果を除く。))。また,xが配列でもポインタでも必ず以下の公式が成り立つ。
 *(x + i) == x[i]
このとき,iは整数ならば正でも負でもよい。よって,ptr[-1] という書き方も有効で,*(ptr - 1) という意味になる。

''f4)'' キーボードから入力された文字列を char 型への配列 str に読み込み、char型へのポインタ ptr を用いて、str 内の1文字目から最後までの文字列、2文字目から最後までの文字列、3文字目から最後までの文字列、…を順に出力するプログラムを作成せよ。以下に出力例を示す。
''★f5)'' 2つの整数x,yを引数に取り,商と余りを同時にもとめる関数divideを作成しなさい。なお,関数の戻り値は,y=0なら0を返し,それ以外なら1を返すようにしなさい。
 int divide(int x, int y, int *sho, int *amari);
使用例
 int a, b, ok;
 ok = divide(5, 3, &a, &b);
 if (ok)
     printf("商 %d  余り %d\n", a, b);
 else
     printf("0では割れません"); 

> 【ポイント】 C言語の関数呼び出しは値渡し(call by value)なので,呼び出された関数の中で変数の値を変えても,呼び出し元の変数にはまったく影響がない。しかし,関数の変数のアドレス(格納場所)を渡せば,呼び出された関数の中で呼び出し元の変数の値を変更できる((scanfが変数に値を読み込めるのはこれを使っているからである。))。
- 参考 http://wisdom.sakura.ne.jp/programming/c/c31.html

** 応用課題 [#v5d40004]

''fx1)'' キーボードから文字列strを読み込み,strの中を指すポインタptrを用いて printf("%s", ptr) という処理をするとどうなるか試してみなさい。それを利用して,str 内の1文字目から末尾までの文字列,2文字目から末尾までの文字列,3文字目から末尾までの文字列,…を順に出力するプログラムを作成しなさい。以下に出力例を示す。
 Tamagawa
 amagawa
 magawa
 agawa
 gawa
 awa
 wa
 a

''★f5)'' 2つの整数x,yを引数に取り、商と余りを同時にもとめる関数divideを作成しなさい。なお、y=0なら0を返し,それ以外なら1を返すようにしなさい。
 int divide(int x, int y, int *sho, int *amari);
使用例
 int a, b, c;
 c = divide(5, 3, &a, &b);
 if (c != 0) printf("商 %d  余り %d\n", a, b);
''fx2)'' 長さ10文字のchar型の配列fromとtoを定義する。キーボードからfromに文字列を読み込んだ後,fromの中の要素を指すポインタとtoの中の要素を指すポインタを用いて,fromからtoに文字列をコピーするプログラムを作成しなさい。

** 応用課題 [#v5d40004]

''fx1)'' 長さ5のint型,char型の配列を作り,各要素に適当な値を代入しなさい。それぞれの配列について,ポインタを使って配列の要素のアドレスと中身を1つずつ表示しなさい。

''fx2)'' 長さ10文字のchar型の配列fromとtoを定義し、キーボードからfromに文字列を読み込んだ後、ポインタ2つを用いてtoにこの文字列をコピーするプログラムを作成しなさい。

> 【ポイント】 ポインタによる連続コピーでは、よく以下のような書き方が用いられる。
> 【ポイント】 ポインタによる連続コピーでは,よく以下のような書き方が用いられる。
 *p++ = *q++;

''fx3)'' 整数型へのポインタを2つ取り、それらの指す整数値を交換する関数swapを作成せよ。
''fx3)'' 整数型の変数へのポインタを2つ取り,それらの指す整数値を交換する関数swapを作成しなさい(swap関数の動作を調べるmain関数もつけること)。
 void swap(int *p, int *q);
使用例
 int a = 3, b = 5;
 swap(&a, &b);
 printf("a=%d b=%d\n", a, b);
 swap(&a, &b);  /* aとbの交換 */
 printf("a=%d b=%d\n", a, b);

''fx4)'' 2つの文字列を比較し,同じなら1,異なる場合は0を返す関数を作成しなさい。※ただし,標準ライブラリ関数strcmpは用いないこと。
''fx4)'' 2つの文字列を比較し,同じなら1,異なる場合は0を返す関数を作成しなさい(関数の動作を調べるmain関数もつけること)。※ただし,標準ライブラリ関数strcmpは''用いない''こと。
 int strequal(char *str1, char *str2);

''fx5)'' キーボードから文字列patternとstringを読み込み、stringにpatternが含まれる場合には「成功」と表示して終了し、含まれない場合には「失敗」表示として成功するまでstringの読み込みを繰り返すプログラムを作成しなさい。※標準ライブラリ関数strstrを使ってよいので調べてみよ。
''fx5)'' キーボードから文字列patternとstringを読み込み,patternがstringのなかに含まれる場合には「成功」と表示して終了し,含まれない場合には「失敗」表示としてから、成功するまでキーボードからstringを読み込み続けるプログラムを作成しなさい。※標準ライブラリ関数strstrを使ってよいので調べてみよ。

** 発展課題 [#f2f615a6]

- 発展課題には''提出期限を設けない''ので,前回までの発展課題でやっていないものも含めて,どれでも自分の面白いと思うものをやってよい。

*** ダンジョン探索 [#pca3cc96]

''fz1)'' RPGシリーズということで,今度はダンジョン(地下迷宮)の地図を作り,プレイヤーがその中を探検できるようにしてみよう。今回はやることが多いので説明はかなり簡略されている。不明な点は自分で考えて適切に設計しながらチャレンジしてほしい。

主人公たちは村外れにたつ小さな古城にたどり着いた。言い伝えでは,この地下は昔牢獄として使われ,今では恐ろしい吸血鬼や悪霊が住み着く迷宮となっているという。村人のためにも,吸血鬼を退治しなければ…。

ダンジョンの地図は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() /* 通路 */
 {
     /* 何も起きない */
 }
 
 void stairway() /* 階段 */
 {
     /* 地上に脱出するかどうか質問する */
     /* 吸血鬼を倒してから脱出すれば,ハッピーエンド */
 }
 
 void door() /* 扉 */
 {
     /* 何かメッセージを表示させるといいだろう */
 }
 
 void cross() /* 十字架 */
 {
     /* 宝箱があり,中に十字架が入っている */
     /* 紙に書かれた謎々を解くと箱を開けられる */
 }
 
 void vampire() /* 吸血鬼 */
 {
      /* 吸血鬼に仲間にならないか誘われるが,断ると対決 */
      /* 十字架を持っていれば勝つことができる */
 }

プログラムは,ユーザに,主人公を東西南北(上下左右)どちらの方向に進ませるか尋ね,入力された方向に壁がなければ,xとyを適切に変化させてその場所の記号に対応する関数を呼び出す。
 printf("どちらの方向に進みますか?(1:東 2:西 3:南 4:北)");
 scanf("%d", &dir);
 switch (dir) {
 case 1:
     if (map[y][x+1] != 9) x++;
     break;
 case 2:
     if (map[y][x-1] != 9) x--;
     break;
 case 3:
     if (map[y+1][x] != 9) y++;
     break;
 case 4:
     if (map[y-1][x] != 9) y--;
     break;
 default
     printf("その方向には進めません。\n");
     /* エラー処理 */
 }
 
 switch (map[y][x]) {
 case 0:
     passage();
     break;
 case 1:
     stairway();
     break;
 case 2:
     door();
     break;
 case 5:
     cross();
     break;
 case 7:
     vampire();
     break;
 }
 
画面には,主人公の周囲の状況を表示するとよい。一例としては,最初階段にいるときに以下のよう表示させる。どんな文字や表示方法を使うか,どこまでの範囲を表示させるかは自由である。
- 文字表示の例
 あなたは,いま上り階段にいます。
 東は壁です。西は壁です。北は壁です。南は通路です。
 どちらの方向に進みますか(東西南北)?
- マップ表示の例
 ■■■
 ■?■
 ■ ■ どちらの方向に進みますか(東西南北)?

吸血鬼に負けてしまった場合は,主人公も吸血鬼になってゲームオーバーである。自分のような冒険者の生き血を求めて,このダンジョンに住み続けることになる…。


トップ   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS