Profile

ys2310

Author:ys2310
2008年春にNew York Cityにあるふる〜い大学を卒業。


Categories


new postings


new comments


new trackbacks


monthly archeive


FC2ブログ 転職
DATE: CATEGORY:スポンサー広告
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
| BLOG TOP |
DATE: CATEGORY:C++ 基礎
さて、今回は「仮想関数」です。いよいよ「オブジェクト指向プログラミング」最後の項目ですね。...で、とりあえず、ポインタの復習をしましょう。ポイ ンタというのは、オブジェクトの場所(メモリ上の場所)を表す値(アドレス)格納する変数です。ポインタにオブジェクト(クラスのインスタンス)のアドレ スを代入して、何かするのでした。オブジェクトは、newを使って生成することもできます。例えば、次のプログラムを見てください。

//hito_sample.cpp
#include <iostream>
using namespace std;

//人を表すクラス
class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    void Jikosyoukai();  //自己紹介関数
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

int main()
{
    Hito *p;       //Hitoオブジェクトへのポインタ。まだ、何も代入されていない。
    p = new Hito(10);  //パワーが10のHitoオブジェクトが生成され、そのアドレスがpに代入される。
    p->Jikosyoukai();   //pの指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    delete p;      //pの指し示すオブジェクトを破棄。
}

 このプログラムを実行すると、Hitoオブジェクトが一度自己紹介して終わります。(なんなんだ。^^;) 何をやっているかわからない例ですが、まあ、説明のための例と思って我慢してください。まず、クラスHitoを定義し、mainの中では、そのアドレスを 格納するポインタpを定義し、Hitoオブジェクトを生成し、そのアドレスをpに代入します。これにより、pを使って、今生成したHitoオブジェクトを 扱えるわけです。実際、Jikosyoukai(自己紹介)という関数を呼び出しています。
 なお、あとで利用するため、Hitoには、powerの値を設定するset_powerと、powerの値を戻すget_powerという関数を付けてありますが、ここでは利用していません。

 ところで、クラスには「継承」というものがありました。Hitoから派生クラスをつくることができます。そして、C++では、派生クラスのアドレスも基底クラスのポインタに代入できることになっています。つまり、今の例では、Hitoから何か派生させた場合、そのクラスのオブジェクトのアドレスをHitoのポインタに代入できるのです。例を見てみましょう。

//hito_sample2.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    void Jikosyoukai();
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数
    void Jikosyoukai();
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

int main()
{
    Hito *p1, *p2;       //Hitoオブジェクトへのポインタ。まだ、何も代入されていない。
    p1 = new Hito(10);  //パワーが10のHitoオブジェクトが生成され、そのアドレスがp1に代入される。
    p1->Jikosyoukai();   //p1の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    p2 = new Samurai(12);  //パワーが12のSamuraiオブジェクトが生成され、そのアドレスがp2に代入される。
    p2->Jikosyoukai();   //p2の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    delete p1;      //p1の指し示すオブジェクトを破棄。
    delete p2;      //p2の指し示すオブジェクトを破棄。実は問題あり。後述。
}

 上のプログラムを実行すると、どうなると思いますか?mainの前半部分は前のプログラムと同じですね。
 p2に関しては、Samuraiオブジェクトを生成して、そのアドレスを代入しています。ちょっと、待てよ!と思った人。するどいです。p2はHito のポインタです。したがってHitoオブジェクトのアドレスを格納することになんの問題はありません。しかし、それだけでなく、Hitoの派生クラスであ るSamuraiのオブジェクトのアドレスを代入することもできるのです。まあ、そのように作られているわけです。もちろん、

Samurai *p3;
p3 = new Samurai(15);

のように、SamuraiオブジェクトのアドレスをSamuraiのポインタに代入することは当然できますが、ここではわざと基底クラスのポインタを使って見せたのです。
 もう一度言いましょう。p2はHitoのポインタであるにもかかわらず(Hitoの派生クラスである)Samuraiのオブジェクトを指し示すことができるのです。ここで

p2->Jikosyoukai();

とすると、どうなるでしょう?この場合、p2はHitoのポインタなのでHitoのJikosyoukaiが呼び出されます。Samuraiは Hitoの派生クラスなので、Samuraiオブジェクトは内部にHitoオブジェクトを持っているようなものでした。そこで、Samuraiのオブジェ クトに対してもHitoの関数を呼び出すことができるのです。

 結局、実行すると次のようになります。

 賢明なる紳士淑女のみなさんは、この辺で何かやるんだろ、やるんなら引き延ばさずに、はやくやってくれと思っていることでしょう。そうです。ここで「仮想関数」の登場です。 Hitoのメンバ関数Jikosyoukai()の宣言を

    void Jikosyoukai();

から、

    virtual void Jikosyoukai();

に変えてみます。これにより、Jikosyokai()は仮想関数というものに なります。仮想関数とはどういうものかというと、「オブジェクトが基底クラスのポインタで示されている場合にも、オブジェクトの型の関数が(正しく)呼び 出される関数」です。つまり、上の例の後半では、SamuraiのオブジェクトがHitoのポインタで表されるわけですが、この場合にも、p-> Jikosyokai()はHitoのでなくSamuraiの関数になるのです。

//hito_sample3.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai();  //仮想関数になる
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数
    void Jikosyoukai();  //基底クラスでvirtual宣言しているので、これも仮想関数になる
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

int main()
{
    Hito *p1, *p2;       //Hitoオブジェクトへのポインタ。まだ、何も代入されていない。
    p1 = new Hito(10);  //パワーが10のHitoオブジェクトが生成され、そのアドレスがp1に代入される。
    p1->Jikosyoukai();   //p1の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    p2 = new Samurai(12);  //パワーが12のSamuraiオブジェクトが生成され、そのアドレスがp2に代入される。
    p2->Jikosyoukai();   //p2の指し示すオブジェクトについてJikosyoukai()を呼ぶ。
    delete p1;      //p1の指し示すオブジェクトを破棄。
    delete p2;      //p2の指し示すオブジェクトを破棄。実は問題あり。後述。
}

 難しく考えないでください。仮想関数とは、上のような働きをするものなのです。なお、virtualは基底クラスの関数の前につければその派生ク ラスの同名のメンバ関数に影響が及ぶので、一度つければ良いことになっています。ただし、派生クラスにも付けて、かまいません。
 想像力のある人は「仮想関数のもつ可能性」がもう見えてしまったかもしれませんが、(私のような)普通の人にはまだピンとこないでしょう。ちょっとだけ 言っておくと、これはたくさんの似て非なるデータがある時に有効なものなのです。次回に簡単な具体例をお見せできると思います。

 なお、ここでちょっと面倒な話をしなければなりません。上のほうで、「Samuraiオブジェクトの中にHitoオブジェクトがある」という話をしました。それでは、

    delete p2;

では、ちゃんとSamuraiオブジェクトは破棄されるでしょうか?答えは、いいえ、です。p2はHitoのポインタなので、上の命令では、 Samuraiオブジェクトの中のHitoオブジェクトだけが破棄されることになります。今の場合、Samuraiは独自のデータを持っていないので、あ まり問題にならないかもしれませんが、よいことではありません。
 そこで、p2がHitoのポインタであっても、正しく「p2が指し示すSamuraiオブジェクト」を破棄するようにしておかなければならないのです。そのためには、Hitoに仮想デストラクタというものを付けることになっています。仮想デストラクタは

virtual ~Hito(){}

のようなものです。virtualが付いているデストラクタだから仮想デストラクタであるわけです。ここで重要なことは「仮想デストラクタがある」 ということです。そのデストラクタ自身がするべき仕事はないので、中カッコの中は空でよいのです。つまり、まとめると、Hitoは、

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    virtual ~Hito(){};    //仮想デストラクタ
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai();  //仮想関数になる
};

としておくべきなのです。こうすれば、「delete p2;」で、ただしくSamuraiオブジェクトが破棄されます。
 どういうときに仮想デストラクタが必要なのでしょうか。それは、hito_sample2.cppのように、「派生クラスのオブジェクトを基底クラスの ポインタで扱い、あとでdeleteをする場合」です。しかし、これはなかなか面倒な「条件」ですね。一度書いたプログラムをあとで書き直す場合、間違い が起こりそうです。そこで、一般には、「クラスが仮想関数を持てば、仮想デストラクタを付けるべき」と言われています。その理由は、仮想関数を持つクラス は、hito_sample3.cppのHitoのような使い方をされると予想されるからです。(実は、hito_sample.cppやhito_sample2.cppは説明のための例で、一般的なプログラムの書き方ではないと思います。)初心者には嫌なところですが、このルールを守れば、とりあえずOKでしょう。

 最後に、上の例に「忍者」「町娘」をいれた例をつくってみましょう。

//hito_sample4.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    virtual ~Hito(){};    //仮想デストラクタ
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai();  //仮想関数になる
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数(これも仮想関数になる)
    void Jikosyoukai();  //基底クラスで同名の関数にvirtualを付けたので、ここにvirtualはいらない
};

//Hitoの派生クラスNinja
class Ninja : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Ninja(int x) : Hito(x){}
    void Jikosyoukai();
};

//Hitoの派生クラスMatimusume
class Matimusume : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Matimusume(int x) : Hito(x){}
    void Jikosyoukai();
};

void Hito::Jikosyoukai()
{
    power--;  //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

void Ninja::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "拙者は忍者でござる。" << endl;
    cout << "拙者のパワーは" << get_power() << "でござる。" <<endl;
}

void Matimusume::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "あたいは江戸っ娘よ。" <<endl;
    cout << "あたいのパワーは" << get_power() << "よ。" <<endl;
}

int main()
{
    Hito One(10);        //人
    Samurai Two(12);  //さむらい
    Ninja Three(14);     //忍者
    Matimusume Four(16);  //町娘
    Hito *p;       //Hitoのインスタンスへのポインタ。まだ、何も代入されていない。

    p = &One;
    p->Jikosyoukai();
    p = &Two;
    p->Jikosyoukai();
    p = &Three;
    p->Jikosyoukai();
    p = &Four;
    p->Jikosyoukai();
}

 この例では、あえてnew/deleteを使わない方法にしました。たとえば、Oneがオブジェクトである場合、&OneがOneのアド レスを表すので、それをHitoのポインタに代入できるわけです。この場合でも、仮想関数の性質により、正しいJikosyoukaiが呼び出されること になります。これは、ポインタを使っているからなのです。

 上のプログラムでは、deleteが使われないので、(一見)仮想デストラクタは不要です。しかし、new/deleteを使うときは必要になるので、削除などしてはいけません。「クラスが仮想関数を持てば、仮想デストラクタを付けるべき」のルールに従いましょう。

簡単に言うと、「クラス、継承、仮想関数を有効に使いこなすプログラム」を書くことが、オブジェクト指向プログラミングと呼ばれるものです。(もちろん、これはとりあえずの簡単な言い方なので、専門家の人は怒らないでくださいね。)

 今回は、前回の例、さむらいたちを使って考えてみましょう。前回のプログラムでは、クラスHito(人)をつくって、そこからSamurai(さ むらい)、Ninja(忍者)、Matimusume(町娘)を派生させました。また、それぞれにJikosyoukai()(自己紹介)という関数を定 義しました。この「自己紹介関数」は、仮想関数にしたので、基底クラスHitoのポインタを経由して呼び出されるても、ちゃんとそれぞれが正しく自己紹介 してくれるのでした。
 なぜ、わざわざ基底クラスのポインタを使うかというと、それによって、「さむらい」も「忍者」も「町娘」も同様に扱えるからです。
以下に簡単な例を紹介しますが、ちょっとだけ注意を先にします。まず、いろいろなオブジェクトを同様に扱うためにHitoのポインタを複数用意する必要があります。そのために、ここでは「ポインタの配列」を作ります。これは、

    Hito *x[5];

などと定義するのです。こうすると、x[0]からx[4]までの5つの変数ができますが、これはHitoのポインタになるのです。(こう書くとそうなると いう約束があるだけです。)このポインタにオブジェクトのアドレスを代入していきます。そうしておいて仮想関数を使うのです。では。

//hito_sample5.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    virtual ~Hito(){}; //仮想デストラクタ
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai(); //仮想関数になる
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数(これも仮想関数になる) 
    void Jikosyoukai(); //基底クラスで同名の関数にvirtualを付けたので、ここにvirtualはいらない
};

//Hitoの派生クラスNinja
class Ninja : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Ninja(int x) : Hito(x){}
    void Jikosyoukai();
};

//Hitoの派生クラスMatimusume
class Matimusume : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Matimusume(int x) : Hito(x){}
    void Jikosyoukai();
};

void Hito::Jikosyoukai()
{
    power--; //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

void Ninja::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "拙者は忍者でござる。" << endl;
    cout << "拙者のパワーは" << get_power() << "でござる。" <<endl;
}

void Matimusume::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "あたいは江戸っ娘よ。" <<endl;
    cout << "あたいのパワーは" << get_power() << "よ。" <<endl;
}

int main()
{
    Hito *x[5];    //Hitoへのポインタを5つ用意

    //オブジェクトの生成と、そのアドレスのポインタへの代入
    x[0] = new Samurai(12);
    x[1] = new Samurai(15);
    x[2] = new Ninja(7);
    x[3] = new Ninja(8);
    x[4] = new Matimusume(18);

    //以下2行がこのサンプルのポイント
    for(int i = 0; i < 5; i++)
        x[i]->Jikosyoukai();


    //オブジェクトの破棄
    for(int i = 0; i < 5; i++)
        delete x[i];

}

 main以外は前回と同じです。(前回に定義したクラスを書いているだけです。)
 上の例でポイントは、mainの中の2行だけです。

    for(int i = 0; i < 5; i++)
        x[i]->Jikosyoukai();

 5人の登場人物にそれぞれ自己紹介をしてもらうのに、このように2行で済んでしまうところを見てもらいたかったのです。つまり、上のプログラムで は、5人のいろいろな人をfor文で一気に処理しても、つまり、場合分けをしなくても、それぞれのオブジェクトの種類によって正しい関数が呼び出されるの です。これが仮想関数の良いところなのです。
 う〜ん、でも、それほど良く見えませんね。それはfor文の前の初期設定がごたごたしているからでしょう。言い訳をすると、一般的に言って、初期設定や 入力部分はどうもすっきりしません。例えばファイルからの入力などなら、比較的きれいになりますが、いつもそうとは限らないのです。ただ、(ここからが言 い訳なのですが)本当のプログラムでは「初期設定」(や「入力」)に比べて「情報処理」の部分が大きな割合を占めると思います。その「情報処理」の部分 は、上のfor文のように、異なる種類のデータを場合分けせずに一括処理できるので、これは大変な「プログラムの簡略化」になるはずなのです。上の例は短 すぎて、このありがたさがあまり見えず、初期設定のごたごたばかりが目に付いているのです。つまり、あとは想像力で私の例を補っていただきたいのです。

 ここで、ちょっと練習に、上のプログラムを、ユーザがデータを入力していくプログラムに変えてみます。ただし、データの数(登場人物の数)は、簡単のため、5に固定したものにします。
 main()以外は、いつも同じものを使えます。ほんとに、クラスを使うプログラムは楽ですよね。(ちょっと強引でしたか?)

//hito_sample6.cpp
#include <iostream>
using namespace std;

class Hito
{
    int power;
public:
    Hito(int x) : power(x){}
    virtual ~Hito(){}; //仮想デストラクタ
    void set_power(int x){ power = x; }
    int get_power() const{ return power; }
    virtual void Jikosyoukai(); //仮想関数になる
};

//Hitoの派生クラスSamurai
class Samurai : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Samurai(int x) : Hito(x){}
    //Hitoと同名のメンバ関数(これも仮想関数になる) 
    void Jikosyoukai(); //基底クラスで同名の関数にvirtualを付けたので、ここにvirtualはいらない
};

//Hitoの派生クラスNinja
class Ninja : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Ninja(int x) : Hito(x){}
    void Jikosyoukai();
};

//Hitoの派生クラスMatimusume
class Matimusume : public Hito
{
public:
    //Hitoのコンストラクタにxを渡す以外は何もしないコンストラクタ
    Matimusume(int x) : Hito(x){}
    void Jikosyoukai();
};

void Hito::Jikosyoukai()
{
    power--; //自己紹介に力を使ってpowerを1減らすことにする
    cout << "俺は人だ。" << endl;
    cout << "俺のパワーは" << power << "だ。" <<endl;
}

void Samurai::Jikosyoukai()
{
    set_power(get_power() - 1);
    //上はpowerの値をget_powerで取り出し、それから1減らした値をpowerにセットしている
    //つまり、これでpowerを1減らしたことになる
    cout << "俺はさむらいだ。" << endl;
    cout << "俺のパワーは" << get_power() << "だ。" << endl;
}

void Ninja::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "拙者は忍者でござる。" << endl;
    cout << "拙者のパワーは" << get_power() << "でござる。" <<endl;
}

void Matimusume::Jikosyoukai()
{
    set_power(get_power() - 1);
    cout << "あたいは江戸っ娘よ。" <<endl;
    cout << "あたいのパワーは" << get_power() << "よ。" <<endl;
}

int main()
{
    Hito *x[5];    //Hitoへのポインタを5つ用意
    int temp, power;

    //オブジェクトの生成と、そのアドレスのポインタへの代入
    cout << "5人のデータを順次入力してください。" << endl;
    for(int i = 0; i < 5; i++){
        cout << "選択してください:" << endl;
        cout << "1 さむらい 2 忍者 3 町娘" << endl;
        cin >> temp;
        cout << "パワーを入力してください:" << endl;
        cin >> power;
        //switch文を使います。入門16を参照してください。
        switch(temp){
            case 1:
                x[i] = new Samurai(power);
                break;
            case 2:
                x[i] = new Ninja(power);
                break;
            case 3:
                x[i] = new Matimusume(power);
                break;
        }
    }

    cout << "それでは各自自己紹介します。よろしいですか?"<<endl;
    cout << "1 はい 2 いいえ" << endl;
    cin >> temp;
    //ユーザが1以外の整数を入力したら終了
    if(temp != 1) return 0; //mainの中での「return 0;」はmainを終了させる

    //自己紹介
    for(int i = 0; i < 5; i++)
        x[i]->Jikosyoukai();

    //オブジェクトの破棄
    for(int i = 0; i < 5; i++)
        delete x[i];
}



[Quote]:http://www.asahi-net.or.jp/~yf8k-kbys/newcpp23.html

| BLOG TOP |

copyright © Manhattan life all rights reserved.Powered by FC2ブログ