2019年5月
 1234
567891011
12131415161718
19202122232425
262728293031  

検索・リンク

派生と継承

隠ぺい化の次のステップとして、派生・継承を説明する。

派生を使わずに書くと…

元となるデータ構造(例えばPersonが名前と年齢)でプログラムを作っていて、 途中でその特殊パターンとして、所属と学年を加えた学生(Student)という データ構造を作るとする。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 元となる構造体(Person)
struct Person {
char name[ 20 ] ; // 名前
int age ; // 年齢
} ;
// 初期化関数
void set_Person( struct Person* p ,
char s[] , int x ) {
strcpy( p->name , s ) ;
p->age = x ;
}
// 表示関数
void print_Person( struct Person* p ) {
printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
struct Person saitoh ;
set_Person( &saitoh , "t-saitoh" , 50 ) ;
print_Person( &saitoh ) ;
}
// 元となる構造体(Person) struct Person { char name[ 20 ] ; // 名前 int age ; // 年齢 } ; // 初期化関数 void set_Person( struct Person* p , char s[] , int x ) { strcpy( p->name , s ) ; p->age = x ; } // 表示関数 void print_Person( struct Person* p ) { printf( "%s %d\n" , p->name , p->age ) ; } void main() { struct Person saitoh ; set_Person( &saitoh , "t-saitoh" , 50 ) ; print_Person( &saitoh ) ; }
// 元となる構造体(Person)
struct Person {
   char name[ 20 ] ; // 名前
   int  age ;        // 年齢
} ;
// 初期化関数
void set_Person( struct Person* p ,
                 char s[] , int x ) {
   strcpy( p->name , s ) ;
   p->age = x ;
}
// 表示関数
void print_Person( struct Person* p ) {
   printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
   struct Person saitoh ;
   set_Person( &saitoh , "t-saitoh" , 50 ) ;
   print_Person( &saitoh ) ;
}

パターン1(そのまんま…)

上記のPersonに、所属と学年を加えるのであれば、以下の方法がある。 しかし以下パターン1は、要素名がname,ageという共通な部分があるようにみえるが、 プログラム上は、PersonとPersonStudent1は、まるっきり関係のない別の型にすぎない。

このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 元のデータに追加要素(パターン1)
struct PersonStudent1 {
// Personと同じ部分
char name[ 20 ] ; // 名前
int age ; // 年齢
// 追加部分
char dep[ 20 ] ; // 所属
int grade ; // 学年
} ;
void set_PersonStudent1( struct PersonStudent1* p ,
char s[] , int x ,
char d[] , int g ) {
// set_Personと同じ処理を書いている。
strcpy( p->name , s ) ;
p->age = x ;
// 追加された処理
strcpy( p->dep , d ) ;
p->grade = g ;
}
// 名前と年齢だけ表示
void print_PersonStudent1( struct PersonStudent1* p ) {
// print_Personと同じ処理を書いている。
printf( "%s %d\n" , p->name , p->age ) ;
}
void main() {
struct PersonStudent1 yama1 ;
set_PersonStudent1( &yama1 ,
"yama" , 22 , "PS" , 2 ) ;
print_PersonStudent1( &yama1 ) ;
}
// 元のデータに追加要素(パターン1) struct PersonStudent1 { // Personと同じ部分 char name[ 20 ] ; // 名前 int age ; // 年齢 // 追加部分 char dep[ 20 ] ; // 所属 int grade ; // 学年 } ; void set_PersonStudent1( struct PersonStudent1* p , char s[] , int x , char d[] , int g ) { // set_Personと同じ処理を書いている。 strcpy( p->name , s ) ; p->age = x ; // 追加された処理 strcpy( p->dep , d ) ; p->grade = g ; } // 名前と年齢だけ表示 void print_PersonStudent1( struct PersonStudent1* p ) { // print_Personと同じ処理を書いている。 printf( "%s %d\n" , p->name , p->age ) ; } void main() { struct PersonStudent1 yama1 ; set_PersonStudent1( &yama1 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent1( &yama1 ) ; }
// 元のデータに追加要素(パターン1)
struct PersonStudent1 {
   // Personと同じ部分
   char name[ 20 ] ; // 名前
   int  age ;        // 年齢

   // 追加部分
   char dep[ 20 ] ;  // 所属
   int  grade ;      // 学年
} ;
void set_PersonStudent1( struct PersonStudent1* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
   // set_Personと同じ処理を書いている。
   strcpy( p->name , s ) ;
   p->age = x ;

   // 追加された処理
   strcpy( p->dep , d ) ;
   p->grade = g ;
}

// 名前と年齢だけ表示
void print_PersonStudent1( struct PersonStudent1* p ) {
   // print_Personと同じ処理を書いている。
   printf( "%s %d\n" , p->name , p->age ) ;
}

void main() {
   struct PersonStudent1 yama1 ;
   set_PersonStudent1( &yama1 ,
                       "yama" , 22 , "PS" , 2 ) ;
   print_PersonStudent1( &yama1 ) ;
}

パターン2(元データの処理を少し使って…)

パターン1では、同じような処理を何度も書くことになり、面倒なので、 元データ用の関数をうまく使うように書いてみる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 元のデータに追加要素(パターン2)
struct PersonStudent2 {
// 元のデータPerson
struct Person person ;
// 追加部分
char dep[ 20 ] ;
int grade ;
} ;
void set_PersonStudent2( struct PersonStudent2* p ,
char s[] , int x ,
char d[] , int g ) {
// Personの関数を部分的に使う
set_Person( &(p->person) , s , x ) ;
// 追加分はしかたない
strcpy( p->dep , d ) ;
p->grade = g ;
}
void print_PersonStudent2( struct PersonStudent2* p ) {
// Personの関数を使う。
print_Person( &p->person ) ;
}
void main() {
struct PersonStudent2 yama2 ;
set_PersonStudent2( &yama2 ,
"yama" , 22 , "PS" , 2 ) ;
print_PersonStudent2( &yama2 ) ;
}
// 元のデータに追加要素(パターン2) struct PersonStudent2 { // 元のデータPerson struct Person person ; // 追加部分 char dep[ 20 ] ; int grade ; } ; void set_PersonStudent2( struct PersonStudent2* p , char s[] , int x , char d[] , int g ) { // Personの関数を部分的に使う set_Person( &(p->person) , s , x ) ; // 追加分はしかたない strcpy( p->dep , d ) ; p->grade = g ; } void print_PersonStudent2( struct PersonStudent2* p ) { // Personの関数を使う。 print_Person( &p->person ) ; } void main() { struct PersonStudent2 yama2 ; set_PersonStudent2( &yama2 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent2( &yama2 ) ; }
// 元のデータに追加要素(パターン2)
struct PersonStudent2 {
   // 元のデータPerson
   struct Person person ;

   // 追加部分
   char          dep[ 20 ] ;
   int           grade ;
} ;

void set_PersonStudent2( struct PersonStudent2* p ,
                         char s[] , int x ,
                         char d[] , int g ) {
   // Personの関数を部分的に使う
   set_Person( &(p->person) , s , x ) ;

   // 追加分はしかたない
   strcpy( p->dep , d ) ;
   p->grade = g ;
}

void print_PersonStudent2( struct PersonStudent2* p ) {
   // Personの関数を使う。
   print_Person( &p->person ) ;
}

void main() {
   struct PersonStudent2 yama2 ;
   set_PersonStudent2( &yama2 ,
                       "yama" , 22 , "PS" , 2 ) ;
   print_PersonStudent2( &yama2 ) ;
}

このパターン2であれば、元データ Person の処理をうまく使っているので、 プログラムの記述量を減らすことはできるようになった。

しかし、print_PersonStudent2() のような処理は、元データ構造が同じなのに、 いちいちプログラムを記述するのは面倒ではないか?

そこで、元データの処理を拡張し、処理の流用ができないであろうか?

基底クラスから派生クラスを作る

オブジェクト指向では、元データ(基底クラス)に新たな要素を加えたクラス(派生クラス)を 作ることを「派生」と呼ぶ。派生クラスを定義するときは、クラス名の後ろに、 「:」「public/protected/private」基底クラス名を書く。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 基底クラス
class Person {
private:
char name[ 20 ] ;
int age ;
public:
Person( const char s[] , int x )
: age( x ) {
strcpy( name , s ) ;
}
void print() {
printf( "%s %d\n" , name , age ) ;
}
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
// 追加部分
char dep[ 20 ] ;
int grade ;
public:
Student( const char s[] , int x ,
const char d[] , int g )
: Person( s , x ) // 基底クラスのコンストラクタ
{ // 追加された処理
strcpy( dep , d ) ;
grade = g ;
}
} ;
void main() {
Person saitoh( "t-saitoh" , 50 ) ;
saitoh.print() ;
Student yama( "yama" , 22 , "PS" , 2 ) ;
yama.print() ;
}
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: // 追加部分 char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } } ; void main() { Person saitoh( "t-saitoh" , 50 ) ; saitoh.print() ; Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; }
// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   // 追加部分
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( const char s[] , int x ,
            const char d[] , int g )
            : Person( s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 50 ) ;
   saitoh.print() ;
   Student yama( "yama" , 22 , "PS" , 2 ) ;
   yama.print() ;
}

ここで注目すべき点は、main()の中で、Studentクラス”yama”に対し、yama.print() を呼び出しているが、パターン2であれば、print_PersonStudent2()に相当するプログラムを 記述していない。 しかし、この派生を使うと Person の print() が自動的に流用することができる。 これは、基底クラスのメソッドを「継承」しているから、 このように書け、名前と年齢「yama 22」が表示される。

さらに、Student の中に、以下のような Student 専用の新しい print()を記述してもよい。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Student ...略... {
...略...
// Student クラス専用の print()
void print() {
// 親クラス Person の print() を呼び出す
Person::print() ;
// Student クラス用の処理
printf( "%s %d\n" , dep , grade ) ;
}
} ;
void main() {
...略...
Student yama( "yama" , 22 , "PS" , 2 ) ;
yama.print() ;
}
class Student ...略... { ...略... // Student クラス専用の print() void print() { // 親クラス Person の print() を呼び出す Person::print() ; // Student クラス用の処理 printf( "%s %d\n" , dep , grade ) ; } } ; void main() { ...略... Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; }
class Student ...略... {
   ...略...

   // Student クラス専用の print() 
   void print() {
      // 親クラス Person の print() を呼び出す
      Person::print() ;
      // Student クラス用の処理
      printf( "%s %d\n" , dep , grade ) ;
   }
} ;
void main() {
   ...略...
   Student yama( "yama" , 22 , "PS" , 2 ) ;
   yama.print() ;
}

この場合は、継承ではなく機能が上書き(オーバーライト)されるので、 「yama 22 / PS 2」が表示される。

派生クラスを作る際の後ろに記述した、public は、他にも protected , private を 記述できる。

public    だれもがアクセス可能。
protected であれば、派生クラスからアクセスが可能。
派生クラスであれば、通常は protected で使うのが一般的。
private   派生クラスでもアクセス不可。

仮想関数への伏線

上記のような派生したプログラムを記述した場合、以下のようなプログラムでは何が起こるであろうか?

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Student ... {
:
void print() {
Person::print() ; // 名前と年齢を表示
printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
}
} ;
void main() {
Person saitoh( "t-saitoh" , 55 ) ;
saitoh.print() ; // t-saitoh 55 名前と年齢を表示
Student mitsu( "mitsuki" , 19 , "E" , 4 ) ;
Student ayuka( "ayuka" , 17 , "EI" , 2 ) ;
mitsu.print() ; // mitsuki 19 / E 4 名前,年齢,所属,学年を表示
ayuka.print() ; // ayuka 17 / EI 2 名前,年齢,所属,学年を表示
Person* family[] = {
&saitoh , &mitsu , &ayuka , // 配列の中に、Personへのポインタと
} ; // Studentへのポインタが混在している
// 派生クラスのポインタは、
// 基底クラスのポインタとしても扱える
for( int i = 0 ; i < 3 ; i++ )
family[ i ]->print() ; // t-saitoh 53/mitsuki 18/ayuka 16
} // が表示される。
class Student ... { : void print() { Person::print() ; // 名前と年齢を表示 printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示 } } ; void main() { Person saitoh( "t-saitoh" , 55 ) ; saitoh.print() ; // t-saitoh 55 名前と年齢を表示 Student mitsu( "mitsuki" , 19 , "E" , 4 ) ; Student ayuka( "ayuka" , 17 , "EI" , 2 ) ; mitsu.print() ; // mitsuki 19 / E 4 名前,年齢,所属,学年を表示 ayuka.print() ; // ayuka 17 / EI 2 名前,年齢,所属,学年を表示 Person* family[] = { &saitoh , &mitsu , &ayuka , // 配列の中に、Personへのポインタと } ; // Studentへのポインタが混在している // 派生クラスのポインタは、 // 基底クラスのポインタとしても扱える for( int i = 0 ; i < 3 ; i++ ) family[ i ]->print() ; // t-saitoh 53/mitsuki 18/ayuka 16 } // が表示される。
class Student ... {
   :
   void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
void main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   saitoh.print() ; // t-saitoh 55 名前と年齢を表示

   Student mitsu( "mitsuki" , 19 , "E" ,  4 ) ;
   Student ayuka( "ayuka" ,   17 , "EI" , 2 ) ;
   mitsu.print() ; // mitsuki 19 / E 4   名前,年齢,所属,学年を表示
   ayuka.print() ; // ayuka 17   / EI 2  名前,年齢,所属,学年を表示

   Person* family[] = {
      &saitoh , &mitsu , &ayuka ,  // 配列の中に、Personへのポインタと
   } ;                             // Studentへのポインタが混在している
                                   // 派生クラスのポインタは、
                                   // 基底クラスのポインタとしても扱える
   for( int i = 0 ; i < 3 ; i++ )
      family[ i ]->print() ;       // t-saitoh 53/mitsuki 18/ayuka 16
}                                  //  が表示される。