代入文での型変換

代入分の左辺と右辺の型が異なる場合、右辺の型が左辺の型に変換される。もし、左辺が右辺よりも大きな型であれば問題はない。その逆の場合は問題ありで、例えば次のプログラムは、「1000」と表示せずに「-24」と表示する。

#include

int main(void)
{
    char ch;
    int i;

    i = 1000;
    ch = i;

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

    return 0;
}
takatoh@nightschool $ ./sample_4_6a
-24

なぜなら、int の i を char の ch に代入する際、i の下位 8 ビットだけがコピーされるから。C ではこのような代入での型変換はエラーにならない。代入文で、大きな整数から小さな整数(char を含む)に変換されるとき、上位ビットが失われるのが基本的な規則。

また、大きな浮動小数点数から小さな浮動小数点数への変換では精度が落ちるし、浮動小数点数から整数への変換では小数部が失われる。
値が変換後の型におさまらない場合は、結果は意味のない値になってしまう。

次の2点は重要。

  • int から float、float から double への変換は、値の表現を変えるだけで精度を上げるものでなないこと。
  • コンパイラによって、char を符号付きとして扱うものと符号なしで扱うものがある。このため 127 よりも大きな値を代入した時の結果は処理系に依存する。これが問題になる場合は明示的に signed あるいは unsigned と宣言すべき。

次のプログラムでは、浮動小数点数を整数に代入することで小数部が失われることがわかる。

#include

int main(void)
{
    int i;
    float f;

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

    return 0;
}
takatoh@nightschool $ ./sample_4_6b
1234.009766 1234

む、浮動小数点数の精度がおかしいな。とはいえ、整数に代入することで、小数部が失われることは分かった。

式内部での型変換

C では1つの式の中に異なる型を混在させることができる。これは、式中に異なる方が現れた時に、どのように解消するかが決められているから。

整数拡張

式中に char や short int が使われていると、その式が評価されるときに必ず int に拡張される。ただし、この拡張は式の評価時だけの一時的なものであって、変数自体が拡張されるわけではないので注意。

型の昇格

整数拡張のあと、コンパイラはすべてのオペランドを一番大きなオペランドに合わせて変換する。これを型の昇格という。昇格の規則は次の通り。上から順に見ていって、当てはまる変換が行われる。

一方のオペランドの型もう一方のオペランドが昇格する型
long doublelong double
doubledouble
floatfloat
unsigned longunsigned long
longlong
unsignedunsigned

要するに、大きな方のオペランドの型にもう一方も変換されるってことだ。

1つ、特殊なケースがある。一方が long、もう一方が unsigned int で、しかもその unsigned int の値を long で表せない場合、両方が unsigned long に変換される。

次のプログラムは、int と float の演算をしているけど、変数 i が float に変換されて、出力は float になる。

#include

int main(void)
{
    int i;
    float f;

    i = 10;
    f = 23.25;

    printf("%f\n", i * f);

    return 0;
}
takatoh@nightschool $ ./sample_4_5a
232.500000

もう1つ。型変換の規則は演算のひとつひとつについて適用されるので、次のプログラムでは、(10 / 3) は整数であり、100.0 を割るときに初めて float に変換されるので、答えは 33.3333… になる(3.0 にはならない)。

#include

int main(void)
{
    printf("%f\n", 100.0 / (10 / 3));

    return 0l;
}
takatoh@nightschool $ ./sample_4_5b
33.333333

変数の初期化

変数を宣言するときに、ついでに初期化することができる。一般的な形式は次の通り。

型 変数名 = 値;

グローバル変数の初期化に使えるのは定数だけ、ローカル変数の初期化には定数、変数、関数呼び出し(戻り値)が使える。
グローバル変数は最初に1回だけ初期化され、ローカル変数は関数が呼び出されるたびに初期化される。
明示的に初期化されないグローバル変数は 0 に設定されるけど、ローカル変数の場合は値が不定になる。

変数をまとめて宣言するとき、初期化する変数としない変数を混ぜることができる。

int min = 0, count, max = 100;

次のプログラムでは、main 関数の中のローカル変数 y をグローバル変数 x で、ローカル変数 z を関数の戻り値で初期化している。

#include

int x = 10; /* global */

int myfunc(int i);

int main(void)
{
    int y = x; /* initialize with global variable */

    int z = myfunc(y); /* initialize with function */

    printf("%d %d\n", y, z);

    return 0;
}

int myfunc(int i)
{
    return i / 2;
}
takatoh@nightschool $ ./sample_4_4
10 5

定数

ここでいう定数は、名前のついた定数、例えば Ruby の FOO とかのことじゃなくて、いわゆるリテラルのこと。

  • 整数の定数は小数点と小数部がない数値。例:10、-100
  • 浮動小数点数の定数は、小数点と小数部がついた数値。例:10.0、-2.5
  • 浮動小数点数の定数は、指数表記もできる。例:1.25E+03
  • 文字定数は、引用符(’ ‘)で囲む。例:’a’
  • 文字列定数は、二重引用符(” “)で囲む。例:”hogehoge”

ここでちょっと注意点。C には文字列というデータ型はない。文字列は文字の配列として扱われている。これはまた後で出てくるはず。

コンパイラが数値の定数を解釈するとき、デフォルトでは互換性のあるデータ型のうちその数値がおさまる最小の型だと解釈する。ただし、浮動小数点数は double と解釈する。これを変えたいときは次のような接尾辞をつける。

  • 浮動小数点数:F をつけると float、L をつけると long double
  • 整数:U をつけると unsigned、L をつけると long int

数値を変数に代入するときには、その変数の型に解釈してくれるのであまり気にすることはないけど、関数の引数として直接使うときには注意が必要。次のプログラムは、「42340」ではなく「-23196」と出力してしまう。

#include

int main(void)
{
    printf("%hd\n", 42340);

    return 0;
}
takatoh@nightschool $ ./sample_4_3
-23196

これは、%hd という指定子から引数の数値が short int だと解釈してしまうから。正しく出力するには、%hu 指定子(符号なし short int)を使う必要がある。

変数の宣言場所

変数を宣言する場所と変数の有効範囲は次の通り。

  • 関数の外で宣言するとグローバル変数
  • 関数の中で宣言するとその関数だけのローカル変数
  • コードブロックの中で宣言するとそのブロックだけのローカル変数

また、次の制約がある。

  • 変数は使うより前に宣言しなければいけない

関数の外だとグローバル、中だとローカルってのはいいとして、コードブロックのローカル変数ってのは使いどころがよくわからないな。でも本には「実際には、ほとんどのCプログラマは1つの関数で使用するすべての変数をその関数のブロックの最初に宣言します。」って書いてあるから、そうしておけば良さそう。

グローバル変数とローカル変数は同じ名前でも構わない。有効な範囲にローカル変数があればそれが使われる。なければグローバル変数が使われる。
次のプログラムには、同じ count という名前のグローバル変数とローカル変数が使われているけど、それぞれ独立している。

#include

void f1(void);

int count; /* global */

int main(void)
{
    count = 10;
    f1();
    printf("value of count in main(): %d\n", count);

    return 0;
}

void f1(void)
{
    int count; /* local */

    count = 1000;
    printf("value of count in f1(): %d\n", count);
}
takatoh@nightschool $ ./sample_4_2
value of count in f1(): 1000
value of count in main(): 10

データ型修飾子とデータのサイズ

void 以外の基本データ型には修飾子をつけることによって、よりきめ細かく指定することができる。データ型修飾子には次の4つがある。

  • long
  • short
  • signed
  • unsigned

long と short はそれぞれデータサイズが変わる。といっても ANSI の規定が厳密じゃないみたいなので、処理系によって違うらしい。signed と unsigned は符号つきか符号なしかの違い。省略すると符号つきと解釈される。
データ型修飾子はそれぞれ適用できるデータ型が決まっているので、表にまとめてみた。

shortint
longint, double
signedchar, int
unsignedchar, int

それぞれのデータサイズは次のプログラムで確認してみる(ググって調べた)。

#include

int main(void)
{
    printf("char = %lu\n", sizeof(char));
    printf("int = %lu\n", sizeof(int));
    printf("short int = %lu\n", sizeof(short int));
    printf("long int = %lu\n", sizeof(long int));
    printf("float = %lu\n", sizeof(float));
    printf("double = %lu\n", sizeof(double));
    printf("long double = %lu\n", sizeof(long double));

    return 0;
}
takatoh@nightschool $ gcc --version
gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

takatoh@nightschool $ ./sample_4_1
char = 1
int = 4
short int = 2
long int = 8
float = 4
double = 8
long double = 16

単位はバイト。

switch文による多分岐選択

switch は Ruby の case と同様の多分岐選択だ。一般的な形式は次の通り。

switch (値) {
    case 定数1:
        文;
        ....
        break;
    case 定数2:
        文;
        ....
        break;
    default:
        文;
        ....
        break;
}

注意点といえば、

  • 値の比較は等価かどうかしか判定できないこと
  • 値には int か char しか使えないこと
  • case のあとの一連の文には { } が不要なこと
  • 一連の文の最後には break; が必要なこと

break; がないと break; に(あるいは switch の最後に)行き当たるまで文が実行されてしまう。これを利用する書き方もあるようだけど、あまりいいとは思えない。

次のプログラムは、入力された整数に対応する単語を出力する。

#include

int main(void)
{
    int i;

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

    switch (i) {
        case 1:
            printf("ONE\n");
            break;
        case 2:
            printf("TWO\n");
            break;
        case 3:
            printf("THREE\n");
            break;
        default:
            printf("MANY\n");
    }

    return 0;
}

実行例:

takatoh@nightschool $ ./sample_3_9
Input integer: 2
TWO
takatoh@nightschool $ ./sample_3_9
Input integer: 4
MANY

continue文

ループの中で continue 文に出会うと、それ以降の処理を飛ばして次の回のループに移る。この時、for ループではインクリメント部が実行されてループ変数は変更される。

次のプログラムは、ユーザーに整数を複数入力させてその合計を出力する(0 で入力終了)。その際、整数を2回入力させて合致するかを確かめている。もし合致しない場合は入力された数を合計に加算せずに continue で次のループに処理が移る。

#include

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

    total = 0;
    do {
        printf("Input number (0 to exit): ");
        scanf("%d", &i);
        printf("Once more: ");
        scanf("%d", &j);
        if (i != j) {
            printf("Error: Try again.\n");
            continue;
        }
        total = total + i;
    } while (i);

    printf("TOTAL: %d\n", total);

    return 0;
}

実行例:

Input number (0 to exit): 1
Once more: 1
Input number (0 to exit): 5
Once more: 5
Input number (0 to exit): 9
Once more: 9
Input number (0 to exit): 0
Once more: 0
TOTAL: 15

実行例2(わざと間違った例):

takatoh@nightschool $ ./sample_3_8
Input number (0 to exit): 1
Once more: 2
Error: Try again.
Input number (0 to exit): 1
Once more: 1
Input number (0 to exit): 5
Once more: 5
Input number (0 to exit): 0
Once more: 0
TOTAL: 6

最初の入力(1, 2)は合致しないので、合計に加算されていない。

break文

break 文はループを脱出する。ただし、break 文の書いてあるループだけだ。

次のプログラムは、内側のループで 1 からインクリメントしながら while ループを回しているけど、5 を超えた時点で break している。外側のループは脱出しないので、1 から 10 までループが回る。

#include

int main(void)
{
    int i, j;

    for (i = 1; i <= 10; i++) {
        j = 1;
        while (j <= 10) {
            if (j > 5) {
                break;
            }
            printf("%d", j);
            j++;
        }
        printf("\n");
    }

    return 0;
}

実行結果:

takatoh@nightschool $ ./sample_3_7
12345
12345
12345
12345
12345
12345
12345
12345
12345
12345