4章 関数とプログラム構造
C言語で書かれたプログラムは大抵、大きな計算処理を小さな多くの関数に分割して作成される
その関数群は一つのソースファイルであるか、全体を通して言えば複数のソースファイルに分割されている
そしていくつかの大きな関数からプログラムができることはまずない
なぜこのような構造をとるかと言われると、それにはいくつかの理由がある
知る必要でない部分以外の操作に関する情報を隠す為
次にコードを細分化し変更を容易にするため
などといったものである
ここではそういった関数に関することやそれらからなるプログラムの構造について話していく
4.1 関数の基本事項
関数の基本事項を説明するために、有名なfizzbuzz問題を汚いコードで無駄に関数に分割して考える
今回そのコードは以下のようにfizzbuzzをとく
範囲は1~50までで、その範囲の数を全て格納した配列を用意する
用意した配列を先頭から値を取り出し
fizzbuzzを倍数によって考えそれぞれの結果でフラグとして値を返す
そのフラグをswitchで条件分岐にかけ結果を出力する
#include<stdio.h> #define MAX 50 #define START 1 #define END 50 #define FIZZBUZZ 1 #define FIZZ 2 #define BUZZ 3 void makeArray(int array[]); int calcFizzBuzz(int num); int main(){ int array[MAX]; int i; makeArray(array); i = 0; while(i < END){ switch(calcFizzBuzz(array[i++])){ case FIZZBUZZ: printf("FizzBuzz\n"); break; case FIZZ: printf("Fizz\n"); break; case BUZZ: printf("Buzz\n"); break; default: printf("%d\n",i); } } return 0; } void makeArray(int array[]){ int i; for(i = START; i <= END; i++){ array[i-1] = i; } } int calcFizzBuzz(int num){ if(num%15 == 0){ return FIZZBUZZ; }else if(num%5 == 0){ return BUZZ; }else if(num%3 == 0){ return FIZZ; } return -1; }
今回は無駄にフラグ関係にdefineを使ったりしてみた
そして、例としてあげたプログラムからわかるように関数は
戻り値の型 関数名(引数){ 処理 }
という形をとり、戻り値の型がvoidか存在しない時returnで値を返す必要があり
return 式;
という形をとり、これ以降に処理や計算を記述した場合、その処理は呼ばれることがないため
無限ループや処理の途中で抜けるようなbreakに近い動作をする
また戻り値が必要ない場合、関数内の処理が全て終わると呼出元にその制御を渡す
ある関数で値をあるところでは返し、またあるところでは返さないという様な構造を取る関数はまず作成するべきではなく
そのような関数はさまざまなエラーやバグの原因となるのでゴミになってしまう
4.2 非整数を返す関数
いままでは何も値を返さない、もしくは整数を返す関数を扱ってきた
それでは整数ではない値を返す関数はどうすべきだろうか
今回はひとつの例としてdoubleを返す関数を扱う
そして今回も4.1に続き説明の都合上、無意味に整数で高さと幅を受け取りその面積をdouble型で返す
dirty_s関数というゴミ同然の関数を作り説明する
#include<stdio.h> double dirty_s(int w,int h); int main(){ int w,h; int i; double sum; double dirty_s(int,int); w = h = 1; sum = 0; for(i = 0; i < 10; i++){ sum += dirty_s(w,h); } printf("%f\n",sum); return 0; } double dirty_s(int w,int h){ return (double)w*h; }
今回は今までにない書き方をしている部分がある
それは10行目の
double dirty_s(int,int);
である
これには非整数を返す場合特に必要はないがつけるのが望ましい
というのも、まだこれは短いプログラムでありソースファイルもひとつしかないが
大きなプログラムになると、外部から独自の関数を呼び出した時それが何型を返すのかわからないとすごく面倒なので
型 関数名(型,....);
のように宣言できる
配列の時は
型 関数名(型 [],....);
などとなる
これはコードの可読性を上げるという面でも覚えておくといい
4.3 外部変数
C言語で書かれたプログラムというのは主に変数と関数、それといくつかの外部オブジェクトによって形成されている
ここで紹介する外部変数というものはすべて関数の外で宣言され多数の関数で使用できる
またC言語では関数内に関数を定義することができないので関数はすべて外部的と言える
さらに、外部変数や関数は同じ名前で参照すれば別個コンパイルされた関数でもすべて同じものの参照となる外部リンクという性質をもっている
外部変数は広域的にアクセスすることができるから、引数や戻り値の代用として扱えたりもする
そして外部変数はその名前でアクセスする限り(基本的に)どの関数からでも参照できる
そうした性質を持つため、長い引数リストを使う関数なんかに外部変数を使用するのがよかったりする
便利そうに見えるこの外部変数というもの、しかしこれらの使用には注意しないといけなくて
プログラム構造が悪くなったり、関数間で多くのデータが結合しているプログラム(変更しにくい)になってしまう
ただ一般的に関数の内部で定義する変数はルーチンに入った時に生成されルーチンが終わると消滅するのに対して
外部変数は永久的であり、ひとつの関数の呼び出しで得た値を次の関数呼び出しても保持しているため
2つの関数間でデータを共有する必要があり、互いにもう一方の関数を呼び出すことをしないなら外部変数を使うべきと言える
これらを踏まえてすごく効率の悪いソートアルゴリズムを組んでみる
今回は少しわかりやすいようにスタックの構造をとってソートする
入力は以下のように仮定する(実際には改行で区切って入力される)
6 1 9 8 2 7 4 5 3 10
#include<stdio.h> #include<string.h> #define MAX 10 #define NEXIST -1//すでに探索したもの void init();//初期化処理 void push(int); int pop(void); int getMAX(int []); int res[MAX];//結果を格納する int base[MAX];//入力から与えられた数列を格納する int stack_top = 0; int main(void){ int i; int num,max; init(); for(i = 0; i < MAX; i++){ scanf("%d",&num); base[i] = num; } while(stack_top < MAX){ max = getMAX(base); push(max); } //結果出力 printf("---sort result---\n"); for(i = 0; i < MAX; i++){ printf("%d\n",pop()); } return 0; } void push(int number){ res[stack_top++] = number; } int pop(){ int num = res[--stack_top]; return num; } int getMAX(int array[]){ int max = 0; int position; int i; for(i = 0; i < MAX; i++){ if(max < array[i]){ max = array[i]; position = i; } } array[position] = NEXIST; return max; } void init(void){ memset(res,0,MAX); memset(base,0,MAX); }
今回外部変数との関係で注目すべきは15行目の
int stack_top = 0;
という外部変数と、それを使用している
void push(int);
と
int pop(void);
という関数
これらはstack_topという外部変数を使いつつ、互いに呼び出すことはないので
先ほど紹介した外部変数を使うに適した構造といえる
また外部変数は2つの関数で変化させているが49~52行のpop関数で正しく表示できるように
他の関数での変更が外部変数に適応されているため、データを共有できている
4.4 通用範囲に関する規則
C言語で書かれたプログラムは関数と外部変数が主体であり、すべてを同時にコンパイルする必要はない
プログラムのソースファイルを数個に分けて保存し、以前コンパイルしたライブラリからロードしてもいい
こうしたことを踏まえてここで話すのは通用範囲、つまりスコープに関することである
変数や関数の通用範囲はその名前を使うことのできるプログラムの部分である
つまり自動変数は他の関数で参照できないというのはこれに関係している
特に自動変数に商店を当てるなら、上の理由から別の関数に同じ名前の局所変数があったとしても関係はない
イメージでいうなら、関数はひとつの処理、データの集まりで関数の内部は関数として独立している
それに比べて関数、外部変数における通用範囲はコンパイルされるべきファイルで宣言された点からファイルの終わりまで続く
つまり同じファイルであれば単に名前を参照するだけで呼び出すことができる
しかし、外部変数が定義される前に参照されたり異なるソースファイルで定義されているとextern宣言が必要になる
4.5 ヘッダーファイル
実際にプログラムを組んでいくと、どのような構成にするかは人それぞれであるが
関数ごとにソースファイルに分割したりして可読性を確保する場合がある
そうしたことをしていくうちに、構成で迷うのが複数のソースファイルで共通する処理をどうするかである
処理と書いたが実際には宣言と定義である
これだけはなるべく集中化しておくのがいいだろう
なぜなら共有部分をまとめて宣言定義しておくことで、プログラムを書き換える場合に比較的少ない箇所を訂正するだけで済むから
そこで共通のデータや処理をヘッダーファイル(.h)にまとめておく
4.4のことも含めていうならヘッダーファイルに宣言、定義類を固めておいて
なおかつ、extern宣言しておくことでinclude先で使いやすくなる
4.6 静的変数
ここまで、外部変数や関数の通用範囲やそれらを超える他のファイルなどの外部変数、関数の通用範囲に応じた定義宣言の話をしてきたが
特に外部変数はそれより外の通用範囲に向けて使う使い方と、そのソースファイル内でしか使用しないものがある
宣言したソースファイルないでのみ使用する外部変数はその場所に使用を限定する場合staticをつけることで、オブジェクトの通用範囲をコンパイルされつつ
そのソースファイルの残りの部分にのみ適応できる
これは主に(というかほとんど)変数に使用されるが、関数に適応することも可能で関数に使用した場合その関数は外部から呼び出せなくなる
更にはstaticは内部変数に適応できて、これもその関数内に限定されるが他のものとは違い
関数が呼び出されるたびに生成されるのではなく、一度生成されると存在し続ける
つまり単一の関数においてその内輪で永久的なメモリが与えられる
4.7 レジスタ変数
register宣言をすることはコンパイラにその変数が頻繁に使われることを知らせるのによく使われる
その目的はregister変数をマシンのレジスタに置くことでプログラムを小さく速くできるからで、この指示はコンパイラが無視する場合がある
これらの宣言は自動変数と関数の仮引数に使える
しかしこれは実際のところハードウェアに左右され、また実際にレジスタに置かれるかどうかにかかわらずレジスタ変数のアドレスは求めることができない
4.8 ブロック構造
C言語では関数の中に関数を作るような構造にはできないが
変数においてはブロック構造のような構造をとることができる
具体的には以下のように宣言、定義できる
if(0 < number){ int i; for(i = 0; i < number; i++){ } }
この場合ではnumberが正であるときの分岐でブロック外にiがあったとしてもそれとは無関係になる
またブロック内で定義された自動変数はブロックに入るたびに初期化されるが
static変数であった場合はブロックに最初に入った時に初期化される
仮引数も含めて自動変数は同じ名前の外部変数や関数を隠す働きがあるため
なので以下の場合
int x,y; void function(double x){ double y; }
関数内でxを参照するとそれは仮引数のdouble型xであり
yについても関数内であれば同様のことがいえる
またスタイルの問題として、外側の通用範囲にある変数名を隠すような書き方は避けるべきで
そうでないと混乱やエラーの原因となってしまう
4.9 初期化
初期化については、これまでのコードの中で度々あったがここではもう少し初期化の規則についてまとめる
まず明示的な宣言がない場合は外部変数と静的変数は0に初期化されることが保証される
しかし自動変数とレジスタ変数は値が不定となるためメモリを無駄遣いしていると言える
スカラー変数においては名前のあとに等号と式を置くことで初期化できる
int x = 1;
外部変数と静的変数においては初期化は定数式でなければいけない
初期化は概念的にプログラムの実行時に一度だけ行われる
自動変数とレジスタ変数は初期化が関数やブロックに入るたびに行われる
また自動変数とレジスタ変数においては初期化が定数式とは限らない
前もって決まっていて正しいもの(関数の引数など)であればそれを含んだ任意の式でも構わない
事実として特に自動変数の初期化式は代入文の省略形でありどちらを好むかは趣味の問題である
一方配列では初期化式のリストをかっこで囲み、カンマで区切ったものをつけることによって初期化する
int p_num[] = {2,3,5,7,11,13};
配列のサイズが省略された場合カッコの中の要素数がその配列のサイズとなる
また指定したサイズより配列にたいする初期化式が少ないなら残り要素は0になる
ただしサイズを超えたものはエラーとして処理される
文字列の初期化についてはダブルクオーテーションで囲まれたものになるが
これは実は特殊で以下のようになっている
char hello[] = "hello"; char hello[] = {'h','e','l','l','o'};
4.10 再帰
C言語の関数は再帰が使える
再帰とは関数の処理がその関数自身を呼び出すことである
簡単な例としては、ユークリッドの互除法を再帰を使って実装できる
#include<stdio.h> int gcd(int,int); int main(){ int a = 5; int b = 25; printf("%d\n",gcd(a,b)); return 0; } int gcd(int a,int b){ if(b == 0){ return a; }else{ return gcd(b,a%b); } }
このようにreturnにつけて行うこともできるし、それがなくても再帰は行える
ただし再帰を使う場合、処理中の値のスタックを保持しないといけないためメモリの節約にならないこともある
さらに処理速度の向上もあまり見込めない
しかし再帰を使わないプログラムに比べてずっと書きやすく理解しやすくなることが多い
再帰は特にツリー構造のようなデータ構造に対して有効である
4.11 Cのプリプロセッサ
Cではプリプロセッサによりある程度の言語仕様が与えられる
これがよく使われるのはコンパイル中にファイルをロードするincludeとトークンを任意の文字列で置換するdefineだろう
4.11.1 ファイルの取り込み
ファイルの取り込みは#defineや宣言の集まりを取り扱うのを簡単にする
#include"ファイル名"
あるいは
#include<ファイル名>
はそのファイルの内容に置き換えられる
ファイル名にダブルクオーテーションがついていた場合呼び出しもとのファイルがある場所からそのファイルを探索する<>で囲まれいる場合は標準ライブラリか、後にインストールされたライブラリを参照する
またincludeされるファイルにincludeが存在してもいい
ソースファイルの先頭は共通のdefine文やextern宣言を取り込むため
またはstdio.hのようなヘッダに出ているライブラリ関数のプロトタイプ宣言にアクセスするためでもある
#include宣言は大きなプログラムで宣言を結びつけるのによく使われ、悪い書き方をしているものを防ぐ役目もある
ただ取り込むべきファイルの内容を変更した時はそれを使うすべてのファイルをコンパイルし直す必要がある
4.11.2 マクロの置換
ここで話すことはつまりはdefineである
#define name 置換テキスト
の形で宣言されるマクロ置換はnameを置換テキストで置き換えることを要求する
#define中の名前は変数名と同じ形を持ち、置換テキストは任意である。
通常、置き換えられるテキストは行の後に続き長い場合行末に/をつけることで次の行に続けることが可能
defineを用いて定義された名前の通用範囲はそのソースファイルの最後までである
また宣言中に以前の定義名を使用することもできるが、ダブルクォーテーションなど引用符に囲まれた中や定義名が含まれるものには置換が発生しない
少し前に記述したように置換テキストは任意であるため以下のような書き方ができる
#define forever for(;;)
これでforeverという名前の新しい無限ループが宣言される
また引数をつけた宣言も可能で
#define max(A,B) ((A) > (B) ? (A) : (B))
のように定義すると
int a,b,c; a = 2; b = 1; c = max(a,b);//c == 2
となる
ここでの仮引数は実引数と置換される
引数を首尾一貫して使う場合にはどんなデータ型でもよく
データ型が違ったとしても関数とは違い新たなmaxを宣言する必要はない
そして少し注意が必要な点がある
マクロ置換は置換テキストが展開されるためインクリメント演算子などを含む式が引数として渡されるの良くないことになる
defineだけでなく名前を定義するのであれば#undefも使用できる
undefはこれはそのルーチンがマクロではなく関数であることを保証するために使用する
#undef getchar int getchar(void){ }
引用符月の文字列の中では仮引数は置換されない
しかし置換テキストの中でパラメータ名の前に#がついているならその組み合わせはパラメータで実引数で置き換える形で引用符付き文字列のなかで展開される
これはデバッグ用のプリントマクロを次のように文字列の連結と組み合わせるのにつかえる
#define dprintf(expr) printf(#expr " = %g\n",expr)
一方プリプロセッサ用の演算子##はマクロ展開の最中に実引数を連結する一つの方法であり
以下のように使える
#define paste(front,back) front ## back
こうすることでpaste(hello,world)ならば
helloworldという文字記号を作れる
4.11.3 条件付き取り込み
defineでも条件分岐ができて
例えばhello.hというヘッダーを確実に一度だけ取り込むなら以下のように書ける
#if !defined(HELLO) #define HELLO /*hello.hの内容*/ #endif
本来はまずif行で取り込まれてそこから#endifまたは#elif、#elseまで取り込まれる
またif文中のdefinedはかっこないの名前が定義されているなら1それでなければ0になるというもの
またifが関係している文はこれ以外にもあり
名前が定義されているかというテストをする特殊形式の#ifndefと#ifdefがあり
これらを使うことでC言語にないtrue or falseを擬似的に実装できる
#ifndef Boolean #define Boolean int #endif #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif
またさっきのdefinedのコードを書き直すこともできて
#ifndef HELLO #define HELLO //ここにhello.hの内容 #endif