Lecture 12 オブジェクト指向

いままで学んできた変数,配列などの多くの機能は, C++以前のすべてのプログラミング言語に備わってきたものです. しかし,プログラムが巨大になり,複雑になり,人命を預かるクリティカルなものに 使われるようになるにつれ,今までの技術や概念では不十分になり, より優れた仕組みが必要になってきました.
こうして取り入れられた新しい機能が「クラス」で, オブジェクト指向プログラミング(Object-Oriented Programing (OOP))の中核をなす技術です.

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

これまでのC言語などの非オブジェクト指向言語でのプログラミングは,「手続き型」と呼ばれ, バラバラに定義されたデータを,色々な関数などの「手続き」が処理していくイメージでした. しかし,この方法では,プログラムが大規模になって処理が複雑になるにつれ,安全なプログラムが 作れないことがわかってきました. そこで,データと,その「データを直接操作するための関数」をセットにした「オブジェクト」を 作ることで安全なプログラムを作る手法が考え出されました.





カプセル化の実際

以下のコードを実際にタイプしてみて,カプセル化を体験しよう. C++でのstructは,「全てがpublicなclass」です.実はインタフェース関数も作ることができますが, すべてがpublicなので意味はありませんね.
struct PersonalData {
    int age;
};

int main(void)
{
    PersonalData data;   // クラス PersonalData のオブジェクト(変数) data を生成

    data.age = -100;     //  年齢 age に,どんな不正な値でも設定ができてしまう.
    ...

    return 0;
}

一方,以下はクラスを使ってデータ隠蔽(カプセル化)したコードです.
//  データ隠蔽の例.
#include <stdio.h>

//  クラス
class CPersonalData {
  private:
    int age;
};

int main(void)
{
    CPersonalData data;  // クラス PersonalData のオブジェクト(変数の実体) data を生成

    data.age = -100;     //  不正な値の設定ができる???

    return 0;
}

練習

  • 上のコードをそれぞれコンパイルしてみよう.
  • privateをpublicに変更してみよう.
  • class キーワードを struct に変更してみよう.


カプセル化の例

以下はカプセル化の例です.
//  データ隠蔽の例.このままだとコンパイルエラーが出ます.
#include <stdio.h>
#include <string.h>

//  クラス
class CPersonalData {
  private:
    char name[100];
    int age;

  public:
    // メンバ関数
    void set(char new_name[], int n) {
        // 名前をセット
        strcpy(name, new_name);

        // 年齢をセット(エラー処理付き)
        if(n < 0 || 150 < n) {     // 不正な値をチェック
            printf("age is wrong!! \n");
            age = 0;
        } else {
            age = n;
        }
    }

    void disp(void) {
        printf("My name is %s, %d years old.\n", name, age);
    }
};

int main(void) {
    CPersonalData data;        // クラス PersonalData のオブジェクト(変数の実体) data を生成

    data.age = -100;          //  (1) メンバ変数にアクセス.コンパイルエラー

    data.set("Taro", -20);     //  (2) メンバ関数 set() による値チェックを呼び出す.
    data.disp();              //  データを表示.メンバ関数 disp() を呼び出す.

    return 0;
}

練習問題

  • 上記2つのソースファイルは,エラーが出る.内容を確認しよう.
  • main中のdata.age=... をコメントアウト(または削除)してコンパイル,実行してみよう.
  • main中のdata.set(...) の第2引数を,正数にして実行結果を確認してみよう.


クラス宣言とメンバ関数を分けて書く例

#include <stdio.h>
#include <string.h>

//  クラス
class CPersonalData {
  private:
    char name[100];
    int age;

  public:
    // メンバ関数のプロトタイプ宣言のみ
    void set(char new_name[], int n);
    void disp(void);
};


// メンバ関数の定義.::はスコープ解決演算子
void CPersonalData::set(char new_name[], int n) {
    // 名前をセット
    strcpy(name, new_name);

    // 年齢をセット(エラー処理付き)
    if(n < 0 || 150 < n) {     // 不正な値をチェック
        printf("age is wrong!! \n");
        age = 0;
    } else {
        age = n;
    }
}

void CPersonalData::disp(void) {
    printf("My name is %s, %d years old.\n", name, age);
}


int main(void) {
    CPersonalData data;        // クラス PersonalData のオブジェクト(変数の実体) data を生成

    data.set("Taro", -20);     //  (2) メンバ関数 set() による値チェックを呼び出す.
    data.disp();              //  データを表示.メンバ関数 disp() を呼び出す.

    return 0;
}


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

クラスを表す実体(=変数)が宣言される際に,メンバ変数の値を予め設定しておきたい. (正確にはクラスの「インスタンスの生成」と呼ぶ) そのために,クラスには特に明示的に呼ばなくても自動的に呼ばれる関数がある. これをコンストラクタという. この逆に,クラスの実体が破棄されるときに自動的に呼ばれる関数を「デストラクタ」と呼ぶ.
  • コンストラクタの名前は,クラス名と同一
  • デストラクタの名前は,クラス名に「~」をつけたもの.
  • コンストラクタ,デストラクタには戻り値はない.
と決められている.
// コンストラクタとデストラクタのテスト
#include <stdio.h>

//  クラスの定義
class CTestClass {
  private:
    //  メンバ変数
    int data;

  public:
    // コンストラクタ
    CTestClass() {
       printf("CTestClass() : Constructor was called.\n");
       // メンバ変数の初期化などを行う
       data = 100;
    }

    // デストラクタ
    ~CTestClass() {
       printf("~CTestClass() : Destructor was called.\n");
       // 終了処理を行う
    }

    // 通常のメンバ関数
    void SayHello(void) {
       printf("Hello !!! data=%d \n", data);
    }
};

int main(void)
{
    CTestClass a;        // クラス CTestData オブジェクト(変数の実体)を生成
                         // この時点でコンストラクタが自動で呼ばれる

    a.SayHello();        // メンバ関数の呼び出し

    return 0;            // main関数から脱出する時点=ローカル変数が消える時点でデストラクタが自動で呼ばれる
}


OOPの実際

クラスを扱うときには,現実の世界に存在する「モノ」の概念に着目します. 例えば「車」といった「モノ」に着目し,プログラムを設計していきます.

まず,以下のコードを見て下さい.このコードは,普通のCで書かれたコードです. stateというクルマの状態を表す変数があり,これを関数が処理していきます. 処理の手順に重点が置かれている,ということから,「手続き型」プログラムと呼ばれます.
// car0.c
//
// 車をモデル化したプログラム〜その0 手続き型
// とりあえず,止まっているか,走っているか,だけの状態を持つ
//
#include <stdio.h>

void run(int& state) {
    printf("run\n");
    state=1;
}

void stop(int& state) {
    printf("stop\n");
    state=0;
}

void show(int& state) {
    printf("state:%d\n", state);
}

int main(void)
{
    int state = 0;   // クルマの「内部」状態を表す変数 state

    show(state);     // 表示
    run(state);      // 走るように「内部」状態を変更
    show(state);     // 内部状態なのに外から丸見え.書き換えもできちゃう.
    stop(state);     // つまり,メモリ破壊も簡単に出来てしまう.怖っ!
    show(state);
}
これ↑は,みなさんが見慣れた書き方だと思いますが,大きな欠点があります. クルマの状態を表す stateという変数が,処理を行う関数とは独立になっていて, それぞれの関数の中で stateがどのように扱われてしまうのかが不透明になりやすい ことです.また,クルマの交通シミュレーションのように,たくさんのクルマを 扱おうとすると,stateが大きな配列などになって,ここのクルマの特性などを 個別に管理しにくくなってしまいます.

クラスオブジェクト化

では次に下のコードを見て下さい.これは「1台の」クルマの状態を管理する, Carクラスを作り,その中に,そのクルマだけの状態を表す state変数を 入れ込んだ形になっています.
// car1.cpp
//
// 車をモデル化したプログラム〜その1
// とりあえず,止まっているか,走っているか,だけの状態を持つ
//
#include <stdio.h>

class Car {      // クルマをモデル化するクラスの定義
  private:       // この宣言以降は外から保護される(見えない)
    int state;   // 内部状態を保持する変数(1:走ってる,0:止まってる)
  public:        // この宣言以降は外から見える
    Car(){ state=0; } // コンストラクタ.オブジェクトを作るときに(自動的に)呼ばれる
    void run(void);   // 走らせる.以下,メンバ(インタフェース)関数
    void stop(void);  // 止める
    void show(void);  // 内部状態を調べる
};

void Car::run(void) { // Carクラスのrun()メンバ関数の中身.
    printf("run\n");
    state=1;
}

void Car::stop(void) {
    printf("stop\n");
    state=0;
}

void Car::show(void) {
    printf("state:%d\n", state);
}

int main(void)
{
    Car mycar;    // Carオブジェクト(クラスインスタンスという)mycar を作る.

    mycar.show(); // オブジェクトにメッセージ show() を送る.
    mycar.run();  // ここでは内部状態変数 state には一切触れていないことに着目!
    mycar.show();
    mycar.stop();
    mycar.show();
}

機能追加〜有限の燃料

実際のクルマは無限に走れるわけではありませんよね.ガソリンなり,充電なり,エネルギーが必要です. 従って,エネルギー残量の概念を導入してみよう.
// car2.cpp
//
// 車をモデル化したプログラム〜その2
// 上の car1.cpp に有限の燃料の概念を付加
//
#include <stdio.h>

class Car {
    int state;
    int gas;	// 燃料の残量
  public:
    Car(void){ state=0; gas=0; }
    void run(void);
    void stop(void);
    void show(void);
    void charge(int g=10);
};

void Car::run(void) {
    printf("run\n");
    if(gas>0) {		// 燃料があれば走る
        state=1;	// 走行状態に
        gas--;		// 走ったので燃料が減る
    } else {
        state=0;	// 燃料がなければ停止状態に
    }
}

void Car::stop(void) {
    printf("stop\n");
    state=0;
}

void Car::show(void) {
    printf("state:%d, gas=%d\n", state, gas);
}

void Car::charge(int g) {
    printf("charge\n");
    gas+=g;
}

int main(void)
{
    Car mycar;
    mycar.show();
    mycar.run();
    mycar.show();


    mycar.charge(3);
    mycar.show();

    mycar.run();
    mycar.show();
    mycar.stop();
    mycar.show();
    mycar.run();
    mycar.show();
    mycar.run();
    mycar.show();
    mycar.run();
    mycar.show();
}

さらに機能追加〜

今度はクルマの数を増やしてみよう.
// car3.cpp
//
// 車をモデル化したプログラム〜その2
// 上の car2.cpp で複数の車に対応させた
//
#include <stdio.h>
#include <stdlib.h>

class Car {
    int state;
    int gas;
    int id;		// クルマの識別番号
  public:
    Car(void){ state=0; gas=0; id=0; }
    void setid(int _id){id=_id;}
    void run(void);
    void charge(int g=10);
    void drive(void);
};

void Car::run(void) {
    printf("[%02d] run, ",id);
    if(gas>0) {
        state=1;
        gas--;
        printf("gas level: %d\n",gas);
    } else {
        state=0;
        printf("gas empty. stopped!!\n");
    }
}

void Car::charge(int g) {
    gas+=g;
    printf("[%02d] charged. gas level: %d\n",id,gas);
}

void Car::drive(void) {    // 運転シミュレーション
    if(rand()%2) {         // 50%の確率で
        run();             //   走ろうとする
    } else {               // 他方の50%の確率で
        if(gas<1)          //   燃料のチェックをし,無ければ
            charge(3);     //   チャージする
    }
}

int main(void)
{
    const int NCARS = 100;  // 100台のクルマ!
    const int NSTEP = 100;  // それぞれが動くステップ数
    Car mycar[NCARS];       // 100個のインスタンス(100台のクルマ)が作られて,
                            // それぞれのコンストラクタが(トータルで100回)呼ばれる

    for(int i=0; i<NCARS; i++) {
        mycar[i].setid(i);  // それぞれの識別番号をつける
        mycar[i].charge(3); // 燃料を入れておく
    }

    for(int i=0; i<NSTEP*NCARS; i++) {
        int car_id = rand()%NCARS;  // 乱数で決めたクルマに対して
        mycar[car_id].drive();      // 走行シミュレーションを実施する
    }
}


Quiz 12

    1. 実数で身長 height と体重 weight を管理するクラスを作成し,以下のメンバ関数を作成(=メソッドを実装)せよ.
      • 身長と体重を設定する関数 void set(float h, float w);
      • 身長と体重を画面に表示する関数 void disp(void);
      • BMI(下記を参照)を計算して返す関数 float GetBMI(void);
      • BMIに応じて,肥満度を文字で画面に表示する関数 void Inspection(void);.Underweight, Normal rangeなど表示する.
      BMI(Body Mass Index)については, WHO Body mass index - BMI や, 日本肥満学会のWebページなどを参照のこと. 計算の際,単位に注意!

      実行例:(結果の値が正しいとは限らない)
      height(cm)? = 170                 (KBDから入力)
      weight(kg)? = 70                  (KBDから入力)
      height = 170 cm, weight = 70 kg   (disp()関数の呼び出し)
      BMI = 24.2                        (GetBMI()関数の戻り値をmain関数で表示)
      Normal weight                     (Inspection()関数の呼び出し)
      

    2. 2次方程式の実数係数 a, b, c をメンバ変数とするクラスを作成せよ. メンバ関数として,
      • 係数を設定する関数 void set(float _a, float _b, float _c);
      • 方程式を表示する関数 void disp(void);
      • 判別式の値を返す関数 float det(void);
      • 2つの解を計算して,画面に表示する関数 void solv_disp(void);,可能であれば虚数解にも対応
      を実装せよ. 平方根の計算には,math.h内の sqrt() 関数を使う.これは実数専用であり,引数に負数を渡すとエラーとなる.

      実行例:(結果の数値は正しいとは限らない)
      a b c =? 1 3 5              (KBDから入力)
      1.0000x^2 + 3.0000x + 5.0000 = 0    (disp()関数の呼び出し)
      D = -11.00000                       (det()関数の戻り値をmain関数で表示)
      Ans = 1.000 + 0.2000i , 1.000 - 0.2.000i   (solv_disp()関数の呼び出し)
      


Assignment(課題)12

課題の提出は Online Judge にて行います.

  • 提出期限があります.できるだけ早めにやりましょう.
  • Online Judge は普通の課題提出方法とは異なり,良い点が取れるまで何度でも挑戦できます.良い点が取れるまで頑張ってやろう.
  • わからないことは,早めに Teams で聞こう.問題は自分で解決すること.
  • それでもどうしてもわからないときは,毎週月曜日の2時限目にZoomによるオフィスアワーを用意しています. ZoomのリンクはOh-o! meijiで確認してください.
  • Online Judge のページはこちら