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

トップページC言語編

このページの概要 🔗

以下は目次です。


暗黙の型変換 🔗

演算子が処理を行うときだとか、宣言する変数に初期値を与えるとき、関数に引数を渡したり、戻り値を受け取ったりするときなどには、2つ以上の値が同時に登場することになりますが、こうしたときに、互いの型は一致していなければなりません

しかし、意味は通じているが型は合っていないというケースは意外に多くあります。たとえば、商品の値段に消費税率を掛け合わせるというコードには、int型と double型の乗算が現れます。型は合っていないものの、意図することは分かるはずです。多少の型の違いをエラーとされるのは不便で仕方がありません。

そこで、意図が理解できる程度の型の不一致であれば、コンパイラは自動的に型を調整して、同じ型での処理になるようにします。これを暗黙の型変換といいます。

意図が理解できる程度というのは、たとえば次のような場合です。

暗黙の型変換は便利である一方で危険も伴っています。というのも、型が変われば表現できる値の範囲が変わるからです。たとえば、次のコードは典型的なバグです。

#include <stdio.h>

int main(void)
{
    int si = -1;
    unsigned int ui = 30;
    if (si < ui) {  // int と unsigned int の比較
        puts("ok");
    }
    else {
        puts("error");
    }
}

実行結果:

error

この if文は -1 < 30 をしていることになるため、“ok” が出力されるように思えますが、実際には “error” が出力されます。si は int型であり、ui は unsigned int型なので、型が一致しておらず、暗黙の型変換が行われます。ルール上、この場合は si のほうが unsigned int型に変換されます。しかし、unsigned int型で -1 を正しく表現することはできず、この場合、とても巨大な正の数として扱われます(第19章)。結果、巨大な正の数 < 30 という比較をしたことになり、“error” を出力するほうに進んでしまいます。

つまり、暗黙の型変換が行われるから、コンパイラに任せっきりにすればいいということではないのです。結局のところ、プログラマーは暗黙の型変換によって何が行われているのか理解する必要があります。この章では、型変換のルールをまとめて確認していきます。

このプログラムの場合、コンパイラは警告を出すことが多いため、それを見逃さないことも重要です。Visual Studio 2015 は、“main.c(7): warning C4018: ‘<’: signed と unsigned の数値を比較しようとしました。” という警告を出しました。

型変換前の値が、型変換後の型でも表現できるのであれば、値に変化は起こりません。

キャスト(明示的な型変換) 🔗

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

キャストは以下の構文で記述します。

(型名)

「式」の型が「型名」の型に変換されます。ここで登場する ()キャスト演算子 (cast operator) です。

【上級】「式」や「型名」の型は、任意のスカラ型です[1]。スカラ型とは、算術型とポインタ型を合わせたもので[2]、算術型は整数型と浮動小数点型を合わせたものです。

キャストの効力はその場限りであって、以降ずっと型変換されたままになるというわけではありません。

int si = 123;
short ss = (short)si;
// 以降も si の型は int 

前の項で取り上げたサンプルプログラムは、次のようにキャストを使うことで正しい判定に修正できます。

#include <stdio.h>

int main(void)
{
    int si = -1;
    unsigned int ui = 30;
    if (si < (int)ui) {
        puts("ok");
    }
    else {
        puts("error");
    }
}

実行結果:

ok

(int)ui としたことによって、ui の型を int に変換しています。si < (int)ui は int型どうしの比較ということになり、これ以上の型変換は起こらず、すなおに -1 < 30 をしていることになります。(unsigned int)si < ui とする選択肢もありえそうですが、これでは暗黙の型変換に任せたときとまったく同じことを明示的にしているだけで、うまくいきません。型変換が暗黙的であろうと明示的であろうと、-1 を unsigned int型で表現できないという事実に変化はありません。

しかし、キャストを使ったこのプログラムにも問題はあります。もし、変数ui の値が、int型の限界値の外にあたるものだった場合、int型にキャストした結果は処理系定義であり(第19章)、おそらくうまくいきません。

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

int main(void)
{
    int si = -1;
    unsigned int ui = UINT_MAX;  // int の上限値よりも大きい値
    if (si < (int)ui) {
        puts("ok");
    }
    else {
        puts("error");
    }
}

実行結果:

error

キャストが必要な場面としてもう1つよくあるのは、整数型の除算結果を浮動小数点型で受け取りたい場合です。

#include <stdio.h>

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

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

実行結果:

33.000000
33.333333
33.333333

変数num の値を 3 で割った値を出力しようとしています。単純に 3 で割っているだけの r1 は、出力結果が 33.000000 となり、小数点以下の情報がなくなってしまっています。変数num も 3 も int型なので、その結果もまた int型になるからです。

r2 のほうは、(double)num のようにキャストしているため、double型に変換された num の値を、3 で割るという計算になります。3 は int型なので、「double / int」の計算になり、暗黙の型変換で「double / double」の計算に直されます。その結果もまた double型です。

r3 はキャストを避けた方法です。結局のところ、除算の2つのオペランドのいずれかが double であれば、暗黙の型変換によって double 同士の計算に直してくれるので、3 のほうを 3.0 と書き直すことで、「int / double」に仕立てたということです。


キャストは、コンパイラの警告を黙らせるために使われることも多いです。コンパイラは暗黙の型変換を行おうとするものの、危険性があると判断した場合にはたいてい警告を出します。暗黙の型変換に任せたときと、キャストで明確にしたときとで結果は変わりませんが、とりあえず警告は消えます。これは自分がしていることを正しく理解したうえで行ってください。警告が出ていないことが、プログラムの正しさを保証するわけではありません。警告は理解して、正しく対処されるべきです。

論理型への変換 🔗

_Bool型は符号無し整数型の一種ですが(第19章)、_Bool型へ変換する場合は、少しだけ特別なルールになっています。

整数型や浮動小数点型を _Bool型へ変換することができ、元の値が 0 なら 0、それ以外のときは 1 になります[3]

#include <stdio.h>

int main(void)
{
    _Bool b = 10;  // int -> _Bool
    printf("%d\n", b);

    b = 1.5;  // double -> _Bool
    printf("%d\n", b);

    b = 0L;  // long -> _Bool
    printf("%d\n", b);
}

実行結果:

1
1
0

_Bool型から整数型や浮動小数点型への変換は、符号付き整数型から整数型や浮動小数点型への変換と同じルールになります。整数型へは「符号付き整数型と符号無し整数型の変換」で、浮動小数点型へは「整数と実浮動小数点数の変換」で取り上げます。

実浮動小数点型の変換 🔗

実浮動小数点型どうしの変換は、上位の型への変換は無条件かつ安全に行えます。つまり以下の3つはいずれも問題ありません。

反対に、下位の型への変換では、元の値を表現できない可能性があるため危険が伴います。この場合は以下のようになります。[4]

整数と実浮動小数点数の変換 🔗

実浮動小数点型の値を、整数型(_Bool型は除く)へ変換する場合、元の値の小数点以下の部分が捨てられます。

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

#include <stdio.h>

int main(void)
{
    double d = 123.456;
    int n = (int)d;      // 小数部が捨てられる
    printf("%d\n", n);
}

実行結果:

123

反対に、整数型から実浮動小数点型へ変換する場合は、以下のようになります。[5]

整数型の変換 🔗

整数型の値を、ほかの整数型(_Bool型は除く)へ変換する場合、変換後の型で値を表現できるなら、値に変化は起こらず、安全に変換できます。したがって、short から int とか、int から long long のように、符号の有無が変わらず、変換前以上の大きさを持つ型への変換は安全です。

一方で、大きさが小さくなる方向への変換や、符号の有無が異なる型への変換では、元の値を表現できない可能性があります。この場合は、以下のようになります(第19章で取り上げたものと同じです)[6]

整数拡張 🔗

int よりもランクの低い整数型に対して演算を行うときには、演算に先立って、int以上のランク(整数変換の順位 (integer conversion rank))の型に変換されるルールがあります。このルールを、整数拡張 (integer promotion) と呼びます。

ランクは、次のように定義されています。上にあるものほど、順位が高いことになります[7]

  1. long long int、unsigned long long int
  2. long int、unsigned long int
  3. int、unsigned int
  4. short int、unsigned short int
  5. char、signed char、unsigned char
  6. _Bool

拡張整数型は、同じ大きさの標準整数型よりも低いランクになります。同じ大きさの拡張整数型が複数定義されている場合の扱いは、処理系定義です。

整数拡張の対象になるのは、int よりもランクが低い型なので、この表で 4~6 にあたる型です。

演算対象の型が _Bool、char、signed char、unsigned char、short int、unsigned short int の場合は、対象の型で表現できる値のすべてを int でも表現できるのなら int型に、表現できない値があるのなら unsigned int型に変換されます[8]たとえば、対象の型が 16ビットの unsigned short で、int型が 32ビットだとすると、unsigned short で表現できる値(0~65535) はすべて、int でも表現できるので int に変換されます。unsigned short も int も 16ビットの処理系では、int の表現範囲は -32768~32767 でしょうから、32768~65535 の範囲が表現できないため、unsigned int に変換されます。

整数拡張には、演算途中で型の限界値を超えてしまい、正しい結果が得られないことを防ぐ意味があります。たとえば、signed char 同士の乗算の結果は signed char では表現できない可能性が高いですが、計算前に int に拡張されるため問題なく計算できます。

#include <stdio.h>

int main(void)
{
    signed char sc1 = 100;
    signed char sc2 = 20;
    int result = sc1 * sc2;  // sc1、sc2 は int型に整数拡張されてから計算する
    printf("%d\n", result);
}

実行結果:

2000

通常の算術変換 🔗

オペランドが2つある演算子で、それぞれが異なる算術型 (arithmetic type) の場合には、両者の型を揃える型変換を行ってから、演算が行われます。算術型とは、整数型と浮動小数点型を合わせた呼び名です。

この型変換は、通常の算術変換 (usual arithmetic conversions) と呼ばれる以下の流れにしたがって行われます[9]

  1. 一方のオペランドが実浮動小数点型なら、他方をその型に変換する
  2. 両方のオペランドに、整数拡張)をおこなう
  3. この時点で両方の型が同じなら終了。異なっていたら、以下を順番に判定し、該当したところで終了する
  4. 両方とも signed、あるいは両方とも unsigned である場合、ランクが低い側が、ランクが高い側の型に変換される
    • 例)long int + int -> long int + long int
    • 例)unsigned int + unsigned long int -> unsigned long int + unsigned long int
  5. unsigned の側のランクが signed の側のランク以上の場合、signed の側が unsigned の側の型に変換される
    • 例)int + unsigned int -> unsigned int + unsigned int
    • 例)int + unsigned long int -> unsigned long int + unsigned long int
  6. signed の側の型が、unsigned の側の型で表現できるすべての値を表現できるのなら、unsigned の側が signed の型に変換される
    • 例)long long int (64bit) + unsigned int (32bit) -> long long int + long long int
  7. 両方とも、signed の側の型に対応する unsigned な型に変換される
    • 例)long int (32bit) + unsigned int (32bit) -> unsigned long int + unsigned long int

実際のコード例を確認してみましょう。

#include <stdio.h>

int main(void)
{
    int a = 100;
    unsigned char b = 50;
    long int c = 100000;

    int a_b = a * b;  // int * int
    printf("%d\n", a_b);

    long int a_c = a * c;  // long int * long int
    printf("%ld\n", a_c);

    long int b_c = b * c;  // long int * long int
    printf("%ld\n", b_c);
}

実行結果:

5000
10000000
5000000

a * b は、int と unsigned char の乗算です。unsigned char は int よりランクが低い型なので、整数拡張が行われて、int に変換されます。int 同士での計算になるので、それ以上の型変換は起こりません。

a * c は、int と long int の乗算です。整数拡張は不要ですが、型は異なっているので、型を揃える変換が起こります。上述のリストにあった「両方とも signed、あるいは両方とも unsigned である場合、ランクが低い側が、ランクが高い側の型に変換される」が適用されて、両方とも long int になります。

b * c は、unsigned char と long int の乗算です。b は整数拡張されて int になります。そのため int と long int の乗算になり、さきほどと同様に、両方とも long int になります。


練習問題 🔗

問題① 次の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);
}

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

#include <stdio.h>

int main(void)
{
    int num = 47;

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


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

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



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

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

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

Programming Place Plus のトップページへ



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