はじめに
int型やchar型、double型などの基本データ型は、
それぞれの基本データ型と同じポインタ型を使って表現できます。
int i; // int型
int* pi; // int型ポインタ
char c; // char型
char* pc; // char型ポインタ
double d; // double型
double* pd; // double型ポインタ
同じように、独自データ型のクラス型も
それぞれの独自データ型と同じポインタ型を使って表現できます。
classA objA; // 独自データ型のclassA型
classA* pobjA; // 独自データ型のclassA型ポインタ
また、int型ポインタの値をchar型ポインタ変数に代入して使う。
と言うような事はできません。
int i; // int型
int* pi = &i; // int型ポインタ
char c; // char型
char* pc = &c; // char型ポインタ
pi = pc; // これはできない
しかし、基本クラスとその派生クラスでは、共通する部分があるため、
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
と言った代入が許されています。
これは、派生クラスポインタ値が代入されると、
基本クラスポインタ型として扱われるのですが、
派生クラスはその中に基本クラスを含んでいるので、
全ての基本クラスメンバにアクセスできるためです。
代入後は、基本クラスポインタとして働くので、
基本的には、派生クラスメンバへアクセスできません。
ただ、ポインタ値そのものは、派生クラスを指しているので、
「派生クラスポインタ型」へキャストすれば、
派生クラスメンバへアクセス可能になります。
ちなみに、その逆の
派生クラスポインタ型変数 = 基本クラスポインタ型変数(基本クラスのアドレス値)
と言った代入は許されていません。
これは、基本クラスポインタ値が代入されると、
派生クラスポインタ型として扱われるのですが、
ポインタ値そのものは、基本クラスを指しているので、
派生クラスメンバにアクセスできないからです。
以上の説明で
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
と言う代入ができる事が分かりました。
ここで1つ次のプログラムを見てみましょう。
基本、派生クラス間のアドレス値代入 プログラム例
class A {
public:
int data1;
void show_data1() { } // 関数内容は省略
};
class B : public A {
public:
int data2;
void show_data2() { } // 関数内容は省略
};
void main() {
A* objA = new A();
B* objB = new B();
objA = objB;
// objB = objA; // エラー
objA->data1 = 10; // OK
objA->show_data1(); // OK
// objA->data2 = 10; // エラー
// objA->show_data2(); // エラー
}
このプログラム例では、
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
の代入が行われています。
逆は無理です。
代入後は、基本クラス型として働くので、
基本的には、派生クラスメンバへアクセスできません。
と説明した通り、
派生クラスのdata2、show_data2にはアクセスできません。
オーバーロードしたメンバやオーバーライドしたメンバへのアクセス
プログラム例
class A {
public:
int data;
void show() { }
void func() { }
void func(int a) { } // 関数のオーバーロード
};
class B : public A {
public:
int data; // 変数のオーバーライド
void show() { } // 関数のオーバーライド
void func(int a, int b) { } // 関数のオーバーロード
};
void main() {
A* objA = new A();
B* objB = new B();
objA = objB;
objA->data = 10; // 基本クラスのdataへアクセス
objA->show(); // 基本クラスのshowへアクセス
objA->func(); // 基本クラスのfunc()へアクセス
objA->func(5); // 基本クラスのfunc(int a)へアクセス
// objA->func(5, 10); // エラー
}
このプログラムで気になるところは、
objA->show(); と言う文です。
この記述は、基本クラスのshow関数を呼ぶのか、
派生クラスのオーバーライドしたshow関数を呼ぶのか分かりますか?
分かるとすれば、どこで区別したらいいでしょう?
また、区別できてもどちらを呼ぶのが正しいでしょうか?
この部分は、あいまいさがあるので、
C++言語では、どちらを呼ぶかは、
プログラマの記述方法によって区別できるようになっています。
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
代入後は、基本クラス型として働くので、
基本的には、派生クラスメンバへアクセスできません。
と、説明しましたが、ある記述を加えれば、
オーバーライドした関数へアクセス可能になります。
静的結合と動的結合
C++言語では、
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
と言う代入後に、
オーバーライドした関数と基本クラスのオーバーライドされた関数の
どちらを呼ぶか決定する処理に名前が付けられていて、
それぞれ静的結合・動的結合と呼びます。
静的結合
まずは、静的結合から説明しましょう。
静的結合では、
呼び出し時に記述する変数の型により、どちらの関数を呼び出すか決定します。
例えば、上の説明で使った
objA->show(); の文の場合、objAの部分に注目します。
objAはA*型の変数なので、Aクラスのshowを呼びます。
たったそれだけの理由です。
class A {
public:
void show() { }
};
class B : public A {
public:
void show() { } // 関数のオーバーライド
};
void main() {
A* objA = new A();
B* objB = new B();
objA = objB;
objA->show(); // objAがクラスA*型の変数なので、
// 基本クラスのshowへアクセス
}
このプログラムでは、
クラスAとクラスBのshow関数がオーバーライドされています。
そして、
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
の代入が行われた後に
objA->show(); の記述があります。
静的結合により、
基本クラスのshow関数が呼び出されます。
静的結合は、スタティックバインディングとも呼びます。
動的結合
次に動的結合を説明しましょう。
動的結合は、呼び出し時に記述する変数の型ではなく、
その変数に入っているポインタの型により、
どちらの関数を呼び出すか決定します。
さらに、動的結合として処理させるには、
オーバーライドされる基本クラスの関数を仮想関数にする必要があります。
仮想関数にするには、関数の定義にvirtualキーワードを付けるだけです。
早速プログラム例を見てみましょう。
class A {
public:
virtual void show() { } // 仮想関数になっている
};
class B : public A {
public:
void show() { } // 関数のオーバーライド
// 仮想関数のオーバーライドは、
// virtualを付けなくても
// 自動的に仮想関数になる。
};
void main() {
A* objA = new A();
B* objB = new B();
objA = objB;
objA->show(); // 仮想関数になっているので、
// 動的結合として処理され、
// 派生クラスのshowへアクセス
}
このプログラムでは、
クラスAとクラスBのshow関数がオーバーライドされています。
さらに、基本クラスのオーバーライドされるshow関数が仮想関数になっています。
そして、
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
の代入が行われた後に
objA->show(); の記述があります。
基本クラスのオーバーライドされるshow関数が仮想関数になっているので、
動的結合として処理されます。
objAには、Bクラスのポインタ値が入っているので、
Bクラスのshow関数が呼び出されます。
ちなみに基本クラスのオーバーライドされる関数を仮想関数にすると、
派生クラスのオーバーライドした関数は、
virtual記述なしでも自動的に仮想関数となります。
仮想関数と言う言葉の意味とvirtualの意味は、
動的結合と結びつきが浅いような感じして分かりづらいですが、
深く考えないようにすると覚えやすいと思います。
仮想関数は、
基本クラスポインタ型変数 = 派生クラスポインタ型変数(派生クラスのアドレス値)
と言う代入後に、
オーバーライドした関数と基本クラスのオーバーライドされた関数の
どちらを呼ぶか決定する処理である動的結合のためだけに用意されたものです。
難しくとらえる必要はありません。
動的結合は、ダイナミックバインディングとも呼びます。
キャストを使って、派生クラスメンバにアクセスする例
class A {
public:
int data1;
void show_data1() { } // 関数内容は省略
};
class B : public A {
public:
int data2;
void show_data2() { } // 関数内容は省略
};
void main() {
A* objA = new A();
B* objB = new B();
objA = objB;
objA->data1 = 10; // OK
objA->show_data1(); // OK
// objA->data2 = 10; // エラー
// objA->show_data2(); // エラー
((B*)objA)->data2 = 10; // エラーにならない
((B*)objA)->show_data2(); // エラーにならない
}
この例では、
基本クラス型変数 = 派生クラス型変数(派生クラスのアドレス値)
の代入が行われた後、
キャストを使って派生クラスメンバにアクセスしている例です。
代入しても、objAには、依然としてクラスBのアドレス値が格納されているので、
キャストすれば、通常通り、全ての派生クラスメンバにアクセス可能となります。
関数の仮引数として基本クラス型の変数を指定する例
class A {
public:
void show() { } // 関数内容は省略
};
class B : public A {
public:
void show() { } // 関数内容は省略
};
void func(A* obj) {
obj->show();
}
void main() {
A* objA = new A();
B* objB = new B();
func(objA);
func(objB);
}
このプログラムは、
これまで説明してきた事を理解していれば、
何も疑問に思う事はないでしょう。
ただ、この例のように
基本クラス型変数を関数の仮引数にするメリットとしては、
基本クラス型のアドレス値も
その派生クラス型のアドレス値も受け取れる事です。
どちらも受け取れると言う事は、
その関数内で基本クラスと派生クラスに関する
同じような処理を行う事ができる。
等の利点があります。
どういう使い方をするかはプログラマ次第ですが、
プログラムの幅が広がる事は確かです。
また、この例では、ポインタ型を仮引数にしていますが、
仮引数を参照型にしても良いと思います。