C言語の学習ではポインタが最大の山場の1つです。
その中でも「何も指さない特別なポインタ値」であるNULLを正しく理解することは、安全で読みやすいコードを書く上で非常に重要です。
本記事ではNULLと0の違い、NULLポインタの正しい比較、そして初心者が陥りやすい落とし穴を、やさしい例とサンプルコードで丁寧に解説します。
C言語のNULLとは何か
NULLの意味
C言語におけるNULLは、「どの有効なオブジェクトや関数のアドレスも指していない」ことを表す特別なポインタ値です。
つまり、int *
やchar *
など、どのポインタ型でも「何も指していない」状態を表現できます。
注意したいのは、ヌルポインタの内部表現(ビット列)が必ずしも0とは限らないことです。
多くの環境では「アドレス0」に対応づけられますが、標準規格は「ヌルポインタはどの有効なポインタとも等しくならない特別な値である」ことのみを保証します。
したがって、NULL
はアドレス0という具体的な場所ではなく、「どこにも指していない」という意味論に注目して使うのが正解です。
NULLの定義場所と書き方
NULL
はマクロとして複数の標準ヘッダで定義されています。
代表的な定義元は<stddef.h>
で、<stdio.h>
や<stdlib.h>
、<string.h>
などをインクルードしても利用できます。
- 例: 一般的な実装では
NULL
は((void*)0)
または0
として定義されています(実装依存)。 - 重要: プログラマは内部定義に依存せず、常に
NULL
を「ヌルポインタ定数」として使うことが推奨されます。
参照例(コメントの定義はあくまで典型例であり、実機のヘッダによって異なります)。
// null_where_defined.c
// NULLが使える代表的なヘッダをインクルード
#include <stddef.h> // 最も素直な定義元
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
// どのヘッダを通してもNULLは使える
int *p = NULL; // 何も指さない
(void)p;
puts("NULLは<stddef.h>や<stdio.h>などをインクルードすると使えます。");
return 0;
}
NULLは<stddef.h>や<stdio.h>などをインクルードすると使えます。
ポインタの初期化にNULLを使う
未初期化ポインタを放置するのは危険です。
宣言時にNULL
で初期化しておけば、使う前にチェックでき、安全性が上がります。
// null_init.c
#include <stdio.h>
int main(void) {
int *p = NULL; // 安全な初期化
int x = 42;
if (p == NULL) {
puts("pはNULLです(まだ何も指していません)。");
}
// 必要になったら有効なアドレスを代入
p = &x;
if (p != NULL) {
printf("pが指す値は%dです。\n", *p);
}
return 0;
}
pはNULLです(まだ何も指していません)。
pが指す値は42です。
このように「宣言時はNULL、使う直前に有効化」という流れにすると、参照前チェックも一貫し、バグを防ぎやすくなります。
C言語のNULLと0の違い
0は整数、NULLはヌルポインタ
C言語では0
(ゼロ)は「整数定数」です。
一方NULL
は「ヌルポインタ定数」を表すマクロです。
整数ゼロはポインタに代入・比較されるとヌルポインタに変換されますが、意味としては別物です。
次の表は0
、NULL
、'\0'
の違いをまとめたものです。
記号 | 種類 | 主な用途 | 代表的な使い方 | 注意点 |
---|---|---|---|---|
0 | 整数定数 | 数値のゼロ | int i = 0; | ポインタ比較に使うとヌルポインタに変換されるが、意味が曖昧になりやすい |
NULL | ヌルポインタ定数(マクロ) | 何も指さないポインタを表す | int *p = NULL; | 内部表現は実装依存。意味で使うこと |
‘\0’ | ヌル文字(文字定数) | 文字列終端 | if (s[i] == '\0') | ポインタではなく文字。NULLとは別物 |
比較ではNULLを使って可読性を上げる
ポインタを比較するとき、可読性の観点からNULL
を使うのが推奨です。
0
でも動作は同じですが、読む人に「これはヌルポインタを意図している」と即座に伝わることが重要です。
// style_compare.c
#include <stdio.h>
int main(void) {
int *p = NULL;
if (p == NULL) { // 明確: ヌルポインタを比較している
puts("p == NULL は読みやすい比較です。");
}
if (p == 0) { // 動作は同じだが、意図がやや曖昧
puts("p == 0 も動くが、NULLの方が意図が明確です。");
}
if (!p) { // 否定の簡略形: pがNULLなら真
puts("!p は p == NULL と同じ意味です。");
}
if (p) { // 非NULLなら真
puts("これは表示されません。");
}
return 0;
}
p == NULL は読みやすい比較です。
p == 0 も動くが、NULLの方が意図が明確です。
!p は p == NULL と同じ意味です。
‘\0’とNULLの違い
'\0'
はヌル文字で、文字列の終端(終わり)を示す文字です。
ポインタではありません。
対してNULL
はポインタの「何も指していない」状態です。
この2つは役割が全く異なります。
// nul_char_vs_nullptr.c
#include <stdio.h>
int main(void) {
char s[] = "Hi"; // 末尾は自動で '// nul_char_vs_nullptr.c
#include <stdio.h>
int main(void) {
char s[] = "Hi"; // 末尾は自動で '\0' が入る
char c = '\0'; // ヌル文字(値は0)
if (s[2] == '\0') {
puts("sの3文字目はヌル文字(終端)です。");
}
if (c == '\0') {
puts("cはヌル文字です(ポインタではありません)。");
}
char *p = NULL; // ヌルポインタ
if (p == NULL) {
puts("pはヌルポインタです(文字ではありません)。");
}
return 0;
}
' が入る
char c = '// nul_char_vs_nullptr.c
#include <stdio.h>
int main(void) {
char s[] = "Hi"; // 末尾は自動で '\0' が入る
char c = '\0'; // ヌル文字(値は0)
if (s[2] == '\0') {
puts("sの3文字目はヌル文字(終端)です。");
}
if (c == '\0') {
puts("cはヌル文字です(ポインタではありません)。");
}
char *p = NULL; // ヌルポインタ
if (p == NULL) {
puts("pはヌルポインタです(文字ではありません)。");
}
return 0;
}
'; // ヌル文字(値は0)
if (s[2] == '// nul_char_vs_nullptr.c
#include <stdio.h>
int main(void) {
char s[] = "Hi"; // 末尾は自動で '\0' が入る
char c = '\0'; // ヌル文字(値は0)
if (s[2] == '\0') {
puts("sの3文字目はヌル文字(終端)です。");
}
if (c == '\0') {
puts("cはヌル文字です(ポインタではありません)。");
}
char *p = NULL; // ヌルポインタ
if (p == NULL) {
puts("pはヌルポインタです(文字ではありません)。");
}
return 0;
}
') {
puts("sの3文字目はヌル文字(終端)です。");
}
if (c == '// nul_char_vs_nullptr.c
#include <stdio.h>
int main(void) {
char s[] = "Hi"; // 末尾は自動で '\0' が入る
char c = '\0'; // ヌル文字(値は0)
if (s[2] == '\0') {
puts("sの3文字目はヌル文字(終端)です。");
}
if (c == '\0') {
puts("cはヌル文字です(ポインタではありません)。");
}
char *p = NULL; // ヌルポインタ
if (p == NULL) {
puts("pはヌルポインタです(文字ではありません)。");
}
return 0;
}
') {
puts("cはヌル文字です(ポインタではありません)。");
}
char *p = NULL; // ヌルポインタ
if (p == NULL) {
puts("pはヌルポインタです(文字ではありません)。");
}
return 0;
}
sの3文字目はヌル文字(終端)です。
cはヌル文字です(ポインタではありません)。
pはヌルポインタです(文字ではありません)。
‘\0’ と NULL を取り違えると、バグやクラッシュにつながります。
文字列処理では'\0'
、ポインタ比較ではNULL
を使い分けましょう。
NULLポインタの正しい比較方法
ifとif
ここでは、2つのよく使われる書き方を整理します。
if (p)
とif (p != NULL)
は同じ意味です。どちらも「pが非NULLなら真」です。if (!p)
とif (p == NULL)
も同じ意味で、「pがNULLなら真」です。
// equivalence.c
#include <stdio.h>
static void check(const int *p) {
if (p) {
puts("if (p): 非NULLです。");
} else {
puts("if (p): NULLです。");
}
if (p != NULL) {
puts("if (p != NULL): 非NULLです。");
} else {
puts("if (p != NULL): NULLです。");
}
}
int main(void) {
int x = 10;
int *a = &x;
int *b = NULL;
puts("aのチェック(非NULL):");
check(a);
puts("\nbのチェック(NULL):");
check(b);
return 0;
}
aのチェック(非NULL):
if (p): 非NULLです。
if (p != NULL): 非NULLです。
bのチェック(NULL):
if (p): NULLです。
if (p != NULL): NULLです。
プロジェクト内では、スタイルを統一することが大切です。
初心者のうちはif (p == NULL)
やif (p != NULL)
の「明示的な書き方」を選ぶと、読み間違いが減らせます。
ifとifの意味と注意
省略形if (p)
や否定形if (!p)
は便利ですが、「代入と比較の取り違え」に特に注意してください。
- 比較:
p == NULL
- 代入:
p = NULL
(バグの原因)
次のようなミスは実行時バグに直結します。
// pitfall_assignment_vs_comparison.c
#include <stdio.h>
int main(void) {
int *p = (int*)0x1234; // 例示のためのダミー(実際に使ってはいけません)
// if (p = NULL) { ... } // ← 間違い: 代入してしまい、代入した値が0以外なら常に真
// 正しくは:
if (p == NULL) {
puts("pはNULLです。");
} else {
puts("pはNULLではありません。");
}
return 0;
}
さらに、複雑な条件式では!
の適用範囲を読み間違えやすくなります。
例えばif (!p && cond)
は「pがNULLかつcondが真」を意味します。
意図が伝わりにくいと感じたら、if (p == NULL && cond)
のように明示的な比較で可読性を確保してください。
なお、関数ポインタであってもNULL
は使えます。
比較や初期化の考え方はデータポインタと同じです。
参照前のNULLチェック
ポインタを間接参照(*p
)する前には、必ずNULLチェックをしましょう。
入力引数やライブラリ関数の戻り値は特に要注意です。
// null_check_before_deref.c
#include <stdio.h>
void print_first_char(const char *s) {
if (s == NULL) {
puts("引数がNULLです。処理を中止します。");
return;
}
if (s[0] == '// null_check_before_deref.c
#include <stdio.h>
void print_first_char(const char *s) {
if (s == NULL) {
puts("引数がNULLです。処理を中止します。");
return;
}
if (s[0] == '\0') {
puts("空文字列です。");
return;
}
printf("先頭文字は '%c' です。\n", s[0]);
}
int main(void) {
print_first_char(NULL); // 安全に弾く
print_first_char(""); // 空文字列
print_first_char("C-lang"); // 通常ケース
return 0;
}
') {
puts("空文字列です。");
return;
}
printf("先頭文字は '%c' です。\n", s[0]);
}
int main(void) {
print_first_char(NULL); // 安全に弾く
print_first_char(""); // 空文字列
print_first_char("C-lang"); // 通常ケース
return 0;
}
引数がNULLです。処理を中止します。
空文字列です。
先頭文字は 'C' です。
「NULLかどうか」と「空かどうか」は別問題です。
前者はポインタの有効性、後者はデータ内容の問題で、チェック条件が異なります。
初心者が避けたい落とし穴
未初期化ポインタを使わない
宣言だけして値を入れていないポインタは、不定のアドレスを持ちます。
これを参照すると未定義動作になります。
必ずNULL
で初期化し、使う直前に有効なアドレスを代入しましょう。
// uninitialized_pointer.c
#include <stdio.h>
int main(void) {
int *p = NULL; // まずNULLで初期化
int v = 100;
// ... 条件が整ったら有効なアドレスを設定
p = &v;
printf("*p = %d\n", *p); // ここで初めて安全に参照できる
return 0;
}
*p = 100
free後はポインタをNULLにする
動的メモリをfree
したあと、ポインタはダングリングポインタ(解放済み領域を指す)になります。
そのまま使うと危険です。
必ずNULL
を代入して無効化しましょう。
なおfree(NULL)
は安全です。
// free_to_null.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = (int*)malloc(sizeof(int));
if (p == NULL) {
puts("メモリ確保に失敗しました。");
return 1;
}
*p = 7;
printf("割り当てた値: %d\n", *p);
free(p); // 解放
p = NULL; // 無効化(ダングリング対策)
// 二重解放を防げる。free(NULL)は安全に何もしない。
free(p);
puts("free後はpをNULLに戻すのが安全です。");
return 0;
}
割り当てた値: 7
free後はpをNULLに戻すのが安全です。
アドレス0へのアクセスは不可
NULLを参照または書き込みすると未定義動作です。
多くのOSで実行時エラー(セグメンテーション違反など)になります。
「0番地に何かあるはず」と考えてアクセスするのは誤りです。
ヌルポインタは「どこにも指していない」ことを示す記号的な値と理解してください。
// do_not_deref_null.c
// 実行しないでください: 教材としての悪い例
/*
int *p = NULL;
*p = 123; // 未定義動作。多くの環境でクラッシュ。
*/
NULLと0の混在で読みづらくなる
同じ意味でNULL
と0
を混在させると、意図が読み取りにくくなります。
プロジェクトのコーディング規約を決め、ポインタにはNULL
、文字列終端には'\0'
、数値には0
という使い分けを徹底すると、レビューや保守が楽になります。
悪い例:
// bad_style.c
if (ptr == 0) { /* ... */ } // ここはNULLの方が意図が明確
if (s[i] == NULL) { /* ... */ } // 文字列終端なら '// bad_style.c
if (ptr == 0) { /* ... */ } // ここはNULLの方が意図が明確
if (s[i] == NULL) { /* ... */ } // 文字列終端なら '\0' を使うべき
' を使うべき
良い例:
// good_style.c
if (ptr == NULL) { /* ... */ }
if (s[i] == '// good_style.c
if (ptr == NULL) { /* ... */ }
if (s[i] == '\0') { /* ... */ }
') { /* ... */ }
型と目的に合った記号を選ぶことが、読みやすさと安全性の両立につながります。
まとめ
NULLは「何も指さないポインタ」を表す特別な値で、整数の0
やヌル文字の'\0'
とは意味が異なります。
比較や初期化ではNULL
を使うことで意図が明確になり、参照前のNULLチェックを徹底すれば多くのクラッシュを未然に防げます。
未初期化ポインタを避け、free後は必ずNULLを代入し、NULL参照は絶対に行わないという基本を守りましょう。
初心者のうちはif (p == NULL)
やif (p != NULL)
のように明示的な比較を使い、「ポインタにはNULL、文字終端には’\0’、数値には0」というルールを体に覚えさせるのがおすすめです。