Contents
この記事の読み方と学習ロードマップ/C規格の要点(C11/C17/C23)
面接や実務で問われやすいC言語の主要トピックを、C11/C17/C23の規格差と実装依存を踏まえて整理します。短期〜長期の学習ロードマップと、主要コンパイラでのサポート状況や参照先も示します。実例とツール操作で検出・修正の流れが分かるようにしています。
用語定義
この節で本記事で使う主要語を簡潔に定義します。
- 未定義動作(UB):規格で定義されない振る舞いで、観測結果は保証されません。
- アトミック操作:分割不能な操作を保証するための機能で、stdatomic.hに依る実装が規定されています。
- アラインメント/パディング:オブジェクトの配置とフィールド間の隙間で、sizeofやoffsetofに影響します。
- VLA(可変長配列):実行時に長さが決まる自動領域の配列。C99で導入され、C11以降は実装依存の扱いとなることがあります。
C規格の要点(C11/C17/C23)
C11以降で導入・整理された機能と、実装差への注意点をまとめます。面接で規格差を問われたときは、規格名と実装状況を併記してください。
C11はメモリモデルとアトミック操作を導入し、並行処理に関する仕様を整備しました。C17は主に瑕疵修正で、C23でもいくつかの利便性向上や整理が行われています。VLAや
実装依存の機能と主要コンパイラの状況
主要コンパイラや標準ライブラリでの採用状況と確認方法の要点を示します。
- stdatomic.h / _Atomic:C11で導入。GCC/Clangは広くサポートしますが、一部ターゲットでは libatomic のリンクが必要となる場合があります。MSVC の C 言語モードでは stdatomic.h のサポートが限定的なため、Windows 向けには Interlocked 系 API や C++ の std::atomic を検討します。参照: https://en.cppreference.com/w/c/atomic
:C11 で定義されたスレッドAPIですが、標準ライブラリ実装(glibc 等)では長らく未実装あるいは限定実装でした。POSIX 環境では pthreads を使うのが実用上の選択です。参照: https://en.cppreference.com/w/c/thread - VLA(可変長配列):C99 で導入、C11 以降は実装依存扱いとなることがあります。コンパイラで STDC_NO_VLA マクロの有無を確認してください。GCC/Clang は一般にサポートしますが、MSVC はサポートしていません。参照: https://en.cppreference.com/w/c/language/array#Variable_length_arrays
- 実装チェック用マクロ:移植性を保つためにコンパイル時に STDC_NO_VLA や STDC_NO_THREADS 等のマクロを確認する習慣を付けてください。参照: C規格ドラフト https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
レベル別(初級/中級/上級)代表質問と模範解答テンプレ
ここでは初級〜上級ごとに、口頭テンプレ・短いコード例・実務解説を示します。コード例はコンパイル可能な最小例にしています。口頭では前提(型や環境)と境界条件を最初に述べる練習をしてください。
初級:配列・ポインタ・sizeof の基本
配列名とポインタの振る舞い、関数呼び出し時の配列の崩壊(decay)や sizeof と strlen の違いを説明します。
口頭の要点は「配列はオブジェクトであり sizeof で全体サイズが得られる。関数引数では配列はポインタに崩壊する(decay)ため sizeof はポインタサイズになる」と簡潔に述べます。
示例(配列とポインタ、関数呼び出し時の崩壊):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> void f(int *p) { printf("in f: sizeof(p)=%zu\n", sizeof(p)); } int main(void) { int a[10]; printf("sizeof(a)=%zu\n", sizeof(a)); /* 配列全体のバイト数 */ f(a); /* 関数呼び出しでは配列がポインタに崩壊する */ return 0; } |
示例(sizeof と文字列):
|
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #include <string.h> int main(void) { char *s = "hello"; /* 文字列リテラルは型 char[N] だが書き換えは未定義動作 */ char t[] = "hello"; /* 配列として初期化すれば書き換え可能 */ printf("sizeof(s)=%zu strlen(s)=%zu\n", sizeof(s), strlen(s)); printf("sizeof(t)=%zu strlen(t)=%zu\n", sizeof(t), strlen(t)); return 0; } |
実務解説:常に「変更しない文字列は const char * として宣言」すること、可変文字列が必要なら配列で初期化することを明示してください。
初級:短いトラップ問題(符号付き/符号無し比較)
短い判定問題で型変換を誤ると間違えます。以下は正しい答えと説明です。
問:int a = -1; if (a > (unsigned)1) はどう評価されるか。
答:条件は true になります。理由は、比較時に両辺で通常変換が適用され、符号なし型が含まれるため a は符号なしに変換され巨大な正数になり、(unsigned)1 より大きく評価されるためです。
確かめ用の最小例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> int main(void) { int a = -1; if (a > (unsigned)1) { puts("true"); } else { puts("false"); } printf("(unsigned)a = %u\n", (unsigned)a); return 0; } |
説明のポイント:符号付きと符号なしを混ぜた比較は、片方が符号なしならもう片方も符号なしに変換される(usual arithmetic conversions)ことを明示してください。参照: https://en.cppreference.com/w/c/language/conversion#Usual_arithmetic_conversions
中級:malloc/realloc と所有権・安全パターン
メモリ確保の失敗処理、realloc の安全な使い方、所有権の明示が面接で問われます。
示例(malloc の最小例):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> #include <stdlib.h> int main(void) { size_t n = 1024; char *buf = malloc(n); if (!buf) { perror("malloc"); return 1; } /* 使用 */ free(buf); return 0; } |
示例(realloc の安全パターン):
|
1 2 3 4 5 6 7 |
char *tmp = realloc(ptr, newsize); if (tmp == NULL) { /* realloc 失敗時の処理(元の ptr はそのまま) */ } else { ptr = tmp; } |
実務解説:所有権(誰が free するか)をAPI仕様に明記し、realloc は一時変数に受けて失敗に備える実装を必須と説明してください。
中級:構造体のパディングと offsetof
構造体のフィールド間のパディングは移植性に影響します。面接では offsetof と sizeof で確認する旨を述べると良いです。
示例(最小例):
|
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> #include <stddef.h> struct S { char c; int i; }; int main(void) { printf("offsetof i=%zu sizeof(S)=%zu\n", offsetof(struct S, i), sizeof(struct S)); return 0; } |
実務解説:通信プロトコルやファイルフォーマットでのレイアウト固定には明示的なエンコーディング(バイト順、パッキングルール)を用いる旨を述べてください。
上級:メモリモデルとアトミックの基本
上級では stdatomic とメモリオーダの意味・トレードオフを説明できる必要があります。性能と可視性のトレードオフを明確に説明してください。
示例(atomic の最小例):
|
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> #include <stdatomic.h> int main(void) { atomic_int ai = ATOMIC_VAR_INIT(0); atomic_fetch_add_explicit(&ai, 1, memory_order_relaxed); printf("%d\n", atomic_load_explicit(&ai, memory_order_relaxed)); return 0; } |
実務解説:初めは memory_order_seq_cst をデフォルトに説明し、性能要件がある場合に memory_order_acquire/release などを検討する旨を述べます。具体的には acquire-release はローカルな同期のために十分であるケースが多く、relaxed は順序を必要としない単純なカウンタ等で使われることを説明してください。参照: https://en.cppreference.com/w/c/atomic
上級:未定義動作の代表例と検出
面接では UB の具体例と検出方法を示して、なぜ危険かを説明することが求められます。符号付き整数のオーバーフロー、境界外アクセス、use-after-free 等が代表です。
示例(符号オーバーフローの最小例):
|
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> #include <limits.h> int main(void) { int x = INT_MAX; x += 1; /* 未定義動作 */ printf("%d\n", x); return 0; } |
検出方法:コンパイル時/実行時ツール(-Wall -Wextra、-fsanitize=undefined,address、Valgrind、clang-tidy 等)を使って検出し、UBSan の出力を提示して説明できるようにしてください。例として UBSan は実行時に "runtime error: signed integer overflow" のような診断を出します。
参照ツール例:gcc/clang の -fsanitize=address,undefined による検出、ThreadSanitizer(-fsanitize=thread)によるデータ競合検出。
低レイヤー基礎:ポインタ・動的メモリ・未定義動作・堅牢化
低レイヤーの理解は実務で最重要です。ここではポインタの説明作法、malloc/freeの落とし穴、UBの具体例、堅牢化の実務的対策を示します。
ポインタ(基本・二重間接参照・所有権)
説明では必ず「誰が free するか(所有権)」と「有効範囲」を最初に述べます。関数でポインタを割り当てる例と、呼び出し側の責任を示します。
示例(関数でポインタを変更する):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> #include <stdlib.h> void allocate_int(int **p) { *p = malloc(sizeof(int)); if (*p) *(*p) = 42; } int main(void) { int *p = NULL; allocate_int(&p); if (p) { printf("%d\n", *p); free(p); } return 0; } |
説明ポイント:API ドキュメントで所有権とエラー処理を明記することを強調してください。
malloc/free の落とし穴と検出・修正例
よくある落とし穴と実務的な検出手順を示します。
- よくあるミス:戻り値未チェック、realloc を直接代入して元を失う、use-after-free、double free。
- 検出手順:最小再現ケースを作る → -g でビルド → AddressSanitizer / UBSan / Valgrind で実行 → 修正 → 回帰テスト。
- 修正例:realloc は一時変数で受ける(前節参照)。
実装例(realloc 安全パターン):
|
1 2 3 4 5 6 7 |
char *tmp = realloc(ptr, newsize); if (!tmp) { /* realloc 失敗時の処理(ptr は不変) */ } else { ptr = tmp; } |
未定義動作(UB)の具体例と検出方法
UB の説明では「なぜ未定義なのか」と「現実にどんな悪影響が出るか」を示すことが重要です。検出にはサニタイザと静的解析を組み合わせます。
- 代表例:符号付き整数オーバーフロー、境界外アクセス、空ポインタ逆参照、use-after-free、シフト量超過、strict aliasing違反等。
- 検出ツール:AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer, Valgrind, clang-tidy。
- サニタイザ利用例(GCC/Clang):
- ビルド例: gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined -g main.c -o main
実行時の UBSan の出力はツールのバージョンや環境で変わりますが、"runtime error: signed integer overflow" のように原因箇所を示す診断が得られます。ログを CI に組み込み回帰検証する運用が効果的です。
バッファオーバーフローと堅牢化の実務対策
実務では入出力の境界チェックとツールを組み合わせて回避します。
- 入力長検査の徹底と、API選択(snprintf/fgets の活用)。
- 文字列関数の誤用(strncpy は終端を保証しない)に注意し、代替として snprintf や explicit length の関数を使う。
- コンパイラ保護(-fstack-protector)、ASLR、非実行メモリ領域の有効化を併用。
- CI に動的解析(ASan/TSan)と静的解析(clang-tidy)のパイプラインを組み込む。
データ構造・アルゴリズム・ビット演算の実戦問題
短時間で解ける問題と拡張設計のセットで面接準備をします。解答には計算量とテストケースを必ず示してください。
短時間問題(インプレース反転など)
代表的な速解問題と最小実装例を示します。計算量とメモリ制約を明示してください。
示例(単方向リストの反転):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <stdio.h> #include <stdlib.h> struct Node { int v; struct Node *next; }; struct Node *reverse(struct Node *head) { struct Node *prev = NULL, *cur = head; while (cur) { struct Node *nxt = cur->next; cur->next = prev; prev = cur; cur = nxt; } return prev; } int main(void) { struct Node *a = malloc(sizeof *a); struct Node *b = malloc(sizeof *b); a->v = 1; b->v = 2; a->next = b; b->next = NULL; struct Node *r = reverse(a); for (struct Node *p = r; p; p = p->next) printf("%d\n", p->v); free(b); free(a); return 0; } |
計算量:時間 O(n)、追加空間 O(1)。
拡張問題(設計・最適化)
メモリ制約や並列化を含む設計問題の着眼点を示します。
- 固定プール(フリーリスト)を用いたヒープ制御。
- 外部ソートの設計(メモリ不足時のチャンク化とマージ戦略)。
- マルチスレッド下でのデータ構造(ロック粒度、楽観的読み取り、lock-free の可否判断)。
設計説明では必ず「前提条件」「性能目標」「失敗時の挙動」を明示してください。
ビット演算・型変換の落とし穴
ビット演算は高速ですが型幅や符号の扱いに注意が必要です。常に無符号型で操作すること、シフト量が型幅未満であることを明示しましょう。
安全なビット操作の例:
|
1 2 3 4 5 6 7 8 9 10 |
static inline void set_bit(unsigned *flags, unsigned bit) { *flags |= (1u << bit); } static inline void clear_bit(unsigned *flags, unsigned bit) { *flags &= ~(1u << bit); } static inline int test_bit(unsigned flags, unsigned bit) { return (flags >> bit) & 1u; } |
qsort の比較関数は単純な減算で差を返すとオーバーフローの可能性があるため、三値化する実装を使ってください。
実務系問:並行処理・I/O・OS・組込み・コンパイラ最適化
並行処理、システムコール、組込み特有の制約、コンパイラ最適化に関する実務上の着眼点と検出手順を示します。
並行処理(pthread, atomic, race, deadlock)
並行処理の設問では、検出方法(TSan 等)と安全実装(mutex や atomic)を説明できることが重要です。
示例(pthread + mutex、最小例):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> #include <pthread.h> int cnt = 0; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; void *inc(void *arg) { for (int i = 0; i < 100000; ++i) { pthread_mutex_lock(&m); ++cnt; pthread_mutex_unlock(&m); } return NULL; } int main(void) { pthread_t t1, t2; pthread_create(&t1, NULL, inc, NULL); pthread_create(&t2, NULL, inc, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("cnt=%d\n", cnt); return 0; } |
TSan(ThreadSanitizer)はデータ競合を検出します。コンパイル例: gcc -fsanitize=thread -g -pthread main.c
volatile と同期の誤用と正しい実装
volatile は主にハードウェア I/O や signal handler で使うもので、スレッド間の同期を保証しません。同期には atomic や mutex を使ってください。
誤った例(volatile を同期に使う誤用):
|
1 2 3 4 5 |
volatile int flag = 0; int data = 0; /* writer */ data = 1; flag = 1; /* reader */ while (!flag) ; printf("%d\n", data); /* 順序が保証されない */ |
正しい例(atomic を使った release/acquire):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <stdatomic.h> #include <pthread.h> atomic_int flag = ATOMIC_VAR_INIT(0); int data = 0; void *writer(void *_) { data = 42; atomic_store_explicit(&flag, 1, memory_order_release); return NULL; } void *reader(void *_) { while (atomic_load_explicit(&flag, memory_order_acquire) == 0) { } printf("data=%d\n", data); return NULL; } int main(void) { pthread_t w, r; pthread_create(&w, NULL, writer, NULL); pthread_create(&r, NULL, reader, NULL); pthread_join(w, NULL); pthread_join(r, NULL); return 0; } |
説明ポイント:release/acquire のペアで書き込みデータの可視化が保証される旨を述べ、memory_order_seq_cst をデフォルトで選ぶ実務上の利点も示してください。
組込み/OS/I/O の実務的設問
組込みでは ISR 内で呼べる関数が限定され、動的割当てやブロッキングは避けるべきです。signal handler では async-signal-safe な関数のみを呼ぶ必要があります(sig_atomic_t をフラグに使う等)。
I/O 周りでは read/write の短期リターン(short read/write)や EINTR に備えたループ処理を実装すること、タイムアウト設計、エラー区別(致命 vs 一時)を示すと良いです。
模擬面接・ツール・教材・FAQ と要点まとめ
模擬面接の設計テンプレと、実務で有効なデバッグツールのワークフロー、よくあるFAQをまとめます。実践で使えるコマンド例や評価基準を提示します。
模擬面接テンプレ(時間配分・採点基準)
短時間試験の時間配分と採点観点を示します。
- 30分想定:問題理解 5分、設計 5分、実装 15分、テスト 5分。
- 採点観点(例):正しさ、境界条件対応、計算量、コード品質、説明力(各1〜5点)。
フィードバック例は「境界ケースの追加」「メモリ所有権を明確に」など具体的な指摘を行ってください。
デバッグ/解析ツールの実務フローとコマンド例
再現性確保→デバッグビルド→ローカル実行→サニタイザ/静的解析→修正→CI で再検証、の流れを推奨します。
- 推奨コンパイル警告: -Wall -Wextra -Wpedantic
- サニタイザ例(GCC/Clang):
- AddressSanitizer / UBSan: gcc -std=c11 -Wall -Wextra -fsanitize=address,undefined -g main.c -o main
- ThreadSanitizer: gcc -fsanitize=thread -g -pthread main.c
- Valgrind: valgrind --leak-check=full ./main
実行時の診断メッセージをログ化して CI に組み込み、回帰検出ルールを設ける運用が効果的です。
FAQ(短答)
- malloc が NULL を返したら? → 戻り値をチェックし、必要なら処理を縮小あるいは適切なエラー経路を設計する。
- volatile と atomic の違いは? → volatile は最適化挙動の抑制(主にデバイスIOやシグナルで有用)で、メモリ順序やデータ競合の防止はしない。atomic は競合を防ぎ、メモリオーダを扱える。
- signed/unsigned 比較で気をつける点は? → 比較時に符号なしへ変換されることがあるため、期待する比較結果にならないことがある。型を揃えるか明示的なキャストを使う。
参考教材・仕様・実装ドキュメント
規格・リファレンス・実装情報の代表的な参照先を示します。
- ISO C 標準(ドラフト): https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
- cppreference(言語機能 / stdatomic / threads 等): https://en.cppreference.com/w/c
- POSIX pthreads: https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_create.html
- コンパイラ対応状況(参考): https://en.cppreference.com/w/c/compiler_support
要点まとめ(面接直前チェック)
以下を短く説明できるようにしておくと有利です。
- 配列とポインタの違いと関数呼び出し時の配列崩壊(decay)。
- malloc/realloc/free の安全パターンと所有権の明示。
- 代表的な未定義動作(符号オーバーフロー、境界外アクセス、use-after-free)とサニタイザでの検出法。
- 構造体のパディングとアラインメントの確認方法(offsetof/sizeof)。
- スレッド同期(mutex/atomic)とデッドロック回避の基本。
- read/write の短い戻りと EINTR 処理の実装パターン。
- gdb/ASan/UBSan/TSan/Valgrind を組み合わせた解析フロー。
参照と実装差を明示し、口頭では「前提(型・環境)」「境界条件」「安全性の観点」を必ず述べる習慣をつけてください。