プログラミング言語は計算機と共に進化してきました。その歴史の中では様々な試行錯誤が行われ、その多くが今はあまり使われていません。 そんな中でC言語は何故か多くの人に使われて繁栄している言語の1つです。 変数、データ型、関数といった仕組みも比較的うまくいっている仕掛けです。ただし、これは唯一の仕組みではありません。実際、異なる仕組みの言語も沢山あります。
※参考Wiki プログラミング言語 プログラミング言語一覧
教科書の順番に沿って説明をしてきましたが、いろんなところで疑問を持って立ち止まっている人も多いようです。しかし、まずはここまでの内容を最後まで読み通して全体がうっすらと見えるようになってください。そこから、戻ってもう一度おさらいしてくれると、初めに疑問だった点が少しづつ解ってくると思います。
※C言語は教育用の言語ではなく、実用を目的とした言語です。計算機の仕組みや、機械語でのプログラムの大変さが良く解っていた人たちが、OSのような大規模なプログラムを書くために作りました。その後も、多くの利用者を得て、互換性の維持のために規格化や時代の要請に応じた拡張も行われ続けています。
※プログラミング言語の真の理解は計算機の開発の歴史をなぞって、計算機を作った人たちと同じ経験と知識を持ったうえで、初めて可能になるのかもしれません。
計算する仕掛けをどのように作るか、様々な試みがありました。
※Computer History Museum の 再現された階差機関 (differrence engine) 機械式の計算機
※Wikipedia ENIAC 真空管を使った初めての実用的な計算機
ノイマン型と呼ばれる計算機は、番地順に並んだメモリ-(記憶装置)に計算するデータだけでなく計算手順どおりに機械語を並べたプログラムを記憶し、制御装置がメモリーの機械語部分を番地順に読みながら演算装置やメモリーを制御して動きます。
膨大な数のメモリーは番地で区別します。0/1の二つの状態を記憶するメモリー量を1ビット、8ビットまとめて1バイトと言います。1バイトごとに1つの番地を付けることが多いようです。例えば4GB(40億バイト)のメモリーを持つ計算機であれば、番地は0から40億までとなります。
C言語はアドレス演算子(&)で変数の値が記憶されたメモリー番地を得たり、配列の各要素を参照するのに添え字演算子([ ])でアドレス計算を行ったりします。序論演習Iでは説明しませんでしたが、ポインター変数のように番地を記憶する変数があり、添え字演算子や間接参照演算子(*)でその番地のメモリーを参照することもできます。
計算機は、メモリーに記憶された機械語のプログラムを1つづつ順番に読みながら実行する機械です。機械語のプログラムを書けば計算機の動作を隅々まで制御して思い通りに動かすことが可能です。しかし、機械語は単純な命令しか用意できないため、複雑な計算をするプログラムを作るのは人間にとって煩雑すぎる作業になります。
※機械語でプログラムを作るにはアセンブリ言語を用います。
C言語のような高級言語は 人間にも理解しやすい人工の言語でプログラムを書きコンパイル(翻訳)して機械語プログラムを作る 仕組みになっています。高級言語でプログラムを書く人は機械語や計算機の仕組みを知らなくても目的とする計算を行うプログラムを書くことができます。その代り、翻訳結果は翻訳プログラム任せなのでどの様な機械語になるか、どこのメモリーを使うのかなどを指示することはできません。
計算機のハードウエアを動かすプログラム、例えばOS(基本ソフト)は、計算機の隅々まで制御する必要があります。このプログラム作りは初期はアセンブリ言語を使って行っていましたが煩雑で大変な作業でした。そこで、C言語は高級言語でありながら機械語プログラムのようにメモリー番地を指定してデータを読み書き可能な言語として作られました。
ハードウエアに近い部分の細かな制御は炊飯器から自動車のエンジン制御まで、色々な場面で必要とされます。このような組み込み機器用のプログラム開発にもC言語は良く使われています。
ハードウエア寄りの言語というだけでなく、構造化プログラミングを可能にする仕組みが用意されているため、OSのような大規模なプログラムも作れます。OSのプログラムを書けることはC言語の重要な存在意義です。
1985年頃からC++、1995年頃からjavaが使われるようになり、今では言語の利用者はjava、C、C++がTOPを争っています。C++やjavaはオブジェクト指向プログラミングを容易にするための言語でGUIを多用したプログラムを作り易いのが特徴です。ソフトウエアの規模が大きくなることへ対応するために造られた言語と言えます。
C++はCにオブジェクト指向プログラミングを容易にする機能を加えた形になっています。javaはオブジェクト指向のプログラムが容易に作れ、かつインターネットで使うことを想定しハードウエアやOSへの依存を無くした言語です。文法的にはCと同じ部分が多いため、C言語からC++やjavaへと移行するのはオブジェクト指向の仕組が解れば難しくありません。
javaやC++があるのにC言語が主要な言語として残っているのは、以下のようになバランスのいい言語だからでしょう。
計算機は0/1のビットパターンを処理する機械です。したがって、整数、実数、などのデータをどのような0/1のパターンで表すかを決めなければ計算機の演算回路を作れません。
C言語では基本の型として計算機が用意した演算回路の仕組みに合わせて、データとビットパターンの対応を決たデータ型を定めています。
※初期のパソコンでは演算回路が16ビット(2バイト)の整数演算を基本としていました。このため、その計算機のC言語処理系ではint型が2バイト(16ビット)の符号付整数となっていました。今では32bit(4バイト)の整数演算回路が一般化しているので、現在のパソコンのC言語処理系はint型に4バイト使うのが普通です。
変数はデータを記録するメモリーの場所を番地ではなくて名前で識別したものです。C言語ではデータ型を付けて変数を定義します。そうすることで、変数のデータ型に応じた処理が行えます。例えば、整数の足し算と実数の足し算は異なる処理が必要です。同じプログラムの記述「c=a+b;」であってもa,b,cがint型の場合とdouble型の場合とでは,コンパイラは変数のデータ型に基づいて異なる機械語に翻訳します。
C言語では基本のデータ型としてchar、int、float、doubleを決めています。これに修飾子unsignd/signdやshort/longを付けてデータとパターンの対応を変えることもできます。序論では基本の型を使うプログラムは書けて読めることを求めています。
int は符号付き整数型でビットパターンとの対応は2の補数表示が一般的。
unsignd int は符号なしの整数型
※2の補数表示のデータは符号なし整数と同じ演算回路で加減算ができます。
int型は多くのパソコンで4バイトのメモリーを使って記憶されます。これは計算機が一度に処理するビット数が32ビット(4バイト)や64ビット(8バイト)のように決められいるからです。
short int int型が4バイトの場合は2バイトの符号付整数。
long int int型が4バイトの場合は4バイトか8バイトの符号付整数。
※C言語では基本のデータ型が計算機や処理系に依存する
浮動小数点数、有効桁数に制限がある。C言語での実数演算はdouble型で行われる。記憶するメモリーを節約したい場合には変数にfloat型を使うこともできる。double型で十進15-16桁、floatで6桁程度の精度がある。
実数を浮動小数点数で近似的に処理していることを忘れるな。
※データは符号に1ビット、2の何乗かで数ビット(指数部)、残りを2進数の小数点数(仮数部.)のビットパターンンで表現されます。実数の精度は仮数部のビット数で決まります。機種依存ですが例えばIEEE754の規格ではfloatの仮数部23ビット、doubleは52bitです。
(−1)符号×2指数×1.(仮数)
※floatやdoubleは2進の小数点数で値を表現しています。小数点以下の桁数は有限です。従って、有限桁に収まらない値は表現できません。 例えば十進数の1.1は2進小数では循環小数になるので正確に表すことはできません。僅かですが差が出てしまいます。
#include<stdio.h> int main(void) { double x; for(x=1.0;x<=2.0;x=x+0.1)/*繰り返しの数が決っているようなfor文で実数型を使うのは危険*/ { printf("%f\n",x); } return 0; } /*実行結果 1.000000 1.100000 1.200000 1.300000 1.400000 1.500000 1.600000 1.700000 1.800000 1.900000 <<1.0に0.1を10回加えた結果は2.0よりも大きいのでfor文の条件は不成立 */
char 1バイトの大きさで基本的には1文字のデータを格納する型。文字以外にも1バイト単位で表すことのできるデータを扱うときに良く使われる。
文字とビットパターンの対応は文字コード表による。パソコンでは代表的な文字コード表であるASCIIコード(7ビット)を8ビットに拡張したコード表を使うことが多い。例えば、日本では8ビットコードとしてJISコードを使うことが多い。
ASCIIコード JISコード
※漢字などは文字の種類が多いのでコード化に2バイトを必要とする。このような文字はchar型変数1個には入りきらない。複数バイトを使って1文字を表す必要がある。
SJISコード
ASCIIコード等の文字コード表ではコードと文字の対応を定めているが,コードの中には制御コードと呼ばれる文字に対応しないものが含まれる。これらをC言語のリテラルとして表記するためにバックスラッシュ(JISコードでは¥になる)を用いたエスケープシークエンスが使われる。
リテラルの記述 | JISコードの値 |
'\0' | 0x00 |
'\n' | 0x0A |
'\r' | 0x0D |
'\\' | 0x5C |
※序論演習Iでは扱いません。後期の序論演習IIから出てきます。さらに2年前期の言語Iで詳しく学びます。
番地(アドレス)を記録する型。C言語ではそのアドレスに書かれたデータの型を合わせてポインター変数を宣言する。ポインター変数はアドレスを記憶するだけでなく、そのアドレスに書かれたデータを読み書きする(参照する)のにも使える。(参照するために,コンパイラにデータ型を示してポインタ変数を宣言することが必要)
int a; int *p; /* ポインターが指すデータの型 * 変数名; */ p=&a; /* pに変数aのアドレスを代入すると */ *p=10; /* 間接参照でaの場所(アドレス)に10を代入できる*/ p[0]=10; /* でも同じ間接参照になる */
配列、構造体、共用体がC言語には用意されています。javaなどの言語を学ぶ上では配列と構造体は理解して欲しい。序論演習Iでは配列を使うプログラムは書けて読めることを求めています。
構造体については後期の序論演習IIで説明します。さらに2年前期の言語Iで詳しく学びます。
複数の同じ型のデータを連続したメモリー番地に記憶する型です。各データは添え字演算子を用いて参照します。有効な添え字は0から要素数-1まで。
.... int a[10]={1,2,3,4,5,6,7,8,9,10}; int i,sum; /* 合計を求めるのに sum=0; sum=sum+a[0]; ... sum=sum+a[9];/*添え字は0から始まるので10番目は9となる*/ としてもいいが、単調なので繰り返しの構文を使って */ sum=0; for(i=0;i<10;i++) { sum=sum+a[i]; } printf("合計=%d",sum); ....
C言語では,文字列を文字コードデータがメモリーアドレス順に並んだモノとして表します。文字列の先頭番地を文字型ポインター変数に記憶して管理します。先頭だけ管理すればいいのは、文字列の終端をコード値0で示すルールになっているためです。
文字列の途中にコード0を含むことができませんが、コード0には文字を割り当てないような文字コード表であれば問題は起きません。
※序論演習Iでは扱いません。後期の序論演習IIから出てきます。さらに2年前期の言語Iで詳しく学びます。
色々な型のデータを並べて記憶する型です。各データはメンバ名で区別して参照します
/*構造体Complex型の定義*/ struct Complex { double real; double imag; }; /*構造体Complex型の変数zの定義*/ struct Complex z;
データを記憶したメモリーの番地でデータを管理するのは人間にとっては煩雑なので、名前を付けてデータを管理するようにしたのが変数です。
※人間が管理するには,メモリーの番地だけでなく,記憶した0/1のパターンと記憶するデータの値の関係を覚えておくことが必要です。例えば1文字のデータ’A'とASCIIコードでの1バイトの0/1パターン「01000001」,整数のデータ123と2進数の4バイトの0/1パターン。
C言語ではメモリーに記憶した0/1のパターンとデータを正しく対応付けるため、データ型と変数名のセットで変数を宣言します。
データ型 変数名;
char a; int b=0; float f=10.23; double d=3.1415; int x[]={1,2,3,4,5,6,7,8,9,10};
※int型の変数に3.14の様な実数を代入するようなプログラムはコンパイラが見つけて警告やエラーを報告してくれます。変数のデータ型を明確にすることでプログラムの間違が見つけやすくなります。
関数などのブロックの内側で定義された変数は自動変数となり、定義されたブロックに入るところでメモリー割り当てが行われ、出るところで割り当てが解除される。解除後のメモリーは他の変数の割り当てに使うことができるので、同じメモリーを有効に利用できます。変数のデータはブロック内を実行中の間だけ保証されることになります。
※ブロック:{と}で囲むことでブロックを作る。
※staticを付けて定義された変数は例外的に静的変数となる
#include<stdio.h> /*この位置にstaticを取って変数sの定義を移すことも可能*/ int fact(int n)/*仮引数も自動変数と同じような割り当てが行われる*/ { int i,ret;/*自動変数*/ static int s=0;/*静的変数 staticを取って関数の外に出してもいい*/ for(i=0;i<s;i++) printf(" ");/*s*4個のスペースを書き出す*/ printf("fact(%d)\n",n); /**/ if(n==0) { ret=1; } else { s++; ret=n*fact(n-1); s--; } for(i=0;i<s;i++) printf(" "); printf("return %d\n",ret); return ret; } int main(void) { fact(5); return 0; } /*実行結果 fact(5) fact(4) fact(3) fact(2) fact(1) fact(0) return 1 return 1 return 2 return 6 return 24 return 120 */
関数の外で定義された変数は静的変数となる。静的変数はプログラムの開始時にメモリーが割り当てられ初期化される、終了時に解除される。従って、プログラム実行中は継続してデータを記憶することができる。
#includeint list[]={1,2,3,4,5,6,7,-1}; int num=0;/*関数の実行順番を調べるための静的変数*/ void print(int max) { int i; printf("%3d print(%d)\n",num++,max);/**/ for(i=0;i<max;i++){ printf("%d ",list[i]); } printf("\n"); return; } int count(void) { int i; printf("%3d count()\n",num++);/**/ i=0; while(list[i]!=-1){ i++; } return i; } void function(int max) { int i; printf("%3d function(%d)\n",num++,max);/**/ for(i=0;i<max;i++){ list[i]=list[i+1]; } print(max); return; } int main( void ) { int max; printf("%3d main()\n",num++);/**/ max=count(); print(max); function(max); return 0; } /*実行結果 0 main() 1 count() 2 print(7) 1 2 3 4 5 6 7 3 function(7) 4 print(7) 2 3 4 5 6 7 -1 */
プログラムは初期化後、main関数の実行を始める。main関数内のプログラムを実行しreturnで戻り値を戻してプログラムは終了する。処理の順番が1本の糸のようにつながっているので、この処理の流れをスレッドと呼ぶ。
関数内のプログラムは基本的には書かれた順番に実行されます。
条件判定して処理を分けるときは if文を用います
繰り返しが必要な時はwhile文やfor文を用い、合わせてbreakで繰り返しからの脱出もできます
main関数の実行中に他の関数の呼び出しがあれば、その関数に実行位置が移動しreturnで戻ってきます。
1) 呼び出す側に書かれた実引数argumentの値を求めて、仮引数parameterを作り値を記憶します。
2)戻る場所を記録して、呼び出す関数に実行位置を変更します。
(呼び出し命令の次の命令の番地を戻り番地として記憶する)
3)呼ばれた関数では仮引数を使って実行を進めreturnで呼び出したところに戻る。
(戻り値を設定した後,戻り番地を次に実行する命令の番地とする)
4)戻ってきたら、戻り値を受け取って関数の呼び出しは終了です
関数を呼びだす部分の機械語への翻訳では手順からわかるように、関数の引数や戻り値の情報が必要になります。プログラムを頭から読みながら翻訳できるようにするため、C言語では関数の引数や戻り値の情報は関数呼び出しの記述の前に書く決まりです。
関数の定義があれば、その後は、その関数の呼び出しが書けます。しかし、これでは関数を書く順番が制限されてしまうので、関数呼び出しに必要な情報だけを関数プロトタイプ宣言として書けるように工夫されています。
事前に関数プロトタイプ宣言が書かれていれば、その関数の定義は最後に書いても大丈夫です。
#includeint num=0;/*実行順番を数える静的変数*/ char list[]="Book"; int length=4;/*上記文字列の長さ*/ void pr(void)/*文字列listを書き出す関数*/ { printf("%2d pr()\n",num++);/**/ printf(list); printf("\n"); return; } void rt(char a,char b);/*mainの後にrtを定義しているので、mainでrt関数呼び出すためにプロトタイプ宣言が必要*/ int main(void)/*プログラムはmain関数を実行する*/ { printf("%2d main()\n",num++);/**/ pr(); rt('k','o');pr();/* o は小文字のオー*/ rt('k','o');pr(); rt('k',0);pr();/* 0 はゼロなので注意*/ return 0; } void rt(char a,char b)/*文字列listの文字aとbを入れ替える*/ { int i=0; printf("%2d rt(%02x,%02x)\n",num++,a,b);/**/ for(i=0;i<length;i++)/*繰り返し*/ { if( list[i]==a) list[i]=b;/*分岐*/ else if(list[i]==b) list[i]=a; } return; } /*実行結果 0 main() 1 pr() Book 2 rt(6b,6f) <<6bはkの文字コード、6fはoの文字コード 3 pr() Bkko 4 rt(6b,6f) 5 pr() Book 6 rt(6b,00) <<kを文字の終端コード0に置き換えたので 7 pr() Boo <<1文字短くなった */