Pregunta ¿Por qué no utilizar punteros para todo en C ++?


Supongamos que defino alguna clase:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

Luego escribe un código usándolo. ¿Por qué debería hacer lo siguiente?

Pixel p;
p.x = 2;
p.y = 5;

Viniendo de un mundo Java siempre escribo:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

Básicamente hacen lo mismo, ¿verdad? Uno está en la pila mientras que el otro está en el montón, así que tendré que eliminarlo más adelante. ¿Hay alguna diferencia fundamental entre los dos? ¿Por qué debería preferir uno sobre el otro?


73
2018-06-30 15:26


origen


Respuestas:


Sí, uno está en la pila, el otro en el montón. Hay dos diferencias importantes:

  • Primero, lo obvio, y menos importante: las asignaciones de Heap son lentas. Las asignaciones de pila son rápidas.
  • En segundo lugar, y mucho más importante es RAII. Como la versión asignada por la pila se limpia automáticamente, es útil. Se llama automáticamente su destructor, lo que le permite garantizar que todos los recursos asignados por la clase se limpien. Esto es esencialmente cómo evitar fugas de memoria en C ++. Puedes evitarlos al no llamar nunca delete usted mismo, en lugar de envolverlo en objetos asignados a la pila que llaman delete internamente, típicamente en su destructor. Si intenta hacer un seguimiento manual de todas las asignaciones, y llame delete en los momentos adecuados, le garantizo que tendrá al menos una pérdida de memoria por cada 100 líneas de código.

Como un pequeño ejemplo, considere este código:

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

Código bastante inocente, ¿verdad? Creamos un píxel, luego llamamos a una función no relacionada y luego eliminamos el píxel. ¿Hay una pérdida de memoria?

Y la respuesta es "posiblemente". Qué pasa si bar arroja una excepción? delete nunca se llama, el píxel nunca se borra y se pierde memoria. Ahora considera esto:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

Esto no perderá memoria. Por supuesto, en este caso simple, todo está en la pila, por lo que se limpia automáticamente, pero incluso si el Pixel la clase había hecho una asignación dinámica internamente, que tampoco se filtraría. los Pixel clase simplemente se le daría un destructor que lo elimina, y se llamaría a este destructor sin importar cómo dejemos el foo función. Incluso si lo dejamos porque bar lanzó una excepción El siguiente ejemplo ligeramente artificial muestra esto:

class Pixel {
public:
  Pixel(){ x=new int(0); y=new int(0);};
  int* x;
  int* y;

  ~Pixel() {
    delete x;
    delete y;
  }
};

void foo() {
  Pixel p;
  *p.x = 2;
  *p.y = 5;

  bar();
}

La clase Pixel ahora internamente asigna memoria de montón, pero su destructor se encarga de limpiarla, por lo que cuando utilizando la clase, no tenemos que preocuparnos por eso. (Probablemente debería mencionar que el último ejemplo aquí se simplifica mucho, para mostrar el principio general. Si tuviéramos que usar realmente esta clase, también contiene varios errores posibles. Si la asignación de y falla, x nunca se libera , y si el Pixel se copia, terminamos con ambas instancias tratando de eliminar los mismos datos. Así que tomemos el último ejemplo aquí con un grano de sal. El código del mundo real es un poco más complicado, pero muestra la idea general)

Por supuesto, la misma técnica se puede extender a otros recursos que las asignaciones de memoria. Por ejemplo, se puede usar para garantizar que los archivos o las conexiones a la base de datos se cierren después de su uso, o que se liberen los bloqueos de sincronización para su código de enhebrado.


185
2018-06-30 15:34



No son lo mismo hasta que agregue la eliminación.
Su ejemplo es demasiado trivial, pero el destructor en realidad puede contener código que hace un trabajo real. Esto se conoce como RAII.

Así que agrega la eliminación. Asegúrese de que suceda incluso cuando se estén propagando excepciones.

Pixel* p = NULL; // Must do this. Otherwise new may throw and then
                 // you would be attempting to delete an invalid pointer.
try
{
    p = new Pixel(); 
    p->x = 2;
    p->y = 5;

    // Do Work
    delete p;
}
catch(...)
{
    delete p;
    throw;
}

Si ha elegido algo más interesante como un archivo (que es un recurso que debe cerrarse). Luego, hazlo correctamente en Java con punteros que necesitas para hacer esto.

File file;
try
{
    file = new File("Plop");
    // Do work with file.
}
finally
{
    try
    {
        file.close();     // Make sure the file handle is closed.
                          // Oherwise the resource will be leaked until
                          // eventual Garbage collection.
    }
    catch(Exception e) {};// Need the extra try catch to catch and discard
                          // Irrelevant exceptions. 

    // Note it is bad practice to allow exceptions to escape a finally block.
    // If they do and there is already an exception propagating you loose the
    // the original exception, which probably has more relevant information
    // about the problem.
}

El mismo código en C ++

std::fstream  file("Plop");
// Do work with file.

// Destructor automatically closes file and discards irrelevant exceptions.

Aunque la gente menciona la velocidad (debido a encontrar / asignar memoria en el montón). Personalmente, este no es un factor decisivo para mí (los asignadores son muy rápidos y se han optimizado para el uso en C ++ de objetos pequeños que se crean / destruyen constantemente).

La razón principal para mí es el tiempo de vida del objeto. Un objeto definido localmente tiene un tiempo de vida muy específico y bien definido y se garantiza que el destructor será llamado al final (y por lo tanto puede tener efectos secundarios específicos). Un puntero por otro lado controla un recurso con una vida dinámica.

La principal diferencia entre C ++ y Java es:

El concepto de quién posee el puntero. Es responsabilidad del propietario eliminar el objeto en el momento apropiado. Es por eso que rara vez ves crudo punteros como ese en programas reales (ya que no hay información de propiedad asociada con un crudo puntero). En cambio, los punteros generalmente están envueltos en punteros inteligentes. El puntero inteligente define la semántica de quién posee la memoria y, por lo tanto, quién es responsable de limpiarla.

Los ejemplos son:

 std::auto_ptr<Pixel>   p(new Pixel);
 // An auto_ptr has move semantics.
 // When you pass an auto_ptr to a method you are saying here take this. You own it.
 // Delete it when you are finished. If the receiver takes ownership it usually saves
 // it in another auto_ptr and the destructor does the actual dirty work of the delete.
 // If the receiver does not take ownership it is usually deleted.

 std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr
 // A shared ptr has shared ownership.
 // This means it can have multiple owners each using the object simultaneously.
 // As each owner finished with it the shared_ptr decrements the ref count and 
 // when it reaches zero the objects is destroyed.

 boost::scoped_ptr<Pixel>  p(new Pixel);
 // Makes it act like a normal stack variable.
 // Ownership is not transferable.

Hay otros.


30
2018-06-30 15:50



Lógicamente hacen lo mismo, excepto por la limpieza. Solo el código de ejemplo que ha escrito tiene una pérdida de memoria en el caso del puntero porque esa memoria no se libera.

Procedente de un entorno Java, puede que no esté completamente preparado para saber cuánto de C ++ gira en torno al seguimiento de lo que se ha asignado y quién es el responsable de liberarlo.

Al usar variables de pila cuando sea apropiado, no tiene que preocuparse por liberar esa variable, se va con el marco de pila.

Obviamente, si eres súper cuidadoso, siempre puedes asignar el montón y liberarlo manualmente, pero parte de la buena ingeniería del software es construir cosas de tal manera que no puedan romperse, en lugar de confiar en tu programador superhumano. Fu para nunca cometer un error.


25
2018-06-30 15:33



Prefiero usar el primer método cada vez que tengo la oportunidad porque:

  • es mas rapido
  • No tengo que preocuparme por la desasignación de memoria
  • p será un objeto válido para todo el alcance actual

24
2018-06-30 15:32



"¿Por qué no usar punteros para todo en C ++"

Una respuesta simple, porque se convierte en un gran problema para administrar la memoria, asignar, eliminar y liberar.

Los objetos automáticos / de pila eliminan parte del trabajo ocupado de eso.

eso es solo lo primero que diría sobre la pregunta.


14
2018-06-30 15:33



Una buena regla general es NUNCA usar algo nuevo a menos que sea absolutamente necesario. Tus programas serán más fáciles de mantener y menos propensos a errores si no usas los nuevos ya que no tienes que preocuparte por dónde limpiarlos.


11
2018-06-30 15:35



El código:

Pixel p;
p.x = 2;
p.y = 5;

no asigna dinámicamente la memoria; no hay búsqueda de memoria libre, no hay actualización del uso de la memoria, nada. Es totalmente gratis El compilador reserva espacio en la pila para la variable en tiempo de compilación; tiene mucho espacio para reservar y crea un solo código de operación para mover el puntero de la pila a la cantidad requerida.

Usar nuevo requiere toda esa sobrecarga de administración de memoria.

Entonces, la pregunta es: ¿Desea utilizar el espacio de la pila o el montón para sus datos? Las variables de pila (o locales) como 'p' no requieren desreferencia mientras que el uso de nuevas agrega una capa de indirección.


11
2018-06-30 15:34



Sí, al principio tiene sentido, viniendo de un fondo Java o C #. No parece un gran problema tener que recordar liberar la memoria que asignó. Pero cuando tengas tu primera pérdida de memoria, te estarás rascando la cabeza, porque SWORE liberaste todo. Luego, la segunda vez que sucede y la tercera te frustrarás aún más. Finalmente, después de seis meses de dolores de cabeza debido a problemas de memoria, comenzará a cansarse y la memoria asignada por la pila comenzará a verse cada vez más atractiva. Qué lindo y limpio, simplemente colócalo en la pila y olvídate de él. Muy pronto usarás la pila cada vez que puedas salirte con la tuya.

Pero - no hay sustituto para esa experiencia. ¿Mi consejo? Pruébalo a tu manera, por ahora. Verás.


10
2018-06-30 15:45



Mi reacción visceral es solo decirte que esto podría llevar a pérdidas de memoria serias. Algunas situaciones en las que podría estar utilizando punteros podrían generar confusión sobre quién debería ser el responsable de eliminarlas. En casos simples como su ejemplo, es bastante fácil ver cuándo y dónde debe llamar eliminar, pero cuando comienza a pasar punteros entre clases, las cosas pueden ponerse un poco más difíciles.

Yo recomendaría buscar en el impulso la biblioteca de punteros inteligentes para sus punteros.


6
2018-06-30 15:31