C++OpenSiv3D入門講座 Vol. 09 new・delete・スマートポインタ

C++OpenSiv3D入門講座

newとdelete

newを使うと、プログラムの実行中にメモリを確保し、オブジェクトを生成できる。 newすると生成したオブジェクトへのポインタが返ってくる。生成したオブジェクトを見失わないように、ポインタを格納する変数を用意する必要がある。

#include <iostream>

class MyClass{
public:
    int x;

    MyClass(int _x):
    x{ _x }
    {
    }

};

int main(){

    MyClass* ptr = new MyClass{ 32 };

    std::cout << "MyClassのxの値 : " << ptr->x << std::endl;

    return 0;
}

deleteを使うと、プログラム実行中にnewで確保された領域を開放することが出来る。 deleteには開放する領域のポインタを渡す。 下の例では、xの値を表示した後deleteするので、メイン関数を抜ける前にデストラクタが呼ばれる。 deleteされた後にptrのxの値を表示するとカオスな値が入っている(動作未定義)。見てみよう。 deleteしなかったらデストラクタがいつ呼ばれるか確認してみよう。

#include <iostream>

class MyClass{
public:
    int x;

    MyClass(int _x):
    x{ _x }
    {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }

    ~MyClass(){
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};

int main(){
    std::cout << "メイン関数に入りました" << std::endl;

    MyClass* ptr = new MyClass{ 12 };

    std::cout << "MyClassのxの値 : " << ptr->x << std::endl;

    delete ptr;

    std::cout << "メイン関数を抜けました" << std::endl;

    return 0;
}

スマートポインタstd::shared_ptr

newしたポインタは必ずdeleteしなくてはいけない。これは非常に面倒だし、忘れるとメモリリークの原因になる。そんな時に便利なのがスマートポインタだ。 スマートポインタはほぼ通常のポインタと同じ使い方でしかも適切なタイミングで自動的にdeleteされる。 スマートポインタを使用するにはmemoryヘッダーをインクルードする。 スコープを抜ける時自動的にsptrが破棄され、newで確保したMyClassを指すスマートポインタがなくなるので、自動的にnewで確保した領域が開放される。

#include <iostream>
#include <memory>

class MyClass{
public:
    int x;
    MyClass(int _x):
        x{ _x }
    {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
    }

    ~MyClass(){
        std::cout << "デストラクタが呼ばれました" << std::endl;
    }
};


int main(){

    std::cout << "メイン関数に入りました" << std::endl;

    {
        std::cout << "スコープに入りました" << std::endl;
        std::shared_ptr<MyClass> sptr( new MyClass{ 100 } );
        std::cout << "objのxの値:" << sptr->x << std::endl;
        std::cout << "スコープを抜けます" << std::endl;
    }

    std::cout << "メイン関数を抜けます" << std::endl;

    return 0;
}

上の例ではshared_ptrに直接newのポインタを渡して、

std::shared_ptr<MyClass> sptr( new MyClass(100) );

のように書いているが、make_sharedを使って

std::shared_ptr<MyClass> sptr = std::make_shared<MyClass>(100);

と書いた方が処理効率が良い。


書き方

std::shared_ptr<クラス名> 変数名 = std::make_shared<クラス名>(引数1, 引数2, ...);

Tips:std::shared_ptr以外のスマートポインタ

スマートポインタは、他にもコピー出来ないがstd::shared_ptrに比べ高速に動作するstd::unique_ptrや、std::shared_ptrが指すメモリを参照し、循環参照問題を防ぐことができるstd::weak_ptrなどがある。std::shared_ptrに慣れたらこちらも使っていくとよい。

Tips:メンバにポインタを持つクラスの注意点

メンバにポインタを持つクラスの例を以下に示す。コンストラクタでnewし、解放忘れがないようにデストラクタでdeleteしている。実行してみると、問題なく処理が終了していることがわかる。

#include <iostream>

class MyClass {
public:
    int* pVal;
    MyClass(int val) {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
        pVal = new int(val);
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
        delete pVal;
    }
    void showVal() {
        //pValが指す値を表示
        std::cout << *pVal << std::endl;
    }
};


int main() {
    MyClass obj{ 10 };
    //値を表示
    obj.showVal();
    
    return 0;
}

ここで、MyClassオブジェクトの値渡し(コピー)を行ってみる。すると、関数を抜けた後の動作がおかしくなってしまう。

#include <iostream>

class MyClass {
public:
    int* pVal;
    MyClass(int val) {
        std::cout << "コンストラクタが呼ばれました" << std::endl;
        pVal = new int(val);
    }
    ~MyClass() {
        std::cout << "デストラクタが呼ばれました" << std::endl;
        delete pVal;
    }
    void showVal() {
        //pValが指す値を表示
        std::cout << *pVal << std::endl;
    }
};

void hoge(MyClass obj) {
    std::cout << "hoge関数に入りました" << std::endl;
    //値を表示
    obj.showVal();
    std::cout << "hoge関数を抜けます" << std::endl;
}

int main() {
    MyClass obj{ 10 };
    //値を表示
    obj.showVal();
    //funcを呼ぶ
    hoge(obj);
    //値を再度表示
    obj.showVal();
    return 0;
}

実行結果

コンストラクタが呼ばれました
10
hoge関数に入りました
10
hoge関数を抜けます
デストラクタが呼ばれました
-572662307
デストラクタが呼ばれました

動作の流れを追ってみる。hoge関数が呼ばれたところでは、以下の図のような状態になっている。値渡しによりobjのコピーが作成され、それぞれのpValが同じ実体を指している。 f:id:chinimuruhi:20211127175759p:plain hoge関数を抜ける際、以下の図のようにobj(hoge関数内)のデストラクタが呼ばれ、pValの指す先がdeleteされてしまう。 f:id:chinimuruhi:20211127175803p:plain これにより、hoge関数を抜けた後、pValの値を使おうとすると未定義のメモリを使うことになり、おかしなことになってしまう。 また、pValを2度delete(二重解放)することになり、エラーが発生する。

二重解放問題の解決策

  1. 今回の場合、objのコピーを行ったことが原因であるため、オブジェクトのコピーが発生しないよう、参照渡し又はポインタ渡しにすることで回避できる。 オブジェクトのコピーは時間がかかり、このような不具合を生む原因にもなるので、理由が無い限り発生しないようにしよう。
    #include <iostream>
    
    class MyClass {
    public:
        int* pVal;
        MyClass(int val) {
            std::cout << "コンストラクタが呼ばれました" << std::endl;
            pVal = new int(val);
        }
        ~MyClass() {
            std::cout << "デストラクタが呼ばれました" << std::endl;
            delete pVal;
        }
        void showVal() {
            //pValが指す値を表示
            std::cout << *pVal << std::endl;
        }
    };
    
    void hoge(MyClass& obj) {
        std::cout << "hoge関数に入りました" << std::endl;
        //値を表示
        obj.showVal();
        std::cout << "hoge関数を抜けます" << std::endl;
    }
    
    int main() {
        MyClass obj{ 10 };
        //値を表示
        obj.showVal();
        //funcを呼ぶ
        hoge(obj);
        //値を再度表示
        obj.showVal();
        return 0;
    }
    
  2. クラスのコピーを禁止したり、コピーコンストラクタ、代入演算子オーバーロードを正しく設定することで回避することも出来る。興味があれば調べてみよう。

まとめ

  • newを使うと、プログラムの実行中にメモリを確保し、オブジェクトを生成できる。
  • newしたポインタは必ずdeleteしなくてはいけない。
  • スマートポインタは適切なタイミングで自動にdeleteをしてくれる
  • クラス内でnew,deleteする場合は気を付けよう

演習問題(コンソール)

  1. 以下のようなクラスを定義した。MyClassへのポインタのvectorを用意し、適当な値のa(0~10ぐらい)を持ったデータ10個をnewで生成・格納し、表示した後にaが5以下の要素を削除せよ。
    #include <iostream>
    
    class MyClass{
    public:
        int a;
    
        MyClass(int _a):
            a{ _a }
        {
        }
    };
    

    ヒント
    vectorの要素のMyClassへのポインタを削除しただけでは動的に確保した領域は解放されていない。 現時点ではremove_ifを使うとdeleteを呼べないので、whileとeraseを使って削除をすること。
    auto it = vec.begin();
    while(it != vec.end()){
        if(条件){
            it = vec.erase(it);
        }else{
            it++;
        }
    }

演習問題(OpenSiv3D)

今回はEnemyを複数出すだけなので、前半課題とは別に、サンプルプロジェクトを元にを作った方が楽かもしれない。 以下のようなEnenyクラスを用意した。

#pragma once
#include <Siv3D.hpp>

class Enemy {
public:
    static const double Radius;

    Vec2 pos;
    Vec2 velocity;
    Enemy(const Vec2& _pos);
    void update();
    void draw();
};
# include "Enemy.h"

const double Enemy::Radius = 30.0;

Enemy::Enemy(const Vec2& _pos):
    pos{ _pos },
    velocity{ RandomVec2(50.0) }
{
}

void Enemy::update() {
    pos += Scene::DeltaTime() * velocity;
}

void Enemy::draw() {
    Circle{ pos, Radius }.draw(Color{ 255, 0, 0 });
}
  1. Enemy型へのポインタを要素に持つvectorを用意し、newを使って敵を複数生成せよ。
  2. シーン(画面)外に行った敵を削除せよ。newで動的確保した領域もdeleteで解放すること。
  3. Enemy型へのスマートポインタを要素に持つvectorを使って書き直せ。

C++OpenSiv3D入門講座