2010-08-17 11 views
10

Estoy tratando de entender algo en C++. Básicamente tengo esto:¿El objeto ya está inicializado en la declaración?

class SomeClass { 
    public: 
     SomeClass(); 
    private: 
     int x; 
}; 

SomeClass::SomeClass(){ 
    x = 10; 
} 

int main() { 
    SomeClass sc; 
    return 0; 
} 

pensé que sc es una variable no inicializada de tipo SomeClass, pero a partir de los diversos tutoriales que he encontrado parece que esta declaración es en realidad una inicialización que llama al contructor SomeClass(), sin mí necesidad de llamar a "sc = new SomeClass();" o algo así.

Como vengo del mundo C# (y conozco un poco C, pero no C++), trato de entender cuándo necesito cosas como nuevas y cuándo liberar objetos como ese. Encontré un patrón llamado RAll que parece no estar relacionado.

¿Cómo se llama este tipo de inicialización y cómo sé si algo es una mera declaración o una inicialización completa?

+6

"Como vengo del mundo C#" ¿Entonces? C++ no es C#, olvida que sabes C# en absoluto. Necesitas un [buen libro para principiantes] (http://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list), y comienzas desde cero. – GManNickG

+0

¡Gracias por la lista! Es cierto que C++ y C# son dos idiomas muy diferentes. Supongo que simplemente no quiero leer acerca de qué bucles o clases son y más acerca de cómo funcionan en C++, por lo tanto, he estado evitando los libros. Veré la lista. –

+0

@Michael: no puedes evitar los libros. :) C++ es su propio idioma. Parece que crees que ya entiendes las clases de C++, pero no son lo mismo que en C#. Claro, es posible que ya sepas qué son las declaraciones de selección o iteración, pero eso es solo un capítulo. – GManNickG

Respuesta

17

creo que hay varias cosas aquí:

  • diferencia entre la variable automática y dinámicamente asignada variables
  • curso de la vida de los objetos
  • RAII
  • C# paralelo

automática vs Dinámico

Una variable automática es una variable cuyo sistema administrará la vida útil. Vamos a zanja variables globales en este momento, es complicado, y concentrarse en el caso habitual:

int main(int argc, char* argv[]) // 1 
{         // 2 
    SomeClass sc;     // 3 
    sc.foo();      // 4 
    return 0;      // 5 
}         // 6 

Aquí sc es una variable automática. Se garantiza que se habrá inicializado por completo (es decir, se garantiza que el constructor se habrá ejecutado) después de que la ejecución de la línea (3) se haya completado correctamente. Su destructor se invocará automáticamente en la línea (6).

Generalmente hablamos del alcance de una variable: desde el punto de declaración hasta el corchete de cierre correspondiente; y el lenguaje garantiza la destrucción cuando se salga del alcance, ya sea con un return o una excepción.

Por supuesto, no hay garantía en el caso de que invoque el temido "Comportamiento indefinido", que generalmente resulta en un bloqueo.

Por otro lado, C++ también tiene variables dinámicas, es decir, variables que asigna usando new.

int main(int argc, char* argv[]) // 1 
{         // 2 
    SomeClass* sc = 0;    // 3 
    sc = new SomeClass();   // 4 
    sc->foo();      // 5 
    return 0;      // 6 
}         // 7 (!! leak) 

Aquí sc sigue siendo una variable automática, sin embargo difieren en su tipo: ahora es un puntero a una variable de tipo SomeClass.

En la línea (3) sc se le asigna un valor de puntero null (nullptr en C++ 0x) porque no apunta a cualquier instancia de SomeClass. Tenga en cuenta que el idioma no garantiza ninguna inicialización por sí mismo, por lo que debe asignar algo explícitamente; de ​​lo contrario, tendrá un valor de basura.

En la línea (4) creamos una variable dinámica (utilizando el operador new) y asignamos su dirección a sc. Tenga en cuenta que la variable dinámica en sí no tiene nombre, el sistema solo nos proporciona un puntero (dirección).

En la línea (7) el sistema destruye automáticamente sc, sin embargo, no destruye la variable dinámica que señalaba, y por lo tanto ahora tenemos una variable dinámica cuya dirección no está almacenada en ninguna parte. A menos que usemos un recolector de basura (que no es el caso en C++ estándar), hemos filtrado la memoria, ya que la memoria de la variable no se recuperará antes de que el proceso finalice ... e incluso entonces el destructor no se ejecutará (muy mal si tuviera efectos secundarios).

por vida de objetos

Herb Sutter tiene unos artículos muy interesantes sobre este tema. Aquí está the first.

A modo de resumen:

  • Un objeto vive tan pronto como su constructor ejecuta hasta el final. Significa que si el constructor tira, el objeto nunca vivió (considéralo un accidente de embarazo).
  • Un objeto está muerto tan pronto como se invoca su destructor, si el destructor arroja (esto es MAL) no se puede intentar de nuevo porque no puede invocar ningún método en un objeto muerto, es un comportamiento indefinido.

Si volvemos al primer ejemplo:

int main(int argc, char* argv[]) // 1 
{         // 2 
    SomeClass sc;     // 3 
    sc.foo();      // 4 
    return 0;      // 5 
}         // 6 

sc está vivo de la línea (4) a la línea (5), ambos inclusive. En la línea (3) está siendo construido (que puede fallar por cualquier cantidad de razones) y en la línea (6) está siendo destruido.

RAII

RAII significa Recursos adquisición es de inicialización. Es una expresión idiomática administrar los recursos, y especialmente asegurar que los recursos finalmente se liberarán una vez que se hayan adquirido.

En C++, ya que no tenemos recolección de basura, esta expresión se aplica principalmente a la gestión de memoria, pero también es útil para cualquier otro tipo de recursos: bloqueos en entornos multiproceso, bloqueos de archivos, enchufes/conexiones en red, etc. ...

Cuando se utiliza para la gestión de memoria, se utiliza para acoplar la vida útil de la variable dinámica a la vida útil de un conjunto determinado de variables automáticas, garantizando que la variable dinámica no las perdure (y se pierda).

En su forma más simple, es acoplada a una única variable automática:

int main(int argc, char* argv[]) 
{ 
    std::unique_ptr<SomeClass> sc = new SomeClass(); 
    sc->foo(); 
    return 0; 
} 

Es muy similar al primer ejemplo, excepto que asignar dinámicamente una instancia de SomeClass. La dirección de esta instancia se entrega al objeto sc, de tipo std::unique_ptr<SomeClass> (es una instalación de C++ 0x, use boost::scoped_ptr si no está disponible). unique_ptr garantiza que el objeto señalado se destruirá cuando se destruya sc.

En una forma más complicada, podría asociarse a varias variables automáticas usando (por ejemplo) std::shared_ptr, que como su nombre lo indica permite compartir un objeto y garantiza que el objeto se destruirá cuando se destruya el último participante. Tenga en cuenta que esto no es equivalente a usar un recolector de basura y que puede haber problemas con los ciclos de referencias, no profundizaré aquí, así que recuerde que std::shared_ptr no es una panacea.

Debido a que es muy complicado de manejar perfectamente la vida de una variable dinámica sin RAII en la cara de las excepciones y código multiproceso, la recomendación es:

  • uso de variables automáticas tanto como sea posible
  • para la dinámica las variables, nunca se invocan delete por su cuenta y siempre hace uso de las instalaciones RAII

personalmente considero cualquier ocurrencia de delete ser fuertemente sospechoso, y yo todos los días s pida su eliminación en las revisiones del código: es un olor a código.

C# paralelo

En C# que utilizan, sobre todo las variables dinámicas *. Esta es la razón:

  • Si sólo declara una variable, sin asignación, su valor es nulo: en esencia, sólo se está manipulando punteros y que por lo tanto tiene un puntero nulo (se garantiza la inicialización, gracias a Dios)
  • Utiliza new para crear valores, esto invoca el constructor de su objeto y le da la dirección del objeto; observe cómo la sintaxis es similar a C++ para las variables dinámicas

Sin embargo, a diferencia de C++, C# es basura recolectada por lo que no tiene que preocuparse por la administración de la memoria.

La recolección de basura también significa que la vida útil de los objetos es más difícil de entender: se crean cuando se solicitan pero se destruyen a la conveniencia del sistema. Esto puede ser un problema para implementar RAII, por ejemplo, si realmente desea liberar el bloqueo rápidamente, y el lenguaje tiene una serie de recursos para ayudarlo con using palabra clave + IDisposable interfaz de la memoria.

*: es fácil de verificar, si después de declarar una variable su valor es null, entonces será una variable dinámica. Creo que para int el valor será 0 indicando que no lo es, pero ya han pasado 3 años desde que jugueteé con C# para un proyecto de curso así que ...

3

Lo que está haciendo en la primera línea de main() es asignar un objeto SomeClass en la pila. El operador new en su lugar asigna objetos en el montón, devolviendo un puntero a la instancia de clase. Esto a la larga conduce a las dos técnicas de acceso diferentes a través de la . (con el ejemplo) o con el -> (con el puntero)

Ya que sabes C, se realizan asignación de pila cada vez que dices, por ejemplo int i;. Por otro lado, la asignación de montón se realiza en C con malloc(). malloc() devuelve un puntero a un espacio recientemente asignado, que luego se convierte en un puntero a algo. ejemplo:

int *i; 
i = (int *)malloc(sizeof(int)); 
*i=5; 

Mientras desasignación de cosas asignado en la pila se realiza automáticamente, desasignación de cosas asignada en el montón debe ser realizada por el programador.

El origen de su confusión proviene del hecho de que C# (que no uso, pero sé que es similar a Java) no tiene asignación de pila. Lo que haces cuando dices SomeClass sc, es declarar una referencia SomeClass que no está inicializada hasta que digas new, que es el momento en que el objeto surge. Antes del new, no tiene ningún objeto. En C++, este no es el caso. No existe un concepto de referencias en C++ que sea similar a C# (o java), aunque tiene referencias en C++ solo durante llamadas a funciones (es un paradigma de paso por referencia, en la práctica. Por defecto, C++ pasa por valor, lo que significa que copiar objetos en la llamada de función). Sin embargo, esta no es toda la historia. Verifique los comentarios para obtener detalles más precisos.

+0

Las comparaciones C#/C++ aquí están en el espíritu correcto, pero en realidad no son precisas en ningún lado. –

+0

@Merlyn No sé nada sobre C#, y muy poco sobre Java, por lo que hay una posibilidad no despreciable de que haya sido inexacto o incluso incorrecto –

+0

@Stefano: el equivalente más cercano al comportamiento automático de C++ en C# es el tipo de valor (estructuras y muchos tipos incorporados). Se copian por valor y no tiene que llamar "nuevo" en ellos. Si llama a "nuevo" en ellos, todavía se asignan en cualquier espacio de memoria en el que se encuentren (apilar si en el ámbito local, montón si está en un objeto asignado en el montón). –

2

En su caso, sc se asigna en la pila, utilizando el constructor predeterminado para SomeClass. Como está en la pila, la instancia será destruida al regresar de la función. (Esto sería más impresionante si ejemplarizado SomeClass sc dentro de una función llamada de main --el memoria asignada para sc sería sin asignar al regreso a main.)

El new de palabras clave, en lugar de asignar memoria en el tiempo de ejecución pila, asigna la memoria en el montón. Como C++ no tiene recolección de basura automática, usted (el programador) es responsable de la asignación de la memoria que asigna en el montón (utilizando la palabra clave delete), para evitar pérdidas de memoria.

+0

Gracias. Entonces, si necesito que mi objeto persista en múltiples métodos no relacionados (como un miembro de la clase?) Necesitaría nuevo/eliminar, mientras que todo dentro de una función puede ir a la pila? –

+0

@Michael: No. Simplemente haces: 'struct foo {int bar; }; ', y cualquier método que' foo' pueda tener también podrá usar 'bar'. 'bar' tiene la misma duración que' foo', guarde el constructor. – GManNickG

+0

@Michael: puede asignar en la pila, y luego hacer que la subrutina acepte una referencia (que es un "alias"), en lugar de usar punteros, pero es muy poco práctico. –

2

Cuando declara una variable (sin extern) en un ámbito de función (por ejemplo, en main) también definió la variable.La variable entra en existencia en el punto en que se alcanza la declaración y desaparece cuando se alcanza el final de su alcance (en este caso, el final de la función main).

Cuando un objeto se crea, si tiene un constructor declarado por el usuario, entonces uno de sus constructores se usa para inicializarlo. Del mismo modo, si tiene un destructor declarado por el usuario, se usa cuando el objeto sale del alcance para realizar las acciones de limpieza necesarias en el punto en el que sale del alcance. Esto es diferente de los lenguajes que tienen finalizadores que pueden ejecutarse o no, y ciertamente no en un punto de tiempo determinista. Es más como using/IDisposable.

Una expresión new se usa en C++ para crear dinámicamente un objeto. Por lo general, se utiliza cuando el tiempo de vida del objeto no puede vincularse a un ámbito particular. Por ejemplo, cuando debe continuar existiendo después de que la función que lo crea finalice. También se usa cuando ahora se conoce el tipo exacto del objeto que se creará en el momento del compilador, p. en una función de fábrica. Los objetos de creación dinámica a menudo se pueden evitar en muchos casos en los que se usan comúnmente en lenguajes como Java y C#.

Cuando se crea un objeto con new, debe en algún momento destruirse mediante una expresión delete. Para asegurarse de que los programadores no olviden hacer esto, es común emplear algún tipo de objeto de puntero inteligente para administrarlo automáticamente, p. a shared_ptr desde tr1 o boost.

2

Algunas otras respuestas básicamente te dicen "sc se asigna en la pila, nuevo asigna el objeto en el montón". Prefiero no pensar en ello de esta manera, ya que combina los detalles de implementación (pila/pila) con la semántica del código. Como estás acostumbrado a la forma en que C# hace las cosas, creo que también crea ambigüedades. En cambio, la forma en que prefiero pensar es la forma en que lo describe el estándar C++:

sc es una variable del tipo SomeClass, declarada en el alcance del bloque (es decir, las llaves que componen la función principal). Esto se llama una variable local . Porque no está declarado static o extern, esto hace que tenga duración de almacenamiento automático. Lo que esto significa es que cada vez que se ejecuta la línea SomeClass sc;, la variable se inicializará (ejecutando su constructor), y cuando la variable sale del alcance al salir del bloque, se destruirá (ejecutando su destructor, ya que lo hace no tiene uno y su objeto es datos simples y antiguos, nada se hará).

Antes he dicho "Debido a que no se ha declarado o staticextern", si se hubiera declarado como tal sería tener una duración de almacenamiento estático . Se inicializaría antes del inicio del programa (técnicamente en el alcance del bloque se inicializaría en el primer uso) y se destruirá después de la finalización del programa.

Al usar new para crear un objeto, crea un objeto con duración de almacenamiento dinámico. Este objeto se inicializará cuando llame al new, y solo se destruirá si llama al delete. Para llamar a eliminar, debe mantener una referencia al mismo y llamar a eliminar cuando haya terminado de usar el objeto. El código C++ bien escrito generalmente no usa mucho este tipo de duración de almacenamiento, en su lugar colocará objetos de valor en contenedores (por ejemplo, std::vector), que administran el tiempo de vida de los valores contenidos. La variable del contenedor en sí puede ir en almacenamiento estático o almacenamiento automático.

Espero que esto ayude a eliminar las ambigüedades un poco, sin acumular demasiados términos nuevos para confundirte.

+2

Eso está mal. std :: vector no administra la vida de los objetos. Sí, si insertas los miembros asignados a la pila, están vinculados al alcance del vector (porque los está copiando), pero presionar los miembros asignados al montón en un estándar :: y esperar que el vector administre su ciclo de vida es simplemente incorrecto y consejos peligrosos para darle a alguien nuevo en C++ – Falmarri

+0

@Falmarri. No entiende el punto, pero traté de aclarar que administra los tipos de valores, no los tipos de referencia. –

Cuestiones relacionadas