LoginSignup
53
53

More than 5 years have passed since last update.

C言語で暗黙の型変換が発生する16のパターン(+演算子の結果型5パターン)

Last updated at Posted at 2016-11-05

C言語(C90)で暗黙の型変換が発生するケースを、思いつく限り網羅してみました。暗黙の型変換は「あ、忘れてた」となってハマることがしばしばあるため、備忘録の意味も込めて随時追加予定です(ある程度固まれば、コードレビューのチェックリストにも使えるかもしれない)。

全部で21パターンありました。

凡例

本記事のコード例は、直上のリスト文のみに付随するものです。この凡例は、以下例において、Hello world!を出力するコード例がリスト文「猫たちは大変にすばらしい存在である。」には関係しないことを示します。猫たち…。

  • 猫たちは大変にすばらしい存在である。
  • printf関数を呼び出す。
コード例
#include <stdio.h>
int main(void)
{
    printf("Hello world!\n");
    return 0;
}
  • 猫たちは大変にすばらしい存在である。

配列

型Tの配列 -> 型Tを指すポインタ

  • 配列型を指し示す左辺値の式は、配列の先頭要素を指すポインタ型に変換される(sizeof演算子のオペランド、単項&演算子のオペランド、文字型配列を初期化する文字列リテラルの場合は除く)。
配列のポインタ変換
const char *p = "Hello world!";  /* ポインタに変換される */
char s[] = "Hello world!";       /* ポインタに変換されない */
sizeof(s);    /* オペランドはポインタに変換されない */
&s;           /* オペランドはポインタに変換されない */
  • 型「型Tを要素とする配列」をもつ仮引数は、型「型Tを指すポインタ」に変換される。
void f(const char s[])
{
    sizeof(s);    /* ポインタのサイズ */
}
int main(void)
{
    const char message[] = "Hello world!";
    sizeof(message);        /* 配列のサイズ */

    f(message);
    return 0;
}

C90では、配列に関するこれらの型変換の適用は配列が左辺値である場合に限られることに注意する。

以下コード例の式f().aは右辺値であるため、式がもつ型はポインタに変換されず、配列のままとなる。これは添字演算子[]の制約(一方のオペランドにオブジェクトへのポインタをとる)に違反するため、違法コードとなる。

なお、C99以降ではf().aの結果はポインタとなるため合法コードとなる。

C90では違法、C99以降では合法
#include <stdio.h>

typedef struct X_ {
    int a[1];
} X;

X f(void)
{
    X x;
    x.a[0] = 255;

    return x;
}

int main(void)
{
    printf("%d\n", f().a[0]);  /* NG : ISO C90 forbids subscripting non-lvalue array */

    return 0;
}

関数

型「型Tを返す関数」-> 型「型Tを返す関数へのポインタ」

  • 関数指示子は、関数を指すポインタ型に変換される(sizeof演算子のオペランド、単項&演算子のオペランドの場合は除く)。
関数指示子の関数ポインタへの変換
printf("Hello world!\n");         /* (1) */

(******printf)("Hello world!\n"); /* (1)と等価 */

(&printf)("Hello world!\n");      /* (1)と等価 */

sizeof(printf);  /* コンパイルエラー */
/* このprintfはポインタに変換されないため、関数型のままとなる。
/* その結果、sizeof演算子の制約(オペランドに関数型をとらない)に違反。 */
  • 型「型Tを返す関数」をもつ仮引数は、型「型Tを返す関数へのポインタ」に変換される。
仮引数が関数型
#include <stdio.h>

void f(void ptr_to_func(void))  /* 仮引数が関数型 */
{
    ptr_to_func();       /* ptr_to_funcは関数ポインタ変数 */
    sizeof(ptr_to_func); /* ポインタ(左辺値)なのでsizeofの制約に違反しない */
    /*sizeof(f);*/       /* fは関数なのでsizeofの制約に違反 */
}
void g(void){
    printf("Hello\n");
}

int main(void)
{
    f(g);    /* Hello */
    return 0;
}

関数呼出しの実引数 -> 既定の実引数拡張

本項目の解説で使用している「関数原型形式」「非原型形式」「仮引数型並び」「既定の実引数拡張」等の用語の意味については「関数定義の一部である関数原型形式の関数宣言子の仮引数型並びと、非原型形式の関数宣言子の識別子並び・宣言並び、および関数定義の一部でない関数原型形式/非原型形式の関数宣言子について」で解説しています。よければご参考下さい。

  • 関数原型形式の関数宣言子が可視の場合、その関数呼出しの実引数について、実引数の型から仮引数の型への代入変換が適用される。

  • 関数原型形式の関数宣言子の仮引数型並びが,...で終わる場合、,...の位置にあたる実引数は、既定の実引数拡張が適用される。

既定の実引数拡張
#include <stdio.h>

int main(void)
{
    char c = 42; 
    float f = 3.14;
    printf("%d %f\n", c, f); 

    return 0;
}

printf関数の原型はint printf(const char *format, ...);である。コード例では、「, ...」の部分にあたる実引数はcfであるが、これら実引数に既定の実引数拡張が適用されるため、それぞれのもつ型は型int、型doubleに変換される。

なおC90では、fprintf系の関数に%lfという変換指定子は定義されていない。そのような記述は未定義の動作を起こす(変換指定子%lfは、fscanf系の関数では定義されている)。

  • 非原型形式をもつ関数呼出しの実引数に、既定の実引数拡張が適用される。
既定の実引数拡張(未定義の動作を起こすコード)
#include <stdio.h>

void f();
/* (修正方法1)可視の関数宣言を関数原型形式にする */
/* void f(char, float); */

int main(void)
{
    char a = 42; 
    float b = 3.14;
    f(a, b); 

    return 0;
}

void f(char a, float b){}
/* → 実引数に既定の実引数拡張を適用した結果の型と、仮引数で型が一致しないため未定義の動作 */

/* (修正方法2)仮引数の型を、実引数に既定の実引数変換を適用した型に合わせる */
/* void f(int a, double b){} */

/* (修正方法3)関数定義を非原型形式にする */
/* void f(a, b)
char a;
float b;
{}
*/

コード例では、実引数の型(型charおよび型float)に既定の実引数拡張を適用後の型(型intおよび型double)と、関数原型の仮引数型並びで指定された仮引数の型(型charおよび型float)が一致しないため、未定義の動作となる。

以下のいずれかの修正を加えることで、合法コードとなる。

  • 関数宣言を関数原型形式(void f(char, float);)に修正する(修正方法1)。
  • 関数定義の仮引数型並びを(int a, double b)に修正する(修正方法2)。
  • 関数定義を非原型形式に修正する(これにより、関数定義の仮引数にも既定の実引数拡張が適用され、実引数の型と仮引数の型が適合する)(修正方法3)。

return文の式 -> 返却値型への代入変換

  • 関数の返却値型が型void以外の場合、return文の式の結果の型は、返却値型への代入変換が適用される。

演算子

演算子 -> 汎整数拡張

オペランドに汎整数拡張が適用される演算子は、+ - ~ << >>の5種類である。

  • 単項演算子+ - ~のオペランドに対して汎整数拡張が適用される。
単項演算子(+,-,~)による汎整数拡張
#include <stdio.h>

int main(void)
{
    char c = 0;
    printf("%d %d %d %d\n",
           sizeof(c), sizeof(+c), sizeof(-c), sizeof(~c));
    /* 1 4 4 4 */

    return 0;
}

とくに~演算子の結果は、拡張後のビット単位の補数となることに注意。例えば、型unsigned charをもつ式をオペランドにとる場合、汎整数拡張により拡張された上位24ビットに対してとられるビット単位の補数も結果に含まれる。そのため、右ビットシフトが算術シフトである処理系において、ビットシフトでその拡張部分のビットを参照した場合、結果はプログラマーの素朴な直観とは異なるものとなるかもしれない。以下にコード例を示す。

sizeof(int)==4、算術シフトの処理系を仮定
unsigned char uc = 0x7f;
printf("0x%x\n",  ~uc>>4);  /* 結果は0x08ではなく、0xfffffff8 */
  • シフト演算子(<< >>)の各オペランドに汎整数拡張が適用される。
シフト演算子による汎整数拡張
#include <stdio.h>

int main(void)
{
    signed char c = 0;
    signed char shift = 0;
    printf("%d %d %d\n", sizeof(c), sizeof(c<<shift), sizeof(c>>shift));
    /* 1 4 4 */

    return 0;
}

なお、オペランドが汎整数拡張の対象となる型をもつからといって、その式の評価が必ずしも汎整数拡張を伴うわけではないことに注意する。

例えば以下コード例において、コンマ演算子(,)の結果は右オペランドの型(char)および値をもつため、式(c,c)の結果の型は型charである。

コンマ演算子(`,`)
char c=0;
printf("%d %d\n", sizeof(c), sizeof(c,c));  /* 1 1 */

演算子 -> 通常の算術型変換

  • 二項算術演算子(* / % + -)、ビット単位演算子(& ^ |)のオペランドに対して通常の算術型変換が適用される(ポインタオペランドを含む場合は除く)。
  • 関係演算子(< > <= >=)、等価演算子(== !=)の算術型オペランドに、通常の算術型変換が適用される。
  • 条件演算子?:の第2・第3オペランドが算術型の場合、結果の型は、両オペランドに通常の算術型変換を適用後の型となる。

演算子 -> 代入変換

代入変換に関して、詳しくは「単純代入演算子について」を参照下さい。

  • 単純代入演算子(=)の式において、代入変換が適用される。
  • 増分(減分)演算子(++ --)、複合代入演算子(*= /= %= += -= <<= >>= &= ^= |=)の式において、通常の算術型変換、および代入変換が適用される。

演算子 -> 関係演算子および等価演算子の型調整

  • 関係演算子(< > <= >=)、および等価演算子(== !=)がポインタオペランドをもち、一方のオペランドが空ポインタ定数の場合、空ポインタ定数を他方のポインタ型へ変換する。一方のオペランドが型void *の場合、他方のオペランドを型void *へ変換する。

演算子 -> 結果の型に注意すべき、その他の演算子

以下は型変換の話題ではないが、演算子の結果の型についてもまとめておく。

  • &演算子(オペランドの式が型Tをもつとする)の結果の型は、型「型Tへのポインタ」。
  • !演算子の結果の型はint
  • sizeof演算子の結果の型は、型size_t(非負の整数)。
  • ポインタ同士の減算の結果の型は、型ptrdiff_t(符号つき整数)。
  • 条件演算子?:の第2・第3オペランドがポインタの場合、結果の型は、両ポインタオペランドの型(void *、空ポインタ定数、それ以外)の組合せに依存する(詳しくは「条件演算子にvoid *が絡んだ時の結果型のCV修飾について」を参照下さい)。

参考文献

  • 「プログラム言語C JISX3010-1993 (ISO/IEC 9899:1990)」
53
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
53