メモリの記憶域の話~デストラクタを添えて~


こんにちはぷないしんです。

 

今日はクラスの外側のメモリ管理の話をします。

 あとちょっぴりデストラクタの話も

 

ということで、まずは配列クラスを使ってクラスの外側の資源の管理方法です。

整数型の配列を実現するクラスを記述します。

 

IntArray.h

//整数型配列クラス

#ifndef ___Class_IntArray
#define ___Class_IntArray

class IntArray{
    int nelem;
    int* vec;

public:
    //コンストラクタ
    IntArray(int size) : nelem(size){
        vec = new int[nelem];
    } 

    //要素の数を返却
    int size() const {
        return nelem;
        }

    //演算子
    int& operator(int i){
        return vec[i];
        }
};

#endif

 

 

このクラスのデータメンバーは要素の数であるnelemと、配列の先頭の要素を指すポインタであるvecの二つです。

要素の数であるnelemの値は、メンバ関数のsizeで調べられます。

 

では、コンストラクタとこの関数についてお話します。

 

コンストラクタ

    IntArray(int size) : nelem(size){
        vec = new int[nelem];
    } 

コンストラクタ本体では、メモリの領域を確保し、配列の本体を動的に作ります。

生成する配列の要素の数は、仮引数であるsizeで受け取った値です。

 

オブジェクトが下の様に定義されたときのコンストラクタの動作ですが

 

intArray x(5);

 

まず、nelem(size)の働きで、データメンバーnelemが5で初期化されます。

次に実行されるコンストラクタではnew演算子で確保したnelem個分、記憶域の先頭要素へのポインタをvecに代入します。

 

f:id:punainen:20210319155904p:plain

 実際に確保しているメモリ域は生成したオブジェクトxの外側にあります。

にもかかわらず、クラスIntaArrayの内部からvec[0]、vec[1]、~~~、vec[4]の式で生成した配列の各要素を先頭から順番でアクセス可能です。

 

添字演算子

 配列の各要素に外部から簡単にアクセスできるように定義しているのが""

どこかの地域では添字演算子とか呼ぶらしいですね。

その演算子関数operatorの返却値の型はなんとint&です。

なぜなら

 

a = x[5]

(代入演算子の右側はintでもint&でもOK)

 

代入の右辺のみで使用するならintでも大丈夫なのですが、下の様に左辺における様にするには、int&じゃないとできないからです。

 

x[2] = 5

(代入演算子の左側はintではNG、int&を使おう)

 

それでは実際にIntArrayを動かしてみましょう。

 

//整数型配列クラスを使う

#include <iostream>
#include "IntArray.h"

using namespace std;

int main()
{
    int n;

    cout << "Select the number of elements:";
    cin >> n;

    IntArray x(n);

    for (int i = 0; i < x.size(); i++)
    x[i] = i;

    for (int i = 0; i < x.size(); i++)
    cout << "x[" << i << "] =" << x[i] << "\n";
}

f:id:punainen:20210319161959p:plain

実行結果(赤枠はキーボードから入力)

n個分の配列を作り、すべての要素に添字と同じ値を代入ついでに表示しています。

 

ではここで下の関数についてちょっと考えてみましょう。

void func(){
    IntArray x(5);
    ~省略~
}

 IntArray型のオブジェクトxは関数内で生成されているので自動的にメモリ域の期間が与えられます。

それがいつまで有効なのかというと、

 

①二つのメンバ"nelem"と"vec"を持つオブジェクト"x"を生成

f:id:punainen:20210319164447j:plain

②コンストラクタでxを初期化

 (nelemが5で初期化、そのあと5個の配列用のメモリ域がnew演算子で確保、先頭要素へのポインタがvecに代入)

f:id:punainen:20210319164806j:plain
③関数funcの実行終了、自動的にメモリ域の期間を持っているオブジェクト"xこの期間が終了し、破棄される。

 だが、new演算子で確保された配列本体用のメモリ域は破棄されることなくメモリ域に残った状態になる。

f:id:punainen:20210319165641j:plain

 

コンストラクタで確保した動的な記憶期間をもつメモリ域は、オブジェクトを破棄したとしても、自動的に解放されることはありません。

 

解放されていない配列はどこからも指定されていない、無駄な領域としてメモリ上に取り残されます。

なのでこの関数を何度も読みだすと、その都度無駄な領域が確保されていき、実際に使えるメモリ域が減っていきます。

 

我々がこの期間をコントロールできるオブジェクトは好きな時に生成し、好きな時に破棄できるのですが、その作業をちゃんと行うのが我々の使命です。

 

なのでこのように確保したオブジェクトはちゃんと指示して解放しないといけないです。

 

そのために、配列本体の領域を破棄する【メンバ関数】を定義しましょう。

void IntArray::delete_vec(){
    delete vec;
}

 こんな感じです。

この関数を呼び出すように上の関数を書き換えます。

void func(){
    IntArray x(5);
    //~省略~
    x.delete_vec();
}

 これでXが破棄される前に配列本体を破棄できます。

 

しかしこの方法は微妙ですね。

delete_vec()を呼び出し忘れる。

・オブジェクトを使う前に間違えてdelete_vec()呼び出してしまう。

ということをやりかねないからです。

 

オブジェクトの外側の資源を管理する場合は、確保と破棄の管理を徹底しましょう。

 

という事でIntArrayを改良していきます。

 

NewIntArray.h

//整数型配列クラス

#ifndef ___Class_IntArray
#define ___Class_IntArray

class IntArray{
    int nelem;
    int* vec;

public:
    //コンストラクタ
    explicit IntArray(int size) : nelem(size){
        vec = new int[nelem];
    } 

    //デストラクタ
    -IntArray() {delete vec;}

    //要素の数を返却
    int size() const {return nelem;}

    //演算子
    int& operator(int i){return vec[i];}
};

#endif

 

コンストラクタを定義するときにexplicitを追加しました、これは宣言するコンストラクタを明示的コンストラクタにするための関数指定子です。

そも、明示的コンストラクタとは暗黙的に行う型変換を抑止するコンストラクタです。

具体例としては

IntAry a = 5;

 (最初のクラスなら大丈夫、このクラスではNG)

IntAry b(5);

 (どちらでも大丈夫)

コンストラクタにexplicitの指定がない最初のクラスではどちらでも可能です。

ですが今回のクラスでは①の記述をするとコンパイルエラーを吐きます。

①の宣言方法は”配列を整数で初期化している”と誤解を生むかもしれません、その紛らわしい初期化をexplicitは抑止してくれます。

 

引数が単一のコンストラクタが”=”形式で起動されるのを防ぐにはexplicitを与えて定義しよう!

 

デストラク

このクラスで新しく追加された

    -IntArray() {delete[] vec;}

この記述はデストラクタと呼ばれる特殊なメンバ関数です。

 

クラス名の前に”-”がついた名前のこの関数は

そのクラスのオブジェクトが破棄されそうになった時に自動的に呼び出されます

 

なので、コンストラクタと対照的な存在ですね。

 

オブジェクト生成時に呼び出されるメンバ関数がコンストラクタ、それとは逆でオブジェクトが破棄される時に呼び出されるメンバ関数がデストラクタ

 

デストラクタはコンストラクタと同じで返却値をもっていません、ただ自動的に呼び出される性質上、コンストラクタと違い引数は受け取りません。

 

このクラスのデストラクタはオブジェクト外部のメモリ域解放を行います(ポインタvecが示す配列の領域)

 

これなら上記のfunc関数を利用した時にデストラクタがvecが指す領域を解放してくれます。

f:id:punainen:20210319180604j:plain

f:id:punainen:20210319180713j:plain

デストラクタ君の仕事

 

こんな感じです。

 

まとめとしては

 

・コンストラクタで確保したメモリ域などのオブジェクトの外側の資源の解放処理はデストラクタで実行しよう!!

 

・オブジェクトが外部の資源を管理するようなクラスは、資源を獲得時に初期化を行おう

 

以上、メモリ領域とデストラクタの話でした。

 

ありがとうございます。

 

ぷないしん