
1オブジェクト指向
プログラムが数万行を超えてくると、全体像を把握するのが困難になるため、後で内容を変更したくなったときや他の人と一緒に開発するときに大変になります。 そのような場合、多くのプログラミング言語で採用されている「オブジェクト指向」を使うと便利です。
「オブジェクト指向」とは、プログラムを「オブジェクト」というまとまりで扱う手法で、全体像を把握したり、後で内容を変更したり、プログラムを別の箇所で再利用したり、他の人と開発するのを容易にします(図1-1)。

これまでのプログラムは全体がひとまとめになっていたため、どの部分がどの部分に影響しているかが解りにくく、プログラムを変更するときにはすべてを把握する必要がありましたが、オブジェクト単位でプログラムを切り分けると、変更したいオブジェクトとオブジェクト間の関係を把握するだけでプログラムが変更できるようになります。
2オブジェクトの機能
さて、切り分けた各オブジェクトが互いにやりとりできるようにするために、オブジェクトは「3つの仕組み」と「3つの要素」という機能を持つことが決められています(図2-1)。
3つの仕組み
- オブジェクトは、データを持つ。
- オブジェクトは、他のオブジェクトにメッセージを送る。
- オブジェクトは、送られたメッセージに応じて処理を行う。
3つの要素
- 継承
- カプセル化
- ポリモーフィズム
それぞれがどのようなものかを順番に説明します。 単にプログラムを切り分けるだけでなく、これらの機能を持たせることで、各オブジェクトは扱いやすい部品になります。
2.13つの仕組み
まずは「3つの仕組み」から見ていきましょう(図2-2)。

オブジェクトには、「データを持つ」「他のオブジェクトにメッセージを送る」「送られたメッセージに応じて処理を行う」という3つの仕組みがあります。
1つ目として、オブジェクトはデータを保持することができ、これを「データメンバ」といいます。 データメンバは言語によって呼び方が異なり、C++では「メンバ変数」、C#では「プロパティ」と呼ばれます。
2つ目として、オブジェクトは他のオブジェクトを操作するために、「メッセージ」を送ることができます。 メッセージとは、いくつかの情報をまとめたものです。
そして3つ目として、送られたメッセージに応じてオブジェクトは処理を行うことができ、これを「メソッド」といいます。 C#でも「メソッド」ですが、C++では「メンバ関数」と呼ばれます。 「データメンバ」と「メソッド」は、合わせて「メンバ」といいます。
この3つの仕組みにより、データを持ったオブジェクトは互いにメッセージを送りあって処理を行うことができます。
2.23つの要素
次に3つの要素、「継承」「カプセル化」「ポリモーフィズム」を見ていきましょう。
1つ目の「継承」とは、オブジェクトのメンバを引き継いで別のオブジェクトを作成できる機能です。 図2-3のように、引き継ぐときに新しいメンバを追加したり、引き継いだメンバの内容を上書きすることもできます。

2つ目の「カプセル化」とは、オブジェクトの一部のメンバを隠すことができる機能です。 図2-4のように、外部に公開する部分を必要最小限にすることで、オブジェクトに対する想定外の操作を防ぐことができます。

3つ目の「ポリモーフィズム」とは、継承によって複製されたオブジェクトたちを、全て継承元と同等とみなして扱える機能です。 図2-5のように、多くの種類のオブジェクトが生まれたとしても、その違いを意識することなくまとめて扱うことができます。

まとめますと「オブジェクト指向」とは、プログラムを「オブジェクト」という単位で扱い、そのオブジェクトに「3つの仕組み」と「3つの要素」を持たせることで、プログラムの開発を容易にする手法です。
3クラスとインスタンス
それでは、プログラム上でオブジェクト指向がどう表現されるかを、具体的に見ていきましょう。 多くの言語はC++に影響を受けていますのでC++を例に解説します。 C++は基本的にはC言語にいくつかの文法と機能を足しただけの言語ですので、今までに説明したC言語の知識が流用できます。
C++のオブジェクトは「クラス」と呼ばれ、複数の変数や関数をひとまとめにして新しい型を作り出したものです。 C++でクラスを作るには、クラスの中身となる変数や関数を「class 型名{};」で囲むように書きます(図3-1)。
- class C
- {
- public:
- int a;
- char b;
-
- void f(void)
- {
- (*this).a = 5;
- }
- };
-
- int main(void)
- {
- C d;
- d.a = 3;
- d.b = 'x';
- d.f();
- return 0;
- }
このプログラムでは1~11行目で「C」という名前のクラスを定義し、中身にメンバ変数「a」「b」とメンバ関数「f」を持たせています。 このクラス「C」は型として扱え、メイン関数の15行目ではC型の変数dを作成しています。 この変数dのような、クラス型の変数のことを「インスタンス」といいます。 このようにC++におけるオブジェクトは、ひな形であるクラスと、それを実体化したインスタンスによって作られます。
そして16~18行目のように、インスタンスdに対し、「d.a」「d.b」「d.f」のように「.」を使って書くことで、クラス内のメンバにアクセスできます。
メンバ関数の内部でも、そのクラスが持つメンバ変数を読み書きできます。 例えばこのプログラムでは「d.f()」を呼び出すと、7行目の関数fが呼ばれ、9行目の「(*this).a=5;」によってd.aに5を代入することができます。 メンバ関数内で自身のインスタンスを表すときは「this」と記述し、ポインタのように扱います。 ちなみに「(*this).」は省略可能となっており、「(*this).a=5;」は単に「a=5;」と書かれることが多いです。
3.13つの仕組み
ここでオブジェクトの「3つの仕組み」とC++のクラスとを比べると、表3-1のようになっていることが解ります。
3つの仕組み | C++のクラス |
---|---|
オブジェクトは、データを持つ。 | メンバ変数 |
オブジェクトは、他のオブジェクトにメッセージを送る。 | 他のオブジェクトのメンバ関数の呼び出し |
オブジェクトは、送られてきたメッセージに応じて処理を行う。 | メンバ関数内の処理 |
C++における「メッセージ」のやりとりは、このようにメンバ関数の呼び出しで代用されています。
ここからはC++における「3つの仕組み」を見ていきましょう。
3.2継承
1つ目は「継承」です。 C++での継承はクラス名の右側に「:継承元」と書くことで行えます(図3-2)。
- class Parent
- {
- double x;
- double y;
- };
-
- class Child : Parent
- {
- double z;
- };
この例では、7行目でクラス「Parent」を継承して新たなクラス「Child」を作成しています。 継承元のメンバが継承先に引き継がれますので、Childは変数zだけでなく、xやyも持ちます。
3.3カプセル化
2つ目は「カプセル化」です。 C++でカプセル化を行うには、「private:」「protected:」「public:」という「アクセス修飾子」を用います。
「private:」と書くと、メンバは外部や継承したクラスから参照できないようになります。 「protected:」は継承したクラスからは参照できるようになり、「public:」は外部からも参照できるようになります(図3-3)。
- class Number
- {
- private:
- int value;
-
- public:
- void Set(int v)
- {
- (*this).value = v;
- }
- };
-
- int main(void)
- {
- Number n;
- n.Set(5);
- return 0;
- }
この例では、メンバSetにはpublic:が設定されているため、16行目のn.Setのように参照することができます。 しかしメンバvalueにはprivate:が設定されているため、n.valueのように外部から値を読み書きすることはできません。 ただし9行目のようにクラス内部からは(*this).valueとして参照可能です。
private:などのアクセス修飾子は、一度設定すると再設定されるまで設定が持続します(図3-4)。
- class Test
- {
- int a;
- public:
- int b;
- int c;
- int d;
- };
この例の場合、メンバb、c、dは全てpublic:になります。 また、3行目のメンバaのようにアクセス修飾子が設定されていないメンバはprivate:となります。
継承関係にもアクセス修飾子を設定することができます(図3-5)。
- class Parent
- {
- public:
- int a;
- };
-
- class Child : protected Parent
- {
- public:
- int b;
- };
継承のときに「:protected 継承元」と書くと、継承元のメンバでpublic:だったものは全てprotected:になって引き継がれます。 また「:private 継承元」と書くと、継承元のメンバは全てprivate:になって引き継がれます。 「:public 継承元」は継承元のアクセス修飾子の設定がそのまま引き継がれます。
このように継承元メンバの公開範囲は制限することができます。 ちなみに、省略して単に「:継承元」と書くと「:private 継承元」と解釈されます。
3.4ポリモーフィズム
3つ目はポリモーフィズムです。 C++では継承先クラスを継承元クラスと同じように扱うことができます。 またメンバ関数の先頭に「virtual」と書くと、継承先クラスで上書きすることができます(図3-6)。
- class Person
- {
- public:
- virtual int GetAge() { return 0; }
- };
-
- class Alice : public Person
- {
- public:
- int GetAge() { return 10; }
- };
-
- class Bob : public Person
- {
- public:
- int GetAge() { return 20; }
- };
-
- int main(void)
- {
- Person person;
- Alice alice;
- Bob bob;
- Person* people[3];
- people[0] = &person;
- people[1] = &alice;
- people[2] = &bob;
- int i;
- for (i = 0; i < 3; i++)
- {
- printf("%d\n", people[i]->GetAge());
- }
- return 0;
- }
7~17行目では、クラスPersonを継承してクラスAliceとクラスBobを作成し、それぞれでメンバ関数GetAgeを上書きしています。 メンバ関数を上書きすることを「オーバーライド」といいます。
main関数の中では、それぞれのクラスのインスタンスを作成し、25~27行目でPerson*型の配列「people」にそれぞれのアドレスを代入しています。 AliceやBobは、Personとは型が異なりますが、このように継承元クラスであるPersonと同じ型として扱えます。
31行目ではPersonクラスのメンバ関数GetAgeを呼んでいますが、インスタンスはそれぞれPerson、Alice、Bobのものですので、それぞれで上書きしたGetAgeが呼ばれます。 よって画面には「0」「10」「20」が表示されます。
4オブジェクトの設計
ここまでオブジェクト指向の考え方とC++での実装方法について解説しました。 さてここからは、プログラムのどこの部分をオブジェクトにすればいいのかについて、オブジェクトの設計方法を説明します。
基本的には、「開発が効率的になるように」オブジェクトを作成すると大丈夫です。 言い換えると、それ以外の観点でオブジェクトを作成すると本末転倒になりますので注意が必要です。
例えば、アクションゲームのプログラムで、主人公のクラス「Hero」と敵のクラス「Enemy」を作成し、どちらもキャラクターでなので、さらにクラス「Character」を作って継承関係にさせるべきかどうかを検討したとしましょう。 このとき、衝突判定や移動などの処理が再利用できそうならば、クラスCharacterを作って継承関係にすることは有意義です。 しかし、細かな処理が異なるために再利用できる部分が少なければ、無理に継承関係にしないほうが有益になることもあります。
特によくある失敗として、プログラムのあらゆる概念をオブジェクトにして大きなピラミッド構造を作りたくなることがあります。 プログラムの最終目的は期待通り動くことであり、秩序正しく美しい階層を構築することではないはずです。 何をオブジェクトにしてどのような継承関係にするかは、開発が効率的になるかどうかに基づいて判断すると良いと思います。
5その他の言語のオブジェクト指向
さて、C++ではクラスを使ってオブジェクト指向を実現しましたが、他の言語ではこれとは異なる方法でオブジェクト指向を実現していることがあります。
例えばJavaScriptには、元々はクラスというものが存在しませんでした。 元々のJavaScriptでは図5-1のように、空の状態のインスタンスにメンバを追加していく流れでオブジェクトを構築していきます。

注釈
JavaScriptには、かつてはクラスというものが存在しませんでした。 が、現在の規格では「クラス構文」というものが導入され、他の言語のクラスと似た記法で書けるようになっています。
また、Java(JavaScriptではない)におけるオブジェクトは、C++の仕組みに近いですが、よりオブジェクト指向を前提とした言語設計になっています。 クラスのメンバでない変数や関数を作成することができず、あらゆるデータや処理は基本的にオブジェクトで管理され、メイン関数も何らかのクラスに含める必要があります(図5-2)。
- public class HelloWorld
- {
- public static void main(String[] args)
- {
- System.out.println("Hello, world!");
- }
- }
今回は、プログラムが大規模になったときに役立つ仕組み「オブジェクト指向」について説明しました。 次回はより速く計算させるために、同時に複数のプログラムを実行する方法について説明します。