CONTENTS / BACK-PAGE / NEXT-PAGE

 ここしばらくの間、新しいことの解説はなく今までの復習をかねて演習をおこなってきました。今回からは、さらに一歩進めて行こうと思っております。返事が遅れることもあるとは思いますが、単純な質問でもOKですから、何でも質問してください。

 


5.C言語特有の文法

 本章ではC言語の特徴を生かすための文法について解説する。ここで解説する内容は、C言語特有の記述ではあるが、その考え方,利用方法,用途は他のプログラミング言語と共通するものがあるということを、頭にとどめておいて欲しい。また、この章は文法の解説が多くなってしまい多少退屈つるかもしてないが、エレガントなプログラムを書くためにしばらく我慢して頂きたい。

5.1 演算子

5.1.1 インクリメント,デクリメント

 繰り返しのfor文を思いうかべてよう。

	  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]; である。

5.1.2 代入演算子

 C言語では、さらに代入演算子と呼ばれる+=,−=,*=,/=,%=がありそれぞれ以下の意味を持つ。

	   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;

と書くことにより、前者であることが一目瞭然である。

 このように代入演算子を使うことにより、プログラムを見やすくするばかりか、プログラマのタイプミスも多少防ぐことができる。また、コンパイラなどの処理系にとっても、効率のよいキカイ語(マシン語)コードを生成するのに役立つ。


5.1.3 代入の連接

 プログラムの変数の初期化で、

	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つの演算を理解しよう。


プログラム5−1 簡単なプログラム(演算子編)

/*  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 */  }


実行結果

x y z = 1 1 1 ++x --y z++ = 2 0 1 x y z = 2 0 2 x y z = 2 2 4

***解説***

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が出力される。


5.1.4 三項演算子

 4.1節で条件文(if文)について学んだが、ここでは同じ1文で条件の制御をする三項演算子を紹介しよう。まず、次のようなif文を考える。

	 if(x>y)
	        max=x;
	 else
	        max=y;
これは、xとyの大きい方の値がmaxに代入される文である。これを三項演算子を用いると

max = (x>y)? x:y;
のように書き換えられる。一般に、三項演算子は、

条件式 ? 式1 : 式2 ;
の形式をしており、条件式が満たされたときは式1の値が、それ以外のときは式2の値がこの式全体の評価値となる。


演習問題

例4−2を三項演算子を用いて書け。


5.1.5 型変換(キャスト)

 プログラムにおいて、一般的には代入の=の左辺と右辺が同じ型でなければならない,算術演算の両者の型が同じでなければならない。文字型の配列変数に算術演算の結果を代入してもまったく意味のないものになってしまうような意味上のエラーチェックを、コンパイラが行なっている。しかし、int型どうしの除算の結果はfloat型になり、実際このような計算結果を要求することがある。このように狭い情報を広い情報に拡張される型変換はおこなってもよく、たとえばint型からfloat型への代入は、自動的に型変換されエラーなく実行される。また、char型は、小さい整数としても解釈されるため、そのまま算術演算が可能である(文字コードがそのまま整数として解釈されるため)。

 自動的な型変換としては、算術式の中で型の異なる被演算子がある場合、すべての変数の値が、一時的に算術式のなかでいちばん大きい型に変換され、計算され、その結果も大きい型になる。

 強制的な型変換を必要とする場合がある。例えば、float型の引数を持つ関数に整数型の変数の値を渡すときには、

関数名( (float) 変数名 );
のように、変数名の前に括弧して変換する型名を書いて、強制型変換ができるようになっている。これをキャストと呼ぶ。

 C言語でシステムの開発をされる人達は、小さい整数として故意にchar型で変数を宣言し、メモリを効率よく利用することを考えるようであるが、特にメモリ効率などの制約がない場合にはなるべくわかりやすい変数の型宣言をお勧めする。


5.1.6 演算の優先順位

 4.1節の終わりに優先順序について少し解説したが、ここでは5.1節で紹介した演算子を加えさらに詳しく解説する。

 数学の式 a−(b+c)*d は、括弧内の b+c を計算し、次にdとのかけ算、最後にaとの除算を行なうことは、ご存じのことであろう。これは、括弧や演算子には計算を行なう順序(演算子の優先順位)が決められているからである。C言語の文法にも同様な規則がある。表5−1に演算子の優先順位と式の計算順位についてまとめた。


表5−1 演算子の優先順位と式の計算順位

演算子の優先順位 演算子 式の評価順位
() [] −> 左から右
! ~ ++ -- - & キャスト sizeof 右から左
* / & 左から右
+ − 左から右
<< >> 左から右
< <= > >= 左から右
== != 左から右
左から右
左から右
10 左から右
11 && 左から右
12 || 左から右
13 ?: 右から左
14 = += -= *= /= %= &= ^= |= <<= >>= 右から左
15 左から右

 表5−1からもわかるように、配列の添字の括弧[]や、丸括弧()内は、最も優先順位が高い。つまり、初めに計算される。また、代入演算子は他のほとんどの演算子よりも優先順位が低い、++,−−は高い優先順位をもつことを覚えておいて欲しい。表の中でまだ解説していない演算子(ビット演算子など)もあるので、それらについては、後ご期待を。

 次のプログラム5−2は、演算子の優先順位を理解するための簡単なプログラムである。


プログラム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 */  }


実行結果

x y z = 39 -17 4 (++x)*6 + (y--) * 4 = 172 x y z = 50 -18 4

***解説***

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が出力される。

***注意***

 関数呼び出しには、副作用と呼ばれる代入が発生する。副作用という言葉は、病気の時に薬を飲んで、その病気がなおってもその薬の影響で違う病気になってしまった時に使われる言葉である。それと同じように、予期しないところで、変数に代入がおこりプログラマが求めようとする結果が得られないことがある。例えば、3つの関数f,g,zの結果を加算し、aに代入する以下の式を考えてみよう。

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; は副作用を持つか?

5.1.7 例題

5.1節の最後として、実際の例を示そう。

例5−3−1

 数値を文字列として入力し、数値に変換するプログラムを作れ。

考え方

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.数値の出力。

プログラム5−3−1 1文字づつ比較する方法

/*  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 */  }

実行結果

1234 1234 -> 1234 5432 5432 -> 5432

***解説***

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が戻るようになっている。


例5−3−2

例5−3−1で文字の比較とpower関数を使わない方法でプログラムせよ。

考え方

  • 1文字づつ比較をしないで文字を数値に変換するためには、文字コード(計算機内のコード)を利用する。幸いにASCIIコードやEBCDICコードでは、0から9の文字は、0から順につながった文字コードである。よって入力された文字を数値に変換するためには、

     入力された文字の数値 <− 入力された文字のコード − 文字'0'のコードで求めることができる。

     たとえば、

    	文字の'5'はASCIIコードで53,
    	文字の'0'はASCIIコードで48,
    
    よって文字'5'を数値に変換するためには、53−48を行なえばよいことになる。

  • power関数を使わない方法

     簡単のために数値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関数を使わなくても、計算できる。


プログラム5−3−2 内部コードを利用した方法

/*  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 */  }

実行結果

456 456 -> 456 987 987 -> 987

***解説***

8−9:プログラム5−3−1と同じ方法で入力する。
12−13:考え方で述べた方法により変数numberに数値が代入される。

***注意***

 プログラム5−3−1の方法は、一般に考えられる方法であり決して悪い方法ではない。素直に考えそのままプログラミングした結果である。しかし、多少プログラミングの経験者であれば、同じ文の繰り返し(16−39)は使わないであろう。このプログラムの欠点は、文字の比較を1つづつ行なっているため、'0'−'9'の10種類であるからこの程度のステップ数で済んだのだが、アルファベット26文字が対象になった場合には、少し考える必要があるということである。また、power関数を用いると毎回10のべき乗を計算するため、計算時間が多くかかってしまうことである。

 プログラム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 が大きくなればなるほど、かけ算の回数も増え計算時間も増して来る。

 このように、計算式を少し変更すれば計算時間の短縮ができたり、アルゴリズムが簡単になったりするような時は、開発に大きな支障がない限り行なうべきである。それがよいプログラミングのための一歩であり、プログラマの責任である。


演習

例5−3の問題で配列を使わない方法で、プログラムせよ。


CONTENTS / BACK-PAGE / NEXT-PAGE