合成(包含)(Composition, Inheritance)
一つ以上のクラスを別のクラスの定義の中で用いることである |
解答
#include <iostream>
#include <>string> using namespace std; class Person { public: Person(char* n="", char* nat="日本", int s=1) : name(n), nationality(nat), sex(s) { } void printName() {cout << name;} void printNationality() {cout << nationality;} private: string name, nationality; int sex; }; int main() { Person creator("Bjarne Strousstrup", "Denmark"); cout << "The creator of C++ was "; creator.printName(); cout << ", who was born in "; creator.printNationality(); cout << ".\n"; } |
実行結果
この例題では,Personクラスの中でstringクラスを合成(包含)している.
次の例題では,別のクラスを定義し,Personクラスと合成し,Personクラスを改良する.
解答
#include <iostream>
using namespace std; class Date { friend istream& operator>>(istream&, Date&); friend ostream& operator<<(ostream&, const Date&); public: Date(int m = 0, int d = 0, int y = 0) : month(m), day(d), year(y) { } void setDate(int m, int d, int y) {month = m; day = d; year = y;} private: int month, day, year; }; istream& operator>>(istream& in, Date& x) { in >> x.month >> x.day >> x.year; return in; } ostream& operator<<(ostream& out, const Date& x) { static char* monthName[13] = {" ", "January", "February","March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; out << monthName[x.month] << ' ' << x.day << ", " << x.year; return out; } int main() { Date peace(11,11,1918); cout << "第1次世界大戦は " << peace << "に終結.\n "; peace.setDate(8,15,1945); cout << "第2次世界大戦は " << peace << "に終結.\n "; cout << "誕生日のmonth, day, および yearを入力: "; Date date; cin >> date; cout << "あなたの誕生日は" << date << "です" << ".\n"; } |
実行結果 Date dateでDate型のオブジェクトdateを作り,cin dateでdate変数にキーボードから誕生日を入力すると,cout dateで誕生日を出力できる.
ここでは,誕生日や死亡日をキーボードからの入力ではなく,このDateクラスをPersonクラスの中で用いることにより,Personクラスのメンバの誕生日と死亡日を表示する方法を学ぶ.
解答 上のDateクラスをDate.hとして保存する.Dateクラスのメンバ関数
setDate(int m, int d, int year)
は日付の設定ができる.そこで,ate dobでDateクラスのオブジェクトdobを作成し,Personクラスの中で,メンバ関数setDOB()を
void setDOB(int m, intd, int y){dob.setDate(m,d,y);}
と定義することにより,Personクラスのオブジェクトauthorの誕生日をauthor.setDOB(15,3,1953);と設定できる.
#include <iostream>
#include "Date.h" #include <string> using namespace std; class Person { public: Person(char* n="", int s=0, char* nat="日本") : name(n), nationality(nat), sex(s) { } void setDOB(int m, int d, int y){dob.setDate(m,d,y);} void setDOD(int m, int d, int y){dod.setDate(m,d,y);} void printName() {cout << name;} void printNationality() {cout << nationality;} void printDOB() {cout << dob;} void printDOD() {cout << dod;} private: string name, nationality; Date dob, dod; // date of birth, date of death int sex; // 0 = female, 1 = male }; int main() { Person author("Thomas Jefferson", "1"); author.setDOB(4,13,1743); author.setDOD(7,4,1826); cout << "アメリカ合衆国の独立宣言の草稿者は"; author.printName(); cout << "で, \n誕生日は "; author.printDOB(); cout << "で,\n死亡日は"; author.printDOD(); cout << "です.\n"; } |
実行結果
ここで,Date.hは以下のファイルである.
#include <iostream>
#include <sstream>
using namespace std;
|
この例題では,Dateクラスのメンバ関数setDate()を用いてPersonクラスのメンバ関数setDOB(), setDOD()を定義している.
has-a |
継承(Inheritance)
is-a |
クラスXからクラスYを派生する記述法は,
class Y : public X{// };
|
解答 まず,上のPersonクラスをPerson.hとして保存する.ここで,基本クラスはPersonクラス,派生クラスはStudentクラスである.学生は人なので,Personクラスを用いてStudentクラスを派生するのは自然である. ここでは,学生番号,入学日,取得単位,成績評価をメンバ変数とするStudentクラスを作成する.
#include <iostream>
#include <string> #include "Person.h" using namespace std; class Student : public Person { public: Student(char* n, int s=0, char* i="") : Person(n,s), id(i), credits(0) { } void setDOM(int m, int d, int y){dom.setDate(m,d,y);} void printDOM() {cout << dom;} private: string id; // 学生番号(id) Date dom; // 入学日(date of matriculation) int credits; // 取得単位(credits) float gpa; // 成績評価(grade point average) }; int main() { Student x("吉田兼好",1,"600813902"); x.setDOB(1,1,1970); x.setDOM(4,3,1993); x.printName(); cout << "\n\t 誕生日: "; x.printDOB(); cout << "\n\t 大学院入学日: "; x.printDOM(); cout << endl; } |
実行結果
ここで,Person.hは以下のファイルである.
#include "Date.h"
class Person { public: Person(char$ast$ n="", int s=0, char* nat="日本") : name(n), nationality(nat), sex(s) { } void setDOB(int m, int d, int y) {dob.setDate(m,d,y);} void setDOD(int m, int d, int y) {dod.setDate(m,d,y);} void printName() {cout {\small $<<$} name;} void printNationality() {cout {\small $<<$} nationality;} void printDOB() {cout {\small $<<$} dob;} void printDOD() {cout {\small $<<$} dod;} private: string name, nationality; Date dob, dod; int sex; }; |
protectedクラスメンバ(Protected Class Members)
上の例題で,Studentクラスはコンストラクタを含むPersonクラスの全てのメンバ関数を継承する.また,コンストラクタ Person( )はPersonクラスのメンバ変数nameを初期化する.しかし,メンバ変数nameはPersonクラスのprivate変数である.よって,直接アクセスすることはできない.同様に,nationality, DOB, DOD, sexに直接アクセスすることはできない.メンバ変数sex以外への直接アクセスのなさは,さほど問題ではない.なぜならば,これらの変数へはPersonクラスのコンストラクタとpublicメンバ関数を用いてアクセスが可能だからである.しかし,Student's sex(学生の性別)に関しては,間接的にもアクセスする方法もない. このような問題を回避する方法として,sexをStudentクラスのデータメンバとしてしまう方法があるが,これは少し強引である.なぜならば,sexはPersonクラスに属しているのであって,Studentクラスだけが持っているわけではない.では,もっとよい方法はというと,privateアクセスをprotectedに変えるのである.これにより,派生したクラスから基本クラスのメンバ変数へのアクセスが可能になる.
解答
#include <iostream>
#include <string> #include "Date.h" using namespace std; class Person { public: Person(char* n="", int s=0, char* nat="日本") : name(n), nationality(nat), sex(s) { } void setDOB(int m, int d, int y){dob.setDate(m,d,y);} void setDOD(int m, int d, int y){dod.setDate(m,d,y);} void printName() {cout << name;} void printNationality() {cout << nationality;} void printDOB() {cout << dob;} void printDOD() {cout << dod;} protected: string name, nationality; Date dob, dod; // date of birth, date of death int sex; // 0 = female, 1 = male }; class Student : public Person { public: Student(char* n, int s=0, char* i="") : Person(n,s), id(i), credits(0) { } void setDOM(int m, int d, int y){dom.setDate(m,d,y);} void printDOM() {cout << dom;} void printSex() {cout << (sex ? "男" : "女"); } protected: string id; // 学生番号(id) Date dom; // 入学日(date of matriculation) int credits; // 取得単位(credits) float gpa; // 成績評価(grade point average) }; int main() { Student x("吉田兼好",1,"600813902"); x.setDOB(1,1,1970); x.setDOM(4,3,1993); x.printName(); cout << "\n\t 誕生日: "; x.printDOB(); cout << "\n\t 性別: "; x.printSex(); cout << "\t 大学院入学日: "; x.printDOM(); cout << endl; } |
実行結果
メンバへのアクセス
privateメンバはクラスの中からとfriendクラスからだけアクセス可能である.
protectedメンバはクラス中からとfriendクラスそしてその派生クラスおよび派生クラスのfriendクラスからもアクセス可能である.
publicメンバはファイル内のどこからでもアクセス可能である.
一般に,protectedはそのクラスの派生クラスを定義する可能性があるときにprivateの代わりに用いる. 派生クラスはその基本クラスのpublicおよびprotectedの全てのメンバを継承する.つまり,派生クラスから見ると,基本クラスのpublicおよびprotectedのメンバは,あたかも派生クラスで宣言されたようになる.例えば,クラスXと派生クラスYが
class X
{ public: int a; protected: int b; private: int c; }; class Y : public X { public: int d; }; で定義され,オブジェクトxとyが X x; Y y; |
クラスXのpublicメンバaはYのpublicメンバとして継承され,クラスXのprotectedメンバbはYのprotectedメンバとして継承されるが,クラスXのprivateメンバcはクラスYに継承されない.
仮想関数(virtual function)
継承されたメンバのオーバーライド(Overriding Inherited Members) Xが基本クラスで,YがXの派生クラス(派生クラス)のとき,YのオブジェクトはXの全てのpublicまたはprotectedメンバ変数およびメンバ関数を継承する.例えば,Personクラスのメンバ変数nameやメンバ関数printName()である.
ところが,ときには,継承されたメンバを派生クラスに合わせた形に定義したいときがある.例えば,aがXのメンバ変数で,YがXの派生クラスのとき,aという名前のメンバ変数をYに定義することができる.このとき, Yで定義されたメンバ変数aはXのメンバ変数aに対し 優位(dominate)であるという. ここで,クラスYのオブジェクトyによるaの参照y.aはXで定義されたaではなく,Yで定義されたメンバ変数aをアクセスする.Xのメンバ変数aをアクセスするときにはy.X :: aを用いる.
メンバ関数に対しても同じことがいえる.もし,f( )がXで定義され,f( )がYでも定義されたならば,y.f( )はYで定義されたf( )を呼び出し,y.X :: f( )はXで定義されたf( )を呼び出すことになる.このとき, ローカル関数y.f( )はf( )をオーバーライド(override)するという. この違いを見るために次の例題を考える.
#include <iostream>
using namespace std; class X{ public: void f() {cout << "X :: f() を実行 \n";} int a; }; class Y : public X { public: void f() {cout << "Y::f() を実行\n";} // Y::f()を優先 int a; // X::aの優位に立つ }; int main() { X x; x.a = 22; cout << "x.a = " << x.a << endl; Y y; y.a = 44; // Yのメンバ変数aに44を代入 y.X::a = 66; // Xのメンバ変数aに66を代入 y.f(); // Yのメンバ関数を呼び出す y.X::f(); // Xのメンバ関数を呼び出す cout << "Y.a = " << y.a << endl; cout << "y.X::a = " << y.X::a << endl; X z = y; cout << "z.a = " << z.a << endl; } |
解答 ここで,yは2つの異なるメンバ変数aと2つの異なるデータ関数f( )にアクセス可能である.ディフォルトは派生クラスで定義されたメンバである.スコープ解決子::はX::の形で用いられディフォルトを優先する.また,Xのオブジェクトzがyで初期化されたとき,Xのメンバが用いられた.つまり,z.aにはy.X::aが代入されたことになる.
実行結果
3つのオブジェクトx,y,zを図で表すと以下のようである.
この例で用いたメンバ変数の優位は普段のプログラミングでは用いない.一般に行う方法として,同じ名前を再利用するときには,その型を変えてやる.つまり,
class Y : public X
{ public: double a; }; |
継承の階層において,ディフォルトのコンストラクタとディストラクタは他のメンバ関数とは違った行動をとる.
#include <iostream>
using namespace std; class Person { public: Person(const char* s) {name = new char[strlen(s) + 1]; strcpy(name, s);} ~Person() {delete [] name;} protected: char* name; }; class Student : public Person { public: Student(const char* s, const char* m) : Person(s) { major = new char[strlen(m) + 1]; strcpy(major, m);} ~Student() {delete [] major; } private: char* major; }; int main() { Person x("Jerry"); { Student y("Sash", "Mathematics"); } } |
解答 オブジェクトxがインスタンス化されたとき,Personコンストラクタ(生成)をよび,"Jerry"を格納するため6バイトのメモリ領域を確保する.つぎに,Personコンストラクタがよばれ,yをインスタンス化するために,"sash"を格納するため5バイトのメモリ領域が確保され,その後,12バイトのメモリ領域が確保される.yのスコープはxのスコープが終了する前に終わる.yのスコープが終了すると,yのデストラクタ(消滅)は"Mathematics"を格納するために確保して12バイトのメモリを開放する.つぎに,Personデストラクタを呼び出し,"sash"を格納するために確保した5バイトのメモリを開放する.最後に,"Jerry"を格納するために確保した6バイトのメモリを開放する.
派生クラスから基本クラスのメンバへのアクセス
すでに説明したように,privateとprotectedの違いは,派生クラスからprotectedメンバはアクセスできるがprivateメンバはできないところである.では,いつメンバをprivateにしたらよいのだろうか.その答えは,principle of information hiding(情報隠蔽原則)で説明される.つまり,最初はアクセスを制限し,後で変更する.もし,将来,メンバ変数の変更を考えているなら,privateで宣言しておくことにより,派生クラスでの変更を必要のないものにする.派生クラスはprivateデータメンバと独立である.
Personクラスのオブジェクトであるpeopleが大卒かどうか知る必要があるとする.このとき,メンバ変数をprotectedで追加することができるが,後で,もっと詳しい学歴の情報を付け加えるかもしれない.よって,この段階ではprivateメンバ変数 collegeを追加し,派生クラスからのアクセスを制限することができる.
解答
class Person
{ public: Person(char* n ="", int s=0, char* nat="日本") : name(n), sex(s), nationality(nat) { } protected: string name, nationality; Date dob, dod; int sex; void setCgraduate(int g){college = g;} int isCgraduate() {return college;} private: int college; }; |
ここで,protectedのアクセス関数を加えておいた.もし将来collegeメンバ変数を他のもので置き換えたとしても,この2つのアクセス関数を変更するだけでよく,派生クラスには何の影響も及ぼさない.
仮想関数と多相(virtual Functions and Polymorphism)
C++の機能のなかで凄いものの1つに,各オブジェクトが同じメッセージを受け取っても,異なる対応が取れることが挙げられる.
この機能を多相(polymorphism)または多様性という.多相は仮想関数(virtual function)を用いることにより可能になる.多相は基本クラスインスタンスへのポインタは,どの派生クラスのインスタンスへのポインタでもあるという事実により実現される.
基本クラスへのポインタの利用
次のプログラムでは,pは基本クラスXのオブジェクトへのポインタとして宣言されている.まず最初に,pをクラスXのインスタンスxへのポインタとし,次にpをクラスYのインスタンスyのポインタとする.
#include <iostream>
using namespace std; class X { public: void f() {cout <<< "X :: f() を実行 \n";} }; class Y : public X { public: void f() {cout << "Y::f() を実行\n";} // X::f()を上書き }; int main() { X x; Y y; X* p = &x; p-> f(); p = &y; p-> f(); } |
解答 2つの関数呼び出しp-f( )が行われる.両方とも基本クラスXで定義された同じ関数f( )を呼び出す.なぜなら,pはクラスXのオブジェクトへのポインタとして宣言されたからである.よって,pがyへのポインタであることは,2つ目の呼び出しp-f( )に何の影響も及ぼさない.
実行結果
仮想関数(Virtual Functions)
基本クラスのポインタpを用いてf()関数を呼び出すと,基本クラスのf()が呼び出されることが分かった.しかし,pがyのポインタの場合,派生クラスのf()関数が呼び出されるほうが直感的である.これを実現するが仮想関数である. 仮想関数とは,基本クラス内で宣言され,派生クラス内で再定義されるメンバ関数のことである.
X::f( )を仮想関数に変換するには,識別子"virtual"をその宣言につければよい.つまり,
class X
{ public: virtual void f( ) {cout << "X::f( ) を実行する\n";} }; |
#include <iostream>
using namespace std; class X { public: virtual void f() {cout << "X :: f() を実行 <\n";} // 仮想関数の定義 }; class Y : public X { public: void f() {cout << "Y::f() を実行\n";} // 仮想関数の再定義 }; int main() { X x; Y y; X* p = &x; p-> f(); p = &y; p-> f(); } |
解答 今度は,2つめのp-f( )により,Y::f( )が呼び出された. これは多相がどんなものか表している.つまり,同じ呼び出しp-f( )により異なる関数が呼び出された.また,pがどのオブジェクトのクラスを指し示しているかによって関数が選ばれた.このことを動的結合(dynamic binding)という.
実行結果
#include <iostream>
using namespace std; class Person { public: Person(char* s) {name = new char[strlen(s) + 1]; strcpy(name, s);} void print() {cout << "私の名前は" << name << "です.\n";} protected: char* name; }; class Student : public Person { public: Student(char* s, float m) : Person(s),gpa(m) { } void print() { cout << "私の名前は" << name << "で,成績は" << gpa << "です.\n";} private: float gpa; }; class Professor : public Person { public: Professor(char* s, int n) : Person(s), publis(n) { } void print() { cout << "私の名前は" << name << "で,論文の数は" << publis << "です.\n";} private: int publis; }; int main() { Person* p; Person x("藤原鎌足"); p = &x; p-> print(); Student y("大伴家持",3.31); p = &y; p-> print(); Professor z("菅原道真",50); p = &z; p-> print(); } |
解答 このプログラムの基本クラスのprint( )関数は仮想関数ではない.よって,p-print( )によって呼び出されるのは常に,基本クラスのprint( )関数である.なぜならば,pはPerson*型を持っている.このポインタpはコンパイル時に基本クラスに静的結合(statically bound)する.
実行結果
#include <iostream>
using namespace std; class Person { public: Person(char* s) {name = new char[strlen(s) + 1]; strcpy(name, s);} virtual void print() {cout << "私の名前は" << name << "です.\n";} protected: char* name; }; class Student : public Person { public: Student(char* s, float m) : Person(s),gpa(m) { } void print() { cout << "私の名前は" << name << "で,成績は" << gpa << "です.\n";} private: float gpa; }; class Professor : public Person { public: Professor(char* s, int n) : Person(s), publis(n) { } void print() { cout << "私の名前は" << name << "で,論文の数は" << publis << "です.\n";} private: int publis; }; int main() { Person* p; Person x("藤原鎌足"); p = &x; p-> print(); Student y("大伴家持",3.31); p = &y; p-> print(); Professor z("菅原道真",50); p = &z; p-> print(); } |
解答 基本クラスのprint()関数は,仮想関数である.したがって,xオブジェクトからprint()関数を呼び出すと,基本クラスのprint()関数が呼び出され,yオブジェクトからprint()関数を呼び出すと,派生クラスStudentのprint()関数が呼び出される.同様に,zオブジェクトからprint()関数を呼び出すと,派生クラスProfessorのprint()関数が呼び出される.
実行結果
仮想デストラクタ(Virtual Destructors)
仮想関数は派生クラスで定義された同名の関数によってオーバーロードされる.しかし,コンストラクタもデストラクタも派生クラスの名前を用いているので,仮想として宣言することはできないように思える.このことは,コンストラクタについては正しい.しかし,デストラクタについては例外がある.
明示的に定義さているかいないかの違いはあるが,全てのクラスは固有のデストラクタを持っている.明示的なデストラクタは仮想として宣言することができる.つぎの例題で見てみよう.
#include <iostream>
using namespace std; class X{ public: X() {p = new int[2];cout << "X() . ";} ~X() {delete [] p; cout << ~X() .\n";} private: int* p; }; class Y : public X { public: Y() {q = new int[1023]; cout << "Y() : Y::q = " << q << ". ";} ~Y() {delete [] q; cout << " Y() . ";} private: int* q; }; int main() { for (int i=0; i < 8; i++){ X* r = new Y; delete r; } } |
解答 forループにおける1回の反復ごとに,動的なオブジェクトを生成する.コンストラクタは最初にX(),次にY()でよばれ,4100バイトを割りあてている.しかし,rはオブジェクトXのポインタとして宣言されているので,Xのディストラクタだけがよばれ,8バイトだけが割り当てられる.よって,1回の反復で4092バイトが失われていることになる.そして,この損失はY::qによって表される.
実行結果
このメモリの漏洩を防ぐには,ディストラクタを仮想関数に変えればよい.
#include <iostream>
using namespace std; class X{ public: X() {p = new int[2]; cout << "X() . ";} virtual ~X() {delete [] p; cout << "~X() .\n";} private: int* p; }; class Y : public X { public: Y() {q = new int[1023]; cout << "Y() : Y::q = " << q << ". ";} ~Y() {delete [] q; cout << "~Y() . ";} private: int* q; }; int main() { for (int i=0; i < 8; i++){ X* r = new Y; delete r; } } |
解答 基本クラスのディストラクタが仮想で宣言されているので,それぞれの反復において,基本クラスと派生クラスのディストラクタを呼び出す.これにより,new演算子で確保されたメモリは開放される.
抽象基本クラス(Abstract Base Classes)
クラス階層の中でそれ自身に属するオブジェクトが存在せず,他のクラスの基本クラスとしてのみ定義されているクラスのことを抽象クラス(abstract class)とよぶ.
よいオブジェクト指向プログラムはクラスの階層を含み,階層関係は下の樹形図のような形で表される.
この樹形図の葉の部分のクラス(例えば,Fish, Owl, Dog)は,それらのクラスの行動を実装するための特定な関数を含んでいる(例えば,Fish.swim( ), Owl.fly( ), Dog.run( )).ところが,これらの関数は全ての派生クラスでも共通であったりする(例えば,Vertebrate.eat( ), Mammal.suckle( ), Primate.peel( )).これらの関数は基本クラスで仮想関数として宣言されることになり,派生クラスでの特定な実装では優先される.
仮想関数が全ての派生クラスで優先されることが分かっているときは,基本クラスに実装する必要は全くない.これを行うには,仮想関数を"純粋"にすればよい.純粋仮想関数(pure virtual member function)とは,
virtual 関数の型 関数名(引数) = 0;
で与えられた関数のことである.
例えば,上のVertebrateクラスで,eat( )関数が全ての派生クラスで優先されるならば,eat( )関数を純粋仮想関数として宣言する.
class Vertebrate
{ public: virtual void eat( ) = 0; }; class Fish : public Vertebrate { public: void eat( ); }; |
クラス階層のそれぞれのクラスは,1つでも純粋仮想関数を含んでいれば,抽象クラスに,それ以外の場合は,具象クラスに分けられる.抽象基本クラス(abstract base class)は1つ以上の純粋仮想関数を含んでいるクラスで,具象派生クラス(concrete derived class)は1つも純粋仮想関数を含まないクラスである.上の例では,Vertebrateクラスは抽象基本クラスでFishクラスは具象派生クラスである. 抽象クラスのオブジェクトは宣言できない
抽象クラスは他のクラスの基本クラスとなって,その派生クラスのインターフェイスや基本的なデータ構造を定義するためにだけ存在し,抽象クラス自身の型のオブジェクトを宣言することは誤りである.
ある派生クラスの基本クラスが抽象クラスである場合,その派生クラスの定義の中で基本クラスの純粋仮想関数を全て再定義する必要がある.上の例で,Vertebrate.eat( ), Mammal.suckle( ),とPrimate.peel( )の3つのメソッドだけが純粋仮想関数だとすると,抽象基本クラス(ABC)はVertebrate, Mammal,とPrimateで,残りの15クラスは具象派生クラス(CDC)となる.そして,これらの15の派生クラスでは,それ自身のeat( )関数の実装をし,Mammalの11個の具象派生クラスはそれ自身のsuckle( )関数の実装をし,2つのPrimateクラスの派生クラスではpeel( )関数の実装をする必要がある.
抽象基本クラスは,概してクラス階層の構築の最初の段階で定義される.そして,抽象基本クラスをもとに,派生クラスの骨組みができあがる.そのとき,純粋仮想関数は階層におけるある種の均一性を規定する.
1. 合成と継承の違いは何か
2. protecedとprivateメンバの違いは何か
3. 仮想関数とは何か
4. 抽象基本クラスとは何か
5. 多相とは何か
6. 具象導出クラスとは何か
7. 静的結合と動的結合の違いは何か
1. 次の定義はどこが間違っているか
class X
{
protected:
int a;
};
class Y : public X
{
public:
void set(X x, int c) {x.a = c;}
};
2. 次のクラス階層を実装せよ.