¿Por qué debería usar un puntero en lugar del objeto mismo?

1602

Vengo de un fondo Java y he comenzado a trabajar con objetos en C ++. Pero una cosa que se me ocurrió es que las personas a menudo usan punteros a los objetos en lugar de los objetos en sí, por ejemplo, esta declaración:

Object *myObject = new Object;

más bien que:

Object myObject;

O en lugar de usar una función, digamos testFunc(), así:

myObject.testFunc();

tenemos que escribir:

myObject->testFunc();

Pero no puedo entender por qué deberíamos hacerlo de esta manera. Supongo que tiene que ver con la eficiencia y la velocidad, ya que tenemos acceso directo a la dirección de memoria. Estoy en lo cierto?

gEdringer
fuente
405
Felicitaciones por cuestionar esta práctica en lugar de simplemente seguirla. La mayoría de las veces, los punteros se usan en exceso.
Luchian Grigore
120
Si no ve una razón para usar punteros, no lo haga. Prefiero objetos. Prefiere objetos antes de unique_ptr antes de shared_ptr antes de punteros sin formato.
stefan
113
nota: en java, todo (excepto los tipos básicos) es un puntero. así que deberías preguntar lo contrario: ¿por qué necesito objetos simples?
Karoly Horvath
119
Tenga en cuenta que, en Java, los punteros están ocultos por la sintaxis. En C ++, la diferencia entre un puntero y un no puntero se hace explícita en el código. Java usa punteros en todas partes.
Daniel Martín
216
¿Cercano como demasiado ancho ? ¿Seriamente? Por favor, tengan en cuenta que esta forma de programación Java ++ es muy común y uno de los problemas más importantes en la comunidad C ++ . Debe ser tratado en serio.
Manu343726

Respuestas:

1575

Es muy lamentable que vea la asignación dinámica tan a menudo. Eso solo muestra cuántos malos programadores de C ++ hay.

En cierto sentido, tiene dos preguntas agrupadas en una. La primera es ¿cuándo debemos usar la asignación dinámica (usando new)? La segunda es ¿cuándo debemos usar punteros?

El mensaje importante para llevar a casa es que siempre debe usar la herramienta adecuada para el trabajo . En casi todas las situaciones, hay algo más apropiado y más seguro que realizar una asignación dinámica manual y / o usar punteros sin procesar.

Asignación dinámica

En su pregunta, ha demostrado dos formas de crear un objeto. La principal diferencia es la duración del almacenamiento del objeto. Al hacerlo Object myObject;dentro de un bloque, el objeto se crea con una duración de almacenamiento automática, lo que significa que se destruirá automáticamente cuando salga del alcance. Cuando lo hace new Object(), el objeto tiene una duración de almacenamiento dinámico, lo que significa que permanece vivo hasta que lo explicite delete. Solo debe usar la duración del almacenamiento dinámico cuando lo necesite. Es decir, siempre debe preferir crear objetos con una duración de almacenamiento automática cuando pueda .

Las dos situaciones principales en las que puede requerir una asignación dinámica:

  1. Necesita el objeto para sobrevivir al alcance actual : ese objeto específico en esa ubicación de memoria específica, no una copia del mismo. Si está de acuerdo con copiar / mover el objeto (la mayoría de las veces debería hacerlo), debería preferir un objeto automático.
  2. Necesita asignar mucha memoria , que puede llenar fácilmente la pila. Sería bueno si no tuviéramos que preocuparnos por esto (la mayoría de las veces no debería tener que hacerlo), ya que está realmente fuera del alcance de C ++, pero desafortunadamente, tenemos que lidiar con la realidad de los sistemas estamos desarrollando para

Cuando realmente requiera una asignación dinámica, debe encapsularla en un puntero inteligente o en algún otro tipo que realice RAII (como los contenedores estándar). Los punteros inteligentes proporcionan semántica de propiedad de objetos asignados dinámicamente. Echa un vistazo a std::unique_ptry std::shared_ptr, por ejemplo. Si los usa de manera adecuada, puede evitar casi por completo realizar su propia administración de memoria (consulte la Regla de cero ).

Punteros

Sin embargo, existen otros usos más generales para los punteros sin procesar más allá de la asignación dinámica, pero la mayoría tiene alternativas que debería preferir. Como antes, siempre prefiera las alternativas a menos que realmente necesite punteros .

  1. Necesita semántica de referencia . A veces, desea pasar un objeto utilizando un puntero (independientemente de cómo se asignó) porque desea que la función a la que lo pasa tenga acceso a ese objeto específico (no una copia del mismo). Sin embargo, en la mayoría de las situaciones, debe preferir los tipos de referencia a los punteros, porque esto es específicamente para lo que están diseñados. Tenga en cuenta que esto no se trata necesariamente de extender la vida útil del objeto más allá del alcance actual, como en la situación 1 anterior. Como antes, si está de acuerdo con pasar una copia del objeto, no necesita semántica de referencia.

  2. Necesitas polimorfismo . Solo puede llamar a las funciones polimórficamente (es decir, según el tipo dinámico de un objeto) a través de un puntero o referencia al objeto. Si ese es el comportamiento que necesita, entonces necesita usar punteros o referencias. Nuevamente, se deben preferir las referencias.

  3. Desea representar que un objeto es opcional al permitir nullptrque se pase uno cuando se omite el objeto. Si se trata de un argumento, debería preferir utilizar argumentos predeterminados o sobrecargas de funciones. De lo contrario, debe usar preferiblemente un tipo que encapsule este comportamiento, como std::optional(introducido en C ++ 17 - con estándares anteriores de C ++, use boost::optional).

  4. Desea desacoplar unidades de compilación para mejorar el tiempo de compilación . La propiedad útil de un puntero es que solo necesita una declaración directa del tipo señalado (para usar realmente el objeto, necesitará una definición). Esto le permite desacoplar partes de su proceso de compilación, lo que puede mejorar significativamente el tiempo de compilación. Ver el idioma Pimpl .

  5. Necesita interactuar con una biblioteca C o una biblioteca de estilo C. En este punto, te ves obligado a usar punteros en bruto. Lo mejor que puede hacer es asegurarse de que solo suelte sus punteros crudos en el último momento posible. Puede obtener un puntero sin formato a partir de un puntero inteligente, por ejemplo, utilizando su getfunción miembro. Si una biblioteca realiza una asignación para usted que espera que desasigne a través de un identificador, a menudo puede envolver el identificador en un puntero inteligente con un eliminador personalizado que desasignará el objeto adecuadamente.

Joseph Mansfield
fuente
83
"Necesita el objeto para sobrevivir al alcance actual". - Una nota adicional sobre esto: hay casos en los que parece que necesita el objeto para sobrevivir al alcance actual, pero realmente no. Si coloca su objeto dentro de un vector, por ejemplo, el objeto se copiará (o moverá) en el vector, y el objeto original es seguro de destruir cuando termine su alcance.
25
Recuerde que s / copy / move / en muchos lugares ahora. Devolver un objeto definitivamente no implica un movimiento. También debe tener en cuenta que acceder a un objeto a través de un puntero es ortogonal a cómo se creó.
Puppy
15
Echo de menos una referencia explícita a RAII en esta respuesta. C ++ es todo (casi todo) sobre la gestión de recursos, y RAII es la forma de hacerlo en C ++ (y el principal problema que generan los punteros sin procesar: Breaking RAII)
Manu343726
11
Los punteros inteligentes existían antes de C ++ 11, por ejemplo, boost :: shared_ptr y boost :: scoped_ptr. Otros proyectos tienen su propio equivalente. No puede obtener la semántica de movimiento, y la asignación de std :: auto_ptr es defectuosa, por lo que C ++ 11 mejora las cosas, pero el consejo sigue siendo bueno. (Y es un truco, no es suficiente tener acceso a un compilador de C ++ 11, es necesario que todos los compiladores que posiblemente desee que su código funcionen sean compatibles con C ++ 11. Sí, Oracle Solaris Studio, estoy mirándote.)
brazalete
77
@ MDMoore313 Puede escribirObject myObject(param1, etc...)
user000001
173

Hay muchos casos de uso para punteros.

Comportamiento polimórfico . Para los tipos polimórficos, se utilizan punteros (o referencias) para evitar el corte:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Semántica de referencia y evitando copiar . Para los tipos no polimórficos, un puntero (o una referencia) evitará copiar un objeto potencialmente costoso

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

Tenga en cuenta que C ++ 11 tiene una semántica de movimiento que puede evitar muchas copias de objetos caros en argumentos de función y como valores de retorno. Pero usar un puntero definitivamente los evitará y permitirá múltiples punteros en el mismo objeto (mientras que un objeto solo se puede mover de una vez).

Adquisición de recursos . Crear un puntero a un recurso utilizando el newoperador es un antipatrón en C ++ moderno. Utilice una clase de recurso especial (uno de los contenedores estándar) o un puntero inteligente ( std::unique_ptr<>o std::shared_ptr<>). Considerar:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

Un puntero en bruto solo debe usarse como una "vista" y no de ninguna manera involucrado en la propiedad, ya sea a través de la creación directa o implícitamente a través de valores de retorno. Consulte también estas preguntas y respuestas de las preguntas frecuentes de C ++ .

Control de vida útil más detallado Cada vez que se copia un puntero compartido (por ejemplo, como argumento de función), el recurso al que apunta se mantiene vivo. Los objetos normales (no creados por newusted, ya sea directamente por usted o dentro de una clase de recurso) se destruyen cuando salen del alcance.

TemplateRex
fuente
17
"Crear un puntero a un recurso usando el nuevo operador es un antipatrón" Creo que incluso podría mejorar eso al tener un puntero sin procesar propio es un antipatrón . No sólo la creación, sino pasar punteros primas como argumentos o valores devueltos en mi humilde opinión que impliquen la transferencia de la propiedad está en desuso desde unique_ptr/ mover la semántica
dyp
1
@dyp tnx, actualizado y referencia a las preguntas y respuestas frecuentes de C ++ sobre este tema.
TemplateRex
44
Usar punteros inteligentes en todas partes es un antipatrón. Hay algunos casos especiales en los que es aplicable, pero la mayoría de las veces, la misma razón que defiende la asignación dinámica (vida arbitraria) también se opone a cualquiera de los punteros inteligentes habituales.
James Kanze
2
@JamesKanze No quise decir que los punteros inteligentes deberían usarse en todas partes, solo para la propiedad, y también que los punteros en bruto no deberían usarse para la propiedad, sino solo para las vistas.
TemplateRex
2
@TemplateRex Eso parece un poco tonto dado que hun(b)también requiere el conocimiento de la firma a menos que esté bien con no saber que proporcionó el tipo incorrecto hasta la compilación. Si bien el problema de referencia generalmente no se detectará en el momento de la compilación y tomaría más esfuerzo depurarlo, si está verificando la firma para asegurarse de que los argumentos sean correctos, también podrá ver si alguno de los argumentos son referencias entonces el bit de referencia se convierte en algo no problemático (especialmente cuando se usan IDEs o editores de texto que muestran la firma de las funciones seleccionadas). Además, const&.
JAB
130

Hay muchas respuestas excelentes a esta pregunta, incluidos los casos de uso importantes de declaraciones directas, polimorfismo, etc.

Examinemos la situación comparando los dos idiomas:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

El equivalente más cercano a esto es:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Veamos la forma alternativa de C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

La mejor manera de pensarlo es que, más o menos, Java maneja (implícitamente) punteros a objetos, mientras que C ++ puede manejar punteros a objetos o los propios objetos. Hay excepciones a esto: por ejemplo, si declara tipos "primitivos" de Java, son valores reales que se copian y no punteros. Entonces,

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

Dicho esto, el uso de punteros NO es necesariamente la forma correcta o incorrecta de manejar las cosas; sin embargo, otras respuestas lo han cubierto satisfactoriamente. Sin embargo, la idea general es que en C ++ tienes mucho más control sobre la vida útil de los objetos y sobre dónde vivirán.

Punto de referencia: la Object * object = new Object()construcción es en realidad lo más parecido a la semántica típica de Java (o C # para el caso).

Gerasimos R
fuente
77
Object2 is now "dead": Creo que te refieres myObject1o más precisamente the object pointed to by myObject1.
Clément
2
¡En efecto! Reformulado un poco.
Gerasimos R
2
Object object1 = new Object(); Object object2 = new Object();Es muy mal código. El segundo nuevo o el segundo constructor de objetos puede arrojar, y ahora se filtra el objeto1. Si está utilizando news sin formato, debe envolver los newobjetos ed en contenedores RAII lo antes posible.
PSkocik
8
De hecho, lo sería si se tratara de un programa, y ​​no pasara nada más a su alrededor. Afortunadamente, esto es solo un fragmento de explicación que muestra cómo se comporta un puntero en C ++, y uno de los pocos lugares donde un objeto RAII no puede ser sustituido por un puntero en bruto, es estudiar y aprender sobre punteros en bruto ...
Gerasimos R
80

Otra buena razón para usar punteros sería para las declaraciones a futuro . En un proyecto lo suficientemente grande, realmente pueden acelerar el tiempo de compilación.

Tostada quemada
fuente
77
Esto realmente se suma a la combinación de información útil, ¡así que me alegra que haya sido una respuesta!
TemplateRex
3
std :: shared_ptr <T> también funciona con declaraciones directas de T. (std :: unique_ptr <T> no )
berkus
13
@berkus: std::unique_ptr<T>funciona con declaraciones a futuro de T. Solo necesita asegurarse de que cuando std::unique_ptr<T>se llama al destructor del , Tsea ​​un tipo completo. Esto generalmente significa que su clase que contiene std::unique_ptr<T>declara su destructor en el archivo de encabezado y lo implementa en el archivo cpp (incluso si la implementación está vacía).
David Stone
¿Los módulos solucionarán esto?
Trevor Hickey
@TrevorHickey Viejo comentario, lo sé, pero para responder de todos modos. Los módulos no eliminarán la dependencia, pero deberían hacer que incluir la dependencia sea muy barato, casi gratis en términos de costo de rendimiento. Además, si la aceleración general de los módulos sería suficiente para que sus tiempos de compilación estuvieran en un rango aceptable, ya no es un problema.
Aidiakapi
79

Prefacio

Java no se parece en nada a C ++, a diferencia del bombo publicitario. La máquina exagerada de Java quiere que creas que debido a que Java tiene una sintaxis similar a C ++, los lenguajes son similares. Nada puede estar más lejos de la verdad. Esta información errónea es parte de la razón por la cual los programadores de Java van a C ++ y usan una sintaxis similar a Java sin comprender las implicaciones de su código.

En adelante vamos

Pero no puedo entender por qué deberíamos hacerlo de esta manera. Supongo que tiene que ver con la eficiencia y la velocidad, ya que tenemos acceso directo a la dirección de memoria. Estoy en lo cierto?

Por el contrario, en realidad. El montón es mucho más lento que la pila, porque la pila es muy simple en comparación con el montón. Las variables de almacenamiento automático (también conocidas como variables de pila) tienen sus destructores llamados una vez que salen del alcance. Por ejemplo:

{
    std::string s;
}
// s is destroyed here

Por otro lado, si usa un puntero asignado dinámicamente, su destructor debe llamarse manualmente. deletellama a este destructor por ti.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Esto no tiene nada que ver con la newsintaxis prevalente en C # y Java. Se usan para propósitos completamente diferentes.

Beneficios de la asignación dinámica

1. No tiene que saber el tamaño de la matriz de antemano

Uno de los primeros problemas con los que se encuentran muchos programadores de C ++ es que cuando aceptan entradas arbitrarias de los usuarios, solo se puede asignar un tamaño fijo para una variable de la pila. Tampoco puede cambiar el tamaño de las matrices. Por ejemplo:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Por supuesto, si usó un std::stringen su lugar, std::stringinternamente cambia su tamaño para que no sea un problema. Pero esencialmente la solución a este problema es la asignación dinámica. Puede asignar memoria dinámica en función de la entrada del usuario, por ejemplo:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Nota al margen : un error que cometen muchos principiantes es el uso de matrices de longitud variable. Esta es una extensión de GNU y también una en Clang porque reflejan muchas de las extensiones de GCC. Por lo tanto int arr[n], no se debe confiar en lo siguiente.

Debido a que el montón es mucho más grande que la pila, uno puede asignar / reasignar arbitrariamente tanta memoria como necesite, mientras que la pila tiene una limitación.

2. Las matrices no son punteros

¿Cómo es esto un beneficio que pides? La respuesta quedará clara una vez que comprenda la confusión / mito detrás de las matrices y los punteros. Se supone comúnmente que son iguales, pero no lo son. Este mito proviene del hecho de que los punteros se pueden suscribir al igual que las matrices y debido a que las matrices decaen a punteros en el nivel superior en una declaración de función. Sin embargo, una vez que una matriz se descompone en un puntero, el puntero pierde su sizeofinformación. Por sizeof(pointer)lo tanto , dará el tamaño del puntero en bytes, que generalmente es de 8 bytes en un sistema de 64 bits.

No puede asignar a matrices, solo inicializarlas. Por ejemplo:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

Por otro lado, puedes hacer lo que quieras con punteros. Desafortunadamente, debido a que la distinción entre punteros y matrices se agita a mano en Java y C #, los principiantes no entienden la diferencia.

3. Polimorfismo

Java y C # tienen funciones que le permiten tratar los objetos como otro, por ejemplo, utilizando la aspalabra clave. Entonces, si alguien quisiera tratar un Entityobjeto como un Playerobjeto, uno podría hacerlo. Player player = Entity as Player;Esto es muy útil si tiene la intención de llamar a funciones en un contenedor homogéneo que solo debería aplicarse a un tipo específico. La funcionalidad se puede lograr de manera similar a continuación:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Entonces, si solo Triángulos tuviera una función Rotar, sería un error del compilador si intentara llamarlo en todos los objetos de la clase. Usando dynamic_cast, puede simular la aspalabra clave. Para ser claros, si un lanzamiento falla, devuelve un puntero no válido. Entonces, !testes esencialmente una abreviatura para verificar si testes NULL o un puntero no válido, lo que significa que el lanzamiento falló.

Beneficios de las variables automáticas.

Después de ver todas las excelentes cosas que puede hacer la asignación dinámica, probablemente se esté preguntando por qué nadie NO usaría la asignación dinámica todo el tiempo. Ya te dije una razón, el montón es lento. Y si no necesita toda esa memoria, no debe abusar de ella. Así que aquí hay algunas desventajas sin ningún orden en particular:

  • Es propenso a errores. La asignación manual de memoria es peligrosa y es propenso a fugas. Si no dominas el uso del depurador o valgrind(una herramienta de pérdida de memoria), puedes sacarte el cabello de la cabeza. Afortunadamente, los modismos RAII y los punteros inteligentes alivian un poco esto, pero debes estar familiarizado con prácticas como La regla de tres y La regla de cinco. Es una gran cantidad de información, y los principiantes que no saben o no les importa caerán en esta trampa.

  • No es necesario. A diferencia de Java y C #, donde es idiomático usar la newpalabra clave en todas partes, en C ++, solo debe usarla si es necesario. La frase común dice, todo parece un clavo si tienes un martillo. Mientras que los principiantes que comienzan con C ++ tienen miedo de los punteros y aprenden a usar variables de pila por hábito, los programadores de Java y C # comienzan usando punteros sin entenderlo. Eso es literalmente pisar el pie equivocado. Debes abandonar todo lo que sabes porque la sintaxis es una cosa, aprender el idioma es otra.

1. (N) RVO - Aka, (Nombre) Optimización del valor de retorno

Una optimización que hacen muchos compiladores son las llamadas elisión y optimización del valor de retorno . Estas cosas pueden obviar copias innecesarias que son útiles para objetos que son muy grandes, como un vector que contiene muchos elementos. Normalmente, la práctica común es usar punteros para transferir la propiedad en lugar de copiar los objetos grandes para moverlos . Esto ha llevado al inicio de la semántica del movimiento y los punteros inteligentes .

Si está utilizando punteros, (N) RVO NO se produce. Es más beneficioso y menos propenso a errores aprovechar (N) RVO en lugar de devolver o pasar punteros si le preocupa la optimización. Pueden producirse fugas de error si el llamador de una función es responsable de deletecrear un objeto asignado dinámicamente y tal. Puede ser difícil rastrear la propiedad de un objeto si los punteros se pasan como una papa caliente. Simplemente use variables de pila porque es más simple y mejor.

usuario3391320
fuente
"¡Entonces! Test es esencialmente una forma abreviada de verificar si la prueba es NULL o un puntero no válido, lo que significa que el lanzamiento falló". Creo que esta oración debe reescribirse para mayor claridad.
berkus
44
"La máquina exagerada de Java quisiera que creyeras", tal vez en 1997, pero ahora es anacrónico, ya no hay motivación para comparar Java con C ++ en 2014.
Matt R
15
Antigua pregunta, pero en el segmento de código { std::string* s = new std::string; } delete s; // destructor called... seguramente esto deleteno funcionará porque el compilador ya no sabrá qué ses.
badger5000
2
NO estoy dando -1, pero no estoy de acuerdo con las declaraciones iniciales tal como están escritas. Primero, no estoy de acuerdo con que exista alguna "exageración", podría haber estado alrededor de Y2K, pero ahora ambos idiomas se entienden bien. En segundo lugar, diría que son bastante similares: C ++ es hijo de C casado con Simula, Java agrega Virtual Machine, Garbage Collector y HEAVILY reduce las características, y C # simplifica y reintroduce las características faltantes en Java. Sí, esto hace que los patrones y el uso válido sean MUY diferentes, pero es beneficioso comprender la infraestructura / diseño común para que uno pueda ver las diferencias.
Gerasimos R
1
@ James Matta: Por supuesto, tiene razón en que la memoria es memoria, y ambos están asignados desde la misma memoria física, pero una cosa a considerar es que es muy común obtener mejores características de rendimiento al trabajar con objetos asignados a la pila porque la pila - o al menos sus niveles más altos: tienen una probabilidad muy alta de estar "activos" en la memoria caché a medida que las funciones entran y salen, mientras que el montón no tiene tal beneficio, por lo que si está persiguiendo el puntero en el montón, puede obtener múltiples errores de caché que que probablemente haría no en la pila. Pero toda esta "aleatoriedad" normalmente favorece a la pila.
Gerasimos R
23

C ++ le ofrece tres formas de pasar un objeto: por puntero, por referencia y por valor. Java lo limita con el último (la única excepción son los tipos primitivos como int, boolean, etc.). Si desea utilizar C ++ no solo como un juguete extraño, será mejor que conozca la diferencia entre estas tres formas.

Java pretende que no existe un problema como "¿quién y cuándo debería destruir esto?". La respuesta es: El recolector de basura, genial y horrible. Sin embargo, no puede proporcionar una protección del 100% contra pérdidas de memoria (sí, Java puede perder memoria ). En realidad, GC te da una falsa sensación de seguridad. Cuanto más grande sea tu SUV, más largo será tu camino hacia el evacuador.

C ++ te deja cara a cara con la gestión del ciclo de vida del objeto. Bueno, hay medios para lidiar con eso ( familia de punteros inteligentes , QObject en Qt y así sucesivamente), pero ninguno de ellos se puede usar de manera 'dispara y olvida' como GC: siempre debes tener en cuenta el manejo de la memoria. No solo deberías preocuparte por destruir un objeto, también debes evitar destruir el mismo objeto más de una vez.

¿Todavía no tienes miedo? Ok: referencias cíclicas, trátalos tú mismo, humano. Y recuerda: mata cada objeto con precisión una vez, a los tiempos de ejecución de C ++ no nos gustan los que se meten con los cadáveres, dejamos a los muertos en paz.

Entonces, volviendo a tu pregunta.

Cuando pasa su objeto por valor, no por puntero o por referencia, copia el objeto (todo el objeto, ya sea un par de bytes o un gran volcado de la base de datos; es lo suficientemente inteligente como para evitar este último, no t you?) cada vez que haces '='. Y para acceder a los miembros del objeto, usa '.' (punto).

Cuando pasa su objeto por puntero, copia solo unos pocos bytes (4 en sistemas de 32 bits, 8 en sistemas de 64 bits), a saber, la dirección de este objeto. Y para mostrar esto a todos, utiliza este elegante operador '->' cuando accedes a los miembros. O puede usar la combinación de '*' y '.'.

Cuando usa referencias, obtiene el puntero que pretende ser un valor. Es un puntero, pero accede a los miembros a través de '.'.

Y, para volverte loco una vez más: cuando declaras varias variables separadas por comas, entonces (mira las manos):

  • El tipo se le da a todos
  • El modificador de valor / puntero / referencia es individual

Ejemplo:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one
Kirill Gamazkov
fuente
1
std::auto_ptrestá en desuso, por favor no lo use.
Neil
2
Estoy bastante seguro de que no puede tener una referencia como miembro sin proporcionar también un constructor con una lista de inicialización que incluya la variable de referencia. (Se debe inicializar una referencia de inmediato. Incluso el cuerpo del constructor es demasiado tarde para establecerla, IIRC.)
cHao
20

En C ++, los objetos asignados en la pila (usando la Object object;instrucción dentro de un bloque) solo vivirán dentro del alcance en el que se declaran. Cuando el bloque de código finaliza la ejecución, el objeto declarado se destruye. Mientras que si asigna memoria en el montón, usando Object* obj = new Object(), continúan viviendo en el montón hasta que llame delete obj.

Crearía un objeto en el montón cuando quisiera usar el objeto no solo en el bloque de código que lo declaró / asignó.

Karthik Kalyanasundaram
fuente
66
Object objno siempre está en la pila, por ejemplo, variables globales o miembros.
diez
2
@LightnessRacesinOrbit Solo mencioné los objetos asignados en un bloque, no las variables globales y miembros. La cosa es que no estaba claro, ahora lo corrigió, agregó "dentro de un bloque" en la respuesta. Espero que no sea información falsa ahora :)
Karthik Kalyanasundaram
20

Pero no puedo entender por qué deberíamos usarlo así.

Compararé cómo funciona dentro del cuerpo de la función si usa:

Object myObject;

Dentro de la función, myObjectse destruirá una vez que esta función regrese. Esto es útil si no necesita su objeto fuera de su función. Este objeto se colocará en la pila de subprocesos actual.

Si escribe dentro del cuerpo de la función:

 Object *myObject = new Object;

entonces la instancia de clase de objeto señalada por myObjectno se destruirá una vez que finalice la función, y la asignación esté en el montón.

Ahora, si usted es un programador de Java, el segundo ejemplo está más cerca de cómo funciona la asignación de objetos en Java. Esta línea: Object *myObject = new Object;es equivalente a java: Object myObject = new Object();. La diferencia es que bajo java myObject se recolectará basura, mientras que bajo c ++ no se liberará, debe llamar explícitamente a `delete myObject; ' de lo contrario, introducirá pérdidas de memoria.

Desde c ++ 11 puede usar formas seguras de asignaciones dinámicas: new Objectalmacenando valores en shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Además, los objetos se almacenan muy a menudo en contenedores, como mapas-s o vectores-s, administrarán automáticamente una vida útil de sus objetos.

marcinj
fuente
1
then myObject will not get destroyed once function endsAbsolutamente lo hará.
Carreras de ligereza en órbita
66
En el caso del puntero, myObjectseguirá siendo destruido, al igual que cualquier otra variable local. La diferencia es que su valor es un puntero a un objeto, no el objeto en sí mismo, y la destrucción de un puntero tonto no afecta a su puntero. Entonces el objeto sobrevivirá a dicha destrucción.
cHao
Se corrigió que, por supuesto, las variables locales (que incluyen el puntero) se liberarán, están en la pila.
marcinj
13

Técnicamente es un problema de asignación de memoria, sin embargo, aquí hay dos aspectos más prácticos de esto. Tiene que ver con dos cosas: 1) Alcance, cuando define un objeto sin puntero, ya no podrá acceder a él después del bloque de código en el que está definido, mientras que si define un puntero con "nuevo", entonces puede acceder desde cualquier lugar donde tenga un puntero a esta memoria hasta que llame "eliminar" en el mismo puntero. 2) Si desea pasar argumentos a una función, quiere pasar un puntero o una referencia para ser más eficiente. Cuando pasa un Objeto, el objeto se copia; si se trata de un objeto que utiliza mucha memoria, puede consumir CPU (por ejemplo, copia un vector lleno de datos). Cuando pasa un puntero, todo lo que pasa es un int (dependiendo de la implementación, pero la mayoría de ellos son un int).

Aparte de eso, debe comprender que "nuevo" asigna memoria en el montón que debe liberarse en algún momento. Cuando no tenga que usar "nuevo", le sugiero que use una definición de objeto regular "en la pila".

en necesidad de ayuda
fuente
6

Bueno, la pregunta principal es ¿Por qué debería usar un puntero en lugar del objeto en sí? Y mi respuesta es que (casi) nunca debe usar el puntero en lugar del objeto, porque C ++ tiene referencias , es más seguro que los punteros y garantiza el mismo rendimiento que los punteros.

Otra cosa que mencionaste en tu pregunta:

Object *myObject = new Object;

¿Como funciona? Crea un puntero de Objecttipo, asigna memoria para adaptarse a un objeto y llama al constructor predeterminado, suena bien, ¿verdad? Pero en realidad no es tan bueno, si asignó memoria dinámicamente (palabra clave utilizada new), también debe liberar memoria manualmente, eso significa que en el código debe tener:

delete myObject;

Esto llama al destructor y libera memoria, parece fácil, sin embargo, en grandes proyectos puede ser difícil detectar si un hilo liberó memoria o no, pero para ese propósito puede probar punteros compartidos , estos disminuyen ligeramente el rendimiento, pero es mucho más fácil trabajar con ellos. ellos.


Y ahora se acabó alguna introducción y volvemos a la pregunta.

Puede usar punteros en lugar de objetos para obtener un mejor rendimiento mientras transfiere datos entre funciones.

Eche un vistazo, tiene std::string(también es un objeto) y contiene mucha información, por ejemplo, XML grande, ahora necesita analizarlo, pero para eso tiene una función void foo(...)que puede declararse de diferentes maneras:

  1. void foo(std::string xml); En este caso, copiará todos los datos de su variable a la pila de funciones, lleva un tiempo, por lo que su rendimiento será bajo.
  2. void foo(std::string* xml); En este caso, pasará el puntero al objeto, a la misma velocidad que la size_tvariable de paso , sin embargo, esta declaración es propensa a errores, ya que puede pasar el NULLpuntero o un puntero no válido. Los punteros generalmente se usan Cporque no tiene referencias.
  3. void foo(std::string& xml); Aquí pasa la referencia, básicamente es lo mismo que pasar el puntero, pero el compilador hace algunas cosas y no puede pasar una referencia no válida (en realidad, es posible crear una situación con una referencia no válida, pero está engañando al compilador).
  4. void foo(const std::string* xml); Aquí es lo mismo que el segundo, solo el valor del puntero no se puede cambiar.
  5. void foo(const std::string& xml); Aquí es lo mismo que el tercero, pero el valor del objeto no se puede cambiar.

Lo que más quiero mencionar es que puede usar estas 5 formas de pasar datos sin importar la forma de asignación que haya elegido (con newo regular ).


Otra cosa para mencionar, cuando crea objetos de manera regular , asigna memoria en la pila, pero mientras la crea con el newmontón. Asignar la pila es mucho más rápido, pero es un poco pequeño para matrices de datos realmente grandes, por lo que si necesita un objeto grande, debe usar el montón, porque puede obtener un desbordamiento de la pila, pero generalmente este problema se resuelve usando contenedores STL y recuerde std::stringtambién es contenedor, algunos chicos lo olvidaron :)

ST3
fuente
5

Digamos que tiene class Aese contenido. class BCuando desee llamar a alguna función de class Bafuera class A, simplemente obtendrá un puntero a esta clase y podrá hacer lo que quiera y también cambiará el contexto class Ben suclass A

Pero ten cuidado con el objeto dinámico

Búsqueda
fuente
5

Hay muchos beneficios de usar punteros para objetar:

  1. Eficiencia (como ya señaló). Pasar objetos a funciones significa crear nuevas copias de objetos.
  2. Trabajando con objetos de bibliotecas de terceros. Si su objeto pertenece a un código de un tercero y los autores pretenden el uso de sus objetos solo a través de punteros (sin constructores de copia, etc.), la única forma en que puede pasar este objeto es mediante punteros. Pasar por valor puede causar problemas. (Problemas de copia profunda / copia superficial).
  3. si el objeto posee un recurso y desea que la propiedad no se comparta con otros objetos.
Rohit
fuente
3

Esto se ha discutido extensamente, pero en Java todo es un puntero. No hace distinción entre las asignaciones de pila y montón (todos los objetos se asignan en el montón), por lo que no se da cuenta de que está utilizando punteros. En C ++, puede mezclar los dos, según sus requisitos de memoria. El rendimiento y el uso de memoria es más determinista en C ++ (duh).

cmollis
fuente
3
Object *myObject = new Object;

Hacer esto creará una referencia a un Objeto (en el montón) que debe eliminarse explícitamente para evitar pérdidas de memoria .

Object myObject;

Hacer esto creará un objeto (myObject) del tipo automático (en la pila) que se eliminará automáticamente cuando el objeto (myObject) salga del alcance.

Palak Jain
fuente
1

Un puntero hace referencia directamente a la ubicación de la memoria de un objeto. Java no tiene nada como esto. Java tiene referencias que hacen referencia a la ubicación del objeto a través de tablas hash. No puede hacer nada como la aritmética de puntero en Java con estas referencias.

Para responder a su pregunta, es solo su preferencia. Prefiero usar la sintaxis similar a Java.

RioRicoRick
fuente
Tablas hash? Tal vez en algunas JVM pero no cuentes con eso.
Zan Lynx
¿Qué pasa con la JVM que viene con Java? Por supuesto, puede implementar CUALQUIER COSA que pueda pensar como una JVM que usa punteros directamente o un método que hace cálculos de punteros. Eso es como decir "la gente no muere del resfriado común" y obtener una respuesta "¡Quizás la mayoría de las personas no lo hacen pero no cuenten con eso! Jaja.
RioRicoRick
2
@RioRicoRick HotSpot implementa referencias de Java como punteros nativos, consulte docs.oracle.com/javase/7/docs/technotes/guides/vm/… Hasta donde puedo ver, JRockit hace lo mismo. Ambos admiten la compresión OOP, pero ninguno usa tablas hash. Las consecuencias de rendimiento probablemente serían desastrosas. Además, "es solo su preferencia" parece implicar que las dos son sintaxis simplemente diferentes para un comportamiento equivalente, que por supuesto no lo son.
Max Barraclough
0

Con punteros ,

  • puede hablar directamente a la memoria.

  • puede evitar muchas pérdidas de memoria de un programa manipulando punteros.

lasan
fuente
44
" en C ++, utilizando punteros, puede crear un recolector de basura personalizado para su propio programa " que suena como una idea terrible.
cuant
0

Una razón para usar punteros es interactuar con las funciones de C. Otra razón es para ahorrar memoria; por ejemplo: en lugar de pasar un objeto que contiene muchos datos y tiene un constructor de copia intensivo en procesador a una función, simplemente pase un puntero al objeto, ahorrando memoria y velocidad, especialmente si está en un bucle, sin embargo, un referencia sería mejor en ese caso, a menos que esté utilizando una matriz de estilo C.


fuente
0

En áreas donde la utilización de la memoria es primordial, los punteros son útiles. Por ejemplo, considere un algoritmo minimax, donde se generarán miles de nodos usando la rutina recursiva, y luego úselos para evaluar el siguiente mejor movimiento en el juego, la capacidad de desasignar o restablecer (como en los punteros inteligentes) reduce significativamente el consumo de memoria. Mientras que la variable sin puntero continúa ocupando espacio hasta que su llamada recursiva devuelve un valor.

seccpur
fuente
0

Incluiré un caso de uso importante del puntero. Cuando está almacenando algún objeto en la clase base, pero podría ser polimórfico.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

Entonces, en este caso no puede declarar bObj como un objeto directo, debe tener un puntero.

usuario18853
fuente
-5

"La necesidad es la madre de la invención." La diferencia más importante que me gustaría señalar es el resultado de mi propia experiencia de codificación. A veces necesitas pasar objetos a funciones. En ese caso, si su objeto es de una clase muy grande, pasarlo como un objeto copiará su estado (que puede que no desee ... Y PUEDE SER GRANDE SOBRE LA CARGA), lo que da como resultado una sobrecarga de copia del objeto. Tamaño de 4 bytes (suponiendo 32 bits). Otras razones ya se mencionan anteriormente ...

sandeep bisht
fuente
14
debería preferir pasar por referencia
bolov
2
Yo recomiendo que pasa por la constante referencia como por variables std::string test;que tenemos void func(const std::string &) {}, pero menos que la función tiene que cambiar la entrada, en cuyo caso te recomiendo el uso de punteros (de modo que cualquiera que lea el código hace previo aviso &, y entiende la función, puede cambiar su entrada)
Top- Domina el
-7

Ya hay muchas respuestas excelentes, pero déjame darte un ejemplo:

Tengo una clase de artículo simple:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Hago un vector para contener un montón de ellos.

std::vector<Item> inventory;

Creo un millón de objetos Item, y los empujo hacia el vector. Ordeno el vector por nombre y luego hago una búsqueda binaria iterativa simple para un nombre de elemento en particular. Pruebo el programa y me lleva más de 8 minutos terminar de ejecutarlo. Luego cambio mi vector de inventario así:

std::vector<Item *> inventory;

... y crear mi millón de objetos Item a través de nuevo. Los ÚNICOS cambios que hago en mi código son usar los punteros a los Elementos, excepto un ciclo que agrego para la limpieza de la memoria al final. Ese programa se ejecuta en menos de 40 segundos, o mejor que un aumento de velocidad de 10x. EDITAR: El código está en http://pastebin.com/DK24SPeW. Con las optimizaciones del compilador, muestra solo un aumento de 3.4x en la máquina en la que lo probé, lo que aún es considerable.

Darren
fuente
2
Bueno, ¿estás comparando los punteros entonces o sigues comparando los objetos reales? Dudo mucho que otro nivel de indirección pueda mejorar el rendimiento. Por favor proporcione el código! ¿Limpiaste adecuadamente después?
stefan
1
@stefan Comparo los datos (específicamente, el campo de nombre) de los objetos tanto para la clasificación como para la búsqueda. Lo limpio correctamente, como ya mencioné en la publicación. la aceleración probablemente se deba a dos factores: 1) std :: vector push_back () copia los objetos, por lo que la versión de puntero solo necesita copiar un puntero por objeto. Esto tiene múltiples impactos en el rendimiento, ya que no solo se copian menos datos, sino que el asignador de memoria de la clase vectorial se agota menos.
Darren
2
Aquí está el código que no muestra prácticamente ninguna diferencia para su ejemplo: ordenar. El código de puntero es un 6% más rápido que el código sin puntero solo para la clasificación, pero en general es un 10% más lento que el código sin puntero. ideone.com/G0c7zw
stefan
3
Palabra clave: push_back. Por supuesto esto copia. Debería haber estado emplaceen el lugar al crear sus objetos (a menos que necesite que se almacenen en caché en otro lugar).
underscore_d
1
Los vectores de punteros casi siempre están equivocados. No los recomiende sin explicar, en detalle, las advertencias y las ventajas y desventajas. Parece que ha encontrado un profesional, que es solo una consecuencia de un contraejemplo mal codificado, y lo ha tergiversado
carreras ligeras en órbita el