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
を使って再現します。
#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) | ポインタへも変換可能だが「整数扱い」されやすい | 高い | 非推奨 |
nullptr | std::nullptr_t | 任意のポインタ(およびメンバポインタ) | ない | 推奨 |
結論: ポインタの「空」を表す値は常にnullptrを使うのがC++の作法です。
nullptrを使うメリット
型安全でバグを防ぐ
nullptrは整数ではないため、ポインタと整数が混同されるバグを未然に防ぎます。
例えば条件式や代入で、意図せず整数変換が起こることがありません。
関数オーバーロードで意図が伝わる
オーバーロードの例をもう一つ示します。
std::nullptr_t
は独自の型なので、意図的に「ヌルポインタであること」をコンパイラに伝えられます。
#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」として渡されると呼び出し規約の不一致で未定義動作になる恐れがあります。
特に%p
はvoid
を期待するため、printf("%p", NULL)
は危険です。
nullptrならvoid
に正しく変換され、安全に振る舞います。
#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は整数として多くの場所に「通ってしまう」ため、実行時まで潜むバグを誘発します。
// コンパイルエラーの例(出力はありません)
// int x = nullptr; // エラー: nullptrは整数に変換できない
nullptrの安全な使い方
ポインタの初期化はnullptrで統一
ポインタは常にnullptrで初期化し、未初期化を避けます。
#include <iostream>
int main() {
int* p = nullptr; // 未初期化のダングリングを防止
if (!p) {
std::cout << "pはヌルポインタです\n";
}
}
pはヌルポインタです
nullチェックはif(p)またはp == nullptr
可読性と意図の明確さを両立する書き方です。
if (p)
は空でないポインタの判定としてよく使われますが、p == nullptr
と書くと「ヌル判定」であることがより明確です。
#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を使うと明確になります。
#include <optional>
#include <string>
// 見つからなければnullptrを返す古典的API
const char* find_cstr(bool found) {
return found ? "ok" : nullptr;
}
整数0が欲しいときは0を使う(ポインタにはnullptr)
数値としての0が必要なら0
、ポインタの空はnullptr
。
この住み分けを徹底します。
int main() {
int zero = 0; // 数値の0
int* p = nullptr; // ヌルポインタ
(void)zero; (void)p;
}
スマートポインタの初期値やリセットにnullptr
スマートポインタでもnullptrを使うと意図が明確です。
#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は任意のポインタ型にヌルとして正しく変換されます。
例えばstrtol
のendptr
に「不要」を伝えるにはnullptr
で問題ありません。
#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に置き換える
最初の一歩は機械的にNULL
をnullptr
へ置き換えることです。
検索しやすい部分から進め、テストを回しながら少しずつ適用します。
ビルド設定をC++11以上にする
C++11以降を有効にします。
例として、GCC/Clangなら-std=c++11
以上、MSVCなら/std:c++14
や/std:c++17
などを指定します。
これによりnullptr
とstd::nullptr_t
が使用可能になります。
エラーが出たらオーバーロード箇所を確認
置き換えにより、意図しないオーバーロードが選ばれていたコードが表面化する場合があります。
例えばf(int)
が呼ばれていた箇所がf(void*)
に切り替わるなどです。
関数シグネチャを見直し、「ポインタならポインタ用」を選ぶ設計に整えましょう。
#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と覚えてください。
移行は段階的に進め、オーバーロードや可変引数の箇所を重点的に確認すれば、確実に安全性と保守性を高められます。