【C++】クラスとオブジェクト (2) コンストラクタ・デストラクタのコピー関数

序文

前の章では、クラスの概念と定義、クラスのアクセス修飾子、クラスのインスタンス化、クラス サイズの計算、C 言語が渡す必要がある this ポインタなど、クラスとオブジェクトの基本的な知識を紹介しました ( C++ で渡す必要はありません。コンパイラが自動的に実装します)

しかし、それだけでは十分ではありません。クラスはオブジェクト指向プログラミング言語で最も重要なものです。引き続きクラスを深く理解する必要があります。最初の 3 つは、の 6 つのデフォルトのメンバー関数。


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

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

デフォルトのメンバー関数: ユーザーによる明示的な実装を行わずにコンパイラによって生成されたメンバー関数は、デフォルトのメンバー関数と呼ばれます。

空のクラス:

class Date
{
    
    

};

ここに画像の説明を挿入

2 番目に、コンストラクター

1. はじめに

コンストラクターの使用シナリオについて、まず簡単なコードを見てみましょう。

#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	void Init(int _capacity = 4)//缺省参数
	{
    
    
		DataType* tmp = (DataType*)malloc(sizeof(DataType) * _capacity);
		if (nullptr == tmp)
		{
    
    
			perror("malloc fail:");
			exit(-1);
		}
		_a = tmp;
		_Top = 0;
		_capacity = _capacity;
	}
	void Push(int num)
	{
    
    
		//判断是否应该扩容
		if (_Top - 1 == _capacity)
		{
    
    
			_capacity *= 2;
			DataType* tmp = (DataType*)realloc(_a,sizeof(DataType) * _capacity);
			if (nullptr == tmp)
			{
    
    
				perror("malloc fail:");
				exit(-1);
			}
			_a = tmp;
		}
		_a[_Top] = num;
		_Top++;
	}
private:
	DataType* _a;
	int _Top;
	int _capacity;
};
int main()
{
    
    
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	return 0;
}

走った後にクラッシュしたのですが、なぜだろうと考えました。
ここに画像の説明を挿入
答えは、スタックを初期化しなかったし、スペースを与えなかったので、当然のことながらデータを挿入できなかったということです。これは正常です。初期化されずにクラッシュする例はたくさんあります。スタックを使用するたびに初期化する必要があり、非常に不快です。オブジェクトを作成するときに自動的に初期化することはできますか? ? 答えは「はい」です。それがコンストラクターです。

2. コンセプト

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

3. 特徴

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

  1. 関数名はクラス名と同じです。
  2. 値を返さないため、戻り値を書き込むことはできません。
  3. コンパイラは、オブジェクトがインスタンス化されるときに、対応するコンストラクターを自動的に呼び出します。
  4. コンストラクターはオーバーロードできます。
  5. クラス内に明示的に定義されたコンストラクターがない場合 (平たく言うと、コンストラクターを自分で作成します)、C++ コンパイラーはパラメーターのないデフォルトのコンストラクターを自動的に生成します。ユーザーがコンパイラーを明示的に定義すると、それは生成されなくなります。

前に述べたように、クラスに何も書かない場合、コンパイラは自動的にコンストラクタを生成します。もちろん、コンパイラによって自動的に生成されたコンストラクタは、私たちが望むものではない可能性があります。コンストラクタを自分で書くこともできます。コンストラクターを自分で作成すると、コンパイラーはコンストラクターを生成しなくなります。

それでは、上記のコードを変更してみましょう

#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	Stack(int capacity = 4)//缺省参数,此类构造函数可以传也可以不传递形参
	{
    
    
		DataType* tmp = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == tmp)
		{
    
    
			perror("malloc fail:");
			exit(-1);
		}
		_a = tmp;
		_Top = 0;
		_capacity = capacity;
	}
	void Push(int num)
	{
    
    
		//判断是否应该扩容
		if (_Top - 1 == _capacity)
		{
    
    
			_capacity *= 2;
			DataType* tmp = (DataType*)realloc(_a, sizeof(DataType) * _capacity);
			if (nullptr == tmp)
			{
    
    
				perror("malloc fail:");
				exit(-1);
			}
			_a = tmp;
		}
		_a[_Top] = num;
		_Top++;
	}
private:
	DataType* _a;
	int _Top;
	int _capacity;
};
int main()
{
    
    
	Stack s1(20);//此处不是函数调用,而是类的实例化顺便给构造函数传参数
	//Stack s1;    //如果是这样则会采用缺省值,即默认开辟4个int类型的空间大小。
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	s1.Push(5);
	s1.Push(6);
	s1.Push(7);
	return 0;
}


ここに画像の説明を挿入

ここに画像の説明を挿入
コードは正常に実行され、コンパイラが私たちが作成したStack関数を自動的に呼び出すことがわかります


。クラスのコンストラクターを見てみましょう。

#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date()//无参数的构造函数
	{
    
    
	}
	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();//报错,错误的调用无参构造函数,会被识别为函数声明!!!
	//warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
	Date d2;//正确的调用无参构造函数
	Date d3(2023,2,10);//正确的调用必须传参的构造函数
};

注: 引数のないコンストラクターを通じてオブジェクトが作成された場合、オブジェクトの後にかっこは必要ありません。そうでない場合は、関数宣言になります。

パラメーターを渡す必要があるコンストラクターとパラメーターを渡す必要のないコンストラクターを読んだ後、コンパイラー自体によって実装されるコンストラクターを見てみましょう。

//不写构造函数
#include<iostream>
using namespace std;
class Date
{
    
    
public:
	void Print()
	{
    
    
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    
    
	Date d1;
	d1.Print();
	return 0;
};

ここに画像の説明を挿入
答えはとても奇妙なものです!コンストラクターを書かないとコンパイラが勝手にコンストラクターを生成してしまうということではないでしょうか?コンストラクターの役割は、オブジェクトに適切な初期化値を与えることですか? 印刷結果がランダムな値になるのはなぜですか? システムが生成したコンストラクターは役に立たないようですか?? ?

答えは次のとおりです。C++ では、型を組み込み型 (基本型) とカスタム型に分割します。組み込み型は、int/char...、ポインターなど、言語によって提供されるデータ型であり、カスタム型は、class/struct/union を使用して独自に定義する型です。次のルールは、コンパイラによって生成されたデフォルトのコンストラクターに適用されます。

デフォルトで生成されるコンストラクターの場合:

  1. 組み込み型のメンバーは処理されません
  2. カスタム型のメンバーの場合、そのデフォルトのコンストラクターが呼び出されます。
    (デフォルト コンストラクターには、すべてのデフォルト コンストラクター、パラメーターを渡さないコンストラクター、システムによって生成されたデフォルト コンストラクターが含まれます)

上記のクラスの場合、クラス内のメンバーはすべて組み込み型であるため、上記の規則に従ってコンパイラーによって生成されるデフォルトのコンストラクターは組み込み型を処理しないため、表示される値は依然としてランダムな値になります。

このルールを理解するために次のコードを見てみましょう。

#include<iostream>
using namespace std;
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;
}

ここに画像の説明を挿入

この例を読んだ後、このルールについてはよく理解できたと思いますが、まだ質問がありますか? 組み込み型をカスタム型で初期化したいだけの場合はどうすればよいでしょうか?

6. C++11 では、組み込み型のメンバーが初期化されない、つまりクラスで宣言されたときに組み込み型のメンバー変数にデフォルト値が与えられるという不具合に対処するパッチが適用されました。(デフォルトパラメータと同様)

#include<iostream>
using namespace std;
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 = 2023;   //给默认值
	int _month = 2;    //给默认值
	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;
	return 0;
}

答えは「いいえ」です。パラメータは渡されません。関数は、パラメータを持たない関数を呼び出すのか、それともすべてデフォルトの関数を呼び出すのかを知りません。

3. デストラクター

1.コンセプト

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

2. 特長

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

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

コード例:

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	Stack(int capacity = 3)
	{
    
    
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
    
    
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
    
    
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 析构函数
	~Stack()
	{
    
    
		if (_array)
		{
    
    
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
		cout << "~Stack()" << endl;
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
    
    
	Stack s;
	s.Push(1);
	s.Push(2);
}

ここに画像の説明を挿入
5. コンパイラが自動生成するデストラクタについては、何か対応されるのでしょうか?次のプログラムでは、コンパイラによって生成されたデフォルトのデストラクターがカスタム型メンバーのデストラクターを呼び出すことがわかります。

#include<iostream>
using namespace std;
class Time
{
    
    
public:
	~Time()
	{
    
    
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
    
    
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
    
    
	Date d;
	return 0;
}

ここに画像の説明を挿入

プログラムが実行されると、次の出力が表示されます。 ~Time() は、
メイン メソッドで Time クラスのオブジェクトを直接作成しません。なぜ最後に Time クラスのデストラクターを呼び出すのですか?
理由: Date オブジェクト d は main メソッドで作成され、d には 4 つのメンバー変数が含まれており、そのうち _year、month、 day は組み込み型メンバーであり、破棄するときにリソースのクリーニングは必要ありません。メモリを再利用し、

_t は Time クラスのオブジェクトであるため、 d が破棄されるときは、それに含まれる Time クラスの _t オブジェクトも破棄する必要があるため、Time クラスのデストラクターを呼び出す必要があります。

ただし: Time クラスのデストラクターを main 関数で直接呼び出すことはできません。実際に解放する必要があるのは Date クラスのオブジェクトであるため、コンパイラーは Date クラスのデストラクターを呼び出します。Date が明示的に指定されていない場合、コンパイラーは Date クラスのデストラクターを呼び出します。 Date クラス Generate にデフォルトのデストラクターを与えます。その目的は、その内部の Time クラスのデストラクターを呼び出すことです。つまり、Date オブジェクトが破棄されるときに、その内部の各カスタム オブジェクトが正しく破棄できることを確認する必要があります。

main 関数は、Time クラスのデストラクターを直接呼び出しませんが、Date クラスのコンパイラーによって生成されたデフォルトのデストラクターを明示的に呼び出します。

注: どのクラスのオブジェクトを作成してそのクラスのデストラクタを呼び出し、そのクラスのオブジェクトを破棄してそのクラスのデストラクタを呼び出します。

4 番目、コピー コンストラクター

1.コンセプト

クラスを使ってオブジェクトを作成すると必ずコピー動作が発生しますが、例えばオブジェクトを作成する際に、既存のオブジェクトと同じオブジェクトを新たに作成できるでしょうか。

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

a. なぜコピー構築が必要なのでしょうか?

C/C++ コンパイラが変数をコピーするとき、それは単純なことではありません。組み込み型の場合、C/C++ コンパイラは変数をそれ自体でコピーできます (バイトごとにコピーします)。カスタム型の場合、C/C++ コンパイラは変数をコピーできません。コピー機能を使用してのみコピーできます。

ここに画像の説明を挿入

(スタックのコピーはディープコピーのコピー機能を使用する必要があります!!!)

2. 特長

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

  1. コピー コンストラクターは、コンストラクターのオーバーロードされた形式です。
//拷贝构造函数
#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
    
    
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(const Date d) // 错误写法:编译报错,会引发无穷递归
	Date(const Date& d) // 正确写法
	{
    
    
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    
    
	Date d1;
	Date d2(d1);//利用拷贝构造创建一个与d1相同的d2
	//Date d2 = d1;//与上一行的意思一致,要调用拷贝构造
	return 0;
}
  1. コピーコンストラクターのパラメーターは 1 つだけで、クラス型オブジェクトへの参照である必要があります。値渡しメソッドが使用されると、無限の再帰呼び出しが発生するため、コンパイラーは直接エラーを報告します。

d1 と同じデータの d2 を作成したい場合は、copy 関数を呼び出す必要があります。
① copy 関数を呼び出すには、パラメータを渡す必要があります。パラメータは仮パラメータであり、仮パラメータは一時的なコピーであるためです。実際のパラメータ。
②そこで、もう一度copy関数を呼び出す必要がありますが、パラメータは仮パラメータであり、仮パラメータは実際のパラメータの一時的なコピーであるため、copy関数を呼び出す際にはパラメータを渡す必要があります。
③そこで、再度コピー関数を呼び出す必要がありますが、パラメータは仮パラメータであり、仮パラメータは実際のパラメータの一時的なコピーであるため、コピー関数を呼び出す際にはパラメータを渡す必要があります。
④そこで、再度コピー関数を呼び出す必要がありますが、パラメータは仮パラメータであり、仮パラメータは実際のパラメータの一時的なコピーであるため、コピー関数を呼び出す際にはパラメータを渡す必要があります。
...
..._
_

論理図:
ここに画像の説明を挿入

もう 1 つの疑問は、なぜコピー構築のパラメータを追加するのかということですconst
答えは、「逆コピーしてしまうのではないかと心配です」です。

ここに画像の説明を挿入

ここに画像の説明を挿入

ここに画像の説明を挿入

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

//默认生成的拷贝构造函数
#include<iostream>
using namespace std;
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 Date
{
    
    
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
    
    
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

ここに画像の説明を挿入

注: コンパイラによって生成されるデフォルトのコピー コンストラクターでは、組み込み型はバイト モードで直接コピーされますが、カスタム型はコピー コンストラクターを呼び出すことによってコピーされます。

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

質問について考えてみましょう。コンパイラによって生成されたデフォルトのコピー コンストラクターはすでにバイト オーダーの値をコピーできますが、それでも組み込み型のコピー コンストラクターを記述する必要がありますか? 答えは、コピー構造を記述するかどうかです。コピー構造の機能 4 を参照する必要があります。サポート アプリケーションは関係していますか。

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

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
    
    
public:
	Stack(size_t capacity = 10)
	{
    
    
		_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);
	return 0;
}

ここに画像の説明を挿入
上記のコードはコピー構築が必要です。そうしないと、2 つのスタックが相互に影響を及ぼします。コピー構造の特徴 4 が、コピー構造を書くか書かないかの基準であることがわかります。

3. コピーコンストラクターの一般的な呼び出しシナリオ

  1. 既存のオブジェクトを使用して新しいオブジェクトを作成する
  2. 関数パラメータの型はクラス型オブジェクトです
  3. 関数の戻り値の型がクラス型オブジェクトである

注意: プログラムの効率を向上させるために、一般オブジェクトにパラメータを渡すときは参照型を使用し、返すときは実際のシーンに応じて可能な限り参照を使用するようにしてください。

V. 結論

この章の内容は初心者にとってはかなり難しいですが、これらのメンバー関数は非常に重要なので、しっかり理解してください。習得すればさらにレベルアップすると思います!

おすすめ

転載: blog.csdn.net/qq_65207641/article/details/128984828