閉じる

【C言語】NULLと0の違いは?NULLポインタの正しい比較方法

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を「ヌルポインタ定数」として使うことが推奨されます。

参照例(コメントの定義はあくまで典型例であり、実機のヘッダによって異なります)。

C言語
// 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で初期化しておけば、使う前にチェックでき、安全性が上がります。

C言語
// 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は「ヌルポインタ定数」を表すマクロです。

整数ゼロはポインタに代入・比較されるとヌルポインタに変換されますが、意味としては別物です。

次の表は0NULL'\0'の違いをまとめたものです。

記号種類主な用途代表的な使い方注意点
0整数定数数値のゼロint i = 0;ポインタ比較に使うとヌルポインタに変換されるが、意味が曖昧になりやすい
NULLヌルポインタ定数(マクロ)何も指さないポインタを表すint *p = NULL;内部表現は実装依存。意味で使うこと
‘\0’ヌル文字(文字定数)文字列終端if (s[i] == '\0')ポインタではなく文字。NULLとは別物

比較ではNULLを使って可読性を上げる

ポインタを比較するとき、可読性の観点からNULLを使うのが推奨です。

0でも動作は同じですが、読む人に「これはヌルポインタを意図している」と即座に伝わることが重要です。

C言語
// 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つは役割が全く異なります。

C言語
// 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なら真」です。
C言語
// 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 (バグの原因)

次のようなミスは実行時バグに直結します。

C言語
// 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チェックをしましょう。

入力引数やライブラリ関数の戻り値は特に要注意です。

C言語
// 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で初期化し、使う直前に有効なアドレスを代入しましょう。

C言語
// 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)は安全です。

C言語
// 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番地に何かあるはず」と考えてアクセスするのは誤りです。

ヌルポインタは「どこにも指していない」ことを示す記号的な値と理解してください。

C言語
// do_not_deref_null.c
// 実行しないでください: 教材としての悪い例
/*
int *p = NULL;
*p = 123;       // 未定義動作。多くの環境でクラッシュ。
*/

NULLと0の混在で読みづらくなる

同じ意味でNULL0を混在させると、意図が読み取りにくくなります

プロジェクトのコーディング規約を決め、ポインタにはNULL、文字列終端には'\0'、数値には0という使い分けを徹底すると、レビューや保守が楽になります。

悪い例:

C言語
// bad_style.c
if (ptr == 0) { /* ... */ }   // ここはNULLの方が意図が明確
if (s[i] == NULL) { /* ... */ } // 文字列終端なら '
// bad_style.c
if (ptr == 0) { /* ... */ }   // ここはNULLの方が意図が明確
if (s[i] == NULL) { /* ... */ } // 文字列終端なら '\0' を使うべき
' を使うべき

良い例:

C言語
// 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」というルールを体に覚えさせるのがおすすめです。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!