派生クラスで override することができるのは、基底クラスで virtual をつけたメンバー関数(=仮想関数)だけです。しかし、もし virtualをつけていない(つけるのを忘れた)メンバー関数(=非仮想関数)を派生クラスで override しようと試みると何が起こるでしょうか?
1. virtual修飾子とoverride指定を適切につけたケース
#include <iostream>
using namespace std;
class Base {
public:
virtual void say() { cout << "Hello" << endl; }
};
class Derived : public Base {
public:
void say() override { cout << "Bonjour" << endl; }
};
int main(void){
Derived derivedObj;
Base* baseObj = &derivedObj;
derivedObj.say(); // => bonjour
baseObj->say(); // => bonjour
return 0;
}
virtual修飾子とoverride指定を適切に記述して、作法通りに仮想関数とoverrideを定義しています。基底クラス Base のポインタ経由でアクセスしても適切に派生クラスのメンバー関数が呼び出されています。
2. virtual修飾子を忘れたケース
もし下記のように基底クラスのメンバー関数にvirtualを付け忘れるとコンパイルエラーが発生します。
#include <iostream>
using namespace std;
class Base {
public:
void say() { cout << "Hello" << endl; }
};
class Derived : public Base {
public:
void say() override { cout << "Bonjour" << endl; } // => compile error
};
int main(void){
Derived derivedObj;
Base* baseObj = &derivedObj;
derivedObj.say();
baseObj->say();
return 0;
}
3. virtual修飾子もoverride指定も忘れたケース
#include <iostream>
using namespace std;
class Base {
public:
void say() { cout << "Hello" << endl; }
};
class Derived : public Base {
public:
void say() { cout << "Bonjour" << endl; }
};
int main(void){
Derived derivedObj;
Base* baseObj = &derivedObj;
derivedObj.say(); // => bonjour
baseObj->say(); // => hello
return 0;
}
上記のように virtual と override の両方の記述を忘れるとコンパイラーはエラーを発しません。ところが実際に実行してみるとプログラマの意に反して(?)、Baseクラスにアップキャストしたポインタを経由してアクセスすると基底クラスのメンバー関数が呼び出されてしまいます。さて何が起こっている(出来上がっている)のでしょうか???
大切なことはスコット・メイヤーズ著『Effective C++ 第3版』に書かれていました。
33項 継承した名前を隠蔽しないようにしよう
ここで「名前の隠蔽」と「再定義」について説明します。
- ローカル変数によるグローバル変数の名前の隠蔽
#include <iostream>
using namespace std;
int x = 3;
int main(void){
int x = 7;
printf("x = %d\n", x); // x = 7
printf("x = %d\n", ::x); // x = 3
return 0;
}
メイン関数の中でローカル変数 x を再定義したことでグローバル変数 x が隠されています。
C言語ではグローバル変数にアクセスする手立てはありませんが、C++ではスコープ解決演算子 ::
を使うことで、main関数の中からグローバル変数にアクセスすることができます。
上記のケースではグローバル変数とローカル変数で x という名前の変数が各々一つずつ存在しています。
5. 基底クラスのメンバー変数を派生クラスで再定義
#include <iostream>
using namespace std;
class Base {
protected:
int x;
public:
Base() { x = 3; };
};
class Derived : Base {
protected:
double x;
public:
Derived() { x = 1.7; };
float get_derived_x() { return x; }; // => derived x = 1.7
int get_base_x() { return Base::x; }; // => base x = 3
};
int main(void){
Derived obj;
cout << "derived x = " << obj.get_derived_x() << endl;
cout << " base x = " << obj.get_base_x() << endl;
return 0;
}
上記のコードでは基底クラスで定義されたメンバー変数 x を派生クラスで再定義しています。基底クラスでは整数型(int)で定義した変数を派生クラスでは倍精度浮動小数点数(double)で敢えて再定義しています。再定義ですから基底クラスの型には関係なく、派生クラスでは異なる型で定義できます。
上記のコードでもスコープ解決演算子を使って派生クラスから基底クラスのメンバー変数にアクセスしています。基底クラスのメンバー変数 x と 派生クラスのメンバー変数 x は名前が同じだけで異なる実体を持つことがわかると思います。
最初の問いへの答えです。
「overrideしたいメンバー関数にvirtualを付け忘れるとどうなるか?」
継承したメンバー関数の名前を隠蔽して、あらたなメンバー関数を再定義します。overrideではなく再定義であるため、派生クラス(のポインタ)を介して呼び出すと派生クラスで再定義されたメンバー関数が呼び出され、アップキャストしたインスタンスのポインタを介して呼び出しを試みると、基底クラスのメンバー関数が呼び出されます。