火曜日, 1月 22, 2008

C++解説4(クラスについて)

今回はオブジェクト指向と言われるもので
このクラス表現と言われるものがかなりの比重を占めています。
一言で言ってしまうとクラスとは構造体に関数を持たせたものです。
もちろん、それだけではない、様々な機能を提供してくれます。
クラスの文法は構造体と基本的に変わりません。

struct 構造体名 {};
に対して
class クラス名 {};
という感じです。

アクセス修飾子のpublic,protected,privateというものがありますが
順々に覚えていけばいいので、はじめは無視してしまいましょう。

class CHuman {
};

終わりです。もちろんこれだけでは何も使用できません。
そこで、名前と年齢という変数を持たせてみると

class CHuman {
char *strName;
int nAge;
};

#include "stdlib"
#include "string"

int main(void) {
CHuman obj;
char *Name = "名前";
obj.strName = (char*)malloc( strlen(Name) + 1 );
strcpy( obj.strName, Name );
obj.nAge = 20;
return 0;
}

となります。まあ、これだけであればクラスなんてものは必要ありません。
構造体と何も変わらないからです。
まあ、構造体の場合はtypedefしないと「struct 構造体名 変数名」としていた箇所を、
クラスの場合はtypedefしなくても
「クラス名 変数名」記述すれば良い分少しだけ良くなったかも!
何て言ったところで覚える価値はありませんね。
それではどんどん拡張して覚える価値のあるものにして行きましょう。

まず最初にアクセス修飾子に触れてみます。

class CHuman {
public:
char *strName;
int nAge;
};

と記述してみます。
先ほど、記述したメインと合わせてコンパイルすると何も変化がありません。
そうです。何もありません。

class CHuman {
private:
char *strName;
int nAge;
};

次にpublicと記述した箇所をprivateと変更してみましょう。
コンパイルエラーとなってしまいました。
privateとはずばり、クラス外には見えなくする修飾子となります。
それに対して、publicとは見えるようにする修飾子です。
つまり、もともと省略していたためpublicと解釈されていただけだったのです。
ビックリしました? まあ、ビックリしませんね。
protectedに付いては、継承のところで説明します。今はこの2つで十分です。

ところで何のために必要なの?
なんて考えてしまった人のために少しだけ補足しましょう。
今回は人間をクラスとして考えていますのでこれを例に取ります。
C言語の構造体の考え方は人間に関係する変数を
一括管理できる事がすばらしい事でした。
しかし、よ~く考えてみると人の年齢を誰が上げたのかが(更新した)
構造体ではまったく分かりません。
もっと言うと、そもそも人の年齢って下がるものなのでしょうか?
現実世界で考えると、人の年齢は誕生日が来る事によって上がる事になります。
これは、不変な事で毎日年を取るとか何て事があると困ってしまいますね。
長寿記録などあっさり更新ですね。
このように、誰でも変更できたものをそうではなくした事が重要なんです。
でもどうやって変更するの?っと思ったら少し考えれば解決です。
なぜなら、publicと修飾されていた時は、誰でも変更できました。
これは、publicで修飾されたものは公開アクセスだったからですね。
公開アクセスとは、誰でも参照できますよって事です。
たっだらいっその事変更する関数をpublicに置けばいいんじゃないの!
何て考えが浮かべばいいですね。
一番最初にクラス説明した時に、
「一言で言ってしまうとクラスとは構造体に関数を持たせたものです。」って
ところがありました。構造体では事ですね。
※実際には関数ポインタなどを駆使して近いものを作成する事ができます

class CHuman {
private:
char *strName;
int nAge;
public:
void UpdateAge(void) {
nAge++;
}
};

こんな感じで書いておけば良いです。年をとる事しかできません。
でも、誰でも変更できるじゃん!と思うかもしれませんが
さっきと決定的に違うのは、変更された時が分かるという事です。
N歳になったら何かあるのであれば、これは問題なく対応可能と言う事です。
ちなみに、privateのような修飾子で変数の内容を隠す事ができます。
このようにする事を一般的に情報隠蔽、カプセル化などど表現します。

で簡単に文法を説明しておきますと

//
// クラス記述方法
//
class クラス名 {
private:
// ここに非公開変数、関数を記述する
// 関数は基本的に宣言のみ。簡単なコードが記述可能
// inlineのからみがあって、複雑なのは書けません
// いずれ機会を作って、inline関数については説明します
public:
// ここに公開変数、関数を記述する
// 関数については、private同様簡単なものだけです
};

//
// クラスメソッド(関数)記述方法
//
返却値 クラス名::関数名() {
// 関数の内容を記述
}

簡単ですが、こんな感じです。


クラスはコンストラクタで初期化を行い、デストラクタで後処理を行います。
クラスとは簡単に言ってしまえば型の定義です。
つまり、プリミティブ型としてlong,doubleなどが用意されているにも関わらず
そんな型じゃ話にならん!!ってな状況ってありますよね。
言ってしまえば、C言語であった構造体だって
型の定義じゃないですかてな話ですよ。
でもC言語の構造体っていろいろと問題があったわけですよ。

たとえば、説明しましたが構造体はすべての構造体の中の変数がグローバルですよね。
これって以外と使えね~って思いませんか。

「構造体の初期化ってどうやってたの?」
「構造体を破棄するときって、何もしなくていいの?」
などなど...
(ここがコンストラクタとデストラクタの説明箇所だから強調させて下さい)

たとえば、平面のポイントを表す構造体を宣言すると

struct Point {
int x_;
int y_;
};

struct Point p;

でその後に、実際今宣言したポイントは何処なのかってな話で

p.x_ = 1;
p.y_ = 1;

って書いていましたですよ! でもこれっておかしくね~~~。
だって宣言する時にはもうポイントが何処かなんて分かっているんですよ!!
だったら、宣言時に初期化させて下さいよ。

すると以下のように書き直します。
struct Point p = { 1, 1 };

でも以下の場合はどうでしょうか?

struct Point {
int x_;
int y_;
};

struct Human {
char Name[12 + 1];
int Age;
struct Point *p;
};

こいつは先ほどの流れで書くと

struct Human h;

strcpy( h.Name, "name" );
h.Age = 20;
h.p = ?????; // あれ、これって先に宣言しておかなければ駄目じゃん

となって

struct Point p = { 1, 1 };
struct Human h = { "name", 20, &p } ;

ってな感じになりますかね。
でもって何が言いたいかと言いますとそもそも構造体を作成する時に
「必要な情報を構造体とは別に私どもが情報を管理するなどど言う事はナンセンスだ!!」
と言う事ですよ。
上の例で言えば、人を作成するまでは、(「struct Human h」の箇所)
何処に人がいるかなどど言う事はまったく関係ない!(「struct Point p」の箇所)
人をあるポイントに作成するのであって、ポイントと人を作って結びつけるなどはあんたがやってくれって感じですよ。
さらに、人が死んだ(消滅した)時に何だってポイントを私どもが消さなくてはならないのですか?
そんなの人が死ぬ時に責任持って消してくれって感じですよ!(2回目)

そんなあなたの要望に答えてくれるものは
コンストラクタでありデストラクタなんですよ。

でコンストラクタから説明します。
ずばり、コンストラクタとはクラス(構造体)の初期化です。
というかその前に、C++においてクラスと構造体の違いを説明しておきましょう。

ずばり、違いはありません。
本当は少しだけあるのですが、無いと言って問題ありません。

class Point {
int x_;
int y_;
public:
int getPosX(void);
int getPosY(void);
};
と書いている人がいたら、思いっきり違いがあります。
なぜなら、構造体はデフォルトがpublicなんです。だから
struct Point {
int x_;
int y_;
public:
int getPosX(void);
int getPosY(void);
};
なんて書こうものなら、すべてpublicと言う事になります。

つまり、クラスを
class Point {
private:
int x_;
int y_;
public:
int getPosX(void);
int getPosY(void);
};
と書いていれば、classをstructに変えても何も違いはありません。
と言う事で極力、デフォルトを使用せずに明記するようにしましょう。
でも、C++ではstructと記述はあまりしませんね。
C言語の構造体のようなものを定義する必要性がある時に
structと記述するのが良いと思います。(定番ですかね)
でもそんな時ってないと思います。

で話を戻してコンストラクタですが初期化です。
この 「= 1」に相当します。
またまた少し挫折します。

// 初期化
int i = 1;

// 代入
i = 1;

C言語において上の例を初期化といい、下の例を代入と言います。
はっきり言って、最終的な結果は何も変わりません。
しかし、代入の「i = 1」の前に「if ( i == 1 )」何て挿入しようものなら
iの値は不定です。
代入と初期化の一番大きい違いは、代入は邪魔できるけど初期化はできないって事です。
そして、コンストラクタとはクラスの初期化です。
つまり、クラス変数を定義した時にはコンストラクタとは常に実行され
定義直後に参照したところで、コンストラクタとの間には入れないって事になります。
コンストラクタ有りのクラスを作成してみます。

class Human {
public:
Human();
};

このように、クラスと同じ名前を持つメソッド(関数)となります。
返却型はありません。何も返却しないからです。
このコンストラクタという関数の中にクラスの初期化処理を記述します。

少し上のクラスを拡張します。

class Human {
private:
char Name[12];
int Age;
char Birthday[9];
public:
Human();
};

このようなprivate変数を管理している場合、コンストラクタでprivate変数を
初期化してしまうのが最もポピュラーな使用方法となるでしょう。

Human::Human() {
strcpy( Name, "NameValue");
Age = 0;
strcpy( Birthday, "20000101" );
}

てな感じです。
う~ん、でもこれではあんまり使えないコンストラクタですね。
名前も、年齢も、誕生日も固定なんて!!
その場合は、コンストラクタに引数として貰えば良いだけです。

public:
Human( char*, int, char* );

何て宣言方法に変わって、そんでもって
Human::Human( char *InName, int InAge, char *InBirthday ) {
strcpy( Name, InName );
Age = InAge;
strcpy( Birthday, InBirthday );
}

ちなみに、引数のまったく持たないコンストラクタを
一般的にデフォルトコンストラクタと言います。
覚えておきましょう!

次はデストラクタです。
これは初期化に対して終了処理、後処理という言い方が正しいと思います。
つまり、クラスを破棄する時にただ破棄して問題がある場合に
ここで後処理を行うという事になります。
たとえば、上のクラスでは名前がName[12]という形式で宣言していました。
しかし、名前なんてどのくらいの長さなのかは分かりません。
そこで、名前はchar*という形式に置き換え、
コンストラクタで指定されたサイズを格納できるように設定するように変更します。

class Human {
private:
char *Name;
int Age;
char Birthday[9];
public:
Human( char*, int, char* );
~Human();
};

Human::Human( char *InName, int InAge, char *InBirthday ) {
Name = new char(strlen(InName));
strcpy( Name, InName );
Age = InAge;
strcpy( Birthday, InBirthday );
}

Human::~Human() {
delete Name;
}

このようにすると単純にクラスを破棄した場合、Nameはメモリーリークする事になります。
そこでデストラクタの登場という訳です。
このデストラクタによってメモリーリークを防ぐという事になります。
それでは実際にprint分を入れて実験してみましょう。

#include "iostream"
#include "cstring"

class Human {
private:
char *Name;
int Age;
char Birthday[9];
public:
Human( char*, int, char* );
~Human();
};

Human::Human( char *InName, int InAge, char *InBirthday ) {
Name = new char(strlen(InName));
strcpy( Name, InName );
Age = InAge;
strcpy( Birthday, InBirthday );
std::cout << "(" << Name << ") Human::Human()\n";
}

Human::~Human() {
std::cout << "(" << Name << ") Human::~Human()\n";
delete Name;
}

int main(void) {
Human obj1( "semona", 0, "20040101" );
Human *obj2 = new Human( "yuuna", 1, "20030101" );

delete obj2;
}

--------------------------------------------------------------------------------
実行結果
--------------------------------------------------------------------------------
(semona) Human::Human() // Human obj1( "semona", 0, "20040101" );
(yuuna) Human::Human() // Human *obj2 = new Human( "yuuna", 1, "20030101" );
(yuuna) Human::~Human() // delete obj2;
(semona) Human::~Human() // }
--------------------------------------------------------------------------------
特に解説は不要だと思いますが、
実行結果の後ろに付けたコメントは実際に
何処で動作しているかのポイントを示しています。
クラスをポインタで定義したobj2はnewでコンストラクタ、
deleteでデストラクタがobj1は宣言時にコンストラクタ、
そしてそれがスコープ外となった時にデストラクタがそれぞれ動いています。

0 件のコメント: