C++ 入門: クラスとオブジェクト (パート 2)

目次

序文:

1: クラスの 6 つのデフォルトのメンバー関数

2: コンストラクター (最初のメンバー)

(1)コンセプト

(2) 特長

(3) 特性の分析

⭐特徴4

⭐特徴5

⭐特徴6

⭐特徴7

3: 初期化リスト

(1) 輸入

(2) コンセプト

(3) 注意事項

4: デストラクター (2 番目のメンバー)       

(1)コンセプト       

(2) 特長

(3) 例

5: コピーコンストラクター (3 番目のメンバー)

(1)コンセプト

(2) 特長

(3) 特性の分析

⭐特徴2

⭐特徴3

⭐特徴5

 6: 演算子のオーバーロード

(1)コンセプト

(2) 例

(3) 注意事項

7: 代入演算子のオーバーロード (4 番目のメンバー)

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

(2) 特長

⭐特徴1

⭐特徴2

8: const 変更メンバー

9: address および const アドレス演算子のオーバーロード (5 番目と 6 番目のメンバー)

10: 友達

(1)フレンド機能

(2)フレンドクラス

11: 比較的完全な日付クラスを実装する

(1) 最初にさらに重要な点についてお話します

⭐ 月の日数を取得する

⭐ 比較演算子のオーバーロードの原理を実装する

⭐ 入力と出力の過負荷

⭐フロント++(--)とポスト++(--)の違い

(2) Dateクラス実装(サブファイル)

⭐Date.h (関数宣言)

⭐Date.cpp (関数実装)

⭐test.cpp (テスト)


序文:

C++ コラムの内容は一貫しており、展開されていない重要な内容はコラムの前号にあります。

文法学習の便宜のために、std 名前空間を直接展開します。

個人ホームページへのリンク: Pai Xiaoxing 233 の Blog_CSDN ブログ - 初等データ構造、C 言語、C++ 初等分野のブロガー


1: クラスの 6 つのデフォルトのメンバー関数

クラスにメンバーが存在しない場合、それを単に空のクラスと呼びます。

しかし、空のクラスには実際には何もないわけではありません。たとえ何も書かなくても、コンパイラはデフォルトで 6 つのメンバー関数を生成します。

class Date{};

 この部分については基本的な理解だけが必要です。後で 1 つずつ説明します。(これらのメンバー関数は特殊なので、通常の関数とは見なさないでください)


2: コンストラクター (最初のメンバー)

(1)コンセプト

次の Date クラスを見てみましょう。

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Init(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

C言語でコードを書くときに初期化関数を書くことが多いのですが、構造体変数を作成するたびに初期化関数を手動で呼び出す必要がありますが、変数を作成するたびに初期化関数を自動的に呼び出すことはできるのでしょうか?

答えは「はい」です。コンストラクターでこの問題を解決できます

コンストラクターは戻り値を持たない (つまり、本当の意味では何もなく、空でさえない) 特別なメンバー関数であり、関数名はクラス名と同じです。コンパイラーはオブジェクトの作成時にこの関数を自動的に呼び出します。そして一度だけ呼び出されます

(2) 特長

コンストラクターは特別なメンバー関数です。コンストラクターの名前はコンストラクターと呼ばれていますが、コンストラクターの主なタスクは
オブジェクトを作成するためのスペースを開くことではなく、オブジェクトを初期化することであることに
注意してください(コンストラクターは初期化を担当します! 初期化!! 初期化!!! )

その特徴は次のとおりです。

  1. 関数名はクラス名と同じです
  2. 戻り値なし (つまり、実際には何もなく、空さえありません)
  3. オブジェクトがインスタンス化されると、コンパイラーはそれ自体でコンストラクターを呼び出します。
  4. コンストラクターはオーバーロードできる
  5. クラスにコンストラクターが明示的に定義されていない場合、C++ コンパイラーはパラメーターのないデフォルトのコンストラクターを自動的に生成します
    ユーザーがコンパイラーを明示的に定義すると、コンパイラーは生成されなくなります。
  6. クラスの各メンバー変数には対応するコンストラクターがあり、クラスのデフォルトのコンストラクターは実際には、対応する型のメンバー変数のコンストラクターを呼び出します(分かりにくいので後ほど詳しく説明します)
  7. パラメーターなしのコンストラクターとデフォルト コンストラクターは両方ともデフォルト コンストラクターと呼ばれ、存在できるデフォルト コンストラクターは 1 つだけです注: 引数のないコンストラクター、すべてのデフォルト コンストラクター、および書き込みなしでコンパイラーによって生成されたコンストラクターはすべて、デフォルト コンストラクターと
    見なされます。

(3) 特性の分析

⭐特徴4

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

class Date
{
public:
	// 1.无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 调用无参构造函数
	Date d1;
	// 调用带参的构造函数
	Date d2(2015, 1, 1); 
	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
}


オーバーロードを形成することはできますが、通常は上記の記述方法を使用せず、コードをより簡潔にするために以前に学習した完全なデフォルト パラメーターを使用できます

以下の書き方は実際にさらに応用されていて、使い方もとても簡単です!

class Date
{
public:
	//利用全缺省参数
	//实际中这个写法非常好用
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 都是调用一个函数,但不传参数,缺省值生效
	Date d1;
	// 都是调用一个函数,传了参数,缺省值无效
	Date d2(2015, 1, 1); 
}

⭐特徴5

  クラス内でコンストラクターが明示的に定義されていない場合、C++ コンパイラーは、パラメーターを持たないデフォルトのコンストラクターを一度生成します。
ユーザーが明示的に定義したコンパイラは生成されなくなります。 

class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
    //这也是为什么建议写成全缺省
    //写成全缺省这里放开也不会报错
	Date(int year, int month, int day)
	{
	    _year = year;
	    _month = month;
	    _day = day;
	}
	*/
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
	// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
	Date d1;
	return 0;
}

コメント欄を解放します

⭐特徴6

クラスの各メンバー変数には対応するコンストラクターがあり、クラスのデフォルトのコンストラクターは、実際には各メンバー変数の対応する型のコンストラクターを呼び出します。

次の 2 つのことに留意する必要があります。

  • デフォルトで生成されるコンストラクターは、組み込み型を処理しません
  • デフォルトで生成されたコンストラクターは、メンバー変数のコンストラクターを呼び出します。メンバー変数がコンストラクターを明示的に定義していない場合は、デフォルトで生成されたコンストラクターを呼び出し、それ以外の場合は、明示的に定義されたコンストラクターを呼び出します

次のコードを見てみましょう。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

この現象を説明してください

  1. 実際、オブジェクト d がインスタンス化されると、クラス Date のデフォルトのコンストラクターが呼び出されます。
  2. このデフォルトで生成されたコンストラクターは、int 型のデフォルトで生成されたコンストラクター (_year、_month、_day はすべて組み込み型であり、処理されません) と Time 型のコンストラクター (クラスはカスタム型) を 3 回呼び出します。
  3. このうちTime型のコンストラクタは明示的に定義されているので、この関数を呼び出して初期化を完了させます。

さらに: C++11では、組み込み型メンバーの非初期化の問題に対するパッチが適用されています。つまり、組み込み型メンバー変数は次のとおりです。
クラスで宣言するときにデフォルト値を与えることができます

​
​
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
    //这个操作可以理解为给了int类型默认生成的构造函数一个缺省值
    //原本这三个变量默认生成的构造函数时一致的
    //给了默认值后编译器会为这三个变量生成不同的构造函数
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

⭐特徴7

パラメーターなしのコンストラクターとデフォルト コンストラクターは両方ともデフォルト コンストラクターと呼ばれ、デフォルト コンストラクターは 1 つだけです。注: 引数のないコンストラクター、すべてのデフォルト コンストラクター、および書き込みなしでコンパイラーによって生成されたコンストラクターはすべて、デフォルト コンストラクターと
見なされます。
(共通しているのは、パラメータなしで呼び出せることです)

 次のコードを見てみましょう。

class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	//调用的时候存在歧义,调用哪一个好像都可以,所以编译器会报错
	Date d1;
}

分析します:

  • 上記のコードは呼び出し時に曖昧さがあり、呼び出せるように見えるのでコンパイラがエラーを報告します。
  • デフォルトのコンストラクターは 1 つしか存在できませんが、各変数を手動で初期化する限り、それらを同時に存在させる必要があります (したがって、別の関数は意味がありません。実際、デフォルトもその効果を失います)。

3: 初期化リスト

(1) 輸入

次のコードを見てみましょう。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
    //我自己写了一个构造函数,不会自动生成构造函数了
    //_t还会被初始化吗?
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

デフォルトで生成されるコンストラクターはメンバー変数のコンストラクターを呼び出すことがわかっているため、現在のコンストラクターは明示的に定義されていますが、それでもメンバー変数のコンストラクターを呼び出すのでしょうか?

答えは「はい」です。これはすべて初期化リストによって行われます。

(2) コンセプト

初期化子リストは、オブジェクトの作成時にメンバー変数を初期化するための C++ の構文形式です。これは、オブジェクト コンストラクターの関数本体の前のコロンの後にメンバー変数の初期化値を列挙することで実現できます。

初期化リストの役割は、オブジェクトのコンストラクター内のメンバー変数に値を直接割り当てることです。これにより、最初のデフォルトの構築とその後の代入の操作が回避され、オブジェクトの構築と初期化の時間が短縮されます

注:初期化リストは、明示的に定義されたコンストラクターに対してのみ存在します。

簡単に言えば、初期化リストが行うことは、コンストラクターの関数本体に入る前にメンバー変数のコンストラクターを呼び出すことであり、このプロセスは手動で制御できます。

コードを見ると、これを理解するのは難しくありません。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
		//初始化列表在这个位置发挥作用
		//大家也可以把这个地方理解为对象的成员开辟了空间
		//展示一下如何人为控制
		:_year(year) 
		,_month(10)  //括号后面也可以自己给值
		,_day(day)  //前面所讲的声明时给默认值其实就是这里给值
	{}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

初期化リストを学習した後は、オブジェクトの構築時にメンバー変数を初期化できるため、均一な初期化に初期化リストを使用することをお勧めします。そうすることで、プログラムの実行効率が向上し、一部の初期化の問題が回避され、より標準化されて明確になります。

ただし、一部の複雑な状況では、関数本体でそれを行う必要があります。(配列の初期化やmallocアプリケーションのスペースチェックなど)

(3) 注意事項

メンバー変数が初期化される順序は宣言の順序によって決まり、初期化リストの順序とは関係ありません。

class Date
{
public:
	Date(int year = 1,int month = 1,int day = 1)
		:_month(12) 
		,_year(_month)
		,_day(day) 
	{}
	void print()
	{
		cout << "年:>" << _year << endl; 
		cout << "月:>" << _month << endl;
		cout << "日:>" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.print();
	return 0;
}

原因:

  • 宣言の順序によって初期化の順序が決まり、_year が最初に初期化されます。
  • しかし、初期化リストでは _year を初期化するために _month を使用しますが、この時点では _month は初期化されておらず、不明な数値であるため、上記の結果が生じます。

4: デストラクター (2 番目のメンバー)       

(1)コンセプト       

デストラクター: コンストラクターの機能とは異なり、デストラクターはオブジェクト自体の破棄を完了しません。ローカル オブジェクトの破棄は コンパイラーによって行われます。オブジェクトが破棄されると、自動的にデストラクターが呼び出され、オブジェクト内のリソースのクリーンアップが完了します    

図:

(2) 特長

デストラクターは、次のような特性を持つ特別なメンバー関数です。

  1.  デストラクター名には、クラス名の前に文字 ~ が付加されます
  2. パラメータも戻り値の型もありません。
  3. クラスにはデストラクターを 1 つだけ含めることができます。明示的に定義されていない場合、システムはデフォルトのデストラクターを自動的に生成します。(注: デストラクターではパラメータを含めることができないと規定されているため、オーバーロードはできません)    
  4. オブジェクトのライフサイクルが終了すると、C++ コンパイル システムは自動的にデストラクターを呼び出します。     
  5. クラスに明示的に定義されたデストラクターがない場合、C++ コンパイラーは自動的にデフォルトのデストラクターを生成します
    ユーザーが明示的に定義すると、コンパイラーはデフォルトのデストラクターを生成しなくなります。
  6. クラスの各メンバー変数には対応するデストラクターがあり、クラスのデフォルトで生成されるデストラクターは実際には、対応する型のメンバー変数のデストラクターを呼び出します(組み込み型に対応するデストラクタは処理されません。この部分の特性は前のコンストラクタと一致します)
  7. クラスにアプリケーション リソースがない場合は、デストラクターを省略し、コンパイラーによって生成されたデフォルトのデストラクターを直接使用できます。

(3) 例

class SeqList
{
public:
	//析构函数
	~SeqList()
	{
		//清理向堆申请的空间
		free(_a);
		_a = nullptr;
	}
	//构造函数
	SeqList(int capacity)
		:_capacity(capacity)
		,_a(nullptr)
		,_size(0)
	{
		_a = (int*)malloc(sizeof(int) * _capacity);
		//检查是否申请成功
		if (_a == nullptr)
		{
			cout << "malloc error" << endl;
			assert(false);
		}
	}
	//一系列成员函数
private:
	int* _a;
	int _capacity;
	int _size;
};

int main()
{
	SeqList s(10);
	return 0;
}

5: コピーコンストラクター (3 番目のメンバー)

(1)コンセプト

仮パラメータは 1 つだけあり、これはこのクラス型のオブジェクト (通常は const によって変更される) への参照であり、既存のクラス型オブジェクトを使用して新しいオブジェクトを作成するときにコンパイラによって自動的に呼び出されます。

例:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	//和构造函数构成函数重载
	A(const A& a)
	{
		_a = a._a;
	}
private:
	int _a;
};

int main()
{
	A a1(10);
	//调用拷贝构造
	A a2(a1);
}

(2) 特長

コピー コンストラクターも、次の特性を持つ特別なメンバー関数です。

  1. コピー コンストラクターは、コンストラクターのオーバーロードされた形式です(コピー コンストラクターが作成された場合、デフォルトのコンストラクターは生成されません)

  2. コピー コンストラクターのパラメーターは 1 つだけで、クラス型オブジェクトへの参照である必要があります。値渡しメソッドが使用されると、無限の再帰呼び出しが発生するため、コンパイラーは直接エラーを報告します。(なぜ無限再帰が起こるのかは後ほど説明します)
  3. 明示的に定義されていない場合、コンパイラはデフォルトのコピー コンストラクターを生成します。デフォルトのコピー コンストラクター オブジェクトは、
    メモリ ストレージに従ってバイト順にコピーされます。この種のコピーは、浅いコピー、または値のコピー
    (つまり、メンバー変数をバイトごとにコピーすること) と呼ばれます。
  4. クラスの各メンバー変数には、対応するコピー コンストラクターがあり、クラスによって生成されるデフォルトのコピー コンストラクターは、実際には、対応する型のメンバー変数のコピー コンストラクターを呼び出します(組み込み型に対応したコピーコンストラクタで処理しますが、この部分の性質が先ほどのコンストラクタとは若干異なります)
  5. クラスにリソース アプリケーションが関与していない場合は、コピー コンストラクターを記述してもしなくても構いません。リソース アプリケーションが関与すると、 
    コピー コンストラクターを記述する必要があります。それ以外の場合は、コピー コンストラクターは浅いコピーになります。(後ほど例を挙げます)
  6. コピー構築の呼び出しを記述するには 2 つの方法があります。1つ目は A a1(a2)2 つ目は A a3 = a2 ですこれら 2 つの記述方法は完全に同等です。この点に留意する必要があります。

(3) 特性の分析

⭐特徴2


コピー コンストラクターのパラメーターは 1 つだけで、クラス型オブジェクトへの参照である必要があります。値渡しメソッドが使用されると、無限の再帰呼び出しが発生するため、コンパイラーは直接エラーを報告します。

理由は非常に単純で、関数のパラメータと関数の戻り値を渡すときに一時変数が作成されますが、渡されたパラメータがクラスオブジェクトの場合、この一時変数の作成は実際にはコピーコンストラクタを呼び出すことになります

すると、次のような状況が形成されます。

この機能を拡張することは、主に、パラメータを渡したり値を返したりするときにオブジェクトが一時変数を作成する必要があること、そしてこのプロセスが copy construction を呼び出すことを知るためです

⭐特徴3

明示的に定義されていない場合、コンパイラはデフォルトのコピー コンストラクターを生成します。デフォルトのコピー コンストラクター オブジェクトは、
メモリ ストレージに従ってバイト順にコピーされます。この種のコピーは、浅いコピー、または値のコピー (つまり、メンバー変数をバイトごとにコピーすること) と呼ばれます。

コード:

class Time
{
public:
	Time()
		:_hour(1)
	{}
private:
	int _hour;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

⭐特徴5

クラスにリソース アプリケーションが関与していない場合は、コピー コンストラクターを記述してもしなくても構いません。リソース アプリケーションが関与すると、 
コピー コンストラクターを記述する必要があります。それ以外の場合は、コピー コンストラクターは浅いコピーになります。

コード (このコードは直接実行するとクラッシュします):

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
		:_size(0)
		,_capacity(capacity)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	//不仅仅是不符合要求,运行的时候会导致崩溃
	//原因是对堆上申请的空间多次释放
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}


 6: 演算子のオーバーロード

残りの 3 つのメンバー関数はすべて演算子のオーバーロードに関係します。演算子のオーバーロードは非常に重要なので、ここでは個別に説明します。

(1)コンセプト

C++ では、コードの可読性を高めるために演算子のオーバーロードが導入されています。演算子のオーバーロードは、特別な関数名を持つ関数であり、戻り値の型、関数名、およびパラメーターのリストも存在します。戻り値の型とパラメーターのリストは、通常の関数と似ています。
(クラスでも演算子を使用できます。演算子式を関連する関数に変換することで、簡潔で読みやすくなります)

関数名は、キーワード演算子の後に、オーバーロードする必要がある演算子記号が続きます。

関数プロトタイプ:戻り値型演算子演算子(パラメータリスト)

(2) 例

コード:

//以 == 重载为例
class A
{
public:
	A(int a)
		:_a(a)
	{}
	bool operator==(const A& x)
	{
		return (x._a == _a);
	}
private:
	int _a;
};

int main()
{
	A a1(5);
	A a2(10);
	A a3(10);
	cout << (a1 == a2) << endl;
	cout << (a1 == a2) << endl;
	//(a1 == a2)实际上就是a1.operator==(&a1,a2)
	//这里加()是因为cout本质也是函数重载,不加括号就会先和a1结合,cout和a1结合去调用函数
	//这个调用的返回值是另一个类的对象,这个对象再和>a2结合,我们没有实现这个函数重载,会报错
	//后面专门讲一下输入输出实现
}

(3) 注意事項

  • 他の記号を連結して新しい演算子を作成することはできません: 例:operator@
  • オーバーロードされた演算子にはクラス型パラメータが必要です
  • クラスのメンバー関数としてオーバーロードされると、メンバー関数の最初のパラメーターが非表示のパラメーターであるため、その仮パラメーターはオペランドの数より 1 少ないように見えます。
  • * (逆参照)、:: (スコープ修飾子)、sizeof 、?: (三項演算子)、. (クラス メンバー アクセス演算子)、上記の 5 つの演算子はオーバーロードできないことに注意してください。これは、記述式の多肢選択問題でよく出てきます。
  • 演算子のオーバーロードの結合順序は、演算子の優先順位と結合性に関係します。
  • ほとんどの場合、演算子のオーバーロードはクラスのメンバー関数として使用されますが、他に方法がない場合は、通常の関数にすることができ、実質的に関数呼び出しに置き換えることができます。(例えば、クラスの入出力を実現する場合については、後ほど別途説明します)

7:代入演算子のオーバーロード (4 番目のメンバー)

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

  • パラメーターの種類: const T&、参照渡しによりパラメーター受け渡しの効率が向上します。
  • 戻り値の型: T&、戻り参照により戻りの効率が向上します。戻り値の目的は、継続的な代入をサポートすることです。
  • 自分自身に値を割り当てているかどうかを確認してください
  • *this: を返して、連続代入の意味を複合します。
コード:
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date& operator=(const Date& d)
	{
		//this指针和传递过来的对象地址一样说明是自己赋值自己,不进行处理
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    Date d1;
    Date d2;
    Date d3(2022,11,11);
    d1 = d2 = d3;
    //连续赋值,相当于先d2 = d3(d2.operator=(&d2,d3)),返回值为d2的引用
	//然后d1 = d2(相当于d1.operator=(&d1,d2))
    return 0;
}

(2) 特長

  1. ユーザーが明示的に実装しない場合、コンパイラはデフォルトの代入演算子のオーバーロードを生成し(これは非常に特殊です)、値の形式で値ごとにバイトをコピーします (これはいわゆる浅いコピーであり、リソースをクリーンアップする必要がある状況)、表示定義は生成されません。注: 組み込み型のメンバー変数は直接割り当てられますが、カスタム型のメンバー変数は、割り当てを完了するために対応するクラスの代入演算子オーバーロードを呼び出す必要があります。
  2. 最初の点に基づいて、別の機能が導入され、代入演算子はクラスのメンバー関数としてのみオーバーロードでき、グローバル関数としてオーバーロードすることはできません

⭐特徴1

割り当てのオーバーロードを自分で実装する必要がある場合は、前に説明したスタックを例として考えてください。

概要:クラスにリソース管理が関与していない場合、代入演算子は実装してもしなくても構いません。リソース管理が関与した場合は、
実装する必要があります。
 

⭐特徴2

コード:

//这一段代码无法通过编译
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}

 理由: 代入演算子が明示的に定義されていない場合、コンパイラはデフォルトの代入演算子を生成します。このとき、ユーザーがクラス の外部で
グローバル代入演算子オーバーロードを実装すると、クラス 内のコンパイラによって生成されるデフォルトの代入演算子オーバーロードと競合するため代入
演算子オーバーロードはクラス のメンバー関数のみにすることができます


8: const 変更メンバー

まず次のコードを見てみましょう。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{}
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	const A a1;
	a1.print();
	return 0;
}

コンパイル結果:

C++ で const で修飾した変数はすでに定数であることはわかっていますが、たとえ定数であっても印刷、比較、コピーマスターとして使用できればよいのですが、上記のコードはコンパイルすらできません。

理由: これには、アクセス許可の拡大と縮小の問題が含まれます。非表示の this ポインターのデフォルトのタイプは -> クラス名 * const this であり、* 前に const が付けられていないため、このポインターが指すオブジェクトが変更可能であることを示します。ただし、上記のオブジェクトは const で変更されており、渡されるポインタの型は -> const クラス名 * const this となり、権限の増幅に属するため許可されません。

解決策: この問題を解決するには、非表示の this 型の前に const を追加する必要がありますが、これは非表示になっています。コンパイラに必要なことをどのように伝えればよいでしょうか?

概要:オブジェクトを変更する必要のない一部のメンバー関数については、コードをより広く使用できるように、関数の後に const を追加するようにしています。(前述の例のほとんどは、実際には const で変更する必要があります) 


9: address および const アドレス演算子のオーバーロード (5 番目と 6 番目のメンバー)

これら 2 つのデフォルトのメンバー関数は通常、再定義する必要はなく、コンパイラによってデフォルトで生成されます。
class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

10: 友達

デフォルトでは、オブジェクトのメンバー変数にはクラス内でのみアクセスできますが、クラスの他の場所にあるメンバー変数にアクセスしたい場合はどうすればよいでしょうか? これには友達が必要です

注: フレンドはより柔軟にできますが、カプセル化が壊れるので、使用は慎重に行う必要があります。

(1)フレンド機能

class A
{
public:
	//关键字friend,在类中进行函数声明即可。
	friend void print(A& a);
	A(int a = 0)
		:_a(a)
	{}
private:
	int _a;
};

//想让非成员函数访问类成员变量
void print(A& a)
{
	a._a++;
	cout << a._a << endl;
}
int main()
{
	A a1(20);
	print(a1);
}

(2)フレンドクラス

class A
{
public:
	//利用关键字friend,A是B的友元,B可以访问A,A不可以访问B
	//A把B当朋友,B不一定把A当朋友
	friend class B;
	A(int a = 0)
		:_a(a)
	{}
private:
	int _a;
};

class B
{
public:
	
	B(int b = 0)
		:_b(b)
	{}
	void print()
	{
		//在B类中访问A类的对象
		cout << "A:>" << a._a << "B:>"<< _b << endl;
	}
private:
	int _b;
	A a;
};

11: 比較的完全な日付クラスを実装する

(1) 最初にさらに重要な点についてお話します

月の日数を取得する

// 获取某年某月的天数
//这个很容易理解,后面经常会用到这个函数
int	Date::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;
}

比較演算子のオーバーロードの原理を実装する

これで、> 比較オーバーロードと == 比較オーバーロードを作成しました。< オーバーロードを実装するには、> 比較コードを直接コピーして比較ロジックを変更することを選択する可能性があります。このアプローチでは、コードが非常に冗長になります。残ります。


このような比較関係の場合、> (または <) 比較と == 比較を記述するだけでよく、他の比較ではこれら 2 つのオーバーロードを直接再利用し、論理的に否定します。

たとえば、< の判定は !((A>B) || (A == B)) と書くことができ、括弧内の条件判定 >= は、>= の論理反転は単なる < ではないでしょうか? 他の状況も類推して推測できます。

⭐ 入力と出力の過負荷

cout は、ostream クラスのインスタンス化されたグローバル オブジェクトです。cout << a(int) は実際にこのオブジェクトのメンバー関数を呼び出します。このメンバー関数には int 型のパラメーターがあり、cout << d(double)、これもメンバーです関数ですが、1 つのパラメーターは double 型であり、前の int に対応する関数で関数オーバーロードを構成できますが、ostream は組み込み型のみをオーバーロードします。

連続印刷の実現は、cout << a(int) << d(double) ですが、実際には、まず a と組み合わせて int に対応するメンバー関数を呼び出し、次に cout の参照を返し、その後 d と組み合わせます。 double に対応するメンバー関数を呼び出します。


cin は、istream クラスのインスタンス化されたグローバル オブジェクトです。型を自動的に識別でき、連続入力の原理は cout と似ています。違いは、変数の値を変更する必要があるため、代わりに参照によって渡されることです。価値のあるもの。


では、入力と出力をメンバー関数として直接オーバーロードしたらどうなるでしょうか?

コード:

class Date
{
public:
	const Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	ostream& operator<<(ostream& out)const
	{
		//<<实现内置类型重载的时候类型是没有用const修饰的
		//这里不能加const修饰,不然就找不到这个成员函数了
		out << _year << "." << _month << "." << _day << endl;
		return out;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d1;
	const Date d2(2022,11,11);

	cout << d1 << d2 << endl;
	//这个先看ostream这个类里面有没有实现这个重载
	//没有看全局有没有第一个参数为ostraem&,第二个参数为Date&的全局函数
	//都没有找到说明没有这个函数,编译报错
}


上記の問題を解決するには、このオーバーロードをグローバル関数として設計できます。このグローバル関数は、オブジェクトのメンバー変数にアクセスする必要があります。このグローバル関数がクラスのフレンド関数であることを宣言する必要があります。

class Date
{
public:
	const Date(int year = 1900, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	//声明,表明这个函数是友元函数
	friend ostream& operator<<(ostream& out,const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "." << d._month << "." << d._day << endl;
	return out;
}

int main()
{
	const Date d1;
	const Date d2(2022,11,11);

	cout << d1 << d2 << endl;
}

入力も印刷も同じです。

フロント++(--)とポスト++(--)の違い

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
	//而temp是临时对象,因此只能以值的方式返回,不能返回引用(之前讲过)
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++; // d: 2022,1,13   d1:2022,1,14
	d = ++d1; // d: 2022,1,15   d1:2022,1,15
	return 0;
}

(2) Dateクラス実装(サブファイル)

まずステートメントに対して自分で書いてみることができます。ほとんどのロジックは比較的単純です。

Date.h (関数宣言)

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using namespace std;

class Date
{
public:
	//两个友元函数
	friend ostream& operator<<(ostream& out,const Date& d);
	friend istream& operator>>(istream& in,Date& d);
	// 获取某年某月的天数
	int GetMonthDay(int year, int month)const;

	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);

	// 拷贝构造函数
	// d2(d1)
	Date(const Date& d)
		:_year(d._year)
		,_month(d._month)
		,_day(d._day)
	{
	}

	// 赋值运算符重载
	// d2 = d3 -> d2.operator=(&d2, d3)
	//不传引用返回会多构造一个
	Date& operator=(const Date& d);
	// 析构函数(日期类可以不写,这里是为了方便观察)
	~Date()
	{
		//cout << "析构" << endl;
	}
	// 日期+=天数
	Date& operator+=(int day);

	// 日期+天数
	Date operator+(int day) const;

	// 日期-天数
	Date operator-(int day) const;

	// 日期-=天数
	Date& operator-=(int day);

	// 前置++
	Date& operator++();

	// 后置++,这个int参数只是为了区分,传什么值又编译器自行处理
	Date operator++(int);

	// 后置--
	Date operator--(int);

	// 前置--
	Date& operator--();

	// >运算符重载
	bool operator>(const Date& d)const;

	// ==运算符重载
	bool operator==(const Date& d) const;

	// >=运算符重载
	bool operator >= (const Date& d) const;

	// <运算符重载
	bool operator < (const Date& d) const;

	// <=运算符重载
	bool operator <= (const Date& d) const;

	// !=运算符重载
	bool operator != (const Date& d) const;

	// 日期-日期 返回天数
	int operator-(const Date& d) const;

	//进行日期检查,合法返回1,否则返回0
	int CheckDate() const;

	//打印
	void print()
	{
		cout << _year << "|" << _month << "|" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

//某一天是星期几
int GetWeek(Date& d);

⭐Date.cpp (関数実装)

​
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"

//打印重载
ostream& operator<<(ostream& out,const Date& d)
{
	cout << d._year << "." << d._month << "." << d._day << endl;
	return out;
}

//输入重载
istream& operator>>(istream& in,Date& d)
{
	cin >> d._year >> d._month >> d._day;
	//输入完检查一下是否合法
	assert(d.CheckDate());
	return in;
}

// 全缺省的构造函数
Date::Date(int year, int month, int day)
	:_year(year)
	, _month(month)
	, _day(day)
{
	//检查一下是否合法
	assert(CheckDate());
}

// 获取某年某月的天数
int	Date::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;
}

// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
//不传引用返回会多构造一个
Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_day = d._day;
		_month = d._month;
		_year = d._year;
	}
	return *this;
}

// 日期+=天数
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}

	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	return *this;
}

// 日期+天数
Date Date::operator+(int day) const
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}

// 日期-=天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}

	_day -= day;
	//减到0要变成下个月的最后一天
	while (_day <= 0)
	{
		_month--;
		//如果减到0,要变成下一年的12月
		if (_month <= 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

// 日期-天数
Date Date::operator-(int day) const
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}

// 前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

// 后置++,这个int参数只是为了构成函数重载,传什么值编译器自行处理
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

// 后置--
Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}

// 前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

// >运算符重载
bool Date::operator>(const Date& d) const
{
	if (_year > d._year)
	{
		return true;
	}
	else if ((_year == d._year) && (_month > d._month))
	{
		return true;
	}
	else if ((_year == d._year) && (_month == d._month) && (_day > d._day))
	{
		return true;
	}
	//前面三种情况都是大于,最后一定是小于
	else
	{
		return false;
	}
}

// ==运算符重载
bool Date::operator==(const Date& d) const
{
	return (_year == d._year)
		&& (_month == d._month)
		&& (_day == d._day);
}

// >=运算符重载
bool Date::operator >= (const Date& d) const
{
	return (*this > d) || (*this == d);
}

// <运算符重载
bool Date::operator < (const Date& d) const
{
	return !((*this > d) || (*this == d));
}

// <=运算符重载
bool Date::operator <= (const Date& d) const
{
	return !(*this > d);
}

// !=运算符重载
bool Date::operator != (const Date& d) const
{
	return !(*this == d);
}

// 日期-日期 返回天数
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int n = 0;
	//一开始默认前面大于后面
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		//如果后面大于前面,最后结果为负数
		flag = -1;
	}

	while (max > min)
	{
		min++;
		n++;
	}
	return n * flag;
}

int GetWeek(Date& d)
{
	Date tmp(1, 1, 1);
	int week = 0; //0 - 周一
	int n = d - tmp;
	week += n;
	return week % 7;
}

//合法返回1,否则返回0
int Date::CheckDate() const
{
	return _month > 0 && _month < 13 && (_day > 0 && _day <= GetMonthDay(_year, _month));
}

​

⭐test.cpp (テスト)

#include "Date.h"

void text1()
{
	//  =
	/*Date d1;
	Date d2(3000, 11, 11);
	Date d3;
	d3 = d1 = d2;
	d1.print();
	d2.print();
	d3.print();*/


	//  +=
	/*Date d1(2022,12,1);
	d1 += 50;
	d1.print();
	d1 += 500;
	d1.print();
	d1 += 5000;
	d1.print();*/

	// + 
	//Date d1(2022, 11, 1);
	//Date d2 = d1 + 50;
	//d1.print();
	//d2.print();


	// -
	/*Date d1(2022, 12, 20);
	Date d2 = d1 - 20;
	d1.print();
	d2.print();*/

	// -=
	//Date d1(2022, 12, 20);
	//d1 -= d1 -= 20;

	// 前置++
	/*Date d1(2022, 11, 30);
	Date d2 = ++d1;*/

	// 后置++
	/*Date d1(2022, 11, 1);
	Date d2 = d1++;*/

	//前置--
	/*Date d1(2022, 11, 1);
	Date d2 = --d1;*/

	// 后置--
	/*Date d1(2022, 11, 1);
	Date d2 = d1--;*/


}
void text2()
{
	Date d1(2022, 12, 11);
	Date d2(2023, 12, 12);
	//cin >> d1 >> d2;
	cout << d1 << d2 << endl;
	// >
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 20);
	cout << (d2 > d1) << endl;*/
	
	// ==
	/*Date d1(2023, 11, 20);
	Date d2(2023, 12, 20);
	cout << (d2 == d1) << endl; */
	
	// >=
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 20);
	cout << (d2 >= d1) << endl;*/ 


	// <
	/*Date d1(2022, 12, 20);
	Date d2(2022, 12, 20);
	cout << (d2 < d1) << endl; */


	// <=
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 20);
	cout << (d2 <= d1) << endl; */


	// !=
	/*Date d1(2022, 12, 20);
	Date d2(2022, 12, 21);
	cout << (d2 != d1) << endl; */


	// -(日期-日期 == 天数)
	/*Date d1(2022, 12, 20);
	Date d2(2023, 12, 30);
	cout << (d2 - d1) << endl; */

}

void menu()
{
	cout << "*****************************" << endl;
	cout << "1.日期加/减天数   2.日期-日期" << endl;
	cout << "3.算星期几        -1.退出    " << endl;
	cout << "*****************************" << endl;

}

//用来计算某一天是星期几,日期间的加减
void text3()
{
	int input = 0;
	int day = 0;
	Date d1;
	Date d2 = d1;
	//用下标来一一对应,0下标代表星期一
	const char* WeekArr[] = { "周一","周二","周三","周四","周五","周六","周日" };
	do
	{
		menu();
		cout << "请输入:>";
		cin >> input;
		if (input == 1)
		{
			cout << "请输入一个日期(空格隔开):>";
			cin >> d1;
			cout << "请输入天数:>";
			cin >> day;
			cout << d1 + day << endl;
		}
		else if (input == 2)
		{
			cout << "请输入两个日期(空格隔开):>";
			cin >> d1;
			cin >> d2;
			cout << d1 - d2 << endl;
		}
		else if (input == 3)
		{
			cout << "请输入一个日期(空格隔开):>";
			cin >> d1;
			cout << WeekArr[GetWeek(d1)] << endl;
		}
		else if (input != -1)
		{
			cout << "非法输入" << endl;
		}
	} while (input != -1);
}


int main()
{
	//text1();
	//text2();
	text3();
	return 0;
}

おすすめ

転載: blog.csdn.net/2301_76269963/article/details/131189359