C言語のきほん|ポインタのミスとNULLポインタ

ポインタのつまずきやすいポイントを先に知れば、バグはぐっと減らせる。

ポインタは、C言語を学ぶうえでとても重要な仕組みです。
変数のアドレスを扱えたり、配列や文字列を効率よく操作できたりと、C言語らしい表現の中心にある存在といってもいいでしょう。

その一方で、ポインタは初心者がつまずきやすいところでもあります。
変数の中身ではなく「場所」を扱うため、見た目が少し似ているコードでも意味が大きく違ったり、ほんの少しの書き間違いで思わぬ不具合につながったりします。

たとえば、

  • ポインタを初期化しないまま使ってしまう
  • 宣言時の * と使用時の * を混同してしまう
  • ポインタそのものに値を入れるつもりが、指している先を変えたつもりになってしまう
  • 指す先の型とポインタの型が合っていない
  • 複数宣言の書き方で勘違いしてしまう

といったミスは、とてもよく起こります。

しかも、ポインタのミスは単なる計算ミスでは終わらず、
意図しないメモリへアクセスする
という深刻な問題に発展することがあります。
実行時エラーになったり、原因のわかりにくいバグになったりするので、早い段階で正しい感覚を身につけることがとても大切です。

そこで役立つのが、NULLポインタの考え方です。

NULLポインタは、「今はどこも指していません」という状態を明示するための仕組みです。
有効なアドレスがまだ決まっていないときや、失敗を表したいときに使うことで、危険なポインタ操作を防ぎやすくなります。

ここでは、ポインタのミスとNULLポインタについて、

  • 初心者が陥りやすい代表的なミス
  • それぞれの誤りの意味
  • どう直せばよいか
  • NULLポインタとは何か
  • なぜ NULL が安全に役立つのか

を、表や図も交えながら、やわらかく丁寧に整理していきます。

ポインタは怖いものではありません。
ただし、曖昧なまま使うと危ない道具でもあります。
だからこそ、ミスしやすいポイントを先に知っておくことが、とても大切なのです。

ポインタで初心者が陥りやすいミス

ポインタのミスにはいろいろありますが、まずは代表的なものを整理して見ていきましょう。

ミスの種類何が問題か
初期化を忘れるどこを指しているかわからない
* の役割を混同する代入先や意味を取り違える
型を間違える正しいデータとして扱えない
複数宣言で勘違いする一部だけポインタになってしまう
NULL を使わず曖昧な状態で持つ安全確認がしにくい

このあたりは、最初のうちにきちんと整理しておくと、後のトラブルをかなり防げます。

ポインタの初期化を忘れる

まず、とてもよくあるのがこれです。
宣言しただけのポインタをそのまま使ってしまうミスです。

誤り例

int value;
int *p;

*p = 10;

このコードの問題は、p にまだ有効なアドレスが入っていないことです。
p は宣言されていますが、どこを指しているか決まっていません
その状態で *p = 10; を実行すると、不正な場所に書き込もうとしてしまいます。

これにより、実行時エラーやセグメンテーション違反が起きることがあります。

修正例

int value;
int *p = &value;

*p = 10;

このように、p に value のアドレスを代入してから使えば安全です。
これで p は value を指すので、*p = 10; は value に 10 を代入する意味になります。

ここで意識したいこと

ポインタを宣言しただけでは、まだ使える状態ではありません。
ポインタは必ず、正しいアドレスを持ってから使う
という感覚を持つことが大切です。

図:未初期化ポインタはどこを指しているかわからない

この図では、変数 value とポインタ p が並んでいます。
value は普通の整数変数として存在していますが、p の中身は不定値のままです。
その状態で *p に代入しようとすると、どこのメモリにアクセスするかわからず危険であることを示します。

この図のポイントは、ポインタは宣言しただけでは安全に使えないということです。
正しいアドレスを入れてはじめて意味を持つ、と考えると理解しやすいです。

* の役割を混同する

ポインタでは * がとてもよく出てきます。
でも、同じ記号でも場面によって意味が違います。
ここを混同すると、ミスが起きやすくなります。

場面* の役割
宣言時ポインタ型の変数であることを示す
使用時ポインタが指す先の値を表す

この違いをひとつずつ見ていきましょう。

① ポインタにアドレスを代入するときの誤り

誤り例

int x = 10;
int *p;

*p = &x;

このコードは、*p に x のアドレスを入れようとしています。
でも *p は「p が指している先の値」です。
そこにアドレスを入れるのは意味が合いません。

修正例

int x = 10;
int *p;

p = &x;

アドレスを代入したい相手は、p 自身です。
だから * は付けません。

ポイント

  • p はアドレスを入れる箱
  • *p はそのアドレスの先にある値

この区別をいつも意識すると、かなり混乱が減ります。

② ポインタ宣言子 * を忘れる

誤り例

int x = 10;
int p = &x;

このコードでは p は int 型の普通の変数です。
アドレスを入れたいなら、int 型ではなく、int を指すポインタ型でなければいけません。

修正例

int x = 10;
int *p = &x;

これで p は int 型の変数 x を指すポインタになります。

ポイント

アドレスを保存したいなら、普通の int ではなく、
int * や double * などのポインタ型
として宣言する必要があります。

③ 間接演算子 * を省略して値を変更しようとする

誤り例

int x = 10;
int *p = &x;

p = 20;

このコードを書いた人は、「x に 20 を代入したい」と思っているかもしれません。
でも実際には、p 自身に 20 という数値を入れようとしています。
つまり、アドレスとして意味のない値を入れてしまう危険なコードです。

修正例

int x = 10;
int *p = &x;

*p = 20;

これなら、p が指している先の値、つまり x の値を 20 に変更できます。

ポイント

  • p = 20;
    → ポインタそのものを書き換える
  • *p = 20;
    → ポインタが指す先の値を書き換える

この違いはとても重要です。

図:p と *p の違い

この図では、変数 x に 10 が入っていて、ポインタ p がそのアドレスを持っています。
p 自身はアドレスを保存しており、*p はその先にある値 10 を表します。
そのため、p = 20; はポインタの中身を書き換えることであり、*p = 20; は x の値を書き換えることだと視覚的に整理します。

この図のポイントは、p と *p はまったく別物だという点です。
ここを曖昧にすると、ポインタのコードが一気にわかりにくくなります。

ポインタの型を間違える

次によくあるのが、指す先の型とポインタの型が一致していないミスです。

誤り例

double x = 3.14;
int *p = &x;

このコードでは、double 型の変数 x を int * 型のポインタで指そうとしています。
でも、これは型が合っていません。

ポインタは、何型のデータを指すのかを持っています。
そのため、指す先の型と一致していないと正しく扱えません。

修正例

double x = 3.14;
double *p = &x;

これで、double 型の値を指すポインタとして正しく宣言できます。

ポイント

指す先ポインタの型
int 型int *
double 型double *
char 型char *

ポインタの型は、指すデータ型に必ず合わせるようにします。

ポインタを複数宣言するときの混乱

これも初心者がかなり混乱しやすいところです。

誤り例

int *p1, p2;

このコードを見て、「p1 も p2 もポインタ」と思ってしまうことがあります。
でも実際には違います。

  • p1 は int * 型
  • p2 は int 型

です。

  • *は型全体にかかるのではなく、変数名ごとに付くと考える必要があります。

修正例

int *p1, *p2;

こう書けば、p1 も p2 も int へのポインタです。

ポイント

複数宣言は読み間違えやすいので、初心者のうちは次のように1行ずつ書くのもおすすめです。

int *p1;
int *p2;

このほうが見やすく、ミスも減りやすいです。

サンプルプログラム

NULL を使いながら安全にポインタを扱う簡単なプログラム例です。

ファイル名:11_10_1.c

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    int score = 88;      /* 点数を表す変数 */
    int *p = NULL;       /* まだどこも指していないことを明示する */

    printf("はじめはポインタが有効か確認します\n");

    if (p != NULL) {
        printf("点数: %d\n", *p);
    } else {
        printf("ポインタはまだNULLです\n");
    }

    p = &score;          /* score のアドレスを代入する */

    printf("有効なアドレスを代入した後の値を表示します\n");

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

    return 0;
}

このプログラムで見てほしいこと

このプログラムでは、最初に

int *p = NULL;

としています。
これは、まだ有効なアドレスを持っていませんという状態を、はっきり示しています。

そのあと、

if (p != NULL)

で確認してから *p を使っているので、NULL のまま誤って参照することを防げます。

そして、

p = &score;

で有効なアドレスを代入したあとに、もう一度同じ確認をして表示しています。

このように、NULL を使うと、
まだ使える状態ではないポインタ
を安全に扱いやすくなります。

NULLポインタとは何か

ここから、NULLポインタについて整理していきましょう。

NULLポインタとは、
どのオブジェクトも指していないポインタ
のことです。

つまり、
「有効なアドレスはまだ入っていません」
ということを明示するために使います。

たとえば次のように書きます。

int *p = NULL;

この p は、まだ何も指していません。
value や score のような実際の変数を指していない状態です。

ここで大切なのは、NULL は「変なアドレス」ではなく、
無効な状態をわざと表すための目印
だということです。

NULL を使う主な理由

NULL が役立つ場面はいくつかあります。
ここでは特に大切な2つを見ておきましょう。

① 初期化の明示

有効なアドレスをすぐには入れられないとき、とりあえず NULL にしておけば、
「まだ使ってはいけないポインタ」
だとすぐわかります。

たとえば、

int *p = NULL;

としておけば、あとで

if (p != NULL)

と確認できます。

これは、未初期化の不定値よりずっと安全です。

② エラー処理

関数が「有効なアドレスを返せませんでした」という失敗を表すときに、NULL が使われることがあります。

たとえば動的メモリ確保を行う malloc は、失敗すると NULL を返します。
このとき、返ってきたポインタをすぐ使うのではなく、NULL かどうか確認することが大切です。

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

この図では、普通のポインタが変数を指している場合と、NULLポインタの場合を並べて示します。
普通のポインタはある変数へ矢印が伸びていますが、NULLポインタはどの変数にも矢印がつながっていません。
これにより、NULL は「まだ有効な対象がない」ことを表すための安全な状態だと理解しやすくなります。

NULLポインタを使うときの基本形

NULLポインタを安全に使うときは、次のような形が基本になります。

if (p != NULL) {
    /* ポインタが有効なときの処理 */
} else {
    printf("ポインタがNULLです。\n");
}

この形にしておけば、NULL のまま *p を使ってしまうミスを防ぎやすくなります。

ここでの考え方はとてもシンプルです。

  • NULL でなければ使う
  • NULL なら使わない

ポインタの安全性は、こうした小さな確認の積み重ねでかなり高くなります。

よくあるミスを表で整理する

ここまでの内容を、一覧で整理しておきましょう。

ミス誤りの例修正の考え方
初期化しないint *p; *p = 10;先に有効なアドレスを入れる
アドレス代入で * を付ける*p = &x;p = &x; と書く
宣言で * を忘れるint p = &x;int *p = &x; にする
値変更で * を忘れるp = 20;*p = 20; にする
型が合わないint *p = &d;指す型に合わせる
複数宣言を誤解するint *p1, p2;必要なら両方に * を付ける
無効状態を曖昧にする未初期化のまま持つNULL で明示する

この表を見ながらコードを読むと、どこで何を間違えやすいかが見えやすくなります。

安全に使うためのコツ

ポインタを安全に使うためには、次のことを意識すると効果的です。

コツ内容
初期化するすぐ使わないなら NULL を入れる
使う前に確認するp != NULL を確認する
型を合わせる指す先の型と一致させる
* の役割を分けて考える宣言か、参照かを意識する
複数宣言を丁寧に書く読みやすさ優先で書く

ポインタは、慣れてくるととても便利です。
でも最初のうちは、安全第一で慎重に書くくらいがちょうどよいです。

ポインタでつまずかないために大切な見方

最後に、ポインタを見るときの基本の見方を整理しておきます。

  • これは値を持っているのか、アドレスを持っているのか
  • 今このポインタは何を指しているのか
    *を付けるべき場面なのか、付けないべき場面なのか
  • そのポインタは本当に有効なのか
  • NULL で確認できる状態になっているか

この見方が身につくと、ポインタのコードがかなり読みやすくなります。
ただ記号を追うのではなく、場所と中身の関係として見られるようになるからです。

ポインタは難しいからこそ、最初の基礎がとても大切です。
初期化を忘れないこと、* の役割を混同しないこと、型を合わせること、そして NULL で安全を確保すること。
このあたりをしっかり押さえておくだけで、ポインタに関するトラブルの多くは防げるようになります。