第2章: 型システムと変数

2.1 型システムの理論

型とは何か

型(Type)とは、データの解釈方法と許可される操作を定義する分類である。

型システムの目的(Cardelli & Wegner, 1985 "On Understanding Types"):

  • 抽象化: 実装の詳細を隠蔽
  • エラー検出: 不正な操作のコンパイル時検出
  • ドキュメンテーション: プログラマへの意図の伝達
  • 最適化: コンパイラによる効率的なコード生成

強い型付け vs 弱い型付け

強い型付け(Strong Typing):

  • 暗黙の型変換を制限
  • 型エラーを厳密に検出
  • 例: Haskell, Rust, Python

弱い型付け(Weak Typing):

  • 暗黙の型変換を許容
  • プログラマの意図を信頼
  • 例: C, JavaScript

C言語は「弱い静的型付け」言語である:

int i = 42;
float f = i;      /* 暗黙の変換(int → float) */
int *p = (int *)f; /* 危険な変換も可能 */

静的型付け vs 動的型付け

静的型付け(Static Typing):

  • コンパイル時に型をチェック
  • 実行前にエラーを検出
  • 例: C, C++, Java, Rust

動的型付け(Dynamic Typing):

  • 実行時に型をチェック
  • 柔軟だがエラーは実行時に発生
  • 例: Python, Ruby, JavaScript

/* C言語: 静的型付け */
int x = 42;
x = "hello";  /* コンパイルエラー */

# Python: 動的型付け
x = 42
x = "hello"  # OK、実行時に型が変わる

2.2 基本型

整数型

C言語の整数型は、サイズが実装依存である。これは移植性の問題を引き起こす。

標準整数型:

| 型 | 最小サイズ | 典型的サイズ(64bit) | |---|---|---| | char | 1バイト | 1バイト | | short | 2バイト | 2バイト | | int | 2バイト | 4バイト | | long | 4バイト | 8バイト(LP64)/ 4バイト(LLP64) | | long long | 8バイト | 8バイト |

データモデル:

データモデル(ILP32, LP64, LLP64):

           char  short  int   long  long long  pointer
ILP32       1     2      4     4      8          4
LP64        1     2      4     8      8          8
LLP64       1     2      4     4      8          8

  • ILP32: 32bitシステム(Linux, Windows)
  • LP64: 64bit Unix/Linux
  • LLP64: 64bit Windows

固定幅整数型(C99):

#include <stdint.h>

int8_t   a = -128;        /* 正確に8ビット */
int16_t  b = -32768;      /* 正確に16ビット */
int32_t  c = -2147483648; /* 正確に32ビット */
int64_t  d = -9223372036854775808LL;

uint8_t  e = 255;         /* 符号なし8ビット */
uint16_t f = 65535;       /* 符号なし16ビット */
uint32_t g = 4294967295U; /* 符号なし32ビット */
uint64_t h = 18446744073709551615ULL;

最小幅・最速幅整数型:

int_least32_t x;  /* 少なくとも32ビット */
int_fast32_t y;   /* 少なくとも32ビットで最速 */

符号付きと符号なし

2の補数表現:

8ビット符号付き整数(int8_t):
 127 = 0111 1111
   1 = 0000 0001
   0 = 0000 0000
  -1 = 1111 1111
-128 = 1000 0000

最上位ビット(MSB)が符号を示す

オーバーフロー動作:

/* 符号付きオーバーフロー: 未定義動作 */
int x = INT_MAX;
x = x + 1;  /* 未定義動作! */

/* 符号なしオーバーフロー: 定義済み(ラップアラウンド) */
unsigned int y = UINT_MAX;
y = y + 1;  /* 必ず0になる */

浮動小数点型

IEEE 754規格(1985年)に基づく浮動小数点表現:

| 型 | サイズ | 有効桁数 | 指数範囲 | |---|---|---|---| | float | 4バイト | 約7桁 | 10^-38 〜 10^38 | | double | 8バイト | 約15桁 | 10^-308 〜 10^308 | | long double | 8〜16バイト | 実装依存 | 実装依存 |

IEEE 754 単精度(float)の構造:

符号(1) 指数部(8)   仮数部(23)
[S][EEEEEEEE][MMMMMMMMMMMMMMMMMMMMMMM]

値 = (-1)^S × 1.M × 2^(E-127)

例: 3.14159 ≈ 0 10000000 10010010000111111011011

浮動小数点の注意点:

/* 等値比較は危険 */
float a = 0.1f;
float b = 0.1f;
if (a == b) { }  /* 危険! */

/* イプシロン比較を使用 */
#include <math.h>
#include <float.h>
if (fabs(a - b) < FLT_EPSILON) { }

/* 累積誤差 */
float sum = 0.0f;
for (int i = 0; i < 1000000; i++) {
    sum += 0.1f;  /* 誤差が累積 */
}
/* sum != 100000.0 */

文字型

char型の特殊性:

char c = 'A';  /* 65(ASCII) */

/* charの符号は実装依存 */
char x = -1;  /* signed charかunsigned charか不明 */

/* 明示的に指定 */
signed char   sc = -1;    /* -1 */
unsigned char uc = -1;    /* 255 */

文字エンコーディング:

/* ASCII(0-127) */
char a = 'A';  /* 65 */
char z = 'Z';  /* 90 */

/* UTF-8マルチバイト */
char utf8[] = "あ";  /* {0xE3, 0x81, 0x82, 0x00} */

/* ワイド文字(C99) */
#include <wchar.h>
wchar_t w = L'あ';

論理型(C99)

#include <stdbool.h>

bool flag = true;
bool empty = false;

/* 内部的にはintとの互換性 */
_Bool b = 42;  /* 1に変換される */

2.3 変数とスコープ

記憶域クラス

C言語には4つの記憶域クラス(Storage Class)がある:

auto:

void func(void) {
    auto int x = 42;  /* デフォルト、省略可能 */
    int y = 42;       /* 同じ */
}
/* スタックに配置、関数終了時に破棄 */

static:

void counter(void) {
    static int count = 0;  /* 静的記憶域 */
    count++;
    printf("%d\n", count);
}
/* 初回呼び出し時のみ初期化 */
/* プログラム終了まで存続 */

register:

void loop(void) {
    register int i;  /* レジスタへの配置を推奨 */
    for (i = 0; i < 1000000; i++) { }
}
/* コンパイラへのヒント(無視される可能性あり) */
/* アドレス取得不可(&i はエラー) */

extern:

/* file1.c */
int global_var = 42;

/* file2.c */
extern int global_var;  /* 他ファイルの変数を参照 */

スコープ

ブロックスコープ:

void func(void) {
    int x = 1;
    {
        int x = 2;  /* 別の変数(シャドウイング) */
        printf("%d\n", x);  /* 2 */
    }
    printf("%d\n", x);  /* 1 */
}

ファイルスコープ:

static int file_private = 42;  /* このファイル内でのみ可視 */
int file_public = 42;          /* 他ファイルからも可視 */

関数プロトタイプスコープ:

void func(int n, int arr[n]);  /* nは配列サイズ */

リンケージ

外部リンケージ:

int global = 42;  /* 他の翻訳単位から参照可能 */
void public_func(void) { }

内部リンケージ:

static int private = 42;  /* この翻訳単位内のみ */
static void private_func(void) { }

リンケージなし:

void func(void) {
    int local = 42;  /* この関数内のみ */
}

2.4 型修飾子

const

constは「読み取り専用」を示す:

const int MAX = 100;  /* 変更不可 */
MAX = 200;  /* コンパイルエラー */

/* ポインタとconst */
const int *p1;        /* pが指す値を変更不可 */
int * const p2;       /* p自体を変更不可 */
const int * const p3; /* 両方変更不可 */

/* 覚え方: constは左にあるものに適用 */

volatile

volatileは「最適化を抑制」する:

/* ハードウェアレジスタ */
volatile int *hardware_reg = (int *)0x40000000;
while (*hardware_reg == 0) { }  /* 最適化されない */

/* シグナルハンドラとの共有 */
volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1;  /* 別のコンテキストから変更 */
}

int main(void) {
    while (flag == 0) { }  /* 最適化されない */
}

restrict(C99)

restrictは「このポインタだけがこのメモリにアクセスする」ことを示す:

/* restrictなし */
void copy(int *dest, int *src, int n) {
    for (int i = 0; i < n; i++)
        dest[i] = src[i];
}
/* destとsrcが重なる可能性を考慮 */

/* restrictあり */
void copy_fast(int * restrict dest, int * restrict src, int n) {
    for (int i = 0; i < n; i++)
        dest[i] = src[i];
}
/* 重なりがないと仮定して最適化可能 */

_Atomic(C11)

_Atomicはアトミック操作を保証する:

#include <stdatomic.h>

_Atomic int counter = 0;

void increment(void) {
    atomic_fetch_add(&counter, 1);  /* スレッドセーフ */
}

2.5 型変換

暗黙の型変換

整数昇格(Integer Promotion):

char a = 10;
char b = 20;
int c = a + b;  /* a, bはintに昇格してから加算 */

通常の算術変換(Usual Arithmetic Conversions):

int i = 10;
double d = 3.14;
double result = i + d;  /* iがdoubleに変換 */

/* 変換の優先順位 */
/* long double > double > float > unsigned long long > ... > int */

明示的型変換(キャスト)

int x = 10;
int y = 3;

/* 整数除算 */
int result1 = x / y;        /* 3 */

/* 浮動小数点除算 */
double result2 = (double)x / y;  /* 3.333... */

/* ポインタキャスト */
void *vp = &x;
int *ip = (int *)vp;

/* 危険なキャスト(未定義動作の可能性) */
float f = 3.14f;
int *bad = (int *)&f;  /* 厳密なエイリアシング違反 */

厳密なエイリアシング

/* 厳密なエイリアシング規則:
   異なる型のポインタを通じて同じメモリにアクセスしてはならない
   (例外: char *, void *, 同じ構造体/共用体) */

float f = 3.14f;
int *ip = (int *)&f;
printf("%d\n", *ip);  /* 未定義動作 */

/* 正しい方法: 共用体を使用 */
union {
    float f;
    int i;
} u;
u.f = 3.14f;
printf("%d\n", u.i);  /* 型パニング */

2.6 sizeof演算子

sizeofはコンパイル時に型のサイズを返す:

/* 型に対するsizeof */
printf("%zu\n", sizeof(int));    /* 4(通常) */
printf("%zu\n", sizeof(char));   /* 必ず1 */

/* 式に対するsizeof */
int arr[10];
printf("%zu\n", sizeof(arr));    /* 40(通常) */
printf("%zu\n", sizeof(arr[0])); /* 4 */

/* 配列要素数の計算 */
size_t n = sizeof(arr) / sizeof(arr[0]);  /* 10 */

VLAとsizeof(C99):

void func(int n) {
    int arr[n];
    printf("%zu\n", sizeof(arr));  /* 実行時に評価 */
}

2.7 まとめ

本章では、C言語の型システムと変数について学んだ:

  • 型システム理論: 弱い静的型付け
  • 基本型: 整数型、浮動小数点型、文字型
  • 固定幅整数: stdint.hの重要性
  • 記憶域クラス: auto, static, register, extern
  • スコープとリンケージ: 可視性の制御
  • 型修飾子: const, volatile, restrict, _Atomic
  • 型変換: 暗黙/明示的変換、エイリアシング
  • 次章では、演算子と式について学ぶ。

    ---

    参考文献

  • Cardelli, L., & Wegner, P. (1985). "On Understanding Types, Data Abstraction, and Polymorphism", Computing Surveys, 17(4)
  • IEEE (1985). "IEEE 754-1985: Standard for Binary Floating-Point Arithmetic"
  • ISO/IEC 9899:2011, Programming languages — C
  • Harbison, S. P., & Steele, G. L. (2002). "C: A Reference Manual", 5th Edition, Prentice Hall
  • King, K. N. (2008). "C Programming: A Modern Approach", 2nd Edition, W. W. Norton