C++のdeleteの役割や正しい使い方について

AI実装検定のご案内

目次

C++のdeleteとは

C++のdeleteは、newによって動的に生成したオブジェクトを破棄し、そのオブジェクトが使用していたメモリ領域を解放するための演算子です。

C++では、通常のローカル変数であればスコープを抜けたタイミングで自動的に破棄されます。

一方、newを使って動的に生成したオブジェクトは、適切に解放しなければ不要なメモリが残り続ける可能性があります。

そのため、従来のC++ではnewdeleteを対応させてメモリを管理する必要がありました。

基本的な書き方は次のとおりです。

int* p = new int(10);

delete p;

このコードでは、new int(10)によってint型のオブジェクトを動的に生成しています。

そして、不要になった時点でdelete pを実行することで、そのオブジェクトを破棄します。

ただし、現在のC++ではstd::unique_ptrstd::vectorなどを活用し、手動でnewdeleteを記述する機会を減らす設計が一般的です。

C++でdeleteが必要になる理由

newで生成したオブジェクトは自動では破棄されない

通常のローカル変数は、変数のスコープが終了すると自動的に破棄されます。

例えば、次のコードでは明示的なメモリ解放は必要ありません。

void func()
{
    int value = 10;
}

func()が終了すると、valueの寿命も終了します。

一方、次のコードではnewを使用しています。

void func()
{
    int* p = new int(10);
}

この場合、pというポインタ変数自体は関数終了時に破棄されます。

しかし、new int(10)で生成されたオブジェクトは、それだけでは適切に破棄されません。

そのアドレスを失ってしまうと、そのオブジェクトを解放する手段も失われます。

メモリリークの原因になる

不要になった動的メモリを解放できなくなった状態は、一般に「メモリリーク」と呼ばれます。

例えば、次のコードです。

void func()
{
    int* p = new int(10);

    // deleteしないまま関数終了
}

このような処理が繰り返されると、プログラムが使用するメモリ量が増え続ける可能性があります。

手動で管理する場合は、不要になった時点で次のように解放します。

void func()
{
    int* p = new int(10);

    delete p;
}

ただし、後述するスマートポインタを利用すれば、このような手動管理そのものを避けられます。

deleteの基本的な使い方

単一オブジェクトにはdeleteを使用する

通常のnewで1つのオブジェクトを生成した場合は、deleteを使用します。

int* p = new int(100);

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

delete p;

基本的な対応関係は次のとおりです。

new → delete

例えば、double型でも同じです。

double* value = new double(3.14);

delete value;

クラス型の場合も同様に使用できます。

Sample* p = new Sample;

delete p;

delete[]とは

new[]で生成した配列にはdelete[]を使用する

動的配列を生成する場合は、new[]を使用できます。

int* numbers = new int[5];

この配列を解放するときは、通常のdeleteではなくdelete[]を使用します。

delete[] numbers;

対応関係は次のようになります。

確保方法解放方法
newdelete
new[]delete[]

例えば、次のコードは正しい組み合わせです。

int* p1 = new int;

delete p1;

int* p2 = new int[10];

delete[] p2;

deleteとdelete[]を間違えてはいけない

次のコードは誤りです。

int* p = new int[10];

delete p;

new[]で生成した配列に通常のdeleteを使用すると、未定義動作になります。

正しくは次のようにします。

delete[] p;

逆に、通常のnewで生成したオブジェクトへdelete[]を使用するのも誤りです。

int* p = new int;

delete[] p;

したがって、

new   と delete
new[] と delete[]

を必ず対応させる必要があります。

deleteを実行すると何が起こるのか

クラスではデストラクタが実行される

クラス型のオブジェクトに対してdeleteを行うと、通常はそのオブジェクトのデストラクタが実行されます。

#include <iostream>

class Sample
{
public:
    ~Sample()
    {
        std::cout << "destructor\n";
    }
};

int main()
{
    Sample* p = new Sample;

    delete p;
}

この場合、delete pによってSampleのデストラクタが実行されます。

つまり、deleteは単純にメモリ領域だけを返却する操作ではありません。

オブジェクトとして必要な破棄処理を行ったうえで、ストレージの解放処理へ進みます。

配列では各要素が破棄される

クラス型の配列をnew[]で生成した場合は、delete[]によって各要素のデストラクタが実行されます。

class Sample
{
public:
    ~Sample()
    {
    }
};

int main()
{
    Sample* objects = new Sample[5];

    delete[] objects;
}

配列の各要素は、基本的に生成時とは逆の順序で破棄されます。

例えば、

Sample* objects = new Sample[3];

であれば、概念的には、

objects[2]
objects[1]
objects[0]

の順番で破棄されます。

deleteした後のポインタに注意する

deleteしてもポインタ変数自体は残る

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

int* p = new int(10);

delete p;

delete pを実行すると、pが指していたオブジェクトは破棄されます。

しかし、ポインタ変数pそのものがその場で消えるわけではありません。

その結果、pはすでに寿命が終了したオブジェクトのアドレスを保持している状態になります。

このようなポインタは一般に「ダングリングポインタ」と呼ばれます。

delete後に値へアクセスしてはいけない

例えば、次のコードは正しくありません。

int* p = new int(10);

delete p;

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

delete pの時点でオブジェクトの寿命は終了しています。

そのため、その後に*pを使ってアクセスすると未定義動作になります。

同様に、次のような処理も避ける必要があります。

delete p;

*p = 100;

解放済みのオブジェクトへアクセスしてはいけません。

delete後にnullptrを代入する方法

ダングリングポインタを残さない方法

手動でdeleteを使用するコードでは、次のように書くことがあります。

int* p = new int(10);

delete p;
p = nullptr;

pnullptrへ変更することで、少なくともp自身が解放済みのオブジェクトを指し続けることを防げます。

ただし、

delete p;
p = nullptr;

を常に書けば安全になるわけではありません。

別のポインタが存在する場合は問題が残る

例えば、次のようなコードです。

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

delete p1;
p1 = nullptr;

この時点でp1nullptrになっています。

しかし、p2は依然として以前のアドレスを保持しています。

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

このため、根本的には「誰がそのオブジェクトを所有しているのか」を明確にする設計が重要になります。

nullptrをdeleteしても問題ない

delete nullptrは有効

C++では、nullポインタをdeleteしても問題ありません。

int* p = nullptr;

delete p;

このコードは有効です。

そのため、通常は次のようなチェックをわざわざ記述する必要はありません。

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

単純に、

delete p;

とできます。

delete[]についても同様です。

int* p = nullptr;

delete[] p;

このコードも有効です。

二重deleteに注意する

同じオブジェクトを2回deleteしてはいけない

次のようなコードは未定義動作になります。

int* p = new int(10);

delete p;
delete p;

最初のdeleteですでにオブジェクトは破棄されています。

同じポインタ値を再びdeleteすることはできません。

このような問題は「二重解放」や「double delete」などと呼ばれます。

nullptrなら再度deleteしても問題ない

次のコードであれば、2回目のdeleteはnullポインタに対して行われるため有効です。

int* p = new int(10);

delete p;
p = nullptr;

delete p;

ただし、二重解放対策として毎回nullptrを代入するよりも、そもそも手動の所有権管理を減らしたほうが安全です。

newで生成していないオブジェクトにdeleteを使わない

ローカル変数をdeleteしてはいけない

例えば、次のコードは誤りです。

int value = 10;

int* p = &value;

delete p;

valueは通常のローカル変数であり、対応するnew式によって生成されたオブジェクトではありません。

このようなアドレスをdeleteしてはいけません。

確保方法と解放方法を一致させる

重要なのは、「ポインタだからdeleteできる」という考え方をしないことです。

そのポインタが指しているオブジェクトが、

どの方法で生成されたのか

を確認する必要があります。

例えば、

int* p = new int[10];

は確かにnewを使用しています。

しかし、配列形式のnew[]なので、

delete[] p;

とする必要があります。

mallocとdeleteを混在させない

mallocにはfreeを使用する

C++では、C言語由来のmalloc()を使うこともできます。

#include <cstdlib>

int* p = static_cast<int*>(
    std::malloc(sizeof(int))
);

std::free(p);

malloc()で確保したメモリはfree()で解放します。

deleteを使用してはいけません。

newにはdeleteを使用する

一方、newを使った場合はdeleteを使用します。

int* p = new int;

delete p;

主な対応関係は次のとおりです。

確保方法解放方法
newdelete
new[]delete[]
malloc()free()
calloc()free()
realloc()で得た領域free()

例えば、

int* p = static_cast<int*>(
    std::malloc(sizeof(int))
);

delete p;

は誤りです。

逆に、

int* p = new int;

std::free(p);

も誤りです。

クラスでdeleteを使う方法

動的に生成したクラスを破棄する

クラスのオブジェクトもnewによって動的に生成できます。

#include <iostream>
#include <string>

class User
{
public:
    User(const std::string& name)
        : name_(name)
    {
    }

    void show() const
    {
        std::cout << name_ << '\n';
    }

private:
    std::string name_;
};

int main()
{
    User* user = new User("Taro");

    user->show();

    delete user;
}

この場合、

new User("Taro")

によってUserオブジェクトを生成しています。

不要になった時点で、

delete user;

によって破棄します。

継承とdeleteでは仮想デストラクタが重要

基底クラスのポインタから派生クラスを削除する場合

C++では、基底クラスへのポインタを使って派生クラスのオブジェクトを扱うことがあります。

例えば、次のようなコードです。

class Base
{
public:
    ~Base() = default;
};

class Derived : public Base
{
};

Base* p = new Derived;

delete p;

このように派生クラスのオブジェクトを基底クラスへのポインタ経由で削除する場合、基底クラスのデストラクタが適切な仮想デストラクタになっていないと、一般的なケースでは未定義動作になります。

virtualデストラクタを定義する

ポリモーフィックに削除する可能性がある基底クラスでは、次のようにします。

class Base
{
public:
    virtual ~Base() = default;
};

すると、

Base* p = new Derived;

delete p;

とした場合でも、派生クラスから基底クラスへ適切にデストラクタが呼び出されます。

特に、仮想関数を利用するポリモーフィックな基底クラスでは、仮想デストラクタの必要性を確認することが重要です。

deleteで起こりやすい間違い

new[]にdeleteを使う

誤った例です。

int* p = new int[10];

delete p;

正しくは次のようにします。

delete[] p;

delete後にアクセスする

誤った例です。

int* p = new int(100);

delete p;

std::cout << *p;

解放済みのオブジェクトへアクセスしているため、未定義動作になります。

同じオブジェクトを2回deleteする

int* p = new int(100);

delete p;
delete p;

これも未定義動作です。

newしたオブジェクトを解放しない

void func()
{
    int* p = new int(100);
}

この場合、pというポインタ変数は関数終了時に消滅します。

しかし、動的に生成したオブジェクトへの参照を失ってしまうため、メモリリークにつながります。

deleteと所有権の関係

ポインタを持っているだけではdeleteしてよいとは限らない

C++では、ポインタを保持していることと、そのオブジェクトの所有者であることは同じではありません。

例えば、

int* owner = new int(10);

int* observer = owner;

とすると、ownerobserverは同じオブジェクトを指しています。

しかし、

delete owner;
delete observer;

とすることはできません。

同じオブジェクトを2回解放してしまうからです。

誰が解放するのかを明確にする

生ポインタだけでは、

このポインタは所有者なのか
単に参照しているだけなのか

がコード上で分かりにくくなります。

この問題を解決しやすくするのが、std::unique_ptrstd::shared_ptrなどのスマートポインタです。

現代のC++ではdeleteを直接使わないことが多い

RAIIを活用する

現代のC++では、RAIIという考え方が非常に重要です。

RAIIは、

Resource Acquisition Is Initialization

の略です。

リソースの寿命をオブジェクトの寿命に結び付ける設計方法を意味します。

例えば、スマートポインタがスコープを抜けたときに管理対象を自動的に破棄する仕組みは、RAIIの代表例です。

std::unique_ptrを使う

単一所有ならunique_ptrが便利

従来は次のように書くことがありました。

int* p = new int(100);

// 処理

delete p;

現代のC++では、単一の所有者が管理するのであればstd::unique_ptrを利用できます。

#include <memory>

int main()
{
    auto p = std::make_unique<int>(100);
}

pがスコープを抜ければ、管理しているオブジェクトも自動的に破棄されます。

そのため、明示的なdeleteは必要ありません。

make_uniqueを利用する

例えば、

auto p = new MyClass;

として手動で管理するよりも、

auto p = std::make_unique<MyClass>();

とするほうが、所有権が明確になりやすく、例外発生時のリソース管理もしやすくなります。

std::shared_ptrを使う

複数の所有者が必要な場合に利用する

1つのオブジェクトを複数箇所で共有所有する必要がある場合は、std::shared_ptrを利用できるケースがあります。

#include <memory>

int main()
{
    auto p1 = std::make_shared<int>(100);

    auto p2 = p1;
}

std::shared_ptrでは、基本的に最後の所有者がなくなったタイミングで管理対象のオブジェクトが破棄されます。

ただし、単純に便利だからという理由でstd::shared_ptrを多用するのは適切ではありません。

共有所有が必要でないなら、std::unique_ptrなど、より明確な所有権を持つ方法を検討したほうがよいでしょう。

動的配列ではvectorを検討する

new[]とdelete[]を使わずに管理できる

例えば、従来は次のように動的配列を作成できます。

int* values = new int[100];

delete[] values;

しかし、多くのケースではstd::vectorを使ったほうが便利です。

#include <vector>

int main()
{
    std::vector<int> values(100);
}

std::vectorは内部のメモリ管理を自動的に行います。

そのため、プログラマがdelete[]を書く必要はありません。

サイズ変更もしやすい

例えば、

std::vector<int> values;

values.push_back(10);
values.push_back(20);
values.push_back(30);

のように、必要に応じて要素を増やせます。

一般的な動的配列では、手動のnew[]delete[]よりもstd::vectorを優先できるケースが多くあります。

deleteとoperator deleteの違い

delete式とoperator deleteは同じではない

C++を詳しく学ぶ場合は、deleteoperator deleteの違いも理解しておくとよいでしょう。

例えば、

delete p;

は「delete式」です。

一方、

operator delete

はストレージの解放に関係するデアロケーション関数です。

通常のケースを概念的に表すと、

delete式
   ↓
オブジェクトの破棄
   ↓
適切なデアロケーション処理

という流れになります。

初心者のうちは、

delete = newで生成したオブジェクトを適切に破棄するための仕組み

と理解しておけば十分ですが、メモリアロケータやクラス固有のメモリ管理を学ぶ際には両者の違いが重要になります。

C++のdeleteを使うときの重要ルール

newとdeleteを対応させる

T* p = new T;

delete p;

new[]とdelete[]を対応させる

T* p = new T[n];

delete[] p;

二重deleteをしない

delete p;
delete p;

このような処理は避けます。

delete後にオブジェクトへアクセスしない

delete p;

*p = 100;

のような処理も避けます。

newで生成していないオブジェクトをdeleteしない

int value = 10;

delete &value;

のようなコードは正しくありません。

mallocとdeleteを組み合わせない

void* p = std::malloc(100);

std::free(p);

のように、確保方法に対応する解放方法を使用します。

C++のdeleteとdelete[]の違い

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

項目deletedelete[]
主な対象単一オブジェクト配列
対応する生成方法newnew[]
使用例delete p;delete[] p;
クラス型1オブジェクトを破棄配列要素をそれぞれ破棄
組み合わせを間違えた場合未定義動作未定義動作

最も重要なのは、

new     → delete
new[]   → delete[]

という対応関係です。

現代のC++におけるdeleteの考え方

deleteそのものが非推奨になったわけではない

deleteは現在のC++でも正式な言語機能です。

そのため、

deleteは古い機能なので使ってはいけない

という理解は正確ではありません。

重要なのは、アプリケーションコードで手動のメモリ管理が本当に必要なのかを検討することです。

可能ならRAIIにメモリ管理を任せる

現代のC++では、次のような機能を利用できます。

std::unique_ptr
std::shared_ptr
std::vector
std::string

これらはオブジェクト自身がリソースの寿命を管理してくれるため、手動でdeleteを書く必要を減らせます。

例えば、

auto p = new MyClass;

// 後でdeleteが必要

よりも、

auto p = std::make_unique<MyClass>();

のほうが、単一所有という意図をコード上で表現しやすくなります。

したがって、現代のC++では、

deleteを正しく書く

だけではなく、

そもそもdeleteを直接書かなくて済む設計にできないか

を考えることが重要です。

C++のdeleteについてのまとめ

C++のdeleteは、newによって生成された単一オブジェクトを破棄するための演算子です。

配列をnew[]で生成した場合は、delete[]を使用します。

基本的な対応関係は次のとおりです。

new     → delete
new[]   → delete[]

また、deleteを使用するときには、二重解放、解放後のアクセス、確保方法と解放方法の不一致などに注意する必要があります。

特に次のような処理は避けなければなりません。

delete p;
delete p;
delete p;
*p = 100;
int* p = new int[10];
delete p;
int value = 10;
delete &value;

さらに、継承関係では、派生クラスのオブジェクトを基底クラスへのポインタ経由で削除する場合に仮想デストラクタが重要になります。

一方、現在のC++では、手動のnewdeleteに頼るよりも、std::unique_ptrstd::shared_ptrstd::vectorなどを利用したRAIIベースの設計が一般的です。

そのため、deleteについては仕組みや危険性を正しく理解したうえで、実際のプログラムでは可能な限り自動的にリソースを管理できる仕組みを活用すると、安全性や保守性の高いC++コードを書きやすくなります。

以上、C++のdeleteの役割や正しい使い方についてでした。

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

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