C言語ではヌルポインタを表すのにNULL
が長く使われてきましたが、C++11ではnullptr
という専用のヌルポインタリテラルが導入されました。
本記事では、なぜNULL
ではなくnullptr
を使うべきなのかを初心者向けに段階を踏んで解説し、実例コードとともに安全な書き方や移行手順を紹介します。
C++のNULLとnullptrの違い
NULLは整数の0相当
C++におけるNULL
は実装依存ですが、一般的には整数リテラル0
や0L
として定義されています。
つまり「ヌルポインタ」そのものではなく、「0という整数がヌルポインタに変換されて使われる」という位置づけです。
これはCとの互換性のための歴史的な経緯によるものです。
そのため、NULL
を使うと「整数として振る舞う」場面が発生し、関数オーバーロードや条件式、テンプレートの型推論で思わぬ挙動を招くことがあります。
実装によってはC++11以降でNULL
をnullptr
として定義するものもありますが、依然として0
扱いの環境が多く、コードの移植性や可読性を下げる原因になりがちです。
以下は、NULL
が整数として扱われやすいことを確かめる簡単な例です。
// 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
です。
これは「ヌルポインタ専用の型」であり、整数とは明確に区別されます。
任意のポインタ型やメンバポインタ型へは変換できますが、整数型には変換されません。
この性質が型安全性を大きく高めます。
#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
はポインタ指向で解決されます。
#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
でも曖昧になることがあります。
そのときは意図した型に明示キャストします。
#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
と比較すればコンパイルエラーとなり、誤用を早期に検出できます。
#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を使うメリット
型安全でバグを防ぐ
nullptr
はstd::nullptr_t
という専用型であり、整数型に紛れません。
整数0
との混同や、思わぬオーバーロード選択を避けられるため、型安全性が向上します。
結果として、意図しない関数呼び出しや条件式の誤判定といったバグを未然に防ぐ効果があります。
関数オーバーロードの曖昧さを解消
「整数パラメータ」と「ポインタパラメータ」の両方がある関数群では、NULL
を渡すと整数扱いになりがちです。
nullptr
であればポインタ側が優先され、意図が明確になります。
曖昧性が残るのは「複数の異なるポインタ型が同時に候補」のときだけで、その場合は明示キャストで解決できます。
テンプレートとautoの型推論が正確
auto a = NULL;
は多くの環境でa
がint
になります。
一方、auto b = nullptr;
はb
がstd::nullptr_t
になります。
テンプレート引数推論でも同様で、nullptr
を使うと「ヌルポインタのつもりが整数として推論される」といったズレを防げます。
#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
由来の整数的な曖昧さが少ないぶん、ツールの誤検知や見落としが減ります。
以下に主な違いを簡潔にまとめます。
観点 | NULL | nullptr |
---|---|---|
型 | 実装依存(多くはintやlongの0) | std::nullptr_t |
整数との関係 | 整数そのもの。整数と容易に混同 | 整数ではない。整数と混同しない |
オーバーロード | 整数パラメータに流れやすい | ポインタパラメータが選ばれやすい |
auto/テンプレート推論 | 整数として推論されがち | ヌルポインタとして正確に推論 |
比較・条件式 | intとの比較が通ってしまう | intとの比較はエラーで誤用検出 |
可変長引数 | 用途により不明確 | 用途明確。必要なら適切なポインタ型にキャスト |
nullptrの安全な使い方
ポインタの初期化とリセットはnullptrで統一
未初期化ポインタは未定義動作の温床です。
生成時と破棄後はnullptr
で明示的に初期化・リセットしましょう。
#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_ptr
やstd::shared_ptr
の利用が推奨されますが、それらでもreset(nullptr)
や= nullptr
が使えます。
条件分岐や比較はnullptrと照合
ポインタの有無判定にはif (p)
でも問題ありませんが、コード規約によってはif (p != nullptr)
のように明示することで可読性が増し、ポインタであることがはっきりします。
#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
を使いましょう。
#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
を使うと意図が明示されます。
#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以上になっているかを確認します。
プリプロセッサでガードする例を示します。
#if __cplusplus < 201103L
#error "このコードはC++11以降が必要です"
#endif
int main() {}
ただし、古いMSVCでは__cplusplus
が正しく上がらない設定があるため、コンパイラオプションを見直してください。
コード中のNULLを一括置換しテスト
基本方針は「NULL
をnullptr
へ機械的に置換し、コンパイルエラー箇所を丁寧に解消」です。
文字列リテラル中の"NULL"
や、NULL
という名前の識別子とは誤置換しないようにツールの条件指定やレビューを行います。
置換後はユニットテストや静的解析を回し、オーバーロードや条件式の挙動が意図通りか確認します。
マクロ定義のNULLに注意
独自ヘッダーで#define NULL 0
などと再定義しているコードは削除または見直しましょう。
標準ヘッダー<cstddef>
のNULL
定義に依存せず、コード側ではnullptr
を直接使うのが安全です。
プロジェクト全体で「NULL
は使わずnullptr
を使う」という方針を統一すると移行がスムーズです。
CのAPIや可変長引数での扱いを確認
Cの関数にnullptr
を渡すこと自体は可能です。
nullptr
は任意のポインタ型に変換できるため、char*
やvoid*
の引数にそのまま渡せます。
ただし、可変長引数(ellipsis, ...
)には注意が必要です。
書式指定に応じた正しい型に明示キャストして渡すのが安全です。
#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では、意図するポインタ型にキャストすると安全です。
// 例: execl("/bin/ls", "ls", "-l", (char*)nullptr); // または static_cast<char*>(nullptr)
このように、Cライブラリや可変長引数では「期待される実引数の型」を明確にし、必要なら適切なポインタ型へキャストしてnullptr
を渡してください。
まとめ
C++におけるNULL
は歴史的経緯により「整数の0相当」であり、オーバーロードや条件式、型推論で意図しない挙動を招きがちです。
C++11で導入されたnullptr
はstd::nullptr_t
という専用型を持つヌルポインタリテラルで、整数との混同がなく、型安全で明確なコードを書けます。
実運用では以下を徹底すると良いでしょう。
- 新しいコードでは
NULL
ではなく常にnullptr
を使う。 - ポインタの初期化・リセット、条件分岐、引数・戻り値、デフォルト引数に
nullptr
を採用する。 - 既存コードはC++11以降を前提に
NULL
をnullptr
へ置換し、テストと静的解析で挙動を確認する。 - CのAPIや可変長引数では、必要に応じて適切なポインタ型にキャストして
nullptr
を渡す。
これらを守ることで、可読性と保守性が高く、安全なC++コードへステップアップできます。