先頭へ戻る

いろいろな式 | Programming Place Plus C言語編 第27章

Programming Place Plus トップページC言語編

先頭へ戻る

この章の概要

この章の概要です。


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

たとえば、「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++ );」は問題があります。

左辺値

結果の型が、オブジェクト型か、void 以外の不完全型になる式を左辺値といいます(型の呼び名については「型の分類表」を参照)。

左辺値が評価されたあとは、特定のオブジェクトを指し示しています。オブジェクトとは、ある型のある値を持つ、メモリ上の一部分のことをいいます。これまでの章では、これはつまり変数のことでした。

【上級】第35章で取り上げる方法を使うと、変数を宣言することなく、メモリ上に任意の値を置けるため、大抵、変数という名称を使って説明されるようなことも、オブジェクトという用語を使わないと、不正確な場面がありえます。

変数は、左辺値の具体例としてシンプルなものです。

int n = 0;

n = 10;  // n が左辺値

この場合、n という左辺値は、メモリ上のどこかにある int型のオブジェクトを指し示しているといえます。

左辺値という名前は、代入式で「=」の左側に置かれることに由来しますが、そういう定義ではないので注意してください。

左辺値に対して、右辺値という言葉を使うことがあります。左辺値は名前に反して「式」ですが、右辺値は「式の値」です。標準規格では右辺値とは呼ばず、「式の値」と表現されています。

左辺値になり得るものが「=」の右側に現れることがありますが、その場合、指し示しているオブジェクトの値に置き換えられ、左辺値ではなくなります。

int n = 0;
int n2;

n2 = n;  // n2 は左辺値。n は 0 に置き換えられる

これは、これまで当然のように理解してきた挙動のとおりでしょう。この挙動は「=」の右側に限らず、多くの演算子で起こります。

【上級】例外的なのは、++演算子、–演算子、sizeof演算子、アドレス演算子(第31章)のオペランドです。これらの演算子では、左辺値のまま扱われます。

【上級】配列の場合、置き換えが起こるときには、先頭の要素を指すポインタに置き換えられます(第32章)。ただし、sizeof演算子、アドレス演算子のオペランドになったときと、文字の配列を初期化するときに使う文字列リテラル(文字列リテラルは配列です(第32章))は、配列のまま扱われます。

void式

通常、式は何らかの結果、つまり値を得ようとするものですが、まれに結果の値がないケースがあります。このような式は、void式と呼びます。結果の値がないとはいえ、その式の評価過程で副作用が起こるのなら、式自体には意味があります。

具体的には、戻り値型が void の関数を呼び出す式が挙げられます。

void f(void)
{
}

int main(void)
{
    f();                // OK
    int n = (int)f();   // コンパイルエラー

    return 0;
}

このサンプルプログラムのように、void式の結果をほかの型へキャストすることはできません。これは型の問題というより、そもそも値がないので当然ではあります。

また、戻り値型が void ではない関数でも、呼び出し側で void型へキャストすると、全体として void式になります。そのような行為に大きな意味はありませんが、戻り値があるのに使っていないときに警告を出すコンパイラに、不要であるという意思を伝えるために行う場合があります。

int f(void)
{
    return 0;
}

int main(void)
{
    (void)f();               // OK。戻り値を捨てた
    int n = (int)(void)f();  // コンパイルエラー

    return 0;
}


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

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

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)
{
    for( int 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つの式は、左側から評価されます。全体としての結果は、一番右側の式を評価した結果になります。

for文の初期設定式で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

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

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

「真のときの式」と「偽のときの式」の型には条件があります。単純にいえば、同じ型であればいいということですが、暗黙の型変換を使って両方の式を同じ型に揃えられるのならば、それでも受け付けます。

たとえば、「真のときの式」と「偽のときの式」が算術型(型の分類表)の場合、それぞれに通常の算術型変換(第21章)を行って得られる型が、結果の型です。よって、一方が int型で 他方が double型であっても受け付けられ、結果は double型になります。

なお、「真のときの式」と「偽のときの式」がともに void型であっても構いません。その場合、結果の値はありません。

条件演算子を使うと、条件分岐のコードを1文で書ける魅力はありますが、あまり多用すると見づらくなりがちです。たとえば、入れ子になるように使用した次のコードは、読みやすいものとは言えません。

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

定数式

コンパイル時に結果を確定できるような式は、定数式と呼ばれます。つまりは、定数だけで構成されている式ということで、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演算子に与える式の中では、これらが含まれていても構いません。可変長配列(第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 「定数式」の項を追加。

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



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

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー