デバッグ技法の開発
プログラムは,現在においてはあらゆる局面で使用されている.例えば航空機の姿勢制御やエンジン制御.
鉄道の運行制御やクルマの自動ブレーキなど.機械の制御でなくとも,銀行やカードの決済システムなど,
だれがどう考えても,プログラムのエラーなどあってはならないことはすぐに分かるだろう.
プログラミングは,そのようなプログラムのバグを排し,誤りなく作成されなければならない.
その一方で,上に挙げたプログラムはどれも大規模で,数万行以上の大きさをもち,
とても一人で作成できるものではないし,また一度に作成できるものでもない.
これらの難しい問題を解決あるいは軽減するために,様々な手法が考えられてきた.
プログラムの構造の変遷
そもそも,情報という目で見えにくいものの一つであるプログラムを,
バグが入りにくく,多数の人の目で確認したり,開発できるようにするために,
プログラム自体の構造が検討されてきた.
- 構造化
かつてのプログラムは,一つのファイルに上から下に向かってただ処理が流れていくだけのものだった.
これではプログラムの再利用も進まず,したがって,「枯れる」という現象を起こしにくかった.
これに対して,小さな,単機能の関数をたくさん使い,プログラムを「構造化」することによって,
バグの入りにくいプログラムを作ることを目指した.
また,構造化をさらに進めて,関数単位でファイル等に細かく分け,
コンパイルも済ませてライブラリ化することにより,関数単体での
テストを行うこともできるようになった.
- モジュール化
ある機能単位ごとにプログラムを分割し,独立させることで,小さな機能単位での入れ替えやテストが
行えるようにしたもの.ネットワーク分散化させて通信で処理をするなど.
- オプジェクト化
オブジェクト指向プログラミング(OOP: Object-Oriented Programming)では,従来の手続き
に注目するのではなく,処理されるデータがより重要である,との観点から,データをカプセル化して
保護する仕組みを導入した.これにより,非常に大規模なプログラムを高い信頼性で作ることが可能になった.
現在主流(というより常識の)プログラム法.C++, JAVA, Python等,
近代的な言語はみなこの機能を備えている.
プログラムのエラーの種類
文法エラー
文法エラーは,プログラムの文法が間違っているものなので,コンパイラ等が発見できる.
この点で比較的誤りを見つけやすい.
- コンパイルエラーになるもの.
以下のプログラムは,画面にHello, world!と表示しようとするものであるが,
コンパイルエラーになる.問題点を指摘して修正しなさい.
#inklode <stdio_h>
int main{} {
printf(Hello, world!\n")
}
- リンカエラーになるもの
以下のプログラムは,2つの整数の和を計算して表示しようとしているが,
リンカエラーになる.問題点を指摘して修正しなさい.
// 2つの値の和を計算する
#include <stdio.h>
int wa(int a, int b);
int main() {
int x = 2;
int y = 3;
printf("answer = %d\n", wa(x,y));
}
論理エラー
論理エラーは,コンパイルエラーにはならないが,計算結果が違ったり,
オーバーフローや0割りなど,計算が途中で終わってしまうものまである.
散々繰り返し計算を行った後でないとおかしな結果にならない場合などもあり,
一般的にバグ追跡は難しいことが多い.
- 以下のプログラムはどちらも,整数の積を計算して表示しようとしているが,
結果は正しいでしょうか?
問題点を指摘して修正しなさい.
#include <stdio.h>
int main() {
char x = 3 * 100;
printf("answer = %d\n", x);
}
#include <stdio.h>
int main() {
int x = 1/3 * 100;
printf("answer = %d\n", x);
}
- 以下は階乗を計算するプログラムである.これは正しいだろうか? 誤っているところを指摘しなさい.
int kaijo(int n) {
int y = 1;
for(int i=0; i<n; i++) {
y *= i;
}
return y;
}
#include <stdio.h>
int main()
{
int N = 10;
printf("answer = %d¥n", kaijo(N) );
}
さて,それではこれらのプログラムのデバッグ手法について考えてみよう.
デバッグの手法
コンパイラのエラーをよく読む!
コンパイルエラーになるとき,コンパイラがいろいろなメッセージを出してくれている.
まずはこれをしっかりみて,何を言っているか理解しよう.メッセージは英語であることもある.
また,エラーがたくさんでることがある.これは一つのエラーが以後の多数のエラーを誘導するからである.
したがって,たくさんエラーが出ているときは,一番最初のエラーから見ていく.
また仮にエラーでなくても,エラーが疑わしい場合は,Warningが出る.
このWarningメッセージもよく読み,できれば出ないように問題点を潰しておきたい.
一つのバグで多数のエラーを引き起こす例:
#include <stdio.h>
int main()
{
for(i=0; i<100; i++){
int x = i*i+ 1;
printf("x(%d)=%d¥n", i, x);
}
}
printfデバッギング
これは計算の途中経過を画面に出力して,どこに原因があるかと探る方法である.
一番初歩的だが,強力なデバッグ手法である.ただし,リアルタイム制御のプログラムなど,
この方法ではデバッグできないものもある.
printfデバッグ法により,上記の問題点を見つけてみよう.
// kaijo
#include <stdio.h>
int kaijo(int n) {
int y = 1;
for(int i=0; i<n; i++) {
y *= i;
printf("debug: i=%d, y=%d\n", i, y); // ここに途中経過表示のprintfを追加
}
return y;
}
int main()
{
int N = 10;
printf("answer = %d¥n", kaijo(N) );
}
プリプロセッサの利用
printfデバッグは良い方法だが,本来不必要なprintf文をプログラムに追加
すこととで,本来の動作が損なわれてしまうという問題がある.
したがって,デバッグ終了時にはprintf文を取り除きたい.
ただし,これを全て手動で行うと,次のような問題がある.
- どこに問題があったか後でわからなくなる.(実はデバッグの痕跡は残しておきたかったりする.
- デバッグ用のコードを追加/除去する過程で,別の誤りを導入してしまうことがある.
- 純粋に面倒.デバッグ終了後の後始末なんてしたくない.
このような問題の対策の一つとして,
コンパイラの機能の一つである,プリプロセッサを用いて,デバッグ用のコードの
ON/OFFを切り替えることができる.
#if defined(DEBUG).... #elif .... #else .... #endif
// kaijo
int kaijo(int n) {
int y = 1;
for(int i=0; i<n; i++) {
y *= i;
#if defined(DEBUG)
printf("debug: i=%d, y=%d\n", i, y); // ここに途中経過表示のprintfを追加
#endif
}
return y;
}
#include <stdio.h>
int main()
{
int N = 10;
printf("answer = %d¥n", kaijo(N) );
}
#if 0 .... #elif .... #else .... #endif
プリプロセッサを用いて,コードの中の有効,無効を制御することができる.
// kaijo
int kaijo(int n) {
#if 0
int y = 1;
for(int i=0; i<n; i++) {
y *= i;
printf("debug: i=%d, y=%d\n", i, y); // ここに途中経過表示のprintfを追加
#endif
}
return y;
}
#if defined(TEST)
#include <stdio.h>
int main()
{
int N = 10;
printf("answer = %d¥n", kaijo(N) );
}
#endif
コンパイルスイッチなどで切り替える
上記の定義を有効にするには,プログラムの先頭で,
#define DEBUG
などとするか,もしくはコンパイルスイッチを用いて
bcc32 -DDEBUG test.cpp
などとする.
assert関数の利用
Cの標準ライブラリ関数には assert() という関数が用意されている.
この関数は,
引数が真であるとき何もしないが,
引数が偽であるときにはエラーを出力してプログラムを強制終了する.
したがって,プログラマの意図として,ここではこの値が必ずこうなってなくてはならない,
と考えている値を書いておく.これはあとからコードを読んだとき,
プログラマの意図がどうあったかを記録する手段
としても役に立つ.
// kaijo
#include <stdio.h>
#include <assert.h>
int kaijo(int n) {
assert(n>0); // nは常に0以上でなければならない.
int y = 1;
for(int i=0; i<n; i++) {
y *= i;
assert(y>0); // yは常に0以上でなければならない.
}
return y;
}
int main()
{
int N = 10;
printf("answer = %d¥n", kaijo(-N) );
printf("answer = %d¥n", kaijo(N) );
}
try-catch (C++)
C++特有の機能で,例外(異常)が起こったとき,
例外の種類に応じて処理をする機能を書いておくと,
そのコードに強制的に処理を移行(catch)できる機能.
assert()はそれが機能すると,必ずプログラムが強制終了させられてしまうが,
飛行機の制御など,終了してしまっては困るプログラムの場合,
プログラムを止めることなくエラーに対処する機会を提供する機能である.
ユニットテスト
プログラムの小さな機能単位(Unit)でテストを実行するための機能.
多くの場合,ライブラリとして提供されている.
有名のものは,Googleが提供する GTest などがある.
デバッガ,統合開発環境(IDE)の利用
printf文は,その実行速度の遅さゆえ,リアルタイム制御のプログラムなど,
速度が重要なプログラム等の場合,デバッグに利用できないことも多い.
そのような場合には,デバッガが用いられる.
組込プロセッサなどの場合には,ハードウェアデバッガが,
それ以外の場合には デバッガプログラムが用いられる.
デバッガプログラムには,gdb など,多くの種類がある.
これらのデバッガでは,プログラム中の変数の値がわかったり,
変数の値の変化を捉えて一旦停止させ,一行ずつ実行させるなど,
様々な機能が提供されている.
これらのデバッガ機能を内包した,統合開発環境を用いてプログラムを
作成することも多い.有名な環境の例としては,Windows用の Visual Studio や,
MacOS用の xcode,Linuxなどでよく用いられる eclipse などがある.
Git, GitHub, CI
皆さんは,今まで沢山のプログラムを作ってきたので,
バグのないプログラムを作ることが如何に難しいか,肌身で感じたでしょう.
良いプログラムを作ることは誰にも難しいのです.
規模の大きなプログラムや,人命に関わるプログラムを作ることは尚の事大変です.
このような難しいプログラム開発を,今までは,
- 最初に設計し,設計通りに一気に作る
(=ウォーターフォール(waterfall)式の開発)
ということをしていました.
しかし,このようなやり方では,顧客が満足するようなソフトウェアが
作れないことがだんだんわかってきました.最初に頭のなかで考えた
プログラムは実際に作って使ってみると,使いにくさが表面化してしまうのです.
そこで,
- プログラムを最初に立てた計画通りに一気に作るのではなく,
徐々に少しずつ,でも常に安定して動くことを優先にした開発
(=アジャイル(ajail)式の開発)
に変わってきました.
現代のフマフォなどで動作しているアプリのほとんど全てはアジャイル開発方式を
採用していると思って良いでしょう.
これらの開発法を支えるのは,バージョン管理システムです.
最も有名なものに,
Git(ギットと読む)があります.
Gitはまた,
GitHubに代表されるように,
クラウド上のサーバを利用することによって,複数人による分散開発もサポートします.
近年のほとんどのソフトウェア開発はこの技術によって支えられていると
言っても過言ではないでしょう.
GitおよびGitHubについて調べて下さい.
また,Gitが使えるようになるように自分で勉強してください.
Quiz 13
- 以下は入力された文字列の中から,大文字だけと小文字だけをそれぞれ抜き出して表示するプログラムである.
これは正しいだろうか? 誤っているところをソースコード中にコメントで(// 問題点の指摘)の様に指摘し,
正しくなるように修正しなさい.また,デバッグに用いた様々な手法(#ifdef, printfなど)の痕跡は
できる限り残しなさい.
-
以下の問題は,半径 r の円を描くプログラムである.このプログラムは未完成であり,
誤りがある.これを正し,綺麗な円を描くように修正しなさい.
int main(void)
{
const int dtheta = M_PI/100
float r = 0
scanf("%lf", r);
for(theta=0; theta<=M_PI*2; theta+=dtheta) {
x = r * cos(theta);
y = r * sin(theta);
printf("%f, %f\n", x, y);
}
}
-
以下の問題は,三次元ベクトル構造体を使って,
2つの三次元ベクトルの成すコサイン角を求めるプログラムである.
プログラムが正しく動くようにデバッグしてください.
struct Vector {
int x, y, z;
}
int dot(Vector& v1, Vector& v2) {
return v1.x * v2.x;
}
int len(Vector& v1) {
return v1.x + v1.y;
}
int cos(Vector& v1, Vector& v2) {
return dot(v1)/len(v1);
}
int main(void)
{
Vector v1 v2;
scanf("%f %f %f", &v1.x, &v1.y, &v1.z);
scanf("%f %f %f", &v2.x, &v2.y, &v2.z);
printf("%lf\n", cos(v1));
}
Assignment(課題)13
課題の提出は Online Judge にて行います.
- 提出期限があります.できるだけ早めにやりましょう.
- Online Judge は普通の課題提出方法とは異なり,良い点が取れるまで何度でも挑戦できます.良い点が取れるまで頑張ってやろう.
- わからないことは,早めに Teams で聞こう.問題は自分で解決すること.
- それでもどうしてもわからないときは,毎週月曜日の2時限目にZoomによるオフィスアワーを用意しています.
ZoomのリンクはOh-o! meijiで確認してください.
- Online Judge のページはこちら