C言語の基本|C言語の型変換

型が変わるタイミングを見抜けば、切り捨てや予想外の計算結果を防げる。暗黙変換とキャストを正しく使い分けよう。

C言語では、すべての値が何らかの型を持っています。

たとえば、整数を扱うint型、小数を扱うdouble型、小さな整数を扱うchar型やshort型などがあります。

int count = 10;
double temperature = 24.5;
char level = 3;

異なる型を一緒に計算したり、ある型の値を別の型の変数へ代入したりすると、型を合わせるための変換が行われます。これが型変換です。

型変換には、コンパイラが自動的に行う暗黙の型変換と、開発者が指定する明示的な型変換があります。

型変換を意識せずに計算すると、次のような問題が起こることがあります。

  • int型同士で割ったため、小数部分が失われる
  • double型の値をint型へ代入し、小数部分が消える
  • 大きな値を小さな型へ変換し、元の値を表現できなくなる
  • 符号付き整数と符号なし整数が混ざり、負の値が大きな正の値として扱われる
  • char型やshort型で計算したつもりでも、式の中ではint型に拡張される

重要なのは、どの型の変数へ代入するかだけではありません。計算がどの型で行われるかを確認することも大切です。

型変換は2種類ある

C言語の型変換は、暗黙の型変換と明示的な型変換に分けられます。

種類変換するもの主な目的
暗黙の型変換コンパイラint型とdouble型の混合計算演算できる共通の型へ合わせる
明示的な型変換開発者(double)a / b意図した型で計算する

暗黙の型変換は、プログラムに変換処理を書かなくても自動的に行われます。

int count = 7;
double rate = 1.5;
double result = count * rate;

この計算では、countの値が一時的にdouble型へ変換されます。

明示的な型変換では、変換先の型を丸括弧で指定します。

(double)count

この書き方をキャストといいます。

型変換は、計算中の値に対して一時的に行われることがあります。キャストを使用しても、変数そのものの型が変更されるわけではありません。

図1:暗黙の型変換と明示的な型変換

この図から分かること

int型とdouble型を一緒に計算すると、コンパイラがint型の値をdouble型へ変換します。これは暗黙の型変換です。

(double)aのように変換先を開発者が指定する方法が、明示的な型変換です。

どちらの場合も、変換されるのは計算で使用する値です。元の変数がint型からdouble型へ作り替えられるわけではありません。

型変換が起こる主なタイミング

この記事で扱う型変換は、主に演算時と代入時に発生します。

タイミング行われる変換
演算時int型 + double型共通の型へ合わせてから計算する
代入時int型の変数 = double型の値右辺を左辺の型へ変換する
キャスト時(double)a指定した型へ一時的に変換する

演算時には、オペランド同士を計算できるように型が調整されます。

代入時には、右辺の値が左辺の変数の型に合わせて変換されます。

この2つを区別すると、計算結果がどの段階で変化したのかを追いやすくなります。

小さい整数型に行われる整数拡張

char型やshort型は、int型より小さい整数型として使われることがあります。

char c = 2;
short s = 3;

これらを式の中で計算すると、通常はint型またはunsigned int型へ変換されます。この変換を整数拡張といいます。

c + c
s + s
c + s

一般的な環境では、cやsが持つ値をint型へ拡張してから加算します。

charまたはshortの値
        ↓
intへ整数拡張
        ↓
int型として計算

変数cやs自体の型が変わるわけではありません。式の計算に使われる値が、一時的に広い整数型へ変換されます。

sizeofで式の型の大きさを確認する

sizeofを使うと、変数や式の型が使用するバイト数を確認できます。

sizeof(c)

これは、変数cの型が使用するバイト数を表します。

sizeof(c + c)

こちらは、c + cという式の型が使用するバイト数を表します。

cがchar型でも、c + cでは整数拡張が行われるため、式の型はint型またはunsigned int型になります。

char型とshort型の整数拡張を確認するプログラム

ファイル名:6_8_1.c

#include <stdio.h>

int main(void)
{
    char sensor_level = 2;
    short sample_count = 3;

    printf("sizeof(char) = %zu\n",
           sizeof(sensor_level));

    printf("sizeof(short) = %zu\n",
           sizeof(sample_count));

    printf("sizeof(int) = %zu\n",
           sizeof(int));

    printf("sizeof(sensor_level + sensor_level) = %zu\n",
           sizeof(sensor_level + sensor_level));

    printf("sizeof(sample_count + sample_count) = %zu\n",
           sizeof(sample_count + sample_count));

    printf("sizeof(sensor_level + sample_count) = %zu\n",
           sizeof(sensor_level + sample_count));

    return 0;
}
実行結果の例

char型が1バイト、short型が2バイト、int型が4バイトの環境では、次のような結果になります。

sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(sensor_level + sensor_level) = 4
sizeof(sample_count + sample_count) = 4
sizeof(sensor_level + sample_count) = 4

整数拡張を確認するプログラムの解説

変数sensor_levelはchar型です。

char sensor_level = 2;

変数そのものの大きさを確認すると、一般的な環境では1バイトです。

sizeof(sensor_level)

しかし、次の式では、sensor_levelの値が整数拡張されてから加算されます。

sensor_level + sensor_level

そのため、式の型の大きさはint型と同じ4バイトになっています。

short型のsample_countも同様です。

sample_count + sample_count

この式でも整数拡張が行われます。

char型やshort型の変数を使用していても、計算まで必ず小さい型で行われるとは限りません。

なお、各型のバイト数は実行環境によって異なる場合があります。プログラムを実行した環境の結果を確認しましょう。

異なる算術型を混ぜたときの変換

異なる型を使って計算する場合、C言語は共通の型へ合わせてから演算します。この仕組みを通常の算術型変換といいます。

浮動小数点型が含まれる場合は、主に次のように変換されます。

式に含まれる型計算時の主な型
intとfloatfloat
intとdoubledouble
floatとdoubledouble
doubleとlong doublelong double

整数型だけが混ざる場合は、整数変換の順位、符号の有無、値を表現できる範囲によって共通の型が決まります。

すべての型を単純な1本の順番として覚えるよりも、次の流れで考えると分かりやすくなります。

  1. char型やshort型には整数拡張が行われる
  2. 浮動小数点型があれば、その型に合わせる
  3. 整数型だけなら、型の順位と符号を使って共通の型を決める
  4. 共通の型へ変換してから演算する

int型とdouble型を一緒に計算する

int型とdouble型を一緒に計算すると、int型の値がdouble型へ変換されます。

int count = 7;
double correction = 1.25;
double result = count + correction;

計算の流れは次のとおりです。

countの値7
        ↓
double型の7.0へ変換
        ↓
7.0 + 1.25
        ↓
結果は8.25

countという変数自体はint型のままです。

int型とdouble型を混ぜて計算するプログラム

ファイル名:6_8_2.c

#include <stdio.h>

int main(void)
{
    int measurement_count = 7;
    double correction_value = 1.25;
    double adjusted_result;

    adjusted_result = measurement_count + correction_value;

    printf("measurement_count = %d\n",
           measurement_count);

    printf("correction_value = %.2f\n",
           correction_value);

    printf("加算結果 = %.2f\n",
           adjusted_result);

    return 0;
}
実行結果の例
measurement_count = 7
correction_value = 1.25
加算結果 = 8.25

混合計算を行うプログラムの解説

右辺には、int型のmeasurement_countと、double型のcorrection_valueがあります。

measurement_count + correction_value

異なる型のままではなく、measurement_countの値7がdouble型の7.0へ変換されます。

int型の7
        ↓
double型の7.0

そのあと、double型同士として加算されます。

7.0 + 1.25 = 8.25

結果の型もdouble型になるため、double型のadjusted_resultへ保存できます。

計算後もmeasurement_countはint型であり、値も7のままです。型変換は計算に使われる値へ一時的に行われています。

代入時は左辺の型に合わせて変換される

代入では、右辺の値が左辺の変数の型へ変換されます。

left = right;

leftがint型でrightがdouble型なら、double型の値をint型へ変換してから保存します。

int recorded_value;
double measured_value = 87.96;

recorded_value = measured_value;

この代入では、87.96がint型へ変換されます。

double型の87.96
        ↓
int型の87
        ↓
recorded_valueへ保存

小数部分の.96は失われます。

図2:広い型と狭い型の間で起こる代入時の変換

この図から分かること

double型の87.96をint型へ代入すると、小数部分が取り除かれて87になります。狭い型へ変換する場合は、情報が失われる可能性があります。

int型の-12をdouble型へ代入すると、-12.0として保存されます。小さな整数を広い型へ変換する場合は、値が保たれやすくなります。

ただし、非常に大きな整数はdouble型でも正確に表現できないことがあるため、広い型への変換が常に完全に安全とは限りません。

double型からint型への変換

double型の値をint型へ変換すると、小数部分は0の方向へ取り除かれます。

正の値では、小数部分が切り捨てられたように見えます。

87.96 → 87

負の値でも0の方向へ取り除かれます。

-87.96 → -87

四捨五入ではない点に注意しましょう。

また、小数部分を取り除いた値がint型で表現できる範囲を超えている場合は、安全な変換になりません。

小数をint型へ代入して変化を確認するプログラム

ファイル名:6_8_3.c

#include <stdio.h>

int main(void)
{
    double measured_voltage = 87.96;
    int stored_voltage;

    stored_voltage = measured_voltage;

    printf("変換前の値 = %.2f\n",
           measured_voltage);

    printf("int型へ代入した値 = %d\n",
           stored_voltage);

    return 0;
}
実行結果の例
変換前の値 = 87.96
int型へ代入した値 = 87

小数をint型へ代入するプログラムの解説

変数measured_voltageはdouble型なので、小数部分を含む87.96を保存できます。

double measured_voltage = 87.96;

stored_voltageはint型です。

int stored_voltage;

次の代入で、右辺のdouble型の値が左辺のint型へ暗黙に変換されます。

stored_voltage = measured_voltage;

変換時に小数部分が取り除かれるため、stored_voltageへ保存される値は87です。

87.96
  ↓
87

これは四捨五入ではありません。小数部分が単純に取り除かれています。

小さい型への代入で起こりやすい問題

値をより小さな型へ変換すると、次のような問題が起こる可能性があります。

  • double型からint型への変換で小数部分が失われる
  • 大きな整数をchar型やshort型へ変換し、値を表現できなくなる
  • 符号付き整数を符号なし整数へ変換し、見た目の値が大きく変わる
  • 変換先の型の範囲を超え、期待した値にならない

代入前には、左辺の型が右辺の値を正しく表現できるか確認することが大切です。

int型からdouble型への変換

int型の値をdouble型へ代入すると、整数が浮動小数点数へ変換されます。

int offset = -12;
double precise_offset;

precise_offset = offset;

この代入では、-12が-12.0へ変換されます。

int型の-12
        ↓
double型の-12.0

一般的な範囲の小さな整数なら、値を保ったままdouble型へ変換できます。

ただし、double型にも正確に表現できる桁数の限界があります。非常に大きな整数をdouble型へ変換すると、近い値へ丸められる可能性があります。

整数をdouble型へ代入するプログラム

ファイル名:6_8_4.c

#include <stdio.h>

int main(void)
{
    int sensor_offset = -12;
    double precise_offset;

    precise_offset = sensor_offset;

    printf("int型の値 = %d\n",
           sensor_offset);

    printf("double型へ代入した値 = %.1f\n",
           precise_offset);

    return 0;
}
実行結果の例
int型の値 = -12
double型へ代入した値 = -12.0

整数をdouble型へ代入するプログラムの解説

sensor_offsetはint型です。

int sensor_offset = -12;

precise_offsetはdouble型なので、小数を扱えます。

double precise_offset;

次の代入では、右辺の-12がdouble型の-12.0へ変換されます。

precise_offset = sensor_offset;

この範囲の値では、整数部分と符号が保たれています。

変換後もsensor_offsetはint型の-12のままです。代入によってsensor_offset自身の型が変わるわけではありません。

明示的な型変換であるキャスト

開発者が変換先の型を指定する方法をキャストといいます。

基本的な書き方は次のとおりです。

(変換先の型)式

int型の変数valueをdouble型として扱いたい場合は、次のように書きます。

(double)value

キャストによって、計算のどの段階から別の型として扱うのかを明確にできます。

特によく使うのが、整数同士の除算を浮動小数点の除算へ変更する場合です。

代入先をdouble型にするだけでは小数にならない

次の変数は、どちらもint型です。

int total_distance = 17;
int sections = 4;

計算結果の保存先をdouble型にしても、除算自体はint型同士で行われます。

double average = total_distance / sections;

計算の順番は次のとおりです。

17 / 4をint型同士で計算
        ↓
結果は4
        ↓
4をdouble型へ変換
        ↓
averageへ4.0を保存

数学上の4.25は得られません。

除算前にdouble型へキャストする

小数を含む結果が必要なら、除算を始める前に一方のオペランドをdouble型へ変換します。

double average = (double)total_distance / sections;

計算の流れは次のとおりです。

total_distanceの17を17.0へ変換
        ↓
17.0 / 4を計算
        ↓
結果は4.25

一方がdouble型になるため、もう一方のsectionsもdouble型に合わせて計算されます。

キャストの位置による除算結果の違いを確認するプログラム

ファイル名:6_8_5.c

#include <stdio.h>

int main(void)
{
    int total_distance = 17;
    int sections = 4;
    double integer_division_result;
    double decimal_division_result;

    /* int型同士で除算してからdouble型へ代入する */
    integer_division_result = total_distance / sections;

    /* 除算前にdouble型へ変換する */
    decimal_division_result =
        (double)total_distance / sections;

    printf("そのまま除算した結果 = %.2f\n",
           integer_division_result);

    printf("キャストして除算した結果 = %.2f\n",
           decimal_division_result);

    return 0;
}
実行結果の例
そのまま除算した結果 = 4.00
キャストして除算した結果 = 4.25

キャストを使った除算プログラムの解説

次の式では、total_distanceとsectionsがどちらもint型です。

total_distance / sections

そのため、整数同士の除算が行われます。

17 / 4 = 4

小数部分が失われたあと、4がdouble型の4.0へ変換されます。

integer_division_result = total_distance / sections;

次の式では、除算前にtotal_distanceをdouble型へ変換しています。

(double)total_distance / sections

この場合は、17.0 / 4として計算されるため、結果は4.25です。

decimal_division_result =
    (double)total_distance / sections;

どちらも代入先はdouble型ですが、除算を行った型が違うため、結果も異なります。

図3:キャストする位置で除算結果が変わる

この図から分かること

x = a / b;では、aとbがint型なら整数除算が先に行われます。結果の4をdouble型へ変換するため、xへ入る値は4.0です。

x = (double)a / b;では、aを先にdouble型へ変換します。そのため、小数を含む4.25が得られます。

x = (double)(a / b);では、キャストが書かれていても、括弧内の整数除算が先です。一度失われた小数部分は、あとからdouble型へ変換しても戻りません。

キャストする位置を比較する

aが17、bが4の場合を整理すると、次のようになります。

書き方計算の流れ結果
x = a / b;int型で除算してからdouble型へ代入4.0
x = (double)a / b;aをdouble型へ変換してから除算4.25
x = (double)(a / b);int型で除算した結果をdouble型へ変換4.0

小数を含む結果が必要な場合は、計算が始まる前にオペランドをdouble型へ変換します。

キャストしても変数自体の型は変わらない

次のキャストを実行しても、total_distance自体はint型のままです。

(double)total_distance

キャストによって作られた一時的な値だけがdouble型になります。

total_distance
型:int
値:17

(double)total_distance
型:double
値:17.0

その後もtotal_distanceを使えば、int型の17として扱われます。

符号付き整数と符号なし整数の混在

整数型には、負の値を扱える符号付き整数と、0以上の値だけを扱う符号なし整数があります。

int signed_value = -1;
unsigned int unsigned_value = 1U;

signed_valueは符号付き、unsigned_valueは符号なしです。

この2つを比較するとき、同じ順位のint型とunsigned int型では、符号付きの値がunsigned int型へ変換されます。

signed_value < unsigned_value

signed_valueの-1がunsigned int型へ変換されると、非常に大きな正の値になります。

一般的な32ビットのunsigned int型では、-1は次の値に対応します。

4,294,967,295

そのため、見た目では-1 < 1が真に思えても、変換後の比較では偽になります。

変換前のイメージ:
-1 < 1

unsigned intへ変換後:
非常に大きな値 < 1

結果:偽

符号付きと符号なしが混ざった計算や比較では、次の点を確認しましょう。

  • どちらの型へ変換されるか
  • 負の値が含まれていないか
  • 変数の型をそろえられないか
  • 比較前のキャストが本当に安全か

unsigned型へ負の値を変換するときの注意

符号なし整数型では、負の値をそのまま表現できません。

負の値をunsigned型へ変換すると、その型で表現できる値の個数を使って対応する値へ変換されます。その結果、非常に大きな正の値になることがあります。

int signed_value = -1;
unsigned int converted_value;

converted_value = signed_value;

このような代入はコンパイルできる場合がありますが、表示される値は-1ではありません。

符号を含む可能性がある値では、安易にunsigned型へ変換しないことが大切です。

型変換を読むときの確認ポイント

型変換を含む式では、次の順番で考えると処理を追いやすくなります。

  1. 各変数や定数の型を確認する
  2. char型やshort型に整数拡張が行われるか確認する
  3. 演算がどの共通型で行われるか確認する
  4. 計算結果の型を確認する
  5. 代入先の型へ変換したときに情報が失われないか確認する
  6. キャストが計算前に行われているか確認する
  7. signed型とunsigned型が混ざっていないか確認する

特に注意したい組み合わせは次のとおりです。

確認すること起こりやすい問題
int型同士の除算か小数部分が失われる
double型からint型への代入か小数部分が取り除かれる
大きな型から小さな型への変換か値を表現できなくなる
signed型とunsigned型が混ざっているか負の値が大きな正の値になる
キャストが計算後になっていないか失われた小数部分が戻らない
char型やshort型の演算かint型へ整数拡張される

型変換を使うときは、変換先だけを見るのではなく、計算が始まる時点の型、計算中の型、代入時の型という順番で確認することが大切です。