livlea blog

Live as if you were to die tomorrow. Learn as if you were to live forever. (Mohandas Karamchand Gandhi)

C++でのクラス初期化方法まとめ

f:id:livlea:20180830011602j:plain

c++でクラスをインスタンス化する際、以下のようにnewを使わない方法、newを使う方法、vectorコンテナを使う方法がある。

Hogeクラス

class Hoge
{
private:
    int num;

public:
    Hoge();
    Hoge(int n);
    void Disp();
    ~Hoge();
};

// デフォルトコンストラクタ
Hoge::Hoge()
{
    num = 0;
}

// 引数付きコンストラクタ
Hoge::Hoge(int n)
{
    num = n; 
}

// メンバ変数の表示
void Hoge::Disp()
{
    std::cout << "num = " << num << std::endl;
}

// デストラクタ
Hoge::~Hoge()
{
}


newを使わないクラスの初期化方法

int main()
{
    // デフォルトコントラクタで初期化
    Hoge a;  // Hoge a = Hoge(); もOK 

    // 引数付きコンストラクタで初期化
    Hoge b(5);   // Hoge b = Hoge(5); もOK

    // デフォルトコンストラクタでクラス配列を初期化
    Hoge c[3];

    // 引数付きコンストラクタでクラス配列を初期化
    Hoge d[3] = {
        Hoge(1),
        Hoge(2),
        Hoge(3) 
    };

    // Disp()の実行
    a.Disp();
    b.Disp();
    c[0].Disp();
    c[1].Disp();
    c[2].Disp();
    d[0].Disp();
    d[1].Disp();
    d[2].Disp();

    return 0;
}

// 実行結果
num = 0  // a.Disp();
num = 5  // b.Disp();
num = 0  // c[0].Disp();
num = 0  // c[1].Disp();
num = 0  // c[2].Disp();
num = 1  // d[0].Disp();
num = 2  // d[1].Disp();
num = 3  // d[2].Disp();


newを使うクラスの初期化方法
(動的確保によるクラスの初期化方法)

int main()
{
    // デフォルトコントラクタで初期化
    Hoge * a = new Hoge;  

    // 引数付きコンストラクタで初期化
    Hoge * b = new Hoge(5);  

    // デフォルトコンストラクタでクラス配列を初期化
    Hoge * c = new Hoge[3];  

    // 引数付きコンストラクタでクラス配列を初期化
    //(ポインタのポインタを使うと引数付きコンストラクタで初期化できる)
    Hoge ** d = new Hoge*[3];  
    d[0] = new Hoge(1);
    d[1] = new Hoge(2);
    d[2] = new Hoge(3);

    // Dispの実行
    a->Disp();
    b->Disp();
    c[0].Disp();  // c[0]->Disp(); はNG
    c[1].Disp();  // c[1]->Disp(); はNG
    c[2].Disp();  // c[2]->Disp(); はNG
    d[0]->Disp();
    d[1]->Disp();
    d[2]->Disp();

    // newした場合はdeleteでメモリ開放が必要
    delete a;
    delete b;
    delete [] c; 
    for (int i=0; i<3; i++) {
        delete d[i];
    }
    delete [] d;

    return 0;
}

// 実行結果
num = 0  // a->Disp();
num = 5  // b->Disp();
num = 0  // c[0].Disp();
num = 0  // c[1].Disp();
num = 0  // c[2].Disp();
num = 1  // d[0]->Disp(); 
num = 2  // d[1]->Disp(); 
num = 3  // d[2]->Disp(); 


vectorコンテナを使うクラスの初期化方法
(動的配列によるクラスの初期化方法)

#include <vector>

int main()
{
    // デフォルトコンストラクタで3つのHogeインスタンスを初期化
    std::vector<Hoge> a(3);

    // 引数付きコンストラクタで3つのHogeインスタンスを初期化
    std::vector<Hoge> b(3, 5);  // 第1引数は配列の要素数、第2引数は引数付きコンストラクタの引数
    std::vector<Hoge> c = {1, 2, 3};  // 1,2,3は各Hogeインスタンスの引数付きコンストラクタの引数

    // Dispの実行
    a[0].Disp();
    a[1].Disp();
    a[2].Disp();
    b[0].Disp();
    b[1].Disp();
    b[2].Disp();
    c[0].Disp();
    c[1].Disp();
    c[2].Disp();

}


// 実行結果
num = 0  // a[0].Disp();
num = 0  // a[1].Disp();
num = 0  // a[2].Disp();
num = 5  // b[0].Disp();
num = 5  // b[1].Disp();
num = 5  // b[2].Disp(); 
num = 1  // c[0].Disp(); 
num = 2  // c[1].Disp(); 
num = 3  // c[2].Disp(); 


newを使わない方法でインスタンスを生成するとスタックと呼ばれるメモリ領域に実体が確保され、newでインスタンスを生成するとフリーストア(≒ヒープ)と呼ばれるメモリ領域に実体が確保されます。newはフリーストアに動的にメモリを確保してそのアドレスを返す演算子であり、そのため、返り値をポインタで受けます。スタックに確保された実体は、スコープを抜けると自動で削除されますが、フリーストアに確保された実体は、スコープを抜けても自動で削除されることはありません。そのため、deleteで解放する必要があります。解放し忘れると使わなくなっても残り続け、さらに、そのアドレスを保持するポインタがなくなってしまうと実体は解放できなくなり、メモリリークになります。

newを使う動的確保では、以下が実現できます。

  • コンパイル時に大きさが決まっていない配列をつくる
  • 巨大な配列を安全につくる (スタックオーバーフローを発生させない)
    • スタックのメモリ領域のサイズはあまり大きくないことが多く、スタック領域に巨大な変数を作るとスタック領域からあふれ出します。これをスタックオーバーフローと言います。
  • 変数の寿命を制御する

vectorはスコープを抜けるとデストラクタが呼ばれますが、deleteできないので所望のタイミングでメモリ解放できません。そこで所望のタイミングで解放したい場合は、以下のようにswapを使います。一時的なオブジェクトはメモリが0でそれと交換することでhogeのメモリが解放され、その後、メモリ0の一時オブジェクトはすぐに解放されるので、結果的にメモリ解放をすることができるという仕組みです。

// 初期化
std::vector<Hoge> hoge(1);

// 解放
std::vector<Hoge>().swap(hoge);
    


スタックとヒープの説明は、以下のサイトが分かりやすいです。
ufcpp.net


動的確保に関してはこちら。
第十一回-02 new 演算子によるメモリの動的確保