C言語編 第27章 いろいろな式

先頭へ戻る

この章の概要

この章の概要です。

ここまであまり踏み込んで考えてきませんでしたが、ここで「」というものについて取り上げておきます。

例えば、「a = b」のようなものが式です。これは全体として、代入式を構成しています。

一方で「a = b;」のように、末尾に「;」が付いていると、式ではなくになります. 文が常に「;」で終わるという訳ではなく、ブロックの末尾「}」で終わる複合文のようなものもあります。式の末尾に「;」を付けた文は、式文と呼びます。

式は、演算子を使って何らかの演算を行い、結果(値)を得ようとするものです

一方、文とは「実行すべき動作を決める」ものです。例えば、if文なら分岐という動作を実行すべき、for文なら繰り返し処理を実行すべき、return文なら関数から戻る動作を実行すべき、という指示を与えているといえます。

「a * 2」は式ですが、これは、乗算演算子を使って乗算を行い、結果を得ようとしています。実際のコードでは、「b = a * 2」だとか「f(a * 2)」といった使い方をするでしょう。これは「a * 2」という式から得られた値を、b という変数へ代入したり、f という関数へ引き渡したりしています。なお、式から値を得ることを、式を評価するといいます

ここで、代入は代入式で行われています。関数呼び出しも、戻り値という形で結果を得る式です。そのため、「b = a * 2」も「f( a * 2 )」も、全体として1つの式であり、その内側に「a * 2」という式が含まれていることになります。このように、式はほかの式の一部になっていることがあります。他の式の一部になっていない式は、完全式(完結式)と呼びます。

完全式のほかの例として、if や switch、while、do の条件式や、for の ( ) 内の3つの式があります。

値を得ることを目的としていない式もあります。例えば「a++」は式ですが、これを「a++;」とだけ書いた場合、「a++」を評価した値の行き場がありません。しかし、変数a 自身の値は変化します。式を評価した結果、変数の値が変化するなど、実行環境に何らかの影響を与えることを、副作用といいます。「a++;」は副作用を得ることだけを目的としていると言えます。「b = a++;」の場合に、変数a の値が変化するのは副作用の結果、変数b の値が変化するのは代入式の結果です。

副作用は、特定のタイミングをもって確定します。このような位置を、副作用完了点といいます。逆にいえば、副作用完了点に到達するまで、副作用による変数の値の変化は確定していません。代表的な副作用完了点は、以下のところにあります。

1つの変数の値を、副作用完了点に到達するまでの間に2回以上変更することは未定義の動作になるため、避けなければなりません。例えば、「a = a++;」や「f( a++, a++ );」は問題があります。


同じ型の変数をまとめて宣言する

幾つかの変数を宣言するとき、今までは次のようにしてきました。

int i;
int j;
int k;

変数の型が同じであれば、次のようにまとめて宣言することができます。

int i, j, k;

初期値がある場合には、次のように書くことができます。

int i = 0, j = 10, k = 20;

一部の変数にだけ初期値を与えることもできます。

int i, j = 10, k;

特に、1番最後のところにだけ初期値を書くと、すべての変数に同じ初期値が与えられているように錯覚することがあるので、注意して下さい。

int i, j, k = 0;  /* i と j は不定値 */

個人的には、初期値を与えるのであれば、これまで通り、別々の行で宣言した方が安全ですし、見やすくもあると思います。

不定値は避けるべきなので、原則的には、宣言と同時に初期値を与えた方が良いです。その意味では、変数をまとめて宣言する構文はあまり使う機会が無いと思います。C95規格までは、ローカル変数をブロックの先頭で宣言しないといけないため、その時点で適切な初期値を与えることができないことがあり、やむを得ないこともあります。

次のプログラムは使用例です。

#include <stdio.h>

int main(void)
{
    int a = 5, b = 10, c = 20;

    printf( "%d %d %d\n", a, b, c);

    return 0;
}

実行結果:

5 10 20

複合代入演算子

これまでの章でも既に何度か使っていますが、「+=」のような演算子を使うと、演算と代入を1つの式で表現できます。 このような操作は、複合代入と呼ばれ、そのための演算子を複合代入演算子と呼びます。 また、対比のため、通常の =演算子による代入を、単純代入と呼ぶことがあります。

複合代入演算子には、「+=」「-=」「*=」「/=」「%=」といったものがあります。

まだ登場していませんが、「&=」「|=」「^=」「<<=」「>>=」もあります。これらは第49章で説明します。

#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 10;

    a += 15;
    printf( "%d\n", a );

    a -= b;
    printf( "%d\n", a );

    a *= 6;
    printf( "%d\n", a );

    a /= 2;
    printf( "%d\n", a );

    a %= b;
    printf( "%d\n", a );

    return 0;
}

実行結果:

15
5
30
15
5

複合代入は、例えば「a = a OP b」という式を、「a OP= b」と書き換えたものです(OP の部分に「+」や「-」などの演算子が入ります)。ですから、複合代入がなくても、他の書き方が必ず存在します。このように、必須とは言えないまでも、読み書きしやすくする目的で導入されている構文のことを総称して、構文糖(シンタックスシュガー)と呼びます。

厳密にいえば、「a = a OP b」の書き方では、a が2回評価される点が、複合代入と異なります。例えば「array[i++] = array[i++] * 2」と「array[i++] *= 2」では、意味も結果も異なるでしょう。しかし、そもそもこういうコードは避けるべきです。

なお、ソースコードを短く書いたからといって、必ずしも実行速度も向上するというものではありません。 ソースコードの長さと、実行効率の間には、明確な比例関係は存在しません。

複合代入の右辺側が、「b * 2」のような式になっていても構いません。 例えば「a += b * 2;」のような文は許されます。 この場合、右辺側の式が先に実行され、その結果を a に加算したものが a へ代入されることになります。 括弧を補うと「a += (b * 2);」ということです。
優先順位の面でやや不安を感じるかも知れませんが、複合代入演算子の優先順位は、普通の代入演算子と同じです (演算子の優先順位については、APPENDIX を参照して下さい)。

代入の連結

同じ値を、複数の変数に代入したい場合があります。これまでは次のようにしてきました。

a = 100;
b = 100;
c = 100;

このように、同じ値を複数の変数に代入するときには、次のようにつなげて書くことができます。

a = b = c = 100;

勿論、変数から変数への代入でも構いません。

a = b = c = x;

次のプログラムは使用例です。

#include <stdio.h>

int main(void)
{
    int a, b, c;
    int x = 99;

    a = b = c = 100;
    printf( "%d %d %d\n", a, b, c );

    a = b = c = x;
    printf( "%d %d %d\n", a, b, c );

    return 0;
}

実行結果:

100 100 100
99 99 99

このような連続した代入は、次のように分解して考えることができます。

a = (b = (c = 100));

これは、代入が「式」であることを示す好例です。 式は何らかの結果を生んでいるので、その結果をまた新たな代入式として使うことができるのです。
一番右側の代入式「c = 100」は「100」という結果を生んでいます。 この結果を使って「b = 100」という代入式を構築できます。これもまた「100」という結果を生みます。 さらにこれを使って「a = 100」という代入式を構築します。

このように、代入式が連結できるのは「式」だからなのです。 例えば if文全体を代入するという行為ができないのは、if が「文」であって、結果を生んでいないからです。

/* コンパイルエラー */
a = if( x != 0 ) {
    x = x * 2;
}

カンマ演算子

カンマ演算子を使うと、複数の式を連結できます。 よくあるのは、for文で 2つ以上のループ制御変数を使うというものです。

#include <stdio.h>

int main(void)
{
    int i, j;

    for( i = 0, j = 20; i < j; ++i, --j ){
        printf( "%d %d\n", i, j );
    }

    return 0;
}

実行結果:

0 20
1 19
2 18
3 17
4 16
5 15
6 14
7 13
8 12
9 11

カンマ演算子(,) で区切られた 2つの式は、左側から処理されます。全体としての式の結果は、一番右側の式の結果が使われます。

カンマ演算子は、2個以上の実引数を渡すときに使う「,」や、配列や構造体を初期化するときに現れる「,」とは別のものです。区別が付かないので、これらの場所ではカンマ演算子を使えません(括弧を補えば使えます)。

条件演算子

条件演算子は、if文の変形のようなもので、演算子を使って分岐構造が実現できます。 正確な名前とは言い難いのですが、三項演算子と呼ばれることもあります。

この演算子は、? と : という 2つの記号を使って、3つの部分に分けて書きます。 構文は次の通りです。

条件式 ? 真のときの式 : 偽のときの式

例えば、絶対値を返す関数を次のように書けます。

#include <stdio.h>

int abs(int num);

int main(void)
{
    printf( "%d\n", abs( 10 ) );
    printf( "%d\n", abs( -10 ) );
    printf( "%d\n", abs( 0 ) );

    return 0;
}

int abs(int num)
{
    return (num < 0) ? (-num) : (num);
}

実行結果:

10
10
0

ソースコードを短く書ける魅力はありますが、あまり多用すると見づらくなりがちです。 例えば、ネストして使用した次のコードは、読みやすいものとは言えません。

res = (a > 0) ? ( (b > 100) ? ('A') : ( (b > 50) ? ('B') : ('C') ) ) : ('D');

if文と違って、式であるため、関数の実引数のところに埋め込めんだり、変数の初期値として使ったりできます。

int num = (x == 0) ? (0) : (x * 2);
printf( "入力は100ですか?  %s\n", (in == 100) ? ("YES") : ("NO") );

定数式

コンパイル時に結果を確定させることができるような式は、定数式と呼ばれます。つまりは、定数だけで構成されている式ということで、「3 + 5」だとか「5.5 * 2」といったものです。

定数式の中にある演算はコンパイル時に処理され、定数式全体は結果の値に置き換わります。例えば、「int a = 3 + 5;」と書いても、コンパイル時に「3 + 5」の部分は計算されて、「int a = 8;」に置き換わるということです。そのため、定数式中の計算が複雑であっても、プログラムの実行速度は落ちません。

このように、定数式は結局、定数値に置き換わるので、定数を使える箇所でなら、いつでも定数式を使うことができます。例えば、switch文の caseラベルのところで、定数式を使っても構いません。

#define BEGIN (100)

switch( n ){
case BEGIN:
    break;
case BEGIN + 1:
    break;
case BEGIN + 2:
    break;
default:
    break;
}

定数式は、コンパイルの時点で結果を得る必要があるため、関数呼び出しを含むことはできません。同じ理由から、変数を使うことができないので、代入を行ったり、インクリメントやデクリメントを行ったりすることもできません。また、カンマ演算子を含めることもできません。

例外として、sizeof演算子に与える式の中では、これらが含まれていても構いません。C99 の可変長配列(第25章)を使う場合を除いて、sizeof演算子は式の評価を行わないためです。

定数を使わねばならない場面として、以下のような箇所があります。これらのすべてで、定数式を使用できます(まだ解説していないものも含んでいます)。

C11 (定数式を使う場面)

C11 ではさらに、以下の箇所で定数(定数式)を使います。


練習問題

問題① 次のプログラムを、できるだけ短く書き直して下さい。

#include <stdio.h>

int main(void)
{
    int num1 = 10;
    int num2 = 20;
    int num3 = 30;

    printf( "%d %d %d\n", num1, num2, num3 );

    num1 = num3;
    num2 = num3;

    printf( "%d %d %d\n", num1, num2, num3 );

    return 0;
}

問題② 次のプログラムを、条件演算子を使って書き直して下さい。

#include <stdio.h>

int main(void)
{
    int num = 90;

    if( num < 100 ){
        printf( "100未満\n" );
    }
    else{
        printf( "100以上\n" );
    }

    return 0;
}

問題③ 次のプログラムを、複合代入演算子を使って書き直して下さい。

#include <stdio.h>

int main(void)
{
    int num = 15;
    int num2 = 5;
    int num3 = 30;

    num = num - 10;
    num2 = num2 + (num * 4);
    num3 = num3 % (num2 - 5);

    printf( "%d %d %d\n", num, num2, num3 );

    return 0;
}

問題④ 10文字の文字列 "abcdefghij" があり、先頭と末尾の両方から 1文字ずつを取り出して出力するプログラムを書いて下さい。 ただし、カンマ演算子をうまく使って下さい。
例として、"abcde" と入力された場合、次のように出力されるようにして下さい。

a e
b d
c c
d b
e a


解答ページはこちら

参考リンク



更新履歴

'2018/5/14 「」の項を大幅に加筆・修正。評価や副作用に関する説明を追加した。

'2018/5/12 「定数式」の項を追加。

'2018/3/2 全面的に文章を見直し、修正を行った。
章のタイトルを「簡略記法」から「いろいろな式」に変更。 「」の項を追加。
「文字列定数の連結」の項を、第46章へ移動。
「C99 (通常の文字列定数と、ワイド文字列定数との連結)」の項を、第47章へ移動。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

≪更に古い更新履歴を展開する≫



前の章へ(第26章 構造体)

次の章へ(第28章 関数形式マクロ)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS