C言語 チートシート

cheatsheetC言語

よく使うC言語の構文や関数を目的別にまとめたクイックリファレンス

1. 基本入出力 (stdio.h) ⌨️

コンソールへの出力や、コンソールからの入力を扱う基本的な関数群です。

目的 関数/書式 説明 コード例
標準出力への書式付き出力 printf() 指定した書式に従って、文字列や変数の値を標準出力(通常は画面)に出力します。改行は \n を含める必要があります。
int age = 25;
printf("私の名前は%sです。\n", "太郎");
printf("年齢は%d歳です。\n", age);
printf("円周率は約%.2fです。\n", 3.14159);
標準入力からの書式付き入力 scanf() 指定した書式に従って、標準入力(通常はキーボード)からデータを受け取り、変数に格納します。アドレス演算子 & が必要です。バッファオーバーフローに注意が必要です。
int num;
char name[50];
printf("整数を入力してください: ");
scanf("%d", #); // 整数を読み込む
printf("名前を入力してください: ");
scanf("%49s", name); // 文字列を読み込む (バッファサイズ制限)
1文字を標準出力へ出力 putchar() 引数で指定された1文字を標準出力に出力します。
putchar('A');
putchar('\n'); // 改行を出力
1文字を標準入力から入力 getchar() 標準入力から1文字を読み込み、その文字のASCIIコードを返します。入力がない場合やエラー時はEOFを返します。
int ch;
printf("文字を入力してください: ");
ch = getchar();
printf("入力された文字: ");
putchar(ch);
putchar('\n');
文字列を標準出力へ出力 puts() 引数で指定された文字列を標準出力に出力し、自動的に改行を追加します。
puts("Hello, World!"); // 自動で改行される
puts("これは2行目です。");
文字列を標準入力から入力 fgets() 標準入力から指定した最大文字数までの文字列(改行文字を含む)を読み込み、指定した文字配列に格納します。gets()より安全です。
char line[100];
printf("一行入力してください: ");
fgets(line, sizeof(line), stdin);
printf("入力された行: %s", line); // fgetsは改行も含む
標準エラー出力への書式付き出力 fprintf(stderr, ...) printf と同様の書式で、標準エラー出力 (stderr) へ出力します。エラーメッセージの表示によく使われます。
fprintf(stderr, "エラー: ファイルが開けません。\n");

2. データ型と変数操作 🔢

C言語で使用できる基本的なデータ型と、変数に関連する操作です。

基本データ型

型名 説明 サイズ例 (環境依存) 値の範囲例 (環境依存)
char 文字または小さな整数 1バイト -128 ~ 127 または 0 ~ 255
int 整数 2バイト or 4バイト -32,768 ~ 32,767 または -2,147,483,648 ~ 2,147,483,647
short 短精度整数 (short int) 2バイト -32,768 ~ 32,767
long 長精度整数 (long int) 4バイト or 8バイト -2,147,483,648 ~ 2,147,483,647 またはそれ以上
long long さらに長精度の整数 (C99以降) 8バイト -(2^63) ~ (2^63)-1
float 単精度浮動小数点数 4バイト 約 ±3.4e +/- 38 (有効桁数6桁程度)
double 倍精度浮動小数点数 8バイト 約 ±1.7e +/- 308 (有効桁数15桁程度)
long double 拡張倍精度浮動小数点数 10バイト, 12バイト, 16バイトなど double より高精度・広範囲
void 型がないことを示す (関数の戻り値、ポインタなど) N/A N/A

修飾子

  • signed: 符号付き (デフォルトで省略されることが多い)
  • unsigned: 符号なし (非負の整数のみを扱う)
  • const: 定数 (値を変更できない変数)
  • volatile: 最適化を抑制 (変数値が予期せず変更される可能性がある場合)
  • static: 静的変数 (関数内: 寿命がプログラム終了まで / ファイルスコープ: 外部参照不可)
  • extern: 外部変数 (他のファイルで定義されたグローバル変数を参照)
  • register: レジスタ変数 (高速アクセスを試みるが、コンパイラ判断)
  • typedef: 既存の型に別名をつける
typedef unsigned long long ull; // unsigned long long に ull という別名
ull large_number = 123456789012345ULL;

const float PI = 3.14159f;
// PI = 3.14; // エラー: const変数は変更不可

static int counter = 0; // 静的変数

型変換 (キャスト)

あるデータ型の値を別のデータ型に変換します。

  • 暗黙的型変換: コンパイラが自動で行う (例: intdouble に代入)
  • 明示的型変換 (キャスト): プログラマが型を指定して変換
int i = 10;
double d = 3.14;
double result_implicit = i + d; // iがdoubleに暗黙的に変換される (10.0 + 3.14)

int i_explicit = (int)d; // dをintに明示的にキャスト (小数点以下切り捨て) -> i_explicitは3になる

float f = 5.0f / 2; // 暗黙変換で 5.0f / 2.0f となり、結果は2.5f
int j = 5 / 2;      // 整数同士の除算なので、結果は2
float f_cast = (float)5 / 2; // 5をfloatにキャスト -> 5.0f / 2 -> 結果は2.5f

3. 演算子 ✨

計算や比較、論理演算などを行うための記号です。

算術演算子

+ (加算), - (減算), * (乗算), / (除算), % (剰余)

int a = 10, b = 3;
int sum = a + b; // 13
int diff = a - b; // 7
int prod = a * b; // 30
int quot = a / b; // 3 (整数除算)
int rem = a % b;  // 1 (剰余)

代入演算子

= (代入), +=, -=, *=, /=, %= (複合代入)

int x = 5;
x += 3; // x = x + 3; -> xは8
x *= 2; // x = x * 2; -> xは16

インクリメント/デクリメント演算子

++ (インクリメント), -- (デクリメント)

  • 前置 (++x, --x): 値を変更してから式で使用
  • 後置 (x++, x--): 式で使用してから値を変更
int count = 0;
printf("%d\n", ++count); // 1 (countを1にしてから出力)
printf("%d\n", count++); // 1 (countを出力してから2にする)
printf("%d\n", count);   // 2

比較演算子

== (等しい), != (等しくない), > (より大きい), < (より小さい), >= (以上), <= (以下)

結果は真 (通常 1) または偽 (0) となります。

int p = 5, q = 10;
if (p < q) { // 真 (1)
    printf("pはqより小さい\n");
}
if (p == 5) { // 真 (1)
    printf("pは5です\n");
}

論理演算子

&& (論理AND), || (論理OR), ! (論理NOT)

短絡評価(ショートサーキット評価)が行われます。

int age = 20;
int score = 85;
if (age >= 20 && score >= 80) { // 真 && 真 -> 真
    printf("合格\n");
}
if (age < 18 || score > 90) { // 偽 || 偽 -> 偽
    printf("特別条件\n");
}
if (!(age < 18)) { // !(偽) -> 真
    printf("18歳以上\n");
}

ビット演算子

& (ビットAND), | (ビットOR), ^ (ビットXOR), ~ (ビットNOT), << (左シフト), >> (右シフト)

unsigned char flags = 0b00001010; // 10 (十進数)
unsigned char mask = 0b00000100;  // 4 (十進数)

if (flags & mask) { // 0b00000000 -> 偽 (0)
    printf("フラグ4が立っています\n");
}

flags |= 0b00000011; // flags = 0b00001011 (ORでフラグを立てる)
flags &= ~mask;     // flags = 0b00001011 (ANDとNOTでフラグを消す)

int shifted = 5 << 2; // 0b101 << 2 -> 0b10100 (20) 右に0を追加
int r_shifted = 20 >> 1; // 0b10100 >> 1 -> 0b1010 (10) 右端を削除 (算術/論理シフト注意)

条件演算子 (三項演算子)

条件式 ? 真の場合の値 : 偽の場合の値

int a = 10, b = 20;
int max_val = (a > b) ? a : b; // a > b が偽なので b (20) が max_val に代入される
printf("最大値: %d\n", max_val);

sizeof 演算子

データ型や変数のサイズをバイト単位で返します。

printf("intのサイズ: %zu バイト\n", sizeof(int));
double arr[10];
printf("double配列arrのサイズ: %zu バイト\n", sizeof(arr)); // 配列全体のサイズ
printf("double配列arrの要素数: %zu\n", sizeof(arr) / sizeof(arr[0])); // 要素数

ポインタ関連演算子

& (アドレス演算子), * (間接参照演算子)

ポインタセクションで詳しく説明します。

演算子の優先順位と結合規則

複雑な式では、演算子の優先順位と結合規則(左から右、右から左など)が重要になります。不明瞭な場合は括弧 () を使用して明示的に順序を指定することが推奨されます。

int result = 5 + 3 * 2; // 乗算が優先 -> 5 + 6 = 11
int result2 = (5 + 3) * 2; // 括弧内が優先 -> 8 * 2 = 16
int x, y, z;
x = y = z = 0; // 代入演算子は右結合 (z=0, y=z, x=y)

4. 制御フロー 🧭

プログラムの実行順序を制御するための構文です。

条件分岐 (if-else)

int score = 75;

if (score >= 90) {
    printf("優\n");
} else if (score >= 70) {
    printf("良\n"); // このブロックが実行される
} else if (score >= 50) {
    printf("可\n");
} else {
    printf("不可\n");
}

条件分岐 (switch)

整数型または文字型の式の結果に基づいて処理を分岐します。case ラベルと break 文の組み合わせが一般的です。

char grade = 'B';

switch (grade) {
    case 'A':
        printf("素晴らしい!\n");
        break; // switch文を抜ける
    case 'B':
        printf("良い成績です。\n");
        // breakがない場合、次のcaseも実行される (フォールスルー)
    case 'C':
        printf("もう少し頑張りましょう。\n");
        break;
    case 'D':
        printf("再試が必要です。\n");
        break;
    default: // どのcaseにも一致しない場合
        printf("評価不能です。\n");
        break;
}

繰り返し (while)

条件式が真の間、ブロック内の処理を繰り返します。最初に条件を評価します。

int i = 0;
while (i < 5) {
    printf("i = %d\n", i);
    i++; // 条件を変える処理を忘れないように
}
// 出力: i = 0, i = 1, i = 2, i = 3, i = 4

繰り返し (do-while)

ブロック内の処理を最低1回実行し、その後、条件式が真の間繰り返します。最後に条件を評価します。

int count = 0;
char choice;
do {
    printf("カウント: %d\n", count);
    printf("続けますか? (y/n): ");
    // scanf(" %c", &choice); // 先頭の空白で前の入力の改行を読み飛ばす
    choice = getchar(); // 1文字読み込み
    while(getchar() != '\n'); // バッファに残った改行などを読み捨てる

    count++;
} while (choice == 'y' || choice == 'Y');

繰り返し (for)

指定された回数や範囲で処理を繰り返すのに適しています。初期化式、条件式、変化式を持ちます。

// 0から9まで10回繰り返す
for (int j = 0; j < 10; j++) {
    printf("j = %d\n", j);
}

// 複数の初期化・変化式も可能 (カンマ区切り)
for (int x = 0, y = 5; x < y; x++, y--) {
    printf("x=%d, y=%d\n", x, y);
}
// 出力: x=0, y=5 / x=1, y=4 / x=2, y=3

ループ制御

  • break: 現在実行中のループ (while, do-while, for) または switch 文を強制的に終了します。
  • continue: 現在のループのイテレーションをスキップし、次のイテレーション(条件評価または変化式)に進みます。
for (int k = 0; k < 10; k++) {
    if (k == 3) {
        continue; // kが3の時は以下のprintfをスキップし、次のk=4へ
    }
    if (k == 7) {
        break;    // kが7になったらループを抜ける
    }
    printf("k = %d\n", k);
}
// 出力: k = 0, k = 1, k = 2, k = 4, k = 5, k = 6

ジャンプ (goto)

プログラム内の指定されたラベル位置に無条件にジャンプします。コードが複雑になりやすいため、通常は使用を避けるべきですが、多重ループからの脱出などで限定的に使われることがあります。

for (int i = 0; i < 5; i++) {
    for (int j = 0; j < 5; j++) {
        if (i * j > 10) {
            printf("条件成立 (%d, %d)\n", i, j);
            goto loop_exit; // ループの外のラベルへジャンプ
        }
    }
}

loop_exit: // ラベル定義
printf("ループ処理終了\n");

5. 関数 🧱

特定の処理をまとめて名前を付け、再利用可能にする仕組みです。

関数の定義

// 戻り値の型 関数名(引数リスト) {
//     処理;
//     return 戻り値; // 戻り値がない (void)場合は不要
// }

// 2つの整数の和を計算する関数
int add(int a, int b) {
    int sum = a + b;
    return sum;
}

// 何も返さない (副作用が目的の) 関数
void print_message(const char* message) {
    printf("%s\n", message);
}

// 引数を取らない関数
int get_random_number() {
    // 実際には乱数生成処理 (stdlib.h の rand() など)
    return 42; // 仮
}

関数の呼び出し

int main() {
    int result = add(5, 3); // add関数を呼び出し、戻り値を受け取る
    printf("5 + 3 = %d\n", result); // 出力: 8

    print_message("Hello from function!"); // print_message関数を呼び出す

    int random_num = get_random_number();
    printf("Random number: %d\n", random_num);

    return 0;
}

関数プロトタイプ宣言

関数を定義する前(または別のファイルで定義されている場合)に呼び出す場合、コンパイラに関数のシグネチャ(戻り値の型、関数名、引数の型リスト)を知らせる必要があります。

#include <stdio.h>

// 関数プロトタイプ宣言
int add(int a, int b);
void print_message(const char* message);

int main() {
    // プロトタイプがあるので、mainより後で定義されていても呼び出せる
    int result = add(10, 20);
    print_message("Function prototype example");
    printf("Result: %d\n", result);
    return 0;
}

// 関数の実際の定義
int add(int a, int b) {
    return a + b;
}

void print_message(const char* message) {
    printf("%s\n", message);
}

引数の渡し方

  • 値渡し (Pass by Value): 関数の仮引数には、実引数の値のコピーが渡されます。関数内で仮引数を変更しても、呼び出し元の実引数には影響しません。C言語のデフォルトです。
  • 参照渡し (Pass by Reference) (ポインタを使用): 引数のアドレスを渡します。関数内でポインタを介して値を変更すると、呼び出し元の変数の値も変更されます。
#include <stdio.h>

// 値渡しの例
void modify_value(int val) {
    val = 100; // この変更は呼び出し元に影響しない
    printf("Inside modify_value: val = %d\n", val);
}

// 参照渡し(ポインタ)の例
void modify_value_by_ref(int* ptr) {
    *ptr = 200; // ポインタ経由で値を変更すると呼び出し元も変わる
    printf("Inside modify_value_by_ref: *ptr = %d\n", *ptr);
}

int main() {
    int x = 10;
    printf("Before modify_value: x = %d\n", x);
    modify_value(x);
    printf("After modify_value: x = %d\n", x); // xは10のまま

    int y = 50;
    printf("Before modify_value_by_ref: y = %d\n", y);
    modify_value_by_ref(&y); // yのアドレスを渡す
    printf("After modify_value_by_ref: y = %d\n", y); // yは200に変わる

    return 0;
}

再帰関数 (Recursive Function)

関数が自分自身を呼び出すことです。階乗の計算や木構造の探索などで使われます。終了条件(ベースケース)が必須です。

// 階乗を計算する再帰関数
long long factorial(int n) {
    if (n < 0) {
        return -1; // エラーケース
    }
    // ベースケース (終了条件)
    if (n == 0 || n == 1) {
        return 1;
    }
    // 再帰ステップ
    return n * factorial(n - 1);
}

int main() {
    printf("5! = %lld\n", factorial(5)); // 120
    return 0;
}

main関数

プログラムの実行が開始される特別な関数です。

  • int main(void): 引数を取らない形式。
  • int main(int argc, char *argv[]): コマンドライン引数を受け取る形式。
    • argc: コマンドライン引数の数(プログラム名を含む)。
    • argv: コマンドライン引数の文字列を格納したポインタ配列 (argv[0] はプログラム名)。
  • 戻り値 int: プログラムの終了ステータスを示します。通常、正常終了は 0、異常終了は 0 以外(例: 1, EXIT_FAILURE)を返します。
#include <stdio.h>
#include <stdlib.h> // EXIT_SUCCESS, EXIT_FAILURE のため

int main(int argc, char *argv[]) {
    printf("プログラム名: %s\n", argv[0]);
    printf("コマンドライン引数の数: %d\n", argc);

    if (argc > 1) {
        printf("与えられた引数:\n");
        for (int i = 1; i < argc; i++) {
            printf("  argv[%d]: %s\n", i, argv[i]);
        }
    } else {
        printf("コマンドライン引数はありません。\n");
    }

    return EXIT_SUCCESS; // return 0; と同じ意味
}

6. 配列 📊

同じ型のデータを連続したメモリ領域に格納するデータ構造です。

1次元配列

  • 宣言: データ型 配列名[要素数];
  • 初期化: データ型 配列名[要素数] = {値1, 値2, ...}; (要素数を省略可能)
  • アクセス: 配列名[インデックス] (インデックスは0から始まる)
int scores[5]; // 要素数5のint型配列を宣言 (値は不定)
scores[0] = 80;
scores[1] = 95;
// ... scores[4] まで

double values[] = {1.1, 2.2, 3.3, 4.4}; // 初期化子から要素数4が決定される

// 配列要素の走査
for (int i = 0; i < 4; i++) {
    printf("values[%d] = %.1f\n", i, values[i]);
}

// 配列サイズと要素数の計算
int numbers[] = {10, 20, 30, 40, 50, 60};
size_t array_size = sizeof(numbers); // 配列全体のバイトサイズ
size_t element_size = sizeof(numbers[0]); // 1要素のバイトサイズ
size_t num_elements = array_size / element_size; // 要素数
printf("要素数: %zu\n", num_elements); // 6

多次元配列

配列の配列として表現されます。主に2次元配列(行列)がよく使われます。

  • 宣言: データ型 配列名[行数][列数];
  • 初期化: データ型 配列名[行数][列数] = {{行1値1, 行1値2,...}, {行2値1, 行2値2,...}, ...};
  • アクセス: 配列名[行インデックス][列インデックス]
int matrix[3][4]; // 3行4列のint型2次元配列

int table[2][3] = {
    {1, 2, 3}, // 1行目 (table[0])
    {4, 5, 6}  // 2行目 (table[1])
};

// 要素へのアクセス
table[1][1] = 55; // 2行目、2列目の要素を55に変更

// 2次元配列の走査
for (int i = 0; i < 2; i++) { // 行ループ
    for (int j = 0; j < 3; j++) { // 列ループ
        printf("table[%d][%d] = %d\t", i, j, table[i][j]);
    }
    printf("\n"); // 各行の終わりで改行
}

配列と関数

配列を関数に渡す場合、通常は配列の先頭要素のアドレス(ポインタ)が渡されます。そのため、関数側では配列のサイズを知るために、別途サイズ情報を引数として渡す必要があります。

#include <stdio.h>

// 配列の要素を表示する関数 (サイズも受け取る)
// void print_array(int arr[], size_t size) { // arr[] は int* arr と等価
void print_array(int* arr, size_t size) {
    printf("[");
    for (size_t i = 0; i < size; i++) {
        printf("%d", arr[i]);
        if (i < size - 1) {
            printf(", ");
        }
    }
    printf("]\n");
    // sizeof(arr) はここではポインタのサイズになるため、要素数計算には使えない
}

// 2次元配列を関数に渡す場合 (列数は固定する必要がある)
void print_matrix(int mat[][3], int rows) { // 列数3を指定
// void print_matrix(int (*mat)[3], int rows) { // ポインタ表記
     for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 3; j++) {
             printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}


int main() {
    int data[] = {10, 20, 30, 40, 50};
    size_t n = sizeof(data) / sizeof(data[0]);
    print_array(data, n); // 配列名(先頭アドレス)と要素数を渡す

    int matrix_data[2][3] = {{1, 2, 3}, {4, 5, 6}};
    print_matrix(matrix_data, 2); // 2次元配列名と行数を渡す

    return 0;
}

可変長配列 (VLA – Variable Length Array) (C99以降)

配列のサイズを実行時に決定できる配列です(スタック領域に確保されることが多い)。関数スコープでのみ使用可能です。

注意: VLAは一部のコンパイラや環境ではサポートされていない、または推奨されていない場合があります。動的なサイズが必要な場合は、後述する動的メモリ確保 (malloc) を使うのが一般的です。

#include <stdio.h>
#include <stdlib.h>

void process_vla(int n) {
    if (n <= 0) return;
    int vla_array[n]; // サイズnの可変長配列 (スタック上に確保)

    for(int i=0; i < n; i++) {
        vla_array[i] = i * 10;
        printf("vla_array[%d] = %d\n", i, vla_array[i]);
    }
    // 関数を抜けると自動的にメモリは解放される
}

int main(int argc, char *argv[]) {
    if (argc > 1) {
        int size = atoi(argv[1]); // コマンドライン引数からサイズを取得
        if (size > 0 && size < 1000) { // 過大なサイズを防ぐ
             process_vla(size);
        } else {
            printf("無効なサイズです。\n");
        }
    } else {
        process_vla(5); // デフォルトサイズ
    }
    return 0;
}

7. ポインタ 👉

メモリ上のアドレスを格納するための変数です。変数の間接的な操作、動的メモリ確保、データ構造の構築などに不可欠です。

ポインタの宣言と初期化

  • 宣言: データ型 *ポインタ変数名;
  • アドレスの取得: &変数名 (アドレス演算子)
  • NULLポインタ: どこも指していないことを示す特別なポインタ (NULL マクロ、または (void*)0)
int var = 10;
int *ptr; // int型へのポインタを宣言 (ptr自体のアドレスは別)

ptr = &var // ptrに変数varのアドレスを代入

int *ptr_null = NULL; // NULLポインタで初期化

double val = 3.14;
double *pd = &val

間接参照 (Dereference)

ポインタが指すアドレスに格納されている値にアクセスします。

  • *ポインタ変数名 (間接参照演算子)
int var = 25;
int *p = &var

printf("varの値: %d\n", var);     // 25
printf("varのアドレス: %p\n", (void*)&var); // アドレス表示 (例: 0x7ff...)
printf("ptrが指すアドレス: %p\n", (void*)p);   // varのアドレスと同じ
printf("ptrが指す先の値 (*p): %d\n", *p); // 25 (間接参照)

// ポインタ経由で値を変更
*p = 50;
printf("変更後のvarの値: %d\n", var); // 50 (varの値が変わる)

注意: 初期化されていないポインタやNULLポインタを間接参照すると、未定義動作(クラッシュなど)を引き起こします。

int *bad_ptr;
// *bad_ptr = 10; // ダメ! 未初期化ポインタの間接参照

int *null_ptr = NULL;
// if (null_ptr != NULL) { // 必ずチェックする
//    *null_ptr = 20;
// }

ポインタ演算

ポインタに対して加算や減算を行うと、指し示すデータ型のサイズ単位でアドレスが移動します。

int arr[] = {10, 20, 30, 40};
int *ptr_arr = arr; // 配列名は先頭要素のアドレスを指すポインタとして扱われる (arr == &arr[0])

printf("ptr_arrが指す値: %d\n", *ptr_arr);         // 10 (arr[0])
printf("ptr_arr+1が指す値: %d\n", *(ptr_arr + 1)); // 20 (arr[1])
printf("ptr_arr+2が指す値: %d\n", *(ptr_arr + 2)); // 30 (arr[2])

ptr_arr++; // ポインタを次の要素へ移動 (intのサイズ分アドレスが増加)
printf("インクリメント後のptr_arrが指す値: %d\n", *ptr_arr); // 20

// ポインタ同士の減算 (同じ配列内の要素を指す場合)
int *ptr1 = &arr[1];
int *ptr3 = &arr[3];
ptrdiff_t diff = ptr3 - ptr1; // 要素数の差 (型は ptrdiff_t)
printf("ポインタ間の要素数差: %td\n", diff); // 2

配列とポインタの関係

配列名は、多くの場合、その配列の先頭要素を指すポインタ(定数ポインタ)として扱われます。array[i]*(array + i) と等価です。

int nums[] = {5, 15, 25};
printf("nums[1] = %d\n", nums[1]);       // 15
printf("*(nums + 1) = %d\n", *(nums + 1)); // 15

int *p_num = nums;
printf("p_num[1] = %d\n", p_num[1]);     // 15 (ポインタ変数でも配列のようにアクセス可能)
printf("*(p_num + 1) = %d\n", *(p_num + 1)); // 15

ポインタへのポインタ (ダブルポインタ)

ポインタ変数のアドレスを格納するポインタです。データ型 **ポインタ名; のように宣言します。

int x = 100;
int *p1 = &x    // xのアドレスを指すポインタ
int **p2 = &p1 // p1のアドレスを指すポインタ (ポインタへのポインタ)

printf("xの値: %d\n", x);       // 100
printf("*p1の値: %d\n", *p1);     // 100
printf("**p2の値: %d\n", **p2);   // 100 (2回間接参照してxの値にアクセス)

printf("p1が指すアドレス: %p\n", (void*)p1);   // xのアドレス
printf("*p2が指すアドレス: %p\n", (void*)*p2);  // p1が指すアドレス (xのアドレス)
printf("p2が指すアドレス: %p\n", (void*)p2);   // p1のアドレス

**p2 = 200; // p2経由でxの値を変更
printf("変更後のxの値: %d\n", x); // 200

ダブルポインタは、関数内でポインタ引数自体を変更したい場合や、ポインタの配列(文字列配列など)を扱う際によく使われます。

関数ポインタ

関数のアドレスを格納するポインタです。関数を引数として渡したり、コールバック関数を実現したりするのに使われます。

  • 宣言: 戻り値の型 (*ポインタ名)(引数の型リスト);
#include <stdio.h>

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

// 関数ポインタを引数に取る関数
void calculate(int x, int y, int (*operation)(int, int)) {
    int result = operation(x, y); // 関数ポインタ経由で関数を呼び出し
    printf("Result: %d\n", result);
}

int main() {
    int (*func_ptr)(int, int); // intを2つ引数に取りintを返す関数へのポインタ

    func_ptr = add; // 関数名を代入 (関数のアドレス)
    calculate(10, 5, func_ptr); // add関数を渡す -> Result: 15

    func_ptr = subtract;
    calculate(10, 5, func_ptr); // subtract関数を渡す -> Result: 5

    // 関数ポインタの配列
    int (*operations[2])(int, int) = {add, subtract};
    printf("Via array[0]: %d\n", operations[0](20, 8)); // add(20, 8) -> 28
    printf("Via array[1]: %d\n", operations[1](20, 8)); // subtract(20, 8) -> 12

    return 0;
}

void ポインタ

特定の型を指さない汎用的なポインタです。void * で宣言します。任意の型のポインタを代入できますが、使用(間接参照)する前に適切な型にキャストする必要があります。malloc の戻り値などが void * です。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int i = 10;
    float f = 3.14f;
    char c = 'A';

    void *vp;

    vp = &i
    printf("void*経由 (intにキャスト): %d\n", *(int*)vp); // int* にキャストしてから間接参照

    vp = &f
    printf("void*経由 (floatにキャスト): %.2f\n", *(float*)vp); // float* にキャスト

    vp = &c
    printf("void*経由 (charにキャスト): %c\n", *(char*)vp); // char* にキャスト

    // malloc の例
    void *mem = malloc(sizeof(int));
    if (mem != NULL) {
        *(int*)mem = 123; // int* にキャストして値を書き込む
        printf("mallocした領域の値: %d\n", *(int*)mem);
        free(mem); // 解放
    }

    return 0;
}

8. 文字列操作 (string.h など) 📝

C言語では、文字列は文字 (char) の配列として扱われ、通常、末尾にヌル文字 (\0) が付加されます。

文字列リテラル

ダブルクォーテーション (") で囲まれた文字シーケンスです。読み取り専用メモリに格納されることが多く、変更は未定義動作となります。

char *str_literal = "Hello"; // ポインタはリテラルを指す (変更不可)
// str_literal[0] = 'J'; // 未定義動作!

char str_array[] = "World"; // 配列として初期化 (変更可能)
str_array[0] = 'F'; // OK -> "Forld"

基本的な文字列関数 (string.h)

関数 説明 コード例
strlen() 文字列の長さ(ヌル文字を含まない)を返す (size_t 型)
char s1[] = "abc";
size_t len = strlen(s1); // len は 3
strcpy() 文字列をコピーする (コピー先に十分な領域が必要)。バッファオーバーフローの危険あり
char src[] = "Copy me";
char dest[20];
strcpy(dest, src); // dest は "Copy me" になる
strncpy() 最大n文字まで文字列をコピーする。コピー元がn文字より短い場合、残りをヌル文字で埋める。n文字コピーしてヌル終端されない場合がある点に注意。
char src[]="Long string";
char dest[5];
strncpy(dest, src, 4);
dest[4] = '\0'; // 手動でヌル終端が必要な場合がある
// dest は "Long"
strcat() 文字列を連結する (連結先に十分な領域が必要)。バッファオーバーフローの危険あり
char str1[20] = "Hello, ";
char str2[] = "World!";
strcat(str1, str2); // str1 は "Hello, World!" になる
strncat() 最大n文字まで文字列を連結する。常にヌル終端される。
char str1[10] = "abc";
char str2[] = "defghijkl";
strncat(str1, str2, 3); // str1 は "abcdef" になる (3文字追加 + \0)
strcmp() 2つの文字列を辞書順で比較する。等しければ0、s1>s2なら正の値、s1<s2なら負の値を返す。
int cmp = strcmp("apple", "banana"); // 負の値
cmp = strcmp("test", "test"); // 0
cmp = strcmp("xyz", "abc"); // 正の値
strncmp() 最大n文字まで2つの文字列を比較する。
int cmp = strncmp("applepie", "apples", 5); // 最初の5文字("apple")は同じなので0
strchr() 文字列の中から指定した文字が最初に出現する位置へのポインタを返す。見つからなければNULLを返す。
char str[] = "find the char";
char *found = strchr(str, 't');
if (found) printf("Found 't' at: %s\n", found); // "the char"
strrchr() 文字列の中から指定した文字が最後に出現する位置へのポインタを返す。
char str[] = "find the char";
char *found = strrchr(str, 'h');
if (found) printf("Last 'h' at: %s\n", found); // "har"
strstr() 文字列の中から指定した部分文字列が最初に出現する位置へのポインタを返す。見つからなければNULL。
char haystack[] = "This is a test string.";
char needle[] = "test";
char *pos = strstr(haystack, needle);
if (pos) printf("Found '%s' at: %s\n", needle, pos); // "test string."

セキュリティ警告: strcpystrcat はバッファオーバーフローを引き起こしやすいため、strncpystrncat、またはより安全な代替関数 (例: snprintf, C11の strcpy_s/strcat_s など) の使用が推奨されます。

書式付き文字列操作 (stdio.h)

  • sprintf(): 書式指定に従って文字列を生成し、文字配列に格納する。バッファオーバーフローの危険あり
  • snprintf(): sprintf と同様だが、最大書き込み文字数を指定でき、より安全。常にヌル終端される(バッファサイズが十分なら)。
  • sscanf(): 文字列から書式指定に従ってデータを読み取り、変数に格納する。
#include <stdio.h>

char buffer[100];
int year = 2025;
const char* event = "Cheatsheet Creation";

// snprintf を使用 (推奨)
int written = snprintf(buffer, sizeof(buffer), "Year: %d, Event: %s", year, event);
// written にはヌル文字を除いて書き込もうとした文字数が返る
if (written >= 0 && written < sizeof(buffer)) {
    printf("Generated string: %s\n", buffer);
} else {
    fprintf(stderr, "Buffer too small or error occurred.\n");
}

// sscanf の例
char data_str[] = "ID:123 Name:Alice Score:95.5";
int id;
char name[50];
float score;
int items_read = sscanf(data_str, "ID:%d Name:%49s Score:%f", &id, name, &score);
if (items_read == 3) {
    printf("Read: ID=%d, Name=%s, Score=%.1f\n", id, name, score);
}

文字種別判定・変換 (ctype.h)

文字が英字か、数字か、空白かなどを判定したり、大文字小文字を変換したりする関数群です。

関数説明
isalpha(c)英字 (a-z, A-Z) かどうか
isdigit(c)数字 (0-9) かどうか
isalnum(c)英数字 (isalpha or isdigit) かどうか
isspace(c)空白文字(スペース、タブ、改行など)かどうか
islower(c)小文字 (a-z) かどうか
isupper(c)大文字 (A-Z) かどうか
isprint(c)表示可能文字(空白含む)かどうか
iscntrl(c)制御文字かどうか
ispunct(c)区切り文字(記号など)かどうか
isxdigit(c)16進数字 (0-9, a-f, A-F) かどうか
tolower(c)大文字を小文字に変換(それ以外はそのまま)
toupper(c)小文字を大文字に変換(それ以外はそのまま)
#include <ctype.h>
#include <stdio.h>

int main() {
    char ch = 'a';
    if (isalpha(ch)) { printf("%c is alpha\n", ch); }
    char upper_ch = toupper(ch); // 'A'
    printf("%c -> %c\n", ch, upper_ch);

    char num_ch = '7';
    if (isdigit(num_ch)) { printf("%c is digit\n", num_ch); }

    return 0;
}

文字列と数値の変換 (stdlib.h)

関数説明
atoi(str)文字列を int に変換
atol(str)文字列を long に変換
atoll(str)文字列を long long に変換 (C99)
atof(str)文字列を double に変換
strtol(str, endptr, base)文字列を long に変換(エラーチェック、基数指定可能)
strtoul(str, endptr, base)文字列を unsigned long に変換(エラーチェック、基数指定可能)
strtoll(str, endptr, base)文字列を long long に変換(C99、エラーチェック、基数指定可能)
strtoull(str, endptr, base)文字列を unsigned long long に変換(C99、エラーチェック、基数指定可能)
strtof(str, endptr)文字列を float に変換(C99、エラーチェック可能)
strtod(str, endptr)文字列を double に変換(エラーチェック可能)
strtold(str, endptr)文字列を long double に変換(C99、エラーチェック可能)

strto* 系の関数は、変換できなかった部分のポインタを endptr に格納したり、エラー発生時に errno を設定したりするため、より堅牢な変換が可能です。

#include <stdlib.h>
#include <stdio.h>
#include <errno.h> // errno のため

int main() {
    char num_str[] = "12345";
    int num_i = atoi(num_str); // 簡単な変換
    printf("atoi: %d\n", num_i);

    char long_str[] = "9876543210";
    char *end;
    errno = 0; // errnoをクリア
    long long num_ll = strtoll(long_str, &end, 10); // 10進数で変換

    if (errno == ERANGE) {
        printf("Error: Number out of range\n");
    } else if (*end != '\0') {
        printf("Error: Invalid characters found: %s\n", end);
    } else {
        printf("strtoll: %lld\n", num_ll);
    }

    char float_str[] = "  -3.14e2rest";
    errno = 0;
    double num_d = strtod(float_str, &end);
     if (errno == ERANGE) {
        printf("Error: Number out of range\n");
    } else {
        printf("strtod: %f\n", num_d); // -314.000000
        printf("Remaining string: %s\n", end); // "rest"
    }

    return 0;
}

9. 構造体と共用体 🧩

異なる型のデータをひとまとめにして扱うための仕組みです。

構造体 (struct)

複数の変数(メンバ)をグループ化します。各メンバは個別のメモリ領域を持ちます。

  • 定義: struct タグ名 { メンバ宣言リスト };
  • 変数宣言: struct タグ名 変数名;
  • メンバアクセス: 変数名.メンバ名 (直接アクセス), ポインタ->メンバ名 (ポインタ経由アクセス)
  • typedef との組み合わせ: typedef struct { ... } 新しい型名;
#include <stdio.h>
#include <string.h>

// 構造体の定義 (タグ名: Point)
struct Point {
    int x;
    double y;
};

// typedef を使った構造体の定義 (型名: Person)
typedef struct {
    char name[50];
    int age;
    float height;
} Person;

int main() {
    // 構造体変数の宣言と初期化
    struct Point p1; // 変数宣言
    p1.x = 10;       // メンバにアクセスして代入
    p1.y = 20.5;

    struct Point p2 = {30, 40.8}; // 初期化子リストで初期化

    printf("p1: x=%d, y=%.1f\n", p1.x, p1.y);
    printf("p2: x=%d, y=%.1f\n", p2.x, p2.y);

    // typedef した型を使う
    Person person1;
    strcpy(person1.name, "Alice");
    person1.age = 30;
    person1.height = 165.5f;

    Person person2 = {"Bob", 25, 175.0f};

    printf("Person1: Name=%s, Age=%d, Height=%.1f\n", person1.name, person1.age, person1.height);

    // 構造体へのポインタ
    Person *ptr_person = &person1

    // ポインタ経由でのメンバアクセス (アロー演算子 ->)
    printf("Pointer access: Name=%s\n", ptr_person->name);
    ptr_person->age = 31; // ポインタ経由で値を変更
    printf("Updated Age: %d\n", person1.age); // person1の値も変わる

    // ポインタ経由でのメンバアクセス (アスタリスクとドット) - やや煩雑
    // (*ptr_person).age = 32;
    // printf("Updated Age again: %d\n", (*ptr_person).age);

    // 構造体のコピー (メンバごとにコピーされる)
    Person person3 = person2;
    printf("Person3 (copied from Person2): Name=%s\n", person3.name);

    return 0;
}

共用体 (union)

複数のメンバが同じメモリ領域を共有します。一度に有効なメンバは1つだけであり、最後に代入されたメンバの値のみが保証されます。サイズは最も大きいメンバのサイズになります。

  • 定義: union タグ名 { メンバ宣言リスト };
  • 変数宣言: union タグ名 変数名;
  • メンバアクセス: 構造体と同じ (. または ->)

メモリ使用量を節約したい場合や、複数の型を同じ場所で表現したい場合に使われます。

#include <stdio.h>

typedef union {
    int i;
    float f;
    char c[4]; // int や float と同じサイズ (環境依存)
} Data;

int main() {
    Data data;

    printf("Size of Data union: %zu bytes\n", sizeof(Data)); // 最大メンバのサイズ

    data.i = 12345;
    printf("As int: %d\n", data.i);
    // この時点で data.f や data.c の値は不定 (またはiのビット表現)
    // printf("As float (after int assignment): %f\n", data.f); // 未定義動作に近い

    data.f = 98.76f;
    printf("As float: %f\n", data.f);
    // この時点で data.i の値は不定 (またはfのビット表現)
    // printf("As int (after float assignment): %d\n", data.i); // 未定義動作に近い

    // 文字列としてアクセス (サイズに注意)
    data.c[0] = 'A';
    data.c[1] = 'B';
    data.c[2] = 'C';
    data.c[3] = '\0'; // ヌル終端
    printf("As char array: %s\n", data.c);
    // この時点で他のメンバの値は不定

    return 0;
}

注意: 共用体のどのメンバが現在有効かを追跡するのはプログラマの責任です。構造体と組み合わせて、どのメンバが有効かを示すフラグを持つことがよくあります。

typedef enum { INT_TYPE, FLOAT_TYPE, CHAR_TYPE } DataType;

typedef struct {
    DataType type;
    union {
        int i_val;
        float f_val;
        char c_val;
    } value;
} Variant;

void print_variant(Variant v) {
    switch(v.type) {
        case INT_TYPE: printf("Int: %d\n", v.value.i_val); break;
        case FLOAT_TYPE: printf("Float: %f\n", v.value.f_val); break;
        case CHAR_TYPE: printf("Char: %c\n", v.value.c_val); break;
        default: printf("Unknown type\n");
    }
}

int main() {
    Variant var1;
    var1.type = INT_TYPE;
    var1.value.i_val = 100;
    print_variant(var1);

    Variant var2;
    var2.type = FLOAT_TYPE;
    var2.value.f_val = 3.14f;
    print_variant(var2);

    return 0;
}

ビットフィールド

構造体メンバのサイズをビット単位で指定できます。メモリ使用量を極限まで切り詰めたい場合や、ハードウェアレジスタのように特定のビット配置を持つデータ構造を表現する場合に使われます。

#include <stdio.h>

// ビットフィールドを持つ構造体
typedef struct {
    unsigned int flag1 : 1; // 1ビットのフラグ
    unsigned int flag2 : 1; // 1ビットのフラグ
    unsigned int type  : 3; // 3ビット (0-7 の値を格納可能)
    unsigned int value : 11; // 11ビット
    // 合計 1 + 1 + 3 + 11 = 16ビット (通常2バイトに収まる)
    // : の後の数字がビット数
} PackedData;

int main() {
    PackedData data;

    printf("Size of PackedData: %zu bytes\n", sizeof(PackedData));

    data.flag1 = 1;
    data.flag2 = 0;
    data.type = 5; // 3ビットで表現可能 (0b101)
    data.value = 1024; // 11ビットで表現可能 (0b10000000000)

    printf("Flag1: %u\n", data.flag1);
    printf("Flag2: %u\n", data.flag2);
    printf("Type: %u\n", data.type);
    printf("Value: %u\n", data.value);

    // ビットフィールドの限界を超える値を代入すると、通常は切り捨てられる
    // data.type = 10; // 3ビットの範囲を超える (未定義動作の可能性もある)

    return 0;
}

注意: ビットフィールドのメモリレイアウト(ビットの順序など)は処理系依存(コンパイラやアーキテクチャに依存)です。移植性が重要な場合は注意が必要です。

10. ファイル操作 (stdio.h) 💾

ディスク上のファイルに対する読み書き操作です。

ファイルポインタ (FILE*)

ファイル操作を行うための中心的な概念です。fopen() で取得し、fclose() で解放します。

ファイルを開く・閉じる

関数 モード 説明
fopen(filename, mode) "r"読み込み用に開く (ファイルが存在しないとエラー: NULLを返す)
"w"書き込み用に開く (ファイルが存在すれば内容は消去、存在しなければ新規作成)
"a"追記用に開く (ファイルが存在すれば末尾から書き込み、存在しなければ新規作成)
"r+"読み書き両用に開く (ファイルが存在しないとエラー)
"w+"読み書き両用に開く (ファイルが存在すれば内容は消去、存在しなければ新規作成)
"a+"読み書き両用に開く (読み込みは先頭から、書き込みは末尾へ追記。存在しなければ新規作成)
上記モードに b を追加 (例: "rb", "wb") するとバイナリモードで開く (テキストモードの改行変換などを行わない)
fclose(fp) 開いているファイルポインタ fp を閉じる。バッファの内容をフラッシュし、リソースを解放する。成功すれば0、エラーならEOFを返す。
#include <stdio.h>
#include <stdlib.h> // exit() のため

int main() {
    FILE *fp_read = NULL;
    FILE *fp_write = NULL;

    // 読み込み用にファイルを開く
    fp_read = fopen("input.txt", "r");
    if (fp_read == NULL) {
        perror("Error opening input.txt for reading"); // エラー内容を表示
        // エラー処理 (例: プログラム終了)
        // return 1;
    } else {
        printf("input.txt opened for reading.\n");
        // ... 読み込み処理 ...
        if (fclose(fp_read) != 0) {
            perror("Error closing input.txt");
        } else {
            printf("input.txt closed.\n");
        }
    }

    // 書き込み用にファイルを開く
    fp_write = fopen("output.txt", "w");
    if (fp_write == NULL) {
        perror("Error opening output.txt for writing");
        return 1; // エラー終了
    } else {
         printf("output.txt opened for writing.\n");
        // ... 書き込み処理 ...
        if (fclose(fp_write) != 0) {
             perror("Error closing output.txt");
             return 1;
        } else {
             printf("output.txt closed.\n");
        }
    }

    return 0;
}

テキストファイルの読み書き

文字単位、行単位、書式付きでの読み書きが可能です。

関数説明
fgetc(fp)ファイルから1文字読み込む (EOFまたはエラーでEOFを返す)
fputc(c, fp)ファイルへ1文字書き込む (成功すれば書き込んだ文字、エラーならEOFを返す)
fgets(str, n, fp)ファイルから1行読み込む (最大n-1文字または改行まで)。ヌル終端される。成功すればstr、EOFまたはエラーでNULLを返す。
fputs(str, fp)ファイルへ文字列を書き込む (ヌル文字は書き込まない)。成功すれば非負の値、エラーならEOFを返す。
fscanf(fp, format, ...)ファイルから書式付きで読み込む (scanfのファイル版)。成功した項目数を返す。
fprintf(fp, format, ...)ファイルへ書式付きで書き込む (printfのファイル版)。書き込んだバイト数を返す。
// (fopen, fcloseは上記例を参照)

// --- fgetc / fputc の例 ---
int ch;
FILE *fin = fopen("input.txt", "r");
FILE *fout = fopen("output.txt", "w");
if (fin && fout) {
    while ((ch = fgetc(fin)) != EOF) { // 1文字読んでEOFでなければループ
        fputc(toupper(ch), fout); // 大文字に変換して書き込む
    }
    fclose(fin);
    fclose(fout);
}

// --- fgets / fputs の例 ---
char line[256];
FILE *fin_line = fopen("config.ini", "r");
FILE *fout_line = fopen("config_copy.ini", "w");
if (fin_line && fout_line) {
    while (fgets(line, sizeof(line), fin_line) != NULL) { // 1行読んでNULLでなければループ
        fputs(line, fout_line); // 読み込んだ行をそのまま書き込む (fgetsは改行も読む)
    }
    fclose(fin_line);
    fclose(fout_line);
}

// --- fscanf / fprintf の例 ---
int id;
char name[50];
float value;
FILE *fin_fmt = fopen("data.txt", "r"); // data.txt 例: "101 Alice 95.5"
FILE *fout_fmt = fopen("summary.txt", "w");
if (fin_fmt && fout_fmt) {
    while (fscanf(fin_fmt, "%d %49s %f", &id, name, &value) == 3) { // 3項目読めたらループ
        fprintf(fout_fmt, "ID: %d, Name: %s, Value: %.2f\n", id, name, value);
    }
    fclose(fin_fmt);
    fclose(fout_fmt);
}

バイナリファイルの読み書き

バイト列としてデータをそのまま読み書きします。構造体などのデータをファイルに保存/復元するのに使われます。

関数説明
fread(ptr, size, count, fp)ファイル fp から、size バイトのデータを count 個読み込み、メモリ領域 ptr に格納する。成功した個数 (count未満の可能性あり) を返す。
fwrite(ptr, size, count, fp)メモリ領域 ptr から、size バイトのデータを count 個、ファイル fp に書き込む。成功した個数 (count未満の可能性あり) を返す。
#include <stdio.h>

typedef struct {
    int id;
    double data;
} Record;

int main() {
    FILE *fbin_out = fopen("records.bin", "wb"); // バイナリ書き込みモード
    if (!fbin_out) { perror("fopen wb"); return 1; }

    Record rec1 = {1, 3.14};
    Record rec2 = {2, 2.71};
    Record records[] = {{3, 1.61}, {4, 0.57}};

    // 1レコードずつ書き込み
    fwrite(&rec1, sizeof(Record), 1, fbin_out);
    fwrite(&rec2, sizeof(Record), 1, fbin_out);
    // 配列をまとめて書き込み
    fwrite(records, sizeof(Record), 2, fbin_out);

    fclose(fbin_out);

    // --- 読み込み ---
    FILE *fbin_in = fopen("records.bin", "rb"); // バイナリ読み込みモード
    if (!fbin_in) { perror("fopen rb"); return 1; }

    Record read_rec;
    size_t items_read;

    printf("Reading records:\n");
    // 1レコードずつ読み込み
    while ((items_read = fread(&read_rec, sizeof(Record), 1, fbin_in)) == 1) {
        printf("ID: %d, Data: %f\n", read_rec.id, read_rec.data);
    }

    if (!feof(fbin_in)) { // EOF以外の理由でループが終了した場合 (読み込みエラー)
        perror("fread error");
    }

    fclose(fbin_in);

    return 0;
}

注意: バイナリファイルは、エンディアン(バイトオーダ)や構造体のパディング(アラインメント)の違いにより、異なる環境間での互換性がない場合があります。

ファイル位置の制御

ファイル内の読み書き位置(ファイルポジションインジケータ)を移動させます。

関数 説明 起点 (whence)
fseek(fp, offset, whence) ファイルポインタ fp の位置を、起点 whence から offset バイト移動させる。成功すれば0、エラーなら非0を返す。 SEEK_SET (ファイルの先頭)
SEEK_CUR (現在の位置)
SEEK_END (ファイルの末尾)
ftell(fp) 現在のファイル位置(ファイル先頭からのバイト数)を返す (long 型)。エラーなら -1L を返す。
rewind(fp) ファイル位置をファイルの先頭に戻す (fseek(fp, 0L, SEEK_SET) とほぼ同じ)。エラーインジケータもクリアする。
fgetpos(fp, pos) 現在のファイル位置を fpos_t 型の変数 pos に格納する。大きなファイル (>2GB) でも使える。成功なら0、エラーなら非0。
fsetpos(fp, pos) ファイル位置を fgetpos で取得した位置 pos に設定する。成功なら0、エラーなら非0。
#include <stdio.h>

int main() {
    FILE *fp = fopen("sample.txt", "w+"); // 書き込み・読み込み両用で新規作成
    if (!fp) { perror("fopen w+"); return 1; }

    fputs("0123456789abcdef", fp);

    // 先頭から5バイト目に移動
    if (fseek(fp, 5L, SEEK_SET) == 0) {
        long pos = ftell(fp);
        printf("Current position: %ld\n", pos); // 5
        // そこから文字を読み込む
        int ch = fgetc(fp);
        printf("Character at position 5: %c\n", (char)ch); // '5'
    } else {
        perror("fseek SEEK_SET");
    }

    // 末尾から3バイト手前に移動
    if (fseek(fp, -3L, SEEK_END) == 0) {
         long pos = ftell(fp);
        printf("Current position (from end): %ld\n", pos); // 13 (0-based index)
        int ch = fgetc(fp);
        printf("Character 3 bytes from end: %c\n", (char)ch); // 'd'
    } else {
         perror("fseek SEEK_END");
    }

    // 先頭に戻る
    rewind(fp);
    long pos = ftell(fp);
    printf("Position after rewind: %ld\n", pos); // 0

    fclose(fp);
    return 0;
}

エラー処理

関数説明
feof(fp)ファイルポインタ fp がファイルの終端 (End Of File) に達しているかチェックする。EOFなら非0、そうでなければ0を返す。
ferror(fp)ファイルポインタ fp に関連するエラーが発生したかチェックする。エラーがあれば非0、なければ0を返す。
clearerr(fp)ファイルポインタ fp のファイル終端インジケータとエラーインジケータをクリアする。
perror(str)直前に発生したシステムエラー(errno に基づく)に対応するエラーメッセージを標準エラー出力に出力する。引数 str はメッセージの前に表示される文字列。
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
    perror("Failed to open nonexistent.txt"); // perrorでエラー理由を表示
    // 出力例: Failed to open nonexistent.txt: No such file or directory
} else {
    // ... 読み書き処理 ...
    if (ferror(fp)) {
        fprintf(stderr, "An error occurred during file operation.\n");
        clearerr(fp); // エラー状態をクリア
    }
    if (feof(fp)) {
        printf("End of file reached.\n");
    }
    fclose(fp);
}

11. メモリ管理 (stdlib.h) 🧠

プログラム実行中に、必要なメモリ領域を動的に確保・解放する機能です。主にヒープ領域と呼ばれるメモリプールから割り当てられます。

動的メモリ確保

関数説明
malloc(size)size バイトのメモリブロックをヒープ領域に確保し、その先頭アドレス (void* 型) を返す。確保された領域の内容は不定(初期化されない)。確保に失敗した場合は NULL を返す。
calloc(count, size)size バイトの要素を count 個格納できるメモリブロック(合計 count * size バイト)を確保し、その先頭アドレス (void*) を返す。確保された領域はゼロで初期化される。失敗した場合は NULL を返す。
realloc(ptr, new_size)ptr が指す以前に確保されたメモリブロックのサイズを new_size バイトに変更する。新しいサイズが元のサイズより大きい場合、追加領域の内容は不定。小さい場合はデータが切り捨てられる。移動が発生する場合がある(その場合、元の ptr は無効になる)。成功すれば新しいメモリブロックのアドレス (void*)、失敗すれば NULL を返す(元のメモリは解放されない)。ptrNULL の場合は malloc(new_size) と同じ。new_size が 0 の場合、free(ptr) と同じ(ただし戻り値は処理系依存)。
free(ptr)malloc, calloc, realloc で確保されたメモリブロック ptr を解放し、ヒープ領域に戻す。解放済みのポインタや不正なポインタを free すると未定義動作となる。NULL ポインタを free しても何も起こらない。
#include <stdio.h>
#include <stdlib.h> // メモリ管理関数
#include <string.h> // memset

int main() {
    int *numbers = NULL;
    int n = 5;

    // --- malloc ---
    // int型n個分のメモリを確保
    numbers = (int*)malloc(n * sizeof(int));
    if (numbers == NULL) {
        perror("malloc failed");
        return 1;
    }
    printf("malloc allocated %zu bytes at %p\n", n * sizeof(int), (void*)numbers);
    // malloc で確保した領域は初期化されていないため、必要なら初期化する
    // 例: memset(numbers, 0, n * sizeof(int));
    for (int i = 0; i < n; i++) {
        numbers[i] = i * 10; // 値を代入
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }
    // malloc で確保したメモリは free で解放する必要がある
    free(numbers);
    numbers = NULL; // 解放後はNULLを入れておくのが安全 (ダングリングポインタ防止)
    printf("Memory freed after malloc.\n\n");


    // --- calloc ---
    // double型n個分のメモリを確保し、ゼロで初期化
    double *values = (double*)calloc(n, sizeof(double));
     if (values == NULL) {
        perror("calloc failed");
        return 1;
    }
    printf("calloc allocated %zu bytes at %p (zero-initialized)\n", n * sizeof(double), (void*)values);
    for (int i = 0; i < n; i++) {
        printf("values[%d] = %f\n", i, values[i]); // 0.000000 が表示されるはず
    }
    // calloc で確保したメモリも free で解放
    free(values);
    values = NULL;
    printf("Memory freed after calloc.\n\n");


    // --- realloc ---
    // 最初は3要素分確保
    int *dynamic_array = (int*)malloc(3 * sizeof(int));
    if (!dynamic_array) { perror("initial malloc for realloc failed"); return 1;}
    dynamic_array[0] = 1; dynamic_array[1] = 2; dynamic_array[2] = 3;
    printf("Initial allocation (3 elements) at %p\n", (void*)dynamic_array);

    // 5要素分に拡張
    int *temp_ptr = (int*)realloc(dynamic_array, 5 * sizeof(int));
    if (temp_ptr == NULL) {
        perror("realloc failed");
        free(dynamic_array); // realloc失敗時は元のメモリを解放する必要がある場合も
        return 1;
    }
    dynamic_array = temp_ptr; // realloc成功時はポインタを更新 (アドレスが変わる可能性があるため)
    printf("Reallocated to 5 elements at %p\n", (void*)dynamic_array);
    // 追加された要素 (dynamic_array[3], dynamic_array[4]) の値は不定
    dynamic_array[3] = 4;
    dynamic_array[4] = 5;
    printf("Elements after realloc:");
    for(int i=0; i<5; i++) printf(" %d", dynamic_array[i]);
    printf("\n");

    // realloc で確保/変更したメモリも free で解放
    free(dynamic_array);
    dynamic_array = NULL;
     printf("Memory freed after realloc.\n");

    return 0;
}

メモリ管理の注意点 ⚠️

  • NULLチェック: malloc, calloc, realloc の戻り値は必ず NULL でないかチェックする。
  • 解放漏れ (Memory Leak): 確保したメモリは、不要になったら必ず free で解放する。解放し忘れると、プログラムが使用できるメモリが徐々に減っていく。
  • 二重解放 (Double Free): 同じメモリ領域を2回以上 free してはならない。未定義動作(クラッシュなど)を引き起こす。
  • 解放後の使用 (Use After Free): free で解放した後のポインタ(ダングリングポインタ)を使用してはならない。未定義動作を引き起こす。解放後にポインタに NULL を代入すると、誤用をある程度防げる。
  • サイズ計算: malloc などでサイズを指定する際は、sizeof を使って正確なバイト数を計算する。
  • キャスト: Cでは malloc 系の戻り値 (void*) を代入先のポインタ型に暗黙的に変換できるため、キャストは必須ではないが、C++との互換性や明示性のためにキャストを書くことも多い。

12. プリプロセッサ ⚙️

コンパイルの前にソースコードを処理する機能です。# で始まるディレクティブ(指示)によって制御されます。

ファイルインクルード (#include)

指定されたファイルの内容をその場所に挿入します。

  • #include <filename>: システム標準のヘッダファイルをインクルードする(標準ライブラリなど)。コンパイラが定義済みの検索パスから探す。
  • #include "filename": ユーザー定義のヘッダファイルをインクルードする。通常はカレントディレクトリや指定されたインクルードパスから探す。
#include <stdio.h>     // 標準入出力ライブラリ
#include <stdlib.h>    // 標準ユーティリティライブラリ
#include "my_header.h" // 自作のヘッダファイル

マクロ定義 (#define)

識別子(マクロ名)を特定の文字列(トークンシーケンス)で置き換えるルールを定義します。定数定義や簡単な関数のような置換(関数形式マクロ)に使われます。

  • オブジェクト形式マクロ (定数): #define マクロ名 置換内容
  • 関数形式マクロ: #define マクロ名(引数リスト) 置換内容
#define PI 3.14159
#define BUFFER_SIZE 1024
#define GREETING "Hello"

// 関数形式マクロ (引数は括弧で囲むことが多い)
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))

int main() {
    double radius = 5.0;
    double area = PI * SQUARE(radius); // PI * ((radius) * (radius)) に展開される
    printf("Area: %f\n", area);

    char buffer[BUFFER_SIZE];
    printf("%s\n", GREETING);

    int max_val = MAX(10, 20); // ((10 > 20) ? 10 : 20) に展開 -> 20
    printf("Max: %d\n", max_val);

    // 副作用に注意!
    int i = 5;
    int j = SQUARE(i++); // ((i++) * (i++)) のように展開される可能性があり、未定義動作に近い
                         // iが2回インクリメントされるか、順序も保証されない
    printf("i=%d, j=%d (undefined behavior risk!)\n", i, j);

    return 0;
}

関数形式マクロの注意点:

  • 置換後の式の優先順位問題を避けるため、引数と式全体を括弧 () で囲むことが強く推奨される。
  • 引数に副作用(i++ など)を持つ式を渡すと、予期せぬ結果や未定義動作を引き起こす可能性がある。
  • 型チェックが行われない。
  • デバッグが難しくなることがある。

現代のC/C++では、定数には const 変数や enum、簡単な関数的処理にはインライン関数 (inline) を使う方が安全で推奨される場合が多いです。

マクロの未定義化 (#undef)

#define で定義されたマクロを無効化します。

#define DEBUG_MODE 1
// ... DEBUG_MODE を使ったコード ...

#undef DEBUG_MODE
// ここ以降、DEBUG_MODE は未定義となる

// #ifdef DEBUG_MODE // これは偽になる
// printf("Debug is still active?\n");
// #endif

条件付きコンパイル

特定の条件に基づいて、ソースコードの一部をコンパイル対象に含めたり除外したりします。デバッグコードの切り替え、環境依存コードの分離などに使われます。

  • #if 条件式
  • #ifdef マクロ名 (マクロが定義されていれば真)
  • #ifndef マクロ名 (マクロが定義されていなければ真)
  • #elif 条件式 (else if)
  • #else
  • #endif (ブロックの終わり)
  • defined(マクロ名): #if#elif 内でマクロが定義されているかチェックする演算子。
#define VERSION 2
#define ENABLE_LOGGING

int main() {

#if VERSION > 1
    printf("Using Version 2 or later features.\n");
#else
    printf("Using legacy features.\n");
#endif

#ifdef ENABLE_LOGGING
    printf("Logging is enabled.\n");
    // ログ出力コード
#endif

#ifndef MAX_USERS
    #define MAX_USERS 100 // MAX_USERSが未定義なら定義する
    printf("MAX_USERS defined as 100.\n");
#endif

#if defined(_WIN32) // Windows環境かどうか (一般的な定義済みマクロ)
    printf("Compiling for Windows.\n");
#elif defined(__linux__) // Linux環境かどうか
    printf("Compiling for Linux.\n");
#elif defined(__APPLE__) // macOS環境かどうか
     printf("Compiling for macOS.\n");
#else
    printf("Compiling for an unknown OS.\n");
#endif

    return 0;
}

ヘッダファイルの多重インクルード防止(インクルードガード)によく #ifndef / #define / #endif が使われます。

// my_header.h
#ifndef MY_HEADER_H // このマクロが未定義なら(初めてインクルードされたら)
#define MY_HEADER_H // マクロを定義する

// ヘッダファイルの内容
struct MyStruct { int data; };
void my_function(void);

#endif // MY_HEADER_H の終わり

または、より現代的な #pragma once ディレクティブも使われます(多くのコンパイラがサポート)。

// my_other_header.h
#pragma once // このヘッダを1回だけインクルードするようにコンパイラに指示

// ヘッダファイルの内容
// ...

エラー・警告指示 (#error, #warning)

  • #error メッセージ: コンパイルを停止し、指定したエラーメッセージを表示する。
  • #warning メッセージ: コンパイルを続行するが、指定した警告メッセージを表示する(コンパイラによってはサポートされない)。
#ifndef REQUIRED_FEATURE
    #error "Required feature macro is not defined! Compilation aborted."
#endif

#if PLATFORM_VERSION < 2
    #warning "Using an older platform version. Some features might be limited."
#endif

行番号制御 (#line)

コンパイラが報告するエラーメッセージなどの行番号とファイル名を変更します。コードジェネレータなどで使われることがあります。

// ... コード ...
#line 100 "generated_code.c"
// ここから下のコードのエラーは "generated_code.c" の 100行目以降として報告される
int generated_variable = 0; // この行は 100行目扱い
// ...

プラグマ指示 (#pragma)

コンパイラ固有の機能や動作を制御するための指示です。内容はコンパイラによって大きく異なります。#pragma once はその一例です。

// 例: 特定の警告を一時的に無効化 (コンパイラ依存)
#pragma warning(push)
#pragma warning(disable : 4996) // 非推奨関数の警告を無効化 (MSVCの例)
    strcpy(dest, src); // 本来なら警告が出るかもしれない strcpy
#pragma warning(pop)

定義済みマクロ

コンパイラや環境によって、事前に定義されているマクロがあります。これらを使うと、コード内でファイル名や行番号、コンパイル日時などを利用できます。

マクロ説明
__FILE__現在のソースファイル名 (文字列リテラル)
__LINE__現在のソースコードの行番号 (整数)
__DATE__コンパイルされた日付 ("Mmm dd yyyy" 形式の文字列リテラル)
__TIME__コンパイルされた時刻 ("hh:mm:ss" 形式の文字列リテラル)
__func__現在の関数名 (文字列リテラル, C99以降)
__STDC__コンパイラが標準Cに準拠している場合に 1 に定義される
__STDC_VERSION__サポートするC標準のバージョンを示す (例: 201112L for C11, C99以降)
#include <stdio.h>

void log_message(const char* message) {
    // __func__ は関数内でのみ利用可能
    printf("[%s %s %d] (%s): %s\n", __DATE__, __TIME__, __LINE__, __func__, message);
}

int main() {
    printf("Compiling file: %s\n", __FILE__);
    log_message("This is a log entry.");

#ifdef __STDC_VERSION__
    printf("C Standard Version: %ld\n", __STDC_VERSION__);
#else
    printf("C Standard Version not specified (likely pre-C99).\n");
#endif

    return 0;
}

13. その他Tips ✨

列挙型 (enum)

関連する整数定数に名前を付ける方法です。コードの可読性が向上します。

#include <stdio.h>

// 曜日の列挙型
typedef enum {
    SUNDAY,    // 0 (デフォルトで0から始まる)
    MONDAY,    // 1
    TUESDAY,   // 2
    WEDNESDAY, // 3
    THURSDAY,  // 4
    FRIDAY,    // 5
    SATURDAY   // 6
} DayOfWeek;

// 開始値を指定することも可能
typedef enum {
    RED = 10,
    GREEN,    // 11
    BLUE = 20,
    YELLOW    // 21
} ColorCode;

int main() {
    DayOfWeek today = WEDNESDAY;
    ColorCode sky_color = BLUE;

    if (today == SATURDAY || today == SUNDAY) {
        printf("It's weekend!\n");
    } else {
         printf("It's weekday (%d).\n", today); // today は 3
    }

    printf("Sky color code: %d\n", sky_color); // sky_color は 20

    return 0;
}

インライン関数 (inline) (C99以降)

関数呼び出しのオーバーヘッドを削減するために、コンパイラに関数本体を呼び出し元に展開するよう要求するヒントです。短い関数に適しています。最終的にインライン化されるかはコンパイラの判断によります。

#include <stdio.h>

// インライン関数の定義 (ヘッダファイルに書くことが多い)
static inline int min(int a, int b) {
    return (a < b) ? a : b;
}
// static inline にすると、この翻訳単位内でのみ有効な内部リンケージのインライン関数となることが多い

int main() {
    int x = 10, y = 5;
    int minimum = min(x, y); // ここが (x < y) ? x : y に展開されるかもしれない
    printf("Minimum of %d and %d is %d\n", x, y, minimum);
    return 0;
}

関数形式マクロよりも型安全で、デバッグもしやすい利点があります。

コメント

  • 単一行コメント: // コメント内容 (C99以降、多くのコンパイラが拡張機能としてC89でもサポート)
  • 複数行コメント: /* コメント内容 */ (C89からサポート)
// これは単一行コメントです
int main() {
    /*
       これは
       複数行にわたる
       コメントです。
    */
    int value = 10; // 変数の説明
    return 0; /* プログラム終了 */
}

注意: 複数行コメント /* ... */ はネストできません (/* コメント /* ネスト */ 終了 */ はエラー)。

より安全な関数の使用 (C11 Annex K – オプショナル)

C11標準では、バッファオーバーフローなどの問題を緩和するための、より安全なバージョンの関数群 (_s 接尾辞が付くもの) が提案されました (Annex K Bounds-checking interfaces)。しかし、これらの関数のサポートはコンパイラによって異なり、広く普及しているとは言えません。

  • strcpy_s()
  • strcat_s()
  • strncpy_s()
  • strncat_s()
  • sprintf_s()
  • scanf_s()
  • fopen_s()
  • gets_s() (getsはC11で廃止)
  • など

これらの関数は、バッファサイズを引数として明示的に取るなどの改良が加えられています。

#define __STDC_WANT_LIB_EXT1__ 1 // 安全な関数を使う意思表示 (コンパイラ依存)
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // errno_t のため (環境による)

int main() {
    char dest[10];
    char src[] = "This is too long";
    errno_t err; // エラーコードを受け取る型 (環境による)

    // strcpy_s の例 (MSVCなど)
    err = strcpy_s(dest, sizeof(dest), src);

    if (err == 0) {
        printf("strcpy_s successful: %s\n", dest);
    } else {
        printf("strcpy_s failed! Error code: %d\n", err);
        // エラー処理 (例: バッファが小さすぎる場合など)
        if (err == STRUNCATE) { // (エラーコードは環境依存)
             printf("Source string was truncated.\n");
             // destは空文字列などに設定される場合がある
        }
    }

    // scanf_s の例 (MSVCなど) - 文字列読み込み時にバッファサイズを指定
    char name[20];
    printf("Enter name (max 19 chars): ");
    // scanf("%19s", name); // 通常の scanf (サイズ指定は書式文字列内)
    int result = scanf_s("%19s", name, (unsigned)_countof(name)); // バッファサイズを引数で渡す (_countofはMSVC拡張)
                                                                   // 標準では sizeof(name) 相当
    if (result == 1) {
         printf("Hello, %s\n", name);
    }


    return 0;
}

推奨: Annex K 関数が利用できない環境も多いため、snprintf, strncpy (ヌル終端確認必須), strncat などを注意深く使うか、他の安全な文字列ライブラリを利用することが一般的です。

コメント

タイトルとURLをコピーしました