C++クラスとオブジェクトの基本概念

一、クラスの定義

1、クラス定義の形式

クラスはC言語の構造体に似ていますが、機能がより豊富です。

classがクラスを定義するキーワードで、その後にクラス名が続き、{}内がクラス本体です。クラス定義の終わりにセミコロンを省略することはできません(C言語の構造体と同様)。クラス本体の内容はクラスのメンバと呼ばれます:クラス内の変数はクラスの属性またはメンバ変数と呼ばれます;**クラス内の関数はクラスのメソッドまたはメンバ関数と呼ばれます(C++の追加機能)。**具体的なテンプレートは以下の通りです

class Stack
{
    public://外部からアクセス可能
    // メンバ関数
    void Initialize(int n = 4)
    {
        //......
    {
    Push(int x)
    {
        //......
    }
    private://外部からアクセス不可
    // メンバ変数
    int* data;
    size_t maxSize;
    size_t top;
}; // セミコロンは省略不可

クラス内で定義されたメンバ関数はデフォルトでinlineになります。

2、アクセス指定子

~C++のカプセル化を実現する方法の一つで、クラスを使ってオブジェクトの属性とメソッドを結合させ、オブジェクトをより完全なものにします。アクセス権を使用して、外部ユーザーが使用できるインターフェースを選択的に提供します。

~publicで修飾されたメンバはクラス外から直接アクセスできます;protectedとprivateで修飾されたメンバはクラス外から直接アクセスできません。protectedとprivateは同じですが、その違いは継承の章で明らかになります。

~アクセス権のスコープは、そのアクセス指定子が現れた位置から次のアクセス指定子が現れるまでです。後ろにアクセス指定子がない場合、スコープはクラスの終わりまでです。

classで定義されたメンバがアクセス指定子で修飾されていない場合、デフォルトでprivateになります。structはデフォルトでpublicになります。

一般的に、メンバ変数はprivate/protectedに制限され、外部で使用する必要があるメンバ関数はpublicに配置されます。

3、クラススコープ

クラスは新しいスコープを定義し、クラスのすべてのメンバはそのクラスのスコープ内にあります。クラスの外側でメンバを定義する際には、::スコープ演算子を使用して、そのメンバがどのクラススコープに属するかを指定する必要があります。操作は以下の通りです:

class Stack
{
    public:
    //......
    private:
    // メンバ変数
    int* data;
    size_t maxSize;
    size_t top;
};
// 宣言と定義を分離する場合、クラススコープを指定する必要があります
void Stack::Initialize(int n)
{
    data = (int*)malloc(sizeof(int) * n);
    if (nullptr == data)
    {    
    perror("mallocによるメモリ確保に失敗");
    return;
    }
    maxSize = n;
    top = 0;
}

二、インスタンス化

~クラス型を使って物理メモリにオブジェクトを作成するプロセスを、クラスのインスタンス化によるオブジェクト生成と呼びます。

~クラスはオブジェクトの一種の抽象的記述であり、モデルのようなものです。クラスがどのようなメンバ変数を持つかを限定しますが、これらのメンバ変数は宣言だけであり、メモリは割り当てられません。クラスからオブジェクトをインスタンス化したときに初めてメモリが割り当てられます。

三、thisポインタ

~DateクラスにInitializeとPrintという2つのメンバ関数があるとします。関数体内では異なるオブジェクトを区別する情報がありません。しかし、d1がInitializeとPrint関数を呼び出したとき、その関数はd1オブジェクトではなくd2オブジェクトにアクセスすべきだとどうしてわかるのでしょうか?ここでC++が提供する暗黙のthisポインタがこの問題を解決します。

~コンパイラがコンパイル後、クラスのメンバ関数はデフォルトで最初のパラメータ位置に、現在のクラス型のポインタを追加します。これをthisポインタと呼びます。例えば、DateクラスのInitializeの実際のプロトタイプは、void Initialize(Date* const this, int year, int month, int day)です。

~クラスのメンバ関数内でメンバ変数にアクセスするのは、本質的にはthisポインタを介してアクセスします。Initialize関数で_yearに値を代入する場合、this->_year = year;となります。

~C++の規則により、thisポインタを引数やパラメータの位置に明示的に記述することはできません(コンパイル時にコンパイラが処理します)。しかし、関数体内でthisポインタを明示的に使用することはできます。

#include<iostream>
using namespace std;
class Calendar
{
public:
void SetDate(int year, int month, int day)
{
    _year = year;
    _month = month;
    _day = day;
}
void Display()
{
    cout << _year << "/" << _month << "/" << _day << endl;
}
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Calendar today;
    Calendar specialDay;
    today.SetDate(2024, 3, 31);
    today.Display();
    specialDay.SetDate(2024, 7, 5);
    specialDay.Display();
    return 0;
}

四、クラスのデフォルトメンバ関数

1、コンストラクタ

役割:

オブジェクトの初期化

コンストラクタの特徴:

1. 関数名はクラス名と同じです。

2. 戻り値なし。(戻り値は何も必要なく、voidと記述する必要もありません。C++の規則です)

3. オブジェクトのインスタンス化時に、システムが自動的に対応するコンストラクタを呼び出します。

4. コンストラクタはオーバーロードできます。

5. クラスに明示的にコンストラクタが定義されていない場合、C++コンパイラは自動的に引数なしのデフォルトコンストラクタを生成します。ユーザーが明示的に定義すると、コンパイラは生成しません。

6. 引数なしのコンストラクタ、全引数のデフォルト値を持つコンストラクタ、ユーザーが記述しない場合にコンパイラがデフォルトで生成するコンストラクタは、すべてデフォルトコンストラクタと呼ばれます(引数を渡さずに呼び出せるコンストラクタであり、自動生成された関数と解釈しないでください)。

7. ユーザーが記述しない場合、コンパイラがデフォルトで生成するコンストラクタは、組み込み型メンバ変数の初期化を要求しません。つまり、初期化されるかどうかはコンパイラに依存します。カスタム型メンバ変数については、そのメンバ変数のデフォルトコンストラクタを呼び出して初期化することが要求されます。このメンバ変数にデフォルトコンストラクタがない場合、エラーが発生し、このメンバ変数を初期化するには初期化リストを使用する必要があります。

説明:C++は型を組み込み型(基本型)とカスタム型に分けます。組み込み型とは、言語が提供する元のデータ型(int/char/double/ポインタなど)であり、カスタム型とは、class/structなどのキーワードを使用してユーザー自身が定義した型です。

2、デストラクタ

役割:

オブジェクトの破棄

デストラクタの特徴:

1. デストラクタ名はクラス名の前に文字~を付けたものです。

2. パラメータなし、戻り値なし。(コンストラクタと同様で、voidを付ける必要はありません)

3. クラスにデストラクタは一つしかありません。明示的に定義しない場合、システムは自動的にデフォルトのデストラクタを生成します。

4. オブジェクトのライフサイクルが終了すると、システムが自動的にデストラクタを呼び出します。

5. コンストラクタと同様に、ユーザーが記述しない場合、コンパイラが自動的に生成するデストラクタは、組み込み型メンバに対しては処理を行いませんが、カスタム型メンバについてはそのデストラクタを呼び出します。

6. さらに、ユーザーが明示的にデストラクタを記述した場合でも、カスタム型メンバについてはそのデストラクタが呼び出されます。つまり、カスタム型メンバはいかなる場合でも自動的にデストラクタが呼び出されます。

7. クラスでリソースを確保していない場合、デストラクタは記述せず、コンパイラが生成するデフォルトデストラクタを使用します。

8. ローカルスコープ内の複数のオブジェクトについては、C++の規則により、後で定義されたオブジェクトが先にデストラクタが呼び出されます。(データ構造のスタックと同様に、後入れ先出しです)

3、コピーコンストラクタ(参照を使用する必要があります)

コピーコンストラクタの特徴:

1.コピーコンストラクタはコンストラクタのオーバーロードの一つです。

2. コピーコンストラクタの最初のパラメータクラス型オブジェクトの参照でなければなりません。値渡し方式を使用すると、コンパイラは直接エラーを報告します。これは構文的に無限再帰呼び出しを引き起こすためです。(しかし、ここで事前に知っておくべき点があります:C++の規則により、カスタム型オブジェクトのコピーアクションは必ずコピーコンストラクタを呼び出す必要があります。そのため、カスタム型の値渡しによる引数渡しと値渡しによる戻り値の両方がコピーコンストラクタを呼び出します)。したがって、以下の図は無限に再帰してしまいます。

ですので、ここではクラス型オブジェクトの参照を渡す必要があります。参照を使用すれば、オブジェクトのコピーは不要で、オブジェクトの別名を直接使用できます。

4. 明示的にコピーコンストラクタが定義されていない場合、コンパイラは自動的にコピーコンストラクタを生成します。自動生成されたコピーコンストラクタは、組み込み型メンバ変数に対しては値コピー/浅いコピー(一バイトずつコピー)を行い、カスタム型メンバ変数についてはそのコピーコンストラクタを呼び出します。

5. クラスのメンバ変数がすべて組み込み型であり、リソース(ヒープ領域に動的にメモリを確保していない)を指していない場合、コンパイラが自動的に生成するコピーコンストラクタで必要なコピーを完了できます。したがって、明示的にコピーコンストラクタを実装する必要はありません。スタックのようなクラスの場合、すべて組み込み型ですが、底層の配列がリソース(ヒープ領域に動的にメモリを確保)を指しているため、コンパイラが自動的に生成するコピーコンストラクタによる値コピー/浅いコピーは私たちの要件を満たしません。そのため、私たち自身で深いコピー(指しているリソースもコピー)を実装する必要があります(コンパイラが生成するコピーコンストラクタを使用すると、配列のアドレスもそのままコピーされてしまい、デストラクタで解放する際に重複解放が発生し、プログラムがクラッシュします)。クラスの内部メンバ変数がカスタム型メンバである場合、コンパイラが自動的に生成するコピーコンストラクタはカスタム型データのコピーコンストラクタを呼び出すため、このクラスのコピーコンストラクタを明示的に実装する必要はありません。ここには小さなコツがあります。あるクラスでデストラクタを明示的に実装しリソースを解放する場合、コピーコンストラクタも明示的に記述する必要があります。そうでない場合は不要です。

6. 値による戻り値は一時オブジェクトを作成しコピーコンストラクタを呼び出しますが、参照による戻り値は戻り値オブジェクトの別名(参照)を返すため、コピーが発生しません。しかし、戻り値オブジェクトが現在の関数のローカルスコープのローカルオブジェクトである場合、関数の終了時に破棄されるため、参照による戻り値は問題があります。この参照は無効な参照と同じです。無効なポインタと似ています。参照による戻り値はコピーを減らせますが、戻り値オブジェクトが現在の関数の終了後も存在することが確実である場合にのみ使用できます。

4、代入演算子のオーバーロード

1、演算子のオーバーロード

役割:

クラス型オブジェクト間の比較や演算に使用されます。

特徴:

• 演算子のオーバーロードは特別な名前を持つ関数で、その名前はoperatorと定義する演算子で構成されます。他の関数と同様に、戻り型、パラメータリスト、関数本体を持ちます。

• オーバーロードされた演算子関数のパラメータ数は、その演算子が作用するオペランドの数と同じです。単項演算子は一つのパラメータを持ち、二項演算子は二つのパラメータを持ちます。二項演算子の左側のオペランドは最初のパラメータに渡され、右側のオペランドは二番目のパラメータに渡されます。

あるオーバーロードされた演算子関数がメンバ関数である場合、最初のオペランドは暗黙的にthisポインタに渡されるため、演算子のオーバーロードをメンバ関数として実装する場合、パラメータ数がオペランドより一つ少なくなります。

• 演算子をオーバーロードした後、その優先順位と結合規則は対応する組み込み型の演算子と同じになります。

• 構文に存在しない記号を連結して新しい演算子を作成することはできません:例えばoperator@。

.* :: sizeof ? : . これら5つの演算子はオーバーロードできません。

• オーバーロードされた演算子は少なくとも一つがクラス型のパラメータである必要があり、演算子のオーバーロードで組み込み型オブジェクトの意味を変更することはできません:例: int operator+(int x, int y)

• クラスでどの演算子をオーバーロードするかは、どの演算子をオーバーロードすると意味があるかによります。例えば、日付クラスでoperator-をオーバーロードする意味がありますが、operator+をオーバーロードする意味はありません。

• ++演算子をオーバーロードする場合、前置++と後置++がありますが、両方の演算子オーバーロード関数名はoperator++となり、うまく区別できません。C++の規則では、後置++をオーバーロードする際にintパラメータを追加し、前置++と関数のオーバーロードを構成して区別できるようにしています。

• >をオーバーロードする場合、グローバル関数としてオーバーロードする必要があります。メンバ関数としてオーバーロードすると、thisポインタが最初のパラメータ位置を占めてしまうため、呼び出し時にオブジェクトが左側のオペランドになってしまいます。

2、代入演算子のオーバーロード

役割:

代入演算子のオーバーロードはデフォルトのメンバ関数で、すでに存在する二つのオブジェクト間のコピー代入を完了するために使用されます。ここで注意すべきは、コピーコンストラクタは一つのオブジェクトをコピーして別の作成されるオブジェクトを初期化するために使用される点です。

代入演算子オーバーロードの特徴:

1. 代入演算子のオーバーロードは演算子のオーバーロードの一つで、必ずメンバ関数としてオーバーロードする必要があります。代入演算子オーバーロードのパラメータはconst現在のクラス型参照と記述することをお勧めします。そうしないと値渡しによるパラメータ渡しでコピーが発生します。

2. 戻り値があり、現在のクラス型参照と記述することをお勧めします。参照による戻り値は効率を向上させます。戻り値があるのは、連続した代入のシナリオをサポートするためです。

3. 明示的に実装しない場合、コンパイラは自動的にデフォルトの代入演算子オーバーロードを生成します。デフォルトの代入演算子オーバーロードの動作はデフォルトのコピーコンストラクタと似ており、組み込み型メンバ変数に対しては値コピー/浅いコピー(一バイトずつコピー)を行い、カスタム型メンバ変数についてはその代入オーバーロードを呼び出します。

  1. 日付のようなクラスでメンバ変数がすべて組み込み型であり、リソースを指していない場合、コンパイラが自動的に生成する代入演算子オーバーロードで必要なコピーを完了できます。したがって、代入演算子オーバーロードを明示的に実装する必要はありません。スタックのようなクラスの場合、すべて組み込み型ですが、_aがリソースを指しているため、コンパイラが自動的に生成する代入演算子オーバーロードによる値コピー/浅いコピーは私たちの要件を満たしません。そのため、私たち自身で深いコピー(指しているリソースもコピー)を実装する必要があります。二つのスタックでキューを実装するMyQueueのような型の場合、主にカスタム型のスタックメンバを内部に持つため、コンパイラが自動的に生成する代入演算子オーバーロードはスタックの代入演算子オーバーロードを呼び出すため、MyQueueの代入演算子オーバーロードを明示的に実装する必要はありません。ここにも小さなコツがあります。あるクラスでデストラクタを明示的に実装しリソースを解放する場合、代入演算子オーバーロードも明示的に記述する必要があります。そうでない場合は不要です。

3、アドレス取得およびconstアドレス取得演算子のオーバーロード

これら二つのデフォルトメンバ関数は通常再定義する必要はなく、コンパイラがデフォルトで生成します。

これらの演算子は通常オーバーロードする必要はなく、コンパイラが生成するデフォルトのアドレス取得オーバーロードを使用すれば十分です。特別な場合のみ、例えば他の人が指定された内容を取得できるようにしたい場合などにオーバーロードが必要です。

五、その他

1、コンストラクタ - 初期化リスト

~以前、コンストラクタを実装する際にメンバ変数を初期化する主な方法は関数体内での代入でした。コンストラクタでの初期化にはもう一つの方法があります。それが初期化リストです。初期化リストの使用方法は、コロンで始まり、コンマで区切られたデータメンバのリストが続き、各"メンバ変数"の後に括弧内の初期値または式が続きます。例は以下の通りです:

class Calendar
{
public:
 Calendar(int& x, int year = 1, int month = 1, int day = 1)
    :_year(year)
    ,_month(month)
    ,_day(day)
{}
private:
    int _year = 2;
    int _month = 2;
    int _day = 2;
};

~各メンバ変数は初期化リストに一度しか現れません(一部のメンバ変数が書かれていない場合、宣言時に指定されたデフォルト値が使用されます)。構文的に理解すると、初期化リストは各メンバ変数を定義して初期化する場所と考えることができます。

~参照メンバ変数constメンバ変数デフォルトコンストラクタを持たないクラス型変数は、必ず初期化リストの位置で初期化する必要があります。そうしないとコンパイルエラーになります。

~C++11は、メンバ変数の宣言位置にデフォルト値を設定することをサポートしています。このデフォルト値は、初期化リストで明示的に初期化されていないメンバに使用されます。(ここでの注意点:これは初期化ではなく、デフォルト値を設定しています。このデフォルト値は初期化リスト用のものであり、初期化リストで明示的に初期化されていない場合、このデフォルト値を使用して初期化されます

~初期化リストを使用して初期化することを尽量してください。なぜなら、初期化リストで初期化されていないメンバも初期化リストを通過するからです。このメンバが宣言位置にデフォルト値が設定されている場合、初期化リストはそのデフォルト値を使用して初期化します。デフォルト値を設定しなかった場合、初期化リストで明示的に初期化されていない組み込み型メンバが初期化されるかどうかはコンパイラに依存し、C++は規定していません。初期化リストで明示的に初期化されていないカスタム型メンバは、このメンバ型のデフォルトコンストラクタを呼び出して初期化されます。デフォルトコンストラクタがない場合、コンパイルエラーになります。

~初期化リスト内では、メンバ変数がクラス内で宣言された順序で初期化されます。メンバが初期化リストに現れる順序とは関係ありません。したがって、宣言順序と初期化リストの順序をできるだけ一致させるようにしてください。

2、型変換(コードを簡単にするため)

~C++は組み込み型からクラス型オブジェクトへの暗黙的な型変換をサポートしており、関連する組み込み型をパラメータとするコンストラクタが必要です

class Item
{
public:
Item(int a=0)
    :_id(a)
    {}
Item(const Item& ii)
    :_id(ii._id)
    {}
void Display()
{
    cout << _id << " " << _value << endl;
}
private:
    int _id;
    int _value;
};
class Inventory
{
public:
    void AddItem(const Item& ii)
    {
        //...
    }
private:
    Item _items[10];
    int _count;
} 
int main()
{
     Inventory inv;
    // このように書く
    Item item3(3);
    inv.AddItem(item3);

    // このように書ける
    // Itemの一時オブジェクトを構築し、その一時オブジェクトでitem3をコピー構築
    // コンパイラは連続した構築+コピー構築(論理的順序)を直接構築(コンパイラが直接最適化した順序)に最適化
    inv.AddItem(3);
    
    Item item1 = 1;
    item1.Display();
    const Item& item2 = 1;
    // C++11以降、多パラメータ変換をサポート
    Item item3 = { 2,2 };
    return 0;
}

~コンストラクタの前にexplicitを付けると、暗黙的な型変換はサポートされなくなります。

~C++11以降、多パラメータ変換がサポートされるようになりました。

3、staticメンバ

~staticで修飾されたメンバ変数は静的メンバ変数と呼ばれ、静的メンバ変数は必ずクラスの外側で初期化する必要があるため、宣言時に値を設定することはできません(宣言は初期化リスト用であり、静的メンバ変数は初期化リストを通過しません)

~静的メンバ変数は、そのクラスのすべてのオブジェクトで共有され、特定のオブジェクトに属さず(メンバ関数と同様)、静的領域に格納されます

~staticで修飾されたメンバ関数は静的メンバ関数と呼ばれ、静的メンバ関数はthisポインタを持ちません

~静的メンバ関数では他の静的メンバにアクセスできますが、非静的メンバにはアクセスできません。なぜならthisポインタがないからです

~非静的メンバ関数は、任意の静的メンバ変数と静的メンバ関数にアクセスできます。

~クラススコープを突破すれば静的メンバにアクセスできます(publicで制限されている場合)。クラス名::静的メンバまたはオブジェクト.静的メンバを使用して静的メンバ変数と静的メンバ関数にアクセスできます。

~静的メンバもクラスのメンバであり、public、protected、privateのアクセス指定子の制限を受けます。

~静的メンバ変数は宣言位置にデフォルト値を設定して初期化することはできません。なぜならデフォルト値はコンストラクタの初期化リスト用であり、静的メンバ変数は特定のオブジェクトに属さず、コンストラクタの初期化リストを通過しないからです。

4、友元

~友元は、クラスのアクセス指定子によるカプセル化を突破する方法を提供します。友元は友元関数友元クラスに分かれます。関数宣言またはクラス宣言の前にfriendを付け、友元宣言をクラスの内部に配置します。

~外部の友元関数はクラスのprivateおよびprotectedメンバにアクセスできますが、友元関数は単なる宣言であり、クラスのメンバ関数ではありません

~友元関数はクラス定義のどこにでも宣言でき、クラスのアクセス指定子の制限を受けません

~一つの関数は複数のクラスの友元関数になることができます。(以下に注意すべき点があります。前方宣言が必要です)

// 前方宣言、さもなくばAの友元関数宣言でBが認識されません
class B;
class A
{
    // 友元宣言
    friend void process(const A& aa, const B& bb);
    private:
    int _a1 = 1;
    int _a2 = 2;
};
class B
{
// 友元宣言
    friend void process(const A& aa, const B& bb);
    private:
    int _b1 = 3;
    int _b2 = 4;
};
void process(const A& aa, const B& bb)
{
    cout << aa._a1 << endl;
    cout << bb._b1 << endl;
}

~友元クラスのメンバ関数はすべて別のクラスの友元関数となり、別のクラスのprivateおよびprotectedメンバにアクセスできます。

以下の図のように:

class Data
{
 // 友元宣言
    friend class Processor;
private:
    int _value1 = 1;
    int _value2 = 2;
};
class Processor
{
public:
    void Process1(const Data& dd)
    {
        cout << dd._value1 << endl;
        cout << _data1 << endl;
    }
    void Process2(const Data& dd)
    {
        cout << dd._value2 << endl;
        cout << _data2 << endl;
    }
private:
    int _data1 = 3;
    int _data2 = 4;
};

~友元クラスの関係は一方向的であり、交換性はありません。例えば、AクラスがBクラスの友元でも、BクラスがAクラスの友元であるとは限りません。

~友元クラスの関係は継承できません。AがBの友元で、BがCの友元でも、AはCの友元であるとは限りません。

~場合によっては便利ですが、友元は結合度を高め、カプセル化を破壊するため、多用すべきではありません。

5、内部クラス(Javaと比較してC++では内部クラスはあまり使用されません)

~一つのクラスが別のクラスの内部で定義されている場合、その内部クラスは内部クラスと呼ばれます。内部クラスは独立したクラスであり、グローバルに定義されるものと比較して、外部クラスのクラススコープとアクセス指定子の制限のみを受けます。したがって、外部クラスのオブジェクトには内部クラスが含まれません

~内部クラスはデフォルトで外部クラスの友元クラスです

~内部クラスは本質的には一種のカプセル化です。AクラスとBクラスが密接に関連しており、Aクラスが主にBクラスのために実装されている場合、AクラスをBの内部クラスとして設計することを検討できます。もしprivate/protected位置に配置すれば、AクラスはBクラス専用の内部クラスとなり、他の場所では使用できません。

6、無名オブジェクト(コードを簡単にするため)

~ 型(引数) で定義されたオブジェクトを無名オブジェクトと呼びます。これまでに定義した 型 オブジェクト名(引数) で定義されたものは有名オブジェクトと呼ばれます。

~無名オブジェクトのライフサイクルは現在の一行のみです。一般的に一時的にオブジェクトを定義して現在使用する場合、無名オブジェクトを定義できます。

タグ: C++ クラス オブジェクト コンストラクタ デストラクタ

7月1日 21:38 投稿