C言語の基本|ポインタのよくあるミスとNULL

ポインタの失敗例を先に知れば、アドレスと値の違いが見え、安全なコードの書き方が身につきます。

C言語を学ぶうえで、ポインタは避けて通れない大切な仕組みです。

変数のアドレスを扱ったり、配列や文字列を効率よく処理したりできるため、C言語らしいプログラムを書くうえで重要な役割を持っています。

その一方で、ポインタは初心者がつまずきやすいテーマでもあります。

普通の変数では値そのものを扱いますが、ポインタでは主にアドレスを扱います。そのため、見た目が少し違うだけのコードでも、実際にはまったく異なる処理になることがあります。

特に注意したいのが、次のようなミスです。

  • ポインタを初期化しないまま使う
  • 宣言時の * と使用時の * を混同する
  • ポインタそのものと、ポインタが指す先の値を取り違える
  • 指す先とポインタの型を一致させない
  • 複数のポインタを宣言するときに * の位置を勘違いする
  • 有効なアドレスがない状態を曖昧なままにする

ポインタのミスが怖いのは、単に計算結果が間違うだけではないところです。

ポインタはメモリ上の場所を扱うため、誤った使い方をすると、本来アクセスしてはいけないメモリへ読み書きしてしまう可能性があります。

そこで重要になるのが NULL ポインタです。

NULL を利用すると、

今のところ、このポインタは有効な対象を指していない

という状態を明確に表せます。

この記事では、C言語のポインタ演習問題として、初心者が間違えやすいコードを確認しながら、

  • 未初期化ポインタが危険な理由
  • p と *p の違い
  • ポインタ型をそろえる理由
  • 複数宣言で注意すること
  • NULL ポインタとは何か
  • NULL を使って安全にポインタを扱う方法

を順番に見ていきます。

間違ったコードと正しいコードを比較しながら、ポインタを安全に扱う感覚を身につけていきましょう。

ポインタで初心者が間違えやすいところ

まず、代表的なミスを整理しておきます。

ミス問題になること
初期化しないどのアドレスを指しているのか分からない
* の意味を取り違えるアドレスと値を混同する
型を間違える指しているデータを正しく扱えない
複数宣言を誤解する一部の変数だけがポインタになる
NULL を使わずに保持する有効なポインタか判断しにくくなる

ポインタでは、常に次の2つを分けて考えることが大切です。

  • ポインタそのものが持っているアドレス
  • そのアドレスの場所に格納されている値

この違いを意識しながら、具体的なミスを見ていきましょう。

ポインタを初期化しないまま使うミス

最初に確認したいのが、ポインタを宣言しただけで使ってしまうケースです。

たとえば、次のコードには問題があります。

int temperature;
int *p;

*p = 25;

p は int 型の値を指すポインタとして宣言されています。

しかし、

int *p;

と宣言しただけでは、p がどの変数を指すのかは決まっていません。

その状態で、

*p = 25;

を実行すると、p が保持している不定なアドレスの場所へ 25 を書き込もうとします。

これは未定義動作につながる危険な処理です。環境によっては異常終了する場合もあります。

正しくは、まず有効な変数のアドレスを代入します。

int temperature;
int *p = &temperature;

*p = 25;

これなら p は temperature を指しています。

そのため、

*p = 25;

は、

temperature = 25;

と同じ対象を書き換えることになります。

つまり、ポインタは宣言しただけでは十分ではありません。

どこを指すのかを決めてから間接参照する

という順序が重要です。

図1:未初期化ポインタが危険な理由

この図から分かること

左側では、p がどこを指しているか決まっていません。

その状態で *p を使うと、どのメモリへアクセスするのか分からないため危険です。

右側では、

p = &temperature;

によって p が temperature のアドレスを持っています。

この状態なら *p が temperature の値を表すことが明確です。

ポインタを使うときは、

  • まず有効なアドレスを持たせる
  • そのあと * を使って指している先へアクセスする

という順序を意識しましょう。

宣言時の * と使用時の * を区別する

ポインタでは * が何度も登場します。

ただし、同じ * でも使われる場所によって役割が異なります。

場面* の意味
宣言時ポインタとして宣言する
式の中ポインタが指している先の値を参照する

たとえば、

int *p;

では、p を int 型の値を指すポインタとして宣言しています。

一方、

*p = 30;

では、p が指している先の値に 30 を代入しています。

この違いをしっかり分けて考えることが大切です。

アドレスを代入するときに * を付けてしまう

次のコードを考えてみましょう。

int count = 10;
int *p;

*p = &count;

これは正しい代入ではありません。

*p は、p が指している先の int 型の値を表します。

一方、

&count

は count のアドレスです。

つまり、値を入れる場所にアドレスを代入しようとしており、型も意味も合っていません。

アドレスを保存するのは p 自身です。

正しくは、

int count = 10;
int *p;

p = &count;

とします。

ここでは、

p

がアドレスを保持し、

*p

がそのアドレスの先にある値を表します。

この2つを分けて覚えると分かりやすくなります。

ポインタ宣言で * を忘れる

次のコードにも問題があります。

int count = 10;
int p = &count;

この宣言では p は普通の int 型変数です。

しかし、&count が表しているのは count のアドレスです。

アドレスを保存したいのであれば、p をポインタとして宣言する必要があります。

int count = 10;
int *p = &count;

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

ポインタを使う場合は、指すデータ型に対応したポインタ型として宣言します。

指したいデータポインタ型
int 型int *
double 型double *
char 型char *

指している先を変更したいのに * を付けない

今度は逆のミスです。

int count = 10;
int *p = &count;

p = 20;

count の値を 20 に変更したいのであれば、この書き方ではいけません。

p はアドレスを保存する変数です。

そのため、

p = 20;

は、p そのものへ 20 という値をアドレスとして入れようとする処理になります。

count を変更したいのであれば、p が指している先へ代入します。

int count = 10;
int *p = &count;

*p = 20;

この場合、

p     → count のアドレス
*p    → count の値

という関係になります。

したがって、

*p = 20;

を実行すると count の値が 20 になります。

図2:p と *p は何が違うのか

この図から分かること

p と *p は、見た目はよく似ていますが役割は大きく違います。

p は count が置かれている場所のアドレスを持っています。

一方、*p はそのアドレスをたどった先にある count の値です。

そのため、

p = ...

ではポインタ自身の持つアドレスを変更し、

*p = ...

ではポインタが指している先の値を変更します。

ポインタを読むときは、まず * が付いているかどうかを確認すると整理しやすくなります。

ポインタの型を間違えるミス

ポインタには、何型のデータを指すのかという情報があります。

たとえば、次のコードには型の不一致があります。

double price = 1280.5;
int *p = &price;

price は double 型ですが、p は int 型を指すポインタです。

この2つは一致していません。

正しくは、

double price = 1280.5;
double *p = &price;

とします。

これなら p は double 型の price を正しく指すことができます。

基本的には、次のように対応させます。

変数の型対応するポインタ
intint *
doubledouble *
charchar *

ポインタ演算や *p による参照では、ポインタの型をもとにデータを扱います。

そのため、指しているデータの型とポインタ型を一致させることが重要です。

複数のポインタを宣言するときの注意

次の宣言も、初心者が間違えやすい書き方です。

int *p1, p2;

この1行を見ると、p1 と p2 の両方がポインタのように見えるかもしれません。

しかし実際には、

  • p1 は int 型を指すポインタ
  • p2 は普通の int 型変数

です。

p1 と p2 の両方をポインタにする場合は、

int *p1, *p2;

と書きます。

  • はそれぞれの宣言子に付いていると考えると理解しやすくなります。

特にポインタに慣れないうちは、

int *p1;
int *p2;

のように1行ずつ宣言するのも分かりやすい方法です。

コードが少し長くなっても、何がポインタなのかを見分けやすくなります。

NULL を使って安全な状態を表す

ここまで見てきたように、ポインタは有効なアドレスを持っている状態で使う必要があります。

しかし、プログラムによっては宣言した時点ではまだ指す対象が決まっていないことがあります。

そのような場合に役立つのが NULL です。

NULL を確認してからポインタを使用するプログラム

ファイル名:11_10_1.c

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

int main(void)
{
    int temperature = 26;
    int *p = NULL;

    printf("最初のポインタの状態を確認します\n");

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

    p = &temperature;

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

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

    return 0;
}
実行結果の例
最初のポインタの状態を確認します
ポインタはまだNULLです
有効なアドレスを代入した後の値を表示します
温度: 26

プログラムの最初では NULL を代入している

最初に次のように宣言しています。

int *p = NULL;

これは、

p は現在、どの有効なオブジェクトも指していない

という状態を明示しています。

未初期化のポインタとは違い、NULL が代入されていることがコードからはっきり分かります。

そのため、

if (p != NULL)

という確認ができます。

NULL のときは *p を使わない

最初の if 文を見てみましょう。

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

この時点では、

p == NULL

なので、*p は実行されません。

NULL ポインタを *p のように間接参照してはいけません。

そこで、p が NULL ではないことを確認してから *p を使っています。

この確認によって、NULL のまま誤って参照してしまうことを防ぎやすくなります。

有効なアドレスを代入する

次に、

p = &temperature;

を実行しています。

これによって、p は temperature のアドレスを持つようになります。

概念的には、

p
↓
temperature
26

という関係になります。

そのあと再び、

if (p != NULL)

を確認します。

今度は p が temperature を指しているため条件が成立し、

printf("温度: %d\n", *p);

によって 26 が表示されます。

このように NULL を利用すると、

  • まだ使えない状態
  • 有効なアドレスが入った状態

を区別しやすくなります。

NULL ポインタとは

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

たとえば、

int *p = NULL;

とすると、p は int 型の値を指すことができるポインタですが、現時点では有効な int 型オブジェクトを指していません。

NULL は、適当なメモリアドレスを入れているわけではありません。

プログラム上で、

このポインタには現在、有効な参照先がない

ことを表すために利用します。

これによって、未初期化のまま放置するよりもポインタの状態を判断しやすくなります。

NULL を初期化に使う理由

ポインタを宣言した時点で、すぐに指す対象を決められない場合があります。

そのようなとき、

int *p;

だけにすると、p の値は不定になります。

一方、

int *p = NULL;

としておけば、まだ有効な対象を指していないことが明確です。

あとから、

if (p != NULL)

と確認して利用できます。

つまり NULL は、ポインタの状態を分かりやすく管理するための目印として役立ちます。

NULL はエラーを表すためにも使われる

NULL は、自分で宣言するポインタの初期状態だけでなく、関数が処理に失敗したことを表すためにも使われます。

代表的な例が malloc です。

malloc による動的メモリ確保が失敗すると NULL が返されます。

そのため、取得したポインタをすぐに間接参照するのではなく、NULL かどうかを確認することが重要です。

基本的な考え方は、

if (p != NULL) {
    /* 有効なポインタを使う処理 */
}

です。

つまり、

  • NULL ではないなら使う
  • NULL なら間接参照しない

という判断を行います。

図3:通常のポインタとNULLポインタの違い

この図から分かること

通常の有効なポインタには、実際の変数へつながるアドレスが入っています。

そのため、*p を使ってその変数の値を参照できます。

一方、NULL ポインタはどの有効な対象も指していません。

したがって、

*p

のように NULL ポインタを間接参照してはいけません。

NULL は安全に参照できる特別な場所ではなく、

現在は有効な参照先がないことを表すための状態

だと理解することが大切です。

NULL ポインタを扱う基本形

NULL を使うポインタでは、次のような確認が基本になります。

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

処理の考え方はシンプルです。

  • p が NULL でなければ、参照先があることを確認したうえで処理する
  • p が NULL なら、*p を使わない

この確認を入れておくことで、NULL ポインタをそのまま参照するミスを防ぎやすくなります。

ただし、p != NULL であることだけで、常にそのポインタが完全に安全だと保証されるわけではありません。

たとえば、すでに寿命が終わったオブジェクトへのポインタなどは NULL ではなくても無効な場合があります。

今回の範囲では、まず NULL のときには絶対に間接参照しない という基本をしっかり押さえておきましょう。

よくあるポインタのミスを整理する

ここまで確認した代表的なミスを一覧にすると、次のようになります。

ミス誤った例正しい考え方
ポインタを初期化しないint *p; のまま *p を使う有効なアドレスを代入してから使う
アドレス代入で * を付ける*p = &x;p = &x; とする
宣言で * を忘れるint p = &x;int *p = &x; とする
指している値の変更で * を忘れるp = 20;*p = 20; とする
ポインタ型が合わないdouble d; int *p = &d;double *p = &d; とする
複数宣言を誤解するint *p1, p2;両方なら int *p1, *p2; とする
無効な状態を曖昧にする未初期化のまま保持する必要に応じて NULL で初期化する
NULL のまま参照する*p をそのまま使うp != NULL を確認してから使う

この表を見ると、ポインタで起こりやすいミスの多くが、

アドレスと値を取り違えること

から生まれていることが分かります。

ポインタを安全に扱うためのコツ

ポインタを扱うときは、次の点を確認する習慣をつけるとミスを減らしやすくなります。

確認すること意識する内容
初期化されているか有効なアドレス、または NULL が入っているか
p は何を持つかアドレスを保持している
*p は何を表すかp が指している先の値
型は一致しているか指すデータ型とポインタ型をそろえる
NULL ではないか間接参照する前に確認する
複数宣言は正しいか各変数に必要な * が付いているか

特に初心者のうちは、ポインタを見たらすぐに、

このポインタはいま何を指しているのか

を考える習慣を付けるのがおすすめです。

ポインタを見るときは場所と中身を分けて考える

たとえば、次のコードを見てみましょう。

int number = 15;
int *p = &number;

この時点では、

number
値      15
場所    あるアドレス

があります。

そして p は、

number が置かれている場所

を保持します。

さらに、

*p

とすると、その場所をたどって 15 を取り出します。

この関係を整理すると、次のようになります。

書き方見ているもの
numbernumber の値
&numbernumber のアドレス
pp が持っているアドレス
*pp が指している場所の値

この4つを区別できるようになると、ポインタのコードはかなり読みやすくなります。

ポインタを使う前に確認したいこと

実際にコードを書くときは、ポインタを間接参照する前に、頭の中で次のことを確認してみましょう。

  • この変数は普通の値を持つ変数なのか、ポインタなのか
  • ポインタには現在どのアドレスが入っているのか
  • 指している先の型とポインタ型は一致しているか
    • を付けた場合、どのデータを参照することになるのか
  • NULL の可能性はないか
  • 今行いたいのはポインタそのものの変更なのか、指している値の変更なのか

たとえば、

p = &number;

なら、number のアドレスを p に保存しています。

一方、

*p = 30;

なら、p が指している number の値を変更しています。

この違いを場所と中身の関係として考えられるようになることが、ポインタでつまずかないための大切なポイントです。

ポインタは慎重に扱う必要がありますが、基本となる考え方は複雑ではありません。

有効なアドレスを持たせてから使うこと、p と *p を区別すること、指す先と型をそろえること、そして有効な参照先がない状態では NULL を利用すること。

こうした基本を意識しながらコードを読むことで、ポインタのミスを見つける力も少しずつ身についていきます。