閉じる

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

C言語ではヌルポインタを表すのにNULLが長く使われてきましたが、C++11ではnullptrという専用のヌルポインタリテラルが導入されました。

本記事では、なぜNULLではなくnullptrを使うべきなのかを初心者向けに段階を踏んで解説し、実例コードとともに安全な書き方や移行手順を紹介します。

C++のNULLとnullptrの違い

NULLは整数の0相当

C++におけるNULLは実装依存ですが、一般的には整数リテラル00Lとして定義されています。

つまり「ヌルポインタ」そのものではなく、「0という整数がヌルポインタに変換されて使われる」という位置づけです。

これはCとの互換性のための歴史的な経緯によるものです。

そのため、NULLを使うと「整数として振る舞う」場面が発生し、関数オーバーロードや条件式、テンプレートの型推論で思わぬ挙動を招くことがあります。

実装によってはC++11以降でNULLnullptrとして定義するものもありますが、依然として0扱いの環境が多く、コードの移植性や可読性を下げる原因になりがちです。

以下は、NULLが整数として扱われやすいことを確かめる簡単な例です。

C++
// NULLの実体は実装依存だが、多くの環境で0または0L
#include <iostream>
#include <type_traits>
#include <cstddef> // NULLを含む

int main() {
    auto a = NULL; // 多くの環境でintになる
    std::cout << std::boolalpha
              << "decltype(NULL)がintか: "
              << std::is_same<decltype(a), int>::value << '\n';
}
実行結果
出力例:
decltype(NULL)がintか: true

上記がtrueでない環境もありますが、その場合でも「環境依存で意味がぶれる」こと自体がNULLの難点です。

nullptrは専用型のヌルポインタ

nullptrはC++11で導入されたヌルポインタリテラルで、型はstd::nullptr_tです。

これは「ヌルポインタ専用の型」であり、整数とは明確に区別されます。

任意のポインタ型やメンバポインタ型へは変換できますが、整数型には変換されません。

この性質が型安全性を大きく高めます。

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

int main() {
    std::cout << std::boolalpha
              << "decltype(nullptr)はstd::nullptr_tか: "
              << std::is_same<decltype(nullptr), std::nullptr_t>::value << '\n'
              << "nullptrはvoid*に変換可能か: "
              << std::is_convertible<std::nullptr_t, void*>::value << '\n'
              << "nullptrはintに変換可能か: "
              << std::is_convertible<std::nullptr_t, int>::value << '\n';
}
実行結果
出力例:
decltype(nullptr)はstd::nullptr_tか: true
nullptrはvoid*に変換可能か: true
nullptrはintに変換可能か: false

nullptrは整数と混同されないため、後述するオーバーロードや条件式での挙動が明確になります。

オーバーロード解決が変わる

NULLは整数的な性質を持つため、次のようなオーバーロードでは意図しない関数が選ばれることがあります。

一方、nullptrはポインタ指向で解決されます。

C++
#include <iostream>
#include <cstddef>

void f(int) { std::cout << "f(int)\n"; }
void f(const char*) { std::cout << "f(const char*)\n"; }

int main() {
    f(0);         // 0はint。f(int)が選ばれる
    f(nullptr);   // ヌルポインタ。f(const char*)が選ばれる

    // f(NULL);   // 環境によってはf(int)が選ばれる。NULLがnullptr定義の環境ではf(const char*)になる
}
実行結果
出力例:
f(int)
f(const char*)

なお、ポインタ同士で複数の候補がある場合は、NULLでもnullptrでも曖昧になることがあります。

そのときは意図した型に明示キャストします。

C++
#include <iostream>

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

int main() {
    // g(0);        // どちらのポインタにも変換できるため曖昧(コンパイルエラー)
    // g(nullptr);  // これも曖昧(コンパイルエラー)

    g(static_cast<char*>(nullptr)); // 明示すれば意図通りに選ばれる
    g(static_cast<void*>(nullptr)); // こちらもOK
}
実行結果
出力例:
g(char*)
g(void*)

比較と条件式の挙動が変わる

条件式でNULLは整数の0として評価されるため、整数変数との比較が通ってしまい、バグが見逃されることがあります。

一方、nullptrは整数ではないため、例えばintと比較すればコンパイルエラーとなり、誤用を早期に検出できます。

C++
#include <iostream>

// 誤用例(コンパイルは通ってしまうためバグが潜伏しやすい)
void check_int_with_NULL(int x) {
    if (x == NULL) { /* 実際はx==0の判定になってしまう */
    }
}

/*
コンパイルエラーになる安全な誤用検出例:
void check_int_with_nullptr(int x) {
    if (x == nullptr) { } // エラー: intとnullptrは比較不可、誤用に気づける
}
*/

void check_ptr(const char* p) {
    if (p == nullptr) {
        std::cout << "pはヌルポインタです\n";
    } else {
        std::cout << "pは有効なアドレスです\n";
    }
}

int main() {
    check_ptr(nullptr);
    const char msg[] = "hello";
    check_ptr(msg);
}
実行結果
出力例:
pはヌルポインタです
pは有効なアドレスです

nullptrを使うメリット

型安全でバグを防ぐ

nullptrstd::nullptr_tという専用型であり、整数型に紛れません。

整数0との混同や、思わぬオーバーロード選択を避けられるため、型安全性が向上します。

結果として、意図しない関数呼び出しや条件式の誤判定といったバグを未然に防ぐ効果があります。

関数オーバーロードの曖昧さを解消

「整数パラメータ」と「ポインタパラメータ」の両方がある関数群では、NULLを渡すと整数扱いになりがちです。

nullptrであればポインタ側が優先され、意図が明確になります。

曖昧性が残るのは「複数の異なるポインタ型が同時に候補」のときだけで、その場合は明示キャストで解決できます。

テンプレートとautoの型推論が正確

auto a = NULL;は多くの環境でaintになります。

一方、auto b = nullptr;bstd::nullptr_tになります。

テンプレート引数推論でも同様で、nullptrを使うと「ヌルポインタのつもりが整数として推論される」といったズレを防げます。

C++
#include <iostream>
#include <type_traits>
#include <cstddef>

template <class T>
void who_am_I(T) {
    std::cout << std::boolalpha
              << "Tがintか: " << std::is_same<T, int>::value << ", "
              << "Tがstd::nullptr_tか: " << std::is_same<T, std::nullptr_t>::value
              << '\n';
}

int main() {
    auto a = NULL;      // 多くの環境でint
    auto b = nullptr;   // std::nullptr_t

    std::cout << std::boolalpha
              << "decltype(a)がintか: "
              << std::is_same<decltype(a), int>::value << '\n'
              << "decltype(b)がstd::nullptr_tか: "
              << std::is_same<decltype(b), std::nullptr_t>::value << '\n';

    who_am_I(NULL);     // 多くの環境でT=int
    who_am_I(nullptr);  // T=std::nullptr_t
}
実行結果
decltype(a)がintか: true
decltype(b)がstd::nullptr_tか: true
Tがintか: true, Tがstd::nullptr_tか: false
Tがintか: false, Tがstd::nullptr_tか: true

可読性と静的解析の精度が向上

nullptrは「ヌルポインタ」を明確に表すため、コードを読むだけで意図が伝わりやすくなります。

また、静的解析ツールやリンターもnullptrを手掛かりに誤用をより的確に検出できます。

NULL由来の整数的な曖昧さが少ないぶん、ツールの誤検知や見落としが減ります。

以下に主な違いを簡潔にまとめます。

観点NULLnullptr
実装依存(多くはintやlongの0)std::nullptr_t
整数との関係整数そのもの。整数と容易に混同整数ではない。整数と混同しない
オーバーロード整数パラメータに流れやすいポインタパラメータが選ばれやすい
auto/テンプレート推論整数として推論されがちヌルポインタとして正確に推論
比較・条件式intとの比較が通ってしまうintとの比較はエラーで誤用検出
可変長引数用途により不明確用途明確。必要なら適切なポインタ型にキャスト

nullptrの安全な使い方

ポインタの初期化とリセットはnullptrで統一

未初期化ポインタは未定義動作の温床です。

生成時と破棄後はnullptrで明示的に初期化・リセットしましょう。

C++
#include <iostream>

struct Node {
    int value;
    Node* next;
};

int main() {
    Node* head = nullptr;          // 初期化はnullptrで
    head = new Node{42, nullptr};  // 動的確保時にメンバポインタもnullptr初期化

    // 使い終わったらdeleteしてnullptrに戻す
    delete head;
    head = nullptr;

    std::cout << std::boolalpha << (head == nullptr) << '\n';
}
実行結果
出力例:
true

補足として、モダンC++では生ポインタよりstd::unique_ptrstd::shared_ptrの利用が推奨されますが、それらでもreset(nullptr)= nullptrが使えます。

条件分岐や比較はnullptrと照合

ポインタの有無判定にはif (p)でも問題ありませんが、コード規約によってはif (p != nullptr)のように明示することで可読性が増し、ポインタであることがはっきりします。

C++
#include <iostream>

void use(const char* p) {
    if (p == nullptr) {
        std::cout << "無効な入力です\n";
        return;
    }
    std::cout << "長さ: " << std::char_traits<char>::length(p) << '\n';
}

int main() {
    use(nullptr);
    use("abc");
}
実行結果
無効な入力です
長さ: 3

関数の引数と戻り値にnullptrを採用

「見つからなかった」「まだ値がない」を表す戻り値や、任意指定の引数にはnullptrを使いましょう。

C++
#include <iostream>
#include <cstring>

const char* find_ext(const char* filename) {
    if (filename == nullptr) return nullptr;
    const char* dot = std::strrchr(filename, '.');
    return dot ? dot + 1 : nullptr; // 拡張子がなければnullptr
}

void print_ext(const char* filename) {
    if (const char* ext = find_ext(filename)) {
        std::cout << "ext: " << ext << '\n';
    } else {
        std::cout << "拡張子なし\n";
    }
}

int main() {
    print_ext("report.pdf");
    print_ext("README");
    print_ext(nullptr);
}
実行結果
ext: pdf
拡張子なし
拡張子なし

デフォルト引数や初期化子にnullptrを指定

オプション引数のデフォルト値や、メンバ初期化でもnullptrを使うと意図が明示されます。

C++
#include <iostream>

void log_message(const char* msg, const char* tag = nullptr) {
    std::cout << (tag ? tag : "no-tag") << ": " << msg << '\n';
}

struct Holder {
    const char* p = nullptr; // メンバの初期化子としてnullptr
};

int main() {
    log_message("起動");                 // tagは未指定→nullptr
    log_message("読み込み完了", "INFO"); // tag指定

    Holder h;
    std::cout << std::boolalpha << (h.p == nullptr) << '\n';
}
実行結果
no-tag: 起動
INFO: 読み込み完了
true

NULLからnullptrへの移行ガイド

C++11以降を前提にする

nullptrはC++11からの機能です。

ビルド設定がC++11以上になっているかを確認します。

プリプロセッサでガードする例を示します。

C++
#if __cplusplus < 201103L
#error "このコードはC++11以降が必要です"
#endif

int main() {}

上記コードでコンパイルエラーが発生する場合は、C++11以上になっていません。何も表示されず正常終了する場合はC++11以上になっています

ただし、古いMSVCでは__cplusplusが正しく上がらない設定があるため、コンパイラオプションを見直してください。

コード中のNULLを一括置換しテスト

基本方針は「NULLnullptrへ機械的に置換し、コンパイルエラー箇所を丁寧に解消」です。

文字列リテラル中の"NULL"や、NULLという名前の識別子とは誤置換しないようにツールの条件指定やレビューを行います。

置換後はユニットテストや静的解析を回し、オーバーロードや条件式の挙動が意図通りか確認します。

マクロ定義のNULLに注意

独自ヘッダーで#define NULL 0などと再定義しているコードは削除または見直しましょう。

標準ヘッダー<cstddef>NULL定義に依存せず、コード側ではnullptrを直接使うのが安全です。

プロジェクト全体で「NULLは使わずnullptrを使う」という方針を統一すると移行がスムーズです。

CのAPIや可変長引数での扱いを確認

Cの関数にnullptrを渡すこと自体は可能です。

nullptrは任意のポインタ型に変換できるため、char*void*の引数にそのまま渡せます。

ただし、可変長引数(ellipsis, ...)には注意が必要です。

書式指定に応じた正しい型に明示キャストして渡すのが安全です。

C++
#include <cstdio>   // printf
#include <cstddef>  // nullptr_t

int main() {
    // printf("%p", ...)はvoid*を期待するので、void*にキャストして渡す
    std::printf("null as pointer = %p\n", static_cast<void*>(nullptr));

    // 文字列引数をとるC APIにnullptrを渡す例(仮想)
    const char* s = nullptr;
    std::printf("string = %s\n", s ? s : "(null)");
}
実行結果
出力例:
null as pointer = 0x0
string = (null)

execlのような「引数列の最後をヌルポインタで示す」APIでは、意図するポインタ型にキャストすると安全です。

C++
// 例: execl("/bin/ls", "ls", "-l", (char*)nullptr); // または static_cast<char*>(nullptr)

このように、Cライブラリや可変長引数では「期待される実引数の型」を明確にし、必要なら適切なポインタ型へキャストしてnullptrを渡してください。

まとめ

C++におけるNULLは歴史的経緯により「整数の0相当」であり、オーバーロードや条件式、型推論で意図しない挙動を招きがちです。

C++11で導入されたnullptrstd::nullptr_tという専用型を持つヌルポインタリテラルで、整数との混同がなく、型安全で明確なコードを書けます。

実運用では以下を徹底すると良いでしょう。

  • 新しいコードではNULLではなく常にnullptrを使う。
  • ポインタの初期化・リセット、条件分岐、引数・戻り値、デフォルト引数にnullptrを採用する。
  • 既存コードはC++11以降を前提にNULLnullptrへ置換し、テストと静的解析で挙動を確認する。
  • CのAPIや可変長引数では、必要に応じて適切なポインタ型にキャストしてnullptrを渡す。

これらを守ることで、可読性と保守性が高く、安全なC++コードへステップアップできます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

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

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

URLをコピーしました!