classが単独(継承関係なし)の時

class A{
    A();
    ~A();
};

パターン1

int main(){
    A samp;
}
  1. インスタンスが生成されるときコンストラクタA()が呼ばれる
  2. インスタンスが消滅するときデストラクタが~A()呼ばれる。
Fig. 1: コンストラクタとデストラクタ

パターン2

int main(){
    A* p =nullptr;
    p = new A;
    delete p;
}
  1. インスタンスが生成されるとき(newをした時)コンストラクタA()が呼ばれる
  2. インスタンスが消滅するとき(deleteした時)デストラクタが~A()呼ばれる。
Fig. 2: オブジェクトの動的確保のとき

逆に言うと、newしてメモリを確保すると、明示的にdeleteしなければデストラクタは呼ばれない
(そもそも確保したオブジェクトのメモリが消えてくれない)

Fig. 3: deleteを呼ばなかった時

classが継承関係を持っている時

class A{
    A();
    ~A();
    foo(){ cout<< "bar\n"; }
};
 
class B
    :public class A
{
    B();
    ~B();
}
 

継承関係のあるクラスでは、基本的に

  1. 親クラスのコンストラクタ
  2. 子クラスのコンストラクタ
  3. 子クラスのデストラクタ
  4. 親クラスのデストラクタ

の順に呼び出されます。

パターン1

Fig. 4: コンストラクタ、デストラクタの順番

 

パターン2

動的にメモリを確保したときも同様です。

Fig. 5: オブジェクトを動的に取得したとき

という事で、ちゃんとdeleteしなければデストラクタが呼ばれないのも一緒です。

Fig. 6: オブジェクトを動的して、delete忘れたとき

仮想関数を持ったclassを継承していて、ポインタ経由でアクセスするとき

派生クラスのインスタンスは、ベースクラスのポインタと関連付けて(ベースクラスのポインタに代入できる)利用することができる。
ただし、その時は派生クラス内のベースクラスのメンバにしかアクセスできない。

さらにその時に、派生クラスで、ベースクラスのメンバ関数をオーバーライドしていると、
(ベースクラスのポインタからアクセスしているにもかかわらず)派生クラスのメンバ関数が呼び出される(意味不明)。

さらに、さらにその時のコンストラクタとデストラクタの呼び出し順を確認してみる。

  • ベースクラスのポインタを用意して、そこに派生クラス内のベースクラスを関連付ける
  • ベースクラスと派生クラスに同名の関数があり、内容が異なっている。
  • 派生クラスは動的に生成され、処理の終わりにdeleteされる。
class A
{
public:
	A(){cout << "const A" << "\n";}	
 	~A(){cout << "dest A" << "\n";}	
	void foo(){ cout<< "bar\n";}
};
 
class B
:public A
{
public:
	B(){cout << "const B" << "\n";}	
     ~B(){cout << "dest B" << "\n";}	
	void foo() { cout<< "barbar\n";}
};
 
int main() {
	A* p1 = nullptr;
	p1 = new B;
	p1->foo();
	delete p1;
}
Fig. 7: ベースクラスのポインタから派生クラスにアクセス

現在ベースクラスと派生クラスのfoo()関数は隠蔽関係にある。

A::foo() は B::foo() によって隠蔽されている。
(BのインスタンスからはAの同名関数は隠されていて見えない)

B samp;
samp.foo();
//barbar が表示される

しかし、以下のソースコードの様にベースのポインタ経由でアクセスすると。。。

        A* p1 = nullptr;
	p1 = new B;
	p1->foo();
	delete p1;

イメージとしては下図のようになる。

A* p = new B;
                        Bのインスタンス
A* p                   +--------------+
+-----------------+--> |0x1111        |
|                 |    +--------------+
|    &A ::A()<----|----|0x1112 A::A() |   
|    &  ::~A()<---|----|0x1113  ::~A()|
|    &  ::foo <---|----|0x1114  ::foo |   
+-----------------+    +--------------+  
                       |0x1115 B:: B()|   
                       |0x1116  ::~B()|
                       |0x1117  ::foo |  
                       +--------------+ 

図のような関係になるので、pからはベース側のfoo()にしかアクセスできない。
したがって、

        A* p1 = nullptr;
	p1 = new B;
	p1->foo();
	delete p1;

の結果は、
(説明のための書き方なのでプログラムではこう書けないよ)

  1. new Bによって、B内のAのコンストラクタが呼ばれる
  2. new Bによって、B内のBのコンストラクタが呼ばれる
  3. A* pにBのアドレスが代入される
  4. p→( A::foo() )が呼ばれる
  5. delete pによって p→( A::~A() ) が呼ばれる

のようになる。

先ほど隠蔽関係だった A::foo() と B::foo() を今度は、AのfooをBのfooで上書きしB::fooの機能を実行するように書き換えてみます。
ベース側の関数を仮想関数宣言(virtual)して、派生側でオーバライドすればいいんだったね。(override)

class A
{
public:
	A(){cout << "const A" << "\n";}	
 	~A(){cout << "dest A" << "\n";}	
	virtual void foo(){ cout<< "bar\n";}
};
 
class B
:public A
{
public:
	B(){cout << "const B" << "\n";}	
 ~B(){cout << "dest B" << "\n";}	
	void foo() override { cout<< "barbar\n";}
};
 
int main() {
		A* p1 = nullptr;
		p1 = new B;
		p1->foo();
		delete p1;
}

仮想関数とオーバーライドの効果により、ポインタのつながりの図がちょっと複雑になります。
しかしながら、A* pからは、ベースクラスであるAのメンバにしかアクセスできていないのは実は変わりません

A* p = new B;
                        Bのインスタンス
A* p                   +---------------------+
+-----------------+--> |0x1111               |
|                 |    +---------------------+
|    &A ::A()<----|----|0x1112 A::A()        |       仮想関数テーブル
|    &  ::~A()<---|----|0x1113  ::~A()       |       +---+-------------------+--------+
|    &  ::foo <---|----|0x1114 virtual foo() +---+   | x | A::foo()          |0x2111  |
+-----------------+    +---------------------+   |   +---+-------------------+--------+
                       |0x1115 B:: B()       |   +---+ O | B::foo() override |0x21112 |
                       |0x1116  ::~B()       |       +---+-------------------+--------+
                       +---------------------+ 

今度は、B内のAにある仮想関数が、B::foo()のアドレスに接続されているのでA::foo()にアクセスすると
強制的に上書きされた方の関数(=B::foo())にアクセスするようになります。
したがって結果は “barbar”と表示されます。

Fig. 8: 仮想関数とオーバーライド

仮想関数の仕組み的には、仮想関数をオーバライドして関数がすげ変わったんでオッケー!と思ったかもしれないですが、よく見ると
Bのコンストラクタが呼ばれていません。。。そりゃそうですね、上のポインタのつながりの図を見ると、Aにしかつながってないもん。
という事で、Bのデストラクタ、Aのデストラクタの順に呼ばれるようにするにはどうしたらいいか考えます。
そうですね、Aのデストラクタを仮想関数にすればよいです。(デストラクタの名前違うからオーバライドではないんだよね)

class A
{
public:
	A(){cout << "const A" << "\n";}	
 	virtual ~A(){cout << "dest A" << "\n";}	
	virtual void foo(){ cout<< "bar\n";}
};
 
class B
:public A
{
public:
	B(){cout << "const B" << "\n";}	
        ~B(){cout << "dest B" << "\n";}	
	void foo() override { cout<< "barbar\n";}
};
 
int main() {
		A* p1 = nullptr;
		p1 = new B;
		p1->foo();
		delete p1;
}

結果は以下のようになり、ちゃんとBのデストラクタ ⇒ Aのデストラクタ の順に呼ばれるようになります。

Fig. 9: ベースクラスのポインタから派生クラスにアクセス完全版

ポインタはアドレスを入れるための、ハコです。
アドレスの値はシステムで固定の長さの整数ですが、ポインタには型があります。
ポインタの型はその先に何が格納されるかを書きます。
この辺は、むかーし昔にやったので、省きます。
さて、ポインタの先のメモリにクラスがあるとどうなるでしょうか?
クラスにはメンバ変数と、メンバ関数などのメンバがあるので、以下のような図で考えてみます。

まず、ベースクラスのポインタ変数にベースクラスのインスタンスのアドレスを代入
A* p = nullptr;
+-----------------+--> nullptr ←インスタンスの先頭アドレス用差込口
|                 |
|    &A ::A()<----| ←A()接続用差込口    
|    &  ::~A()<---| ←~A()接続用差込口
|    &  ::foo <---| ←foo()接続用差込口   
+-----------------+      

これにインスタンスを接続(メモリを確保して、アドレスを代入)してみます。
まずインスタンスを生成します。newにより実体が確保されその先頭アドレス(0x1111)が返されます。

A* p = nullptr;
p = new A;

    アドレス0x1111にAのインスタンスが生成される。
    +--------------+
    |0x1111        |
    +--------------+
    |0x1112 A::A() |   
    |0x1113  ::~A()|
    |0x1114  ::foo |   
    +--------------+   

図のように、インスタンスの先頭アドレスの他に、その中にパックされているメンバもそれぞれアドレスを持っています。
(アドレスの値などは説明のためのでたらめな値です)
次に p = new A; の文でA* 型のポインタ変数pに、newで取得したインスタンスのアドレス(=0x1111)を代入します。

A* p                   +--------------+
+-----------------+--> |0x1111        |
|                 |    +--------------+
|    &A ::A()<----|----|0x1112 A::A() |   
|    &  ::~A()<---|----|0x1113  ::~A()|
|    &  ::foo <---|----|0x1114  ::foo |   
+-----------------+    +--------------+   

その結果、

  • p 0x1111
  • p→A() 0x1112
  • p→~A() 0x1113
  • p→foo() 0x1114

のように、ポインタ変数pからインスタンスの各メンバにアクセスできるようになります。

次に、ベースクラスのポインタ変数に派生クラスのインスタンスのアドレスを代入

C++では、派生クラスのインスタンスは必ず内部にベースクラスを含んでいます。

Fig. 10: クラスの派生

このときに、先ほどと同じようにclass Bのインスタンスを動的に作った時を考えてみます。
class Bで追加されたメンバはコンストラクタとデストラクタのみなので、以下のようになります。

B* p = nullptr;
p = new B;

    アドレス0x1111にBのインスタンスが生成される。
    +--------------+
    |0x1111        |
    +--------------+
    |class A       |
    |0x1112 A:: A()|   
    |0x1113  ::~A()|
    |0x1114  ::foo |   
    +--------------+
    |0x1115 B:: B()|   
    |0x1116  ::~B()|
    +--------------+ 

このときに、生成されたclass Bのインスタンスは丸々class Aを含んでいます。
(逆に言うとB内には必ずAが存在する)
ので、class AのポインタにBのA部分を接続することができます。

A* p = new B;

A* p                   +--------------+
+-----------------+--> |0x1111        |
|                 |    +--------------+
|    &A ::A()<----|----|0x1112 A::A() |   
|    &  ::~A()<---|----|0x1113  ::~A()|
|    &  ::foo <---|----|0x1114  ::foo |   
+-----------------+    +--------------+  
                       |0x1115 B:: B()|   
                       |0x1116  ::~B()|
                       +--------------+ 

これが、派生クラスにベースクラスのポインタからアクセスする仕組みです。
ただし、図を見ると当たり前そうですが、接続されているのはB内のAのメンバのみです。
なので、pからはBの持つAのメンバにしかアクセスできません。

  • p 0x1111
  • p→A() 0x1112
  • p→~A() 0x1113
  • p→foo() 0x1114
  • p→B() アクセス不可
  • p→~B() アクセス不可
  • game-engineer/classes/2021/game-programing-1/second-term/02/02-28-12-0.txt
  • 最終更新: 4年前
  • by root