開発の効率化2(分割コンパイル)

大規模なプログラムの開発作業では, 1個の巨大なソースファイルのまま編集すると, 余計な手間・時間が費やされがちとなる. 複数の小さなファイルに分割し, 編集作業を楽にしよう.

教科書の該当範囲:第11.2節
ソース分割によって, コンパイルの作業は複雑化しがちとなる. 今回は,その苦労も経験しておこう.
そして次回,unix コマンド make を利用し, コンパイル作業も自動化・効率化する.

ソースファイルの分割

分割対象の準備

ソース分割の対象となるプログラムの準備として, 前回作成した?プログラム ttt を更に大規模・複雑化しておこう. フルスクリーン表示の機能を追加し,ver.6 としたい.

まず,前回のディレクトリにある?ver.5 のソースファイルを 今回のディレクトリに ver.6 としてコピーし,編集を進めよう.

$ cp ~/c-1202/ttt-5.c ./ttt-6.c
$ vim ttt-6.c
まだ,ver.5 まで作れていない場合, こちらを利用できます. コードを充分に解読してから作業してください.

編集作業では,基本的には, ヘッダファイル stdio.hncurses.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(...) { ... }
基礎知識(質疑応答)

上記の目標とするソースファイルの内容について, 色々と新しい記述が登場した:

練習問題

上記目標の「ファイル構成」と「ファイル内容」の通り, 実際に分割ソースファイルを作成しよう.

作業方法としては, 単独のソースファイルを複数のファイルとしてコピーしておき, 各ファイルの内容を編集(主に余計なコードの削除, 少しだけ追加)してゆけば楽でしょう. 例えば:

$ 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 -c -Wall ... も試すべき.

全オブジェクトファイルが完成したら,リンク・実行:

$ 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 も...)
...

これでも同じ結果なので, この一括コンパイルだけ使えば良いのでは?

そもそも,ソースの分割も面倒だったぞ... 同じ結果なら分割する必要は無いのでは?

分割コンパイルの利点

ソースファイルやコンパイルを分割することは, ただ面倒を増やしているだけに見えるかも知れない. しかし,大規模なプログラムを開発する場合には, 多くのメリットがある:

補足

分割ソースの複数個のファイルを 単独の圧縮ファイルとして固めておけば, 持ち運び(リモートワーク利用)等に便利となる.

圧縮や展開の際,作業時のカレントディレクトリ位置に注意が必要. 展開では,既存ファイルが上書き変更されてしまう危険もある.
圧縮コマンドには,tar の他,zip 等もある. 利用方法について,オンラインマニュアル等を調べよう.

次回,コンパイルの作業を自動化し, 開発の効率化を追求します.