ejemplo - Miembros de la clase que son objetos-¿Punteros o no? C++
que es un objeto en c++ (11)
Si creo una clase MyClass y tiene algún miembro privado que diga MyOtherClass, ¿es mejor hacer de MyOtherClass un puntero o no? ¿Qué significa también tenerlo como un puntero en términos de dónde está almacenado en la memoria? ¿Se creará el objeto cuando se crea la clase?
Noté que los ejemplos en QT generalmente declaran a los miembros de la clase como punteros cuando son clases.
Saludos
marca
Si creo una clase MyClass y tiene algún miembro privado que diga MyOtherClass, ¿es mejor hacer de MyOtherClass un puntero o no?
en general, debe declararlo como un valor en su clase. será local, habrá menos posibilidades de errores, menos asignaciones, en última instancia, menos cosas que podrían salir mal, y el compilador siempre podrá saber que está allí en un desplazamiento especificado, por lo que ... ayuda a la optimización y reducción binaria a algunos niveles habrá algunos casos en los que sabe que tendrá que lidiar con el puntero (es decir, polimórficos, compartidos, requiere reasignación), generalmente es mejor utilizar un puntero solo cuando sea necesario, especialmente cuando es privado / encapsulado.
¿Qué significa también tenerlo como un puntero en términos de dónde está almacenado en la memoria?
su dirección será cercana a (o igual a) this
- gcc (por ejemplo) tiene algunas opciones avanzadas para volcar datos de clase (tamaños, tablas, compensaciones)
¿Se creará el objeto cuando se crea la clase?
sí, el tamaño de MyClass crecerá en sizeof (MyOtherClass), o más si el compilador lo realinea (por ejemplo, su alineación natural)
¿Dónde está tu miembro almacenado en la memoria?
Echale un vistazo a éste ejemplo:
struct Foo { int m; };
struct A {
Foo foo;
};
struct B {
Foo *foo;
B() : foo(new Foo()) { } // ctor: allocate Foo on heap
~B() { delete foo; } // dtor: Don''t forget this!
};
void bar() {
A a_stack; // a_stack is on stack
// a_stack.foo is on stack too
A* a_heap = new A(); // a_heap is on stack (it''s a pointer)
// *a_heap (the pointee) is on heap
// a_heap->foo is on heap
B b_stack; // b_stack is on stack
// b_stack.foo is on stack
// *b_stack.foo is on heap
B* b_heap = new B(); // b_heap is on stack
// *b_heap is on heap
// b_heap->foo is on heap
// *(b_heap->foo is on heap
delete a_heap;
delete b_heap;
// B::~B() will delete b_heap->foo!
}
Definimos dos clases A
y B
A
almacena un miembro público foo
de tipo Foo
. B
tiene un miembro foo
de tipo pointer to Foo
.
¿Cuál es la situación de A
:
- Si crea una variable
a_stack
de tipoA
en la pila , entonces el objeto (obviamente) y sus miembros también están en la pila . - Si crea un puntero a
A
likea_heap
en el ejemplo anterior, solo la variable del puntero estará en la pila ; todo lo demás (el objeto y sus miembros) están en el montón .
¿Cómo se ve la situación en el caso de B
?
- creas
B
en la pila : entonces tanto el objeto como su miembrofoo
están en la pila , pero el objeto al que apuntafoo
(el punto) está en el montón . En resumen:b_stack.foo
(el puntero) está en la pila, pero*b_stack.foo
el (puntaje) está en el montón. - creas un puntero a
B
llamadob_heap
:b_heap
(el puntero) está en la pila,*b_heap
(el pointee) está en el montón , así como el miembrob_heap->foo
y*b_heap->foo
.
¿El objeto se creará automágicamente?
- En el caso de A: Sí,
foo
creará automáticamente llamando al constructor implícito predeterminado deFoo
. Esto creará uninteger
pero no lo inicializará (¡tendrá un número aleatorio)! - En el caso de B: si omite nuestro ctor y dtor,
foo
(el puntero) también se creará e inicializará con un número aleatorio, lo que significa que apuntará a una ubicación aleatoria en el montón. ¡Pero tenga en cuenta que el puntero existe! Tenga en cuenta también que el constructor predeterminado implícito no asignará algo parafoo
para usted, tiene que hacer esto explícitamente . Es por eso que normalmente necesita un constructor explícito y un destructor acompañante para asignar y eliminar el punto del puntero de miembro. No se olvide de la semántica de copia : ¿qué le sucede al punto si copia el objeto (mediante la construcción o asignación de copias)?
¿Cuál es el objetivo de todo esto?
Hay varios casos de uso de usar un puntero a un miembro:
- Para señalar un objeto que no es de su propiedad. Digamos que su clase necesita acceso a una gran estructura de datos que es muy costosa de copiar. Entonces podría simplemente guardar un puntero a esta estructura de datos. Tenga en cuenta que en este caso la creación y eliminación de la estructura de datos está fuera del alcance de su clase. Alguien más tiene que cuidarse.
- Aumenta el tiempo de compilación, ya que en el archivo de encabezado no es necesario definir el punto.
- Un poco más avanzado; Cuando su clase tiene un puntero a otra clase que almacena todos los miembros privados, la "idiomática Pimpl": http://c2.com/cgi/wiki?PimplIdiom , eche un vistazo a Sutter, H. (2000): Excepcional C ++ , pag. 99--119
- Y algunos otros, mira las otras respuestas
Consejo
Tenga especial cuidado si sus miembros son indicadores y usted los posee. Tienes que escribir constructores, destructores adecuados y pensar en los constructores de copias y los operadores de asignación. ¿Qué le sucede al punto si copia el objeto? Por lo general, también tendrás que copiar la construcción de la punta.
Algunas ventajas del miembro del puntero:
- El objeto secundario (MyOtherClass) puede tener un tiempo de vida diferente al original (MyClass).
- El objeto puede posiblemente compartirse entre varios objetos MyClass (u otros).
- Al compilar el archivo de encabezado para MyClass, el compilador no necesariamente tiene que conocer la definición de MyOtherClass. No tiene que incluir su encabezado, lo que disminuye los tiempos de compilación.
- Hace que el tamaño de MyClass sea más pequeño. Esto puede ser importante para el rendimiento si su código realiza una gran cantidad de copia de objetos MyClass. Puede simplemente copiar el puntero de MyOtherClass e implementar algún tipo de sistema de conteo de referencias.
Ventajas de tener el miembro como un objeto:
- No tiene que escribir explícitamente código para crear y destruir el objeto. Es más fácil y menos propenso a errores.
- Hace que la administración de memoria sea más eficiente porque solo se debe asignar un bloque de memoria en lugar de dos.
- Implementar operadores de asignación, copiar / mover constructores, etc. es mucho más simple.
- Más intuitivo
Depende... :-)
Si utiliza punteros para decir una class A
, debe crear el objeto de tipo A, por ejemplo, en el constructor de su clase
m_pA = new A();
Además, no te olvides de destruir el objeto en el destructor o tienes una pérdida de memoria:
delete m_pA;
m_pA = NULL;
En cambio, tener un objeto de tipo A agregado en su clase es más fácil, no puede olvidarse de destruirlo, porque esto se hace automáticamente al final de la vida útil de su objeto.
Por otro lado, tener un puntero tiene las siguientes ventajas:
Si su objeto está asignado en la pila y el tipo A usa mucha memoria, esto no se asignará desde la pila, sino desde el montón.
Puede construir su objeto A más tarde (por ejemplo, en un método
Create
) o destruirlo anteriormente (en el métodoClose
)
En C ++, los punteros son objetos por derecho propio. No están realmente vinculados a lo que señalan, y no hay interacción especial entre un puntero y su punta (¿es una palabra?)
Si crea un puntero, crea un puntero y nada más . No crea el objeto que podría o no señalar. Y cuando un puntero sale del alcance, el objeto apuntado no se ve afectado. Un puntero no afecta de ninguna manera la vida de lo que sea que señale.
Entonces, en general, no deberías usar punteros por defecto. Si su clase contiene otro objeto, ese otro objeto no debería ser un puntero.
Sin embargo, si su clase conoce acerca de otro objeto, entonces un puntero podría ser una buena forma de representarlo (ya que múltiples instancias de su clase pueden apuntar a la misma instancia, sin tomar posesión de ella, y sin controlar su duración)
Esta pregunta podría ser deliberada sin fin, pero los conceptos básicos son:
Si MyOtherClass no es un puntero:
- La creación y destrucción de MyOtherClass es automática, lo que puede reducir errores.
- La memoria utilizada por MyOtherClass es local para MyClassInstance, lo que podría mejorar el rendimiento.
Si MyOtherClass es un puntero:
- La creación y destrucción de MyOtherClass es su responsabilidad
- MyOtherClass puede ser
NULL
, lo que podría tener un significado en su contexto y podría ahorrar memoria - Dos instancias de MyClass podrían compartir el mismo MyOtherClass
La sabiduría común en C ++ es evitar el uso de punteros (desnudos) tanto como sea posible. Punteros especialmente simples que apuntan a la memoria asignada dinámicamente.
La razón es porque los punteros hacen que sea más difícil escribir clases sólidas, especialmente cuando también debe considerar la posibilidad de que se emitan excepciones.
Lo simple es declarar a sus miembros como objetos. De esta forma, no tiene que preocuparse por la construcción, destrucción y asignación de copias. Todo esto se soluciona automáticamente.
Sin embargo, todavía hay algunos casos en los que desea punteros. Después de todo, los lenguajes administrados (como C # o Java) realmente contienen objetos miembros mediante punteros.
El caso más obvio es cuando el objeto que se guarda es polimórfico. En Qt, como señaló, la mayoría de los objetos pertenecen a una gran jerarquía de clases polimórficas, y mantenerlos con punteros es obligatorio ya que no sabe con anticipación qué tamaño tendrá el objeto miembro.
Tenga cuidado con algunos errores comunes en este caso, especialmente cuando maneja clases genéricas. La seguridad de las excepciones es una gran preocupación:
struct Foo
{
Foo()
{
bar_ = new Bar();
baz_ = new Baz(); // If this line throw, bar_ is never reclaimed
// See copy constructor for a workaround
}
Foo(Foo const& x)
{
bar_ = x.bar_.clone();
try { baz_ = x.baz_.clone(); }
catch (...) { delete bar_; throw; }
}
// Copy and swap idiom is perfect for this.
// It yields exception safe operator= if the copy constructor
// is exception safe.
void swap(Foo& x) throw()
{ std::swap(bar_, x.bar_); std::swap(baz_, x.baz_); }
Foo& operator=(Foo x) { x.swap(*this); return *this; }
private:
Bar* bar_;
Baz* baz_;
};
Como puede ver, es bastante engorroso tener constructores seguros de excepciones en presencia de punteros. Debería ver RAII y punteros inteligentes (hay muchos recursos aquí y en otro lugar en la web).
Si convierte el objeto MyOtherClass en miembro de su MyClass:
size of MyClass = size of MyClass + size of MyOtherClass
Si convierte el objeto MyOtherClass en miembro puntero de su MyClass:
size of MyClass = size of MyClass + size of any pointer on your system
Es posible que desee mantener MyOtherClass como miembro de puntero porque le brinda la flexibilidad para dirigirlo a cualquier otra clase que se derive de él. Básicamente lo ayuda a implementar el polimorfismo de la dinamicidad.
Sigo la siguiente regla: si el objeto miembro vive y muere con el objeto de encapsulado, no use punteros. Necesitará un puntero si el objeto miembro tiene que sobrevivir al objeto encapsulado por algún motivo. Depende de la tarea en cuestión.
Por lo general, utiliza un puntero si el objeto miembro se le ha entregado y no lo ha creado usted. Entonces usualmente no tienes que destruirlo tampoco.
Una ventaja de la clase principal que mantiene la relación con un objeto miembro como un puntero (std :: auto_ptr) para el objeto miembro es que puede reenviar declarar el objeto en lugar de tener que incluir el archivo de encabezado del objeto.
Esto desacopla las clases durante el tiempo de compilación, lo que permite modificar la clase de encabezado del objeto miembro sin causar que todos los clientes de la clase principal se vuelvan a compilar, aunque probablemente no accedan a las funciones del objeto miembro.
Cuando utilizas un auto_ptr, solo tienes que encargarte de la construcción, lo que normalmente puedes hacer en la lista de inicializadores. La destrucción junto con el objeto principal está garantizada por el auto_ptr.