C++/クラスの定義や継承

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

オブジェクト指向[編集]

オブジェクト指向とクラス[編集]

オブジェクト指向プログラミングとは、データや手続きを統一的に記述することで、プログラムの内容を理解しやすくするプログラミング手法です。

クラスはクラスベースのオブジェクト指向の中核となる概念で[1]、 様々な言語でクラスを定義することが出来ます。 C++以外ではJava, Python, Ruby, Swift, Scala, Kotlin などがクラスベースのオブジェクト指向言語として有名です。

単純に、クラスベースのオブジェクト指向プログラミングに興味があるのなら、PythonRubyなどの動的言語のクラスベースのオブジェクト指向プログラミングの方が、学習が容易と期待できるので、動的言語も検討するのも良いでしょう。

クラスの基本[編集]

クラス(class)とは、データ構造であり、データと関数をメンバーとして持ち、実体(インスタンスと呼びます)を生成する時(インスタンス化)の雛型となります。

クラスの宣言[編集]

C++でクラスを宣言するには、キーワードclassを用いて次のように記述します。

クラス定義の構文
class クラス名 {
	// 非公開データメンバーと非公開メンバー関数
public:
	// 公開データメンバーと公開メンバー関数
};

class内の関数をメンバー関数(しばしばメソッド)、変数のことをデータメンバー(しばしばメンバー変数)と呼び、総称してメンバーと呼びます。 メンバーにはオブジェクトの外からアクセスできない非公開のもの private と、アクセスできる公開のもの public があります(継承の説明まで、protected の説明は留保)。 classのメンバーはデフォルトで非公開 private です。

メンバー関数がインライン化できない場合など、クラスの宣言の外にその定義を記述する事ができる。

クラス定義はヘッダーに、メソッドの実装は通常のソースコードに記述することになります。

その場合、メンバー関数の定義は、関数名の前に「クラス名::」を付けます。 「::(コロン2文字)」のことをスコープ解決演算子(scope resoluthion operator)と呼ぶ。 次のように記述する。

戻り値の型 クラス名::関数名(仮引数の並び) {
	// 関数の本体
};
メンバー関数の定義をクラスの宣言の中に書く例
Point
#ifndef POINT_H
#define POINT_H

// Point: 二次元座標上の1点
class Point {
  int x, y;
public:
  inline int X() const { return x; } // 非破壊的なメソッドは const で修飾します
  inline int X(int x){ return this->x = x; }
  inline int Y() const { return y; } // 非破壊的なメソッドは const で修飾します
  inline int Y(int y){ return this->x = y; }
};
#endif
メンバー関数の定義をクラスの宣言の外に書く例
Point
#ifndef POINT_H
#define POINT_H
// Point: 二次元座標上の1点
class Point {
  int x, y;
 public:
  int X() const;
  int X(int);
  int Y() const;
  int Y(int);
};
#endif
Point.cpp
#include "Point"

int Point::X() const { return x; }
int Point::X(int x)  { return this->x = x; }
int Point::Y() const { return y; }
int Point::Y(int y)  { return this->y = y; }
上の例では,メンバー関数 X(), X(int), Y() と Y(int) を持ち,x と y という int データを持つクラスを定義しています(二次元座標上の2点を表す事を意図しています)。
クラスのメンバーはデフォルトでprivateです。
X() は x の内容を取得するメソッドで、X(int) は x を設定するメソッドです。
これらは、aがprivateであるために必要なものです。
この様な隠蔽されたメンバーへのアクセス手段を提供するメソッドを、アクセサー(ゲッター・セッター)と呼びます[2]
X()では、メソッド呼出しを行ったオブジェクトへのポインターを表す、thisが使われています(X() の様に単に x とすると仮引数の x にシャドーイングされます)。
this はポインターなので、参照には -> を使用します。
int Point::X() const { return x; }を、int Point::X() const { return this->x; }と書くことも出来ます。

オブジェクトの生成[編集]

オブジェクトの生成(インスタンス化)は次のように記述する。

クラス名 オブジェクト名;
複数のオブジェクトを生成する場合は、,(コンマ)で区切る。
この構文は 型名 変数名;そのもので、クラスはユーザー定義型・オブジェクトはクラス型の変数です。
Pointクラスのオブジェクトpt,p2を生成する例
Point pt, p2;

メンバーへのアクセス[編集]

メンバーにアクセスするには、ドット(.)演算子を用いて次のように記述する。

オブジェクト名.メンバー名
ptのXメンバー関数を呼び出す例
pt.X(1234);

オブジェクトのコピー[編集]

クラスの詳細[編集]

カプセル化[編集]

カプセル化とは、オブジェクト指向のクラスにおいて、オブジェクトのデータ構造を直接操作するのではなく、クラスに用意してメソッド集合を使って操作する事です。

これは、クラスがオブジェクトの扱うためのすべてのメソッドを提供すべきだという考えに基づいており、関数や他のクラスのメソッドに頼るのは、クラス設計に問題があるという考え方です。

カプセル化の目的 ・オブジェクトへの統一的なインターフェースを提供し体系化できる。 ・内部構造を隠蔽することで、内部構造の変更が(継承したクラスを含む)クラスを利用するコードに波及しないようにできる。 ・設計手法的に観ると、分割統治法の一種として捉えられ、問題規模の縮小が図られる。

Javaなどの言語でも、カプセル化がサポートされています。 Pythonでは、カプセル化はデコレータなどで実現され、C++やJavaとは異なる方法でサポートされています。

C++では、public, private, protectedの3つのキーワードがアクセス制御として与えられています。 privateは、同じクラスに属するメソッドからしか値が呼び出すことができなくなります[3]。 publicは、プログラムの他の部分からも呼び出せるようになります。 protectedは、クラスを継承した先から用いることが出来るようになります。 しかし、プログラムの他の部分から呼び出すことは出来ません[3]

カプセル化の例
#include <iostream>
using namespace std;

class Point {
  int x, y;
 public:
  inline int X() const { return x; }
  inline int X(int x) { return this->x = x; }
  inline int Y() const { return y; }
  inline int Y(int y) { return this->y = y; }
};

int main() {
	Point pt;
	//pt.x = 1234; //この行はコンパイルエラーになる。
	pt.X(1234);
	cout<<"ptのxの値は"<<pt.X()<<endl;
}
上の例では、クラスPointのメンバー変数xが非公開になっているため、オブジェクトの外からアクセスするには、公開になっているメソッドX()及びメソッドX()を使用しなければならない。

ポリモーフィズム[編集]

ポリモーフィズム(polymorphism;多態性・多相性の訳語が充てられることが多い)は、同じ名前で複数の異なる振る舞いを作り、それを利用するオブジェクト指向の概念である。

ポリモーフィズムは、C++では関数のオーバーローディングによって実現される。 関数オーバーローディングとは、同じ名前の関数を複数実装・使用することである。 ただし、引数の型や個数が異ならないと関数オーバーロードできない。 また、戻値の型だけが異なる場合も関数オーバーロードできない。

ポリモーフィズムの例
#include <iostream>
using namespace std;

int add(int a, int b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }

int main() {
  cout << "1+2は" << add(1, 2) << endl;
  cout << "1+2+3は" << add(1, 2, 3) << endl;
}
上の例では、2つの引数を加算するadd関数と3つの引数を返すadd関数が関数オーバーローディングで定義されています。
もしこれをC言語で行うとすれば、add2 add3 の様に別の名前にしないと、コンパイル時あるいはリンク時に関数定義の衝突あるいはシンボル定義の衝突となり、コンパイルあるいはリンクを完遂できない。

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

C++のクラスには、初期化・終了処理を自動的に行うコンストラクタ・デストラクタ関数という仕組みがあります。

コンストラクタ関数[編集]

コンストラクタ関数constructor function)あるいは単にコンストラクタは、オブジェクトが生成される際に呼び出され、主に初期化処理を記述する。

クラスにコンストラクタを追加するには、クラス宣言に以下のように記述します。

クラス名(仮引数のリスト) {
	// 実行する処理
}

コンストラクタの名前は、そのクラス名と同じです。 コンストラクタは戻り値を返さないので、戻り値の型は指定しません。 コンストラクタには、引数を指定することができます。 コンストラクタも、関数オーバーロードできます。 引数のないコンストラクタを、ディフォルトコンストラクタと呼びます。


デストラクタ関数[編集]

デストラクタ関数(destructor function)あるいは単にデストラクタは、オブジェクトがスコープを似ける時、あるいは delete 演算子で解放される際に暗黙に呼び出され、主に終了処理を記述します。

クラスにデストラクタ関数を追加するには、クラス宣言に以下のように記述します。

~クラス名() {
	//実行する処理
}

デストラクタの名前は、そのクラス名の前に~チルダーを付けます。 デストラクタは戻り値を返さないので、戻り値の型は指定しません。 デストラクタには、引数を指定することができません。 デストラクタは、関数オーバーロードできません。 引数も戻り値の型も指定しない。

コンストラクタ関数とデストラクタ関数の例
#include <iostream>
using namespace std;

class Point {
  int x, y;

 public:
  Point(int x = 0, int y = 0) : x(x), y(x) { cout << "コンストラクタ呼び出し。" << endl; }
  ~Point() { cout << "デストラクタ呼び出し。" << endl; }
  inline int X() const { return x; }
  inline int X(int x)  { return this->x = x; }
  inline int Y() const { return y; }
  inline int Y(int y)  { return this->y = y; }
};

int main() {
  Point pt;

  cout << "ptのxの値は" << pt.X() << ", "
       << "ptのyの値は" << pt.Y()
      << endl;
  pt.X(1234);
  pt.Y(5678);
  cout << "ptのxの値は" << pt.X() << ", "
       << "ptのyの値は" << pt.Y()
      << endl;
}
実行結果
コンストラクタ呼び出し。
ptのxの値は0, ptのyの値は0
ptのxの値は1234, ptのyの値は5678 
デストラクタ呼び出し。
kon

フレンド関数[編集]

フレンド関数(friend function)は、メンバー関数ではない普通の関数から、クラスのprivateまたはprotectedメンバーにアクセスしたい場合に使います[4]

この特徴だけ書くと、フレンド関数はカプセル化を破壊するように思えますが(事実その様な設計の失敗は観られますが)、メンバー関数では出来ない呼出し形式がふさわしい場合に利用価値があります。

具体的には、演算子オーバーライドがメンバー関数では出来ない呼出し形式の1つです。 ユーザー定義クラスを、ストリームに << で出力する場合

class Point {
  int x, y;

 public:
  Point(int x = 0, int y = 0) : x(x), y(y) {}
  friend ostream& operator<<(ostream& os, const Point& pt) {
    return os << "(" << pt.x << "," << pt.y << ")";
  };
};
の様に friend 関数を使って定義します。

演算子オーバーロードにおけるメンバー関数とフレンド関数の違い[編集]

メンバー関数
演算の左辺のクラスのケースにしか対応できません。
フレンド関数
右辺のオブジェクトの隠蔽されたメンバーにもアクセスできるので、右辺に対象のクラスを含む式にも対応できます。

C++11からは、キーワード friend を使った構文が拡張され、 class を指定できるようになりました。 friend 宣言された class のメソッドとstaticメンバー関数は、friend 宣言した class のメンバーに、フリーハンドにアクセスできます。

new 演算子と delete 演算子[編集]

C言語では、標準ライブラリ関数の malloc() と free() などを使って動的にメモリーを確保しました。

C言語の動的メモリー確報と開放
#include <stdio.h>  // printf(), puts()
#include <stdlib.h> // malloc(), free()

int main(void) {
  const int len = 10;
  int *p = malloc(sizeof(*p) * len);

  for (int i = 0; i < len; i++)
    p[i] = i * i;

  for (int i = 0; i < len; i++)
    printf("%d ", p[i]);
  puts("");

  free(p); 
}

C++では、同様の機能を new 演算子と delete 演算子 を使って実現します。

C言語の動的メモリー確報と開放
#include <iostream> // std::cout, std::endl
using std::cout, std::endl;

int main(void) {
  const int len = 10;
  int *p = new int[len];

  for (int i = 0; i < len; i++)
    p[i] = i * i;

  for (int i = 0; i < len; i++)
    cout << p[i] << " ";
  cout << endl;

  delete[] p;
}
いきなり配列版を例示していましたが、int* pp = new int; delete pp; の様に配列以外もnew/deleteで確保できます。

new / delete と コンストラクター[編集]

new / delete と コンストラクターの例
#include <iostream>
using std::cout, std::endl;

class Point {
  int x, y;
 public:
  Point(int x = 0, int y = 0) : x(x), y(x) { cout << "コンストラクタ呼び出し。" << endl; }
  ~Point() { cout << "デストラクタ呼び出し。" << endl; }
  void dump() {
      cout << "Point( x = " << x << ", y = " << y << " )" << endl;
  }
};

int main() {
  Point* pp = new Point; // ディフォルトコンストラクター(全パラメーターにディフォルト値を持つので)
  pp->dump();
  Point* p2 = new Point(12, 34); // アロケートしたての領域を this にしてコンストラクターを呼出し
  p2->dump();
  delete p2; // ここで暗黙にディズトラクターが呼ばれる
  delete pp; // likewise
}
実行結果
コンストラクタ呼び出し。
Point( x = 0, y = 0 )
コンストラクタ呼び出し。
Point( x = 12, y = 12 )
デストラクタ呼び出し。 
デストラクタ呼び出し。
確保と逆順に呼んでいることに深い意味はありません(アロケーターの都合を想像してみましょう)

初期化と代入[編集]

初期化と代入は、どちらも等号を使って表現できるので混同しがちですが、両者は明確に異なるものです。

コピーコンストラクターとコピー代入演算子[編集]

初期化と代入の例
T src;      // ディフォルトコンストラクター T::T() を呼出し
 
T x = src;  // C言語風の初期化:コピーコンストラクター  T::T(const T&) を呼出し
T y(src);   // 古いC++の初期化:コピーコンストラクター  T::T(const T&) を呼出し
T z{src}    // 単一初期化構文: コピーコンストラクター  T::T(const T&) を呼出し

T q;        // ディフォルトコンストラクター T::T() を呼出し
q = src ;   // コピー代入演算子:T& T::operator = (const T&) を呼出し
Tは、何らかのクラスとします。
ディフォルトコンストラクター
T::T()
初期値を与えない時に使われるコンストラクター
コピーコンストラクター
T::T(const T&)
同じクラスのインスタンス(リテラルかも知れない)を初期値としたコンストラクター
コピー代入演算子
T& T::operator = (const T&)
同じクラスのインスタンスを代入した時に使われる

もしクラスで、ディフォルトコンストラクター・コピーコンストラクター・コピー代入演算子を定義しなかった場合は、C++コンパイラは、

ディフォルトコンストラクター
なにもしない
コピーコンストラクター
構築中のインスタンスのメンバーを、初期値インスタンスのメンバーで1対1で初期化
コピー代入演算子
左辺(this側)のインスタンスのメンバーに、右辺(パラメータ側)のインスタンスのメンバーを1対1で代入

と自動的にコードを生成します。この動作は、メンバーがすべて静的な場合には好ましいものです。

コンパイラの生成するコード(イメージ)
class T {
  int a, b, c;
  // コピーコンストラクター
  T(const T& src) : a(src.a), b(src.b), c(src.c) {}

  // コピー代入演算子
  T& operator = (const T& src) {
    a = src.a;
    b = src.b;
    c = src.c;
    return *this;
  }
};

逆に言うと、いずれかのメンバーが動的な場合、具体的には new 演算子で確保された領域へのポインター(やリファレンス)が含まれていた場合、ポインター(あるいはリファレンス)の単純な複製が初期化先あるいは代入先に複製されると、2つのインスタンスが同じ領域を(不本意に)共有してしまうので好ましくありません。 この様な場合は、コンパイラのお仕着せではなく、カスタムメイドのコピーコンストラクターとコピー代入演算子を用意する事になります。


継承[編集]

継承とは、オブジェクト指向の考え方の一つで、 あるクラスの性質を受け継いだ別のクラスを定義することである。 継承によって、汎用的なクラスから特化したクラスを作ることができる。

あるクラスを別のクラスが継承するとき、 継承されるクラスを基本クラス(base class)と呼び、 継承するクラスを派生クラス(derived class)と呼ぶ。

基本クラスでは、共通の性質を定義し、 派生クラスでは、その共通の性質を受け継いだうえで、 個別の性質を定義する。 クラスの継承の仕方は次のとおりである。

class 派生クラス名 : アクセス指定子 基本クラス名 {
	//...
};

派生クラス名で派生クラスの名前をしてする。 アクセス指定子ではpublic, private, protectedのいずれかを指定する。 基本クラス名で基本クラスの名前を指定する。

継承の例
#include <iostream>
using namespace std;

class Point {
  int x, y;
 public:
  Point(int x = 0, int y = 0) : x(x), y(x) { cout << "Point: コンストラクタ呼び出し。" << endl; }
  ~Point() { cout << "Point: デストラクタ呼び出し。" << endl; }
  inline int X() const { return x; }
  inline int X(int x)  { return this->x = x; }
  inline int Y() const { return y; }
  inline int Y(int y)  { return this->y = y; }
};

class Square : public Point {
  int wh;
 public:
  Square(int x = 0, int y = 0, int wh = 0) : Point::Point(x, y), wh(wh) { cout << "Square: コンストラクタ呼び出し。" << endl; }
  ~Square() { cout << "Square: デストラクタ呼び出し。" << endl; }
  int get_wh() const { return wh; }
  int set_wh(int wh) { return this->wh = wh; }
};

int main() {
  Square sq;
  sq.X(1234);
  sq.Y(5678);
  sq.set_wh(9012);
  cout << "sqのxの値は" << sq.X() << endl;
  cout << "sqのyの値は" << sq.Y() << endl;
  cout << "sqのwhの値は" << sq.get_wh() << endl;
}
実行結果
Point: コンストラクタ呼び出し。
Square: コンストラクタ呼び出し。
sqのxの値は1234
sqのyの値は5678
sqのwhの値は9012
Square: デストラクタ呼び出し。 
Point: デストラクタ呼び出し。
クラスPointを継承して新たにクラスSquareを定義している。
クラスPointのメンバー変数xにアクセスするメソッドX()及びメソッドX()が、Squareでも使用できる。

仮想関数[編集]

純粋仮想関数[編集]

純粋仮想関数の例
#include <iostream>
using namespace std;

class Point {
  int x, y;

 public:
  Point(int x = 0, int y = 0) : x(x), y(y) {}
  ~Point() {}
  string to_string() const {
    return string("Point(x: " + ::to_string(x) + ",y: " + ::to_string(y) + ")");
  }
};

class Shape {
 protected:
  Point pt;  // Shape は Point を(継承ではなく)包含している
 public:
  Shape(int x = 0, int y = 0) : pt(x, y) {}
  ~Shape() {}
  virtual int area() const = 0;  // 純粋仮想関数
  virtual string to_string() const { return pt.to_string(); }
};

class Square : public Shape {
  int wh;

 public:
  Square(int x = 0, int y = 0, int wh = 0) : Shape::Shape(x, y), wh(wh) {}
  ~Square() {}
  string to_string() const override {
    return ::string("Square(" + pt.to_string() + ", wh: " + ::to_string(wh) + ")");
  }
  int area() const override { return wh * wh; }
};

class Rectangle : public Shape {
  int w, h;

 public:
  Rectangle(int x = 0, int y = 0, int w = 0, int h = 0)
      : Shape::Shape(x, y), w(w), h(h) {}
  ~Rectangle() {}
  string to_string() const {
    return ::string("Rectangle(" + pt.to_string() + ", w: " + ::to_string(w) + ", h: " + ::to_string(h) + ")");
  }
  int area() const override { return w * w; }
};

class Circle : public Shape {
  int r;

 public:
  Circle(int x = 0, int y = 0, int r = 0) : Shape::Shape(x, y), r(r) {}
  ~Circle() {}
  string to_string() const {
    return ::string("Circle(" + pt.to_string() + ", r: " + ::to_string(r) + ")");
  }
  int area() const override { return 3.14 * r * r; }
};

int main() {
  Shape* shapes[] = {
      new Square(12, 34, 56),
      new Rectangle(34, 56, 100, 50),
      new Circle(56, 78, 80),
  };
  for (auto shape : shapes) {
    cout << shape->to_string() << ": area() = " << shape->area() << endl;
  }
}
実行結果
Square(Point(x: 12,y: 34), wh: 56): area() = 3136
Rectangle(Point(x: 34,y: 56), w: 100, h: 50): area() = 10000 
Circle(Point(x: 56,y: 78), r: 80): area() = 20096
この例では、SquareをPointから継承するのではなく、Pointを抽象クラスShapeのメンバーとし、Square, Rectangle と Circle がShapeを継承している。
Shape は virtual int area() const と virtual string to_string() const を定義し、派生クラスでこれをオーバライドすることで図形固有の文字列表現と面積の計算を実現した。
main() では、異種配列 shapes[] を用意し、拡張forでイテレーション出来ることを示した。
このコードは、new 演算子で確保した領域を delete 演算子で開放していないのでメモリーリークがある。

クラス・構造体・共用体の比較[編集]

クラスと構造体の比較[編集]

クラスと構造体の違いは、クラスはデフォルトでメンバーがprivateであるのに対し、構造体はデフォルトでメンバーがpublibであるだけです。

struct 型名{
	// 公開関数と変数
private:
	// 非公開関数と変数
} オブジェクトリスト;

クラスと共用体の比較[編集]

一方、共用体にはいくつかの制限があります。

  • 継承したりされたりできない。
  • メンバーにstaticを指定できない。
  • コンストラクタ・デストラクタがあるオブジェクトをメンバーにできない。
  • コンストラクタ・デストラクタは持てる。
  • 仮想メンバー関数を持つことはできない。

テンプレート:Nev

脚註[編集]

  1. ^ オブジェクト指向言語には、クラスベースのオブジェクト指向言語と、プロトタイプベースのオブジェクト指向言語があります。プロトタイプベースのオブジェクト指向言語には、SelfJavaScriptLuaがあります。
  2. ^ プログラミングにおいて、ミューテーターメソッドは、変数の変更を制御するために使用されるメソッドです。また、セッターメソッドとしても広く知られている。セッターはしばしばゲッターを伴い(共にアクセッサーとも呼ばれる)、プライベートメンバ変数の値を返す。
  3. ^ 3.0 3.1 フレンド関数・フレンドメンバー関数・フレンドクラスは例外です。
  4. ^ フレンド関数は、メンバー関数ではないので this はありません。