こんにちは、ナナです。
オブジェクト指向言語であるC++で、中心となる機能が「クラス」です。
クラスは非常に大きな機能のため、全てを一度に語ることはできません。オブジェクト指向言語を最初に学ぶ方は、まず「クラス」のイメージを知ることです。
オブジェクト指向で登場する「クラス」とは何か?
はい、はーい!僕も「オブジェクト指向」にチャレンジするときがやってまいりました!で、で、で「クラス」という言葉が急に登場したんですよ。
僕は高校時代は3年B組のクラスだったのですが、クラスメートが懐かしいです。
「クラス」とは特定のものを分類分けしてまとめたもの、という意味があるんです。そういう意味では学校の「クラス」は学生を分類分けしたということになりますね。
それでは、オブジェクト指向における「クラス」とは、いったい何を分類分けしてまとめているのでしょうか?それを学んでいきましょう!
オブジェクト指向言語に学び始めて、最も大事なキーワードが「クラス」です。
「クラス」こそがオブジェクト指向の中心となる概念であり、非常に多くの機能が備わっているのです。
一度に全ての機能を理解することは不可能なため、少しずつ「クラス」というものを知っていきましょう。
クラスとは「構造体」と「関数」をまとめて管理するもの
C言語を理解している方が最初にクラスをイメージするときは
クラス = 「構造体」 + 「関数」
と考えてよいです。
「構造体」とは複数のデータをパッケージ化する機能であり、「関数」とはデータを処理する機能です。
この2つをひとつにまとめて管理してしまおう、というのが「クラス」の概念です。
構造体は「データ」のみしかメンバとして登録できませんでしたが、クラスでは「データ」と「関数」を一緒にメンバとして登録することができます。
クラスのイメージは、構造体メンバの中に「関数」を含ませることができるようになった拡張機能として捉えるとよいでしょう。
「クラス」の型定義方法
C言語の技術者が、まず抑えておきたいことは
「クラス」は「型」であるということ
です。
そのため「クラス」を利用するには構造体と同じく、事前に「型」を定義する必要があります。まずは基本となるクラスの型定義方法をプログラムで示しましょう。
C言語:構造体の型定義
typedef struct
{
double x;
double y;
} POS;
C++:クラスの型定義
class POS
{
public:
double x;
double y;
};
X、Y座標を管理するための「POS」を構造体とクラスでそれぞれ定義してみました。
クラスの型定義は次の形で構成されています。
「構造体」の場合はtypedefキーワードを使って定義するのが一般的ですが、「クラス」の場合はtypedefは不要です。
このようにC言語では手間だったものが、C++になることで改善されているポイントがあります。
このクラスにはまだ「関数」は登録されていません。後ほど解説します。
ここではまず、基本的なクラスの型定義の構成を把握しましょう。
クラス型をメモリへ実体化:オブジェクトの生成とは
なるほど~。「クラス」とは構造体と同じく僕たちが新たに作ることができる「型」のひとつなのですね。
ん?、ん?、ん?、てことは、変数を作らないと使えないってことですか?
よく理解しているね。その通り、クラスはユーザーが定義できる「型」であり、使うためには、クラスの型を使って変数を定義する必要があります。
C言語は「型」の言語であり、C++もその特性を引き継いでいます。つまり、構造体を拡張した「クラス」は、結局「型」でしかないということです。
プログラムで情報を扱うためには、「型」を元にメモリ上に実体を作り出す必要があります。これはクラスでも同じなのです。
クラス型の変数定義:オブジェクトの作り方
クラスの変数を作り出す方法をプログラムから見てみましょう。先ほどの型定義と合わせて紹介しましょう。
C言語:構造体の変数定義
#include <stdio.h>
typedef struct
{
double x;
double y;
} POS;
int main()
{
POS pos; // 構造体の変数定義
pos.x = 100.0;
pos.y = 200.0;
printf("x:%lf y:%lf\n", pos.x, pos.y);
return 0;
}
C++:クラスの変数定義
#include <stdio.h>
class POS
{
public:
double x;
double y;
};
int main()
{
POS pos; // クラスの変数定義
pos.x = 100.0;
pos.y = 200.0;
printf("x:%lf y:%lf\n", pos.x, pos.y);
return 0;
}
main関数の中で定義された変数と処理を見ると、構造体とクラスで全く同じプログラムになっています。ドット演算子で「x」「y」のメンバを参照するのも全く同じです。
このように、クラスと言っても変数を作りたければ、C言語と同じように変数定義すればよいのです。
作成された変数は、もちろんメモリ上に実体が存在します。
この「オブジェクト」こそがオブジェクト指向の主役となる存在なのです。
「構造体」と「クラス」は、非常に近い存在のため構造体を理解している人は扱い方を知るのは容易ですね。
メモリ上のクラス型の実体のことを「インスタンス」と呼ぶこともあります。
クラスとオブジェクトの関係性
ここで「クラス」と「オブジェクト」の関係性をイメージとして捉えておきましょう。
「クラス」とはオブジェクトを作るための設計図であり、「オブジェクト」は設計図をもとに作られた実際の製品です。
設計図は1枚あれば十分であり、その設計図から何体もの製品を作り出すことができます。
「クラス」という設計図から「変数」という製品を作り出すのです。変数は何個も作り出すことができます。
クラス型へメンバ関数を登録してみよう
ほほほーい。クラスは「構造体」と「関数」がまとめられたものなんですよね?でもでも、関数はどうやってクラスの中に入れるんですか?
そうだね。ここまで紹介したクラスはデータを管理するための構造体と一緒で、まだ「関数」が含まれてませんね。
それでは、クラスへの関数の登録方法を学びましょう!
クラスのメリットは、データだけでなく処理を行う「関数」も含むことができることです。
クラスに登録した関数のことを「メンバ関数(メソッド)」と呼ぶことを覚えておきましょう。
クラス型へ「メンバ関数」を登録する方法と定義方法
クラスには好きな処理を行う「メンバ関数」を自由に登録することができます。しかし、どのような処理を行う関数を登録すればよいのでしょうか?
それは、
クラスが管理するデータを扱うための関数を登録する
ことです。
先ほどのプログラムをもう一度見てみましょう。
int main()
{
POS pos; // クラスの変数定義
pos.x = 100.0;
pos.y = 200.0;
printf("x:%lf y:%lf\n", pos.x, pos.y);
return 0;
}
ここでは座標情報を可視化するために、main関数がprintf関数を使って表示していますね。それではこの「座標を画面に表示する」という処理をクラスに関数として登録してみましょう。
class POS
{
public:
double x;
double y;
void print(); // 関数のプロトタイプ宣言
};
// 座標表示の関数の定義
void POS::print()
{
printf("x:%lf y:%lf\n", x, y);
}
白抜きの部分が新たに追加した内容となります。プログラムの意味を解説しましょう。
注意点は関数定義の名前の指定方法です。「クラス名::関数名」の形式で記述する必要があります。
これには理由が明確にあります。仮に、C言語のように名前だけで関数定義をしたとします。
void print()
{
printf("x:%lf y:%lf\n", x, y);
}
そうすると、この「print関数」がどこの誰のものなのかという所属がわからないのです。
つまり、「POSクラスに所属するprint関数ですよ」という所属関係を表明するため、クラス名を指定する必要があるのです。
「::」は、C++の新しい演算子で「スコープ解決演算子」と呼びます。
言語仕様を身に付けるときは、暗記するのではなくて常に「なぜ、このようなルールが必要なのだろうか?」を考える癖を身に付けましょう。
その答えがわかることで論理的にルールを覚えることができます。
オブジェクトからメンバ関数の呼び出してみよう
はい、はーい。質問です。
クラスに「メンバ関数」を登録したのですが、これでいったい何ができるんでしょうか?
「関数」というものはC言語と同じで、関数定義を行うだけでは何の意味もありません。「関数」は呼び出すことで初めて意味があるのです。それは、メンバ関数も同じなのです。
それでは、登録したメンバ関数を呼び出してみましょう。
メンバ関数の呼び出し方
実際のプログラムを使ってメンバ関数を呼び出してみます。main関数に着目しましょう。
#include <stdio.h>
class POS
{
public:
double x;
double y;
void print(); // 関数のプロトタイプ宣言
};
// 座標表示のメンバ関数の定義
void POS::print()
{
printf("x:%lf y:%lf\n", x, y);
}
int main()
{
POS pos; // クラスオブジェクトの生成
pos.x = 100.0;
pos.y = 200.0;
pos.print(); // 座標の表示要求
return 0;
}
x:100.000000 y:200.000000
白抜きの部分がメンバ関数の呼び出し箇所です。つまり、メンバ関数は次の方法で呼び出すことができます。
構造体メンバの参照方法と同じで「ドット演算子」を使って参照することができます。メンバ変数の参照方法と変わりませんね。
メンバ関数のプログラムが参照しているデータとは?
ここでもう一度、メンバ関数として登録したPOS::printメンバ関数をよく見てみましょう。
void POS::print()
{
printf("x:%lf y:%lf\n", x, y);
}
ここで、「x」と「y」という変数が参照されていますね。関数内に変数定義は存在していないため、ローカル変数ではないのはわかりますね。
それでは、この「x」「y」はいったいどこの変数を参照しているのでしょうか?
そうです。この変数は「メンバ変数」なのです。
ここで大事なことは、呼び出したオブジェクトのメンバ変数が参照できることです。
「呼び出したオブジェクト」というのがポイントですよ!この意味をしっかりと理解しないと、オブジェクト指向を理解できません。
異なるオブジェクトからのメンバ関数の呼び出しは実行結果が変化する
「呼び出したオブジェクトのメンバ変数が参照できる」ということの意味を正確に理解するため、2つのオブジェクトを生成してメンバ関数を呼び出してみましょう。
「pos1」と「pos2」をオブジェクトとして作成し、printメンバ関数をそれぞれのオブジェクトで呼び出してみます。
int main()
{
POS pos1;
POS pos2;
pos1.x = 100.0;
pos1.y = 200.0;
pos2.x = 300.0;
pos2.y = 400.0;
pos1.print(); // pos1オブジェクトへの呼び出し
pos2.print(); // pos2オブジェクトへの呼び出し
return 0;
}
x:100.000000 y:200.000000
x:300.000000 y:400.000000
print関数の呼び出し方は同じですが、実行結果は異なる値が表示されていますね。
理由は、「pos1」と「pos2」でオブジェクトが管理しているデータが異なるからです。
このようにオブジェクトによって振る舞いが変化するということが、オブジェクト指向の特徴です。
オブジェクト指向の便利さを理解するためのイメージ
ほー、確かにprint関数を動かした結果が異なりますね。でもでも、もともとmain関数で表示していた座標の表示結果と、メンバ関数にしたときの表示結果って一緒ですよね~?
わざわざ、メンバ関数にした意味ってあるんですか?どっちでもよくないですか?
確かに表示された結果だけ見たら同じなので、意味がないように思えますね。でもね、この2つは誰が仕事の役割を担うのかという観点で大きく異なるんですよ。
誰が何の仕事を行うのか、というのはソフトウェア開発においてすごく大事なことなんです。
もう一度、メンバ関数前のプログラムと、メンバ関数後のプログラムを見比べてみましょう。
main関数で座標表示
int main()
{
POS pos;
pos.x = 100.0;
pos.y = 200.0;
printf("x:%lf y:%lf\n", pos.x, pos.y);
return 0;
}
メンバ関数で座標表示
int main()
{
POS pos;
pos.x = 100.0;
pos.y = 200.0;
pos.print(); // 座標の表示要求
return 0;
}
この2つのプログラムは、結果は同じ表示内容でも、仕事の仕方のアプローチが全く異なるのです。
オブジェクトはロボットのようなもの
私は「オブジェクト」を「ロボット」のようなものとしてイメージしています。皆さんが思い描く「ロボット」って、こんなものじゃないですか?
「指示を与えるだけで、オブジェクト自身が判断して動いてくれる」そんな世界がオブジェクト指向なのです。
先ほどのプログラムをロボットに置き換えて表現してみましょう。
このようなイメージで見ると、main関数の役割りはロボットに指示を行うことであり、ロボットは指示に従い適切に動く、という構図が出来上がります。
私はプログラムという世界をいかに現実世界のものに置き換えてイメージするかということを大事にしています。
このイメージによって、文字で表現されるわかりづらいプログラムの世界を論理的に理解することができるようになります。
オブジェクト指向の便利さとは
オブジェクト指向の便利さとは、「オブジェクト」という様々な役割りを持ったロボットを大量に作り出し、ロボットに対して命令を行うだけでミッションを達成できるということなのです。
オブジェクト指向言語を使う時は、このように「オブジェクトを使う側」と「オブジェクトとして使われる側」を明確に分離する意識で見るとよいでしょう。
オブジェクトを使う側を主役とすると、オブジェクトに指示さえすればよいわけですから仕事の負担を軽減できますよね。
この便利さこそが「オブジェクト指向言語」が流行っている理由なのです。
オブジェクト指向の便利さとは、クラスとして定義される「オブジェクト」が仕事を請け負ってくれることで、利用する側が楽ができるということなんです。
Q&A:クラスに関するよくある質問
「関数」も「オブジェクト」も指示して動くのは同じじゃないの?
はーい、質問です。「オブジェクト」はロボットのようなもので、指示すると動いてくれるっていうのはわかります。でも、それって「関数」も同じじゃないですか?
「関数」だって呼び出して動いてくれるじゃないですか?
ふむふむ、それは確かにそうだね~。「関数」というものも、関数呼び出しという指示によって動いてくれますね。
では、「関数」と「オブジェクト」は何が違うのでしょうかね?
「関数」も見方によっては指示を行うことで願いを叶えてくれる「ロボット」のようなものと捉えることもできますね。
では、「関数」と「オブジェクト」では何が違うのでしょうか?
それは「オブジェクト」の方が「関数」よりも自立したモノとして存在していることです。
先ほどのプログラムを比較してみましょう。
関数を使った指示出し
printf("x:%lf y:%lf\n", pos.x, pos.y);
オブジェクトを使った指示出し
pos.print();
「オブジェクト」はクラスという枠組みによって、「データ」と「処理」をまとめて管理できます。そのため、オブジェクトに対する指示では、引数に「XY座標」を与える必要がなくなっています。
これこそが、オブジェクトが「データ」と「処理」を合わせて持つことのメリットです。
オブジェクトは「データ」と「処理」を一緒に管理することにより、「関数」よりも自立したモノとして存在できるのです。