C言語進化論
「サルでもできるC言語」から
C言語入門までの
ミッシングリンク。
紐解くことになりますかや。
注意)ココで書かれている内容は、必ずしも正しいものではありません。実際下の3項目は「手続き」などと書いていますが、「3構造」の間違いです。
基本3項目
C言語に限らず、すべての言語に共通する、言語基本3項目というのがあります。
それは、以下の三つ。
- 式を計算する
- if文(条件文)
- for文(くり返し文)
実は、これら三つの項目「のみ」を用いることによって、すべてのアルゴリズムを、
解くことができることが、「数学的に証明されている」のです。
つまり、プログラムを組むには、この3要素をつかっていれば、「書けないプログラムはない」のです。
(正確には、1.は、「式を上からしたまで、順に計算する」というのが正しいです。これを「連接」といいます。)
よって、言語特有の、switch文、do-while文、goto文などは、「必要ない」のです。
よって、これからは、これら3項目のみを使うことにします。
ちなみに、これらで書いた一通りの集まり(上の3つ)は、「手続き」と呼ばれます。
関数
よく、C言語は「関数によって、構成されている」、などどいいます。
たしかにこれは正しいのです。
C言語は、一番初めはmain関数から処理がスタートし、その処理の途中で、他の関数を呼び出しています。
しかし、C言語は「関数型言語」では、「ない」のです。
C言語は、分野としては、「手続き型言語」なのです。(前節にも「手続き」という文句が出ましたね。)
関数型というのは、純粋に数学的な関数、y=f(x)なんかの、形式を持ったものをいいます。たしかに、C言語でも、y=f(x)などど書くことができますが、数学的「関数」と決定的に、ちがった能力を持っています。f(x)は数学的には、xのみに依存する関数なのですが、C言語では、x意外に依存してもいいのです。
f(x)とはまったく関係ない変数、たとえば、zなどに「依存してもいい」のです。
つまり、「C言語の関数」は、「数学的な関数」をもっと、拡張して、自由ふんだんに使うことが、できるものなのです。
もっといえば、たんに、「値を返す」という関数の性質「のみ」を持ったものなのです。そして、C言語は関数の値をかえすまでに、関数の中で「手続き」を行っています。要するに、BASICでいう、サブルーチンに、「値を返す」という性質のみを付け加えたものなのです。ちなみに、「値を返さない」でも、いいのです。(このときvoid関数と呼ぶ。voidは値のないの意。)
以上で見てきたように、C言語の「関数」とはかなりいいかげんなものです。
「値を返しても返さなくてもいい手続き」の意が、ぴったりくるでしょう。
ちなみに、「関数型言語」には、ML, Miranda,
Mathematica などが、あります。
C言語ってどうやって動かすの?
では、C言語の基本文法(前述した3項目)、でどんなことができるの?という、疑問がもたげくるでしょう。
ゲームなんかを見ていると、画面がスクロールしたり、フェードアウトしたりしているのを、見た事があるでしょう。
あーゆうのも、C言語でできるの?何てことも思いますでしょう。
答えをいいますと、...、「できません」。というのが相当でしょう。
しかし、「無理をすれば」できます。と、いうこともできます。
曖昧ですが、「無理をすれば」の方は、インラインアセンブラや、ポインタなどの概念、これらは普通「低水準」などと呼ばれるものを使って実現されています。(「低水準」というのは、人間が認識できる基準で見たもので、人間の言語に近いものを高水準、機械の言語に近いものを低水準とよびます。つまり、低水準といっても人間が使うのは神業にちかいのです。)
つまり、この方法は、「現実的でない」のです。
普通は、この問題を解決するには、「ライブラリを使う」のです。こうすると「現実的に実現できる」のです。
ライブラリとは、「ライブラリ関数」と呼ばれる「関数群」です。これも、C言語の関数なので、C言語から「簡単に」呼ぶことができるのですが、その実態は、アセンブラや低水準C言語を使ったものです。つまり「現実的でない」の部分を「他人が実現してくれている」のです。こういうのは、フリーでも、販売してあるのもあります。たとえば、BIO100%のmaster.libや、マイクロソフトのWinG、DirectXなどがあります。
ユーザーは、これらライブラリをC言語の手続きの途中で、順に呼び出してその機能を使い、スプライトを動かしているのです。もちろんその中では、前述の基本3項目を使い倒しています。
結局、プログラムとは、基本3項目とライブラリ関数で構成されているのです。あと、「ユーザー定義関数」も、サブルーチンを書く時など使用します。
ライブラリって何?
よく童話の世界に出てくる妖精さん達です。彼らは、非常に働き者で、こっちの縦横無尽な命令にしたがって、しこしことコンピュータをこっちの代わりに動かしてくれる、大切な使用人です。
彼らのおかげで、僕等はただ単に実現したいフィーチャーを、組み上げるのに必要な、「命令」を順番に下すことだけで良いのです。もちろんこの「命令」を下す順番を考えるのは、僕たちですが、直接コンピュータを動かすよりももっとずっと簡単になり,ます。
ここで、僕たちが下す「命令」とは、「手続き」のことで、それはまた、C言語の世界では、「関数」となります。
でも、僕たちが書くC言語プログラムも、「関数」で構成します。そこで、僕たちの書く「関数」と、ライブラリ「関数」との違いはどうなるか疑問に思えてくるのですが...。答えは簡単です。「どちらも同じ関数」なのです。しかしそれでは困るので、一応区別しておきたいというのが人情です。簡単に言えば、「自分で作った関数」を「ユーザー定義関数」、それ以外を「ライブラリ関数」と考えておけばいいでしょう。でも、文法上は両者はまったく等価です。つまり、僕たちも「ライブラリ関数」を書くことができるのです。これはCの場合よくあることで、させる仕事にそんなに区別のない定型処理なんかを一度作っておいて、それをあとに「ライブラリ」ってことで残して、のちのちの定型処理のときに再利用するのです。
ですが、ライブラリ関数はふつー常人ができないような高度なテクニックを使っていたりしてますので、1ユーザーがはじめからライブラリ関数を自作するより、ライブラリに頼ったほうがよりパフォーマンスがあり、有用でしょう。(そうでない人たちもいますが)
ちなみに、コンパイラは、ライブラリ関数を認識していて、既にたくさんあるライブラリ群のなかから、ユーザーが使っているライブラリのみを取ってきて、ユーザーのプログラムにくっつけます。賢くできていますね。
続・関数ってなに?
C言語の関数には複数の側面があります。取り敢えず今回は、サブルーチン敵な側面を考えてみましょう。
さて、関数はサブルーチンであると考えます。これは、一番簡単な場合のC言語の関数の使い方です。
では、以下にその関数(サブルーチン)を「呼び出す」ところを見てみましょう。
get_it();
これだけです。「get_it」は、関数名であり「;」は、文の終わり(ここでは「関数呼び出し文」の終わり)を示します。さて注目は、(、)です。見てのとおり、「フツーの数学的関数」
y = f(x) の、「y」と「x」がありません。「こんなんでも関数といってよいのか?」なんて声が聞こえてきますが、「こんなんでもいい」んです。ここで、get_itは、引数xを(必要がないので)省略し、また、結果のyは(のちのち使うこともないので)省略しています。
上で、「呼び出す」と書いていますが、実際にはどうなるのでしょうか。答えは、それまでの「処理の手続き」の「流れ」をいったん停止し、今度は「get_it」関数の示しているサブルーチンの手続きをはじめます。で、無事「get_it」の手続きがすべて終わったら、再び、以前の処理の流れに戻り、何事もなかったように、手続きを続けます。
式の評価(計算)と評価バッファ
で,基本3項目の一番目に出てくる式の評価(計算)ですが,これは非常に重要です.
普通はコンピュートすることは計算することと訳されますが,単純に四則演算をするわけだけではありません.「手続き」も計算されます.
式の評価で重要なことは評価バッファの存在を考えることです.バッファとは「物をためるもの」といったところでしょうか.式の計算途中の計算結果をためるものです.
例を見てみましょう.
a = 3 + 2 * 6;
ここで「=」は「右の式の計算結果を左の変数に代入する」という意味です.数学で書く「=」は方程式の両辺が等しいことをあらわしますが,Cではこのように違った意味に用いられます.要約するなら「代入」です.なお,「式の両辺が等しい」ことはCでも使いますので用意されています.「==」です.「a
== b + 1」などのように用いられます.
で,上述の式ですが,演算子の優先順序により,掛け算が先に「評価」されます.2*6の結果は「12」です.そして「3+ 」の部分を計算するのですが,その足し算の右の対象は,先ほど計算された「12」です.しかし,式の中では「12」などは出てきません.これは計算途中に出てきたものですので,明示的には表されません.つまり,この無名の変数というべきものが「評価バッファ」に代入され,次の計算式で「3+ 評価バッファ」として計算されるのです.そして結果の「15」を変数
a に代入します.
これが評価バッファの使われ方です.
では,「手続き」はどのようにして計算されるのでしょうか.C言語で言う「手続き」とは「関数」のことでした.そして関数の式は「get_it()」でした.実はこのときの関数名の後ろにある「(,)」が,「演算子」なのです.これは「後置演算子」と呼ばれます.「後置演算子」には「配列」などがあります.たとえば「hairetu[0]」などです.これは,[,]が演算子で,配列「hairetu」の1番目の要素を取り出す式です(C言語では配列は0から始まります).
では,関数演算子「(,)」はどのように計算するのでしょうか?簡単です.関数名「get_it」の関数を実行し,その戻り値を評価バッファに代入する.ということです.関数には戻り値があっても良いことを思い出してください.この辺の動作は,数学的関数と同じですね.「y=f(x)」は引数xに対応する関数「f」の戻り値が「y」となるということです.もちろん,関数は手続きとしても用いられますから,戻り値がなくてもかまいません.「void」関数を思い出してください.値を返すことはできません.ここでは評価バッファにvoidが入ることになり,エラーとなります.この辺はコンパイラもわきまえているので,式の計算途中にvoidがあるぞ!といって警告を発します.では,void関数はどこに置いたらいいのでしょうか?簡単です.単独で,「ThisIsVoid():
」と置けばよいのです.
また,たとえ手続き的関数であっても,戻り値を返すことが好まれます.これは手続きの途中でエラーが発生したことを呼び出し元に知らせるためです.次の例を見てください.
ret = tetuduki( hikisuu );
if( ret == 1 ){ error(); }
まず関数「tetuduki」を引数「hikisuu」で呼び出し,その戻り値を,変数「ret」に代入しています.次にif文でretが1と等しいかどうかチェックしています.そして,retが1の場合,関数errorを呼び出しています(iif文のあとの{,}で囲まれた部分はifの条件式「ret==1が正しい場合実行されます.それ以外では無視されます).
変数のタイプと演算子のオーバーロード
さて,これまで変数,変数といってきましたが,この変数とは何でしょう?数式では一般に整数,有理数,実数などの変化する値でしたが,C言語では,変数のタイプによって使い分けが行われています.以下は一部だけ抜粋し,そのタイプとその意味を示します.
char:8ビット整数
short:16bit整数
int: コンピュータのCPUが16bitの場合16bit整数,32bitの場合,32bit整数.
long:32bit整数
float:32bit浮動小数(実数)
doubele:64bit浮動小数(実数)
このように,char, short, int, long, float,
doubleなど,いろいろな名前が出てきますが,これらのタイプを持つ変数は,使用する前に,「宣言しなくてはなりません」.
宣言は簡単です.たとえばint型の変数を名前 hensu で宣言したいなら,
int hensu;
とするだけです.これで,変数hensuを利用することができるようになりました.
ただし,「変数の宣言」は,手続きや式を書く「前に」,使用する変数を「すべて」,列挙しておかなくてはなりません.
以下のようにあすることはできません.
main(){
int i;
i = 3;
int j ;
j = i + 1;
}
これは,jを宣言する前にすでにiが式の中に使われているからです.
さて,次に演算子オーバーオードの話をします.普段何気なく使っている数式では,たとえば演算子+の両辺の内で,片方は実数,もう片方は整数とすることを簡単にやっています.しかし,コンピュータ言語では,これは厳密に場合分けされます.
たとえば,int同士の計算では,みなintですから,以下のようにそのまま計算されます.
jint1 = int2 + int3;
しかし,この足し算+の片方がlongの場合はどうでしょうか?
int1 = int2 +long1;
int同士の計算では,int同士を足し算する演算子として+が使われました.次はintとlongを計算しなければならなないのですが,ここでは,int同士の慶安に使われた同じ「+」が使われています.しかし,この同じ表記を持つ「+」は実は上の2例では違う演算子なのです.表記は同じですが,呼び出される足し算手続きが違います.演算子の両辺を引数と見れば,あたかも,plus(
int2, int 3): というような関数が呼ばれることになります.2番目の例では,plus(
int2, long1 ):;です.
このように,表記は同じでも,引数が違う関数のことを,演算子(手続きの)のオーバーロードといいます.それぞれの手続きは違うのですが,プログラム中に出てくる表記は同じなのです.このように,オーバーロードすると,引数のタイプが違っても,簡単に(コンパイラ任せで)違う手続きを呼び出せるので,便利なのです.
んが,しかし,C言語では,演算子のオーバーロードは,組み込みタイプ(前述したchar,
int., long, floatなどの最初からC言語で用意されている変数の型)にしか,用意されていません.C++になると任意の変数について,オーバーロードすることができるようになります.
が,組み込み型の演算子でも十分です.これらは賢くできていて,小さい大きさの数と大きい大きさの数に演算子をかますと,自動的に小さい数を大きい数に変換して計算してくれます.たとえば以下の様にです.
long1 = int1 + long2;
ここで,評価バッファに,int1をlong型にして代入し,long3とする.(評価バッファは必要なときに必要な分だけ作られるので,どんなタイプのものでも代入できる)
long1 = long3 + long2;
結果,計算途中で扱っている変数の大きさがintのサイズを超えても,longとして計算されているので,longの大きさまでの数なら,計算できる.
関数の引数.値渡しの引数と,参照渡しの引数
さて,一般に関数は引数をつけて呼び出されます.これは呼び出しもとの状況の情報を関数側にも伝え,関数に状況に依存した処理を行わせるためです.
たとえば,サイン関数は以下のようにしますね.
y = sin(x);
これはxという引数を渡して,それに対応するサインの値を戻し,yに代入しています.状況とはここの場合xのことです.その時々のxの値によって,戻す値が違ってきます.
もっと一般的な,手続きの「定義」とは以下のようなものです.
void Tetuduki( int x ){
if( x == 0 ) { return 0; }
else {return cos(x);}
}
これは,もし(if)xが0だったら,0を戻す(return).それ以外だったら(else),cos(x)を戻す.と,言う意味です.
このとき,Tetudukiの引数には int x;とかかれています.しかし呼び出し元にはTetuduki(x)としか書きません.
手続きの定義では,引数の型を指定し,手続きの呼び出し元は,暗にもう引数の型がわかっているものと解釈し,型は書かなくても良いのです.もっといえば,引数の型のみしかコンパイラチェックしないので,引数の名前が一致しないでもいいのです.たとえば,int型変数zが状況を表すとして,「Tetuduki(z)」として呼び出してもよいのです.
また,引数の渡し方は,[値渡し」と,「参照渡し」の二つがあります.正確にはC言語には参照渡しはなくて,代わりにポインタ渡しがあるのですが,ポインタは災いの元ですので,ポインタ渡しのサブセットである参照渡しのほうを採用するものとします.(C++にはポインタ渡し,参照渡しともにあります)
実は関数の定義での引数はデフォルトで値渡しとなっています.これを参照渡しに変更するなら,引数の手前に「&」をつけて指定します.
上の例では,
void Tetudiki( int &x ){
if( x == 0 ) { return 0; }
else {return cos(x);}
}
となります.
値渡しと,参照渡しの違いは簡単です.
値渡しは,呼び出された関数内で,その引数の値が変わっても,呼び出しもとの変数の値は変わらない.
参照渡しは,呼び出された関数内で,その引数の値が変わると,呼び出しもとの変数の値も変わる.
以上です.
値渡しは安全です.もし誤って,関数内で引数の値を変えてしまっても,呼び出しもとの変数の値は変わらないので,呼び出しもとの「状況」は変化しません.そのため,呼び出された関数は,与えられた手続きを実行することだけに専念することができます.実際,値渡しでない場合,その関数内で宣言した変数に一度,引数を代入して,引数に変化が出ることを避けなくてはならないので,面倒です.値渡しなら,そんな手間も省けます.
参照渡しは,呼び出しもとの状況を変えてしまうのですが,ここではそのことが参照渡しの有利な点となっています.知ってのとおり,関数はただ一つだけしか,戻り値を戻せません.しかし,状況によっては2つも3つも,戻してもらいたいときがあります.そのような場合,関数内での変化がそのまま呼び出し元に伝わる参照渡しによって,関数内で,適切な値を引数にセットしておけば,戻ったときでも,引数にした変数を使うことによって,その値を伝えることができます.
さて,組み込み型を引数にするとどうなるでしょうか?
答えは以下です.
char, int, long, float, double などの数値変数:デフォルトでは値渡し
配列(char a[10]など):デフォルトではポインタ渡し
ユーザー定義型(構造体,共用体):デフォルトでは値渡し
と,なっています.
ここで,配列がポインタ私になっていますが,なぜでしょうか?実は値渡しは,引数を一時バッファにコピーして渡します.しかし,配列は一般に非常に大きな値ですので,関数呼出しごとに,いちいちバッファにコピーしていたら,処理速度が大幅に落ちます.そのため,その配列の名前だけを指すポインタというものをかわりに渡します.まさにポインタは「指す」という意味です.ポインタ自体は小さいサイズなので,処理効率が上がります.使うときは,演算子*を使って(この*は掛け算と同じだが,掛け算は2項演算子,ポインタは単項演算子となっているのでコンパイラは区別する)ポインタが指しているものを変数として使います.この場合ポインタは呼び出しもとの変数を指しているので,たとえ呼び出された関数でも,呼び出しもとの変数をいじくることになります.そのため,ポインタ渡しは参照渡しと同じく呼び出しもとの変数を変化させます.
上の例で示せば,
void Tetudiki( int *x ){
if( *x == 0 ) { return 0; }
else {return cos(*x);}
}
となります.
ポインタは,簡単に動作不良を起こさせるプログラムが書けるの,これkらは使わないことにします.ポインタがなくとも,参照渡しがあれば十分です.
