プログラミング序論 page6update:2017/09/07)
[ index | prev | next ] まとめ C言語の式

6. 演算子

(2.入出力と演算子2.3)

C言語では計算処理を数式に似た式で記述できます。しかし、計算機の中身は01を処理する仕掛けなので、このC言語の式は数式とは異なるのです。例えば、

a = b  + c

は数式であれば b と c の値の和が a の値に等しいという変数の関係を表します。しかし、C言語の式であれば b と c の値の和を計算し、その値を変数 a に書き込む(代入:assignment)という手順のプログラムです

a = a + 10

は数式としてみると、aの値は普通の数ではありえないことになります。しかし、C言語の式であれば a の値に10を足し、その値を変数 a に書き込むという手順です。したがって、この式は変数aの値を10増やすプログラムとなります。

C言語の式は処理手順を示すプログラム

C言語のは右辺の値と左辺の値を加算する処理で、結果として合計値を戻します。

C言語のは等しいという条件ではなくて、右辺の値を左辺の変数に代入し,代入した値を戻す。

C言語ではだけでなく演算子と呼びます。

演算子は何らかの処理を行って結果の値を戻します

6-1. 演算子(operator)

まず計算プログラムを関数で書く場合と演算子で書く場合の違いを、三角形の面積を計算する例で考えてみましょう。

底辺aと高さhの三角形の面積を計算するプログラム

.....
double a,h, S;
printf("三角形の底辺と高さを入力して下さい\n");
scanf("%lf%lf",&a,&h);/*変数a,hに標準入力から値を読み取る。*/
/* のような計算を行い面積Sの値を得たい。*/
printf("面積は%f",S);/*面積Sの値を標準出力に書き出す。*/
 .....

関数を使って書くとしたら

仮定の話ですが、代入の関数assignment、掛算の関数product、割算の関数divideがあったと仮定すると、面積の計算を

assignment( divide( product( a, h ), 2 ), &S );

とプログラムできます。この表現は複雑な計算では単調で長くなり、どのような計算か人間には解かり難いものになるのが欠点です。
assignment、product、divideの様な関数は実在しません^^。

演算子を使って書くと

最初の高級言語Fortranの売りは計算プログラムを数式に近い形で書けることでした。C言語も関数の組み合わせではなくて演算子を用いて 数式に近い形で表現できます。面積の計算は

S = a * h / 2;

と記述できます。

演算子を実行する順番

関数で書かれた面積の計算

assignment(
divide(product(a,h),2),&S)

は、掛算=>割り算=>代入 の順番に実行することが明白です。ところが、演算子を使った式

S=a*h/2

で左から順番に演算子を実行すると期待した結果にはなりません。数式と違いすぎると、数式に似せて書く意味が無くなります。そこで、掛算や割り算の演算子を先に実行し代入の演算子は最後に実行するよう、演算子の実行順番における優先順位が決められています。

※数式でも a×b+c×d は+よりも×が優先します

演算子(オペレーター:operator)と対象になる被演算数(オペランド:operand

演算子operator)の対象になるものを被演算数operand)といいます。演算子+や=のオペランドは演算子の左辺と右辺です。C言語では数式には無いようなものも演算子と呼んで、様々な処理を記述するために使います。たとえば前回でてきた変数のアドレスを表す&もアドレス演算子と呼びます。アドレス演算子のオペランドは右辺のみです。

C言語には多数の演算子が在りますが、以下のように演算子とオペランドの配置関係で形式的に分類されます。

オペランドと演算子の位置関係による分類

演算子の位置による分類 配置 C言語に用意された例
中置演算子
infix operator
オペランド1 演算子 オペランド2 a+b
前置演算子
prefix operator
演算子 オペランド -a
後置演算子
postfix notation
オペランド 演算子 ++

同じ記号マイナスでも中置演算子なら引き算を行って値を戻します、前置演算子なら値の符号反転を行い、その値を戻します。

オペランドの数による分類

オペランド数による分類 C言語に用意された例
単項演算子
unary operator
-a、++a、~a
2項演算子
binary operator
a+b、a-b、a=1、、、
3項演算子
trinary operator
式1  式2  式3
式1が真なら式2を評価、偽なら式3を評価して値を戻す
※3項演算子はこの一つしかありません

※3項演算子はこの序論では使いません。次回説明する if 文を利用して同様の処理ができることが多いからです。3項演算子は、1つの式として値を戻す必要があるときに使われるものです。

6-2. 色々な演算子

 C言語では色々な演算子が用意されていますが、必ず値を戻すことを記憶しておいてください。

 例: 4+5はオペランド4と5を加算した値9を戻す。

以下に主なものを紹介していますが、関係演算子や論理演算子は次回にまわします。

6-2-1. 算術演算子 arithmetic operator

算術演算子は数値の演算を行いその結果の値を戻す。

演算子   書式 戻す値
(単項の)+   +a aの値を戻す
(単項の)- 符号反転 -a aの符号を逆にした値を戻す
+ 加算 a+b aにbを加えた値を戻す
- 減算 a-b aからbを引いた値を戻す
* 積算 a*b aとbの積の値を戻す
/ 除算 a/b aをbで割った商を戻す
a,bが共に整数なら商も整数を戻す。5/2は2
aかbが実数なら実数の割り算結果を戻す。5.0/2は2.5
% 剰余算 a%b aをbで割った余りを戻す(左右は整数) 5%2は1

6-2-2. 代入演算子 assignment operator 

1)C言語では代入も演算子で行う。
演算子「=」は左辺の変数に右辺の値を代入する。「=」は値が等しいことを示す記号ではない。

a=10;
はaに値10を代入する。

2)代入演算子は代入した値を戻す

a=b=10;
の様な演算はb=10が戻す値10をaに代入する。

3)演算と組み合わせた代入演算子も用意されている。

total = total + n;
の様な演算を伴う代入は
total += n;
の様に簡略に表現できる。 この書き方は変数totalに加算する事を明確に示す効果がある。

※複数の書き方が可能な処理では、プログラムを理解しやすいか?間違いが起きにくいか?といった基準で書き方を選択する。実行速度も問題だが、それはコンパイラの最適化能力に依存する。

演算子   書式 代入する値(戻す値でもある)
= 代入 a=b

bの値をaに代入する

+= 加算と代入 a+=b

aにbを加えた値をaに代入する (a=a+bと同じ)

-= 減算と代入 a-=b

aからbを引いた値をaに代入する

*= 積算と代入 a*=b

aとbの値の積をaに代入する

/= 除算と代入 a/=b

aをbで割った商をaに代入する
※下記プログラム参照
aが整数型の変数なら整数の割り算結果を戻す。
aが実数型の変数なら実数の割り算結果を戻す。

%= 剰余算と代入 a%=b

aをbで割った余りをaに代入する

※以下のようにプログラムを書いて演算し「/=」の動作を確認してみた

#include<stdio.h>
int main(void)
{
  int a=8;
  double b=5.0;
  
  /*int型の変数の場合*/
  a/=2;
  printf("%d ",a);
  a/=2.0;/* a=a/2.0と同じ計算になるが,a/2.0はdouble型の値これをint型の変数aに代入するので翻訳時に警告がでる*/
  printf("%d\n",a);
  
  /*double型の変数の場合*/
  b/=2;/* b=b/2の計算では2をdouble型の2.0に変換してから計算する。自動型変換可能なので警告なし*/
  printf("%lf ",b);
  b/=2.0;
  printf("%lf\n",b);
  return 0;
}
/*
翻訳結果
 warning C4244: '/=' : 'double' から 'int' への変換です。データが失われる可能性があります。
実行結果
4 2
2.500000 1.250000
*/

6-2-3. インクリメント/ディクリメント 演算子 increment/decrement operator

変数の値を1増減する演算子、戻す値は演算子が前置か後置かで異なる。

演算子 副作用 書式 戻す値
++ 変数の値を1増加 ++a 前置演算子の場合は増加の値を戻す
++ 変数の値を1増加 a++ 後置演算子の場合は増加の値を戻す
-- 変数の値を1減少 --a 前置演算子の場合は減少の値を戻す
-- 変数の値を1減少 a-- 後置演算子の場合は減少の値を戻す


a=a+1;

と書くとaの値と1の値を加算回路に入力し、出力をaに代入する形でaの値を1増やす。このような1増やすような処理は プログラムの中に多く現れる。そこで、CPUは汎用の加算処理とは別に、カウントアップ専用処理を用意していることが多い。機械語にカウントアップ命令があれば、加算する値1をメモリー上に用意する必要も無くなるので機械語も短く できる。

++a;
はaの値をカウントアップする ことを明確にした書き方。a=b+1やa=a+2と書き間違える可能性も無くなる。

※賢いコンパイラはa=a+1と書いても最適化してカウントアップの機械語に翻訳するかもしれない。

6-2-4. ビット演算子 bitwise operator

現在の計算機はデータをbit単位で保持している。C言語ではこのbitデータを操作する演算子も用意されている。
※最後の付録で少し詳しく説明

演算子

 

書式 戻す値
~ ビット反転 ~a ビットを反転した値を戻す
& ビット単位のAND a & b aとbのビット毎のANDをとった値
| ビット単位のOR a | b aとbのビット毎のORをとった値
^ ビット単位のXOR a ^ b aとbのビット毎のXORをとった値
<< 上位bit側へのシフト a << n aのビットを上位側にn個シフトした値
 下位ビットは0を補充
>> 下位bit側へのシフト a >> n  aのビットを下位側にn個シフトした値             
 符号無しの変数は上位に0を補充(論理シフト)
 符号付の変数は最上位ビットの値を補充(算術シフト)

6-3. 演算子の優先順位と結合規則

式の中に複数の演算子があるときは、演算子の実行順番が問題になる。この順番を決めるために用意されたルールが優先順位結合規則です。

6-3-1 優先順位 precedence

複数の演算子を含む式は優先順位の高い順に実行される。

1×2+3×4は演算子×が優先されて(1×2)+(3×4)で14になるが、この様な数式のルールとCプログラムの演算結果が合うように演算子には実行の順番を決める優先順位が定められている。

複数の演算子を含む式は優先順位の高い順に実行される。たとえば
a=b*c+d*e は (a=((b*c)+(d*e))) の順に実行される。

※順番が分かりにくくなる場合は括弧( )を使って順番を明確にするのが良い。

演算子の優先順位を高い順に示すと以下の表のようになる。
この表に在るのは一部分です。詳しくは教科書の最後のページを参照しよう

 
優先順位−−高い−− 結合規則
関数呼び出し (  )  配列要素 [ ]
※こんなものも演算子です
左から右 123456789→
アドレス演算子 & 
単項 + 単項 - 
インクリメント演算子 ++ ディクリメント演算子 --
左から右
論理演算の否定 ! ビットの反転 ~ 
sizeof演算子 キャスト演算子など
←987654321 右から左
乗算 * 除算 / 剰余算 % 左から右
加算 + 減算 - 左から右
ビットシフト <<  >> 左から右
比較 < > <= >= 左から右
ビット単位のAND & 左から右
ビット単位のXOR ^ 左から右
ビット単位のOR | 左から右
論理演算のAND && 左から右
論理演算のOR || 左から右
条件演算子 ? : (3項演算子) 右から左
代入演算子 = +=  右から左
カンマ演算子 , 左から右
−−低い−−  

結合規則については次節で説明しています。

※ 式(expression)
演算子とオペランドを組み合わせたもの。式は戻り値を戻す。式の計算を行うことを、式を評価すると言う。
式の後にセミコロンが付いて文(statement)と成ったものを式文と言う。

※ カンマ演算子(順次演算子) オペランド1 , オペランド2

オペランド1を評価し、続いてオペランド2を評価する。戻り値は右のオペランド2の値。複数の式を1つの式文として書くときに使う。

複数の文: a=10; b=20; c=a+b; /*3個の式文*/
一つの文: a=10, b=20, c=a+b;   /*カンマ演算子で繋いで1個の式文*/

for文の中の初期化や再設定部のように複数の文が書けない所では使われることもある。複数の文で記述できる所では使う必要は無いし、使わない方が良いでしょう。

#include<stdio.h>
int main(void)
{
    int m,n;
    for(m=1, n=5; 0<n; m++, n--){
        printf("%d*%d=%d\n", m, n, m * n);
    }
    return 0;
}
/* 実行結果
1*5=5
2*4=8
3*3=9
4*2=8
5*1=5
*/

※java言語などではC言語の演算子は同様に使えることが多いが、カンマ演算子はjava言語には無い。

6-3-2 結合規則 associativity

優先順位が同じ演算子が連続しているときの実行順番を決める規則

演算子の優先順位を決めるだけでは同じ順位の演算子が連続した場合に実行順番が決まらない。数学の場合は連続した足し算や引き算はどんな順番で行っても結果は同じになる。しかし、計算機の計算は必ずしも同じ結果にならない場合が出てくる。例えば

例)1234567890123456×101- 1234567890123456×101 + 1を計算する場合
(1234567890123456.0e+1     - 1234567890123456.0e+1) + 1  は確かに1.0となるが順番を変えて
 1234567890123456.0e+1 + (- 1234567890123456.0e+1 + 1 ) は0.0となる。
これは(-1234567890123456.0e+1 +1)の計算結果がdouble型の有効数字の関係で-1234567890123456.0e+1とな り、+1が無視される結果となる為

#include<stdio.h>
int main()
{
    printf("%f\n",  1234567890123456E+1);
    printf("%f\n",   1234567890123456E+1    -1234567890123456E+1 +1    );
    printf("\n");
    printf("%f\n", ( 1234567890123456E+1     -1234567890123456E+1 )  +1    );
    printf("%f\n",    1234567890123456E+1 + (-1234567890123456E+1    +1 )  );
    return 0;
}
/*実行結果
12345678901234560.000000
1.000000

1.000000
0.000000
*/

この様に、同じ順位の演算子が連続した場合でも、その実行順番を明確に定める必要がある。この規則が結合規則で多くの場合左から右に実行するが、代入演算子の様に逆の場合もある。

注)実行順番が込み入っている場合は通常の数式と同様に( )を使って計算の順番を指定しましょう。

もう一つ例を示す

こちらは有効数字ではなくて、整数と実数の割り算の違いによるものです。割り算の演算子は整数の割り算も実数の割り算も優先順位は同じです。結合規則は左→右なので最初の割り算が上の式では実数の割り算に下の式では整数の割り算になる。

#include<stdio.h>
int main(void)
{
    printf("%f\n", 2/0.5/3);
    printf("%f\n", 2/3/0.5);

    return 0;
}
/* 実行結果
1.333333
0.000000
*/


2/0.5/3=>(2/0.5)/3=>4.0/3=>1.33333...
2/3/0.5=>(2/3)/0.5=>0/0.5=>0.00000...

6-4. キャスト演算子(cast operator)

データ型を変換する演算子、無制限で変換可能だが結果は自己責任
書式は
(変換先の型名)変換の対象

注)情報が落ちることも有る。

#include <stdio.h>

int main( void )
{
        /*キャスト演算子*/
        printf("(int)3.1415=>%d\n",(int)3.1415);
        printf("(int)2.7=>%d\n",(int)2.7);
        printf("(int)-2.7=>%d\n",(int)-2.7);
        printf("(char)321=>「%c」 十進%d",(char)321,(char)321);
        return 0;
}
/*
(int)3.1415=>3
(int)2.7=>2
(int)-2.7=>-2
(char)321=>「A」 十進65
*/

暗黙的型変換 implicit conversion
(自動型変換:automatic conversion)

数式の中で必要ならchat=>int=>long=>float=>doubleの型変換は自動的に行われる。

演算子(オペレータ)の被演算数(オペランド)の型が演算子の想定に一致しない場合に、自動的に型変換が行われる。例えば割り算は対象が共に整数もしくは共に実数の必要があり、2つの場合で演算内容も異なる。もし、演算対象が実数と整数の組合わせの場合は精度の高い実数型に合わせる自動型変換が行われた後で実数の割り算が行われる。

例  5/2.0は整数定数5をdoubleの値5.0に自動変換してから計算5.0/2.0が行われる。 代入演算では演算対象の型は一致する必要があるが、この場合も型が違えば、代入先の型に変換してから代入が行われる。

#include<stdio.h>

int main( void )
{
        /*基本データ型の変数を用意*/
        char Char;
        short Short;
        int Int;
        float Float;
        double Double;
        
        printf("--------暗黙的型変換-----\n");
        printf("Int = Short = Char =\'A\'; を実行\n");
        Int = Short = Char = 'A';    
        printf("Char  = %d 文字としてはChar = %c\n", Char, Char);
        printf("Short = %d\n",Short);
        printf("Int   = %d\n",Int);
        printf("\n");
        
        printf("Float = Int = 1234567890; を実行\n");
        Float = Int = 1234567890;/*コンパイルで警告有*/
        printf("Int   = %d\n",Int);
        printf("Float = %f\n",Float);
        printf("Double = Int = 1234567890; を実行\n");
        Double = Int = 1234567890;
        printf("Double= %f\n",Double);
        printf("\n");
        
        printf("--------キャストによる型変換-----\n");
        printf("Char = (char)(Short = (short)(Int = 0x12345678)); を実行\n");
        /*キャスト演算子の優先順位が高いので括弧で実行順番を指定しました*/
        Char = (char)(Short = (short)(Int = 0x12345678));    
        printf("Int   = %d 16進表記ではInt = %8x\n", Int, Int); 
        printf("Short = %d 16進表記ではShort =%4x\n", Short, Short);
        printf("Char  = %d 16進表記ではChar = %2x\n", Char, Char);
        printf("\n");
        
        printf("Int = (int)(Float = (float)(Dotble = 12345.6789)); を実行\n");
        Int = (int)(Float = (float)(Double = 12345.6789));
        printf("Double= %f\n",Double);
        printf("Float = %f\n",Float);
        printf("Int   = %d\n",Int);
        
        return 0;
}
/*実行結果
--------暗黙的型変換-----
Int = Short = Char ='A'; を実行
Char  = 65 文字としてはChar = A
Short = 65
Int   = 65

Float = Int = 1234567890; を実行
Int   = 1234567890
Float = 1234567936.000000  <<暗黙的型変換でも精度が落ちることが在ります(コンパイルでの警告あり)
Double = Int = 1234567890; を実行
Double= 1234567890.000000

--------キャストによる型変換-----
Char = (char)(Short = (short)(Int = 0x12345678)); を実行
Int   = 305419896 16進表記ではInt = 12345678
Short = 22136 16進表記ではShort =5678        <<下の2バイトを切り出しただけです
Char  = 120 16進表記ではChar = 78            <<下の1バイトを切り出しただけです

Int = (int)(Float = (float)(Dotble = 12345.6789)); を実行
Double= 12345.678900
Float = 12345.678711 <<精度が落ちています
Int   = 12345         <<小数点以下が切り捨てになりました

*/

6-5. 宿題

課題6.(初期ファイル

 次の数式を実数で計算して値を標準出力へ書き出すプログラムを作成しなさい。

合計を記録する0で初期化された変数totalを用意すること。totalにn=1〜5の範囲で1/n2を足し算する形のプログラムとすること。結果が合えばいいというわけではありません。

期待される結果は%fの書式指定では

1.463611

注意:
1)割り算では値が実数になるようにすること。
2)2乗は同じ値を2度掛け合わせる方が関数を使うよりも簡単で早い。
    ヒント 1.0/(2*2)

[付録] ビット演算子

序論の範囲ではここまでは要求しません。一読する程度でかまいません。

/*ビットシフトの動作確認*/
#include <stdio.h>

void printBits(char c);

int main( void ) {
        signed char sc=-10;
        unsigned char uc=-10;
        printf("---signed char---\n");
        printf("sc     ");printBits(sc);
        printf("sc<<1  ");printBits(sc<<1);
        printf("sc<<2  ");printBits(sc<<2);
        printf("sc<<3  ");printBits(sc<<3);
        printf("sc<<4  ");printBits(sc<<4);
        printf("sc<<5  ");printBits(sc<<5);
        printf("sc<<6  ");printBits(sc<<6);
        printf("sc<<7  ");printBits(sc<<7);
        
        printf("sc<<100  ");printBits(sc<<100);/*コンパイル時点で範囲外の警告*/
        
        printf("--------\n");
        printf("sc     ");printBits(sc);        
        printf("sc>>1  ");printBits(sc>>1);
        printf("sc>>2  ");printBits(sc>>2);
        printf("--unsigned char--\n");
        printf("uc     ");printBits(uc);        
        printf("uc<<1  ");printBits(uc<<1);
        printf("uc<<2  ");printBits(uc<<2);
        printf("--------\n");
        printf("uc     ");printBits(uc);        
        printf("uc>>1  ");printBits(uc>>1);
        printf("uc>>2  ");printBits(uc>>2);     
        return 0;
}
void printBits(char c){
        int i;
        unsigned char mask=0x80;/*maskは2進数1000000*/
        for(i=0;i<8;i++){/*上位bitから書き出す*/
                if(mask & c)putchar('1');
                else putchar('0');
                mask=(mask>>1);
        }
        printf("\n");
}
/*
---signed char---
sc     11110110
sc<<1  11101100
sc<<2  11011000
sc<<3  10110000
sc<<4  01100000
sc<<5  11000000
sc<<6  10000000
sc<<7  00000000
sc<<100  01100000 ※?? sc<<4と同じ数値 確かに100%8は4になるけど?
--------
sc     11110110
sc>>1  11111011 ※この向きのシフトでは符号ビットが補充される
sc>>2  11111101
--unsigned char--
uc     11110110
uc<<1  11101100
uc<<2  11011000
--------
uc     11110110
uc>>1  01111011
uc>>2  00111101
*/

教科書に例が有る

ビット演算の用途

C言語の最小のデータ型はcharで、データ長は1バイト(8ビット)である。しかしデータの中にはもっと少ないビット数で表わせるものもある。たとえは真偽は1ビット有れば表現できるので、1バイトの中に8個まで記憶できる。この様なバイトよりも小さな単位でデータを効率よく操作する為にビット演算子は用意されている。

●特定のビットだけを判定

  1. 調べたいビットだけが1のint型の変数maskを用意する
  2. 調査対象のデータはint型変数dataに格納する
  3. if( data & mask )ビットが1の場合の処理;elseビットが0の場合の処理 の様に&演算子で判定可能

●特定のビットを1に設定

  1. 設定したいビットだけが1のint型の変数maskを用意する
  2. 設定対象のデータはint型変数dataに格納する
  3. data = data | mask; でdataの他のビットを変えることなく目的のビットだけ1に設定可能

●特定のビットを反転したい

  1. 反転したいビットだけが1のint型の変数maskを用意する
  2. 設定対象のデータはint型変数dataに格納する
  3. data = data ^ mask; でdataの他のビットを変えることなく目的のビットだけ反転可能

●その他 自分で考えてみよう
1)特定のビットを0に設定
2)途中の3ビットの値を3桁の2進数の値(10進で0-7)として取り出す


[ index | prev | next ]