
クラスとインスタンス
クラス( Classes )は、オブジェクト指向ブログラミング言語の分類の一つ
「クラスベースのオブジェクト指向ブログラミング言語」
の主要な概念の1つです。
C++もクラスベースのオブジェクト指向ブログラミング言語の1つです。
クラスは、データやデータへの参照変更を含む操作をまとめる手段です。
新しいクラスを定義することは、新しい型を定義することを意味し、
その型を使って、インスタンスを作成することができるようになります。
各クラスのインスタンスには、その状態を維持するための
データメンバーを持つことができます。
また、クラスには、そのクラスに属するインスタンスの(あるいはクラスそのものの)
状態を参照および変更するためのメンバー関数を持つことができます。
「オブジェクト指向プログラミング#オブジェクトとクラス」も参照
インスタンスって何?
クラスは型です。型であるクラスを実体化したものがインスタンスです。
#include <string>
int main() {
std::string s{"abc"};
int i{0};
}
s は string クラスのインスタンスです。
i は int クラスのインスタンスではなく
i は int 型の値です。
インスタンスは値の一種です。多くの場合、
インスタンスや値は初期化や代入で変数に束縛されているので
インスタンス=変数と誤解しがちですが、インスタンスは変数に束縛された値の方です。
このことは、ポインターと配列や参照を学ぶときに重要になります。
小まとめ
クラス ⊂ 型
インスタンス ⊂ 値
クラス定義
クラス定義
#include <iostream>
#include <string>
class Car {
public:
std::string owner;
std::string colour;
int number;
};
auto main() -> int {
auto a = Car();
a.owner = "山田";
a.colour = "blue";
a.number = 1234;
std::cout << "a.owner = " << a.owner << ", a.colour = " << a.colour
<< ", a.number = " << a.number << std::endl;
}
実行結果
a.owner = 山田, a.colour = blue, a.number = 1234
クルマを抽象化したクラス Class を定義しています。
owner
所有者
colour
色
numbr
ナンバー
を保持します。
public: としているので、Carのメンバーは自由に参照できます。
この状態は丁度 struct と同じです。
コンストラクター
コンストラクターは、クラスをインスタンス化するための特別なメンバー関数で、
クラス名が関数名になります。
コンストラクター
#include <iostream>
#include <string>
class Car {
public:
std::string owner;
std::string colour;
int number;
Car(const char *owner, const char *colour, int number)
: owner{owner}, colour{colour}, number{number} {}
};
auto main() -> int {
Car a = Car("山田", "blue", 1234);
auto b = Car{"伊藤", "red", 3355};
std::cout << "a.owner = " << a.owner << ", a.colour = " << a.colour
<< ", a.number = " << a.number << std::endl;
std::cout << "b.owner = " << b.owner << ", b.colour = " << b.colour
<< ", a.number = " << b.number << std::endl;
}
実行結果
a.owner = 山田, a.colour = blue, a.number = 1234
b.owner = 伊藤, b.colour = red, a.number = 3355
16行目は、C++-03で導入された一様初期化
( uniform initialization )で、Cの構造体の構文でコンストラクターを呼出せる仕組みです。
Cならば、(struct Car){"伊藤", "red", 3355} と書くところですが
C++ではクラスは型なので、Car{"伊藤", "red", 3355}となります。
クラスの配列でのコンストラクターの使用
クラスの配列でのコンストラクターの使用
#include <iostream>
#include <string>
class Car {
public:
std::string owner;
std::string colour;
int number;
Car(const char *owner, const char *colour, int number)
: owner{owner}, colour{colour}, number{number} {}
};
auto main() -> int {
Car ary[] = {
{"山田", "blue", 1234},
{"伊藤", "red", 3355},
{"佐藤", "yellow", 845},
};
for (const auto &el : ary) {
std::cout << "owner = " << el.owner << ", colour = " << el.colour
<< ", number = " << el.number << std::endl;
}
}
実行結果
owner = 山田, colour = blue, number = 1234
owner = 伊藤, colour = red, number = 3355
owner = 佐藤, colour = yellow, number = 845
メンバー関数
クラスにはインスタンスの持つメンバーとして関数も持つことができます。
この関数のメンバーのことをメンバー関数と言います。
メンバー関数を定義するには、関数を定義したようにクラス定義中に定義します。
する方法とクラス定義中ではメンバー関数の宣言だけ行い、
クラス定義の外で実装する方法があります。
メンバー関数の定義と実行
#include <iostream>
#include <string>
class Car {
std::string owner;
std::string colour;
int number;
public:
Car(const char* owner, const char* colour, int number) : owner{owner}, colour{colour}, number(number)
{}
auto show() const -> void{
std::cout << "owner = " << owner
<< ", colour = " << colour
<< ", number = " << number << std::endl;
}
};
auto main() -> int {
auto a = Car{"山田", "blue", 1234};
auto b = Car{"伊藤", "red", 3355};
a.show();
b.show();
}
実行結果
owner = 山田, colour = blue, number = 1234
owner = 伊藤, colour = red, number = 3355
public: の位置をコンストラクターの前に持ってきたので、
データメンバーには、メンバー関数からしかアクセスできなくなしました。
メンバーへのアクセスを制限する目的は、
内部の実装を詳らかにすると、それをクラスのユーザー(=プログラマー)
が参照してしまい、結果的にクラスの実装を変更したときに、
ユーザーが影響を被ってしまうことを避けることなどは目的です。
このに内部構造へのアクセス制限を設けることを
「カプセル化(内部構造の隠蔽)」と言います。
21世紀になってからの新興言語は、
メンバーごとに private/protected/public (アクセス指定子といいます)
を個別に指定する言語が多く、
クラス中のメンバーのレイアウトを自由に変更しても無害ですが、
C++は private/protected/public は「それ以降のメンバーに有効」なので、
新興言語の感覚でカジュアルにクラスメンバーのレイアウトを変えると、
アクセス指定子が意図せず変わってしまい混乱します。
新しく定義したメンバー関数 show() は public なので
クラス外からもクラスのインスタンスを通して、 a.show() の様にドット記法で呼び出せます。
アクセサー
カプセル化の結果、クラスのデータメンバーへのアクセスが制限されましたが、
様々な事情でデータメンバーを参照あるいは変更する必要が出てきます。
このような時のために、セッター・ゲッターあるいはアクセサーと
呼ばれるメンバー関数セットを用意しておくことが多く行われます。
アクセサー
#include <iostream>
#include <string>
class Car {
std::string owner;
std::string colour;
int number;
public:
Car(const char* owner, const char* colour, int number) : owner{owner}, colour{colour}, number{number}
{}
auto show() const -> void{
std::cout << "owner = " << owner
<< ", colour = " << colour
<< ", number = " << number << std::endl;
}
std::string Owner() const { return owner; }
std::string Owner(std::string newOwner) { return owner = newOwner; }
std::string Colour() const { return colour; }
std::string Colour(const char* newColour) { return colour = newColour; }
int Number() const { return number; }
int Number(int newNumber) { return number = newNumber; }
};
auto main() -> int {
auto a = Car{"山田", "blue", 1234};
std::cout << "a.owner = " << a.Owner()
<< ", a.colour = " << a.Colour()
<< ", a.number = " << a.Number() << std::endl;
a.Owner("鈴木");
a.Colour("pink");
a.Number(4423);
std::cout << "a.owner = " << a.Owner()
<< ", a.colour = " << a.Colour()
<< ", a.number = " << a.Number() << std::endl;
a.show();
}
実行結果
a.owner = 山田, a.colour = blue, a.number = 1234
a.owner = 鈴木, a.colour = pink, a.number = 4423
owner = 鈴木, colour = pink, number = 4423
クルマ a の所有者は山田さんから鈴木さんに、
色はブルーからピンクに、ナンバーも1234から4423に変わりました。
このようにデータメンバーの名前の先頭を大文字にしたメンバー関数を定義して、
パラメーターがなかったらはGetter、あったらSerrerにする流儀はよく見られますが、
必ずしもその必要はありません。
仮想的なアクセサー
アクセサーが返す値・設定する値が必ずしもデータメンバーと
対応している必要はありません。
仮想的なアクセサー
#include <cmath>
#include <iostream>
#include <string>
const double PI = acos(-1);
class Point {
double x, y;
public:
Point(double x, double y) : x{x}, y{y} {}
auto show() const -> void {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
double length() const { return hypot(x, y); }
double length(double len) {
auto t = angle();
x = len * sin(t);
y = len * cos(t);
return len;
}
double angle() const { return atan2(x, y); }
double angle(double t) {
auto len = length();
x = len * sin(t);
y = len * cos(t);
return t;
}
};
auto main() -> int {
auto pt = Point{3.0, 4.0};
pt.show();
std::cout << pt.length() << std::endl;
pt.length(10.0);
std::cout << pt.length() << std::endl;
pt.show();
std::cout << pt.angle() << std::endl;
pt.angle(PI / 4);
std::cout << pt.angle() << std::endl;
pt.show();
}
実行結果
(3, 4)
5
10
(6, 8)
0.643501
0.785398
(7.07107, 7.07107)
内部表現は、x,y のペアの直交座標ですが、
アクセサーは長さと角度の極座標になっています。
このように、
アクセサーは内部表現と外部表現のインターフェースを担うことが目的といえます。
Accessors and Mutators
日本語圏では、データメンバーを参照するためのメンバー関数をゲッター、
データメンバーを変更するためのメンバー関数をセッターと呼び総称してアクセサー
と呼びます。 これに対し、
英語圏ではデータメンバーを参照するためのメンバー関数をAccessors、
データメンバーの値を変更するためのメンバー関数をMutatorsと呼びます。
特に、アクセサーとAccessorsを混同する可能性があるので、
意思の疎通に齟齬が生じないよう気をつけましょう。
「w:en:Mutator method」も参照
[TODO:コピーコンストラクター・代入演算子のオーバーロード]
オブジェクトと名前空間
C++では、coutやcinなどのオブジェクトも、名前空間で分類されています。
たとえばオブジェクトcoutは、
正式にはstd::coutというオブジェクトです。
つまり、coutはstd名前空間に所属しています。
『C++/はじめに』では説明を省略していました。
using namespace std; とは、
「名前空間が省略された場合、
グローバル名前空間に識別子がなかったら std 名前空間の中から識別子を探す」
という意味の宣言だったのです。
stdとは、標準ライブラリ(standard library)を意味する略語です。
cinも同様に、
std::cinというオブジェクトです。
つまり、cinはstd名前空間に所属しています。
スコープ演算子::をつかうことから分かるように、
std名前空間に含まれるオブジェクトとして、coutやcinを扱っています。
書式は、
名前空間名::識別子
です。
プログラミング言語に用意されている
標準ライブラリのオブジェクトは、
膨大にあるので、名前空間によって分類する必要があり、
使用時には本来、その名前空間名をつける必要があるのです。
なぜなら、そうしないと、もし、自分で新しく関数を定義しようと思ったときに、
名前につけようと思ってた識別子が、
すでに標準ライブラリで使われている可能性があるからです。
このような工夫を、
(使いたい名前が既に他の用途で使われてしまっているような事態をふせぐための工夫を)
「名前の衝突をふせぐ」などと、言います。
using namespace std; の問題点
多くのプログラムで
using namespace std;
のように、名前空間丸ごとスコープ限定演算子なしにアクセスできるようにする
コードを見受けます。 これでは、std::には数多の識別子が定義されているので、
無自覚に名前の衝突を招いてしまう可能性があります。
これを回避するためには、
using std::cout, std::endl;
のように識別子を限定して using 宣言するよう心がけましょう。
この例では、
cout と endl の2つの識別子だけがスコープ演算子なしに参照出来るようになります。
また using 宣言にもスコープがあり、関数スコープにすることも検討の価値があります。
名前空間の定義
名前空間は、ユーザープログラムからも定義できます。
名前空間の定義
namespace NS1 {
int i = 3;
int f() { return 42; }
}
int x = NS1::i; // ⇒ 3
int y = NS1::f(); // ⇒ 42
名前空間は、入れ子に出来ます。
入れ子名前空間の定義
namespace NS1 {
namespace NS2 {
int j = 9;
int g() { return 108; }
}
}
int u = NS1::NS2::j; // ⇒ 9
int v = NS1::NS2::g(); // ⇒ 108
C++14 から、入れ子名前空間の簡略表記が使えるようになりました。
入れ子名前空間の簡略表記による定義
namespace NS1::NS2 {
int j = 9;
int g() { return 108; }
}
int u = NS1::NS2::j; // ⇒ 9
int v = NS1::NS2::g(); // ⇒ 108
継承
既存のクラスをベースに新しいクラスを作成することを
クラス継承( class inheritance )といいます。
プライベート継承
#include <iostream>
using namespace std;
class MyBaseClass {
public: const char * name() {
return "MyBaseClass";
}
};
class MySubClass: MyBaseClass {
public: int ten() {
cout << "ten::" << this->name() << endl; // これはエラーにならない
return 10;
}
};
int main(void) {
auto base = new MyBaseClass();
auto sub = new MySubClass();
cout << base -> name() << endl;
// cout << sub->name() << endl; Main.cpp:22:16: error: 'name' is a private member of 'MyBaseClass'
cout << sub -> ten() << endl;
// cout << base->ten() << endl; Main.cpp:24:17: error: no member named 'ten' in 'MyBaseClass'
}
実行結果
MyBaseClass
ten::MyBaseClass
10
この例では、派生クラスの定義でアクセス修飾子をしていしていないので、
デフォルトの private が仮定され
class MySubClass: private MyBaseClassと同義で「プライベート継承」と呼びます。
プライベート継承でも、基底クラスのメンバーが
public であれば、派生クラスメソッドから
基底クラスのpublicメンバーにアクセスできません。
プライベート継承では、基底クラスのメンバーが public でも、
派生クラスのインスタンス経由では、publicメンバーにアクセスできません。
また、基底クラスのインスタンスは、派生クラスメンバー
を持たないのは当然ですが、念の為の例です。
構文
class 派生クラス : アクセス修飾子 基底クラス {};
パブリック継承
#include <iostream>
using namespace std;
class MyBaseClass {
public: const char * name() {
return "MyBaseClass";
}
};
class MySubClass: public MyBaseClass {
public: int ten() {
cout << "ten::" << this->name() << endl;
return 10;
}
};
int main(void) {
auto base = new MyBaseClass();
auto sub = new MySubClass();
cout << base -> name() << endl;
cout << sub->name() << endl; // Ok
cout << sub -> ten() << endl;
// cout << base->ten() << endl; Main.cpp:24:17: error: no member named 'ten' in 'MyBaseClass'
}
実行結果
MyBaseClass
MyBaseClass
ten::MyBaseClass
10
先程の例との違いは、
class MySubClass: public MyBaseClass です。
この影響で、main() からも sub->name() にアクセス可能になりました。
組合せ的にプロテクテッド継承もありますが、
派生クラスの定義での基底クラスのアクセス修飾子は
「派生クラスのインスタンス経由でアクセスできるか?」
の違いなのでプライベート継承との違いはありません。
また、継承先の派生クラスで望ましくないアクセスや
オーバーライドが行われないことを「期待」するのではなく、
final でクラスかメンバーを保護する「防衛的」な実装が望まれます。
多重継承
C++の継承では、複数のクラスを基底クラスとし派生クラスは作成することが可能です。
class D : public B, public class E {
};
クラスBとクラスEが、基底クラスです。クラスDは、派生クラスです。
複数のクラスからの継承を多重継承といいます。
多重継承は、多重継承でないと実現できない命題が想定しにくい上に、
2つの継承元に同じ名前のメンバーがあった場合(衝突があった場合)
の解消法方法に一般則がないこと、さらに継承元の2つのクラスが
共通するクラスから派生していた場合の問題
(ダイヤモンド継承問題)など、多くの問題が多重継承にはあるので、
「多重継承が設計上の最適な選択か?」を自問してから使うようにしましょう。
「w:菱形継承問題」も参照
実装例
C++に、「Go/メソッドとインターフェース」の
「都市間の大圏距離を求めるメソッドを追加した例」を移植してみました。
実装例
#include <cmath>
#include <iostream>
#include <string>
class GeoCoord {
double longitude, latitude;
public:
GeoCoord(double lng, double lat) : longitude(lng), latitude(lat) {}
std::string toString() {
const auto *ew = "東経";
const auto *ns = "北緯";
auto lng = this->longitude; // long はCでは予約語なので lng に
auto lat = this->latitude;
if (lng < 0.0) {
ew = "西経";
lng = -lng;
}
if (lat < 0.0) {
ns = "南緯";
lat = -lat;
}
return std::string("(") + ew + ": " + std::to_string(lng) + ", " + ns + ": " + std::to_string(lat) + ")";
}
double distance(GeoCoord &other) {
// C言語では円周率の値は標準では定義されていません。
const auto i = 3.1415926536 / 180;
const auto R = 6371.008;
return std::acos(
std::sin(this->latitude * i) * std::sin(other.latitude * i) +
std::cos(this->latitude * i) * std::cos(other.latitude * i) *
std::cos(this->longitude * i - other.longitude * i)) * R;
}
};
int main(void) {
struct {
const char *name;
GeoCoord gc;
} sites[] = {
{"東京駅", GeoCoord(139.7673068, 35.6809591)},
{"シドニー・オペラハウス", GeoCoord(151.215278, -33.856778)},
{"グリニッジ天文台", GeoCoord(-0.0014, 51.4778)},
};
for (auto x : sites) {
std::cout << x.name << ": " << x.gc.toString() << std::endl;
}
for (int i = 0, len = sizeof sites / sizeof *sites; i < len; i++) {
const auto j = (i + 1) % len;
std::cout << sites[i].name << " - " << sites[j].name << ": "
<< sites[i].gc.distance(sites[j].gc) << std::endl;
}
}
実行結果
東京駅: (東経: 139.767307, 北緯: 35.680959)
シドニー・オペラハウス: (東経: 151.215278, 南緯: 33.856778)
グリニッジ天文台: (西経: 0.001400, 北緯: 51.477800)
東京駅 - シドニー・オペラハウス: 7823.27
シドニー・オペラハウス - グリニッジ天文台: 16987.3
グリニッジ天文台 - 東京駅: 9560.55