C++/クラスの定義や継承
オブジェクト指向[編集]
オブジェクト指向とクラス[編集]
オブジェクト指向プログラミングとは、データや手続きを統一的に記述することで、プログラムの内容を理解しやすくするプログラミング手法です。
クラスはクラスベースのオブジェクト指向の中核となる概念で[1]、 様々な言語でクラスを定義することが出来ます。 C++以外ではJava, Python, Ruby, Swift, Scala, Kotlin などがクラスベースのオブジェクト指向言語として有名です。
単純に、クラスベースのオブジェクト指向プログラミングに興味があるのなら、PythonやRubyなどの動的言語のクラスベースのオブジェクト指向プログラミングの方が、学習が容易と期待できるので、動的言語も検討するのも良いでしょう。
クラスの基本[編集]
クラス(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> class Point { int x, y; public: explicit Point(int _x = 0, int _y = 0) : x{_x}, y{_y} { std::cout << "Point: コンストラクタ呼び出し。" << std::endl; } ~Point() { std::cout << "Point: デストラクタ呼び出し。" << std::endl; } [[nodiscard]] auto X() const -> int { return x; } auto X(int _x) -> int { return this->x = _x; } [[nodiscard]] auto Y() const -> int { return y; } auto Y(int _y) -> int { return this->y = _y; } }; class Square : public Point { int wh; public: explicit Square(int _x = 0, int _y = 0, int _wh = 0) : Point::Point(_x, _y), wh{_wh} { std::cout << "Square: コンストラクタ呼び出し。" << std::endl; } ~Square() { std::cout << "Square: デストラクタ呼び出し。" << std::endl; } [[nodiscard]] auto get_wh() const -> int { return wh; } auto set_wh(int _wh) -> int { return this->wh = _wh; } }; auto main() -> int { Square sq; sq.X(1234); sq.Y(5678); sq.set_wh(9012); std::cout << "sqのxの値は" << sq.X() << std::endl; std::cout << "sqのyの値は" << sq.Y() << std::endl; std::cout << "sqのwhの値は" << sq.get_wh() << std::endl; }
- 実行結果
Point: コンストラクタ呼び出し。 Square: コンストラクタ呼び出し。 sqのxの値は1234 sqのyの値は5678 sqのwhの値は9012 Square: デストラクタ呼び出し。 Point: デストラクタ呼び出し。
- クラス
Point
を継承して新たにクラスSquare
を定義している。 - クラス
Point
のメンバー変数x
にアクセスするメソッドX()
及びメソッドX()
が、Square
でも使用できる。
STLは継承を使っていない |
STL(Standard Template Library)は、継承を使っていません。STLは、テンプレートメタプログラミングとポリシーベースの設計を中心に構築されています。これにより、柔軟性やパフォーマンスを高めながら、継承に伴うオーバーヘッドや複雑さを避けることができます。
STLのコンテナやアルゴリズムは、テンプレートを使用して実装されており、これによりジェネリックなプログラミングが可能になります。また、ポリシーベースの設計を採用することで、コンテナやアルゴリズムの振る舞いを柔軟にカスタマイズすることができます。 継承を使わないことにより、STLはシンプルで効率的な設計を実現し、様々なプログラミング環境や要件に対応できるようになっています。 |
仮想関数[編集]
純粋仮想関数[編集]
- 純粋仮想関数の例
#include <iostream> #include <memory> // std::unique_ptr #include <string> #include <tuple> // std::tuple class Point { int x, y; public: explicit Point(int x = 0, int y = 0) : x(x), y(y) {} ~Point() = default; [[nodiscard]] auto to_string() const -> std::string { return std::string("Point(x: " + std::to_string(x) + ",y: " + std::to_string(y) + ")"); } }; class Shape { protected: Point pt; // Shape は Point を(継承ではなく)包含している public: explicit Shape(int x = 0, int y = 0) : pt(x, y) {} virtual ~Shape() = default; [[nodiscard]] virtual auto area() const -> int = 0; // 純粋仮想関数 [[nodiscard]] virtual auto to_string() const -> std::string { return pt.to_string(); } }; class Square : public Shape { int wh; public: explicit Square(int x = 0, int y = 0, int wh = 0) : Shape::Shape(x, y), wh(wh) {} ~Square() override = default; [[nodiscard]] auto to_string() const -> std::string override { return std::string("Square(" + pt.to_string() + ", wh: " + std::to_string(wh) + ")"); } [[nodiscard]] auto area() const -> int override { return wh * wh; } }; class Rectangle : public Shape { int w, h; public: explicit Rectangle(int x = 0, int y = 0, int w = 0, int h = 0) : Shape::Shape(x, y), w(w), h(h) {} ~Rectangle() override = default; [[nodiscard]] auto to_string() const -> std::string override { return std::string("Rectangle(" + pt.to_string() + ", w: " + std::to_string(w) + ", h: " + std::to_string(h) + ")"); } [[nodiscard]] auto area() const -> int override { return w * h; } }; class Circle : public Shape { int r; public: explicit Circle(int x = 0, int y = 0, int r = 0) : Shape::Shape(x, y), r(r) {} ~Circle() override = default; [[nodiscard]] auto to_string() const -> std::string override { return std::string("Circle(" + pt.to_string() + ", r: " + std::to_string(r) + ")"); } [[nodiscard]] auto area() const -> int override { return 3.14 * r * r; } }; auto main() -> int { auto shapes = std::tuple{std::make_unique<Square>(12, 34, 56), std::make_unique<Rectangle>(34, 56, 100, 50), std::make_unique<Circle>(56, 78, 80)}; std::apply( [](auto&... args) { ((std::cout << args->to_string() << ": area() = " << args->area() << std::endl), ...); }, shapes); return 0; }
- 実行結果
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、Rectangle、Circle)を表すクラスと、それらの図形を操作するためのメソッドを提供しています。各図形は、座標を表すPointクラスを含んでおり、継承ではなく包含関係を持っています。また、Shapeクラスは純粋仮想関数を持ち、派生クラスでそれぞれの図形の面積を計算するメソッドを実装することを強制します。
main()
関数では、各図形のインスタンスをstd::tuple
に格納し、std::apply
を使用して各図形のto_string()
メソッドとarea()
メソッドを呼び出しています。これにより、コードをよりシンプルにして、柔軟性を高め、冗長性を排除しています。
クラス・構造体・共用体の比較[編集]
クラスと構造体の比較[編集]
クラスと構造体の違いは、クラスと構造体がデフォルトでメンバーのアクセス指定子が異なる点にあります。クラスでは、デフォルトでメンバーのアクセス指定子がprivate
となり、外部からのアクセスが制限されます。一方、構造体では、デフォルトでメンバーのアクセス指定子がpublic
となり、外部からのアクセスが可能となります。
この違いは、クラスと構造体が異なる用途で使用されることが多い理由の1つです。クラスは、オブジェクト指向プログラミングの概念に基づいて、データとそのデータを操作する関数を組み合わせてまとめるために使用されます。そのため、データの隠蔽(カプセル化)が重要となり、デフォルトでprivate
となるのが一般的です。一方、構造体は、関連するデータをグループ化するために使用され、外部からのアクセスが容易であることが求められる場合に利用されます。
クラスと共用体の比較[編集]
一方、共用体にはいくつかの制限があります。
- 継承したりされたりできない。
- メンバーにstaticを指定できない。
- コンストラクタ・デストラクタがあるオブジェクトをメンバーにできない。
- コンストラクタ・デストラクタは持てる。
- 仮想メンバー関数を持つことはできない。
脚註[編集]
- ^ オブジェクト指向言語には、クラスベースのオブジェクト指向言語と、プロトタイプベースのオブジェクト指向言語があります。プロトタイプベースのオブジェクト指向言語には、Self・JavaScript・Luaがあります。
- ^ プログラミングにおいて、ミューテーターメソッドは、変数の変更を制御するために使用されるメソッドです。また、セッターメソッドとしても広く知られている。セッターはしばしばゲッターを伴い(共にアクセッサーとも呼ばれる)、プライベートメンバ変数の値を返す。
- ^ 3.0 3.1 フレンド関数・フレンドメンバー関数・フレンドクラスは例外です。
- ^ フレンド関数は、メンバー関数ではないので this はありません。