よくある落とし穴と安全なコーディング
C言語の落とし穴は道路の穴のようなものです——無害に見えますが、踏み込むとプログラムがクラッシュします。これらの穴を認識して避けて通れるようになりましょう。
演算子優先順位の落とし穴
C言語には15レベルの演算子優先順位があり、一部の組み合わせは間違いを起こしやすいです:
最もよくある優先順位の間違い
int *p = malloc(10 * sizeof(int));
if (p == NULL)
これは間違いではありませんが、次は間違いです:
int a = 1, b = 2, c = 3;
int result = a & b == 0;
==は&より優先順位が高いため、これは実際にはa & (b == 0)、つまり1 & 0 = 0となり、(a & b) == 0ではありません。
優先順位クイックリファレンス(間違いやすいケース)
| 落とし穴の式 | 実際の意味 | 意図した意味 | 修正 |
|---|---|---|---|
a & b == 0 |
a & (b == 0) |
(a & b) == 0 |
括弧を追加 |
a << 2 + 1 |
a << (2+1) |
(a<<2)+1 |
括弧を追加 |
*p++ |
*(p++) |
(*p)++ |
括弧を追加 |
a | b + c |
a | (b+c) |
(a|b)+c |
括弧を追加 |
配列の境界外アクセス
C言語は配列のインデックスをチェックしません——境界外アクセスは未定義動作です。ゴミ値を読む、クラッシュする、または「たまたま動く」(これが最も危険)可能性があります。
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 100;
arr[-1] = 99;
どちらの行も境界外書き込みです。コンパイラは文句を言いませんが、他の変数を破壊したりクラッシュしたりする可能性があります。
古典的な境界外:ループのオフバイワン
int arr[5];
for (int i = 0; i <= 5; i++) {
arr[i] = 0;
}
i <= 5はi < 5であるべきです。このオフバイワンエラーによりarr[5]が書き込まれます。
文字列の境界外
char buf[5];
strcpy(buf, "Hello, World!");
bufは5バイトの領域しかありませんが、文字列には14バイト('\0'を含む)が必要です——典型的なバッファオーバーフローです。
ダングリングポインタ
既に解放されたメモリをまだ参照しているポインタです:
int *create_value(void) {
int x = 42;
return &x;
}
xはローカル変数です。関数が返るとスタックフレームが解放され、&xはダングリングポインタになります。これにアクセスするとゴミ値が読まれる可能性があります。
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);
free(p)の後、pが指すメモリは返却されています。pを通じてアクセスするのは未定義動作です。
free(p); p = NULL;。これにより、後で誤って使用しても、追跡困難なランダムエラーよりも少なくともセグフォルトで気づけます。
メモリリーク
確保されたメモリが解放されないことです:
void leak_example(void) {
int *p = malloc(100 * sizeof(int));
if (p == NULL) return;
if (some_error) return;
free(p);
}
some_errorが真のとき、関数は早期リターンし、free(p)がスキップされ、100個のint分のメモリがリークします。
正しいバージョン
void no_leak(void) {
int *p = malloc(100 * sizeof(int));
if (p == NULL) return;
if (some_error) {
free(p);
return;
}
free(p);
}
未定義動作
未定義動作(UB)はC言語で最も危険な概念です——規格は「動作は未定義」と言い、何が起こってもよいことを意味します。
よくある未定義動作
| 動作 | 説明 |
|---|---|
| 配列の境界外アクセス | インデックスが配列の範囲を超える |
| ヌルポインタの間接参照 | *NULL |
| ダングリングポインタの使用 | 解放済みメモリへのアクセス |
| 符号付き整数のオーバーフロー | INT_MAX + 1 |
| ビット幅を超えるシフト | 1 << 100 |
| 1つの式で同じ変数を2回変更 | i = i++ + 1 |
| ゼロ除算 | int x = 1 / 0 |
| 初期化されていない変数の読み取り | int x; printf("%d", x); |
例
i = i++ + 1の落とし穴:
#include <stdio.h>
int main(void) {
int i = 3;
i = i++ + 1;
printf("%d\n", i);
return 0;
}
コンパイラによって4、5、またはその他の値が出力される可能性があります。i++がiを変更し、代入もiを変更するため、1つの文で同じ変数を2回変更することは未定義動作です。
バッファオーバーフローの防止
バッファオーバーフローはC言語で最も有名なセキュリティ脆弱性です。1988年のモリスワームはバッファオーバーフロー攻撃を利用しました。
危険な関数と安全な代替
| 危険な関数 | 問題 | 安全な代替 |
|---|---|---|
gets(buf) |
長さ制限なし | fgets(buf, size, stdin) |
strcpy(dst, src) |
宛先サイズの確認なし | strncpy(dst, src, size-1) |
sprintf(buf, fmt, ...) |
宛先サイズの確認なし | snprintf(buf, size, fmt, ...) |
strcat(dst, src) |
残り容量の確認なし | strncat(dst, src, size-strlen(dst)-1) |
scanf("%s", buf) |
入力長の制限なし | scanf("%99s", buf)またはscanf_s |
安全な関数の詳細
strncpy
char *strncpy(char *dest, const char *src, size_t n);
最大n文字をコピーします。srcがn文字未満の場合、destの残りは'\0'で埋められます。しかし、srcがちょうどn文字の場合、destは自動的にヌル終端されません!
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[6];
strncpy(buf, "Hello World", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
printf("%s\n", buf);
return 0;
}
Hello
strncpyは自動的に'\0'を追加しません!コピー後に手動で追加する必要があります。そうしないと、後続の文字列操作が境界外になる可能性があります。
snprintf
int snprintf(char *str, size_t size, const char *format, ...);
最大size-1文字を書き込み、自動的に'\0'を追加します。
#include <stdio.h>
int main(void) {
char buf[10];
int n = snprintf(buf, sizeof(buf), "Value is %d", 12345);
printf("出力: \"%s\"\n", buf);
printf("必要な長さ: %d, 実際の容量: %d\n", n, (int)sizeof(buf));
return 0;
}
出力: "Value is 1"
必要な長さ: 14, 実際の容量: 10
例
安全な入力処理パターン:
#include <stdio.h>
#include <string.h>
int main(void) {
char name[32];
printf("名前を入力してください: ");
if (fgets(name, sizeof(name), stdin) == NULL) {
fprintf(stderr, "入力に失敗しました\n");
return 1;
}
name[strcspn(name, "\n")] = '\0';
if (strlen(name) == 0) {
fprintf(stderr, "名前は空にできません\n");
return 1;
}
printf("こんにちは、%sさん!\n", name);
return 0;
}
fgets + strcspnで改行を削除するのが、ユーザー入力を処理する標準的な安全パターンです。getsは決して使わないでください。
例
安全な文字列連結:
#include <stdio.h>
#include <string.h>
int main(void) {
char path[256] = "/home/user";
const char *subdir = "/documents/work/projects/2024";
size_t current_len = strlen(path);
size_t remaining = sizeof(path) - current_len - 1;
if (strlen(subdir) < remaining) {
strncat(path, subdir, remaining);
} else {
fprintf(stderr, "パスが長すぎます、連結できません\n");
return 1;
}
printf("パス: %s\n", path);
return 0;
}
パスが長すぎます、連結できません
安全なscanfの使い方
scanfは初心者が最もよく使う入力関数であり、同時に最も危険でもあります:
char buf[10];
scanf("%s", buf);
ユーザーが9文字以上入力するとオーバーフローします。安全なバージョン:
char buf[10];
scanf("%9s", buf);
またはC11をサポートするコンパイラでscanf_sを使います:
char buf[10];
scanf_s("%9s", buf, (unsigned)sizeof(buf));
scanf_sはC11のオプション附属書Kの一部です。MSVCはサポートしていますが、gcc/clangはサポートしていない場合があります。クロスプラットフォームのコードではfgets + sscanfの組み合わせを推奨します。
整数のオーバーフロー
符号付き整数のオーバーフローは未定義動作です:
#include <stdio.h>
#include <limits.h>
int main(void) {
int a = INT_MAX;
int b = a + 1;
printf("%d + 1 = %d\n", a, b);
return 0;
}
出力は必ずしもINT_MINではありません——コンパイラはこの加算を最適化で削除する可能性があります。
安全な確認方法:
#include <limits.h>
int safe_add(int a, int b) {
if (a > 0 && b > INT_MAX - a) return 0;
if (a < 0 && b < INT_MIN - a) return 0;
return a + b;
}
その他のよくある落とし穴
配列パラメータでのsizeof
void func(int arr[]) {
printf("%zu\n", sizeof(arr));
}
配列が関数パラメータとして渡されると、ポインタに崩壅します。sizeof(arr)は配列サイズではなくポインタサイズ(4または8)を返します。長さは別のパラメータとして渡す必要があります。
マクロ定義の落とし穴
#define SQUARE(x) x * x
SQUARE(2 + 3)
これは2 + 3 * 2 + 3 = 11に展開され、25になりません。正しいバージョン:
#define SQUARE(x) ((x) * (x))
しかしSQUARE(i++)にはまだ問題があります(iが2回インクリメントされる)。インライン関数の方が安全です。
==と=
if (x = 5) {
}
これは比較ではなく代入です!代入式の値は5(非ゼロ)なので、条件は常に真になります。一部のコンパイラは警告を出しますが、すべてでこの警告が有効になっているわけではありません。
❓ よくある質問
valgrind --leak-check=full ./program。WindowsではVisual StudioのデバッグヒープやAddressSanitizerが使えます。-fsanitize=addressでコンパイルして有効にします。境界外アクセス、解放後使用、メモリリークなどの問題を検出できます。📖 まとめ
- 演算子優先順位に迷ったら括弧を追加——最も簡単な防御策
- 配列境界外アクセスとバッファオーバーフローはC言語で最もよくあるセキュリティ問題
getsの代わりにfgets、sprintfの代わりにsnprintf、strcpyの代わりにstrncpyを使う- 未定義動作は「たまたま動く」可能性があるが、コンパイラや最適化レベルによって動作が異なる場合がある
- free後にポインタをNULLに設定、mallocの戻り値を確認、符号付きオーバーフローに注意
📝 練習問題
- オーバーフローせず、常にヌル終端する安全な文字列コピー関数
safe_strcpy(char *dst, size_t dst_size, const char *src)を作成してください - 意図的に配列境界外アクセスと整数オーバーフローを起こすプログラムを作成し、AddressSanitizerでコンパイル・実行して出力を観察してください
- 次のコードを監査し、すべてのセキュリティ問題を見つけて修正してください:
char buf[8]; gets(buf); sprintf(buf, "Result: %d", value);



