web-dev-qa-db-ja.com

暗黙の型昇格ルール

この投稿は、FAQ)として使用することを意図しています。

例1)
なぜこれは255ではなく、奇妙な大きな整数を与えるのですか?

unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y); 

例2)
なぜ「-1が0よりも大きい」のでしょうか?

unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
  puts("-1 is larger than 0");

例3)
上記の例のタイプをshortに変更すると問題が解決するのはなぜですか?

unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
  puts("-1 is larger than 0"); // will not print

(これらの例は、16ビットの短い32ビットまたは64ビットのコンピューターを対象としています。)

45
Lundin

Cは、式で使用されるオペランドの整数型を暗黙的かつ暗黙的に変更するように設計されました。言語がコンパイラにオペランドをより大きな型に変更するか、符号を変更することを強制する場合がいくつかあります。

この背後にある理論的根拠は、算術中の偶発的なオーバーフローを防ぐだけでなく、異なる符号付きのオペランドが同じ式に共存できるようにすることです。

残念なことに、暗黙の型昇格の規則は、C言語の最大の欠陥の1つになる可能性があるという点で、善よりもはるかに害をもたらします。これらのルールは、平均的なCプログラマーによっても知られていないことが多く、したがって、あらゆる種類の非常に微妙なバグを引き起こしています。

通常、プログラマは「xにキャストするだけで機能する」というシナリオを見ますが、その理由はわかりません。または、このようなバグは、一見単純で単純なコード内から発生するまれで断続的な現象として現れます。暗黙の昇格は、ビット操作を行うコードでは特に面倒です。なぜなら、Cのほとんどのビット単位演算子は、符号付きオペランドを指定した場合の動作の定義が不十分だからです。


整数型と変換ランク

Cの整数型はcharshortintlong、_long long_およびenumです。
__Bool_/boolは、型の昇格に関しては整数型としても扱われます。

すべての整数には、指定された変換ランクがあります。 C11 6.3.1.1、最も重要な部分の強調:

すべての整数型には、次のように定義された整数変換ランクがあります。
— 2つの符号付き整数型は、同じ表現であっても、同じランクを持つことはできません。
—符号付き整数型のランクは、精度の低い符号付き整数型のランクよりも大きくなければなりません。
— _long long int_のランクは_long int_のランクより大きく、intのランクより大きくなければなりません。 _short int_のランクは、_signed char_のランクより大きくなければなりません。
—符号なし整数型のランクは、対応する符号付き整数型のランクがあればそれと等しくなります。

—標準整数型のランクは、同じ幅の拡張整数型のランクよりも大きくなければなりません。
— charのランクは、signed charおよびunsigned charのランクと等しくなります。
— _Boolのランクは、他のすべての標準整数型のランクよりも低くなければなりません。
—列挙型のランクは、互換性のある整数型のランクと等しくなければなりません(6.7.2.2を参照)。

_stdint.h_の型もここでソートされ、特定のシステムで対応する型と同じランクになります。たとえば、32ビットシステムでは、_int32_t_のランクはintと同じです。

さらに、C11 6.3.1.1は、小整数型と見なされる型を指定します(正式な用語ではありません):

intまたは_unsigned int_を使用できる場合は、次の式を式で使用できます。

—整数変換ランクがintおよび_unsigned int_のランク以下である整数型(intまたは_unsigned int_以外)のオブジェクトまたは式。

このやや不可解なテキストが実際に意味するものは、__Bool_、charおよびshort(および_int8_t_、_uint8_t_など)が「小さな整数型」であることです。 「。以下に説明するように、これらは特別な方法で扱われ、暗黙の昇格の対象となります。


整数プロモーション

式で小さな整数型が使用される場合は常に、常に符号付きのintに暗黙的に変換されます。これは、整数プロモーションまたは整数プロモーションルールとして知られています。

正式には、ルールには(C11 6.3.1.1)とあります:

intが元の型のすべての値を表すことができる場合(ビットフィールドの幅によって制限される場合)、値はintに変換されます。それ以外の場合は、_unsigned int_に変換されます。これらは、整数プロモーションと呼ばれます。

これは、ほとんどの式で使用される場合、符号の有無に関係なく、すべての小さな整数型が(符号付き)intに暗黙的に変換されることを意味します。

このテキストは、「すべての小さな符号付き整数型は符号付き整数に変換され、すべての小さな符号なし整数型は符号なし整数に変換される」と誤解されることがよくあります。これは間違っています。ここでの符号なし部分は、たとえば_unsigned short_オペランドがあり、intが特定のシステムでshortと同じサイズになっている場合にのみ、_unsigned short_オペランドは_unsigned int_に変換されます。のように、注目すべきことは何も起こりません。ただし、shortintよりも小さい型である場合、それは常に(符号付き)intに変換されます。符号なし

整数の昇格によって引き起こされる厳しい現実は、charshortのような小さな型ではCの操作をほとんど実行できないことを意味します。操作は常にint以上のタイプで実行されます。

これはナンセンスに聞こえるかもしれませんが、幸いなことに、コンパイラはコードを最適化できます。たとえば、2つの_unsigned char_オペランドを含む式では、オペランドがintに昇格され、演算はintとして実行されます。ただし、コンパイラーは、予想されるように、実際に8ビット操作として実行されるように式を最適化できます。ただし、ここで問題が発生します。コンパイラーはnot整数の昇格によって引き起こされる符号の暗黙の変更を最適化することを許可されています。なぜなら、プログラマが意図的に暗黙の昇格に依存しているのか、それが意図的でないのかをコンパイラが判断する方法がないからです。

これが、質問の例1が失敗する理由です。両方のunsigned charオペランドはint型に昇格され、演算はint型で実行され、_x - y_の結果はint型になります。予想される_-1_の代わりに_255_を取得することを意味します。コンパイラは、intの代わりに8ビット命令でコードを実行するマシンコードを生成できますが、符号付きの変化を最適化しない場合があります。つまり、負の結果になり、_printf("%u_が呼び出されたときに、奇妙な数値になります。例1は、操作の結果をタイプ_unsigned char_にキャストすることで修正できます。

_++_やsizeof演算子のようないくつかの特別な場合を除き、整数の昇格は、単項演算子、二項(または三項)演算子が使用されているかどうかにかかわらず、Cのほとんどすべての操作に適用されます。


通常の算術変換

Cで2項演算(2つのオペランドを持つ演算)を実行するときは常に、演算子の両方のオペランドが同じ型である必要があります。したがって、オペランドのタイプが異なる場合、Cは一方のオペランドを他方のオペランドのタイプに暗黙的に変換します。これがどのように行われるかについてのルールは、通常のアーティマティック変換と呼ばれます(非公式には「バランス」と呼ばれます)。これらは、C11 6.3.18で指定されています。

(このルールは、長いネストされた_if-else if_ステートメントと考えてください。読みやすいかもしれません:))

6.3.1.8通常の算術変換

算術型のオペランドを期待する多くの演算子は、変換を引き起こし、同様の方法で結果の型を生成します。目的は、オペランドと結果の共通の実数型を決定することです。指定されたオペランドについて、各オペランドは、型ドメインを変更せずに、対応する実型が共通実型である型に変換されます。特に明記しない限り、共通の実数型は結果の対応する実数型でもあり、その型ドメインは同じ場合はオペランドの型ドメインであり、そうでない場合は複素数です。このパターンは通常の算術変換と呼ばれます:

  • まず、いずれかのオペランドの対応する実数型が_long double_である場合、もう一方のオペランドは、型ドメインを変更することなく、対応する実数型が_long double_である型に変換されます。
  • それ以外の場合、いずれかのオペランドの対応する実数型がdoubleである場合、他のオペランドは、型ドメインを変更することなく、対応する実数型がdoubleである型に変換されます。
  • それ以外の場合、いずれかのオペランドの対応する実数型がfloatである場合、他のオペランドは、型ドメインを変更することなく、対応する実数型がfloatの型に変換されます。
  • それ以外の場合、整数プロモーションは両方のオペランドで実行されます。次に、昇格されたオペランドに次のルールが適用されます。

    • 両方のオペランドの型が同じ場合、それ以上の変換は不要です。
    • それ以外の場合、両方のオペランドに符号付き整数型があるか、両方に符号なし整数型がある場合、整数変換ランクが小さい型のオペランドは、ランクが大きいオペランドの型に変換されます。
    • それ以外の場合、符号なし整数型のオペランドのランクが他のオペランドの型のランク以上である場合、符号付き整数型のオペランドは符号なし整数型のオペランドの型に変換されます。
    • そうでない場合、符号付き整数型のオペランドの型が符号なし整数型のオペランドの型のすべての値を表すことができる場合、符号なし整数型のオペランドは符号付き整数型のオペランドの型に変換されます。
    • それ以外の場合、両方のオペランドは、符号付き整数型のオペランドの型に対応する符号なし整数型に変換されます。

ここで注目すべきは、通常の算術変換が浮動小数点変数と整数変数の両方に適用されることです。整数の場合、整数プロモーションは通常の算術変換内から呼び出されることにも注意できます。その後、両方のオペランドのランクが少なくともintである場合、演算子は同じ符号付きで同じ型にバランスが取られます。

これが、例2の_a + b_が奇妙な結果を与える理由です。両方のオペランドは整数であり、少なくともランクintであるため、整数のプロモーションは適用されません。オペランドは同じタイプではありません-aは_unsigned int_であり、bは_signed int_です。したがって、演算子bは一時的に_unsigned int_型に変換されます。この変換中に、符号情報が失われ、最終的に大きな値になります。

例3で型をshortに変更すると問題が解決する理由は、shortが小さな整数型だからです。両方のオペランドが、符号付きのint型に昇格された整数であることを意味します。整数の昇格後、両方のオペランドは同じ型(int)になり、それ以上の変換は不要です。そして、期待どおりに操作を署名付きタイプで実行できます。

55
Lundin

前回の投稿によると、各例についてさらに情報を提供したいと思います。

例1)

int main(){
    unsigned char x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Unsigned charはintよりも小さいため、整数プロモーションを適用し、(int)x-(int)y =(int)(-1)とunsigned int(-1)= 4294967295を取得します。

上記のコードの出力:(予想と同じ)

4294967295
-1

修正方法は?

以前の投稿が推奨することを試しましたが、実際には機能しません。前回の投稿に基づいたコードは次のとおりです。

そのうちの1つをunsigned intに変更します

int main(){
    unsigned int x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

Xはすでに符号なし整数であるため、整数のプロモーションのみをyに適用します。次に(unsigned int)x-(int)yを取得します。まだ同じ型ではないため、通常の算術変換を適用し、(unsigned int)x-(unsigned int)y = 4294967295を取得します。

上記のコードからの出力:(予想と同じ):

4294967295
-1

同様に、次のコードでも同じ結果が得られます。

int main(){
    unsigned char x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

両方をunsigned intに変更します

int main(){
    unsigned int x = 0;
    unsigned int y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
}

両方とも符号なし整数であるため、整数の昇格は必要ありません。通常の算術変換(同じ型を持つ)、(unsigned int)x-(unsigned int)y = 4294967295。

上記のコードからの出力:(予想と同じ):

4294967295
-1

コードを修正する可能な方法の1つ:(最後に型キャストを追加)

int main(){
    unsigned char x = 0;
    unsigned char y = 1;
    printf("%u\n", x - y); 
    printf("%d\n", x - y);
    unsigned char z = x-y;
    printf("%u\n", z);
}

上記のコードからの出力:

4294967295
-1
255

例2)

int main(){
    unsigned int a = 1;
    signed int b = -2;
    if(a + b > 0)
        puts("-1 is larger than 0");
        printf("%u\n", a+b);
}

どちらも整数であるため、整数の昇格は必要ありません。通常の算術変換により、(unsigned int)a +(unsigned int)b = 1 + 4294967294 = 4294967295が得られます。

上記のコードの出力:(予想と同じ)

-1 is larger than 0
4294967295

修正方法は?

int main(){
    unsigned int a = 1;
    signed int b = -2;
    signed int c = a+b;
    if(c < 0)
        puts("-1 is smaller than 0");
        printf("%d\n", c);
}

上記のコードからの出力:

-1 is smaller than 0
-1

例3)

int main(){
    unsigned short a = 1;
    signed short b = -2;
    if(a + b < 0)
        puts("-1 is smaller than 0");
        printf("%d\n", a+b);
}

最後の例では、整数の昇格によりaとbが両方ともintに変換されるため、問題が修正されました。

上記のコードからの出力:

-1 is smaller than 0
-1

いくつかの概念が混同された場合は、お知らせください。ありがとう〜

1
Lusha Li