C++/初心者むけ/クラス

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++


クラスとインスタンス[編集]

クラス( Classes )は、オブジェクト指向ブログラミング言語の分類の一つ「クラスベースのオブジェクト指向ブログラミング言語」の主要な概念の1つです[1]。 C++もクラスベースのオブジェクト指向ブログラミング言語の1つです。

クラスは、データやデータへの参照変更を含む操作をまとめる手段です。 新しいクラスを定義することは、新しい型を定義することを意味し、その型を使って、インスタンスを作成することができるようになります。 各クラスのインスタンスには、その状態を維持するためのデータ メンバーを持つことができます。 また、クラスには、そのクラスに属するインスタンスの(あるいはクラスそのものの)状態を参照および変更するためのメンバー関数を持つことができます。

インスタンスって何?
クラスは型です。型であるクラスを実体化しものがインスタンスです。
#include <string>

int main() {
  std::string s{"abc"};
  int i{0};
}
sstring クラスのインスタンスです。
iint クラスインスタンスではなく
iint です。
インスタンスは値の一種です。多くの場合、インスタンスや値は初期化や代入で変数に束縛されているので
インスタンス=変数と誤解しがちですが、インスタンスは変数に束縛された値の方です。
このことは、ポインターと配列や参照を学ぶときに重要になります。

小まとめ

  • クラス ⊂ 型
  • インスタンス ⊂ 値


クラス定義[編集]

クラス定義
#include <iostream>
using namespace std;

class Car {
public:
  const char* owner;
  const char* colour;
  int number;
};

int main() {
  auto a = Car();
  a.owner = "山田";
  a.colour = "blue";
  a.number = 1234;
  cout << "a.owner = " << a.owner
       << ", a.colour = " << a.colour
       << ", a.number = " << a.number << endl;
}
実行結果
a.owner = 山田, a.colour = blue, a.number = 1234
クルマを抽象化したクラス Class を定義しています。
owner
所有者
colour
numbr
ナンバー
を保持します。
public: としているので、Carのメンバーは自由に参照できます。
この状態は丁度 struct と同じです。

コンストラクター[編集]

コンストラクターは、クラスをインスタンス化するための特別なメンバー関数で、クラス名が関数名になります。

コンストラクター
#include <iostream>
using namespace std;

class Car {
public:
  const char* owner;
  const char* colour;
  int number;
  
  Car(const char* owner, const char* colour, int number) : owner(owner), colour(colour), number(number) {}
};

int main() {
  auto a = Car("山田", "blue", 1234);
  auto b = Car{"伊藤", "red", 3355};

  cout << "a.owner = " << a.owner
       << ", a.colour = " << a.colour
       << ", a.number = " << a.number << endl;
  cout << "b.owner = " << b.owner
       << ", b.colour = " << b.colour
       << ", a.number = " << b.number << endl;
}
実行結果
a.owner = 山田, a.colour = blue, a.number = 1234 
b.owner = 伊藤, b.colour = red, a.number = 3355
15行目は、C++-03で導入された一様初期化 ( uniform initialization )で、Cの構造体の構文でコンストラクターを呼出せる仕組みです。
Cならば、(struct Car){"伊藤", "red", 3355} と書くところですが
C++ではクラスは型なので、Car{"伊藤", "red", 3355}となります。

クラスの配列でのコンストラクターの使用[編集]

一様初期化は、配列の初期化をよりスマートにかけます。

クラスの配列でのコンストラクターの使用
#include <iostream>
using namespace std;

class Car {
public:
  const char *owner;
  const char *colour;
  int number;

  Car(const char *owner, const char *colour, int number)
      : owner(owner), colour(colour), number(number) {}
};

int main() {
  auto ary = {
    Car{"山田", "blue", 1234},
    Car{"伊藤", "red", 3355},
    Car{"佐藤", "yellow", 845},
  };

  for (auto el : ary) {
    cout << "owner = " << el.owner << ", colour = " << el.colour
         << ", number = " << el.number << endl;
  }
}
実行結果
owner = 山田, colour = blue, number = 1234
owner = 伊藤, colour = red, number = 3355 
owner = 佐藤, colour = yellow, number = 845

メンバー関数[編集]

クラスにはインスタンスの持つメンバーとして関数も持つことができます。この関数のメンバーのことをメンバー関数と言います。 メンバー関数を定義するには、関数を定義したようにクラス定義中に定義します。する方法とクラス定義中ではメンバー関数の宣言だけ行い、クラス定義の外で実装する方法があります。

メンバー関数の定義と実行
#include <iostream>
using namespace std;

class Car {
  const char* owner;
  const char* colour;
  int number;

public:
  Car(const char* owner, const char* colour, int number) : owner(owner), colour(colour), number(number) {}
  void show() {
    cout << "owner = " << owner
         << ", colour = " << colour
         << ", number = " << number << endl;
  }
};

int main() {
  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>
using namespace std;

class Car {
  const char* owner;
  const char* colour;
  int number;

public:
  Car(const char* owner, const char* colour, int number) : owner(owner), colour(colour), number(number) {}
  void show() {
    cout << "owner = " << owner
         << ", colour = " << colour
         << ", number = " << number << endl;
  }
  const char* Owner() const { return owner; }
  const char* Owner(const char* newOwner) { return owner = newOwner; }
  const char* Colour() const { return colour; }
  const char* Colour(const char* newColour) { return colour = newColour; }
  int Number() const { return number; }
  int Number(int newNumber) { return number = newNumber; }
};

int main() {
  auto a = Car("山田", "blue", 1234);
  cout << "a.owner = " << a.Owner()
       << ", a.colour = " << a.Colour()
       << ", a.number = " << a.Number() << endl;
  a.Owner("鈴木");
  a.Colour("pink");
  a.Number(4423);
  cout << "a.owner = " << a.Owner()
       << ", a.colour = " << a.Colour()
       << ", a.number = " << a.Number() << 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 <iostream>
using namespace std;
#include <cmath>

const double PI = acos(-1);

class Point {
  double x, y;

public:
  Point(double x, double y) : x(x), y(y) {}
  void show() {
    cout << "(" << x
         << ", " << y
         << ")" << 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; 
  }
};

int main() {
  auto pt = Point(3.0, 4.0);
  pt.show();
  cout << pt.length() << endl;
  pt.length(10.0);
  cout << pt.length() << endl;
  pt.show();
  cout << pt.angle() << endl;
  pt.angle(PI/4);
  cout << pt.angle() << 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を混同する可能性があるので、意思の疎通に齟齬が生じないよう気をつけましょう。


[TODO:コピーコンストラクター・代入演算子のオーバーロード]

オブジェクトと名前空間[編集]

C++では、coutcinなどのオブジェクトも、名前空間で分類されています。

たとえばオブジェクトcoutは、正式にはstd::coutというオブジェクトです。つまり、coutはstd名前空間に所属しています。 『C++/はじめに』では説明を省略していました。 using namespace std; とは、「名前空間が省略された場合、グローバル名前空間に識別子がなかったら std 名前空間の中から識別子を探す」という意味の宣言だったのです。

stdとは、標準ライブラリ(standard library)を意味する略語です。

cinも同様に、std::cinというオブジェクトです。つまり、cinはstd名前空間に所属しています。

スコープ演算子::をつかうことから分かるように、std名前空間に含まれるオブジェクトとして、coutcinを扱っています。

書式は、

名前空間名::識別子

です。

プログラミング言語に用意されている 標準ライブラリのオブジェクトは、膨大にあるので、名前空間によって分類する必要があり、使用時には本来、その名前空間名をつける必要があるのです。 なぜなら、そうしないと、もし、自分で新しく関数を定義しようと思ったときに、名前につけようと思ってた識別子が、すでに標準ライブラリで使われている可能性があるからです。

このような工夫を、(使いたい名前が既に他の用途で使われてしまっているような事態をふせぐための工夫を)「名前の衝突をふせぐ」などと、言います。

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つのクラスが共通するクラスから派生していた場合の問題(ダイヤモンド継承問題)など、多くの問題が多重継承にはあるので、「多重継承が設計上の最適な選択か?」を自問してから使うようにしましょう。

実装例[編集]

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
  1. ^ クラスベースのオブジェクト指向ブログラミング言語の他に、プロトタイプベースのオブジェクト指向ブログラミング言語があり Self, JavaScript, Lua が代表的です。