第2章:型システムの仕様レベル理解

この章で学ぶこと

  • 基本型の厳密な定義
  • 整数昇格(Integer Promotion)
  • 通常の算術型変換(Usual Arithmetic Conversions)
  • 互換型(Compatible Types)
  • 複合型(Composite Types)
  • 型修飾子の意味論
  • ---

    2.1 基本型の分類

    型のカテゴリ

    C言語の型は以下のように分類されます:

    型
    ├── オブジェクト型
    │   ├── 基本型
    │   │   ├── 文字型(char)
    │   │   ├── 符号付き整数型
    │   │   ├── 符号なし整数型
    │   │   ├── 浮動小数点型
    │   │   └── _Bool
    │   ├── 列挙型(enum)
    │   ├── 派生型
    │   │   ├── 配列型
    │   │   ├── 構造体型
    │   │   ├── 共用体型
    │   │   ├── ポインタ型
    │   │   └── 関数型
    │   └── void型(不完全型)
    └── 関数型
    

    整数型の順位(Integer Conversion Rank)

    整数昇格を理解するために、整数型の「順位」の概念が重要です。

    規格 6.3.1.1 より、順位は以下の順序です(高い順):

  • long long int, unsigned long long int
  • long int, unsigned long int
  • int, unsigned int
  • short int, unsigned short int
  • signed char, unsigned char, char
  • _Bool

同じ順位の符号付きと符号なしは、変換で同じ扱いをされます。

標準整数型と拡張整数型

標準符号付き整数型:

  • signed char
  • short int
  • int
  • long int
  • long long int

拡張符号付き整数型(処理系定義):

  • int128_t など

対応する符号なし型 が各々存在します。

---

2.2 整数昇格(Integer Promotion)

規則

整数昇格は、int より低い順位の整数型を、演算前に int または unsigned int に変換する規則です。

規格 6.3.1.1p2:

> If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int.

具体例

char c1 = 100;
char c2 = 100;
int result = c1 + c2;

このとき:

  • c1c2 は整数昇格により int に変換される
  • int + int = int として計算される
  • 結果は 200(int に収まる)

符号付き char と符号なし char

/* char が符号付き8ビットの場合 */
unsigned char uc = 255;
signed char sc = -1;

/* 整数昇格後 */
/* uc → int: 255 */
/* sc → int: -1 */

if (uc == sc)  /* 255 == -1 → false */

陥りやすいバグ

unsigned char a = 200;
unsigned char b = 200;
unsigned char c = a + b;  /* 結果は 144 になる? */

実際の動作:

  • abint に昇格
  • 200 + 200 = 400(int として)
  • 400 を unsigned char に変換 → 400 % 256 = 144
  • 計算自体は int で行われ、オーバーフローは起きていません。

    ---

    2.3 通常の算術型変換

    規則の概要

    二項演算子のオペランドが異なる型の場合、「通常の算術型変換」により共通の型に揃えられます。

    規格 6.3.1.8:

  • まず整数昇格が行われる
  • 以下の順序で型を決定:
- どちらかが long double なら、他方を long double に - どちらかが double なら、他方を double に - どちらかが float なら、他方を float に - 整数型同士の場合は、複雑な規則に従う

整数型同士の変換規則

両オペランドが整数型の場合:

  • 両方が符号付き または 両方が符号なし
順位が低い方を高い方に変換

  • 符号なしの順位 >= 符号付きの順位
符号付きを符号なしに変換

  • 符号付きの順位 > 符号なしの順位
- 符号付き型が符号なし型のすべての値を表現できる → 符号なしを符号付きに - そうでなければ → 両方を符号付き型に対応する符号なし型に

具体例

int si = -1;
unsigned int ui = 1;

if (si < ui)  /* false! -1 が unsigned に変換され UINT_MAX になる */

long sl = -1;
unsigned int ui = 1;

/* long が unsigned int のすべての値を表現できる場合(通常そう) */
if (sl < ui)  /* true: ui が long に変換される */

危険なパターン

size_t size = 5;
int index = -1;

if (index < size) {  /* false! */
    /* 期待通りに動かない */
}

size_t は符号なし、int は符号付き。index が符号なしに変換され、巨大な値になります。

対策

if (index < 0 || (size_t)index < size) {
    /* 安全 */
}

---

2.4 互換型(Compatible Types)

定義

2つの型が「互換」であるとは、同じオブジェクトまたは関数を指定するために交換可能であることを意味します。

規格 6.2.7:

> Two types have compatible type if their types are the same.

互換性の規則

基本型:同一の型は互換

列挙型:基となる整数型と互換

修飾型:修飾されていない版と互換

const int a;
int b;
/* const int と int は互換 */

配列型

  • 要素型が互換
  • 両方がサイズ指定なし、または同じサイズ

int a[10];
int b[];
/* int[10] と int[] は互換 */

関数型

  • 戻り値の型が互換
  • パラメータ数が同じ(または一方が old-style)
  • 対応するパラメータの型が互換

互換性の重要性

互換でない型を通じた同一オブジェクトへのアクセスは、未定義動作です(厳密なエイリアシング規則)。

int i = 42;
float *fp = (float *)&i;
*fp = 3.14f;  /* 未定義動作 */

---

2.5 複合型(Composite Types)

定義

複合型は、2つの互換型から構成される型で、両方の型と互換です。

規格 6.2.7p3:

> A composite type can be constructed from two types that are compatible

配列の複合型

/* 翻訳単位1 */
int arr[];

/* 翻訳単位2 */
int arr[10];

/* 複合型: int[10] */

関数の複合型

/* 宣言1 */
int f(int);

/* 宣言2 */
int f(int n);

/* 複合型: int f(int n) — パラメータ名を含む */

Old-style と Prototype の複合

/* old-style 宣言 */
int g();

/* プロトタイプ */
int g(int, double);

/* 複合型: int g(int, double) */

---

2.6 型修飾子

const

意味:オブジェクトを通じた変更を禁止

const int ci = 10;
ci = 20;  /* 制約違反:診断必須 */

int i = 10;
const int *p = &i;
*p = 20;  /* 制約違反:診断必須 */
i = 20;   /* OK */

volatile

意味:副作用を持つアクセスである可能性。最適化を抑制。

volatile int vi;

while (vi == 0) {  /* 毎回読み込む */
    /* ... */
}

用途:

  • メモリマップドI/O
  • シグナルハンドラとの通信
  • setjmp/longjmp をまたぐ変数
  • restrict(C99以降)

    意味:ポインタが指すオブジェクトへのアクセスは、このポインタを通じてのみ行われる

    void copy(int * restrict dest, const int * restrict src, size_t n)
    {
        for (size_t i = 0; i < n; i++)
            dest[i] = src[i];
    }
    

    コンパイラは destsrc がオーバーラップしないと仮定し、最適化できます。

    restrict 契約の違反は未定義動作です:

    int arr[10];
    copy(arr, arr + 1, 5);  /* 未定義動作:オーバーラップ */
    

    _Atomic(C11以降)

    意味:アトミックアクセスを保証

    _Atomic int counter;
    
    counter++;  /* アトミック */
    

    ---

    2.7 型の互換性と関数

    関数ポインタの互換性

    関数ポインタの型が互換でない場合、呼び出しは未定義動作です。

    void f(int);
    
    void (*fp)(short) = (void (*)(short))f;
    (*fp)(1);  /* 未定義動作 */
    

    可変個引数とデフォルト引数昇格

    プロトタイプなしで呼ばれる関数、または ... に対応する引数には「デフォルト引数昇格」が適用されます。

  • 整数昇格
  • floatdouble
  • void f(int, ...);
    
    f(1, 'a', 3.14f);
    /* 'a' は int に昇格 */
    /* 3.14f は double に昇格 */
    

    va_arg で受け取る型は、昇格後の型でなければなりません:

    char c = va_arg(ap, char);   /* 未定義動作 */
    char c = va_arg(ap, int);    /* OK */
    

    ---

    2.8 実践:型変換のデバッグ

    警告を有効にする

    gcc -Wall -Wextra -Wconversion -Wsign-conversion -pedantic
    

    型変換の可視化

    #include <stdio.h>
    
    #define PRINT_TYPE(x) _Generic((x), \
        char: "char", \
        signed char: "signed char", \
        unsigned char: "unsigned char", \
        short: "short", \
        unsigned short: "unsigned short", \
        int: "int", \
        unsigned int: "unsigned int", \
        long: "long", \
        unsigned long: "unsigned long", \
        long long: "long long", \
        unsigned long long: "unsigned long long", \
        float: "float", \
        double: "double", \
        long double: "long double", \
        default: "other")
    
    int main(void)
    {
        char c = 'A';
        short s = 1;
        int i = 1;
        unsigned int ui = 1;
    
        printf("c + s: %s\n", PRINT_TYPE(c + s));  /* int */
        printf("i + ui: %s\n", PRINT_TYPE(i + ui));  /* unsigned int */
    
        return 0;
    }
    

    ---

    2.9 この章のまとめ

    学んだこと

  • 整数型の順位: _Bool < char < short < int < long < long long
  • 整数昇格: int より低い順位は int または unsigned int
  • 通常の算術型変換: 異なる型を共通の型に揃える
  • 互換型: 交換可能な型の概念
  • 複合型: 2つの互換型から構成される型
  • 型修飾子: const, volatile, restrict, _Atomic
  • 重要な注意点

  • 符号付きと符号なしの混合演算に注意
  • size_tint の比較は危険
  • 整数昇格は暗黙的に行われる
  • restrict の契約違反は未定義動作

次の章の予告

次章では、オブジェクト、値、表現を学びます。オブジェクトの寿命、有効型、値の表現について規格に基づいて解説します。

---

確認問題

問題1

以下のコードで、c1 + c2 の型は何ですか?

char c1 = 'A', c2 = 'B';

解答

int

char は整数昇格により int に変換され、int + int = int

問題2

以下のコードの出力は?

int si = -1;
unsigned int ui = 1;
printf("%d\n", si < ui);

解答

0(false)

siunsigned int に変換され、UINT_MAX になる。UINT_MAX < 1 は false。

問題3

restrict 修飾子の意味と目的は?

解答

restrict はポインタ修飾子で、そのポインタが指すオブジェクトへのすべてのアクセスは、そのポインタ(またはそれに基づくポインタ)を通じてのみ行われることを示す。

目的はコンパイラの最適化を可能にすること。エイリアシングがないことを保証する。

問題4

va_arg(ap, char) が未定義動作である理由は?

解答

可変個引数にはデフォルト引数昇格が適用され、charint に昇格される。va_arg で指定する型は昇格後の型でなければならないため、va_arg(ap, int) とすべき。

---

次の章では、オブジェクト、値、表現について学びます。