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

概要
プログラミング手法において,現在主流であるオブジェクト指向プログラミング(以下ではOOPと表記する)について学習する.ここで用いるプログラミング言語はC言語のオブジェクト指向拡張であるC++である.なお,C言語と共通の制御構文等については本章では取り扱わないので,適宜C言語の資料等を参照すること.
目次

構造体からクラスへ

オブジェクト指向において,最も重要な概念がクラスである.

クラスとは,あるデータとそのデータの処理を一つにまとめたものである.

直感的には,関数を持つことが出来るようになった構造体と考えてもらっても始めのうちは構わない.

以下ではOOPの例として,2次元座標を考えてみよう.

この問題を扱うため,2次元座標をC言語での構造体SPoint2Dによる表現から,C++言語によるクラスCPoint2Dに変更する際に必要となるC++の表記方法の違いを見ていく.

構造体での表現

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  // 2次元座標を表す構造体
  struct SPoint2D {
    double x;
    double y;
  };
  void add(SPoint2D a, SPoint2D b) {
    a.x += b.x;
    a.y += b.y;
  }
  int main() {
    SPoint2D a, b;
    a.x = 0.0; a.y = 1.0;
    b.x = 2.0; b.y = 3.0;
    // 座標aに座標bを加算
    add(a, b);
    return 0;
  }

クラスでの表現

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  // 2次元座標を表すクラス
  class CPoint2D {
  public:
    // コンストラクタ
    CPoint2D(double x, double y) {
      _x = x;
      _y = y;
    }

    // デストラクタ
    ~CPoint2D() {}
    // メンバ関数
    double x() { return _x; }
    double y() { return _y; }
    void add(CPoint2D p) {
    _x += p.x();
    _y += p.y();
    }

  private:
    // メンバ変数
  double _x;
  double _y;
  };
  int main() {
    CPoint2D a(0.0, 1.0);
    CPoint2D b(2.0, 3.0);
    // 座標aに座標bを加算
    a.add(b);
    return 0;
  }

もちろん,これを見ただけでは何をいっているのかわからないかも知れない. しかし,構造体での表現に比べ,クラスでの表現はプログラムの行数がかなり長くなっているということは最低限わかるだろう. これは,OOPに伴う後述のカプセル化やアクセス制御子によるところが大きい.

また,構造体では2次元座標を表す構造体の定義と,それを処理するための関数addの定義がバラバラになってしまうが,クラス表現ではそれをひとまとまりに出来ていることも読み取れる. 以下では,上記の例をもとにして,OOPの解説を行なっていく.

最低限知っておきたいOOP用語

OOPで必要となる新しい概念はたくさんあるが,以下の用語は最低限押さえておきたい.

クラス・インスタンス

クラスとは,機械製品でいうところの設計図や鋳型に値する. そして,その設計図や鋳型をもとに作成された個々の製品がインスタンスだ. 上の例では,CPoint2Dがクラス,変数a,bがCPoint2Dのインスタンスである. では,オブジェクト指向でいうところのオブジェクトとは何か?ということになる. 明確に定義はされていないが,ここでいうオブジェクトとはつまりクラスを指していると考えて不都合ないだろう.

メンバ変数・メンバ関数

上でも述べたが,OOPの神髄は,オブジェクトを「データとそのデータの処理する関数を一つにまとめたもの」として解釈することにある. ここでいうデータがメンバ変数を表し,データを処理する関数がメンバ関数を表す.

これにより,外部からはデータ(メンバ変数)へのアクセスがメンバ関数を介してしか行なえなくなり,ユーザ側の可読性と設計者側の安全性・隠蔽性を向上することが期待できる.

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

コンストラクタは初期化処理,デストラクタは終了処理を行なう特別なメンバ関数で,いつでもクラス名がそのまま関数名に使われる.

初期化とは,C言語で

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  int a = 0;

といった具合に変数を初期化することと同様に,インスタンスのメンバ変数を初期化するために用いられ,以下のようにインスタンス作成時に呼び出される.

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  CPoint2D p(0.0, 1.0);
  int a(0); // 値型についても,()記法で初期化することが可能

もう一つのデストラクタでは,インスタンス自身が解放されたときに,確保していたメンバ変数のメモリ解放やファイルのクローズなど,使わなくなった資源の解放処理を記述するために用いられる. このとき,引数などは指定できない.

OOPの3大概念

今までの部分は,C言語でもある程度同じようにコーディングすることは可能だと考える人もいるだろう. なぜなら,メンバ関数も関数ポインタを構造体が持てば,クラス表記を真似ることが出来るからだ.

しかし,以下の概念を理解することで,OOPの本当の特性,メリットを知ることが出来るようになる.

カプセル化

カプセル化とは,クラスのメンバ変数,メンバ関数へのアクセスを制御することを意味する. これにより,クラス間の独立性を高めることが可能となる.

また,アクセスレベルには以下の3つのレベルが用意されており,必要に応じて選択する必要がある.

  • public: どのクラスからもアクセス可能
  • protected: 自分自身もしくは自分自身を継承する子クラスのみアクセス可能
  • private: 自分自身のみアクセス可能

2次元座標の例ではコンストラクタとメンバ変数はpublic宣言されているので,どのクラスからもアクセス出来るが,メンバ変数はprivate宣言されているため,外部からは直接アクセスすることは出来ない. これにより,座標値の変更はメンバ関数addを通してしか行なえなくなる.

protected宣言については,次節で述べる.

継承

継承を行なうことにより,既存のクラス(親クラス,基底クラス,スーパークラス)の定義の全てを受け継いで,新しいクラス(子クラス,派生クラス,サブクラス)を定義することが可能となる.

例として,2次元座標クラスCPoint2Dを継承し,3次元座標クラスCPoint3Dを定義することを考える.

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  // 2次元座標を表すクラス
  class CPoint2D {
  public:
    // この部分は前回と同じ
  protected:
    // メンバ変数
  double _x;
  double _y;
  };
  // 3次元座標を表すクラス
  class CPoint3D : public CPoint2D {
  public:
    // コンストラクタ
    CPoint3D(double x, double y, double z) {
   _x = x;
   _y = y;
   _z = z;
    }
    // デストラクタ
    ~CPoint3D() {}
    // メンバ関数
  double z() { return _z; }
    void add(CPoint3D p) {
  _x += p.x(); // CPoint2D参照
  _y += p.y(); // CPoint2D参照
  _z += p.z();
    }
  protected:
    // メンバ変数
  double _z;
  };

C++では継承の表記方法として,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  class ChildClass : public ParentClass {}

という表記で継承を表現する.

ここで,ParentClassの前に書かれているpublic宣言の意味に関しては,OOP入門の範囲を超えるので,ここでは説明を割愛する. ここでは便宜上,CPoint2Dのメンバ変数のアクセス制御子をprivateからprotectedに変更している. この宣言の効果により,親クラスCPoint3Dのメンバ変数_x,_yが子クラスCPoint3Dやその孫クラスでも利用可能となる. 本当はもう少しスマートな書き方があるが,とりあえずは構わない. もちろん,public宣言されていたメンバ関数x()やy(),add(CPoint2D)も継承しているので,CPoint3Dでも利用可能だ.

ポリモーフィズム

ここで,メンバ関数addが親クラスと子クラスで重複して定義されていることに気づいた人もいるかも知れない. これを可能にするのが,最後の概念であるポリモーフィズム(多様性)だ.

ポリモーフィズムは,以下の機構によって実現されている.

オーバーライド

親クラスで定義したメンバ関数の定義を子クラスで上書きする. オーバーライドの仕組みを実現する機構で,親クラスでは定義のみを行ない,実装に関しては子クラスに任せることで,実行時に動的に適切な関数を呼び出すことが出来る.

例えば,何かの図形を表す親クラスShapeが描画を行なうメンバ関数drawを持っていたとしよう. 特定の図形を表す子クラスCircle,Square,Triangleではdrawをオーバーライドする.

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  // 図形を表す抽象クラス
  class Shape {
  public:
    virtual void draw() {}
    // virtual void draw() = 0; ← この場合,純粋仮想化仮数でもOK
  };
  class Circle {
  public:
    void draw() { /* 円の描画 */ }
  };
  class Square {
  public:
    void draw() { /* 四角形の描画 */ }
  };
  class Triangle {
  public:
    void draw() { /* 三角形の描画 */ }
  };
  int main() {
    Shape* shapes[3];
    shapes[0] = new Circle();
    shapes[1] = new Square();
    shapes[2] = new Triangle();
  for (std::size_t i = 0; i < 3; i++)
      shapes[i]->draw();
    return 0;
  }

メイン関数の部分で新しいC++記法が記述されているが,それを無視して見れば,新しいものはShapeクラスのvirtualという宣言だろう. C++では,このvirtualによる仮想化宣言によって,子クラスによるオーバーライドを許可する.

発展的内容

今回はOOPの入門であるので,OOPとして絶対知っておきたい内容は網羅したつもりだ. しかし,ここでは紹介しなかったC++のより詳細なコーディング方法を知れば,よりOOPの汎用性が理解できるはずである.

ここではその内容について,少し取り上げてみよう. 詳しい内容については,各自で調べてみること.

オーバーロード

シグネチャ(引数と返り値)の違う,同じ名前の関数を複数定義でき,実行時に適切な関数を自動的に呼び出してくれる.

この機能を使うことにより,C言語では数学ライブラリmath.hで絶対値を返す関数absは,整数と小数によって,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  int abs(int value);
  float fabs(float value);

といった接頭語を付けたような関数名の命名を強いられていたが,C++ではオーバーロードを用いることで,

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  int abs(int value);
  float abs(float value);

というように,全く同じ関数名を命名することが可能となった.

演算子オーバーロード

C++では,関数と同じように演算子もオーバーロードすることが可能である. 演算子は四則演算のみに限らず,[]や()などほぼありとあらゆる演算子が対象となる. プログラミング中に一番よく出くわす標準入出力の記号"<<",">>"も実は演算子オーバーロードによって実現している.

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  std::ofstream file("output.txt");
  std::stringstream ss("");
  std::cout << "Hello World!" << std::endl; // 標準出力
  file << "Hello World!" << std::endl; // ファイル出力
  ss << "Hello World!" << std::endl; // 文字列出力

テンプレート

関数がオーバーロードによって,多種多様なシグニチャの同名関数を定義できることはわかった. しかし,別段コーディングの苦労が減った訳ではない. このシグニチャの異なる関数,あるいはクラスを自動的に作る機構がテンプレートである. 余力があれば積極的に使っていきたい技術だ.

以下のテンプレート関数swapはどんなオブジェクトでも第1引数と第2引数の中身を入れ替えることが可能だ.

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  template <class T> void swap(T& x, T& y) {
    T temp = x;
    x = y;
    y = temp;
  }
  int main() {
    int a = 0, b = 1;
    std::string c = "foo", d = "bar";
    swap(a, b);
    swap(c, d);
    // swap(a, c); ← コンパイルエラー
    return 0;
  }

STL,boostライブラリ

コーディングする上で,ぜひとも避けたいのが車輪の再発明だ. そこで役立つのがライブラリの存在である.

ここで紹介するのはC++の標準ライブラリであるSTLと,C++の次期バージョンで一部が標準ライブラリに採用されるboostである. どちらも上記のテンプレートを巧みに用いることで,汎用的に利用することが出来る.

STLでこれから特に利用していきたいのが,std::vectorとstd::listである. C++では配列を使う代わりに,これらを用いる方が使い勝手がよい. もちろん,スタックやキュー,ハッシュテーブルなどの主なデータ構造も既に定義されている. boostでは,マルチスレッド処理やディレクトリ操作などOS依存の部分が存在するプログラムにおいても,OSに依存することなくコーディング出来るようになる.

詳しくはテンプレートとSTL を参考に.

名前空間

何かの問題を解くクラスとしてSolverというクラスを定義しようとしたとき,しかし,Solverという名前はごく一般的であるため,既に誰かによって使われているかも知れない. これまでは,使用しているライブラリの中で同じ名前のクラスがないかを調べる必要があった. この問題を解決するのが名前空間という考え方である.実際,C++でも標準ライブラリ群はstdという名前空間を割り当てている. これがわかれば,今までおまじないのように書いていた

length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1360.
length() used on @lines (did you mean "scalar(@lines)"?) at /usr/bin/code2html line 1370.
  using namespace std;

の意味が分かるようになるだろう.

例外

指定したファイルが見つからない,あるいは0で除算してしまったなどの異常状態から復帰する機構として例外と呼ばれるプログラミング記法がある. ただ,C++では例外は本当に例外的な場面でしか使用しないのが今の考え方なので,詳しく知る必要はないかも知れない.

static,const,参照型,メンバ初期化リスト

OOPとは直接関係はないが,C++で安全に,もしくは効率的にコーディングしていく上で必要となる3つの記法を紹介する.

  • static:あるクラスにおいて,全てのインスタンスが共通の関数,変数を持つことを可能にする
  • const:初期化後には一切書き換えできなくなる定数表現が可能になる
  • 参照型:値型ともポインタ型とも違う3つ目の型表現
  • メンバ初期化リスト:コンストラクタで記述するメンバ変数の初期化には,特別な記述方法としてメンバ初期化リストという手法があり,こちらの方が好ましい

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-08-30 (木) 07:17:06 (75d)