C++言語によるプログラミング入門6
クラスを作ろう(補足)

 こんにちは。前回はNekoというクラスを作ってみました。宿題の「犬」はできましたか?(これは最後に考えます。)
 今回は、クラスNekoを、もう少し書き直してみます。プログラムの実行そのものに変化はなく、ちょっと足踏みな感じがするかもしれませんが、一気に新しいneko3.cppまで読んでみてください。
 前回は次のようなクラスNeko(猫)を書いてみました。

class Neko
{
private:
    string name;
public:
    Neko(string s){
        name = s;
    }
    void naku(){
        cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
    }
};

 説明は長かったですが、コード量はどうということはありません。わかりにくくてもプログラム(前回のneko.cpp)をコンパイルし実行してみてください。だんだんわかってくると思います。わかってしまえば簡単なのです。

 このクラスについて、いくつか補足があります。

●用語について
 クラス内にある変数は、メンバ変数とかデータメンバといいます。また、クラス内の関数はメンバ関数といいます。要するに、クラス内のものはメンバなのですね。

●コンストラクタの定義について
 Nekoのコンストラクタは、

    Neko(string s){
        name = s;
    }

です。その処理内容は「引数の値(sで表される)を、メンバ変数nameに代入する」ですね。つまり、オブジェクト生成時に行われることは、「オブジェクト内で、まず、nameという変数を作って、それから、その変数に値を代入する」ということになります。実は、「いったん変数を作って、それからその変数に値を代入する」のではなく「変数を作るときに値を入れる」ということもできます。それは

    Neko(string s) : name(s){}

とするのです。ここで「name(s)」が、「変数nameを作るときにsの値を格納する」という意味になるのです。このようにnameにデータを与えると、コンストラクタはほかにすることがなくなるので、中カッコの中は空でよくなります。この方が、ひと手間少ないですし、これからは、このように書くことにします。

●オブジェクトのデータを変えないメンバ関数について
 オブジェクトのデータを変えないメンバ関数には、後にconstというキーワードを付けるのがよいとされています。これは、「この関数は実行してもデータを変えないよ」という印です。今のところ、あってもなくてもよいようですが、いずれ「間違いのない(間違いの少ない)プログラム」書くのに重要になってきます。そこで、今後は、オブジェクトのデータを変えない関数には、constを付けることにします。
 すると、nakuは、オブジェクトのデータ(今はnameのことです)を使っているけど、変更はしていないので、constを付けるべき関数ということになります。そのように書くと、nakuの定義は次のようになります。

    void naku() const{
        cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
    }

 なお、C++では、意味のある部分の途中でなければ、自由に改行や空白をいれてよいことになっています。(実は、改行も空白の一種と考えられます。一方、日本語の空白(全角の空白)は、空白ではなく文字と解釈されるので、注意してください。)
 そのため、nakuは

    void naku() const{ cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl; }

などと書くこともできます。しかし、なるべく見やすいように書いた方がよいでしょう。

●private:について
 クラス定義の1番上のprivate:は省略してもよいことになっています。これがなくても、public:の上にあるものは、プライベート(非公開)になるのです。そこで、これも省略することにします。

 ところで、どうしてデータを非公開にするのだろうと思っているかもしれません。これは、初心者にはなかなか難しい話題です。簡単に言うと、「間違ってデータを変更できないようにする」ためなのです。実は、長いプログラムでは、変更すべきでないデータを間違って変更してしまうというミスをしがちです。これを防ぐことができるのです。
 neko.cppは短いプログラムですが、長いプログラムだと想像してみてください。Nekoのnameには、オブジェクトを生成するときにだけ、コンストラクタを通して値(名前)を与えることができます。nakuはnameの値を変更しないので、結局、nameはオブジェクト生成後には変更できないデータになっているのです。もちろん、変更したい場合には、変更する方法が必要になる(作ることもできます)わけですが、変更の必要がなければ、その手段もないように作っておく方が間違いが少ないのです。つまり、このようにデータを守ることができるんだ、と思ってください。(コード例を一番最後にお見せします。)

 以上をまとめると、Nekoというクラスは次のように書けることになります。

class Neko
{
    string name;
public:
    Neko(string s) : name(s){}
    void naku() const{
        cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
    }
};

 ふうーーーっ。
 やっと終わりかと思ったら、もう1つあります。実は、コンストラクタやメンバ関数の処理内容が長い場合、その定義は、クラス定義の外に書くのが普通なのです。

 クラス定義の中にメンバ関数の定義を書くと、それはコンパイラに「できれば、関数を使う場所それぞれに、同じ処理を書き込んでくれ」と要求することになるのです。
 ちょっとわかりにくいですね。つまり「ある関数を5箇所で使う場合なら、その5箇所全部に直接関数の処理を書き込んでほしい。10箇所なら10箇所全部に」と要求することになるのです(そうでない場合はすぐに説明します)。このように「使われる場所」全部にその内容が書き込まれる関数をインライン関数などといいます。この言葉を使えば、「クラス定義の中にメンバ関数を書くと、コンパイラに、その関数をインライン化するよう要求することになる」と言えます。
 インライン関数(の処理)は、「使われる場所」すべてに書き込まれることになるので、インライン関数を使うと実行可能ファイルが大きくなります。そのため、処理が長い関数はインライン化しないのが、普通です。(コンパイラにインライン化を要求しても、コンパイラの判断でインライン化しない場合もあります。)
 そのような場合、クラス定義の中には、「こういう関数があるよ」という宣言だけ残し、その関数の定義はクラス定義の外に書くのです。そうすると、コンパイラにインライン化を要求しないことになるのです。
 ところで、インラインでない関数では処理がどう行われるか、説明していませんでした。どうなるかというと、どこかにひとつだけ「関数の処理」が書かれ、「(関数が)使われる場所」では、その「関数の処理」が呼び出され使われることになるのです。したがって、インラインでない関数を使うのには手間が少しかかるのです。そのため、インライン関数より実行時間がかかりますが、「関数の処理」は一箇所に書かれているだけなので、実行可能ファイルは小さくできます。

 たとえば、Nekoのコンストラクタもnakuの処理もそれほど大きくないですが、特に、インライン化する必要もないので、クラスの外に書くとすると、次のようになります。

class Neko
{
    string name;
public:
    Neko(string s);
    void naku() const;
};

Neko::Neko(string s) : name(s){}

void Neko::naku() const{
    cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
}

 まずクラス定義の中を見てください。「Neko();」が「コンストラクタがあるよ」、「void naku() const;」が「nakuという関数があるよ」という宣言なのです。これは、定義部分をとっぱらって、「;」をつけただけですね。
 クラス定義の下にあるのが、クラス定義の外にあるコンストラクタとnakuの定義です。
 ここでNeko::Nekoと書いてあるのが、コンストラクタです。冗長な感じがしますが、クラス定義の外ではコンストラクタをこのように書くのです。一般に、「Neko::」は「クラスNekoの」という意味です。したがって、「Neko::Neko」は「NekoというクラスのNekoという関数(コンストラクタ)」という意味になるのです。
 また、クラス定義の外では、nakuはNeko::nakuと書きます。これは、「Nekoというクラスのnakuという関数」という意味ですね。クラス定義の外では、クラス名をはっきり書かなければ、どのクラスのメンバ関数かわからなくなってしまうので、「Neko::」が必要なのです。(「Neko::」を書かなければ、クラスに属さない関数とみなされてしまいます。)

 長い長い補足でした。書き直したNekoで前回のプログラムneko2.cppを書き直してみましょう。プログラム名はneko3.cppにします。

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

class Neko
{
    string name;
public:
    Neko(string s);
    void naku() const;
};

Neko::Neko(string s) : name(s){}

void Neko::naku() const{
    cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
}

int main()
{
    string s;
    cout<<"猫を生成します。名前を入力してください。"<<endl;
    cin>>s;

    Neko dora(s);  //コンストラクタが実行され、文字列sの名前のdoraが生成される
    cout<<"あなたの名づけた猫がメモリ上に生成されました。"<<endl;
    cout<<"猫が鳴きます。"<<endl;

    dora.naku();   //doraに対してnakuを実行
}

 mainは前回と同じなので、実行の様子も前回と同じになります。

Fig.1 neko3.exeの実行画面

 これだけでは面白くないので、前回の宿題・「犬」のクラスを、今回の書き方で書いてみましょう。

 ただ、「書き方」についてあと1つコメントがあります。Nekoのコンストラクタの宣言にはsという変数を書きましたが、定義がここにない場合、省略してもよいことになっています。そこに定義がないので、どうせそこでは使わないからです。その場合、コンストラクタの宣言は「Neko(string);」となります。(もちろん、定義ではsを使っているので、クラス定義の外の方でsを省略することはできません。)犬のクラスInuでは、そのように書いてみます。

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

class Inu
{
    string name;
public:
    Inu(string);   //「Inu(string s);」のsを省略した
    void naku() const;
};

Inu::Inu(string s) : name(s){}

void Inu::naku() const{
    cout<<"わん。僕は"<<name<<"だ。"<<endl;
}

int main()
{
    string s;
    cout<<"犬を生成します。名前を入力してください。"<<endl;
    cin>>s;

    Inu dog(s); 
    cout<<"あなたの名づけた犬がメモリ上に生成されました。"<<endl;
    cout<<"犬が鳴きます。"<<endl;

    dog.naku();
}

Fig.2 犬だってできる
 


 最後に、private:(省略されていても、一番上にあると考えます)の意味について、実際のコードで試しながら説明したいと思います。まだ興味のわかない人はななめ読みくらいにしてください。
 たとえば、上のneko3.cppで、mainの中の「dora.naku();」の直前に「dora.name = "太郎";」というコードを間違って書いてしまったとしましょう。「dora.name」と書くと、それは「doraのname」という意味になるのです。しかし、Nekoの定義では、nameをprivateにしたのでした。すると、privateなメンバ変数(つまり非公開のメンバ変数)を、クラスの外で変更しようとしているので、コンパイル時にエラーになるのです。(mainは、Nekoクラスの外であることに注意してください。)

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

class Neko
{
    string name;
public:
    Neko(string s);   //「Neko(string);」でも可
    void naku() const;
};

Neko::Neko(string s) : name(s){}

void Neko::naku() const{
    cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
}

int main()
{
    string s;
    cout<<"猫を生成します。名前を入力してください。"<<endl;
    cin>>s;

    Neko dora(s);   //コンストラクタが実行され、文字列sの名前のdoraが生成される
    cout<<"あなたの名づけた猫がメモリ上に生成されました。"<<endl;
    cout<<"猫が鳴きます。"<<endl;

    dora.name = "太郎";    //エラーになる
    dora.naku();
}

Fig.3 privateのものに直接アクセスするコードはコンパイルエラーになる(BorlandC++の例)

 しかし、Nekoを

class Neko
{
public:
    string name;     //public:の下に持ってきた
    Neko(string s);  //「Neko(string);」でも可
    void naku() const;
};

と変えると、nameはpublicになります。すると、このプログラムはコンパイルエラーではなくなるのです。もちろん、実行もできます。すると次のようになります。

Fig.4 nameをpublicにするとコンパイル・実行できる

 「nameの値を簡単に変えられるので、こっちのほうが便利じゃん」と思う人も多いようです。しかし、データを簡単に(直接)変更できてしまうのは危険なのです。プログラマがまちがって、「dora.name = "太郎";」のようにして、値を変更してしまうかもしれないからです。
 意図しないで「dora.name = "太郎"; 」なんて書いたりしないと思うかもしれませんが、長いプログラムだとそうでもありません。あるいは、そこまでハッキリしたコードではないところで、実質そのようなことをしてしまうのです。すると、「あれ、猫の名前をどう入力してもみんな太郎になるぞ。どうしてだ???」ということになるかもしれないのです。
 初心者はコンパイルエラーを嫌がりますが、コンパイルエラーは利用できるのです。つまり、nameをprivateにしておけば(publicの上に書いておけば)、間違ってnameを変更してしまうコードに対して、コンパイラがエラーを出してくれるのです。すると、プログラマは、その間違いにすぐ気が付くわけです。
 実際、恐ろしいのは実行時のエラーではないでしょうか。しかし、nameをpublicにしておくと、あとで実行時のエラーになるかもしれないのです。
 メンバ変数は、特別な理由がない限り、privateにしましょう。


 トップページにも書きましたが、細部にこだわるより、全体の流れを見てください。neko3.cppを書き換えてinu.cppができたなら、これまでのところは、わかっていることになるのです。
 もっと根本的に「クラスが何の役に立つのか」と思い始めたかもしれません。これは、おいおい説明していくつもりです。


補足(はじめは読み飛ばしてください)

 neko3.cppで、Nekoクラスを

class Neko
{
private:
    string name;
public:
    Neko(string s) : name(s){}
    void naku(){
        cout<<"にゃあ。俺様は"<<name<<"だ。"<<endl;
    }
};

のように書くと、コンパイラが警告を出すかもしれません。たとえば、Borland C++ 5.6では、コンストラクタに関して

「値でクラスを渡す引数を持つ関数はインライン展開されない」

という警告が出ます。クラス定義内のコンストラクタの定義が

    Neko(string s){
        name = s;
    }

でも同じです。これは、「このコンストラクタはインラインにできない」という意味です。一般に、警告は重要です。が、今の段階で、この警告は気にしなくてもよいと思います。この警告をなくす1つの方法は、neko3.cppのようにコンストラクタの定義を外に書くというものです。また、参照(リファレンス)というものを使う方法もあります。この警告の意味は、「参照」というものがわかってはじめてわかるのです。気になる人は、「C++入門」を一通り読み終えてから、C++入門補足3を読んでみてください。


目次のページ

前のページ  のページ