代入演算子の応用

多重代入

変数1 = 変数2 = 変数3 = ... = 変数N = 値;
#include

int main(void)
{
    int i, j, k;

    i = j = k = 99;

    printf("%d %d %d\n", i, j, k);

    return 0;
}
takatoh@nightschool $ ./sample_11_8a
99 99 99

演算子の省略記法

例えば

x = x + 3;

という代入があったとき、これを省略記法で

x += 3;

と書ける。省略記法で書ける演算子は次のとおり。

+ - * / % << >> & | ^
#include

int main(void)
{
    int i;

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

    return 0;
}
takatoh@nightschool $ ./sample_11_8b
0
2
4
6
8
10
12
14
16
18

条件演算子(?)

Ruby にもある三項演算子 ? : 。

変数 = 条件 ? 式1: 式2;

次のプログラムは、入力された整数が 0 以上であれば 1 を、0 より小さければ -1 を表示する。

#include

int main(void)
{
    int i;

    printf("Input number: ");
    scanf("%d", &i);

    i = i >= 0 ? 1: -1;

    printf("%d\n", i);

    return 0;
}
takatoh@nightschool $ ./sample_11_7
Input number: 7
1
takatoh@nightschool $ ./sample_11_7
Input number: -3
-1

シフト演算子

シフト演算子は、変数の各ビットを左または右に指定したビット数だけ移動する。移動して範囲からはみだしたビットは消失し、その反対側のビットは 0 で埋められる。

<<左シフト
>>右シフト

シフト演算子の一般的な形式は次のとおり。

値 << ビット数;
値 >> ビット数;

次のプログラムは、左に 1 ビットシフトしたあと、右に 1 ビットシフトしている。元の値には戻らずに、いちばん左のビット(1)が失われているのがわかる。

#include

void show_binary(unsigned short u);

int main(void)
{
    unsigned short u;

    u = 45678;

    show_binary(u);
    u = u << 1;
    show_binary(u);
    u = u >> 1;
    show_binary(u);

    return 0;
}

void show_binary(unsigned short u)
{
    unsigned short n;

    for (n = 32768; n > 0; n = n / 2) {
        if (u & n) {
            printf("1");
        } else {
            printf("0");
        }
    }

    printf("\n");
}
takatoh@nightschool $ ./sample_11_6
1011001001101110
0110010011011100
0011001001101110

ビットごとの演算子

ビット単位で演算を行う演算子として次の4つがある。

&ビットごとの AND
|ビットごとの OR
^ビットごとの XOR(排他的論理和)
~1 の補数

1 の補数というのは、ビットの 0 と 1 を反転させる単項演算子だ。次のプログラムで、その動作を確認することができる。

#include

void print_binary(char ch);

int main(void)
{
    char ch;

    ch = 'a';

    print_binary(ch);

    ch = ~ch;

    print_binary(ch);

    return 0;
}

void print_binary(char ch)
{
    int i;

    for (i = 128; i > 0; i = i / 2) {
        if (i & ch) {
            printf("1");
        } else {
            printf("0");
        }
    }
    printf("\n");
}
takatoh@nightschool $ ./sample_11_5
01100001
10011110

ちゃんと各ビットが反転してるね。

typedef

typedef は既存の型に新しい名前をつける。一般的な形式は次のとおり。

typedef 既存の型名 新しい型名;

次の例では、signed char に新しい型名 smallint をつけて、変数 i を宣言するのに使っている。

#include

typedef signed char smallint;

int main(void)
{
    smallint i;

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

    return 0;
}

typedef を使っても元の型名が無効になるわけではない。上の例では signed char も型として使える。 また、typedef を何度も使って同じ型に違う名前をいくつもつけることができる。しかも、typedef でつけた型名は、既存の型として扱うこともできる。次の例では、変数 d の型は depth として宣言しているけど、実体としては int だ。

typedef int height;
typedef height width;
typedef width depth;
depth d;

typedef を使って新しい型名をつけるのには、2つのメリットがある。 1つはプログラムの移植性に関わる問題で、例えば2バイト長の変数が使いたい場合、int が2バイトの環境では次のようにして、変数の型を myint にしておく。

typedef int myint;
myint i;

そして、2バイトの型が short int の環境に移植するときには、typedef 文だけを変更すればいい。

typdef short int myint;

プログラム中で使っている myint を修正する必要はない。 もう1つは、型名に自己説明的な名前をつけることができることだ。次のようにしておけば、subtotal として宣言された変数が「小計」を表すことをプログラムから読み取れる。

typedef double subtotal;

列挙型

列挙型(enumeration)は、名前付きの整数定数のリストで、整数が使用できるところならどこでも使用できる。列挙型の宣言の一般的な形式は次のとおり。

enum タグ名 {列挙リスト} 変数リスト;

「タグ名」と「変数リスト」のどちらかは省略できる。構造体と同様、「タグ名」が列挙型の型名となる(より厳密には「enum タグ名」で型を表す)。

次のプログラムは、列挙型 color_type とその変数 color を使って、値を出力している。
列挙型の実体は整数で、デフォルトではコンパイラによって 0 から割り当てられる。プログラムの出力を見ると、そのようになっているのがわかる。

#include

enum color_type {red, green, blue};

int main(void)
{
    enum color_type color;

    color = red;
    printf("%d\n", color);

    color = green;
    printf("%d\n", color);

    color = blue;
    printf("%d\n", color);

    return 0;
}
takatoh@nightschool $ ./sample_11_3
0
1
2

型修飾子

前にデータ型修飾子というのが出てきた。「型修飾子」は名前が似てて紛らわしいけど、別のもの。変数を宣言するときに、その変数の性質をコンパイラに伝えることができる。

const

宣言時に型の前に const をつけると、その変数はプログラム中で変更できなくなる。ただし、宣言時に初期化することはできる。次の例は正しい例。

#include

int main(void)
{
    const int i = 10;

    printf("%d\n", i);

    return 0;
}
takatoh@nightschool $ ./sample_11_2a
10

こっちは正しくない例。コンパイル時に read-only の変数に代入しているというエラーが出ている。

#include

int main(void)
{
    const int i = 10;

    i = 20;
    printf("%d\n", i);

    return 0;
}
takatoh@nightschool $ gcc sample_11_2b.c -o sample_11_2b
sample_11_2b.c: In function ‘main’:
sample_11_2b.c:8:5: error: assignment of read-only variable ‘i’
     i = 20;
     ^

宣言時に初期化しないで、あとから代入したらどうだろう?

#include

int main(void)
{
    const int i;

    i = 20;
    printf("%d\n", i);

    return 0;
}
takatoh@nightschool $ gcc sample_11_2c.c -o sample_11_2c
sample_11_2c.c: In function ‘main’:
sample_11_2c.c:8:5: error: assignment of read-only variable ‘i’
     i = 20;
     ^

同じエラーが出た。const で修飾した変数は、宣言時に初期化しないといけない。

volatile

宣言時に、型の前に volatile をつけると、その変数がプログラム中で明示されない方法で変更される可能性があることを、コンパイラに伝えることになる。「プログラム中で明示されない方法」というのは、例えば外部からの割り込みによって変更されるような場合だ。
コンパイラはプログラムをコンパイルするときに最適化を行うけど、volatile で修飾された変数は参照するたびに内容を調べ直すようになる。

記憶クラス指定子

C には、変数の格納方法を指定する「記憶クラス指定子」として、次の4つがある。

  • auto
  • static
  • extern
  • register

auto

auto は自動変数を宣言するためのもので、自動変数(automatic variables)とは、要するに単なるローカル変数のこと。ローカル変数はデフォルトで auto になるので、明示的に auto を使うことはまずない。

static

ローカル変数は、関数から抜けるとメモリ領域から削除されるけど、static を指定すると、ローカル変数の内容を複数の関数呼び出しにまたがって保持することができる。関数呼び出しのたびに初期化される普通のローカル変数と違って、static な変数の初期化は1回しか行われない。そして、値はプログラムが終了するまで保持される。
次のプログラムは、関数 f を10回呼び出している。関数 f の中の static な変数 count の値が、呼び出しのたびに初期化されることなく、前回の呼び出しの時の値を保持していることがわかる。

#include

void f(void);

int main(void)
{
    int i;

    for (i = 0; i < 10; i++) {
        f();
    }

    return 0;
}

void f(void)
 {
    static int count = 0;
    count++;
    printf("count: %d\n", count);
}
takatoh@nightschool $ ./sample_11_1a
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
count: 10

extern

プログラムのサイズが大きくなった場合、C ではファイルを複数に分けることができる。このとき複数のファイルからアクセスする必要があるグローバル変数があったとする。グローバル変数は1回しか宣言することが出来ないので、複数のファイルそれぞれに宣言を書くことは出来ない。かといって、宣言がないとコンパイラが変数の宣言を見つけられずにエラーになってしまう。
こういう場合、通常の方法でグローバル変数を宣言するのは1つのファイルだけにしておいて、その他のファイルでは extern 指定子を使って宣言する。extern はそのグローバル変数が別のファイルで宣言されていることをコンパイラに伝える役割をする。次の例は、ファイル sample_11_1b1.c で宣言されたグローバル変数 count を、ファイル sample_11_1b2.c でも使えるように extern 指定子を使っている。

#include

int count;

void f1(void);

int main(void)
{
    int i;

    f1(); /* set value to count */

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

    return 0;
}
#include

extern int count;

void f1(void)
{
    count = rand();
}
takatoh@nightschool $ gcc sample_11_1b1.c -c
takatoh@nightschool $ gcc sample_11_1b2.c -c
takatoh@nightschool $ gcc sample_11_1b1.o sample_11_1b2.o -o sample_11_1b
takatoh@nightschool $ ./sample_11_1b
1
2
3
4
5
(略)

register

register を指定すると、コンパイラは変数をメモリではなく CPU のレジスタに格納しようとする。初期の C では、register を適用できるのは char か int のローカル変数あるいはポインタに限られ、これらの変数は CPU のレジスタに格納されていた。ANSI C 標準ではこれらの制限が緩められて必ず CPU のレジスタに格納されるという要件は撤廃されているけど、依然として register 変数をアクセス時間が最小になるような方法で格納するように規定されている。だから、char と int はレジスタに格納されることになる。
ただし、レジスタの数は限られているので、レジスタに格納できない変数は、たとえ register が指定されていても通常の変数に変えられてしまう。だから本当に高そくにアクセスしたい変数を慎重に選ぶ必要がある。一般的に、1つの関数につき2つの変数は register 指定できると考えていいようだ。

共用体

共用体(union)は構造体と似ているけど、メンバ変数が同じメモリ領域を共有するもの。メモリを共有するメンバ変数が同じ型である必要はないけど、同時に使うことは出来ない。
定義の方法も構造体とよく似ていいて、一般的な形式は次のとおり。

union タグ名 {
    型 メンバ1;
    型 メンバ2;
    ...
    型 メンバN;
} 変数リスト;

「タグ名」と「変数リスト」のどちらかを省くことができるのも構造体と一緒。

次の共用体は、整数、文字配列、double 型の3つのメンバを持つ。

union u_type {
    int i;
    char c[4];
    double d;
} sample;

このとき、各メンバはメモリ領域を共有していて、d(double 型、8バイト)の上位4バイトを i (int 型、4バイト)が共有している。また、同時に同じ上位4バイトの領域を c[4] も共有している。各メンバが別々のメモリ領域を占めるのではない。
共用体のサイズはコンパイル時に決定され、最大のメンバが入る大紀佐の憑依機が割り当てられる。サイズを求めるときには sizeof 演算子を使うこと。メンバのうちの最大サイズが共用体のサイズとは限らない。環境によってはワード境界に揃えられることがあるから(ワード境界ってなんだ?)。

共用体のメンバにアクセスするには、構造体と同様にドット演算子を使う。

sample.d = 123.456;

また、ポインタを介してのアクセスにはアロー演算子だ。

p->i = 10;

なんとなくわかったような気がするけど、どうも使いドコロがわからないな。

次のプログラムは、整数(short int)の上下バイトを入れ替えて暗号化・復号化する。暗号化・復号化するためのデータとして共用体を使っている。

#include

short encode(short i);

int main(void)
{
    short i;

    i = encode(10);
    printf ("Encode 10 -> %d\n", i);
    i = encode(i);
    printf("Decode i -> %d\n", i);

    return 0;
}

short encode(short i)
{
    union crypt_type {
        short num;
        char c[2];
    } crypt;

    unsigned char ch;

    crypt.num = i;

    /* Swap bytes of short int */
    ch = crypt.c[0];
    crypt.c[0] = crypt.c[1];
    crypt.c[1] = ch;

    return crypt.num;
}
takatoh@nightschool $ ./sample_10_5
Encode 10 -> 2560
Decode i -> 10

ビットフィールド

C では、構造体のメンバにビットフィールドが使える。ビットフィールドというのは、バイト単位ではなくビット単位のデータで、1つあるいは複数のビットに名前をつけてアクセスすることができる。
例えば、真と偽しか取らないようなメンバでは1ビットあれば用が足りるので、そんなときにビットフィールドを使えばメモリの節約になる。
ビットフィールドの定義の一般的な形式は次のとおり。

型 名前: サイズ;

「型」には int か unsigned を指定する。「サイズ」はビットフィールドのビット数。「名前」と「サイズ」はコロンで区切る。
次の構造体は、在庫情報を格納するためにビットフィールドを使っている。

struct b_type {
    unsigned department: 3;    /* 担当部署(最大7部署) */
    unsigned instock: 1;       /* 1: 在庫あり 0: 在庫なし */
    unsigned backorderd : 1;   /* 1: 未納 0: 納品済み */
    unsigned lead_time: 3;     /* 発注から納品までの月数 */
} inv[MAX_ITEM];

この例では、構造体すべての情報を8ビット(1バイト)に詰め込んでいる。

ビットフィールドの参照方法は、普通の構造体メンバと変わらない。ドット演算子を使えばいい。

inv[9].department = 3;

ビットフィールドはの合計をバイト単位に合わせる必要はない。次にようなものでも構わない。

struct b_type {
    int a: 2;
    int b: 3;
};

同じ構造体の中に、普通のメンバとビットフィールドを混在させても構わない。次の例は最初の例に、品目名の文字列を追加している。

struct b_type {
    char name[40];             /* 品目名 */
    unsigned department: 3;    /* 担当部署(最大7部署) */
    unsigned instock: 1;       /* 1: 在庫あり 0: 在庫なし */
    unsigned backorderd : 1;   /* 1: 未納 0: 納品済み */
    unsigned lead_time: 3;     /* 発注から納品までの月数 */
} inv[MAX_ITEM];