例外をもう一度送って☆彡
こんにちは
ぷないしんです。
今日はプログラム上で例外をキャッチしたとき、その例外の状態に対処できない、あるいは対処すべきではないと判断される場合に行うのが、例外の再送出です。
再送出ではちょっとピンとこないので例外の処理の仕方の話をしようと思います。
例外を補足したときに、それに対する処理が完了できないのであれば、その例外をそのまま再送出するか、あるいは別の例外として送出するとよいということなんですが、さっそく具体例を見ていきましょう。
rethrow.cpp
実行結果
xに読み込んだ値が1,7,99以外の時は例外は送出されません。
・1の時
まずint型の1が創出され、int型用の例外ハンドラに補足され、『func:int型の例外』と表示します。これで例外に関する処理は終わりです。
・7の時
まずdouble型の7.0が創出され、double型用の例外ハンドラに補足され、『func:double型の例外』と表示します。
さらに"Lucky seven!"という文字列の例外を送出します。
つまり、double型の例外を受け取った後に、const char*という別の型の例外として送出します。
・99の時
まず文字列型の"99例外"が創出されます。文字列(const char*型)用の例外ハンドラに補足され、『func:文字列型の例外』と表示し、さらに受け取った例外をそのまま再送出します。
再送出された例外は、main関数中の文字列用の例外ハンドラで補足され、『main:文字列"99例外"を補足』と表示されます。
このような動きを行う別のプログラムを書いてみます。
これはある特定の範囲に限定して整数をよみこむプログラムです。
このプログラムでは数値の読み込み方を工夫しています。
たとえばint型の整数値を抽出子>> でcinから読み込もうとしているときに、アルファベトや記号文字などの文字が入力されたくないので、キーボードからの入力を文字列として読み込んで置き、それを解析して数値に変換するということをしています。
それでは各関数の説明に行きましょう。
・string_to_int
関数のget_intとget_int_boundから下請け的に呼び出される関数です。
仮引数strに受け取った文字列をint型の整数値に変換します。
ただし"13X"みたいに整数に見れない場合はFormatErrorを返します。
・get_int
キーボードからの入力をいったん文字列として読み込んで置き、string_to_intに依頼し、整数値に変換してからその値を返却する関数です。
FormatError例外を補足した場合は、「数字以外の文字が入力されました。」と表示します。表示後は例外を再送出します。
もしも再送出を行わなければ、本館数を呼び出したmain巻子では例外を補足できないです。(つまり、整数値を正しく読み込めたかどうかの判断が行えません。)
・get_int_bound
get_intと同様に、文字列として整数値を組み込んで返却する関数です。ただしキーボードから入力される数値がlow以上high以下(main関数の指示によって、10以上99以下)であることを期待して読み込む点が異なります。
読み込んだ整数が期待した範囲内でなければ、ValueError例外を送ります。
この関数は内部でget_intを呼び出していますが、そこからさらに関数string_to_intが呼び出されます。
仮想関数???
こんにちはぷないしんです。
今日は仮想関数
バーチャルな関数・・・のお話です。
前置きとして
せっかくなので以前使った”会員”のクラスに【シルバーメンバー】クラスを追加していこうと思います。
一般会員クラスとの差異は
・シニア特典でレベルによって内容の違うint形のデータメンバsenior_level
・上記のメンバの度合いを取得、設定するゲッターとセッターget_senior_level、set_care_level
それでは早速ヘッダとソースです。
silver.h
SilveMember.cpp
セッターの動きによって、Levelの値は必ず0以上、5以下になります。
また、メンバ関数printは、そのレベルを表示します。
下の様にクラスによって表示項目が違います。
・No.1 Kimura Takuma is 46.5kg
↑一般クラス:会員番号、氏名、体重
・No.3 Kusanagi Takeshi is 58.5kg Special is Ippon Satisfaction
↑VIPクラス:会員番号、氏名、体重、特典
・No.2 Nakai Masamune is 59.7kg Level = 3
↑シルバークラス:会員番号、氏名、体重、レベル
VIPクラスと同じように、シルバークラスは一般クラスの派生なので構成としてはこんな感じですね。
Memberからpublic派生しているシルバー会員クラスは”一般クラスの一種”となります。
(is-Aの関係が成立するということです)
メンバ関数のいんぺい
基底クラスのMemberにメンバ関数printが存在し、派生クラスであるVIPMember、SilverMemberにもこの関数があります。
この様に、基底クラスのメンバ関数と同じ名前のメンバ関数が派生先のクラスで定義されると、派生クラスのメンバ関数は基底クラスのメンバ関数を隠します。
そのことを次のソースで確認してみましょう。
内容はVIPMember型、SilverMember型の各オブジェクトに対して、メンバ関数printを呼び出すだけの簡単なものです。
MemberPrint.cpp
kusanagiに対してはVIPMember::printが呼び出され、NakaiはSilverMember::printが呼び出されています。
この例では関数printの仮引数の型と個数は同一です。とまれ、関数名さえ同じなら仮引数の型や戸数が違っていても隠ぺいは行われます。
基底クラスのメンバ関数と同一名の派生クラスのメンバ関数は、仮引数の型や数が違っていても、基底クラスのメンバ関数を隠ぺいする。
VIPMemberとSilverMemberは、is-Aの関係であるpublic派生でMemberクラスから派生しています。
なので、Memberから継承したメンバ関数printは、VIPMemberとSilverMemberでも公開メンバとして存在します。
継承したメンバ関数は、名前が隠されているだけで会って、存在が消える訳ではありません。
実際に隠ぺいされている基底クラスのメンバ関数は、クラスの外部から呼び出せます。
それを確認してみましょう。
Memberprint2.cpp
有効範囲解決演算子「::」を使い、Member::print()により、基底クラスMemberのメンバ関数printを呼び出しています。
基底クラスから継承したメンバと同名のメンバが派生クラス内にある時、”基底クラス名::メンバ名”で基底クラスから継承した(隠ぺいされている)メンバにアクセスできる。
静的な型
次は下のコードです。
関数put_memberは、引数mに受け取った会員の情報を表示します。
その時、体重が55kg以上ならば先頭に@を付けます。
Memberprintref.cpp
関数put_memberが受け取るmの型は(constな)Member&型です。
main関数から三回呼び出している実引数は
・Member型kimura
・VIPMember型kusanagi
・SilverMember型Nakai
への参照です。
kusanagiへの実行結果から
①呼び出された関数put_memberの仮引数mがVIPMember型であるkusanagiを参照すること
②put_member宣言時でmに対して呼び出されるprintがVIPMember::printではなく、Member::printになっていること(特典が表示されていない)
が分かります。
①については、
基底クラスへのポインタ/参照は、派生クラスのオブジェクトを指す/参照できるので不思議はありません。
②になる理由は単純でmの型が【Memberへの参照型】だからです。
基底クラスへのポインタ/参照が指す/参照する式を実行して得られるのは、派生クラスの型ではなく、基底クラス型です。
つまり
オブジェクトmの静的な型はMemberである
ということです。
静的な型の表示方法はtypeidを使えば出せるので確認してみましょう。
MemberStaticType.cpp
はい、ということでどちらもVIPMember型ではなく、Member型ですね。
仮想関数
ということで本題の仮想関数の話に入っていきます。
先ほどのままではさまざまな制約を受けます
この問題を解決する手段の一つして仮想関数を使います
では一般会員クラスのヘッダ部分を改良していきましょう。
Member.h
改良するといいながら前のヘッダーと違う所はprintの前にvirtualを追加したことだけです。
これでMemberPrintrfをコンパイル、実行してみましょう。
とまあこんな感じで特典情報やレベルが表示されました。
今までは呼び出される関数は、プログラムをコンパイルするときに静的に決定されていましたが、こうすることにより
mの参照先オブジェクトのクラスに所属するメンバ関数printの呼び出しで
基底クラスであるMember形への参照mを通じて、Memberのメンバ関数が呼び出されたり、派生クラスであるVipMemberやSeniorMemberのメンバ関数が呼び出されています。
なので、当然mの参照先のオブジェクトがどのクラスであるのかはコンパイル時には決定できません。
そのため、どのクラスのメンバ関数printを呼び出すのかはコンパイル時ではなく、プログラムを実行したとき、動的に決定されます。
軽傷で済んだので継承していく
こんにちは、ぷないしんです。
昨日の記事を継承し、続きの話に入っていこうと思います。
昨日の記事の最後に
プログラム上のあちらこちらに”似て非なる”クラスが大量に存在し、開発効率、保守性が下がります。
と書きましたが、これらの問題を解決する手段として”派生”を使いましょう。
派生とは、既存クラスの資産を継承するクラスを作り出すことです。
(派生の時は、データメンバ、メンバ関数などの資産を単純に継承するだけではなく、追加したり上書きしたりできる)
定義の仕方はこんな感じで
↓派生
クラスBaseと、それを継承するDerivedを定義しています。
クラスDerivedを定義するときにクラス名のDerivedの後ろに【:】、そのあとに継承元のbase
これでbaseから派生したクラスDerivedが出来ました。
呼び方としては
派生元
・基底クラス、上位クラス、親クラス、スーパークラス
派生先
・派生クラス、下位クラス、、子クラス、サブクラス
等があるみたいで、C++では基底クラス、派生クラスと呼ぶことが多いみたいですね。
さて、この二つのクラスが持つ資産を概略を表してみます。
この図のように、
・基底クラス base
【a】と【b】の二つの変数があり、関数は≪func≫の一つです。
・派生クラスDerived
定義を行っているときは【x】と≪method≫だけが宣言、定義されています。
ですが、baseを継承しているのでそれら二つを合わせ変数は3個、関数は2個になります。
派生クラスは基底クラスの資産を継承すると同時に、それを部分として含むクラスの事である。
(ちなみにコンパイラによって自動的に定義されるデフォルトコンストラクタとデフォルトデストラクタ、代入演算子なども、各クラスの資源として含まれる、またフレンド関係は継承されることはない)
クラス階層図
派生クラスは基底クラスの”子供”のようなもので、その親子関係を表してみます。
派生クラスDerivedの定義":base"の部分は
「私の親はクラスbaseです」
という宣言です。
つまり、親クラス"base"の知らないところで子供が生まれています。
子供は親を知っているのですが、親は子供を知りません。
親として、子供がいるのか、いないのか、もしいるのなら何人いるのかといった情報を親は持ちえません。
基底クラスの方で『○○クラスを私の子供にします』といった宣言はできないのです。
なので、矢印の向きは派生クラス→基底クラスになります。
ところで、派生は一度だけではなく、何度でも出来ます。
クラスAからクラスBが派生し、クラスBからクラスC、クラスDが派生しています。
クラスBはクラスAの子で、クラスC、DはクラスBの子です。
つまり、クラスC、DはクラスAの孫ですね。
一部では直接の親にあたるクラスを直接基底クラスと呼び、直接の親ではないが、先祖になるクラスを間接基底クラスとよぶこともあるみたいです。
クラスD視点でみると、クラスBは直接基底クラスで、クラスAは間接基底クラスになるといった具合です。
また、「クラスDはクラスAから間接派生している/クラスBから直接派生している」といった呼ばれかたをすることもあります。
派生の形態
外部に公開したほうがいいデータや手続きのみを公開し、そうでないものを非公開とするのがクラス設計時の原則です。
派生クラスは基底クラスの資産を継承するが、それらをクラス外部に公開するかどうかは別問題です。
基底クラスと派生クラス内のメンバのアクセス関係はこの三種類の派生形態によって変わります。
・private派生
・protected派生
・public派生
これら指定の仕方は派生クラスを定義するときに、基底クラス名の前にアクセス指定子を書いて行います。
例えばこの様に書いた場合はpublic派生です。
class Derived : public Base {}:
また、アクセス指定子を省略したときは、自動的に【private派生】になります。
(派生クラスを定義する時のキーワードがstructの時は【public派生】になる)
三つの派生形態の例としてですが
private派生
クラスSuperからprivate派生を行う例です
Private.cpp
(コンパイルエラーになるところはコメントアウト済み)
実行しても何も起きないプログラムです。
private派生を行うと、派生クラスSubにとってクラスSuperは非公開の基底クラスになります。
アクセス性は以下の通りです。
派生クラスの内部からは基底クラスの【privateメンバ】はアクセス出来ません。
また、基底クラスの【protectedメンバ】と【publicメンバ】は派生クラス内では【privateメンバ】として扱われ、派生クラスの利用者に公開されません。
コメントアウトした部分がコンパイルエラーになり理由ですが
①派生クラスsubの内部において、基底クラスSuperの【privateメンバ】priのアクセスは出来ない。
②基底クラスsuperの全メンバは、派生クラスsubの利用者に対して非公開である。
限定公開(protected)メンバは外部に対して存在を隠すが、直接派生クラスに対しては存在を隠さない
という事です。
protected派生
クラスSuperからprotected派生を行う例です。
protected.cpp
(コンパイルエラーになるところはコメントアウト済み)
アクセス性は以下の通りです。
派生クラスのメンバ関数から基底クラスの【privateメンバ】をアクセス出来ないのはprivate派生と同じです。
ただ、基底クラスの【protectedメンバ】と【publicメンバ】が派生クラス内で【protectedメンバ】として扱われる点がprivate派生と違う所です。
(これらのメンバは派生クラスの利用者に対しては非公開)
protectedメンバはSubから派生したクラスの内部ではアクセス出来ますが、その外部からは不可能です。
public派生
クラスSuperからpublic派生を行う例です。
public.cpp
(コンパイルエラーになるところはコメントアウト済)
アクセス性はこちらです
他の派生と同様に、派生クラスのメンバ関数から基底クラスの【privateメンバ】をアクセスするのは出来ません。
基底クラスの【protectedメンバ】は派生クラス中でも【protectedメンバ】としての扱いで、基底クラスの【publicメンバ】は派生クラスでも【publicメンバ】として扱われるので、派生クラスの利用者に公開されます。
つまり、基底クラスのprivate以外のメンバ(protected、public)のアクセス性が派生クラスでも維持されています。
派生のカタチ
三種類の派生(private、protected、public)に共通する原理や規則性を纏めると
・どの派生でも基底クラスの非公開(private)メンバは派生クラスからはアクセスできない
・”〇〇派生”を行うと、基底クラスの公開(public)メンバが派生クラスの”○○部”に所属するようになる
・限定公開(protected)メンバは外部には公開されないが自分の子(直接派生するクラス)には公開される
こんなところでしょうか。
以上がクラスの継承の話でした。
協力者のキムラ、クサナギ(敬称略)に感謝をしつつ本日はこのあたりにしておきます。
ありがとうございました。
ぷないしん
クラスでケガしたけど軽傷ですんだ
こんにちはぷないしんです。
タイトルに深い意味はありません。
C++の大きな特徴であるクラス、今日はその継承について今一度振り返ってみます。
という事でよく説明や試験問題で使われる”会員”クラスを作っていき、例によってそれらを解説、改良していきながらやっていきます。
ではまずヘッダーから
Member.h
このクラスのデータメンバは下の三つです・
・名前”full_name”
・会員番号"number"
・体重"weight"
コンストラクタは【name】【no】【w】に受け取った三つの値でそれぞれのメンバを初期化します。
コンストラクタの他、5つのメンバ関数が定義されています。
それぞれ
名前を取得する【name】 会員番号を取得する【no】体重を取得する【get_weight】、また設定を行う【set_weight】、会員情報を表示する【print】
ですね。
つづいてこちらを
Member.cpp
体重(メンバのweight)を設定するメンバ関数のset_weightはweightがマイナスにならないように調整(もしwにマイナスを受けとった場合、weightに0を入れる)
コンストラクタの
部分はset_weightに体重の設定を任せています。
また、氏名のfull_nameと番号のnumberの初期化はコンストラクタの初期化子で行っています。↓
では、Member型のオブジェクトを一つ作り、各ゲッターを呼び出すだけの簡単なものを書いていきます。
Membertest.cpp
ここではパンピーのキムラ君を表しているのがMember型のオブジェクトkimuraです。
キムラ君の会員番号は1で、体重は48kg、ところが、キムラ君は1.5kgのダイエットを成功したのでget_weightとset_weightを使い、体重を46.5kgに更新してます。
VIP会員クラス
さて、このクラブは特典を付けた【VIP会員】制度が出来ました。
会員ごとに内容が違う特典をstring型のメンバで表す【VIPクラス】を作ります。
パンピークラスMemberを元に、VIPクラスを作るのは簡単ですね。
ヘッダーとソースの各ファイルをコピーし、部分的な追加と変更をするだけです。
この方法で作った”試作版”のVIPクラスのヘッダーとソース部を作ります。
VIPMember0.h
特典のstring型のデータメンバspecialを作ると同時に、特典の取得と設定をするget_special、set_specialを追加しました
(specialのセッターset_specialは、もし仮引数に空文字を受け取ったとき、文字列"Unregistered"をspecialに代入するようにしています)
それとMember.cppの方も手を入れていきましょう。
VIPMember0.cpp
コンストラクタの仕様も変えています
特典用の文字を受け取る仮引数【spe】が増えると同時に、その値の設定を処理するところを追加しています。
(メンバ【special】の値設定はメンバ関数の【set_special】に任せているので、仮引数【spe】に空文字を受け取った時、【special】には"Unregistered"が表示される)
そして、これらを利用する実装部です。
VIPMember0test.cpp
はい
VIP会員のクサナギタケシ君を表しているのが、クラスVIPMember0型のオブジェクトであるKusanagiです。
会員番号は3番で、体重は60kgからダイエットに成功し58.5kg、特典は一本サティスファクションです。
さて、この一般会員クラスとVIPクラスを両方を使うプログラムを書いてみます。
Wtest.cpp
二つのdiet関数は会員のダイエット処理を行う関数です。
同じ処理をしている関数が各クラス毎に作られ、多重定義をしています。
なぜかというと、関数の処理を行う対象の変数の”型”が違うからです。
(この関数の違いは、引数"m"の型だけで、その他は全部同じ)
一般クラスとVIPクラスの見た目は殆ど同じなのですが、コンパイラ視点で見ると、何の関係もないクラスです。
同一、または似たようなクラスを仕様の違うクラスとして書いてしまうと、
プログラム上のあちらこちらに”似て非なる”クラスが大量に存在し、開発効率、保守性が下がります。
そしてこの話を継承して明日へ…
ぷないしん。
メモリの記憶域の話~デストラクタを添えて~
こんにちはぷないしんです。
今日はクラスの外側のメモリ管理の話をします。
あとちょっぴりデストラクタの話も
ということで、まずは配列クラスを使ってクラスの外側の資源の管理方法です。
整数型の配列を実現するクラスを記述します。
IntArray.h
このクラスのデータメンバーは要素の数であるnelemと、配列の先頭の要素を指すポインタであるvecの二つです。
要素の数であるnelemの値は、メンバ関数のsizeで調べられます。
では、コンストラクタとこの関数についてお話します。
コンストラクタ
コンストラクタ本体では、メモリの領域を確保し、配列の本体を動的に作ります。
生成する配列の要素の数は、仮引数であるsizeで受け取った値です。
オブジェクトが下の様に定義されたときのコンストラクタの動作ですが
intArray x(5);
まず、nelem(size)の働きで、データメンバーnelemが5で初期化されます。
次に実行されるコンストラクタではnew演算子で確保したnelem個分、記憶域の先頭要素へのポインタをvecに代入します。
実際に確保しているメモリ域は生成したオブジェクトxの外側にあります。
にもかかわらず、クラスIntaArrayの内部からvec[0]、vec[1]、~~~、vec[4]の式で生成した配列の各要素を先頭から順番でアクセス可能です。
添字演算子
配列の各要素に外部から簡単にアクセスできるように定義しているのが""
どこかの地域では添字演算子とか呼ぶらしいですね。
その演算子関数operatorの返却値の型はなんとint&です。
なぜなら
a = x[5]
(代入演算子の右側はintでもint&でもOK)
代入の右辺のみで使用するならintでも大丈夫なのですが、下の様に左辺における様にするには、int&じゃないとできないからです。
x[2] = 5
(代入演算子の左側はintではNG、int&を使おう)
それでは実際にIntArrayを動かしてみましょう。
n個分の配列を作り、すべての要素に添字と同じ値を代入ついでに表示しています。
ではここで下の関数についてちょっと考えてみましょう。
IntArray型のオブジェクトxは関数内で生成されているので自動的にメモリ域の期間が与えられます。
それがいつまで有効なのかというと、
①二つのメンバ"nelem"と"vec"を持つオブジェクト"x"を生成
②コンストラクタでxを初期化
(nelemが5で初期化、そのあと5個の配列用のメモリ域がnew演算子で確保、先頭要素へのポインタがvecに代入)
③関数funcの実行終了、自動的にメモリ域の期間を持っているオブジェクト"xこの期間が終了し、破棄される。
だが、new演算子で確保された配列本体用のメモリ域は破棄されることなくメモリ域に残った状態になる。
コンストラクタで確保した動的な記憶期間をもつメモリ域は、オブジェクトを破棄したとしても、自動的に解放されることはありません。
解放されていない配列はどこからも指定されていない、無駄な領域としてメモリ上に取り残されます。
なのでこの関数を何度も読みだすと、その都度無駄な領域が確保されていき、実際に使えるメモリ域が減っていきます。
我々がこの期間をコントロールできるオブジェクトは好きな時に生成し、好きな時に破棄できるのですが、その作業をちゃんと行うのが我々の使命です。
なのでこのように確保したオブジェクトはちゃんと指示して解放しないといけないです。
そのために、配列本体の領域を破棄する【メンバ関数】を定義しましょう。
こんな感じです。
この関数を呼び出すように上の関数を書き換えます。
これでXが破棄される前に配列本体を破棄できます。
しかしこの方法は微妙ですね。
・delete_vec()を呼び出し忘れる。
・オブジェクトを使う前に間違えてdelete_vec()呼び出してしまう。
ということをやりかねないからです。
オブジェクトの外側の資源を管理する場合は、確保と破棄の管理を徹底しましょう。
という事でIntArrayを改良していきます。
NewIntArray.h
コンストラクタを定義するときにexplicitを追加しました、これは宣言するコンストラクタを明示的コンストラクタにするための関数指定子です。
そも、明示的コンストラクタとは暗黙的に行う型変換を抑止するコンストラクタです。
具体例としては
①IntAry a = 5;
(最初のクラスなら大丈夫、このクラスではNG)
②IntAry b(5);
(どちらでも大丈夫)
コンストラクタにexplicitの指定がない最初のクラスではどちらでも可能です。
ですが今回のクラスでは①の記述をするとコンパイルエラーを吐きます。
①の宣言方法は”配列を整数で初期化している”と誤解を生むかもしれません、その紛らわしい初期化をexplicitは抑止してくれます。
引数が単一のコンストラクタが”=”形式で起動されるのを防ぐにはexplicitを与えて定義しよう!
デストラクタ
このクラスで新しく追加された
この記述はデストラクタと呼ばれる特殊なメンバ関数です。
クラス名の前に”-”がついた名前のこの関数は
そのクラスのオブジェクトが破棄されそうになった時に自動的に呼び出されます。
なので、コンストラクタと対照的な存在ですね。
オブジェクト生成時に呼び出されるメンバ関数がコンストラクタ、それとは逆でオブジェクトが破棄される時に呼び出されるメンバ関数がデストラクタ
デストラクタはコンストラクタと同じで返却値をもっていません、ただ自動的に呼び出される性質上、コンストラクタと違い引数は受け取りません。
このクラスのデストラクタはオブジェクト外部のメモリ域解放を行います(ポインタvecが示す配列の領域)
これなら上記のfunc関数を利用した時にデストラクタがvecが指す領域を解放してくれます。
こんな感じです。
まとめとしては
・コンストラクタで確保したメモリ域などのオブジェクトの外側の資源の解放処理はデストラクタで実行しよう!!
・オブジェクトが外部の資源を管理するようなクラスは、資源を獲得時に初期化を行おう!
以上、メモリ領域とデストラクタの話でした。
ありがとうございます。
ぷないしん
変換関数や演算子関数について
こんにちは、ぷないしんです。
昨日のカウンタークラスを作成するにあたってサラっと変換関数や演算子関数などとのたまっていましたが、今日はもう少し掘り下げていきたいと思います。
最初に昨日のヘッダーファイルを張っておきます
NewCounter.h
まず変換関数とは
特定の型の値を生成して返すメンバ関数のことで、例えば"Type型への変換関数"とは下のような名前のメンバ関数と定義しています。
operator Type
(Type型への変換関数の関数名)
前回のカウンタクラスで使用していたのはunsigned型への変換関数で、関数名としては
"operator unsigned"になり、定義としては次のようにします。
operator unsigned() const { return cnt;}
関数名として"operator unsigned"は二つの単語を使い構成しています。
というのも関数名それ自体が返却値の型を表しているからで、返却値の型を指定することはできないし、引数を受け取る事もできません。
(変換関数は基本的にconstで実現する)
この変換関数"operator unsigned"を使えばCounterからunsignedへ型を変換する時、明示的にも暗黙的にもキャストできるというわけです。
ソースとしては次のような感じですね。
一番下は暗黙的キャストで上3つは明示的キャストです。
このどれもがcntのカウンタをunsigned型の整数値として取り出します。
実はこれ、組み込み型で行う型変換と同じ形ですね。
これは分かりやすいし使いやすい。
ちなみに、変換関数"operator unsigned"はCounterクラスのメンバ関数でもあるのでドット演算子を使って呼び出すことも可能です。
x = cnt.operator unsigned();
こんな感じですね
でも長くなるだけで特に便利でもないのでこの形で使うことは普通は無いでしょう。
まとめると、
オブジェクトをType型に変換する必要が多いなら、Type型へ変換するoperator Typeを変換関数として定義すると便利ということですね。
演算子関数
次に演算子関数についてですが、これも変換関数と同じで演算子関数の定義の仕方も簡単です。
例えば”💩演算子”は下の名前の関数として定義をします。
operator 💩
演算子関数"operator 💩"を定義するとクラス型オブジェクトにてその💩演算子使えるようになります。
Counterクラスで定義した三つの演算子(!、++、--)を見ていきましょう。
論理否定演算子!
初めに”!”演算子から見ていきます。
Counterクラスではカウンタの値が0かどうかを判定するという内容で定義します
関数名としては"operator !"で下の様に書きます。
bool operator! () const { return cnt == 0;}
("operator !"も二つの単語で構成されているがoperatorと"!"の間にスペースを入れても大丈夫!)
この関数はカウンタの値が0であればtrue、そうでなければfalseを返します。
ということはC++の標準である!演算子と同じ仕様になりましたね。
なのでこの様に利用できてしまいます。
if (!cnt) 動作
(cntが0であれば動作を実行)
これなら使い方をいちいち覚える必要ないので非常に便利ですね。
ポイントとしては演算子関数を定義するときは、その演算子の本来の仕様と出来る限り同じか類似した仕様になるように注意しましょう!
増分演算子、減分演算子
クラスに対してこれらを定義する際は前置きと後置きを区別しましょう。
下がよくある宣言形式です。
前置の時は引数を受け取らない形で、後置はint形の引数を受け取る形です。
また、各関数の返却値型Typeは任意なのですが、一応
前置は C&型
後置は C型
これで組み込み型で使う++と同じ仕様になります。
両者の違いとしては
・前置(++💩)は左辺値式(代入の左辺にも右辺にも置ける式)
・後置(💩++)は右辺値式(代入の右辺にしか置けない式)
Counterクラスの前置++演算子を定義するときはこんな感じです。
インクリをした後<自分>へ参照を返すために*thisによる返却をしています。
(クラスCの前置の増分/減分演算子はその呼び出しするときの式が左辺値式にするため、C&型の*thisを返すように定義する)
Counterクラスの後置++演算子を定義するときはこんな感じです。
インクリをする前の値を返す必要があるので、前置のときより手順が複雑です
①自分自身の*thisのコピーを作業用変数xに一時保存
②カウンタをインクリ
③関数から抜け出るとき、保存しておいたインクリ前のxを返却
と、こんな感じで後置きの場合はいったん*thisをコピーしておき、そのコピーを返却する流れですね。
(クラスCの後置の増分/減分演算子はインクリ/デクリ前の自身の値を返却するように定義しよう)
また前置、後置を比較したところ
++、--の演算子関数は前置より後置の方がコスト的に高くなるかもしれないので、前置でも後置でもどちらでもよい場合は前置の方を使ったほうがいいですね。
ところで、この前置も後置も
if (cnt < std::numeric_limits<unsigned>::max()) cnt++;
のところは同じですね。
同じソースならこの部分を呼び出せば重複を解消できます。
だから上記のソースでは
と表すことが出来ますね。
最後に、これら定義した演算子関数をクラスのオブジェクトで適用することは、メンバ関数の演算子関数を呼び出していることと同義です、つまり下の様に解釈できます。
++x → x.operator++()
前置(引数無し)
x++ → x.operator++(0)
後置(ダミーの引数が渡される)
規定により、後置きの場合はダミーの値として0が渡されます。
また一応こいつらも変換関数と同じで下の様に表せますが読みにくくなるだけで使いませんね。
x.operator++()
(前置を呼び出す++xと同じ)
x.operator++(0)
(後置を呼び出すx++と同じ)
以上、変換関数と演算子関数でした。
これらを組み合わせて昨日のカウンタクラスを書いているのでぜひもう一度見てみてください。
ありがとうございました。
ぷないしん。
C++実践編(カウンタークラスを作成する)
こんにちは、ぷないしんです。
昨日の関数の渡し方の詳しい話はまた今度にして今日は、C++のクラスを作成していきたいと思います。
今回作成するのはズバリ
【カウンタークラス】
内容としては
・0~9の1桁の整数値を数え上げる
・初期化、生成すると同時にカウンタを0にする。
・カウントアップ(値をインクリ)
・カウントダウン(値をデクリ)
・カウンタの値を調べる
今回のクラスは小規模なのですべてのメンバ関数をインライン関数にし、ヘッダだけで実現してみます。
ということでさっそくコードを記述します。
【counter.h】
カウンタを格納しているのが非公開のデータメンバcnt
型はunsigned
すなわち、カウンタとして表現できる数値はunsigned型で使える範囲と同じです。
ということは加減が0で上限がnumeric_limits<unsigned>::max()になります。
numeric_limits<unsigned>::max()
について
まずunsigned型についてのおさらいですが、unsigned型とは
2バイトまた4バイトの符号なし整数の値を記憶できる型です。
次にnumeric_limitsですがこれはまず最初に<limits>ヘッダーをインクルードした時に使える標準ライブラリで::maxを使用すればその直前に指定した型の最大値を取得できます。
なので今回はunsigned型の最大値を取得するクラスととりあえず理解しておきます。
次に各メンバ関数の解説をしておくと
・コンストラクタ カウンタの値を0にしています。
・increment関数 if文でカウンタの値が最大値より小さい時、カウンタの値をインクリメントします。
・decrement関数 if文でカウンタの値が0より大きい時、カウンタの値をデクリメントします。
・value関数 カウンタの値を返却しています。
それではCounterクラスを実際に利用してみましょう。
まず最初に、CounterのオブジェクトとしてXを生成、カウンタの値を表示します。
オブジェクト生成時にカウンタが0になっています。
生成後は、キーボードからint形の変数noに入れた回数だけ、メンバ関数incrementによるカウントアップを行いながら値を表示します。
それが終わると、もう一度キーボードからint形変数noに入れた数値の回数だけカウントダウンと値の表示を行います。
もちろんこのまま利用することも出来るのですが、改良点がいくつかあります。
ユーザー定義型であるクラスCounterを使う時は、値を調べる、カウントアップ/ダウン(インクリ/デクリ)を行うためにvalue関数、increment関数、decrement関数を呼び出す必要があります。
int形やlong形の組み込み型と比較すると
・タイプ数が増える→タイプミスしやすくなる
・プログラムが冗長になる→読みにくくなる
といったデメリットがあります。
クラス型オブジェクトに対してインクリやデクリを行う演算子を使えれば、int形やlong形と同じ感じで利用できるはず!
という事で改良していきましょう!
NewCounter.h
では早速利用してみましょう。
countertest.cpp
xには後置き、yには前置きの演算子を使いました。
ちゃんと使い分けられてますね。
変換関数と演算子関数を定義したので組み込み型と同じ感じでCounterクラスを使えるようなりました。
最初のプログラムと比べてみると使いやすい上に利用する時に簡単ですし読みやすくなりましたね。
変換関数や演算子の定義の方法はまた次回として今日はこのあたりにしておきます。
ありがとうございました。
ぷないしん