C++マニアック

クラス、class

クラス(class)は、C++ では、プログラミングの中心的役割を果たします。ここでは、その基本について解説します。

  1. クラスとは
  2. クラスの定義
  3. クラスの定義のもうひとつの側面(重要)
  4. クラス関連本

クラスとは ページの先頭へ

クラスとは何か、ということをひとことで言えば、クラスとは、C++ における変数であるといえます。

C 言語や C++ には、最初から組み込まれている boolintlongfloatdouble などの変数型がありますね。クラスは、これらの単純な変数型では表せないモノをあらわすために、ユーザが定義可能な変数型であると考えてよいでしょう。

変数型というものについて int を例にとって考えてみましょう。int 型の変数には、整数値を保存することができます。保存した整数値を読み出すこともできますね。他の整数と加算、減算、乗算、除算することもできます。また、float 型の変数に代入することもできますね。

このように、変数には、その状態である値を保存したり、読み出したりできるという性質と、それに加えて、加算、乗算などの操作ができるという性質があります。

この、変数の性質を一般的に考えると、変数とは内部に何らかの状態を持ち、何らかの操作が可能なものということができます。上記の int の例では、状態というのは、保存されている整数値であり、操作というのは、2 を掛けると 2 倍の値になるというようなことに対応しています。

上記で変数と呼んできたものを、オブジェクト(=モノ)と考えることもできます。int、float なども値を持ち、計算ができるという性質を持つモノと考えるわけです。特に、クラスでモノを表す場合、クラス変数とかクラスオブジェクトとか呼びます。どちらも意味は同じです。変数=オブジェクト、と考えてください。

クラスの定義 ページの先頭へ

では、早速、新しい変数型(=クラス)を定義してみましょう。最初は最も単純なものから。

class CManiac {};

上記は、CManiac という名前のクラスの定義です。このクラス(モノ)の性質は、変数型が CManiac と言う名前であるということです。クラスに状態や、操作を定義するには、{} の中にいろいろ書くことになるのですが、上記の例では中身は空っぽです。これ以上単純なモノはないですね。クラス(class)の定義なので、クラス名を C から初めて、CManiac という名前にしましたが、文法的には、名前を C で始める必要はありません。しかし、クラスであることが分かりやすいので、私は C で始めるようにしています。

上記は、クラスの定義でしたが、次に CManiac オブジェクト(=変数)を定義してみましょう。

int main() {
    CManiac maniac;
    return 0;
}

これで、maniac という名前の CManiac オブジェクトが定義され、使われたことになります。CManiac 型には、状態や操作が何も定義されていないので、これは、ほぼ無意味ですが、文法的にはこれで全く正しいわけです。このような、内容の無いクラスに全く意味がないかというとそうでもなく、例えば、new でメモリを取れなかった場合に投げられる bad_alloc という例外オブジェクトは、CManiac と同様、bad_alloc という名前であるという性質しかありません。なので、上記の CManiac も、あながち、馬鹿にしたものではありません。

とはいえ、これでは、あっさりしすぎなので、クラス定義に少しずつ要素を付け加えていきましょう。CManiac は、マニアックの程度を表すクラスであるとしましょう。そこで、状態として、どの程度マニアックなのかを整数値で保存することにします。通常、状態を保存するにはプライベートなメンバ変数を使います。

class CManiac {
private:
    int m_iHowManiac;                       // マニアックの程度を保存する内部の変数
};

この段階では、C 言語の構造体と殆ど同じですが、メンバ変数がプライベートであるため、外から m_iHowManiac にアクセスすることはできません。つまり、次のように書いても、コンパイルエラーになります。

int main() {
    CManiac maniac;
    maniac.m_iHowManiac;    // こう書いても m_iHowManiac がプライベートなので、アクセスできない。
    return 0;
}

また、m_iHowManiac の初期値も決まっていません。まずこれを解決しましょう。そのためには、コンストラクタを使います。コンストラクタとは、クラスのメンバの初期化を行うメンバ関数で、名前はクラス名と同じにします。また、コンストラクタは、外からアクセスできる必要があるので、通常はパブリックにします。

class CManiac {
private:
    int m_iHowManiac;                       // マニアックの程度を保存する内部の変数
public:
    CManaic() : m_iHowManiac(0) {}     // コンストラクタ、最初のマニアックの程度は 0 とする。
};

上記のコンストラクタでは、関数の中身は何もありません。m_iHowManiac の初期化は、: に続けて m_iHowManiac のコンストラクタを呼んでいます。この部分を初期化子並びとか初期化子リストと呼びます。m_iHowManiac は int 型であり、クラスオブジェクトではないので、本来コンストラクタはありませんが、このように組み込み型の変数もコンストラクタと同じ形式で初期化を記述します。上記の例はメンバ変数がクラスオブジェクトではないので、次のように関数本体の中で初期化しても問題ありません。メンバ変数がクラスオブジェクトの場合には、コンストラクタの中で初期化しようとしても手遅れな場合が多く、その場合には必然的に初期化子並びにコンストラクタを記述して、初期化することになります。

class CManiac {
private:
    int m_iHowManiac;                       // マニアックの程度を保存する内部の変数
public:
    // コンストラクタ
    CManaic()  {
        m_iHowManiac = 0;
    }
};

さて、初期化はできましたが、このままではマニアックの程度を読むことも書くこともできません。そこで、まず、マニアックの程度を加減するためのメンバ関数 AddiHowManiac を記述しましょう。

class CManiac {
private:
    int m_iHowManiac;                       // マニアックの程度を保存する内部の変数
public:
    CManaic() : m_iHowManiac(0) {}          // コンストラクタ、最初のマニアックの程度は 0 とする。
    void AddiHowManiac(int iManiacChange) { // マニアックの程度を加減する。
        m_iHowManiac += iManiacChange;
    }
};

まだ、これでは、どの程度マニアックか読めませんので、読み取り用のメンバ関数 GetiHowManiac を定義しましょう。

class CManiac {
private:
    int m_iHowManiac;                       // マニアックの程度を保存する内部の変数
public:
    CManiac() : m_iHowManiac(0) {}          // コンストラクタ、最初のマニアックの程度は 0 とする。
    void AddiHowManiac(int iManiacChange) { // マニアックの程度を加減する。
        m_iHowManiac += iManiacChange;
    }
    int GetiHowManiac() {              // どの程度マニアックか読み出す。
        return m_iHowManiac;
    }
};

これで、一応、使えるクラスになりました。

次に、マニアックな人の名前を保存できるようにしましょう。名無しの場合もありえることにして、次のようにします。文字列を保存する場合には、私は通常 STL の string を使いますが、ここでは、後始末の説明のためあえて char* を使います。

class CManiac {
private:
    int   m_iHowManiac;                     // マニアックの程度を保存する内部の変数
    char* m_pcPersonName;              // マニアックな人の名前
public:
    CManiac() : m_iHowManiac(0), m_pcPersonName(0) {} // 最初のマニアックの程度は 0 とする。
    void AddiHowManiac(int iManiacChange) { // マニアックの程度を加減する。
        m_iHowManiac += iManiacChange;
    }
    int GetiHowManiac() {                   // どの程度マニアックか読み出す。
        return m_iHowManiac;
    }
    void SetPersonName(char* pcName) {      // 人物の名前をセットする。
        if (pcName) {
            m_pcPersonName = new char[strlen(pcName) + 1]; // 普通こんなことはしないが、...
            strcpy(m_pcPersonName, pcName);                // こういうコードはめまいがしますね、...
        }
    }
};

このままでは、人の名前をセットできても読み出せませんが、それは後回しとして、次に、デストラクタを定義しましょう。デストラクタは、クラスオブジェクトを使い終わって、壊すときに呼ばれるメンバ関数で、関数名はクラスの名前の前に ~ をつけます。人の名前を new で確保したメモリ領域 m_pcPersonName に保存するようにしたので、これを後始末する必要があります。

ちなみに、C++ では、中味が 0 のポインタを delete しても、何も起きないことが保証されているので、delete しようとするポインタが非 0 であることを確認するコードは不要です。

class CManiac {
private:
    int   m_iHowManiac;                     // マニアックの程度を保存する内部の変数
    char* m_pcPersonName;                   // マニアックな人の名前
public:
    ~CManiac() {                        // デストラクタ
        delete m_pcPersonName;          // new で取ったメモリ領域を開放する。
    }
    CManiac() : m_iHowManiac(0), m_pcPersonName(0) {} // 最初のマニアックの程度は 0 とする。
    void AddiHowManiac(int iManiacChange) { // マニアックの程度を加減する。
        m_iHowManiac += iManiacChange;
    }
    int GetiHowManiac() {                   // どの程度マニアックか読み出す。
        return m_iHowManiac;
    }
    void SetPersonName(char* pcName) {      // 人物の名前をセットする。
        if (pcName) {
            m_pcPersonName = new char[strlen(pcName) + 1];  // 普通こんなことはしないが、...
            strcpy(m_pcPersonName, pcName);
        }
    }
};

最後に、人物の名前を読み出すメンバ関数を定義して、一応完成としましょう。

class CManiac {
private:
    int   m_iHowManiac;                     // マニアックの程度を保存する内部の変数
    char* m_pcPersonName;                   // マニアックな人の名前
public:
    ~CManiac() {                            // デストラクタ
        delete m_pcPersonName;              // new で取ったメモリ領域を開放する。
    }
    CManiac() : m_iHowManiac(0), m_pcPersonName(0) {} // 最初のマニアックの程度は 0 とする。
    void AddiHowManiac(int iManiacChange) { // マニアックの程度を加減する。
        m_iHowManiac += iManiacChange;
    }
    int GetiHowManiac() {                   // どの程度マニアックか読み出す。
        return m_iHowManiac;
    }
    void SetPersonName(char* pcName) {      // 人物の名前をセットする。
        if (pcName) {
            m_pcPersonName = new char[strlen(pcName) + 1];  // 普通こんなことはしないが、...
            strcpy(m_pcPersonName, pcName);
        }
    }
    char* GetpcPersonName() {             // 人物の名前を読み出す。
        return m_pcPersonName;
    }
};

いつのまにか、クラス定義の内容が少し大きくなってきましたが、順を追って追加してきたので、何も難しいところはありませんね。クラスはこのようにして作成していくわけです。また、上記では、クラスのメンバ関数の定義をすべてクラスの内部(つまりこれは、ヘッダの中ということになります)に記述しましたが、外部に(つまりヘッダに対応する *.cpp ファイルの中に)記述することも出来ます。IDE でクラスを作成するとヘッダと *.cpp に分けて記述するようになるのが一般的のようですが、私は、クラスの内部に書けるものはすべて書くのが好みなので、そのようにしています。テンプレートクラスでは、*.cpp を使えないので、これにあわせるということにもなります。

ちなみに、私は普通には char* ではなく string を使うので、上記とほぼ、同一内容のクラスの定義は次のようになります。ご参考までに。

#include <string>
using namespace std;

class CManiac {
private:
    int    m_iHowManiac;                     // マニアックの程度を保存する内部の変数
    string m_strPersonName;                  // マニアックな人の名前
public:
    ~CManiac() {}                            // デストラクタ
    CManiac() : m_iHowManiac(0) {}           // 最初のマニアックの程度は 0 とする。
    void AddiHowManiac(int iManiacChange) {  // マニアックの程度を加減する。
        m_iHowManiac += iManiacChange;
    }
    int GetiHowManiac() {                    // どの程度マニアックか読み出す。
        return m_iHowManiac;
    }
    void SetPersonName(const string& rstrName) { // 人物の名前をセットする。
        m_strPersonName = rstrName;
    }
    string GetstrPersonName() {              // 人物の名前を読み出す。
        return m_stePersonName;
    }
};

クラスの定義のもうひとつの側面(重要)ページの先頭へ

上記では、クラスを作成し、いわば気ままに必要なメンバ変数やメンバ関数を追加してきました。クラスを単独で使用する場合にはこれでもよいのですが、C++ の変数としての一貫性という意味では、このようなやり方では配慮不足といわざるを得ません。ここでは、クラスに変数としての一貫性を持たせるための定義の仕方について解説します。

ナオ : 上のクラスって、これじゃダメなの?
店主 : そう、char* を使ったほうはダメなんだ。だけど、string を使ったほうのクラスは、実は、うまく出来てるんだ。何も考えないでメンバを追加していくだけ、という「やり方」では、ダメな場合があるということだね。
ナオ : ふーん。どちらもよく似ているように思うけど。

といっても、単純なことです。結論から言えば、クラスには、次のものを定義する必要があります。

  1. デストラクタ
  2. デフォルトコンストラクタ
  3. コピーコンストラクタ
  4. 代入演算子(= コピー演算子 = イコール演算子)

注:この中で、デストラクタは、クラスがメモリを new で確保していなければ不要かもしれませんが、形式的にだけでも定義しておいて悪いことはありません。

では、簡単に事情を解説しましょう。クラス CManiac オブジェクトを、次のように使うことを考えてみてください。

int main() {
    CManiac maniac1;            // @デフォルトコンストラクタが使用される。
    CManiac maniac2(maniac1);   // Aコピーコンストラクタが使用される。
    maniac1 = maniac2;          // B代入演算子が使用される。
    return 0;                   // Cスコープを外れると 2 個のオブジェクトは破壊される。
}

@で、オブジェクト maniac1 が構築されます。このとき、コンストラクタに与えている引数がないので、デフォルトコンストラクタが使用されます。クラス CManiac にコンストラクタが一つも定義されていなければ、コンパイラによって「何もしないデフォルトコンストラクタ」が自動的に付加されたようにコンパイルされますが、もし一つでも引数付きのコンストラクタを定義した場合には、@のような呼び出しをするには、引数なしのデフォルトコンストラクタを、たとえそれが何もしないとしても、陽に定義する必要があります。クラスが C++ の変数として、どのような場合にも通用するやり方としては、デフォルトコンストラクタは必ず定義すると覚えておけば良いでしょう。

注:というより、引数がないからデフォルトコンストラクタというのですが。

Aでは、元となるオブジェクト maniac1 を与えて、これをコピーする形で構築が行われます。この場合、元となるオブジェクトを引数とするコピーコンストラクタが使用されます。Aでは main() 関数の中で陽にコピーコンストラクタを使っていますが、例えば STL で vector<CManiac> のオブジェクトを resize() しようとすると、コピーコンストラクタが陰に使用されます。このように、コピーコンストラクタは、隠れたところで必要になることが多いので、オブジェクトに変数としての一貫性を持たせるという意味では、コピーコンストラクタは必ず定義すると覚えておけばよいでしょう。

Bは、オブジェクト同士の代入によるコピーですが、クラスに代入演算子が定義されていない場合には、コンパイラによってメンバ変数同士が自動的にコピーされます。それで問題ない場合には良いのですが、例えば、メンバ変数にメモリ領域へのポインタがあり、それが、一つのメモリ領域を指している場合、メンバ変数同士をコピーすると、二つのオブジェクトが同じメモリ領域を指すという問題が生じます。これを回避するには、代入演算子を定義して、その中でポインタを正しく処理するということが必要になります。そういうわけで、オブジェクトに変数としての一貫性を持たせるためには、代入演算子も必ず定義すると覚えておけば良いでしょう。

以上を考慮すると、クラスの定義は、簡単なものでもほぼ次の例のような形になるといえるでしょう。コードの量としては少し大きいのですが、クラスオブジェクトが C++ の変数として一貫性を持つということは、STL を使う場面などでは、非常に重要な性能なので、最低でも、この程度のことは必要なのだと覚えておきましょう。以下の例では、メンバ変数 m_pcString に文字列を入れる事にして、メモリの確保や開放のコードを入れています。このサンプルは、あくまでも、解説のためのもので、C++ マニアック的には、普通は char* などは使用せず string 型のメンバ変数で済ますところです。なので、良い子はマネしないでください。

#include <cstdio>
#include <string.h>

class CManiac {
private:
    char* m_pcString;       // 文字列を入れるメモリ領域を指すとする。
public:
    // デストラクタ
    ~CManiac() {
        delete m_pcString;  // 確保したメモリを開放する。
    }
    // デフォルトコンストラクタ
    CManiac() : m_pcString(0) {}
    // コピーコンストラクタ
    CManiac(const CManiac& rmaniac) : m_pcString(0) {
        CopyString(rmaniac.m_pcString);
    }
    // 代入演算子
    CManiac& operator=(const CManiac& rmaniac) {
        CopyString(rmaniac.m_pcString);
        return *this;
    }
private:
    void CopyString(char* pcString) {
        delete m_pcString; m_pcString = 0;
        if (pcString) {
            m_pcString = new char[strlen(pcString) + 1];
            strcpy(m_pcString, pcString);
        }
    }
};

ナオ : 上のクラスって、文字列をどうやって書き込んだり取り出したりするの?
店主 : ... まさか、本気で言っているんではないよね。
ナオ : へへっ、ジョークよ、ジョーク。
店主 : STL の vector に入れるクラスについて、vector に入れる要素の作り方(その 1)に詳しく書いたので、そちらも見ておいて欲しいんだけど...
ナオ : オール OK っすよ。