C++ クラスとオブジェクト 2: デフォルトのメンバー関数

 このポインタを通して、C++ が実際に多くのものを隠していることがわかります。また、C++ はコンパイル中に多くのことを処理します。最も重要なクラスとオブジェクトとして、通常は表示されない他のものも隠しているのでしょうか?物事について?

空のクラスを作成し、そこには何も入れません。

class Text{};

 何もないように見えますが、実際にはデフォルトのメンバー関数が存在します。そこだけでなく、完全な 6 つのメンバー関数もあります。

目次

デフォルトのメンバー関数:

 1. コンストラクター:

 デフォルトで生成されるコンストラクターの特性

デフォルトのコンストラクター:

2. デストラクター:

デストラクターの適用方法:

3. コンストラクターをコピーします。

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

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

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

 自己実装された割り当てのオーバーロード:

デフォルトでコンパイラによって生成される代入オーバーロード:

ストリーム挿入およびストリーム抽出演算子のオーバーロード

 友達について話す:

定数メンバー

5 および 6. address および const アドレス演算子のオーバーロード


デフォルトのメンバー関数:

 1. コンストラクター:

この関数は初期化ですが、その名前は構築ですが、オブジェクトが作成された直後にのみ初期化されますそれは非常に奇妙な機能であり、非常に特別です。コンストラクターには、現在のクラス名と同じ名前を付ける必要があります

まず、コンストラクターを作成しない場合、コンパイラーはデフォルトのコンストラクターを直接作成します。

コンストラクターはオーバーロードできます。つまり、複数のコンストラクターと複数の初期化スキームを提供できます。

簡単に要約すると、次のようになります。

コンストラクターの機能:

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


 日付クラスを例に挙げると、デフォルト値と組み合わせてコンストラクターをオーバーロードできます。

class Date
{
public:

	Date(int year=2022, int month=12, int day=29)
	{
		_year = year ;
		_month= month ;
		_day = day   ;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


 デフォルトで生成されるコンストラクターの特性

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

では、コンパイラによって生成されるデフォルトのコンストラクターはどうなるでしょうか?

オーバーロードされたコンストラクターをコメントアウトして直接出力してみましょう

 検索値はランダムな値です。

次に、コンパイラ自体がデフォルトのコンストラクターを生成し、このコンストラクターがランダムな値しか与えることができない場合、まったく役に立たないのではないかという疑問が生じます。しかし実際には、コンストラクターは次のように非常に「二重標準」になっています。

ダブルスタンダードコンストラクター:

コンストラクターは組み込み型に対しては何も行いません非組み込み型のみを初期化し、非組み込み型には int char などの言語に付属する型は除外されるため、作成されたオブジェクトが現在の Date や Structure などのカスタム型である場合、そのデフォルトのメンバーはカスタム型のメンバーに対して関数が呼び出されますが、逆に、コンストラクターは非組み込み型をまったく無視し、非組み込み型はランダムな値を格納します。たとえば、日付クラス内のメンバー変数はすべてランダムな値です。

カスタム型を使用してデフォルトのコンストラクターを呼び出すと、その効果は次のようになります。


class Text
{
public:
	Text()
	{
		cout << "这个自定义类型的构造函数已被调用" <<  endl;
	}

private:
	int text;

};


class Date
{
public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}

private:
	int _year;
	int _month;
	int _day;
	Text tx;
};

Date クラスでオブジェクトを作成するとき、内部の Tx が直接初期化される、つまりこの型のオブジェクトが直接作成され、作成の基礎はクラス A 内のコンストラクターがトリガーされることがわかります。

この機能用に C11 パッチが追加されました

明らかに、この種の二重標準の問題は少なからずあるため、C11 バージョンにはパッチが追加され、組み込み型を宣言するときにデフォルト値を指定できるようになりました。これは、ここでの初期化とよく似ています。実際、彼の操作ロジックはデフォルト値のロジック、つまりパラメータが渡されない場合のデフォルト値です。

 あまり便利ではありませんが、少なくともランダムな値を入れないようにするための救済策と見なすことができます。


デフォルトのコンストラクター:

先入観の誤解があります。つまり、コンパイラ自体がデフォルト コンストラクタと呼ぶのは、自分が書かないときだけだと思っていますが、実際には、デフォルト コンストラクタの実際の概念は、引数のないコンストラクタと完全なコンストラクタです。デフォルトコンストラクター Functions、つまり次の 2 つの関数もデフォルト コンストラクターとみなされます。

 要約すると、パラメータを渡さないものがデフォルトのコンストラクタです。

デフォルトのコンストラクターは 1 つだけあり、このルールを回避しようとするとエラーが報告されます。

 


2. デストラクター:

コンストラクターはオブジェクトを作成し、オブジェクトの破棄はデストラクターです。

class Date
{
public


    //构造函数
    Date()
    {}

    //析构函数
    ~Date()
    {}


	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}

private:
	int _year;
	int _month;
	int _day;

};

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

デストラクターの特徴:

1. デストラクター名はクラス名の前の文字 ~ です。
2. パラメーターも戻り値の型もありません。
3. クラスにはデストラクターを 1 つだけ含めることができます。明示的に定義されていない場合、システムはデフォルトのデストラクターを自動的に生成します。注: デストラクターはオーバーロードできません。
4. オブジェクトのライフサイクルが終了すると、C++ コンパイル システムが自動的にデストラクターを呼び出します。

デストラクターはカスタム型に直面しており、そのデストラクターが呼び出されます。

デストラクターの破棄順序:スタックの性質に従って、後で定義されたものが最初に破棄されます。

例:

 答え: BADC

分析: デストラクターの破棄順序は、後の破棄が最初であり、次にこの質問の作成順序を最初に観察します: CABD

ただし、さまざまな修飾子は変数の宣言サイクルや変数の作成範囲に影響を与えます C はグローバル変数であり、プログラムの最初に作成される変数であり、最後には破棄されます。そして D は、Static はローカル変数の生存範囲を変更するため、ローカル変数が破棄された後に静的変数も破棄されるため、答えは BADC です。


デストラクターの適用方法:

デストラクターはカスタム型のコンストラクターを呼び出しますが、コンストラクター自体がスペースをクリーンアップして解放するため、使用する場合は次のメソッドに従うだけで済みます。

クラスにリソース アプリケーションが存在しない場合、デストラクターを記述することはできません。Date クラスなど、コンパイラによって生成されたデフォルトのデストラクターが直接使用されます。リソース アプリケーションがある場合は、それを記述する必要があり、存在しない場合は、 Stack クラスなどのリソース漏洩の原因となる


3. コンストラクターをコピーします。

d1 の形で新しいオブジェクトを再度作成する場合は、この時点でコピー コンストラクターを使用します。

したがって、C++ では、そのような要件が発生した場合、コピー コンストラクターが使用されます。

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

1. コピー コンストラクターも特別なメンバー関数であり、コンストラクターのオーバーロード形式です。

2. コピー コンストラクターにはパラメーターが 1 つだけあり、そのパラメーターの型はこの型のオブジェクトへの参照です。値による呼び出しは、無限再帰が発生するため直接エラーを報告します。

// Date(const Date& d) // 正确写法
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

なぜ無限再帰が起こるのでしょうか? 値による呼び出しは正確に何を間違っているのでしょうか?

まず、コピー コンストラクターがいつトリガーされるかを見てみましょう。

Date d1(2022,9,26)

Date d2(d1);

 コピー コンストラクターが次のように誤って書かれたらどうなるでしょうか?

Date(const Date d)

1 つ目は、値による呼び出しの最も基本的な機能、つまり仮パラメータの生成です。ここで、仮パラメータは実際のパラメータのコピーであることをもう一度確認しますこのとき、値渡し呼び出しで生成されるコピーは、コピーコンストラクターの条件を再度発動すること、つまり、同じクラスで作成されたオブジェクトのうち、同じ型の新しいオブジェクトを作成することと等価です。

 概要: コピー コンストラクターが値によって呼び出される場合、仮パラメーターのコピーによりコピー コンストラクターが何度もトリガーされるため、正しく使用される場合は同じ種類の参照を使用する必要があります。

コピー コンストラクターを明示的に宣言する場合、逆書き込みエラーを防ぐためにパラメーターに const を追加する必要があります。参照は変数のエイリアスであるため、このエラーが発生した場合は、コピーされる元のオブジェクトに変更され、const は効率的に実行できます。この問題の質問を解決します。


では、コピー コンストラクターはどのように正確にコピーされるのでしょうか?

同様に、コンストラクターのオーバーロードであるため、その本質も二重標準です。デフォルトでコンパイラによって生成されるコピー コンストラクターを使用すると、そのコピーの状況は次のようになります。

組み込み変数の場合、元のオブジェクト内の組み込み変数をターゲット変数にバイト単位でコピーしますが、このコピーは本質的に浅いコピーです

浅いコピー:

浅いコピーは、memcpy とよく似た取得と上書きに似ています。memcpy の基本的な実装を確認してみましょう。memcopy は、メモリに適した上書きとコピーを実装するために、char* を使用して 1 つずつコピーします。コピーの正確性を効果的に保証できます。ポインタに直面したときに問題が発生する可能性もあります。

コピー コンストラクターを使用して 2 つのオブジェクト間のコピーを実現する場合、この型のオブジェクト内にポインターがある場合、コピー コンストラクターのシャロー コピー原理により、元のポインターのみがコピーされ、それが新しいポインターに与えられます。そのため、サブワードは追加のスペースを開くという基本的なニーズとは異なり、コンパイラが同じスペースを 2 回破壊するため、デストラクターが呼び出されたときにクラッシュします。

そしてここでは、浅いコピーによって引き起こされる一連の問題を回避するために、深いコピーを使用する必要があります。つまり、スペースを空けるために独自のコピー コンストラクターを作成します。

コンストラクターのコピー関数がカスタム型を処理する場合、独自のコピー コンストラクターを直接呼び出します。

 コピー コンストラクターの一般的な呼び出しシナリオは次のとおりです。
 1. 既存のオブジェクトを使用して新しいオブジェクトを作成します。
     2. 関数パラメーターの型はクラス型オブジェクトです。
       3. 関数の戻り値の型はクラス型オブジェクトです。


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

代入演算子のオーバーロードは前のメンバー関数と同じで、明示的に呼び出されない場合にはデフォルトの関数が生成されます。しかし、それを理解する前に、演算子のオーバーロードの概念を整理しましょう。


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

加算、減算、乗算、および除算の演算子は、変数を都合よく計算することしかできません。C++ では、カスタム変数の型を操作するために、演算子のオーバーロード機能が設定されています。つまり、カスタム オブジェクトが演算子を使用できるようにします。

この関数は、サイズの比較、加算、減算、乗算、除算などのカスタム タイプの計算効果を実現したり、日付タイプを例にして 2 つの日付間の日数の差を実現したりすることができます。

関数名は、キーワード演算子の後に、オーバーロードする必要がある演算子記号が続きます。
関数プロトタイプ:戻り値型演算子演算子(パラメータリスト)

 リロードプロセス

 たとえば、2 つのクラスが等しいかどうかをチェックする単純な演算子を、楽しみのためにオーバーロードしてみましょう。

関数を記述する場合とは異なり、仮パラメータを記述するときは、値による呼び出しを記述すべきではありません。 そうしないと、コピー コンストラクターが呼び出され、無駄な作業が行われるため、コピー コンストラクターの場合と同じ理由で、参照を使用する必要があります。間違える問題が発生するのを防ぐためにconstを追加します。

したがって、平等を求める私たちの論理によれば、次のように書くのは難しくないはずです。

 しかし、ここでエラーが報告され、プライベート変数にアクセスしようとしていることがわかりました。

 やむを得ずお休みさせていただいたのですが、どうすればいいでしょうか?

今はこの問題を無視しましょう。オーバーロードされた演算子が有効になった場合の出力形式を見てみましょう。

 

ストリーム挿入演算子の操作優先順位が == よりも高いため、これにより大量のエラーが報告されます。

かっこを追加すると、ストリームの操作順序によって引き起こされるエラー報告の問題を効果的に回避できます。

 

先ほどの質問に戻りますが、プライベート メンバー変数を取得するにはどうすればよいでしょうか?

ここではフレンドを使用できますが、後でコーナーを開いて関数を使用してフレンドを取り出すこともできます。つまり、クラス内に直接メンバー関数を作成し、プライベート メンバー変数の値を返すことができます。

しかし、これは比較的単調で、実際には、リダイレクト関数をクラスに直接組み込む方が良い方法です。

しかし、問題は再び発生し、コンパイルはこのように失敗しました。なぜでしょうか?

 パラメータが多すぎるのはなぜですか? 私が使用している演算子が 2 つのオペランドを必要とするのは合理的ではないでしょうか? ここでの問題は、実際にはこのポインターの存在によって引き起こされます。メンバー関数にはデフォルトで This がパラメータとして含まれるため、実際には 3 つのパラメータがあり、非常に簡単です。1 つを直接削除できます。

ここでも、コンパイラは非常に賢く、コンパイラはこの演算子とグローバル変数の使用を検出すると、次の形式を直接使用します。

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

 自己実装された割り当てのオーバーロード:

 前回の伏線は終わったので、正式に本題である代入代入演算子のオーバーロードに入ります。

コピー コンストラクターとは異なり、2 つの既存のオブジェクト間のコピーは代入のオーバーロードです。

int main ()
{
    Date d1(2022,12,31);
    Date d2(2023,1,1);

    d1 = d2;
}

余談:

そこで、難しい質問が生じます。このコピー構築または代入はオーバーロードではないでしょうか?

Date d3 = d2;

しかし、実際には、難しいことは何もありません。これは依然としてコピー構造であり、d4 にはエンティティさえありません。これをどうやって代入と呼ぶことができるでしょうか?

次に、代入のオーバーロードを実装してみましょう

	//赋值操作符重载,为了防止触发拷贝构造,使用传引用
	void operator = (const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}

このように書いても問題ありませんが、連鎖アクセスが実装されていないため、通常の代入演算子は連鎖代入の機能、つまり i = j = 10 を実装する必要があります。

次に、戻り値を含むバージョンを作成します。

	//实现链式访问版本
	Date operator = (const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}

しかし、これでもまだ十分ではありません。なぜでしょうか?

重複したコピーが発生しているため、参照を合理的に使用し、戻り値を参照として設定する必要があります。このプロセスでは、*this ではなく this ポインターが破棄され、*this が指す d1 はまだ破棄されていないことに注意してください。

	//优化版本
	Date& operator = (const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}

デフォルトでコンパイラによって生成される代入オーバーロード:

代入のオーバーロードはデフォルトのメンバー関数のメンバーであるため、それを作成しない場合のコンパイラのデフォルトの効果は何でしょうか?

コンパイラによってデフォルトで生成される代入オーバーロード関数は、コピー コンストラクターと同様の汎用性を備えており、追加のデストラクターを作成する必要のない日付クラスの代入には十分ですが、その原理はコピー コンストラクターと同じです。機能は同じですが、行われるのは浅いコピーです。スタック型に値を代入するためにデフォルトの代入オーバーロード関数を使用すると、同じ空間を 2 回分解するという問題も発生するため、このタイプの型に対して代入オーバーロード関数を自分で記述する必要があります。

しかし、実際には、コンパイラ自体によって生成されたエンディアン コピーが実現されていることがわかりました。

例としてスタックの代入オーバーロードを考えてみましょう。デフォルトでコンパイラによって生成される代入オーバーロード関数を使用します。

 なぜクラッシュしたのでしょうか?

理由:

したがって、解決策は、割り当てられたスペースを直接解放し、ソースと同じサイズのスペースを再度開き、memcpy を使用してデータをコピーし、this ポインターを返すことです。

スタックの挿入と拡張を例に取ると、トリッキーな角度、つまり自分自身に値を代入する操作の発生を避ける必要があるため、確認する必要があります

	Stack& operator =(const Stack& st1)
	{
		if (this != &st1)
		{
			free(_a);
			int* tmp = (int*)malloc(st1._capacity * sizeof(int));
			if (tmp == nullptr)
			{
				perror("malloc fail!");
				exit(-1);
			}
			_a = tmp;
			memcpy(_a, st1._a, st1._top);
			_top = st1._top;
			_capacity = st1._capacity;
		}
		return *this;
	}

 this ポインタは、この関数のスコープから出ても破棄されず、まだ存在するため、最適な解決策として、参照を使用できます。

次に、自己インクリメント型オーバーロードである ++ と -- を作成します。

自動インクリメントは単一オペランドの単一演算子ですが、コンパイラはそれが前位置であるか後位置であるかをどのようにして判断するのでしょうか?

したがって、C++ は次のようなマークを付けました。

 ただし、ここでは、この int はマーキングのみに使用され、パラメーターを渡す必要はなく、自己インクリメントも必要ありません。


ストリーム挿入およびストリーム抽出演算子のオーバーロード

> C++ での組み込み変数のオーバーロードをサポートしており、クラスのフロー挿入および抽出を実現するために、クラスをオーバーロードできます。Cout を呼び出す必要がある場合、C++ はこの型、つまり ostream をサポートします。この型を使用して変数を作成します。ostream out

同様に、この ostream は出力であり、入力は istream です。

オーバーロードがストリーム出力の書き換えに相当する場合、次のように記述されます。

 しかし、リロードするとエラーが報告されます

 

 表現を変えてもそうならないよ

これは非常に苦痛です。アクロバットをさせるためにリロードしたわけではありません。あなたの見た目は使用習慣と一致していません。

その理由は、このポインタがデフォルトで最初のオペランドを占めるためです。

したがって、これら 2 つの演算子をオーバーロードする場合、通常はメンバー関数としては記述されません。Date オブジェクトのデフォルトは左側のオペランドであり、使用習慣に従っていないためです。また、クラスに記述される最初のオペランドは間違いなく this になります。ポインターなので、一般的にはストリーム挿入オーバーロードをメンバー関数として記述しません。

それからそれを取り出して次のように書きます。

 しかし、ここでエラーが報告されました。その理由は実際にはグローバル関数の定義が繰り返されているためです。

 ここでは、プロジェクトの宣言と定義が分離されているため、ヘッダー ファイルの複数のインクルードの問題と同様の古い問題が発生します。

 

どちらのファイルにも head.h が 1 回含まれているため、このオーバーロードされた関数が再定義されます。当時のソリューションとは異なり、呼び出しの繰り返しを防ぐために pragma Once を使用しましたが、Pragma Once はファイルが拡張されないのではなく、ファイルが 2 回拡張されることを防ぐことしかできません。

もちろん、このオーバーロードされた関数がこのようなエラーを報告するだけでなく、グローバル変数を含む他のグローバル関数にもこの問題が発生します。

 解決:

1. 宣言と定義の分離

定義と宣言はシンボル テーブルに入力されるため、宣言と定義も解決できます。

2.静的要素を追加する

static がこの問題を解決できる理由は、その基本定義にも起因する必要があります: static がグローバル変数を変更すると、変数のライフサイクルに影響します。もちろん、グローバル関数と変数を変更すると、それらのリンク属性も変更されますつまり、現在のファイルでのみ表示されるように変更されます。

要約すると、 .h 内でグローバル変数を定義しないでください。

 この問題が解決された後、私たちが直面する別の問題は、プライベート変数にアクセスする方法です。フレンドはここで使用できます。


 友達について話す:

フレンドは、プライベート変数にアクセスするためのグリーン チャネルに相当します。

クラス内のどこでもフレンドを宣言して、ホワイトリスト上の関数にチャネルを提供できます。必要な関数の前にフレンドを追加するだけです。

 しかし、これは実際には、カプセル化の本来の意図を損なうものであり、通常、私たちは友人を使用する必要がある場合にのみ友人を使用します。


定数メンバー

const型のクラスのオブジェクトを作成した場合、そのメンバー関数を呼び出すとエラーが報告されます。

その理由は実際には電力増幅にあります

非表示の this ポインターの形式を確認してみましょう: Date* const this

オブジェクトを渡すパラメーターの形式: const Date d2

メンバー関数、つまり現在のオブジェクト自体、つまり d2 のアドレスとその型が const Date* を渡します。

2 つの型を比較します。1 つは this ポインター自体に const を追加するもの、もう 1 つはこのポインターが指すオブジェクトに const を追加するものです。

 

 それでは、今何をすべきでしょうか?const オブジェクトであっても、独自のメンバー関数を呼び出すことはできません。

解決策は次のとおりです。C++ ではメンバー関数の後に const を追加できるため、d2 のようなオブジェクト型が通常どおりメンバー関数を使用できるようになります。ここで追加する const 修飾は this ポインタそのものではなく、this ポインタが指す変数、つまりconst Date* constとなり、わかりやすく、ポインタとそのポインタが指す変数をロックするものです。

 しかし、ここで疑問があります。d2 に加えた変更後も、どうすれば素朴なオブジェクト d1 を呼び続けることができるのでしょうか?

理由はパーミッションの削減で、この仮パラメータは明らかに const によってロックされていますが、パラメータを渡す際の初期化を妨げるものではありません。また、const オブジェクトを変更できるのは、初期化されたときだけです

このため、サンプルも書きました。

  const 修飾された「メンバー関数」は const メンバー関数と呼ばれます。const 修飾されたクラス メンバー関数は、実際にはメンバー関数の暗黙的な this ポインターを変更し、メンバー関数内でクラスのメンバーを変更できないことを示します。

要約すると、内部的に変更されないメンバー変数、つまり *このオブジェクト データの場合、これらのメンバー関数は const で追加する必要があります。

とにかく一つは手放さない原則を貫く


5 および 6. address および const アドレス演算子のオーバーロード

これら 2 つのマスターにはあまり注意を払う必要はありません。デフォルトで生成されたマスターで十分です。手動でリロードする必要はありません。ここでは怠惰にしましょう。

 もちろん、アドレスを返さない場合でもオーバーロードする必要があるなど、難しい要件を回避する必要があります。

 


この時点で、すべてのメンバー関数の概要が説明されました。しかし、まだ終わっていないのです!クラスとオブジェクトは本当に理解しにくいですよね。ただし、残りはほんの一部です。

読んでくれてありがとう!少しでもお役に立てれば幸いです!

 

おすすめ

転載: blog.csdn.net/m0_53607711/article/details/128482624