コンテンツにスキップ

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

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

オブジェクト指向

[編集]

オブジェクト指向とクラス

[編集]

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

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

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

クラスの基本

[編集]

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

クラスの宣言

[編集]

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

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

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

メンバー関数がインライン化できない場合など、クラスの宣言の外にその定義を記述することができます。クラス定義はヘッダーに、メソッドの実装は通常のソースコードに記述します。その場合、メンバー関数の定義は、関数名の前に「クラス名::」を付けます。「::(コロン2文字)」のことをスコープ解決演算子(scope resolution 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->y = 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 を設定するメソッドです。これらは、x が 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);

オブジェクトのコピー

[編集]

オブジェクトのコピーは、既存のオブジェクトから新しいオブジェクトを生成するプロセスです。C++において、オブジェクトのコピーには主に二つの方法があります:コピーコンストラクタと代入演算子です。

コピーコンストラクタ

[編集]

コピーコンストラクタは、既存のオブジェクトを引数として受け取り、新しいオブジェクトを初期化する特別なコンストラクタです。コピーコンストラクタは次のように定義されます。

コピーコンストラクタの構文
クラス名(const クラス名& obj);

以下は、Point クラスのコピーコンストラクタの例です。

class Point {
  int x, y;
public:
  Point(int x, int y) : x(x), y(y) {} // コンストラクタ
  Point(const Point& p) : x(p.x), y(p.y) { // コピーコンストラクタ
    // xとyをコピー
  }
};

この例では、Point クラスのコピーコンストラクタは、既存の Point オブジェクトの xy の値を新しいオブジェクトにコピーします。

代入演算子

[編集]

代入演算子は、既存のオブジェクトに別のオブジェクトの値を代入するための特別なメソッドです。代入演算子は次のように定義されます。

代入演算子の構文
クラス名& operator=(const クラス名& obj);

以下は、Point クラスの代入演算子の例です。

Point& Point::operator=(const Point& p) {
  if (this != &p) { // 自己代入のチェック
    x = p.x; // xをコピー
    y = p.y; // yをコピー
  }
  return *this; // 自分自身を返す
}

この例では、Point クラスの代入演算子は、自己代入をチェックした上で、既存のオブジェクトの xy の値を引数の Point オブジェクトからコピーします。代入演算子は、オブジェクト自身を返すことで、連鎖代入(例:a = b = c;)を可能にします。

注意点

[編集]

コピーコンストラクタや代入演算子をカスタマイズする際は、以下の点に注意する必要があります。

  1. 自己代入のチェック: 代入演算子において、自己代入をチェックすることで、不正なメモリアクセスを防ぎます。
  2. 深いコピー: オブジェクトが動的に割り当てられたメモリを持つ場合、コピーコンストラクタや代入演算子で深いコピーを行う必要があります。単純な代入では、元のオブジェクトとコピー先オブジェクトが同じメモリを指してしまうためです。

このように、C++におけるオブジェクトのコピーは、特にクラスがリソースを管理する際に重要な概念です。適切なコピーコンストラクタと代入演算子の実装は、プログラムの安全性と安定性を向上させることができます。

クラスの詳細

[編集]

オブジェクト指向の基本概念

[編集]

C++のクラスは以下の主要な概念に基づいています:

  • カプセル化: データと操作をまとめ、適切なアクセス制御を提供
  • ポリモーフィズム: 同じインターフェースで異なる実装を可能にする
  • 継承: 既存のクラスの機能を拡張または特化する
  • 抽象化: 実装の詳細を隠蔽し、使用者に必要なインターフェースのみを提供

カプセル化

[編集]

カプセル化は、クラスの内部データを外部から直接操作されることを防ぎ、代わりにメソッドを通じてデータを操作する設計手法です。

アクセス制御

[編集]

C++では、以下のアクセス指定子を提供しています:

  • private: 同じクラス内からのみアクセス可能
  • public: どこからでもアクセス可能
  • protected: 同じクラスおよび派生クラスからアクセス可能

カプセル化の例

[編集]
class Point {
private:
    int x, y;  // メンバー変数はprivate
public:
    // ゲッター・セッター
    [[nodiscard]] int x() const { return x; }
    void setX(int value) { x = value; }
    [[nodiscard]] int y() const { return y; }
    void setY(int value) { y = value; }
};

ポリモーフィズム

[編集]

C++では、以下の形式でポリモーフィズムを実現できます:

  • コンパイル時ポリモーフィズム
    • 関数オーバーロード
    • 演算子オーバーロード
    • テンプレート
  • 実行時ポリモーフィズム
    • 仮想関数
    • 純粋仮想関数

関数オーバーロードの例

[編集]
class Calculator {
public:
    int add(int a, int b) { return a + b; }
    double add(double a, double b) { return a + b; }
    int add(int a, int b, int c) { return a + b + c; }
};

コンストラクタとデストラクタ

[編集]

コンストラクタの種類

[編集]
  • デフォルトコンストラクタ: 引数を取らないコンストラクタ
  • コピーコンストラクタ: 同じ型のオブジェクトから新しいオブジェクトを作成
  • ムーブコンストラクタ (C++11以降): リソースの所有権を移動
  • 委譲コンストラクタ (C++11以降): 他のコンストラクタに初期化を委譲

[編集]
class Point {
    int x, y;
public:
    // デフォルトコンストラクタ
    Point() : x(0), y(0) {}
    
    // 通常のコンストラクタ
    Point(int x, int y) : x(x), y(y) {}
    
    // コピーコンストラクタ
    Point(const Point& other) : x(other.x), y(other.y) {}
    
    // ムーブコンストラクタ
    Point(Point&& other) noexcept : x(other.x), y(other.y) {
        other.x = 0;
        other.y = 0;
    }
    
    // デストラクタ
    ~Point() = default;
};

メモリー管理

[編集]

スマートポインタ

[編集]

C++11以降では、手動のメモリー管理よりもスマートポインタの使用が推奨されます:

  • std::unique_ptr: 単一の所有権を持つポインタ
  • std::shared_ptr: 共有所有権を持つポインタ
  • std::weak_ptr: shared_ptrへの弱参照
#include <memory>

class Resource {
    // リソースの定義
};

void example() {
    auto ptr = std::make_unique<Resource>();  // 推奨される生成方法
    auto shared = std::make_shared<Resource>();
}

継承

[編集]

継承の種類

[編集]
  • public継承: インターフェースとインプリメンテーションを継承
  • protected継承: インプリメンテーションの継承
  • private継承: 実装の詳細として継承を使用

final指定子

[編集]

C++11以降では、finalキーワードを使用して継承やオーバーライドを禁止できます:

class Base {
    virtual void foo() = 0;
};

class Derived final : public Base {  // これ以上の継承を禁止
    void foo() override final { }    // オーバーライドを禁止
};

仮想関数と純粋仮想関数

[編集]
  • 仮想関数: 派生クラスでオーバーライド可能な関数
  • 純粋仮想関数: 実装を持たず、派生クラスでの実装を強制する関数
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;  // 純粋仮想関数
    virtual void draw() { /* デフォルト実装 */ }  // 通常の仮想関数
};

モダンC++の機能

[編集]

override指定子

[編集]

C++11以降では、overrideキーワードを使用して仮想関数のオーバーライドを明示できます:

class Derived : public Base {
    void foo() override { }  // Base::fooをオーバーライド
};

default/delete指定子

[編集]

特殊メンバ関数の自動生成を制御できます:

class NonCopyable {
public:
    NonCopyable() = default;  // デフォルト実装を使用
    NonCopyable(const NonCopyable&) = delete;  // コピーを禁止
    NonCopyable& operator=(const NonCopyable&) = delete;
};

クラス・構造体・共用体の比較

[編集]

クラスと構造体の比較

[編集]

クラスと構造体の違いは、主にデフォルトのメンバーのアクセス指定子にあります。クラスでは、デフォルトのメンバーのアクセス指定子がprivateであり、外部からのアクセスが制限されます。一方、構造体では、デフォルトのメンバーのアクセス指定子がpublicであり、外部からのアクセスが可能です。

この違いは、クラスと構造体が異なる用途で使われる理由の1つです。クラスはオブジェクト指向プログラミングの概念に基づき、データとそのデータを操作するメンバー関数を一体化するために使用されます。そのため、データの隠蔽(カプセル化)が重要視され、デフォルトでprivateとなるのが標準です。一方、構造体は関連するデータをグループ化するために使われ、アクセスの容易さが求められる場面で有用です。

クラスと共用体の比較

[編集]

共用体(union)には以下の制限があります。

  • 継承することも、継承されることもできません。
  • メンバーとしてstaticを指定することはできません。
  • コンストラクタやデストラクタを持つことは可能ですが、コンストラクタ・デストラクタを持つオブジェクトをメンバーに含めることはできません。
  • 仮想メンバー関数を持つことはできません。

これらの制限により、共用体は主に異なる型のデータを同じメモリ領域で扱うような特殊な用途に適しています。

C++で共用体(union)の代替として使われる技術には、主に以下のものがあります。

1. std::variant

[編集]

std::variantはC++17で導入された型で、異なる型のオブジェクトを安全に格納し、動的にどの型がアクティブであるかを追跡できます。共用体とは異なり、std::variantは型安全で、どの型が有効かを容易に判定できるため、バグを防ぐのに役立ちます。

#include <variant>
#include <iostream>

auto main() -> int {
    std::variant<int, float, std::string> data;
    data = 10;
    std::cout << std::get<int>(data) << std::endl;

    data = "Hello";
    std::cout << std::get<std::string>(data) << std::endl;

    return 0;
}

std::variantは共用体のように複数の型を1つのメモリ領域に格納しますが、型の追跡機能と安全なアクセスを提供します。

2. std::any

[編集]

std::anyはC++17で追加された型で、任意の型の値を格納でき、std::variantよりも柔軟ですが、型情報が保持されていないため、アクセス時には型チェックが必要です。

#include <any>
#include <iostream>

auto main() -> int {
    std::any data = 5;
    if (data.type() == typeid(int)) {
        std::cout << std::any_cast<int>(data) << std::endl;
    }

    data = 3.14;
    if (data.type() == typeid(double)) {
        std::cout << std::any_cast<double>(data) << std::endl;
    }

    return 0;
}

std::anyは、型情報を動的に保持してキャストする必要があるため、std::variantに比べて少し複雑です。

3. クラス階層とポリモーフィズム

[編集]

クラス階層とポリモーフィズムを利用して、動的に異なる型のオブジェクトを格納する方法もあります。これは、継承と仮想関数を用いた設計に基づいており、ポインタやスマートポインタを使って型を動的に扱うことができます。

#include <iostream>
#include <memory>

class Base {
public:
    virtual void print() const = 0;
    virtual ~Base() = default;
};

class Derived1 : public Base {
public:
    void print() const override {
        std::cout << "Derived1" << std::endl;
    }
};

class Derived2 : public Base {
public:
    void print() const override {
        std::cout << "Derived2" << std::endl;
    }
};

auto main() -> int {
    std::unique_ptr<Base> obj = std::make_unique<Derived1>();
    obj->print(); // Outputs "Derived1"

    obj = std::make_unique<Derived2>();
    obj->print(); // Outputs "Derived2"

    return 0;
}

この方法は、動的な型の選択と多様な動作を必要とする場合に有効です。

まとめ

[編集]
  • std::variant: 型安全で共用体の代替として非常に適している。
  • std::any: 柔軟だが、使用時の型チェックが必要。
  • クラス階層とポリモーフィズム: より複雑な型の使用や動的な動作を求める場合に適している。

これらの代替技術は、共用体の持つ制約を超え、型安全性や柔軟性を高めることができます。


脚註

[編集]
  1. ^ オブジェクト指向言語には、クラスベースのオブジェクト指向言語とプロトタイプベースのオブジェクト指向言語があります。プロトタイプベースのオブジェクト指向言語には、SelfJavaScriptLuaなどがあります。
  2. ^ プログラミングにおいて、ミューテーターメソッドは、変数の変更を制御するために使用されるメソッドです。セッターはしばしばゲッターを伴い(共にアクセッサーとも呼ばれる)、プライベートメンバ変数の値を返す。