[C++] クラスとオブジェクトの 6 つのデフォルトのメンバー関数 (中) (1)

目次

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

2. コンストラクター

2.1 コンストラクターの概念

2.2 特徴

2.2.1 コンストラクターのオーバーロード:

2.2.2 すべてのデフォルトのコンストラクター:

3. デストラクター

3.1 デストラクタの概念

3.2 特徴

4. コンストラクターのコピー

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

4.2 特徴


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

クラスにメンバーが存在しない場合、そのクラスは単に空のクラスと呼ばれます。空のクラスには本当に何もないのでしょうか?いいえ、どのクラスも何も書き込まない場合、コンパイラは次の 6 つのデフォルトのメンバー関数を自動的に生成します。

2. コンストラクター

2.1 コンストラクターの概念

ここで日付クラスの初期化を見てみましょう。

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();
    
    return 0;
}

操作結果:

私たちは C++ を初めて使用するため、次のように初期化する必要があります。

インスタンス化するオブジェクトが多すぎてオブジェクトの初期化を忘れると、プログラムの実行結果がランダムな値になったり、問題が発生したりする可能性があります。

ここで、C++ の族長がそれを考えて、私たちのためにコンストラクターを設計しました。

まず、初期化を忘れて直接印刷した場合の結果を見てみましょう。

これはランダムな値ですが、なぜこれなのでしょうか? 下を見てみましょう。

コンストラクターは、クラス名と同じ名前を持つ特別なメンバー関数であり、クラス型オブジェクトの作成時にコンパイラーによって自動的に呼び出され、各データ メンバーが適切な初期値を持つようにし、生涯に 1 回だけ呼び出されます。オブジェクトのサイクル。

2.2 特徴

コンストラクターは特別なメンバー関数です。コンストラクターの名前はコンストラクターと呼ばれていますが、コンストラクターの主なタスクはオブジェクトを作成するためのスペースを開くことではなく、オブジェクトを初期化することであることに注意してください。
特徴は以下のとおりです。
1. 関数名はクラス名と同じです。
2. 戻り値はありません(void ではないため、書き込む必要はありません)。
3. コンパイラは、オブジェクトがインスタンス化されるときに、対応するコンストラクターを自動的に呼び出します。
4. コンストラクターはオーバーロードできます。

まず、日付クラスのコンストラクターを作成して確認してみましょう。

class Date
{
public:
	Date()//构造函数,无参构造
	{
		cout << "Date()" << endl;
		_year = 1;
		_month = 1;
		_day = 1;
	}

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


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

テストしてみましょう:

main 関数ではコンストラクターは呼び出されませんが、作成したマークがここに出力されます。ここでは、オブジェクトがインスタンス化されるときにコンストラクターが自動的に呼び出されるという実験を行いました。
作成したコンストラクターをコメントアウトすると何が起こるかを見てみましょう。

コメントアウト後も出力できることがわかりますが、これは単なるランダムな値です。なぜなら、記述しないと、コンパイラがデフォルトのコンストラクタを自動的に生成し、それを自動的に呼び出すからです。

C++ では、型を int、char、double、int* などの組み込み型 (基本型) に分割します (カスタム型* も同様です)。

カスタムタイプ: クラス、構造体、共用体など...

ここで、組み込み型のメンバーは処理されないことがわかります。C++11 では、抜け穴を埋めるものと見なすことができるデフォルト値を与えるメンバー変数がサポートされています

2.2.1 コンストラクターのオーバーロード:

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
		_year = 1;
		_month = 1;
		_day = 1;
	}
	
	Date(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.Print();
	Date d2(2023, 8, 1);//这里初始化必须是这样写,这是语法
	d2.Print();

    return 0;
}

操作結果:

:オブジェクトをインスタンス化するとき、呼び出されたコンストラクターにパラメーターがない場合、構文で規定されているように、オブジェクトの後にかっこを追加することはできません。

このように書くと、コンパイラはこれが関数宣言なのか呼び出しなのか判断できません値の受け渡しがあり、関数宣言がそのように表示されないため、d2 は混乱しません。

2.2.2 すべてのデフォルトのコンストラクター:

実際に上記の 2 つのコンストラクターを 1 つに組み合わせることができます。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_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.Print();

	Date d2(2023, 8, 1);
	d2.Print();

	Date d3(2023, 9);
	d3.Print();

    return 0;
}

操作結果:

完全なデフォルトのコンストラクターが最も適用可能です。引数なしの構造と完全なデフォルトは同時に存在できますが、この方法で記述することはお勧めできません。エラーは報告されませんが、完全なデフォルトを呼び出すときにパラメータを渡したくありません。コンパイラは、どの構造を使用するかを知りません。電話をかけたいのですが、曖昧さが生じます。

2 つのスタックを持つキューを実装する場合の問題を見てみましょう。

class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	void Destort()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

通常は初期化方法を決めるためにコンストラクタを自分で書く必要がありますが、メンバ変数はすべてユーザー定義型なので、コンストラクタを書かないことも考えられます。カスタム型のデフォルトのコンストラクターが呼び出されます。

概要: 引数のないコンストラクター、完全なデフォルト コンストラクター、および記述しない場合にコンパイラーによってデフォルトで生成されるコンストラクターはすべてデフォルト コンストラクターと見なされ、存在できるデフォルト コンストラクターは 1 つだけです (複数が共存すると曖昧さが生じます)。

3. デストラクター

3.1 デストラクタの概念

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

3.2 特徴

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

まず、日付クラスのデストラクターを見てみましょう。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
        cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
		_year = year;
		_month = month;
		_day = day;
	}

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

	~Date()
	{
		cout << "~Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;

    return 0;
}

操作結果:

ここで、デストラクターも自動的に呼び出されることがわかります。

私たちは記述しません。コンパイラーはデフォルトのデストラクターを自動的に生成します。

デストラクターの呼び出し順序はスタックの呼び出し順序と似ており、後でインスタンス化されたものが最初に破棄されます。

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

絵を描いて見てみましょう:

スタック内のデストラクターは、スタックの破壊を置き換えます。

class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_top = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_top = -1;
			_capacity = n;
		}
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	//void Destort()
	//{
	//	free(_a);
	//	_a = nullptr;
	//	_top = _capacity = 0;
	//}

	void Push(int n)
	{
		if (_top + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_top++] = n;
	}

	int Top()
	{
		return _a[_top];
	}

	void Pop()
	{
		assert(_top > -1);
		_top--;
	}

	bool Empty()
	{
		return _top == -1;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

スタックなどでは、破壊関数がデストラクタに置き換えられ、デストラクタが自動的に呼び出されます。以前は、手動で破壊関数のインターフェイスを呼び出す必要がありましたが、現在は呼び出す必要がありません。

したがって、コンストラクターとデストラクターの最大の利点は、それらが自動的に呼び出されることです。

4. コンストラクターのコピー

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

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

4.2 特徴

コピー コンストラクターも特殊なメンバー関数であり、その特徴は次のとおりです。
1. コピー コンストラクターはコンストラクターのオーバーロード形式です。
2. コピー コンストラクターのパラメーターは1 つだけであり、同じ型の object への参照である必要があります値渡しメソッドを使用すると、コンパイラーにより無限の再帰呼び出しが発生します。
3.明示的に定義されていない場合、コンパイラはデフォルトのコピー コンストラクターを生成します。デフォルトのコピー コンストラクター オブジェクトは、メモリ ストレージに従ってバイト オーダーでコピーされます。この種のコピーは、浅いコピー、または値のコピーと呼ばれます。

コピー構築はコピー&ペーストに似ています。

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

値によるコピーは無限再帰を引き起こすため、コピー コンストラクターを作成しましょう。

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

	//拷贝构造
	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};
void func(Date d)
{
	d.Print();
}

int main()
{
	Date d1(2023, 8, 2);
	func(d1);

    return 0;
}

組み込み型のコピーは直接コピーであり、カスタム型のコピーはコピー構築を呼び出して完了する必要があります。

vs2019 では、パラメーターを値で渡すためのコンパイラーはエラーを報告します。

したがって、コピー コンストラクターを作成する場合、仮パラメーターは同じ型の参照である必要があります。

参照は変数の別名であり、デストラクタの自動呼び出しの順序は定義後に破棄することになっているが、コピーする際にはd1は破棄されていないので参照を利用できるため、再帰コピーが発生することはない。

作成したコピー コンストラクターをマスクアウトして、何が起こるかを見てみましょう。

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

	//拷贝构造
	//Date(Date& d)
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

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

	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

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

int main()
{
	Date d1(2023, 8, 2);
	Date d2(d1);
	d2.Print();
	
    return 0;
}

操作結果:

 記述しなくても、コピーできることがわかりました。これは、記述しない場合、コンパイラーがデフォルトでコピー コンストラクターを生成するためです。日付クラスのような浅いコピーの場合、デフォルトで生成されたコンストラクターは、コピーを実現します。

スタックのコピー構造をもう一度見てみましょう。

typedef int DataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	//拷贝构造
	Stack(Stack& s)
	{
		_a = s._a;
		_size = s._size;
		_capacity = s._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	
    return 0;
}

ここではスタックのコピー構築を記述します。コピー構築を試してみましょう。

なぜここで例外が発生するのでしょうか?

デバッグして見てみましょう:

ここで、s1 の _a のアドレスが s2 の _a のアドレスと同じであることがわかります。s2 がコピーされると、s2 は破棄されます。s2 の _a が解放された後、s1 は再びデストラクタを呼び出します。_a を解放すると、 _a のスペースが解放されているため、null ポインタ例外が発生します。

したがって、スペース アプリケーションを持つオブジェクトの場合、コピー構築を記述するときにディープ コピーを実行する必要があります。

コードを修正しましょう:

typedef int DataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	//拷贝构造
	Stack(Stack& s)
	{
		cout << "Stack(Stack& s)" << endl;
		//深拷贝
		_a = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (nullptr == _a)
		{
			perror("malloc fail:");
			exit(-1);
		}

		memcpy(_a, s._a, sizeof(DataType) * (s._size+1));
		_size = s._size;
		_capacity = s._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

操作結果:

要約: Date と同様、コピー構造を実装する必要はなく、デフォルトで生成されたものを使用できます。スタックはディープ コピーのコピー構造を実装する必要がありますが、デフォルトのものでは問題が発生します。その必要はありません。カスタム タイプのすべてのメンバーのコピーを書き込む場合、カスタム タイプのコピー コンストラクターが呼び出されます。

拡大:

おすすめ

転載: blog.csdn.net/Ljy_cx_21_4_3/article/details/132089939