関数を作る

この記事の目的

この記事は部員向けに書いています.初めて壁情報を保存しようと頑張っている人,使いやすい関数を作りたい人,キーの打ち間違えが多くて困っている人は読んでみてください.
壁の追加関数を作りながら,関数の作り方を学習していきましょう.

この記事ではC言語の関数のみを扱います.また,あくまで僕の考えをまとめたものなので,すべての内容が正しいことは保証されません.

関数とは

関数とはなんでしょうか?
具体的なコードを見てみましょう.

1
2
3
4
5
6
7
8
int sum(int a, int b){
return a+b;
}

int main(){
int n = sum(3, 5);
printf("%d", n);
}
1
8

関数sumは,与えた2つの引数の和を返す関数です.超便利な関数ですよね!
「’+’使えばいいじゃん」
……なるほど,ではこれはどうでしょうか?

1
2
3
4
5
6
7
8
9
10
11
12
13
int sum2(int a, int b){
int i;
int w=0;
for(i=a; i<=b; ++i){
w += i;
}
return w;
}

int main(){
int n = sum2(3, 5);
printf("%d", n);
}
1
12

関数sum2は,与えた引数a以上b以下の数字の和を返す関数です.なるほどこれは便利だ!

という感じで,ある一つの機能を持った小さなプログラムが関数だと思ってください.

関数を作る

作る関数を決める

では,関数を作ってみることにしましょう.今作りたい機能は,マイクロマウスを走らせながら迷路(壁)情報を保存し,読み出すことができる機能だとします.
初めにすべきことは,機能の分割です.今言った機能はすごく大雑把な表現なので,どのような関数が必要か考えてみましょう.

壁を何らかの変数(おそらく配列)に格納すると思います.この変数に,任意の1枚の壁を追加する関数が必要そうです.同時に,この変数から任意の1枚の壁が存在するかどうかを調べる関数も必要でしょう.
というわけで,まずは1枚の壁を追加する関数を作っていきたいと思います.

さて,関数を作る時に意識すべきことは,使いやすい引数を設定することです.
壁を追加する時に,どのような命令で壁を指定すると便利でしょうか?

僕は「壁の面する座標と,壁の存在する方角」を引数にすると便利だと考えました.というわけで,関数のプロトタイプ宣言はこのようにしてみます.

1
int addSingleWall(int x, int y, int dir);

引数として追加したい壁のx,y座標,壁の存在する方角を取り,
返り値は壁の追加に成功した場合0,失敗した場合それ以外の数字を返すようにします.

そういえば,引数にある int dir は具体的にどうやって指定すればいいのでしょうか? いまいちわかりにくいので,改良しておきましょう.

1
2
3
4
5
6
7
8
enum Direction {
DIR_NORTH,
DIR_EAST,
DIR_SOUTH,
DIR_WEST
}

int addSingleWall(int x, int y, enum Direction dir);

関数を実装する

では,addSingleWallを実装していきましょう!
まずはとりあえず実装してみます.
(壁情報の持ち方については,旧ブログ記事を参照してください)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned short wall_column[15];
unsigned short wall_row[15];

int addSingleWall(int x, int y, enum Direction dir){
switch(dir){
case DIR_NORTH:
wall_row[y] |= (0x1 << (15-x));
break;
case DIR_EAST:
wall_column[x] |= (0x1 << (15-y));
break;
case DIR_SOUTH:
wall_row[y-1] |= (0x1 << (15-x));
break;
case DIR_WEST:
wall_column[x-1] |= (0x1 << (15-y));
break;
}
return 0;
}

さて,このプログラムは正しく動作するでしょうか?
もちろんお気づきかと思いますが,問題がありますね.引数値として,例えば(0, 0, DIR_WEST)を与えた場合に配列の領域外にアクセスすることになります.もしくは,(-100, 150, DIR_NORTH)などの範囲外を与えた場合にもエラーが返って来ず,恐ろしいことになりそうです.

今回気をつけなければいけなかったことは,次のようなことです.

  • 正しい処理をする
    言うまでもなく,適切な壁を追加してくれないと困ります.
  • 誤った引数が来ても問題なくする
    今回の場合,壁が追加できないような引数が来た場合には何もせずにエラーコードを返すべきです.
    誤った壁を追加したり,配列の領域外にアクセスするといったことは絶対にしないように気をつけます.

以上を踏まえた上で実装を行いました.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
unsigned short wall_column[15];
unsigned short wall_row[15];

int isInRange(int x, int min, int max){
if(x >= min && x <= max) return 1;
else return 0;
}

int addSingleWall(int x, int y, enum Direction dir){
switch(dir){
case DIR_NORTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 0, 14)) return 1;
wall_row[y] |= (0x1 << (15-x));
break;
case DIR_EAST:
if(!isInRange(x, 0, 14)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wall_column[x] |= (0x1 << (15-y));
break;
case DIR_SOUTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 1, 15)) return 1;
wall_row[y-1] |= (0x1 << (15-x));
break;
case DIR_WEST:
if(!isInRange(x, 1, 15)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wall_column[x-1] |= (0x1 << (15-y));
break;
default:
return 1;
break;
}
return 0;
}

壁が存在するかを読み取る関数や,他に必要と思った関数は自分で実装をしてみてください.

関数を拡張する

さて,先程作ったaddSingleWall関数を改良してみましょう.

壁情報を複数個持ちたい場合があるかもしれません.まずは壁情報であるunsigned short wall_column[15]unsigned short wall_row[15]をまとめて構造体にしておきます.

1
2
3
4
struct WallData{
unsigned short column[15],
unsigned short row[15]
}

では,安直に次のように実装してみます.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct WallData wd1;
struct WallData wd2;

int isInRange(int x, int min, int max){
if(x >= min && x <= max) return 1;
else return 0;
}

int addSingleWall1(int x, int y, enum Direction dir){
switch(dir){
case DIR_NORTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 0, 14)) return 1;
wd1.row[y] |= (0x1 << (15-x));
break;
case DIR_EAST:
if(!isInRange(x, 0, 14)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wd1.column[x] |= (0x1 << (15-y));
break;
case DIR_SOUTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 1, 15)) return 1;
wd1.row[y-1] |= (0x1 << (15-x));
break;
case DIR_WEST:
if(!isInRange(x, 1, 15)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wd1.column[x-1] |= (0x1 << (15-y));
break;
default:
return 1;
break;
}
return 0;
}

int addSingleWall2(int x, int y, enum Direction dir){
switch(dir){
case DIR_NORTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 0, 14)) return 1;
wd2.row[y] |= (0x1 << (15-x));
break;
case DIR_EAST:
if(!isInRange(x, 0, 14)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wd2.column[x] |= (0x1 << (15-y));
break;
case DIR_SOUTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 1, 15)) return 1;
wd2.row[y-1] |= (0x1 << (15-x));
break;
case DIR_WEST:
if(!isInRange(x, 1, 15)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wd2.column[x-1] |= (0x1 << (15-y));
break;
default:
return 1;
break;
}
return 0;
}

……どう考えても冗長なコードです(先日部員がこのようなコードを書いていて驚愕しました).
これでは満足行かないので,addSingleWallの引数を次のように工夫してみます.

1
int addSinglewall(int x, int y, enum Direction, struct WallData *);

実装すると次のような感じでしょうか.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct WallData wd;

int isInRange(int x, int min, int max){
if(x >= min && x <= max) return 1;
else return 0;
}

int addSingleWall(int x, int y, enum Direction dir, struct WallData *wall){
switch(dir){
case DIR_NORTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 0, 14)) return 1;
wall->row[y] |= (0x1 << (15-x));
break;
case DIR_EAST:
if(!isInRange(x, 0, 14)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wall->column[x] |= (0x1 << (15-y));
break;
case DIR_SOUTH:
if(!isInRange(x, 0, 15)) return 1;
if(!isInRange(y, 1, 15)) return 1;
wall->row[y-1] |= (0x1 << (15-x));
break;
case DIR_WEST:
if(!isInRange(x, 1, 15)) return 1;
if(!isInRange(y, 0, 15)) return 1;
wall->column[x-1] |= (0x1 << (15-y));
break;
default:
return 1;
break;
}
return 0;
}

まとめ

関数は使いやすさが第一です.自分の使いやすい関数を作り,楽に開発を進めましょう.
機能ごとに関数を分割することでデバッグも楽になるはずです.