C++のnullptrの意味や使い方について

AI実装検定のご案内

nullptrとは、C++11で導入された、ヌルポインタを表すための専用リテラルです。

簡単にいえば、「このポインタは現在、有効なオブジェクトを指していない」という状態を表すために使います。

たとえば、次のように記述します。

int* ptr = nullptr;

このコードでは、ptrint型の値を指すためのポインタですが、現時点では何も指していません。

C++11以降では、ヌルポインタを表す際にNULL0ではなく、基本的にnullptrを使うことが推奨されています。

目次

nullptrの意味

nullptrはヌルポインタリテラル

厳密には、nullptrそのものがポインタというわけではありません。

nullptrは「ヌルポインタリテラル」と呼ばれる特別な値です。

型は次のとおりです。

std::nullptr_t

nullptrは、さまざまなポインタ型のヌルポインタ値へ変換できます。

たとえば、次のコードはいずれも有効です。

int* p1 = nullptr;
double* p2 = nullptr;
char* p3 = nullptr;

クラスへのポインタにも使えます。

class User
{
};

User* user = nullptr;

つまり、nullptrは特定のポインタ型専用ではなく、「有効な参照先がない」という状態を共通して表現するための仕組みです。

nullptrが導入された理由

C++11以前は0やNULLが使われていた

C++11より前では、ヌルポインタを表すために0NULLがよく使われていました。

int* p1 = 0;
int* p2 = NULL;

これらの書き方も、多くの場合はヌルポインタとして機能します。

しかし、0NULLは整数として扱われる可能性があるため、型の判定や関数オーバーロードで問題が起こることがあります。

nullptrならポインタである意図が明確になる

nullptrはヌルポインタを表すために設計された専用リテラルです。

そのため、次のコードを見れば、整数の0ではなく「何も指していないポインタを表している」と明確に分かります。

int* ptr = nullptr;

可読性と型安全性の両面で、NULL0より優れています。

nullptrとNULLの違い

NULLは実装依存のマクロ

NULLはC言語や従来のC++で広く使われてきました。

環境によっては、概念的に次のような形で定義されていることがあります。

#define NULL 0

ただし、実際の定義は処理系や言語モードによって異なります。

そのため、「NULLは必ず整数の0である」と断定するのは正確ではありません。

重要なのは、C++ではNULLが整数型として扱われるケースがあり、オーバーロード解決などで意図しない挙動につながる可能性があることです。

nullptrには専用の型がある

一方、nullptrには専用の型があります。

std::nullptr_t

これにより、整数の0とヌルポインタを明確に区別できます。

nullptrと0の違い

ポインタ初期化ではどちらも使える

次のコードはいずれも有効です。

int* p1 = 0;
int* p2 = nullptr;

どちらも結果的にはヌルポインタになります。

ただし、型安全性や可読性を考えると、C++11以降ではnullptrを使う方が適切です。

nullptrは整数ではない

次のコードはエラーになります。

int value = nullptr;

nullptrは整数値ではないため、通常のint型変数へそのまま代入することはできません。

一方、ポインタであれば問題ありません。

int* ptr = nullptr;

この違いによって、コンパイラは「整数の0」と「ヌルポインタ」を区別できます。

関数オーバーロードにおけるnullptrのメリット

0では整数版の関数が選ばれる

次のようなオーバーロードを考えてみます。

#include <iostream>

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

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

int main()
{
    func(0);
}

この場合、0int型の整数リテラルなので、次の関数が選択されます。

void func(int);

もしプログラマーが「ヌルポインタを渡したつもり」だった場合、意図とは異なる結果になります。

nullptrならポインタ版が選ばれる

nullptrを使うと、意図が明確になります。

#include <iostream>

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

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

int main()
{
    func(nullptr);
}

この場合は、次のポインタを受け取る関数が選ばれます。

void func(int*);

このように、関数オーバーロードにおける曖昧さを減らせる点は、nullptrの大きなメリットです。

nullptrの基本的な使い方

ポインタの初期化に使う

ポインタの参照先がまだ決まっていない場合は、nullptrで初期化できます。

int* ptr = nullptr;

次のように初期化しないまま宣言するより安全です。

int* ptr;

ローカル変数として宣言された未初期化ポインタは、不定な値を持つ可能性があります。

誤って使用すると、未定義動作につながる可能性があります。

そのため、参照先が決まっていない場合は、最初からnullptrで初期化しておく方が安全です。

リスト初期化でも書ける

現代C++では、次のような書き方もできます。

int* ptr{nullptr};

意味は同じです。

nullptrかどうかを判定する方法

== nullptrで判定できる

ポインタがヌルポインタかどうかは、次のように確認できます。

int* ptr = nullptr;

if (ptr == nullptr)
{
    std::cout << "何も指していません\n";
}

逆に、ヌルポインタではないことを確認する場合は次のように書けます。

if (ptr != nullptr)
{
    std::cout << *ptr << '\n';
}

if(ptr)でも判定できる

ポインタは条件式の中で判定できます。

if (ptr)
{
    std::cout << *ptr << '\n';
}

ptrがヌルポインタならfalseとして扱われ、ヌルポインタでなければtrueとして扱われます。

逆に、次のようにも書けます。

if (!ptr)
{
    std::cout << "ptrはnullptrです\n";
}

実際のC++コードでは、if (ptr)if (!ptr)という書き方もよく使われます。

nullptrをデリファレンスしてはいけない

nullptrの参照先にはアクセスできない

次のコードは危険です。

int* ptr = nullptr;

std::cout << *ptr << '\n';

*ptrは、ポインタが指しているオブジェクトへアクセスする操作です。

しかし、ptrは有効なオブジェクトを指していません。

そのため、ヌルポインタをデリファレンスすると未定義動作になります。

必ずクラッシュするとは限らない

ヌルポインタをデリファレンスすると、実際の環境ではプログラムがクラッシュすることがあります。

ただし、C++規格として「必ずクラッシュする」と保証されているわけではありません。

未定義動作であるため、どのような結果になるかは保証されません。

必要に応じて、デリファレンスする前にポインタを確認します。

if (ptr != nullptr)
{
    std::cout << *ptr << '\n';
}

nullptrを代入してもオブジェクトは削除されない

ポインタの参照先が変わるだけ

次のコードを考えてみます。

int value = 100;
int* ptr = &value;

ptr = nullptr;

この場合、ptrvalueを指さなくなります。

しかし、valueそのものが削除されるわけではありません。

nullptrをポインタへ代入することと、オブジェクトの寿命を終了させることは別の処理です。

delete後にnullptrを代入する使い方

deleteしただけではポインタはnullptrにならない

次のコードを考えてみます。

int* ptr = new int(100);

delete ptr;

deleteによって動的に確保したオブジェクトは破棄されます。

しかし、ptrという変数自体が自動的にnullptrになるわけではありません。

この時点のptrは、すでに寿命が終了したオブジェクトのアドレス値を保持しているダングリングポインタになります。

そこで、次のように書くことがあります。

delete ptr;
ptr = nullptr;

これにより、そのptrという変数については「有効な参照先を持っていない」という状態を明確にできます。

他のポインタまで安全になるわけではない

ただし、nullptrを代入すればすべて安全になるわけではありません。

int* p1 = new int(100);
int* p2 = p1;

delete p1;
p1 = nullptr;

この場合、p1nullptrになります。

しかし、p2は解放済みオブジェクトのアドレスを保持したままです。

つまり、p2はダングリングポインタです。

p1 = nullptr;によって安全になるのは、あくまでp1という変数についてだけです。

現代C++ではスマートポインタを使う

unique_ptrにもnullptrを使える

現代C++では、単純な所有権管理のためにnewdeleteを直接扱うより、std::unique_ptrなどのスマートポインタを使う方が一般的です。

#include <memory>

std::unique_ptr<int> ptr = nullptr;

この場合、ptrはオブジェクトを所有していません。

オブジェクトを生成する場合は、次のようにstd::make_uniqueを使えます。

auto ptr = std::make_unique<int>(100);

スマートポインタを空に戻すこともできる

std::unique_ptrを空の状態に戻す場合は、reset()を使えます。

ptr.reset();

また、次のようにnullptrを代入することもできます。

ptr = nullptr;

所有中のオブジェクトがある場合は適切に破棄され、その後スマートポインタは空の状態になります。

std::nullptr_tとは

nullptr専用の型

nullptrの型はstd::nullptr_tです。

std::nullptr_t<cstddef>で定義されています。

#include <cstddef>

std::nullptr_t nullValue = nullptr;

通常のアプリケーションコードでstd::nullptr_t型の変数を直接作る機会は多くありません。

しかし、テンプレートやオーバーロードなど、型を厳密に扱う処理では理解しておくと役立ちます。

decltypeでも型を確認できる

nullptrの型は、decltypeを使って確認できます。

decltype(nullptr)

これはstd::nullptr_tと同じ型です。

void*にもnullptrを使える

voidポインタのヌル値にも使える

nullptrvoid*にも変換できます。

void* ptr = nullptr;

そのため、void*を扱うC言語由来のAPIなどでも利用できます。

ただし、外部APIを使用する場合は、そのAPIが求めている引数の仕様を確認する必要があります。

メンバーポインタにもnullptrを使える

通常のポインタ以外にも使用可能

nullptrは通常のオブジェクトポインタだけでなく、メンバーポインタ型にも使用できます。

class Sample
{
public:
    int value;
};

int Sample::* ptr = nullptr;

日常的なC++プログラミングでは登場頻度の低い機能ですが、nullptrが単なる整数の0ではないことを理解するうえでは重要です。

関数の引数としてnullptrを使う

「対象がない」ことを表現できる

ポインタを引数に取る関数では、nullptrを使って「対象となるオブジェクトがない」ことを表現できます。

#include <iostream>

void printValue(const int* ptr)
{
    if (ptr == nullptr)
    {
        std::cout << "値がありません\n";
        return;
    }

    std::cout << *ptr << '\n';
}

呼び出し側では次のように使えます。

int value = 100;

printValue(&value);
printValue(nullptr);

ただし、「値がある場合とない場合」を表現する目的であれば、設計によってはC++17以降のstd::optionalなどが適切なケースもあります。

関数の戻り値としてnullptrを使う

「見つからなかった」を表現できる

ポインタを返す関数では、対象が存在しなかった場合にnullptrを返す設計があります。

int* findValue(int* values, int size, int target)
{
    for (int i = 0; i < size; ++i)
    {
        if (values[i] == target)
        {
            return &values[i];
        }
    }

    return nullptr;
}

呼び出し側では、戻り値を確認してから使用します。

int values[] = {10, 20, 30};

int* result = findValue(values, 3, 20);

if (result != nullptr)
{
    std::cout << *result << '\n';
}

見つからなかった場合はnullptrが返されるため、結果の有無を判定できます。

nullptrチェックだけではポインタの安全性を保証できない

nullptrではなくても無効なポインタは存在する

次のコードを考えてみます。

int* ptr = new int(100);

delete ptr;

if (ptr != nullptr)
{
    // ptrはnullptrではない可能性がある
}

delete後のptrは、自動的にはnullptrになりません。

そのため、ptr != nullptrであっても、参照先が有効とは限りません。

ダングリングポインタはnullptrチェックでは検出できない

nullptrチェックで分かるのは、基本的に「ヌルポインタ値かどうか」です。

ポインタが解放済みオブジェクトを指しているかどうかや、参照先の寿命が終了しているかどうかまでは判定できません。

したがって、次のように考えるのは危険です。

if (ptr != nullptr)
{
    // 必ず安全とは限らない
}

ポインタの安全性を確保するには、オブジェクトの寿命や所有権も適切に管理する必要があります。

nullptrとNULLと0の違い

主な違いを比較

nullptrNULL0の違いを整理すると、次のようになります。

書き方主な意味型安全性C++11以降での推奨度
nullptrヌルポインタを表す専用リテラル高い推奨
NULL従来のヌルポインタ表現nullptrより低い新規コードでは原則避ける
0整数0、またはヌルポインタ定数nullptrより低い新規コードでは原則避ける

既存の古いC++コードではNULL0を見ることがありますが、新しくコードを書く場合はnullptrを基本にするとよいでしょう。

nullptrを使う主な場面

参照先がまだ決まっていない場合

User* user = nullptr;

ポインタを安全な初期状態にしておきたい場合に使えます。

オブジェクトが存在しないことを表す場合

process(nullptr);

関数へ「対象なし」という意味を渡したい場合に利用できます。

検索失敗などを戻り値で表す場合

return nullptr;

ポインタを返す関数で、「該当するオブジェクトがなかった」という意味を表現できます。

ポインタを空の状態に戻したい場合

ptr = nullptr;

現在の参照先を持たない状態に戻したい場合に使えます。

nullptrを使った実用例

値を検索してポインタを返す

次のプログラムでは、配列から指定された値を検索しています。

#include <iostream>

int* findNumber(int* numbers, int size, int target)
{
    for (int i = 0; i < size; ++i)
    {
        if (numbers[i] == target)
        {
            return &numbers[i];
        }
    }

    return nullptr;
}

int main()
{
    int numbers[] = {10, 20, 30, 40, 50};

    int* result = findNumber(numbers, 5, 30);

    if (result != nullptr)
    {
        std::cout << "見つかりました: " << *result << '\n';
    }
    else
    {
        std::cout << "見つかりませんでした\n";
    }

    return 0;
}

findNumber()は、目的の値を見つけた場合、その要素へのポインタを返します。

見つからなかった場合は、次のようにnullptrを返します。

return nullptr;

呼び出し側では、戻り値がnullptrでないことを確認してからデリファレンスしています。

if (result != nullptr)
{
    std::cout << *result << '\n';
}

このように「対象が存在する場合はポインタを返し、存在しない場合はnullptrを返す」という設計は、C++でよく使われるパターンです。

C++11以降ではnullptrを使うのが基本

C++11以降のコードでは、ヌルポインタを表す際は次の書き方を基本にするとよいでしょう。

int* ptr = nullptr;

古いコードでは、次のような書き方を見ることがあります。

int* ptr = NULL;

または、

int* ptr = 0;

これらも既存コードでは珍しくありません。

しかし、新しいC++コードでは、特別な理由がない限りnullptrを選ぶのが適切です。

nullptrについて覚えておきたいポイント

nullptrは、単に「NULLの新しい書き方」ではありません。

C++11で導入された、ヌルポインタを表すための型安全な専用リテラルです。

特に重要なのは、次のポイントです。

  • nullptrはC++11で導入されたヌルポインタリテラルです。
  • nullptrの型はstd::nullptr_tです。
  • int* ptr = nullptr;のように、参照先を持たないポインタを表現できます。
  • NULL0よりも型安全で、関数オーバーロードの問題を避けやすくなります。
  • nullptrをデリファレンスすると未定義動作になります。
  • ptr == nullptrif (!ptr)などでヌルポインタかどうかを確認できます。
  • nullptrを代入しても、元のオブジェクトが自動的に削除されるわけではありません。
  • delete後にポインタが自動的にnullptrになるわけでもありません。
  • nullptrチェックだけではダングリングポインタなどの問題を検出できません。
  • C++11以降の新規コードでは、原則としてNULL0よりnullptrを使うのがおすすめです。

C++のポインタを扱ううえでは、「参照先がない状態をnullptrで明示する」「ヌルポインタをデリファレンスしない」「ポインタの寿命や所有権にも注意する」という3点を意識すると、安全で読みやすいコードを書きやすくなります。

以上、C++のnullptrの意味や使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次