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

トップページC言語編

このページの概要 🔗

以下は目次です。


式と文 🔗

ここまであまり踏み込んで考えてきませんでしたが、ここで、 (expression) と (statement) について取り上げておきます。

たとえば、a = b は式です(末尾に ; がないことに注意)。この場合、代入を行う式なので、代入式 (assignment expression) と呼びます。

一方で a = b; のように、末尾に ; が付くと、文 です。文がつねに ; で終わるというわけではなく、ブロックの末尾(})で終わる複合文 (compound statement) のようなものもあります。式の末尾に ; を付けた文は、式文 (expression statement) と呼びます。

式は、演算子とオペランドによって構成され、演算子の種類に応じた演算を行うものです[1]式を実行すると、結果となる値が生み出されます。式から値を得ることを、式を評価 (evaluation) するといいます。また、式を評価すると、後述する副作用が発生する場合があります。

一方、文は、実行すべき動作を決めるものです。[2]たとえば、if文は分岐という動作を実行すべき、for文なら繰り返し処理を実行すべき、return文なら関数から戻る動作を実行すべき、という指示を記述したものだといえます。文からは最終的な値は生み出されません。

a * 2 という式には乗算演算子が使われています。そのため、この式を評価することによって、乗算が行われ、その結果の値が作られます。結果の値はふつう、どこかの変数に入れたり、関数に渡したりします。b = a * 2 とすれば、a * 2 の結果の値が変数b に代入されます。f(a * 2) とすれば、a * 2 の結果の値が関数f に渡されます(関数を呼び出す () も演算子であり、f(a * 2) 全体も式です)。

b = a * 2f(a * 2) も、全体として1つの式になっており、その内側に a * 2 という式が含まれています。このように、式はほかの式の一部になっていることがあります。他の式の一部になっていない式は、完全式(完結式) (full expression) と呼びます。完全式の一部になっている式を、部分式 (subexpression) と呼びます。

副作用 🔗

式のほとんどは、演算の結果として作られる値を得ることを目的にしていますが、そうでないものもあります。a++ は式ですが、a++; という記述だけで完結してしまう使い方はよくあります。この場合、インクリメント演算子による演算を行った結果の値の行き場がありません。しかし、インクリメント演算子はオペランドの値を直接 +1 しますから、変数a 自身の値は変化します。

このように、式を評価した結果、変数の値が変化したり、ファイルに変更が加わったりするといった変化が起こることを、副作用 (side effect) といいます。

a++; は、副作用を得ることだけを目的としているといえます。b = a++; のようにした場合は、変数a の値が変化するのは副作用の結果であり、変数b の値が変化するのは代入式を評価した結果だということになります。

式が起こす副作用は、ある時点をもって完了(確定)されます。この「ある時点」のことを、副作用完了点 (sequence point) といいます。代表的な副作用完了点は、以下のところにあります。

ある副作用完了点から、次の副作用完了点までのあいだに、1つのオブジェクトの値を変更する回数は1回きりでなければなりません[3]。たとえば、a = a++;f(a++, a++); はいずれも、変数a の値が2回変化することになり、このようなコードは未定義の動作です。

void式 🔗

式の結果の値が存在しない(void型である)場合、そのような式は、void式 (void expression) と呼びます。void式は、副作用を得ることを目的に評価する式です。

たとえば、戻り値型が void である関数を呼び出す式は void式です。

void hello(void)
{
    puts("Hello.");
}

int main(void)
{
    hello();  // void式
}

実行結果:

Hello

void式の結果をほかの型へキャストすることはできません。そもそも値が存在していないので当然ではあります。

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

int f(void)
{
    return 0;
}

int main(void)
{
    (void)f();    // OK。戻り値を捨てている
}

実行結果:

左辺値 🔗

結果の型が、オブジェクト型 (object type) か、void 以外の不完全型 (incomplete type) になる式を左辺値 (lvalue) といいます(型の呼び名については「型の分類表」を参照)。左辺値という名前は、代入式で = の左側が左辺値でなければならないことに由来します。[4]

左辺”値” という呼び方でありながら、これは「式」であることに注意してください。しかし、式を評価すると値が得られるのですから、それほどおかしなことでもありません。

左辺値を評価した結果は、特定のオブジェクト (object) を指し示していなければなりません。オブジェクトとは、ある型のある値を持つ、メモリ上の一部分のことをいいます[5]。変数を定義することは、変数の型を持った値をメモリ上に置くことを意味していますが、それはつまり、オブジェクトを作っているということです。

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

左辺値を評価した結果、特定のオブジェクトを指し示さないようなコードは、ここまでのページの範囲では(妙なバグを作りこまないかぎりは)ありえませんが、ポインタ(第31章)を使うと容易に作れてしまいます。

たとえば、定義済みの変数の名前が左辺にあらわれる n = 10 という式において、n は左辺値です。

int n = 0;

n = 10;  // n が左辺値

n は、変数n のことであり、変数n の値はメモリ上にオブジェクトとして存在しているのですから、n は特定のオブジェクトを指し示しているといえます。そのため、n は左辺値として適切です。

ただし、同じ構図にも関わらず、コンパイルが通らないケースもあります。

const int cn = 0;
cn = 10;  // エラー。cn は変更できない

int array1[] = {0, 5, 10};
int array2[3];
array2 = array1;  // エラー。配列に配列を代入できない

これらがコンパイルできないのは、cnarray2変更可能な左辺値 (modifiable lvalue)[6] ではないからです(左辺値であることには変わりありません)。


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

int n = 0;
int n2;

n2 = n;  // n は 0 に置き換えられてから、代入される

これは、これまで当然のように理解してきた挙動のとおりでしょう。この挙動は = 以外にも、多くの演算子で起こりますが、++演算子、–演算子、sizeof演算子などは例外で、左辺値のまま取り扱われます[7]

【上級】そのほかアドレス演算子(第31章)も置き換えは起こりません[7]

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

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

int i;
int j;
int k;

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

int i, j, k;

初期値がある場合には、次のように書けます。

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

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

int i, j = 10, k;

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

int i, j, k = 0;  // i と j は未初期化

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

#include <stdio.h>

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

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

実行結果:

5 10 20

カンマ演算子 🔗

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

#include <stdio.h>

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

実行結果:

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

カンマ演算子(,) のオペランドは2つの式です。まず、左側に書いた式が void式として評価されます。また、この時点をもって副作用完了点になります。そのあと右側のオペランドが評価され、その結果が、式全体としての値になります。[8]

for文の初期設定式で2つ(以上)の変数を宣言することもできますが、これは「同じ型の変数をまとめて宣言する」の項でみた方法を、初期設定式のところに入れているだけのことです。そのため、宣言しようとするそれぞれの変数は、同じ型でなければなりません。

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


練習問題 🔗

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

#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);
}

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

#include <stdio.h>

int main(void)
{
    int num = 90;

    if (num < 100) {
        printf("Less than 100.\n");
    }
    else {
        printf("Over 100.\n");
    }
}

問題③ 10文字の文字列 “abcdefghij” があります。先頭と末尾の両方から 1文字ずつを取り出して出力するプログラムを、カンマ演算子を使って書いてください。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

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



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

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る