C++のオブジェクト指向とは
C++のオブジェクト指向とは、データと、そのデータを操作する処理を「オブジェクト」という単位にまとめてプログラムを設計する考え方です。
C++では、classやstructを使ってオブジェクトの設計図を定義し、その設計図から実体となるオブジェクトを生成します。
オブジェクト指向を利用すると、プログラムを役割ごとに整理しやすくなり、大規模なソフトウェアでも保守しやすい構造を作りやすくなります。
C++における代表的なオブジェクト指向の要素としては、次のようなものがあります。
- クラスとオブジェクト
- カプセル化
- 継承
- ポリモーフィズム
- 抽象化
ただし、C++はオブジェクト指向専用の言語ではありません。
手続き型プログラミングやジェネリックプログラミングなども組み合わせられる、マルチパラダイム言語です。
C++におけるクラスとオブジェクト
クラスとは
クラスは、オブジェクトが持つデータや処理を定義する設計図です。
C++では、classキーワードを使ってクラスを定義できます。
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
void introduce() const {
std::cout << "名前: " << name << '\n';
std::cout << "年齢: " << age << '\n';
}
};
int main() {
Person person;
person.name = "Taro";
person.age = 20;
person.introduce();
return 0;
}
この例では、Personというクラスを定義しています。
nameやageのようなクラス内のデータは、正式には「データメンバ」と呼ばれます。
一般的な解説では「メンバ変数」と呼ばれることもあります。
また、introduce()のようなクラス内の関数は「メンバ関数」と呼ばれます。
オブジェクトとは
クラスをもとに実際に生成された実体がオブジェクトです。
Person person;
このコードでは、Personクラスをもとにpersonというオブジェクトを生成しています。
クラスが設計図であるなら、オブジェクトは設計図から作られた実物と考えると理解しやすいでしょう。
C++のカプセル化とは
カプセル化の基本
カプセル化とは、データとそれを操作する処理をクラスにまとめ、外部からのアクセスを適切に制御する考え方です。
C++では、主に次のアクセス指定子を使います。
| アクセス指定子 | 主な意味 |
|---|---|
public | 外部からアクセスできる |
private | 通常の外部コードから直接アクセスできない |
protected | 基底クラス自身や派生クラスなどから利用できる |
privateで内部状態を保護する
たとえば、銀行口座を表すクラスを考えてみます。
#include <iostream>
class BankAccount {
private:
int balance = 0;
public:
void deposit(int amount) {
if (amount > 0) {
balance += amount;
}
}
int getBalance() const {
return balance;
}
};
int main() {
BankAccount account;
account.deposit(1000);
std::cout << account.getBalance() << '\n';
return 0;
}
balanceはprivateなので、通常の外部コードから次のように直接変更することはできません。
account.balance = -10000;
代わりに、
account.deposit(1000);
のようなメンバ関数を通して状態を変更します。
これにより、不正な値が設定されることを防ぎやすくなります。
なお、厳密にはprivateメンバはクラス自身のメンバだけでなく、friendとして許可された関数やクラスからもアクセスできます。
C++の継承とは
継承の基本
継承とは、既存のクラスをもとに新しいクラスを定義する仕組みです。
継承元を「基底クラス」、継承して作られたクラスを「派生クラス」と呼びます。
#include <iostream>
class Animal {
public:
void eat() const {
std::cout << "食べています\n";
}
};
class Dog : public Animal {
public:
void bark() const {
std::cout << "ワン!\n";
}
};
int main() {
Dog dog;
dog.eat();
dog.bark();
return 0;
}
ここでは、DogがAnimalをpublic継承しています。
class Dog : public Animal
そのため、DogはAnimalが持つeat()を利用できます。
public継承ではis-a関係が重要
public継承では、単にコードを再利用できるという点だけでなく、型として自然な関係が成立するかが重要です。
たとえば、
Dog is an Animal
つまり、「DogはAnimalの一種である」と考えられる関係なら、継承が自然です。
一方、単に機能を使い回したいだけで継承すると、設計が複雑になることがあります。
C++のポリモーフィズムとは
ポリモーフィズムの基本
ポリモーフィズムは、日本語で「多態性」と呼ばれます。
同じインターフェースを使いながら、実際のオブジェクトに応じて異なる処理を実行できる仕組みです。
C++では、virtual関数を利用することで代表的な実行時ポリモーフィズムを実現できます。
#include <iostream>
class Animal {
public:
virtual void speak() const {
std::cout << "Animal sound\n";
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override {
std::cout << "ワン!\n";
}
};
class Cat : public Animal {
public:
void speak() const override {
std::cout << "ニャー!\n";
}
};
int main() {
Dog dog;
Cat cat;
Animal* animal1 = &dog;
Animal* animal2 = &cat;
animal1->speak();
animal2->speak();
return 0;
}
実行結果は次のようになります。
ワン!
ニャー!
どちらもAnimal*として扱っていますが、実際のオブジェクト型に応じて適切なspeak()が呼ばれます。
virtualの役割
基底クラス側で、
virtual void speak() const
と宣言すると、その関数は仮想関数になります。
基底クラスのポインタや参照を通して呼び出した場合でも、実際のオブジェクトに応じてオーバーライド先が選択されます。
overrideを使う理由
派生クラスでは、
void speak() const override
のようにoverrideを付けることが推奨されます。
overrideを付けると、基底クラスの仮想関数を本当に正しくオーバーライドしているかをコンパイラが確認できます。
関数名や引数、constの有無などを間違えていた場合に、コンパイル時に検出しやすくなります。
純粋仮想関数と抽象クラス
純粋仮想関数とは
C++では、次のように= 0を付けることで純粋仮想関数を定義できます。
virtual void speak() const = 0;
たとえば、次のように記述できます。
class Animal {
public:
virtual void speak() const = 0;
virtual ~Animal() = default;
};
純粋仮想関数を持つクラスは抽象クラスになります。
そのため、次のように抽象クラスのオブジェクトを直接生成することはできません。
Animal animal;
派生クラス側で具体的な処理を定義します。
class Dog : public Animal {
public:
void speak() const override {
std::cout << "ワン!\n";
}
};
抽象クラスは、複数のクラスに共通のインターフェースを定義したい場合に便利です。
C++の抽象化とは
必要な機能だけを外部へ公開する
抽象化とは、複雑な内部処理を利用者から隠し、必要な操作だけを分かりやすく公開する考え方です。
たとえば、車を操作する場合、利用者はエンジン内部の燃焼処理まで理解していなくても運転できます。
プログラムでも同様に、次のような設計ができます。
class Car {
public:
void start();
void stop();
void accelerate();
private:
void injectFuel();
void ignite();
};
利用者はstart()やstop()を使えばよく、内部でどのような処理が行われているかを知る必要はありません。
カプセル化と抽象化の違い
カプセル化と抽象化は関連していますが、同じ意味ではありません。
カプセル化は、内部状態や実装へのアクセスを制御することに重点があります。
一方、抽象化は、必要な機能だけを表に出して複雑な詳細を隠すことに重点があります。
C++のコンストラクタとは
オブジェクト生成時に実行される特殊な関数
コンストラクタは、オブジェクトが生成される際に呼び出される特殊なメンバ関数です。
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
Person(const std::string& n, int a)
: name(n), age(a) {
}
void introduce() const {
std::cout << name << " " << age << '\n';
}
};
int main() {
Person person("Taro", 20);
person.introduce();
return 0;
}
この例では、
Person(const std::string& n, int a)
がコンストラクタです。
メンバ初期化リスト
次の部分は、メンバ初期化リストと呼ばれます。
: name(n), age(a)
C++では、メンバを初期化する場合、メンバ初期化リストを利用するのが基本です。
特に、参照メンバやconstメンバはメンバ初期化リストによる初期化が必要です。
C++のデストラクタとは
オブジェクト破棄時に実行される特殊な関数
デストラクタは、オブジェクトが破棄される際に呼ばれる特殊なメンバ関数です。
#include <iostream>
class Sample {
public:
Sample() {
std::cout << "生成\n";
}
~Sample() {
std::cout << "破棄\n";
}
};
int main() {
Sample obj;
return 0;
}
実行すると、通常は次のように表示されます。
生成
破棄
デストラクタは、クラス名の前に~を付けて定義します。
~Sample()
C++ではオブジェクトの寿命が重要
スコープを抜けると自動的に破棄される
C++では、オブジェクトのライフタイムを理解することが重要です。
たとえば、
void func() {
Person person("Taro", 20);
}
このpersonは、自動記憶域期間を持つローカルオブジェクトです。
通常は、func()のスコープを抜けると自動的に破棄されます。
この仕組みは、C++のリソース管理と深く関係しています。
RAIIとは
オブジェクトの寿命とリソース管理を結び付ける
C++で重要な設計思想の一つがRAIIです。
RAIIは、Resource Acquisition Is Initializationの略です。
オブジェクトがリソースを取得し、そのオブジェクトの寿命が終わったときにリソースを自動的に解放する設計です。
たとえば、次のような標準ライブラリの型でもRAIIが利用されています。
std::stringstd::vectorstd::unique_ptrstd::fstream
RAIIによって、メモリやファイル、ロックなどのリソースを安全に管理しやすくなります。
C++のスマートポインタ
unique_ptrによる所有権管理
動的に生成したオブジェクトを管理する場合、現代的なC++では生のnewやdeleteを直接多用するよりも、スマートポインタを利用する設計が一般的です。
#include <iostream>
#include <memory>
class Animal {
public:
virtual void speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override {
std::cout << "ワン!\n";
}
};
int main() {
std::unique_ptr<Animal> animal =
std::make_unique<Dog>();
animal->speak();
return 0;
}
std::unique_ptrは、所有しているオブジェクトが不要になった際に自動的に破棄してくれます。
そのため、通常は手動でdeleteを書く必要がありません。
ただし、そもそも動的確保が不要であれば、
Dog dog;
のように直接オブジェクトを生成するほうが単純です。
仮想デストラクタが重要な理由
基底クラス経由で安全に削除する
ポリモーフィズムを利用する基底クラスでは、デストラクタをvirtualにすることが重要です。
class Animal {
public:
virtual ~Animal() = default;
};
たとえば、
Animal* animal = new Dog;
delete animal;
のように基底クラスポインタを通して派生クラスのオブジェクトを削除する場合、基底クラスのデストラクタが仮想でなければ未定義動作になります。
そのため、多態的に利用する基底クラスでは、
virtual ~Animal() = default;
と定義するのが一般的です。
ただし、すべてのクラスに無条件で仮想デストラクタが必要なわけではありません。
C++の多重継承
複数の基底クラスを継承できる
C++では、一つのクラスが複数のクラスを継承する多重継承が可能です。
class Printer {
public:
void print() {
}
};
class Scanner {
public:
void scan() {
}
};
class MultiFunctionPrinter
: public Printer,
public Scanner {
};
MultiFunctionPrinterは、PrinterとScannerの両方を継承しています。
ダイヤモンド継承には注意が必要
多重継承では、次のような構造が生まれることがあります。
A
/ \
B C
\ /
D
このような構造では、Dが同じAを複数経路から継承することで問題が起きる場合があります。
これを一般にダイヤモンド継承問題と呼びます。
C++には仮想継承という仕組みも用意されていますが、初心者の場合はまず単一継承を理解してから学ぶとよいでしょう。
C++の静的ポリモーフィズム
テンプレートでも多態性を実現できる
C++では、virtualによる実行時ポリモーフィズムだけでなく、テンプレートを使った静的ポリモーフィズムも広く利用されます。
template <typename T>
void speak(T& object) {
object.speak();
}
たとえば、
Dog dog;
Cat cat;
speak(dog);
speak(cat);
のように異なる型を扱えます。
この場合、どの処理を使うかは基本的にコンパイル時に決定されます。
C++では、継承とvirtualだけが多態性を実現する方法ではありません。
C++20以降ではConceptsも利用できる
テンプレートが要求する条件を明示できる
C++20以降では、Conceptsを利用してテンプレートに必要な条件をより明確に記述できます。
template <typename T>
concept Speakable = requires(T obj) {
obj.speak();
};
これにより、「speak()を持つ型だけを受け入れる」といった条件をコード上で分かりやすく表現できます。
C++のclassとstructの違い
デフォルトのアクセス指定が異なる
C++では、classとstructは非常によく似ています。
大きな違いの一つは、デフォルトのアクセス指定です。
classでは、アクセス指定子を書かない場合、メンバはprivateになります。
class Sample {
int value;
};
一方、structではデフォルトでpublicです。
struct Sample {
int value;
};
継承時のデフォルトも異なる
継承でも違いがあります。
class A : B {
};
この場合はデフォルトでprivate継承です。
一方、
struct A : B {
};
ではデフォルトでpublic継承になります。
それ以外については、C++のclassとstructは基本的に同等のクラス機能を利用できます。
structでもコンストラクタやメンバ関数、継承、仮想関数などを利用できます。
継承よりコンポジションが適切な場合もある
has-a関係ではコンポジションを検討する
継承は便利ですが、すべての関係を継承で表現すればよいわけではありません。
たとえば、車とエンジンの関係を考えてみます。
class Engine {
};
class Car {
private:
Engine engine;
};
この場合、
Car has an Engine
という関係です。
「CarはEngineの一種である」わけではないため、継承よりもコンポジションのほうが自然です。
一般的に、継承はis-a関係、コンポジションはhas-a関係として考えると理解しやすくなります。
C++のオブジェクト指向のメリット
コードを役割ごとに整理しやすい
クラス単位で責任を分けることで、プログラムの構造を整理しやすくなります。
たとえば、
User
Product
Order
Payment
のように、それぞれの役割を別クラスとして設計できます。
内部状態を保護しやすい
privateなどを使うことで、外部から内部状態を直接変更されることを防げます。
これにより、不正な状態を作りにくくなります。
共通インターフェースを作れる
抽象クラスや仮想関数を使うことで、異なる実装を同じインターフェースとして扱えます。
Payment
├─ CreditCardPayment
├─ BankTransferPayment
└─ ElectronicMoneyPayment
このように設計すれば、利用側は具体的な決済方法を意識せず、共通の操作として扱えるようになります。
変更の影響を限定しやすい
適切にカプセル化されたクラスでは、内部実装を変更しても外部コードへの影響を抑えやすくなります。
その結果、保守性や拡張性の向上につながります。
C++のオブジェクト指向の注意点
クラスを増やしすぎない
オブジェクト指向だからといって、すべての処理を細かいクラスに分割する必要はありません。
クラス数や継承階層が増えすぎると、かえって構造を理解しにくくなることがあります。
継承をコード再利用だけの目的で使わない
継承を単純なコード再利用の手段として使うと、クラス間の結び付きが強くなり、変更しにくい設計になる場合があります。
public継承では、特に「派生クラスを基底クラスの一種として自然に扱えるか」を考えることが重要です。
virtualを必要以上に使わない
仮想関数は便利ですが、すべてのメンバ関数をvirtualにする必要はありません。
実行時ポリモーフィズムが必要な部分だけに使うのが基本です。
C++はオブジェクト指向専用言語ではない
マルチパラダイム言語として設計されている
C++では、次のようなさまざまなプログラミングスタイルを利用できます。
- 手続き型プログラミング
- オブジェクト指向プログラミング
- ジェネリックプログラミング
- 関数型に近いプログラミング
- コンパイル時プログラミング
そのため、単純な処理であれば無理にクラスを作る必要はありません。
int add(int a, int b) {
return a + b;
}
このような通常の関数もC++では広く利用されます。
重要なのは、問題に応じて適切な設計方法を選ぶことです。
C++のクラス設計ではコピーとムーブも重要
コピーコンストラクタ
C++では、オブジェクトをコピーできます。
Person a("Taro", 20);
Person b = a;
クラスが独自にリソースを管理している場合、コピーコンストラクタやコピー代入演算子の設計が重要になります。
ムーブセマンティクス
C++11以降では、ムーブセマンティクスも重要です。
Person b = std::move(a);
コピーではなくリソースを移動させることで、不要な複製を避けられる場合があります。
Rule of Zeroとは
リソース管理を標準ライブラリに任せる
現代的なC++では、可能な限り独自にリソース管理処理を書かず、標準ライブラリのクラスへ任せる設計が推奨されます。
この考え方はRule of Zeroと呼ばれます。
たとえば、
std::stringstd::vectorstd::unique_ptr
などを利用すれば、コピーや破棄、メモリ管理について自分で複雑な処理を書く必要を減らせます。
結果として、安全で保守しやすいクラス設計につながります。
C++のオブジェクト指向の仕組みを整理
C++のオブジェクト指向は、基本的に次のような流れで理解できます。
クラスを定義する
↓
オブジェクトを生成する
↓
データと処理をまとめる
↓
カプセル化で内部状態を守る
↓
必要に応じて継承する
↓
virtualで実行時ポリモーフィズムを実現する
↓
抽象クラスで共通インターフェースを定義する
ただし、C++ではテンプレートによる静的ポリモーフィズムや、コンポジションを使った設計も重要です。
単純にクラスや継承を多用することがオブジェクト指向の目的ではありません。
プログラム内の責任や依存関係を整理し、変更しやすく理解しやすい構造を作ることが本来の目的です。
C++のオブジェクト指向を学ぶおすすめの順番
基礎から段階的に理解する
初心者であれば、次の順番で学ぶと理解しやすくなります。
1. classとオブジェクト
↓
2. データメンバとメンバ関数
↓
3. public・private・protected
↓
4. コンストラクタ・デストラクタ
↓
5. カプセル化
↓
6. 継承
↓
7. virtual
↓
8. override
↓
9. 純粋仮想関数・抽象クラス
↓
10. スマートポインタ
↓
11. RAII
↓
12. コンポジション
↓
13. コピー・ムーブ
↓
14. テンプレートと静的ポリモーフィズム
まずは、クラスにデータと処理をまとめ、内部状態を適切に管理するという基本を理解することが重要です。
その後、継承やポリモーフィズム、RAII、スマートポインタ、コピー・ムーブなどを学ぶと、C++らしいオブジェクト設計をより深く理解できるようになります。
まとめ
C++のオブジェクト指向では、クラスを使ってデータと処理をまとめ、カプセル化によって内部状態を管理できます。
継承を使えば、基底クラスと派生クラスの関係を表現できます。
さらに、virtualやoverrideを利用することで、実行時ポリモーフィズムを実現できます。
純粋仮想関数を使えば抽象クラスを作成でき、複数のクラスに共通するインターフェースを定義できます。
一方で、C++では何でも継承すればよいわけではありません。
is-a関係なら継承、has-a関係ならコンポジションを検討するなど、目的に応じて適切な設計方法を選ぶことが重要です。
また、C++はオブジェクト指向だけを利用する言語ではなく、テンプレートや通常の関数なども柔軟に組み合わせられます。
そのため、「すべてをオブジェクトにする」のではなく、可読性、安全性、保守性を考えながら適切な設計を選択することが、C++を扱ううえで重要です。
以上、C++のオブジェクト指向の特徴や仕組みについてでした。
最後までお読みいただき、ありがとうございました。
