C++のクラス定義について

AI実装検定のご案内

C++のクラス定義とは、データと処理をひとまとまりにして扱うための仕組みです。

プログラムでは、名前、年齢、点数、残高、座標、商品名、価格など、さまざまな情報を扱います。

これらの情報を単なる変数としてバラバラに管理すると、プログラムが大きくなるにつれて分かりにくくなります。

そこでC++では、関連するデータと、そのデータに対する操作をまとめるためにクラスを使います。

たとえば「人」を表すなら、名前や年齢といった情報を持ち、その人を紹介する処理を持たせることができます。

「銀行口座」を表すなら、口座名義や残高を持ち、入金や出金の処理を持たせることができます。

このように、クラスは現実世界のものやプログラム上の概念を表現するための設計図のようなものです。

目次

クラスとオブジェクトの関係

クラスは設計図

クラスは、オブジェクトを作るための設計図です。

たとえば「車」というクラスがあるとします。

このクラスには、車種、色、速度などのデータを持たせることができます。

また、走る、止まる、加速するなどの処理も持たせられます。

ただし、クラスそのものはあくまで設計図です。

実際に使うためには、その設計図をもとに具体的な実体を作る必要があります。

オブジェクトは実体

クラスから作られた実体を、オブジェクトまたはインスタンスと呼びます。

「車」というクラスが設計図だとすると、「赤い車」「青い車」「白い車」のような具体的な車がオブジェクトです。

同じクラスから作られたオブジェクトでも、それぞれ異なるデータを持てます。

たとえば、同じ「人」クラスから作られたオブジェクトでも、ある人は「田中さん」、別の人は「佐藤さん」というように、別々の名前や年齢を持つことができます。

クラス定義の基本構造

クラス名

クラスには名前を付けます。

クラス名は、そのクラスが何を表すのか分かりやすい名前にするのが基本です。

たとえば、人を表すなら「Person」、商品を表すなら「Product」、銀行口座を表すなら「BankAccount」のような名前が使われます。

C++では、クラス名は大文字から始めることが多いです。

これは文法上の決まりではありませんが、読みやすさのための一般的な慣習です。

メンバ変数

クラスの中に定義される変数を、メンバ変数と呼びます。

メンバ変数は、そのクラスが持つデータを表します。

たとえば、人を表すクラスであれば、名前や年齢がメンバ変数になります。

商品を表すクラスであれば、商品名や価格がメンバ変数になります。

銀行口座を表すクラスであれば、口座名義や残高がメンバ変数になります。

メンバ変数は、オブジェクトごとに別々の値を持ちます。

同じクラスから複数のオブジェクトを作っても、それぞれのオブジェクトは独自のメンバ変数を持ちます。

メンバ関数

クラスの中に定義される関数を、メンバ関数と呼びます。

メンバ関数は、そのクラスが持つデータに対して何らかの処理を行います。

たとえば、人を表すクラスなら、自己紹介をする処理がメンバ関数になります。

商品を表すクラスなら、価格を表示する処理がメンバ関数になります。

銀行口座を表すクラスなら、入金する処理や出金する処理がメンバ関数になります。

メンバ関数は、同じクラス内のメンバ変数を扱えます。

そのため、データと処理を自然にまとめることができます。

アクセス指定子

public

public は、クラスの外部からアクセスできることを表します。

外部から呼び出してよいメンバ関数や、外部に公開してよいデータは public に置きます。

ただし、実務ではメンバ変数を直接 public にすることは慎重に考える必要があります。

なぜなら、外部から自由に値を書き換えられると、不正な状態が発生しやすくなるからです。

たとえば、年齢や残高のような値は、マイナスになってはいけない場合があります。

そのようなデータを直接外部から変更できるようにすると、誤った値が入り込む可能性があります。

private

private は、クラスの外部から直接アクセスできないことを表します。

通常、重要なメンバ変数は private にします。

外部から直接変更できないようにして、必要な操作だけを public のメンバ関数として公開します。

これにより、クラスの内部状態を安全に保ちやすくなります。

たとえば、銀行口座の残高を外部から直接書き換えられるようにするのではなく、入金や出金という処理を通してだけ変更できるようにします。

すると、残高不足の出金やマイナス金額の入金などを防ぎやすくなります。

protected

protected は、クラスの外部からはアクセスできませんが、そのクラスを継承した派生クラスからはアクセスできます。

初心者の段階では、まず publicprivate を理解することが大切です。

protected は継承を学ぶ段階で詳しく理解すれば十分です。

ただし、protected を多用すると、基底クラスと派生クラスの結びつきが強くなります。

そのため、実務では慎重に使うべきアクセス指定子です。

classとstructの違い

基本的な違い

C++では、classstruct は非常によく似ています。

主な違いは、デフォルトのアクセス指定子です。

class では、何も指定しなければメンバは private になります。

一方、struct では、何も指定しなければメンバは public になります。

また、継承時のデフォルトアクセス指定子も異なります。

class で継承するとデフォルトでは private 継承になり、struct で継承するとデフォルトでは public 継承になります。

実務での使い分け

文法上は、struct でもメンバ関数やコンストラクタを持てます。

逆に、class でも単純なデータだけを持たせることができます。

しかし、慣習としては次のように使い分けることが多いです。

単純なデータのまとまりには struct を使います。

たとえば、座標、サイズ、範囲、設定値のようなものです。

一方、内部状態を守りながら操作を提供するものには class を使います。

たとえば、銀行口座、ユーザー、商品管理、ファイル管理、ゲームキャラクターなどです。

つまり、単なるデータ構造なら struct、振る舞いやルールを持つものなら class という使い分けが一般的です。

カプセル化

カプセル化とは

カプセル化とは、データを外部から直接触れないようにし、必要な操作だけを公開する考え方です。

C++のクラス設計では非常に重要な考え方です。

たとえば、年齢を表すデータがある場合、年齢は通常マイナスになってはいけません。

残高を表すデータなら、不正な操作によって突然マイナスになるのは困ります。

このようなデータを外部から自由に変更できるようにしてしまうと、クラスの状態が壊れやすくなります。

そこで、データ自体は private にして、値を変更するための処理を public のメンバ関数として用意します。

カプセル化のメリット

カプセル化のメリットは、データを安全に管理できることです。

たとえば、年齢を変更する処理の中で「0以上の値だけ受け付ける」というルールを入れておけば、不正な年齢が設定されることを防げます。

また、銀行口座の出金処理で「残高以上の出金はできない」というルールを入れておけば、残高の整合性を保てます。

このように、クラスの内部状態を守ることで、プログラム全体の信頼性が高まります。

何でも隠せばよいわけではない

カプセル化は重要ですが、何でも複雑に隠せばよいという意味ではありません。

単純なデータ構造であれば、すべてを public にした struct の方が分かりやすい場合もあります。

大切なのは、そのデータに守るべきルールがあるかどうかです。

守るべきルールがあるなら private にして、適切な操作を通じて扱う設計が向いています。

getterとsetter

getterとは

getterとは、private なメンバ変数の値を取得するためのメンバ関数です。

たとえば、名前や年齢の値を外部から確認したい場合、メンバ変数を直接公開するのではなく、値を返す関数を用意します。

これにより、内部データを直接触らせずに、必要な情報だけを外部へ渡すことができます。

setterとは

setterとは、private なメンバ変数の値を設定するためのメンバ関数です。

ただし、setterを使う場合は注意が必要です。単に値を代入するだけのsetterを何でも用意すると、結局メンバ変数を public にしているのとあまり変わらなくなることがあります。

setterを用意するなら、不正な値を防ぐためのチェックを入れるのが望ましいです。

getterとsetterを作りすぎない

C++のクラス設計では、何でもgetterやsetterにするのが良いわけではありません。

たとえば、銀行口座クラスに「残高を自由に設定するsetter」を用意すると、外部から残高を好きな値に変更できてしまいます。

これは銀行口座として不自然です。

この場合は、残高を直接設定するのではなく、入金や出金といった意味のある操作を用意する方が自然です。

つまり、クラスは単なるデータ入れ物ではなく、「そのデータに対してどのような操作が許されるのか」を表現するものです。

コンストラクタ

コンストラクタとは

コンストラクタとは、オブジェクトが作られるときに自動的に呼ばれる特別なメンバ関数です。

主な役割は、メンバ変数を初期化することです。

たとえば、人を表すオブジェクトを作るときに、名前や年齢を最初に設定したい場合があります。

そのような初期設定を行うのがコンストラクタです。

コンストラクタの特徴

コンストラクタにはいくつかの特徴があります。

まず、コンストラクタの名前はクラス名と同じです。

また、戻り値の型を書きません。

通常の関数のように戻り値を書くと、コンストラクタではなく別の関数として扱われてしまいます。

さらに、コンストラクタはオブジェクトの生成時に自動的に呼ばれます。

明示的に呼び出すというより、オブジェクトを作る流れの中で実行される特別な処理です。

デフォルトコンストラクタ

引数を取らないコンストラクタを、デフォルトコンストラクタと呼びます。

デフォルトコンストラクタは、オブジェクトを作るときに特別な値を指定しない場合に使われます。

たとえば、名前を「未設定」、年齢を「0」のように初期化したい場合に利用できます。

現代C++では、メンバ変数の宣言時に初期値を与えることもできます。

その場合、単純な初期化であれば、明示的にコンストラクタを書かなくても済むことがあります。

メンバ初期化リスト

メンバ初期化リストとは

メンバ初期化リストとは、コンストラクタでメンバ変数を初期化するための仕組みです。

C++では、コンストラクタ本体で値を代入するよりも、メンバ初期化リストで初期化する方が望ましい場合が多いです。

なぜなら、コンストラクタ本体での処理は、厳密には「初期化」ではなく「代入」だからです。

初期化と代入の違い

初期化とは、オブジェクトが作られるときに最初の値を設定することです。

一方、代入とは、すでに作られたものに後から値を入れることです。

文字列や他のクラス型のメンバ変数では、一度デフォルトの状態で作られてから代入するよりも、最初から目的の値で初期化した方が効率的です。

また、const メンバや参照メンバは後から代入できないため、メンバ初期化リストで初期化する必要があります。

初期化順序に注意する

C++では、メンバ変数はメンバ初期化リストに書いた順番ではなく、クラス内で宣言された順番に初期化されます。

そのため、メンバ初期化リストの順番も、クラス内でのメンバ変数の宣言順に合わせるのが望ましいです。

順番がずれていると、警告が出たり、意図しない初期化順序によってバグの原因になったりすることがあります。

デストラクタ

デストラクタとは

デストラクタとは、オブジェクトが破棄されるときに自動的に呼ばれる特別なメンバ関数です。

主に、オブジェクトが持っているリソースを解放するために使われます。

リソースとは、動的メモリ、ファイル、ネットワーク接続、OSのハンドルなど、使い終わったら適切に片付ける必要があるものです。

通常は自分で書かなくてよい

現代C++では、多くの場合、デストラクタを自分で書く必要はありません。

理由は、標準ライブラリの型がリソース管理を自動で行ってくれるからです。

たとえば、文字列には標準の文字列型を使い、可変長配列には標準の配列コンテナを使い、動的メモリ管理にはスマートポインタを使うことで、手動でリソースを解放する必要が減ります。

この考え方は、現代C++では非常に重要です。

継承での注意点

継承を使い、基底クラスのポインタや参照を通して派生クラスを扱う場合、基底クラスのデストラクタは仮想デストラクタにするのが基本です。

特に、ポリモーフィズムを使う基底クラスでは、仮想デストラクタを用意することが重要です。

これを忘れると、基底クラスのポインタ経由で派生クラスのオブジェクトを削除したときに、正しく後始末されない可能性があります。

thisポインタ

thisとは

this は、現在のオブジェクト自身を指すポインタです。

メンバ関数の中では、自分自身のメンバ変数やメンバ関数にアクセスできます。

そのとき、明示的に this を使うこともできます。

thisを使う場面

this は、メンバ変数名と引数名が同じ場合によく使われます。

たとえば、メンバ変数の名前と、関数の引数の名前が同じだと、どちらを指しているのか分かりにくくなります。

そのような場合に this を使うと、「このオブジェクトのメンバ変数」を明確に示せます。

ただし、実務ではメンバ変数に特定の接尾辞を付けるなどして、this を頻繁に使わなくても分かるようにする書き方もよく使われます。

constメンバ関数

constメンバ関数とは

メンバ関数の末尾に const を付けると、その関数はオブジェクトの状態を変更しない関数として扱われます。

値を取得するだけの関数や、内容を表示するだけの関数には、基本的に const を付けるのが望ましいです。

constを付けるメリット

const を付けることで、その関数がオブジェクトの状態を変更しないことを明示できます。

これはコードを読む人にとって分かりやすいだけでなく、コンパイラによるチェックにも役立ちます。

また、const なオブジェクトに対しては、const が付いたメンバ関数しか呼び出せません。

そのため、読み取り専用の関数には const を付けておくと、より柔軟に使えます。

厳密な注意点

const メンバ関数は、通常のメンバ変数を変更しないことを意味します。

ただし、mutable が付いたメンバ変数は、const メンバ関数の中でも変更できます。

また、ポインタをメンバとして持っている場合、そのポインタが指す先のデータを変更できてしまうこともあります。

そのため、const は「完全に何も変えない」というより、「そのオブジェクトの論理的な状態を変えない関数」と考えると理解しやすいです。

staticメンバ

staticメンバ変数

通常のメンバ変数は、オブジェクトごとに別々に存在します。

一方、static メンバ変数は、クラス全体で1つだけ存在します。

たとえば、作成されたオブジェクトの数を数えたい場合などに使われます。

各オブジェクトが個別に数を持つのではなく、クラス全体で共有する値として管理できます。

staticメンバ関数

static メンバ関数は、特定のオブジェクトに属さないメンバ関数です。

オブジェクトを作らなくても、クラス名を通じて呼び出すことができます。

ただし、static メンバ関数は特定のオブジェクトに紐づいていないため、通常のメンバ変数には直接アクセスできません。

アクセスできるのは、基本的に static メンバ変数や、引数として渡されたオブジェクトなどです。

C++17以降の補足

C++17以降では、inline static を使うことで、クラス内で static メンバ変数を初期化しやすくなりました。

古いC++では、static メンバ変数はクラスの外で定義する必要がありましたが、現代C++ではより簡潔に書ける場合があります。

クラス内定義とクラス外定義

クラス内定義

メンバ関数は、クラスの中に直接定義できます。

短い関数や単純な関数であれば、クラス内に書くこともあります。

たとえば、値を返すだけのgetterなどはクラス内に書かれることがあります。

クラス内に書いたメンバ関数は、暗黙的にインライン関数として扱われます。

クラス外定義

メンバ関数は、クラスの外に分けて定義することもできます。

この場合、クラスの中には関数の宣言だけを書き、実際の処理は別の場所に書きます。

実務では、クラスの宣言をヘッダファイルに書き、メンバ関数の実装をソースファイルに書くことがよくあります。

これにより、クラスのインターフェースと実装を分けられ、コードの見通しが良くなります。

ヘッダファイルとソースファイル

ヘッダファイルの役割

ヘッダファイルには、クラスの宣言を書くことが多いです。

クラス名、メンバ変数、メンバ関数の宣言などを記述し、他のファイルからそのクラスを使えるようにします。

ヘッダファイルは複数のソースファイルから読み込まれることがあるため、余計な影響を与えないように書くことが重要です。

ソースファイルの役割

ソースファイルには、メンバ関数の具体的な処理を書くことが多いです。

ヘッダファイルでは「どのような関数があるか」を示し、ソースファイルでは「その関数が実際に何をするか」を書きます。

このように分けることで、大きなプログラムでも管理しやすくなります。

include guardとpragma once

ヘッダファイルでは、同じ内容が二重に読み込まれないようにする必要があります。

そのために使われるのが、include guardです。

また、多くのコンパイラでは #pragma once も使えます。

これは簡潔に二重読み込みを防げるため、実務でもよく使われます。

ただし、#pragma once は標準C++で正式に規定された機能ではありません。

とはいえ、主要なコンパイラでは広くサポートされています。

ヘッダでusing namespaceを避ける

ヘッダファイルに using namespace std; のような記述を書くのは避けるべきです。

ヘッダは多くのファイルから読み込まれるため、そこで名前空間を広げてしまうと、読み込んだ側のファイルにも影響します。

その結果、名前の衝突や分かりにくいエラーの原因になることがあります。

実務的には、ヘッダファイルでは標準ライブラリの名前に明示的な名前空間を付ける書き方が望ましいです。

継承

継承とは

継承とは、既存のクラスをもとにして新しいクラスを作る仕組みです。

元になるクラスを基底クラス、そこから作られるクラスを派生クラスと呼びます。

たとえば、動物を表すクラスを作り、それをもとに犬や猫のクラスを作るような場合に継承が使えます。

継承はis-a関係で使う

継承は、「AはBの一種である」という関係が成り立つ場合に使うのが基本です。

たとえば、「犬は動物である」は自然です。

この場合、犬クラスが動物クラスを継承する設計は分かりやすいです。

一方、「車はエンジンである」は不自然です。

車はエンジンを持っていますが、エンジンそのものではありません。

この場合は継承ではなく、車クラスがエンジンをメンバとして持つ設計が自然です。

継承の使いすぎに注意する

継承は便利ですが、使いすぎると設計が複雑になります。

特に、単に機能を再利用したいだけで継承を使うと、クラス同士の関係が分かりにくくなることがあります。

現代C++では、継承よりも、部品として別のクラスを持つ設計を選ぶことも多いです。

これを合成またはコンポジションと呼びます。

仮想関数とオーバーライド

仮想関数とは

仮想関数とは、基底クラスで定義しておき、派生クラスで上書きできる関数です。

基底クラスのポインタや参照を通じて関数を呼び出したときに、実際のオブジェクトの型に応じた処理を実行できます。

この仕組みをポリモーフィズムと呼びます。

overrideの役割

派生クラスで基底クラスの仮想関数を上書きするときは、override を付けるのが望ましいです。

override を付けると、本当に基底クラスの関数を正しく上書きしているかをコンパイラが確認してくれます。

もし関数名を間違えたり、引数や const の有無が一致していなかったりすると、コンパイル時にエラーとして検出できます。

これはバグを防ぐうえで非常に有効です。

仮想デストラクタの重要性

ポリモーフィズムを使う基底クラスでは、仮想デストラクタを用意することが重要です。

基底クラスのポインタを通して派生クラスのオブジェクトを削除する場合、基底クラスのデストラクタが仮想でないと、派生クラス側の後始末が正しく行われない可能性があります。

そのため、仮想関数を持つような基底クラスでは、仮想デストラクタもセットで考えるのが基本です。

純粋仮想関数と抽象クラス

純粋仮想関数とは

純粋仮想関数とは、基底クラスでは具体的な処理を書かず、派生クラスに実装を強制するための関数です。

「この関数は必ず派生クラスで実装してください」という設計を表すために使います。

たとえば、図形を表す基底クラスを作り、面積を求める関数を用意するとします。

しかし、図形そのものには具体的な形がないため、面積の計算方法を決められません。

このような場合、基底クラスでは面積を求める関数を純粋仮想関数として宣言し、円や長方形などの派生クラスで具体的に実装します。

抽象クラスとは

純粋仮想関数を持つクラスは、抽象クラスになります。

抽象クラスは、そのままオブジェクトを作ることができません。

あくまで共通のインターフェースとして使います。

派生クラスが純粋仮想関数を実装することで、具体的なオブジェクトを作れるようになります。

抽象クラスの使いどころ

抽象クラスは、複数のクラスに共通の操作を持たせたいときに便利です。

たとえば、図形であれば、円、長方形、三角形などはすべて面積を求められます。

しかし、面積の計算方法はそれぞれ異なります。

このような場合に、抽象クラスで共通の関数だけを定義し、具体的な処理は派生クラスに任せる設計が有効です。

friend

friendとは

friend は、特定の関数やクラスに対して、privateprotected のメンバへアクセスする権限を与える仕組みです。

通常、private メンバにはクラスの外からアクセスできません。

しかし、friend として指定された関数やクラスは、特別にアクセスできます。

friendの使いどころ

friend は、演算子オーバーロードなどで使われることがあります。

たとえば、オブジェクトの内容を出力する処理や、左右対称に扱いたい演算子を定義するときに便利です。

friendの注意点

friend は便利ですが、使いすぎるとカプセル化を弱めます。

本来ならクラスの内部に隠しておきたい情報へ外部からアクセスできるようになるため、設計が複雑になりやすいです。

そのため、friend は必要な場面に限定して使うのが望ましいです。

演算子オーバーロード

演算子オーバーロードとは

演算子オーバーロードとは、自作クラスに対して、加算、比較、出力などの演算子の動作を定義する機能です。

たとえば、座標を表すクラスであれば、2つの座標を足す操作を定義できます。

金額を表すクラスであれば、金額同士の加算や比較を自然な形で書けるようにできます。

使うメリット

演算子オーバーロードを使うと、独自の型を自然な表現で扱えるようになります。

たとえば、数値のように足し算できるもの、比較できるもの、出力できるものなどは、演算子を定義するとコードが読みやすくなります。

使いすぎに注意する

演算子オーバーロードは便利ですが、意味が分かりにくい使い方をすると逆に読みにくくなります。

たとえば、足し算の記号なのに内部では削除処理を行うような設計は不自然です。

演算子は、一般的な意味から外れない範囲で使うことが大切です。

コピーコンストラクタ

コピーコンストラクタとは

コピーコンストラクタとは、既存のオブジェクトをもとに新しいオブジェクトを作るときに使われるコンストラクタです。

たとえば、ある人オブジェクトをコピーして、同じ内容を持つ別の人オブジェクトを作るような場合に使われます。

多くの場合は自分で書かなくてよい

メンバ変数が標準ライブラリの型や通常の値型で構成されている場合、コピーコンストラクタはコンパイラが自動で生成してくれます。

そのため、多くの場合、自分で明示的にコピーコンストラクタを書く必要はありません。

自分で書く必要があるのは、主に生ポインタでリソースを所有している場合や、コピー時に特別な処理をしたい場合です。

浅いコピーと深いコピー

コピーで注意すべき考え方に、浅いコピーと深いコピーがあります。

浅いコピーとは、ポインタの値だけをコピーして、指している先のデータまでは複製しないコピーです。

これにより、複数のオブジェクトが同じリソースを指してしまい、二重解放などの問題が起こることがあります。

深いコピーとは、指している先のデータも含めて新しく複製するコピーです。

現代C++では、このような問題を避けるために、標準ライブラリのコンテナやスマートポインタを使う設計が推奨されます。

コピー代入演算子

コピー代入演算子とは

コピー代入演算子とは、すでに存在しているオブジェクトに、別のオブジェクトの内容を代入するときに使われる処理です。

コピーコンストラクタは「新しく作るときのコピー」で、コピー代入演算子は「すでにあるものへ代入するときのコピー」です。

自己代入への注意

コピー代入では、自分自身を自分に代入する自己代入が起こる可能性があります。

通常のメンバだけを持つクラスではあまり問題になりませんが、リソースを直接管理しているクラスでは、自己代入を考慮しないと不具合が発生することがあります。

自動生成に任せるのが基本

多くのクラスでは、コピー代入演算子もコンパイラが自動生成するもので十分です。

標準ライブラリの型をメンバとして使っていれば、それぞれの型が適切にコピー処理を行ってくれます。

自分でコピー代入演算子を書くのは、特別なリソース管理が必要な場合に限るのが基本です。

ムーブコンストラクタ

ムーブとは

ムーブとは、コピーではなく、リソースの所有権を移動する考え方です。

大きなデータをコピーすると、処理に時間がかかります。

しかし、もう使わない一時オブジェクトからリソースを受け取るだけなら、データの中身を丸ごと複製しなくても済みます。

このような効率的な移動を実現するのがムーブです。

ムーブコンストラクタとは

ムーブコンストラクタは、別のオブジェクトからリソースを移動して新しいオブジェクトを作るためのコンストラクタです。

C++11以降で導入された重要な仕組みです。

ムーブにより、大きなデータを扱うクラスでも、無駄なコピーを減らせます。

実務では標準ライブラリに任せることが多い

ムーブコンストラクタは重要な仕組みですが、初心者が自分で実装する場面は多くありません。

現代C++では、標準ライブラリのコンテナやスマートポインタを使うことで、コピーやムーブの多くを安全に任せられます。

特に、生ポインタを直接所有するクラスでは、コピー、代入、ムーブ、破棄のすべてを慎重に設計する必要があります。

そのため、可能な限り標準ライブラリに任せる設計が望ましいです。

Rule of Three、Rule of Five、Rule of Zero

Rule of Three

Rule of Threeとは、デストラクタ、コピーコンストラクタ、コピー代入演算子のどれかを自分で定義するなら、他の2つも必要になる可能性が高いという考え方です。

これは主に、手動でリソースを管理するクラスで重要です。

たとえば、動的メモリを直接管理している場合、破棄だけでなく、コピー時や代入時の動作も正しく設計しなければなりません。

Rule of Five

C++11以降では、ムーブコンストラクタとムーブ代入演算子が加わりました。

そのため、リソース管理を自分で行うクラスでは、デストラクタ、コピーコンストラクタ、コピー代入演算子、ムーブコンストラクタ、ムーブ代入演算子の5つを考える必要があります。

これをRule of Fiveと呼びます。

Rule of Zero

Rule of Zeroとは、これらの特殊な関数をできるだけ自分で書かず、標準ライブラリに任せるべきという考え方です。

現代C++では、このRule of Zeroが特に重要です。

文字列には標準の文字列型を使い、配列には標準コンテナを使い、所有権のあるポインタにはスマートポインタを使うことで、自分でデストラクタやコピー・ムーブ処理を書く必要を減らせます。

これにより、コードが短くなり、安全性も高まります。

実務的なクラス設計の考え方

メンバ変数は基本的にprivateにする

実務では、メンバ変数は基本的に private にすることが多いです。

これは、外部から直接値を書き換えられることを防ぐためです。

特に、値にルールがある場合は、必ず private にして適切なメンバ関数経由で操作するのが望ましいです。

意味のある操作をpublicにする

クラスの外部には、単なるデータ操作ではなく、意味のある操作を公開することが大切です。

たとえば、銀行口座であれば、残高を自由に設定する操作ではなく、入金、出金、残高確認といった操作を公開します。

商品であれば、価格を変更する、割引を適用する、税込価格を取得するなど、そのクラスの意味に合った操作を用意します。

クラスの責任を明確にする

良いクラスは、責任が明確です。

人を表すクラスなら、人に関係するデータや処理に集中します。

銀行口座を表すクラスなら、口座の状態や入出金に関係する処理に集中します。

1つのクラスに何でも詰め込みすぎると、修正しにくく、再利用しにくいコードになります。

出力処理をクラスに入れるか考える

学習用のコードでは、クラスのメンバ関数内で画面出力を行うことがよくあります。

これは理解しやすい一方で、実務では注意が必要です。

クラスが直接画面出力を行うと、そのクラスを別の場面で使いにくくなることがあります。

たとえば、画面ではなくファイルに出力したい場合や、Webアプリで使いたい場合に困ることがあります。

そのため、実務ではクラスはデータや状態の管理に集中し、表示は別の処理に任せる設計にすることも多いです。

C++クラス定義でよくあるミス

クラス定義の最後にセミコロンを忘れる

C++では、クラス定義の最後にセミコロンが必要です。

これは初心者が非常によく間違えるポイントです。

通常の関数定義とは異なり、クラス定義の閉じかっこの後にはセミコロンを付けます。

privateメンバに外からアクセスしようとする

private にしたメンバ変数には、クラスの外から直接アクセスできません。

外部から値を取得したり変更したりしたい場合は、適切なメンバ関数を用意する必要があります。

ただし、すべてのメンバにgetterやsetterを用意すればよいわけではありません。

外部に公開する必要がある情報や操作だけを公開するのが基本です。

コンストラクタに戻り値を書いてしまう

コンストラクタには戻り値を書きません。

通常の関数のように戻り値を書いてしまうと、それはコンストラクタではなく、別のメンバ関数として扱われます。

初心者のうちは、コンストラクタは「クラス名と同じ名前で、戻り値を書かない特別な関数」と覚えるとよいです。

constを付け忘れる

値を取得するだけの関数や、状態を変更しない関数には const を付けるのが望ましいです。

const を付け忘れても動く場合はありますが、const なオブジェクトから呼び出せなかったり、関数の意図が伝わりにくくなったりします。

クラス設計では、「この関数は状態を変えるのか、変えないのか」を意識することが重要です。

using namespaceをヘッダに書く

ヘッダファイルに using namespace std; を書くのは避けるべきです。

ヘッダは多くのファイルから読み込まれるため、名前空間の影響が広がってしまいます。

学習用の短いコードでは使われることもありますが、実務では標準ライブラリの名前を明示する書き方が安全です。

初心者が最初に押さえるべきポイント

クラスはデータと処理をまとめる仕組み

C++のクラスは、関連するデータと処理をまとめるための仕組みです。

単に変数をまとめるだけではなく、そのデータに対してどのような操作を許可するのかも表現できます。

privateで内部状態を守る

メンバ変数は基本的に private にし、外部から直接変更できないようにするのが基本です。

特に、値に制約がある場合は、直接変更させず、メンバ関数を通じて安全に変更できるようにします。

publicで必要な操作だけを公開する

外部に公開するのは、クラスを使う側にとって必要な操作だけにします。

すべてのデータを公開するのではなく、そのクラスの役割に合った操作を公開することが重要です。

コンストラクタで正しく初期化する

オブジェクトは、作られた時点で有効な状態になっていることが理想です。

そのため、コンストラクタで必要な初期値を設定し、不正な状態のオブジェクトが作られないようにします。

constを意識する

状態を変更しないメンバ関数には、const を付けることを意識します。

これはコードの安全性や読みやすさにつながります。

現代C++では標準ライブラリを活用する

メモリ管理やリソース管理を自分で行うのは難しく、バグの原因になりやすいです。

現代C++では、標準ライブラリの型を活用し、できるだけ自分でデストラクタやコピー・ムーブ処理を書かない設計が推奨されます。

まとめ

C++のクラス定義は、データと処理をまとめ、プログラムを分かりやすく安全にするための重要な仕組みです。

クラスは設計図であり、そこから作られた実体がオブジェクトです。

クラスの中には、データを表すメンバ変数と、処理を表すメンバ関数を定義できます。

クラス設計で特に重要なのは、内部状態を守ることです。

メンバ変数を private にし、必要な操作だけを public として公開することで、不正な値や意図しない操作を防ぎやすくなります。

また、コンストラクタで正しく初期化し、状態を変更しない関数には const を付けることも大切です。

継承、仮想関数、抽象クラス、コピー、ムーブなどは少し発展的な内容ですが、C++のクラスを深く理解するうえで欠かせない要素です。

現代C++では、手動でメモリ管理を行うよりも、標準ライブラリを活用し、できるだけ安全でシンプルな設計にすることが推奨されます。

最初は、名前、年齢、商品、銀行口座、座標など、身近な題材で小さなクラスを作ってみると理解しやすいです。

C++のクラスは難しく感じるかもしれませんが、基本は「関連するデータと処理をひとまとめにする」という考え方です。

以上、C++のクラス定義についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次