C++におけるnullの意味と扱い方について

AI実装検定のご案内

C++における「null」とは、主にポインタが有効なオブジェクトや関数を指していない状態を表す概念です。

ただし、C++にはJavaやC#のように、あらゆる参照型に共通して使えるnullというキーワードはありません。

C++では、nullポインタを表す方法として、主に次の3つが使われてきました。

nullptr
NULL
0

現在のC++では、原則としてnullptrを使用します。

目次

nullポインタの意味

何も指していないポインタ

ポインタは、変数やオブジェクト、関数などの場所を表す値を保持する型です。

たとえば、次のコードではptrが変数numberを指しています。

int number = 10;
int* ptr = &number;

一方、ポインタが有効な対象を指していないことを明示するには、nullptrを代入します。

int* ptr = nullptr;

この状態のポインタを、nullポインタと呼びます。

nullポインタはアドレス0とは限らない

nullポインタは、必ずしもメモリアドレスの0番地を指しているわけではありません。

C++の仕様上は、有効なオブジェクトや関数を指していない特殊なポインタ値として扱われます。

多くの処理系では内部的に0に相当するビット表現が使われますが、すべての環境で同じ表現になるとは限りません。

そのため、nullポインタは「アドレス0」ではなく、「何も指していないポインタ」と理解するのが適切です。

nullptrの意味と使い方

nullptrはC++11で導入された

nullptrは、C++11で導入されたnullポインタを表すためのキーワードです。

次のように使用します。

int* ptr = nullptr;

nullptrは整数の0ではなく、std::nullptr_tという専用の型を持っています。

そのため、整数とポインタをコンパイラが明確に区別できます。

ポインタをnullptrで初期化する

ポインタを宣言した時点で指し示す対象がない場合は、nullptrで初期化します。

int* ptr = nullptr;

後から変数のアドレスを代入することもできます。

int number = 100;
int* ptr = nullptr;

ptr = &number;

ポインタを宣言時に初期化しておくことで、未初期化ポインタによる不具合を防ぎやすくなります。

nullptrと比較する

ポインタがnullかどうかは、nullptrと比較して確認できます。

int* ptr = nullptr;

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

nullではないことを確認する場合は、次のように記述します。

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

ポインタは条件式の中でboolへ変換されるため、次のように短く書くこともできます。

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

nullであることを確認する場合は、次のように書けます。

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

ただし、この確認で分かるのは、ポインタがnullかどうかだけです。

nullではないポインタが、必ず有効なオブジェクトを指しているとは限りません。

nullptrの型

std::nullptr_tとは

nullptrの型はstd::nullptr_tです。

std::nullptr_tという型名を明示的に使う場合は、通常<cstddef>をインクルードします。

#include <cstddef>

std::nullptr_t nullValue = nullptr;

std::nullptr_t型の値は、オブジェクトポインタ、関数ポインタ、メンバポインタなどのnull値へ変換できます。

int* intPtr = nullptr;
double* doublePtr = nullptr;
void (*funcPtr)() = nullptr;

一方、一般的な整数型へ暗黙的に変換することはできません。

int value = nullptr; // コンパイルエラー

この仕組みによって、nullptrは整数の0と明確に区別されます。

nullptr自体はポインタ型ではない

nullptrは、ポインタ型の値そのものではありません。

std::nullptr_tという独立した型を持つ値であり、必要に応じて各種ポインタ型のnullポインタ値へ変換されます。

そのため、「ポインタ専用のnull値」と説明することはできますが、厳密にはnullptr自体がポインタ型というわけではありません。

NULLとの違い

NULLはマクロである

NULLは、古いCおよびC++コードでnullポインタを表すために使われてきたマクロです。

int* ptr = NULL;

NULLはC++のキーワードではありません。

標準ライブラリのヘッダーで定義されるマクロであり、具体的な定義は処理系によって異なります。

従来の処理系では、次のような整数定数として定義されることが一般的でした。

#define NULL 0

処理系によっては、0Lnullptrに相当する値として定義される場合もあります。

NULLはオーバーロードで問題を起こすことがある

次のようなオーバーロード関数があるとします。

void print(int value) {
    std::cout << "整数\n";
}

void print(int* ptr) {
    std::cout << "ポインタ\n";
}

NULLが整数の0として定義されている処理系では、次の呼び出しで整数版が選ばれたり、呼び出しが曖昧になったりする可能性があります。

print(NULL);

一方、nullptrを渡すと、ポインタ版が選択されます。

print(nullptr);

nullptrは専用の型を持つため、整数の0とnullポインタを区別できます。

現代のC++ではnullptrを使う

C++11以降のコードでは、基本的にNULLではなくnullptrを使用します。

int* ptr = nullptr;

古いコードやC言語との互換性が必要な場面ではNULLを見かけることがあります。

しかし、新しくC++コードを書く場合は、型安全性と可読性の面からnullptrが適しています。

0をnullポインタとして使う方法

整数リテラル0はnullポインタ定数になれる

従来のC++では、整数リテラル0をnullポインタとして使用していました。

int* ptr = 0;

この場合、整数の0がポインタ型のnullポインタ値へ変換されます。

ただし、0は本来整数でもあるため、コードの意図が分かりにくくなります。

void process(int value);
void process(int* ptr);

process(0);

このコードでは、通常は整数版のprocessが呼ばれます。

ポインタを渡したい場合は、nullptrを使う方が明確です。

process(nullptr);

nullポインタを参照するとどうなるか

nullポインタをデリファレンスしてはいけない

ポインタが指している値へアクセスする操作を、デリファレンスまたは間接参照と呼びます。

int number = 10;
int* ptr = &number;

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

しかし、nullポインタをデリファレンスしてはいけません。

int* ptr = nullptr;

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

このコードの動作は未定義動作です。

未定義動作とは、C++規格がプログラムの結果を保証しない状態です。

実行時にクラッシュする場合もあれば、一見正常に動作しているように見える場合もあります。

nullになる可能性があるポインタを使う場合は、事前に確認します。

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

nullポインタからメンバへアクセスしてはいけない

オブジェクトへのポインタがnullの場合、アロー演算子->でメンバへアクセスすることもできません。

struct User {
    int age;
};

User* user = nullptr;

std::cout << user->age << '\n';

このコードも未定義動作になります。

安全にアクセスするには、先にnullチェックを行います。

if (user != nullptr) {
    std::cout << user->age << '\n';
}

未初期化ポインタとの違い

未初期化ポインタはnullとは限らない

自動記憶域期間を持つローカルポインタを、初期化せずに宣言すると、使用可能な初期値は設定されません。

int* ptr;

このptrnullptrである保証はありません。

不定な状態のポインタを読み取ったり、デリファレンスしたりすると、未定義動作につながります。

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

一方、nullptrで初期化したポインタは、何も指していないことが明確です。

int* ptr = nullptr;

静的記憶域期間のポインタはゼロ初期化される

すべてのポインタが、初期化子を省略すると不定な状態になるわけではありません。

グローバル変数やstatic変数など、静的記憶域期間を持つポインタはゼロ初期化され、nullポインタになります。

int* globalPtr;

static int* staticPtr;

ただし、ローカル変数として使うポインタは、原則として宣言時に初期化するべきです。

波かっこでもnullポインタにできる

ポインタを空の波かっこで初期化すると、nullポインタ値になります。

int* ptr{};

この書き方も正しいですが、nullであることを明示したい場合は、nullptrを使う方が意図を読み取りやすいことがあります。

int* ptr = nullptr;

ダングリングポインタとの違い

ダングリングポインタとは

ダングリングポインタとは、以前は有効なオブジェクトを指していたものの、そのオブジェクトの寿命が終了したため、無効になったポインタです。

たとえば、動的に確保したメモリを解放した後のポインタが該当します。

int* ptr = new int(10);

delete ptr;

deleteを実行しても、ptrが自動的にnullptrになるわけではありません。

この時点のptrは、解放済みの領域を指しているダングリングポインタです。

std::cout << *ptr << '\n'; // 未定義動作

解放後にnullptrを代入する

必要に応じて、解放後にnullptrを代入します。

delete ptr;
ptr = nullptr;

これにより、そのポインタ変数を誤って再利用する可能性を下げられます。

ただし、同じアドレスを指している別のポインタがある場合、その別のポインタはダングリングポインタのままです。

int* ptr1 = new int(10);
int* ptr2 = ptr1;

delete ptr1;
ptr1 = nullptr;

// ptr2はダングリングポインタ

そのため、nullptrの代入だけで所有権や寿命の問題を完全に解決できるわけではありません。

deleteとnullptrの関係

nullptrにdeleteを実行しても安全

C++では、nullポインタに対してdeleteを実行しても安全です。

int* ptr = nullptr;

delete ptr;

配列用のdelete[]についても同様です。

int* array = nullptr;

delete[] array;

nullポインタをdeleteしても、オブジェクトの破棄やメモリの解放は行われません。

そのため、次のようなnullチェックは必須ではありません。

if (ptr != nullptr) {
    delete ptr;
}

単純に次のように書けます。

delete ptr;

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

ポインタへnullptrを代入しても、指していたオブジェクトは削除されません。

int* ptr = new int(10);

ptr = nullptr;

このコードで、ptrが確保したオブジェクトを指す唯一のポインタだった場合、解放する手段を失い、メモリリークが発生します。

手動で管理する場合は、先にdeleteする必要があります。

delete ptr;
ptr = nullptr;

ただし、現代のC++では、手動でnewdeleteを管理するよりも、スマートポインタを利用する方が安全です。

関数の引数でnullptrを使う

値が存在しないことを表せる

関数の引数としてポインタを使い、対象が存在しない場合にnullptrを渡す設計があります。

#include <iostream>
#include <string>

void printName(const std::string* name) {
    if (name != nullptr) {
        std::cout << *name << '\n';
    } else {
        std::cout << "名前は設定されていません\n";
    }
}

int main() {
    std::string name = "Taro";

    printName(&name);
    printName(nullptr);
}

この設計では、関数内でポインタがnullになる可能性を考慮する必要があります。

必須の引数には参照を検討する

引数が必ず存在しなければならない場合は、ポインタではなく参照を使う方法があります。

void printName(const std::string& name) {
    std::cout << name << '\n';
}

通常の参照は、ポインタのようにnullptrを使って「対象が存在しない状態」を表すことはできません。

そのため、次のように使い分けられます。

void process(const Data& data); // dataは必須
void process(const Data* data); // nullptrを許容する可能性がある

ただし、ポインタ型だからといって、必ずnullを許可するわけではありません。

関数の仕様やコメント、命名などで、nullを受け付けるかどうかを明確にする必要があります。

また、参照も参照先の寿命が終了すると、ダングリング参照になる可能性があります。

関数の戻り値でnullptrを使う

見つからなかったことを表す

検索関数などでは、対象が見つからなかった場合にnullptrを返す設計があります。

#include <cstddef>

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

    return nullptr;
}

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

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

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

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

この方法では、ポインタがnullかどうかによって検索結果を表現できます。

std::optionalが適している場合もある

値が存在するかどうかだけを表したい場合は、std::optionalが適していることがあります。

#include <optional>

std::optional<int> findValue() {
    return std::nullopt;
}

std::optionalは、値が存在する状態と、存在しない状態を明示的に表せます。

std::nulloptstd::optionalに値がないことを表すものであり、nullptrとは別の概念です。

ポインタは主にオブジェクトの場所や参照関係を表します。

一方、std::optionalは値そのものの有無を表します。

スマートポインタとnullptr

std::unique_ptrのnull状態

std::unique_ptrは、オブジェクトの所有権を1つのスマートポインタで管理する仕組みです。

何も所有していない状態は、nullptrで表せます。

#include <memory>

std::unique_ptr<int> ptr = nullptr;

デフォルト構築した場合も、何も所有していない状態になります。

std::unique_ptr<int> ptr;

オブジェクトを所有しているかどうかは、条件式で確認できます。

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

所有しているオブジェクトを破棄して空の状態にするには、reset()を使います。

ptr.reset();

nullptrを代入することもできます。

ptr = nullptr;

std::shared_ptrのnull状態

std::shared_ptrも、何も所有していない状態を表せます。

#include <memory>

std::shared_ptr<int> ptr = nullptr;

有効性は条件式で確認できます。

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

スマートポインタを使うと、オブジェクトの破棄を自動的に管理できます。

そのため、解放忘れや二重解放などの問題を減らせます。

文字列とnullの関係

std::string自体はnullにならない

std::stringはクラス型のオブジェクトであり、ポインタではありません。

そのため、std::string自体にnull状態を設定することはできません。

std::string text = nullptr; // 現在のC++ではコンパイルエラー

空の文字列を表したい場合は、デフォルト構築するか、空文字列を代入します。

std::string text;
std::string text2 = "";

空文字列とnullポインタは異なる概念です。

空文字列は長さ0の有効な文字列です。

一方、nullポインタは何も指していないポインタです。

const char*はnullptrになり得る

C形式の文字列を指すポインタは、nullptrになる可能性があります。

const char* text = nullptr;

この状態のポインタを、C文字列として文字列関数や出力処理へ渡してはいけません。

std::cout << text; // 正しい動作は保証されない

使用前にnullチェックを行います。

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

また、nullではない場合でも、有効なnull終端文字列を指していることが必要です。

配列とnullptrの関係

配列そのものはnullptrにならない

通常の配列そのものをnullptrにすることはできません。

int array[5];

一方、配列の要素を指すポインタはnullptrになれます。

int* ptr = nullptr;

動的配列を管理するポインタにも、nullptrを設定できます。

int* array = new int[5];

delete[] array;
array = nullptr;

ただし、現代のC++では、固定長配列にはstd::array、可変長配列にはstd::vectorを使う方が安全です。

#include <vector>

std::vector<int> values(5);

標準コンテナを使えば、メモリの解放を自分で管理する必要がありません。

関数ポインタとnullptr

関数を指していない状態を表せる

nullptrは、オブジェクトへのポインタだけでなく、関数ポインタにも使用できます。

void (*funcPtr)() = nullptr;

関数を代入すると、呼び出せるようになります。

#include <iostream>

void hello() {
    std::cout << "Hello\n";
}

int main() {
    void (*funcPtr)() = nullptr;

    funcPtr = hello;

    if (funcPtr != nullptr) {
        funcPtr();
    }
}

nullの関数ポインタを呼び出すと未定義動作になります。

そのため、必要に応じて事前に確認します。

voidポインタとnullptr

void*にもnullptrを代入できる

nullptrvoid*にも変換できます。

void* ptr = nullptr;

void*は、特定の型が決まっていないオブジェクトへのポインタを保持するために使われます。

ただし、void*を直接デリファレンスすることはできません。

値へアクセスするには、適切な型のポインタへ変換する必要があります。

int number = 10;
void* ptr = &number;

int* intPtr = static_cast<int*>(ptr);

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

C++では型安全性の観点から、void*の使用は必要な場面に限定するのが一般的です。

メンバポインタとnullptr

メンバポインタにも使用できる

nullptrは、クラスのメンバを指すメンバポインタにも使用できます。

struct Data {
    int value;
};

int Data::* memberPtr = nullptr;

メンバポインタは、通常のオブジェクトポインタとは異なる型です。

しかし、nullptrからnullメンバポインタ値へ変換できます。

nullptrとboolの関係

条件式ではfalseとして扱われる

nullポインタは、条件式の中ではfalseとして評価されます。

int* ptr = nullptr;

if (ptr) {
    std::cout << "有効です\n";
} else {
    std::cout << "nullです\n";
}

一方、有効なアドレスを保持するポインタはtrueとして評価されます。

int number = 10;
int* ptr = &number;

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

ただし、trueと評価されたからといって、ポインタが必ず安全とは限りません。

ダングリングポインタも、nullでなければtrueと評価されます。

nullptrを扱う際の注意点

nullチェックだけでは安全性を保証できない

ポインタがnullptrではないからといって、必ず有効なオブジェクトを指しているとは限りません。

int* ptr = new int(10);

delete ptr;

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

delete後もptrの値は自動的には変わりません。

そのため、ptr != nullptrが成立していても、ポインタはダングリングポインタになっています。

nullチェックで確認できるのは、nullポインタ値かどうかだけです。

オブジェクトの寿命や所有権まで確認できるわけではありません。

nullptrを代入しても別のポインタには影響しない

同じオブジェクトを複数のポインタが指している場合、1つのポインタをnullptrにしても、ほかのポインタには影響しません。

int number = 10;

int* ptr1 = &number;
int* ptr2 = ptr1;

ptr1 = nullptr;

この時点でも、ptr2は引き続きnumberを指しています。

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

ポインタ変数は、それぞれ独立して値を保持しています。

所有権にはスマートポインタを使う

動的に確保したオブジェクトの所有権を管理する場合は、std::unique_ptrstd::shared_ptrを検討します。

#include <memory>

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

スマートポインタを使うことで、解放忘れ、二重解放、例外発生時のメモリリークなどを防ぎやすくなります。

C++でnullを扱う基本方針

nullポインタにはnullptrを使う

C++11以降では、nullポインタを表すためにnullptrを使用します。

int* ptr = nullptr;

NULL0よりも型安全で、コードの意図が明確です。

ポインタを未初期化のままにしない

ポインタを宣言するときは、有効なアドレスまたはnullptrで初期化します。

int* ptr = nullptr;

未初期化ポインタの使用は、重大な不具合につながる可能性があります。

デリファレンス前に状態を確認する

nullになる可能性があるポインタは、使用前に確認します。

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

ただし、nullではないことだけでは、有効なオブジェクトを指していることまでは保証できません。

値の有無にはstd::optionalも検討する

単に値が存在するかどうかを表したい場合は、std::optionalが適していることがあります。

std::optional<int> result = std::nullopt;

ポインタ、参照、std::optionalは、それぞれ異なる目的を持っています。

用途に応じて使い分けることが重要です。

C++におけるnullのまとめ

C++におけるnullは、主にポインタが有効なオブジェクトや関数を指していない状態を表します。

現代のC++では、nullポインタを表すためにnullptrを使用するのが基本です。

int* ptr = nullptr;

nullptrstd::nullptr_tという専用の型を持つため、整数の0とnullポインタを明確に区別できます。

一方、NULLや整数リテラル0は、古いC++コードで使われることがあります。

しかし、関数のオーバーロードなどで意図しない結果を招く可能性があるため、新しいコードではnullptrを使う方が安全です。

また、nullポインタをデリファレンスしたり、nullポインタからメンバへアクセスしたりすると、未定義動作になります。

ただし、ポインタがnullではないことだけでは、そのポインタが有効であるとは限りません。

ダングリングポインタ、オブジェクトの寿命、所有権についても適切に管理する必要があります。

安全で保守しやすいC++コードを書くには、nullptr、参照、std::optional、標準コンテナ、スマートポインタなどを目的に応じて使い分けることが大切です。

以上、C++におけるnullの意味と扱い方についてでした。

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

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