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

< 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

「継承」とは編集

すでに存在するクラスをもとにして、新しい別のクラスを作成することが、クラスの継承です。

細かく言えば、「継承」(けいしょう、inheritance)とは、既に存在しているクラスについての定義文をもとにして、新しいクラスをつくる事を、クラスの「継承」といいます。


クラスBをもとにして、新しくクラスDをつくるとき、

class D : public B {

};
「クラスDをクラスBをpublic継承(公開継承)している」と読み、クラスBはクラスDの基底クラスと呼び。クラスDを派生クラスと呼びます。

さきほどのクラスの定義文の文例でも、コロン記号の前にある「class D」について、内容は「public B {}」であると説明しているわけです。

とにかく、クラスの継承の仕方は、次のようになります。

class 派生クラス名 : public 基底クラス名 {

};


実際に継承を使って単純なコードを書いて実行して見ましょう。

コード例

#include <iostream>
using namespace std;

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


class Info : public Car{
public:

  const char* national; // 国籍

private:
  // 特になし
};



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;
  

  auto d = Info();
  d.owner = "山田d";
  d.colour = "blue d";
  d.number = 12345;
  
  d.national = "Japan";
  

  cout << "d.owner = " << d.owner
       << ", d.colour = " << d.colour
       << ", d.number = " << d.number 
       << ", d.national = " << d.national 
       << endl;
             
   cout << "a.owner = " << a.owner
       << ", a.colour = " << a.colour
       << ", a.number = " << a.number << endl;
  
  
}

実行結果

a.owner = 山田, a.colour = blue, a.number = 1234
d.owner = 山田d, d.colour = blue d, d.number = 12345, d.national = Japan
a.owner = 山田, a.colour = blue, a.number = 1234


なお、上記コードは実務的な意味はないです。敬称の仕組みを知るための実験的なコードです。


Info クラスのブロック内にはどこにも colour メンバも number メンバもないのに、main関数でのインスタンスの宣言後( auto d = Info();の部分) に代入可能です。

このように、継承を使うことで省略できますし、また、タイプミスなどでnumberと間違えて「nunber」(3文字目nが違う)などを作ってしまうようなミスを防げます。

また、上記コード例のようにインスタンスa と継承されたインスタンスdは別物なので、dに何を代入しようがaの内容はそのままです。


ほか

基底クラス側でprivateにアクセス指定されていたメンバーは、派生クラスからはアクセスできません。また、privateをつけると、外部の一般の関数からも、アクセスできません。privateをつけると、main()関数からも、直接はアクセスできません。要するに、原則的に、private指定されたメンバーには、外部からはアクセスできません。

private指定されたメンバーに、main関数や他の一般の関数からアクセスしようとすると、エラーになります。

なお、クラス定のアクセス指定子を省略すると、privateとして扱われます。


基底クラス側でpublicにアクセス指定されていたメンバーは、派生クラスからアクセスできます。なお、他のクラスやグローバルな関数からも、publicにアクセス指定されたメンバーにはアクセスできます。

基底クラス側でprotectedというふうにアクセス指定すると、他のクラスやグローバルな関数からはアクセスできなくなりますが、しかし、派生クラスのメンバーからはアクセスできます。

つまり、 「protected」とは何かというと、自分自身のクラス以外とのアクセスについては、派生クラスのメンバーからのアクセスだけを許すためのアクセス指定こそが protected です。


なお、private, public, protected などをまとめて、アクセス指定子といいます。


ほか2

C++ではクラスを用いずとも構造体でも継承を使うことは可能です。

#include <iostream>
using namespace std;

struct Car {

  const char* owner;
  const char* colour;
  int number;
};


struct Info : public Car{

  const char* national; // 国籍
};



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;
  
  auto d = Info();
  d.owner = "山田st";
  d.colour = "blue st";
  d.number = 555;
  
  d.national = "Japan st";
  

  cout << "d.owner = " << d.owner
       << ", d.colour = " << d.colour
       << ", d.number = " << d.number 
       << ", d.national = " << d.national 
       << endl;
  
}

実行結果

a.owner = 山田, a.colour = blue, a.number = 1234
d.owner = 山田st, d.colour = blue st, d.number = 555, d.national = Japan st


多重継承編集

C++の継承では、派生クラスは複数の基底クラスを持つことができます。

つまり、クラスDの派生元が、クラスBとクラスEというふうに、複数個の基底クラスがあるような継承も、可能であります。

class D : public B, public class E {

};

上記のコードで、クラスBとクラスEが、基底クラスであります。クラスDは、派生クラスであります。

このように、複数個の派生元によって行われる継承のことを多重継承という。

多重継承のコードの記述では、複数個ある基底クラスはカンマ「,」で区切って、複数記述します。


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