さてさて、新人研修も終わってある程度お仕事モードになってきました。
とは言え、新人に行き成りお仕事出すほど切羽詰った状況でもない?ようですし
私も、何でも持ってきてくださいと言えない新人エンジニアな訳でして><
で、これからプログラム漬けの毎日が始まっていきますので、
復習がてら、C++の低レベルな部分の動作を見ていこうと思います。
基礎固めということで。
注意(間違ってたらごめんなさい。ぼろくそ言って下さい。修正します)
環境として、
VS.net2003のC++コンパイラ
を、使用しています。
他の、実装では違う動作もするかもしれませんが、ここでは気にしません。
GCCの方が良いかもしれませんが、サイトがサイトなので皆さんVSが多いでしょうし^^;
最終的には、どうして継承するクラスのデストラクタをvirtualに
した方が良いのか実装の方面から、やる予定...です。
一先ず、コンストラクタとデストラクタ、継承、キャスト、
仮想関数の実装方法(vptr,vtable)などを
分かりやすく纏められたらっと思ってます。
1、継承と委譲によるコンストラクタとデストラクタの動作順について
クラス継承の場合のコンストラクタとデストラクタの動作順序は
教科書にも書かれてるくらい基本的なことと思いますが、
メンバ変数としてクラスを持つ場合の順序はどうなるのでしょうか?
確認してみまっしょよい。
using namespace std;
class B1
{
public:
B1(){cout << "B1" << endl;};
~B1(){cout << "~B1" << endl;};
};
class B2
{
public:
B2(){cout << "B2" << endl;};
~B2(){cout << "~B2" << endl;};
};
class D:public B1, public B2
{
public:
D(){cout << "D" << endl;};
~D(){cout << "~D" << endl;};
};
class I
{
public:
I(){cout << "I" << endl;};
~I(){cout << "~I" << endl;};
B1 b1;
B2 b2;
};
int main()
{
{
I i;
}
cout << endl;
{
D d;
}
return 0;
}
//出力
B1
B2
I
~I
~B2
~B1
B1
B2
D
~D
~B2
~B1
継承の場合は、教科書どうりに
基本クラスコンストラクタ
派生クラスコンストラクタ
派生クラスデストラクタ
基本クラスデストラクタ
の、順に動作しています。
今回クラスの多重継承を用いましたが
多重継承させた基本クラスの順序は
class D:public B1, public B2
上記のコードより、先に継承させてるB1の方が優先?されてるようです。
イメージ的には、
D* pd = new D(new B2(new B1()));
delete B1( delete(B2 ( delete(D) ) ) );//修正(イメージ的にはこちらの表記ですね)
こんな感じですね。
委譲の場合、純粋にコードの上から順番にメモリ確保されているようですが、
これは、あくまでVC++の実装ですから、他の処理系ではどうなるか分かりません。
まあ、して思いましたけど、あんまり意味ありませんね。
これよりメンバ変数のメモリ確保はコードの上部から、行なわれることが分かりますが、
それを前提にしたコードを書くべきではないでしょうね。
(どうすれば、それを前提としたコードが書けるのか分かりませんが^^;)
2、クラスが確保するメモリ領域について
次は、クラスの確保するメモリ領域について調べてみまっしょい。
int main()
{
I *pi = new I();
cout << pi << endl;
cout << &(pi->b1) << endl;
cout << &(pi->b2) << endl;
delete pi;
D *pd = new D();
cout << pd << endl;
cout << static_cast<B1*>(pd) << endl;
cout << static_cast<B2*>(pd) << endl;
delete pd;
return 0;
}
//出力
00343D98
00343D98
00343D99
00343D98
00343D98
00343D99
継承の場合、B1のアドレスとDのアドレスが一致しています。
単一継承の場合、このことより派生クラスから基本クラスのアドレスへと
キャストする場合のコストがなくなります。
class B
{
private:
int b1;
int b2;
}
class D : public B
{
private:
int d1;
int d2;
}
int main()
{
D* pd = new D();
B* pb = static_cast<B>(pd);
}
address value
0x0000 *pb,*pd->B::b1
0x0004 B::b2
0x0008 D::d1
0x000C D::d2
上記のような構造です。
派生クラスのメンバ変数は、基本クラスのメンバ変数の後に追加される様に、
配置されています。派生クラスは基本クラスのメンバ変数にアクセスできますから、
namespace B{
int b1;
int b2;
}
namespace D{
using namespace B;
int d1;
int d2;
}
namespace B{
//Bの関数
}
namespace D{
//Dの関数
}
こんな感じのイメージですかね。
//注意、このあたり自分の勝手な解釈です。スコープの限定?が
どのように実装されてるかコンパイラの知識は
乏しいもので確実なことは分かりません。
これだと、明示的なクラスDからの基底クラスBメンバ変数b1への
pd->B::b1
と、上記のメモリ領域とおなじ感覚でアクセスできますね。
ただ、基本クラス関数のオーバーロードは表現しづらい気もしますが。
で、本題ですが、同時に単一継承は、メモリアドレス的に同一なので
型情報の変更で完結します。
この辺のシステムの単純さが、javaやC#で単一継承が用いられてる理由の一つでしょうか。
もちろん多重継承の問題による理由もあるでしょうけど。
派生クラスを基底クラスへとキャストを行なうと
基底クラス型のサイズは8なのでアクセスは基底クラスメンバに限定されます。
スコープもBに限定されるので、クラスDのメンバ変数、関数へのアクセスが出来なくなります。
委譲の場合は、先頭のメンバ変数がクラスのアドレスと一致しています。
これは、クラスがメンバ変数にoffsetを使ってアクセスしているからです。
C++の場合、クラスはCの構造体と同様に、メンバ変数のメモリ領域を順に
取得していきます。
struct S{
int i;
int j;
int k;
};
S *ps = new S();
ps.i //*(ps+0)
ps.j //*(ps+4)
ps.k //*(ps+8)
上記のように、psの先頭アドレス+オフセットでメンバ変数にアクセスします。
//もし、最後まで読んだ下さった方いましたら、ごめんなさい。
//書いてて自分でもわかりづらいと、思います。
//一寸後で、修正します。
<追記>
修正及び追加をしました。もう一度修正いるかな?