C言語を学ぶ上で最も重要なのは、抽象的な文法ではなく「メモリと値の関係性」を意識した思考習慣です。以下に、数学的直感を基にしながら、実行時の挙動を正確に予測できるようになるための要点を再構成します。
1. 関数:入力と出力の明示的なマッピング
関数は「入力値から出力値を一意に導くルール」であり、数学の写像と同一視できます。Cでは型宣言がこの対応関係を厳密に保証します。
// 線形関数:f(x) = 3x − 8
int compute_linear(int input) {
return 3 * input - 8;
}
// 条件付き関数:g(x, y) = { f(x)+y (x>0), f(x−y+5) (x≤0) }
int compute_conditional(int x, int y) {
if (x > 0) {
return compute_linear(x) + y;
} else {
return compute_linear(x - y + 5);
}
}
// 再帰的フィボナッチ(n ≥ 0)
int fibonacci(int n) {
if (n <= 1) return 1; // 基底条件
return fibonacci(n - 1) + fibonacci(n - 2);
}
再帰呼び出しはスタック上に独立したフレームを積み重ねるため、各呼び出しのローカル変数は干渉しません。終了条件に達すると、スタックを巻き戻すように値が伝播します。
2. 基本データ型:サイズと解釈の分離
型とは「メモリ上のビット列をどう解釈するか」を定義する仕様です。同じ32ビットでも int と float では意味が異なります。
#include <stdio.h>
int main(void) {
short s_val = 89; // 16-bit 整数
long l_val = 345L; // 64-bit 整数(L接尾辞でリテラルを明示)
float f_val = 15635030.56F; // 32-bit 浮動小数点(F接尾辞推奨)
double d_val = 35.546; // 64-bit 浮動小数点
char c_sym = '@'; // 8-bit 文字(ASCII値として格納)
int ascii_code = '#'; // char を int で受け取るとASCII値が得られる
printf("ASCII of '#' is %d\n", ascii_code); // 出力: 35
return 0;
}
float の有効数字は約6~7桁であり、絶対値が大きいほど相対誤差が顕著になります。精度比較は「ビット数」ではなく「表現可能な値の分布」で考えるべきです。
3. 構造体:メモリ領域のカスタムレイアウト
構造体は複数のメンバーを単一の論理単位として束ね、そのメモリ配置をプログラマが制御できる型です。
#include <stdio.h>
typedef struct {
int id;
char flag;
} UserRecord;
typedef struct {
int a;
int b;
float c;
} DataBlock;
int main(void) {
UserRecord record1; // スタック上に sizeof(UserRecord) バイト確保
struct UserRecord record2; // 同じ型の別表記
DataBlock block;
// 注意:構造体メンバーは初期化されず、未定義値(ガーベジ)を含む
printf("record1.id = %d, record1.flag = %d\n", record1.id, record1.flag);
record1.id = 100;
record1.flag = 'Y';
printf("After init: %d, %c\n", record1.id, record1.flag);
return 0;
}
構造体変数の宣言時にメモリが確保され、そのサイズはメンバーの合計+パディング(アライメント調整)で決まります。初期値はゼロでないことに注意が必要です。
4. ポインタ:アドレス操作の本質
4.1 基本的な整数ポインタ
ポインタは「他の変数のアドレスを保持する変数」であり、間接参照(*)によって値にアクセスします。
int main(void) {
int target = 123;
int *ptr_to_target = ⌖ // アドレス取得演算子 &
*ptr_to_target = 456; // 間接代入 → target が 456 に変わる
printf("target = %d\n", target); // 456
return 0;
}
不正なアドレス(例:ptr = 44;)への代入は未定義動作であり、実行時エラーまたはセグフォールトを引き起こします。
4.2 構造体ポインタとドット/アロー演算子
構造体ポインタでは -> 演算子が (*ptr).member の省略形です。
typedef struct {
int code;
char status;
} Device;
int main(void) {
Device device_a;
Device *dev_ptr = &device_a;
device_a.code = 777;
dev_ptr->status = 'R'; // (*dev_ptr).status と同じ
printf("code=%d, status=%c\n", device_a.code, device_a.status);
return 0;
}
4.3 ポインタ型の別名定義
typedef を用いてポインタ型を直接定義できます。
typedef struct {
int x;
char y;
} Point, *PointPtr; // PointPtr は "Point*" の別名
int main(void) {
PointPtr p1 = malloc(sizeof(Point)); // malloc は void* を返す
if (p1 != NULL) {
p1->x = 10;
p1->y = 'Z';
free(p1);
}
return 0;
}
4.4 ダイナミックメモリ確保
malloc() はヒープ上に要求バイト数の連続領域を確保し、その先頭アドレスを返します。型キャストはC11以降必須ではありませんが、明示性のために推奨されます。
#include <stdlib.h>
int main(void) {
int *dynamic_array = (int*)malloc(10 * sizeof(int)); // 10要素分
if (dynamic_array == NULL) return 1;
for (int i = 0; i < 10; ++i) {
dynamic_array[i] = i * i;
}
free(dynamic_array); // 必ず解放
return 0;
}
4.5 ポインタと配列の等価性と違い
配列名は「その先頭要素のアドレス」として振る舞いますが、配列自体は固定アドレスのラベルであり、再代入できません。
int main(void) {
int static_arr[] = {1, 2, 3, 4};
int *ptr = static_arr; // OK: 配列名 → 先頭アドレス
// static_arr = ptr; // NG: 配列名は左辺値ではない
ptr[0] = 99; // static_arr[0] を変更
printf("%d\n", static_arr[0]); // 99
// ポインタ配列:各要素が別のアドレスを指す
int *ptr_array[3];
ptr_array[0] = &static_arr[0];
ptr_array[1] = malloc(sizeof(int));
ptr_array[2] = NULL;
free(ptr_array[1]);
return 0;
}
5. 補足:関数引数と型の振る舞い
- 関数の仮引数はすべてローカル変数であり、スコープと寿命はその関数内に限定されます。
- 配列を引数に渡すと、自動的にポインタにデカイズされます(
int arr[5]はint *arrと等価)。 - そのため、配列長を知るには明示的なサイズ引数が必要です:
void process(int data[], size_t len)。 - 自己参照構造体では、メンバは必ずポインタでなければならず、
sizeofが有限になります:struct Node { int val; struct Node *next; };。