閉じる

C++のNULLとnullptrの違いは?nullptrのメリットと安全な使い方

C++ではポインタに「何も指していない」ことを表す方法としてNULLとnullptrが知られていますが、両者は見た目が似ていても性質が大きく異なります。

本記事では、NULLとnullptrの違いを丁寧に整理し、なぜnullptrを使うのが現代的で安全か、そして実務での安全な使い方と移行のコツまで解説します。

C++初心者の方でも、明日から自信を持ってnullptrを使えるようになることを目指します。

C++のNULLとnullptrの違い

NULLとは何か(整数0のマクロ)

C言語起源のNULLは、多くの実装で0もしくは0Lに展開されるマクロです。

ヘッダ<cstddef>などで定義され、「ヌルポインタ定数」として使えるように見えますが、型としては単なる整数リテラルにすぎません。

つまり、NULLは「整数0」であって「ポインタ型」ではありません

この性質が、オーバーロード解決やテンプレート推論の場面で思わぬ混乱を招きます。

nullptrとは何か(ポインタ専用の空値)

C++11で導入されたnullptrは、専用の型std::nullptr_tを持つ「ヌルポインタ」です。

任意のポインタ型(およびメンバポインタ型)へ暗黙変換できますが、整数型へは変換できません

このため、ポインタを使う文脈と整数を使う文脈が明確に分かれ、コードの意図が正しく伝わります。

型の違いが生む問題(型安全とオーバーロード)

NULLの正体が整数であるために、オーバーロード解決が「整数版」を選んでしまう典型例を示します。

実装依存を避けるため、ここでは意図的に0を使って再現します。

C++
#include <iostream>

// オーバーロードの用意
void f(int) { std::cout << "f(int) が呼ばれました\n"; }
void f(const char*) { std::cout << "f(const char*) が呼ばれました\n"; }

int main() {
    // これはNULLを0と仮定したデモ。多くの実装でNULLは0または0Lです。
    f(0);          // 多くの場合、f(int) が選ばれる
    f(nullptr);    // ポインタ専用の空値。f(const char*) が選ばれる
}
実行結果
f(int) が呼ばれました
f(const char*) が呼ばれました

NULLは整数なのでf(int)が選ばれやすく、意図と異なる関数が呼ばれる危険があります

一方でnullptrは「ポインタ専用」なのでf(const char*)といったポインタ系のオーバーロードが選ばれます。

C++11以降はnullptrが標準

C++11以降の標準では、nullptrの使用が推奨されます。

レガシーなNULLは依然として使えますが、新しいコードでNULLを選ぶ理由は基本的にありません

ビルドは-std=c++11(GCC/Clang)や/std:c++14(MSVC)以降を有効にしましょう。

以下に、実装上の違いを整理します。

トークン主な変換先整数との誤解推奨度
NULL実装依存(多くはint/longの0)ポインタへも変換可能だが「整数扱い」されやすい高い非推奨
nullptrstd::nullptr_t任意のポインタ(およびメンバポインタ)ない推奨

結論: ポインタの「空」を表す値は常にnullptrを使うのがC++の作法です。

nullptrを使うメリット

型安全でバグを防ぐ

nullptrは整数ではないため、ポインタと整数が混同されるバグを未然に防ぎます

例えば条件式や代入で、意図せず整数変換が起こることがありません。

関数オーバーロードで意図が伝わる

オーバーロードの例をもう一つ示します。

std::nullptr_tは独自の型なので、意図的に「ヌルポインタであること」をコンパイラに伝えられます

C++
#include <iostream>
#include <cstddef>  // std::nullptr_t

void g(int) { std::cout << "g(int)\n"; }
void g(void*) { std::cout << "g(void*)\n"; }
void g(std::nullptr_t) { std::cout << "g(nullptr_t)\n"; }

int main() {
    g(0);         // g(int)
    g(nullptr);   // g(nullptr_t) が最も適合
    void* p = nullptr;
    g(p);         // g(void*)
}
実行結果
g(int)
g(nullptr_t)
g(void*)

NULLでは「整数扱い」されがちですが、nullptrならヌルポインタの意図が明確です

テンプレートや可変引数でも誤解されにくい

可変引数(… / printf系)では、NULLが「int 0」として渡されると呼び出し規約の不一致で未定義動作になる恐れがあります。

特に%pvoidを期待するため、printf("%p", NULL)は危険です。

nullptrならvoidに正しく変換され、安全に振る舞います。

C++
#include <cstdio>

int main() {
    // %p は void* を期待する。nullptr は適切に変換される
    std::printf("ptr=%p\n", nullptr);

    // 注意: 実装やOSにより出力表現は (nil) / 0x0 など異なります。
}
実行結果
ptr=(nil)
実行結果
ptr=0000000000000000

テンプレートでも、例えばh(T)のような汎用引数に渡すと、h(NULL)T=intと推論されがちですが、h(nullptr)ならT=std::nullptr_tと推論され、意図が明確になります

可読性が上がる(0との混同を防ぐ)

0は「数値の0」、nullptrは「ヌルポインタ」と見た瞬間に分かります。

読み手が迷わないコードは保守も容易です。

コンパイル時に間違いを検出できる

nullptrは整数に変換できないため、ポインタを期待しない場所で使うとコンパイルエラーになります。

これは早期に問題を発見できる利点です。

逆にNULLは整数として多くの場所に「通ってしまう」ため、実行時まで潜むバグを誘発します。

C++
// コンパイルエラーの例(出力はありません)
// int x = nullptr; // エラー: nullptrは整数に変換できない

nullptrの安全な使い方

ポインタの初期化はnullptrで統一

ポインタは常にnullptrで初期化し、未初期化を避けます。

C++
#include <iostream>

int main() {
    int* p = nullptr;  // 未初期化のダングリングを防止
    if (!p) {
        std::cout << "pはヌルポインタです\n";
    }
}
実行結果
pはヌルポインタです

nullチェックはif(p)またはp == nullptr

可読性と意図の明確さを両立する書き方です。

if (p)は空でないポインタの判定としてよく使われますが、p == nullptrと書くと「ヌル判定」であることがより明確です。

C++
#include <iostream>

void use(int* p) {
    if (p) {
        std::cout << "*p = " << *p << "\n";
    } else if (p == nullptr) {
        std::cout << "pはnullptrです\n";
    }
}

int main() {
    int x = 42;
    use(&x);
    use(nullptr);
}
実行結果
*p = 42
pはnullptrです

引数や戻り値にnullptrを使って意図を明確に

「返すべきオブジェクトがない」や「引数は任意」という意図は、戻り値やデフォルト引数でnullptrを使うと明確になります。

C++
#include <optional>
#include <string>

// 見つからなければnullptrを返す古典的API
const char* find_cstr(bool found) {
    return found ? "ok" : nullptr;
}

整数0が欲しいときは0を使う(ポインタにはnullptr)

数値としての0が必要なら0、ポインタの空はnullptr

この住み分けを徹底します。

C++
int main() {
    int zero = 0;            // 数値の0
    int* p = nullptr;        // ヌルポインタ
    (void)zero; (void)p;
}

スマートポインタの初期値やリセットにnullptr

スマートポインタでもnullptrを使うと意図が明確です。

C++
#include <memory>
#include <iostream>

int main() {
    std::unique_ptr<int> up = nullptr; // 何も所有していない状態
    up = std::make_unique<int>(10);
    std::cout << *up << "\n";
    up.reset(nullptr); // 所有を破棄してヌルへ
    std::cout << std::boolalpha << static_cast<bool>(up) << "\n";
}
実行結果
10
false

CのAPIに渡す場合も基本はnullptrでOK

Cの関数をC++から呼ぶ場合でも、nullptrは任意のポインタ型にヌルとして正しく変換されます。

例えばstrtolendptrに「不要」を伝えるにはnullptrで問題ありません。

C++
#include <cstdlib>
#include <iostream>

int main() {
    const char* s = "123";
    // 第2引数endptrにnullptrを渡して「不要」を示す
    long v = std::strtol(s, nullptr, 10);
    std::cout << v << "\n";
}
実行結果
123

可変引数のC API(例: printf)にポインタを渡す場合もnullptrを使うとvoid*として正しく渡せます

NULLを渡すと「int 0」として渡って未定義動作になる恐れがあるため避けましょう。

既存コードの移行(初心者向けガイド)

まずはNULLをnullptrに置き換える

最初の一歩は機械的にNULLnullptrへ置き換えることです。

検索しやすい部分から進め、テストを回しながら少しずつ適用します。

ビルド設定をC++11以上にする

C++11以降を有効にします。

例として、GCC/Clangなら-std=c++11以上、MSVCなら/std:c++14/std:c++17などを指定します。

これによりnullptrstd::nullptr_tが使用可能になります。

エラーが出たらオーバーロード箇所を確認

置き換えにより、意図しないオーバーロードが選ばれていたコードが表面化する場合があります。

例えばf(int)が呼ばれていた箇所がf(void*)に切り替わるなどです。

関数シグネチャを見直し、「ポインタならポインタ用」を選ぶ設計に整えましょう。

C++
#include <iostream>

void h(int) { std::cout << "h(int)\n"; }
void h(void*) { std::cout << "h(void*)\n"; }

int main() {
    // 以前はNULLでh(int)に流れていたかもしれないが、
    // nullptrに置換するとh(void*)が選ばれる。
    h(nullptr);
}
実行結果
h(void*)

コード規約にnullptr使用を明記

チームの規約に「ポインタの空値は常にnullptr」と記載し、レビューで徹底します。

静的解析やリンタのルール(例: clang-tidyのmodernize-use-nullptr)を導入すると、継続的に品質を維持できます。

まとめ

NULLは実体が整数であるため、型安全性やオーバーロード解決で不利になり、可変引数などでは未定義動作の危険すらあります。

対してnullptrはstd::nullptr_tという専用型を持つ、ポインタ専用の空値であり、安全で意図が明確、可読性も高いというメリットがあります。

C++11以降の現代的なコードでは、ポインタの空を表すのは常にnullptrと覚えてください。

移行は段階的に進め、オーバーロードや可変引数の箇所を重点的に確認すれば、確実に安全性と保守性を高められます。

実践TIPS - C++の作法とモダンな書き方
この記事を書いた人
エーテリア編集部
エーテリア編集部

C++をこれから学ぶ方に向けて、基礎的な文法や標準ライブラリの使い方を紹介します。モダンな書き方も初心者に合わせてやさしく説明しています。

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

URLをコピーしました!