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

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++
ナビゲーションに移動 検索に移動

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

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

クラスは、データや機能をまとめて提供する手段です。 新しいクラスを定義することは、新しい型を定義することを意味し、その型の新しいインスタンスを作成することができます。 各クラスのインスタンスには、その状態を維持するためのメンバーを持つことができます。 また、クラスのインスタンスには、その状態を変更するための(クラスによって定義された)メソッドを持つことができます。

おおまかな仕組み[編集]

オブジェクトとメンバー[編集]

たとえば、ある車が山田くんちの車だとして、データ名を「yamadacar」としましょう。車には、ナンバープレートがあります。そのナンバープレート番号(変数名:「number」)が文字列"1234"だったとしましょう。

このとき、yamadacar.numberという表記により、yamadacarのナンバープレート番号を表現するのが、クラスをもちいた場合の記法です。(どうやって、「クラス」の機能を利用できる状態にするかは、あとの節で説明します。)

「yamadacar」と「number」の間にある「.」は、ドット記号「.」です。

この派生元のデータ「yamadacar」は、クラスのオブジェクト名というものです。

そして、派生先のデータ「number」は、クラスのメンバーというものです。


さて、車が、3台あったとしましょう。

1台目の車(変数名:yamadacar)のナンバープレート番号が「1234」だとしましょう。
2台目の車(変数名:itoucar)のナンバープレート番号が「7722」だとしましょう。
3台目の車(変数名:nakanocar)のナンバープレート番号が「8181」だとしましょう。

このような場合、itoucar.numberという表記で、2台目の車のナンバープレート番号「7722」の事を表現できます。

このように、あるデータをもとにして(例の場合は「車」)、別の変数が派生して存在するとき(例の場合は「ナンバープレート番号」)、クラスを使うと、コード内にある、階層性のあるデータと、そのデータから派生した変数とを、階層化できます。そして、このように、クラスをもちいて、データと変数を階層化することにより、コード内容を把握しやすくなります。

そして、「yamadacar」も「itoucar」も「nakanocar」も、すべて車なので、「Car」というクラス名に所属させます。

手順[編集]

※ 前節のように、「車」で説明するので、先に前節をお読みください。

手順として、まず、クラスとメンバーとの関係を定義させるため、次のようにコードを書きます。

class Car{
   int number;
};

つまり、クラス名の定義時に いっしょにメンバーが定義されます。

そして、次のコード

Car yamadacar;

のように、「クラス名 オブジェクト名」のような書式により、クラスと各オブジェクトとの関係を定義します。 このように、あらかじめ、Carというクラス名と内容(メンバー)を定義したあとで、Car yamadacar;と宣言することにより、yamadacarをクラスCarのオブジェクトとして定義できます。


オブジェクト定義のこのような定義の記法については、クラス名を、まるで新しい型のように解釈すると、あたかも変数の宣言と似ていることに、注目してください。このため書籍によっては、クラスについての説明で、「変数に対する型のようなもの」のような解説をしている書籍もあります。

ともかく、上記のコードのようにして、それぞれのオブジェクトをクラスに所属させる事ができます。

よって、まとめると、まず、

class Car{
   int number;
};

int main()
{
   Car yamadacar;
   
}

のように、記述する必要があります。


そして、Car yamadacar; 以降に、

   yamadacar.number = 1234;

と記述すれば、各オブジェクトのメンバーに、具体的な数値を代入できます。(上記のコードの例の場合は値1234を代入している。)

まとめると、

class Car{
   int number;
};

int main()
{
   Car yamadacar;
   yamadacar.number = 1234; // このコードは不完全なので、この箇所でコンパイルエラーになります。
}

のように、記述する必要があります。このように、オブジェクトの定義よりも先に、クラス名といっしょにメンバーが定義されます。

つまり、クラス機能を利用するために行う定義・宣言の順序は、

1番目: クラス名の定義、および、クラスの構造としてメンバーを定義。
2番目: 各オブジェクトの定義。(そのオブジェクトを、対応するクラスと関連づける方法で、オブジェクトの構造を定義する。)
3番目: 各オブジェクトのメンバーへの、数値の代入など。

という順番になります。

メンバーについて言うと、具体的な個別のデータとしてのメンバーについては、メンバーはオブジェクトから派生したものになりますが、しかし、メンバーの定義の時では、メンバーはクラスの構造として記述したものになります。

そして、オブジェクトを定義する際に、そのオブジェクトが、どのクラス名のクラスから派生されたのかを宣言することにより、オブジェクトの構造を定義します。

こうすることにより、もしもオブジェクトが何個も大量にあるときに、オブジェクトの定義の手間を、単にクラスに当てはめるだけでオブジェクトを定義できるので、オブジェクト定義の手間を簡略化できます。

コード例[編集]

次に示すコードは、実際に動作するコードです。

#include <iostream>
using namespace std;

class Car{
    public:
        int number;
        double fuel;
};

int main()
{
    Car yamadacar;
    yamadacar.number = 1234;

    Car itoucar;
    itoucar.number = 7722;
    cout << "山田くんちの車の番号は" << yamadacar.number << endl;
    return 0;
}

上記のコードをコンパイルして実行すると、

山田くんちの車の番号は1234

と表示します。

「public」など未紹介のキーワードについては、のちの節で後述します。とりあえず、本書を読み進めてください。

なんのために、クラスを使うか?[編集]

実は、節「おおまかな仕組み」で説明した仕組みは、「構造体(struct)」の仕組みと、ほぼ同じです(構造体はディフォルトのアクセス指定子が publicclass です。classのディフォルトのアクセス指定子は private です)。 メンバーをprivateとした場合、クラスの外部からは、そのメンバーが存在しないものとして扱われるので、クラスの外部からは、そのクラスのそのメンバーを参照する事ができません。 クラスのメンバーは変数の他、関数をメンバーとする事もできます。 あるクラスのメンバーとなっている関数のことを「メンバー関数」といいます。メンバー関数の内容が書かれている記述場所は、クラスの宣言部の外部です。 このように、クラスの宣言の外部に、メンバー関数の内容は記述されていますが、メンバー関数は非公開設定されたメンバーにアクセスすることができます。

では、なんのために、そんな外部からのアクセスのできない非公開メンバーをわざわざ用意するのかというと、その理由は、クラス宣言内部で、外部からアクセスの可能な公開メンバーのデータを決定するときに、ほかの外部アクセス不可能な非公開メンバーのデータの内容にもとづいて、外部アクセス可能な公開メンバーのデータを決定する場合があるからです。つまり、たとえメンバーを外部アクセス不可能な非公開メンバーとして決定していても、そのクラス宣言の内部およびメンバー関数内部だけなら、その外部アクセス不可能な非公開メンバーでも参照することができます。

逆にいうと、もし、あなたが、こういう事(クラス宣言やメンバー関数の内部だけで非公開メンバーを参照をしたい場合)をする必要のない処理をしたい場合には、わざわざクラスを宣言する必要は少なくて、構造体の宣言ですみます。

構造体は、C言語にも昔から存在している機能ですが、C++においては「ディフォルトのアクセス指定子が public なクラス」と再定義されており、C++の struct は(仮想メンバー関数を含む)メンバー関数を持つことができるなどC言語とは機能的に別物です。

さて、クラス機能で、あるクラスのメンバーが、外部アクセスのできる状態であることの宣言方法は「public」で宣言します。いっぽう、あるクラスのメンバーが、外部アクセスのできない状態であることの宣言方法は「private」で宣言します。(この他にも「protected」という状態も宣言できるのだが、「継承」などの概念が必要になるので、説明を、あとの節に回し、この節では説明を省略する。)


なお、われわれのいう「外部アクセス可能/不可能の設定機能」のことを、プログラミング業界の専門用語で、「カプセル化」といいます。

つまり、カプセル化したい要素がない場合、クラス機能を使わずとも「構造体」機能でも済んでしまうでしょう[2]

複数個のメンバーの場合[編集]

クラスのメンバーは、複数個あっても、かまいません。

まず、前節までに作成したコードを再掲します。

class Car{
   int number;
};

メンバーとは、上記のコードでは、「number」がメンバーです。

では、複数個のメンバーがある場合を考えます。

クラスのメンバーとして、さらに、ある車の燃料の量(変数名「fuel」)があったとしましょう。

そのような場合、コードは単に、

class Car{
   int number;
   double fuel;
};

このように定義するだけです。

クラスの中のクラスにアクセスしたい場合[編集]

下記のようになります。

#include <iostream>
using namespace std;

class Car {
  public:
    int number;
    double fuel;

    class inner {
      public:
        int test1;
        double test2;
    };
};

int main() {
    Car::inner yamadayear;
    yamadayear.test1 = 57;

    cout << "山田さん家のおじさんの年齢は" << yamadayear.test1 << endl;
    return 0;
}

実行結果

山田さん家のおじさんの年齢は57

上記コードのようにクラス宣言した場合、下記のような方法ではアクセスできません。

// エラーになります。
    Car yamadayear;
    yamadayear.inner.test1 = 57;

下記コードのような方法もあります。外側クラス内のintやdoubleにも変数が宣言されているのと同様に、内側クラスを宣言する際に変数を宣言することで、 intやdoubleと同じようにドット演算子でアクセスできます。

#include <iostream>
using namespace std;

class Car {
  public:
    int number;
    double fuel;

    class inner {
      public:
        int test1;
        double test2;
    } v; // 変数を追加
};

int main() {
    Car yamadaClass;
    yamadaClass.v.test1 = 45;

    cout << "山田さん家の奥さんの年齢は" << yamadaClass.v.test1 << endl;
    return 0;
}

実行結果

山田さん家の奥さんの年齢は45

メンバー関数[編集]

クラスを定義するとき、メンバーには、変数メンバーの他にも、関数をメンバーとする事もできます。

あるクラスのメンバーとなる関数のことを関数メンバーといいます。「関数メンバー」は、もちろん、関数であります。

では、「車」の例で、メンバー関数を学びましょう。まず、「車のナンバーは◯◯です。」(◯◯には、その車のナンバープレートの番号(円数名「number」)が入る。)と表示する関数を、クラスCarに追加しましょう。

class Car{
   int number;
   void kansuu();
};

上記のコードで、「kansuu()」がメンバー関数です。

そして、メンバー関数の内容を定義は、クラスの定義部の外で行うことになります。 メンバー関数の内容を定義するコードの記述方法は、下記のようになります。

void Car::kansuu()
{
   cout << "車のナンバーは" << number << "です" << endl;
}


さて、上記のコード中にもある演算子「::」は、スコープ演算子といいます。コロン「:」を2回、つづけて入力するだけです。(直接入力。)

メンバー関数の書式は「クラス名::メンバー関数名()」のようになります。


さて、上記のコードの例では、クラスは1つだけでした。しかし一般に、プログラムによっては、複数個のクラスを定義するプログラムもあります。

そして、クラスが複数個あるプログラムの場合、異なるクラスにおいて、同名のメンバーを定義する事が、できます。

たとえば、あるプログラムで、クラスCarとクラスuntensyu(運転手)を定義して、両方のクラスとも、メンバーとしてnumberを使うこともできます。

また、メンバーが関数の場合(つまりメンバー関数では)、複数個の異なるクラスにある、同名のメンバー関数の内容を、まったく別々の内容にできます。


そのため、メンバー関数の内容を定義するさい、まず、どのクラスに所属するメンバー関数を定義しようとしてるかを、コードに記述する必要があります。

そのため、まず、クラス名を書いてから、直後にスコープ演算子「::」に書いて、そのうしろにメンバー関数名を書く必要があります。

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

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

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

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

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

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

書式は、

名前空間名::識別子

というようになっています。

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

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

コンストラクタとデストラクタ[編集]

コンストラクタ[編集]

オブジェクトの作成時に、自動的に、ある関数を呼び出すことができます。

オブジェクトの定義時に自動的に呼び出される関数のことをコンストラクタといいます。

クラスのメンバー関数を定義するときに、メンバー関数名を、クラス名と同じ名称にすると、そのメンバー名の内容が、コンストラクタの内容になります。

つまり、コード

class Car{
   int number;
   void kansuu();
   Car();
};

の、メンバー関数 Car() が、コンストラクタです。

コンストラクタにアクセスする場合は、メンバー関数へのアクセスと同様にスコープ演算子「::」を使うことになります。

たとえば、上記のクラスCarのコンストラクタを記述する場合なら、

Car::Car()

と記述することにより、クラスCarのコンストラクタを記述できます。

例として、クラスCarのコンストラクタの内容を、初期値としてメンバー変数に0を代入するとした場合、下記のコードのようになります。

Car::Car()
{
   number = 0;
}

コンストラクタも関数の一種なので、コンストラクタの内容の記述方法は、関数とほぼ同様の記述ですが、しかし、戻り値(冒頭のvoidなど)がない事に注目してください。

そして、オブジェクトが生成されたときにコンストラクタが呼び出されるわけですから、つまり、

Car yamadacar;

または

Car itoucar;

などによってyamadacarやitoucarを生成したときに、コンストラクタが自動的に呼び出されます。


「継承」とは[編集]

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

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


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

class D : public B{

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

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


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

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

};


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

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

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


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

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

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


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

※ 備考[編集]

よそのプログラミング言語では、基底クラスのことを「親クラス」といい、派生クラスのことを「子クラス」という場合もあります。

ほかにも、あるプログラミング言語では、基底クラスのことを「スーパークラス」といい、派生クラスのことを「サブクラス」という場合もあります。

継承に関するクラスの名前
派生もとのクラス 派生さきのクラス
基底クラス 派生クラス
親クラス 子クラス
スーパークラス サブクラス

しかし、C++言語では、「基底クラス」と「派生クラス」という言い方が使用されるのが一般的です。

多重継承[編集]

継承では、派生元のクラス(基底クラス)は、複数個であってもいい。

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

class D : public B, public class E{

};

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

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

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

  • 多重継承で、異なる基底クラスに、同名のメンバーがあるとき
(※ 調査中)

スコープ演算子::で、「クラス名::メンバー名」の順序で記述することにより、区別します。


入れ子クラス[編集]

クラスの中に、別のクラスを入れることができる。このような手法を「入れ子クラス」(いれこ クラス)という。

(コード例)

#include <iostream>
using namespace std;

class Car {
public:
  class inner {
  public:
    int number;
    double fuel;
  };
};

int main() {
  Car::inner yamadacar;
  yamadacar.number = 1234;

  Car::inner itoucar;
  itoucar.number = 7722;
  cout << "クラスの入れ子" << endl;
  cout << "山田くんちの車の番号は" << yamadacar.number << endl;

実行結果

クラスの入れ子
山田くんちの車の番号は1234


内側にあるほうのクラスのことを「内部クラス」とか「インナークラス」(inner class)という。上記のコードでは「inner」が内部クラスである。

いっぽう、外側にあるほうのクラスを「外部クラス」とか「アウタークラス」(outer class)などという。上記コードでは「Car」が外部クラスである。

「入れ子」(いれこ)のことを英語で nest ネストというので、もしネットで調べたいときは「nest class」(ネストクラス)などで調べれば良い。


  • 構造体の入れ子

なお、構造体でも同様に、ある構造体のなかに、別の構造体を入れることができる(構造体はディフォルトでメンバーがpublicなクラスです)。

#include <iostream>
using namespace std;

struct Car {
  struct inner {
  public:
    int number;
    double fuel;
  };
};

int main() {
  Car::inner yamadacar;
  yamadacar.number = 1234;

  Car::inner itoucar;
  itoucar.number = 7722;
  cout << "構造体の入れ子" << endl;
  cout << "山田くんちの車の番号は" << yamadacar.number << endl;
}

実行結果

構造体の入れ子
山田くんちの車の番号は1234

「ゲッター」と「セッター」[編集]

データーメンバーをpublicにするのはカプセル化の理想・理念に反します。 そこでデーターメンバーをprivateとし、データーメンバーの値を参照するためのメンバー関数(ゲッター)とデーターメンバーを変更するためのメンバー関数(セッター)を用意し内部構造の隠蔽を図ります。 ゲッターとセッターにより内部構造の隠蔽のほか、変換や受入検査などを行うことができるようになります。 演算子オーバーロードでゲッター・セッターを実装すると、クラスを利用するコードはゲッター・セッターを使っていることすら意識しないですみます。

ゲッターとセッターの例
#include <iostream>

class TestClass {
private:
  int priv;

public:
  TestClass(const int i) : priv(i) {}         // Default constructor
  TestClass(TestClass &tc) : priv(tc.priv) {} // Copy constructor
  const int value() { return this->priv; }    // Accessor (ゲッター)
  void value(const int i) { this->priv = i; } // Mutator (セッター)
};

int main() {
  TestClass x(12);                     // Default constructor call
  auto y = x;                          // Copy constructor call
  std::cout << x.value() << std::endl; // Accessor call
  x.value(42);                         // Mutator call
  std::cout << x.value() << std::endl; // Accessor call
  std::cout << y.value() << std::endl; // Accessor call
}
実行結果
12
42 
12
Accessors and Mutators

日本語圏では、データーメンバーを参照するためのメンバー関数をゲッター、データーメンバーを変更するためのメンバー関数をセッターと呼び総称してアクセサーと呼びます。 これに対し、英語圏ではデーターメンバーを参照するためのメンバー関数をAccessors、データーメンバーの値を変更するためのメンバー関数をMutatorsと呼びます。 特に、アクセサーAccessorsを混同する可能性があるので、意思の疎通に齟齬が生じないよう気をつけましょう。

  1. ^ クラスベースのオブジェクト指向ブログラミング言語の他に、プロトタイプベースのオブジェクト指向ブログラミング言語があり Self, JavaScript, Lua が代表的です。
  2. ^ 実際は構造体でも、setter/getterなどのaccessorを定義できるのでカプセル化が行えないわけではない。