クラスとオブジェクトに関する解説をまとめます。
0. 目次
1. クラスとオブジェクト概論
クラスの基本は C 言語で言うところの構造体に『操作』する関数も追加になったものです。構造体は『一まとめにすると扱いやすい』変数郡を表す『型』でした。先ずは『型』と『インスタンス』についておさらいしましょう。
1.1. 型とインスタンス
仮に本屋さんで使用するシステムを考えて見ましょう。本の入荷状況と売り上げ、在庫を管理するシステムです。まず、一種類の本に関するタイトル・著者情報、入荷冊数、販売冊数を管理したい。C言語でできることから考えた場合、構造体でまとめてデータベースを作ることになるでしょう。
// 書籍データ用構造体 struct Book { char m_pszTitle[256]; // タイトル char m_pszAuther[256]; // 筆者 int m_nPrice; // 値段(円) int m_nArrivedValue; // 入荷した数 int m_nSelled;Value; // 売れた数 };
管理するのが『ハリーポッター』と『スターウォーズ』の2種類だったとして、以下のようにインスタンスを作るわけですね。
// 書籍ハリーポッター
Book HarryPotter;
strcpy( HarryPotter.m_pszTitle, "ハリーポッター賢者の石" );
strcpy( HarryPotter.m_pszAuther, "J.K.ローリング" );
HarryPotter.m_nPrice = 1900;
HarryPotter.m_nArrivedValue = 52;
HarryPotter.m_nSelledValue = 0;
// 書籍スターウォーズ
Book StarWars;
strcpy( StarWars.m_pszTitle, "スターウォーズ暗黒の旅路 上巻" );
strcpy( StarWars.m_pszAuther, "エレイン・カニンガム" );
StarWars.m_nPrice = 780;
StarWars.m_nArrivedValue = 12;
StarWars.m_nSelledValue = 0
Book が 『型』 であり、HarryPotter、StarWars が 『インスタンス』 となりますが、ここでぜひ考察してい見て欲しいのです。日本語で、「本」が 型 であり、「ハリーポッター」と「スターウォーズ」がインスタンスです。同様のアナロジーで、「鳥」が 型 であり、「雀」「燕」「鶏」はインスタンスです。つまり現実事象の記述で「定義 ( define ) 」と「存在 (presence) 」の関係と、「型」と「インスタンス」の関係にあるアナロジーは非常によくマッチしているのです。この点、後のオブジェクト指向の考え方に繋がっていきますので、注目してください。
1.2. 型について考える
型というものが定義のアナロジーとして考えると良くマッチすることを 1.1. 型とインスタンス で述べました。ここがソフトウェアにとって大きな発見です。構造化言語よりのソフトウェアの悩みは、ソフトウェア仕様とソースコードが一致できない点です。仕様をよく読んで、そこから構造化で組み立てなおす必要がありました。もし、ソースコードが実際の仕様と言うものに対して近い存在(つまり、現実事象とソースコードが近い存在)として考えることができたら、効率が上がりそうです。その手法として、先のアナロジーを取り込んでしまおう、と言うのがオブジェクト指向のコトの発端なのです。全ての動作仕様を「定義」と「存在」で抽象化していくと言う手法です。しかしながら、プログラムの世界でC言語の型とインスタンスだけでは「存在」できる定義を設計するのが難しい。なぜなら、プログラムの世界ではデータはそこに「ある」だけで、その振る舞いを記述するのは「コード」であるからです。であるならば、「コード」も構造体へ含んでしまおう、と言うのが次に述べる「クラス」と言うわけです。
1.3. クラスとオブジェクト
クラスとオブジェクトの関係は 1.1. で述べた、型とインスタンスの関係です。クラスは「定義」のより良い抽象化のために「コード」が含まれた構造体「型」であり、付随してオブジェクトはこの「コード」を呼び出すことのできるインスタンスです。この枠組みが基本中の基本です。クラスとは「型」であり、オブジェクトとは「インスタンス」です。この点は何度でも立ち返って見てください。
2. クラスを記述する
1. クラスとオブジェクト概論 で登場した本屋さんシステムの例を用いて、実際にクラスを設計してみましょう。
2.1. 操作の検討
構造体 Book はこのシステムにおける本の定義です。このシステムにおける本は、どんなことをするでしょうか。今、Book はタイトル、筆者、値段、入荷した数、売れた数と言うデータを持っています。勘所としては、これらデータに仕様に対してどんな作業が必要か検討していきます。今回は、以下のような作業を Book の定義に加えて見ましょう。
- データの初期化
- 在庫冊数の算出
- 売り上げの算出
- 一冊売れたことの通知
- データ表示
これら「操作」が先に述べた「コード」の部分となります。C++言語ではそれらコードは全て関数として記述し、構造体の中に含めることができるのです。これを(メンバ変数に対して)メンバ関数と言います。
2.2. 実装
それではクラスを実装して行きましょう。
2.2.1. 型名とメンバ変数を定義
C++言語のクラスは構造体の拡張です。構造体のように型とデータを定義をします。型の名前はクラスである C を先頭につけて CBook とします。
// 書籍クラス
class CBook
{
const char* m_pszTitle; // タイトル
const char* m_pszAuther; // 筆者
const int m_nPrice; // 値段(円)
const int m_nArrivedValue; // 入荷した数
int m_nSelledValue; // 売れた数
};
この色で書かれた箇所に注目ください。まず、構造体と違うことは struct が class になっている点、メンバ変数のものによって const が追加されました。const の意味は最初の一回だけ初期化され、その後変更がないことをメンバ変数に対して示すものです。これらメンバ変数に対して、書き込みを行うとコンパイルエラーとなります。
2.2.2. メンバ関数を定義
いよいよクラスの本懐、メンバ関数を実装しましょう。
2.2.2.1. 基本的なメンバ関数を実装
今回追加する操作のうち、データの初期化を除く、機能を追加してみましょう。データの初期化を行うメンバ関数は 2.2.2.3. コンストラクタ、デストラクタを実装 にて実装します。
メンバ関数をクラスに定義するには、以下のように class 定義のブロック { 〜 } の中に関数を定義します。
// 書籍クラス
class CBook
{
const char* m_pszTitle; // タイトル
const char* m_pszAuther; // 筆者
const int m_nPrice; // 値段(円)
const int m_nArrivedValue; // 入荷した数
int m_nSelledValue; // 売れた数
// -----------------------------------
// Summary:
// 売り上げの算出
// Returns:
// 売り上げ額(円)
int GetSells() const
{
return m_nSelledValue * m_nPrice;
}
// -----------------------------------
// Summary:
// 残数の算出
// Returns:
// 残数
int GetRestValue() const
{
return m_nArrivedValue - m_nSelledValue;
}
// -----------------------------------
// Summary:
// 一冊売れた
void Selled()
{
m_nSelledValue++;
}
// -----------------------------------
// Summary:
// 状態の表示
void Disp()
{
cout << m_pszTitle << "のデータ" << endl;
cout << "--------------------------" << endl;
cout << "著者:" << m_pszAuther << endl;
cout << "値段:" << m_nPrice << endl;
cout << "入荷した冊数:" << m_nArrivedValue << endl;
cout << "販売した冊数" << m_nSelledValue << endl;
cout << "売り上げ:" << GetSells() << endl;
cout << "残冊数:" << GetRestValue() << endl;
}
};
この色で書かれている const の意味ですが、これはそれらメンバ関数がクラスのメンバ変数に書き込みを行わないことを明示するためのものです。
2.2.2.2. インターフェースを考える
次に、定義した CBook メンバ関数のうち、どのメンバ関数が外から呼び出されるか?を仕様から検討します。今回、レジにこのシステムは導入され、店員さんが「一冊売れたな」と思ったらボタンでも押してその旨を通知。その都度状態を表示する仕様であると考えましょうか。
とすると、外部に公開するべきは、CBook::Selled() と CBook::Disp() だけとなりそうです。この宣言は以下の赤色で示すように行います。
// 書籍クラス
class CBook
{
// 以下は公開しないメンバ
protected:
const char* m_pszTitle; // タイトル
const char* m_pszAuther; // 筆者
const int m_nPrice; // 値段(円)
const int m_nArrivedValue; // 入荷した数
int m_nSelledValue; // 売れた数
// -----------------------------------
// Summary:
// 売り上げの算出
// Returns:
// 売り上げ額(円)
int GetSells() const
{
return m_nSelledValue * m_nPrice;
}
// -----------------------------------
// Summary:
// 残数の算出
// Returns:
// 残数
int GetRestValue() const
{
return m_nArrivedValue - m_nSelledValue;
}
// 以下は公開するメンバ
public:
// -----------------------------------
// Summary:
// 一冊売れた
void Selled()
{
m_nSelledValue++;
}
// -----------------------------------
// Summary:
// 状態の表示
void Disp()
{
cout << m_pszTitle << "のデータ" << endl;
cout << "--------------------------" << endl;
cout << "著者:" << m_pszAuther << endl;
cout << "値段:" << m_nPrice << endl;
cout << "入荷した冊数:" << m_nArrivedValue << endl;
cout << "販売した冊数" << m_nSelledValue << endl;
cout << "売り上げ:" << GetSells() << endl;
cout << "残冊数:" << GetRestValue() << endl;
}
};
protected: で示されるより下のメンバ変数・メンバ関数が非公開、public: 以下が公開となるわけです。このようにする理由は、外側から見たこのクラスの抽象度を上げる役割によるものです。仮に他のプログラマがこのクラスを用いる場合、public: 以下だけを参照すれば良いわけです。また、バグ修正も、public: として公開したもの以外は自由に改変ができます。逆に、公開したメンバは、その名前や引数を絶対変えてはいけません。呼び出し側ソースコードも書き換える必要が出てしまうからです。このように外部に公開され、仕様化されたメンバ関数を『インターフェース』と言うわけです。また、このように内容をできる限り外側から見えなくすることを『隠蔽(エンカプセレーション)』と呼びます。
その他、 private: と言うさらに強い非公開指定もありますが、後に述べる継承を用いない限りは protected: で十分でしょう。
2.2.2.3. コンストラクタ・デストラクタを実装
コンストラクタは日本語で生成子、デストラクタは消滅子と言い、いずれも特殊なメンバ関数です。コンストラクタはクラスがオブジェクトとして宣言された時に初期化用として呼び出されます。デストラクタは宣言されたオブジェクトがメモリから消滅する前に呼び出されるメンバ関数です。クラス名と同じ名前でそれぞれ、以下のように定義します。
コンストラクタ
[クラス名]( { 引数リスト… } )
デストラクタ
~[クラス名]()
一般にコンストラクタの中ではメンバ変数の初期化を行います。最新の C++ コンパイラでは、初期化が行われないとコンパイルエラーとなります。(過去のコンパイラではエラーとしない場合もあります)以下、実装例を示します。
// 書籍クラス class CBook { // 以下は公開しないメンバ protected: const char* m_pszTitle; // タイトル const char* m_pszAuther; // 筆者 const int m_nPrice; // 値段(円) const int m_nArrivedValue; // 入荷した数 int m_nSelledValue; // 売れた数 // ----------------------------------- // Summary: // 売り上げの算出 // Returns: // 売り上げ額(円) int GetSells() const { return m_nSelledValue * m_nPrice; } // ----------------------------------- // Summary: // 残数の算出 // Returns: // 残数 int GetRestValue() const { return m_nArrivedValue - m_nSelledValue; } // 以下は公開するメンバ public: // ----------------------------------- // Summary: // コンストラクタ // Arguments: // pszTitle: タイトル // pszAuther: 著者 // nPrice: 値段 // nArrivedValue: 入荷した数 // nSelledValue: 売れた数 CBook( const char* pszTitle, const char* pszAuther, const int nPrice, int nArrivedValue, int nSelledValue ) :m_pszTitle( pszTitle ), m_pszAuther( pszAuther ), m_nPrice( nPrice ), m_nArrivedValue( nArrivedValue ) { m_nSelledValue = nSelledValue; } // ----------------------------------- // Summary: // デストラクタ virtual ~CBook(){} // ----------------------------------- // Summary: // 一冊売れた void Selled() { m_nSelledValue++; } // ----------------------------------- // Summary: // 状態の表示 void Disp() { cout << m_pszTitle << "のデータ" << endl; cout << "--------------------------" << endl; cout << "著者:" << m_pszAuther << endl; cout << "値段:" << m_nPrice << endl; cout << "入荷した冊数" << m_nArrivedValue << endl; cout << "販売した冊数" << m_nSelledValue << endl; cout << "売り上げ" << GetSells() << endl; cout << "残冊数" << GetRestValue() << endl; } };
コンストラクタの中ではメンバ変数の初期化を行いますが、const を宣言したメンバ変数への代入は許されません。その場合、赤字の以下のコードにあるように記述します。この動作はオブジェクトの種類を決定するだけのメンバ変数(この例だと本のタイトルや著者)に使用します。
:m_pszTitle( pszTitle ), m_pszAuther( pszAuther ), m_nPrice( nPrice ), m_nArrivedValue( nArrivedValue )
今回の例ではデストラクタで何も行っていません。ですが、virtual と言うキーワードが見えます。このデストラクタに対する virtual 宣言は「騙された」と思って必ず行ってください。理由の詳細はポリモーフィズムの章にてお話します。
3. オブジェクトを使う
2. クラスを記述する でクラスの定義が完成しました。実際にオブジェクトを宣言して使用してみましょう。
int main()
{
/////////////////////////////////////////////////
// ★1 オブジェクトの生成
// 書籍ハリーポッターオブジェクト
CBook HarryPotter( "ハリーポッター賢者の石",
"J.K.ローリング",1900, 52, 0 );
// スターウォーズオブジェクト
CBook StarWars( "スターウォーズ暗黒の旅路 上巻",
"エレイン・カニンガム", 780, 12, 0 );
/////////////////////////////////////////////////
// ★2 メンバ関数呼び出し
// ハリーポッター1冊売れた
HarryPotter.Selled();
HarryPotter.Disp(); // 売り上げ表示
// スターウォーズ2冊売れた
StarWars.Selled();
StarWars.Selled();
StarWars.Disp(); // 売り上げ表示
return -1;
}
★1 オブジェクトの生成が CBook からオブジェクトを生成しているコードです。ちょうど構造体のインスタンス宣言と同じですが、コンストラクタで定義した引数を渡して、メンバ変数(属性と言う言い方をすることもあります)を初期化する必要があります。この時点で 2.2.2.2 で述べたコンストラクタが呼び出されています。なお、main 関数ブロックの auto なオブジェクトですから、return -1 として main 関数を抜ける際、各オブジェクトはメモリ上から消滅( delete )します。そのタイミングではデストラクタが呼び出されています。
★2 メンバ関数呼び出しが CBook のインターフェース呼び出しの例です。 構造体のメンバ変数のアクセスに似ていますね。以下に実行結果を示します。

3.1. 動的に生成して、ポインタで使う
書籍データを追加したり削除したりする機能を実現する場合、やはり動的にオブジェクトを生成・消滅させたいところです。また、リスト構造を使って管理すると、ソースコードはさらに見通し良くなることが往々にしてあります。以下に HarryPotter、StarWars オブジェクトを動的生成してリスト管理で操作するサンプルを示します。
int main()
{
// オブジェクトの生成
CBook* pHarryPotter =
new CBook( "ハリーポッター賢者の石",
"J.K.ローリング",1900, 52, 0 );
CBook* pStarWars =
new CBook( "スターウォーズ暗黒の旅路 上巻",
"エレイン・カニンガム", 780, 12, 0 );
// オブジェクトへのポインタ列
CBook* BooksArray[2] = { pHarryPotter, pStarWars };
// ハリーポッター1冊売れた
pHarryPotter->Selled();
// スターウォーズ2冊売れた
pStarWars->Selled();
pStarWars->Selled();
// 一括表示
for( int i = 0; i < 2; i++ ){
BooksArray[i]->Disp();
}
// オブジェクトの破棄
delete pHarryPotter;
delete pStarWars;
getchar();
return -1;
}
インターフェースの呼び出しは、構造体の時がそうであったように -> を使います。new / delete は C言語で言うところの malloc / free 関数です。
4. 補足事項
以下、補足事項です。
4.1. 参照型
C++言語の拡張された記述方法の一つに参照型というものがあります。参照型とは可変引数のことです。
void IncrementValue( int& nValue )
{
nValue++;
}
int main()
{
int nValue = 3;
cout << "最初の値:%d" << nValue << endl;
IncrementValue( nValue );
cout << "結果の値:%d" << nValue << endl;
}
出力結果は4となります。C言語において可変引数はポインタを用いることで実現されていましたが、C++ では 「&」 を型の後につけることで実現できます。これはメンバ関数の引数としても当然使用可能です。
4.2. デフォルト引数
以下のようにして、引数にデフォルト値と言うものを定義することが可能です。
void MyFunction( int nArg1, int nArg2 = 3, int nArg3 = 5 )
{
cout << "Arg1=%d; Arg2=%d; Arg3=%d" << nArg1 << nArg2 << nArg3 << endl;
}
int main()
{
MyFunction( 0, 0, 0 ); // 0, 0, 0 が表示
MyFunction( 0, 0 ); // 0, 0, 5 が表示
MyFunction( 0 ); // 0, 3, 5 が表示
}
注意点は、デフォルトを持つ引数は、引数リストの右側(この例だと int nArg3 = 5 )から定義しなければなりません。nArg3 のデフォルト引数なしに nArg2 のデフォルト引数を定義することはできません。
4.3. 定義ファイル、実装ファイル
クラスを作る場合、コンパイル効率を良くするために、一つのクラス毎に定義ファイル(*.h)と実装ファイル(*.cpp)に分けて記述するのが常套的です。これはクラス定義と、メンバ関数の実装部を分けると言うことです。以下のような形と記述なります。
book.h ファイル ============================================================
// 書籍クラス
class CBook
{
// 以下は公開しないメンバ
protected:
const char* m_pszTitle; // タイトル
const char* m_pszAuther; // 筆者
const int m_nPrice; // 値段(円)
const int m_nArrivedValue; // 入荷した数
int m_nSelledValue; // 売れた数
int GetSells() const;
int GetRestValue() const;
// 以下は公開するメンバ
public:
CBook( const char* pszTitle, const char* pszAuther,
const int nPrice, int nArrivedValue, int nSelledValue );
~CBook();
void Selled();
void Disp();
};
book.cpp ファイル ==========================================================
// -----------------------------------
// Summary:
// 売り上げの算出
// Returns:
// 売り上げ額(円)
int CBook::GetSells() const
{
return m_nSelledValue * m_nPrice;
}
// -----------------------------------
// Summary:
// 残数の算出
// Returns:
// 残数
int CBook::GetRestValue() const
{
return m_nArrivedValue - m_nSelledValue;
}
// -----------------------------------
// Summary:
// コンストラクタ
// Arguments:
// pszTitle: タイトル
// pszAuther: 著者
// nPrice: 値段
// nArrivedValue: 入荷した数
// nSelledValue: 売れた数
CBook::CBook( const char* pszTitle, const char* pszAuther,
const int nPrice, int nArrivedValue, int nSelledValue )
:m_pszTitle( pszTitle ), m_pszAuther( pszAuther ),
m_nPrice( nPrice ), m_nArrivedValue( nArrivedValue )
{
m_nSelledValue = nSelledValue;
}
// -----------------------------------
// Summary:
// デストラクタ
CBook::~CBook()
{
}
// -----------------------------------
// Summary:
// 一冊売れた
void CBook::Selled()
{
m_nSelledValue++;
}
// -----------------------------------
// Summary:
// 状態の表示
void CBook::Disp()
{
cout << m_pszTitle << "のデータ" << endl;
cout << "--------------------------" << endl;
cout << "著者:" << m_pszAuther << endl;
cout << "値段:" << m_nPrice << endl;
cout << "入荷した冊数:" << m_nArrivedValue << endl;
cout << "販売した冊数" << m_nSelledValue << endl;
cout << "売り上げ:" << GetSells() << endl;
cout << "残冊数:" << GetRestValue() << endl;
}
定義ファイルはインスタンスを作るために様々な箇所から呼び出されるため、定義ファイルを変更すると呼び出している全てのファイルをコンパイルしなおさなければならなくなってしまいます。頻繁に変更するであろう実装部を別ファイルにした、と言うのがこのようにする理由の一つです。また、定義と言うものはその時点でコンパイラがコードをはくものではありません。実際にオブジェクトとして宣言されて始めてデータ領域が確保され、メンバ関数コードとリンクするわけです。その意味でこの2つを分けた方が良いのです。
5. 演習課題
名刺管理を行うコマンドラインアプリケーションを、以下のガイドラインにしたがって、作成してください。
- 『名刺』でクラス定義することを検討する。
- 複数枚の名刺を管理する配列を作成する。データは固定で良い。
- 名前順にソートできるようにする。