第一章:基礎知識
8.クラス
クラスは構造体とほぼ同じような物で、変数や関数の集合体です。オブジェクト指向開発といえばほぼ間違いなく、このクラスが利用されます。
クラスと構造体の違いは、クラスでは、メンバーに対して細かいアクセス制限を掛けることが出来るようになっていることです。
クラスの例を示します。
class _base{ protected: int p_x; private: int m_x; public: int a; int ret_x(){ return m_x; } _base(){ m_x=999; a=5; } }; _base *b=new _base(); Caption=b->ret_x(); delete b;
ここで注目するところは、protected: private: public:という部分です。
クラスでは、このprivate:という表記の下に変数や関数を書くと、そのクラスの中だけでしか利用できない変数や関数を定義することが出来るようになります。
public:の下に書くと、その変数や関数は外部からもアクセス出来るようになります。
これにどういうメリットがあるかというと、外部から直接アクセスしてクラス内で利用する重要な変数に全く意味の異なる値が代入されたりすることが防げると言うことです。
クラス内で独自に計算して設定している値に外部からアクセス出来てしまうというのは言い換えると、自分の家の金庫の位置を他の人が直接移動させるのと同じような意味です。
自分で管理しているのに、勝手を知ってるからと、他人や友人に部屋のレイアウトを変えられると困りますね。そんな感じです。
public:では自由に使ってもいい変数や関数を置くことで、例えばprivate:に変数を置いてあるとして、public:にその変数の値を貸し出す関数を置いてやると、その関数からクラスの変数の値を借りてくることも出来るようになります。
逆にpublicの関数を実行することで、引数に指定した変数をprivateの変数に代入することも出来るようになります。
次にprotectedですが、ここに書かれた変数は、クラスを単独で使う場合はprivateとして機能するのですが、次で説明する継承によってその動作に違いが出てきます。
クラスには継承と呼ばれる親子関係のような機能があります。親になるクラスを基底クラス(スーパークラス)と呼び、子になるクラスを派生クラス(サブクラス)と呼びます。次の例を見てください。
class _abc : public _base { protected: private: public: int a_func(){ return p_x; } };
クラスの宣言部にclass _abcの後に : public _baseとあります。
これは、一番上の例で作成した_baseクラスを元に_abcというクラスを作成しています。
この_abcクラスでは関数しか作成していません。ここで注目する所は、_baseのprotectedで宣言したp_xにアクセス出来るということです。
_baseのprivateに宣言された変数にはアクセス出来ませんが、protectedとpublicに宣言された物にはアクセス出来ます。
しかし、_abcクラスを通常使用するときはpublicに宣言された物にしかアクセス出来ません。
クラス外 | クラス内 | 継承先 | |
protected | × | ○ | ○ |
private | × | ○ | × |
public | ○ | ○ | ○ |
このようになります。
構造体でもstruct _base : _abc{}という宣言で継承することが出来ますが、クラスのように変数や関数へのアクセス制限を掛けることは出来ません。
C++Builderで利用されるVCLコンポーネントはクラスで作成されています。エディタの下のタブにUnit1.hという部分があるのでクリックしてみてください。TForm1のクラスの宣言が書かれています。
C++BuilderではVCLを利用するための独自拡張がされているので、__published:という部分がありますが、ここは統合環境が自動的に記入する部分で、通常この下に書き込むことはありません。
TForm1のprivateやpublicに変数や関数を描き込むことで、TForm1のメンバーとして利用することが出来るようになります。
継承の宣言で:public _baseと書きました、このpublicという宣言をprivateと書き換えることも出来ます。
こうすると、継承元のpublicはprivate扱いになり、派生クラスを利用するとき外部から基底クラスのpublicにアクセスすることが出来なくなります。派生クラス内から基底クラスのpublicにはアクセス出来ます。
派生クラスを作成するときにprivateと宣言することで、変化するのは基底クラス内のpublicの扱いがprivateになり、基底クラスのprotectedは、有効範囲を派生させたそのクラスまでに制限されます。privateで派生されたクラスの、さらにそこから派生したクラスからは、親の親のprotectedを見ることは出来ません。
class _base{ protected: int p_x; private : int x; public : int ret_x(){ return x; } _base(){ x=2; } }; class _abc : private _base{ private : int z; public: int a; _abc(){ z=2; } int func(int p){ return p*ret_x(); } }; class _xyz:public _abc{ private: public: _xyz(){ this->a=100; } int xyzfunc(){return a}; }; _abc aa; _xyz xz;
この例では、_abc aa;としてaaを利用するようになっています。このaaからアクセス出来るのは、_abcの変数aと関数funcだけです。_abcクラスの中では、_baseの変数p_xと関数ret_x のみにアクセス出来ます。ret_xは_baseの変数xの値を取ってくるので、結果的に、aaは_baseのxの値を間接的に利用することが出来ます。
宣言をpublicにしてあると、基底クラスのprotectedの有効範囲は派生の派生の派生でもprivate変数として利用することが出来ます。
xzでは、_baseのp_xにアクセスすることは出来ませんし、その_xyzクラスの中からもp_xにアクセスすることは出来ません。
通常クラスは動的にメモリを確保します。その際、mallocは使わず、newを利用します。
これは、malloc free関数では、クラスのコンストラクタとデストラクタが実行されないからです。
void __fastcall TForm1::Button1Click(TObject *Sender) { class _base{ private: public: _base(){ Form1->Caption="コンストラクタ"; } ~_base(){ Sleep(5000); Form1->Caption="デストラクタ"; } }; _base *b=new _base(); delete b; }
このサンプルで_base *b=(_base*)malloc(sizeof(_base));free(b);とした場合と、_base *b=new _base();delete b;とした場合を確認してみてください。newした方は、5秒後にデストラクタと表示されますが、malloc freeの方はコンストラクタもデストラクタも表示されません。
C++BuilderのVCLは必ずnewで動的に確保しなければいけない仕様になっていて、TButton btn;と、しただけでは
利用できないようになっています。
クラスにメモリを割り当てたとき、クラスはコンストラクタを実行しますこれはクラスの初期化を実行する物ですが、クラス内の変数をその都度、指定の値で初期化してから利用したい場合もあるかと思います。
このとき利用できるのがコピーコンストラクタです。
コピーコンストラクタは次のように宣言し使用します。
class _base{ public: int x; _base(){ x=0; } _base(int &a){ x=a; } }; _base *a=new _base(55); Caption=a->x; delete a;
このようにコンストラクタの括弧の中に引数を付けて宣言します。同名の関数が宣言されていますが、これは関数のオーバーロードといい、関数名は同じでも、引数が違えば同名の関数でもOkというC++の機能です。
ここで、newでメモリを確保するときクラス名の後ろに括弧を付けて、その中にコンストラクタに割り当てた引数と同じ変数などを指定することでコピーコンストラクタの_base(int &a)が実行されメンバのxがその値で初期化されます。
仮想関数とは、基底クラスで宣言した関数を派生先でも必ず宣言させる為の記述のことで、virtualというキーワードを関数の宣言の前に記述します。
class _base{ public: int x; virtual void setup(){ x=0; } }; class _abc:public _base{ public: void setup(){ x=100; } }; class _xyz:public _base{ public: void setup(){ x=999; } };
このように、基底クラスで宣言されたsetup関数を派生先で、再度、同名、同引数の関数を宣言することで派生元の関数を上書きします。この関数を上書きすることをオーバーライドと呼びます。この手法のメリットは、_abcや_xyzを_baseクラスにキャストする事により、同一のポインタ変数からそれぞれの派生先で上書きされた関数が実行できるということです。
void baseSet(_base *b) { b->setup(); } void __fastcall TForm1::Button1Click(TObject *Sender) { _abc *a=new _abc; _xyz *z=new _xyz; baseSet((_base*)a); baseSet((_base*)z); Caption=z->x; delete a; delete z; }
このように外部の関数で基底クラスを引数に取ることで、派生クラスをキャストさせた状態で参照させることにより、基底クラスで宣言したsetup()を実行すると、派生先の関数で上書きされたsetup()が実行されるようになります。
void __fastcall TForm1::Button1Click(TObject *Sender) { TButton *btn=dynamic_cast<TButton*>Sender; ShowMessage(btn->Name+"が押されました"); }
Button1のOnClickイベントに上のサンプルのように書き、このほかにButton2を用意します。そしてButton2のイベントでOnClickの所のプルダウンメニューを開き、Button1Clickを選択します。
これを実行すると、クリックしたボタンの名前を表示するメッセージが表示されるようになります。
TButton *btn=dynamic_cast<TButton*>Sender;の命令で、Senderに格納されたアドレスをTButtonのポインタに変更し、btnに入ったアドレスがクリックされたボタンコンポーネントなので、その名前を表示できるようになります。
ここで注意することは、dynamic_castというキャストの方法で、このdynamic_castはSenderに格納されている値が必ずTButtonか、それより前の基底クラスでなければ例外を送出します。
仮に、Button1ClickをTPanelのOnClickなどに設定してPanelをクリックするとSenderに格納されるアドレスは、TButtonではないので例外が発生します。発生した例外を受けて対応する処理を書くには、次のように修正します。
TButton *btn; try{ btn=dynamic_cast<TButton*>(Sender); ShowMessage(btn->Name+"が押されました"); }catch(...){ ShowMessage("Button1Clickで例外が発生しました"); }
例外は意図的に発生させるエラーで通常のエラーとの違いは例外が発生するであろう場所にtry catch文を配置することで、例外を補足し、適切な処理を実行することでエラーを回避できることです。
btn=(TButton*)Sender;でも今回のプログラムでは実行しただけでは不具合はありませんが、TPanelなどを割り当ててもエラーが起こらず実行できてしまいます。これでは万が一TButtonにしかない機能を使った場合、回避できないエラーとなってしまい危険です。
今回(TButton*)でキャストする方法でエラーにならない理由は、NameプロパティがTButton、TPanelどちらもが継承しているTComponentから継承された物だからです。