Punteros en C++: por qué los necesitamos, cuándo los usamos, en qué se diferencian del acceso al propio objeto
Aunque la mayoría de los programadores entienden cuál es la diferencia entre los objetos y los punteros a los mismos, a veces no está del todo claro cuál de las formas de acceder al objeto hay que elegir. Hemos tratado de responder a esta pregunta a continuación.
Pregunta
Vengo de una formación en Java y he empezado a trabajar con objetos en C++. Pero una cosa que se me ocurrió es que la gente a menudo utiliza punteros a objetos en lugar de los propios objetos, por ejemplo esta declaración:
Object *myObject = new Object;
en lugar de:
Object myObject;
O en lugar de utilizar una función, digamos testFunc(), así:
myObject.testFunc();
tenemos que escribir:
myObject->testFunc();
Pero no puedo averiguar por qué debemos 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?
Respuesta
Por cierto, debemos tener en cuenta, que los punteros en Java no se utilizan explícitamente, por ejemplo, un programador no puede acceder a un objeto en código a través de un puntero a él. Sin embargo, en Java todos los tipos, excepto la base, son referenciados: el acceso a ellos pasa por el enlace, aunque no se puede pasar explícitamente el parámetro por enlace. Además de eso, new en C++ y Java o C# son cosas diferentes.
Para dar una ligera idea sobre los punteros en C++ , daremos dos fragmentos de código similares:
Java:
Object object1 = new Object(); //A new object is allocated by JavaObject object2 = new Object(); //Another new object is allocated by Javaobject1 = 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 este, es:
C++:
Object * object1 = new Object(); //A new object is allocated on the heapObject * object2 = new Object(); //Another new object is allocated on the heapdelete 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 STACKObject object2; //Another new object is allocated on the STACKobject1 = 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...
¿Se gana en velocidad, accediendo directamente a la memoria?
En realidad, no. Los punteros se suelen utilizar para el acceso al heap mientras que los objetos se ubican en la pila – es una estructura más sencilla y rápida. Si eres principiante, tenemos para ti un material en el que contamos con detalle qué es una pila y un heap.
En sentido estricto, esta pregunta combina dos cuestiones diferentes. Primero: ¿cuándo utilizamos la asignación de memoria dinámica? Segundo: ¿cuándo es mejor utilizar punteros? Claro, no vamos a prescindir de las palabras comunes que siempre debe elegir la herramienta más adecuada para el trabajo. Casi siempre hay una mejor realización que el uso de la asignación dinámica manual (asignación dinámica) y / o punteros en bruto.
Es muy lamentable que se ve la asignación dinámica tan a menudo. Eso sólo demuestra cuántos malos programadores de C++ hay.
En cierto sentido, tienes 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 es que siempre hay que usar la herramienta apropiada para el trabajo. En casi todas las situaciones, hay algo más apropiado y seguro que realizar una asignación dinámica manual y/o utilizar punteros sin procesar.
Asignación dinámica
En tu pregunta, has demostrado dos formas de crear un objeto. La principal diferencia es la duración del almacenamiento del objeto. Al hacer Objeto miObjeto; 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 ámbito. Cuando haces new Object(), el objeto tiene una duración de almacenamiento dinámica, lo que significa que permanece vivo hasta que lo elimines explícitamente. Sólo debes utilizar la duración de almacenamiento dinámico cuando lo necesites. Es decir, siempre deberías preferir crear objetos con duración de almacenamiento automática cuando puedas.
Las dos principales situaciones en las que podrías requerir la asignación dinámica:
- Necesitas que el objeto sobreviva al ámbito actual – ese objeto específico en esa ubicación de memoria específica, no una copia del mismo. Si usted está bien con la copia / mover el objeto (la mayoría de las veces debe ser), usted debe preferir un objeto automático.
- Usted necesita para asignar una gran cantidad de memoria, que puede fácilmente llenar la pila. Sería agradable si no tuviéramos que preocuparnos por esto (la mayoría de las veces no deberías tener que hacerlo), ya que realmente está fuera del ámbito de C++, pero desafortunadamente tenemos que lidiar con la realidad de los sistemas para los que estamos desarrollando.
- No sabes exactamente el tamaño del array, que tendrás que usar. Como sabes, en C++ el tamaño de los arrays es fijo. Esto puede causar problemas, por ejemplo, durante la lectura de la entrada del usuario. El puntero define sólo esa sección de la memoria, donde el comienzo de una matriz se escribirá, no limitar su tamaño.
Si un uso de la asignación dinámica es necesario, usted debe encapsular utilizando puntero inteligente o de otro tipo que soporta el modismo «Adquisición de recursos es la inicialización» (contenedores estándar apoyan – es un modismo, de acuerdo con el cual el recurso: un bloque de memoria, archivo, conexión de red, etc – se inicializan mientras que conseguir en el constructor, y luego cuidadosamente son destruidos por destructor). Por ejemplo, los punteros inteligentes son std::unique_ptr y std::shared_ptr
Pointers
Sin embargo, hay otros usos más generales para los punteros crudos más allá de la asignación dinámica, pero la mayoría tienen alternativas que usted debe preferir. Como antes, prefiera siempre las alternativas a menos que realmente necesite punteros.
- Necesita semántica de referencia. A veces quieres pasar un objeto usando un puntero (independientemente de cómo fue asignado) porque quieres que la función a la que se lo pasas tenga acceso a ese objeto específico (no a una copia del mismo). Sin embargo, en la mayoría de las situaciones, deberías preferir los tipos de referencia a los punteros, porque esto es específicamente para lo que están diseñados. Tenga en cuenta que no se trata necesariamente de extender el tiempo de vida del objeto más allá del ámbito actual, como en la situación 1 anterior. Como antes, si usted está bien con pasar una copia del objeto, usted no necesita la semántica de referencia.
- Usted necesita polimorfismo. Sólo puedes llamar a 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 necesitas, entonces necesitas usar punteros o referencias. Una vez más, las referencias deben ser preferidas.
- Quieres representar que un objeto es opcional permitiendo pasar un nullptr cuando el objeto está siendo omitido. Si es un argumento, deberías preferir usar argumentos por defecto o sobrecargas de funciones. De lo contrario, debería preferir utilizar un tipo que encapsule este comportamiento, como std::optional (introducido en C++17 – con los estándares anteriores de C++, utilice boost::optional).
- Quiere desacoplar las unidades de compilación para mejorar el tiempo de compilación. La propiedad útil de un puntero es que sólo se requiere una declaración hacia adelante del tipo apuntado (para utilizar 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. Vea el lenguaje Pimpl.
- Necesita interconectarse con una biblioteca C o una biblioteca de estilo C. En este punto, estás obligado a utilizar punteros en bruto. Lo mejor que puedes hacer es asegurarte de que sólo sueltas los punteros crudos en el último momento posible. Puedes obtener un puntero crudo de un puntero inteligente, por ejemplo, utilizando su función miembro get. Si una biblioteca realiza alguna asignación por ti que espera que desocupes a través de un manejador, a menudo puedes envolver el manejador en un puntero inteligente con un eliminador personalizado que desocupará el objeto apropiadamente.