C言語編 第21章 型変換

この章の概要

この章の概要です。

符号の混在時

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

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

#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 が表現できません。 そのため、実行結果にあるような、巨大な正の数になってしまいます。


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

#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型 という型の大小関係があります。 このため、大きい方の型から小さい方の型へ代入を行うと、値が表現しきれなくなる可能性があります。 この場合、多くのコンパイラは警告を発しますが、代入そのものが不可能ということではありません。

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

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

という変換が行われます。この結果は 10進数の 34464 で、これは 2Byte符号無しで表現できる値です。 一応、プログラムで確認しておきます。

#include <stdio.h>

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

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

	return 0;
}

実行結果:

34464

もちろんこれは、long型が 4Byte、short型が 2Byte である環境を想定したプログラムですから、 自分の環境がそうなっていなければ、型を変えて試して下さい。

実行結果は確かに 34464 になりました。 恐らく、コンパイラは警告を発すると思いますが、このように、型が小さくなるような代入は危険性を伴います。 警告は決して無視せず、その意味を理解するように努めて下さい。 今回の場合、この警告を黙らせる手段が存在します。これは後で触れます(⇒ジャンプ)。

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

汎整数拡張

C言語では、式の中に登場する整数型の大きさが int型以下の場合、int型や unsigned int型に拡張されます。 これを、汎整数拡張(インテグラルプロモーション)と呼びます。 例えば、

signed char c1 = 15;
signed char c2 = 40;
short s;

s = c1 + c2;

最後の1文で、signed char型同士の加算を行っています。 このとき、変数c1、c2 は一時的に int型に拡張され、それぞれ、int型の 15 と 40 になります。 そして、その加算結果が short型の変数に代入されることになります。

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

この例で、int型に拡張されたのは、登場する整数がいずれも int型の範囲で表現可能だからです。 もし、int型の表現範囲を超える値が登場する場合には、unsigned int型に拡張されることになっています。
この規則のため、汎整数拡張によって、元の値が異なる値に変化してしまうということはありません。 変化するのは「型」だけであり、「値」も「符号」も変化することはありません。

通常の算術型変換

2つのオペランドを持つ演算子を使う場面でも、暗黙的に型変換が行われています。 この型変換は、2つのオペランドの型を統一することが目的で、 変換後の型は、演算の結果の型でもあります。

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

int型よりも小さくなることはありません。 つまりここでも、汎整数拡張は生きています。

このような変換規則があるため、例えば、

if( -1 < 1U ){
}

のような判定は、恐らく意図通りには行われません。 この場合、-1 は int型、1U は unsigned int型なので、両者は unsigned int型に合わせられます。 -1 を unsigned int型で表現することはできないため、非常に巨大な正の数になってしまいます。 従って、この条件式は偽となってしまうでしょう。


ところで、long型よりも float型の方が強いことには注意が必要です。 float型の精度は、10進数で 6桁分程度ですから、long型で表現できる値の全てが float型で表現できる保証はありません。 float型を避けるべき最大の理由がここにあります。 大きな long型の値を、float型と混在させるとトラブルの元になります。

キャスト

ここまで見てきたような、暗黙的に行われる型変換以外に、明示的に行う型変換もあります。 これをキャストと呼びます。

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

#include <stdio.h>

int main(void)
{
	int num = 100;
	double third1 = num / 3;
	double third2 = (double)num / 3;
	double third3 = num / 3.0;


	printf( "%f\n", third1 );
	printf( "%f\n", third2 );
	printf( "%f\n", third3 );

	return 0;
}

実行結果:

33.000000
33.333333
33.333333

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

third2 の例では、(double)num という変わった記述があります。これがキャストです。

(型名)値

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

「(double)num / 3」は、キャストの効力によって「double型 / int型」になります。 更に、通常の算術型変換によって、これは「double型 / double型」として計算されることとなり、小数点以下も正しく計算されます。

キャストを使う以外の手段が、third3 の例にあります。 「num / 3.0」という記述だと、「int型 / double型」になりますから、これもやはり、通常の算術型変換によって、 「double型 / double型」として計算されます。

整数と小数の混在時

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

現実は更に複雑で、符号の有無であったり、そもそも値が巨大過ぎて、代入先の型で表現しきれなかったもしますが、 いずれにせよ、表現可能な範囲を超えれば、それはオーバーフローしたことになり、基本的に結果は不定です。

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


ある浮動小数点数を、近い整数に確実に変換したければ、 ceil関数(⇒リファレンス)や floor関数(⇒リファレンス)を利用できます。

#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型であり、math.h を #include する一文が必要になります。

整数を返すといいながら、戻り値が double型なのは、普通、double型の方が巨大な値を扱えるからです(精度の問題は別として)。 そのため、int型などの整数型が欲しければ、キャストが必要になります。

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 ){}

問題A 次のプログラムで出力される 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;
}

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

#include <stdio.h>

int main(void)
{
	int num = 47;

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

	return 0;
}


解答ページはこちら

参考リンク

S・P・ハービソン3世とG・L・スティール・ジュニアのCリファレンスマニュアル 第5版
 -- 規格上の定義の確認に。

更新履歴

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

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

'2009/7/10 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ