Antes de escribir una estrategia en C++, es necesario tener conocimientos básicos, no un conocimiento de Confucio, al menos conocer estas reglas. A continuación se incluyen las transcripciones.
Si una persona se considera un experto en programación y no sabe nada de memoria, entonces te puedo decir que debe estar presumiendo. Escribir programas en C o C++ requiere más atención a la memoria, no solo porque la asignación de memoria razonable afecta directamente a la eficiencia y el rendimiento del programa, sino también porque, principalmente, cuando manejamos la memoria, se producen problemas por descuido, y muchas veces, estos problemas no son fáciles de detectar, como una fuga de memoria, como un puntero suspendido. El autor no está aquí hoy para discutir cómo evitar estos problemas, sino para conocer los objetos de memoria en C++ desde otra perspectiva.
Sabemos que C++ divide la memoria en tres áreas lógicas: pilas, columnas y áreas de almacenamiento estático. Siendo así, los objetos que se encuentran entre ellas se denominan objetos de pila, columnas y objetos estáticos. Entonces, ¿cuál es la diferencia entre estos diferentes objetos de memoria? ¿Cuáles son las ventajas y desventajas de los objetos de pila y columnas? ¿Cómo se prohíbe la creación de objetos de pila o columnas? Estos son los temas de hoy.
1 Conceptos básicos
En primer lugar, veamos . , generalmente utilizado para almacenar variables o objetos locales, como los objetos que declaramos en una definición de función con una declaración similar a la siguiente:
Type stack_object ;
stack_object es un objeto de pila cuya vida comienza en el punto de definición y termina cuando la función que la contiene regresa.
Además, casi todos los objetos temporales son objetos de la barra. Por ejemplo, la siguiente definición de función:
Type fun(Type object);
Esta función produce al menos dos objetos temporales, primero, los argumentos 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 no es objeto, sino objeto_copy1, que se usa dentro de la función. Naturalmente, objeto_copy1 es un objeto de archivo que se libera cuando la función lo devuelve; y esta función es un valor devuelto, y cuando la función lo devuelve, si no se considera la optimización de los valores de devolución ((NRV), también se produce un objeto temporal object_copy2, que se libera durante un período de tiempo después de la función de devolución. Por ejemplo, una función tiene el siguiente código:
Type tt ,result ; //生成两个栈对象
tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
La ejecución de la segunda sentencia anterior es la siguiente: primero, la función fun genera un objeto temporal object_copy2 cuando se devuelve, y luego llama al operador de asignación para ejecutar
tt = object_copy2 ; //调用赋值运算符
¿Lo ves? El compilador genera tantos objetos temporales para nosotros sin que nos demos cuenta, y el gasto de tiempo y espacio para generar estos objetos temporales puede ser muy grande, así que, tal vez entiendas por qué es mejor para los objetos de la barra de la pirámide pasar con referencia constante en lugar de pasar los parámetros de la función por valor.
En C++, la creación y destrucción de todos los objetos de la pila es responsabilidad del programador, por lo que, si no se maneja bien, se producen problemas de memoria. Si se asigna un objeto de la pila y se olvida de liberarlo, se produce una fuga de memoria; y si se ha liberado un objeto, pero no se ha establecido el puntero correspondiente como NULL, el indicador es el llamado puntero de suspensión suspendido, y el uso de este indicador nuevamente, se produce un acceso ilegal, lo que puede causar un colapso del programa en casos graves.
Entonces, ¿cómo se distribuyen los objetos de la pila en C++? La única manera de hacerlo es con new (por supuesto, también se puede obtener memoria de pila en C con instrucciones tipo malloc), siempre que use new, se distribuirá un pedazo de memoria en la pila y se devolverá el puntero que apunta a ese objeto de pila.
En realidad, antes de que se ejecute el código de visualización en la función principal, se llama a una función _main () generada por el compilador, mientras que la función _main () realiza la construcción e inicialización de todos los objetos globales. Antes de que finalice la función principal, se llama a la función exit generada por el compilador para liberar todos los objetos globales. Por ejemplo, el siguiente código:
void main(void)
{
... // 显式代码
}
// 实际上转化为这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
... // 显式代码
...
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
Así que, sabiendo esto, se pueden extraer algunas técnicas, como, suponiendo que vamos a hacer algunos trabajos preparatorios antes de que la función main () se ejecute, entonces podemos escribir estos trabajos preparatorios en la función de construcción de un objeto global personalizado, de modo que, antes de que el código gráfico de la función main () se ejecute, la función de construcción de este objeto global se llame y realice el movimiento esperado, y así se logra nuestro objetivo. Si hablamos de objetos globales en el área de almacenamiento estático, ¿entonces hay objetos estáticos locales?
También hay un objeto estático, que es un miembro estático de la clase. Considerando esta situación, se involucran algunos problemas más complejos.
El primer problema es la vida útil de los objetos de los miembros estáticos de la clase, que se crean con la creación del primer objeto de la clase y se extinguen al final de toda la aplicación. Es decir, existe una situación en la que definimos una clase en la que la clase tiene un objeto estático como miembro, pero durante la ejecución de la aplicación, si no creamos ninguno de los objetos de la clase, no se creará el objeto estático que contiene la clase.
El segundo problema es cuando ocurre lo siguiente:
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 = …… ;
Observe las tres declaraciones arriba marcadas como negras, y vea si el s_object que visitan es el mismo objeto. La respuesta es afirmativa, y si lo son, es que se refieren al mismo objeto, y eso no suena como cierto, ¿verdad? Pero es cierto, y puede escribir un simple fragmento de código para comprobarlo por sí mismo. Lo que voy a hacer es explicar por qué es así.
Imaginemos que cuando pasamos un objeto de tipo Derived1 a una función que acepta un parámetro de tipo Base sin referencia, se produce un corte, ¿cómo se hace? Creo que ahora ya lo sabes, es simplemente sacar el subobjeto del objeto de tipo Derived1 y ignorar todos los otros miembros de datos que Derived1 ha personalizado, y luego pasar este subobjeto a la función (en realidad, la función usa una copia de este subobjeto).
Todos los objetos de las clases derivadas de la clase Base heredada contienen un subobjeto de tipo Base (que es el punto de referencia de un objeto Derived1 con un puntero de tipo Base, lo cual es lógico también en el caso de las claves de varios estados), mientras que todos los subobjetos y todos los objetos de tipo Base comparten el mismo objeto s_object. Naturalmente, todas las instancias de la clase en el sistema de herencia entero derivado de la clase Base comparten el mismo objeto s_object. La disposición de los objetos de los ejemplos 1, 2 y 3 mencionados anteriormente se muestra en la siguiente gráfica:
2 Comparación de los tres objetos de memoria
La ventaja de los objetos de la barra es que se generan automáticamente cuando es apropiado y se destruyen automáticamente cuando es apropiado, sin que los programadores se preocupen; y la velocidad de creación de los objetos de la barra es generalmente más rápida que la de los objetos de la pila, ya que al distribuir los objetos de la pila, se invoca la operación de nuevo operador, el nuevo operador utiliza algún tipo de algoritmo de búsqueda de espacio en la memoria, y el proceso de búsqueda puede ser muy costoso, generar objetos de la barra no es tan difícil, solo se necesita mover el puntero de la barra.
Los objetos de la pila, sus momentos de generación y destrucción deben ser definidos por el programador, es decir, el programador tiene control total sobre la vida de los objetos de la pila. A menudo necesitamos un objeto de este tipo, por ejemplo, necesitamos crear un objeto que pueda ser accesible por varias funciones, pero no queremos que sea global, entonces crear un objeto de la pila es sin duda una buena opción en este momento, y luego transmitir el puntero de este objeto de la pila entre las funciones, para poder compartirlo. Además, en comparación con el espacio libre, la capacidad de la pila es mucho más grande.
Ahora veamos los objetos estáticos.
En primer lugar, los objetos globales. Los objetos globales proporcionan una forma más sencilla de comunicación entre clases y funciones, aunque no es elegante. En general, en lenguajes completamente orientados a objetos, los objetos globales no existen, como C#, ya que los objetos globales significan inseguridad y alta integración, y el uso excesivo de objetos globales en el programa reduce en gran medida la robustez, estabilidad, mantenimiento y reutilización del programa.
El segundo elemento es el miembro estático de la clase. Como se mencionó anteriormente, todos los objetos de la clase base y sus derivados comparten este objeto de miembro estático, por lo que este miembro estático es sin duda una buena opción cuando se necesita compartir datos o comunicarse entre estas clases o entre estos objetos de clase.
Luego están los objetos locales estáticos, que se utilizan principalmente para preservar el estado intermedio en el que se encuentra el objeto durante las llamadas repetidas de la función, y uno de los ejemplos más destacados es la función recursiva, que todos sabemos que se llama a sí misma. Si se define un objeto local no estático en una función recursiva, el gasto es enorme cuando el número de repetidas es bastante grande. Esto se debe a que el objeto local no estático es un objeto pirata, cada llamada recursiva produce un objeto así, y cada vez que regresa, libera este objeto, y, como tal, los objetos se limitan a la capa de la llamada actual, son invisibles para las capas de embutido más profundas y las capas exteriores más superficiales.
En el diseño de funciones recursivas, se pueden usar objetos estáticos en lugar de objetos locales no estáticos, lo que no solo reduce el gasto de generar y liberar objetos no estáticos cada vez que se llama y devuelve una función recursiva, sino que también se puede guardar el estado intermedio de la llamada recursiva y acceder a él para cada capa de llamada.
3 Las cosechas inesperadas de objetos usados para cocinar
Como se ha mencionado anteriormente, los objetos de silicio se crean en el momento adecuado y luego se liberan automáticamente en el momento adecuado, es decir, los objetos de silicio tienen funciones de administración automática. Entonces, ¿en qué se liberan automáticamente los objetos de silicio? Primero, al final de su vida útil; segundo, cuando ocurren anomalías en la función en la que se encuentran.
Cuando un objeto de la pila se libera automáticamente, llama a su propia función de descomposición. Si encierra recursos en un objeto de la pila y ejecuta la acción de liberación de recursos en la función de descomposición del objeto de la pila, la probabilidad de fuga de recursos se reduce considerablemente, ya que el objeto de la pila puede liberar recursos automáticamente, incluso cuando ocurre una anomalía en la función en la que se encuentra. El proceso real es el siguiente: cuando la función echa una anomalía, se produce lo que se conoce como stack_unwinding (enrollar la pila hacia atrás), es decir, la pila se expande, ya que los objetos de la pila, que existen en la pila natural, se ejecutan en el proceso de la vuelta de la pila, lo que libera los recursos de su encierre.
4 Se prohíbe la creación de objetos de pila
Como se mencionó anteriormente, si decides prohibir la generación de un tipo de objeto de pila, puedes crear una clase de envase de recursos que solo se puede generar en la pila, lo que permite liberar automáticamente los recursos envueltos en casos excepcionales.
Entonces, ¿cómo se prohíbe la generación de objetos de pila? Ya sabemos que la única forma de generar objetos de pila es con la operación new, pero si se prohíbe el uso de new no se puede. Más adelante, la operación new se ejecuta con la llamada al operador new, y el operador new se puede volver a cargar.
#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 es ahora una clase que prohíbe objetos de la pila si escribes el siguiente código:
NoHashObject* fp = new NoHashObject() ; // Error de compilación!
delete fp ;
El código anterior generará errores de compilación. Bueno, ahora que ya sabes cómo diseñar una clase que prohíba objetos de la pila, tal vez tengas la misma duda que yo, ¿no es cierto que no se puede generar ese tipo de objeto de la pila si la definición de la clase NoHashObject no se puede cambiar? No, hay un método, lo que yo llamo la piratería de la violencia de la cola.
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 de arriba es problemática, y esta implementación casi no se usa en la práctica, pero de todos modos escribo el camino, porque entenderla es bueno para que entendamos los objetos de memoria en C++.
Los datos en un bloque de memoria son constantes, y el tipo es la lente que usamos, y cuando usamos una lente, interpretamos los datos en la memoria con el tipo correspondiente, de modo que las diferentes interpretaciones obtienen información diferente.
La llamada conversión de tipo forzada es en realidad cambiar de gafas y volver a ver el mismo tipo de datos en la memoria.
También hay que recordar que la disposición de los datos de los miembros de un objeto puede ser diferente en diferentes compiladores, por ejemplo, la mayoría de los compiladores colocan los miembros del puntero ptr de NoHashObject en los primeros 4 bytes del espacio del objeto para garantizar que el siguiente movimiento de conversión de la frase se ejecute como se espera:
Resource* rp = (Resource*)obj_ptr ;
Sin embargo, no todos los compiladores lo son.
Ahora que podemos prohibir la creación de un tipo de objeto de pila, ¿podemos diseñar una clase que no pueda generar objetos de pila?
5 Se prohíbe la producción de objetos de cocina
Como se mencionó anteriormente, al crear un objeto de la barra, se mueve el puntero de la copa para extraer el espacio del tamaño apropiado de la barra, y luego se llama directamente a la función de construcción correspondiente en este espacio para formar un objeto de la barra, y cuando la función regresa, se llama a su función de composición para liberar el objeto, y luego se ajusta el puntero de la copa para recuperar el bloque de la barra. La memoria de la barra no requiere el operador new/delete en este proceso, por lo que configurar el operador new/delete como privado no puede cumplir el propósito.
Esto sí es posible, y es lo que voy a hacer. Pero antes de eso, hay algo que hay que tener en cuenta, es decir, que si configuramos la función de construcción como privada, no podremos usar new para generar objetos de la pila directamente, ya que new también llamará a su función de construcción después de asignar espacio para los objetos.
Si una clase no pretende ser una clase base, la solución habitual es declarar su función de descomposición como privada.
Para limitar los objetos de pared, pero no limitar la herencia, podemos declarar la función de descomposición como protected, lo cual es lo mejor de ambos.
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
A continuación, se puede usar la clase NoStackObject de la siguiente manera:
NoStackObject* hash_ptr = new NoStackObject() ;
… … // ejecutar el objeto al que se dirige el hash_ptr
hash_ptr->destroy() ; ¿No te parece un poco extraño que creemos un objeto con new y no lo eliminemos con delete, sino que lo destruyamos? Obviamente, los usuarios no están acostumbrados a este tipo de uso extraño. Así que decidí configurar la función de construcción como privada o protegida.
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//调用保护的构造函数
}
void destroy()
{
delete this ;//调用保护的析构函数
}
};
Ahora puedes usar la clase NoStackObject de la siguiente manera:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
… … // ejecutar el objeto al que se dirige el hash_ptr
hash_ptr->destroy() ;
hash_ptr = NULL; // evita el uso de los punteros en suspensión
Ahora se siente mucho mejor, ya que la generación de objetos y la liberación de objetos son idénticas.
Muchos programadores de C o C++ se niegan a la recolección de residuos, porque creen que la recolección de residuos es menos eficiente que administrar la memoria dinámica, y que la recuperación obligará al programa a detenerse allí, mientras que si se controla la administración de la memoria, el tiempo de distribución y liberación es estable y no provocará el paro del programa. Finalmente, muchos programadores de C/C++ están convencidos de que no se puede implementar un mecanismo de recolección de residuos en C/C++.
En realidad, el mecanismo de recuperación de basura no es lento, incluso es más eficiente que la distribución de memoria dinámica. Ya que solo podemos distribuir sin liberar, entonces la asignación de memoria solo requiere obtener nueva memoria constantemente de la pila y mover el puntero de la cima de la pila es suficiente; y el proceso de liberación se omite, y naturalmente también acelera la velocidad. Los algoritmos modernos de recuperación de basura han evolucionado mucho, los algoritmos de recolección incremental ya pueden hacer que el proceso de recuperación de basura se realice por etapas, evitando interrumpir el funcionamiento del programa.
La base de los algoritmos de recuperación de la basura generalmente se basa en escanear y marcar todos los bloques de memoria que se pueden usar actualmente, recuperando la memoria no marcada de toda la memoria que ya se ha asignado. La idea de que no se puede realizar la recuperación de la basura en C/C++ generalmente se basa en la imposibilidad de escanear correctamente todos los bloques de memoria que aún se pueden usar, pero lo que parece imposible en realidad no es tan complicado. En primer lugar, mediante el escaneo de los datos de la memoria, las punteras que apuntan a la memoria distribuida dinámicamente por la pila son fácilmente identificables, y si se detecta un error, solo se pueden incluir algunos datos no punteros como punteros, sin incluir los punteros como datos no punteros.
En la recuperación de basura, solo se necesita escanear el segmento bss, el segmento de datos y el espacio de cadrado que se está utilizando actualmente para encontrar la cantidad de indicadores de memoria dinámica que pueden ser, y el escaneo recursivo de memoria referida puede obtener toda la memoria dinámica que se está utilizando actualmente.
Si quieres implementar un buen reciclador de basura para tu proyecto, es posible mejorar la velocidad de la administración de memoria e incluso reducir el consumo total de memoria. Si estás interesado, puedes buscar en la web los artículos existentes sobre reciclaje de basura y las bibliotecas implementadas, y es especialmente importante para un programador ampliar el horizonte.
Se reproduce a partir deHK Zhang
#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 también modificar, pero ese acceso es incierto. Las direcciones de las variables locales se encuentran en la propia pila del programa, y después de que la variable local de la autoridad finalice, su valor seguirá existiendo siempre que no se le dé la dirección de memoria de la variable local a otra variable. Pero si se modifica, es más peligroso, ya que esta dirección de memoria puede dar a las otras variables del programa, y si se forza la modificación a través del puntero, puede causar el colapso del programa.