[C++] Six fonctions membres par défaut dans les classes et les objets (milieu) (1)

Table des matières

1. Les six fonctions membres par défaut de la classe

2. Constructeur

2.1 La notion de constructeur

2.2 Caractéristiques

2.2.1 Surcharge de constructeurs :

2.2.2 Tous les constructeurs par défaut :

3. Destructeur

3.1 Le concept de destructeur

3.2 Caractéristiques

4. Copier le constructeur

4.1 Le concept de constructeur de copie

4.2 Caractéristiques


1. Les six fonctions membres par défaut de la classe

S'il n'y a pas de membres dans une classe, on l'appelle simplement une classe vide. N'y a-t-il vraiment rien dans la classe vide ? Non, lorsqu'une classe n'écrit rien, le compilateur génère automatiquement les 6 fonctions membres par défaut suivantes.

2. Constructeur

2.1 La notion de constructeur

Jetons un coup d'œil à l'initialisation de la classe de date ici :

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;
}

résultat de l'opération :

Nous sommes nouveaux en C++, nous devons donc initialiser comme ceci.

Si nous instancions trop d'objets et oublions d'initialiser les objets, le résultat de l'exécution du programme peut être des valeurs aléatoires, ou des problèmes peuvent survenir.

Ici, le patriarche C++ y a pensé et a conçu un constructeur pour nous.

Regardons d'abord le résultat de l'oubli d'initialiser et d'imprimer directement :

Voici une valeur aléatoire, alors pourquoi est-ce? Regardons en bas.

Le constructeur est une fonction membre spéciale portant le même nom que le nom de la classe, qui est automatiquement appelée par le compilateur lors de la création d'un objet de type classe pour s'assurer que chaque membre de données a une valeur initiale appropriée, et n'est appelée qu'une seule fois dans toute la vie. cycle de l'objet.

2.2 Caractéristiques

Le constructeur est une fonction membre spéciale. Il convient de noter que bien que le nom du constructeur soit appelé construction, la tâche principale du constructeur n'est pas d'ouvrir de l'espace pour créer des objets, mais d'initialiser des objets.
Ses caractéristiques sont les suivantes :
1. Le nom de la fonction est le même que le nom de la classe.
2. Aucune valeur de retour (non vide, pas besoin d'écrire). 3. Le compilateur appelle automatiquement le constructeur correspondant
lorsque l'objet est instancié . 4. Le constructeur peut être surchargé.

Écrivons d'abord un constructeur de classe de date pour voir :

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;
};

Testons-le :

Le constructeur n'est pas appelé dans notre fonction principale, mais la marque que nous avons faite est imprimée ici, et ici nous avons expérimenté que le constructeur est automatiquement appelé lorsque l'objet est instancié.
Voyons ce qui se passe lorsque nous commentons le constructeur que nous avons écrit :

Nous pouvons voir qu'après avoir commenté, il peut toujours être imprimé, mais ce n'est qu'une valeur aléatoire. Parce que lorsque nous n'écrivons pas, le compilateur génère automatiquement un constructeur par défaut et l'appelle automatiquement.

C++ divise les types en types intégrés (types de base) : tels que int, char, double, int*... (le type personnalisé* l'est également) ;

Types personnalisés : tels que class, struct, union...

Et ici, nous pouvons voir que les membres des types intégrés ne seront pas traités.En C++ 11, les variables membres sont prises en charge pour donner des valeurs par défaut, ce qui peut être considéré comme remplissant des failles.

2.2.1 Surcharge de constructeurs :

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;
}

résultat de l'opération :

Remarque : Lorsque nous instancions un objet, lorsque le constructeur appelé n'a pas de paramètres, nous ne pouvons pas ajouter de parenthèses après l'objet, la syntaxe le stipule.

S'il est écrit comme ceci, le compilateur ne peut pas dire s'il s'agit d'une déclaration de fonction ou d'un appel . d2 ne sera pas confondu car il y a passage de valeur, et la déclaration de fonction n'apparaîtra pas de cette façon.

2.2.2 Tous les constructeurs par défaut :

Nous pouvons en fait combiner les deux constructeurs ci-dessus en un seul

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;
}

résultat de l'opération :

Le constructeur complet par défaut est le plus applicable. La construction sans argument et la valeur par défaut complète peuvent exister en même temps, mais il n'est pas recommandé d'écrire de cette façon. Bien qu'aucune erreur ne soit signalée, nous ne souhaitons pas transmettre de paramètres lors de l'appel de la valeur par défaut complète. Le compilateur ne sait pas quelle construction nous voulez appeler, ce qui entraînera une ambiguïté.

Examinons le problème de l'implémentation d'une file d'attente avec deux piles :

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;

};

Dans des circonstances normales, nous devons écrire le constructeur par nous-mêmes pour déterminer la méthode d'initialisation. Les variables membres sont toutes des types définis par l'utilisateur, nous pouvons donc envisager de ne pas écrire le constructeur. Le constructeur par défaut du type personnalisé sera appelé .

Résumé : Les constructeurs sans argument, les constructeurs par défaut complets et les constructeurs générés par défaut par le compilateur si nous ne les écrivons pas peuvent tous être considérés comme des constructeurs par défaut, et il ne peut y avoir qu'un seul constructeur par défaut (une coexistence multiple entraînera une ambiguïté) .

3. Destructeur

3.1 Le concept de destructeur

Destructeur : contrairement à la fonction du constructeur, le destructeur ne complète pas la destruction de l'objet lui-même, et la destruction de l'objet local est effectuée par le compilateur. Lorsque l'objet est détruit, il appellera automatiquement le destructeur pour terminer le nettoyage des ressources dans l'objet.

3.2 Caractéristiques

Le destructeur est une fonction membre spéciale et ses caractéristiques sont les suivantes :
1. Le nom du destructeur est le caractère ~ ajouté avant le nom de la classe.
2. Aucun paramètre et aucun type de retour.
3. Une classe ne peut avoir qu'un seul destructeur. S'il n'est pas explicitement défini, le système générera automatiquement un destructeur par défaut (le type intégré ne sera pas traité et le type personnalisé appellera son propre destructeur). Remarque : Les destructeurs ne peuvent pas être surchargés.
4. Lorsque le cycle de vie de l'objet se termine, le système de compilation C++ appellera automatiquement le destructeur.

Regardons d'abord le destructeur de la classe date :

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;
}

résultat de l'opération :

Nous pouvons voir ici que le destructeur est également appelé automatiquement.

Nous n'écrivons pas, le compilateur génère automatiquement un destructeur par défaut.

L'ordre d'appel du destructeur est similaire à celui de la pile, et ceux qui sont instanciés plus tard sont détruits en premier.

S'il n'y a pas d'application de ressource dans la classe, le destructeur ne peut pas être écrit, et le destructeur par défaut généré par le compilateur est utilisé directement, comme la classe Date ; lorsqu'il y a une application de ressource, elle doit être écrite, sinon elle sera provoquer une fuite de ressources, comme la classe Stack.

Faisons un dessin pour voir:

Le destructeur dans la pile remplace la destruction de la pile :

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;

};

Pour des choses comme la pile, notre destructeur remplace la fonction de destruction, et le destructeur sera appelé automatiquement. Dans le passé, nous devions appeler manuellement l'interface de la fonction de destruction, mais maintenant nous n'avons plus besoin de l'appeler.

Par conséquent, le plus grand avantage des constructeurs et des destructeurs est qu'ils sont appelés automatiquement.

4. Copier le constructeur

4.1 Le concept de constructeur de copie

Constructeur de copie : il n'y a qu'un seul paramètre formel , qui est une référence à l'objet de ce type de classe (généralement une décoration const), qui est automatiquement appelé par le compilateur lors de la création d'un nouvel objet avec un objet de type classe existant.

4.2 Caractéristiques

Le constructeur de copie est également une fonction membre spéciale et ses caractéristiques sont les suivantes :
1. Le constructeur de copie est une forme surchargée du constructeur.
2. Le paramètre du constructeur de copie est unique et doit être une référence au même type d'objet , et le compilateur provoquera des appels récursifs infinis lors de l'utilisation de la méthode pass-by-value.
3. S'il n'est pas explicitement défini, le compilateur générera un constructeur de copie par défaut. L'objet constructeur de copie par défaut est copié dans l'ordre des octets en fonction du stockage en mémoire. Ce type de copie est appelé copie superficielle ou copie de valeur.

La construction de copie est comme copier et coller.

Le paramètre du constructeur de copie est un seul et doit être une référence à un objet de type classe, et le compilateur provoquera des appels récursifs infinis lors de l'utilisation de la méthode pass-by-value.

La copie par valeur entraînera une récursivité infinie, écrivons donc un constructeur de copie.

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;
}

La copie du type intégré est une copie directe et la copie du type personnalisé doit être complétée en appelant la construction de copie.

Dans vs2019, le compilateur de passage des paramètres par valeur signalera une erreur :

Par conséquent, si nous écrivons un constructeur de copie, le paramètre formel doit être une référence du même type :

La référence est d'aliaser la variable, et l'ordre de l'appel automatique du destructeur est de détruire après la définition. Lors de la copie, d1 n'a pas été détruit, donc la référence peut être utilisée, de sorte qu'elle ne causera pas de copie récursive.

Masquons le constructeur de copie que nous avons écrit et voyons ce qui se passe :

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;
}

résultat de l'opération :

 Nous avons constaté que si nous ne l'écrivons pas, nous pouvons toujours le copier. C'est parce que si nous ne l'écrivons pas, le compilateur génère un constructeur de copie par défaut. Pour les copies superficielles comme la classe date, le constructeur généré par défaut peut réaliser la copie.

Regardons à nouveau la construction de copie de la pile :

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;
}

Ici, nous écrivons la construction de copie pour la pile, essayons la construction de copie :

Pourquoi une exception est-elle levée ici ?

Déboguons et voyons :

Ici, nous pouvons voir que l'adresse de _a de s1 est la même que celle de _a de s2. Lorsque s2 est copié, il sera détruit. Après la libération de _a de s2, s1 appellera à nouveau le destructeur. Lorsque vous relâchez _a, l'espace de _a a été libéré, ce qui provoquera une exception de pointeur nul.

Par conséquent, pour les objets avec application spatiale, une copie approfondie doit être effectuée lors de l'écriture de la construction de copie.

Corrigeons le code :

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;
};

résultat de l'opération :

Résumé : Comme Date, nous n'avons pas besoin d'implémenter la structure de copie, et celle générée par défaut peut être utilisée ; Stack a besoin de nous pour implémenter la structure de copie de copie profonde, et celle par défaut causera des problèmes ; il n'est pas nécessaire pour écrire une copie pour tous les membres des types personnalisés Construction, le constructeur de copie du type personnalisé sera appelé.

Extension:

Je suppose que tu aimes

Origine blog.csdn.net/Ljy_cx_21_4_3/article/details/132089939
conseillé
Classement