先頭へ戻る

型変換 | Programming Place Plus C言語編 第21章

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

先頭へ戻る

この章の概要

この章の概要です。


暗黙の型変換

いくつかの演算子では、必要があれば、オペランドの型を自動的に変換します。型を変換することを、型変換といい、自動的に行われる型変換を、暗黙の型変換といいます。

また、プログラマーが自ら型変換を指定する方法もあり、こちらは、キャスト明示的な型変換)といいます。キャストについては後程取り上げます

たとえば、符号付き整数型と符号無し整数型を混在できるのは、暗黙の型変換が行われているからです。

#include <stdio.h>

int main(void)
{
    int snum = -5;
    unsigned int unum = 10;

    int ans = snum + unum;  // int = int + unsigned int
    printf( "%d\n", ans );

    return 0;
}

実行結果:

5

int型の snum と、unsigned int型の unum を加算演算子で加算しています。このように、型が異なるとき、型変換が行われます。この場面では、通常の算術型変換というルールによって、snum が unsigned int型に変換されてから加算処理が行われます。通常の算術型変換の詳細は、後程取り上げます。

加算処理を unsigned int型で行ったため、その結果も unsigned int型です。結果を int型の変数 ans の初期値にしていますが、ここでも暗黙の型変換が起きています。今度は、unsigned int型を int型に型変換しているわけです。

変数を初期化する際の「=」は、代入を意味する代入演算子ではありませんが、代入と同じルールで型変換されることになっています。

暗黙的な型変換、明示的な型変換を問わず、型変換には注意が必要です。たとえば、long int型から short int型のように、表現できる情報量が減る方向の型変換(縮小変換)では、変換先の型で、変換元の値をそのまま表現できるとは限りません。また、符号付き整数と符号無し整数が混在できるからといって、-10 のような負数を符号無し整数で表現できるわけではありません。

特に、暗黙の型変換は分かりづらく、トラブルの原因になりやすいため、ある程度理解しておく必要があります。ただ、非常に難解なルールであり、注意していても間違いやすいものです。細かいことを丁寧に覚えておくよりも、トラブルに巻き込まれにくいやり方を身に付けると良いでしょう。


整数変換の順位

整数型の型変換のルールの根底には、整数変換の順位というルールがあります。

整数変換の順位は、多数ある整数型を順位付けしたもので、暗黙の型変換の際に、どの型へ変換するかを決定するために使われます。

整数変換の順位は、次のように定められています。

1つ目のルールがあるため、整数変換の順位を、型の大きさの大小でとらえることは間違っています。たとえば、「sizeof(short) == sizeof(int)」が真になることはあり得ますが、その場合でも short型の順位はつねに、int型よりも低くなります。

整数拡張

整数型を要求している場面で、整数変換の順位が int型よりも低い型の値を指定した場合、暗黙的に int型か unsigned int型へ変換されるというルールがあります。この型変換を、整数拡張 (Integral Promotion) といいます。

C99規格よりも前には、汎整数拡張という訳語が使われていました。

整数拡張は、変換前の型で表現できるすべての値が、int型で表現できる範囲に収まる場合は int型に変換し、収まらない場合は unsigned int型に変換します。

たとえば、short型が 16bit、int型が 32bit、2の補数表現コンピュータサイエンス編基数 のページを参照)を使う環境で、整数拡張前の型が signed short型だったとします。signed short型の表現範囲は -32768~32767、int型の表現範囲は -2147483648~2147483647 なので、signed short型で表現できるすべての値が、int型で表現できます。よって、整数拡張後の型は int型が選択されます。

一方、short型と int型がともに 32bit、2の補数表現を使う環境で、整数拡張前の型が unsigned short型だったとします。unsigned short型の表現範囲は 0~4294967295、int型の表現範囲は -2147483648~2147483647 なので、unsigned short型で表現できるすべての値を int型で表現することができません。よって、整数拡張後の型は unsigned int型が選択されます。

つまり、整数拡張のルールでは、値が変わらないことを保証する代わりに、型の符号の有無が変わってしまうことには目をつぶっているといえます。

整数拡張は、通常の算術型変換(後で取り上げます)の一部として起こるほか、単項の演算子(+、-、~)、シフト演算子(<<、>>)、実引数を与えるときに起こります。

~演算子、シフト演算子については第49章で取り上げます。

たとえば、次のコードでは整数拡張が起きています。

#include <stdio.h>

int main(void)
{
    short s = 10;
    int n;

    n = -s;    // int型に整数拡張される
    printf( "%d\n", n );

    return 0;
}

実行結果:

-10

通常の算術型変換

算術型の2つのオペランドを持つ演算子を使う場面では、それぞれの型を合わせるための型変換が行われます。このような変換を、通常の算術型変換と呼びます。

結果が int型の 0 か 1 になる等価演算子や関係演算子は例外的ですが、ほかの演算子では、通常の算術型変換を適用した後の型が、演算の結果の型になります。

通常の算術型変換は、次の順序で行われます。

  1. 一方が long double型ならば、long double型で統一
  2. 一方が double型ならば、double型で統一
  3. 一方が float型ならば、float型で統一

以上に該当しない場合は、2つのオペランドに整数拡張を行ったうえで、以下の順序で変換を適用します。

  1. 両者が同じ型ならば何もしない
  2. 両者がともに符号無し整数型ならば(あるいはともに符号付き整数型ならば)、整数変換の順位が低いほうを、高いほうの型に変換する
  3. 符号無し整数型をもつオペランドの整数変換の順位が、他方の整数変換の順位以上なら、他方を、符号無し整数型をもつオペランドの型へ変換する
  4. 符号付き整数型をもつオペランドの型が、他方の型で表現できるすべての値を表現可能なら、他方を、符号付き整数型をもつオペランドの型へ変換する
  5. 両者を、符号付き整数型をもつオペランドの型に対応した符号無し整数型へ変換する

通常の算術型変換があるため、たとえば、以下のような判定がうまく動作しないことに注意が必要です。

#include <stdio.h>

int main(void)
{
    if( -1 < 1U ){
        puts( "TRUE" );
    }
    else{
        puts( "FALSE" );
    }

    return 0;
}

実行結果:

FALSE

この場合、-1 は int型、1U は unsigned int型なので、両者は unsigned int型に合わせられます。-1 を unsigned int型で表現できないため、非常に巨大な正の数になります。従って、この条件式は、巨大な正の数と 1U との比較になるため、必ず「偽」になります。この結果は明らかに、式の見た目と食い違っています。


キャスト

暗黙の型変換に対して、プログラマーが明示的に行う型変換もあります。これをキャストと呼びます。

キャストが必要な場面の一例として、次のプログラムを見てください。

#include <stdio.h>

int main(void)
{
    int num = 100;
    double r1 = num / 3;
    double r2 = (double)num / 3;
    double r3 = num / 3.0;

    printf( "%f\n", r1 );
    printf( "%f\n", r2 );
    printf( "%f\n", r3 );

    return 0;
}

実行結果:

33.000000
33.333333
33.333333

変数num の値を 3 で割った値を出力しようとしています。単純に 3 で割るだけの r1 の例では、出力結果が「33.000000」のようになり、小数点以下の情報がなくなってしまっています。変数num も 3 も int型なので、その結果もまた int型になるからです。出力結果に「.000000」が並ぶのは、printf関数に "%f" の変換指定子を指定しているからです。

r2 の例では、(double)num という変わった記述があります。これがキャストです。キャストは以下のような構文で記述します。

(型名)値

このような構文で、値を強制的に任意の型に変換します。キャストの効力はその場限りであって、以降ずっと型変換されたままになるというわけではありません。

キャストによる型変換による値の変化の規則は、暗黙の型変換と同じです。

「(double)num / 3」は、キャストがあるので「double型 / int型」です。2つのオペランドの型が異なっていますから、通常の算術型変換によって「double型 / double型」として計算されます。そのため、小数点以下も正しく計算されます。

キャストを使う以外の手段が、r3 の例です。「num / 3.0」とあるので、「int型 / double型」です。ここでもやはり、通常の算術型変換によって「double型 / double型」として計算されます。そのため、小数点以下も正しく計算されます。

キャストは、いわば「無理やり」型を調整する機能です。これは、間違った型変換を無理やり行ってしまうミスが起こる危険性もあります。キャストは不用意に行わないようにするべきです。必要性があるのなら、そのキャストが確固たる目的を持って行われていることをコメントで書き残すことも検討しましょう。

また、暗黙の型変換が起こる場所に、あえてキャストを記述するという手もよく使われています。こうすると、ソースコード上で、型変換が起きている箇所が明示的に表されますから、分かりやすくなる可能性があります。ただやはり、間違った型変換を無理やり行ってしまうミスには注意が必要です。

整数型の変換

整数型の値を、ほかの整数型(ただし _Bool型は除く)へ型変換した場合、元の値が変換後の型で表現できるのであれば、値は変換しません。_Bool型への変換は、元の値が 0 なら 0 になり、0以外なら 1 になります。

変換後の型で表現できない場合の結果は、変換後の型が符号無し整数型なのか、符号付き整数型なのかによって異なります。

変換後が符号無し整数型なのであれば、表現できる値になるまで、2N を加算あるいは減算し続けたものを結果にします。N は、変換後の型のビット数です。

#include <limits.h>
#include <stdio.h>

int main(void)
{
    int n1 = USHRT_MAX + 1;
    int n2 = -1;

    unsigned short us1 = n1;
    unsigned short us2 = n2;

    printf( "%hu\n", us1 );
    printf( "%hu\n", us2 );

    return 0;
}

実行結果:

0
65535

unsigned short型の限界値 USHRT_MAX より 1 大きい値は、当然表現できませんから、値が調整されます。unsigned short型が 16bit ならば、216 を減算して 1 が得られます。-1 を代入する方も、やはり表現できませんから、値を調整します。こちらは、216 を加算して 65535 になります。

一方、変換後の型で表現できず、変換後が符号付き整数型なのであれば、処理系定義の値となるか、処理系定義のシグナルを生成するかのいずれかになります。移植性を考えると、この事態を避けるように気を付けなければなりません。具体的には、型変換する前に、結果が表現できるかどうかを調べます。

#include <limits.h>
#include <stdio.h>

int main(void)
{
    unsigned int ui = UINT_MAX;
    short s;
    
    if( ui <= SHRT_MAX ){  // short型で表現できるか
        s = (short)ui;     // キャストで警告を黙らせる
    }
    else {
        // エラー処理
    }

    printf( "%hd\n", s );

    return 0;
}

整数と浮動小数点数の型変換

浮動小数点型の値を、整数型(ただし _Bool型は除く)へ型変換する場合、元の値の小数点以下の部分が捨てられます。_Bool型への変換は、元の値が 0 なら 0 になり、0以外なら 1 になります。

元の値の整数部分が、変換後の整数型で表現できない場合、未定義の動作になります。ある浮動小数点数を、整数型へ変換するときには、標準ライブラリに用意されている各種の丸め関数を使います。これらは第48章で紹介します。

#include <stdio.h>

int main(void)
{
    double d = 123.456;
    int n = (int)d;      // 小数部が捨てられる
    _Bool b = d;         // 0 か 1 にしかならない

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

    return 0;
}

実行結果:

123
1

一方、整数型を浮動小数点型へ変換する場合、浮動小数点数で正確に表現できるのならば、確実にその値になります。正確に表現できない場合は、さらに2つの場合に分けられます。

1つは、正確に表現できないものの、表現可能な値の範囲内にある場合です。この場合は、その値よりも大きくて表現可能な一番近い値か、その値よりも小さくて表現可能な一番近い値のいずれかになります(処理系定義)。

もう1つは、表現可能は値の範囲内にない場合です。この場合は、未定義の動作です。


浮動小数点型から浮動小数点型への変換のルールは、整数型から浮動小数点型への変換と同様です。正確に表現できるのなら、確実にその値になります。より精度が高い型への変換はつねに問題ないことになります。


練習問題

問題① 次の3つの if文の中から、結果が真になるものを選んでください(何個あるかは不明)。

short snum = -10;
long lnum = -10;

if( snum == lnum ){}
if( snum == (short)lnum ){}
if( (unsigned short)snum == -10 ){}

問題② 次のプログラムで出力される 3つの値は、それぞれいくつか答えてください。

#include <stdio.h>

int main(void)
{
    short snum = 1000;
    short num1 = snum + snum;
    short num2 = (int)snum + (int)snum;
    short num3 = (int)(snum + snum);

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

    return 0;
}

問題③ 次のプログラムを、出力される値が 0.47 になるように修正してください。 何通りの修正方法が考えられますか?

#include <stdio.h>

int main(void)
{
    int num = 47;

    printf( "%f\n", num / 100 );

    return 0;
}

問題④ 次のプログラム片を見てください。

signed char c1 = 120;
signed char c2 = 60;
signed char c3 = -100;
signed char result = c1 + c2 + c3;

signed char型で表現できる最大値は 127 であり、c1 + c2 の段階で溢れ出してしまうように思えます。 実際には、最終的な result の値は、80 となり正しく計算できます。問題が起こらない理由を説明してください。


解答ページはこちら

参考リンク


更新履歴

'2018/6/4 第30章から練習問題⑩を移動してきて、練習問題④とした。

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



前の章へ (第20章 数値の大きさと符号)

次の章へ (第22章 スコープ)

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

Programming Place Plus のトップページへ



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