1. ホーム
  2. c++

C++で動的に割り当てられたオブジェクトへのポインタのベクトルを使用する場合、メモリリークを回避する方法は?

2023-10-14 18:05:08

質問

オブジェクトへのポインタのベクトルを使用しています。これらのオブジェクトはベースクラスから派生したもので、動的に割り当てられて格納されます。

例えばこんな感じです。

vector<Enemy*> Enemies;

で、Enemyクラスから派生して、派生クラスのメモリを動的に確保する、こんな感じです。

enemies.push_back(new Monster());

メモリリークなどの問題を回避するために注意すべきことは何ですか?

どのように解決するのですか?

std::vector はいつものようにあなたのためにメモリを管理しますが、このメモリはオブジェクトではなくポインタになります。

このことが意味するのは、ベクターがスコープ外に出た時点で、あなたのクラスはメモリから失われるということです。例えば

#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<base*> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(new derived());

} // leaks here! frees the pointers, doesn't delete them (nor should it)

int main()
{
    foo();
}

必要なことは、ベクターがスコープ外に出る前に、すべてのオブジェクトを削除することです。

#include <algorithm>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<base*> container;

template <typename T>
void delete_pointed_to(T* const ptr)
{
    delete ptr;
}

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(new derived());

    // free memory
    std::for_each(c.begin(), c.end(), delete_pointed_to<base>);
}

int main()
{
    foo();
}

しかし、これは何らかのアクションを実行することを覚えておかなければならないので、保守が困難です。さらに重要なのは、要素の割り当てと割り当て解除のループの間に例外が発生した場合、割り当て解除のループは実行されず、どのみちメモリリークで立ち往生することになります! これは例外安全性と呼ばれ、割り当て解除が自動的に行われる必要がある重要な理由です。

より良いのは、ポインタが自分自身を削除する場合です。これはスマートポインタと呼ばれ、標準ライブラリが提供する std::unique_ptr std::shared_ptr .

std::unique_ptr は、あるリソースへの一意な(非共有、単一所有の)ポインタを表します。これはデフォルトのスマートポインタであり、生のポインタの使用を完全に置き換えるものであるべきです。

auto myresource = /*std::*/make_unique<derived>(); // won't leak, frees itself

std::make_unique は C++11 標準からは見落とされていますが、自分で作ることができます。直接 unique_ptr (を直接作成するには、以下のようにします。 make_unique よりはお勧めしません)、こうしてください。

std::unique_ptr<derived> myresource(new derived());

一意なポインタは移動セマンティクスのみを持ち、コピーすることはできません。

auto x = myresource; // error, cannot copy
auto y = std::move(myresource); // okay, now myresource is empty

と、これだけで、コンテナの中で使うことができます。

#include <memory>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<std::unique_ptr<base>> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(make_unique<derived>());

} // all automatically freed here

int main()
{
    foo();
}

shared_ptr は参照カウントコピーセマンティクスを持ち、複数の所有者がオブジェクトを共有することを可能にします。これは shared_ptr がいくつ存在するかを追跡し、最後の 1 つが存在しなくなると (そのカウントがゼロになると)、ポインタを解放します。コピーは単に参照カウントを増やすだけです(そして移動は、より低い、ほとんど無料のコストで所有権を移します)。これらを作るには std::make_shared を使って作ります (あるいは上に示したように直接作りますが shared_ptr は内部でアロケーションを行う必要があるため、 一般に、より効率的で技術的に例外安全な方法として make_shared ).

#include <memory>
#include <vector>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

typedef std::vector<std::shared_ptr<base>> container;

void foo()
{
    container c;

    for (unsigned i = 0; i < 100; ++i)
        c.push_back(std::make_shared<derived>());

} // all automatically freed here

int main()
{
    foo();
}

一般に std::unique_ptr をデフォルトとして使いたいことを思い出してください。さらに std::shared_ptr から構成することができます。 std::unique_ptr (から構成することができます(逆はできません)ので、小さなことから始めても大丈夫です。

あるいは、オブジェクトへのポインタを格納するために作成されたコンテナ、例えば boost::ptr_container :

#include <boost/ptr_container/ptr_vector.hpp>

struct base
{
    virtual ~base() {}
};

struct derived : base {};

// hold pointers, specially
typedef boost::ptr_vector<base> container;

void foo()
{
    container c;

    for (int i = 0; i < 100; ++i)
        c.push_back(new Derived());

} // all automatically freed here

int main()
{
    foo();
}

一方 boost::ptr_vector<T> は C++03 で明らかに使用されていましたが、現在ではその関連性を語ることはできません。 std::vector<std::unique_ptr<T>> を使用でき、おそらく同等のオーバーヘッドがほとんどないため、今は関連性を話すことはできませんが、この主張は検証されるべきです。

関係なく コード内で明示的に物事を解放しない . ラップして、リソース管理が自動的に行われるようにします。コードの中でポインターを生で所有することはないはずです。

ゲームでのデフォルトとして、私はおそらく std::vector<std::shared_ptr<T>> . いずれにせよ、私たちは共有することを期待していますし、プロファイリングがそうでないと言うまでは十分に速く、安全で、使いやすいからです。