クラス

C++の最も重要な機能はクラスです。Cにはありませんし、クラスなしでもプログラムを組むことができます。しかし、クラスをうまく使うと複雑なプログラムを分かりやすく書くことができます。 最近ではクラスを使ってプログラムを書くことはプログラマにとって必須の能力といえます。

クラスの使い方の一つの例として、新しい型をクラスを使って作るということがあります。intcharは標準で組み込まれている型ですが、クラスを使うと自分で好きな型を作ることが出来ます。 状況に応じて適切な型を作るとプログラムがとてもに簡単になります。


クラスの説明に入る前に文字列について考えてみます。

文字列の比較、例えば

char[ ] text1 = "TEXT1";
char[ ] text2 = "TEXT2";

とした場合text1とtext2の内容が同じかどうかを調べるにはどうしたらいいでしょうか。残念ながら

if(text1 == text2){...}

のように書いても上手くいきません。text1とtext2の内容は文字列のポインタですから、上の書き方では文字列がメモリ上の同じ位置に格納されているかどうかを調べることになります。

一つの方法は、

int compare(char[] text1, char[] text2)
{
    for(int i = 0; text1[i] != '\0' && text2[i] != '\0')
    {
        if(text1[i] != text2[i]) return 1;
    }
    return 0;
}

のような関数を自分で作って調べることです。各文字列の要素を一つずつ比較します。しかし、このような関数は既に用意されています。

#include <string.h>

として、

if(strcmp(text1, text2) == 0)
{
    printf("文字列%sと%sは同じ文字列です。", text1, text2);
}

のように書けます。strcmp (string comparison)は文字列が同じときに0を返します。

 

文字列をつなぐ場合はどうしたらいいでしょうか。やはり、

char[ ] text3 = text1 + text2;

としてもうまく行きません。これも単にポインタの足し算になってしまいます。 アドレスとアドレスを足すだけですからtext3は意味のない場所を指すことになります。

文字列を繋ぐ関数も用意されていて、

char text3[100]; //配列の要素は必ず text1とtext2の文字列の合計より多いこと(終端記号も含む)
text3[0] = '\0'; //最初はtext3=""にする。
strcat(text3, text1);
strcat(text3, text2);
printf("%sと%sを結合した結果は%sです。", text1, text2, text3);
 

のようにstrcat (string concatenation)を使います。これもstring.hで定義されていますので使うときはインクルード(つまり、先頭に#include<string.h>を書く)してください。

ところで、文字列を ==で比較したり、+で結合したりできないものでしょうか。クラスを使うとできます。



クラスの基本

まずは、基本的な事柄を説明します。

//プロジェクト名: class1

class MyChar
{
public:
    char* text;//メンバ変数(プロパティ)
    int length;//メンバ変数(プロパティ)
};//クラスの定義の最後には;が必要

int main()
{
    MyChar myText; //MyChar型の変数myTextを宣言
    myText.text = "hello!";
    myText.length = 7;//文字数のセット
    printf("text=「%s」,length=「%d」", myText.text, myText.length);
    fgetc(stdin);
    return 0;
}

上の部分でクラスを定義しています。MyCharはクラス名で好きな名前をつけることができます。ただし既に定義されている名前を使うことはできません。 クラスは、既にある型を組み合わせて新しい型を作ることができます。クラスを別のクラスの一部として使うこともできます。 上の例のchar* text;やint length;のような変数をメンバ(member)変数といいます。最近では、他のプログラミング言語の呼び方に習ってプロパティやフィールドと呼ばれることもあります。どれも同じ意味です。VSでも、GUIプログラミングではプロパティーと呼ばれます。

クラスの中のpublic:はアクセス制御子(access specifier)と呼ばれるものでクラスを利用するもののアクセスを許可したり制限したりするためのものです。 public:と書くとそれ以降が外側からアクセス可能になります。publicのほかにprivateprotectedがあります。何も書かないとprivateが指定されたことになります。privateはクラスの中からしかメンバにアクセスできません。protecedはよく使われますが、もう少し前提知識が必要なのでここでの説明は省略します。

アクセス制御にpublicが指定されていると上の例のように

myText.text = "hello!";

のようにメンバ変数にアクセスできます。myTextのメンバ変数textを表すにはピリオドを使ってmyText.textとします。上のプログラムのpublicprivateにすると外部からアクセスできなくなるのでコンパイル時にエラーになります。

上のMyCharのようなクラスの場合、

MyChar myText;

のような書き方は「MyCharのインスタンスmyTextを生成する」とも言います。しかし、int a;などと同じと考えてほとんど問題ありません。

クラスには、メンバ関数というクラス専用の関数を定義することができます。

表示用のメンバ関数を書いてみます。以下の例を見てください。

//class1を変更する。
#include <stdio.h>
class MyChar
{
public:
    char* text; //メンバ変数(プロパティ)
    int length; //メンバ変数(プロパティ)
    void print() //メンバ関数(メソッド)
    {
        printf("text=「%s」,length=「%d」", text, length);
    }
};//クラスの定義の最後には;が必要

int main()
{
    MyChar myText; //MyChar型の変数myTextを宣言
    myText.text = "hello!";
    myText.length = 7;//文字数のセット
    myText.print(); //文字を表示する
    fgetc(stdin);
    return 0;
}

メンバ変数をセットするメンバ関数も書きましょう。文字列の長さはstrlenという関数で数えることができるのでそれを使います。strlenを使うには#includeとしてstring.hをインクルードする必要があります。

//class1を変更する。
#include <stdio.h>
#include <string.h>

class MyChar
{
public:
    char* text;
    int length;
    void print()
    {
        printf("text=「%s」,length=「%d」", text, length);
    }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
    }
};//クラスの定義の最後には;が必要

int main()
{
    MyChar myText; //MyChar型の変数myTextを宣言
    myText.set("hello!");
    myText.print(); //文字を表示する
    fgetc(stdin);
    return 0;
}
 

上のようにすると、もうメンバ変数のtextとlengthはメンバ関数を介してしかアクセスされませんので、次の例のようにprivateに出来ます。

//class1を変更する。
class
MyChar
{
private://省略しても良い
    char* text;
    int length;
public:
    void print()
    {
        printf("text=「%s」,length=「%d」", text, length);
    }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
  
 }
};//クラスの定義の最後には;が必要

メンバ変数は、このようにprivate又はprotectedにしてなるべく外部から直接アクセスせずにメンバ変数を介してアクセスするようにすると良いと言われています。ここでは詳しい説明は省略しますが、そのようにしたほうが変更に強いプログラムになりやすいからです。

次に文字列が等しいかどうか調べるメンバ関数を追加しましょう。

まず、メンバ変数textを返すメンバ関数getTextを追加します。

//class1を変更する。
class
MyChar
{
private://省略しても良い
    char* text;
    int length;
public:
    void print()
     {
        printf("text=「%s」,length=「%d」", text, length);
     }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
   
}

    char* getText()
    {
        return text;
    }
};//クラスの定義の最後には;が必要

そして、自分と別のMyChar型の変数(インスタンス)の文字列が等しいかどうか調べるメンバ関数equalを追加します。

//class1を変更する。
class
MyChar
{
private://省略しても良い
    char* text;
    int length;
public:
    void print()
     {
        printf("text=「%s」,length=「%d」", text, length);
     }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
   
}

    char* getText()
    {
        return text;
    }

    bool equal(MyChar& c)
    {
        char* buf = c.getText();
        if(strcmp(text, buf)==0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

};//クラスの定義の最後には;が必要

クラスは型として振舞うのでメンバ関数のbool equal(MyChar& c)のように関数の引数に指定することが出来ます。boolintなどと同じようにあらかじめ用意されている型でfalsetrueの2つの値のいずれかをとる型です。equal関数は等しいか等しくないかを調べるので2通りの値をとるbool型の値を返すのが意味として分かりやすいのです。

それでは、この新しいMyCharをテストするプログラムを実行してみてください。

//class1を変更する。
#include <stdio.h>
#include <string.h>

class MyChar
{
private://省略しても良い
    char* text;
    int length;
public:
    void print()
     {
        printf("text=「%s」,length=「%d」", text, length);
     }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
   
}

    char* getText()
    {
        return text;
    }

    bool equal(MyChar& c)
    {
        char* buf = c.getText();
        if(strcmp(text, buf)==0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

};//クラスの定義の最後には;が必要


int main()
{
    MyChar myText1, myText2, myText3;
    myText1.set("hello!");
    myText2.set("good-by!");
    myText3.set("hello!");

    if(myText1.equal(myText2) == true)
    {
        printf("myText1はmyText2と等しいです。\n");
    }
    else
    {
        printf("myText1はmyText2と異なります。\n");
    }

    if(myText1.equal(myText3) == true)
    {
        printf("myText1はmyText3と等しいです。\n");
    }
    else
    {
        printf("myText1はmyText3と異なります。\n");
    }

    fgetc(stdin);
}

セットする文字列の内容を変えて色々試してみてください。

メンバ関数equalまで作りましたが、何とか

if(myText1 == myText2){ ... }

のように書けないものでしょうか。実は出来ます。

実は+や==なども関数として扱われています。大雑把に説明すると、+や==などの演算子の場合は 1 == 2 と書かれたら ==(1, 2)のような関数を実行するという約束になっています。つまり

bool ==(int& a, int& b){ ... }

のような関数があらかじめ定義されているわけです。

MyCharに==を追加してみましょう。

//class1を変更する。
class
MyChar
{
private://省略しても良い
    char* text;
    int length;
public:
    void print()
     {
        printf("text=「%s」,length=「%d」", text, length);
     }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
   
}

    char* getText()
    {
        return text;
    }

    bool operator==(MyChar& c)
    {
        char* buf = c.getText();
        if(strcmp(text, buf)==0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }


};//クラスの定義の最後には;が必要

bool equal(MyChar& c)をbool operator==(MyChar& c)に変えただけです。==のような演算子を定義する場合にはその前にoperatorを付けます。

それでは、メイン関数も書き換えましょう。

//class1を変更する。
int
main(int argc, char* argv[])
{
    MyChar myText1, myText2, myText3;
    myText1.set("hello!");
    myText2.set("good-by!");
    myText3.set("hello!");

    if(myText1 == myText2)
    {
        printf("myText1はmyText2と等しいです。\n");
    }
    else
    {
        printf("myText1はmyText2と異なります。\n");
    }

    if(myText1 == myText3)
    {
        printf("myText1はmyText3と等しいです。\n");
    }
    else
    {
        printf("myText1はmyText3と異なります。\n");
    }

    fgetc(stdin);
}

実際に実行して確かめてください。

ここで、

myText1 == myText2

myText1.opretor==(myText2)

と解釈されると考えてください。operator== がequalに相当します。operator==もequalもbool型を返す関数です。書き方が違うだけです。

 

クラスの派生

そうすると代入=も定義できそうです。==の場合と同様にMyCharを書き換えてもいいのですが、ここでは、MyCharを他の人が別のプログラムで使っていてMyCharの定義を勝手に書き換えると怒られる場合を想定してみてください。

そういう時はMyCharの機能を受け継ぎながら別のクラスを定義するのが良い方法です。そのために派生という方法と使います。

MyCharがプログラムの上のほうで定義されていると仮定して、

class MyChar2 : public MyChar
{
};

とすると、MyCharの機能をそっくり受け継いだ新しいクラスMyChar2が定義されました。上の例のpublicはMyCharのpublicprotectedのメンバを受け継ぎます。 多くの場合派生のときはpublicを指定してよいので、その他のケースの説明は省略します。

こうするとmain関数の中のMyCharをMyChar2に置き換えても上手く動きます。

それでは、=を追加してみましょう。

class MyChar2 : public MyChar
{
public:
    void operator=(char* c)
    {
        set(c); //MyCharの機能を引き継いでいるのでMyCharのpublicなメンバは使うことが出来る。
    }
};

 

それでは、完全なプログラムを以下に示します。ちゃんと実行してくださいね。

//class1を変更する。
#include <stdio.h>
#include <string.h>

class MyChar
{
private://省略しても良い
    char* text;
    int length;
public:
    void print()
     {
        printf("text=「%s」,length=「%d」", text, length);
     }

    void
set(char* t)
    {
        text = t;
        length = strlen(text)+1;//strlenは終端記号を数えない。
   
}

    char* getText()
    {
        return text;
    }

    bool operator==(MyChar& c)
    {
        char* buf = c.getText();
        if(strcmp(text, buf)==0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }


};//クラスの定義の最後には;が必要

class MyChar2 : public MyChar
{
public:
    void operator=(char* c)
    {
        set(c); //MyCharの機能を引き継いでいるのでMyCharのpublicなメンバは使うことが出来る。
    }
};

int main()
{
    MyChar2 myText1, myText2, myText3;//MyCharの代わりにMyChar2
    myText1 = "hello!"; //=が使えるようになった。
    myText2 = "good-by!";
    myText3 = "hello!";

    if(myText1 == myText2)
    {
        printf("myText1はmyText2と等しいです。\n");
    }
    else
    {
        printf("myText1はmyText2と異なります。\n");
    }

    if(myText1 == myText3)
    {
        printf("myText1はmyText3と等しいです。\n");
    }
    else
    {
        printf("myText1はmyText3と異なります。\n");
    }

    fgetc(stdin);

    return 0;
}

ここでは、派生の重要性はあまりピンと来ないかもしれません。しかし、派生はものすごく重要です。プログラムを作っていると分かるのですが、自分が作ろうとしているものは 大抵既に誰かが作っています。そしてそれは、クラスとして用意されていて、コンパイラにprintfなどの関数と同じようにincludeするだけで使えるように組み込まれています。それらは、たいてい自分で作るより優れていますし誤りもありません。ただし、用意されているものはそのまま使えるのではなく 、自分のプログラムのためにわずかな修正や機能の追加が必要になります。そこで、今あるクラスを再利用しながら自分用にカスタマイズするのに派生は大活躍するのです。用意されているクラスの一つにはSTLというものがあります。これは後で説明します。


MyCharは勉強にために定義しましたが、実はわざわざ自分で文字を扱うクラスを定義しなくても既に用意されています。

以下のプログラムを見てください。

//プロジェクト名: class2
#include <stdio.h>
#include <string> //.hが無いことに注意!!

int main(int argc, char* argv[])
{
    std::string text1, text2, text3;
    text1 = "hello!";
    text2 = "good-by!";
    text3 = "hello!";

    if(text1 == text2)
    {
        printf("text1はtext2と等しいです。\n");
    }
    else
    {
        printf("text1はtext2と異なります。\n");
    }

    if(text1 == text3)
    {
        printf("text1はtext3と等しいです。\n");
    }
    else
    {
        printf("text1はtext3と異なります。\n");
    }

    fgetc(stdin);

    return 0;
}

std::stringというのが文字列を扱う型です。これはSTL (standard template library)で定義されているクラス(正しくはテンプレートクラス)です。STLとして定義されているものは必ずstd::で始まります。std::stringはとても便利です。

include文に注意してください。STLの定義されているファイルは拡張子がありません。std::stringを使うには#include<string>とします。string.hとは別物です。

//プロジェクト名: class3
#include <stdio.h>
#include <string> //.hが無いことに注意!!

int main(int argc, char* argv[])
{
    std::string text1, text2, text3;
    text1 = "hello!";
    text2 = "good-by!";
    text3 = "hello!";

    text1 = text3 + " and " + text2;//文字列を連結できます。

    printf("%s\n", text1.c_str());//c_str()はMyCharのgetTextにあたるものです。

    fgetc(stdin);

    return 0;
}

 

まだまだ、色々あります。詳しくはHELPでstringを検索してみてください。


構造体は...

ところで構造体の説明ですが、C++では構造体とクラスの違いはほとんどなくなりました。

構造体ではclassの代わりにstruct(structure)を使います。

先ほどのMyClass2は以下のようにかけます。

struct MyChar2 : public MyChar
{
    void operator=(char* c)
    {
        set(c); //MyCharの機能を引き継いでいるのでMyCharのpublicなメンバは使うことが出来る。
    }
};

唯一の違いはアクセス指定子を省略した場合にpublicになることです。classではprivateでした。

それでは、更にクラスについて勉強しましょう。