Notes sur le langage C++

  1. Flux d'entrée/sortie

Pour les opérations d'entrée/sortie en C, nous utilisons souvent

#include <stdio.h>


Dans C++, le style de nommage des fichiers d'en-tête est différent sans l'extension .h

#include <iostream>

using namespace std;


Explication de l'utilisation de ces deux lignes (blog personnel)

Pourquoi utiliser <iostream> et using namespace std ; dans C++ - Blog CSDN

cin cout

Ces fonctions sont définies dans l'espace de nom std

#include
using namespace std;
int main() {
int a;
cin>>a;
cout << "La valeur de a est :" << a << endl;
return 0;
}


#include <iostream>
int main() {
	int a = 10;
  	 std::cin >>a;
	std::cout << "La valeur de a est :" << a << std::endl;
	return 0;
}


Fonction ceil() pour arrondir vers le haut

Fonction round() pour arrondir à l'entier le plus proche

#include <iostream>

using namespace std;
int main() {
	int a = 10;
	cout<<ceil(3/2.0)<<endl;
	return 0;
}


  1. Type bool

Le type bool n'existe pas en C, il est un type de données intégré en C++

Utilise un seul octet

0 signifie false

1 signifie true

Tout nombre non nul est vrai

  1. Signification d'un objet

Un espace mémoire typé

  1. Allocation de mémoire dans la zone de tas avec new (et delete)

En C, pour allouer de la mémoire dans la zone de tas, nous utilisons malloc, et pour libérer, nous utilisons free

Si nous ne libérons pas, cette mémoire devient inutilisable par la suite, entraînant une fuite de mémoire

En C++, nous utilisons new pour allouer de la mémoire dans la zone de tas et delete pour la libérer

#include <iostream>

using namespace std;
int main() {
	//int* p = (int*)malloc(sizeof(int)); En C
	int* p = new int;
	return 0;
}


La fonction new et malloc ont en commun le fait qu'elles retournent l'adresse du premier élément de l'espace alloué

Mais elles diffèrent dans leur type de retour : new retourne automatiquement le type approprié, tandis que malloc retourne void* nécessitant un cast explicite

#include <iostream>

using namespace std;
int main() {
	//int *p=(int*)malloc(sizeof(int)*4);
	int* p = new int[4] {1,2,3,4};//Alloue 4 espaces de type int dans la zone de tas et retourne l'adresse du premier élément
	for (int i = 0; i < 4;i++) {
		cout << p[i] << " ";
	}
	return 0;
}


Remarque : lors de l'allocation, l'initialisation est impossible avec malloc en C, mais possible avec new en C++

Il n'y a pas d'égalité entre les accolades

Sans initialisation, les données sont aléatoires

delete

#include <iostream>

using namespace std;
int main() {
	//int *p=(int*)malloc(sizeof(int)*4);
	int* p = new int[4] {1,2,3,4};//Alloue 4 espaces de type int dans la zone de tas et retourne l'adresse du premier élément
	for (int i = 0; i < 4;i++) {
		cout << p[i] << " ";
	}
	delete[]p;//Libère toute la zone mémoire pointée par p
	//delete p//Ne libère que le premier élément, contrairement à free()
    //Si ce n'est pas la zone de tas pointée par p, cela provoque une fuite de mémoire
    p=NULL;
	return 0;
}


#include <iostream>

using namespace std;
int main() {
	//int *p=(int*)malloc(sizeof(int)*4);
	int* p = new int(3);//Initialise un seul élément
	return 0;
}


  1. Qu'est-ce qu'une référence ?

En syntaxe C++, une référence est un alias pour une variable, ne consommant pas d'espace

  1. Format de référence

Type& NomDeReference = EntitéRéférence

  1. Points importants sur les références

a. Une référence doit être initialisée dès sa déclaration, et ne peut pas être initialisée à null

Une référence est un alias pour une variable, donc il faut nécessairement une variable réelle

Initialiser une référence à null n'a aucun sens

c. La relation de référence ne peut pas être modifiée

Cela est lié à la nature d'une référence

Une référence est en réalité une constante pointeur, un pointeur dont la direction ne peut pas changer, donc elle ne peut pas modifier sa cible

int& b=a;
int* const b=&a;


#include <iostream>

using namespace std;

int main() {
	int a = 5;
	int& b3;//Une référence doit être initialisée
	int& b2 = NULL;//Une référence ne peut pas être initialisée à null
	int& b1 = a;
	int* const b1 = &a;//Essentiellement --> pointeur constant (la direction ne change pas, mais le contenu peut être modifié)
	cout << b1 << " " << *b2;
	return 0;
}


  1. Valeurs gauche et valeur droite en C++

Valeur gauche

Une valeur accessible et ayant un nom de variable

Valeur droite

Une valeur non accessible, généralement sans nom de variable

Référence gauche

int a=10;//a est une valeur gauche, a a un nom et est accessible (&x trouve l'adresse)
int& b=a;


Référence droite

#include <iostream>
using namespace std;
int main() {
	int&& c = 10;//10 est une valeur droite, non accessible (&10 est incorrect) 
	return 0;
}


Référence universelle

Type const & NomDeReference = EntitéRéférence

int main() {
	const int a = 10;
	const int& b = a;
	const int& c = 10;
	return 0;
}


  1. Applications des références

Utilisation comme paramètre de fonction

Il y a deux modes de passage des paramètres :

Passage par valeur (simple copie), mais la valeur passée peut être une adresse ou une donnée de base

Passage par référence

#include <iostream>

using namespace std;
void text1(int a,int b) {
	int t;
	t = a;
	a = b;
	b = t;
}//Passage par valeur
void text2(int*a,int*b) {
    assert(a);
    assert(b);
	int t;
	t = *a;
	*a = *b;
	*b = t;
}//Passage par adresse
void text3(int& a,int& b) {//int& a=a; int& b=b; 
	int t;
	t = a;
	a = b;
	b = t;
}//Passage par référence
void text4(int* const a,int* const b) {//int* const a=&a; int* const b=&b;
	int t;
	t = *a;
	*a = *b;
	*b = t;
}
int main() {//Utiliser une référence comme paramètre permet de modifier directement les variables passées
	int a = 5;
	int b = 10;
	text1(a, b);//Passage par valeur
	text2(&a,&b);//Passage par adresse
	text3(a,b);//Passage par référence
	text4(&a,&b);
	return 0;
}


Avantages

Les pointeurs doivent être vérifiés pour éviter les erreurs, mais les références ne le nécessitent pas

Utilisation comme valeur de retour de fonction

Aucune copie n'est créée en mémoire

  1. Différences entre références et pointeurs

  1. Une référence doit être initialisée, contrairement aux pointeurs qui peuvent être non initialisés (pointeurs sauvages)
  2. Une référence ne peut pas être initialisée à null, contrairement aux pointeurs qui peuvent l'être (pointeurs nuls)
  3. La relation entre référence et variable ne peut pas être changée, contrairement au pointeur qui peut pointer vers un autre endroit
  4. La taille d'une référence est celle du type de données, tandis que les pointeurs occupent 4 ou 8 octets
#include <iostream>
using namespace std;
int main() {
	int a = 10;
	int& b = a;
	int* c = &a;
	cout<<"Taille de la référence : "<< sizeof(b)<<endl<<"Taille du pointeur : "<<sizeof(c)<<endl;
	return 0;
}


  1. Il n'y a pas de références multiples, mais plusieurs niveaux de pointeurs existent
  2. L'incrémentation d'une référence incrémente la valeur de l'espace mémoire référencé, tandis que l'incrémentation d'un pointeur déplace le pointeur
  1. Avantages de l'utilisation de références comme valeurs de retour de fonction

Évite l'appel au constructeur de copie, économisant du temps

  1. Paramètres par défaut de fonctions

Pile de fonctions

L'appel de fonction consiste à transmettre des arguments de la fonction appelante à la fonction appelée, puis à recevoir le résultat de la fonction appelée par la fonction appelante

Ce processus utilise une pile de fonctions

La pile grandit vers le bas, c'est-à-dire de haute adresse vers basse adresse

Le tas grandit vers le haut, de basse adresse vers haute adresse

Les paramètres formels sont poussés dans la pile de droite à gauche, alors que les arguments sont transmis de gauche à droite (ordre inverse de la sortie de la pile)

Les paramètres par défaut sont appliqués pendant l'appel de fonction, avant que les arguments soient transmis aux paramètres formels

Format : TypeRetour NomFonction(Paramètre=ValeurParDéfaut) { }

#include

using namespace std;
int func(int a = 1, int b = 2, int c = 3) {//Paramètres par défaut
cout << "Bonjour" << endl;
}
int func1(int a = 1, int b, int c = 3) {
cout << "OK" << endl;
}
int main() {
func(3, 2, 1);
//Attendu : 30 20 10
func1(2, 3);
return 0;
}


Analysons l'exécution : lorsque la fonction est appelée, une nouvelle pile de fonctions est créée. Les paramètres formels sont poussés dans l'ordre c, b, a (de droite à gauche). Ensuite, les arguments sont transmis de gauche à droite.

Note : si un paramètre a une valeur par défaut, tous les paramètres suivants doivent également avoir une valeur par défaut

Raisons : Si vous pensez que 2 est passé à b, en réalité, il est transmis à a, car les arguments sont transmis de gauche à droite. Cela provoquera une erreur.

#include <iostream>

using namespace std;
int func1(int a = 1, int b, int c = 3) {
	cout << "OK" << endl;
}
int main() {

	func1(2);
	return 0;
}


  1. Surcharge des fonctions

Pourquoi C++ a-t-il une surcharge de fonctions, mais pas C ?

Cause : La composition des fonctions en C et C++ diffère

En C, une fonction est composée uniquement de son nom

En C++, une fonction est composée de son nom + ses paramètres

Ainsi, en C++, des fonctions ayant le même nom mais des paramètres différents sont différentes, contrairement à C où toute fonction portant le même nom est identique

Donc, C n'a pas de surcharge de fonctions

Utilité de la surcharge des fonctions

Permet d'utiliser le même nom de fonction, améliorant la réutilisabilité

Lors de l'appel à une fonction avec le même nom, la distinction se fait selon la liste des paramètres

Différences dans la liste des paramètres : nombre de paramètres, ordre des paramètres, types des paramètres, etc.

#include

using namespace std;
void fun(int a, int b) {
cout << 1 << endl;
}
void fun(int a, float b) {
cout << 2 << endl;
}
void fun(double a) {
cout << 3 << endl;
}
void fun(float a) {
cout << 4 << endl;
}
int fun(int a, double b) {
return 5;
}
//int fun(float a) {
//	cout << 5 << endl;
//}//Erreur : la surcharge ne tient pas compte du type de retour
void fun(int a, int b, int c) {
cout << 5 << endl;
}
int main() {
fun(1, 1);
fun(1, 1.1f);
cout << fun(1, 1.1)<

using namespace std;
void func(int &b) {
cout << "1" << endl;
}
void func(const int &b) {
cout << "2" << endl;
}
void func(int&& b) {
cout << "3" << endl;
}
int main() {
int a = 2;
func(a);
func(2);
return 0;
}


Les références comprennent les références gauche, droite et universelles

Elles peuvent servir de critère pour distinguer les paramètres des fonctions

Surcharge des fonctions avec paramètres par défaut

Regardons l'exemple ci-dessous : cela respecte la définition de surcharge (même nom, listes de paramètres différentes), mais observez que la première fonction a un paramètre par défaut. Théoriquement, elle pourrait recevoir deux valeurs, mais la deuxième fonction prend également deux paramètres, ce qui empêche le compilateur de déterminer quelle fonction utiliser

#include

using namespace std;
void func(int a,int b,int c=1) {
cout << "1" << endl;
}
void func(int a,int b) {
cout << "2" << endl;
}
int main() {
func(1,2);
return 0;
}


Une fonction peut être surchargée indépendamment de son type de retour

#include

using namespace std;
void func(int a, int b) {
coutt << "1"<
struct Student {
int a;
int b;
void show() {
//Invalide
}
};
int main() {

return 0;
}


#include <iostream>

using namespace std;

struct Student {
	int a;
	int b;
	void show() {
		//Valide
	}
};
int main() {
	
	return 0;
}


À partir de cet exemple, nous voyons que

Le type struct en C ne peut pas définir de fonctions, mais en C++, il peut

Regardons l'exemple 2

Exemple 2

#include <stdio.h>

struct Student {
	int a;
	int b;
};
int main() {
	struct Student s1;//Valide
	Student s1;//Invalide
	return 0;
}


#include <iostream>

using namespace std;

struct Student {
	int a;
	int b;
	void show() {
		//Valide
	}
};
int main() {
	struct Student s1;//Valide
	Student s1;//Valide
	return 0;
}


À partir de l'exemple 2, nous voyons qu'en C, sans typedef, on ne peut définir une variable de structure que par struct Student, tandis qu'en C++, les deux méthodes sont valides

En C, le nom d'une structure ne peut pas être utilisé pour définir une varible, mais en C++, le nom d'une structure peut définir une variable

En combinant ces deux points, nous voyons que C++ a amélioré le type struct

"Amélioration" –> Amélioration des fonctions et de la définition de variables, rendant tout plus pratique

Cette amélioration transforme struct en classe en C++

C'est le sujet de notre journée, qui introduit

  1. Classes

On peut considérer une classe comme une version améliorée de struct en C

La seule différence entre struct et classe est l'accès par défaut et l'héritage

Trois caractéristiques de la programmation orientée objet en C++

1. Encapsulation

2. Héritance

3. Polymorphisme

!!! En C++, tout est un objet, possédant des propriétés et des comportements

Propriétés –> Variables membres

Comportements –> Fonctions membres

Encapsulation

Format : class NomClasse { PermissionsAccès : Propriétés, Comportements };

N'oubliez pas le point-virgule !!

int a=10;


Et int a = 10, il faut ajouter le point-virgule

Signification : Rassembler les propriétés et comportements ensemble avec un contrôle d'accès

En C, trop de liberté permettait de modifier directement les variables de la structure depuis la fonction principale

En C++, elles sont encapsulées avec un contrôle d'accès

Permissions d'accès : Publiques, protégées et privées

1.public

2.protected

3.private

En C++, la permission d'accès par défaut d'une classe est private

En C, la permission d'accès par défaut d'une structure est public, pour compatibilité avec la structure en C

#include
using namespace std;
struct Student {
//La structure en C n'a pas précisé de permissions, donc par défaut public
//La classe en C++ n'a pas précisé de permissions, donc par défaut private
string _name;
string _num;
void Set_name(string name) {
_name = name;
}
void Set_num(string num) {
_num = num;
}
void show() {
cout << _name << endl << _num << endl;
}
};
int main() {
Student s1;//Valide
s1._name = "123";//Valide, par défaut public
return 0;
}


#include<iostream>
using namespace std;
class Student {
	//La structure en C n'a pas précisé de permissions, donc par défaut private
	//La classe en C++ n'a pas précisé de permissions, donc par défaut private
	string _name;
	string _num;
	void Set_name(string name) {
		_name = name;
	}
	void Set_num(string num) {
		_num = num;
	}
	void show() {
		cout << _name << endl << _num << endl;
	}
};
int main() {
	Student s1;//Valide
	s1._name = "123";//Invalide, par défaut private, inaccessible
	return 0;
}


Portée de la classe

public –> Portée publique : Fonctions internes, classes dérivées et objets
protected –> Portée protégée : Fonctions internes et classes dérivées
private –> Portée privée : Fonctions internes

Mot-clé this

Regardons un exemple

#include<iostream>
using namespace std;
class Student {
	//La structure en C n'a pas précisé de permissions, donc par défaut private
	//La classe en C++ n'a pas précisé de permissions, donc par défaut private
private:
	string _name;
	string _num;
public:
	void Set_name(string name) {
		_name = name;
	}
	void Set_num(string num) {
		_num = num;
	}
	void show() {
		cout << _name << endl << _num << endl;
	}
};
int main() {
	Student s1;//Valide
	Student s2;
	s1.Set_name("syc");//Instruction 1
	s2.Set_name("lh");//Instruction 2
	return 0;
}


L'instruction 1 assigne la chaîne "syc" à l'_name de s1

L'instruction 2 assigne la chaîne "lh" à l'_name de s2

Le pointeur this contient l'adresse de s1 et s2

Le mot-clé this est un pointeur vers l'objet courant. Qui appelle la fonction, this pointe vers lui
Essentiel : Le pointeur this est en réalité un paramètre implicite de la fonction membre, reçue par le compilateur via le registre ecx, sans besoin de transmission manuelle
#include<iostream>
using namespace std;
class Student {
private:
	string _name;
	string _num;
public:
	void Set_name(Student* const this1,string name) {//Pointeur constant, direction fixe
		this1->_name = name;
	}
	void Set_num(Student* const this2,string num) {
		this2->_num = num;
	}
	void show() {
		cout << _name << endl << _num << endl;
	}
};
int main() {
	Student s1;//Valide
	Student s2;
	s1.Set_name(&s1,"syc");
	s2.Set_name(&s2,"lh");
	return 0;
}


Caractéristiques : Les caractéristiques du pointeur this ont été largement expliquées dans le module précédent. Nous résumons ici :

1.Type du pointeur this : Pointeur constant vers l'objet appelant

2.Ne peut être utilisé que dans les fonctions membres

3.Le pointeur this est en réalité un paramètre implicite de la fonction membre, reçu par le compilateur. L'objet ne stocke pas le pointeur this.

4.C'est le premier paramètre implicite de la fonction membre, généralement transmis automatiquement par le compilateur via le registre ecx, sans intervention de l'utilisateur

Portée de la classe et alignement de la structure

Note : Le calcul de la taille d'une classe ne comprend pas les fonctions membres, seulement les variables membres

  1. Constructeur

Qu'est-ce qu'un constructeur ?

Un constructeur est une fonction qui attribue des valeurs aux variables membres lors de la création d'un objet

<Remarque>

Le constructeur attribue des valeurs aux variables membres, pas les initialise

Car l'objet existe déjà, les variables membres sont instanciées, puis le constructeur attribue des valeurs

Format du constructeur

NomClasse+()

Observer ce format, on peut voir les similarités et différences avec une fonction normale

Similitudes : Il a un nom de fonction, une liste de paramètres et un corps de fonction

Différences : Il n'a pas de type de retour et le nom doit être le nom de la classe

Le nom de la classe comme nom de fonction permet de distinguer clairement le constructeur

Constructeur par défaut sans paramètres

Explication : Si aucune fonction constructeur n'est définie dans la classe, le compilateur fournit automatiquement un constructeur par défaut sans paramètres

<Remarque> : Si un constructeur est défini, le compilateur ne fournit pas le constructeur par défaut

Classification et appel des constructeurs

Classification

Selon les paramètres : Constructeur avec paramètres, constructeur sans paramètres

Selon le type : Constructeur normal, constructeur de copie

//Classification des constructeurs
Pointer() {//Constructeur sans paramètre
	cout << "Appel du constructeur sans paramètre" << endl;
}
//explicit 
Pointer(int* _p1) {//Constructeur avec paramètres interdit pour l'appel implicite
	cout << "Appel du constructeur avec paramètre, allocation de mémoire dans la zone de tas" << endl;
}
Pointer(int a) {//Constructeur avec paramètres
	p1 = new int[a];
	cout << "Appel du constructeur avec paramètre" << endl;
}


Appel du constructeur
  1. Appel avec parenthèses (courant)
  2. Appel explicite
  3. Appel implicite

Comment interdire l'appel implicite –> Utiliser le mot-clé explicit

int main() {
	//Classification de l'appel du constructeur
	int a=2;
	int* pt = &a;
	Pointer s1(pt);			 //Appel avec parenthèses
	Pointer s2 = Pointer(pt);//Appel explicite
	Pointer s3=pt;			 //Appel implicite --> ne fonctionne que pour les constructeurs avec un seul paramètre
	//Mot-clé explicit pour interdire l'appel implicite
	Pointer* s4 = new Pointer(a);
	delete s4;
	return 0;
}


Pointer(int a) {//Constructeur avec paramètres
	p1 = new int[a];
	cout << "Appel du constructeur avec paramètre" << endl;
}
Pointer* s4 = new Pointer(a);


Le schéma de partition mémoire de ce code est comme suit

Implémentation concrète du constructeur

#include
#include
using namespace std;
class Pointer {
public:
int* p1;
int p2;
//Constructeur par défaut simulé, quand il n'y a pas de constructeur dans la classe, le compilateur génère automatiquement
Pointer() {
cout << "Constructeur sans paramètre" << endl;
}
//Constructeur avec paramètres
Pointer(int n) {
if (n) {
p1 = new int[n];
cout << "Utilisation du constructeur avec paramètre, allocation de " << n << " espaces de type int dans la zone de tas, l'attribut p1 pointe vers cet espace" << endl;
}
else
cout << "n est invalide" << endl;
}
//Surcharge du constructeur, le deuxième et troisième constructeurs ont le même nom mais des listes de paramètres différentes, conformément à la surcharge des fonctions
Pointer(int a, int b) {
p2 = a + b;
cout << "Le constructeur a été surchargé !" << endl;
}
//Vérifier si la fonction peut être surchargée, attention aux paramètres par défaut
//Pointer(int a, int b, int c = 1) {
//	cout << "";
//}//Conforme à la surcharge des fonctions, mais avec paramètre par défaut, le compilateur est perdu, ne sait pas quel constructeur appeler
};
int main() {
Pointer s1;//Appel du constructeur par défaut
Pointer s2();//Ressemble à un constructeur sans paramètre mais le compilateur le reconnaît comme une déclaration de fonction
Pointer s3(5);//Appel du constructeur avec paramètres
Pointer s4(1, 4);//Surcharge du constructeur avec paramètres et paramètre par défaut
//Pointer s5(1, 3, 5);//Surcharge du constructeur
return 0;
}


Remarque

Surcharge des constructeurs

Les constructeurs peuvent être surchargés comme les fonctions normales, avec le même nom de classe et des listes de paramètres différentes

Mais attention, comme les fonctions normales, lorsqu'il y a des paramètres par défaut, il faut vérifier si cela respecte la surcharge

Regardez ce code :

Pointer() {
cout << "Constructeur sans paramètre" << endl;
}
Pointer s2();//Erreur, ressemble à un constructeur sans paramètre mais le compilateur le reconnaît comme une déclaration de fonction
Pointer s2;//Correct


Ressemble-t-il à un constructeur sans paramètre ? Mais le compilateur le reconnaît comme une déclaration de fonction

Lors de l'appel d'un constructeur par défaut, n'utilisez pas de parenthèses !! Sinon le compilateur le reconnaîtra comme une déclaration de fonction !!!

  1. Destructeur

Qu'est-ce qu'un destructeur ?

Pour libérer l'espace mémoire pointé par les variables membres dans la zone de tas

Quand le destructeur est-il appelé ?

Avant la destruction de l'objet, le compilateur appelle automatiquement le destructeur

Format du destructeur

~NomClasse() { }

Comparé au constructeur, il y a un ~ pour indiquer qu'il s'agit d'un destructeur

Le destructeur ne peut pas être surchargé, sa liste de paramètres est vide

~Pointer() {
cout << "Destructeur appelé" << endl;
}


Pointer* s5= new Pointer();
delete s5;


delete appelle d'abord le destructeur, libère l'espace mémoire pointé par les variables membres, puis appelle free pour libérer l'espace mémoire de l'objet

  1. Différences entre new et malloc

En ce qui concerne les valeurs de retour

new retourne automatiquement l'adresse convertie

malloc retourne l'adresse qui nécessite un cast explicite

En ce qui concerne les noms

new est un opérateur, il peut surcharger l'opérateur (operator) pour personnaliser

malloc est une fonction de bibliothèque en C, C n'a pas de surcharge de fonctions, donc malloc ne peut pas être surchargé

En ce qui concerne les paramètres

new ne nécessite pas de paramètre, sa taille est calculée par le compilateur selon le type

malloc nécessite un paramètre, la taille en octets de l'espace à allouer

En ce qui concerne le bas niveau

new appelle malloc d'abord, alloue dans la zone de tas, si le type est une classe, appelle le constructeur pour attribuer des valeurs aux variables membres

malloc alloue simplement dans la zone de tas

En ce qui concerne les conséquences d'une erreur d'allocation

new échoue et lance une exception

malloc retourne un pointeur null

En ce qui concerne l'expansion de mémoire

new ne supporte pas l'expansion de mémoire

malloc peut utiliser realloc pour étendre la mémoire

  1. Différences entre delete et free

1. En ce qui concerne les paramètres –> delete ne nécessite pas de paramètre, free en nécessite un

2. En ce qui concerne le bas niveau

delete appelle d'abord le destructeur, puis free

Processus : delete appelle d'abord le destructeur pour libérer l'espace mémoire pointé par les variables membres de l'objet, puis appelle free pour libérer l'espace mémoire de l'objet

free ne fait pas appel au destructeur, il libère uniquement l'espace mémoire de l'objet

Question 1 : Pourquoi delete n'appelle-t-il pas d'abord free ??

Pointer() {
		cout << "Constructeur sans paramètre" << endl;
	}
Pointer s2();//Erreur, ressemble à un constructeur sans paramètre mais le compilateur le reconnaît comme une déclaration de fonction
Pointer s2;//Correct


Regardez ce code, son schéma de partition mémoire est comme suit

Si nous appelions d'abord free, l'espace mémoire de l'objet serait libéré, donc l'espace mémoire pointé par les variables membres ne serait plus accessible, causant une fuite de mémoire

Mais si nous appelons d'abord le destructeur, nous libérons d'abord l'espace mémoire pointé par les variables membres, évitant ce problème

!!! Donc delete appelle d'abord le destructeur, puis free

Question 2 : new peut-il être utilisé avec free ?

new devrait être utilisé avec delete –> free + destructeur

Donc, lorsque les variables membres ne sont pas allouées dans la zone de tas, c'est-à-dire que le destructeur n'est pas actif, delete peut remplacer free. Mais évitez de le faire, cela complique les choses !

  1. Liste d'initialisation est utilisée dans les constructeurs

  1. Format de la liste d'initialisation

La liste d'initialisation est étroitement liée au constructeur

Après la liste des paramètres du constructeur, ajoutez : variableMembre(valeurListeParamètres), variableMembre(valeurListeParamètres)...

A(int a, int b, int c) :_b(b),_a(_b), _c(c) {//Liste d'initialisation
	cout << _a << " " << _b << " " << _c << endl;
}


  1. Fonction de la liste d'initialisation ?

1. Initialiser les variables membres héritées de la classe parente

Lorsque la classe parente n'a pas de constructeur sans paramètre, il faut appeler explicitement le constructeur de la classe parente en utilisant le nom de la classe

2. Initialiser les variables membres de la classe courante

Variables non membres, initialisation par nomVariable(valeur)

Variables membres de classe, initialisation par objet.constructeur(valeur)

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class A{
    int a1;
    int a2;
public:
    A(int _a1,int _a2){
        a1=_a1;
        a2=_a2;
    }
};
class B:public A{
    int b1;
    A a3;
public:
    B(int _a1,int _a2,int _b1,int _a31,int _a32):
    //Liste d'initialisation --> 1. Initialiser les variables membres héritées de la classe parente, doit être appelée explicitement avec le nom de la classe
    //            2. Initialiser les variables membres de la classe courante, les variables membres de classe par objet.constructeur(valeur)
    A(_a1,_a2),a3(_a31,_a32){
        b1=_b1;
    }
};
void text2_01(){
    B b(1,2,3,4,5);
    //Appel du constructeur sans paramètre sans parenthèses
} 


  1. Pourquoi utiliser la liste d'initialisation ?

Cause 1 : Les variables membres de type const et les références doivent être initialisées, mais les constructeurs attribuent des valeurs, pas les initialisent. Cela entraîne une erreur de compilation

Cause 2 : Si une classe A contient une classe B, et que B a un constructeur, le compilateur ne fournit pas de constructeur sans paramètre par défaut. Sans liste d'initialisation, cela entraînerait une erreur

Cause 3 : Utiliser la liste d'initialisation est plus efficace que l'affectation normale

  1. Caractéristiques de la liste d'initialisation

a. Ne peut être utilisée que dans un constructeur

Parce que l'initialisation des objets se produit uniquement lors de la création de l'objet, et la création d'objets appelle le constructeur

Le constructeur a deux étapes d'exécution

  1. Phase d'initialisation : Toutes les variables membres sont initialisées, qu'elles soient dans la liste d'initialisation ou non
  2. Phase d'affectation : Affectation selon le contenu du corps de la fonction

b. L'ordre d'initialisation des variables membres dépend de leur ordre de déclaration dans la classe, pas de l'ordre dans la liste d'initialisation

int _a;
int _b;
int _c;
A(int a, int b, int c) :_b(b),_a(_b), _c(c) {//Liste d'initialisation
	cout << _a << " " << _b << " " << _c << endl;
}
A(1, 2, 3);


c. Les variables const, les références et les variables membres de classe doivent être initialisées dans la liste d'initialisation

Parce qu'ils doivent être initialisés lors de la création !!!

Les variables de type const et les références doivent être initialisées, sinon cela provoque une erreur

d. Objets de classe comme membres

B contient un membre de classe A, il faut créer le membre de classe avant d'appeler le constructeur de B, puis affecter les membres de B

Appeler d'abord le constructeur de la classe membre, puis le constructeur de la classe courante, et enfin le destructeur de la classe courante avant celui de la classe membre

#include <iostream>

using namespace std;
class A {
	int a;
	int b;
	int c;
public:
	A(int _a, int _b, int _c) {
		a = _a;
		b = _b;
		c = _c;
		cout << "Appel du constructeur de A" << endl;
	}
	~A() {
		cout << "Appel du destructeur de A" << endl;
	}
};
class B {
	int x1;
	int x2;
	const int cn;
	int& m;
	A s;
public:
	B(int _x1,int _x2,int _cn, int _m, int _a, int _b, int _c) :cn(_cn), m(_m), s(_a, _b, _c) {
		x1 = _x1;
		x2 = _x2;
		cout << "Appel du constructeur de B" << endl;
	}
	~B() {
		cout << "Appel du destructeur de B" << endl;
	}
};

int main() {
	int x = 1;
	B b(6,7,1, x, 2, 3, 4);
	return 0;
}


  1. Variables membres statiques

Format : Dans la classe → Ajouter static devant le type de variable membre

Une variable membre statique ne peut être initialisée qu'une fois

Elle est stockée dans la zone statique, ne prend pas d'espace dans l'objet, est allouée à la compilation, avant la fonction principale

Une variable membre statique appartient à la classe, pas à l'objet, plusieurs objets partagent la même variable membre statique

Doit être définie dans la classe et initialisée en dehors de la classe

Ne peut pas utiliser le constructeur pour l'initialiser, car le constructeur est pour la classe, pas pour l'objet

Peut être accédée via le nom de l'objet ou le nom de la classe

//NomObjet.variableMembreStatique
//NomClasse::variableMembreStatique


Les variables membres statiques sont partagées entre classes parentes et enfants

Qu'est-ce qui s'exécute avant la fonction principale ?

L'initialisation des variables membres statiques et des variables globales

#include<iostream>

using namespace std;
class B {
public:
	B() {
		cout << "Constructeur de B" << endl;
	}
};
class A {
	int a;
public:
	static B m;//Membre de classe statique
	static int n;
	static void Show() {
		cout << "hello" << endl;
	}
};
int A::n = 0;//Initialisation en dehors de la classe
B A::m=B();
/*
* 1. Les variables membres statiques appartiennent à la classe, pas à l'objet, plusieurs objets partagent une variable membre statique
* 2. Les variables membres statiques sont définies dans la classe et initialisées en dehors
* 3. Peut être accédée via le nom de l'objet ou le nom de la classe
* 4. Les variables membres statiques sont allouées à la compilation, avant la fonction principale
* 5. Qu'est-ce qui s'exécute avant la fonction principale ?
* L'initialisation des variables membres statiques et des variables globales
*/
int main() {
	cout << "__________________________" << endl;
	A a1,a2,a3;
	a1.n++;
	a2.n++;
	a3.n++;
	A::n++;
	cout << A::n << endl;
	cout << a1.n << endl;
	cout << a2.n << endl;
	cout << a3.n << endl;
	return 0;
}


  1. Fonctions membres statiques

Les fonctions membres sont stockées dans la zone de code, ne prennent pas d'espace dans l'objet

Les fonctions membres statiques appartiennent à la classe, pas à l'objet

Peut être appelée via le nom de la classe ou le nom de l'objet

Les fonctions membres statiques n'ont pas de this, ne peuvent accéder qu'aux membres statiques

Parce qu'il n'y a pas de this, le compilateur ne peut pas déterminer sur quel objet agir

Les fonctions membres ne prennent pas d'espace dans l'objet, elles sont toutes dans la zone de code. Lorsqu'un objet appelle une fonction membre, il recherche le code dans la zone de code. Les fonctions membres utilisent this pour distinguer les objets concernés

#include <iostream>
 
using namespace std;
class A {
	int b;
	static int a;
public:
	static void func1() {

	}
	static void func() {
		this->b = 1;
		func1();
		a = 2;
	}
};
int A::a = 0;

int main() {

	return 0;
}


  1. En C++, un pointeur nul peut accéder aux fonctions membres

#include <iostream>

using namespace std;

class A {
	int a = 2;
public:
	void func1() {
		a = 1;
		cout << "1" << endl;
	}
	static void func2() {
		cout << "2" << endl;
	}
};
int main() {
	A* a = nullptr;
	return 0;
	a->func1();
	a->func2();
	A::func2();
}


Mais attention, si la fonction utilise this, le pointeur nul ne peut pas accéder à la fonction, car this est nul, et l'objet est vide

Pour assurer la robustesse du programme, on peut vérifier si la fonction est nulle

if(this==NULL) return ;


  1. Différences entre pointeur this dans les fonctions membres statiques et constantes

Fonction membre statique : Pas de this, ne peut pas accéder à l'objet, donc ne peut pas accéder aux variables membres non statiques

Fonction membre constante : A un this, mais de type pointeur constant, ne peut pas modifier les variables membres de l'objet

  1. Pourquoi avoir un pointeur this ?

—————————————————————————————————————————————————————

  1. La taille d'une classe vide est de 1 octet

Parce qu'une classe est un espace mémoire typé, donc si elle est vide, on lui donne 1 octet

  1. Objet constant

Format : const Type a;//Objet constant

Propriétés :

1. Comme il y a const, les variables membres de l'objet constant ne peuvent pas être modifiées

2. Un objet constant ne peut appeler que des fonctions constantes

3. Un objet non constant appelle d'abord les fonctions non constantes, puis les fonctions constantes si les premières n'existent pas

  1. Fonctions constantes

Format : Ajouter const après la liste des paramètres de la fonction membre

void get_nums() const {
		this->a = 2;
}


Propriétés :

1. Une fonction constante a un this, mais de type pointeur constant, ne peut pas modifier les variables membres de l'objet, ni appeler les fonctions non constantes

2. Les fonctions constantes et non constantes peuvent être surchargées

Si deux fonctions ont le même nom et les mêmes paramètres, sous quelles conditions se fait la surcharge ?

Lorsqu'une est constante et l'autre non, car le compilateur choisit selon si l'objet est constant ou non

3. Les variables membres déclarées avec mutable peuvent être modifiées dans les fonctions constantes

4. Les fonctions constantes ne peuvent être définies que dans la classe, hors de la classe cela est incorrect

5. Les fonctions constantes ne peuvent pas appeler les fonctions non constantes, car le this de la fonction constante est un pointeur constant, incompatible avec le type de this des fonctions non constantes

#include <iostream>

using namespace std;
class Person {
	mutable int a;
	char b;
public:
	Person() {
		cout << "Constructeur" << endl;
	}
	~Person() {
		cout << "Destructeur" << endl;
	}
	void get_nums() {
		this->a = 2;
	}
	void get_nums() const {
		this->a = 2;
	}
	void get_nums2() {//Fonction non constante
		//this est un pointeur constant --> Type* const this (la direction ne change pas, mais le contenu peut être modifié)
		a = 2;
	}
	void get_nums2cst() const {//Fonction constante
		//this est un pointeur constant --> const Type* const this (ni la direction ni le contenu ne peuvent être modifiés)
		a = 2;//Fonction constante, ne peut pas modifier les variables membres
	}
};
void show() const {//La fonction constante ne peut être définie que dans la classe
//Erreur!
}
int main() {
	Person p1;//Objet non constant
	const Person p2;//Objet constant --> les variables membres ne peuvent pas être modifiées
	p1.get_nums2();//Appel de la fonction non constante
	p1.get_nums2cst();//Appel de la fonction constante
	p2.get_nums2cst();//Appel de la fonction constante
	p2.get_nums2();//Un objet constant ne peut pas appeler une fonction non constante
	/*
	* Résumé : Un objet constant ne peut appeler que des fonctions constantes, un objet non constant appelle d'abord les fonctions non constantes, puis les constantes si les premières n'existent pas
	*/
	p1.get_nums();
	p2.get_nums();//Surcharge de fonctions, le compilateur choisit entre la fonction constante et non constante selon l'objet
	return 0;



<Remarque> : Si le nom de la fonction et la liste des paramètres sont identiques, la surcharge peut se produire car il y a des fonctions constantes et non constantes, le compilateur distingue selon l'objet constent ou non

  1. Différences entre pointeur this dans les fonctions constantes, non constantes et membres statiques

Dans les fonctions non constantes : this est un pointeur constant, la direction ne peut pas changer

Dans les fonctions constantes : this est un pointeur constant, la direction et le contenu ne peuvent pas changer, c'est pourquoi les fonctions constantes ne peuvent pas modifier les propriétés des membres

Dans les fonctions membres statiques : il n'y a pas de this, donc impossible de modifier les variables membres de l'objet

  1. Pourquoi utiliser friend ?

Une fonction extérieure à une classe (fonction globale) ou une classe souhaite accéder aux membres privés d'une autre classe, on utilise friend

Mot-clé : friend

  1. Trois cas d'utilisation de friend

1. Fonction globale comme friend

Dire à la classe que je suis votre ami, pouvez-vous me permettre d'accéder aux membres privés

#include <iostream>

using namespace std;
class Building {
	friend void GoodFreind(Building& b);
	string bedroom;//Privé
public:
	string sittingroom;
	Building();//Déclaration du constructeur dans la classe
	/*{
		this->bedroom = "Chambre";
		this->sittingroom = "Salon";
	}*/
};
Building::Building() {
	this->bedroom = "Chambre";
	this->sittingroom = "Salon";
}
void GoodFreind(Building& b) {//Quand une fonction globale veut accéder aux membres privés d'une classe, déclarer cette fonction comme friend
	cout << "Un ami entre" << b.sittingroom << endl;
	cout << "Un ami veut entrer dans" << b.bedroom << endl;
}

int main() {
	Building b;
	GoodFreind(b);
	return 0;
}


2. Classe comme friend

Friend peut aussi être une classe, c'est-à-dire qu'une classe peut être friend d'une autre classe. Cela signifie que toutes les fonctions membres de cette classe sont friend de la classe concernée.

Lorsqu'une fonction est friend d'une autre classe, elle peut accéder aux membres privés de cette classe

#include <iostream>

using namespace std;

class Building {
	friend Friend;//Friend est friend de Building
	string bedroom;//Privé
public:
	string sittingroom;
	Building();//Déclaration du constructeur dans la classe
};
class Friend {
	Building *b;
public:
	Friend();//Déclaration du constructeur dans la classe
	~Friend();
	void vist();7
};
Building::Building() {
	this->bedroom = "Chambre";
	this->sittingroom = "Salon";
}
Friend::Friend(){
	b = new Building();
}
Friend::~Friend() {
	if(b) delete b;
}
void Friend::vist() {//Une classe veut accéder aux membres privés d'une autre classe, la rendre friend
	cout << "Un ami veut entrer dans" << b->sittingroom;
	cout << "Un ami veut entrer dans" << b->bedroom;
}
int main() {
	Building b;
	return 0;
}


3. Fonction membre d'une classe comme friend

#include <iostream>

using namespace std;
class Friend {
	Building* b;
public:
	Friend();//Déclaration du constructeur dans la classe
	~Friend();
	void vist();
};

class Building {
	friend void Friend::vist();//La fonction membre de Friend est friend de Building
	string bedroom;//Privé
public:
	string sittingroom;
	Building();//Déclaration du constructeur dans la classe
};
class A{
public:
    void visit(); 
};
Building::Building() {
	this->bedroom = "Chambre";
	this->sittingroom = "Salon";
}
Friend::Friend(){
	b = new Building();
}
Friend::~Friend() {
	if(b) delete b;
}
void Friend::vist() {//Une classe veut accéder aux membres privés d'une autre classe, la rendre friend
	cout << "Un ami veut entrer dans" << b->sittingroom;
	cout << "Un ami veut entrer dans" << b->bedroom;
}
int main() {
	Building b;
	return 0;
}


  1. Avantages et inconvénients des friends

Avantages : Accès aux membres privés de la classe

Inconvénients : Brise l'encapsulation

Deux méthodes pour accéder aux membres privés d'une classe

1. Appel des fonctions get/set via l'objet

2. Déclaration de la classe et de la fonction globale comme friend

Il y a deux façons de surcharger les opérateurs : soit dans la classe (fonction membre), soit en dehors (fonction globale). La surcharge dans la classe peut réduire un paramètre, mais tous les opérateurs ne peuvent pas être surchargés dans la classe, par exemple << >>

  1. Surcharge de l'opérateur '+'

Retour par valeur

Pourquoi surcharger l'opérateur d'addition ?

Parce que l'opérateur d'addition fourni par C++ ne peut gérer que les additions de types de données simples. Si on veut additionner deux objets de la même classe, cela provoquera une erreur

Pour permettre l'addition de deux objets de la même classe, on doit encapsuler ce processus dans une fonction, avec juste un mot-clé supplémentaire operator pour aider le compilateur à identifier cette fonction de surcharge d'opérateur

Utilité : Permettre l'addition de deux opérateurs personnalisés

Surcharge de l'opérateur dans une fonction membre

Peut réduire un paramètre

#include <iostream>

using namespace std;

class Box {
	int length;
	int width;
	int height;
public:
	Box() {
		length = 0;
		width = 0;
		height = 0;
	}
	Box(int length,int width,int height) {
		this->length = length;
		this->width = width;
		this->height = height;
	}
	Box(const Box& other) {
		length = other.length;
		width = other.width;
		height = other.height;
	}
	//Surcharge de l'opérateur d'addition dans une fonction membre
	Box operator+(const Box& other) {
		Box p;
		p.length = this->length + other.length;
		p.width = this->width + other.width; 
		p.height = this->height + other.height;
		return p;
	}
};
int main() {
	Box a(1,2,3);
	Box b(2, 3, 4);
	Box c = a + b;//Appel direct
	Box d;
	d = a.operator+(b);//Appel de la fonction de surcharge d'opérateur
	return 0;
}


Surcharge de l'opérateur dans une fonction globale

#include <iostream>

using namespace std;

class Box {
	int length;
	int width;
	int height;
	friend Box operator+(const Box& other1, const Box& other2);
	friend Box operator+(const Box& other1, int val);
public:
	Box() {
		length = 0;
		width = 0;
		height = 0;
	}
	Box(int length,int width,int height) {
		this->length = length;
		this->width = width;
		this->height = height;
	}
	Box(const Box& other) {
		length = other.length;
		width = other.width;
		height = other.height;
	}
};
//Surcharge de l'opérateur d'addition dans une fonction globale
Box operator+(const Box& other1,const Box& other2) {//Impossible d'accéder aux membres privés sans friend, il faut déclarer la fonction comme friend
	Box p;
	p.length = other1.length + other2.length;
	p.width = other1.width + other2.width;
	p.height = other1.height + other2.height;
	return p;
}
Box operator+(const Box& other1,int val) {
	Box p;
	p.length = other1.length + val;
	p.width = other1.width + val;
	p.height = other1.height + val;
	return p;
}
int main() {
	Box a(1,2,3);
	Box b(2, 3, 4);
	Box c = a + b;
	Box d;
	d=operator+(a,b);
	return 0;
}


  1. Surcharge de l'opérateur '+='

Retour par référence

#include <iostream>

using namespace std;
class Box {
	int length;
	int width;
	int high;
	friend Box& operator+=(Box& other1, Box& other2);
public:
	Box() {
		length = 1;
		width = 2;
		high = 3;
	}
	/*Box& operator+=(const Box& other) {
		this->length += other.length;
		this->width += other.width;
		this->high += other.high;
		return *this;
	}*/
	int get_length() {
		return this->length;
	}
};
Box& operator+=(Box& other1,const Box& other2) {
	other1.length += other2.length;
	other1.width += other2.width;
	other1.high += other2.high;
	return other1;
}
int main() {
	Box a, b,c;
	a += b+=c;//Appel implicite de la fonction
	cout << a.get_length();
	return 0;
}


  1. Surcharge de l'opérateur '<<'

Retour par référence

cout est un objet de la classe ostream

cin est un objet de la classe istream

#include <iostream>

using namespace std;
class Box {
	int length;
	int width;
	int high;
	friend ostream& operator<<(ostream& o, const Box& b);
public:
	Box() {
		length = 1;
		width = 2;
		high = 3;
	}
};
ostream& operator<<(ostream& o,const Box& b) {
	o << b.length << ' ' << b.width << ' ' << b.high << endl;
	return o;
}
int main() {
	Box a,b,c;
	//cout << a; Pas d'opérateur correspondant à ces opérandes
	/*
	* Pour surcharger un opérateur, soit dans la classe, soit en dehors
	* Mais cout est un objet de la classe ostream, que nous ne pouvons pas modifier, donc il faut surcharger en dehors avec une fonction globale
	*/
	cout << a << b << c;
	return 0;
}


  1. Surcharge de l'opérateur '>>'

Retour par référence

#include <iostream>

using namespace std;
class Box {
	int length;
	int width;
	int high;
	friend ostream& operator<<(ostream& o, const Box& b);
	friend istream& operator>>(istream& i, Box& b);
public:
	Box() {
		length = 1;
		width = 2;
		high = 3;
	}
};
ostream& operator<<(ostream& o,const Box& b) {
	o << b.length << ' ' << b.width << ' ' << b.high << endl;
	return o;
}
istream& operator>>(istream& i,Box& b) {
	i >> b.length;
	i >> b.width;
	i >> b.high;
	return i;
}
int main() {
	Box a, b,c;
	//cout << a; Pas d'opérateur correspondant à ces opérandes
	/*
	* Pour surcharger un opérateur, soit dans la classe, soit en dehors
	* Mais cout est un objet de la classe ostream, que nous ne pouvons pas modifier, donc il faut surcharger en dehors avec une fonction globale
	*/
    cin >> a>>b>>c;
    cout << a << b << c;
	return 0;
}


  1. Surcharge de l'opérateur '++'

Pré-increment –> Retour par référence

Post-increment –> Retour par valeur

#include <iostream>
using namespace std;
class A {
	//private dans la classe 
	int a;
	int b;
	int c;
	friend ostream& operator<<(ostream& o, const A& other);
public:
	A() {
		this->a = 0;
		this->b = 0;
		this->c = 0;
	}
	A& operator++() {//Pré-increment
		a++;
		return *this;
	}
	A operator++(int) {//Post-increment
		//Ne pas retourner une référence à une variable locale
		A t = *this;
		a++;
		return t;
	}
};
//
ostream& operator<<(ostream& o, const A& other) {
	o << other.a << endl;
	return o;
}
int main() {
	A s;
	cout << s++;
	cout << ++s;
	return 0;
}


  1. Surcharge des opérateurs de comparaison

#include <iostream>
#include <iostream>
using namespace std;

class Person {
	string name;
	int age;
public:
	Person(string name, int age) {
		this->name = name;
		this->age = age;
	}
	bool operator==(const Person& other) const{
		if (this->age == other.age) {
			return 1;
		}
		return 0;
	}
	bool operator>(const Person& other) const{
		if (this->age > other.age) {
			return 1;
		}
		return 0;
	}
	bool operator<(const Person& other) const{
		if (this->age < other.age) {
			return 1;
		}
		return 0;
	}
	bool operator!=(const Person& other) const{
		if (this->age != other.age) {
			return 1;
		}
		return 0;
	}
};
int main() {
	Person p0("Shi Yichen", 20);
	Person p1("Tian Xuesong", 100);
	if (p0 == p1) {
		cout << "Âge égal" << endl;
	}
	else if (p0 > p1) {
		cout << "p0 supérieur à p1" << endl;
	}
	else if (p0 < p1) {
		cout << "p0 inférieur à p1" << endl;
	}
	else
		cout << "p0 différent de p1" << endl;
	return 0;
}


  1. Surcharge de l'opérateur '='

Retour par référence

Le compilateur fournit automatiquement un opérateur d'affectation de copie superficielle. Si les variables membres pointent vers la zone de tas, cela provoquera une erreur de double destruction

#include <iostream>
#include <iostream>
using namespace std;

class Person {
	string name;
	int age;
	int* password;
	friend ostream& operator<<(ostream& o, const Person& p);
public:
	Person(string name, int age, int mm) {
		this->name = name;
		this->age = age;
		this->password = new int[10];
		*password = mm;
	}
	~Person() {
		if (password) delete[]password;
	}
};
ostream& operator<<(ostream& o, const Person& p) {
	o << p.name << ' ' << p.age << *(p.password) << endl;
	return o;
}
int main() {
	Person p0("Shi Yichen", 20, 12345);
	Person p1("Tian Xuesong", 100, 123425);
	p0 = p1;//Le compilateur fournit un opérateur d'affectation de copie superficielle. Si les variables membres pointent vers la zone de tas, cela provoque une erreur
	cout << p0 << p1;
	return 0;
}


Donc, si les variables membres pointent vers la zone de tas, il faut surcharger l'opérateur d'affectation, en mode copie profonde

#include <iostream>
#include <iostream>
using namespace std;

class Person {
	string name;
	int age;
	int* password;
	friend ostream& operator<<(ostream& o, const Person& p);
public:
	Person(string name, int age, int mm) {
		this->name = name;
		this->age = age;
		this->password = new int[10];
		*password = mm;
	}
	~Person() {
		if (password) delete[]password;
	}
	Person& operator=(const Person& other) {//Opérateur d'affectation en mode copie profonde
		this->name = other.name;
		this->age = other.age;
		this->password = new int[10];
		*password = *(other.password);
		return *this;
	}
};
ostream& operator<<(ostream& o, const Person& p) {
	o << p.name << ' ' << p.age << *(p.password) << endl;
	return o;
}
int main() {
	Person p0("Shi Yichen", 20, 12345);
	Person p1("Tian Xuesong", 100, 123425);
	p0 = p1;//Appel de la fonction de surcharge de l'opérateur d'affectation personnalisée
	cout << p0 << p1;
	return 0;
}


Pas d'erreur !

  1. Surcharge de l'opérateur '()'

Appelé aussi fonction objet

#include <iostream>

using namespace std;

class Myprint {
public:
	void operator()(string text) {
		cout << text << endl;
	}
};
class Myadd {
public:
	int operator()(int a, int b) {
		return a + b;
	}
};
void text01() {
	Myprint myprint;
	myprint("abc");
	myprint.operator()("dce");
}
void text02() {
	int a = 1;
	int b = 2;
	Myadd myadd;
	cout << myadd.operator()(1, 2) << endl;
	cout << myadd(3, 4) << endl;
}
int main() {
	text01();
	text02();
	return 0;
}


  1. Syntaxe de base de l'héritance

class NomClasse : PermissionHéritage ClasseParente {

}

class A{
	int a;
	int b;
	int c;
};
class B:public A{
	//Dans la classe fille, abc ont déjà été hérités, pas besoin de répéter le code
};


A est appelée classe parente, classe de base

B est appelée classe fille, classe dérivée

L'héritance permet de réduire le code dupliqué

  1. Permissions d'héritance

Même principe que les permissions d'accès, trois types

Première permission d'héritage privée

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class father{
private:
    int pri;
    void prifun();
protected:
    int pro;
    void profun();
public:
    int pub;
    void pubfun();
};
class son:private father{//Permission d'héritage : réduit la portée d'accès des membres de la classe parente dans la classe fille, ne peut qu'atténuer, pas augmenter
    /*
     * La permission d'héritage privée réduit la portée d'accès de la classe fille
     * Les membres pro et profun deviennent privés dans la classe fille, comme montré dans grandson
     * Les membres pub et pubfun deviennent privés
     */
    void fun(){
        pri=1;//Erreur
        prifun();//Erreur, les membres privés de la classe parente ne peuvent pas être accessibles
        pro=1;//Les membres protégés et publics de la classe parente peuvent être accessibles
        profun();
        pub=1;
        pubfun();
    }

};
class grandson:public son{
    void fun(){
        pro=1;//Erreur
        profun();//Erreur
        pub=1;//Erreur
        pubfun();//Erreur
    }
};
void text01(){
    grandson s;
    s.pub=1;//Erreur
}


Deuxième permission d'héritage protégée

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class father{
private:
    int pri;
    void prifun();
protected:
    int pro;
    void profun();
public:
    int pub;
    void pubfun();
};
class son:protected father{//Permission d'héritage : réduit la portée d'accès des membres de la classe parente dans la classe fille, ne peut qu'atténuer, pas augmenter
    void fun(){
        pri=1;//Erreur
        prifun();//Erreur, les membres privés de la classe parente ne peuvent pas être accessibles
        pro=1;//Les membres protégés et publics de la classe parente peuvent être accessibles
        profun();
        pub=1;
        pubfun();
    }

};
class grandson:public son{
    void fun(){
        pro=1;
        profun();
        pub=1;
        pubfun();
    }
};
void text01(){
    grandson s;
    s.pub=1;//Erreur
}


Troisième permission d'héritage publique

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class father{
private:
    int pri;
    void prifun();
protected:
    int pro;
    void profun();
public:
    int pub;
    void pubfun();
};
class son:public father{//Permission d'héritage : réduit la portée d'accès des membres de la classe parente dans la classe fille, ne peut qu'atténuer, pas augmenter
    /*
     * La permission d'héritage privée réduit la portée d'accès de la classe fille
     * Les membres pro et profun deviennent privés dans la classe fille, comme montré dans grandson
     * Les membres pub et pubfun deviennent privés
     */
    void fun(){
        pri=1;//Erreur
        prifun();//Erreur, les membres privés de la classe parente ne peuvent pas être accessibles
        pro=1;//Les membres protégés et publics de la classe parente peuvent être accessibles
        profun();
        pub=1;
        pubfun();
    }

};
class grandson:public son{
    void fun(){
        pro=1;
        profun();
        pub=1;
        pubfun();
    }
};
void text01(){
    grandson s;
    s.pub=1;//Possible
}


À partir de ces trois exemples, nous pouvons voir

Les membres privés de la classe parente, quelle que soit la méthode d'héritage, ne peuvent pas être accessibles dans la classe fille

Bien qu'ils ne puissent pas être accessibles, ils sont hérités et conservés, mais cachés, occupant encore de l'espace dans la classe fille

  1. Ordre de construction/déconstruction dans l'héritage

Constructeur

Lors de la création d'un objet de classe fille, le constructeur de la classe parente est appelé en premier, puis celui de la classe fille

Pourquoi ?

Parce que l'objet de la classe fille doit hériter des variables membres de la classe parente, donc créer d'abord les variables membres de la classe parente, puis les assigner, donc appeler le constructeur de la classe parente, ensuite créer ses propres variables membres, appeler son constructeur

Déstructeur

Cela dépend de l'ordre d'empilage/dépilage

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class father{
public:
    father(){
        cout<<"Constructeur de father"<<endl;
    }
    ~father(){
        cout<<"Destructeur de father"<<endl;
    }
};
class son:public father{
public:
    son(){
        cout<<"Constructeur de son"<<endl;
    }
    ~son(){
        cout<<"Destructeur de son"<<endl;
    }
};
class grandson:public son{
public:
    grandson(){
        cout<<"Constructeur de grandson"<<endl;
    }
    ~grandson(){
        cout<<"Destructeur de grandson"<<endl;
    }
};
void text01(){
    grandson gr;
}


  1. Gestion des fonctions homonymes dans l'héritage

Utiliser la portée pour distinguer les fonctions membres des différentes classes

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class father{
public:
    father(){
        cout<<"Constructeur de father"<<endl;
    }
    void fun(){
        cout<<"Fonction fun de father"<<endl;
    }
};
class son:public father{
public:
    son(){
        cout<<"Constructeur de son"<<endl;
    }
    void fun(){
        cout<<"Fonction fun de son"<<endl;
    }
};
class grandson:public son{
public:
    grandson(){
        cout<<"Constructeur de grandson"<<endl;
    }
    void fun(){
        cout<<"Fonction fun de grandson"<<endl;
    }
};
void text01(){
    father f;
    son s;
    grandson gr;
    f.father::fun();
    s.son::fun();
    gr.grandson::fun();

}


  1. Gestion des variables membres statiques homonymes dans l'héritage

Deux façons d'appeler les variables membres statiques, par portée et par nom d'objet

Mais quelque soit la méthode, dans l'héritage des variables membres statiques homonymes, il faut toujours utiliser la portée

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class father{
public:
    static int a;
};
int father::a=1;

class son:public father{
public:
    static int a;
};
int son::a=2;

class grandson:public son{
public:
    static int a;
};
int grandson::a=3;
void text01(){
    grandson gr;
    cout<<gr.father::a<<endl;//Appel par objet
    cout<<gr.son::a<<endl;
    cout<<gr.a<<endl;
    cout<<father::a<<endl;//Appel par portée
    cout<<son::a<<endl;
    cout<<grandson::a<<endl;
}


  1. Héritance multiple

L'ordre d'appel des constructeurs dans l'héritage multiple dépend de l'ordre d'héritage, la classe écrite en premier est héritée en premier

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class A{
public:
    int a;
    A(){
        cout<<"Constructeur de A"<<endl;
    }
};

class B{
public:
    int a;
    B(){
        cout<<"Constructeur de B"<<endl;
    }
};

class C:public B,public A{
public:
    C(){
        cout<<"Constructeur de C"<<endl;
    }
};
void text01(){
    C c;
    c.B::a;//Problème d'ambiguïté dans l'héritage multiple --> solution : portée
    c.A::a;
}


  1. Héritage en losange

Même problème d'ambiguïté, mais différent de l'héritage multiple, peut être résolu avec virtual, ou par portée

#pragma once
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
class A{
public:
    int a;
    A(){
        cout<<"Constructeur de A"<<endl;
    }
};

class B:virtual public A{
public:
    B(){
        cout<<"Constructeur de B"<<endl;
    }
};

class C:virtual public A{
public:
    C(){
        cout<<"Constructeur de C"<<endl;
    }
};
class D:public B,public C{
public:
    D(){
        cout<<"Constructeur de D"<<endl;
    }
};
void text01(){
    D d;
    d.B::a;
    d.a;
}


Étiquettes: C++ Programmation orientée objet héritance classes fonctions

Publié le 29 septembre à 01h55