仮想関数???

こんにちはぷないしんです。

 

今日は仮想関数

バーチャルな関数・・・のお話です。

 

前置きとして

せっかくなので以前使った”会員”のクラスに【シルバーメンバー】クラスを追加していこうと思います。

 

一般会員クラスとの差異は

・シニア特典でレベルによって内容の違うint形のデータメンバsenior_level

・上記のメンバの度合いを取得、設定するゲッターとセッターget_senior_level、set_care_level

 

それでは早速ヘッダとソースです。

 

silver.h

#ifndef ___SilverMember
#define ___SilverMember

#include <string>
#include "Member.h"

//シルバー会員クラス
class SilverMember : public Member{
    int senior_level;

public:
    //コンストラクタ
    SilverMember(const std::string& nameint nodouble wint level = 0);
    //senior_levelのゲッター
    int get_senior_level() const { return senior_level; }
    //senior_levelのセッター
    void set_senior_level (int level){
        senior_level = (level >= 1 && level <= 5) ? level :0;
    }

    void print() const;
};

#endif

 SilveMember.cpp

#include <string>
#include <iostream>
#include "silver.h"

using namespace std;


//コンストラクタ
SilverMember::SilverMember(const stringnameint nodouble wint level)
                            :Member(namenow)
{
    set_senior_level(level);
}

void SilverMember::print() const
{
    cout << "No," << no() << ":" << name() << "(" << get_weight() << "kg)"
         << "Level =" << senior_level << "\n";
}

 セッターの動きによって、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クラスと同じように、シルバークラスは一般クラスの派生なので構成としてはこんな感じですね。

f:id:punainen:20210407140247p:plain

クラス階層図

Memberからpublic派生しているシルバー会員クラスは”一般クラスの一種”となります。

(is-Aの関係が成立するということです)

メンバ関数のいんぺい

基底クラスのMemberにメンバ関数printが存在し、派生クラスであるVIPMember、SilverMemberにもこの関数があります。

この様に、基底クラスのメンバ関数と同じ名前のメンバ関数が派生先のクラスで定義されると、派生クラスのメンバ関数は基底クラスのメンバ関数隠します。

そのことを次のソースで確認してみましょう。

内容はVIPMember型、SilverMember型の各オブジェクトに対して、メンバ関数printを呼び出すだけの簡単なものです。

 

MemberPrint.cpp

#include <iostream>
#include "VIPMember0.h"
#include "silver.h"

using namespace std;


int main(){
    VIPMember0 Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    SilverMember Nakai("Nakai Masamune"259.73);

    Kusanagi.print();
    Nakai.print();
}

f:id:punainen:20210407151026p:plain

実行結果

kusanagiに対してはVIPMember::printが呼び出され、NakaiはSilverMember::printが呼び出されています。

この例では関数printの仮引数の型と個数は同一です。とまれ、関数名さえ同じなら仮引数の型や戸数が違っていても隠ぺいは行われます。

基底クラスのメンバ関数と同一名の派生クラスのメンバ関数は、仮引数の型や数が違っていても、基底クラスのメンバ関数を隠ぺいする。

 

VIPMemberとSilverMemberは、is-Aの関係であるpublic派生でMemberクラスから派生しています。

なので、Memberから継承したメンバ関数printは、VIPMemberとSilverMemberでも公開メンバとして存在します。

 

継承したメンバ関数は、名前が隠されているだけで会って、存在が消える訳ではありません。

実際に隠ぺいされている基底クラスのメンバ関数は、クラスの外部から呼び出せます。

 

 

それを確認してみましょう。

Memberprint2.cpp

#include <iostream>
#include "VIPMember.h"
#include "silver.h"

using namespace std;


int main(){
    VIPMember Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    SilverMember Nakai("Nakai Masamune"259.73);

    Kusanagi.Member::print();
    Nakai.Member::print();
}

 

f:id:punainen:20210407155903p:plain

実行結果

有効範囲解決演算子「::」を使い、Member::print()により、基底クラスMemberのメンバ関数printを呼び出しています。

 

基底クラスから継承したメンバと同名のメンバが派生クラス内にある時、”基底クラス名::メンバ名”で基底クラスから継承した(隠ぺいされている)メンバにアクセスできる。

静的な型

次は下のコードです。

関数put_memberは、引数mに受け取った会員の情報を表示します。

その時、体重が55kg以上ならば先頭に@を付けます。

Memberprintref.cpp

#include <iostream>
#include "Member.h"
#include "VIPMember.h"
#include "silver.h"

using namespace std;

void put_member(const Member& m)
{
    cout << (m.get_weight() >= 55 ? "@" : " ");
    m.print();
}

int main(){
    Member Kimura("Kimura Takuma"148);
    VIPMember Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    SilverMember Nakai("Nakai Masamune"259.73);

    put_member(Kimura);
    put_member(Kusanagi);
    put_member(Nakai);
}

 

 

f:id:punainen:20210407161030p:plain

実行結果

 関数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

#include <iostream>
#include <typeinfo>
#include "Member.h"
#include "VIPMember.h"
#include "silver.h"

using namespace std;

int main(){
    VIPMember Kusanagi("Kusanagi Takeshi"358.5,"Ippon satisfaction bar");
    
    Memberptr = &Kusanagi;
    Memberref =  Kusanagi;
    
    cout << typeid(*ptr).name() << "\n";
    cout << typeid(ref).name() << "\n";

}

 

f:id:punainen:20210407162803p:plain

実行結果

 はい、ということでどちらもVIPMember型ではなく、Member型ですね。

 

仮想関数

ということで本題の仮想関数の話に入っていきます。

先ほどのままではさまざまな制約を受けます

 この問題を解決する手段の一つして仮想関数を使います

では一般会員クラスのヘッダ部分を改良していきましょう。

 

Member.h

//パンピークラス

#ifndef ___Member
#define ___Member

#include <string>

class Member{
    std::string full_name;
    int     number;
    double  weight;

public:
    //コンストラクタ
    Member(const std::string& nameint nodouble w);
    //名前
    std::string name()const{
        return full_name;
    }
    //番号
    int no() const {
        return number;
    }
    //体重返却
    double get_weight() const {
        return weight;
    }
    //体重設定
    void set_weight(double w){
        weight = (w > 0) ? w : 0;
    }
    //情報表示
    virtual void print() const;
};
#endif

 改良するといいながら前のヘッダーと違う所はprintの前にvirtualを追加したことだけです。

 

これでMemberPrintrfをコンパイル、実行してみましょう。

f:id:punainen:20210407165554p:plain

実行結果

 

軽傷で済んだので継承していく

こんにちは、ぷないしんです。

 

昨日の記事を継承し、続きの話に入っていこうと思います。

 

m0can.hatenablog.com

 

昨日の記事の最後に

プログラム上のあちらこちらに”似て非なる”クラスが大量に存在し、開発効率、保守性が下がります。

と書きましたが、これらの問題を解決する手段として”派生”を使いましょう。

派生とは、既存クラスの資産を継承するクラスを作り出すことです。

(派生の時は、データメンバ、メンバ関数などの資産を単純に継承するだけではなく、追加したり上書きしたりできる)

 

定義の仕方はこんな感じで

class base {
    int a;
    int b;
public:
    void func(){ /*省略*/}
};

↓派生

class Derived : base {
    int x;
public:
    void method() {/*省略*/}
}

クラスBaseと、それを継承するDerivedを定義しています。

クラスDerivedを定義するときにクラス名のDerivedの後ろに【:】、そのあとに継承元のbase

これでbaseから派生したクラスDerivedが出来ました。

 

呼び方としては

派生元

・基底クラス、上位クラス、親クラス、スーパークラス

派生先

・派生クラス、下位クラス、、子クラス、サブクラス

等があるみたいで、C++では基底クラス、派生クラスと呼ぶことが多いみたいですね。

 

さて、この二つのクラスが持つ資産を概略を表してみます。

f:id:punainen:20210326163158p:plain

この図のように、

・基底クラス base

【a】と【b】の二つの変数があり、関数は≪func≫の一つです。

・派生クラスDerived

定義を行っているときは【x】と≪method≫だけが宣言、定義されています。

ですが、baseを継承しているのでそれら二つを合わせ変数は3個、関数は2個になります。

 

派生クラスは基底クラスの資産継承すると同時に、それを部分として含むクラスの事である。

(ちなみにコンパイラによって自動的に定義されるデフォルトコンストラクタとデフォルトデストラクタ、代入演算子なども、各クラスの資源として含まれる、またフレンド関係は継承されることはない)

クラス階層図

派生クラスは基底クラスの”子供”のようなもので、その親子関係を表してみます。

f:id:punainen:20210326164435p:plain

クラス階層図

派生クラスDerivedの定義":base"の部分は

「私の親はクラスbaseです」

という宣言です。

つまり、親クラス"base"の知らないところで子供が生まれています。

 

子供は親を知っているのですが、親は子供を知りません。

親として、子供がいるのか、いないのか、もしいるのなら何人いるのかといった情報を親は持ちえません。

基底クラスの方で『○○クラスを私の子供にします』といった宣言はできないのです。

なので、矢印の向きは派生クラス→基底クラスになります。

 

ところで、派生は一度だけではなく、何度でも出来ます。

class A{
    //省略
};

class B : A{
    //省略
};

class C : B{
    //省略
};

class D : B{
    //省略
};

 

f:id:punainen:20210326170038p:plain

クラス階層図

クラス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派生】になる)

 

三つの派生形態の例としてですが

#ifndef ___Super
#define ___Super

class Super{
private:
    int pri;
protected:
    int pro;
public:
    int pub;
};

#endif
private派生

クラスSuperからprivate派生を行う例です

Private.cpp

#include "Super.h"

class Sub : private Super {
    void f(){
//        pri = 1;
        pro = 2;
        pub = 3;
    }
};

int main()
{
    Sub x;

//    x.pri = 5;
//    x.pro = 6;
//    x.pub = 7;
}

 (コンパイルエラーになるところはコメントアウト済み)

実行しても何も起きないプログラムです。

private派生を行うと、派生クラスSubにとってクラスSuperは非公開の基底クラスになります。

アクセス性は以下の通りです。

f:id:punainen:20210326180530p:plain

private派生時のアクセス性

派生クラスの内部からは基底クラスの【privateメンバ】はアクセス出来ません。

また、基底クラスの【protectedメンバ】と【publicメンバ】は派生クラス内では【privateメンバ】として扱われ、派生クラスの利用者に公開されません。

 

コメントアウトした部分がコンパイルエラーになり理由ですが

①派生クラスsubの内部において、基底クラスSuperの【privateメンバ】priのアクセスは出来ない。

②基底クラスsuperの全メンバは、派生クラスsubの利用者に対して非公開である。

限定公開(protected)メンバは外部に対して存在を隠すが、直接派生クラスに対しては存在を隠さない

という事です。

protected派

クラスSuperからprotected派生を行う例です。

protected.cpp

#include "Super.h"

class Sub : protected Super {
    void f(){
//        pri = 1;
        pro = 2;
        pub = 3;
    }
};

int main()
{
    Sub x;

//    x.pri = 5;
//    x.pro = 6;
//    x.pub = 7;
}

 (コンパイルエラーになるところはコメントアウト済み)

アクセス性は以下の通りです。

f:id:punainen:20210326181859p:plain

protected派生時のアクセス性

派生クラスのメンバ関数から基底クラスの【privateメンバ】をアクセス出来ないのはprivate派生と同じです。

ただ、基底クラスの【protectedメンバ】と【publicメンバ】が派生クラス内で【protectedメンバ】として扱われる点がprivate派生と違う所です。

(これらのメンバは派生クラスの利用者に対しては非公開)

protectedメンバはSubから派生したクラスの内部ではアクセス出来ますが、その外部からは不可能です。

public派生

クラスSuperからpublic派生を行う例です。

public.cpp

#include "Super.h"

class Sub : public Super {
    void f(){
//        pri = 1;
        pro = 2;
        pub = 3;
    }
};

int main()
{
    Sub x;

//    x.pri = 5;
//    x.pro = 6;
      x.pub = 7;
}

 (コンパイルエラーになるところはコメントアウト済)

アクセス性はこちらです

f:id:punainen:20210326185255p:plain

public派生時のアクセス性

他の派生と同様に、派生クラスのメンバ関数から基底クラスの【privateメンバ】をアクセスするのは出来ません。

基底クラスの【protectedメンバ】は派生クラス中でも【protectedメンバ】としての扱いで、基底クラスの【publicメンバ】は派生クラスでも【publicメンバ】として扱われるので、派生クラスの利用者に公開されます。

つまり、基底クラスのprivate以外のメンバ(protected、public)のアクセス性が派生クラスでも維持されています。

派生のカタチ

三種類の派生(private、protected、public)に共通する原理や規則性を纏めると

どの派生でも基底クラスの非公開(private)メンバは派生クラスからはアクセスできない

”〇〇派生”を行うと、基底クラスの公開(public)メンバが派生クラスの”○○部”に所属するようになる

限定公開(protected)メンバは外部には公開されないが自分の子(直接派生するクラス)には公開される

 

こんなところでしょうか。

以上がクラスの継承の話でした。

協力者のキムラ、クサナギ(敬称略)に感謝をしつつ本日はこのあたりにしておきます。

 

ありがとうございました。

 

ぷないしん

クラスでケガしたけど軽傷ですんだ

こんにちはぷないしんです。

 

タイトルに深い意味はありません。

 

C++の大きな特徴であるクラス、今日はその継承について今一度振り返ってみます。

 

という事でよく説明や試験問題で使われる”会員”クラスを作っていき、例によってそれらを解説、改良していきながらやっていきます。

 

ではまずヘッダーから

Member.h

//パンピークラス

#ifndef ___Member
#define ___Member

class Member{
    std::string full_name;
    int     number;
    double  weight;

public:
    //コンストラクタ
    Member(const std::string& nameint nodouble w);
    //名前
    std::string name()const{
        return full_name;
    }
    //番号
    int no() const {
        return number;
    }
    //体重返却
    double get_weight() const {
        return weight;
    }
    //体重設定
    void set_weight(double w){
        weight = (W > 0) ? w : 0;
    }
    //情報表示
    void print() const;
#endif

 

 このクラスのデータメンバは下の三つです・

・名前”full_name

・会員番号"number"

・体重"weight"

 

コンストラクタは【name】【no】【w】に受け取った三つの値でそれぞれのメンバを初期化します。

コンストラクタの他、5つのメンバ関数が定義されています。

それぞれ

 

名前を取得する【name】 会員番号を取得する【no】体重を取得する【get_weight】、また設定を行う【set_weight】、会員情報を表示する【print

ですね。

 

つづいてこちらを

Member.cpp

#include <iostream>
#include "Member.h"

using namespace std;

//コンストラクタ
Member::Member(const stringnameint nodouble w)
                : full_name(name),number(no){
                    set_weight(w);
                }

//情報表示
void Member::print() const{
    cout << "No" << number << ":" << full_name << "(" << weight << "kg)\n";
}

 体重(メンバのweight)を設定するメンバ関数のset_weightはweightがマイナスにならないように調整(もしwにマイナスを受けとった場合、weightに0を入れる)

コンストラクタの

set_weight(w);

部分はset_weightに体重の設定を任せています。

 

また、氏名のfull_nameと番号のnumberの初期化はコンストラクタの初期化子で行っています。↓

 full_name(name),number(no)

 

では、Member型のオブジェクトを一つ作り、各ゲッターを呼び出すだけの簡単なものを書いていきます。

 Membertest.cpp

#include <iostream>
#include "Member.h"

using namespace std;

int main(){
    Member kimura("Kimura Takuma"148);

    double weight = kimura.get_weight();    //キムラの体重
    kimura.set_weight(weight - 1.5);        //キムラが減量(-1.5kg)

    cout << "No:" << kimura.no() << " " <<kimura.name() << " is " << kimura.get_weight() << "kg\n";
}

 

f:id:punainen:20210325143002p:plain

実行結果

ここではパンピーのキムラ君を表しているのがMember型のオブジェクトkimuraです。

キムラ君の会員番号は1で、体重は48kg、ところが、キムラ君は1.5kgのダイエットを成功したのでget_weightとset_weightを使い、体重を46.5kgに更新してます。

VIP会員クラス

さて、このクラブは特典を付けた【VIP会員】制度が出来ました。

会員ごとに内容が違う特典をstring型のメンバで表す【VIPクラス】を作ります。

パンピークラスMemberを元に、VIPクラスを作るのは簡単ですね。

ヘッダーとソースの各ファイルをコピーし、部分的な追加と変更をするだけです。

この方法で作った”試作版”のVIPクラスのヘッダーとソース部を作ります。

 

VIPMember0.h

//VIPクラス

#ifndef ___VIPMember0
#define ___VIPMember0

#include <string>

class VIPMember0{
    std::string full_name;
    int     number;
    double  weight;
    std::string special;

public:
    //コンストラクタ
    VIPMember0(const std::string& nameint nodouble wconst std::string& spe);
    //名前
    std::string name()const{
        return full_name;
    }
    //番号
    int no() const {
        return number;
    }
    //体重返却
    double get_weight() const {
        return weight;
    }
    //体重設定
    void set_weight(double w){
        weight = (w > 0) ? w : 0;
    }
    //情報表示
    void print() const;
    //特典
    std::string get_special() const {return special;}
    //特典設定
    void set_special(const std::string& spe){
        special = (spe != "") ? spe : "Unregistered";
    }
};

#endif

 特典のstring型のデータメンバspecialを作ると同時に、特典の取得と設定をするget_special、set_specialを追加しました

specialのセッターset_specialは、もし仮引数に空文字を受け取ったとき、文字列"Unregistered"をspecialに代入するようにしています)

 

それとMember.cppの方も手を入れていきましょう。

 

VIPMember0.cpp

#include <iostream>
#include <string>
#include "VIPMember0.h"

using namespace std;

//コンストラクタ
VIPMember0::VIPMember0(const stringnameint nodouble wconst stringspe)
                : full_name(name),number(no){
                    set_weight(w);
                    set_special(spe);
                }

//情報表示
void VIPMember0::print() const{
    cout << "No" << number << ":" << full_name << "(" << weight << "kg)\n"
    << "Special =" << special << "\n";
}
 

 コンストラクタの仕様も変えています

特典用の文字を受け取る仮引数【spe】が増えると同時に、その値の設定を処理するところを追加しています。

(メンバ【special】の値設定はメンバ関数の【set_special】に任せているので、仮引数【spe】に空文字を受け取った時、【special】には"Unregistered"が表示される)

 

そして、これらを利用する実装部です。

 

VIPMember0test.cpp

 

#include <iostream>
#include "VIPMember0.h"

using namespace std;

int main(){
    VIPMember0 Kusanagi("Kusanagi Takeshi"360"Ippon satisfaction");

    double weight = Kusanagi.get_weight();    //クサナギの体重
    Kusanagi.set_weight(weight - 1.5);        //クサナギが減量(-1.5kg)

    cout << "No:" << Kusanagi.no() << " " <<Kusanagi.name() << " is " << Kusanagi.get_weight() << "kg\n"
    << "Special is " << Kusanagi.get_special() <<"\n";
    }

 

f:id:punainen:20210326154346p:plain

実行結果

はい

VIP会員のクサナギタケシ君を表しているのが、クラスVIPMember0型のオブジェクトであるKusanagiです。

会員番号は3番で、体重は60kgからダイエットに成功し58.5kg、特典は一本サティスファクションです。

 

さて、この一般会員クラスとVIPクラスを両方を使うプログラムを書いてみます。

Wtest.cpp

#include <iostream>
#include "Member.h"
#include "VIPMember0.h"

using namespace std;

//パンピーダイエット(体重をdw減らす)
void diet(Member& mdouble dw){
    double weight = m.get_weight();
    if (weight > dwm.set_weight(weight - dw);
}
//VIPダイエット(体重をdw減らす)
void diet(VIPMember0& mdouble dw){
    double weight = m.get_weight();
    if (weight > dwm.set_weight(weight - dw);
}
int main(){
    Member Kimura("Kimura Takuma"148);
    VIPMember0 Kusanagi("Kusanagi Takeshi"360"Ippon satisfaction");

    diet(Kimura,0.3);       //キムラが0.3kgダイエット
     cout << "No:" << Kimura.no() << " " <<Kimura.name() << " is " << Kimura.get_weight() << "kg\n";

    diet(Kusanagi1.2);    //クサナギが1.2kgダイエット
    cout << "No:" << Kusanagi.no() << " " <<Kusanagi.name() << " is " << Kusanagi.get_weight() << "kg\n"
    << "Special is " << Kusanagi.get_special() <<"\n";
    }

 二つのdiet関数は会員のダイエット処理を行う関数です。

同じ処理をしている関数が各クラス毎に作られ、多重定義をしています。

なぜかというと、関数の処理を行う対象の変数の”型”が違うからです。

(この関数の違いは、引数"m"の型だけで、その他は全部同じ)

 

一般クラスとVIPクラスの見た目は殆ど同じなのですが、コンパイラ視点で見ると、何の関係もないクラスです。

同一、または似たようなクラスを仕様の違うクラスとして書いてしまうと、

プログラム上のあちらこちらに”似て非なる”クラスが大量に存在し、開発効率、保守性が下がります。

 

そしてこの話を継承して明日へ…

 

ぷないしん。

 

 

 

 

メモリの記憶域の話~デストラクタを添えて~


こんにちはぷないしんです。

 

今日はクラスの外側のメモリ管理の話をします。

 あとちょっぴりデストラクタの話も

 

ということで、まずは配列クラスを使ってクラスの外側の資源の管理方法です。

整数型の配列を実現するクラスを記述します。

 

IntArray.h

//整数型配列クラス

#ifndef ___Class_IntArray
#define ___Class_IntArray

class IntArray{
    int nelem;
    int* vec;

public:
    //コンストラクタ
    IntArray(int size) : nelem(size){
        vec = new int[nelem];
    } 

    //要素の数を返却
    int size() const {
        return nelem;
        }

    //演算子
    int& operator(int i){
        return vec[i];
        }
};

#endif

 

 

このクラスのデータメンバーは要素の数であるnelemと、配列の先頭の要素を指すポインタであるvecの二つです。

要素の数であるnelemの値は、メンバ関数のsizeで調べられます。

 

では、コンストラクタとこの関数についてお話します。

 

コンストラクタ

    IntArray(int size) : nelem(size){
        vec = new int[nelem];
    } 

コンストラクタ本体では、メモリの領域を確保し、配列の本体を動的に作ります。

生成する配列の要素の数は、仮引数であるsizeで受け取った値です。

 

オブジェクトが下の様に定義されたときのコンストラクタの動作ですが

 

intArray x(5);

 

まず、nelem(size)の働きで、データメンバーnelemが5で初期化されます。

次に実行されるコンストラクタではnew演算子で確保したnelem個分、記憶域の先頭要素へのポインタをvecに代入します。

 

f:id:punainen:20210319155904p:plain

 実際に確保しているメモリ域は生成したオブジェクト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を動かしてみましょう。

 

//整数型配列クラスを使う

#include <iostream>
#include "IntArray.h"

using namespace std;

int main()
{
    int n;

    cout << "Select the number of elements:";
    cin >> n;

    IntArray x(n);

    for (int i = 0; i < x.size(); i++)
    x[i] = i;

    for (int i = 0; i < x.size(); i++)
    cout << "x[" << i << "] =" << x[i] << "\n";
}

f:id:punainen:20210319161959p:plain

実行結果(赤枠はキーボードから入力)

n個分の配列を作り、すべての要素に添字と同じ値を代入ついでに表示しています。

 

ではここで下の関数についてちょっと考えてみましょう。

void func(){
    IntArray x(5);
    ~省略~
}

 IntArray型のオブジェクトxは関数内で生成されているので自動的にメモリ域の期間が与えられます。

それがいつまで有効なのかというと、

 

①二つのメンバ"nelem"と"vec"を持つオブジェクト"x"を生成

f:id:punainen:20210319164447j:plain

②コンストラクタでxを初期化

 (nelemが5で初期化、そのあと5個の配列用のメモリ域がnew演算子で確保、先頭要素へのポインタがvecに代入)

f:id:punainen:20210319164806j:plain
③関数funcの実行終了、自動的にメモリ域の期間を持っているオブジェクト"xこの期間が終了し、破棄される。

 だが、new演算子で確保された配列本体用のメモリ域は破棄されることなくメモリ域に残った状態になる。

f:id:punainen:20210319165641j:plain

 

コンストラクタで確保した動的な記憶期間をもつメモリ域は、オブジェクトを破棄したとしても、自動的に解放されることはありません。

 

解放されていない配列はどこからも指定されていない、無駄な領域としてメモリ上に取り残されます。

なのでこの関数を何度も読みだすと、その都度無駄な領域が確保されていき、実際に使えるメモリ域が減っていきます。

 

我々がこの期間をコントロールできるオブジェクトは好きな時に生成し、好きな時に破棄できるのですが、その作業をちゃんと行うのが我々の使命です。

 

なのでこのように確保したオブジェクトはちゃんと指示して解放しないといけないです。

 

そのために、配列本体の領域を破棄する【メンバ関数】を定義しましょう。

void IntArray::delete_vec(){
    delete vec;
}

 こんな感じです。

この関数を呼び出すように上の関数を書き換えます。

void func(){
    IntArray x(5);
    //~省略~
    x.delete_vec();
}

 これでXが破棄される前に配列本体を破棄できます。

 

しかしこの方法は微妙ですね。

delete_vec()を呼び出し忘れる。

・オブジェクトを使う前に間違えてdelete_vec()呼び出してしまう。

ということをやりかねないからです。

 

オブジェクトの外側の資源を管理する場合は、確保と破棄の管理を徹底しましょう。

 

という事でIntArrayを改良していきます。

 

NewIntArray.h

//整数型配列クラス

#ifndef ___Class_IntArray
#define ___Class_IntArray

class IntArray{
    int nelem;
    int* vec;

public:
    //コンストラクタ
    explicit IntArray(int size) : nelem(size){
        vec = new int[nelem];
    } 

    //デストラクタ
    -IntArray() {delete vec;}

    //要素の数を返却
    int size() const {return nelem;}

    //演算子
    int& operator(int i){return vec[i];}
};

#endif

 

コンストラクタを定義するときにexplicitを追加しました、これは宣言するコンストラクタを明示的コンストラクタにするための関数指定子です。

そも、明示的コンストラクタとは暗黙的に行う型変換を抑止するコンストラクタです。

具体例としては

IntAry a = 5;

 (最初のクラスなら大丈夫、このクラスではNG)

IntAry b(5);

 (どちらでも大丈夫)

コンストラクタにexplicitの指定がない最初のクラスではどちらでも可能です。

ですが今回のクラスでは①の記述をするとコンパイルエラーを吐きます。

①の宣言方法は”配列を整数で初期化している”と誤解を生むかもしれません、その紛らわしい初期化をexplicitは抑止してくれます。

 

引数が単一のコンストラクタが”=”形式で起動されるのを防ぐにはexplicitを与えて定義しよう!

 

デストラク

このクラスで新しく追加された

    -IntArray() {delete[] vec;}

この記述はデストラクタと呼ばれる特殊なメンバ関数です。

 

クラス名の前に”-”がついた名前のこの関数は

そのクラスのオブジェクトが破棄されそうになった時に自動的に呼び出されます

 

なので、コンストラクタと対照的な存在ですね。

 

オブジェクト生成時に呼び出されるメンバ関数がコンストラクタ、それとは逆でオブジェクトが破棄される時に呼び出されるメンバ関数がデストラクタ

 

デストラクタはコンストラクタと同じで返却値をもっていません、ただ自動的に呼び出される性質上、コンストラクタと違い引数は受け取りません。

 

このクラスのデストラクタはオブジェクト外部のメモリ域解放を行います(ポインタvecが示す配列の領域)

 

これなら上記のfunc関数を利用した時にデストラクタがvecが指す領域を解放してくれます。

f:id:punainen:20210319180604j:plain

f:id:punainen:20210319180713j:plain

デストラクタ君の仕事

 

こんな感じです。

 

まとめとしては

 

・コンストラクタで確保したメモリ域などのオブジェクトの外側の資源の解放処理はデストラクタで実行しよう!!

 

・オブジェクトが外部の資源を管理するようなクラスは、資源を獲得時に初期化を行おう

 

以上、メモリ領域とデストラクタの話でした。

 

ありがとうございます。

 

ぷないしん

 

 

変換関数や演算子関数について

こんにちは、ぷないしんです。

 

昨日のカウンタークラスを作成するにあたってサラっと変換関数や演算子関数などとのたまっていましたが、今日はもう少し掘り下げていきたいと思います。

 

最初に昨日のヘッダーファイルを張っておきます

 

NewCounter.h

//カウンタクラス

#ifndef ___Class_Counter
#define ___Class_Counter

#include <limits>

class Counter{
    unsigned cnt;   //カウンタ

public:
    //コンストラクタ カウンタを0に
    Counter() : cnt(0){}

    //unsigned型へ変換する
    operator unsigned() const {
        return cnt;
        }

    bool operator!() const {
         return cnt == 0;
         }

    //前置増分演算子++
    Counter& operator++(){
         if (cnt < std::numeric_limits<unsigned>::max()) cnt++;
         return *this;
    }

    //後置増分演算子++
    Counter operator++(int){
        Counter x = *this;
        ++(*this);
        return x;
    }

    //前置減分演算子--
    Counter& operator--(){
        if (cnt > 0) cnt--;
        return *this;
    }

    //後置減分演算子--
    Counter operator--(int){
        Counter x = *this;
        --(*this);
        return x;
    }
};

#endif

 

 

まず変換関数とは

特定の型の値を生成して返すメンバ関数のことで、例えば"Type型への変換関数"とは下のような名前のメンバ関数と定義しています。

 

operator Type

(Type型への変換関数の関数名)

 

前回のカウンタクラスで使用していたのはunsigned型への変換関数で、関数名としては

"operator unsigned"になり、定義としては次のようにします。

 

operator unsigned() const { return cnt;}

 

関数名として"operator unsigned"は二つの単語を使い構成しています。

というのも関数名それ自体が返却値の型を表しているからで、返却値の型を指定することはできないし、引数を受け取る事もできません。

(変換関数は基本的にconstで実現する)

 

この変換関数"operator unsigned"を使えばCounterからunsignedへ型を変換する時、明示的にも暗黙的にもキャストできるというわけです。

ソースとしては次のような感じですね。

 

unsigned x;
Counter cnt;
~省略~
x = unsigned(cnt);
x = (unsigned)cnt;
x = static_cast<unsigned>(cnt);
x = cnt;

 

一番下は暗黙的キャストで上3つは明示的キャストです。

このどれもがcntのカウンタをunsigned型の整数値として取り出します。

 

実はこれ、組み込み型で行う型変換と同じ形ですね。

 

int i;
double a;
~省略~
a = double(i);
a = (double)i;
a = static_cast<double>(i);
a = i;

 

これは分かりやすいし使いやすい。

 

ちなみに、変換関数"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であれば動作を実行)

 

これなら使い方をいちいち覚える必要ないので非常に便利ですね。

 

ポイントとしては演算子関数を定義するときは、その演算子の本来の仕様と出来る限り同じか類似した仕様になるように注意しましょう!

 

増分演算子、減分演算子

次に増分演算子(++)と減分演算子(--)ですが

クラスに対してこれらを定義する際は前置き後置きを区別しましょう。

下がよくある宣言形式です。

 

class C{
    ~省略~
public:
    Type operator++();
    Type operator++(int);
}

 

前置の時は引数を受け取らない形で、後置はint形の引数を受け取る形です。

また、各関数の返却値型Typeは任意なのですが、一応

前置は C&型

後置は C型

 

これで組み込み型で使う++と同じ仕様になります。

 

両者の違いとしては

・前置(++💩)は左辺値式(代入の左辺にも右辺にも置ける式)

・後置(💩++)は右辺値式(代入の右辺にしか置けない式)

 

Counterクラスの前置++演算子を定義するときはこんな感じです。

Counter& operator++(){
    if (cnt < std::numeric_limits<unsigned>::max()) cnt++;
    return *this;
}

 

インクリをした<自分>へ参照を返すために*thisによる返却をしています。

 

(クラスCの前置の増分/減分演算子はその呼び出しするときの式が左辺値式にするため、C&型の*thisを返すように定義する)

 

Counterクラスの後置++演算子を定義するときはこんな感じです。

Counter operator++(int){
    Counter x = *this;
    if (cnt < std::numeric_limits<unsigned>::max()) cnt++;
    return x;
}

 インクリをするの値を返す必要があるので、前置のときより手順が複雑です

 

①自分自身の*thisのコピーを作業用変数xに一時保存

②カウンタをインクリ

③関数から抜け出るとき、保存しておいたインクリ前のxを返却

 

と、こんな感じで後置きの場合はいったん*thisをコピーしておき、そのコピーを返却する流れですね。

 

(クラスCの後置の増分/減分演算子はインクリ/デクリ前の自身の値を返却するように定義しよう

 

また前置、後置を比較したところ

++、--の演算子関数は前置より後置の方がコスト的に高くなるかもしれないので、前置でも後置でもどちらでもよい場合は前置の方を使ったほうがいいですね。

 

ところで、この前置も後置も

if (cnt < std::numeric_limits<unsigned>::max()) cnt++;

のところは同じですね。

 

同じソースならこの部分を呼び出せば重複を解消できます。

だから上記のソースでは

    Counter operator++(int){
        Counter x = *this;
        ++(*this);
        return x;
    }

 と表すことが出来ますね。

 

最後に、これら定義した演算子関数をクラスのオブジェクトで適用することは、メンバ関数の演算子関数を呼び出していることと同義です、つまり下の様に解釈できます。

 

++x → x.operator++()

前置(引数無し)

 

x++ → x.operator++(0)

後置(ダミーの引数が渡される)

 

規定により、後置きの場合はダミーの値として0が渡されます。

 

また一応こいつらも変換関数と同じで下の様に表せますが読みにくくなるだけで使いませんね。

 

x.operator++()

(前置を呼び出す++xと同じ)

x.operator++(0)

(後置を呼び出すx++と同じ)

 

以上、変換関数と演算子関数でした。

 

これらを組み合わせて昨日のカウンタクラスを書いているのでぜひもう一度見てみてください。

 

ありがとうございました。

 

ぷないしん。

C++実践編(カウンタークラスを作成する)

こんにちは、ぷないしんです。

 

昨日の関数の渡し方の詳しい話はまた今度にして今日は、C++のクラスを作成していきたいと思います。

 

 

今回作成するのはズバリ

【カウンタークラス】

内容としては

・0~9の1桁の整数値を数え上げる

・初期化、生成すると同時にカウンタを0にする。

・カウントアップ(値をインクリ)

・カウントダウン(値をデクリ)

・カウンタの値を調べる

 

今回のクラスは小規模なのですべてのメンバ関数をインライン関数にし、ヘッダだけで実現してみます。

 

ということでさっそくコードを記述します。

 

【counter.h】

//カウンタクラス

#ifndef ___Class_Counter
#define ___Class_Counter

#include <limits>

class Counter{
    unsigned cnt;   //カウンタ

public:
    //コンストラクタ カウンタを0に
    Counter() : cnt(0){}

    //カウントアップ (カウンタの上限はunsigned型の最大値になる)
    void increment(){
         if (cnt < std::numeric_limits<unsigned>::max()) cnt++;
    }
    
    //カウントダウン(カウンタの加減は0)
    void decrement(){
        if (cnt > 0) cnt--;
    }

    //カウンタの返却(cntのゲッター)
    unsigned value(){
        return cnt;
    }
};

#endif

 

カウンタを格納しているのが非公開のデータメンバ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クラスを実際に利用してみましょう。

 

#include <iostream>
#include "Counter.h"

using namespace std;

int main()
{
    int no;
    Counter x;

    cout << "Current counter value:" << x.value() << "\n";

    cout << "Count up count:";
    cin >> no;

    for (int i = 0; i < no; i++){
        x.increment();              //Count up
        cout << x.value() << "\n";
    }

    cout << "Count down count:";
    cin >> no;

    for (int i = 0; i < no; i++){
        x.decrement();
        cout << x.value() << "\n";
    }
}

 

f:id:punainen:20210317154520p:plain

実行結果(赤枠はキーボードより入力)

 

まず最初に、CounterのオブジェクトとしてXを生成、カウンタの値を表示します。

オブジェクト生成時にカウンタが0になっています。

生成後は、キーボードからint形の変数noに入れた回数だけ、メンバ関数incrementによるカウントアップを行いながら値を表示します。

それが終わると、もう一度キーボードからint形変数noに入れた数値の回数だけカウントダウンと値の表示を行います。

 

 

もちろんこのまま利用することも出来るのですが、改良点がいくつかあります。

ユーザー定義型であるクラスCounterを使う時は、値を調べる、カウントアップ/ダウン(インクリ/デクリ)を行うためにvalue関数、increment関数、decrement関数を呼び出す必要があります。

 

int形やlong形の組み込み型と比較すると

・タイプ数が増える→タイプミスしやすくなる

 ・プログラムが冗長になる→読みにくくなる

といったデメリットがあります。

 

クラス型オブジェクトに対してインクリやデクリを行う演算子を使えれば、int形やlong形と同じ感じで利用できるはず!

 

という事で改良していきましょう!

 

NewCounter.h

//カウンタクラス

#ifndef ___Class_Counter
#define ___Class_Counter

#include <limits>

class Counter{
    unsigned cnt;   //カウンタ

public:
    //コンストラクタ カウンタを0に
    Counter() : cnt(0){}

    //unsigned型へ変換する
    operator unsigned() const {
        return cnt;
        }

    bool operator!() const {
         return cnt == 0;
         }

    //前置増分演算子++
    Counter& operator++(){
         if (cnt < std::numeric_limits<unsigned>::max()) cnt++;
         return *this;
    }

    //後置増分演算子++
    Counter operator++(int){
        Counter x = *this;
        ++(*this);
        return x;
    }

    //前置減分演算子--
    Counter& operator--(){
        if (cnt > 0) cnt--;
        return *this;
    }

    //後置減分演算子--
    Counter operator--(int){
        Counter x = *this;
        --(*this);
        return x;
    }
};

#endif

 

 前置きの演算子と後置きの演算子を二つ用意しました。

では早速利用してみましょう。

 

countertest.cpp

#include <iostream>
#include "NewCounter.h"

using namespace std;

int main()
{
    int no;
    Counter x;
    Counter y;

    cout << "Count up count:";
    cin >> no;

    for (int i = 0; i < no; i++)
        cout << x++ << " " << ++y << "\n";
    

    cout << "Count down count:";
    cin >> no;

    for (int i = 0; i < no; i++)
        cout << x-- << " " << --y << "\n";
    
    if(!x)
        cout << "x is 0\n";
    else
        cout << "x is not 0\n";
}

 

f:id:punainen:20210317163312p:plain

実行結果(赤枠はキーボードより入力)

xには後置き、yには前置きの演算子を使いました。

ちゃんと使い分けられてますね。

 

変換関数と演算子関数を定義したので組み込み型と同じ感じでCounterクラスを使えるようなりました。

最初のプログラムと比べてみると使いやすい上に利用する時に簡単ですし読みやすくなりましたね。

 

変換関数や演算子の定義の方法はまた次回として今日はこのあたりにしておきます。

 

ありがとうございました。

 

ぷないしん

C++における関数へ変数を渡すときの話

こんにちは、ぷないしんです。

 

関数への値を渡す方法の話をしたいと思います。

 

関数へ値を渡すときの記述方法ですが下記の三つがあるみたいです。

 

①普通の渡し方(値渡しと言われる)

②ポインタ渡し

③参照渡し

 

普通の渡し方とポインタ渡しはCにもありますが、参照渡しについてはC++特有の渡し方です。

 

という事で上記の三つを詳しく見ていきましょう。

 

①普通の渡し方

まずコードを記述してみます。

#include <iostream>

using namespace std;

void kansu(int a){
    a = a + 1;
    cout << a << "\n";
}

int main(){
    int x = 1 ;
    cout << x << "\n";
    kansu( x );
    cout << x << "\n";
}

これで実行結果は

f:id:punainen:20210315132556p:plain

関数1

こうなりました。

渡された数値を+1し、その値を出力するだけの関数を作成しました。

 

ここで注目してほしいのはxの値は関数では+1されずに1のままという事です。

 

これは何を意味しているかというと

xの値を直接処理しているのではなくコピーしたものを処理しているという事です。

なのでコピーされた引数の値をいくら変更しようが、関数を呼び出した側の変数の処理は行われません。

 

調べてみたところ、わざわざコピーを作成しているので、引数が大きな値であれば処理に時間がかかるので、そのような可能性があるときにはこの渡し方は推奨されないとのことです。

 

次に②のポインタ渡し

例によってコードの記述から行います

#include <iostream>

using namespace std;

void kansu(int* a){
    *a = *a + 1;
    cout << *a << "\n";
}

int main(){
    int x = 1 ;
    cout << x << "\n";
    kansu( &x );
    cout << x << "\n";
}

①との差異は

・関数の定義時にint*

・関数の中身の変数それぞれにa*

・メイン関数で関数呼び出し時、変数に&x

 

そして結果の方は

 

f:id:punainen:20210315134319p:plain

関数2

こうなりました。

 

本来はnullチェックというものを行わないといけないようなのですが、それはまた別の機会にお話しするとして結果の方に注目してみましょう。

 

関数を呼び出した後にxの値が書き変わっているではありませんか、これはつまり

関数の呼び出し元の変数を書き換えることができるということですね。

 

ポインタなのでメモリ上のアドレスを渡しているという言い方になります。

渡してもらったアドレスを関節的に参照することによってこのような処理になっています。

 

 

そして③参照渡しですがこれはCには存在せず、C++に独自に実装されている渡し方で、下記の様に記述します。

#include <iostream>

using namespace std;

void kansu(int& a){
    a = a + 1;
    cout << a << "\n";
}

int main(){
    int x = 1 ;
    cout << x << "\n";
    kansu( x );
    cout << x << "\n";
}

 

結果の方は

f:id:punainen:20210315134319p:plain

②のポインタ渡しと同じ結果ですね。

 

この渡し方で注目してほしいのは、記述の方法です。

 

なんと関数を定義するときにint&とするだけで、ポインタ渡しと同じ結果になったではありませんか。

 

どういう渡し方なのかという元の変数に別の名前を付ける渡し方と言われています。

 

どういうことかというと

 

変数Nipponに対して参照渡しで(int& Japan)で渡すと変数Nipponも変数Japanも同じメモリアドレスを指す事になります。

 

なので呼び出し元の変数にアクセスし、書き換えることが出来るという訳です。

 

つまり、参照渡しは変数を関数で受け渡す時にポインタ渡しの記述を簡易的に行えてしまうという事でとりあえず理解してけばいいかもしれないですね。

参照渡しをするとポインタ渡しの独特な記述がいらないので、我々のような初心者には扱いやすくなります。

 

 

 

どの渡し方も使い方が違いますので一概にこの渡し方をしておけば問題ない!ということはないので状況によってちゃんと使い分けるようにしていきたいです。

 

それぞれの詳しい話はまた後日にします。

 

それではお疲れ様でした。

 

ぷないしん