ソフトウェア開発実践セミナー
I
∼ソフトウエアを残すために
2
土村 展之
[email protected]
http://www.misojiro.t.u-tokyo.ac.jp/˜tutimura/
情報理工学系研究科 数理情報学専攻
2002
年
4
月
25
日
分割コンパイルの意義
•
大規模
/
多人数によるプログラム開発
•
デバッグ時に部分コンパイルで時間短縮
•
関数
/
変数のスコープをファイル単位に制限
•
パーツの再利用
•
Makefile
の利用が前提か
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.2/??2Makefile
とは
•
ファイル生成手順を書き並べたもの
•
make
コマンドで手順を実行
ファイルの新旧を比較して必要なファイルのみ生成しなおす
•
ある程度の手順はあらかじめ登録されている
•
GNU make
をはじめとして書式にいくつかの方言
(独自拡張)
•
タブとスペースの区別を厳密に
•
やはり雛型の利用を
Makefile
利用の意義
•
コンパイル・リンク方法の明文化
•
開発時の分割コンパイルを効率化
•
不要ファイル削除方法を明文化
•
プログラム開発以外にも
L
A
TEX
コンパイル等の応用
•
必要ファイルをアーカイブ(=バージョン管理)
•
テスト実行方法の明文化
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.4/??2core
ができた時の
デバッガの簡単な使い方
(1)
gcc, gdb
の場合
•
コンパイル時のオプションに
-g
または
-ggdb
を加
える
% gcc hogehoge.c -O2 -Wall -g
•
テスト実行(ほぼ速度低下しない)
% ./a.out
•
Segmentation fault
が出て止まると
core
ファイ
ルができる
sh/bash
系では
’ulimit -c’, csh/tcsh
系では
’limit -h’
コマンド
core
ができた時の
デバッガの簡単な使い方
(2)
•
デバッガを起動
% gdb ./a.out core
・
止まった関数の呼び出された引数と、関数の中で
の止まった場所が表示される
・
コンパイルオプション
-g/-ggdb
なしだと、ここ
で表示される情報が少なくなる
・
デバッガ内でのコマンド
list
ソースを表示
where
関数の呼出し関係を表示
変数の値を表示
quit
終了
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.6/??2Java
では
...
例外発生時に関数の呼出し関係を表示させる
try {
...
} catch (Exception e) {
e.printStackTrace();
}
標準入出力とは
•
UNIX
をはじめとする
OS
では
画面とキーボードはファイルの一種!
キーボードの入力終了は
CTRL+D
•
コンソール
(shell)
でプログラムを走らせる時は
標準入力,標準出力,標準エラー出力が利用可能
普通の接続先
file
へ接続先変更
標準入力
キーボード
command < file
標準出力
画面
command > file
(上書き)
command >> file
(付け足し)
標準
画面
(sh/bash)
command 2> file
エラー入力
(csh/tcsh)
command >& file
フィルタ
—
標準入出力を使う
•
1
入力
1
出力ならば
fopen()/fclose()
を使わなく
てよい
—
入力ファイルは1度だけ読み込める
•
画面に表示したものはファイルに保存できる
ファイルに書きたくないものは標準エラー出力を
fprintf(stderr, "..")
C
C++
Fortran
Java
標準入力
stdin
cin
5
System.in
標準出力
stdout
cout
6
System.out
標準
stderr
cerr
多くは
System.err
標準入出力では力不足なのは
•
入力ファイルが2つ以上
•
出力ファイルが2つ以上
標準エラー出力は2つめの出力ファイルとしては不適当
(画面に表示されることが期待されている)
•
入力ファイルを先頭に戻って同じ内容をもう一度読み
込む
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.10/??2パイプの利用
•
% command1 | command2
では
“command1”
の標準出力が
“command2”
の標準
入力につながる
•
% command1 > tempfile
% command2 < tempfile
と結果は等価
•
中間ファイルがディスクに書き込まれなくなるので、
処理が早くなることが期待できる
パイプの実用
後戻りして見る
“command | less”
行単位のソート
“command | sort”
表示しながらファイルに保存
“command | tee outputfile”
データ圧縮
“gzip -cd input.gz | command”
(入力が圧縮済)
“command | gzip > output.gz”
(出力を圧縮)
バージョン管理
/
アーカイブ製作
(1)
•
UNIX
なら
tar.gz
“hogehoge-1.0.tar.gz”
の中身は
“hogehoge-1.0/README”
などとディレクトリを作って
製作
tar cvf - hogehoge-1.0/ | gzip > hogehoge-1.0.tar.gz確認
gzip cd hogehoge1.0.tar.gz | tar tvf
-展開
gzip cd hogehoge1.0.tar.gz | tar xvf
-tar
のバージョンによっては、以下も使用可能
製作
tar czvf hogehoge-1.0.tar.gz hogehoge-1.0/
確認
tar tzvf hogehoge-1.0.tar.gz
バージョン管理
/
アーカイブ製作
(2)
•
MS-Windows
なら
zip/lzh
“hogehoge-1.0.zip”
の中身は
“README”
などとフォルダを作らず
製作
zip -r ../hogehoge-1.0.zip *
確認
unzip -l hogehoge-1.0.zip
展開
unzip hogehoge-1.0.zip
→
手作業ではせず、スクリプトや
Makefile
で
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.14/??2diff
と
patch
•
2
つのテキストの差異を抽出するツール
・
ファイル名2つを指定
・
-r
でディレクトリ名2つを指定
・
-c, -u
で出力形式を変更
•
patch
コマンドで逆の操作
差分と片方のテキストからもう片方のテキストを生成
diff -ur software-20020424/memory_leak.c software-20020425/memory_leak.c --- software-20020424/memory_leak.c Sun Apr 21 12:29:45 2002
+++ software-20020425/memory_leak.c Wed Apr 24 22:52:46 2002 @@ -7,7 +7,7 @@
int *x;
x = malloc(sizeof(x[0]) * 100); - for (i=0; i<101; i++) {
C
言語の種類
K&R (1973)
関数プロトタイプ宣言なし
,
void*
型なし
ANSI (1989)
関数プロトタイプ宣言あり
,
void*
型あり
現在の主流
C99 (1999)
//
コメントあり
,
標準複素数ライブラリあり
これから広まるのか?
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.16/??2汎用パーツを作る人種
•
実コードの抜粋 ≠ 汎用パーツ
•
実コードは汎用化への第一歩
•
汎用化のための手間や複雑化と効率化や信頼性確保の
トレードオフ
•
汎用化しやすい部分を見抜く
•
誰もが汎用パーツを作れるわけではない
汎用パーツ製作に溺れないよう
...
例題
-III
問
:2
つの長方形領域
[x
1
, x
2
]
× [y
1
, y
2
]
と
[x
3
, x
4
]
× [y
3
, y
4
]
に共通する部分
((
面積
)
>
0)
があるかどうかを判定する関数
を作れ。
int is_overlapped(int x1, int x2, int y1, int y2,
int x3, int x4, int y3, int y4)
{
(ここを考えて下さい)
}
以下の動作を期待
is_overlapped(0,5, 0,5, 2,6, 2,6) ==> TRUE
is_overlapped(0,5, 0,5, 6,9, 6,9) ==> FALSE
is_overlapped(0,5, 0,5, 0,5, 5,9) ==> FALSE
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.18/??2x
軸方向と
y
軸方向に分けて考える
•
x
軸方向に射影して、重なりがある。
•
y
軸方向に射影して、重なりがある。
この両方が成り立つときに限って、長方形に重なりがある。
int is_overlapped(int x1, int x2, int y1, int y2,
int x3, int x4, int y3, int y4)
{
return is_overlapped2( x1, x2, x3, x4 )
&& is_overlapped2( y1, y2, y3, y4 );
}
ちょっと脱線
同じ処理を
2
度書くと
...
•
保守性、再利用性が下がる。
バグが見付かるたびに
2
ヶ所を書き直す必要がある。
•
動作確認が困難になる。
後述するテストの手間が増える。
•
下請け関数を独立して使えなくなる。
•
3
次元以上への拡張が困難になる。
•
x
軸と
y
軸に対称な性質を持っていることが、ソース
コードから読みとれなくなる。
(むしろ、異なる性質を持っているように読める。
)
→
冗長性が高いとロクなことがない。
(開発速度は早いかも)
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.20/??2話を戻して
問題を単純化
/*
閉区間
(a,b)
と
(c,d)
に共通部分があるかどうか
*/
int is_overlapped2(int a, int b, int c, int d)
{
(ここを考える)
}
条件チェックを忘れない
#include <assert.h>
/*
閉区間
(a,b)
と
(c,d)
に共通部分があるかどうか
*/
/*
ただし
a < b, c < d */
int is_overlapped2(int a, int b, int c, int d)
{
assert( a < b );
assert( c < d );
return ( c < b && a < d );
}
→ a = b
や
c
= d
の処理も追加できそう。
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.22/??2条件を緩める
#define TRUE
1
#define FALSE 0
#include <assert.h>
/*
閉区間
(a,b)
と
(c,d)
に共通部分があるかどうか
*/
/*
ただし
a <= b, c <= d */
int is_overlapped2(int a, int b, int c, int d)
{
assert( a <= b );
assert( c <= d );
if ( a == b || c == d ) return FALSE;
return ( c < b && a < d );
}
構成の見直し
#define TRUE 1#define FALSE 0
#include <assert.h>
/* 閉区間 (a,b) と (c,d) に共通部分があるかどうか (a<=b, c<=d) */ int is_overlapped(int a, int b, int c, int d) {
assert( a <= b ); assert( c <= d ); if ( a == b || c == d ) return FALSE; return ( c < b && a < d ); } /* 長方形領域 [x1,x2] × [y1,y2] と [x3,x4] × [y3,y4] に共通面積があるかどうか */ /* (x1<=x2, y1<=y2, x3<=x4, y3<=y4) */
int is_overlapped2(int x1, int x2, int y1, int y2, int x3, int x4, int y3, int y4) { return is_overlapped( x1, x2, x3, x4 )
&& is_overlapped( y1, y2, y3, y4 ); }
テストプログラムを書く
#include <stdio.h>
void test(int a, int b, int c, int d, int answer) {
int ret = is_overlapped(a, b, c, d);
if (ret && !answer) {
printf("[%d,%d] and [%d,%d] "
"should not have any overlap.\n",
a, b, c, d);
} else if (!ret && answer) {
printf("[%d,%d] and [%d,%d] "
"should have an overlap.\n",
a, b, c, d);
行ったテストを残す
int main() { test(0,3, 4,9, FALSE); test(0,4, 3,8, TRUE); test(0,8, 3,4, TRUE); test(0,8, 3,8, TRUE); test(0,8, 0,8, TRUE); test(0,0, 0,0, FALSE); test(0,8, 1,1, FALSE); /* 長さ 0 の区間が包括されている */ test(0,1, 8,8, FALSE); test(0,0, 0,8, FALSE); test(0,4, 4,9, FALSE); return 0; } ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.26/??2単体テスト(ユニットテスト)の方針
(1)
•
境界条件を確認するのが重要
•
理想的には網羅的に条件を列挙したい
•
書いたプログラムの各行は、すべからく実行される
べし
•
本体と平行して作る(本体よりも先に作る人もいる)
•
本体にバグが見付かるたびにテストを追加する
•
本体を修正するたびに、以前のテストをすべてパスす
ることを確認しよう
•
エラーがあった時のみ報告させる
単体テスト(ユニットテスト)の方針
(2)
•
自分のバグだけでなく、コンパイラの信じられない動
作が見付かることもある
•
コピー&ペーストをしてたら、どのぐらいテストすれ
ば安心できます?
•
テスト作成に手間がかかりすぎることも
...
•
デバッグの困難な部分だけをテストすればほぼ十分
ソフトウェア開発実践セミナーI∼ソフトウエアを残すために – p.28/??2例題
-IV
2つの区間
[a
, b]
と
[c
, d]
の共通する部分の長さを返す関
数を作れ。
/* [a,b]
と
[c,d]
の共通部分の長さを返す
*/
int intersection(int a, int b, int c, int d)
{
(ここを考えて下さい)
}
以下の動作を期待
intersection(1,5, 2,6) ==> 3
intersection(1,2, 3,4) ==> 0
解答例
#include <assert.h>
#define MAX(a,b) ( ( (a)>=(b) ) ? (a):(b) ) /* 有名なマクロ */ #define MIN(a,b) ( ( (a)<=(b) ) ? (a):(b) ) /* 自作しない */ /* [a,b] と [c,d] の共通部分の長さを返す(a<=b,c<=d の制限付き) */ int intersection(int a, int b, int c, int d) {
int lower, upper, length; assert(a <= b && c <= d);
lower = MAX(a,c); upper = MIN(b,d);
length = upper - lower; if (length < 0) return 0; return length;
}
悪い解答例
#include <assert.h>/* 変数名の悪い例 */
/* [a,b] と [c,d] の共通部分の長さを返す(a<=b,c<=d の制限付き) */ int intersection(int a, int b, int c, int d) {
int i, j, k; assert(a <= b && c <= d); if (a > c) i = a; else i = c; /* i = max(a,c); */ if (b > d) j = d; else j = b; /* j = min(b,d); */ k = i - j; if (k < 0) return 0; return k; }
構成の見直し
/* [a,b] と [c,d] の共通部分の長さを返す(a<=b,c<=d の制限付き) */ int intersection_normalized(int a, int b, int c, int d) {
(以前の内容)
}
/* [a,b] と [c,d] の共通部分の長さを返す(区間 [2,1] はエラー) */ int intersection(int a, int b, int c, int d) {
if ( a > b ) { fprintf(stderr, "intersection: a > b\n"); exit(1); } if ( c > d ) { fprintf(stderr, "intersection: c > d\n"); exit(1); } return intersection_normalized(a,b,c,d);
}
/* [a,b] と [c,d] の共通部分の長さを返す(区間 [2,1] の長さは 0 とする) */ int intersection2(int a, int b, int c, int d) {
if ( a > b ) return 0; if ( c > d ) return 0;
return intersection_normalized(a,b,c,d); }
テストプログラム例
void test(int a, int b, int c, int d, int answer) { int ret = intersection(a, b, c, d);
if (ret != answer) {
printf("error! [%d,%d] [%d,%d] ==> %d, which should be %d.\n", a, b, c, d, ret, answer); } } int main() { test(1,5, 2,6, 3); test(5,1, 6,2, 3); test(1,2, 3,4, 0); test(1,1, 1,1, 0); test(0,0, 0,0, 0); test(-1,-1, -1,-1, 0);