C++   クラス

ホーム   C/C++チュートリアル


この「C++入門」は、wxWidgesのコードを読むための必要最小限な説明をしています。
他の言語などの予備知識は必要としていません。

クラス

クラスとは、ある目的のためのデータ(変数)と関数を一つにまとめたものです。

class.cpp


#include <iostream>

class  Class
{
//private:
    std::string name;
public:
    Class(std::string s)
    {
        name = s;
    }
    void Iam()
    {
        std::cout << "I am " << name << "." << std::endl;
    }
};

int main()
{
    Class jane("Jane");
    Class mary("Mary");
    jane.Iam();
    mary.Iam();
    
    Class *bob = new Class("Bob");
    Class *tom = new Class("Tom");
    bob->Iam();
    tom->Iam();
    
    delete bob;
    delete tom;
    
    return 0;
}
    

実行結果


I am Jane.
I am Mary.
I am Bob.
I am Tom.
    

一つのクラスから、複数のインスタンスを作ることができます。それぞれのインスタンスは、それぞれが別の個性を持っています。

コード説明

  1. クラスは、class キーワードの次に任意のクラス名を記述して作ります。クラスの内容は { と }; の間に記述します。終わりの } の後に ; がつくことに注意してください。
  2. クラスの中に定義される変数と関数は、クラスのメンバー(member)と呼ばれます。
  3. クラスのメンバーにはアクセス制限がつけることができます。アクセス制限を記述しない場合は自動的に「プライベート(private:)」が設定されていることになります。プライベートなメンバーには、そのクラスの中からしかアクセスできません。
  4. 「パブクリック(public:)」と記述すると、それ以降のメンバーは、パブリックなメンバーになります。パブリックなメンバーには、クラスの外からでもアクセスできます。
  5. 戻り値がなく、クラス名と同じ名前のメンバ関数は、コンストラクターと呼ばれる特殊な関数です。コンストラクタは、クラスからインスタンス(instance、実体)が作成されるときに自動的に呼び出され、メンバ変数などに値を設定することができます。コンストラクタは、必ずパブリックなメンバーにしなければなりません。
  6. クラスは、そのクラスのインスタンス(instance、実体)を作って使います。
  7. クラスのインスタンスを普通の変数に代入する場合は、「クラス名 変数名(初期値);」と記述します。
  8. 普通の変数に代入したインスタンスのメンバ関数を呼び出すには、「変数名.関数名();」と記述します。このドット( . )のことをドット演算子と呼びます。
  9. なお、メンバ変数 s は、プライベートに設定されているために、main関数からはアクセスできません。
  10. クラスのインスタンスのアドレスをポインターに代入する場合は、「クラス名 *ポインター名 = new クラス名(初期値);」と記述します。
  11. インスタンスのパブリックなメンバ関数を呼び出すには、「ポインター名->関数名();」と記述します。この -> のことをアロー演算子と呼びます。
  12. ポインターを含め変数に割り当てられたメモリーは、関数が終了するときに自動的に解放されます。
  13. しかし、ポインターが指し示すアドレスに確保されたメモリーは自動的には解放されません。ポインターを使った後は、必ず delete キーワードでそのポインターが指し示すアドレスに確保されたメモリーを解放しなければ「メモリーリーク」という状態を起こします。
  14. メモリーリークとは、アプリケーションが終了しても、メモリーが使用状態になっていることです。

クラスのコンストラクタの役割は、おもに、メンバ変数の初期化ですが、コンストラクタは次のように書くこともできます。

class.cpp


#include <iostream>

class  Class
{
    std::string name;
public:
    Class(std::string s) : name(s) {}
    void Iam()
    {
        std::cout << "I am " << name << "." << std::endl;
    }
};

int main()
{
    Class jane("Jane");
    Class mary("Mary");
    jane.Iam();
    mary.Iam();
    
    Class *bob = new Class("Bob");
    Class *tom = new Class("Tom");
    bob->Iam();
    tom->Iam();
    
    delete bob;
    delete tom;
    
    return 0;
}
    
  1. コンストラクタの記述に続けてコロン : を書きます。
  2. 続けてメンバ変数(初期値)と記述します。
  3. メンバ変数が複数ある場合は、コンマ , で区切って幾つでも記述することができます。
  4. メンバ変数の初期化以外に処理がない場合は、最後は { } とします。

  5. クラスの定義と、そのメンバ関数の実装は、分けて記述することができます。

    class.cpp

    
    #include <iostream>
    
    class  Class
    {
        std::string name;
    public:
        Class(std::string s);
        void Iam();
    };
    
    Class::Class(std::string s) : name(s) {}
    
    void Class::Iam()
    {
        std::cout << "I am " << name << "." << std::endl;
    }
    
    int main()
    {
        Class jane("Jane");
        Class mary("Mary");
        jane.Iam();
        mary.Iam();
        
        Class *bob = new Class("Bob");
        Class *tom = new Class("Tom");
        bob->Iam();
        tom->Iam();
        
        delete bob;
        delete tom;
        
        return 0;
    }
        

    コード説明

    1. クラスの定義部分には、メンバ関数のプロトタイプだけを記述します。
    2. メンバ関数の実装部分は「戻り値 クラス名::関数名(引数)」と記述します。
    3. メンバ関数の実装の最後の } には、セミコロン ; をつけません。
    4. コンストラクの場合は、戻り値は記述しません。
    5. クラスの定義と実装を分けると、そのメンバ関数は、インスタンスをいくつ作っても一つしか作られません。反対に定義と実装を分けない場合は、そのメンバ関数は、インスタンスの数だけ作られます。定義と実装を分けるとメモリの節約になりますが実行速度は遅くなります。反対に定義と実装を分けない場合は、実行速度は速くなりますが、インスタンスを複数作った場合は、メモリの使用量が増えます。


    さらに、実際のプログラミングでは、クラスの定義部分と実装部分を別のファイルに記述します。main関数の部分も別のファイルに記述します。そして、クラスの定義部分には .h という拡張子をつけます。この .h ファイルのことをヘッダーファイル、もしくはヘッダーと呼びます。

    class.h

    
    #include <iostream>
    
    class  Class
    {
        std::string name;
    public:
        Class(std::string s);
        void Iam();
    };
        

    class.cpp

    
    #include "class.h"  // class.hを読み込んでいます
    
    Class::Class(std::string s) : name(s) {}
    
    void Class::Iam()
    {
        std::cout << "I am " << name << "." << std::endl;
    }
        

    main.cpp

    
    #include "class.h"  // class.hを読み込んでいます
    
    int main()
    {
        Class jane("Jane");
        Class mary("Mary");
        jane.Iam();
        mary.Iam();
        
        Class *bob = new Class("Bob");
        Class *tom = new Class("Tom");
        bob->Iam();
        tom->Iam();
        
        delete bob;
        delete tom;
        
        return 0;
    }
        

    この3つのファイルをコンパイルするには次のようにします。g++ コマンドには、ヘッダファイルは指定しません。

    
    g++ class.cpp main.cpp
        

    なお、「wxWidgetsでGUIプログラミング」では、クラスの定義と実装は分けていますが、ファイルは分けず、一つのファイルに記述しています。ファイルを分割するとコードを読みにくくなるからです。しかし実際のプログラミング現場では、コード量が多いため、ファイルを分割したほうが管理しやすくなります。

    継承

    クラスには「継承(inheritance)」という重要な仕組みがあります。
    継承とは、あるクラスのすべてのメンバーを引き継いだ、新しいのクラスを作る仕組みです。元のなったクラスをスーパークラスと呼び、新しく作られたクラスをサブクラスと呼びます。サブクラスではスーパークラスのメンバーを定義せずに使えるだけでなく、新しいメンバーも追加できます。

    inheritance.cpp

    
    #include <iostream>
    
    class Animal
    {
    protected:
        std::string name;
    public:
        Animal(std::string name)
        {
            this->name = name;
        }
        // Animal(std::string name) : name(name) {} とも記述できます。
        
    };
    
    class Dog : public Animal
    {
    public:
        Dog(std::string name) : Animal(name)
        {
        }
        void OnVoice()
        {
            std::cout << "ワンワン、私は" << name << "よ" << std::endl;
        }
    };
    
    class Cat : public Animal
    {
    public:
        Cat(std::string name) : Animal(name)
        {
        }
        void OnVoice()
        {
            std::cout << "ニャ〜ン、私は" << name << "よ" << std::endl;
        }
    };
    
    int main()
    {
        Dog pochi("ポチ");
        Dog shiro("シロ");
        pochi.OnVoice();
        shiro.OnVoice();
        
        Cat *mike = new Cat("ミケ");
        Cat *kuro = new Cat("クロ");
        mike->OnVoice();
        kuro->OnVoice();
        delete mike;
        delete kuro;
        
        return 0;
    }
        

    実行結果

    
    ワンワン、私はポチよ
    ワンワン、私はシロよ
    ニャ〜ン、私はミケよ
    ニャ〜ン、私はクロよ
        

    コード説明

    1. protected: プロテクッドを指定したメンバーは、そのクラスを引き継いだサブクラスでも使えます。
    2. this->name = name; メンバー変数の名前と引数の名前が同じ場合は、メンバー変数に「this->」をつけると区別することができます。
    3. このコンストラクタは、Animal(std::string name) : name(name) {} とも記述できます。
    4. class Dog : public Animal あるクラスを継承する場合は、クラス宣言のあとにコロン : と public を記述して、そのあとに継承するクラス名を記述します。
    5. Cat(std::string name) : Animal(name) サブクラスのコンストラクタからスーパークラスのコンストラクタを呼び出すには、サブクラスのコンストラクタのあとにコロン : を記述して、そのあとにスーパークラスのコンストラクタを記述します。
    6. void OnVoice() サブクラスには、新しいメンバーを追加することができます。
    7. std::cout << "ワンワン、私は" << name << "よ" << std::endl; サブクラスからはスーパークラスのプロテクテッドもしくはパブリックなメンバーを自分のクラスのメンバーのように使えます。

    オーバーライド

    前節の inheritance.cpp では、Dog クラスにも、Cat クラスにも OnVoice というメンバ関数があります。このメンバ関数を「オーバーライド(over ride)」と言う仕組みを使って、もう少し整理されたコードにすることができます。

    override.cpp

    
    #include <iostream>
    
    class Animal
    {
    protected:
        std::string name;
    public:
        Animal(std::string name)
        {
            this->name = name;
        }
        void OnVoice()
        {
            std::cout << "私は" << name << "よ" << std::endl;
        }
    };
    
    class Dog : public Animal
    {
    public:
        Dog(std::string name) : Animal(name)
        {
        }
        void OnVoice()
        {
            std::cout << "ワンワン、";
            Animal::OnVoice();
        }
    };
    
    class Cat : public Animal
    {
    public:
        Cat(std::string name) : Animal(name)
        {
        }
        void OnVoice()
        {
            std::cout << "ニャ〜ン、";
            Animal::OnVoice();
        }
    };
    
    int main()
    {
        Dog pochi("ポチ");
        Dog shiro("シロ");
        pochi.OnVoice();
        shiro.OnVoice();
        
        Cat *mike = new Cat("ミケ");
        Cat *kuro = new Cat("クロ");
        mike->OnVoice();
        kuro->OnVoice();
        delete mike;
        delete kuro;
        
        return 0;
    }
        

    実行結果

    
    ワンワン、私はポチよ
    ワンワン、私はシロよ
    ニャ〜ン、私はミケよ
    ニャ〜ン、私はクロよ
        

    コード説明

    サブクラスにスーパクラスと同名の関数が存在する場合は、そのサブクラスでは、そのサブクラスの関数が優先的に呼び出されます。スーパークラスの同名の関数を呼び出したい場合は、
    スーパークラス::関数(); という形で呼び出します。
    このようにスーパークラスのメンバ関数を、サブクラスで書き換えることを関数の再定義もしくはオーバーライドと言います。

    仮想関数

    virtual.cpp

    
    #include <iostream>
    
    class Animal
    {
    protected:
        std::string name;
    public:
        Animal(std::string name)
        {
            this->name = name;
        }
        // 次のコードを virtual void OnVoice() に変えてみてください
        void OnVoice()
        {
            std::cout << "私は" << name << "よ" << std::endl;
        }
        
    };
    
    class Dog : public Animal
    {
    public:
        Dog(std::string name) : Animal(name) {}
        void OnVoice()
        {
            std::cout << "ワンワン、";
            Animal::OnVoice();
        }
    };
    
    class Cat : public Animal
    {
    public:
        Cat(std::string name) : Animal(name) {}
        void OnVoice()
        {
            std::cout << "ニャ〜ン、";
            Animal::OnVoice();
        }
    };
    
    int main()
    {
        Animal *shiro = new Animal("ポチ");
        Animal *pochi = new Dog("シロ");
        Animal *mike  = new Cat("ミケ");
        shiro->OnVoice();
        pochi->OnVoice();
        mike ->OnVoice();
        delete shiro;
        delete pochi;
        delete mike;
        
        return 0;
    }
        

    クラスのインスタンスをポインターで扱う場合は、スパークラスのポインターにサブクラスのインスタンスを代入することができます。しかし、そのインスタンスのメンバーを呼び出すと、スーパークラスのメンバーが呼び出されます。

    実行結果

    
    私はポチよ
    私はシロよ
    私はミケよ
        

    次に、void OnVoice() を virtual void OnVoice() に変えてみてください。今度は、サブクラスのメンバーが呼び出されるようになります。

    実行結果

    
    私はポチよ
    ワンワン、私はシロよ
    ニャ〜ン、私はミケよ
        

    このように virtual キーワードのついたメンバ関数のことを「仮想関数」と言います。

    純粋仮想関数

    サブクラスで再定義しなければならないことを前提としたメンバ関数を作ることもできます。このようなメンバ関数を「純粋仮想関数」と呼びます。そして、純粋仮想関数を含むクラスは「抽象クラス(abstract class)」と呼ばれます。抽象クラスからはインスタンスを作ることはできません。必ずサブクラスを作って、そこからインスタンスを作らなければなりません。

    純粋仮想関数は、virtual void OnVoice() = 0; というように関数の定義の際に 実装部分である { と } を無くし、 = 0; と記述すことによって作ります。

    purevirtual.cpp

    
    #include <iostream>
    
    class Animal
    {
    protected:
        std::string name;
    public:
        Animal(std::string name)
        {
            this->name = name;
        }
        virtual void OnVoice() = 0;
    };
    
    class Dog : public Animal
    {
    public:
        Dog(std::string name) : Animal(name) {}
        void OnVoice()
        {
            std::cout << "ワンワン、私は" << name << "よ" << std::endl;
        }
    };
    
    class Cat : public Animal
    {
    public:
        Cat(std::string name) : Animal(name) {}
        void OnVoice()
        {
            std::cout << "ニャ〜ん、私は" << name << "よ" << std::endl;
        }
    };
    
    int main()
    {
        //Animal *shiro = new Animal("シロ"); これはエラーになります。
        Dog *pochi = new Dog("ポチ");
        Cat *mike  = new Cat("ミケ");
        pochi->OnVoice();
        mike->OnVoice();
        delete pochi;
        delete mike;
    
      return 0;
    }
      

    実行結果

    
    ワンワン、私はポチよ
    ニャ〜ん、私はミケよ
      

    抽象クラスは、抽象クラスで定義された純粋仮想関数が、 抽象クラスを継承したサブクラスに 必ず実装(再定義)されている事を保証(明確に)するために用いられます。

    デストラクタ

    クラスには、インスタンスが消滅する(破棄される)ときに呼び出される デストラクタ(destructor)というメンバ関数も作ることができます。
    デストラクタは、~クラス名( ) {} と記述し、戻り値はなく、先頭にチルダ( ~ )をつけます。

    destructor.cpp

    
    #include <iostream>
    
    class Base
    {
    public:
        Base()
        {
            std::cout << "Hello" << std::endl;
        }
        ~Base()
        {
            std::cout << "Bye" << std::endl;
        }
    };
    
    class Sub : public Base
    {
    protected:
        std::string name;
    public:
        Sub(std::string name) : name(name)
        {
            std::cout << "Hello " << name << std::endl;
        }
        ~Sub()
        {
            std::cout << "Bye " << name << std::endl;
        }
    };
    
    int main()
    {
        Sub *jane = new Sub("Jane");
        delete jane;    // この行を削除するとどうなるかみてください
        Sub mary("Mary");
        return 0;
    }
      

    実行結果

    
    Hello
    Hello Jane
    Bye Jane
    Bye
    Hello
    Hello Mary
    Bye Mary
    Bye
      

    インスタンスが作成される時には、スーパークラスのコンスタクタが呼び出され、次にサブクラスのコンストラクが呼びされます。
    反対にインスタンが破棄される時には、サブクラスのデストラクタが呼び出され、次にスーパークラスのデストラクタが呼び出されます。

    delete jane; の行を削除すると次のようになります。

    
    Hello
    Hello Jane
    Hello
    Hello Mary
    Bye Mary
    Bye
      

    ポインターに代入したインスタンスは、自動的には破棄されません。必ず delete キーワードを使って、ポインターが指し示す実体を破棄するようにしましょう。

    デストラクの重要な役割の一つは、クラス内のポインターを delete することですが、 destructor.cpp では、クラス内にポインターはありませんので、特に delete することは何もありません。


85 visits
Posted: Nov. 30, 2019
Update: Dec. 10, 2019

ホーム   C/C++チュートリアル   目次