見出し画像

C言語をしっかりと学びたい! (その2 0とnullとNULLポインタと'\0')

今回のテーマ

0とnullとNULL(ポインター)と'\0'について理解する

*NULLはC言語で使われるあれ、nullはコンピューター用語として一般的に使われるnullとしてこの記事では使い分けます。

0とnullの違い

まずはコンピューター用語のnull とはどういう意味か wikipedia で調べてみると、

Null(ヌル、ナル)は、何もない、という意味で、プログラミング言語などコンピュータ関係では、「何も示さないもの」を表すのに使われる。

この「何も示さないもの」というのが分かりにくいと感じる人が多い気がします。そういう場合には次のように覚えてください。

「存在しない、無効なもの」を表す

また、どこで見かけたのか覚えていませんが、0とnullの違いに関して個人的に気に入っている表現があるので紹介。

あなた「今ファミチキっていくつありますか?」
ファミマ店員「ファミチキはないです。」
ローソン店員「ファミチキはないです。」

です。前者は、単純に個数が0個であると言っていますね。一方後者は、そもそもファミチキなんてものは「存在しない、無効なもの」だと言っています。なので、個数を聞いてもある種のエラーが返ってきます。

NULLポインター

そもそもNULLってのは、大文字であることからも想像できる通りマクロです。<stdio.h>で次のように定義されています。

#define NULL ((void *)0)

これを見てわかるように、NULLはポインターです。まあヌルポインターとかって呼ばれていますしね。これはNULLという名前からも推察できる通り存在しない、無効なアドレスを示しています

前回の記事でも書いた通り、ポインタ変数も所詮は特定のメモリの0と1の羅列を記憶するだけです。そうである以上、常にポインタ変数は0000000000000000~FFFFFFFFFFFFFFFFの値のどれかをとります。そしてC言語では、そのアドレスのメモリーを確保しているかどうかに関わらず(読み取り不可ではないなら)参照できてしまいます。なので次のようなプログラムも普通に動作しちゃいます。

#include <stdio.h>

void  f(int *pi)
{
    printf("あなたの点数は%dです", *pi);
}

int main(void)
{
    int i;
    int *pi;
    int *ppp;

    printf("あなたの点数を入力してください\n");
    scanf("%d", &i);

    ppp = &i;

    f(pi);

    return (0);
}

このプログラム、mainでとりあえずポインタ変数のメモリを確保していますが、pi はその確保した時点でのよくわからない(何の意味も持たない)0と1の羅列を保持しています。なので当然、pi の参照先のメモリの値も何の意味も持たないよくわからない値です。なのに間違えて f(ppp) ではなく f(pi) としてしまっていますね。これだと大変な結果になってしまいます。(実行結果が「あなたの点数は63198722点です」みたいになっちゃう)
さらに言えば、今回はメモリを読み取るだけでしたが、その値を変更するようなプログラムだった場合おそらくセグフォを起こしてしまいます。

では次のようなコードはどうでしょうか?

#include <stdio.h>

void  f(int *pi)
{
    if (pi = 12345)
        printf("無効なアドレスです");
    else
        printf("あなたの点数は%dです", *pi);
}

int main(void)
{
    int i;
    int *pi = 12345;
    int *ppp = 12345;

    printf("あなたの点数を入力してください\n");
    scanf("%d", &i);
    
    ppp = &i;

    f(pi);

    return (0);
}

こういうふうにポインターはすべて12345で初期化する、というルールにすればちゃんと「無効なアドレスです」と出力されそうですね。でもこれにも一つ問題が。それは i のアドレスが偶然12345だった時に、ちゃんと f(ppp)としても無効なアドレスと言われてしまうことです。もちろん確率は低いですが絶対に起こってほしくないですね。

ならばどうすればいいか、、、
答えは簡単。メモリのアドレスから12345をなくしちゃえばいいんです。メモリのアドレスは12344の次は12346にします。そうすれば i のアドレスが偶然12345になることなんて起こりません!

、、、

まあとは言えこれはあまりにも不自然ですよね(笑)プログラムをするときにもいろいろと不便そうです。(ループでポインタをインクリメントする時とか面倒くさいことこのうえない)

なので実際は12345ではなく0が採用されています。つまりメモリのアドレスから0を消して(メモリのアドレスを1から始めて)います。そうすると

#include <stdio.h>

void  f(int *pi)
{
    if (pi = (int *)0)
        printf("無効なアドレスです");
    else
        printf("あなたの点数は%dです", *pi);
}

int main(void)
{
    int i;
    int *pi = (int *)0;
    int *ppp = (int *)0;

    printf("あなたの点数を入力してください\n");
    scanf("%d", &i);
    
    ppp = &i;

    f(pi);

    return (0);
}

これならいい感じ!間違っても i のアドレスが0をとることはないので、ちゃんと f(ppp) とした時にも「無効なアドレスです」なんて言われることはありません。

んで、毎回 (型名 *)0って書くのが面倒くさいからそれをマクロにしてNULLと書いている、ということですね。

まあ要するに、
重要なのはNULLは「何も示さない」というよりも、「無効であるという共通認識」ということですね。(メモリは1から始まるので認識の問題だけではなく実際に無効ですが)

'\0'(ヌル文字)

これは本当に秒で終わります。
日本語でいう句点(。)、英語でいうピリオドです。文章の終わりを意味します。重要なのはこれもやはり、'\0'は「文の終わりであるという共通認識」だということです。数値としては1バイトの 0 です。コンパイラ的には'\0'と(char)0は全く同じ。多分プリプロセスらへんで(char )0に置き換えられるんじゃないかなと。(プリプロセスに関してもいずれやりたい)

*念のために
ヌル文字は何も出力されないと勘違いしている人もいますが、ちゃんと出力されます。(次のようなプログラムをイメージしてもらえればと)

char c = '\0';
write(1, &c, 1);

句点を書いてくださいと言われたら「。」と書くと思います。ピリオドの場合は「.」ですね。もしヌル文字を書く場合は「」です(見た目上は何も出力されていません)。例えばテキストファイルにヌル文字を10文字出力した場合、ファイルはちゃんと10byteだけ大きくなります。単純に目に見えないだけです。

この記事が気に入ったらサポートをしてみませんか?