Recuperación de memoria C++ de puntos de conocimiento de la política de escritura de C++

El autor:Un sueño pequeño., Creado: 2017-12-29 11:00:02, Actualizado: 2017-12-29 11:25:11

Recuperación de memoria C++ de puntos de conocimiento de la política de escritura de C++

Antes de escribir la política de C++, hay algunos conocimientos básicos que se necesitan saber, sin necesidad de conocimiento de la forma de Confucius, al menos se deben conocer estas reglas. Los siguientes son para reproducción:

  • La guerra de los objetos de memoria en C++

Si una persona se llama programadora y no sabe nada de memoria, entonces puedo decirte que debe estar alardeando. Escribir un programa en C o C++ requiere una mayor atención a la memoria, no solo porque la asignación razonable de la memoria afecta directamente la eficiencia y el rendimiento del programa, sino también porque cuando manejamos la memoria surgen problemas que a menudo no se detectan fácilmente, como las fugas de memoria, como los punteros suspendidos.

Sabemos que C++ divide la memoria en tres áreas lógicas: pila, pila y área de almacenamiento estático. Como tal, llamo a los objetos que se encuentran en ellas objetos de pila, objetos de pila y objetos estáticos. Entonces, ¿qué diferencia hay entre estos diferentes objetos de memoria?

  • 1 conceptos básicos

    Primero, veamos que ──, generalmente se usa para almacenar variables locales u objetos, como los que declaramos en la definición de funciones con una declaración similar a la siguiente:

    Type stack_object ; 
    

    El objeto stack_object es un objeto de pila cuya vida comienza en el punto de definición y termina cuando la función en la que se encuentra regresa.

    Además, casi todos los objetos temporales son objetos de parámetro. Por ejemplo, la definición de la función siguiente:

    Type fun(Type object);
    

    Esta función produce al menos dos objetos temporales, primero, los parámetros se transmiten por valor, por lo que se llama a la función de construcción de copias para generar un objeto temporal object_copy1, que se usa dentro de la función no como object, sino como object_copy1, naturalmente, object_copy1 es un objeto de pared, que se libera cuando la función regresa; también esta función es de valor de retorno, y cuando la función regresa, también se produce un objeto temporal object_copy2, si no consideramos la optimización del valor de retorno.

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

    La ejecución de la segunda sentencia anterior es la siguiente: primero se genera un objeto provisional object_copy2 cuando la función fun regresa, y luego se llama al operador de asignación para ejecutarlo.

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

    ¿Ven? Los compiladores generan tantos objetos temporales para nosotros sin nuestra percepción, y generar estos objetos temporales puede ser un gran gasto de tiempo y espacio, por lo que, tal vez entiendan por qué es mejor transmitir parámetros de función por valor en lugar de const referencias para objetos con constelar.

    A continuación, veamos la pila. La pila, también llamada zona de almacenamiento libre, se asigna dinámicamente durante la ejecución del programa, por lo que su característica principal es la dinámica. En C++, todos los objetos de la pila son creados y destruidos por el programador, por lo que, si se tratan mal, se producen problemas de memoria.

    Entonces, ¿cómo se asignan los objetos de la pila en C++? El único método es usar new (por supuesto, también se puede obtener memoria de pila de tipo C con la instrucción tipo malloc), ya que con new se asigna un pedazo de memoria en la pila y se devuelve el puntero que apunta al objeto de la pila.

    Volvamos a ver el área de almacenamiento estática. Todos los objetos estáticos y globales están asignados a la zona de almacenamiento estática. En cuanto a los objetos globales, se asignan antes de la ejecución de la función main. De hecho, antes de ejecutar el código de visualización en la función main, se llama a una función _main))) generada por el compilador, mientras que la función _main))) realiza el trabajo de construcción e inicialización de todos los objetos globales.

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

    Así que, sabiendo esto, se pueden deducir algunos trucos de esto, como, suponiendo que vamos a hacer algunos preparativos antes de que la función main() se ejecute, entonces podemos escribir estos preparativos en la función de construcción de un objeto global definido, de modo que, antes de que se ejecute el código expreso de la función main(), la función de construcción de este objeto global sea llamada y realice las acciones esperadas, y así se alcance nuestro objetivo.

    También hay un objeto estático, que es un miembro estático de una clase. Considerando esto, surgen algunos problemas más complejos.

    El primer problema es la vida útil de los objetos de los miembros estáticos de una clase, los objetos de los miembros estáticos de una clase surgen con la creación del primer objeto de clase y desaparecen al final del programa. Es decir, existe la situación de que en el programa definimos una clase que tiene un objeto estático como miembro, pero en el proceso de ejecución del programa, si no creamos ninguno de los objetos de clase, tampoco se producirá el objeto estático que contiene la clase.

    El segundo problema es cuando surgen las siguientes situaciones:

    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 = …… ; 
    

    La respuesta es que sí, y que sí apuntan al mismo objeto, lo que no suena a ser cierto, ¿verdad? Pero es cierto, puedes escribir tu propio código simple para comprobarlo. Lo que voy a hacer es explicar por qué sucede.

    Imaginemos que cuando pasamos un objeto de tipo Derived1 a una función que acepta parámetros de tipo Base sin referencia, se produce un corte. ¿Cómo se hace el corte?

    Todos los objetos de las clases derivadas de la clase Base que heredan contienen un subobjeto de tipo Base (esto es, la clave que puede ser apuntada a un objeto Derived1 con el puntero de tipo Base, que también es una clave de polinomio), mientras que todos los subobjetos y todos los objetos de tipo Base comparten el mismo s_object, naturalmente, los ejemplos de clases derivados de la clase Base en todo el sistema de herencia comparten el mismo s_object.

  • 2 Comparación de los tres objetos de memoria

    La ventaja de los objetos de pila es que se generan automáticamente en el momento adecuado y se destruyen automáticamente en el momento adecuado, sin necesidad de que el programador se preocupe; además, los objetos de pila generalmente se crean más rápido que los objetos de pila, ya que al asignar los objetos de pila, se llama el operador new, el operador new utiliza algún algoritmo de búsqueda de espacio en memoria, y el proceso de búsqueda puede ser muy lento. La generación de objetos de pila no es tan problemática, solo se necesita mover el puntero de la bóveda.

    Los objetos de pila, que deben ser definidos por el programador en el momento de su creación y en el momento de su destrucción, es decir, que el programador tiene un control total sobre la vida de los objetos de pila. A menudo necesitamos objetos de este tipo, por ejemplo, necesitamos crear un objeto que pueda ser visitado por varias funciones, pero no queremos que sea universal, entonces es indudablemente una buena opción crear un objeto de pila en este momento y luego pasar el puntero de este objeto de pila entre las funciones para lograr el intercambio de ese objeto. Además, la capacidad de la pila es mucho mayor en comparación con el espacio de pila.

    A continuación, vamos a ver los objetos estáticos.

    Primero, los objetos globales. Los objetos globales ofrecen la forma más sencilla para la comunicación entre clases y entre funciones, aunque esta no es una forma elegante. En general, en un lenguaje totalmente orientado a objetos, no existen objetos globales, como en C#, porque los objetos globales significan inseguridad y alta adhesión, y el uso excesivo de objetos globales en el programa reducirá enormemente la robustez, estabilidad, mantenimiento y repetibilidad del programa.

    El siguiente es el miembro estático de la clase, ya mencionado, todos los objetos de la clase base y sus clases derivadas comparten este objeto miembro estático, por lo que este miembro estático es sin duda una buena opción cuando se necesita compartir o comunicar datos entre estas clases o entre estos objetos de clase.

    Luego está el objeto local estático, que se utiliza principalmente para conservar el estado intermedio durante el tiempo en que la función en la que se encuentra se llama repetidamente, uno de los ejemplos más destacados son las funciones recursivas, que todos sabemos que las funciones recursivas son aquellas que llaman a su propia función, y si se define un objeto local no estático en la función recursiva, entonces cuando el número de recurrentes es bastante grande, el desembolso que se produce también es enorme. Esto se debe a que los objetos locales no estáticos son objetos de enlace, cada recurrente produce un objeto así, cada vez que se llama, se libera este objeto, y, además, tales objetos solo se limitan a la capa de llamada actual, son invisibles para las capas más profundas y las capas más superficiales. Cada nivel tiene sus propios elementos locales y parámetros.

    En el diseño de funciones recursivas, los objetos estáticos pueden reemplazar a los objetos locales no estáticos (es decir, objetos de pared), lo que no solo reduce el gasto de generar y liberar objetos no estáticos en cada llamada recursiva y regreso, sino que también permite que los objetos estáticos conserven el estado intermedio de las llamadas recursivas y sean accesibles para las diferentes capas de llamada.

  • 3 Recolección accidental con objetos de aluminio

    Ya se ha mencionado anteriormente que los objetos de pared se crean en el momento adecuado y se liberan automáticamente en el momento adecuado, es decir, los objetos de pared tienen una función de administración automática. Entonces, ¿en qué se liberan automáticamente los objetos de pared?

    El objeto de pila, cuando se libera automáticamente, llama a su propia función de descomposición. Si envuelvo recursos en el objeto de pila y ejecutamos la acción de liberar recursos en la función de descomposición del objeto de pila, la probabilidad de filtración de recursos se reduce mucho, ya que los objetos de pila pueden liberar recursos automáticamente, incluso cuando su función es anormal. El proceso práctico es el siguiente: cuando la función se deshace de lo normal, se produce lo que se llama stack_unwinding (revolver a la pila), es decir, se desarrolla en la pila, ya que los objetos de pila, que existen naturalmente en la pila, se ejecutan durante el proceso de descomposición, liberando así los pequeños recursos envasados.

  • 4 Prohibir la generación de objetos de pila

    Como se ha mencionado anteriormente, si decides prohibir la generación de un determinado tipo de objetos de pila, entonces puedes crear una clase de envase de recursos que solo se puede generar en la pila, para liberar automáticamente los recursos de la envase en caso de excepciones.

    Entonces, ¿cómo se prohíbe la generación de objetos de pila? Ya sabemos que la única manera de generar objetos de pila es usando new, y si prohibimos el uso de new, ¿no funciona?. Más adelante, cuando se ejecuta la nueva operación, se llama al operador new, y el operador new se puede volver a cargar. Hay una manera de hacerlo, es hacer que el nuevo operador sea privado, y para la simetría, es mejor volver a cargar el operador como privado.

    #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 ahora es una clase que prohíbe objetos de pila si escribe el siguiente código:

    NoHashObject* fp = new NoHashObject (()) ; // Error de compilación!

    borrar fp;

    El código anterior produce errores de compilación. Bien, ahora que sabes cómo diseñar una clase que prohíbe objetos de pila, tal vez te preguntes como yo, ¿no es posible producir objetos de pila de este tipo si no se puede cambiar la definición de la clase NoHashObject?

    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对象所占的堆空间。
    
        } 
    

    La implementación anterior es problemática, y esta implementación casi no se usa en la práctica, pero aún así escribí el camino, porque entenderla es beneficioso para nuestra comprensión de los objetos de memoria de C++. ¿Qué es lo más fundamental de todas las conversiones de tipo forzadas anteriores?

    Los datos en un pedazo de memoria son invariables, y el tipo son las gafas que usamos, y cuando usamos una gafas, interpretamos los datos en la memoria con el tipo correspondiente, de modo que las diferentes interpretaciones dan una información diferente.

    La conversión forzada de tipo es en realidad cambiar otro par de gafas para volver a ver el mismo dato de memoria.

    También hay que recordar que los diferentes compiladores pueden tener diferentes disposiciones de los datos de los miembros de los objetos, por ejemplo, la mayoría de los compiladores organizan los miembros del puntero ptr de NoHashObject en los primeros 4 bytes del espacio de los objetos para garantizar que las acciones de conversión de la siguiente frase se ejecuten como esperábamos:

    Recursos* rp = (Recursos*) obj_ptr ;

    Sin embargo, no todos los compiladores son así.

    Si podemos prohibir la generación de objetos de un tipo de pila, ¿podemos diseñar una clase para que no produzca objetos de pila?

  • 5 Prohibir la producción de objetos de aluminio

    Como se ha mencionado anteriormente, cuando se crea un objeto de pila, se mueve el puntero de la bóveda para quitar el espacio del tamaño adecuado de la bóveda, y luego se llama directamente a la función de construcción correspondiente en este espacio para formar un objeto de pila, y cuando la función regresa, se llama su función de descomposición para liberar el objeto y luego se ajusta el puntero de la bóveda para recuperar la memoria de la bóveda. No se requiere el operador new/delete en este proceso, por lo que no se puede establecer el operador new/delete como privado. Por supuesto, de la narrativa anterior, puede haber pensado: la descomposición establece la función de construcción o la función de construcción como privada, de modo que el sistema no puede usar la función de descomposición / descomposición de descomposición y, por supuesto, no puede generar objetos en la bóveda.

    Eso sí, y yo también tengo la intención de hacerlo. Pero antes de eso, hay un punto que hay que tener en cuenta, es que si ponemos la función de construcción como privada, entonces no podemos usar new para generar objetos de pila directamente, ya que new también llama su función de construcción después de asignar el espacio para el objeto. Así que, solo voy a establecer la función de descomposición como privada.

    Si una clase no está destinada a ser una clase base, el método más común es declarar su función de descomposición como privada.

    Para restringir los objetos de parámetro, pero no restringir la herencia, podemos declarar la función de parámetro como protegida, así que ambas cosas son buenas.

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

    Luego, puedes usar una clase de NoStackObject como esta:

    NoStackObject* hash_ptr = nuevo NoStackObject() ;

    ...... // para operar sobre el objeto a que hace referencia el hash_ptr

    En este caso, el usuario puede usar el código de código de la aplicación. ¿Qué es esto? Oh, ¿no es un poco extraño que creemos un objeto con new, pero no lo eliminemos con delete, sino con el método de destrucción? Obviamente, los usuarios no están acostumbrados a este uso extraño. Así que decidí establecer la función de construcción también como privada o protegida. Esto vuelve a la pregunta que intenté evitar anteriormente, es decir, ¿cómo generar un objeto sin new?

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

    Ahora puedes usar la clase NoStackObject de esta manera:

    NoStackObject* hash_ptr = NoStackObject::createInstance() ;

    ...... // para operar sobre el objeto a que hace referencia el hash_ptr

    hash_ptr->destroy() ;

    hash_ptr = NULL; // evita el uso de un puntero colgante

    Ahora se siente mejor, la operación de generar objetos y liberar objetos coincide.

  • El método de reciclaje de basura en C++

    Muchos programadores de C o C++ se niegan a la reciclaje de basura, pensando que la reciclaje de basura es definitivamente menos eficiente que ellos mismos para administrar la memoria dinámica, y que en el momento de la reciclaje el programa se detendrá allí, mientras que si controlan la administración de la memoria, el tiempo de asignación y liberación es estable y no causa la interrupción del programa. Finalmente, muchos programadores de C/C++ creen que no se puede implementar el mecanismo de reciclaje de basura en C/C++. Estas ideas erróneas se basan en la falta de comprensión de los algoritmos de reciclaje de basura.

    De hecho, los mecanismos de reciclaje de basura no son más lentos e incluso más eficientes que la asignación de memoria dinámica. Como solo podemos asignar sin liberar, la asignación de memoria solo requiere obtener nueva memoria de la pila en todo momento, basta con el puntero de la pila móvil; y el proceso de liberación se omite y, naturalmente, se acelera. Los algoritmos modernos de reciclaje de basura han evolucionado mucho, los algoritmos de recolección incremental ya pueden hacer que el proceso de reciclaje de basura se realice por etapas, evitando la interrupción del proceso.

    La base de los algoritmos de reciclaje de basura generalmente se basa en escanear y etiquetar todos los bloques de memoria que pueden estar actualmente en uso, y recuperar la memoria no etiquetada de todos los bloques de memoria que ya se han asignado. En C/C++, la idea de que no se puede realizar una reciclaje de basura generalmente se basa en no poder escanear correctamente todos los bloques de memoria que pueden estar todavía en uso, pero lo que parece imposible en realidad no es complicado. En primer lugar, al escanear los datos de la memoria, se puede identificar fácilmente los puntos de referencia que se asignan dinámicamente a la memoria en la pila, y si hay errores de identificación, solo se puede usar algunos datos que no son puntos de referencia como puntos de referencia, y no los puntos de referencia como datos de referencia.

    En el caso de la recolección de basura, basta con escanear el segmento bss, el segmento de datos y el espacio de referencia que se está utilizando para encontrar la cantidad de puntos de memoria dinámica que podrían ser, y el escaneo recurrente de la memoria a la que se hace referencia puede obtener toda la memoria dinámica que se está utilizando actualmente.

    Si usted está dispuesto a implementar un buen reciclaje de basura para su proyecto, es posible mejorar la velocidad de la gestión de la memoria, o incluso reducir el consumo total de memoria. Si está interesado, puede buscar en la red los artículos que ya existen sobre la reciclaje de basura y las bibliotecas implementadas, la visión de vanguardia es especialmente importante para un programador.

    Traducido porHK Zhang fue el primero

  • ¿Por qué el ciclo de vida de una variable local puede extenderse hasta el final del programa cuando se le da una dirección al puntero?

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

    No sólo se puede acceder, sino que también se puede modificar, pero ese acceso es incierto. Las direcciones de las variables locales se encuentran en el propio programa, y después de que la variable de autoridad termine, su valor persistirá siempre que no se le dé la dirección de la variable local a otra variable. Pero si se modifica, es más peligroso, ya que esta dirección de memoria puede darse a otras variables del programa, lo que puede causar el colapso del programa si se modifica por la fuerza del puntero.

    Csdn y bbs


Más.