C言語は絶えず型のあいだで値を変換します。その一部はキャストとして自分で書くものですが、大半はあなたが選んだわけではないルールに従ってコンパイラが静かに行うものです。どちらがどちらかを知っていることが、「なぜ平均がいつも3になるんだ?」と、書いたとおりに動くコードとの分かれ目になります。
暗黙の変換
ある型の値が別の型を期待する文脈に現れると、そのたびにC言語は変換を行います。
情報を失いえない変換(int から double、char から int、short から long)は拡大変換であり、常に安全です。逆向きの変換は縮小変換でデータを失う可能性があります - 上の 3.9 は 3 になりましたが、-Wconversion で頼まないかぎり警告は出ません。
通常の算術変換
2項演算子のオペランドの型が異なるとき、C言語は処理の前に共通の型へ変換します。そのはしごを下から順に見ていきましょう。
intより小さいもの(char、short、_Bool)はすべてintに昇格されます。これが整数昇格で、常に最初に行われます。- どちらかが
long doubleなら、もう一方もlong doubleになります。 - そうでなくどちらかが
doubleなら、もう一方もdoubleになります。 - そうでなくどちらかが
floatなら、もう一方もfloatになります。 - そうでなければ、整数型のなかでランクの高いほうが勝ち、ランクが並んだ場合は符号なしが勝ちます。
実際のバグを引き起こすのは最後のルールです。残りは直感どおりです。
整数昇格があるからこそ char の算術は思ったようにはオーバーフローせず - そして結果を char に戻して格納したときにオーバーフローするのです。
明示的なキャスト
キャストは丸かっこに入れた変換先の型です。
(double)x
(int)3.9
(char)65
(unsigned int)n
直後の式にかかり、結合はとても強く - *、/、+ よりも強く結びつきます。
1行目は整数として割り(3 になります)、それから 3 を 3.0 に変換します - 手遅れです。2行目は割り算の前に total を変換するので、/ 演算子は double と int を見て int を昇格させ、浮動小数点の除算を行います。
片方のオペランドをキャストすれば十分です。もう片方は通常の算術変換が面倒を見てくれます。
整数除算を直す
これが、C言語でキャストを書く最も一般的な理由です。
パーセントの行は教訓的です。passed / n は 3 / 5 で、整数としては 0、そして 0 * 100 は 0 です。割る前に掛ける(100 * passed / n)とキャストなしでも直りますが、それは 300 / 5 が割り切れるからにすぎず、数値が都合よく協力してくれるときにしか通用しません。確実な修正はキャストです。
四捨五入ではなく切り捨て
浮動小数点の値を整数にキャストすると小数部分は捨てられます。ゼロ方向への切り捨てであり、丸めではありません。
これを自分のマシンでビルドするなら、Linux では math.h のリンク時に -lm が必要なことを忘れずに。
もう1つの危険:整数型に対して大きすぎる浮動小数点の値を変換するのは、巻き戻しではなく未定義動作です。(int)1e20 は何を返してもおかしくありません。値が自分の管理下にないときは、キャストの前に範囲を確認しましょう。
char と int
C言語の char は、文字コードを保持する小さな整数そのものです。両者を行き来する変換は日常業務です。
digit - '0' は数字文字をその値に変える定番の慣用句で、10個の数字文字が連続していることが保証されているために成り立ちます。英字については + 32 の算術より ctype.h の toupper() や tolower() を選びましょう - あの差は ASCII の事実であって、C言語の保証ではありません。
関連する罠:isdigit や toupper といった ctype.h の関数は、EOF か unsigned char として表現できる値でなければならない int を受け取ります。負の値を持つ素の char を渡すのは(素の char は符号ありでありうるので、これは起こります)未定義です。isdigit((unsigned char)c) とキャストしましょう。
符号あり・符号なしの罠
変換のはしごのステップ5 - 並んだら符号なしが勝つ - が、C言語で最も驚かされる比較を生みます。
-1 は unsigned int に変換され、そのビットパターンが 4,294,967,295 として解釈し直されます。これは 1 より大きいので、比較は偽になります。
同じ変換がループを永遠に回らせます。
/* BUG: i は符号なしなので i >= 0 は常に真。i が 0 のとき i-- は巻き戻る。 */
for (size_t i = n - 1; i >= 0; i--) { ... }
そして長さのチェックも失敗させます。
/* BUG: strlen は size_t(符号なし)を返す。文字列が 5 より短ければ
len - 5 は巨大な数に巻き戻り、テストは通ってしまう。 */
if (strlen(s) - 5 > 0) { ... }
if (strlen(s) > 5) と書き直せば、引き算はそもそも起こりません。
防御策:個数と添字は全体を通して符号の種類をそろえる、-Wsign-compare(-Wextra に含まれます)でコンパイルする、そして混ぜざるをえないときは値が負にならないことを確かめてから明示的にキャストする、です。
ポインタのキャスト
キャストはポインタ型どうしの変換も行い、こちらは本物の危険を伴います。バイトそのものではなく、メモリの解釈のしかたを変えるからです。
リトルエンディアンのマシンでは 01 00 00 00 と表示されます。unsigned char * を通してオブジェクト表現を調べるのは、規格が明示的に認めている数少ないポインタキャストの1つです。
ほかの大半のポインタキャストは認められていません。float * を通して int を読むのは厳密なエイリアシング規則に違反し、コンパイルは通っても未定義動作です。バイトを解釈し直したいなら代わりに memcpy を使いましょう。
知っておく価値のある慣習が2つあります。C言語では void * は任意のオブジェクトポインタ型とキャストなしで相互変換されます。だからこそ malloc の結果をキャストすべきではないのです。
int *arr = malloc(n * sizeof *arr); /* 正しいC */
int *arr = (int *)malloc(n * sizeof *arr); /* 不要;ヘッダの書き忘れを隠す */
このキャストは C++ では必須で、だからこれほど多くのコードに付いています。C言語では <stdlib.h> を忘れたという誤りを覆い隠しかねません。
そして printf("%p", ...) は void * を期待するので、そこでのポインタ引数には本当にキャストが必要です:printf("%p", (void *)p)。
キャストが答えではないとき
キャストはコンパイラを黙らせます。コンパイラのほうが正しいこともあります。
long big = 5000000000L;
int small = (int)big; /* このキャストは本物のデータ損失を隠す */
値が本当に収まるなら、キャストは「確認済みである」ことを示す記録になります。収まらないかもしれないなら、キャストは警告を静かな誤答へ変えてしまったことになります。書く前に、代わりに変数の型を変えるのが正しい修正ではないか考えましょう - int ではなく double、int ではなく size_t、long ではなく long long。キャストが正しい道具なのは、(double)sum / count のように、1つの演算のために2つの正しい型が出会わなければならないときが主です。
よくある質問
C言語ではどうやってキャストしますか?
値の前の丸かっこに変換先の型を書きます:(double)x、(int)3.9、(char)65。キャストは直後の式にかかるので、(double)a / b は先に a を変換してから割り算し、(double)(a / b) は整数として割ってから結果を変換します。
C言語で int を float に変換するには?
代入すれば暗黙に行われます - double d = 5; は 5.0 を格納します。式の中では明示的なキャストが必要なことが多く、(double)total / count と書くことで整数除算ではなく浮動小数点の除算を強制できます。
C言語で float を int にキャストするとどうなりますか?
小数部分は捨てられます - ゼロ方向への切り捨てであり、四捨五入ではありません。(int)3.9 は 3、(int)-3.9 は -3 です。丸めたいなら、正の値ならキャスト前に 0.5 を足すか、math.h の round()、floor()、ceil() を使いましょう。
符号ありと符号なしの int を比較すると、なぜ答えが間違うのですか?
C言語の通常の算術変換が符号あり側を符号なしに変換するため、-1 < 1u は偽になります:-1 が巨大な正の数になるのです。個数やサイズは符号の種類をそろえるか、値が負にならないと確かめたうえで明示的にキャストしましょう。