こんにちは。田原です。
本日からC++らしい機能である「クラス」の解説を始めます。C言語にオブジェクト指向プログラミングのコアである「クラス」を追加した部分がC++の最大の拡張の1つですので、ここからは本格的なC++講座となります。少し難しくなりますが、なるべくサンプル・ソースを使って分かりやすく解説するようにしたいと思います。
今回はそのクラスの基本であるデータと関数をセットにする仕組みとコンストラクタ/デストラクタの概要を解説します。
C++のクラスは構造体と実はほぼ同じものです。細かい相違は後述しますので、まずは同じものとして捉えてくだいさい。
まず、クラスは次のように変数と関数があり、それぞれ非static、staticがあります。
- 非staticなメンバ変数
- 非staticなメンバ関数
- staticなメンバ変数
- staticなメンバ関数
非staticなメンバ変数とメンバ関数を一々「非staticな」と表現すると却って混乱しやすいため、当講座では「非staticな」は特に強調したい時を除き省略します。逆に「staticな」は省略しません。
そして、クラスの最も基本的な構成要素は、メンバ変数とメンバ関数です。今回はこの2つについて解説します。なお、staticなものについては後日解説します。 まずはstaticでない方のメンバ変数とメンバ関数に注力下さい。
第13回目で解説したように構造体はメンバ変数を集めたものです。クラスも同じくメンバ変数を集めたものとなります。
さて、C言語のようにグローバル関数しか無かった場合でも、このメンバ変数を操作する関数を定義することができます。
例えば、下記のような定義が考えられます。(この例題ではSetされた後の一回だけmDataをPrintしています。)
#include <iostream>
struct Foo
{
int mData;
bool mIsChanged;
};
void FooSet(Foo* foo, int iData)
{
foo->mData=iData;
foo->mIsChanged=true;
}
void FooPrint(Foo* foo)
{
if (foo->mIsChanged)
{
foo->mIsChanged=false;
std::cout << "mData=" << foo->mData << "\n";
}
else
{
std::cout << "No changed\n";
}
}
int main()
{
Foo now_data;
FooSet(&now_data, 123);
FooPrint(&now_data);
FooPrint(&now_data);
}
これらのFooSet()やFooPrint()は非常にFooと結びつきが強いです。だからこそプリフィックスFooをつけています。
そして、このような使い方はたいへん良く行いますのでC++ではこれがより便利になっています。
同じ処理を次のようにクラスの中へ記述することができます。
#include <iostream>
struct Foo
{
int mData;
bool mIsChanged;
void Set(int iData)
{
mData=iData;
mIsChanged=true;
}
void Print()
{
if (mIsChanged)
{
mIsChanged=false;
std::cout << "mData=" << mData << "\n";
}
else
{
std::cout << "No changed\n";
}
}
};
int main()
{
Foo now_data;
now_data.Set(123);
now_data.Print();
now_data.Print();
}
これらのSet()やPinrt()をメンバ関数と呼びます。
元のグローバル関数と大差はありませんが、fooを書かなくてはいけない場所が大幅に減り、かつ、Set()やPrintf()がFoo専用の関数であることを示すために一々プリフィックスを定義する必要がなくなります。
このプリフィクスを書かなくて良いことが、意外に便利です。例えば、Bar構造体を別途定義し、それもSet()とprint()を持っていたとします。
#include <iostream>
struct Bar
{
double mData;
void Set(double iData)
{
mData=iData;
}
void Print()
{
std::cout << "mData=" << mData << "\n";
}
};
int main()
{
Bar now_data;
now_data.Set(123.456);
now_data.Print();
now_data.Print();
}
このnow_dataの型がFooかBarであることを知っていれば、どっちなのか解らなくても使えます。(もちろん、結果は異なります。)
この性質は異なるものでも同じ性質を持っていれば同じ手順で取り扱えるということです。非常に便利な性質であり、後日解説するポリモーフィズムと言う仕組みで積極的に使われています。
C++のメンバ関数は先に書いたようにFooやfooを省略できるので、FooPrint()の定義が少し単純になりました。
この書き換えは単純作業ですね。実はこの書き換えをコンパイラが自動的にやっています。
上記のBar構造体はコンパイラが内部的に次のように書き換えてます。(画面はイメージです。実際のものとは異なります。)
#include <iostream>
struct Bar
{
double mData;
};
void BarSet(Bar* const iThis, double iData)
{
iThis->mData=iData;
}
void BarPrint(Bar* const iThis)
{
std::cout << "mData=" << iThis->mData << "\n";
}
int main()
{
Bar bar;
BarSet(&bar, 123.456);
BarPrint(&bar);
}
つまり、メンバ関数はクラス名をプリフィクスとし(*1)、隠しパラメータとしてクラスへのポインタを受け取る(*2)ようにコンパイラにより自動的に変換されて、内部的にはグローバル関数として実装されるのです。
(*1)クラス名をプリフィクスとする
これはマングリング(mangling)と呼ばれる仕組みです。同じ名前の関数を内部的に異なる関数として取り扱うために、必要なプリフィクスやポストフィクスを自動的に追加します。後日説明する関数のオーバーロードを実現するためにも使われます。
(*2)クラスへのポインタを受け取る
これはthisポインタとよばれます。例ではiThisとしていますが、メンバ関数の中ではthisと書いて全く同じ意味で使えます。隠しパラメータとして渡されます。なお、thisポインタは変数ではなく定数ですのでconstが付いています。thisポインタが指す先にある構造体の中身を変更できますが、thisポインタに設定されているアドレスを変更することはできません。(constはかなり難易度の高い概念ですので、しばらく先で解説する予定です。)
先にも述べたようにC++のクラスは構造体と事実上同じものです。唯一の違いは、デフォルトでメンバを外部に公開しているか居ないかです。これは以下のアクセス指定子で指定します。
アクセス指定子は、classやstruct内部で定義したメンバを外部に公開するかどうかを指定します。
public:と書いた後に記述されたメンバは外部から使うことが出来ます。
class Foo
{
public:
int mData0;
short mData1;
};
int main()
{
Foo foo;
foo.mData0=123; // 外部からのアクセスが許可されているのでエラーにならない
foo.mData1=456; // 外部からのアクセスが許可されているのでエラーにならない
}
private:と書いた後に記述されたメンバは外部から使うことが出来ません。
原則としてクラス内のメンバ関数からのみ使えます。
class Bar
{
private:
int mData0;
short mData1;
public:
void setData(int iData0, short iData1)
{
mData0=iData0;
mData1=iData1;
}
};
int main()
{
Bar bar;
// bar.mData0=12; // 外部からのアクセスが禁止されているのでエラー
// bar.mData1=34; // 外部からのアクセスが禁止されているのでエラー
bar.setData(12, 34); // 外部からのアクセスが許可されているのでエラーは出ない
}
publicやprivate等のアクセス指定子を指定する前のアクセス指定のデフォルト値だけが異なります。
struct Foo
{
int mData0; // デフォルトはpublicなので公開
short mData1;
};
int main()
{
}
class Bar
{
int mData0; // デフォルトはprivateなので非公開
short mData1;
public:
void setData(int iData0, short iData1)
{
mData0=iData0;
mData1=iData1;
}
};
int main()
{
Foo foo;
foo.mData0=123; // 外部からのアクセスが許可されているのでエラーにならない
foo.mData1=456; // 外部からのアクセスが許可されているのでエラーにならない
Bar bar;
// bar.mData0=12; // 外部からのアクセスが禁止されているのでエラー
// bar.mData1=34; // 外部からのアクセスが禁止されているのでエラー
bar.setData(12, 34); // 外部からのアクセスが許可されているのでエラーは出ない
}
このように機能的には事実上同じものですので、使い分けが悩ましいと思います。
大まかな考え方としては、以下の方針が考えられます。
- 構造体(struct)
データを保持することが目的です。複数の変数を取りまとめたい場合で、そのデータ構造とそのデータ構造を使う処理が密に結びついてないケースで多く使われます。
例えば、2次元平面の座標を表現するためにメンバ変数x, yを持つPointという構造体かクラスを定義したとします。座標をどのように取り扱うのかはPoint自身とはあまり密接に関係しません。例えば3つのPointを与えて三角形の面積を計算する関数をPointのメンバ関数として実装することはないでしょう。また、1つのPointだけを操作したいケースも多くはないと思います。このようなPointは構造体として定義されることが多いです。
このようにどちらかと言うと、構造体を取り扱う関数が構造体専用ではなく、他の処理の過程で構造体を取り扱うような場合に構造体で定義することが多いです。 -
クラス(class)
基本的にはオブジェクト指向プログラミングを行うために用います。
オブジェクト指向プログラミングはたいへん強力なパラダイムですし、多くの人は通常classを用いますので、悩んだ時はclassにしておけば大きく外すことはまずありません。
クラスや構造体の初期化処理としてコンストラクタがあることを第13回目に解説しました。
そして、実に有り難いことにC++のクラスと構造体は「自動的に」呼ばれる終了処理としてデストラクタを定義することができます。
定義した変数の寿命が尽きる時、デストラクタが自動的に呼ばれます。これはびっくりする程便利です。
デストラクタは次のようにして定義します。クラス名の頭に ~(チルダ)をつけます。仮引数はありませんので、定義できるデストラクタは1つだけです。
~クラス名()
{
実行文;
}
具体例です。例えば、C++では動的に要素数が決まる配列を使うことができませんが、デストラクタのお陰で簡単に使えるようになります。
#include <iostream>
class IntArray
{
std::size_t mSize;
int* mElements;
public:
IntArray(std::size_t iSize) : mSize(iSize), mElements(new int[mSize]{})
{
std::cout << "constructed IntArray(" << mSize << ")\n";
}
~IntArray()
{
delete[] mElements;
std::cout << "destructed IntArray(" << mSize << ")\n";
}
int& at(std::size_t index) { return mElements[index]; }
std::size_t size() { return mSize; }
};
int main()
{
std::size_t aSize;
std::cout << "Array size ? ";
std::cin >> aSize;
{
std::cout << "start of inner block\n";
IntArray aArray(aSize);
std::cout << "post define aArray\n";
for (std::size_t i=0; i < aArray.size(); ++i)
{
aArray.at(i)=i*2+1;
}
for (std::size_t i=0; i < aArray.size(); ++i)
{
std::cout << aArray.at(i) << " ";
}
std::cout << "\n";
std::cout << "end of inner block\n";
}
std::cout << "end of main()\n";
}
以下の手順でデストラクタの呼ばれ方を確認して見て下さい。
この自動的な後始末の仕組みを理解すると、本当に様々な場面で応用できます。
std::cout << "end of inner block\n";の次の行にブレーク・ポイントを張ってデバッグ実行し、sizeを入力して下さい。
ブレーク・ポイントで停止したら、ステップイン(F11)とステップオーバー(F10)で適宜実行して、デストラクタがどこで呼ばれるのか確認してみてください。
std::cout << "end of inner block\n";の行にブレーク・ポイントを張ってデバッグ実行し、sizeを入力して下さい。
ブレーク・ポイントで停止したら、Step intoでステップ実行し、デストラクタがどこで呼ばれるのか確認してみてください。
如何でしたでしょうか?
今回はC++のクラスの極基本的な部分を解説しました。特に重要なことは、次の2点です。
- メンバ関数の内部的な実装はグローバル関数と大きな差はなく、クラスへのポインタが隠しバラメータとして渡されていること
- 後始末関数でデストラクタはクラス型の変数が破棄されるタイミングで自動的に呼ばれること
特に後者の性質は重要です。メモリや各種のリソースを確保する時、デストラクタで開放するようにし、ローカル変数のように必要がなくなった時に開放されるようにしておくことで自動的にリソースが開放されます。これにより頭の痛いリソース・リークを回避するプログラミングが非常に容易になります。
さて、次回はコンストラクタとデストラクタについてもう少し細かい仕組みや性質について解説します。
少し難しいですが、把握しておくと無用なトラブルに巻き込まれにくくなります。
