ホーム » スタッフ » 斉藤徹 » 講義録 » オブジェクト指向 (ページ 5)

オブジェクト指向」カテゴリーアーカイブ

2025年5月
 123
45678910
11121314151617
18192021222324
25262728293031

検索・リンク

抽象クラス(純粋仮想基底クラス)

前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。

この仮想関数の機能を逆手にとったプログラムの記述方法として、抽象クラス(純粋仮想基底クラス)がある。その使い方を説明する。

JavaのGUIにおける派生の使い方

先週の講義では、派生を使ったプログラミングは、GUI で使われていることを紹介したが、例として Java のプログラミングスタイルを少し紹介する。

例えば、Java で アプレット(ブラウザの中で動かすJavaプログラム)を作る場合の、最小のサンプルプログラムは、以下のようになる。

import java.applet.Applet; // C言語でいうところの、Applet 関連の処理を include
import java.awt.Graphics;

public class App1 extends Applet {  // Applet クラスから、App1 クラスを派生
    public void paint(Graphics g) { // 画面にApp1を表示する必要がある時に呼び出される。
        g.drawString("Hello World." , 100 , 100);
    }
}

この例では、ブラウザのGUIを管理する Applet クラスから、App1 というクラスを派生(extendsキーワード)し、App1 固有の処理として、paint() メソッドを定義している。GUI のプログラミングでは、本来ならマウスが押された場合の処理などを記述する必要があるが、このプログラムでは paint() 以外何も書かれていない。これはマウス処理などは、基底クラスのAppletのマウス処理が継承されるので、省略してもうまく動くようになっている。

純粋仮想基底クラス

純粋仮想基底クラスとは、見かけ上はデータを何も持たないクラスであり、本来なら意味がないデータ構造となってしまう。しかし、派生クラスで要素となるデータと仮想関数で機能を与えることで、基底クラスという共通部分から便利な活用ができる。(実際には、型を区別するための型情報を持っている)

例えば、C言語であれば一つの配列に、整数、文字列、実数といった異なる型のデータを記憶させることは本来ならできない。しかし、以下のような処理を記載すれば、可能となる。

C言語では、1つの記憶域を共有するために共用体(union)を使うが、C++では仮想関数が使えるようになり、型の管理をプログラマーが行う必要のある「面倒で危険な」共用体を使う必要はなくなった。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 純粋仮想基底クラス
class Object {
public:
virtual void print() const = 0 ;
// 中身の無い純粋基底クラスで、
// 仮想関数を記述しない時の書き方。
} ;
// 整数データの派生クラス
class IntObject : public Object {
private:
int data ;
public:
IntObject( int x ) {
data = x ;
}
virtual void print() const {
printf( "%d\n" , data ) ;
}
} ;
// 文字列の派生クラス
class StringObject : public Object {
private:
char data[ 100 ] ;
public:
StringObject( const char* s ) {
strcpy( data , s ) ;
}
virtual void print() const {
printf( "%s\n" , data ) ;
}
} ;
// 実数の派生クラス
class DoubleObject : public Object {
private:
double data ;
public:
DoubleObject( double x ) {
data = x ;
}
virtual void print() const {
printf( "%lf\n" , data ) ;
}
} ;
// 動作確認
int main() {
Object* data[3] = {
new IntObject( 123 ) ,
new StringObject( "abc" ) ,
new DoubleObject( 1.23 ) ,
} ;
for( int i = 0 ; i < 3 ; i++ ) { // 123
data[i]->print() ; // abc
} // 1.23 と表示
return 0 ;
} ;
// 純粋仮想基底クラス class Object { public: virtual void print() const = 0 ; // 中身の無い純粋基底クラスで、 // 仮想関数を記述しない時の書き方。 } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() const { printf( "%d\n" , data ) ; } } ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() const { printf( "%s\n" , data ) ; } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() const { printf( "%lf\n" , data ) ; } } ; // 動作確認 int main() { Object* data[3] = { new IntObject( 123 ) , new StringObject( "abc" ) , new DoubleObject( 1.23 ) , } ; for( int i = 0 ; i < 3 ; i++ ) { // 123 data[i]->print() ; // abc } // 1.23 と表示 return 0 ; } ;
// 純粋仮想基底クラス
class Object {
public:
   virtual void print() const = 0 ;
   // 中身の無い純粋基底クラスで、
   // 仮想関数を記述しない時の書き方。
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
} ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%lf\n" , data ) ;
   }
} ;

// 動作確認
int main() {
   Object* data[3] = {
      new IntObject( 123 ) ,
      new StringObject( "abc" ) ,
      new DoubleObject( 1.23 ) ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) { // 123
      data[i]->print() ;            // abc
   }                                // 1.23 と表示
   return 0 ;
} ;

このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。

仮想関数を使うと、Object型の中に自動的に型情報が保存されるようになる。一般的な実装では、各派生クラス用の仮想関数のポインタテーブル(vtable)へのポインタが使われる。

Javaなどのオブジェクト指向言語では、全てのクラス階層のスーパークラスとして、Object を持つように作られている。

様々な型に適用できるプログラム

次に、純粋仮想基底クラスの特徴を応用したプログラムの作り方を説明する。

例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
int a[5] = { 11, 55, 22, 44, 33 } ;
void my_sort( int array[] , int size ) {
for( int i = 0 ; i < size - 1 ; i++ ) {
int max = i ;
for( int j = i + 1 ; j < size ; j++ ) {
if ( array[j] > array[max] )
max = j ;
}
int tmp = array[i] ;
array[i] = array[max] ;
array[max] = tmp ;
}
}
int main() {
my_sort( a , 5 ) ;
}
int a[5] = { 11, 55, 22, 44, 33 } ; void my_sort( int array[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( array[j] > array[max] ) max = j ; } int tmp = array[i] ; array[i] = array[max] ; array[max] = tmp ; } } int main() { my_sort( a , 5 ) ; }
int a[5] = { 11, 55, 22, 44, 33 } ;

void my_sort( int array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j] > array[max] )
            max = j ;
      }
      int tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
int main() {
   my_sort( a , 5 ) ;
}

しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。

C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。しかし、1件あたりのデータサイズや、データ実体へのポインタを正しく理解する必要がある。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
#include <stdlib.h>
int a[ 4 ] = { 11, 33, 22, 44 } ;
double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ;
// 並び替えを行いたいデータ専用の比較関数を作る。
// a>bなら+1, a=bなら0, a<bなら-1を返す関数
int cmp_int( int* pa , int* pb ) { // int型用コールバック関数
return *pa - *pb ;
}
int cmp_double( double* pa , double* pb ) { // double型用コールバック関数
if ( *pa == *pb )
return 0 ;
else if ( *pa > *pb )
return 1 ;
else
return -1 ;
}
int main() { // C言語の怖さ
qsort( a , 4 , sizeof( int ) , // このあたりの引数を書き間違えると
(int(*)(void*,void*)) cmp_int ) ; // とんでもない目にあう。
qsort( b , 3 , sizeof( double ) ,
(int(*)(void*,void*)) cmp_double ) ;
}
#include <stdio.h> #include <stdlib.h> int a[ 4 ] = { 11, 33, 22, 44 } ; double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ; // 並び替えを行いたいデータ専用の比較関数を作る。 // a>bなら+1, a=bなら0, a<bなら-1を返す関数 int cmp_int( int* pa , int* pb ) { // int型用コールバック関数 return *pa - *pb ; } int cmp_double( double* pa , double* pb ) { // double型用コールバック関数 if ( *pa == *pb ) return 0 ; else if ( *pa > *pb ) return 1 ; else return -1 ; } int main() { // C言語の怖さ qsort( a , 4 , sizeof( int ) , // このあたりの引数を書き間違えると (int(*)(void*,void*)) cmp_int ) ; // とんでもない目にあう。 qsort( b , 3 , sizeof( double ) , (int(*)(void*,void*)) cmp_double ) ; }
#include <stdio.h>
#include <stdlib.h>
int a[ 4 ] = { 11, 33, 22, 44 } ;
double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ;
// 並び替えを行いたいデータ専用の比較関数を作る。
// a>bなら+1, a=bなら0, a<bなら-1を返す関数
int cmp_int( int* pa , int* pb ) { // int型用コールバック関数
   return *pa - *pb ;
}
int cmp_double( double* pa , double* pb ) { // double型用コールバック関数
   if ( *pa == *pb )
      return 0 ;
   else if ( *pa > *pb )
      return 1 ;
   else
      return -1 ;
}
int main() {                                   // C言語の怖さ
   qsort( a , 4 , sizeof( int ) ,              //   このあたりの引数を書き間違えると
          (int(*)(void*,void*)) cmp_int ) ;    //   とんでもない目にあう。
   qsort( b , 3 , sizeof( double ) ,
          (int(*)(void*,void*)) cmp_double ) ;
} 

このように、自分が作っておいた関数のポインタを、関数に渡して呼び出してもらう方法は、コールバックと呼ぶ。
JavaScript などの言語では、こういったコールバックを使ったコーディングがよく使われる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
f() ;
}
// コールバックされる関数 foo()
function foo() {
console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
console.log( "anonymous" ) ;
} ) ;
// コールバック関数 f を呼び出す関数 function exec_callback( var f ) { f() ; } // コールバックされる関数 foo() function foo() { console.log( "foo()" ) ; } // foo() を実行する。 exec_callback( foo ) ; // 無名関数を実行する。 exec_callback( function() { console.log( "anonymous" ) ; } ) ;
// コールバック関数 f を呼び出す関数
function exec_callback( var f ) {
   f() ;
}
// コールバックされる関数 foo()
function foo() {
   console.log( "foo()" ) ;
}
// foo() を実行する。
exec_callback( foo ) ;
// 無名関数を実行する。
exec_callback( function() {
                  console.log( "anonymous" ) ;
               } ) ;

任意のデータを並び替え

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Object {
public:
virtual void print() const = 0 ;
virtual int cmp( Object* ) = 0 ;
} ;
// 整数データの派生クラス
class IntObject : public Object {
private:
int data ;
public:
IntObject( int x ) {
data = x ;
}
virtual void print() const {
printf( "%d\n" , data ) ;
}
virtual int cmp( Object* p ) {
int pdata = ((IntObject*)p)->data ; // 本当はこのキャストが危険
return data - pdata ; // ↓安全な実装したいなら↓
} // IntObject* pi = dynamic_cast<IntObject*>(p) ;
} ; // return pi != NULL ? data - pi->data : 0 ;
// 文字列の派生クラス
class StringObject : public Object {
private:
char data[ 100 ] ;
public:
StringObject( const char* s ) {
strcpy( data , s ) ;
}
virtual void print() const {
printf( "%s\n" , data ) ;
}
virtual int cmp( Object* p ) {
char* pdata = ((StringObject*)p)->data ;
return strcmp( data , pdata ) ; // 文字列比較関数
}
} ;
// 実数の派生クラス
class DoubleObject : public Object {
private:
double data ;
public:
DoubleObject( double x ) {
data = x ;
}
virtual void print() const {
printf( "%lf\n" , data ) ;
}
virtual int cmp( Object* p ) {
double pdata = ((DoubleObject*)p)->data ;
if ( data == pdata )
return 0 ;
else if ( data > pdata )
return 1 ;
else
return -1 ;
}
} ;
// Objectからの派生クラスでcmp()メソッドを
// 持ってさえいれば、どんな型でもソートができる。
void my_sort( Object* array[] , int size ) {
for( int i = 0 ; i < size - 1 ; i++ ) {
int max = i ;
for( int j = i + 1 ; j < size ; j++ ) {
if ( array[j]->cmp( array[max] ) > 0 )
max = j ;
}
Object* tmp = array[i] ;
array[i] = array[max] ;
array[max] = tmp ;
}
}
// 動作確認
int main() {
Object* idata[3] = {
new IntObject( 11 ) ,
new IntObject( 33 ) ,
new IntObject( 22 ) ,
} ;
Object* sdata[3] = {
new StringObject( "abc" ) ,
new StringObject( "defghi" ) ,
new StringObject( "c" ) ,
} ;
my_sort( idata , 3 ) ; // 整数のソート
for( int i = 0 ; i < 3 ; i++ )
idata[i]->print() ;
my_sort( sdata , 3 ) ; // 文字列のソート
for( int i = 0 ; i < 3 ; i++ )
sdata[i]->print() ;
return 0 ;
} ;
class Object { public: virtual void print() const = 0 ; virtual int cmp( Object* ) = 0 ; } ; // 整数データの派生クラス class IntObject : public Object { private: int data ; public: IntObject( int x ) { data = x ; } virtual void print() const { printf( "%d\n" , data ) ; } virtual int cmp( Object* p ) { int pdata = ((IntObject*)p)->data ; // 本当はこのキャストが危険 return data - pdata ; // ↓安全な実装したいなら↓ } // IntObject* pi = dynamic_cast<IntObject*>(p) ; } ; // return pi != NULL ? data - pi->data : 0 ; // 文字列の派生クラス class StringObject : public Object { private: char data[ 100 ] ; public: StringObject( const char* s ) { strcpy( data , s ) ; } virtual void print() const { printf( "%s\n" , data ) ; } virtual int cmp( Object* p ) { char* pdata = ((StringObject*)p)->data ; return strcmp( data , pdata ) ; // 文字列比較関数 } } ; // 実数の派生クラス class DoubleObject : public Object { private: double data ; public: DoubleObject( double x ) { data = x ; } virtual void print() const { printf( "%lf\n" , data ) ; } virtual int cmp( Object* p ) { double pdata = ((DoubleObject*)p)->data ; if ( data == pdata ) return 0 ; else if ( data > pdata ) return 1 ; else return -1 ; } } ; // Objectからの派生クラスでcmp()メソッドを // 持ってさえいれば、どんな型でもソートができる。 void my_sort( Object* array[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( array[j]->cmp( array[max] ) > 0 ) max = j ; } Object* tmp = array[i] ; array[i] = array[max] ; array[max] = tmp ; } } // 動作確認 int main() { Object* idata[3] = { new IntObject( 11 ) , new IntObject( 33 ) , new IntObject( 22 ) , } ; Object* sdata[3] = { new StringObject( "abc" ) , new StringObject( "defghi" ) , new StringObject( "c" ) , } ; my_sort( idata , 3 ) ; // 整数のソート for( int i = 0 ; i < 3 ; i++ ) idata[i]->print() ; my_sort( sdata , 3 ) ; // 文字列のソート for( int i = 0 ; i < 3 ; i++ ) sdata[i]->print() ; return 0 ; } ;
class Object {
public:
   virtual void print() const = 0 ;
   virtual int  cmp( Object* ) = 0 ;
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%d\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      int pdata = ((IntObject*)p)->data ;  // 本当はこのキャストが危険
      return data - pdata ;                //  ↓安全な実装したいなら↓
   }                                       // IntObject* pi = dynamic_cast<IntObject*>(p) ;
} ;                                        // return pi != NULL ? data - pi->data : 0 ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() const {
      printf( "%s\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      char* pdata = ((StringObject*)p)->data ;
      return strcmp( data , pdata ) ; // 文字列比較関数
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() const {
      printf( "%lf\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      double pdata = ((DoubleObject*)p)->data ;
      if ( data == pdata )
         return 0 ;
      else if ( data > pdata )
         return 1 ;
      else
         return -1 ;
   }
} ;

// Objectからの派生クラスでcmp()メソッドを
//   持ってさえいれば、どんな型でもソートができる。
void my_sort( Object* array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j]->cmp( array[max] ) > 0 )
            max = j ;
      }
      Object* tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
// 動作確認
int main() {
   Object* idata[3] = {
      new IntObject( 11 ) ,
      new IntObject( 33 ) ,
      new IntObject( 22 ) ,
   } ;
   Object* sdata[3] = {
      new StringObject( "abc" ) ,
      new StringObject( "defghi" ) ,
      new StringObject( "c" ) ,
   } ;
   my_sort( idata , 3 ) ; // 整数のソート
   for( int i = 0 ; i < 3 ; i++ )
      idata[i]->print() ;
   my_sort( sdata , 3 ) ; // 文字列のソート
   for( int i = 0 ; i < 3 ; i++ )
      sdata[i]->print() ;
   return 0 ;
} ;

このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。

ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。

また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない。

こういう場合、最近の C++ ではテンプレート機能が使われる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
template <typename T>
void my_sort( T a[] , int size ) {
for( int i = 0 ; i < size - 1 ; i++ ) {
int max = i ;
for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] )
max = j ;
}
T tmp = a[i] ;
a[i] = a[max] ;
a[max] = tmp ;
}
}
int main() {
int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ;
double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ;
// typename T = int で int::mysort() が作られる
my_sort<int>( idata , 5 ) ;
for( int i = 0 ; i < 5 ; i++ )
printf( "%d " , idata[i] ) ;
printf( "\n" ) ;
// typename T = double で double::mysort() が作られる
my_sort<double>( fdata , 4 ) ;
for( int i = 0 ; i < 4 ; i++ )
printf( "%lf " , fdata[i] ) ;
printf( "\n" ) ;
return 0 ;
}
template <typename T> void my_sort( T a[] , int size ) { for( int i = 0 ; i < size - 1 ; i++ ) { int max = i ; for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] ) max = j ; } T tmp = a[i] ; a[i] = a[max] ; a[max] = tmp ; } } int main() { int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ; double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ; // typename T = int で int::mysort() が作られる my_sort<int>( idata , 5 ) ; for( int i = 0 ; i < 5 ; i++ ) printf( "%d " , idata[i] ) ; printf( "\n" ) ; // typename T = double で double::mysort() が作られる my_sort<double>( fdata , 4 ) ; for( int i = 0 ; i < 4 ; i++ ) printf( "%lf " , fdata[i] ) ; printf( "\n" ) ; return 0 ; }
template <typename T>
void my_sort( T a[] , int size ) {
  for( int i = 0 ; i < size - 1 ; i++ ) {
    int max = i ;
    for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] )
        max = j ;
    }
    T  tmp = a[i] ;
    a[i] = a[max] ;
    a[max] = tmp ;
  }
}

int main() {
  int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ;
  double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ;

  // typename T = int で int::mysort() が作られる
  my_sort<int>( idata , 5 ) ;
  for( int i = 0 ; i < 5 ; i++ )
    printf( "%d " , idata[i] ) ;
  printf( "\n" ) ;

  // typename T = double で double::mysort() が作られる
  my_sort<double>( fdata , 4 ) ;
  for( int i = 0 ; i < 4 ; i++ )
    printf( "%lf " , fdata[i] ) ;
  printf( "\n" ) ;
  return 0 ;
}

C++のテンプレート機能は、my_sort( int[] , int ) で呼び出されると、typename T = int で、整数型用の my_sort() の処理が自動的に作られる。同じく、my_sort( double[] , int ) で呼び出されると、typename = double で 実数型用の my_sort() が作られる。

テンプレート機能では、各型用のコードが自動的に複数生成されるという意味では、出来上がったコードがコンパクトという訳ではない。

仮想関数レポート課題

ここで示したプログラムを参考に、独自のデータ(例えば、複素数のデータや名前と誕生日といったデータ)について、my_sort() などで並び替えるプログラムを作成せよ。並び替える時の順序も、各自て定義すればいい。(複素数なら絶対値順とか、名前と誕生日なら、誕生日順とか)

派生と継承と仮想関数

前回の派生と継承のイメージを改めて記載する。

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 print() {
Person::print() ; // 基底クラスPersonで名前と年齢を表示
printf( "- %s %d\n" , dep , grade ) ;
}
} ;
int main() {
Person saitoh( "t-saitoh" , 55 ) ;
Student yama( "yamada" , 21 , "ES" , 1 ) ;
Student nomu( "nomura" , 22 , "PS" , 2 ) ;
saitoh.print() ; // 表示 t-saitoh 55
yama.print() ; // 表示 yamada 21
// - ES 1
nomu.print() ; // 表示 nomura 22
return 0 ; // - PS 2
}
// 基底クラス 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 print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; saitoh.print() ; // 表示 t-saitoh 55 yama.print() ; // 表示 yamada 21 // - ES 1 nomu.print() ; // 表示 nomura 22 return 0 ; // - PS 2 }
// 基底クラス
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 print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ; 
   saitoh.print() ; // 表示 t-saitoh 55
   yama.print() ;   // 表示 yamada 21
                    //      - ES 1
   nomu.print() ;   // 表示 nomura 22
   return 0 ;       //      - PS 2
}

このような処理でのデータ構造は、次のようなイメージで表される。

派生クラスでの問題提起

基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。

以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
int main() {
Person saitoh( "t-saitoh" , 55 ) ;
Student yama( "yamada" , 21 , "ES" , 1 ) ;
Student nomu( "nomura" , 22 , "PS" , 2 ) ;
Person* table[3] = {
&saitoh , &yama , &nomu ,
} ;
for( int i = 0 ; i < 3 ; i++ ) {
table[ i ]->print() ;
}
return 0 ;
}
int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[ i ]->print() ; } return 0 ; }
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[ i ]->print() ;
   }
   return 0 ;
}

C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。

このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。

t-saitoh 55
yamada   21
nomura   22

派生した型に応じた処理

上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2
// 混在したPersonを表示 for( int i = 0 ; i < 3 ; i++ ) table[i]->print() ; // Student は、所属と名前を表示して欲しい t-saitoh 55 yamada 21 - ES 1 nomura 22 - PS 2
// 混在したPersonを表示
for( int i = 0 ; i < 3 ; i++ )
   table[i]->print() ;
// Student は、所属と名前を表示して欲しい
t-saitoh 55
yamada 21
- ES 1
nomura 22
- PS 2

上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という型の識別番号を追加し、type=1ならば、Student型として扱うようにしてみた。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 基底クラス
class Person {
private:
int type ; // 型識別情報
char name[ 20 ] ;
int age ;
public:
Person( int tp , const char s[] , int x )
: type( tp ) , age( x ) {
strcpy( name , s ) ;
}
int type_person() { return type ; }
void print() {
printf( "%s %d\n" , name , age ) ;
}
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
char dep[ 20 ] ;
int grade ;
public:
Student( int tp , const char s[] , int x ,
const char d[] , int g )
: Person( tp , s , x ) // 基底クラスのコンストラクタ
{ // 追加された処理
strcpy( dep , d ) ;
grade = g ;
}
void print() {
Person::print() ; // 基底クラスPersonで名前と年齢を表示
printf( "- %s %d\n" , dep , grade ) ;
}
} ;
int main() {
// type=0 は Person 型、type=1は Student 型
Person saitoh( 0 , "t-saitoh" , 55 ) ;
Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;
Person* table[3] = {
&saitoh , &yama , &nomu ,
} ;
for( int i = 0 ; i < 3 ; i++ ) {
switch( table[i]->type_person() ) {
case 0 :
table[i]->print() ;
break ;
case 1 :
// 強制的にStudent*型として print() を呼び出す。
// 最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
((Student*)table[i])->print() ;
break ;
}
}
return 0 ;
}
// 基底クラス class Person { private: int type ; // 型識別情報 char name[ 20 ] ; int age ; public: Person( int tp , const char s[] , int x ) : type( tp ) , age( x ) { strcpy( name , s ) ; } int type_person() { return type ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( int tp , const char s[] , int x , const char d[] , int g ) : Person( tp , s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( 0 , "t-saitoh" , 55 ) ; Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ; Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { switch( table[i]->type_person() ) { case 0 : table[i]->print() ; break ; case 1 : // 強制的にStudent*型として print() を呼び出す。 // 最近のC++なら、(static_cast<Student*>(table[i]))->>print() ; ((Student*)table[i])->print() ; break ; } } return 0 ; }
// 基底クラス
class Person {
private:
   int  type ; // 型識別情報
   char name[ 20 ] ;
   int  age ;
public:
   Person( int tp , const char s[] , int x )
     : type( tp ) , age( x ) {
      strcpy( name , s ) ;
   }
   int type_person() { return type ; }
   void print() {
      printf( "%s %d\n" , name , age ) ;
   }
} ;
// 派生クラス(Student は Person から派生)
class Student : public Person {
private:
   char dep[ 20 ] ;
   int  grade ;
public:
   Student( int tp , const char s[] , int x ,
            const char d[] , int g )
            : Person( tp , s , x ) // 基底クラスのコンストラクタ
   {  // 追加された処理
      strcpy( dep , d ) ;
      grade = g ;
   }
   void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( 0 , "t-saitoh" , 55 ) ;
   Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      switch( table[i]->type_person() ) {
      case 0 :
         table[i]->print() ;
         break ;
      case 1 :
         // 強制的にStudent*型として print() を呼び出す。
         //   最近のC++なら、(static_cast<Student*>(table[i]))->>print() ;
         ((Student*)table[i])->print() ;
         break ;
      }
   }
   return 0 ;
}

しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。

また、関数を呼び出す際に、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。

もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。巨大な switch 文は、選択肢だけの if else-if else-if が並ぶと処理効率も悪い。

仮想関数

上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。

型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。

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 ) ;
}
virtual 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 ;
}
virtual void print() {
Person::print() ; // 基底クラスPersonで名前と年齢を表示
printf( "- %s %d\n" , dep , grade ) ;
}
} ;
int main() {
// type=0 は Person 型、type=1は Student 型
Person saitoh( "t-saitoh" , 55 ) ;
Student yama( "yamada" , 21 , "ES" , 1 ) ;
Student nomu( "nomura" , 22 , "PS" , 2 ) ;
Person* table[3] = {
&saitoh , &yama , &nomu ,
} ;
for( int i = 0 ; i < 3 ; i++ ) {
table[i]->print() ;
}
return 0 ;
}
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } virtual 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 ; } virtual void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[i]->print() ; } return 0 ; }
// 基底クラス
class Person {
private:
   char name[ 20 ] ;
   int  age ;
public:
   Person( const char s[] , int x )
     : age( x ) {
      strcpy( name , s ) ;
   }
   virtual 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 ;
   }
   virtual void print() {
      Person::print() ;       // 基底クラスPersonで名前と年齢を表示
      printf( "- %s %d\n" , dep , grade ) ;
   }
} ;
int main() {
   // type=0 は Person 型、type=1は Student 型
   Person saitoh( "t-saitoh" , 55 ) ;
   Student yama( "yamada" , 21 , "ES" , 1 ) ;
   Student nomu( "nomura" , 22 , "PS" , 2 ) ;

   Person* table[3] = {
      &saitoh , &yama , &nomu ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) {
      table[i]->print() ;
   }
   return 0 ;
}

クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。

また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。

関数ポインタ

仮想関数の仕組みを実現するためには、関数ポインタが使われる。

以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
int add( int a , int b ) {
return a + b ;
}
int mul( int a , int b ) {
return a * b ;
}
int main() {
int (*fp)( int , int ) ;
fp = add ;
printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
fp = mul ;
printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12
int (*ftable[2])( int , int ) = {
add , mul ,
} ;
for( int i = 0 ; i < 2 ; i++ )
printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
return 0 ;
}
int add( int a , int b ) { return a + b ; } int mul( int a , int b ) { return a * b ; } int main() { int (*fp)( int , int ) ; fp = add ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7 fp = mul ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12 int (*ftable[2])( int , int ) = { add , mul , } ; for( int i = 0 ; i < 2 ; i++ ) printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ; return 0 ; }
int add( int a , int b ) {
   return a + b ;
}
int mul( int a , int b ) {
   return a * b ;
}
int main() {
   int (*fp)( int , int ) ;
   fp = add ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7
   fp = mul ;
   printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12

   int (*ftable[2])( int , int ) = {
      add , mul ,
   } ;
   for( int i = 0 ; i < 2 ; i++ )
      printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;
   return 0 ;
}

仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。

仮想関数の実装方法

仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。この場合、仮想関数の呼び出しは、object->table[n]( arg… ) のような処理が行われる。

派生と継承

隠ぺい化の次のステップとして、派生・継承を説明する。オブジェクト指向プログラミングでは、一番基本となるデータ構造を宣言し、その基本構造に様々な機能を追加した派生クラスを記述することでプログラムを作成する。今回は、その派生を理解するためにC言語で発生する問題点を考える。

説明が中途半端になるので、講義後半は先週のレポート課題の時間とする。

派生を使わずに書くと…

元となるデータ構造(例えば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 ) ;
}
int main() {
struct Person saitoh ;
set_Person( &saitoh , "t-saitoh" , 50 ) ;
print_Person( &saitoh ) ;
return 0 ;
}
// 元となる構造体(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 ) ; } int main() { struct Person saitoh ; set_Person( &saitoh , "t-saitoh" , 50 ) ; print_Person( &saitoh ) ; return 0 ; }
// 元となる構造体(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 ) ;
}
int main() {
   struct Person saitoh ;
   set_Person( &saitoh , "t-saitoh" , 50 ) ;
   print_Person( &saitoh ) ;
   return 0 ;
}

パターン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 ) ;
printf( "- %s %d¥n" , p->dep , p->grade ) ;
}
int main() {
struct PersonStudent1 yama1 ;
set_PersonStudent1( &yama1 ,
"yama" , 22 , "PS" , 2 ) ;
print_PersonStudent1( &yama1 ) ;
return 0 ;
}
// 元のデータに追加要素(パターン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 ) ; printf( "- %s %d¥n" , p->dep , p->grade ) ; } int main() { struct PersonStudent1 yama1 ; set_PersonStudent1( &yama1 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent1( &yama1 ) ; return 0 ; }
// 元のデータに追加要素(パターン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 ) ;
   printf( "- %s %d¥n" , p->dep , p->grade ) ;
}

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

パターン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 ) ;
printf( "- %s %d¥n" , p->dep , p->grade ) ;
}
int main() {
struct PersonStudent2 yama2 ;
set_PersonStudent2( &yama2 ,
"yama" , 22 , "PS" , 2 ) ;
print_PersonStudent2( &yama2 ) ;
return 0 ;
}
// 元のデータに追加要素(パターン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 ) ; printf( "- %s %d¥n" , p->dep , p->grade ) ; } int main() { struct PersonStudent2 yama2 ; set_PersonStudent2( &yama2 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent2( &yama2 ) ; return 0 ; }
// 元のデータに追加要素(パターン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 ) ;
   printf( "- %s %d¥n" , p->dep , p->grade ) ; 
}

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

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

しかし、print_PersonStudent2() のような処理は、名前と年齢だけ表示すればいいという場合、元データ構造が同じなのに、 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 ;
}
} ;
int main() {
Person saitoh( "t-saitoh" , 50 ) ;
saitoh.print() ;
Student yama( "yama" , 22 , "PS" , 2 ) ;
yama.print() ; // "yama 22"が表示される
return 0 ;
}
// 基底クラス 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 ; } } ; int main() { Person saitoh( "t-saitoh" , 50 ) ; saitoh.print() ; Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; // "yama 22"が表示される return 0 ; }
// 基底クラス
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 ;
   }
} ;

int main() {
   Person saitoh( "t-saitoh" , 50 ) ;
   saitoh.print() ;
   Student yama( "yama" , 22 , "PS" , 2 ) ;
   yama.print() ;  // "yama 22"が表示される
   return 0 ;
}

ここで注目すべき点は、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   派生クラスでもアクセス不可。

C言語で無理やりオブジェクト指向の”派生”を使う方法

オブジェクト指向の機能の無いC言語で、このような派生と継承を実装する場合には、共用体を使う以下のようなテクニックが使われていた。
unix の GUI である X11 でも共用体を用いて派生を実装していた。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 基底クラス
struct PersonBase { // 基底クラス
char name[ 20 ] ;
int age ;
} ;
struct PersonStudent { // 派生クラス
struct PersonBase base ;
char dep[ 20 ] ;
int grade ;
} ;
//(base) //(student)
union Person { // name //[name]
struct PersonBase base ; // age //[age ]
struct PersonStudent student ; // dep
} ; // grade
void person_Print( struct Person* p ) {
printf( "%s %d\n" , p->base.name , p->base.age ) ;
}
int main() {
struct PersonBase tsaitoh = { "tsaitoh" , 55 } ;
struct PersonStudent mitsuki = { { "mitsuki" , 21 } , "KIT" , 4 } ;
print_Person( (struct Person*)&tsaitoh ) ;
print_Person( (struct Person*)&mitsuki ) ; // 無理やり print_Person を呼び出す
return 0 ;
}
// 基底クラス struct PersonBase { // 基底クラス char name[ 20 ] ; int age ; } ; struct PersonStudent { // 派生クラス struct PersonBase base ; char dep[ 20 ] ; int grade ; } ; //(base) //(student) union Person { // name //[name] struct PersonBase base ; // age //[age ] struct PersonStudent student ; // dep } ; // grade void person_Print( struct Person* p ) { printf( "%s %d\n" , p->base.name , p->base.age ) ; } int main() { struct PersonBase tsaitoh = { "tsaitoh" , 55 } ; struct PersonStudent mitsuki = { { "mitsuki" , 21 } , "KIT" , 4 } ; print_Person( (struct Person*)&tsaitoh ) ; print_Person( (struct Person*)&mitsuki ) ; // 無理やり print_Person を呼び出す return 0 ; }
// 基底クラス
struct PersonBase {     // 基底クラス
   char name[ 20 ] ;
   int  age ;
} ;

struct PersonStudent {  // 派生クラス
   struct PersonBase base ;
   char dep[ 20 ] ;
   int  grade ;
} ;
                                   //(base) //(student)
union Person {                     // name  //[name]
   struct PersonBase    base ;     // age   //[age ]
   struct PersonStudent student ;           // dep
} ;                                         // grade

void person_Print( struct Person* p ) {
   printf( "%s %d\n" , p->base.name , p->base.age ) ;   
}

int main() {
   struct PersonBase    tsaitoh = { "tsaitoh" , 55 } ;
   struct PersonStudent mitsuki = { { "mitsuki" , 21 } , "KIT" , 4 } ;
   print_Person( (struct Person*)&tsaitoh ) ;
   print_Person( (struct Person*)&mitsuki ) ;  // 無理やり print_Person を呼び出す
   return 0 ;
}

仮想関数への伏線

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

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 ) ; // 所属と学年を表示
}
} ;
int main() {
Person saitoh( "t-saitoh" , 55 ) ;
saitoh.print() ; // t-saitoh 55 名前と年齢を表示
Student mitsu( "mitsuki" , 20 , "KIT" , 3 ) ;
Student ayuka( "ayuka" , 18 , "EI" , 4 ) ;
mitsu.print() ; // mitsuki 20 / KIT 3 名前,年齢,所属,学年を表示
ayuka.print() ; // ayuka 18 / EI 4 名前,年齢,所属,学年を表示
Person* family[] = {
&saitoh , &mitsu , &ayuka , // 配列の中に、Personへのポインタと
} ; // Studentへのポインタが混在している
// 派生クラスのポインタは、
// 基底クラスのポインタとしても扱える
for( int i = 0 ; i < 3 ; i++ )
family[ i ]->print() ; // t-saitoh 55/mitsuki 20/ayuka 18
return 0 ; // が表示される。
} // # "mitsuki 20/KIT 3" とか "ayuka 18/EI 4"
// # が表示されてほしい?
class Student ... { : void print() { Person::print() ; // 名前と年齢を表示 printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示 } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; saitoh.print() ; // t-saitoh 55 名前と年齢を表示 Student mitsu( "mitsuki" , 20 , "KIT" , 3 ) ; Student ayuka( "ayuka" , 18 , "EI" , 4 ) ; mitsu.print() ; // mitsuki 20 / KIT 3 名前,年齢,所属,学年を表示 ayuka.print() ; // ayuka 18 / EI 4 名前,年齢,所属,学年を表示 Person* family[] = { &saitoh , &mitsu , &ayuka , // 配列の中に、Personへのポインタと } ; // Studentへのポインタが混在している // 派生クラスのポインタは、 // 基底クラスのポインタとしても扱える for( int i = 0 ; i < 3 ; i++ ) family[ i ]->print() ; // t-saitoh 55/mitsuki 20/ayuka 18 return 0 ; // が表示される。 } // # "mitsuki 20/KIT 3" とか "ayuka 18/EI 4" // # が表示されてほしい?
class Student ... {
   :
   void print() {
      Person::print() ;                    // 名前と年齢を表示
      printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示
   }
} ;
int main() {
   Person saitoh( "t-saitoh" , 55 ) ;
   saitoh.print() ;                // t-saitoh 55 名前と年齢を表示

   Student mitsu( "mitsuki" , 20 , "KIT" ,  3 ) ;
   Student ayuka( "ayuka" ,   18 , "EI" ,   4 ) ;
   mitsu.print() ;                 // mitsuki 20 / KIT 3  名前,年齢,所属,学年を表示
   ayuka.print() ;                 // ayuka 18   / EI  4  名前,年齢,所属,学年を表示

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

複素数クラスによる演習

複素数クラスの例

隠蔽化と基本的なオブジェクト指向の練習課題として、前回の授業では、直交座標系による複素数クラスを示した。今回の授業では、演習を行うとともに直交座標系を極座標系にクラス内部を変更したことにより、隠蔽化の効果について考えてもらい、第1回レポートとする。

直交座標系

前回の授業で示した直交座標系のクラス。比較対象とするために再掲。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
#include <math.h>
// 直交座標系の複素数クラス
class Complex {
private:
double re ; // 実部
double im ; // 虚部
public:
void print() {
printf( "%lf + j%lf¥n" , re , im ) ;
}
Complex( double r , double i ) // 実部虚部のコンストラクタ
: re( r ) , im( i ) {}
Complex() // デフォルトコンストラクタ
: re( 0.0 ) , im( 0.0 ) {}
void add( Complex z ) {
// 加算は、直交座標系だと極めてシンプル
re = re + z.re ;
im = im + z.im ;
}
void mul( Complex z ) {
// 乗算は、直交座標系だと、ちょっと煩雑
double r = re * z.re - im * z.im ;
double i = re * z.im + im * z.re ;
re = r ;
im = i ;
}
double get_re() {
return re ;
}
double get_im() {
return im ;
}
double get_abs() { // 絶対値
return sqrt( re*re + im*im ) ;
}
double get_arg() { // 偏角
return atan2( im , re ) ;
}
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
int main() {
// 複素数を作る
Complex a( 1.0 , 2.0 ) ;
Complex b( 2.0 , 3.0 ) ;
// 複素数の計算
a.print() ;
a.add( b ) ;
a.print() ;
a.mul( b ) ;
a.print() ;
return 0 ;
}
#include <stdio.h> #include <math.h> // 直交座標系の複素数クラス class Complex { private: double re ; // 実部 double im ; // 虚部 public: void print() { printf( "%lf + j%lf¥n" , re , im ) ; } Complex( double r , double i ) // 実部虚部のコンストラクタ : re( r ) , im( i ) {} Complex() // デフォルトコンストラクタ : re( 0.0 ) , im( 0.0 ) {} void add( Complex z ) { // 加算は、直交座標系だと極めてシンプル re = re + z.re ; im = im + z.im ; } void mul( Complex z ) { // 乗算は、直交座標系だと、ちょっと煩雑 double r = re * z.re - im * z.im ; double i = re * z.im + im * z.re ; re = r ; im = i ; } double get_re() { return re ; } double get_im() { return im ; } double get_abs() { // 絶対値 return sqrt( re*re + im*im ) ; } double get_arg() { // 偏角 return atan2( im , re ) ; } } ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね int main() { // 複素数を作る Complex a( 1.0 , 2.0 ) ; Complex b( 2.0 , 3.0 ) ; // 複素数の計算 a.print() ; a.add( b ) ; a.print() ; a.mul( b ) ; a.print() ; return 0 ; }
#include <stdio.h>
#include <math.h>

// 直交座標系の複素数クラス
class Complex {
private:
   double re ; // 実部
   double im ; // 虚部
public:
   void print() {
      printf( "%lf + j%lf¥n" , re , im ) ;
   }
   Complex( double r , double i ) // 実部虚部のコンストラクタ
     : re( r ) , im( i ) {}
   Complex()                      // デフォルトコンストラクタ
     : re( 0.0 ) , im( 0.0 ) {} 
   void add( Complex z ) {
      // 加算は、直交座標系だと極めてシンプル
      re = re + z.re ;
      im = im + z.im ;
   }
   void mul( Complex z ) {
      // 乗算は、直交座標系だと、ちょっと煩雑
      double r = re * z.re - im * z.im ;
      double i = re * z.im + im * z.re ;
      re = r ;
      im = i ;
   }
   double get_re() {
      return re ;
   }
   double get_im() {
      return im ;
   }
   double get_abs() { // 絶対値
      return sqrt( re*re + im*im ) ;
   }
   double get_arg() { // 偏角
      return atan2( im , re ) ;
   }
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
int main() {
   // 複素数を作る
   Complex a( 1.0 , 2.0 ) ;
   Complex b( 2.0 , 3.0 ) ;

   // 複素数の計算
   a.print() ;
   a.add( b ) ;
   a.print() ;
   a.mul( b ) ;
   a.print() ;

   return 0 ;
}

極座標系

上記の直交座標系の Complex クラスは、加減算の関数は単純だけど、乗除算の関数を書く時には面倒になってくる。この場合、極座標系でプログラムを書いたほうが判りやすいかもしれない。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 局座標系の複素数クラス
class Complex {
private:
double r ; // 絶対値 r
double th ; // 偏角 θ
public:
void print() {
printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ;
}
Complex() // デフォルトコンストラクタ
: r( 0.0 ) , th( 0.0 ) {}
// 表面的には、同じ使い方ができるように
// 直交座標系でのコンストラクタ
Complex( double x , double y ) {
r = sqrt( x*x + y*y ) ;
th = atan2( y , x ) ; // 象限を考慮したatan()
}
// 極座標系だと、わかりやすい処理
void mul( Complex z ) {
// 極座標系での乗算は
r = r * z.r ; // 絶対値の積
th = th + z.th ; // 偏角の和
}
// 反対に、加算は面倒な処理になってしまう。
void add( Complex z ) {
; // 自分で考えて
}
// ゲッターメソッド
double get_abs() {
return r ;
}
double get_arg() {
return th ;
}
double get_re() { // 直交座標系との互換性のためのゲッターメソッド
return r * cos( th ) ;
}
double get_im() {
return r * sin( th ) ;
}
} ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;
// 局座標系の複素数クラス class Complex { private: double r ; // 絶対値 r double th ; // 偏角 θ public: void print() { printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ; } Complex() // デフォルトコンストラクタ : r( 0.0 ) , th( 0.0 ) {} // 表面的には、同じ使い方ができるように // 直交座標系でのコンストラクタ Complex( double x , double y ) { r = sqrt( x*x + y*y ) ; th = atan2( y , x ) ; // 象限を考慮したatan() } // 極座標系だと、わかりやすい処理 void mul( Complex z ) { // 極座標系での乗算は r = r * z.r ; // 絶対値の積 th = th + z.th ; // 偏角の和 } // 反対に、加算は面倒な処理になってしまう。 void add( Complex z ) { ; // 自分で考えて } // ゲッターメソッド double get_abs() { return r ; } double get_arg() { return th ; } double get_re() { // 直交座標系との互換性のためのゲッターメソッド return r * cos( th ) ; } double get_im() { return r * sin( th ) ; } } ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;
// 局座標系の複素数クラス
class Complex {
private:
   double r ;  // 絶対値 r
   double th ; // 偏角   θ
public:
   void print() {
      printf( "%lf ∠ %lf¥n" , r , th / 3.14159265 * 180.0 ) ;
   }
   Complex() // デフォルトコンストラクタ
     : r( 0.0 ) , th( 0.0 ) {}

   // 表面的には、同じ使い方ができるように
   //  直交座標系でのコンストラクタ
   Complex( double x , double y ) {
      r  = sqrt( x*x + y*y ) ;
      th = atan2( y , x ) ;    // 象限を考慮したatan()
   }
   // 極座標系だと、わかりやすい処理
   void mul( Complex z ) {
      // 極座標系での乗算は
      r = r * z.r ;    // 絶対値の積
      th = th + z.th ; // 偏角の和
   } 
   // 反対に、加算は面倒な処理になってしまう。
   void add( Complex z ) {
      ; // 自分で考えて
   }
   // ゲッターメソッド
   double get_abs() {
      return r ;
   }
   double get_arg() {
      return th ;
   }
   double get_re() {  // 直交座標系との互換性のためのゲッターメソッド
      return r * cos( th ) ;
   }
   double get_im() {
      return r * sin( th ) ;
   }
} ; // ←しつこく繰り返すけど、セミコロン忘れないでね(^_^;

このように、プログラムを開発していると、当初は直交座標系でプログラムを記述していたが、途中で極座標系の方がプログラムが書きやすいという局面となるかもしれない。しかし、オブジェクト指向による隠蔽化を正しく行っていれば、利用者に影響なく「データ構造」や「その手続き(メソッド)」を書換えることも可能となる。

このように、プログラムをさらに良いものとなるべく書換えることは、オブジェクト指向ではリファクタリングと呼ぶ。
正しくクラスを作っていれば、クラス利用者への影響が最小にしながらリファクタリングが可能となる。

const 指定 (経験者向け解説)

C++ では、間違って値を書き換えるような処理を書けないようにするための、const 指定の機能がある。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void bar( char* s ) { // void bar( const char* s ) {...}
printf( "%s\n" , s ) ; // で宣言すべき。
}
void foo( const int x ) {
// ~~~~~~~~~~~
x++ ; // 定数を書き換えることはできない。
printf( "%d\n" , x ) ;
}
int main() {
const double pi = 3.141592 ;
// C言語で #define PI 3.141592 と同等
bar( "This is a pen" ) ; // Warning: string constant to 'char*' の警告
int a = 123 ;
foo( a ) ;
return 0 ;
}
void bar( char* s ) { // void bar( const char* s ) {...} printf( "%s\n" , s ) ; // で宣言すべき。 } void foo( const int x ) { // ~~~~~~~~~~~ x++ ; // 定数を書き換えることはできない。 printf( "%d\n" , x ) ; } int main() { const double pi = 3.141592 ; // C言語で #define PI 3.141592 と同等 bar( "This is a pen" ) ; // Warning: string constant to 'char*' の警告 int a = 123 ; foo( a ) ; return 0 ; }
void bar( char* s ) {       // void bar( const char* s ) {...}
   printf( "%s\n" , s ) ;   //  で宣言すべき。
}

void foo( const int x ) {
   //     ~~~~~~~~~~~
   x++ ; // 定数を書き換えることはできない。
   printf( "%d\n" , x ) ;
}
int main() {
   const double pi = 3.141592 ;
   // C言語で #define PI 3.141592 と同等

   bar( "This is a pen" ) ; // Warning: string constant to 'char*' の警告
   int a = 123 ;
   foo( a ) ;

   return 0 ;
}

前述の、getter メソッドの例では要素を参照するだけで、オブジェクトの中身が変化しない。逆に言えば、getter のメソッド内にはオブジェクトに副作用のある処理を書いてはいけない。こういった用途に、オブジェクトを変化させないメソッド宣言がある。先の、get_re() は、

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class ... {
:
inline double get_re() const {
// ~~~~~
re = 0 ; // 文法エラー
return re ;
}
} ;
class ... { : inline double get_re() const { // ~~~~~ re = 0 ; // 文法エラー return re ; } } ;
class ... {
   :
   inline double get_re() const {
               //         ~~~~~
      re = 0 ; // 文法エラー
      return re ;
   }
} ;

クラスオブジェクトを引数にする場合

前述の add() メソッドでは、”void add( Complex z ) { … }” にて宣言をしていた。しかし、引数となる変数 z の実体が巨大な場合、この書き方では値渡しになるため、データの複製の処理時間が問題となる場合がある。この場合は、(書き方1)のように、z の参照渡しにすることで、データ複製の時間を軽減する。また、この例では、引数 z の中身を間違って add() の中で変化させる処理を書いてしまうかもしれない。そこで、この事例では(書き方2)のように const 指定もすべきである。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// (書き方1)
class Complex {
:
void add( Complex& z ) {
re += z.re ;
im += z.im ;
}
} ;
// (書き方2)
class Complex {
:
void add( const Complex& z )
{ // ~~~~~~~~~~~~~~~~
re += z.re ;
im += z.im ;
}
} ;
// (書き方1) class Complex { : void add( Complex& z ) { re += z.re ; im += z.im ; } } ; // (書き方2) class Complex { : void add( const Complex& z ) { // ~~~~~~~~~~~~~~~~ re += z.re ; im += z.im ; } } ;
// (書き方1)
class Complex {
   :
   void add( Complex& z ) {
      re += z.re ;
      im += z.im ;
   }
} ;
// (書き方2)
class Complex {
   :
   void add( const Complex& z )
   {  //     ~~~~~~~~~~~~~~~~
      re += z.re ;
      im += z.im ;
   }
} ;

レポート1(複素数の加減乗除)

授業中に示した、直交座標系・極座標系の複素数のプログラムをベースに、記載されていない減算・除算のプログラムを作成し、レポートを作成する。 レポートには、下記のものを記載すること。

  • プログラムリスト
  • プログラムへの説明
  • 動作確認の結果
  • プログラムより理解できること。
  • 実際にプログラムを書いてみて分かった問題点など…

複素数クラスのプログラム例への質問

授業で扱った複素数クラスのプログラムについて、以下のようなプログラムだと、a.add( b ) を実行すると、a の値が書き換わる。このため、次に a.mul( b ) を実行すると、(3+j5) * (2+j3) を実行する。もっと直感的な結果になるように、a の値が書き換わらないようにできないのか? といった趣旨の質問があった。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Complex {
private:
double re , im ;
public:
Complex( double r , double i ) : re( r ) , im( i ) {}
void print() { printf( "%f+j%f\n" , re , im ) ; }
void add( Complex z ) {
re = re + z.re ;
im = im + z.im ;
}
void mul( Complex z ) {
double r = re * z.re - im * z.im ;
double i = re * z.im + im * z.re ;
re = r ;
im = i ;
}
} ;
int main() {
Complex a( 1 , 2 ) , b( 2 , 3 ) ;
a.add( b ) ; // a = a + b ;
a.print() ; // 3 + j5
a.mul( b ) ; // a = a * b ; ← aの値はすでに3+j5に変わった後
a.print() ; // (6-15) + j(10+9) = -9+j19
}
class Complex { private: double re , im ; public: Complex( double r , double i ) : re( r ) , im( i ) {} void print() { printf( "%f+j%f\n" , re , im ) ; } void add( Complex z ) { re = re + z.re ; im = im + z.im ; } void mul( Complex z ) { double r = re * z.re - im * z.im ; double i = re * z.im + im * z.re ; re = r ; im = i ; } } ; int main() { Complex a( 1 , 2 ) , b( 2 , 3 ) ; a.add( b ) ; // a = a + b ; a.print() ; // 3 + j5 a.mul( b ) ; // a = a * b ; ← aの値はすでに3+j5に変わった後 a.print() ; // (6-15) + j(10+9) = -9+j19 }
class Complex {
private:
    double re , im ;
public:
    Complex( double r , double i ) : re( r ) , im( i ) {}
    void print() { printf( "%f+j%f\n" , re , im ) ; }
    void add( Complex z ) {
        re = re + z.re ;
        im = im + z.im ;
    }
    void mul( Complex z ) {
        double r = re * z.re - im * z.im ;
        double i = re * z.im + im * z.re ;
        re = r ;
        im = i ;
    }
} ;
int main() {
    Complex a( 1 , 2 ) , b( 2 , 3 ) ;
    a.add( b ) ;  // a = a + b ;
    a.print() ;   // 3 + j5
    a.mul( b ) ;  // a = a * b ; ← aの値はすでに3+j5に変わった後
    a.print() ;   // (6-15) + j(10+9) = -9+j19
}

a の値が書き換わらないようにしたいのなら、以下のようなコードになるだろう。

対象オブジェクトを変化させない書き方

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Complex {
:
Complex add( Complex z ) {
return Complex( re + z.re , im + z.im ) ;
}
Complex mul( Complex z ) {
return Complex( re * z.re - im * z.im , // Complex オブジェクトを作って
re * z.im + im * z.re ) ; // 返り値として返す。
}
} ;
int main() {
Complex a( 1 , 2 ) , b( 2 , 3 ) ;
a.add( b ).print() ; // 3+j5
a.mul( b ).print() ; // (2-6)+j(3+4) = -4+j7
}
class Complex { : Complex add( Complex z ) { return Complex( re + z.re , im + z.im ) ; } Complex mul( Complex z ) { return Complex( re * z.re - im * z.im , // Complex オブジェクトを作って re * z.im + im * z.re ) ; // 返り値として返す。 } } ; int main() { Complex a( 1 , 2 ) , b( 2 , 3 ) ; a.add( b ).print() ; // 3+j5 a.mul( b ).print() ; // (2-6)+j(3+4) = -4+j7 }
class Complex {
     :
    Complex add( Complex z ) {
        return Complex( re + z.re , im + z.im ) ;
    }
    Complex mul( Complex z ) {
        return Complex( re * z.re - im * z.im ,    // Complex オブジェクトを作って
                        re * z.im + im * z.re ) ;  //   返り値として返す。
    }
} ;
int main() {
    Complex a( 1 , 2 ) , b( 2 , 3 ) ;
    a.add( b ).print() ;  // 3+j5
    a.mul( b ).print() ;  // (2-6)+j(3+4) = -4+j7
}

ただ、このコードは、add() や mul() が Complex オブジェクトを作って返り値を返すが、その新しいオブジェクトはどのように呼び出し側に返されるのか?誰が廃棄するの? といった点で、単純なC言語の知識だけでは動作を理解しづらいことから、最初のコードにて説明を行った。でも、後者の方が計算結果のイメージは直感的だし、return コンストラクタ(…) の書き方に慣れてしまえば、プログラムも読みやすい!!

const メソッドとオブジェクトの参照渡し

前者のプログラムは、add() により 対象オブジェクトに副作用が発生する。後者は対象オブジェクトは変化しない。メソッドを呼び出す際にも、対象オブジェクトに副作用が発生しないことを明示したconstメソッドとして定義することで、オブジェクトを間違って破壊することから守ることもできる。

また、add() , mul() の引数は void add( Complex z ) {…} のような書き方では値渡しが行われる。つまり、メソッド呼び出し時点で実引数を仮引数にコピーする処理が発生する。このため、処理効率を考えるとポインタ渡し(参照渡し)の方がムダなコピーが発生しない。

一方で、参照渡しを行うと、ポインタを経由して引数に副作用を及ぼすことも可能となるため、参照渡しに const 宣言をつけることで、引数によるオブジェクト破壊を防ぐことができる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Complex {
private:
double re , im ;
public:
Complex( double r , double i ) : re( r ) , im( i ) {}
void print() const { // 表示だけで副作用は発生しない
printf( "%f+j%f\n" , re , im ) ;
}
Complex add( const Complex & z ) const { // 対象オブジェクトは変化しない
// ~~~~~ ~~~ ~~~~~
// zに副作用なし 参照渡 constメソッド
return Complex( re + z.re , im + z.im ) ;
}
Complex mul( const Complex & z ) const {
return Complex( re * z.re - im * z.im ,
re * z.im + im * z.re ) ;
}
} ;
int main() {
Complex a( 1 , 2 ) , b( 2 , 3 ) ;
a.add( b ).print() ; // 3+j5
a.mul( b ).print() ; // (2-6)+j(3+4) = -4+j7
}
class Complex { private: double re , im ; public: Complex( double r , double i ) : re( r ) , im( i ) {} void print() const { // 表示だけで副作用は発生しない printf( "%f+j%f\n" , re , im ) ; } Complex add( const Complex & z ) const { // 対象オブジェクトは変化しない // ~~~~~ ~~~ ~~~~~ // zに副作用なし 参照渡 constメソッド return Complex( re + z.re , im + z.im ) ; } Complex mul( const Complex & z ) const { return Complex( re * z.re - im * z.im , re * z.im + im * z.re ) ; } } ; int main() { Complex a( 1 , 2 ) , b( 2 , 3 ) ; a.add( b ).print() ; // 3+j5 a.mul( b ).print() ; // (2-6)+j(3+4) = -4+j7 }
class Complex {
private:
    double re , im ;
public:
    Complex( double r , double i ) : re( r ) , im( i ) {}
    void print() const {                   // 表示だけで副作用は発生しない
        printf( "%f+j%f\n" , re , im ) ;
    }
    Complex add( const Complex & z ) const {  // 対象オブジェクトは変化しない
        //       ~~~~~        ~~~    ~~~~~
        //       zに副作用なし 参照渡  constメソッド
        return Complex( re + z.re , im + z.im ) ;
    }
    Complex mul( const Complex & z ) const {
        return Complex( re * z.re - im * z.im ,
                        re * z.im + im * z.re ) ;
    }
} ;
int main() {
    Complex a( 1 , 2 ) , b( 2 , 3 ) ;
    a.add( b ).print() ;  // 3+j5
    a.mul( b ).print() ;  // (2-6)+j(3+4) = -4+j7
}

オブジェクト指向の基本プログラム

C++のクラスで表現

前回の講義での、構造体のポインタ渡しをC++の基本的なクラスで記述した場合のプログラムを再掲する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
#include <string.h>
// この部分はクラス設計者が書く
class Person {
private: // クラス外からアクセスできない部分
// データ構造を記述
char name[10] ; // メンバーの宣言
int age ;
public: // クラス外から使える部分
// データに対する処理を記述
void set( char s[] , int a ) { // メソッドの宣言
// pのように対象のオブジェクトを明記する必要はない。
strcpy( name , s ) ;
age = a ;
}
void print() {
printf( "%s %d¥n" , name , age ) ;
}
} ; // ← 注意ここのセミコロンを書き忘れないこと。
// この部分はクラス利用者が書く
int main() {
Person saitoh ;
saitoh.set( "saitoh" , 55 ) ;
saitoh.print() ;
// 文法エラーの例
printf( "%d¥n" , saitoh.age ) ; // phoneはprivateなので参照できない。
return 0 ;
}
#include <stdio.h> #include <string.h> // この部分はクラス設計者が書く class Person { private: // クラス外からアクセスできない部分 // データ構造を記述 char name[10] ; // メンバーの宣言 int age ; public: // クラス外から使える部分 // データに対する処理を記述 void set( char s[] , int a ) { // メソッドの宣言 // pのように対象のオブジェクトを明記する必要はない。 strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d¥n" , name , age ) ; } } ; // ← 注意ここのセミコロンを書き忘れないこと。 // この部分はクラス利用者が書く int main() { Person saitoh ; saitoh.set( "saitoh" , 55 ) ; saitoh.print() ; // 文法エラーの例 printf( "%d¥n" , saitoh.age ) ; // phoneはprivateなので参照できない。 return 0 ; }
#include <stdio.h>
#include <string.h>

// この部分はクラス設計者が書く
class Person {
private: // クラス外からアクセスできない部分
   // データ構造を記述
   char name[10] ; // メンバーの宣言
   int  age ;
public: // クラス外から使える部分
   // データに対する処理を記述
   void set( char s[] , int a ) { // メソッドの宣言
      // pのように対象のオブジェクトを明記する必要はない。
      strcpy( name , s ) ;
      age = a ;
   }
   void print() {
      printf( "%s %d¥n" , name , age ) ;
   }
} ; // ← 注意ここのセミコロンを書き忘れないこと。

// この部分はクラス利用者が書く
int main() {
   Person saitoh ;
   saitoh.set( "saitoh" , 55 ) ;
   saitoh.print() ;

   // 文法エラーの例
   printf( "%d¥n" , saitoh.age ) ; // phoneはprivateなので参照できない。
   return 0 ;
}

この様にC++のプログラムに書き換えたが、内部の処理は元のC言語と同じであり、オブジェクトへの関数呼び出し saitoh.set(…) などが呼び出されても、set() は、オブジェクトのポインタを引数して持つ関数として、機械語が生成されるだけである。

用語の解説:C++のプログラムでは、データ構造とデータの処理を、並行しながら記述する。 データ構造に対する処理は、メソッド(method)と呼ばれる。 データ構造とメソッドを同時に記載したものは、クラス(class)と呼ぶ。 そのデータに対し具体的な値や記憶域が割り当てられたものオブジェクト(object)と呼ぶ。

C++では隠蔽化をさらに明確にするために、private:public: といったアクセス制限を指定できる。private: は、そのメソッドの中でしか使うことができない要素や関数であり、public: は、メソッド以外からでも参照したり呼出したりできる。オブジェクト指向でプログラムを書くとき、データ構造や関数の処理方法は、クラス内部の設計者しか触れないようにしておけば、その内部を改良することができる。しかし、クラスの利用者が勝手に内部データを触っていると、内部設計者が改良するとそのプログラムは動かないものになってしまう。

隠蔽化を的確に行うことで、クラスの利用者はクラスの内部構造を触ることができなくなる。一方でクラス設計者はクラスの外部への挙動が変化しないようにクラス内部を修正することに心がければ、クラス利用者への影響がないままクラスの内部を改良できる。このように利用者への影響を最小に、常にプログラムを修正することリファクタリングと呼ぶ。

クラス限定子

前述のプログラムでは、class 宣言の中に関数内部の処理を記述していた。しかし関数の記述が長い場合は、書ききれないこういう場合はクラス限定子を使って、メソッドの具体的な処理をクラス宣言の外に記載する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
private:
char name[10] ;
int age ;
public:
// メソッドのプロトタイプ宣言
void set( char s[] , int a) ;
void print() ;
} ;
// メソッドの実体をクラス宣言の外に記載する。
void Person::set( char s[] , int a ) { // Person::set()
strcpy( name , s ) ;
age = a ;
}
void Person::print() { // Person::print()
printf( "%s %d¥n" , name , age ) ;
}
class Person { private: char name[10] ; int age ; public: // メソッドのプロトタイプ宣言 void set( char s[] , int a) ; void print() ; } ; // メソッドの実体をクラス宣言の外に記載する。 void Person::set( char s[] , int a ) { // Person::set() strcpy( name , s ) ; age = a ; } void Person::print() { // Person::print() printf( "%s %d¥n" , name , age ) ; }
class Person {
private:
   char name[10] ;
   int  age ;
public:
   // メソッドのプロトタイプ宣言
   void set( char s[] , int a) ;
   void print() ;
} ;

// メソッドの実体をクラス宣言の外に記載する。
void Person::set( char s[] , int a ) {  // Person::set() 
   strcpy( name , s ) ;
   age = a ;
}
void Person::print() {                  // Person::print()
   printf( "%s %d¥n" , name , age ) ;
}

inline 関数と開いたサブルーチン

オブジェクト指向では、きわめて簡単な処理な関数を使うことも多い。
例えば、上記のプログラム例で、クラス利用者に年齢を読み出すことは許しても書き込みをさせたくない場合、以下のような、inline 関数を定義する。(getterメソッド)
# 逆に、値の代入専用のメソッドは、setterメソッドと呼ぶ

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
private:
char name[10] ;
int age ;
public:
// メソッドのプロトタイプ宣言
inline int get_age() { return age ; } // getter
inline void set_age( int a ) { age = a ; } // setter
} ;
class Person { private: char name[10] ; int age ; public: // メソッドのプロトタイプ宣言 inline int get_age() { return age ; } // getter inline void set_age( int a ) { age = a ; } // setter } ;
class Person {
private:
   char name[10] ;
   int  age ;
public:
   // メソッドのプロトタイプ宣言
   inline int get_age() { return age ; } // getter
   inline void set_age( int a ) { age = a ; } // setter
} ;

ここで inline とは、開いた関数(開いたサブルーチン)を作る指定子である。通常、機械語を生成するとき中身を参照するだけの機械語と、get_age() を呼出したときに関数呼び出しを行う機械語が作られる(閉じたサブルーチン)が、age を参照するだけのために関数呼び出しの機械語はムダが多い。inline を指定すると、入り口出口のある関数は生成されず、get_age() の処理にふさわしい age を参照するだけの機械語が生成される。

# 質問:C言語で開いたサブルーチンを使うためにはどういった機能があるか?

コンストラクタとデストラクタ

プログラムを記述する際、データの初期化忘れや終了処理忘れで、プログラムの誤動作の原因になることが多い。

このための機能がコンストラクタ(構築子)とデストラクタ(破壊子)という。

コンストラクタは、返り値を記載しない関数でクラス名(仮引数…)の形式で宣言し、オブジェクトの宣言時に初期化を行う処理として呼び出される。デストラクタは、~クラス名() の形式で宣言し、オブジェクトが不要となる際に、自動的に呼び出し処理が埋め込まれる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
private:
// データ構造を記述
char name[10] ;
int age ;
public:
Person() { // (A) 引数なしのコンストラクタ
name[0] = '<pre class="EnlighterJSRAW" data-enlighter-language="cpp">class Person {
private:
// データ構造を記述
char name[10] ;
int age ;
public:
Person() { // (A) 引数なしのコンストラクタ
name[0] = '\0' ;
age = 0 ;
}
Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ
strcpy( name , s ) ;
age = a ;
}
~Person() { // デストラクタ
print() ;
}
void print() {
printf( "'%s' = %d¥n" , name , age ) ;
}
} ;
int main() {
Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化
Person tomoko ; // 引数なしのコンストラクタで初期化される。
return 0 ;
// main を抜ける時にオブジェクトsaitohは不要になるので、
// デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。
// 同様に tomoko のデストラクタでは、'' = 0 を表示。
}
</pre>' ;
age = 0 ;
}
Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ
strcpy( name , s ) ;
age = a ;
}
~Person() { // デストラクタ
print() ;
}
void print() {
printf( "'%s' = %d¥n" , name , age ) ;
}
} ;
int main() {
Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化
Person tomoko ; // 引数なしのコンストラクタで初期化される。
return 0 ;
// main を抜ける時にオブジェクトsaitohは不要になるので、
// デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。
// 同様に tomoko のデストラクタでは、'' = 0 を表示。
}
class Person { private: // データ構造を記述 char name[10] ; int age ; public: Person() { // (A) 引数なしのコンストラクタ name[0] = '<pre class="EnlighterJSRAW" data-enlighter-language="cpp">class Person { private: // データ構造を記述 char name[10] ; int age ; public: Person() { // (A) 引数なしのコンストラクタ name[0] = '\0' ; age = 0 ; } Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ strcpy( name , s ) ; age = a ; } ~Person() { // デストラクタ print() ; } void print() { printf( "'%s' = %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化 Person tomoko ; // 引数なしのコンストラクタで初期化される。 return 0 ; // main を抜ける時にオブジェクトsaitohは不要になるので、 // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。 // 同様に tomoko のデストラクタでは、'' = 0 を表示。 } </pre>' ; age = 0 ; } Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ strcpy( name , s ) ; age = a ; } ~Person() { // デストラクタ print() ; } void print() { printf( "'%s' = %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化 Person tomoko ; // 引数なしのコンストラクタで初期化される。 return 0 ; // main を抜ける時にオブジェクトsaitohは不要になるので、 // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。 // 同様に tomoko のデストラクタでは、'' = 0 を表示。 }
class Person {
private:
   // データ構造を記述
   char name[10] ;
   int  age ;
public:
   Person() { // (A) 引数なしのコンストラクタ
      name[0] = '
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
private:
// データ構造を記述
char name[10] ;
int age ;
public:
Person() { // (A) 引数なしのコンストラクタ
name[0] = '\0' ;
age = 0 ;
}
Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ
strcpy( name , s ) ;
age = a ;
}
~Person() { // デストラクタ
print() ;
}
void print() {
printf( "'%s' = %d¥n" , name , age ) ;
}
} ;
int main() {
Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化
Person tomoko ; // 引数なしのコンストラクタで初期化される。
return 0 ;
// main を抜ける時にオブジェクトsaitohは不要になるので、
// デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。
// 同様に tomoko のデストラクタでは、'' = 0 を表示。
}
class Person { private: // データ構造を記述 char name[10] ; int age ; public: Person() { // (A) 引数なしのコンストラクタ name[0] = '\0' ; age = 0 ; } Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ strcpy( name , s ) ; age = a ; } ~Person() { // デストラクタ print() ; } void print() { printf( "'%s' = %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化 Person tomoko ; // 引数なしのコンストラクタで初期化される。 return 0 ; // main を抜ける時にオブジェクトsaitohは不要になるので、 // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。 // 同様に tomoko のデストラクタでは、'' = 0 を表示。 }
class Person {
private:
   // データ構造を記述
   char name[10] ;
   int  age ;
public:
   Person() { // (A) 引数なしのコンストラクタ
      name[0] = '\0' ;
      age = 0 ;
   }
   Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ
      strcpy( name , s ) ;
      age = a ;
   }
   ~Person() { // デストラクタ
      print() ;
   }
   void print() {
      printf( "'%s' = %d¥n" , name , age ) ;
   }
} ;

int main() {
   Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化
   Person tomoko ;  // 引数なしのコンストラクタで初期化される。
   return 0 ;
   // main を抜ける時にオブジェクトsaitohは不要になるので、
   // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。
   // 同様に tomoko のデストラクタでは、'' = 0 を表示。
}
' ; age = 0 ; } Person( char s[] , int a ) { // (B) 引数ありのコンストラクタ strcpy( name , s ) ; age = a ; } ~Person() { // デストラクタ print() ; } void print() { printf( "'%s' = %d¥n" , name , age ) ; } } ; int main() { Person saitoh( "saitoh" , 55 ) ; // オブジェクトsaitohを"saitoh"と55で初期化 Person tomoko ; // 引数なしのコンストラクタで初期化される。 return 0 ; // main を抜ける時にオブジェクトsaitohは不要になるので、 // デストラクタが自動的に呼び出され、'saitoh' = 55 が表示。 // 同様に tomoko のデストラクタでは、'' = 0 を表示。 }

このクラスの中には、(A)引数無しのコンストラクタと、(B)引数ありのコンストラクタが出てくる。C++では、同じ名前の関数でも引数の数や型に応じて呼出す関数を適切に選んでくれる。(関数のオーバーロード)

デストラクタは、データが不要となった時に自動的に呼び出してくれる関数で、一般的にはC言語でのファイルの fopen() , fclose() のようなものを使う処理で、コンストラクタで fopen() , デストラクタで fclose() を呼出すように使うことが多いだろう。同じように、コンストラクタで malloc() を呼出し、デストラクタで free() を呼出すというのが定番の使い方だろう。

複素数クラスの例

隠蔽化と基本的なオブジェクト指向の練習課題として、複素数クラスをあげる。ここでは、複素数の加算・乗算を例に説明をするので、減算・除算などの処理を記述することで、クラスの扱いに慣れてもらう。

直交座標系の複素数クラス

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
#include <math.h>
// 直交座標系の複素数クラス
class Complex {
private:
double re ; // 実部
double im ; // 虚部
public:
void print() {
printf( "%lf + j%lf¥n" , re , im ) ;
}
Complex( double r , double i ) // コンストラクタで要素の
: re( r ) , im( i ) { // 初期化はこのように書いてもいい
} // re = r ; im = i ; の意味
Complex() // デフォルトコンストラクタ
: re( 0.0 ) , im( 0.0 ) {
}
void add( Complex z ) {
// 加算は、直交座標系だと極めてシンプル
re = re + z.re ;
im = im + z.im ;
}
void mul( Complex z ) {
// 乗算は、直交座標系だと、ちょっと煩雑
double r = re * z.re - im * z.im ;
double i = re * z.im + im * z.re ;
re = r ;
im = i ;
}
double get_re() {
return re ;
}
double get_im() {
return im ;
}
double get_abs() { // 絶対値
return sqrt( re*re + im*im ) ;
}
double get_arg() { // 偏角
return atan2( im , re ) ;
}
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
int main() {
// 複素数を作る
Complex a( 1.0 , 2.0 ) ;
Complex b( 2.0 , 3.0 ) ;
// 複素数の計算
a.print() ;
a.add( b ) ;
a.print() ;
a.mul( b ) ;
a.print() ;
return 0 ;
}
#include <stdio.h> #include <math.h> // 直交座標系の複素数クラス class Complex { private: double re ; // 実部 double im ; // 虚部 public: void print() { printf( "%lf + j%lf¥n" , re , im ) ; } Complex( double r , double i ) // コンストラクタで要素の : re( r ) , im( i ) { // 初期化はこのように書いてもいい } // re = r ; im = i ; の意味 Complex() // デフォルトコンストラクタ : re( 0.0 ) , im( 0.0 ) { } void add( Complex z ) { // 加算は、直交座標系だと極めてシンプル re = re + z.re ; im = im + z.im ; } void mul( Complex z ) { // 乗算は、直交座標系だと、ちょっと煩雑 double r = re * z.re - im * z.im ; double i = re * z.im + im * z.re ; re = r ; im = i ; } double get_re() { return re ; } double get_im() { return im ; } double get_abs() { // 絶対値 return sqrt( re*re + im*im ) ; } double get_arg() { // 偏角 return atan2( im , re ) ; } } ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね int main() { // 複素数を作る Complex a( 1.0 , 2.0 ) ; Complex b( 2.0 , 3.0 ) ; // 複素数の計算 a.print() ; a.add( b ) ; a.print() ; a.mul( b ) ; a.print() ; return 0 ; }
#include <stdio.h>
#include <math.h>

// 直交座標系の複素数クラス
class Complex {
private:
   double re ; // 実部
   double im ; // 虚部
public:
   void print() {
      printf( "%lf + j%lf¥n" , re , im ) ;
   }
   Complex( double r , double i )  // コンストラクタで要素の
     : re( r ) , im( i ) {         //  初期化はこのように書いてもいい
   }                               // re = r ; im = i ; の意味
   Complex()      // デフォルトコンストラクタ
     : re( 0.0 ) , im( 0.0 ) {
   }

   void add( Complex z ) {
      // 加算は、直交座標系だと極めてシンプル
      re = re + z.re ;
      im = im + z.im ;
   }
   void mul( Complex z ) {
      // 乗算は、直交座標系だと、ちょっと煩雑
      double r = re * z.re - im * z.im ;
      double i = re * z.im + im * z.re ;
      re = r ;
      im = i ;
   }
   double get_re() {
      return re ;
   }
   double get_im() {
      return im ;
   }
   double get_abs() { // 絶対値
      return sqrt( re*re + im*im ) ;
   }
   double get_arg() { // 偏角
      return atan2( im , re ) ;
   }
} ; // ←何度も繰り返すけど、ここのセミコロン忘れないでね
int main() {
   // 複素数を作る
   Complex a( 1.0 , 2.0 ) ;
   Complex b( 2.0 , 3.0 ) ;

   // 複素数の計算
   a.print() ;
   a.add( b ) ;
   a.print() ;
   a.mul( b ) ;
   a.print() ;

   return 0 ;
}

練習課題

  • 上記の直交座標系の複素数のクラスのプログラムを入力し、動作を確認せよ。
  • このプログラムに減算や除算の処理を追加せよ。

この練習課題は、次週に予定している「曲座標系の複素数クラス」に変更となった場合のプログラムを加え、第1回のレポート課題となります。

構造体とオブジェクト指向

初回の講義では、欠席者もいたので前回の講義資料も扱いながら説明を行う。最初に、前回講義資料の値渡し・参照渡し・ポインタ渡しを復習してから、構造体の話につなげていく。

構造体

上記資料を元に説明。 最初に構造体が無かったら、名前・国語・算数・理科の1クラス分のデータをどう表現しますか?

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// まずは基本の宣言
char name[ 50 ][ 20 ] ;
int kokugo[ 50 ] ;
int sansu[ 50 ] ;
int rika[ 50 ] ;
// もしクラスが最初20人だったら、20→50に変更する際に、
// 文字列長の20も書きなおしちゃうかも。
// 50とか20とかマジックナンバーは使わないほうがいい。
#define SIZE 50
#define LEN 20
char name[ SIZE ][ LEN ] ;
int kokugo[ SIZE ] ;
:
// 2クラス分のデータ(例えばEI科とE科)を保存したかったら?
// case-1(配列2倍にしちゃえ)
char name[ 100 ][ 20 ] ; // どこからがEI科?
int kokugo[ 100 ] ;
:
// case-2(2次元配列にしちゃえ)
char name[ 2 ][ 50 ][ 20 ] ; // 0,1どっちがEI科?
int kokugo[ 2 ][ 50 ] ;
:
// case-3(目的に応じた名前の変数を作っちゃえ)
char ei_name[ 50 ][ 20 ] ; // EI科は一目瞭然
int ei_kokugo[ 50 ] ; // だけど変数名が違うから
: // 処理を2度書き
char ee_name[ 50 ][ 20 ] ;
int ee_kokugo[ 50 ] ;
:
// まずは基本の宣言 char name[ 50 ][ 20 ] ; int kokugo[ 50 ] ; int sansu[ 50 ] ; int rika[ 50 ] ; // もしクラスが最初20人だったら、20→50に変更する際に、 // 文字列長の20も書きなおしちゃうかも。 // 50とか20とかマジックナンバーは使わないほうがいい。 #define SIZE 50 #define LEN 20 char name[ SIZE ][ LEN ] ; int kokugo[ SIZE ] ; : // 2クラス分のデータ(例えばEI科とE科)を保存したかったら? // case-1(配列2倍にしちゃえ) char name[ 100 ][ 20 ] ; // どこからがEI科? int kokugo[ 100 ] ; : // case-2(2次元配列にしちゃえ) char name[ 2 ][ 50 ][ 20 ] ; // 0,1どっちがEI科? int kokugo[ 2 ][ 50 ] ; : // case-3(目的に応じた名前の変数を作っちゃえ) char ei_name[ 50 ][ 20 ] ; // EI科は一目瞭然 int ei_kokugo[ 50 ] ; // だけど変数名が違うから : // 処理を2度書き char ee_name[ 50 ][ 20 ] ; int ee_kokugo[ 50 ] ; :
// まずは基本の宣言
char name[ 50 ][ 20 ] ;
int  kokugo[ 50 ] ;
int  sansu[ 50 ] ;
int  rika[ 50 ] ;
// もしクラスが最初20人だったら、20→50に変更する際に、
// 文字列長の20も書きなおしちゃうかも。
// 50とか20とかマジックナンバーは使わないほうがいい。
#define SIZE 50
#define LEN 20
char name[ SIZE ][ LEN ] ;
int  kokugo[ SIZE ] ;
:
// 2クラス分のデータ(例えばEI科とE科)を保存したかったら?
// case-1(配列2倍にしちゃえ)
char name[ 100 ][ 20 ] ;  // どこからがEI科?
int  kokugo[ 100 ] ;
:

// case-2(2次元配列にしちゃえ)
char name[ 2 ][ 50 ][ 20 ] ; // 0,1どっちがEI科?
int  kokugo[ 2 ][ 50 ] ;
:

// case-3(目的に応じた名前の変数を作っちゃえ)
char ei_name[ 50 ][ 20 ] ; // EI科は一目瞭然
int  ei_kokugo[ 50 ] ;     // だけど変数名が違うから
:                      // 処理を2度書き
char ee_name[ 50 ][ 20 ] ;
int  ee_kokugo[ 50 ] ;
:

このような問題に対応するために構造体を用いる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
struct Person { // Personが構造体名(タグ名)
char name[ 20 ] ;
int kokugo ;
int sansu ;
int rika ;
} ;
struct Person saitoh ;
struct Person ei[ 50 ] , ee[ 40 ] ;
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.kokugo = 100 ;
ei[ 0 ].sansu = 80 ;
ee[ 1 ].rika = 75 ;
struct Person { // Personが構造体名(タグ名) char name[ 20 ] ; int kokugo ; int sansu ; int rika ; } ; struct Person saitoh ; struct Person ei[ 50 ] , ee[ 40 ] ; strcpy( saitoh.name , "t-saitoh" ) ; saitoh.kokugo = 100 ; ei[ 0 ].sansu = 80 ; ee[ 1 ].rika = 75 ;
struct Person {  // Personが構造体名(タグ名)
   char name[ 20 ] ;
   int  kokugo ;
   int  sansu ;
   int  rika ;
} ;
struct Person saitoh ;
struct Person ei[ 50 ] , ee[ 40 ] ;
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.kokugo = 100 ;
ei[ 0 ].sansu = 80 ;
ee[ 1 ].rika = 75 ;

このように構造体を使うことで、複数のデータを1つのデータの塊として扱えるようになる。

構造体の参照渡し

構造体のデータを関数の呼び出しで記述する場合には、参照渡しを利用する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
struct Person {
char name[ 20 ] ;
int age ;
} ;
void print( struct Person* p ) {
printf( "%s %d¥n" , p->name , p->age ) ;
}
void main() {
struct Person saitoh ;
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.age = 50 ;
print( &saitoh ) ; // ポインタによる参照渡し
}
struct Person { char name[ 20 ] ; int age ; } ; void print( struct Person* p ) { printf( "%s %d¥n" , p->name , p->age ) ; } void main() { struct Person saitoh ; strcpy( saitoh.name , "t-saitoh" ) ; saitoh.age = 50 ; print( &saitoh ) ; // ポインタによる参照渡し }
struct Person {
   char name[ 20 ] ;
   int  age ;
} ;
void print( struct Person* p ) {
   printf( "%s %d¥n" , p->name , p->age ) ;
}
void main() {
   struct Person saitoh ;
   strcpy( saitoh.name , "t-saitoh" ) ;
   saitoh.age = 50 ;
   print( &saitoh ) ;  // ポインタによる参照渡し
}

このようなプログラムの書き方をすると、「データ saitoh に、print() せよ…」 といった処理を記述したようになる。 これを発展して、データ saitoh に、print という命令をするイメージにも見える。

この考え方を、そのままプログラムに反映させ、Personというデータは、 名前と年齢、データを表示するprintは…といったように、 データ構造と、そのデータ構造への処理をペアで記述すると分かりやすい。

オブジェクト指向の導入

構造体でオブジェクト指向もどき

例えば、名前と年齢の構造体で処理を記述する場合、 以下の様な記載を行うことで、データ設計者データ利用者で分けて 仕事ができることを説明。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// この部分はデータ構造の設計者が書く
// データ構造を記述
struct Person {
char name[10] ;
int age ;
} ;
// データに対する処理を記述
void setPerson( struct Person* p , char s[] , int a ) {
// ポインタの参照で表記
strcpy( (*p).name , s ) ;
(*p).age = a ;
}
void printPerson( struct Person* p ) {
// アロー演算子で表記 "(*p).name" は "p->name" で書ける
printf( "%s %d¥n" ,
p->name , p->age ) ;
}
// この部分は、データ利用者が書く
int main() {
// Personの中身を知らなくてもいいから配列を定義(データ隠蔽)
struct Person saitoh ;
setPerson( &saitoh , "saitoh" , 55 ) ;
struct Person table[ 10 ] ; // 初期化は記述を省略
for( int i = 0 ; i < 10 ; i++ ) {
// 出力する...という雰囲気で書ける(手続き隠蔽)
printPerson( &table[i] ) ;
}
return 0 ;
}
// この部分はデータ構造の設計者が書く // データ構造を記述 struct Person { char name[10] ; int age ; } ; // データに対する処理を記述 void setPerson( struct Person* p , char s[] , int a ) { // ポインタの参照で表記 strcpy( (*p).name , s ) ; (*p).age = a ; } void printPerson( struct Person* p ) { // アロー演算子で表記 "(*p).name" は "p->name" で書ける printf( "%s %d¥n" , p->name , p->age ) ; } // この部分は、データ利用者が書く int main() { // Personの中身を知らなくてもいいから配列を定義(データ隠蔽) struct Person saitoh ; setPerson( &saitoh , "saitoh" , 55 ) ; struct Person table[ 10 ] ; // 初期化は記述を省略 for( int i = 0 ; i < 10 ; i++ ) { // 出力する...という雰囲気で書ける(手続き隠蔽) printPerson( &table[i] ) ; } return 0 ; }
// この部分はデータ構造の設計者が書く
// データ構造を記述
struct Person {
   char name[10] ;
   int  age ;
} ;
// データに対する処理を記述
void setPerson( struct Person* p , char s[] , int a ) {
   // ポインタの参照で表記
   strcpy( (*p).name , s ) ;
   (*p).age = a ;
}
void printPerson( struct Person* p ) {
   // アロー演算子で表記 "(*p).name" は "p->name" で書ける
   printf( "%s %d¥n" ,
           p->name , p->age ) ;
}
// この部分は、データ利用者が書く
int main() {
   // Personの中身を知らなくてもいいから配列を定義(データ隠蔽)
   struct Person saitoh ;
   setPerson( &saitoh , "saitoh" , 55 ) ;

   struct Person table[ 10 ] ; // 初期化は記述を省略
   for( int i = 0 ; i < 10 ; i++ ) {
      // 出力する...という雰囲気で書ける(手続き隠蔽)
      printPerson( &table[i] ) ;
   }
   return 0 ;
}

このプログラムの書き方では、mainの中を読むだけもで、 データ初期化とデータ出力を行うことはある程度理解できる。 この時、データ構造の中身を知らなくてもプログラムが理解でき、 データ実装者はプログラムを記述できる。これをデータ構造の隠蔽化という。 一方、setPerson()や、printPerson()という関数の中身についても、 初期化・出力の方法をどうするのか知らなくても、 関数名から動作は推測できプログラムも書ける。 これを手続きの隠蔽化という。

C++のクラスで表現

上記のプログラムをそのままC++に書き直すと以下のようになる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
#include <string.h>
// この部分はクラス設計者が書く
class Person {
private: // クラス外からアクセスできない部分
// データ構造を記述
char name[10] ; // メンバーの宣言
int age ;
public: // クラス外から使える部分
// データに対する処理を記述
void set( char s[] , int a ) { // メソッドの宣言
// pのように対象のオブジェクトを明記する必要はない。
strcpy( name , s ) ;
age = a ;
}
void print() {
printf( "%s %d¥n" , name , age ) ;
}
} ; // ← 注意ここのセミコロンを書き忘れないこと。
// この部分はクラス利用者が書く
int main() {
Person saitoh ;
saitoh.set( "saitoh" , 55 ) ;
saitoh.print() ;
// 文法エラーの例
printf( "%d¥n" , saitoh.age ) ; // age は private なので参照できない。
return 0 ;
}
#include <stdio.h> #include <string.h> // この部分はクラス設計者が書く class Person { private: // クラス外からアクセスできない部分 // データ構造を記述 char name[10] ; // メンバーの宣言 int age ; public: // クラス外から使える部分 // データに対する処理を記述 void set( char s[] , int a ) { // メソッドの宣言 // pのように対象のオブジェクトを明記する必要はない。 strcpy( name , s ) ; age = a ; } void print() { printf( "%s %d¥n" , name , age ) ; } } ; // ← 注意ここのセミコロンを書き忘れないこと。 // この部分はクラス利用者が書く int main() { Person saitoh ; saitoh.set( "saitoh" , 55 ) ; saitoh.print() ; // 文法エラーの例 printf( "%d¥n" , saitoh.age ) ; // age は private なので参照できない。 return 0 ; }
#include <stdio.h>
#include <string.h>

// この部分はクラス設計者が書く
class Person {
private: // クラス外からアクセスできない部分
   // データ構造を記述
   char name[10] ; // メンバーの宣言
   int  age ;
public: // クラス外から使える部分
   // データに対する処理を記述
   void set( char s[] , int a ) { // メソッドの宣言
      // pのように対象のオブジェクトを明記する必要はない。
      strcpy( name , s ) ;
      age = a ;
   }
   void print() {
      printf( "%s %d¥n" , name , age ) ;
   }
} ; // ← 注意ここのセミコロンを書き忘れないこと。

// この部分はクラス利用者が書く
int main() {
   Person saitoh ;
   saitoh.set( "saitoh" , 55 ) ;
   saitoh.print() ;

   // 文法エラーの例
   printf( "%d¥n" , saitoh.age ) ; // age は private なので参照できない。
   return 0 ;
}

用語の解説:C++のプログラムでは、データ構造データの処理を、並行しながら記述する。 データ構造に対する処理は、メソッド(method)と呼ばれる。 データ構造とメソッドを同時に記載したものは、クラス(class)と呼ぶ。 そのclassに対し、具体的な値や記憶域が割り当てられたものをオブジェクト(object)と呼ぶ。

オブジェクト指向/2022/ガイダンス

専攻科2年のオブジェクト指向プログラミングの授業の1回目。

最近のプログラミングの基本となっているオブジェクト指向について、その機能についてC++言語を用いて説明し、後半では対象(オブジェクト)をモデル化して設計するための考え方(UML)について説明する。

評価は、3つの課題と最終テストを各25%づつで評価を行う。

オブジェクト指向プログラミングの歴史

最初のプログラム言語のFortran(科学技術計算向け言語)の頃は、処理を記述するだけだったけど、 COBOL(商用計算向け言語)ができた頃には、データをひとまとめで扱う「構造体」(C言語ならstruct {…}の考えができた。(データの構造化)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// C言語の構造体
struct Person { // 1人分のデータ構造をPersonとする
char name[ 20 ] ; // 名前
int b_year, b_month, b_day ; // 誕生日
} ;
// C言語の構造体 struct Person { // 1人分のデータ構造をPersonとする char name[ 20 ] ; // 名前 int b_year, b_month, b_day ; // 誕生日 } ;
// C言語の構造体
struct Person { // 1人分のデータ構造をPersonとする
   char name[ 20 ] ;             // 名前
   int  b_year, b_month, b_day ; // 誕生日
} ;

一方、初期のFortranでは、プログラムの処理順序は、繰り返し処理も if 文と goto 文で記載し、処理がわかりにくかった。その後のALGOLの頃には、処理をブロック化して扱うスタイル(C言語なら{ 文 … }の複文で 記述する方法ができてきた。(処理の構造化)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// ブロックの考えがない時代の雰囲気をC言語で表すと
int i = 0 ;
LOOP: if ( i >= 10 ) goto EXIT ;
if ( i % 2 != 0 ) goto NEXT ;
printf( "%d " , i ) ;
NEXT: i++ ;
goto LOOP ; // 処理の範囲を字下げ(インデント)で強調
EXIT:
---------------------------------------------------
// C 言語で書けば
int i ;
for( i = 0 ; i < 10 ; i++ ) {
if ( i % 2 == 0 ) {
printf( "%d¥n" , i ) ;
}
}
---------------------------------------------------
! 構造化文法のFORTRANで書くと
integer i
do i = 0 , 9
if ( mod( i , 2 ) == 0 ) then
print * , i
end if
end do
// ブロックの考えがない時代の雰囲気をC言語で表すと int i = 0 ; LOOP: if ( i >= 10 ) goto EXIT ; if ( i % 2 != 0 ) goto NEXT ; printf( "%d " , i ) ; NEXT: i++ ; goto LOOP ; // 処理の範囲を字下げ(インデント)で強調 EXIT: --------------------------------------------------- // C 言語で書けば int i ; for( i = 0 ; i < 10 ; i++ ) { if ( i % 2 == 0 ) { printf( "%d¥n" , i ) ; } } --------------------------------------------------- ! 構造化文法のFORTRANで書くと integer i do i = 0 , 9 if ( mod( i , 2 ) == 0 ) then print * , i end if end do
      // ブロックの考えがない時代の雰囲気をC言語で表すと
      int i = 0 ;
LOOP: if ( i >= 10 ) goto EXIT ;
         if ( i % 2 != 0 ) goto NEXT ;
            printf( "%d " , i ) ;
NEXT:    i++ ;
      goto LOOP ;   // 処理の範囲を字下げ(インデント)で強調
EXIT:
--------------------------------------------------- 
      // C 言語で書けば
      int i ;
      for( i = 0 ; i < 10 ; i++ ) {
         if ( i % 2 == 0 ) {
            printf( "%d¥n" , i ) ;
         }
      }
---------------------------------------------------
      ! 構造化文法のFORTRANで書くと
      integer i
      do i = 0 , 9
        if ( mod( i , 2 ) == 0 ) then
          print * , i
        end if
      end do

このデータの構造化・処理の構造化により、プログラムの分かりやすさは向上し、このデータと処理をブロック化した書き方は「構造化プログラミング(Structured Programming)」 と呼ばれる。

雑談

ここで紹介した、最古の高級言語 Fortran や COBOL は、今でも使われている。Fortran は、スーパーコンピュータなどで行われる数値シミュレーションでは、広く利用されている。また COBOL は、銀行などのシステムでもまだ使われている。しかしながら、新システムへの移行で COBOL を使えるプログラマーが定年を迎え減っていることから、移行トラブルが発生している。特に、CASEツール(UMLなどの図をベースにしたデータからプログラムを自動生成するツール)によって得られた COBOL のコードが移行を妨げる原因となることもある。

この後、様々なプログラム言語が開発され、C言語などもできてきた。 一方で、シミュレーションのプログラム開発(例simula)では、 シミュレーション対象(object)に対して、命令するスタイルの書き方が生まれ、 データに対して命令するという点で、擬人法のようなイメージで直感的にも分かりやすかった。 これがオブジェクト指向プログラミング(Object Oriented Programming)の始まりとなる。略記するときは OOP などと書くことが多い。

この考え方を導入した言語の1つが Smalltalk であり、この環境では、プログラムのエディタも Smalltalk で記述したりして、オブジェクト指向がGUIのプログラムと親和性が良いことから、この考え方は多くのプログラム言語へと取り入れられていく。

C言語にこのオブジェクト指向を取り入れ、C++が開発される。さらに、この文法をベースとした、 Javaなどが開発されている。最近の新しい言語では、どれもオブジェクト指向の考えが使われている。

この授業の中ではオブジェクト指向プログラミングにおける、隠蔽化, 派生と継承, 仮想関数 などの概念を説明する。

構造体の導入

C++でのオブジェクト指向は、C言語の構造体の表記がベースになっているので、まずは構造体の説明。詳細な配布資料を以下に示す。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 構造体の宣言
struct Person { // Personが構造体につけた名前
char name[ 20 ] ; // 要素1
int phone ; // 要素2
} ; // 構造体定義とデータ構造宣言を
// 別に書く時は「;」の書き忘れに注意
// 構造体変数の宣言
struct Person saitoh ;
struct Person data[ 10 ] ;
// 実際にデータを参照 構造体変数.要素名
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.phone = 272925 ;
for( int i = 0 ; i < 10 ; i++ ) {
scanf( "%d%s" , data[ i ].name , &(data[ i ].phone) ) ;
}
// 構造体の宣言 struct Person { // Personが構造体につけた名前 char name[ 20 ] ; // 要素1 int phone ; // 要素2 } ; // 構造体定義とデータ構造宣言を // 別に書く時は「;」の書き忘れに注意 // 構造体変数の宣言 struct Person saitoh ; struct Person data[ 10 ] ; // 実際にデータを参照 構造体変数.要素名 strcpy( saitoh.name , "t-saitoh" ) ; saitoh.phone = 272925 ; for( int i = 0 ; i < 10 ; i++ ) { scanf( "%d%s" , data[ i ].name , &(data[ i ].phone) ) ; }
// 構造体の宣言
struct Person {      // Personが構造体につけた名前
   char name[ 20 ] ; // 要素1
   int  phone ;      // 要素2
} ;                  // 構造体定義とデータ構造宣言を
                     // 別に書く時は「;」の書き忘れに注意
// 構造体変数の宣言
struct Person saitoh ;
struct Person data[ 10 ] ;
// 実際にデータを参照 構造体変数.要素名
strcpy( saitoh.name , "t-saitoh" ) ;
saitoh.phone = 272925 ;
for( int i = 0 ; i < 10 ; i++ ) {
   scanf( "%d%s" , data[ i ].name , &(data[ i ].phone) ) ;
}

構造体に慣れていない人のための課題

  • 以下に、C言語の構造体を使った基本的なプログラムを示す。このプログラムでは、国語,算数,理科の3科目と名前の5人分のデータより、各人の平均点を計算している。このプログラムを動かし、以下の機能を追加せよ。レポートには プログラムリストと動作結果の分かる結果を付けること。
    • 国語の最低点の人を探し、名前を表示する処理。
    • 算数の平均点を求める処理。
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#include <stdio.h>
struct Student {
char name[ 20 ] ;
int kokugo ;
int sansu ;
int rika ;
} ;
struct Student table[5] = {
// name , kokugo , sansu , rika
{ "Aoyama" , 56 , 95 , 83 } ,
{ "Kondoh" , 78 , 80 , 64 } ,
{ "Saitoh" , 42 , 78 , 88 } ,
{ "Sakamoto" , 85 , 90 , 36 } ,
{ "Yamagosi" ,100 , 72 , 65 } ,
} ;
int main() {
int i = 0 ;
for( i = 0 ; i < 5 ; i++ ) {
double sum = table[i].kokugo + table[i].sansu + table[i].rika ;
printf( "%-10.10s %3d %3d %3d %6.2lf\n" ,
table[i].name , table[i].kokugo , table[i].sansu , table[i].rika ,
sum / 3.0 ) ;
}
return 0 ;
}
#include <stdio.h> struct Student { char name[ 20 ] ; int kokugo ; int sansu ; int rika ; } ; struct Student table[5] = { // name , kokugo , sansu , rika { "Aoyama" , 56 , 95 , 83 } , { "Kondoh" , 78 , 80 , 64 } , { "Saitoh" , 42 , 78 , 88 } , { "Sakamoto" , 85 , 90 , 36 } , { "Yamagosi" ,100 , 72 , 65 } , } ; int main() { int i = 0 ; for( i = 0 ; i < 5 ; i++ ) { double sum = table[i].kokugo + table[i].sansu + table[i].rika ; printf( "%-10.10s %3d %3d %3d %6.2lf\n" , table[i].name , table[i].kokugo , table[i].sansu , table[i].rika , sum / 3.0 ) ; } return 0 ; }
#include <stdio.h>

struct Student {
  char name[ 20 ] ;
  int  kokugo ;
  int  sansu ;
  int  rika ;
} ;

struct Student table[5] = {
  // name ,      kokugo , sansu , rika                                          
  { "Aoyama" ,   56 ,     95 ,    83 } ,
  { "Kondoh" ,   78 ,     80 ,    64 } ,
  { "Saitoh" ,   42 ,     78 ,    88 } ,
  { "Sakamoto" , 85 ,     90 ,    36 } ,
  { "Yamagosi" ,100 ,     72 ,    65 } ,
} ;

int main() {
  int i = 0 ;
  for( i = 0 ; i < 5 ; i++ ) {
    double sum = table[i].kokugo + table[i].sansu + table[i].rika ;
    printf( "%-10.10s %3d %3d %3d %6.2lf\n" ,
            table[i].name , table[i].kokugo , table[i].sansu , table[i].rika ,
            sum / 3.0 ) ;
  }
  return 0 ;
}

値渡し,ポインタ渡し,参照渡し

C言語をあまりやっていない学科の人向けのC言語の基礎として、関数との値渡し, ポインタ渡しについて説明する。ただし、参照渡しについては電子情報の授業でも細かく扱っていない内容なので電子情報系学生も要注意。

オブジェクト指向のプログラムでは、構造体のポインタ渡し(というよりは参照渡し)を多用するが、その基本となる関数との値の受け渡しの理解のため、以下に値渡し・ポインタ渡し・参照渡しについて説明する。

ポインタと引数

値渡し(Call by value)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 値渡しのプログラム
void foo( int x ) { // x は局所変数(仮引数は呼出時に
// 対応する実引数で初期化される。
x++ ;
printf( "%d¥n" , x ) ;
}
int main() {
int a = 123 ;
foo( a ) ; // 124
// 処理後も main::a は 123 のまま。
foo( a ) ; // 124
return 0 ;
}
// 値渡しのプログラム void foo( int x ) { // x は局所変数(仮引数は呼出時に // 対応する実引数で初期化される。 x++ ; printf( "%d¥n" , x ) ; } int main() { int a = 123 ; foo( a ) ; // 124 // 処理後も main::a は 123 のまま。 foo( a ) ; // 124 return 0 ; }
// 値渡しのプログラム
void foo( int x ) {  // x は局所変数(仮引数は呼出時に
                     // 対応する実引数で初期化される。
   x++ ;
   printf( "%d¥n" , x ) ;
}
int main() {
   int a = 123 ;
   foo( a ) ;  // 124
               // 処理後も main::a は 123 のまま。
   foo( a ) ;  // 124
   return 0 ;
}

このプログラムでは、aの値は変化せずに、124,124 が表示される。ここで、関数 foo() を呼び出しても、関数に「値」が渡されるだけで、foo() を呼び出す際の実引数 a の値は変化しない。こういった関数に値だけを渡すメカニズムは「値渡し」と呼ぶ。

値渡しだけが使われれば、関数の処理後に変数に影響が残らない。こういった処理の影響が残らないことは一般的に「副作用がない」という。

大域変数を使ったプログラム

でも、プログラムによっては、124,125 と変化して欲しい場合もある。どのように記述すべきだろうか?

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 大域変数を使う場合
int x ;
void foo() {
x++ ;
printf( "%d¥n" , x ) ;
}
int main() {
x = 123 ;
foo() ; // 124
foo() ; // 125
return 0 ;
}
// 大域変数を使う場合 int x ; void foo() { x++ ; printf( "%d¥n" , x ) ; } int main() { x = 123 ; foo() ; // 124 foo() ; // 125 return 0 ; }
// 大域変数を使う場合
int x ;
void foo() {
   x++ ;
   printf( "%d¥n" , x ) ;
}
int main() {
   x = 123 ;
   foo() ;  // 124
   foo() ;  // 125
   return 0 ;
}

しかし、このプログラムは大域変数を使うために、間違いを引き起こしやすい。大域変数はどこでも使える変数であり、副作用が発生して間違ったプログラムを作る原因になりやすい。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 大域変数が原因で予想外の挙動をしめす簡単な例
int i ;
void foo() {
for( i = 0 ; i < 2 ; i++ )
printf( "A" ) ;
}
int main() {
for( i = 0 ; i < 3 ; i++ ) // このプログラムでは、AA AA AA と
foo() ; // 表示されない。
return 0 ;
}
// 大域変数が原因で予想外の挙動をしめす簡単な例 int i ; void foo() { for( i = 0 ; i < 2 ; i++ ) printf( "A" ) ; } int main() { for( i = 0 ; i < 3 ; i++ ) // このプログラムでは、AA AA AA と foo() ; // 表示されない。 return 0 ; }
// 大域変数が原因で予想外の挙動をしめす簡単な例
int i ;
void foo() {
   for( i = 0 ; i < 2 ; i++ )
      printf( "A" ) ;
}
int main() {
   for( i = 0 ; i < 3 ; i++ )  // このプログラムでは、AA AA AA と
      foo() ;                   // 表示されない。
   return 0 ;
}

ポインタ渡し(Call by pointer)

C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。(副作用の及ぶ範囲を限定する) こういった、値の受け渡し方法は「ポインタ渡し」と呼ぶ。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// ポインタ渡しのプログラム
void foo( int* p ) { // p はポインタ
(*p)++ ;
printf( "%d¥n" , *p ) ;
}
int main() {
int a = 123 ;
foo( &a ) ; // 124
// 処理後 main::a は 124 に増えている。
foo( &a ) ; // 124
return 0 ; // さらに125と増える
}
// ポインタ渡しのプログラム void foo( int* p ) { // p はポインタ (*p)++ ; printf( "%d¥n" , *p ) ; } int main() { int a = 123 ; foo( &a ) ; // 124 // 処理後 main::a は 124 に増えている。 foo( &a ) ; // 124 return 0 ; // さらに125と増える }
// ポインタ渡しのプログラム
void foo( int* p ) {  // p はポインタ
   (*p)++ ;
   printf( "%d¥n" , *p ) ;
}
int main() {
   int a = 123 ;
   foo( &a ) ;  // 124
                // 処理後 main::a は 124 に増えている。
   foo( &a ) ;  // 124
   return 0 ;   // さらに125と増える
}

ポインタを利用して引数に副作用を与える方法は、ポインタを正しく理解していないプログラマーでは、危険な操作となる。

参照渡し(Call by reference)

C++では、ポインタ渡しを極力使わないようにするために、参照渡しを利用する。ただし、ポインタ渡しも参照渡しも、機械語レベルでは同じ処理にすぎない。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// ポインタ渡しのプログラム
void foo( int& x ) { // xは参照
x++ ;
printf( "%d¥n" , x ) ;
}
int main() {
int a = 123 ;
foo( a ) ; // 124
// 処理後 main::a は 124 に増えている。
foo( a ) ; // 124
return 0 ; // さらに125と増える。
}
// ポインタ渡しのプログラム void foo( int& x ) { // xは参照 x++ ; printf( "%d¥n" , x ) ; } int main() { int a = 123 ; foo( a ) ; // 124 // 処理後 main::a は 124 に増えている。 foo( a ) ; // 124 return 0 ; // さらに125と増える。 }
// ポインタ渡しのプログラム
void foo( int& x ) {  // xは参照
   x++ ;
   printf( "%d¥n" , x ) ;
}
int main() {
   int a = 123 ;
   foo( a ) ;  // 124
               // 処理後 main::a は 124 に増えている。
   foo( a ) ;  // 124
   return 0 ;  // さらに125と増える。
}

大きなプログラムを作る場合、副作用のあるプログラムの書き方は、間違ったプログラムの原因となりやすい。そこで関数の呼び出しを中心としてプログラムを書くものとして、関数型プログラミングがある。

2021年度授業アンケート

年度末恒例の授業アンケート結果の総まとめ

情報制御基礎(3年学際科目)

3年の他学科学生も受講する学際科目なので、内容には苦労するけど、ポイントとしては良い値。授業資料をできれば印刷配布してほしいとの意見もあったけど、講義資料やテスト過去問題などもWeb公開しているので、事前に各自対応としたい。

情報構造論(4EI)

若干ポイントも昨年度より上がっているけど、アンケート回答数をみると未回答の人もある程度いるようで、まじめに授業を受けた人がまじめにアンケートに回答してくれたことの影響が多いと思う。マニアックなネタを興味もってくれたり、既に習っている内容がアルゴリズムとしてどういう意味を持っているのかの理解になったとの積極的な意見があってよかった。

データベース(5EI)

こちらもポイントは若干上がっているけど、アンケート回答数からすれば、まじめな授業参加者のおかげかな。他の授業も含め、全講義資料のWeb公開、テスト過去問題の公開に加え、SQLの演習環境をWebで学内向け公開なども実施しているし、好印象を持ってもらえたと思う。

オブジェクト指向プログラミング(専攻科生産システム2年)

昨年度の92.6ポイントより、88.9ポイントと若干低下しているが、概ね良い評価であり、今後も授業の内容を時節に応じて変化させながら実施していきたい。

オブジェクト指向プログラミング2021全講義録

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー