このエントリーをはてなブックマークに追加

 

手を動かして さくさく理解する
C++ クラス 入門
Copyright (C) 2014 by Nobuhide Tsuda

 

※ イラスト提供:かわいいフリー素材集「いらすとや」様

C++ のクラス とは

「クラス」は C++ の看板娘である。クラスをマスターせずに C++ をマスターしたことにはならない。
そもそも、C++ は最初「C with classes(クラス付きのC)」という名前だったくらいだ。
ただしいつもニコニコして座っているだけの看板娘ではない。結構自己主張が強く、多才で、芸達者だ。簡単にくどき落とすことは出来ないぞ。

「C++ のクラス」とは、型として識別可能な名前を持ち、構造体にメンバ関数を追加したものだ。
クラスとはなんらかの物体・概念を抽象化したもので、メンバ変数により状態を保持することができ、メンバ関数によりそれを参照・操作することが出来る。

クラスは UML という形式で下図のように図示することが出来る。

箱を3つに区切り、一番上にはクラス名を記述する。2番めにはメンバ変数、3番目にはメンバ関数を1行ずつ記述する。

具体的な例としては下図を参照してほしい。Vector クラスの(簡略化した)クラス図の例だ。

UML は C++ 言語特有のものではないので、変数・関数の記述は C/C++ の文法とはちょっと違っている。 最初に変数・関数名を記述し、コロン(:)の後に型を指定する。

要は、「クラス=クラス名+メンバ変数+メンバ関数」ということだ。

C++ のクラスは非常に多機能で奥が深い。なので、全部を一度に勉強するのは混乱の元だ。
そこで、本稿では C++ クラスの必須事項についてのみ順次解説していくことにする。 高度に難しいことは出てこないから某生主も安心だ。

クラスの大きな特徴のひとつとして、既存のクラスを元にその派生クラスを定義することで機能を拡張出来る、という点がある。
が、いろいろややこしいことが出てくるので、本稿では派生クラスについては説明しないことにする。いずれ別稿で解説したい。

オブジェクト指向プログラミングに慣れていない人にとって、「クラス」は難しい概念に思えるかもしれないが、 慣れてしまえばたいしたことはない。
毎度言っているが、演習問題を数多くといて場数を踏めば、クラスなど恐れるに足りずだ。
クラスをマスターし、本当のC++使いになってもらいたい。

最も簡単なクラスの宣言とオブジェクトの生成

class MyClass
{
};

上記は、最も簡単なクラス「MyClass」の宣言だ。
"class" を書き、その直後にクラス名を記述し、その後に "{"、"};" を記述する。
このように定義すると、「MyClass」という名前のクラスが出来上がる。
ただし、現在この MyClass には中身が何もない。空っぽだ。

たとえ中身が空っぽでも、定義されたクラス名は、変数等の型として使用することが出来る。

class MyClass
{
};
int main()
{
    MyClass a;    //  MyClass 型のオブジェクト a を生成
    // ↓ 間違った例
    MyClass b();  //  最後に () を付けると、MyClass オブジェクトを返す関数宣言になってしまうので注意
    return 0;
}

上記のように MyClass を型名として指定してオブジェクトを生成することができる。
中身が空っぽなので、特に使い道はないが、生成することは出来る。
※ 生成されたオブジェクトのことをインスタンスと呼ぶ場合もある。ほぼ同義だ。

上記以外にも、new でオブジェクトを動的に生成する方法がある。

int main()
{
    MyClass *ptr = new MyClass;      //  動的にオブジェクトを生成
    ...
    delete ptr;     // 解放を忘れないように
}

配列としてメモリを new する場合は「new 型[要素数]」と記述する。
この場合、メモリを解放するときは「delete [] ポインタ」と記述する。

    MyClass *ptr = new MyClass[10];      //  動的にオブジェクトの配列を生成
    ...
    delete [] ptr;     // 配列の場合は [] を忘れないように

※ 実は、メモリ解放を自動的にやってくれるスマートポインタというちょっと便利なものもあるのだが、 初心者にはちょっと難しいかもしれないので、そのうち別稿で解説するつもりだ。

動的に複数のオブジェクトを生成したい場合、std::vector 等のコンテナクラスを使うという選択肢もある。
例えば、MyClass 型のオブジェクトを10個生成したい場合は、以下のように書いてもよい。

#include <vector>
.....
    {
        std::vector<MyClass> v(10);     //  MyClass 型のオブジェクトを10個生成
        // v[0] ~ v[9] を利用
        .....
    }  // スコープを抜けると、メモリは自動的に解放される

コンテナクラスで複数のオブジェクトを取り扱うようにすれば、メモリの解放を自動的に行なってくれるので便利だぞ。

初心者は、「クラス」と「オブジェクト」を混同しがちだ。「クラス」は型であり、「オブジェクト」の形式を規定するもので、実体ではない。 「オブジェクト」はクラスの規定にのっとって生成される実体のことだ。

下図の様に、クラスはオブジェクトの金型と理解するといいかもしれない。
金型に金属板を当ててプレスすると部品などが続々できてくる。
クラス=金型、オブジェクト=出来上がる部品、そして(後の章で説明する)コンストラクタはプレスすることに相当するのだ。

ひとつの金型からたくさんの部品が作れるように、ひとつのクラスから複数のオブジェクトを生成することが出来るのもいっしょだ。

演習問題:

  1. グローバル変数・ローカル変数として、また、動的に MyClass のオブジェクトを生成するコードを書き、デバッガで実行して中身を見てみなさい。
  2. class MyClass         // 中身が空のクラス宣言
    {
    };
    
    MyClass g_object;      // グローバル変数
    int main() {
        MyClass object;     //  ローカル変数
        MyClass *ptr = new MyClass;      //  動的にオブジェクト生成
        delete ptr;             // 動的オブジェクトを破棄
        return 0;
    }
    
  3. MyClass 型、要素数10個の配列を宣言しなさい。
  4.     MyClass ar[10];
    
  5. MyClass 型、要素数10個の vector を宣言しなさい。
  6.     std::vector<MyClass> v(10);
        std::vector<MyClass> v2[10];  // ← 間違った例。こう書くと、要素数0の vector が10個生成される
    
  7. MyClass 型、要素数10個の配列を new で動的に生成しなさい。
  8.     MyClass *ptr = new MyClass[10];
        .....
        delete [] ptr;  // 注意:配列の解放には [] が必要
    

クラス固有の定数

ソースコード中に数値(マジックナンバーと呼ばれる)を直接書くのはよろしくない。

あとでソースを読み返したとき、その数字が何を意味するかがわからなくなることがよくあるからだ。
また、その数値が2箇所以上にあると、値を変えるときに全部修正しなくてはならず、バグ混入の元となる。

単純に定数を定義したいのであれば、#define または グローバルに const 定数を宣言するとよい。

#define     N      100
const double PAI = 3.1415926535;
class MyClass {
public:
    MyClass()
        : m_n(N)              //  定義した定数を普通に利用可能
        , m_pai(PAI)
    {
    }
private:
    int    m_n;
    double  m_pai;
};

だが、上記のようにして定数をヘッダファイル(myclass.h)に記述していると、他のファイルからそれ(myclass.h)をインクルードすると、 定数定義がインクルードしたファイルでも有効になってしまい、識別子が衝突して困る場合がある。

なので、クラス固有の定数は、それなりの方法で定義しておく方が無難だ。

クラス固有の定数を定義する方法は2種類ある。ひとつは const を使う方法、もうひとつは enum を使う方法だ。

class MyClass {
    const int N = 100;      // 定数のメンバ変数として宣言
public:
    MyClass()
        : m_n(N)              //  定義した定数を普通に利用可能
    {
    }
};

上記のように、メンバ変数を const 付で宣言すると定数となる。

しかし、この場合は、各オブジェクトに const 変数の領域が確保されてしまうので、メモリ使用量的に好ましくない。
なので、通常は下記のように static を付け、クラス固有の定数とするとよい。

class MyClass {
    static const int N = 100;     //  クラス固有の定数として宣言
public:
    MyClass()
        : m_n(N)              //  定義した定数を普通に利用可能
    {
    }
};

もうひとつの記述方法として enum を使用する方法がある。

class MyClass {
    enum {
        N = 100,
    };
public:
    MyClass()
        : m_n(N)              //  定義した定数を普通に利用可能
    {
    }
};

static const 宣言された定数は型を持っていて、どんな型でも(double や string も)可能だが、enum は整数のみである。
しかし、一連の定数を宣言する場合は enum の方が便利である。
以下のように記述すると、BLACK=1, WHITE=2, WALL=3 と値が自動的に割り振られる。いちいち値を明記しなくていいので便利だ。

class MyClass {
    enum {
        SPACE = 0,
        BLACK,
        WHITE,
        WALL,
    };
    .....
};

static const または enum 定数宣言を public 領域に記述すると、定数を「クラス名::定数名」で外部からアクセスすることが出来るぞ。

class MyClass {
public:
    const int N = 100;
    enum {
        N123 = 123,
    };
    .....
};
int main() {
    std::cout << MyClass::N << "\n";               // 100 が表示される
    std::cout << MyClass::N123 << "\n";          // 123 が表示される
}

まとめ:

  • enum または static const で定数宣言しよう
  • public 領域であれば、外部から クラス名::定数名 で参照可能
  • マジックナンバーは記述しないようにしましょう

メンバ変数

class 宣言の中括弧({})の中で、変数宣言を行うと、それがクラスのメンバ変数となる。
メンバ変数とはクラスの各オブジェクトが保持する情報のことだ。 メンバ変数がいろいろな値を持つことで、各オブジェクトは状態を変えることが出来る。

class MyClass
{
private:          //  アクセス指定子
    int    m_a;     // int 型のメンバ変数。名前は m_a
};

上記は、MyClass に int 型のメンバ変数 m_a を追加した例だ。
「private」はアクセス指定子と言って、クラスのメンバが外部からアクセス可能かどうかを指定する。
メンバ変数は通常 private にする。クラス外からはアクセス不可にし、情報を隠蔽するのが普通だ。

この例では、メンバ変数名に「m_」を前置している。これはソースを見た時、メンバ変数であることが一目瞭然にするためだ。 単に「_」を付ける規約もある。どのように記述するかは会社・チームのコーディング規約次第だ。

メンバ変数は、private でなくて public であれば、「オブジェクト名.メンバ変数」で外部からアクセスすることが出来る。

class MyClass
{
public:          //  アクセス指定子
    int    m_a;     // int 型のメンバ変数。名前は m_a
};
int main()
{
    MyClass a;        //  オブジェクト a を生成
    a.m_a = 123;    // メンバ変数に代入
    std::cout << a.m_a << "\n";    //  メンバ変数を参照
    return 0;
}

アクセス指定子が public であっても、メンバ変数が const 指定されていると、値を変更することが出来ない。

class MyClass
{
public:          //  アクセス指定子
    const int    m_a;     // const なint 型のメンバ変数。名前は m_a
};
int main()
{
    MyClass a;        //  オブジェクト a を生成
    a.m_a = 123;    // const なのでエラーとなる
    std::cout << a.m_a << "\n";    //  メンバ変数を参照
    return 0;
}

const なメンバ変数をどうやって初期化するかと言うと、次章で説明するコンストラクタで初期化する。詳しくは次章だ。

演習問題:

  1. public で double 型の身長・体重メンバ変数を持つ Person クラスを定義してみなさい。
  2. class Person {
    public:
        double  m_height;      // 身長
        double  m_weight;       // 体重
    };
    
  3. Person オブジェクトを2つ生成し、一人は身長を 170、体重を 60 に、もう一人は 身長160、体重50に設定しなさい。
  4.     Person a;
        a.m_height = 170;
        a.m_weight = 60;
        Person b;
        b.m_height = 160;
        b.m_weight = 50;
    
  5. Person オブジェクトの身長・体重を表示する関数 void print(const Person &psn) を定義し、上記のオブジェクトの内容をそれぞれ表示してみなさい。
  6. void print(const Person &psn)
    {
        std::cout << psn.m_height << "\n";
        std::cout << psn.m_weight << "\n";
    }
    int main() {
        .....
        print(a);
        print(b);
        .....
    }
    

メンバ変数まとめ

  • 個々のオブジェクトはメンバ変数を持つことができ、その値を変更することでオブジェクトの状態を変えることが出来る。
  • クラス宣言時、{ } の中で、変数宣言したものはメンバ変数となる。
  • メンバ変数は通常 private にするが、public にしておくと、メンバ変数の値を外部から参照・変更出来る。

コンストラクタ

コンストラクタとは、クラスオブジェクトを初期化するために、オブジェクト生成時にコールされる関数のようなものである。
先の、金型の比喩では、プレスしてオブジェクトを作る機能に対応する。

コンストラクタはクラス名と同じ識別子を使用し、型を持たない関数の形式で宣言する(下記コード参照)。
コンストラクタは、通常 public: 領域に記述する。そうしないと外部からオブジェクトを生成することが出来ない。
※ シングルトンなどのように、あえて public 以外にし、オブジェクトを勝手に生成出来ないようにする上級テクニックもある (static の シングルトン 参照)。

"MyClass.h":

class MyClass {
public:
    MyClass();         // コンストラクタ宣言
};

"MyClass.cpp":

#include "MyClass.h"
// コンストラクタの実装
MyClass::MyClass()
{
    .....       // なんらかの初期化
}

コンストラクタは、上記の様にクラス宣言の外で、 型を持たない関数の形式(クラス名::クラス名() { ... })で実装する。

小さなプログラムでは、宣言と実装を同じファイルにすることもあるが、通常、宣言は宣言ファイル(.h)に、 実装は実装ファイル(.cpp)に記述するのが普通である。

class MyClass {
public:
    MyClass()          // コンストラクタ宣言
    {
        .....       // なんかの初期化
    }
};

上記の様に、実装をインラインで記述することも出来る。
ちょっとしたクラスや、テンプレートを使用したクラスはインライン実装するのが普通である。

コンストラクタの仕事は主に、メンバ変数の初期化である。
メンバ変数の初期化は、基本的に初期化子(: の後にメンバ変数名(値) 形式)を使用する。

引数を持たないコンストラクタを「デフォルトコンストラクタ」と呼ぶ。
通常は、デフォルトコンストラクタを定義し、メンバ変数を全て明示的に初期化しておいた方がよい。 そうでないと、メンバ変数が不定のオブジェクトが出来上がってしまうことがある。

class MyClass {
public:
    MyClass()          // コンストラクタ宣言
        : m_a(123)       //  メンバ変数の初期化
    {
    }
private:
    int    m_a;
};

メンバ変数を複数初期化する場合は、カンマで区切ってメンバ変数名(値) を並べる。

class MyClass {
public:
    MyClass()          // コンストラクタ宣言
        : m_a(123)       //  メンバ変数の初期化
        , m_b(45)
    {
    }
private:
    int    m_a;
    int    m_b;
};

下記のように、コンストラクタには仮引数を持たせることができるので、引数によりメンバ変数を初期化することが出来る。

class MyClass {
public:
    MyClass(int a)
        : m_a(a)       // 引数の値でメンバ変数を初期化
    {
    }
private:
    int    m_a;
};

当然ながら、下記のように仮引数のデフォルト値を指定できる。
全ての仮引数にデフォルト値が指定されているものは、デフォルトコンストラクタとなる。

class MyClass {
public:
    MyClass(int a = 123)    //  デフォルト値を指定可能
        : m_a(a)       // 引数の値でメンバ変数を初期化
    {
    }
private:
    int    m_a;
};

メンバ変数の初期化は、下記のように、コンストラクタ本体内で代入文として記述してもいいのだが、メンバ変数が const の場合は代入が出来ない。

class MyClass {
public:
    MyClass
    {
        m_a = 123;       // コンパイルエラーとなる
        .....
    }
private
    const int m_a;       //  const なメンバ変数
};

メンバ変数が const の場合は、下記の様に初期化子を使って、初期化を行う。

class MyClass {
public:
    MyClass
        : m_a(123)   //  m_a の初期化、const でもOK
    {
        .....
    }
private
    const int m_a;       //  const なメンバ変数
};

ただし、メンバ変数が配列で forループを使ってそれを特定のパターンに初期化するようなことは、 配列要素数が膨大だと初期化子で初期化するのは無理があるので、{ } 内に記述する方がいいだろう。

class MyClass {
public:
    MyClass()
        // ここに : m_data({0, 1, 2, .... 9999}) と書くのは無理がある
    {
        for(int i = 0; i < std::end(m_data) - std::begin(m_data); ++i)
            m_data[i] = i;
    }
private:
    int    m_data[10000];
};

引数付きコンストラクタを用意することで、オブジェクトの状態を指定してオブジェクトを生成できるようになり便利だ。
下記コードを見ると、それがわかるでしょ。

    // デフォルトコンストラクタで生成し、後でメンバ変数を設定する書き方
    Person a;
    a.m_height = 170;
    a.m_weight = 60;
    // コンストラクタ引数で、メンバ変数を設定する書き方
    Person b(160, 50);     // 身長160, 体重50

先の金型の例で言えば、後からメンバ変数をするのは、部品をプレスで作ったあとで、状態を変えるようなものだ。
引数付きコンストラクタは、金型をちょちょいといじって、部品をプレスするときに、状態の異なる部品を作るイメージだ。

 

下記のように、同じクラスの参照を引数で受け取り、コピーを生成するものを「コピーコンストラクタ」と呼ぶ。

class MyClass {
public:
    MyClass(const MyClass &x)      // コピーコンストラクタ
        : m_a(x.m_a)   //  m_a の初期化
    {
    }
private
    int m_a;       //  const なメンバ変数
};

コンストラクタには、本稿で説明したもの以外にも C++11 で追加された「ムーブ コンストラクタ(move constructor)」というものもあるが、本稿では解説しないことにする。 いずれ別稿で解説したい。

演習問題:

  1. private で、double 型の身長・体重メンバ変数を持つ Person クラスのデフォルトコンストラクタを定義してみなさい。
  2. class Person {
    public:
        Person();
    private:
        double  m_height;
        double  m_weight;
    };
    Person::Person()
        : m_height(0)    // 0 に初期化
        , m_weight(0)   //  0 に初期化
    {
    }
    
  3. 上記 Person クラスに、身長・体重を引数として指定できるコンストラクタを追加しなさい。
    Person a(170, 65); で Person オブジェクトを生成し、メンバ変数が正しく初期化されていることをデバッガで確認しなさい。
  4. class Person {
    public:
        Person();
        Person(double height, double weight);
        .....
    };
    Person::Person(double height, double weight)
        : m_height(height)    // height に初期化
        , m_weight(weight)   //  weight に初期化
    {
    }
    int main() {
        Person a(170, 65);
        .....
    }
    
  5. 上記 Person クラスに、const string 型の名前を追加し、名前・身長・体重を引数として指定できるコンストラクタを追加しなさい。
    Person a("okap", 170, 65); で Person オブジェクトを生成し、メンバ変数が正しく初期化されていることをデバッガで確認しなさい。
  6. class Person {
    public:
        Person();
        Person(double height, double weight);
        Person(const std::string &name, double height, double weight);
    private:
        const std::stringm_name;       // 変更不可の名前
        double  m_height;
        double  m_weight;
    };
    Person::Person(const std::string &name, double height, double weight)
        : m_name(name)      //  name に初期化
        , m_height(height)    // height に初期化
        , m_weight(weight)   //  weight に初期化
    {
        // ここで m_name = name; と書くとコンパイルエラーとなる
    }
    int main() {
        Person a("okap", 170, 65);
        .....
    }
    
  7. 上記 Person クラスにコピーコンストラクタを追加し、実装しなさい。
    Person okap("okap", 180, 75); Person okap2(okap); を実行し、中身が正しくコピーされていることを確認しなさい。
  8. class Person {
    public:
        Person(const Person &x)
            : m_name(x.m_name)
            , m_height(x.m_height)
            , m_weight(x.m_weight)
        {
        }
        .....
    };
    int main() {
        Person okap("okap", 180, 75);
        Person okap2(okap);
        .....
    }
    
  9. 各マス目が、空白:0、黒石:1, 白石:2 の値を持つ private な char m_boad[8][8]; をメンバ変数として持つ OthelloBoard クラスを宣言し、 デフォルトコンストラクタで盤面を初期化しなさい。
  10. class OthelloBoard {
        static const int WD = 8;
        static const int HT = 8;
        enum {
            SPACE = 0,
            BLACK,
            WHITE,
        };
    public:
        OthelloBoard();
    private:
        char  m_boad[WD][HT];      //  盤面の状態
    };
    OthelloBoard::OthelloBoard()
    {
        for(int y = 0; y < HT; ++y) {
            for(int x = 0; x < WD; ++x)
                m_boad[x][y] = SPACE;             // とりあえず全部空白に設定
            }
        }
        m_boad[3][4] = m_boad[4][3] = BLACK;    //  黒石
        m_boad[3][3] = m_boad[4][4] = WHITE;    //  白石
    }
    
  11. 8x8 の白黒画像(bool m_pixel[8][8]; 値が0なら黒、値が1なら白)を保持する Bitmap クラスを宣言し、 デフォルトコンストラクタでピクセルを全て0に初期化しなさい。
  12. class Bitmap {
        static const int WD = 8;
        static const int HT = 8;
    public:
        Bitmap()
        {
            for(int x = 0; x < WD; ++x) {
                for(int y = 0; y < HT; ++y) {
                    m_pixel[x][y] = 0;
                }
            }
        }
    private:
        bool  m_pixel[WD][HT];
    };
    
  13. 上記 Bitmap クラスに、画像を文字列で指定できるコンストラクタ Bitmap(const char *ptr) を追加しなさい。
    指定文字列中の 空白、'0', '.' は値0として、それ以外は値1と解釈しなさい。
    文字列は各ピクセルの値を左上から右方向、その次の行... という順序で指定するものとする。
    例えば Bitmap("1111111110000011100001011000100110010001101000011100000111111111") は、 矩形(□)にスラッシュ(/)を合成したような画像(〼)を生成する。
  14. class Bitmap {
        static const int WD = 8;
        static const int HT = 8;
    public:
        Bitmap(const char *ptr)
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x) {
                    if( *ptr == '\0' || *ptr == ' ' || *ptr == '0' || *ptr == '.' ) {
                        m_pixel[x][y] = 0;
                    } else {
                        m_pixel[x][y] = 1;
                    }
                    if( *ptr != '\0' ) ++ptr;      //  文字が残っていればポインタを進める
                }
            }
        }
        .....
    };
    
  15. 上記 Bitmap クラスに、64ビット整数値で画像を指定できるコンストラクタ Bitmap(__int64 bits) を追加しなさい。
    例えば Bitmap(0xff83858991a1c1ff) は、矩形(□)にスラッシュ(/)を合成したような画像(〼)を生成する。
  16. ※ ヒント:ビット演算を使用する。よくわからない人は ここ とかを読んでね。

    class Bitmap {
        static const int WD = 8;
        static const int HT = 8;
    public:
        Bitmap(__int64 bits)
        {
            unsigned __int64 mask = (__int64)1<<63;
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x) {
                    if( (bits & mask) == 0 ) {
                        m_pixel[x][y] = 0;
                    } else {
                        m_pixel[x][y] = 1;
                    }
                    mask >>= 1;        //  マスクを右に1ビットシフト
                }
            }
        }
        .....
    };
    

コンストラクタ まとめ

  • オブジェクト作成時に呼ばれる
  • クラス名と同じ名前のメンバ関数
  • 型を持たない
  • 引数が無いコンストラクタを「デフォルトコンストラクタ」と呼ぶ
  • 引数を取ることが出来る
  • 引数が異なるコンストラクタを複数宣言・実装出来る。
  • 引数はデフォルト値を指定可能
  • コンストラクタの主な仕事は、メンバ変数の初期化
  • 初期化子で、const なメンバ変数を初期化することが出来る

デストラクタ

デストラクタはオブジェクトが破棄される時に、自動的にコールされる関数のようなものである。
コンストラクタと同じで型を持たない関数のように定義する。識別子としては ~クラス名 を用いる。

class MyClass {
public:
    MyClass();     // コンストラクタ
    ~MyClass();  //  デストラクタ
};

デストラクタの主な仕事は、コンストラクタで確保したメモリの解放である。
int などの通常型(POD)や、std::vector などが確保したメモリは自動的に解放されるので、特に何も書く必要は無い。

下記のように、動的にメモリを確保した場合は、デストラクタでの解放が必須となる。

※ ただし、スマートポインタを使用しておけば、メモリの解放を自動的に行ってくれる。

class MyClass {
public:
    MyClass()      // コンストラクタ
    {
        m_data = new char[100];       // メモリを確保した場合は、
    }
    ~MyClass()    // デストラクタ
    {
        delete [] m_data;          //  解放を忘れずに
    }
private:
    char  *m_data;
};

演習問題:

  1. 上記の MyClass オブジェクトを生成・破棄し、デストラクタが正しく呼ばれていることを確認しなさい。
  2. class MyClass {
    public:
        MyClass() {}
        ~MyClass()
        {
            std::cout << "destructor\n";
        }
    };
    int main() {
        {
            MyClass a;
        }  //  a が破棄されるので、デストラクタが呼ばれるはず
        return 0;
    }
    

デストラクタまとめ

  • クラス名の前に ~ を付けた、型指定無しの関数と同じ書式
  • オブジェクト破棄時に呼ばれる
  • メンバ変数の解放が主な任務

メンバ関数

メンバ関数とは、クラスオブジェクト固有の処理を行う関数のことである。メソッドと呼ばれることもある。
オブジェクトの状態を参照するだけの const なメンバ関数と、オブジェクトの状態を変更する可能性のある非constな(要するに普通の)メンバ関数を定義することが出来る。

オブジェクト.メンバ関数名(実引数...) または オブジェクトへのポインタ->メンバ関数名(実引数...) でメンバ関数を呼び出せる。

    MyClass obj;  //  MyClass 型のオブジェクト obj を生成
    obj.func();    //  メンバ関数 func() コール
    MyClass *ptr = new MyClass;       // MyClass 型のオブジェクトを new し、ptr がそれを指す
    ptr->func();      //  メンバ関数 func() コール

メンバ関数コール演算子が2種類あるのは初級者にとっては混乱の元で、今となっては問題な仕様だ。 演算子はどちらかだけにし、型で判別可能なはずだが、C コンパイラが出来た当時はCPU資源が貧弱で賢いコンパイラを作ることができず、 そうせざるをえなかったのだと推測される。

メンバ関数の実装内では、オブジェクトのメンバ変数の値を参照・更新することが出来る。
通常、メンバ変数は private で、外部からアクセスすることが出来ず、メンバ関数からのみアクセス出来る。

それなりのメンバ関数が用意されていれば、それらを使うことにより、外部からオブジェクトの状態を間接的に参照・変更することが出来る。 これを「情報隠蔽」とか「カプセル化」と呼ぶ。

メンバ関数宣言

クラス宣言の "{" と "};" の間で、通常の関数と同じように関数宣言すると、それがメンバ関数となる。

"person.h":

class Person {
    .....
public:
    void  print();      // メンバ関数の宣言

private:
    double  m_height;
    double  m_weight;
};

通常は .h ファイルにクラス宣言を記述し、.cpp ファイルに実装コードを記述する。

public 領域に記述すると、クラス外からもメンバ関数を呼び出すことが出来る。
クラス外部からはコール出来ず、クラス内部からのみコールできるメンバ関数は private または protected 領域に記述する。
※ private と protected の違いは派生クラスを作ったときに出てくる。本稿では 派生クラスについては言及しない。

他のファイルでも、宣言ファイルを include することで、定義したクラスを利用することが出来るようになる。

メンバ関数宣言の最後に const を付けると、そのメンバ関数は const となり、メンバ変数を変更不可となる。
また、const なオブジェクトであっても、コール可能となる。

class Person {
    .....
public:
    void  print() const;      // const なメンバ関数の宣言
    void  diet();               //  非const なメンバ関数の宣言
    .....
};

int main() {
    const Person cp;
    cp.print();                  // コール可能
    cp.diet();                   // ビルドエラーとなる
}

メンバ関数実装

クラス宣言とは別に、型 クラス名::メンバ関数名() { ... } で、メンバ関数を実装することが出来る。
const なメンバ関数の場合は 型 クラス名::メンバ関数名() const { ... } と記述する。

"Person.cpp":

#include "person.h"
void Person::print()
{
    std::cout << m_height << "\n";
    std::cout << m_weight << "\n";
}

メンバ関数内では、private なメンバ変数も参照・代入することが出来る。

メンバ関数のインライン実装

簡単なメンバ関数は宣言ファイルで実装することがある。

テンプレートクラスも宣言ファイルでインライン実装するのが普通だ。

class Person {
    .....
public:
    double  getHeight() const { return m_height; }     //  身長を返す
public:
    void      setHeight(double ht) { m_height = ht; }   // 身長を設定
    .....
};

演習問題: 解答例

  1. private で、double 型の身長・体重メンバ変数を持つ Person クラスに、public で、double BMI() const; メンバ関数を追加し、動作確認しなさい。
    ただし、BMI は 体重 / (身長*身長) で計算、単位は キログラム, メートル とする。
  2. class Person {
    public:
        Person(double height, double weight)
            : m_height(head)
            , m_weight(weight)
        {
        }
    public:
        double BMI() const { return m_weight / (m_height * m_height); }
    private
        double  m_height;      // 身長(単位:メートル
        double  m_height;      // 体重(単位:kg)
    };
    int main()
    {
        Person a(1.7, 60);      // 1.7メートル、60kg の人物 a さんを生成
        std::cout << a.BMI() << "\n";
        return 0;
    }
    
  3. 各マス目が、空白:0、黒石:1, 白石:2 の値を持つ char m_boad[8][8]; のメンバ変数を持つ OthelloBoard クラスを宣言し、 オセロの初期状態に初期化するメンバ関数 void init() を宣言・実装しなさい。
  4. class OthelloBoard {
    public:
        enum {
            SPACE = 0,
            BLACK,
            WHITE,
            
            WD = 8,
            HT = 8,
        };
    public:
        void  init()
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x)
                    m_boad[x][y] = SPACE;             // とりあえず全部空白に設定
                }
            }
            m_boad[3][4] = m_boad[4][3] = BLACK;    //  黒石
            m_boad[3][3] = m_boad[4][4] = WHITE;    //  白石
        }
    private:
        char m_boad[WD][HT];
    };
    
  5. OthelloBoard の状態を、空欄は「・」で、白石は「○」で、黒石は「●」で表示するメンバ関数 void print() const; を実装し、動作確認しなさい。
  6. void OthelloBoard::print() const
    {
        for(int y = 0; y < HT; ++y) {
            for(int x = 0; x < WD; ++x)
                switch( m_boad[x][y] ) {
                case SPACE:
                    cout << "・";
                    break;
                case BLACK:
                    cout << "●";
                    break;
                case WHITE:
                    cout << "○";
                    break;
                }
            }
            cout << "\n";
        }
        cout << "\n";
    }
    
  7. 8x8 の白黒画像(bool m_pixel[8][8]; 値が0なら黒、値が1なら白)を保持する Bitmap クラスを宣言し、 値が0なら .(ピリオド)で、値が1なら *(アスタリスク)で、以下のように画像を表示するメンバ関数 void print() const を実装、動作確認しなさい。
  8. ........
    .*......
    .*......
    .*......
    .*****..
    .*....*.
    .*....*.
    .*****..
    
    class Bitmap {
        enum {
            WD = 8,
            HT = 8,
        };
    public:
        void print() const
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x) {
                    std::cout << (m_pixel[x][y] ? '*' : '.');
                }
                std::cout << "\n";
            }
            std::cout << "\n";
        }
    private:
        bool  m_pixel[WD][HT];
    };
    
  9. Bitmap クラスに、ランダムな画像を設定するメンバ関数 void setRandom() を追加し、動作確認しなさい。
  10. #include <random>
    std::random_device g_rnd;      // 非決定的乱数生成器
    class Bitmap {
        .....
    public:
        void setRandom()
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x) {
                    m_pixel[x][y] = (g_rnf() & 1) != 0;      //  偶数ならfalse, 奇数なら true
                }
            }
        }
        .....
    };
    
  11. Bitmap クラスに、左上の1/4の領域にランダムな画像を設定するメンバ関数 void setRandomQ() を追加し、動作確認しなさい。
  12. #include <random>
    std::random_device g_rnd;      // 非決定的乱数生成器
    class Bitmap {
        .....
    public:
        void setRandomQ()
        {
            for(int y = 0; y < HT/2; ++y) {
                for(int x = 0; x < WD/2; ++x) {
                    m_pixel[x][y] = (g_rnf() & 1) != 0;      //  偶数ならfalse, 奇数なら true
                }
            }
        }
        .....
    };
    
  13. Bitmap クラスに、文字列で指定する画像を設定するメンバ関数 void assign(const char *) を追加しなさい。 文字は '*' 以外は0を表すものとする。 bmp.assign(".........*.......*.......*.......*****...*....*..*....*..*****.."); を実行し、中身が 'b' になることを確認しなさい。
  14. class Bitmap {
        .....
    public:
        void assign(const char *ptr)
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x) {
                    m_pixel[x][y] = *ptr == '*';
                    if( *ptr != '\0' )
                        ++ptr;
                }
            }
        }
        .....
    };
    
  15. Bitmap クラスに、画像を白黒反転するメンバ関数 void reverse() を追加し、ランダムパターンが反転されることを確認しなさい。
  16. class Bitmap {
        .....
    public:
        void reverse()
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD; ++x) {
                    m_pixel[x][y] = !m_pixel[x][y];
                }
            }
        }
        .....
    };
    
  17. Bitmap クラスに、画像を左右反転するメンバ関数 void reverseHorizontal() を追加し、「b」が「d」に変換されることを確認しなさい。
  18. class Bitmap {
        .....
    public:
        void reverseHorizontal()
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = 0; x < WD/2; ++x) {
                    std::swap(m_pixel[x][y], m_pixel[WD-1-x][y];
                }
            }
        }
        .....
    };
    
  19. Bitmap クラスに、画像を上下反転するメンバ関数 void reverseVertical() を追加し、「b」が「p」に変換されることを確認しなさい。
  20. class Bitmap {
        .....
    public:
        void reverseVertical()
        {
            for(int y = 0; y < HT/2; ++y) {
                for(int x = 0; x < WD; ++x) {
                    std::swap(m_pixel[x][y], m_pixel[x][HT-1-y];
                }
            }
        }
        .....
    };
    
  21. Bitmap クラスに、画像の縦と横を反転するメンバ関数 void transpose() を追加し、「b」が下図のように縦横交換されることを確認しなさい。
  22. ........
    .*......
    .*......
    .*......
    .*****..
    .*....*.
    .*....*.
    .*****..
    
    ↓ x方向とy方向を入れ替え
    
    ........
    .*******
    ....*..*
    ....*..*
    ....*..*
    ....*..*
    .....**.
    ........
    
    //  下図の■の部分のみを対角線対称位置と交換する([x][y] ←→[y][x])とよい
    // □■■■■■■■  → x
    // □□■■■■■■
    // □□□■■■■■
    // □□□□■■■■
    // □□□□□■■■
    // □□□□□□■■
    // □□□□□□□■
    // □□□□□□□□
    // ↓y
    class Bitmap {
        .....
    public:
        void transpose()
        {
            for(int y = 0; y < HT - 1; ++y) {
                for(int x = y+1; x < WD; ++x) {
                    std::swap(m_pixel[x][y], m_pixel[y][x]);
                }
            }
        }
        .....
    };
    
  23. Bitmap クラスに、画像を90度時計回りに回転するメンバ関数 void rot90() を追加し、「b」が下図のように90度回転されることを確認しなさい。
  24. ........
    .*......
    .*......
    .*......
    .*****..
    .*....*.
    .*....*.
    .*****..
    
    ↓ 時計回りに90度回転
    
    ........
    *******.
    *..*....
    *..*....
    *..*....
    *..*....
    .**.....
    ........
    
    //  縦横交換し、横方向反転すると90度時計回り回転となる
    class Bitmap {
        .....
    public:
        void rot90()
        {
            transpose();
            reverseHorizontal();
        }
        .....
    };
    
  25. Bitmap クラスに、文字画像が入っているとき、それを太字にするメンバ関数 void bold() を追加し、「b」が下図のようにボールド化されることを確認しなさい。
  26. ボールド化の例:

    ........
    .*......
    .*......
    .*......
    .*****..
    .*....*.
    .*....*.
    .*****..
    
    ↓ ホールド化
    
    ........
    .**.....
    .**.....
    .**.....
    .******.
    .**...**
    .**...**
    .******.
    
    class Bitmap {
        .....
    public:
        void bold()
        {
            for(int y = 0; y < HT; ++y) {
                for(int x = WD; --x > 0; ) {     // 必ず後ろから処理
                    if( m_pixel[x-1][y] )            //  前のピクセルが true の場合
                        m_pixel[x][y] = true;
                }
            }
        }
        .....
    };
    

まとめ:

  • オブジェクトの状態を参照または変更するメンバ関数を定義出来る。
  • 通常は宣言と実装を別ファイルに別ける。
  • インライン実装することも可能。
  • 実装コード内では、private であってもメンバ変数を参照・変更可能

参考

以下に、本格的なクラスを実装する演習問題を用意している。ぜひ挑戦してみてほしい。

いつか書く:

  • 継承・派生クラス
  • C++11 ムーブ コンストラクタ
  • スマートポインタ?