今回は《型変換について学ぼう》というテーマで記事をまとめます。
「型変換」を習得すると、よりバリエーションに富んだプログラムを書くことができます。しっかり学んでいきましょう!
尚、「型変換」は「変数の型」の知識が必須となります。「変数の型」の基本を習得していない方は、まずは下記の記事を参考に学習してみてください。
また、「ポインタ」もまた「変数」の一種なので、「変数の型」が存在します。「ポインタ」の基本を習得していない方は、まずは下記の記事を参考に学習してみてください。(今回の記事に特化すると、ポインタ基礎編①、②の分だけで十分です)
尚、現在執筆中のC言語入門記事は以下の人を対象としています。
- プログラミング未経験の人
- C言語プログラミングの学習をこれから始めたい人
- C言語プログラミングの学習を始めたばかりの人
型変換について学ぼう
型変換とは
変数には様々な型が存在しますね。
- 基本的な型(int、char、float、...)
- 構造体
- ポインタ
ある変数に別の変数の値を代入する時、同じ型の変数であれば何も考えずに代入することができますね。
異なる型の変数を代入した場合はどうなるのでしょう?
答えはシンプルです。代入先の変数の型に変換する必要があります。
これを「型変換」と呼びます。
そして、型変換は大きく分けて2種類に分類されます。
- 暗黙の型変換
- 明示的型変換(「キャスト」と呼ぶ)
最初に基本となる「数値に対する型変換」の概念を解説した後、それぞれの型変換について解説していきたいと思います。
【まずは基本】数値に対する型変換
それでは、「数値に対する型変換」の概念について解説します。まずは、さらっと読んでいただき、具体的な型変換の方法を学んだあとにもう一度読んでいただく方法がオススメです。
まず、数値に対する「型変換」は『元の型』と『変換後の型』の組み合わせによって変換結果が異なります。
▽浮動小数点 ⇒ 整数
小数点以下が切り捨てられます。
例)20.5(double型) ⇒ 20(int型)
▽整数 ⇒ 浮動小数点
小数点以下は 0 で変換されます。
例)20(int型) ⇒ 20.0(double型)
▽バイト数が小さい ⇒ バイト数が大きい
特別な変換は行われません。
例)20(char型) ⇒ 20(int型)
20.5(float型) ⇒ 20.5(double型)
▽バイト数が大きい ⇒ バイト数が小さい(共通)
変換後の型の有効範囲内であれば、特別な変換は行われません。
例)20(int型) ⇒ 20(char型)
20.5(double型) ⇒ 20.5(float型)
変換後の型(バイト数が小さい方の型)の有効範囲外の場合、整数と浮動小数点で変換結果が異なります。
▽バイト数が大きい ⇒ バイト数が小さい(整数)
上位ビットが切り捨てられます。
例)0x12345678(int型) ⇒ 0x78(char型)
つまり、値としてはまったく別物になるので注意が必要です。『意図して変換する場合』以外は使用しないほうが良いでしょう。
▽バイト数が大きい ⇒ バイト数が小さい(浮動小数点)
浮動小数点の型変換の話を理解するには、前提知識として浮動小数点型変数の仕組みを知る必要があります。ご存じの方は読み飛ばしてください。
※補足:浮動小数点の仕組み
浮動小数点型の変数は「指数部」と「仮数部」の2つに分かれます。(正確には「符号部(±の情報)」を加えた3つですが、ここでは割愛します)
数値を A × BC で表現したとき、Aを仮数部、Bを基数部、Cを指数部と言います。
具体例) 12345 = 1.2345 × 104
⇒仮数:1.2345、基数:10、指数:4
C言語では(というか、ほぼ全ての言語では)基数部が10固定なので、「指数部」と「仮数部」の2つの情報で浮動小数点を表すことができます。
そして、float型とdouble型で「指数部」と「仮数部」のサイズが異なります。次の表をご覧ください。
型 | 指数部 | 仮数部 |
float | 8ビット | 23ビット |
double | 11ビット | 52ビット |
double型(バイト数が大きい浮動小数点型)をfloat型(バイト数が小さい浮動小数点型)に型変換した場合、指数部が切り捨てられます。もしも、仮数部がfloat型の上限を超えている場合、変換失敗となります(オーバーフローを表す「inf」が格納される)。
▽まとめ
次の項目については情報の欠落・破壊が行われる可能性があることに注意してください。
- 浮動小数点 ⇒ 整数
- バイト数が大きい ⇒ バイト数が小さい
上記以外は特に気にする必要はありません。
暗黙の型変換
「暗黙の型変換」は、型変換を明確に記述しなくても代入時や式の中で自動的に型変換が行われるものです。
代入
左辺の型と右辺の型が異なっている場合、自動的に左辺の型に変換されます。
例えば、double型の変数をint型の変数に代入した場合、小数点以下が切り捨てられます。
double dbl = 12.345; int num = 0; num = dbl; /* num には12が格納される */
式の中
式の中に異なる型の変数・定数が記述されている場合、最も精度の高い型に統一されます。精度の高い型とは、より多い情報量を格納できる型という認識で問題ありません。
その関係は次のようになります。
char < short < int <= long < long long < float < double
実は、正確な言い方をすると「変数の型の精度」の関係は実行OSによって異なります。
とはいえ、C言語の仕様として「変数の型の精度」は次の条件でその順番が決定されます。この点だけはきちんと押さえておきましょう。
- 整数 < 不動小数点
- バイト数が小さい < バイト数が大きい
尚、上記を踏まえると、実行OSがWindowsの場合は次のような関係となります。
char < short < int = long < long long < float < double
※Windowsはint型とlong型の変数サイズが同じ
明示的型変換(キャスト)
「明示的型変換」は、開発者が意識して型変換を行いたい場合に使用します(自動的には行われない型変換)。
「明示的型変換」のことを一般的に「キャスト」と呼びます(むしろ、「明示的型変換」とは呼ばない)。
明示的型変換(キャスト)の構文
明示的型変換(キャスト)の構文は次のようになります。
(型名)変数名
具体的なコードも紹介しておきます。
int value_int = 10; double value_dbl = 20.5; int result_int = 0; double result_dbl = 0; double result_dbl2 = 0; /* double型 → int型 (小数点以下は除去される) */ result_int = (int)value_dbl; /* int型 → double型 (小数点以下は0) */ result_dbl = (double)value_int; /* 式の中で「value_dbl」に対して『double型 → int型』を行い、 */ /* 計算結果に対して『int型 → double型』を行う */ result_dbl2 = value_int + (int)value_dbl;
上記のコードを実行してもらうと、「result_dbl2」には 30.0 が格納されているのがわかると思います。処理フローは次のようになります。
- 式の中で「value_dbl」に対して『double型 → int型』(20.5 → 20)
- 計算(10 + 20 = 30)
- 「result_dbl2」へ代入時、計算結果に対して『int型 → double型』(30 → 30.0)
このように、式の中で明示的型変換(キャスト)を行うと、最終的な計算結果が変化します。利用シーンはいくつも存在するので、使用方法をしっかりと習得しておいてくだしあ。
異なる型のポインタの型変換
ポインタも「変数」の一種なので、「変数の型」が存在ます(「~~型のポインタ型」という考え方が一番わかりやすいと思います)。
さて、ポインタは「変数のアドレス」を管理する変数であり、定義された型で「使用するメモリ領域のサイズ」を管理する変数でしたね(例として「int*」の場合、使用するメモリ領域のサイズは4バイト)。
つまり、ポインタを型変換された場合、「変数のアドレス」は同じですが「使用するメモリ領域のサイズ」が変化します。すると、値を正しく参照(代入や取得)ができなくなる為、注意が必要です。
int num = 0; int* pnum = NULL; char* pchr = NULL; /* ポインタと参照先変数の型が同じ */ pnum = # *pnum = 10000; /* 正しく参照できる */ /* ポインタと参照先変数の型が異なる */ pchr = (char*)pnum; *pchr = 10000; /* 正しく参照できない */
「void*」は必ず型変換する
ポインタの型には「void*」というものが存在します。
「void*」とはなんだろうと考えたとき、賢い皆さんなら「void*」=「void型のポインタ」ということはわかると思います。
では、「void型」とはなんでしょう?
「void型」とは、「型がないことを表す型」です。
『「型がないことを表す型」って、どういうこと?』とお思いでしょう。
「変数は、変数の型によって使用するメモリ領域のサイズが異なる」ということはご存じですね。
つまり、「void型」とは「使用するメモリ領域のサイズが決まっていない変数の型」であり、「void型のポインタ」とは「使用するメモリ領域のサイズは決まってないけど、アドレスは確保できるポインタ」となります。
当然ですが、使用するメモリ領域のサイズが決まらないまま変数を使用することはできません。一度型変換して使用するメモリ領域のサイズを決定する必要があります。
int num = 10; void* pvoid = NULL; int* pint = NULL; /* void型のポインタに変数アドレスを代入 */ pvoid = # /* この書き方はNG(コンパイルエラーになります) */ *pvoid = 20; /* この書き方はOK */ pint = pvoid; *pint = 20; /* 【応用】この書き方もOK */ *(int*)pvoid = 20;
サンプル
それでは、サンプルを用意したので実行してみてください。尚、一部の型変換にてワーニングが出力されますが、あえてそのような記述をしているので無視してください(ワーニングが出力されるコードには、その旨のコメントを記述してあります)。
#include <stdio.h> void main(){ int value_int = 10; double value_dbl = 20.5; int result_int = 0; double result_dbl = 0; double result_dbl2 = 0; void* pvoid = NULL; int* pint = NULL; /*---------------------------------*/ /* 【暗黙の型変換】 */ /*---------------------------------*/ /* double型 → int型 (小数点以下は除去される) */ /* ※ワーニングが出力されます */ result_int = value_dbl; /* int型 → double型 (小数点以下は0) */ result_dbl = value_int; /* 式の中の変数はint型とdouble型なので、 */ /* より精度の高いdouble型で統一される */ result_dbl2 = value_int + value_dbl; /* 結果表示 */ printf("【暗黙の型変換】結果\n"); printf(" result_int = %d\n", result_int); printf(" result_dbl = %lf\n", result_dbl); printf("result_dbl2 = %lf\n", result_dbl2); printf("\n"); /*---------------------------------*/ /* 【明示的型変換(キャスト)】 */ /*---------------------------------*/ /* double型 → int型 (小数点以下は除去される) */ result_int = (int)value_dbl; /* int型 → double型 (小数点以下は0) */ result_dbl = value_int; /* 式の中で「value_dbl」に対して『double型 → int型』を行い、 */ /* 計算結果に対して『int型 → double型』を行う */ result_dbl2 = value_int + (int)value_dbl; /* 結果表示 */ printf("【明示的型変換(キャスト)】結果\n"); printf(" result_int = %d\n", result_int); printf(" result_dbl = %lf\n", result_dbl); printf("result_dbl2 = %lf\n", result_dbl2); printf("\n"); /*---------------------------------*/ /* 【void型ポインタの型変換】 */ /*---------------------------------*/ /* void型ポインタに変数アドレスを代入 */ pvoid = &value_int; /* void* → int* */ pint = pvoid; *pint = 2000; /* 結果表示 */ printf("【void型ポインタの型変換】結果①\n"); printf(" value_int = %d\n", value_int); printf("\n"); /* 【応用】この書き方もOK */ *(int*)pvoid = 3000; /* 結果表示 */ printf("【void型ポインタの型変換】結果②\n"); printf(" value_int = %d\n", value_int); printf("\n"); }
次回は「メモリの動的確保」
今回は「変数の型変換」について学んでいただきました。重要なファクターの1つでもあるので、理解が浅いと感じている方は復習をオススメします(オススメの勉強法は、色んなソースコードのパターンを用意してトライ&エラーすることです)。
次記事の内容は「メモリの動的確保」です。前記事で基礎編が一旦終了した「ポインタ」ですが、次記事の内容とポインタは密接に関係しています。
今までは使用する変数の数や配列の要素数が固定(これを「メモリの静的確保」という)でしたね。これでは、単純な処理しか実装できなかったり、プログラムの制限が多くなります。
しかし、「メモリの動的確保」を習得すれば、複雑な処理を実装できたり、プログラムの制限も大きく緩和されます。
ご期待ください。