C++におけるクラスとオブジェクト:デフォルトメンバ関数の詳細

一.クラスの6つのデフォルトメンバ関数

前回の記事ではクラスについて学びました。クラスについて言及する際、避けて通れないのがクラスの6つのデフォルトメンバ関数です。

もしクラスの中に何も記述しない、いわゆる空のクラスの場合:

class MyClass {}; // これが空のクラスです

しかし、本当に空のクラスは何も持っていないのでしょうか?答えは否定的です。何も記述しない場合でも、コンパイラはそのクラスに対して6つのデフォルトメンバ関数を生成します。

デフォルトメンバ関数:ユーザーが明示的に実装しない場合、コンパイラが生成するメンバ関数のことをデフォルトメンバ関数と呼びます。

二.コンストラクタ

① コンストラクタの概念

コンストラクタとは何でしょうか?以前学んだスタックでは、InitStack関数を使用してスタックを初期化する必要がありました。そこでコンストラクタが登場し、初期化の作業を実行してくれます。非常に便利です!

簡単な日付クラスの例を見てみましょう:

class Calendar
{
public:
 void Initialize(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 c1;
     c1.Initialize(2023, 8, 15);
     c1.Display();
     Calendar c2;
     c2.Initialize(2023, 8, 16);
     c2.Display();
 return 0;
}

ここではパブリック関数Initializeを使用してクラスオブジェクトの日付を設定しています。新しいオブジェクトを毎回作成する際に、Init関数を使用してそのオブジェクトの日付を設定する必要があり、少し面倒に感じるかもしれません。C言語では私たちはこのようにやっていました。

では、オブジェクトを作成する際に情報を設定できないでしょうか?答えは肯定的です。これがC++のコンストラクタです。

コンストラクタ: コンストラクタは特殊なメンバ関数で、その名前はクラス名と同じです。クラスオブジェクトが作成される際にコンパイラによって自動的に呼び出され、各データメンバが適切な初期値を持つことを保証し、オブジェクトのライフサイクル全体で一度だけ呼び出されます。

② コンストラクタの特性

コンストラクタは単なる特殊なメンバ関数です。名前がコンストラクタと呼ばれますが、オブジェクトのためのメモリ領域を確保してオブジェクトを作成するのではなく、オブジェクトを初期化するものです。

その特徴は以下の通りです:

  1. 関数名はクラス名と同じ。
  2. 戻り値の型はありません。
  3. オブジェクトのインスタンス化時にコンパイラが対応するコンストラクタを自動的に呼び出します。
  4. コンストラクタはオーバーロードできます。

次に、デストラクタの使用方法を見ていきましょう:

class Calendar
 {
  public:
      // 1.引数なしのコンストラクタ
      Calendar()
      {}
  
      // 2.引数付きコンストラクタ
      Calendar(int year, int month, int day)
     {
          _year = year;
          _month = month;
          _day = day;
     }
  private:
      int _year;
      int _month;
      int _day;
 };
  
  void TestCalendar()
 {
      Calendar c1; // 引数なしのコンストラクタを呼び出す
      Calendar c2(2023, 1, 1); // 引数付きコンストラクタを呼び出す
  
      // 注意:引数なしのコンストラクタでオブジェクトを作成する場合、オブジェクトの後に括弧をつけないでください。さもないと関数宣言になってしまいます
      // 以下のコードは関数:引数なしでCalendar型のオブジェクトを返すc3関数を宣言しています
      Calendar c3();//これは間違いです
 }

ここでc1とc2はそれぞれ2つのコンストラクタを呼び出しており、2つのコンストラクタは関数のオーバーロードを形成しています。

ここでは明示的にコンストラクタを定義しているため、コンパイラはデフォルトのコンストラクタを生成しません。もしコンストラクタを記述しなければ、コンパイラは自動的に引数なしのデフォルトコンストラクタを生成します。

class Calendar
 {
  public:
 /*
 // ユーザーが明示的にコンストラクタを定義した場合、コンパイラは生成しません
 Calendar(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クラスのコンストラクタをコメントアウトした場合、コードはコンパイルできます。なぜならコンパイラが引数なしのデフォルトコンストラクタを生成するからです
 // Calendarクラスのコンストラクタを有効にすると、コードのコンパイルに失敗します。なぜなら一度でも明示的にコンストラクタを定義すると、コンパイラは生成しなくなるからです

 // 引数なしのコンストラクタを有効にするとエラーが発生:error C2512: "Calendar": 適切なデフォルトコンストラクタがありません
 // c1は引数なしのコンストラクタを必要としています。もし引数付きのコンストラクタを記述すると、コンパイラはデフォルトの引数なしコンストラクタを生成しません
 // つまり、c1には適切なデフォルトコンストラクタがないためエラーが発生します
    Calendar c1;
    c1.Display();
 return 0;
 }

ここではコンストラクタを記述していないため、コンパイラは引数なしのデフォルトコンストラクタを生成しました。Print関数を呼び出して値を確認できます。

ここで疑問が生じます。私たちがデフォルトコンストラクタを明示的に実装しない場合にコンパイラが生成するデフォルトコンストラクタなぜメンバ変数を初期化しないのでしょうか?なぜ_year/_month/_dayが依然としてランダム値なのでしょうか。つまり、ここでコンパイラが生成したデフォルトコンストラクタは何の役にも立たないのでしょうか。

ここでC++の型の分類について説明します。C++は型を組み込み型とユーザー定義型に分けます。

組み込み型とは言語が提供するデータ型で、int/char/doubleなどです。 ユーザー定義型とはclass/struct/unionなどで自分で定義した型です。 次のプログラムを見ると、コンパイラが生成するデフォルトコンストラクタがユーザー定義型のメンバ_tに対してそのデフォルトメンバ関数を呼び出していることがわかります。

class Time
{
public:
 Time()
 {
     cout << "Time()" << endl;
     _hour = 0;
     _minute = 0;
     _second = 0;
}
private:
     int _hour;
     int _minute;
     int _second;
};
class Calendar
{
private:
     // 基本型(組み込み型)
     int _year;
     int _month;
     int _day;
     // ユーザー定義型
     Time _t;
};
int main()
{
   Calendar c;
 return 0;
}

ここでCalendarクラスのオブジェクトを作成していますが、Timeクラスのコンストラクタが呼び出されています。ここからわかるように、クラスに対して生成されるデフォルトコンストラクタは組み込み型に対しては処理を行いませんが、ユーザー定義型に対してはそのデフォルトコンストラクタを呼び出します。

しかし組み込み型を処理しないことへの補填として、C++11ではパッチが当てられました:組み込み型のメンバ変数はクラス内で宣言時にデフォルト値を指定できます。

class Calendar
 {
  public:
 void Display()
 {
      cout << _year << "/" << _month << "/" << _day << endl;
 }
  private:
     int _year=2024;
     int _month=1;
     int _day=1;
     // ただし、これは宣言であり、初期化ではありません。オブジェクトが作成されたときに初期化が行われます
 };
  
  int main()
  {
      Calendar c;
      c.Display();
      return 0;
  }

次に、引数なしのコンストラクタと全引数デフォルトのコンストラクタを書いて、どうなるか見てみましょう?

class Calendar
{
public:
 Calendar()
 {
     _year = 1900;
     _month = 1;
     _day = 1;
 }
 Calendar(int year = 1900, int month = 1, int day = 1)
 {
     _year = year;
     _month = month;
     _day = day;
 }
private:
     int _year;
     int _month;
     int _day;
};
// 以下のテスト関数はコンパイルできますか?
int main()
{
   Calendar c1;
   return 0;
}

ここでは大きなエラーが発生します。オーバーロードされた関数の呼び出しが不明です。ここで一点説明します。

引数なしのコンストラクタと全引数デフォルトのコンストラクタはどちらもデフォルトコンストラクタと呼ばれ、デフォルトコンストラクタは一つしか存在できません。 注意:引数なしのコンストラクタ、全引数デフォルトのコンストラクタ、私たちが記述しなかったコンパイラのデフォルトコンストラクタは、すべてデフォルトコンストラクタとみなすことができます。

ここで作成されたオブジェクトは、引数なしのコンストラクタと全引数デフォルトのコンストラクタの両方を呼び出しますが、デフォルトコンストラクタは一つしか存在できないため、エラーが発生します。引数なしのコンストラクタと全引数デフォルトのコンストラクタは構文的には存在できますが、関数のオーバーロードを形成しますが、二義性を引き起こします。

三.デストラクタ

① デストラクタの概念

前のコンストラクタはスタックのInitのようなもので、初期化の作業を完了し、オブジェクトがどのように作成されるかを知っています。では、オブジェクトがどのように消えるのでしょうか?

ここでデストラクタについて説明します。これはスタックのDestroy関数のようなもので、クリーンアップの作業を完了します。

デストラクタ: コンストラクタとは逆の機能を持ちます。デストラクタはオブジェクト自体の破壊を完了するものではなく、ローカルオブジェクトの破壊作業はコンパイラが行います。オブジェクトは破壊時に自動的にデストラクタを呼び出し、オブジェクト内のリソースのクリーンアップ作業を完了します。

要するに、デストラクタはオブジェクトの後始末をするものです。

② デストラクタの特性

デストラクタも特殊なメンバ関数です。その特徴は以下の通りです:

  1. デストラクタ名はクラス名の前に文字 ~ を付けたものです。
  2. 引数と戻り値の型がありません。
  3. 一つのクラスに一つのデストラクタしか存在できません。明示的に定義しない場合、システムは自動的にデフォルトのデストラクタを生成します。注意:デストラクタはオーバーロードできません
  4. オブジェクトのライフサイクルが終了すると、C++コンパイラシステムが自動的にデストラクタを呼び出します。
typedef int DataType;
class Stack
{
public:
 Stack(size_t capacity = 3)
 {
     _array = (DataType*)malloc(sizeof(DataType) * capacity);
     if (NULL == _array)
     {
        perror("malloc: メモリ確保に失敗!!!");
        throw std::bad_alloc();
     }
     _capacity = capacity;
     _size = 0;
 }
 void Push(DataType data)
 {
     // CheckCapacity();
     _array[_size] = data;
     _size++;
 }
 // その他のメソッド...
 ~Stack()// デストラクタでメモリを解放
 {
     if (_array)
     {
     free(_array);
     _array = NULL;
     _capacity = 0;
     _size = 0;
     }
 }
private:
     DataType* _array;
     int _capacity;
     int _size;
};
int main()
{
    Stack s;
    s.Push(1);
    s.Push(2);
    return 0;
}

これはスタックの実装です。コンストラクタでmalloc関数を使用してヒープ上にメモリを確保しているため、オブジェクトのライフサイクルが終了する際にデストラクタを呼び出してリソースのクリーンアップを行う必要があります。

class Time
{
public:
 ~Time()
 {
 	cout << "~Time()" << endl;
 }
private:
     int _hour;
     int _minute;
     int _second;
};
class Calendar
{
private:
     // 基本型(組み込み型)
     int _year = 1970;
     int _month = 1;
     int _day = 1;
     // ユーザー定義型
     Time _t;
};
int main()
{
     Calendar c;
     return 0;
}

ここではCalendarオブジェクトのみを作成していますが、最後にTimeクラスのデストラクタが呼び出されています。これは実際にはコンストラクタと同じ原理です。生成されるデフォルトデストラクタは組み込み型に対しては処理を行いませんが、ユーザー定義型に対してはそのデフォルトデストラクタを呼び出します。

まとめ: クラス内でリソースを確保していない場合、デストラクタは記述する必要がなく、コンパイラが生成するデフォルトのデストラクタを使用できます。Calendarクラスのような場合です。 リソースを確保している場合、必ず記述する必要があり、そうしないとリソースリークが発生します。Stackクラスのような場合です。

四.コンストラクタとデストラクタの順序

class Time
{
public:
    Time()
    {
        cout << "Time()" << endl;
    }
    ~Time()
    {
        cout << "~Time()" << endl;
    }

private:
    int _hour;
    int _minute;
    int _second;
};
class Calendar
{
public:
    Calendar()
    {
        cout << "Calendar()" << endl;
    }
    ~Calendar()
    {
        cout << "~Calendar()" << endl;
    }
private:
    int _year = 1970;
    int _month = 1;
    int _day = 1;
};
int main()
{
    Calendar c;
    Time t;
    return 0;
}

コンストラクタは先に作成されたオブジェクトが先に呼び出され、デストラクタはコンストラクタとちょうど逆で、先に作成されたものが後で呼び出されます。

五.コピーコンストラクタ

① コピーコンストラクタの概念

コピーコンストラクタの文字通りの意味を理解すると、オブジェクトをコピーして新しいオブジェクトを構築することです。簡単に言えば、まったく同じオブジェクトを複製することです。

コピーコンストラクタ: 単一の仮引数を持ち、その仮引数はクラス型オブジェクトへの参照です(一般的にconstで修飾されます)。既存のクラス型オブジェクトを使用して新しいオブジェクトを作成する際に、コンパイラによって自動的に呼び出されます。

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

Calendar(const Calendar& c) // コピーコンストラクタはこのようになります

見ての通り、これはコンストラクタに似ていませんか?没错、彼はコンストラクタの兄弟です。

デストラクタも特殊なメンバ関数で、特徴は以下の通りです:

1. コピーコンストラクタはコンストラクタのオーバーロード形式の一つです。(これがコンストラクタの兄弟です)

2. コピーコンストラクタの引数は一つだけです。そしてそれはクラス型オブジェクトの参照でなければなりません。値渡し方式を使用するとコンパイラが直接エラーを報告し、無限再帰呼び出しを引き起こします。

class Calendar
{
public:
 Calendar(int year = 1900, int month = 1, int day = 1)
 {
     _year = year;
     _month = month;
     _day = day;
 }
 // Calendar(const Calendar& c)   // 正しい書き方
    Calendar(const Calendar c)   // 間違いの書き方:コンパイルエラー、無限再帰を引き起こします
 {
     _year = c._year;
     _month = c._month;
     _day = c._day;
 }
private:
     int _year;
     int _month;
     int _day;
};
int main()
{
     Calendar c1;
     Calendar c2(c1);// これはコピーコンストラクタの呼び出しです。c1オブジェクトを使用してc2オブジェクトをコピーします
 return 0;
}

コピーコンストラクタの呼び出しには2つの方法があります。一つはCalendar c2(c1);で、もう一つはCalendar c2=c1;です。この2つの形式は等価です。

このプログラムを実行するとエラーが発生します。なぜなら仮引数の部分で参照渡し方式を使用しなかったため、無限再帰を引き起こすからです。なぜ無限再帰を引き起こすのでしょうか?

なぜなら引数渡しの際にコピーコンストラクタが呼び出され、コピーコンストラクタがさらに引数を必要とし、引数渡しがさらにコピーコンストラクタを呼び出す…これは無限ループです。無限に続きます。

そこで参照渡し方式を使用します。参照は別名なので、引数渡しの際にコピーコンストラクタが呼び出されることはなく、無限再帰を引き起こすことはありません。また、ここでの引数はconstで修飾することをお勧めします。これにより、cオブジェクトのメンバ変数を誤って変更することを防ぎます。

3. 明示的に定義しない場合、コンパイラはデフォルトのコピーコンストラクタを生成します。 デフォルトのコピーコンストラクタはオブジェクトをメモリ上のストレージに基づいてバイト単位でコピーし、このコピーを浅いコピー(または値コピー)と呼びます。

class Time
{
public:
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time(const Time& t)
    {
        _hour = t._hour;
        _minute = t._minute;
        _second = t._second;
        cout << "Time::Time(const Time&)" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};
class Calendar
{
private:
    // 基本型(組み込み型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // ユーザー定義型
    Time _t;
};
int main()
{
    Calendar c1;

    // 既存のc1を使用してc2をコピーコンストラクタで作成します。ここではCalendarクラスのコピーコンストラクタが呼び出されます
    // しかしCalendarクラスには明示的にコピーコンストラクタが定義されていないため、コンパイラはCalendarクラスにデフォルトのコピーコンストラクタを生成します
    Calendar c2(c1);
    return 0;
}

c1からc2をコピーコンストラクタで作成していますが、Calendarクラスにはコピーコンストラクタがないため、コンパイラはデフォルトのコピーコンストラクタを生成します。3つの組み込み型に対して単純な値コピーを行い、組み込み型Timeクラスに対してはそのコピーコンストラクタを呼び出し、その文が表示されます。

4. コンパイラが生成するデフォルトのコピーコンストラクタは、バイト単位の値コピーを完了することができます。それでも自分で明示的に実装する必要があるのでしょうか? もちろん、日付クラスのようなクラスでは必要ありません。

しかし、私たちがスタックのようなクラスを実装し、コピーコンストラクタstack s1(s2)を呼び出す際に、自分でコピーコンストラクタを実装しない場合、コンパイラはデフォルトのコピーコンストラクタを生成し、値コピーを行います。そのためs1の内容がそのままs2にコピーされます。つまりs1とs2は同じメモリ空間を指します。ここには2つのオブジェクトがあり、デストラクタを2回呼び出すことになり、同じメモリ空間を2回連続して解放しようとしてプログラムがクラッシュします。

まとめ: リソースを確保しているクラスでは、自分でコピーコンストラクタを実装する必要があります。リソースを確保していないクラスでは、書いても書かなくても構いません。

六.代入演算子のオーバーロード

① 演算子のオーバーロード

C++はコードの可読性を向上させるために演算子のオーバーロードを導入しました。演算子のオーバーロードは特殊な関数名を持つ関数で、戻り値の型関数名引数リストを持ちます。戻り値の型と引数リストは通常の関数と似ています。

ここでの関数名は通常の関数名とは少し異なります。

演算子のオーバーロード関数名: キーワードoperatorの後にオーバーロードする演算子を付けます。例えばoperator++、operator--、operator>などです。

演算子のオーバーロードに関する注意点:

  • 他の記号を連結して新しい演算子を作成することはできません:例えばoperator@
  • オーバーロードされた演算子には少なくとも一つのクラス型の引数が必要です。なぜなら演算子のオーバーロードはほとんどクラス内で行われるため、仮引数には暗黙のthisポインタがあります。
  • 組み込み型の演算子については、その意味を変更するオーバーロードはできません。例えば'+'は加算演算ですが、他の意味にオーバーロードすることはできません。
  • . * :: sizeof ?: . 上記5つの演算子はオーバーロードできません。

簡単なクラスを作成して、演算子のオーバーロードを使用してみましょう。

// グローバルのoperator==
class Calendar
{ 
public:
 Calendar(int year = 1900, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }    
//private:
 int _year;
 int _month;
 int _day;
};
bool operator==(const Calendar& c1, const Calendar& c2)
{
    return c1._year == c2._year
   && c1._month == c2._month
        && c1._day == c2._day;
}
int main()
{
    Calendar c1(2023, 9, 15);
    Calendar c2(2023, 9, 16);
    cout<<(c1 == c2)<<endl;
    return 0;
}

このように書くと、operator==をグローバル関数として書くと、クラス外になります。クラス内のメンバ変数にアクセスするには、クラス内のメンバ変数をpublicに公開する必要があります。しかし、これをカプセル化性を保証できなくなります。後の学習では友元関数でこの問題を解決できます。そこで、この演算子のオーバーロード関数をメンバ関数として書くことにします。

class Calendar
{
public:
    Calendar(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // bool operator==(Calendar* this, const Calendar& c2)
    bool operator==(const Calendar& c2)// ここにもthisポインタがあります
    {
        return _year == c2._year
            && _month == c2._month
            && _day == c2._day;
    }
    // これは不要です
    /*bool operator==(const Calendar& c1, const Calendar& c2)
     {
        return c1._year == c2._year
     && c1._month == c2._month
        && c1._day == c2._day;
     }*/
    
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Calendar c1(2023, 9, 15);
    Calendar c2(2023, 9, 16);
    cout << (c1 == c2) << endl;
    return 0;
}

実行結果:

小さなポイント:ここでの(c1==c2)はコンパイラによって**c1.operator ==(&c1, c2)**と認識されます。しかし私たちは2つの操作対象と中間の演算子を書くだけで、非常に便利です。

② 代入演算子のオーバーロード

1.代入演算子のオーバーロードの形式

  • 引数:const &を使用して渡し、効率を向上させます。
  • 戻り値:T&、戻り値に参照を使用して効率を向上させます。連続代入の場合などに役立ちます。
MyClass obj1,obj2,obj3;
obj1=obj2=obj3;

もしオーバーロードされた代入演算子がオブジェクト自体の参照を返す場合、連続代入によって連鎖操作を実現できます。これは特定の場合でコードの簡潔性と可読性を向上させることができます。

代入演算子のオーバーロードをメンバ関数として書く:

class Calendar
{ 
public :
 Calendar(int year = 1900, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }
 
 Calendar (const Calendar& c)
   {
        _year = c._year;
        _month = c._month;
        _day = c._day;
   }
 // ここにも暗黙のthisポインタがあります
 // Calendar& operator=(Calendar*this,const Calendar& c)
 Calendar& operator=(const Calendar& c)
 {
       if(this != &c)
       {
            _year = c._year;
            _month = c._month;
            _day = c._day;
       }
        return *this;
 }
private:
 int _year ;
 int _month ;
 int _day ;
};

**では代入演算子のオーバーロードをグローバル関数として書くのはどうでしょうか?**答えはできません。確認してみましょう。

class Calendar
{
public:
 Calendar(int year = 1900, int month = 1, int day = 1)
 {
     _year = year;
     _month = month;
     _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 代入演算子のオーバーロードをグローバル関数として書く。注意:グローバル関数としてオーバーロードする場合、thisポインタがなくなるため、2つの引数が必要です
Calendar& operator=(Calendar& left, const Calendar& right)
{
 if (&left != &right)
 {
     left._year = right._year;
     left._month = right._month;
     left._day = right._day;
 }
 return left;
}

このコードをVSに置くと直接エラーが発生します。なぜならoperator=はメンバ関数でなければならないからです。

代入演算子のオーバーロードがなぜメンバ関数でなければならず、グローバル関数にはできないのか

答え: 代入演算子のオーバーロードもデフォルトのメンバ関数の一つです。クラス内で代入演算子のオーバーロードを実装しない場合、コンパイラはそのクラスにデフォルトの代入演算子のオーバーロードを生成します。もしクラス外にもグローバルな代入演算子のオーバーロードを実装すると、2つの代入演算子のオーバーロードが競合するため、代入演算子のオーバーロードはクラスのメンバ関数でなければなりません。

2. ユーザーが明示的に実装しない場合、コンパイラはデフォルトの代入演算子のオーバーロードを生成し、値方式でバイト単位にコピーします。 注意:組み込み型のメンバ変数は直接代入され、ユーザー定義型のメンバ変数は対応するクラスの代入演算子のオーバーロードを呼び出して代入を行う必要があります。

代入演算子のオーバーロード関数はコピーコンストラクタと同じです。クラス内でリソースを確保していない場合、自分で書いても書かなくても構いません。逆に、リソースを確保している場合、必ずこの2つの関数を自分で書く必要があります。

③ 前置++と後置++のオーバーロードの違い

class Calendar
{
public:
 Calendar(int year = 1900, int month = 1, int day = 1)
 {
     _year = year;
     _month = month;
     _day = day;
 }
 // 前置++:+1された結果を返す
 // 注意:thisが指すオブジェクトは関数終了後も破棄されないため、参照方式で返すことで効率を向上させます
 Calendar& operator++()
 {
     _day += 1;
     return *this;
 }
 // 後置++:
 // 前置++と後置++はどちらも単項演算子です。前置++と後置++を正しくオーバーロードするために
 // C++は後置++のオーバーロード時にint型の引数を一つ追加することを規定していますが、関数を呼び出す際にはその引数を渡す必要はありません。コンパイラが自動的に渡します
 // 注意:後置++は先に使用してから+1するため、+1される前の古い値を返す必要があります。そのため実装時にthisを一時的に保存し、その後にthis+1する必要があります
 // そしてtempは一時オブジェクトであるため、値方式で返す必要があり、参照で返すことはできません
 Calendar operator++(int)
 {
     Calendar temp(*this);// 古いthisを保存します
     _day += 1;
     return temp;
 }
private:
     int _year;
     int _month;
     int _day;
};
int main()
{
     Calendar c;
     Calendar c1(2023, 1, 13);
     c = c1++;    // c: 2023,1,13   c1:2023,1,14
     c = ++c1;    // c: 2023,1,15   c1:2023,1,15
 return 0;
}

前置++:

後置++:

これは単純な日付クラスなので、自分でコピーコンストラクタ、代入演算子のオーバーロード関数、デストラクタを実装する必要はありません。コンパイラが生成するデフォルトのものが十分に使用できます。ここでは多くの関数を再利用することができ、この日付クラスの実装は比較的簡単です。ここには良い記事があり、日付クラスを詳細に実装しています:C++を笑って学ぶ

7.constメンバ

class Calendar
{
public:
    Calendar(int year=1, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Display()
    {
        cout << "Display()" << endl;
    }
private:
    int _year; // 年
    int _month; // 月
    int _day; // 日
};
int main()
{
    Calendar c1;
    c1.Display();
    return 0;
}

ここでc1オブジェクトがDisplay関数を呼び出すのは自然にできます。

しかし、このオブジェクトの前にconstを付けるとどうなるでしょうか?

理由は権限が拡大しているからです。constオブジェクトがメンバ関数を呼び出せるようにするためには、メンバ関数の後にconstを追加する必要があります。

Display()メンバ関数には隠れたthisポインタがあり、そのプロトタイプはCalendar *const thisです。このconstはthisポインタ自体が変更されないことを修飾しています。この関数の後にconstを追加すると、プロトタイプがconst Calendar *const thisになります。新しく追加されたconstは*thisを修飾し、メンバ変数が変更されないように保護します。

私たちが書く:
void Display() const 
{
   cout << "Display()" << endl;
}

実際には:
void Display(const Calendar *const this) 
{
   cout << "Display()" << endl;
}

コードの信頼性を向上させるため、メンバ関数の後にconstを追加することをお勧めします。

constについて:

  • 権限は縮小できます
  • 権限は平移できます
  • 権限は拡大できません
  1. constオブジェクトは非constメンバ関数を呼び出せますか? 答:できます。権限が縮小しているからです
  2. 非constオブジェクトはconstメンバ関数を呼び出せますか? 答:できません。権限が拡大しているからです
  3. constメンバ関数内で他の非constメンバ関数を呼び出せますか? 答:できません。権限が拡大しているからです
  4. 非constメンバ関数内で他のconstメンバ関数を呼び出せますか? 答:できます。権限が縮小しているからです

constはすべてのメンバ関数の後に追加できるわけではありません。コンストラクタの後にconstを追加することはできません。なぜならコンストラクタの目的はオブジェクトの状態を初期化すること、つまりオブジェクトの状態を変更することだからです。しかしconstキーワードはメンバ関数がオブジェクトの状態を変更しないことを示すために使用されるため、コンストラクタの後にconstを追加することは不正です。

constの使用基準:読み取り専用関数にはconstを追加でき、内部で変更を伴いません。 constが追加されたメンバ関数は、非constオブジェクトとconstオブジェクトの両方を呼び出すことができます。

八.日付クラスの実装:デフォルトメンバ関数の理解

class Calendar
{
public:
    // 特定の年月の日数を取得
    int GetMonthDay(int year, int month) const
    {
        static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
        int day = days[month];
        if (month == 2
            && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
        {
            day += 1;
        }
        return day;
    }

    // 全引数デフォルトのコンストラクタ
    Calendar(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Display() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
    // コピーコンストラクタ
    // c2(c1)
    //Calendar(const Calendar& c);

    // 代入演算子のオーバーロード
    // c2 = c3 -> c2.operator=(&c2, c3)
    //Calendar& operator=(const Calendar& c);
    // デストラクタ
    //~Calendar();
    // 日付に日数を加算
    Calendar& operator+=(int day) // 内部メンバの変更を設計するため、constを追加できません
    {
        _day += day;
        while (_day > GetMonthDay(_year, _month))
        {
            _day -= GetMonthDay(_year, _month);
            _month++;
            if (_month > 12)
            {
                _year++;
                _month = 1;
            }
        }
        return *this;
    }
    // 日付に日数を加算
    Calendar operator+(int day) const
    {
        Calendar tmp(*this);
        tmp += day;
        return tmp;
    }
    // 日付から日数を減算
    Calendar operator-(int day) const
    {
        Calendar tmp(*this);
        tmp -= day;
        return tmp;
    }
    // 日付から日数を減算
    Calendar& operator-=(int day)// 内部メンバの変更を設計するため、constを追加できません
    {
        _day -= day;
        while (_day < 0)
        {
            _day += GetMonthDay(_year, _month);
            _month--;
            if (_month < 1)
            {
                _year--;
                _month = 12;
            }
        }
        return *this;
    }
    // 前置++
    Calendar& operator++()
    {
        *this += 1;
        return *this;
    }
    // 後置++
    Calendar operator++(int)
    {
        Calendar tmp(*this);
        *this += 1;
        return tmp;
    }
    // 後置--
    Calendar operator--(int)
    {
        Calendar tmp(*this);
        *this -= 1;
        return tmp;
    }
    // 前置--
    Calendar& operator--()
    {
        *this -= 1;
        return *this;
    }

    // >演算子のオーバーロード
    bool operator>(const Calendar& c) const
    {
        if (_year > c._year)
        {
            return true;
        }
        else if (_year == c._year && _month > c._month)
        {
            return true;
        }
        else if(_year == c._year && _month == c._month&&_day>c._day)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    // ==演算子のオーバーロード
    bool operator==(const Calendar& c) const
    {
        return _year ==c._year&&
            _month == c._month
            && _day == c._day;
    }
    // >=演算子のオーバーロード
    bool operator >= (const Calendar& c) const
    {
        return (*this > c) || (*this == c);
    }

    // <演算子のオーバーロード
    bool operator < (const Calendar& c) const
    {
        return !(*this >= c);
    }
    // <=演算子のオーバーロード
    bool operator <= (const Calendar& c) const
    {
        return !(*this < c);
    }
    // !=演算子のオーバーロード
    bool operator != (const Calendar& c) const
    {
        return !(*this == c);
    }
    // 日付-日付 日数を返す
    int operator-(Calendar& c) const
    {
        Calendar max(*this);
        Calendar min(c);
        int flag = 1;
        if (*this < c)
        {
            max = c;
            min = *this;
            flag = -1;
        }
        int count = 0;
        while (max != min)
        {
            min++;
            count++;
        }
        return count * flag;
    }
private:
    int _year;
    int _month;
    int _day;
};

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

class Calendar
{ 
public :
 Calendar* operator&()
 {
    return this ;
 }
 const Calendar* operator&()const
 {
    return this ;
 }
private :
   int _year ; // 年
   int _month ; // 月
   int _day ; // 日
};

これらの2つのデフォルトメンバ関数は通常再定義する必要はありません。コンパイラはデフォルトで生成します。

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

6月14日 23:01 投稿