C++ ces choses notes d'étude de partie avancée de base

calcul de la taille de la classe

  • Les classes vides ont une taille de 1 octet
  • Dans une classe, la fonction virtuelle elle-même, les fonctions membres (y compris statiques et non statiques) et les données membres statiques n'occupent pas l'espace de stockage de l'objet de classe.
  • Pour une classe contenant des fonctions virtuelles, quel que soit le nombre de fonctions virtuelles, il n'y a qu'un seul pointeur virtuel, de la taille de vptr.
  • Héritage ordinaire, la classe dérivée hérite de toutes les fonctions et membres de la classe de base, et la taille doit être calculée en fonction de l'alignement des octets
  • L'héritage des fonctions virtuelles, qu'il s'agisse d'un héritage unique ou d'un héritage multiple, hérite du vptr de la classe de base. (4 octets pour le système d'exploitation 32 bits, 8 octets pour le système d'exploitation 64 bits) !
  • Héritage virtuel, héritant du vptr de la classe de base.

virtuel

Fonctions virtuelles pures et classes abstraites

Les fonctions virtuelles pures (ou fonctions abstraites) en C++ sont des fonctions virtuelles que nous n'implémentons pas ! Nous le déclarons simplement ! Nous déclarons une fonction virtuelle pure en affectant la valeur 0 dans la déclaration !

// 抽象类
Class A {
    
    
public: 
    virtual void show() = 0; // 纯虚函数
    /* Other members */
}; 

Fonction virtuelle pure : une fonction virtuelle sans corps de fonction
Classe abstraite : une classe qui contient une fonction virtuelle pure

Les constructeurs ne peuvent pas être virtuels et les destructeurs peuvent être virtuels

Lorsqu'un pointeur de classe de base pointe vers un objet de classe dérivée et que l'objet est supprimé, nous pouvons souhaiter appeler le destructeur approprié. Seuls les destructeurs de classe de base peuvent être appelés si le destructeur n'est pas virtuel.

Fonctions virtuelles et polymorphisme d'exécution

Les appels de fonctions virtuelles dépendent du type de l'objet pointé ou référencé, et non du type du pointeur ou de la référence elle-même.

Les fonctions statiques ne peuvent pas être déclarées comme fonctions virtuelles, ni modifiées par les mots-clés const et volatile

Les fonctions membres statiques n'appartiennent à aucun objet de classe ou instance de classe, donc même l'ajout de virtual à cette fonction n'a aucun sens.
Les fonctions virtuelles sont gérées par vptr et vtable. vptr est un pointeur, créé et généré dans le constructeur de la classe, et n'est accessible qu'avec ce pointeur. Les fonctions membres statiques n'ont pas ce pointeur, donc vptr n'est pas accessible.

Fonctions virtuelles et tables virtuelles

Pour implémenter des fonctions virtuelles, C++ utilise une forme spéciale de liaison tardive appelée table virtuelle. La table virtuelle est une table de recherche pour résoudre les appels de fonction de manière dynamique/liaison tardive. Les tables virtuelles sont parfois appelées par d'autres noms, tels que "vtable", "table de fonctions virtuelles", "table de méthodes virtuelles" ou "table de répartition".

Les tables virtuelles sont en fait très simples, bien qu'un peu compliquées à décrire avec des mots. Tout d'abord, chaque classe qui utilise des fonctions virtuelles (ou dérive d'une classe qui utilise des fonctions virtuelles) possède sa propre table virtuelle . La table est juste un tableau statique défini par le compilateur au moment de la compilation . La table virtuelle contient une entrée pour chaque fonction virtuelle pouvant être appelée par les objets de la classe . Chaque entrée de ce tableau est simplement un pointeur de fonction vers une fonction dérivée accessible par cette classe.
Deuxièmement, le compilateur ajoute également un pointeur caché à la classe de base, que nous appelons vptr. vptr est automatiquement défini lorsqu'une instance de classe est créée afin qu'elle pointe vers la table virtuelle de cette classe . Contrairement au pointeur this, qui est en fait un paramètre de fonction utilisé par le compilateur pour résoudre les auto-références, vptr est un vrai pointeur.
Par conséquent, cela agrandit l'allocation de chaque objet de classe de la taille d'un pointeur. Cela signifie également que vptr est hérité par les classes dérivées, ce qui est important.

La table virtuelle de la sous-classe copie la table virtuelle de la classe parente et le Func1 de la sous-classe remplace le Func1 de la classe parente. (L'écrasement fait référence à la couverture de la fonction virtuelle dans la table virtuelle)
Réécriture de la fonction virtuelle : concept de couche de syntaxe, la sous-classe réécrit l'implémentation de la fonction virtuelle héritée de la classe mère.
Couverture de la fonction virtuelle : Le concept de la couche principale, la table virtuelle de la sous-classe, copie la table virtuelle de la classe mère et la modifie, et remplace la fonction virtuelle.
Réécriture et couverture des fonctions virtuelles, la réécriture est appelée au niveau de la syntaxe et la couverture est appelée au niveau du principe.

Lorsque le programme est en cours d'exécution, lorsqu'un pointeur appelle une fonction, il va d'abord déterminer le type de l'objet pointé par le pointeur, puis appeler la fonction correspondante en fonction de la table virtuelle de l'objet.
insérez la description de l'image ici

structure

structure en C

  • En C, struct est simplement utilisé comme un type composite de données, c'est-à-dire que seuls les membres de données peuvent être placés dans la déclaration de structure, mais les fonctions ne peuvent pas y être placées.
  • Les modificateurs d'accès C++ tels que public, protected et private ne peuvent pas être utilisés dans les déclarations de structure C, mais peuvent être utilisés en C++.
  • Pour définir une variable de structure en C, si la définition suivante est utilisée, struct doit être ajouté. Les structures C ne peuvent pas être héritées (un tel concept n'existe pas).
  • Si le nom de la structure est le même que le nom de la fonction, elle peut s'exécuter normalement et être appelée normalement ! Par exemple : void Base() {} qui n'entre pas en conflit avec struct Base peut être défini.

structure en C++

Comparez avec C comme suit :

  • Dans la structure C++, non seulement les données peuvent être définies, mais également les fonctions peuvent être définies.
  • Les modificateurs d'accès peuvent être utilisés dans les structures C++, telles que : public, protected, private.
  • La structure C++ peut être utilisée directement sans structure.
  • Héritage C++
  • Si le nom de la structure est le même que le nom de la fonction, elle peut s'exécuter normalement et être appelée normalement ! Mais lors de la définition des variables de structure, seules celles avec struct peuvent être utilisées !

structure et classe

En général, struct est plus approprié pour être considéré comme le corps de réalisation d'une structure de données, et class est plus approprié pour être considéré comme le corps de réalisation d'un objet.

Différence :
L'une des différences les plus essentielles est le contrôle d'accès par défaut
et les droits d'accès hérités par défaut. La structure est publique et la classe est privée.
Struct est le corps de réalisation de la structure de données, son contrôle d'accès aux données par défaut est public et la classe est le corps de réalisation de l'objet, son contrôle d'accès aux variables membres par défaut est privé.

syndicat

Union (union) est une classe spéciale qui économise de l'espace. Une union peut avoir plusieurs membres de données, mais un seul membre de données peut avoir une valeur à la fois. Lorsqu'une valeur est affectée à un membre, les autres membres deviennent indéfinis. Les syndicats ont les caractéristiques suivantes :

  • Le caractère de contrôle d'accès par défaut est public
  • Peut contenir des constructeurs, des destructeurs
  • Ne peut pas contenir de membres de type référence
  • Ne peut pas hériter d'autres classes et ne peut pas être utilisé comme classe de base
  • Ne peut pas contenir de fonctions virtuelles
  • Un syndicat anonyme peut accéder directement aux membres du syndicat dans la portée où se trouve la définition
  • L'union anonyme ne peut pas contenir de membres protégés ou de membres privés
  • Les syndicats anonymes mondiaux doivent être statiques

explicite

Le mot-clé explicit en C++ ne peut être utilisé que pour modifier un constructeur de classe avec un seul paramètre. Sa fonction est d'indiquer que le constructeur est explicite et non implicite. Un autre mot-clé lui correspondant est implicite, c'est-à-dire est caché, le constructeur de classe est déclaré comme implicite par défaut

  • Lorsque explicite décore le constructeur, il peut empêcher la conversion implicite et l'initialisation de la copie
  • Lorsque explicite décore une fonction de conversion, les conversions implicites peuvent être empêchées, à l'exception des conversions par contexte

références et pointeurs

insérez la description de l'image ici

référence lvalue et référence rvalue

Une lvalue est une expression représentant des données, comme un nom de variable, une variable de pointeur déréférencée. Généralement, nous pouvons obtenir son adresse et lui attribuer une valeur, mais la lvalue modifiée par const ne peut pas lui attribuer de valeur, mais son adresse peut toujours être obtenue.
En général, un objet dont l'adresse peut être relevée est une lvalue.

// 以下的a、p、*p、b都是左值
int a = 3;
int* p = &a;
*p;
const int b = 2;

Une rvalue est également une expression représentant des données, telles que : constante littérale, valeur de retour d'expression, valeur de retour d'une fonction par valeur (retour par valeur, pas de retour par référence), rvalue ne peut pas apparaître sur le côté gauche du symbole d'affectation et Impossible pour récupérer l'adresse.
En général, les objets qui ne peuvent pas être adressés sont des rvalues.

double x = 1.3, y = 3.8;
// 以下几个都是常见的右值
10;                 // 字面常量
x + y;             // 表达式返回值
fmin(x, y);        // 传值返回函数的返回值

Une référence lvalue est une référence à une lvalue, aliasant la lvalue.

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

Une référence rvalue est une référence à une rvalue, aliasant la rvalue.
L'expression de la référence rvalue consiste à ajouter deux & après le nom du type de variable spécifique, par exemple : int&& rr = 4;.

// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

Résumé des références lvalue :

  1. Les références Lvalue ne peuvent faire référence qu'à des lvalues, pas directement à des rvalues.
  2. Mais les références const lvalue peuvent faire référence à la fois aux lvalues ​​et aux rvalues.
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;

//int& rt2 = 8;  // 编译报错,因为10是右值,不能直接引用右值

// 2.但是const左值引用既可以引用左值
const int& rt3 = t;

const int& rt4 = 8;  // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);

Q : Pourquoi une référence const lvalue peut-elle également faire référence à une rvalue ?
Réponse : Avant la création de la norme C++11, il n'existait pas de concept de références rvalue. À cette époque, si vous vouliez un type pouvant recevoir à la fois des lvalues ​​et des rvalues, vous deviez utiliser des références const lvalue, telles que push_back pour les conteneurs standard Interface : void push_back (const T& val).
En d'autres termes, si les références const lvalue ne peuvent pas faire référence à rvalues, certaines interfaces ne sont pas bien prises en charge.

Résumé des références rvalue :

  1. Les références Rvalue ne peuvent faire référence qu'à des rvalues, pas directement à des lvalues.
  2. Mais une référence rvalue peut faire référence à une lvalue qui est déplacée.

move, cet article fait référence à std::move (C++11), est utilisé pour forcer une lvalue à une rvalue pour obtenir une sémantique de déplacement.
Une fois la lvalue déplacée, elle devient une rvalue, de sorte qu'une référence rvalue peut être référencée.

// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;

int t = 10;
//int&& rrt = t;  // 编译报错,不能直接引用左值


// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

Signification de la référence lvalue
Passer des paramètres par valeur et retourner par valeur générera des copies, certaines même des copies profondes, ce qui est très coûteux. L'importance réelle des références lvalue est qu'à la fois en tant que paramètres et en tant que valeurs de retour peuvent réduire la copie, améliorant ainsi l'efficacité.
Lacunes des références lvalue
Bien que les références lvalue résolvent parfaitement la plupart des problèmes, certains problèmes ne peuvent toujours pas être résolus correctement.
Lorsque l'objet existe toujours après la portée de la fonction, il peut être renvoyé par référence lvalue, ce qui ne pose aucun problème.
Mais lorsque l'objet (l'objet est un objet local dans la fonction) n'existe pas après avoir quitté la portée de la fonction, il ne peut pas être renvoyé à l'aide d'une référence lvalue.
Par conséquent, pour le second cas, la référence lvalue ne peut rien faire, elle ne peut retourner que par valeur.
L'importance des références rvalue
Ainsi, afin de résoudre le problème de copie mentionné ci-dessus de retour par valeur, la norme C++11 ajoute des références rvalue et une sémantique de déplacement.
Déplacer la sémantique : Déplacer des ressources d'un objet à un autre (transfert de contrôle des ressources).
Move construction : Transfère la ressource du paramètre rvalue pour se construire.

En général, si ces deux fonctions sont définies dans la classe, lors de la construction de l'objet :
si la lvalue est utilisée comme paramètre, alors le constructeur de copie sera appelé pour faire une copie (si elle est dans l'espace du tas comme chaîne S'il y a est une classe contenant des ressources, une copie complète sera créée à chaque fois que la construction de la copie sera appelée).
Si une rvalue est utilisée comme paramètre, alors la construction de déplacement sera appelée, et l'appel de la construction de déplacement réduira la copie (s'il s'agit d'une classe comme une chaîne qui a des ressources sur l'espace de tas, alors chaque fois que la construction de déplacement est appelée, une copie moins profonde sera faite).

Déplacer l'affectation : transfert des ressources de la rvalue du paramètre à s'affecter.

En général, si ces deux fonctions sont définies dans la classe, lors de l'affectation d'un objet :
si une lvalue est utilisée en paramètre, alors l'affectation de copie sera appelée et une copie sera faite (si elle est dans le tas comme chaîne S'il y a sont des classes de ressources dans l'espace, une copie complète sera effectuée chaque fois que l'affectation de copie est appelée).
Si une rvalue est utilisée comme paramètre, alors l'affectation de déplacement sera appelée et l'appel d'affectation de déplacement réduira la copie (s'il s'agit d'une classe comme une chaîne qui a des ressources sur l'espace de tas, alors chaque appel d'affectation de déplacement enregistrera une copie profonde ).

Référence universelle :
&& d'un certain type représente une référence rvalue (par exemple : int&&, string&&),
mais && dans un modèle de fonction ne représente pas une référence rvalue, mais une référence universelle. Le type de modèle doit être déterminé par inférence, et il recevra une lvalue est déduite comme une référence lvalue, et sera déduite comme une référence rvalue après avoir reçu une rvalue.
Cependant, dans les références universelles, les références rvalue perdront les propriétés des références rvalue.
Une transmission parfaite
signifie que dans un modèle de fonction, les paramètres sont passés à une autre fonction dans le modèle de fonction actuel en fonction du type de paramètre du modèle.
Par conséquent, afin d'obtenir une transmission parfaite, en plus d'utiliser des références universelles, nous devons également utiliser std::forward (C++11), qui conserve les propriétés de type d'origine de l'objet pendant le processus de transmission des paramètres.
De cette manière, la référence rvalue peut conserver les propriétés de la rvalue pendant le processus de transfert.

void Func(int& x) {
    
     cout << "左值引用" << endl; }

void Func(const int& x) {
    
     cout << "const左值引用" << endl; }

void Func(int&& x) {
    
     cout << "右值引用" << endl; }

void Func(const int&& x) {
    
     cout << "const右值引用" << endl; }


template<typename T>
void PerfectForward(T&& t)  // 万能引用
{
    
    
	Func(std::forward<T>(t));  // 根据参数t的类型去匹配合适的重载函数
}

int main()
{
    
    
	int a = 4;  // 左值
	PerfectForward(a);

	const int b = 8;  // const左值
	PerfectForward(b);

	PerfectForward(10); // 10是右值

	const int c = 13;
	PerfectForward(std::move(c));  // const左值被move后变成const右值

	return 0;
}

insérez la description de l'image ici
En plus des scénarios d'utilisation ci-dessus, les fonctions d'interface pertinentes du conteneur STL standard C++11 permettent également une transmission parfaite, de sorte que la valeur des références rvalue peut être réellement réalisée.
Par exemple, la liste des conteneurs dans la bibliothèque STL

Résumé
Les références Rvalue permettent aux programmes C++ de s'exécuter plus efficacement.

article de blog de référence

L'introduction d'opérations de référence en C++ garantit la sécurité et la commodité de l'utilisation des références tout en ajoutant davantage de restrictions sur l'utilisation des références, et peut également maintenir l'élégance du code. Utilisez des opérations appropriées dans des situations appropriées, et l'utilisation de références peut éviter la situation de "pointeurs volant partout dans le ciel" dans une certaine mesure, et cela a également une certaine signification positive pour améliorer la stabilité du programme. Enfin, les implémentations sous-jacentes des pointeurs et des références sont les mêmes, il n'y a donc pas lieu de s'inquiéter de l'écart de performances entre les deux.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_45184581/article/details/129491269
conseillé
Classement