ここしばらくの間、新しいことの解説はなく今までの復習をかねて演習をおこなってきました。今回からは、さらに一歩進めて行こうと思っております。返事が遅れることもあるとは思いますが、単純な質問でもOKですから、何でも質問してください。
for(i=1; i<10; i=i+1) { 繰り返しの文 }繰り返しの増分のところで、変数の値に1加える,1引くなどの操作をよく行なう。一般にこのような操作はプログラムのいろいろなところで使われる。そこでC言語では、便利な演算子が
i=i+1の代わりに i++ または ++i が i=i−1の代わりに i−− または −−i が用意されている。この++が1を加える演算子でインクリメント,−−が1引く演算子でデクリメントと呼ばれる。上記のfor文は、
for(i=1; i<10; i++) { 繰り返しの文 }のようにインクリメント演算子を使って書き換えることができる。
インクリメント,デクリメント演算子を変数の前に付けたときと、後に付けたときでは結果が変わることがあるので注意が必要である。例えば、 x=a++ と x=++a を考えてみよう。x=a++ はaの値をxに代入した後にaの値に1を加える。つまり x=a; a=a+1;と同じである。これに対して、x=++a はaの値に1を加えた後にaの値をxに代入するので、a=a+1; x=a; と同じである。結果として、aの値は等しいがxの値が異なる結果になる。
インクリメント、デクリメント演算子は、プログラムの行数を減らすだけでなく、1を加える、1を引くといった関数のように扱えるため、プログラムを見やすくわかりやすくする効果がある。
また、++、−−は配列変数にも利用可能である。配列x[i]の内容に1を加えたいときは、++x[i]またはx[i]++とする。配列の添字についても利用できるが、演算子の置く位置によって結果が変わることは、前に述べた通りである。
y=x[i++]; は y=x[i]; i++; であり、 y=x[++i]; は i++; y=x[i]; である。
x += y; → x = x+y; x −= y; → x = x−y; x *= y; → x = x*y; x /= y; → x = x/y; x %= y; → x = x%y;代入演算子を使う理由として、次のような複雑な代入文を書いた時によくわかる。
W[ xyz[abc+bcd] + xzy[acd+dcb] ] = W[ xyz[abc+bcd] + xzy[acd+dcb] ] + 1;この場合、左辺の配列の値に1加えているのか? それとも別の配列の値に1を加えたものを代入しているのか? 判別するまでに多少時間を要する。しかし、
W[ xyz[abc+bcd] + xzy[acd+dcb] ] += 1;と書くことにより、前者であることが一目瞭然である。
このように代入演算子を使うことにより、プログラムを見やすくするばかりか、プログラマのタイプミスも多少防ぐことができる。また、コンパイラなどの処理系にとっても、効率のよいキカイ語(マシン語)コードを生成するのに役立つ。
x=0; y=0; z=0;のように書いた経験があるであろう。C言語では、これらを1行でつぎのように書くことができる。
x=y=z=0;また、
x=(y=(z=0));のように書いても、同じ結果になる。これは、括弧内の評価値は代入された変数の値になるため、zに0が代入され、その評価値つまり0がyに代入され、同様にxにも0が代入されることになる。この考え方は、プログラム4−8 ステップ12の
while( ( c = getchar() ) != '\n' )でも既に使っていた。この場合、getchar関数で読み込んだ文字をcに代入し、この式に評価値つまりcに代入した値が '\n' と等しいか否かを判定している。
簡単なプログラム例を用いて、上記4つの演算を理解しよう。
/* 1 */ /* Program No.5-1 */ /* 2 */ #include <stdio.h> /* 3 */ main() /* 4 */ { /* 5 */ int x, y, z; /* 6 */ /* 7 */ x = y = z = 1; /* 8 */ /* 9 */ printf(" x y z = %d %d %d\n", x, y, z); /* 10 */ printf(" ++x --y z++ = %d %d %d\n", ++x, --y, z++); /* 11 */ printf(" x y z = %d %d %d\n", x, y, z); /* 12 */ y += x; /* 13 */ z *= x; /* 14 */ printf(" x y z = %d %d %d\n", x, y, z); /* 15 */ }
7: | x,y,zそれぞれを1で初期化する。 |
9: | 当然のことながら、ここではすべて1が出力される。 |
10: | xの前には、インクリメントが付いているため出力は2になる。yの前には、デクリメントが付いているため出力は0になる。zの後ろには、インクリメントが付いているので、printfの関数が終了後+1されるため、ここでは1が出力される。 |
11: | 上で述べたように、zの値は2になっている。 |
12,13: | 代入演算子の使用例である。 |
14: | 11の結果に12,13の演算の結果が出力されることになるため、11の結果に対し、xはそのまま、yはy+xが、zはz*xが出力される。 |
if(x>y) max=x; else max=y;これは、xとyの大きい方の値がmaxに代入される文である。これを三項演算子を用いると
max = (x>y)? x:y;のように書き換えられる。一般に、三項演算子は、
条件式 ? 式1 : 式2 ;の形式をしており、条件式が満たされたときは式1の値が、それ以外のときは式2の値がこの式全体の評価値となる。
自動的な型変換としては、算術式の中で型の異なる被演算子がある場合、すべての変数の値が、一時的に算術式のなかでいちばん大きい型に変換され、計算され、その結果も大きい型になる。
強制的な型変換を必要とする場合がある。例えば、float型の引数を持つ関数に整数型の変数の値を渡すときには、
関数名( (float) 変数名 );のように、変数名の前に括弧して変換する型名を書いて、強制型変換ができるようになっている。これをキャストと呼ぶ。
C言語でシステムの開発をされる人達は、小さい整数として故意にchar型で変数を宣言し、メモリを効率よく利用することを考えるようであるが、特にメモリ効率などの制約がない場合にはなるべくわかりやすい変数の型宣言をお勧めする。
数学の式 a−(b+c)*d は、括弧内の b+c を計算し、次にdとのかけ算、最後にaとの除算を行なうことは、ご存じのことであろう。これは、括弧や演算子には計算を行なう順序(演算子の優先順位)が決められているからである。C言語の文法にも同様な規則がある。表5−1に演算子の優先順位と式の計算順位についてまとめた。
演算子の優先順位 | 演算子 | 式の評価順位 |
1 | () [] −> | 左から右 |
2 | ! ~ ++ -- - & キャスト sizeof | 右から左 |
3 | * / & | 左から右 |
4 | + − | 左から右 |
5 | << >> | 左から右 |
6 | < <= > >= | 左から右 |
7 | == != | 左から右 |
8 | & | 左から右 |
9 | ^ | 左から右 |
10 | | | 左から右 |
11 | && | 左から右 |
12 | || | 左から右 |
13 | ?: | 右から左 |
14 | = += -= *= /= %= &= ^= |= <<= >>= | 右から左 |
15 | , | 左から右 |
次のプログラム5−2は、演算子の優先順位を理解するための簡単なプログラムである。
/* 1 */ /* Program No.5-2 */ /* 2 */ #include <stdio.h> /* 3 */ main() /* 4 */ { /* 5 */ int x = 1, y = 1, z = 1; /* 6 */ /* 7 */ x = 10 - ( y -= (z +=3) *4 +2) * 2 + 5; /* 8 */ printf(" x y z = %d %d %d\n", x, y, z); /* 9 */ printf("(++x)*6 + (y--) * 4 = %d %d %d\n", (++x)*6 + (y--) *4); /* 10 */ printf(" x y z = %d %d %d\n", x, y, z); /* 11 */ }
4: | x,y,zの宣言と初期化。 |
6: | この文を複文にすると以下のようになる。
z = z + 3; y = y - ( z * 4 + 2 ); x = 10 - y * 2 + 5; |
9: | 8での出力結果から (++39)*6 + (-17)*4 の結果つまり172が出力される。子の場合、xの前にインクリメント演算子が付いているため計算の時には、39+1の値が使われる。また、yの後ろにデクリメント演算子が付いているが、この計算時には−17の値が使われ、printf関数が終了後の、−17−1される。 |
10: | 8の出力結果に対して、x+1,y−1、zが出力される。 |
a = f(x) + g(x) + z(x);この関数の中で、xの値を変更するような文がある場合、3つの関数の評価の順序によってaの値が変わることがある。この場合、3つの関数に同じxが渡される保証がないためである。この式の関数の評価順序は処理系(コンパイラ)によって異なるため、同じプログラムでも実行する計算機に環境によって正常に動作しなくなる可能性がある。このように見かけ上、知らない所で変数の値が変化してしまうことを副作用と呼ぶ。実際の例を示そう。
いま、xに自然数5が代入されている。この状態で
printf(" %d! = %d \n", ++x, fact(x) );が実行されると、結果は
6! = 120と出力されるか、
6! = 720と出力されるかは、はっきりと言うことができない。つまり、printfを実行する直前のxの値は5であるが、factに渡されるxの値が5であるのか、6(++5)であるのかが、C言語の規格では確定されていないため、実行環境によって異なった結果がでてくる。表5−1で示したように演算の優先順位や評価順序は決められているが、関数引数間の計算順序は決められていない。
C言語によらず、このように計算順序に曖昧がある場合は、曖昧さを持たないプログラミングを行なわなければならない。また、誰がみても確実に同じに読みとれるような、わかりやすいプログラムを書くべきである。上記の例な場合、
++x; printf(" %d! = %d \n", x, fact(x) );のように、関数factにわたされるxの値をはっきりとすべきである。
1. printf(" %d! = %d \n", x++, fact(x) ); は副作用を持つか? 2. p[i] = i++; は副作用を持つか? 3. p[i] = ++i; は副作用を持つか?
1.文字列の読み込む。 ・1つの数字を1つの文字として読み込み、順次 配列s[]に格納する。 同時に数字の桁数もカウントする。−−> 結果をketa に代入。 ・改行の文字(コード)がきたら、読み込みを終える。 2.文字列を数字に変換する。 ・1文字を1つの数値に変換する。 s[i] −−> mun[i] ・数値 <−− mun[0]*10^(keta-1) + num[1]*10^(keta-2) + ... + num[keta-1] 3.数値の出力。
/* 1 */ /* Program No.5-3-1 */ /* 2 */ #include <stdio.h> /* 3 */ int power(int basenumber, int n); /* 4 */ main() /* 5 */ { /* 6 */ char c, s[100]; /* 7 */ int i, num[100], number = 0, keta = 0; /* 8 */ /* 9 */ while(((c = getchar()) != EOF) && ( c != '\n') && (keta < 100)) /* 10 */ s[keta++] = c; /* 11 */ s[keta] = '\0'; /* 12 */ /* 13 */ for (i = 0; i < keta; i++) /* 14 */ switch (s[i]) /* 15 */ { /* 16 */ case '0' : num[i] = 0; /* 17 */ break; /* 18 */ case '1' : num[i] = 1; /* 19 */ break; /* 20 */ case '2' : num[i] = 2; /* 21 */ break; /* 22 */ case '3' : num[i] = 3; /* 23 */ break; /* 24 */ case '4' : num[i] = 4; /* 25 */ break; /* 26 */ case '5' : num[i] = 5; /* 27 */ break; /* 28 */ case '6' : num[i] = 6; /* 29 */ break; /* 30 */ case '7' : num[i] = 7; /* 31 */ break; /* 32 */ case '8' : num[i] = 8; /* 33 */ break; /* 34 */ case '9' : num[i] = 9; /* 35 */ break; /* 36 */ default : num[i] = 0; /* 37 */ break; /* 38 */ } /* 39 */ /* 40 */ for(i = 0; i < keta; i++) /* 41 */ number = number + num[i] * power(10, keta-i); /* 42 */ /* 43 */ printf("%s -> %d\n", s, number); /* 44 */ } /* 45 */ /* 46 */ /* power function : basenumber^n (n>=0) */ /* 47 */ int power(int basenumber, int n) /* 48 */ { /* 49 */ int i, num; /* 50 */ /* 51 */ num = 1; /* 52 */ for(i = 1; i <= n; ++i) /* 53 */ num = num * basenumber; /* 54 */ return num; /* 55 */ }
9−10: | 文字の入力部である。読み込んだ文字がエンドオフファイルコード(EOF)か改行コード(\n)か桁数が100以上になるまで繰り返し、配列sに代入する。9:でketa++文があるので、配列sに文字cが代入された後に、ketaが+1される。 |
11: | 配列sの最後の文字列の最後を意味するコードの挿入。43:でsを文字列として出力するため。 |
13−38: | 一文字づつ入力された文字を比べ、対応する数値を配列numに代入している。 |
36−37: | 0から9以外の文字に対しては、numには0を代入するようにしている。 |
40−41: | 配列numに格納されている数値から1つの数値を、考え方の2に基づき生成する。 |
数値 ← mun[0]*10^(keta-1) + num[1]*10^(keta-2) + ... + num[keta-1]ここで、べき乗を計算するために関数powerを呼んでいる。
47: | べき乗を計算する関数powerのヘッダ部である。この関数は、basenumberのn乗を戻すint型の関数である。 |
52−53: | basenumberをn回かけ算した結果をnumに代入している。n=0の時はfor文は実行されないので、basenumber^0の時はnumの初期値である、1が戻るようになっている。 |
入力された文字の数値 <− 入力された文字のコード − 文字'0'のコードで求めることができる。
たとえば、
文字の'5'はASCIIコードで53, 文字の'0'はASCIIコードで48,よって文字'5'を数値に変換するためには、53−48を行なえばよいことになる。
簡単のために数値1234で考えてみよう。プログラム5−3−1の方法では、
num = 1*10^3 + 2*10^2 + 3*10^1 + 4*10^0であった。上記の式を10でくくって変形すると、
num = 1*10^3 + 2*10^2 + 3*10^1 + 4 = 10 * ( 10 * ( ( 10 * 1 ) + 2 ) + 3 ) +4のようになる。これを言葉で言い換えると、一番左の数値を10倍しそれに次の桁の数値を加える。さらにその値を10倍しそのまた次の数値を加える。以下、この操作を繰り返す。このような方法を用いればpower関数を使わなくても、計算できる。
/* 1 */ /* Program No.5-3-2 */ /* 2 */ #include <stdio.h> /* 3 */ main() /* 4 */ { /* 5 */ char c, s[100]; /* 6 */ int i, num[100], number = 0, keta = 0; /* 7 */ /* 8 */ while(((c = getchar()) != EOF) && ( c != '\n') && (keta < 100)) /* 9 */ s[keta++] = c; /* 10 */ s[keta] = '\0'; /* 11 */ /* 12 */ for (i = 0; i < keta; i++) /* 13 */ number = number * 10 + ( s[i] - '0') ; /* 14 */ /* 15 */ printf("%s -> %d\n", s, number); /* 16 */ }
8−9: | プログラム5−3−1と同じ方法で入力する。 |
12−13: | 考え方で述べた方法により変数numberに数値が代入される。 |
プログラム5−3−2は、アルファベット26文字対応するプログラムであってもプログラムのステップ数にはまったく影響ない。しかし、アルファベットの計算機の内部コードがつながっている場合にしか適用できない。例えば、EBCDICコードではアルファベット26文字が続いた内部コードをもっていないため、プログラム5−3−2の12−13:で行なった同様な操作ができない。このように内部コードの並びがわかっていて、他のコードで動く計算機への移植を考える時には、プログラム5−3−1で行なったように1文字づつ比較する方法が確かである。
power関数はこの場合は、使わない方がよいと思われる。もとの式を変形しなくてもよいこと以外はpower関数を使うメリットがなく、計算時間,power関数の適用範囲,プログラムの変更の手間等を考えるとデメリットの方が多いからである。
かけ算の回数により計算時間を比較してみる。プログラム5−3−1では keta + keta-1 + keta-2 + ... + 1 回、プログラム5−3−2では keta 回ですんでいる。 keta が大きくなればなるほど、かけ算の回数も増え計算時間も増して来る。
このように、計算式を少し変更すれば計算時間の短縮ができたり、アルゴリズムが簡単になったりするような時は、開発に大きな支障がない限り行なうべきである。それがよいプログラミングのための一歩であり、プログラマの責任である。