C++

関数へオブジェクトを値渡しすると、一時オブジェクトが発生するので、デストラクタが複数回呼び出される問題を前回取り上げました。
この問題を解決する方法は、実はあなたはご存知です。

想4:この問題を解決する方法とは?


そう、ポインタはどこから見ても同じメモリ上のアドレスを表すが故に、オブジェクトのコピーが行われると不都合になるのでした。
であればそのポインタを利用してやればよいのです。
実値ではなく、実値を指すポインタ変数を渡す関数を作成してみましょう。

C++では、一番下の関数PowerUPは、中段のPowerUPと引数が異なるので、違う関数として扱われます。名前が同じで引数が違う関数を作成することを、関数をオーバーロードするとも言います(戻り値が違っても引数が同じであれば同じ関数として扱われ、コンパイルエラーとなります)

新しく作成したPowerUP関数では、オブジェクトを直接受け取らず、オブジェクトへのポインタ変数を受け取ります。ポインタ変数とは、単にアドレスが入った変数のことでした。
つまりこの関数はオブジェクトのアドレスを受け取るということです。
なので、PowerUPを使用する時は、PowerUP(&Enemy1);と書くことができます。

今まで、クラスのメンバーへアクセスする際は、.を使ってきましたが、それはオブジェクトから直接アクセスする場合においてです
オブジェクトのアドレスが格納されたポインタ変数からメンバーへアクセスする時は、->を使います。
これはただの文法規則なので意味は洞察しないで下さい。
オブジェクトから直接アクセス .
オブジェクトのアドレスが格納された、オブジェクト型のポインタ変数からのアクセス ->


また、->(アロー演算子)で実値を表現しているので、オブジェクト型においては*で実値を表現する必要はありません。

アローでの実値の表現(オブジェクト)ep->HP
アスタリスクでの実値の表現(整数)*HP


更に、ポインタによってオブジェクトを遠隔で直接操作しているのでreturnで値を返す必要はありません。



これでエラー落ちすることはなくなりました。
なぜならデストラクタが呼び出されないからです。
再びiostreamをEnemyCharacter.hにインクルードし、デストラクタが呼び出されているか確認してみましょう。

159

デストラクタは呼ばれませんでした! 関数も正しく動作しています。
このエントリーをはてなブックマークに追加

ところで以前、コンストラクタで実行時メモリ確保を行い、デストラクタで破棄する手法をご紹介しましたよね。実はその手法には欠点があるのです。


コンストラクタでメモリを実行時に確保し、デストラクタで破棄しています。
このソースコードは一見正しく見えますが、実行すると例外が発生し、プログラムが落ちます。

2枚目のソースファイルを見て下さい。生成したEnemy1オブジェクトを関数へ渡していますね。
そうです。値渡しです。つまりこのオブジェクトEnemy1は、PowerUP関数へオブジェクトのコピーが渡され、returnする時に破棄され、ここでデストラクタが呼び出されます

その後、戻り値を戻す際にも一瞬オブジェクトのコピーが生成され、そこでもMPが指すメモリ上のアドレスが解放されます。同じアドレスを2回deleteするので、例外が発生するのです。
戻り値を戻す際になぜコピーが発生するかというと、戻り値の返却も値渡しだからです。
158

int* MP;はアドレス変数です。newで領域を確保し、その領域の場所をMPへ格納しているだけなので、MPをコピーしても新しく領域が確保される訳ではありません。値渡しの際に作成されるコピーも、戻り値を戻す際に一瞬作成されるコピーも、main関数で定義されたEnemy1オブジェクトも、全て同じ領域を指します。
なので、main関数が終了すると、更にデストラクタでメモリ上の領域を解放するのです。

この問題を解決する最も簡単な手法を次回、ご紹介致します。
このエントリーをはてなブックマークに追加

Cでは実行時のメモリの確保はmallocとfreeを使っていました。
しかしC++ではmalloc と freeの代わりに、newdeleteを使用できます。

newdeleteはmallocより洗練され、より直感的かつ簡単にメモリを確保できるようになりました。
C++の構文なので、特別なにかをインクルードする必要はなく、型をキャストする必要もなく、割り当てるメモリのバイト数を明示的に指定する必要もありません。


このプログラムは、コンパイル時にint型の領域EXEMemoryが確保されるのではなく、実行時の処理がnewへ移った時にint型のEXEMemory領域が確保されます。

*EXEMemoryは EXEMemoryがアドレス変数なので、*で実際のデータへアクセスします。
最後にdeleteでEXEMemoryを解放しています。
deleteへは、解放すべき領域のアドレスを渡します。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
newで実行時にメモリを確保する際、変数を初期化することもできます。

確保する領域の型の後ろに()を付け、対応するデータを渡して下さい。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
また、newで動的にオブジェクトを生成することもできます。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
newで配列を確保することもできます。しかし、初期化することはできません。

初期化はできないので、手作業で必要な値を入れる必要があります(forでループさせるなど)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
本講義で作成したプログラムは、可読性を高める為に確保した値を確認する機構を用意していません。値を確認したい方は、各自iostreamをインクルードし、std::coutで確かめてみて下さい。
このエントリーをはてなブックマークに追加

デストラクタ関数はコンストラクタ関数と対になる存在です。
デストラクタはオブジェクトが破棄された時、自動的に呼び出されます。

ローカルオブジェクトが破棄される時はオブジェクトを保持する関数がreturnした時です。
グローバルオブジェクトが破棄される時は、そのプログラムの実行が終了した時です。

デストラクタを宣言、定義する方法は、コンストラクタの書式に加え、関数の先頭に~を付けることです。
また、戻り値と引数を持たせることはできません。


このクラスから生成されたオブジェクトは、破棄される際にnameとHPをそれぞれヌル文字と0へ戻します。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
上の例はあまり実用的ではありません。ですが、コンストラクタで実行時メモリ確保を行い(malloc)デストラクタで実行時メモリ解放(free)を行うのは大変実用的です。
デストラクタの主な使い方はこれです。しかし、実行時メモリ確保は頻繁に使用するテクニックではない為、試したい方はもう一度mallocとfreeの使い方を復習して下さい。
【F限目】The Final Lesson※一番下です。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
デストラクタには副作用があります。厳密には副作用ではなく、正常な挙動なのですが、人間から見るとどうしても直感に反するというか、つまるところ都合の悪い挙動を起こすので副作用と呼ばれています。

ところでオブジェクトは変数のような存在とお話しました。
そのことを確認する為に、オブジェクトを引数として受け取り、オブジェクトを返却する関数を書いてみましょう。


PowerUP関数は、EnemyCharacterオブジェクトを受け取り、そのHPを+100して返却します。
EnemyCharacterクラスの下へ書いている点にご注目下さい。EnemyCharacterの上に書くと、コンパイラがEnemyCharacterの存在を把握せずにPowerUP関数を解読しようとする為、コンパイルエラーとなります。

では、PowerUP関数をmain関数から使用してみましょう。

156


Enemy1をPowerUP関数へ渡し、戻ってきたオブジェクトをそのままEnemy1へ代入しています。
これにより、魔王ラストボスのHPは100上昇しました。

さて、一見問題なく動いているように見えるこのコード、実はバグの温床があるのです。
それは……デストラクタ関数です。

想3:PowerUPへオブジェクトを渡した時、デストラクタは呼び出されるでしょうか?


デストラクタが2回呼び出されるとはどういうことでしょうか。
実は、関数へ変数やオブジェクトを渡す時、値渡しが行われます。
値渡しとは、渡す変数やオブジェクトと全く同じコピー変数あるいはコピーオブジェクトを作成し、関数内ではそれを操作する手法のことです。

つまり、PowerUP関数の終了時に、Enemy1のコピーオブジェクトのデストラクタが発動するのです。実際にはEnemy1はmain関数内でまだ使用しているにもかかわらず。

最も、コピーオブジェクトのデストラクタが発動しようが、main関数側のオブジェクトには関係ありません。
しかし、実はまだデストラクタは発動するのです。
なんと、C++のコンパイラは、変数やオブジェクトを戻す際にも、一時オブジェクトを作成するのです。これにより、戻り値としてオブジェクトを戻す際にも一時オブジェクトが作成され、すぐ破棄されるので、デストラクタが呼び出されます。
戻り値を戻す際になぜコピーが発生するかというと、戻り値の返却も値渡しだからです。まあこれもmain関数側のオブジェクトには関係ない一時オブジェクトなのでよいのですが。

まとめると、関数の終了時と、関数の返却時にコピーオブジェクトが生成され、関数の終了時と関数の返却時にコピーオブジェクトが破棄されるので、PowerUP関数へオブジェクトを渡した場合、デストラクタは意図せず(?)2回呼び出されるのです。

実際に確認してみましょう。
iostreamをEnemyCharacter.h側にもインクルードし、デストラクタの処理として文字列を画面へ出力します。

157
このエントリーをはてなブックマークに追加

コンストラクタの話をする前に、クラスを作成する練習をしましょう。
今回作成するクラスは敵キャラを生成するクラスです。
前回作成したCalculationクラスは削除して構いません。(右クリック→削除(V)→削除(D))
ソリューション エクスプローラーを右クリックし、追加→新しい項目 を選んで下さい。
146


今回はEnemyCharacter.hを作成します。なお、.hを付けなくても拡張子は自動的に.hになります。
152


敵キャラなので、名前とステータスを作成しましょうか。


さてこの時点でお気づきの方もいるかもしれませんが、
この敵キャラクラスから実際の敵キャラを生成するとどうなるでしょうか?

153


そうです。全く同じキャラが生成されるのです。しかも名前もHPもありません。
では、Cスタイルで初期化しておきましょう。


……ちょっと待って下さい。これでは魔王ラストボスが2体生成されるだけです。
154


なんというか、全然柔軟じゃないですよね。
宣言したオブジェクト毎に違う値で初期化できないものでしょうか……?

その機能を叶えてくれるのがコンストラクタ関数です。
コンストラクタ関数は、オブジェクトを生成する際に1度だけ自動実行される関数です。

コンストラクタ関数を作成するには、クラス名と同じ関数を宣言、定義します。
この際戻り値を持たせることはできません。そもそも必要ありません。


strcpyは文字列定数を文字配列へ代入してくれる関数です。cstringをインクルードして使います。また、仮引数に=1を付けていますが、これはデフォルト仮引数と言い、引数が指定されなかった時にhの値が1となることを意味します。


オブジェクトを生成する時に、コンストラクタ関数へ渡す引数も宣言します。
EnemyCharacter Enemy1("魔王ラストボス", 5000);
EnemyCharacter Enemy2("その辺の魔物", 20);

下記のサンプルコードを実行し、各オブジェクトが違うデータを保持していることを確認して下さい。

155


クラスには2種類の使い方があります。
単独のオブジェクトを生成し、関数を動かすのが主である管理クラスと
複数のオブジェクトを生成し、データを保持するのが主であるデータクラスです。
このエントリーをはてなブックマークに追加

↑このページのトップヘ