Upload
sunaemon
View
145
Download
0
Embed Size (px)
Citation preview
C++言語講習会第2回資料
書いた人:@sunaemon0(回路屋2年)
1 継承
あるクラスにあるメンバ変数やメンバ関数を受け継いだクラスを作ることができます。
継承元のクラスを基底クラス、継承して新たに作られるクラスを派生クラスと言います。
list 1 inheritance0.cpp1 #include <iostream >23 class A {4 public:5 int value;6 virtual int f() = 0;7 };
8 class B : public A {9 public:
10 int f() { return 1; }11 };
1213 int main(int, char**) {
14 B *b = new b;15 A &a = b;
1617 a.value = 1;
18 std::cout << b.f_B() << std::endl;
19 std::cout << b.f_A() << std::endl;
20 }
前回はprivateとpublicしかやりませんでしたが、アクセス指定子は全部でpublic, protected, privateの三種類あります。基本的な意味は以下のとおりです。
• publicなメンバ変数/メンバ関数は、どこからでもアクセスできます。• protectedなメンバ変数/メンバ関数は、それが属すクラスとそれを継承するクラスのメンバ関数とフレンド関数からアクセスすることができます。• privateなメンバ変数/メンバ関数は、それが属すクラスとフレンド関数からアクセスすることができます。
実は基底クラスにもpublic, protected, privateの三種類があります。classの場合は特に明示しない場合は暗黙的に
private基底になりま fす。structならpublic基底になります。上で述べたのはpublic基底の場合の話です。private基底やprotected基底は特に普通の設計では使わないので省きます。
2 アップキャスト
list 2 upcast0.cpp1 class Base {};2 class Derived0 : private Base {};3 class Derived1 : protected Base {};4 class Derived2 : public Base {};56 int main(int, char **) {7 Derived0 d0;
8 Derived1 d1;
9 Derived2 d2;
1011 Base &a0 = d0; // error: ‘’Base is an inaccessible base of ‘’Derived012 Base &a1 = d1; // error: ‘’Base is an inaccessible base of ‘’Derived113 Base &a2 = d2; // OK
14 }
基底クラスのリファレンスには、派生クラスのインスタンスを代入することができます。これをアップキャストと言います。
ダイアモンド継承を、仮想基底を使わずに行ったりなどの場合を除けばアップキャストは無条件に成功します。
基底クラスへのリファレンスから派生クラスのリファレンスへのキャストをダウンキャストと言います
ダウンキャストは常に成功するとは限りません。dynamic castやstatic castを使う必要があります。
3 仮想関数
list 3 virtual0.cpp1 #include <iostream >23 class A {4 public:5 int f() { return 0; }6 };
7 class B : public A {8 public:9 int f() { return 1; }
10 };
1112 int main(int, char**) {13 B b;
14 A a;
15 A &a0 = b;
16 A &a1 = a;
17 std::cout << b.f() << std::endl; // 1
18 std::cout << a0.f() << std::endl; // 0
19 std::cout << a1.f() << std::endl; // 0
20 }
上のように定義した場合は、どのクラスのインスタンスかと
いうのには関係なしに、そのリファレンスの型によって呼び出される関数が決定されます。
list 4 virtual1.cpp1 #include <iostream >23 class A {4 public:5 virtual int f() { return 0; }6 };
7 class B : public A {8 public:9 int f() override { return 1; }
10 };
1112 int main(int, char**) {13 B b;
14 A a;
15 A &a0 = b;
16 A &a1 = a;
17 std::cout << b.f() << std::endl; // 1
18 std::cout << a0.f() << std::endl; // 1
19 std::cout << a1.f() << std::endl; // 0
20 }
一方、上のように定義した場合は、どのクラスのインスタンスであるのかによってどの関数が呼ばれるのか変わります。
virtual0のように基底クラスに非仮想関数A::fと、その派生クラスにB::fがあるときB::fはA::fを隠すと言います。
virtual1のように基底クラスに仮想関数A::fと、派生クラスにB::fがあり、両者の方が一致するとき、B::fはA::fをオーバーライドすると言います。
A::fは自動的に仮想関数になります。
この時、B::fにoverrideというキーワードを付けることでオーバーライドしなかった時にエラーが出るように出来ます。
4 仮想関数テーブル
gccなどにといてどのように仮想関数が実装されているのか簡単に説明します。
仮想関数のあるクラスのインスタンスには暗黙的に仮想関数テーブルへのポインタが含まれます。
仮想関数テーブルには、そのクラスのインスタンスのある仮想関数が呼ばれた時に実行されて欲しい関数へのポインタなどを登録してあります。
virtual関数への呼び出しは、インスタンスのvptrを参照した後、仮想関数テーブルから呼び出した仮想関数へのアドレスを取得それを呼び出すことによって行われます。
大体以下のようなことが行われています。
list 5 vtable0.cpp1 #include <iostream >23 int get_value_Base() {4 return 1;5 }
67 int get_value_Derived() {8 return 2;9 }
1011 struct vtbl {12 int (*get_value)();13 };
1415 vtbl Base_table = { get_value_Base };
16 vtbl Derived_table = { get_value_Derived };
1718 struct Base {19 vtbl *vptr;
20 Base() {
21 vptr = &Base_table;
22 }
23 };
2425 struct Derived : Base{26 Derived() {
27 vptr = &Derived_table;
28 }
29 };
3031 int main() {32 Derived d;
33 Base *d_in_b = &d;
34 Base b;
3536 std::cout << d.vptr->get_value() << std::endl; // 2
37 std::cout << d_in_b->vptr->get_value() << std::endl; // 2
38 std::cout << b.vptr->get_value() << std::endl; // 1
39 }
クラス型へのリファレンスの中に実際どのクラスのインスタンスが入っているのか知りたい時があります。これは実行時にしかわからないので実行時型情報 (RTTI)と呼ばれます。RTTIの情報も仮想関数テーブルに載っています。
5 純粋仮想関数
クラスの宣言で=0が付けられた関数は純粋仮想関数と呼ばれます。
純粋仮想関数を直接持つか、派生クラスで直接ないし間接的な基底クラスにある純粋仮想関数を全てオーバーロードしない関数は抽象クラスと言います。
抽象クラスでないクラスは具体クラスと言います。
抽象クラスはインスタンスを作ることができません。
6 finalある仮想関数をオーバーライドされたくない場合、finalを付けることでオーバーライドできなくなります。
7 コンストラクタとデストラクタ
コンストラクタは以下のことを順番に行います。
• 基底クラスのコンストラクタを呼び出す。(多重継承していれば左側から、仮想基底クラスについては別のところで呼ばれていればもう呼ばない)• vptrを自分のクラスのものに更新。• メンバ変数のコンストラクタを宣言順に呼ぶ。• コンストラクタ本体に書いてある処理を実行
デストラクタは逆順に行います。
• vptrを自分のクラスのものに更新。• デストラクタ本体に書いてある処理を実行。• メンバ変数を宣言順と逆順に解体。• 基底クラスのコンストラクタを呼び出す。(多重継承していれば右側から、仮想基底クラスについては別のところで呼ばれていればもう呼ばない)
あるインスタンスを解体する際、それが属するクラスのデストラクタを呼ぶ必要があります。そのため普通デストラクタは仮想関数にします。
8 ポリモーフィズム
大事なことなのでもう一度言います。
基底クラスのリファレンスには、派生クラスのインスタンスを代入することができます。この際型が変わるのでこれをアップキャストと言います。
つまりクラス型へのリファレンスは中に派生クラスのインスタンスが入っている可能性があわけです。
これをポリモーフィズムと言います。
テンプレートでも似たようなことができますが、テンプレートだと型はコンパイル時に決定されます。なのでテンプレートを使って同じようなことをするのを静的ポリもーフィズムと言います、
一方継承の場合、どのような関数が呼び出されるかは実行時に決定されます。これを動的ポリモーフィズムといいます。
もちろん両者は共存することができます。
9 多重継承と仮想基底
C++では基底クラスは必ずしも一つではありません。その結果として、
list 6 diamond0.cpp1 #include <iostream >23 class A {4 public:5 int f() { return 0; }6 };
7 class B : public A {8 public:9 int f() { return 1; }
10 };
11 class C : public A {12 public:13 int f() { return 2; }14 };
15 class D : public B, public C {1617 };
18 int main(int, char**) {19 D d;
20 A &a = d;
21 std::cout << d.f() << std::endl; // ?!
22 }
このような不思議な状況が発生しえます。継承グラフが木構造になっていないこのような状況をダイアモンド継承と言い
ます。
また、基底には、virtualな基底とvirtualでない基底があります。さっきまでやってきたのはvirtualでない基底ですが、virtualな基底にはオーバーロードの決定や、初期化において闇が潜んでいるのでここではやりません。
list 7 diamond1.cpp1 #include <iostream >23 class A {4 public:5 virtual int f() { return 0; }6 };
7 class B : virtual public A {8 public:9 int f() override { return 1; }
10 };
11 class C : virtual public A {12 public:13 int f() override { return 2; }14 };
15 class D : public B, public C {1617 };
18 int main(int, char**) {19 D d;
20 A &a = d;
21 std::cout << d.f() << std::endl; // ?!
22 }
10 例外
Cでのエラー通知は、例えば負の値を返すことなどによって実現されていました。
しかし、これだと以下のように資源管理をちゃんとしようとすると面倒です。また戻り値をチェックとい本筋の処理と関係ないコードがあって面倒です。また一つ一つの関数でエラー処理を行なっているため、エラーが起きていない時も大量の if文を通過する必要が出てしまっています。
list 8 exception0.cpp1 int func(char *a, char *b)2 {
3 if(!a || !b)4 throw exception();5 }
6
7 int dosth() {8 char *a, *b;9 if(!(a=malloc(10)))
10 return -1;1112 if(!(b=malloc(10))) {13 free(a);
14 return -2;15 }
1617 if(func(a,b) < 0) {18 free(a);
19 free(b);
20 return -3;21 }
2223 // etc....
2425 free(a);
26 free(b);
27 return 0;28 }
それを回避するためにC++では例外を使います。
list 9 exception1.cpp1 #include <iostream >2 #include <exception >3 #include <stdexcept >45 using namespace std;67 int main() {8 try {9 cerr << "a" << endl;
10 throw exception();11 cerr << "b" << endl; // never called
12 } catch(exception &e) {13 cerr << e.what() << endl;
14 }
15 }
throw式で例外を投げられます。基本的には、std::exceptionの派生クラスを投げるようにしてください。
投げられた例外は最も内側の try文の後ろにある例外ハンドラでマッチングされます。例外ハンドラというのはcatch文
のことです。
基本的には型が厳密に一致する例外ハンドラが呼び出されますが、派生クラスのリファレンスは基底クラスのリファレンスを受け取る例外ハンドラにマッチします。
catch文のなかで throw;と書くと今処理している例外を再度投げられます。これを rethrowと言います。
そのあと制御は最後のcatchの後に移ります。
11 スタック巻き戻し
例外は関数の中から外にも投げられます。
詳しく言うと、例外が投げられると、関数を飛び越えて直近と tryブロックまでジャンプします。
普通ローカル変数のインスタンスは関数から returnするときに解体されます。
例外処理の場合はreturnするわけではないですから別の仕組みで解体を行います。
具体的には tryブロックが呼ばれたところまでスタックを下ろして、そのインスタンスを解体するという事を行います。
この際デストラクタが呼ばれるのですが、デストラクタ自体
が例外を投げた場合スタック巻き戻しが失敗してしまってプログラムが強制終了してしまいます。なのでデストラクタからは例外を投げては行けません。
12 RAII関数内である資源を獲得したとします。
関数の最後にその資源を開放する処理を書いたとしても途中で例外が投げられた場合その処理は実行されない恐れがあります。
それを避けるためC++では、資源を管理するためのクラスを作りコンストラクタでその資源を獲得、デストラクタでその資源を開放するようにした上で、そのクラスのインスタンスをローカル変数として持つという事が普通行われます。
これこそ前回やったRAIIです。
13 演習問題
仮想関数テーブルの大きさを調べよ。