推薦図書は以下のようなものです。ちなみにこの文書は以下の本の流れにそって書かれています。
- カーニハン、リッチー(石田 訳):「プログラミング言語C 第2版 ANSI 規格準拠」、共立出版
- S. Oualline (望月 監訳、谷口 訳):「C実戦プログラミング 第3 版」、オライリージャパン
- 湯田:「図解 TURBO C プログラミング入門」、オーム社
ここで、
このあたりの文字コードに関する話も本当はするべきなのですが、別の機会 に譲ります。
逆に、 #ではじまる行以外は、改行、空白、タブ等は基本的に無視されます。 つまり、続けてかかれてようと、行を分けていようと、同じ扱いです。
例えば
printf("Hello!\n");
の場合、printf が関数名、() で囲まれた部分
(この場合は"" で挟まれた文字列)がパラメータ(関数に渡す値)となり
これらがあわさって関数呼出
し(式)となり、それに;がついて文となります。この場合は画面に
Hello!
と表示しろ、という文になります。ちなみに、\n は改行をあらわす特
殊文字(シーケンス)です。
☆非常に簡単な(代表的な?)プログラムの例としては以下のようなものがあげ られます。
/* 最初の最初のプログラム by まえだとしゆき */ #include main() { printf("Hello,World\n"); }
逆に考えてみると、perl や JavaScript のような型無し言語の場合、どん なデータ型が代入されるかわからないし、場合によっては(数字を文字列と して取り扱う、等)型の変換なども処理系で面倒をみるかわりに、処理系の 負担(メモリを余分に消費したり、CPU資源を食いつぶしたり)が多くな りその結果実行速度は遅くなってしまっています。C言語は効率優先で設計 されているので、プログラマ(人)が処理系に型を教えてやることになって ます。また、これはコンパイル時に型チェックをすることでバグのチェック もできる、という効用もあります。
例えば pc2617.heisei-u.ac.jp (Pentium/linux2.0.38/libc5) の場合;
を実行すると結果は「2 4 4 4 8 12 4 」となります。#include main() { printf("%d ",sizeof(short)); printf("%d ",sizeof(int)); printf("%d ",sizeof(long)); printf("%d ",sizeof(float)); printf("%d ",sizeof(double)); printf("%d ",sizeof(long double)); printf("%d ",sizeof(void*)); }
である、が、いずれにしても 216=65536 通りの区別をしているに過 ぎないことに注意してください!int i で int が 16 ビットの場合、i の取りうる値は -32768 ≦ i ≦ 32767 unsigned int i の場合、i の取りうる値は 0 ≦ i ≦ 65535
[以下、中級以上の人向き]代表的な内部型式(IEEE 754) では、float は、符号 1ビット、指数 8ビット (基数2で、127 を加えた数字)、仮数 23 ビット (1+m) の合計 32ビットとな ります。double の場合は指数11ビット、仮数が 52 ビットで合計64 ビットとなります。
仮数は、常に先頭が 1 になるように正規化され、その 1 を省略した残りを
float の場合23 ビットに納め、従って、精度は 24ビットとなります。それぞ
れは2進数表現で、2 指数部- 127×(1+仮数部(小数))となり
ます。
例えば
0 10000101 01010000000000000000000
のビット列を考えてみると、
仮数部は0101 であるから、これを2進数の小数とみて
1/2 * 0 + 1/4 * 1 + 1/8 + 0 + 1/16 * 1 = 5/16 = 0.3125
これに1を加えた 1.3125 が仮数となり、指数部は 10000101 = 133 だから 133-127=6 となり、1.3125 * 26 = 84 となります。
この場合、シフトして仮数部を0にして簡単に求めることも可能です。 仮数が 0.0101(2) ではなく 1.0101 であることと、 左に小数点を1ビットシフトすると指数部が1減ることに注意すると、1.0101(2) * 26 = 10101.0(2) * 22 = 21 * 4 = 84
となります。
/* 型について by まえだとしゆき */ #include main() { int i; float f; i = 1000000000; f = 1.2; printf("i=%d,f=%f\n",i,f); }
二項というぐらいなので、2つじゃないのもあります。
a = b = c;のようなことが可能になるわけです。この場合、まず b=c が計算され、その結 果、元の c の値がb に代入されるとともに、この(b=cという)式の値自体も c の値になります。そして、その値がa に代入される、というわけです(順序の詳 細は後述)。
++,-- どちらとも前置と後置の場合(区別)があります。これは、式の評価(変 数の値はいくつか?)と、演算(1増減させる)のどちらを先にするかの違いで す。例えばi=1のとき i をインクリメントするといずれにしても i は2ですが、 「j=++i」の場合、先に演算してから(iが2になってから)式の評価をするので、 j は2になります。一方、「j=i++」の場合は先に式の評価をして、すなわち j に 1 を代入してから i を1増やします。
じゃぁ、同じ順位だったらどうするか、という疑問が出てくるでしょうが、 その場合にも(大抵の場合には)問題なく評価することができます。それが 結合規則というもので、「同じ順位の演算子が並んでたらどちらから先に評 価するか」というものです。先の例の「a=b=c」の場合、=は右から左に行 うことが規定されてますので、先に b=c を評価して、その後でその式の値 を a に代入する、という順序が保障されています。
これまで出てきていないものも含めて、C言語での演算子の評価順位(上ほど高 い)と結合規則をまとめておきます。出てきてないものは適宜解説していきます。 2、3項演算子の場合、その複数ある被演算数(変数など)の評価の順序は(論 理演算など一部の例外を除き) 処理系依存(つまり、どっちから評価されても文句は言えない→そのような コーディングはしてはいけない)です。
たとえばi = 2; j = ++i * (--i);の場合、先に ++ して そのあと -- する、とすると 3*2 で j は 6 になり ますが、先に -- しちゃうと 2*1 で j は 2にしかなりません。
演算子の優先度と結合規則 | |
演算子 | 結合規則 |
() [] -> . (参照絡み) | 左から右 |
! ~ ++ -- + - * & (type) sizeof (単項) | 右から左 |
* / % (算術演算) | 左から右 |
+ - (算術演算) | 左から右 |
<< >> (ビットシフト) | 左から右 |
< <= > >= (比較) | 左から右 |
== != (比較) | 左から右 |
& (ビットAND) | 左から右 |
^ (ビットEOR) | 左から右 |
| (ビットOR) | 左から右 |
&& (論理AND) | 左から右 |
|| (論理OR) | 左から右 |
?: ((三項)条件式) | 右から左 |
= += -= *= /= %= &= ^= |= <<= >>= (代入) | 右から左 |
, (コンマ…並記できる。式の値は最後の式) | 左から右 |
/* 演算子について by まえだとしゆき */ #include main() { int i,j,k,l; i = j = k = 1; l = ++j; k -= l; printf("i=%d,j=%d,k=%d,l=%d\n",i,j,k,l); }
関数については詳細は後述しますが、ここでは;
☆例として、
☆別の例として、/* define マクロの使い方 */ #include #define NUMBER 1 main() { int i; i = NUMBER ; printf("i=%d\n",i); }
この場合、かならず () をつけるようにしないとだめです。というのはたとえば#define SQUARE(x) x * xとすると、SQUARE(i-1) は(#define はC言語の文法は気にせず展開するので) 「i-1*i-1」となり、これは 優先順位 を考える と「i-i-1」となり、i の値にかかわらず -1 になります(!)
[ 以下、中級以上向け ]
といいつつ、実はこれで安心しては駄目で、たとえばSQUARE(++i)のような呼び出され方をした場合、これは(++i)*(++i)と展開されますので、i が 2 だったとしたら、3*4 で 12 になってしまい、 意図した 3^2 => 9 とは違ってしまいます(要するに 処理系依存 とにたようなバグを産む)…逆に言うと、関数呼び出しの 場合はマクロになっている場合も頭において、「引数として渡すと同時に内 容を変更する」といったことをしないように心掛けるべきでしょう。
/* define マクロの使い方(2) */ #include #define SQUARE(x) (x)*(x) main() { int i; i = 3 ; i = SQUARE(i-1) ; printf("i=%d\n",i); }
WinNT のコマンドプロンプトや、UNIX 系OSでのシェルなどでは、この標準入出 力を柔軟に扱えるようになっています。入力をキーボードから打つかわりに事前 に作成されているファイルからおこなったり逆に出力を画面ではなくファイルに 書き込むことが可能です(リダイレクションといいます)。また、あるプログラ ムの標準出力と、別のプログラムの標準入力を繋げることで、あたかも2つのプ ログラムが1つになってデータ処理を行うようにするのも容易です(パイプとい います)。
puts():1行出力する。引数は文字列(1つ)。
「文字列」とは、厳密には、
文字の配列(の先頭アドレス)か、文字へのポインタです、が、詳細は
後述
します。
☆例として、
/* 標準出力の例1 */ #include main() { int i; i = 97; printf("%d(dec) is %x(hex), and '%c'(char).\n",i,i,i); }
☆別の例として、
/* 標準出力の例2 */ #include main() { int i; printf("Charactors (48-57) are [ "); for (i = 48; i < 58 ; i++) putchar(i); puts(" ]."); }
標準入力 (standard input)
基本的に、出力に対応したものたちがあります。
gets(): 1行入力する。引数は文字列(のポインタ)。入力した結果がそのポイ ンタ(アドレス)で示された文字の配列の領域へ格納されます。
ここで重要なのは、第2引数以降は変数ではなく変数へのポインタを渡さなくて はいけない、ということです。これは、 左辺値と右辺値 のところでも述べましたが、データを代入するためには左辺値、すなわち変数を 器(あき箱)として扱わないといけないのですが、関数で引数として渡せるのは 値(箱の中身)だけなので、変数のアドレス(在処)を「値」として渡すことで、 変数(=箱)に値をいれてもらえるようにするわけです。
このあたりの正確なところはポインタを学習しないと理解できないかもしれませ んので、当面は scanf() のときは変数へのポインタを渡す(文字列の時以外は 前に&をつける)と覚えておいてください。
結局これらはUNIX 流の「キーボードもディスプレーも抽象的にファイルと とらえ、その(文字)データの流れ(=ストリーム)をファイルからの入出 力ととらえる」ということに落ち着きます。stdio により stdin とstdout をデフォルトの入出力ファイルとみなす、ということです。具体的な例だと、 scanf(...) は fscanf(stdin, ...) の省略形、とみなしたらいいですよ、 というふうなことです。
さらにファイルからではなく既に文字列としてあるものから(型変換なども 含めて)別の変数にいれなおす sscanf() というものがあります(詳細はこ こでは略します)。
[中級以上の人向き]実は、現実の(まともな)プログラムでは、gets() や scanf() はまず使いませ ん。というのは、これまたポインタを勉強してないので詳細は述べられないです が、scanf() などでは入力される文字数を指定できない(入力する人を信用して る)ので、本来入力できないメモリ領域まで書き込んでしまい、それがシステム を壊す可能性があるからです。
よくbuffer overflow のバグをついたクラッキング(不正な Web page 書き換え など)が報道されたりしますが、これらの大多数は入力文字数チェックをしてい ない(ある意味手抜きというか、素人的)プログラムだからです。
じゃ、どうするのかというと、例えば入力領域をchar buf[1024];
と確保したとき、fgets(buf,1024,stdin);
のようにして最大 1024-1 バイトまで読みこむ、としておいて、返り値が NULL になってたらエラーなのでそれをチェックしてから、その読みこんだ データ(文字列)に関して sscanf() する、という手順をふみます。たとえ ば10進数を読みこみたい場合は以下のようになります。int i; char buf[1024]; if (fgets(buf,1024,stdin) != NULL) sscanf(buf,"%d",&i); とはいえ、講義では簡単のため(入力者を信じて?)scanf() も使っていき ます。
☆例として、
/* 標準入出力の例 */ #include main() { int i; printf("input integer: "); scanf("%d",&i); printf("%d(decimal) is %o(octal), %x(hexadecimal).\n",i,i,i); }
Pascal という言語はこの構造化プログラミングを強く意識して設計された 言語で、Cもその流れを組んでいます。それ以前の FORTRAN や COBOL など にはこのような考え方はなくで、(機械語レベルの jump に対応する) GOTO 文 をつかって制御の流れをかえていたのですが、こ れを多用すると全体の見通しが非常に悪くなり、バグの温床となることが指 摘されていました。
if 文
if と else について、
とパターン化されると思いますが、これについて少し考えてみます。
if (条件) 文であり、「文」で1つ以上複数の文を書きたいときに {} でブロックを作っ てあたかも1つの文のようにふるまわせる、というほうが構文的には正しい 解釈で、かつ覚えることが少ないです。ちなみに{文;} と1つだけでも当然 OKです。
if (条件) 文 else 文の後の文がたまたま別の if 文であった、という解釈が妥当です。強いて書くな ら
if (条件1) { 文1 } else { if (条件2) 文2 }で、上の理屈により {} が省略されている、という考え方です。
if (条件1) 文1 else if (条件2) 文2 else if (条件3) 文3 else 文4でも全然問題ないです。これは構文的には以下のような入れ子構造になるこ とを確認しておいてください。
if (条件1) { 文1 } else { if (条件2) { 文2 } else { if (条件3) { 文3 } else { 文4 } } }
但し、perl などでは elsif を見た目通り (switch のように)条件の並列とあ つかいますし、C でもコンパイラによってはそのような最適化をしてる可能性は あります。
☆例として、
/* if 文の例 */ #include main() { int i; printf("Input your point: "); scanf("%d",&i); if (i < 60) printf("You fail..\n"); else printf("You pass!!\n"); }
while (条件) 文であり、「文」で1つ以上複数の文を書きたいときに {} でブロックを作っ てあたかも1つの文のようにふるまわせる、というほうが構文的には正しい 解釈で、かつ覚えることが少ないです。
while (条件) 文の場合、まずはじめに式を評価して、それが真なら文を実行するので、 もし式が最初に偽だった場合には文は一回も実行されないです。
最低1回はかならず実行して、その後で判定する場合は
文 while (条件) 文でもいいのですが、これは冗長なので、
do 文 while (条件);という構文が用意されています(が、あまり使わないかもしれません)。
while (1) 文のように書くと、条件部が常に1(=真)なので、無限ループになります (後述)。
for (式1;式2;式3) 文で、
for (式1;式2;式3) 文は
式1; while (式2) { 文; 式3; }と(ほぼ)同じ処理をします。
ちなみに(データを読み飛ばしたりする時なんかは)「式3」でやりたいこ とが全てで、「文」で処理したいことが無かったりします。そんな場合、for (cp=str1;*cp;cp++);のように「文」のところが空文(";"だけ)になることもありえます。
for (;;) 文と書くことが出来ます。そしてこの場合は無限ループになります。
利用例をあげると、for の中ではループから抜ける条件は1つしか書けませ んが、複数の条件の時にそれぞれ状況をかえてループから抜けたいような場 合は
for (;;) { 共通処理; /* 入力させたりすることもあり */ if (条件1) { 個別処理1; break; } else if (条件2) { 個別処理2; break; } }と書いたほうが見通しがよくなります。
[中級以上の人向き]ちなみに、無限ループを作りたい場合、
for (;;) 文でもwhile (1) 文(1は要するに常に真ということ)でも可能です。しかし、普通は前者の for 文のほうを用います。なぜなら、後者はループがまわるたびに1 を評価 する(=処理を行う)わけですが、前者なら無条件でループするわけで、処 理が(少しだけ?)軽いからです。1回のループでの差は小さくても、ルー プは何回もおなじ処理が行われるわけですから、ちりも積もれば高い山にな るかもしれません。
☆例として、
/* ループの例 */ #include main() { int i,j,k; printf("Input: "); scanf("%d",&j); for (i=1,k=0;i<=j;i++) k += i; printf("Summation=%d .\n",k); }
if (条件1) … else if (条件2) … else if (条件3) … else if (条件4) … else if (条件5) … else if (条件6) … else if (条件7) …のような場合、 if 文 のところでも述べたように、条 件7は深い入れ子の底にあり、これのみが合致する場合にでも、条件1〜6を チェックして、それらが全て偽になることを確認しなければ条件7のチェッ クまでこれない、という構文になっています。
switch (条件式) { case 定数式1 : 文1 ; case 定数式2 : 文2 ; ‥ case 定数式n : 文n ; default: 文n+1 ; }となります。条件式が下のいずれかの定数式に一致すれば、その文を実行し ます。この場合の文は複数かいても({} でかこまなくても)大丈夫です。
実際、実行時には switch 文にしても上から順番に条件チェックをしている 可能性はありますが、賢いコンパイラならうまく最適化してすばやく分岐で きるコードを生成してると期待できます。さらに、条件の排他性が保証しや すいという利点もあります。
どの定数式にも一致しない場合は default: ラベル(後述)がふられている 文を実行します。ただし default: は無くてもかまわなくて、無い場合には、 上の定数式に一致しない条件の時にはなにも行いません。
ちなみに、この default ラベルの綴を (defalt のように) 間違っってもコ ンパイルエラーにはならなくて、(switch とは関係のない)単なるラベルと して取り扱われてしまう、すなわち意味をなさないことになって、バグの原 因となる場合が多いので注意してください。
そこで、(こちらのほうがほとんどだと思われますが)例えば上の場合で、 定数式2以降の文は実行しない、というときには、文1(のあつまり)の最後 に break; 文をいれることで、この switch 文全体(のブロック)から抜け る、ということが可能になります(例題参照)。
if (条件1) goto cond1; else if (条件2) goto cond2; ……… cond1: printf("条件1でした\n"); exit(1); cond2: printf("条件2です\n"); exit(2);のような使い方が可能です。
while (条件){ 実行文; …… }は
loop: if (! 条件) goto loopend; 実行文; …… goto loop; loopend:で実現できます(確かめてみてください)。
☆例として、
/* switch の例 */ #include main() { int i; printf("数字を入力してください: "); scanf("%d",&i); switch (i) { case 1 : printf("1つですね\n"); break; case 2 : printf("2つですね\n"); break; default: printf("たくさんですね\n"); break; } }
配列の添字が0から始まるのも、ここからきています。つまり、0番目とい うことはオフセットが0ということで、つまり配列の一番最初の要素、とい うことなわけです。
☆例として、
/* bubble sort */ #include main() { int i,j,temp,a[10]; for (i=0;i<10;i++) { printf("data %d : ",i); scanf("%d",&a[i]); } for (i=0;i<10;i++) { for (j=i+1;j<10;j++) { if (a[i]>a[j]) { temp=a[i]; /* You cannot exchange data directly! */ a[i]=a[j]; a[j]=temp; } } } printf("Outputs="); for (i=0;i<10;i++) { printf("%d ",a[i]); } printf("\n"); }
ここがC言語をマスターできるかどうかの分かれ目ですので、 こころして(!?)学習してください。
malloc/freeについては構造体のところで再度ふれる予定です。
☆例として、
#include main() { int a,*p; p = &a ; a = 1; printf("%d\n",*p); a = 2; printf("%d\n",*p); }
ちなみにここでは、a で文字列全体をあらわしている、と言えなくもないで す。初期値の文字列を用意した場合に限り、宣言の宣言で [] と数字を省略する ことができ、必要な配列の要素数は処理系が自動で用意してくれます。ちな みに[] の中に数字を指定した場合;
☆例として、
このような簡潔な書き方は実際のプログラムでは非常に多いので、 見馴れておくとよいでしょう。
#include main() { char str[8], *cp; int i; cp = "APROG" ; for(i=0;*cp;cp++,i++) /* before end-of-string */ str[i]=*cp; /* copy each character */ str[i]='\0'; /* treat end-of-string */ printf("%s\n",str); }
さらに string について(補助資料)
素朴な気持ちとして
char a[4] = "abc" ; chat b[4];の場合、
b = a;で文字列変数 b に "abc" という文字列が入って欲しい、というのは理解はでき ます。し、実際 perl など他のプログラム言語では可能です。
しかし、C ではあくまでも「文字列」は実現としては文字の配列で、その取り扱 いについては文字配列の先頭アドレス(ポインタ)をもって行う、という決まり になってます。だから、前の例ではアドレス同士の演算なので、実際の文字列操 作にはなってないわけです。
ちなみにC++ や Java では、String クラスという文字列用のクラスを準備して、 さらに "=" (代入演算子)を overload (上書き?)することで、String a = "abc"; String b; b = a;のような記述は可能となっています(詳細は略します)。
そこで、strcpy などのような標準関数が用意されるわけです。この場合は stdio.h だけではなく string.h というヘッダもインクルードする必要があるこ とにも注意しておいてください。
文字列操作関数としては、以下のようなものがあります。
strcpy(a,b);でOKです。この場合、文字配列 a には b 以上のメモリ領域が事前に確保 してある(=配列の長さが十分ある)必要があります。
strcat(a,b);でOKです。この場合、文字配列 a には b をくっつけても十分なメモリ領域が 事前に確保してある(=配列が十分長い)必要があります。
strcmp(a,b);とすると、a が(文字列として)b より
if (! strcmp(a,b)) {/* 文字列が等しいときの処理... */}のように書かれます。
上下どちらの例にしても(結局文字列はポインタ扱いなので);
実際には cd は built-in command の場合も多いですが…このようなコマンドをC言語で開発する場合には、なんらかの手段でそのパ ラメータをプログラムに取り込まなくてはならないわけですが、そのやりか たは標準規格で決まっています。すなわち、main 関数の宣言で;
☆例として、
#include main() { char *strs[2]; int i; strs[0] = "hannan-u"; strs[1] = ".ac.jp"; for (i=0;i<2;i++) printf("%s",strs[i]); printf("\n"); }
しかし、他人のプログラムを読む場合のことを考えて、最低限の理解は必要 でしょう。
記憶クラス指定子の区分は以下の通りです。
呼び出す側と呼び出される側と間のデータの受渡しには以下の3通りがあり ます。
他のプログラミング言語では値渡し、参照渡しの他、変数渡しという機構が サポートされているものもあります。その場合は実装はまた別となります。
#include int beki(int i, int j); main() { int i = 2, j = 3; printf("%d\n",beki(i,j)); } int beki(int i, int j){ int ii, ret = 1; for (ii=0;ii
関数の部分は、以下のように考えられるでしょう。
n! は、n * (n-1)! で求められる。 (n-1)! は、(n-1) * (n-2)! で求められる。 (n-2)! は、(n-2) * (n-3)! で求められる。 : : 3! は、3 * 2! で求められる。 2! は、2 * 1! で求められる。 1! は、1 です。
すなわち、関数 fact は受け取ったパラメータが1の時は1を 返し、それ以外の時は n*(n-1)! を返すものです。
ここで、 if (n==1) の条件文が再帰からの脱出の出口をなっていることに注意 してください。再帰関数はかならず脱出口を持っています。でないと無限ループ におちいってしまいます。
実際のプログラミング場面では、構造体を局所的 (local) に宣言すること はほとんどないといってよいです。なぜなら関数の引数で渡したり返り値で もらったりするには共通の型情報が必要だからです。下の場合はあくまでも 練習問題だから、と捉えておいてください。
class は C++ の予約語なので使わないほうが無難です…
#include main() { struct kumi { int bangou; char sensei[10]; }; static struct kumi classes[3]={ 1, "yamada", 2, "sato", 3, "suzuki", }; int i; for (i=0;i<3;i++){ printf("Class no.%d: %s-sensei\n", classes[i].bangou, classes[i].sensei); } }
前例をすこし書き換えた以下のプログラムをみてください。
例えばメモリをシステムからもらってくる関数 malloc() は void* 型 と決まってるので、整数や文字列や構造体の場合に応じてその型へ変換して あげないと変数の値として使えない、等。
#define による書換えも似たようなことが出来ます。しかし、異なる点は、 #define は字面だけで変換しているのに対して、typedef は型の名前である、 という意味を(コンパイラが)理解しながら変換する、という点です。