C言語編 第21章 型変換

先頭へ戻る

この章の概要

この章の概要です。


符号の混在時

第19章で説明したように、整数型の場合には、signed や unsigned というキーワードを付けることによって、符号の有無を指定できます。char型の場合を除いて、signed は省略できます。

符号の有無によって、表現できる値の範囲が異なるため、両者を混ぜて使おうとすると問題になる場合があります。

#include <stdio.h>

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

    printf( "%u\n", unum );

    return 0;
}

実行結果:

4294967291

int型の 5 から、10 を引いた結果を使って、unsigned int型の変数の初期値にしています。int型は符号付き整数なので -5 を表現できますが、unsigned int型は符号無し整数なので、-5 が表現できません。そのため、実行結果にあるような、巨大な正の数になってしまいます。

ところで、ある型の値が、代入や初期化、実引数を渡すときなどで、受付側の型と一致しないときに、自動的に型を変換しようとします。これは、暗黙的な型変換と呼ばれます。

先程のサンプルプログラムでは、変数 unum は unsigned int型で宣言されていますが、その初期値として使う値は、int型の snum と 10 という値から作り出された値で、これもまた int型です。そのため、int型から unsigned int型への暗黙的な型変換が行われています。

C言語では、暗黙的な型変換はごく当たり前のように行われるものですが、ミスが入り込みやすくもあります。安易に型を混在させるべきではありません。

逆に、符号付き整数で表現できない場合も確認しておきましょう。

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

int main(void)
{
    unsigned int unum = UINT_MAX;
    int snum = unum;

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

    return 0;
}

実行結果:

-1

int型では、UINT_MAX を表現することは不可能です。実行結果では、-1 が出力されていますが、こうなる保証はありません。

ここまで見てきたように、表現できる範囲を超えてしまうことを、オーバーフローといいます。オーバーフローが起こる場合、結果が正しいことが保証できないため、避けることが望ましいと言えます。そして、これを避けるためにまず考えるべきことは、「本当に unsigned を使う必要があるのか」という点です。どうしても、符号付き整数と符号無し整数の混在が避けられない場面では、オーバーフローする可能性を考慮しなければなりません。

なお、符号付き整数と符号無し整数を混在して使っても、その値が、どちらの型でも表現できる範囲内であれば、問題は起こりません。例えば、100 という正数は、どんな整数型でも常に表現できます(ただし、後で触れるように、計算の結果が 100 になるが、計算過程で一時的に表現不可能な値になり得る場合には、事情が変わってきます)。

小さい型への代入時

整数型の場合、char型<short型≦int型≦long型 という型の大小関係があります。このため、大きい方の型から小さい方の型へ代入を行うと、値が表現しきれなくなる可能性があります。この場合、多くのコンパイラは警告を発しますが、代入できないということではありません。

もし、符号無し整数であれば、小さい型に代入すると、単純に上位のビットが削り落とされます。例えば、32ビットの long型変数に 100000 が格納されており、これを 16ビットの short型変数に代入するとします。この場合、以下のような変換が行われます。

11000011010100000    /* 100000 を 2進数にしたもの */
       ↓
 1000011010100000    /* 下位の 2バイト(16ビット) が残されて、上位は削られる */

この結果は 10進法で書くと 34464 です。これは 16ビットの符号無し整数型で表現できる範囲内に収まっています。一応、プログラムで確認しておきます。

#include <stdio.h>

int main(void)
{
    unsigned long ulnum = 100000;
    unsigned short usnum = ulnum;

    printf( "%hu\n", usnum );

    return 0;
}

実行結果:

34464

このプログラムは、long型が 32ビット、short型が 16ビットの環境を想定したプログラムですから、自分の環境がそうなっていなければ、型を変えて試してください。

実行結果は確かに 34464 になりました。

恐らく、コンパイラは警告を発すると思います。このように、型が小さくなるような代入は危険性を伴うからです。警告は決して無視せず、その意味を理解するように努めてください。今回の場合、この警告を黙らせる手段があります。この件については後で触れます

ここで確認した挙動は、符号無し整数の場合にしか当てはまらないことに注意してください。符号付き整数の場合、オーバーフローしたときの結果は不定です

汎整数拡張

C言語では、式の中に登場する整数型が int型よりも変換順位が低い場合、一時的に int型や unsigned int型に変換します。この型変換は、汎整数拡張(Integral Promotion)と呼ばれています。

C99 では、整数拡張(Integer Promotion)という用語に変更されているようです。

変換順位は、次の順序です。

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

そのため、int型よりも変換順位が低い型とは、short や char を含んだ型のことです。イメージ的には、大きさが小さい型ほど低い変換順位ということのように思えますが、例えば、long型と int型の大きさが同じであったり、int型と short型の大きさが同じであったりする可能性があるため、大きさで考えることは不適切です。例えば、「sizeof(short) == sizeof(int)」が真になる環境であっても、short型から int型への型変換は発生します。

C99 (整数変換の順位)

C99 では、変換順位の定義の中に、long long型(第19章)と、_Bool型(第13章)が加わっています。

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

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

unsigned int型へ拡張される例としては、例えば、short型と int型の大きさが同じ環境で、unsigned short型からの拡張が挙げられます。このような環境では、unsigned short型の最大値は、signed int型では表現できませんから、unsigned int型へ拡張されます。

例えば、以下のようなコードで、汎整数拡張が起こります。

#include <stdio.h>

int main(void)
{
    signed char c1 = 15;
    signed char c2 = 40;
    short s;

    s = c1 + c2;  /* c1 と c2 を int型へ拡張して計算する */
    printf("%hd\n", s);

    return 0;
}

実行結果:

55

「c1 + c2」のところで、signed char型同士の加算を行っています。signed char型は、int型よりも変換順位が低いので、ここで汎整数拡張が行われます。いずれも int型へ変換され、int型の 15 と 40 の加算を行います。

加算結果を、short型の変数に代入していますが、恐らく、いくつかのコンパイラは、ここで警告を発するでしょう。この警告は、「int型を short型に切り詰めます」といった内容になるはずです。一見、int型が登場しないように見えるソースコードですが、このように計算過程で汎整数拡張が行われることが理由です。

この例で int型に拡張されたのは、元の型が signed char型で、この型で表現できる値の範囲は、int型でも表現できるからです。もし、元の型が unsigned char型だったらどうでしょう?

#include <stdio.h>

int main(void)
{
    unsigned char c1 = 15;
    unsigned char c2 = 40;
    short s;

    s = c1 + c2;  /* c1 と c2 を int型へ拡張して計算する */
    printf("%hd\n", s);

    return 0;
}

実行結果:

55

これでもやはり、int型へ拡張されます。なぜなら、unsigned char型で表現できる値の範囲は、int型でも表現できるからです。厄介なのは、元の型は unsigned であるのに、拡張先は signed であることです。つまり、汎整数拡張によって、値は変化しないものの、型は符号の有無も含めて変化し得るということです。

通常の算術型変換

2つのオペランドを持つ演算子を使う場面では、それぞれの型を合わせるために、暗黙的な型変換が行われます。等価演算子や関係演算子は例外的ですが、ほかの演算子では、変換後の型が、演算の結果の型でもあります。

このような変換のことを、通常の算術型変換と呼びます。「算術型」は、整数型と浮動小数点型を合わせた用語です。

通常の算術型変換の変換規則を正確に書くと、とても長くなりますが、簡潔に言えば「大きい方の型に合わせられる」ということです。正確には、次の順序で変換が適用されます。

一番最後までいくと int型ということですが、式の中で汎整数拡張が行われるので、そもそも int型よりも小さい型で演算が行われることはありません。

long型よりも float型の方が上にあることは注目すべきでしょう。float型で表現できる値は 10進数で 6桁分程度なので、long型で表現できる値の全てが float型で表現できる保証はありません。そのため、大きな long型の値を、float型と混在させるとトラブルの元になります。

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

if( -1 < 1U ){
}

この場合、-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型」です。ここでさらに、通常の算術型変換が行われていますから、「double型 / double型」として計算されます。そのため、小数点以下も正しく計算されます。

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

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

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

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

浮動小数点型の値を、整数型へ変換する場合、小数点以下が切り捨てられます。しかし、実際にはそれだけとは限らず、整数部分に関しても、値が変化する可能性があります。前章でも少し触れたように、浮動小数点数には精度があるので、整数型へ変換した結果は微妙に異なってしまうことがあるのです。

現実はさらに複雑で、符号の有無であったり、そもそも値が巨大過ぎて、変換先の型では表現しきれなかったりもします。いずれにしても、表現できる範囲を超えてしまったら、基本的に結果がどうなるか保証できません。

逆に、整数型を浮動小数点型へ変換する場合、浮動小数点数で正確に表現できるのならば、確実にその値になります。精度の都合で、正確に表現できない場合は、正確に表現できる一番近い値に変換されます

ある浮動小数点数を、近い整数に確実に変換したければ、ceil関数floor関数を利用できます。これらの関数を使うには、math.h を #include する必要があります

#include <stdio.h>
#include <math.h>

int main(void)
{
    double d   = 345.678;
    int dCeil  = (int)ceil( d );
    int dFloor = (int)floor( d );

    printf( "%d\n", dCeil );
    printf( "%d\n", dFloor );

    return 0;
}

実行結果:

346
345

ceil関数は、double型の引数を受け取り、その値以上の最小の整数を返します。floor関数は、double型の引数を受け取り、その値以下の最大の整数を返します。

どちらの関数も、戻り値の型は double型です。整数を返すといいながら double型で返すのは、通常、double型の方が巨大な値を扱えるからです。

C99 では、float型バージョンの ceilf関数floorf関数、long double型バージョンの ceill関数floorl関数が追加されています。


練習問題

問題① 次の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章から練習問題⑩を移動してきて、練習問題④とした。

'2018/2/25 算術型という用語について補足した。

'2018/2/23 全面的に文章を見直し、修正を行った。

'2018/2/21 文章中の表記を統一(bit、Byte -> ビット、バイト)

'2013/12/2 printf関数に %lf を指定していた箇所を %f に修正。

'2009/10/6 if( -1 < 1U ) が真になると記述されていたのを、偽になる、に修正。

'2009/7/10 新規作成。



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

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

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

Programming Place Plus のトップページへ


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