大規模なプログラムの開発作業では, 1個の巨大なソースファイルのまま編集すると, 余計な手間・時間が費やされがちとなる. 複数の小さなファイルに分割し, 編集作業を楽にしよう.
ソース分割の対象となるプログラムの準備として, 前回作成した?プログラム ttt を更に大規模・複雑化しておこう. フルスクリーン表示の機能を追加し,ver.6 としたい.
まず,前回のディレクトリにある?ver.5 のソースファイルを 今回のディレクトリに ver.6 としてコピーし,編集を進めよう.
$ cp ~/c-1202/ttt-5.c ./ttt-6.c $ vim ttt-6.c
編集作業では,基本的には, ヘッダファイル stdio.h を ncurses.h に, 入出力関数 scanf() と printf() とを, それぞれ,mvscanw() と mvprintw() とに変更することになる. その他,curses ライブラリ関数を適切に追加してゆく. また,ゲーム本体の処理を main() から Game() に分離する. さらに,これらの変更に適合するよう,Title() の表示内容も修正する.
/* Tic-Tac-Toe 超簡易版 */ // ver.6 フルスクリーン化 /* * 主な変更点: * main() の一部を Game() に移動. * Title() の表示内容. * 入出力処理を curses化. */ #include <locale.h> #include <ncurses.h>// #include <stdio.h>#include <stdlib.h> // ゲーム盤の構造体型の定義 typedef struct { ... } Board; // ゲーム盤などの表示開始位置 #define Y0 3 #define X0 3 // 駒の表示記号(グローバル変数) char *sym[3] = {"・", "○", "×"}; // 空欄,プレーヤ1,プレーヤ2 // グローバル変数(関数の外で定義)は,別々の関数の間で共通利用. // ローカル変数(関数の中で定義)は,その関数の中だけで限定利用. int Title(void) { erase(); mvprintw(Y0+ 0, X0, "+-------------------------------+"); mvprintw(Y0+ 1, X0, "| The Most Simplest TIC-TAC-TOE |"); mvprintw(Y0+ 2, X0, "+-------------------------------+"); mvprintw(Y0+ 4, X0, " 位置の指定は行番号y,列番号x の順ね."); mvprintw(Y0+ 5, X0, " (番号は 0 から数えるよ.)"); mvprintw(Y0+ 6, X0, " 勝敗については自分達で判定すれや."); mvprintw(Y0+ 8, X0, " プレーヤ1:%s", sym[1]); mvprintw(Y0+ 9, X0, " プレーヤ2:%s", sym[2]); mvprintw(Y0+11, X0, " 終了:ここで [Q] or ゲーム中に [Enter]"); mvprintw(Y0+12, X0, " 開始:[Enter]"); refresh(); return (getch()); } ... /* ゲーム盤を表示する関数 */ void Draw(Board *bd) { ...// printf("\n");for (...) { for (...) { mvprintw(y + Y0, 2*x + X0, "%s", sym[Get(bd, y, x)]); }// printf("\n");} printw("\n\n"); } ... // ゲームの本体... main() から移動・変更 int Game(void) { Board *bd; int n; int y, x, player; erase(); mvprintw(Y0, X0, "ゲーム盤のサイズ > "); refresh(); if (scanw("%d", &n) == ERR) return (0); // 数値入力無しだと ERR になるよ bd = New(n); if (bd == NULL) goto ERROR; Clear(bd); player = 1; while (1) { CONTINUE: erase(); Draw(bd); refresh(); while (1) { printw("%s の番 > ", sym[player]); refresh(); if (scanw("%d %d", &y, &x) == ERR) goto END; if (Get(bd, y, x) == 0) break; printw("そこには置けません!!\nもう一度 "); } Set(bd, y, x, player); player = player%2 + 1; } END: printw("中止?(y/n)"); if (getch() != 'y') goto CONTINUE; Free(bd); return (0); ERROR: printw("メモリ確保失敗\n"); getch(); return (1); } int main(void) {/* Board *bd; int n; int y, x, player; */setlocale(LC_ALL, ""); // 日本語を使うよ initscr(); // 端末制御を開始 while (1) { if (Title() == 'q') break;/* printf("ゲーム盤のサイズ > "); ... Free(bd); */if (Game()) break; } endwin(); // 端末制御を終了 return (0); }
コンパイル:
$ cc ttt-6.c -lncursesw -o ttt-6 # システム環境によっては,-ltinfo も必要. $ ./ttt-6 ...
実行結果として,表示方法が変わっただけで, 処理内容・操作方法は,ほぼこれまで通り.
単独のソースファイルの ttt ver.6 を元にして, 複数のソースファイルとヘッダファイルとに分割し, ver.7 を作成したい.
ソース分割の基本方針:
ただし,グローバル変数の定義は,ソースファイルに記述.
これから作成する ttt ver.7 で目標とする分割ソースの構成: (以下は作業結果の予定...まだ実行できませんよ.)
$ cd ttt-7/ $ ls *.c board.c # Board 構造体関連の関数の定義など game.c # ゲーム本体の関数の定義など main.c # メイン関数の定義など $ ls *.h board.h # Board 構造体の定義など game.h # game.c と main.c で共通利用する変数とマクロなど
目標とする各ファイルの内容:(同上.)
$ cat board.h #ifndef BOARD_H #define BOARD_H typedef struct { ... } Board; extern int Get(...); extern void Set(...); extern void Clear(...); extern void Free(...); extern Board *New(...); #endif $ cat board.c #include <stdlib.h> // malloc(),free() #include "board.h" // Board型など int Get(...) { ... } void Set(...) { ... } void Clear(...) { ... } void Free(...) { ... } Board *New(...) { ... } $ cat game.h #ifndef GAME_H #define GAME_H #include "board.h" // Board型など #define Y0 ... #define X0 ... extern char *sym[3]; extern void Draw(Board *bd); // ヘッダ内でも他のヘッダが必要 extern int Game(...); #endif $ cat game.c #include <ncurses.h> // erase(),mvprintw(),等 #include "board.h" // New(),Clear(),等 #include "game.h" // Y0,X0,等 char *sym[3] = { ... }; void Draw(...) { ... } int Game(...) { ... } $ cat main.c #include <locale.h> // setlocale() #include <ncurses.h> // printw(),initscr(),等 #include "game.h" // sym 等 int Title(...) { ... } int main(...) { ... }
上記の目標とするソースファイルの内容について, 色々と新しい記述が登場した:
どちらでも良いので,使い分けてます:
関数のプロトタイプや変数の型だけの宣言. 関数の定義(処理内容)や変数の定義(メモリ領域確保や初期値設定)ではない. 「定義は外部(external,他のファイル内)に記述されてます」ということ.
多重インクルード(#include したヘッダファイル内で さらに #include を続けている場合に発生する 同じヘッダファイルの2回以上の取り込み) によって発生する多重定義エラーを回避するための技 「インクルードガード」.
まず,1回目の #include でフラグマクロが #define されるので, 2回目の #include では #ifndef が不成立となり, そのヘッダファイルの内容は1回だけしか取り込まれなくなる.
例えば,game.c から board.h と game.h を #include しているけど, game.h からも board.h を取り込んでいるので, game.c からのインクルード対象は game.h だけでよいハズ...
確かに,そうなんですが... より大規模・複雑なプログラムの場合, そうした依存関係のすべてを 正確に記述しようとする努力や 間違いを修正する手間を減らしたい訳なのです. 人間の手作業を減らし, コンピュータを活用して自動化・効率化すべき.
上記目標の「ファイル構成」と「ファイル内容」の通り, 実際に分割ソースファイルを作成しよう.
作業方法としては, 単独のソースファイルを複数のファイルとしてコピーしておき, 各ファイルの内容を編集(主に余計なコードの削除, 少しだけ追加)してゆけば楽でしょう. 例えば:
$ mkdir ttt-7 $ cp ttt-6.c ttt-7/main.c $ cd ttt-7/ $ cp main.c board.c $ cp main.c board.h $ cp main.c game.c $ cp main.c game.h $ vim main.c ... $ vim board.c ... $ vim ... ...
今回の作業では, ソースファイルがかなり大きくなってから分割する, という開発方法を採用した. このため,分割作業はとても面倒になってしまったかもしれない. 本来ならば,肥大化しすぎる前に あらかじめ,こまめに分割しておくべきだ.
普通の大規模プロジェクトでは, 開発作業の効率化のため, 最初から(大きくなることを予想して) 複数のソースファイルとして作り始める, という開発方法を採用することになるハズ. ま,予想以上に肥大化してしまった場合には, やはり分割作業が追加で必要になってくるのだが... 面倒なので分割せず,そのまま続行してしまうと, 肥大化が加速し,尚更に面倒が増えてしまうことになるだろう.
分割ソースをコンパイル・実行しよう.
まずは,コンパイル:
$ cc -c board.c # ソース board.c からオブジェクト board.o を生成 $ cc -c game.c # 〃 game.c から 〃 game.o を 〃 $ cc -c main.c # 〃 main.c から 〃 main.o を 〃 $ ls *.o # オブジェクトを確認... board.o game.o main.o # ...できてる
全オブジェクトファイルが完成したら,リンク・実行:
$ cc board.o game.o main.o -lncursesw -o ttt-7 # オブジェクトとライブラリとを連結し,実行形式 ttt-7 を生成 # (環境によっては -ltinfo も...) $ ls ttt-7 # 実行形式を確認... ttt-7 # ...できたー $ ./ttt-7 # 実行 ...
実行結果は ver.6 と同じハズ.
分割コンパイルは面倒くさいぞ... 手間なく,全ソースを一度にコンパイルするには...
$ cc *.c -lncursesw -o ttt-7 # (システムによっては -ltinfo も...) ...
これでも同じ結果なので, この一括コンパイルだけ使えば良いのでは?
そもそも,ソースの分割も面倒だったぞ... 同じ結果なら分割する必要は無いのでは?
ソースファイルやコンパイルを分割することは, ただ面倒を増やしているだけに見えるかも知れない. しかし,大規模なプログラムを開発する場合には, 多くのメリットがある:
同じ関数を作り直す必要はない. printf( ) や sin( ) などのライブラリ関数と同様に, 過去に作成したソースファイルやオブジェクトファイルをそのまま再利用し, コンパイル時に連結するだけで済む.
例えば,今回作成した board.o は, 他のボードゲームアプリ(オセロやチェスなど)の開発にも流用できそうだ.
分割ソースの複数個のファイルを 単独の圧縮ファイルとして固めておけば, 持ち運び(リモートワーク利用)等に便利となる.
$ tar zcvf 圧縮ファイル.tgz ディレクトリ/
$ tar ztvf 圧縮ファイル.tgz
$ tar zxvf 圧縮ファイル.tgz
次回,コンパイルの作業を自動化し, 開発の効率化を追求します.