c-lang:関数
プロトタイプ宣言
従来(K & R の時代)のCは引数のチェック機能が弱く、プログラマの目から見てあきらかに誤っているプログラムもコンパイルできてしまった。 そこで、ANSI Cは、引数の型と個数をチェックするという目的で、関数のプロトタイプを用意した。
プロトタイプ宣言は必ず行う。(省略してはならない)
外部関数については、ヘッダ中においてプロトタイプ宣言を行うこと。
main 関数
code:cpp
int
main (
int argc, /* 第1引数 */
char *argv[] /* 第2引数 */
)
{
/* プログラム本文 */
return 0; /* 大抵は0が正常終了値 */
}
ANSIでは、main 関数の型は、必ずint 型になる。(そう決まっている)
引数を利用する場合には、習慣的にargc(あーぎゅしー)、argv(あーぎゅう゛い)を利用する。
このような引数をとるのは、UNIXのコマンドラインの解釈に由来する。
第1引数、int argc; … argv 配列の長さ
第2引数、char *argv[]; … コマンドライン引数の文字列(のポインター)の配列
第2引数は、char **argvと書く時もある。
引数を利用しない場合は、main( void)と書いても問題ない。
データ渡しの方法
値による呼び出し(call by value)
関数の呼び出しにおいて、実引数が示すオブジェクトの値が仮引数に渡される方式。
C言語の引渡し機構は、この「値による呼び出し」である。
そして、関数内で仮引数の値を変更しても実引数に影響を及ぼさない。
アドレスによる呼び出し(call by reference)
関数の呼び出しにおいて、実引数が指すオブジェクトのアドレス値を仮引数に渡す方式。
これは、「値による呼び出し」の一種であるが、オブジェクトの値ではなく、アドレスが渡される点が異なる。
オブジェクトを指しているポインタはアドレス値であり、オブジェクトを参照しているものである。
グローバル変数によるデータ渡し
とくに関数間でデータのやり取りをしなくても、引数と戻り値の代用にすることができる。
変数名の衝突が起こりやすく、変数の値がいつ変化するか分からない。 乱用すべきでない。
戻り値を返す
1個の戻り値を返す
return 文にを与える。
複数の戻り値を返す
return 文では、複数の値を返すことができない。
引数をアドレス渡しにし、その引数の値を戻り値に使う。
文字列を返す
return 文に文字列の先頭アドレスを指定して返す。
関数の書き方
以下のスタイルに従う
code:cpp
関数の型
関数名(
引数1, /* 引数1の説明 */
引数2 /* 引数2の説明 */
)
{
関数の中身
return文(void 型でも必ず書く)
}
特に関数の型と関数名を別の行に書くようにし、関数名が行頭から始まるようにする。
単一のファイル内でのみ使われる関数はstaticで定義すること。
関数の定義
関数は一つだけの機能を果たすように設計する。
関数の機能をコメントする。
main を除くすべての関数はプロトタイプ宣言する。
関数内で使用するデータだけを引数として渡す。
関数の引数は最大7個程度に限定する。
それ以上の数の引数を渡す必要がある場合には、構造体や配列などの構造化データを利用する。
原則として、引数で与えられていない大域変数をアクセスしない。 大域変数をアクセスする関数は、その旨をコメントする。
値を返すように宣言されている関数は、必ず return文で値を返す。
関数内のreturn文の使用は、できるだけ少なくなるように工夫する。
戻り値のある関数を呼び出したルーチンは、戻り値を検査する。
関数の使い方
void 型の関数には、どのような機能を実現するのか必ず書く。
void 型の関数は、前後の関係が読みにくくなるので、原則として使わないこと。
関数の戻り値は、必ずチェックすること。
引数を受けとったら、必ずチェックすること。
引数により、動作が不定にならないようにすること。
例えば、範囲以外の引数を受けとったら、エラーを返すようにすること。
関数は、ひとつの機能をうまく実現するようにしよう。
欲張ってはいけない。
プログラムを読みやすく、早く完成させるには、すでに実績のある関数を多く使うことである。
なにもしない関数
最小の関数は、
code:cpp
dummy(){}
であり、これは何もせず、何も返さない関数である。
こうした何もしない関数は、プログラム開発中に場所を確保するのに有用なことがある。
ポインタによる関数呼び出し
関数名とは、関数本体へのポインタである。
code:cpp
foo()
と
code:cpp
(*foo)()
は同じ意味である。
分岐テーブル
分岐までの時間が一定となる。
分岐数が増えても処理が変わらない。
code:cpp
/* 関数のポインタの配列 */
static void( *function_table[])(void) = {
function_a,
function_b,
function_c,
function_d,
function_e,
function_f,
function_g,
};
int
main( void) {
int fno;
while( 1) /* forever */
{
fno = (int)(toupper(getchar()) - 'A');
if ( -1 < fno && fno < 7) {
/* 関数呼び出し */
}
}
return 0;
}
/* 構造体の宣言 */
typedef struct {
int x1;
int y1;
int x2;
int y2;
int (*func)();
} MOUSE;
/* プロトタイプ */
static int file_menu( int lb, int rb);
static int edit_menu( int lb, int rb);
static int opt_menu( int lb, int rb);
static int help_menu( int lb, int rb);
/* 構造体の配列 */
MOUSE Mi[] = {
0, 0, 40, 16, file_menu,
40, 0, 80, 16, edit_menu,
80, 0, 120, 16, opt_menu,
120, 0, 160, 16, help_menu,
0, 0, 0, 0, NULL,
};
int
main( void) {
int x;
int y;
int rb;
int lb;
MOUSE *m;
/*
* マウスがクリックされたときにマウス状態とボタンの状態を返す関数とする。
*/
MouseWait( &x, &y, &rb, &lb);
/*
* クリックポイントの調査と関数実行
*/
m=Mi;
while( 1) /* forever */
{
if (m->func == NULL) {
break;
}
if ( x > m->x1
&& x < m->x2
&& y > m->y1
&& y < m->y2 ) {
m->func(lb, rb);
break;
}
++m;
}
return 0;
}
再帰( Recursion)
関数内で使用する変数は、自動変数であること。
関数への引数渡しは、「値渡し」であること。
再帰を使う場合、処理中の値のスタックを保持しなければならないから、メモリの節約にはならないことがある。
また、より速くもならないであろう。
しかし、再帰的なプログラムは、よりコンパクトになり、再帰を使わないプログラムに比べてずっと書き易く、理解しやすくなることも多い。
再帰はツリーのような再帰的に定義されるデータ構造にはとくに便利である。
code:cpp
void
printd(
int n /* */
)
{
if( n < 0 ){
putchar('-');
n = -n;
}
if ( n / 10){
printd( n /10);
}
putchar( n % 10 + '0');
}
ラッパー関数 ( Lapper, Wrapper )
サランラップ、ラッピングともいう。
呼び出しを汎用化し、使いやすくするために、オリジナルのルーチンに 被せる(包み込む)インターフェイス関数のことをラッパー関数と言う。
コールバック関数 ( callback function )
関数呼び出しの際に、関数のアドレスをパラメータとして渡すことで、呼び出した関数から自身の提供する関数を呼び出すようにすることがある。
この際の呼び出した関数から実行される関数をコールバック関数と呼ぶ。
ハンドラ( Handler )
「何か」が起きた時に呼び出される関数。
その「何か」に対処(ハンドリング)する実装を持つ。
通常、特別な構文はなく、意味的なもの。
通常、ハンドラ関数はプログラマーが作り、デフォルトのものと置き換え、「何か」が発生したときに自動的に呼び出される。 よって、プログラマーが直接呼び出すことはない。
例外ハンドラのような「関数ポインタを渡す」ものと、 MFC メッセージハンドラのような「仮想関数やマクロを使用する」ものが主なハンドラ。
ハンドル( Handle )
ウィンドウズオブジェクトへのポインタの総称。
ウィンドウズのオブジェクト、たとえばウィンドウやアイコンなどは、ウィンドウズが管理しておりアプリケーションが直接操作することはできない。
代わりにこれらのオブジェクトへの特別なポインタ型、たとえば HWND や HICON をAPIに渡すことでウィンドウズに操作してもらう。
このポインタ型のことを「ハンドル」と呼ぶ。
MFC の CWnd などは、これらハンドルのラッパークラスであり、ハンドルを内部に持っている。
そのため、実際にハンドルを使用する機会は少ない。
可変個引数を持つ関数
簡単に思いつく方法としては、デバッグ表示用の関数を作っておいて、そこで出し先を変えたり、複数に出したり、出さなくしたりする方法です。
基本的にはこれで良いのですが、できれば、printf()のように書式を指定してそれに応じた引数を与えて送りたいものです。
そうでないと、文字列以外の物を表示させたい場合には専用の関数を用意するか、sprintf()などで一度文字列にしてから渡す必要があり、大変です。やはり、使い慣れたprintf()形式で行きたいものです。
そこで登場するのが、先程の可変引数です。
これが使えればprintf()のように自由な個数の引数が受渡しできます。
では、早速作ってみましょう。可変引数の関数を使う場合には必ずvarargs.hをインクルードする必要があります。
code:cpp
int debug_print(va_alist)
va_dcl
{
va_list args;
char *fmt;
va_start(args);
fmt=va_arg(args,char *);
vsprintf(buf,fmt,args);
va_end(args);
fprintf(stderr,"DEBUG %s \n",buf); return 0;
}
全く変な書き方ですが、関数の引数としてva_alistを指定し、引数の型を宣言するところでva_dclと書きます。 ここにはセミコロンも付けません。 そして、関数内で、va_list型のargs(名前は自由)を宣言します。
引数を得る前に必ずva_strart(args)でargsが先頭を指すように初期化します。
そして、va_arg()を使う度に順に引数が得られます。 va_arg()では取り出す型を指定します。
char *と書けば文字のポインターという型で取り出されます。
最後はva_end()で可変引数の処理の終りを示します。
この例ではvsprintf()を呼び出す為に最初の書式の部分だけ取り出して、後はそのままvsprintf()に渡しています。
vsprintf()によって書式通りに展開してもらってbufに格納してもらい、それをDEBUG[]で囲って標準エラー出力に出しています。
可変引数関数に対して、char、floatの値を引数として渡せるか?
char, float としては渡すことが出来ない。
可変引数の場合、char/short は int に、float は double に変換される。
例えば、printf で int 値と char 値を両方とも %d で受けられるのは、双方とも受ける側が int にしか思っていないからである。
ちなみに、可変引数関数に限らず関数に対して char、float、short の値を引数として受け渡すことができないのは K&R 時代の C 言語の仕様であり、ANSI C ではプロトタイプ宣言により時代遅れになったはずなのだが、可変引数関数に限っては互換のため残ってしまっている。
また、関数プロトタイプがない関数の引数も同様の扱いを受ける。
printf関数
一行に表示されるメッセージは、原則として一つの printf関数呼出しで 書く。
複数行に渡るメッセージを一つのprintf関数で出力しない。
code:cpp
void
debug_print (
char *message,
...
)
{
va_list ap;
va_start (ap, message);
fprintf (stderr, "\nError: ");
vfprintf (stderr, message, ap);
fprintf (stderr,"\n");
va_end (ap);
}
データ入力について
scanf関数を用いた複数データの一括入力は、エラー処理機能が不十分な ので、できるだけ避ける。
文字データは scanf関数ではなく、getchar関数で読む。
文字列データは、scanf関数ではなく、fgets関数で読む。
関数の作成
以下に、プログラム作成を行なうための系統的な手順を示す。
実際のプログラミングの際に参考にすると良い。
1. 関数の設計と検査
プログラムの入出力を明確にする。
プログラムに名前をつける。
アルゴリズムを(日本語で)記述する。
アルゴリズムに必要なデータを定義する。
以上の作業を反復確認する。
2. 関数のコーディング
プログラムの宣言部を書く。
アルゴリズムの各行を高水準のコメントとする。
それぞれのコメントの下に対応するコードを埋め込む。
アルゴリズムとコードが一貫しているか確認する。
以上の作業を反復確認する。
3. コードの検査
プログラム(関数)を机上で実行する。
コンパイルエラーを修正する。
プログラムを実行してテストする。
エラーが発見されれば、デバッグを行なう。
直前のステップが完了してから、次のステップに進むこと。
モジュール化 (modularization)
1. モジュール化の概念
設計作業とは「分割;分解」と「組み立て;統合」である。
理解が難しい時解る大きさまで分割して理解する。
おおもと -> 主モジュール
分解したもの -> 下位モジュール
トップモジュール -> 概念的
下位 -> 段階的に詳細(抽象化の度合が低くなる)な記述になる
↑※トップダウン(プログラミング)手法
1. 手続きに着目して階層構造化した場合。(下位になるほど他のモジュールとの相互作用が大きくなる)
2. データに着目してモジュール化を進める場合。
分割は1機能1モジュールとする方法をとるとよいとされる
つまり 『階層的機能分割:モジュラプログラミング』
☆モジュールは、他のモジュールとの相互作用のない独立したプログラム単位を考慮する。
組み立ては下位の構成要素であるモジュールを合成する作業
2. モジュール間のインターフェース
Key-word 呼び出し、パラメータのやりとり(引数)、戻り値
抽象化
1. 抽象化とは
『抽象』(abstraction)
複雑な現象を理解して行く過程において、人間の理解力の助けになる最も強力な道具である。
現実界における特定の範囲の『対象物』『状態』あるいは『過程』などの間の類似点を認識し、それに注意を集中することに決め、相違点はとりあえず無視することからはじまる。
類似点のなかでも、どれが将来の事象の予測や制御に関して重要であるか明らかになればその類似点こそ基本的なものである。
その他の差異は不要のものとみなす。
2. 抽象化の目的
複雑な問題、あるいは単純でも大規模な問題を、『抽象化記述』して、簡潔に表現された本質をとらえることが容易に理解出来るようにかえていくこと。
3. プログラミングの方法論
※抽象化の考え方
機械語のアセンブラによる記号化
変数をひとまとめにした配列による抽象化
関連データを1つにまとめたレコードや構造体
プログラム言語のintやfloatなどの「データ型」は整数や実数一般を抽象化したものである
関数やサブルーチンなどはプログラムの構造を解りやすくするためのツールである
4. 表記法
抽象化されたモジュールは内部の詳細な処理を隠してくれるため「プログラム全体を見通す事が楽」になると同時に、「汎用性」を持たせる事が容易である。 プログラムの分解によって似たような処理をするモジュールは別々に作らずに1つのモジュールにまとめて共用するのがよい。
汎用性のある抽象モジュールはライブラリに登録しておけば「再利用」できる。
5. 情報隠蔽
情報隠蔽(information hiding 1972 Parnasの提案 必要な情報以外は全て隠してしまう記述方法)
情報隠蔽とは、モジュラー間の情報(データと機能)の相互作用を取り除くため、モジュールを利用するための必要最小限の情報だけを見せ、それ以外の詳細は外部のモジュールからアクセスできないように隠してしまう。
これはモジュール間のみでなくモジュール内部のブロック間にも適用される。
※ブロック...「プログラム構造単位」のことモジュールや関数も一種のブロックである