6日でできる 新C言語入門|NULLポインタへのアクセス

以下に、記事としてそのまま使いやすい形で作成しました。

ポインタは便利だからこそ、安全確認が大切。
NULLポインタへのアクセスを防いで、クラッシュしにくいC言語プログラムを書こう。

C言語でポインタを使うと、変数のアドレスを扱ったり、関数の中から呼び出し元の変数を書き換えたりできます。ポインタはとても強力な仕組みですが、そのぶん注意して扱わなければならない場面もあります。

その代表的なものが、NULLポインタへのアクセスです。

NULLポインタとは、どの有効なメモリ領域も指していないポインタのことです。つまり、このポインタは今どこも指していません、という状態を表します。

たとえば、ポインタ変数をまだ使う予定がないときや、メモリ確保に失敗したとき、解放後に安全な状態へ戻したいときなどにNULLを使います。

しかし、NULLのままのポインタに対して、*pのように値を読み書きしようとすると危険です。参照先が存在しないため、実行時エラーやアクセス違反、プログラムの異常終了につながることがあります。

C言語では、ポインタがどこを指しているかをプログラマがきちんと管理する必要があります。便利な反面、間違った場所を参照しないように気をつけることが大切です。

この記事では、NULLポインタとは何か、未初期化ポインタとの違い、NULLポインタへアクセスすると何が起こるのか、NULLチェックで安全に扱う方法、よくあるミスと防ぎ方を、サンプルプログラムを使ってやさしく解説していきます。

NULLポインタとは何か

NULLポインタは、どの有効なメモリ領域も指していないポインタです。

ポインタ変数は、本来どこかの変数や配列、確保したメモリ領域のアドレスを保存します。しかし、まだ有効なアドレスを持っていない状態を表したいことがあります。そのときにNULLを代入します。

int* p = NULL;

このpは、int型の値を指すポインタとして宣言されていますが、今はどのint型変数も指していません。

用語意味
有効なポインタ変数や配列など、実際に存在するメモリ領域を指しているint* p = &value;
NULLポインタどこも指していないことを明示しているint* p = NULL;
未初期化ポインタ何が入っているか分からない危険な状態int* p;

NULLポインタと未初期化ポインタは、似ているようで大きく違います。

NULLポインタは、どこも指していないと明示された状態です。
未初期化ポインタは、何を指しているか分からない状態です。

未初期化ポインタには、たまたまどこかのアドレスのような値が入っていることがあります。そのまま使うと、意図しないメモリを参照してしまう可能性があります。

そのため、ポインタ変数をすぐに有効なアドレスで初期化しない場合は、NULLで初期化する習慣をつけると安全です。

NULLは安全な印だが、アクセスしてよい場所ではない

NULLは、どこも指していないことを表すための値です。

ここで大切なのは、NULLは安全にアクセスできる場所ではないということです。

NULLを入れておけば、ポインタが未初期化のまま不明な場所を指す危険を避けやすくなります。しかし、NULLのまま*pのように参照してよいわけではありません。

状態してよいことしてはいけないこと
p == NULLNULLかどうか確認する*pで値を読む
p == NULL別の有効なアドレスを代入する*pで値を書き込む
p != NULL指す先の値を扱える型が違う使い方をする

NULLは、まだ使えませんという目印です。
そのため、ポインタを使う前には、NULLではないかを確認することが大切です。

図:NULLポインタはどこも指していない状態

この図から分かること

この図から分かるのは、有効なポインタとNULLポインタの違いです。

有効なポインタは、実際に存在する変数やメモリ領域を指しています。一方、NULLポインタはどこも指していません。そのため、NULLポインタを使って値を読み書きしようとすると、参照先が存在せず、実行時エラーの原因になります。

NULLポインタへアクセスすると危険

NULLポインタに対して、*pのように書いて値を読み書きすることを、NULLポインタの逆参照といいます。

たとえば、次のような処理は危険です。

int* p = NULL;
*p = 10;

pはどこも指していません。
それにもかかわらず*p = 10;を実行すると、存在しない場所へ値を書き込もうとすることになります。

このような処理は、環境によって次のような結果になります。

表示される可能性がある内容意味
アクセス違反許可されていないメモリへアクセスした
Segmentation Fault不正なメモリ参照が発生した
実行時エラー実行中に危険な処理が検出された
プログラムの異常終了OSや実行環境によって強制終了された

NULLポインタへのアクセスは、C言語の文法として書けてしまう場合があります。しかし、実行すると危険な動作になります。

そのため、ポインタを使う前にNULLチェックを行うことが重要です。

NULLポインタへのアクセス例

次のプログラムは、NULLポインタへアクセスしようとする危険な例です。

このコードは、ポインタの危険性を理解するための学習例です。通常の学習では、危険な行をコメントアウトしたまま確認するか、安全な例で動作を確認しましょう。

プロジェクト/ファイル名: Lesson52_1/main.c

#include <stdio.h>

int main(void) {
    double* pricePtr = NULL; // double型のポインタをNULLで初期化する

    printf("pricePtrはNULLで初期化されています。\n");

    // 次の行を実行すると、NULLポインタへ書き込もうとして危険です。
    // 学習時はコメントアウトしたままにしてください。
    // *pricePtr = 1980.5;

    // 次の行も、NULLポインタから値を読もうとするため危険です。
    // printf("価格: %.2f\n", *pricePtr);

    printf("NULLポインタには、値の読み書きをしてはいけません。\n");

    return 0;
}

実行結果

pricePtrはNULLで初期化されています。
NULLポインタには、値の読み書きをしてはいけません。

このプログラムでは、pricePtrをNULLで初期化しています。

double* pricePtr = NULL;

pricePtrはどこも指していない状態です。

そのため、次のような処理は危険です。

*pricePtr = 1980.5;

この処理は、pricePtrが指している先へ1980.5を書き込むという意味です。しかし、pricePtrはNULLなので、書き込み先がありません。

また、次のように値を読むことも危険です。

printf("価格: %.2f\n", *pricePtr);

NULLポインタは読み込み先も持っていません。書き込みだけでなく、読み込みも危険です。

Visual Studioでは、このようなコードに対してNULLポインターを逆参照しているという警告が表示される場合があります。警告が出た場合は、実行する前にコードを見直しましょう。

NULLポインタへのアクセスがエラーになる理由

ポインタは、メモリ上の場所を保存する変数です。

有効なポインタであれば、ポインタが指す先に実際の変数や配列があります。そのため、*pで値を読んだり、*p = 10;で値を書き込んだりできます。

しかし、NULLポインタはどこも指していません。

ポインタの状態*pでアクセスした場合
有効なアドレスを持っている指す先の値を読み書きできる
NULLである参照先がないため危険
未初期化であるどこを指すか不明で危険
解放済みメモリを指しているすでに使えない場所なので危険

NULLポインタに対して*pを使うと、存在しない場所へアクセスしようとします。

多くのOSや実行環境では、このようなアクセスを不正なメモリアクセスとして扱い、プログラムを停止させます。これは、ほかの重要なメモリ領域を壊さないための安全機構でもあります。

NULLチェックで安全に防ぐ

NULLポインタへのアクセスを防ぐ基本は、使う前にNULLでないことを確認することです。

if (p != NULL) {
    printf("%d\n", *p);
}

このように書けば、pが有効なアドレスを持っているときだけ*pを使います。

pがNULLの場合は、値を参照せず、別の処理を行います。

条件処理
p != NULL指す先の値を使う
p == NULL値を参照せず、エラーメッセージなどを表示する

この確認を習慣にしておくと、NULLポインタによる実行時エラーを防ぎやすくなります。

図:NULLチェックで危険な参照を防ぐ

この図から分かること

この図から分かるのは、ポインタを使う前にNULLチェックを入れることで、危険な参照を避けられるということです。

pがNULLでなければ、*pで値を参照できます。pがNULLなら、値を読まずにメッセージ表示などの安全な処理へ進みます。NULLチェックは、ポインタを安全に扱うための基本です。

NULLチェックを行う安全なプログラム

次のプログラムでは、ポインタがNULLかどうかを確認してから値を表示します。

プロジェクト/ファイル名: Lesson52_2/main.c

#include <stdio.h>

// ポインタが有効なら値を表示し、NULLなら警告を表示する関数
void showPointValue(int* p) {
    if (p != NULL) {
        printf("受け取った値は %d です。\n", *p);
    } else {
        printf("ポインタがNULLです。値を参照できません。\n");
    }
}

int main(void) {
    int score = 85;
    int* validPtr = &score;
    int* nullPtr = NULL;

    // 有効なポインタを渡す
    showPointValue(validPtr);

    // NULLポインタを渡す
    showPointValue(nullPtr);

    return 0;
}

実行結果

受け取った値は 85 です。
ポインタがNULLです。値を参照できません。

このプログラムでは、showPointValue関数の中でNULLチェックを行っています。

if (p != NULL) {
    printf("受け取った値は %d です。\n", *p);
} else {
    printf("ポインタがNULLです。値を参照できません。\n");
}

pがNULLでない場合だけ、*pで値を表示しています。

一方、pがNULLの場合は、値を参照せずにメッセージを表示します。

このように、関数の引数としてポインタを受け取る場合は、関数内でNULLチェックを行うと安全性が高まります。

関数でポインタを受け取るときは特に注意する

関数の引数にポインタを使う場合、呼び出し元が必ず有効なアドレスを渡すとは限りません。

たとえば、次のような関数があるとします。

void showPointValue(int* p)

この関数は、int型の値を指すポインタを受け取ります。

呼び出し元は、有効なポインタを渡すこともできます。

int score = 85;
showPointValue(&score);

一方で、NULLを渡すこともできてしまいます。

int* p = NULL;
showPointValue(p);

そのため、ポインタ引数を受け取る関数では、関数の中でNULLチェックをしておくと安全です。

特に、他の人が使う関数や、再利用する予定の関数では、NULLチェックを入れておくと予期しないクラッシュを防ぎやすくなります。

未初期化ポインタも危険

NULLポインタと同じくらい注意したいのが、未初期化ポインタです。

次のような書き方は危険です。

int* p;
*p = 10;

pは宣言されているだけで、どこを指しているか分かりません。
この状態で*p = 10;を実行すると、どこか不明なメモリに書き込もうとしてしまいます。

NULLで初期化しておけば、少なくともどこも指していない状態であることが分かります。

int* p = NULL;
書き方状態
int* p;どこを指すか分からない
int* p = NULL;どこも指していないと分かる
int value = 10; int* p = &value;valueを指している

ポインタを宣言したら、すぐに有効なアドレスを代入するか、NULLで初期化する習慣をつけましょう。

メモリ確保に失敗したときのNULL

mallocなどで動的メモリを確保する場合、確保に失敗するとNULLが返ることがあります。

そのため、mallocの戻り値を使う前には、NULLチェックが必要です。

int* data = malloc(sizeof(int) * 5);

if (data == NULL) {
    printf("メモリを確保できませんでした。\n");
    return 1;
}

NULLチェックをせずにdataを使うと、メモリ確保に失敗した場合にNULLポインタへアクセスしてしまう可能性があります。

処理注意点
mallocでメモリ確保失敗するとNULLが返ることがある
戻り値の確認data == NULLを確認する
成功時のみ使用dataがNULLでない場合だけアクセスする
使用後freeで解放する

動的メモリ確保を使うようになると、NULLチェックはさらに重要になります。

解放後のポインタにも注意する

freeでメモリを解放したあと、そのポインタをそのまま使うのも危険です。

free(data);
*data = 10;

freeしたあとのdataは、以前のアドレスを持ったままかもしれません。しかし、そのメモリ領域はすでに使ってよい場所ではありません。

そのため、freeしたあとはNULLを代入しておくと安全です。

free(data);
data = NULL;

こうしておけば、あとで誤って使いそうになった場合でも、NULLチェックで防ぎやすくなります。

状態危険性対策
free後のポインタ解放済みメモリを指している可能性があるfree後にNULLを代入する
NULL代入後どこも指していないことが分かる使用前にNULLチェックする

解放後のポインタを使う問題は、NULLポインタとは別の種類の危険ですが、NULLを活用することで見つけやすくできます。

図:ポインタを安全に扱う基本の流れ

この図から分かること

この図から分かるのは、ポインタを安全に扱うためには、宣言してすぐ使うのではなく、初期化、確認、使用、後始末という流れを意識することが大切だということです。

NULLで初期化し、有効なアドレスが入ったことを確認してから値にアクセスします。使い終わったあとは、必要に応じてNULLを代入しておくと、あとから誤って使う危険を減らせます。

よくあるNULL関連のミス

NULLポインタに関するミスは、学習中にも実務でもよく発生します。

状況危険な例防ぎ方
初期化忘れint* p; のまま使うint* p = NULL;で初期化する
NULLチェック忘れNULLのまま*pを使うp != NULLを確認する
malloc失敗の確認忘れmalloc後すぐに使う戻り値がNULLか確認する
free後のアクセスfree後に*pを使うfree後にp = NULL;を行う
関数引数の確認不足NULLを受け取ってそのまま参照する関数内でNULLチェックする

ポインタを使うときは、今このポインタはどこを指しているかを常に意識することが大切です。

NULLチェックを入れる場所の考え方

NULLチェックは、ポインタを使う直前に入れるのが基本です。

特に、次のような場面では確認を入れると安全です。

場面NULLチェックの理由
関数でポインタ引数を受け取るとき呼び出し元がNULLを渡す可能性がある
mallocの戻り値を使うときメモリ確保に失敗する可能性がある
free後に再利用する可能性があるとき解放済み領域へのアクセスを防ぐ
配列や構造体をポインタで扱うとき参照先が有効か確認するため

毎回すべての場所に機械的に書けばよいというより、ポインタが外部から渡ってくる場所、メモリ確保の結果を受け取る場所、解放後に再利用されそうな場所では特に意識しましょう。

NULLポインタへのアクセスを防ぐ習慣

NULLポインタへのアクセスを防ぐには、日ごろから次の習慣を持つことが大切です。

習慣内容
ポインタはNULLで初期化する未初期化状態を避ける
使う前にNULLチェックする参照先があるか確認する
mallocの戻り値を確認するメモリ確保失敗に対応する
free後はNULLを代入する解放済みポインタの再利用を防ぎやすくする
警告を無視しない実行前に危険なコードを見直す

C言語では、ポインタの扱いをプログラマが丁寧に管理する必要があります。NULLポインタへのアクセスは、実行時エラーの原因になりやすいため、早い段階から安全な書き方を身につけておくことが大切です。

NULLを正しく扱うとポインタの安全性が高まる

NULLポインタは、どこも指していないことを示す特別な値です。

ポインタをNULLで初期化しておくと、まだ有効なアドレスが入っていないことを明確にできます。ただし、NULLはアクセスしてよい場所ではありません。NULLのまま*pで値を読んだり、*p = 10;のように書き込んだりすると、実行時エラーやプログラムの異常終了につながります。

安全に使うためには、ポインタを参照する前にp != NULLのように確認します。関数でポインタ引数を受け取る場合も、NULLが渡される可能性を考えてチェックしておくと安心です。

また、mallocの戻り値がNULLでないかを確認すること、free後にポインタへNULLを代入することも、ポインタを安全に扱ううえで重要です。

ポインタはC言語の強力な機能ですが、NULLの扱いを間違えると大きな不具合につながります。ポインタを使う前に確認する、使い終わったら状態を整理するという基本を意識して、クラッシュしにくいプログラムを書けるようになりましょう。