Recyclage de la mémoire C++ des points de connaissances de la stratégie de programmation C++

Auteur:Le petit rêve, Créé: 2017-12-29 11:00:02, mis à jour: 2017-12-29 11:25:11

Recyclage de la mémoire C++ des points de connaissances de la stratégie de programmation C++

Avant d'écrire des stratégies C++, il est nécessaire d'avoir des connaissances de base, au moins de connaître ces règles. Voici les informations à télécharger

  • Bataille des objets de mémoire C++

Si quelqu'un se prétend être un bon programmeur et ne sait rien de la mémoire, alors je peux vous dire qu'il doit être en train de se vanter. Écrire un programme en C ou en C++ nécessite de se concentrer davantage sur la mémoire, non seulement parce que l'allocation raisonnable de la mémoire a un impact direct sur l'efficacité et les performances du programme, mais surtout parce que des problèmes peuvent survenir lorsque nous manipulons la mémoire par inadvertance, et souvent, ces problèmes ne sont pas faciles à détecter, comme les fuites de mémoire, comme les pointeurs suspendus.

Nous savons que C++ divise la mémoire en trois zones logiques: la pile, la soupape et la zone de stockage statique. Je nomme donc les objets qui se trouvent dans ces zones des objets de pile, des objets de soupape et des objets statiques. Alors, quelle est la différence entre ces différents objets de mémoire?

  • 1 Concepts de base

    Tout d'abord, voyons que ──, est généralement utilisé pour stocker des variables locales ou des objets, comme ceux que nous déclarons dans la définition des fonctions avec des statements similaires:

    Type stack_object ; 
    

    un objet stack_object est un objet de pile dont la durée de vie commence au point de définition et se termine lorsque sa fonction est renvoyée.

    De plus, presque tous les objets temporaires sont des objets de type "square"; par exemple, la définition de la fonction suivante:

    Type fun(Type object);
    

    Cette fonction produit au moins deux objets temporaires. Premièrement, les paramètres sont passés par valeur, donc l'appel de la fonction de construction de copie génère un objet temporaire object_copy1, qui n'est pas utilisé au sein de la fonction, mais object_copy1, naturellement, object_copy1 est un objet de la matrice, qui est libéré lorsque la fonction est retournée; et cette fonction est une valeur retournée, qui, lorsque la fonction est retournée, génère également un objet temporaire object_copy2, qui est libéré un certain temps après le retour de la fonction.

    Type tt ,result ; //生成两个栈对象
    
    tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
    

    L'exécution de la deuxième phrase ci-dessus est la suivante: la fonction fun génère un objet temporaire object_copy2 lors de son retour, puis appelle l'opérateur d'attribution pour l'exécuter.

    tt = object_copy2 ; //调用赋值运算符
    

    Voyez-vous? Le compilateur génère tant d'objets temporaires pour nous sans que nous en soyons conscients, ce qui peut être très coûteux dans le temps et dans l'espace pour les générer, alors vous comprenez peut-être pourquoi il est préférable de passer les paramètres de la fonction par rapport aux objets de l'architecture en utilisant des références const au lieu de les faire par valeur.

    Ensuite, regardez la pile. La pile, également appelée zone de stockage libre, est dynamiquement attribuée au cours de l'exécution du programme, de sorte que sa plus grande caractéristique est la dynamique. En C++, tous les objets de la pile sont créés et détruits par le programmeur, donc des problèmes de mémoire peuvent survenir si elles sont mal traitées.

    Alors, comment attribuer les objets de la pile en C++? La seule façon est d'utiliser new (bien sûr, les instructions de type malloc permettent également d'obtenir de la mémoire de la pile en C), si vous utilisez new, vous attribuez un bloc de mémoire dans la pile et retournez le pointeur vers l'objet de la pile.

    Revenons à la zone de stockage statique. Tous les objets statiques, tous les objets globaux, sont alloués à la zone de stockage statique. En ce qui concerne les objets globaux, ils sont alloués avant l'exécution de la fonction main. En fait, avant l'exécution du code d'affichage dans la fonction main, une fonction main générée par le compilateur est appelée, tandis que la fonction main effectue la construction et l'initialisation de tous les objets globaux.

    void main(void)
    {
        ... // 显式代码
    }
    
    // 实际上转化为这样:
    
    void main(void)
    {
        _main(); //隐式代码,由编译器产生,用以构造所有全局对象
        ...      // 显式代码
        ...
        exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
    }
    

    Donc, sachant cela, on peut en déduire quelques astuces, par exemple, supposons que nous voulons faire des préparatifs avant l'exécution de la fonction main(), alors nous pouvons écrire ces préparatifs dans une fonction de construction d'un objet global défini, de sorte que, avant l'exécution du code explicite de la fonction main(), la fonction de construction de cet objet global soit appelée et exécute les actions prévues, ce qui atteint notre objectif.

    Il y a un autre objet statique, qui est un membre statique d'une classe.

    Le premier problème est la durée de vie des objets membres statiques de la classe, les objets membres statiques de la classe sont créés avec la création du premier objet de classe et disparaissent à la fin de l'ensemble du programme. C'est-à-dire qu'il existe une situation où, dans le programme, nous définissons une classe dont la classe a un objet statique comme membre, mais que, pendant l'exécution du programme, nous ne produisons pas l'objet statique que contient la classe si nous n'en créons pas un.

    La deuxième question est de savoir si les situations suivantes se produisent:

    class Base
    {
    public:
        static Type s_object ;
    }
    
    class Derived1 : public Base / / 公共继承
    {
    
        ... // other data 
    
    }
    
    class Derived2 : public Base / / 公共继承
    {
    
        ... // other data 
    
    }
    
    Base example ;
    
    Derivde1 example1 ;
    
    Derivde2 example2 ;
    
    example.s_object = …… ;
    
    example1.s_object = …… ; 
    
    example2.s_object = …… ; 
    

    Remarquez que les trois phrases ci-dessus marquées comme noir sont les mêmes objets que s_object qu'ils visitent? La réponse est oui, elles pointent vers le même objet, ce qui ne semble pas vrai, n'est-ce pas? Mais c'est vrai, vous pouvez écrire un morceau de code simple pour vérifier par vous-même.

    Imaginons qu'un découpage se produise lorsque nous transmettons un objet de type Derived1 à une fonction qui accepte des paramètres de type Base non référencés. Comment le découpage se fait-il? Je crois que vous savez maintenant que c'est simplement enlever le sujet dans l'objet de type Derived1, en ignorant tous les autres membres de données définies par Derived1, puis en transmettant ce sujet à la fonction (en fait, la fonction utilise une copie de ce sujet).

    Tous les objets des classes dérivées de la classe BASE possèdent un sous-objet de type BASE (qui est la clé qui permet de pointer vers une Objet Derived1 avec un pointeur de type BASE, ce qui est naturellement une clé polymétrique), tandis que tous les sous-objets et tous les objets de type BASE partagent le même objet s_object, naturellement, les instances de classe dans l'ensemble du système d'héritage dérivé de la classe BASE partagent le même objet s_object.

  • 2 Comparaison des trois objets de mémoire

    L'avantage des objets de pile est qu'ils sont générés et détruits automatiquement au bon moment, sans que les programmeurs aient à s'en soucier; et la vitesse de création des objets de pile est généralement plus rapide que celle des objets de pile, car l'opérateur new est utilisé pour appeler les objets de pile, et l'opérateur new utilise un algorithme de recherche d'espace mémoire, ce qui peut être très long.

    Nous avons souvent besoin d'un objet comme celui-ci, par exemple, nous avons besoin de créer un objet accessible par plusieurs fonctions, mais nous ne voulons pas le rendre universel, alors il est sans aucun doute une bonne option de créer un objet de pile à ce moment-là, puis de passer le pointer de cet objet de pile entre les fonctions, afin de partager l'objet. De plus, la capacité de la pile est beaucoup plus grande que dans l'espace de pile. En fait, lorsque la mémoire physique n'est pas suffisante, si une nouvelle mémoire physique doit être générée, il n'y a généralement pas d'erreur lors de l'exécution, mais le système utilise la mémoire virtuelle pour étendre la mémoire physique réelle.

    Nous allons voir les objets statiques.

    Tout d'abord, les objets globaux. Les objets globaux offrent le moyen le plus simple de communication entre classes et entre fonctions, bien que cette façon ne soit pas élégante. En général, il n'y a pas d'objets globaux dans un langage entièrement orienté objet, comme C #, car les objets globaux signifient une insécurité et une forte cohérence.

    Ensuite, il y a les membres statiques de la classe, comme mentionné ci-dessus, tous les objets de la classe de base et de ses classes dérivées partagent cet objet membre statique, de sorte qu'il est sans aucun doute un bon choix lorsque vous avez besoin de partager des données ou de communiquer entre ces classes ou entre ces objets de classe.

    Ensuite, il y a les objets locaux statiques, principalement utilisés pour conserver l'état intermédiaire pendant lequel la fonction dans laquelle l'objet est situé est appelée à plusieurs reprises. L'exemple le plus notable est la fonction récurrente, dont nous savons que la fonction récurrente est celle qui appelle sa propre fonction.

    Dans la conception de fonctions récursives, les objets statiques peuvent être utilisés pour remplacer les objets locaux non statiques (c.-à-d. les objets de la chaîne), ce qui réduit non seulement les coûts de production et de libération d'objets non statiques à chaque appel récursif et retour, mais les objets statiques peuvent également conserver l'état intermédiaire des appels récursifs et être accessibles pour les différentes couches d'appel.

  • 3 Récolte accidentelle avec des objets en cuivre

    Comme nous l'avons mentionné précédemment, les objets en forme d'ancrage sont créés au bon moment, puis relâchés automatiquement au bon moment, ce qui signifie qu'ils ont une fonction de gestion automatique. Dans quel cas les objets en forme d'ancrage sont-ils relâchés automatiquement?

    L'objet de pile, lorsqu'il est automatiquement libéré, appelle sa propre fonction de décomposition. Si nous enveloppons les ressources dans l'objet de pile et que nous exécutons l'action de libération des ressources dans la fonction de décomposition de l'objet de pile, la probabilité de fuite des ressources est considérablement réduite, car l'objet de pile peut libérer automatiquement les ressources, même lorsque ses fonctions sont anormales. Le processus pratique est le suivant: lorsque les fonctions sont lancées anormalement, il y a ce qu'on appelle stack_unwinding (en bas de la sécurité de la pile), c'est-à-dire qu'elles se déploient dans la pile, et comme les objets de pile existent naturellement dans la pile, la fonction de décomposition de l'objet de pile est exécutée dans la pile, libérant ainsi les petites ressources enveloppées.

  • 4 Interdiction de créer des objets de pile

    Comme mentionné ci-dessus, si vous décidez d'interdire la création d'un certain type d'objets de pile, vous pouvez créer vous-même une classe d'enveloppe de ressources, qui ne peut être générée que dans la pile, afin de libérer automatiquement les ressources de l'enveloppe en cas d'exception.

    Alors, comment interdire la création d'objets de pile? Nous savons déjà que la seule façon de créer des objets de pile est d'utiliser l'opération new, et si nous interdisons l'utilisation de new, cela ne fonctionne pas. Par la suite, l'opération new est appelée l'opérateur new lors de l'exécution, et l'opérateur new est rechargeable.

    #include <stdlib.h> //需要用到C式内存分配函数
    
    class Resource ; //代表需要被封装的资源类
    
    class NoHashObject
    {
    private: 
        Resource* ptr ;//指向被封装的资源
    
        ... ... //其它数据成员
    
        void* operator new(size_t size) //非严格实现,仅作示意之用
        { 
            return malloc(size) ; 
        } 
    
        void operator delete(void* pp) //非严格实现,仅作示意之用
        { 
            free(pp) ; 
        } 
    
    public: 
        NoHashObject() 
        { 
            //此处可以获得需要封装的资源,并让ptr指针指向该资源
    
            ptr = new Resource() ; 
        } 
        
        ~NoHashObject() 
        { 
    
            delete ptr ; //释放封装的资源
        } 
    }; 
    

    NoHashObject est maintenant une classe qui interdit les objets de la pile si vous écrivez le code suivant:

    NoHashObject* fp = new NoHashObject (()) ; // Une erreur de compilation!

    supprimer fp;

    Le code ci-dessus génère des erreurs de compilation. Maintenant que vous savez comment concevoir une classe qui interdit les objets de la pile, vous vous demandez peut-être, comme moi, si vous ne pouvez pas créer des objets de la pile de ce type si la définition de la classe NoHashObject ne peut pas être modifiée.

    void main(void)
    {
        char* temp = new char[sizeof(NoHashObject)] ; 
    
        //强制类型转换,现在ptr是一个指向NoHashObject对象的指针
    
        NoHashObject* obj_ptr = (NoHashObject*)temp ; 
    
        temp = NULL ; //防止通过temp指针修改NoHashObject对象
    
        //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
    
        Resource* rp = (Resource*)obj_ptr ; 
    
        //初始化obj_ptr指向的NoHashObject对象的ptr成员
    
        rp = new Resource() ; 
    
        //现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
    
        ... ... 
    
        delete rp ;//释放资源
    
        temp = (char*)obj_ptr ; 
    
        obj_ptr = NULL ;//防止悬挂指针产生
    
        delete [] temp ;//释放NoHashObject对象所占的堆空间。
    
        } 
    

    L'implémentation ci-dessus est fastidieuse, et cette implémentation est rarement utilisée dans la pratique, mais j'ai quand même écrit le chemin, car la comprendre est bénéfique pour notre compréhension des objets de mémoire C++.

    Les données dans un bloc de mémoire sont inchangées, et le type est les lunettes que nous portons, et lorsque nous portons un ensemble de lunettes, nous utilisons le type correspondant pour interpréter les données dans la mémoire, de sorte que différentes interprétations donnent des informations différentes.

    La conversion de type forcée est en fait un changement de lunettes pour revoir le même morceau de mémoire.

    Il est également important de rappeler que la disposition des données des membres d'un objet peut être différente selon les compilateurs. Par exemple, la plupart des compilateurs placent les membres du pointeur ptr de NoHashObject dans les 4 premiers octets de l'espace des objets pour garantir que l'action de conversion de la phrase suivante s'exécute comme nous l'espérons:

    Ressource* rp = (Resource*) obj_ptr ;

    Cependant, ce n'est pas toujours le cas pour tous les compilateurs.

    Puisque nous pouvons interdire la production d'objets de type, est-il possible de concevoir une classe pour qu'elle ne produise pas d'objets de type?

  • 5 Interdiction de produire des objets en silicone

    Comme nous l'avons mentionné précédemment, la création d'un objet en soie consiste à déplacer le pointeur de la soie pour déplacer la soie de la taille appropriée de l'espace, puis à appeler directement sur cet espace la fonction de construction correspondante pour former un objet en soie, et lorsque la fonction revient, elle appelle sa fonction de dialyse pour libérer l'objet, puis modifie le pointeur de la soie pour récupérer le bloc en soie. Dans ce processus, l'opérateur new/delete n'est pas nécessaire, donc le paramètre new/delete n'est pas utile.

    C'est possible, et j'ai l'intention de le faire. Mais avant cela, il y a un point à considérer: si nous définissons la fonction de construction comme privée, nous ne pouvons pas non plus générer directement des objets de la pile avec new, car new appelle sa fonction de construction après avoir alloué de l'espace à l'objet. Donc, j'ai l'intention de définir uniquement la fonction de dialyse comme privée.

    Si une classe n'est pas destinée à être une classe de base, la solution habituelle est de déclarer sa fonction de parallélisation comme privée.

    Afin de limiter les objets de la chaîne sans limiter l'héritage, nous pouvons déclarer les fonctions de calcul comme protégées, ce qui est parfait.

    class NoStackObject
    {
    protected: 
    
        ~NoStackObject() { } 
    
    public: 
    
        void destroy() 
    
        { 
    
            delete this ;//调用保护析构函数
    
        } 
    }; 
    

    Ensuite, vous pouvez utiliser une classe NoStackObject comme ceci:

    NoStackObject* hash_ptr = nouveau NoStackObject() ;

    ...... // Opération sur l'objet désigné par hash_ptr

    Le code est le suivant: hash_ptr->destroy (); Je ne sais pas. Eh bien, n'est-ce pas un peu étrange que nous créons un objet avec new, mais que nous ne l'enlèvions pas avec deletes, mais que nous le détruisions. Évidemment, les utilisateurs ne sont pas habitués à ce type d'utilisation étrange. J'ai donc décidé de définir la fonction de construction comme privée ou protégée. Cela revient à la question que j'ai essayée d'éviter ci-dessus, à savoir comment créer un objet sans new.

    class NoStackObject
    {
    protected: 
    
        NoStackObject() { } 
    
        ~NoStackObject() { } 
    
    public: 
    
        static NoStackObject* creatInstance() 
        { 
            return new NoStackObject() ;//调用保护的构造函数
        } 
    
        void destroy() 
        { 
    
            delete this ;//调用保护的析构函数
    
        } 
    };
    

    Vous pouvez maintenant utiliser les classes NoStackObject comme suit:

    NoStackObject* hash_ptr = NoStackObject::creatInstance ((() ;

    ...... // Opération sur l'objet désigné par hash_ptr

    le mot-clé est le suivant:

    hash_ptr = NULL ; // empêche l'utilisation du pointeur suspendu

    Il semble maintenant que l'opération de génération d'objets et de libération d'objets soit en harmonie.

  • Les méthodes de recyclage des ordures en C++

    Beaucoup de programmeurs C ou C++ se moquent du recyclage de la corbeille, pensant que le recyclage de la corbeille est certainement moins efficace que la gestion de la mémoire dynamique et qu'il doit arrêter le programme au moment de la récupération, tandis que si vous contrôlez la gestion de la mémoire, le temps d'attribution et de libération est stable et ne provoque pas l'arrêt du programme. Enfin, de nombreux programmeurs C/C++ sont convaincus que le mécanisme de recyclage de la corbeille ne peut pas être mis en œuvre en C/C++. Ces idées erronées sont le résultat d'une mauvaise compréhension de l'algorithme de recyclage de la corbeille.

    En fait, les mécanismes de recyclage de déchets ne sont pas lents et sont même plus efficaces que l'allocation de la mémoire dynamique. Puisque nous ne pouvons que distribuer sans libérer, il suffit d'acquérir de la mémoire lors de l'allocation de la mémoire, et les pointeurs des piles mobiles suffisent; le processus de libération est omis et naturellement accéléré. Les algorithmes modernes de recyclage de déchets ont beaucoup évolué, les algorithmes de collecte en série permettent déjà de faire avancer le processus de recyclage de déchets par étapes, évitant ainsi l'interruption du processus.

    La base des algorithmes de recyclage de la corbeille est généralement basée sur le fait de scanner et de marquer tous les blocs de mémoire qui peuvent être actuellement utilisés, et de récupérer la mémoire non marquée de tous les blocs de mémoire qui ont déjà été alloués. En C/C++, l'impossibilité de recycler de la corbeille est généralement basée sur l'impossibilité de scanner correctement tous les blocs de mémoire qui pourraient être utilisés, mais ce qui semble impossible n'est pas vraiment compliqué. Premièrement, il est facile d'identifier les points dynamiques alloués à la mémoire sur la pile en scannant les données de la mémoire.

    Lors du recyclage, il suffit de scanner le segment bss, le segment de données et l'espace d'encadrement actuellement utilisé pour trouver la quantité de pointers potentiellement dynamiques, et le balayage récurrent de la mémoire référencée permet d'obtenir tout le mémoire dynamique actuellement utilisé.

    Il est possible d'améliorer la vitesse de la gestion de la mémoire, voire de réduire la consommation totale de mémoire, si vous souhaitez réaliser un bon récupérateur pour votre projet. Si vous êtes intéressé, vous pouvez rechercher les articles et les bibliothèques disponibles sur Internet sur la récupération de la mémoire.

    Traduit deHK Zhang

  • Pourquoi un pointer peut-il prolonger son cycle de vie jusqu'à la fin de l'ensemble du programme lorsqu'il est adressé à une variable locale?

    #include<stdio.h>
    int*fun(){
        int k = 12;
        return &k;
    }
    int main(){
        int *p = fun();    
        printf("%d\n", *p);
    
        getchar();
        return 0;
    }
    

    L'accès est non seulement accessible, mais il peut être modifié, sauf que cet accès est incertain. L'adresse de la variable locale est dans le programme lui-même, et après la fin de la variable de l'autorité, sa valeur est maintenue tant que l'adresse de mémoire de la variable locale n'est pas donnée à une autre variable. Mais si elle est modifiée, c'est plus dangereux, car cette adresse de mémoire peut être donnée à d'autres variables du programme, et une modification forcée par le pointeur peut provoquer un crash du programme.

    Les résultats de l'enquête


Plus de