ホーム » スタッフ » 斉藤徹 » 講義録 » オブジェクト指向 » 構造体からクラスの導入

2025年4月
 12345
6789101112
13141516171819
20212223242526
27282930  

検索・リンク

構造体からクラスの導入

構造体の参照渡し

構造体のデータを関数で受け渡しをする場合は、参照渡しを利用する。

struct Person {
   char name[ 20 ] ;
   int  age ;
} ;
void print( struct Person* this ) {
   printf( "%s %d¥n" , this->name , this->age ) ;
}
void main() {
   struct Person saitoh ;
   strcpy( saitoh.name , "t-saitoh" ) ;
   saitoh.age = 50 ;
   print( &saitoh ) ;  // ポインタによる参照渡し
}

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

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


オブジェクト指向の導入

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

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

// この部分はデータ構造の設計者が書く
// データ構造を記述
struct Person {
   char name[10] ;
   int  age ;
} ;
// データに対する処理を記述
void setPerson( struct Person* this , char s[] , int a ) {
   // ポインタの参照で表記
   strcpy( (*this).name , s ) ;
   (*this).age = a ;
}
void printPerson( struct Person* this ) {
   // アロー演算子で表記 "(*this).name" は "this->name" で書ける
   printf( "%s %d¥n" ,
           this->name , this->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++に書き直すと以下のようになる。

#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 ) ;  // メソッドは隠れた引数としてオブジェクトへのポインタ this がある。
      age = a ;             // このため strcpy( this->name , s ) ; , this->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)と呼ぶ。

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

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

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

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

クラス限定子

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

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メソッドと呼ぶ

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言語で開いたサブルーチンを使うためにはどういった機能があるか?

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

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

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

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

class Person {
private:
   // データ構造を記述
   char name[10] ;
   int  age ;
public:
   Person() { // (A) 引数なしのコンストラクタ
      name[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() を呼出すというのが定番の使い方だろう。