03. C++: creación de objetos

Publicado por
En la nota anterior aprendimos a crear una clase, ahora probaremos dicha clase creando objetos de ella y usándolos. En esta ocasión aplicaremos algunos conceptos de la teoría contenidos en las notas 01. POO: pensar en objetos y 03.POO: comunicación y organización de objetos de la primera temporada de la serie.

Creando una persona

Con nuestra clase Persona ya escrita podemos crear cuantas personas queramos simplemente instanciando objetos de esta clase. Recordemos que los objetos se crean con ayuda de un constructor, el cual es un método especial que reserva la memoria para todos los atributos del objeto además de darnos la opción de inicializarlos. Un constructor tiene las siguientes características:
  • Debe tener como identificador el mismo de la clase
  • No puede retornar datos, por lo que no debe llevar un tipo de retorno en su declaración
  • No puede ser heredado
  • Debe tener visibilidad pública, ya que siempre se usa fuera de la clases.
En el código de la clase que escribimos tenemos el siguiente constructor que pide algunos valores para inicializar el objeto:
// Constructor declarado en la definición de la clase como función prototipo
Persona(const std::string& nombre,int edad, float peso, float estatura);

// Implementación fuera de la definición de la clase
Persona::Persona(const string& nombre,int edad, float peso, float estatura){
  this -> nombre = nombre; 
  this -> edad = edad;
  this -> peso = peso;
  this -> estatura = estatura;
}
La sintaxis para construir un objeto es la siguiente:
// Para un manejo automático de la memoria
NombreClase nombre_objeto;  // constructor por defecto, la llamada va sin paréntesis
NombreClase nombre_objeto(arg1, arg2,..., argn); // constructor con parámetros

// Usando un apuntador y reservando memoria explícitamente
NombreClase* nombre_objeto = new NombreClase(); 
NombreClase* nombre_objeto = new NombreClase(arg1, arg2,..., argn);

Error común: para usar el constructor por defecto con manejo automático de la memoria hay que evitar poner los paréntesis ya que de no ser así el compilador creerá que intentamos declarar el prototipo de una función.

// Error común, aquí el compilador cree que declaramos una función nueva llamada nombre_objeto, sin parámetros y que devuelve un objeto de NombreClase.
NombreClase nombre_objeto();
Hay dos maneras de usar el constructor: una que permite crear  un objeto con manejo automático de la memoria que ocupa, y otra que nos permite utilizar un apuntador para manejar la memoria manualmente:
  • Objetos temporales: la primera manera nos otorga un objeto que podremos usar de manera muy similar a cualquier tipo de dato primitivo del lenguaje, por ejemplo un entero int. El objeto creado de esta manera permanecerá en memoria según el ámbito en que haya sido declarado, es decir, que una vez que el programa  deje su ámbito, automáticamente se destruye el objeto y se libera la memoria que ocupaba.
    • Ventaja: nos podemos despreocupar por el manejo de la memoria.
    • Desventaja: hay que olvidarse de poder hacer polimorfismo con herencia (en notas posteriores discutiremos a fondo este punto).
  • Apuntadores a objeto: la segunda manera proporciona la memoria necesaria para el objeto y nos otorga la dirección en un apuntador. Con esta alternativa debemos pedir la memoria explícitamente con la palabra reservada new y seremos responsables de liberarla cuando sea necesario, en otras palabras, nos toca destruir el objeto manualmente.
    • Ventaja: tenemos la posibilidad de usar técnicas avanzadas de programación (por ejemplo, polimorfismo).
    • Desventaja: debemos lidiar con la complejidad que implica el uso de apuntadores.
Ahora veamos cómo instanciar una persona:
#include "Persona.h" //debemos incluir el archivo donde hicimos la declaración de la clase
int main(){
  Persona persona("Verónica", 22, 60, 1.65); //objeto temporal
  Persona* persona2; //apuntador a Persona
  persona2 = new Persona("Verónica",22,60,1.65);//asignación de la memoria para el objeto
  delete persona2; //liberación de la memoria apuntada por persona2
  return 0;
}
Como podemos observar se han creado dos objetos diferentes, de las dos manera explicadas anteriormente:
  • persona: es un objeto creado en el ámbito de la función main(), existirá mientras la ejecución del programa no deje el bloque de dicha función; al salir de ella la memoria es liberada automáticamente. Para acceder a los miembros del objeto (métodos y atributos) se utlizará el operador punto .”:
persona.saluda();
  • persona2: es un apuntador a objeto al cual se le ha asignado la dirección de memoria para manipular un objeto de la clase Persona. La liberación de la memoria depende de nosotros; así como la hemos pedido con new, debemos liberarla usando delete, si no lo hacemos es posible que esa memoria quede ocupada sin poder ser reasignada, al menos hasta que el programa deje de ejecutarse. La manera de acceder a los miembros del objeto es mediante el operador flecha ->”:
persona2->saluda();

Algunas cuestiones sobre constructores

En nuestra clase de ejemplo únicamente tenemos un constructor, sin embargo, cabe destacar que podemos escribir más de uno y así tener la posibilidad de construir objetos de distintas maneras, utilizando la sobrecarga de métodos. Las dos posibilidades que hasta ahora conocemos son constructor por defecto y constructor con parámetros, analicemos un poco ambas alternativas:
  • constructor por defecto: no permite datos como argumentos, sin embargo puede inicializar atributos. Normalmente se utiliza para crear objetos de manera rápida y tener los valores por defecto en sus atributos.
  • constructor con parámetros: nos otorga la posibilidad de dar valores para inicializar el objeto al ser creado, los argumentos pueden ser de cualquier tipo de dato. Este tipo de constructor proporciona muchas alternativas de inicialización de objetos, por ejemplo un constructor de copia que recibe como argumento la referencia de otro objeto de la misma clase y copia los atributos al nuevo.
Una cualidad interesante de C++, o mejor dicho, de los compiladores de C++ es la creación automática de funciones miembro en las clases, entre ellas el constructor. Cuando olvidamos escribir un constructor, el compilador nos proporciona uno por defecto, por eso es que una clase sin constructor puede ser compilada sin problemas. Aunque es de gran ayuda esta característica, es mejor escribirlos uno mismo y asegurarse de que los objetos se inicialicen correctamente.

Todo lo que es construido debe ser destruido

Hasta ahora vimos como construir objetos, sin embargo también se destruyen y para esto se usan los destructores, que son los métodos contrarios a los constructores y que se encargan justamente de lo opuesto: destruir objetos y liberar la memoria que ocupan. La mayoría de las veces estos importantes métodos son olvidados y llegan a faltar en las clases, por suerte los compiladores crean también destructores por defecto y es por eso que los programas compilan y parecen funcionar bien aún cuando prescindimos de ellos. Sin embargo, tal como sucede con los constructores, es aconsejable escribir uno mismo los destructores para asegurarse de que todo finaliza correctamente. Un destructor tiene las siguientes características:
  • Tiene el mismo identificador que la clase, con el símbolo ~” como prefijo
  • No debe llevar ningún tipo de retorno, igual que el constructor no retorna datos
  • No debe tener parámetros
  • No puede ser heredado
  • Debe ser público
  • No puede ser sobrecargado, ya que no tiene tipo de retorno ni parámetros
En nuestra  clase de ejemplo no tenemos un destructor ya que el que proporciona el compilador nos sirve bien y es que todos los atributos son destruidos de manera correcta y no requieren un tratamiento especial de liberación de memoria (como el caso de los apuntadores). Mi consejo es siempre escribir un destructor cuando tengamos apuntadores como atributos, para liberar correctamente la memoria.
Agreguemos un destructor para mantener la clase más completa y apreciar cuando los objetos son destruidos:
// Dentro de la definición de la clase
public:
  ~Persona();
// Fuera de la definición de la clase
Persona::~Persona(){
// Una vez que nuestras personas vayan a ser destruidas se van a despedir. 
  cout << nombre << ": ¡Adiós!" << endl;
}

Un programa con personas

Con todo esto queda la clase completa, ahora probemos un ejemplo en el que construyamos objetos, usemos sus métodos y los destruyamos:
#include <iostream>
#include "Persona.h" // Para usar la clase Persona
using namespace std;

int main(){ // Función principal  
// Persona 1
  Persona persona1("Verónica",16,60,1.60);
  persona1.saluda();
  cout << "Un año después:" << endl;
  persona1.cumpleAnios();
  persona1.saluda();
  
// Persona 2
  Persona* persona2;
  persona2 = new Persona("Vianey",22,65,1.65);
  persona2 -> saluda();
  cout << "Un año después:" << endl;
  persona2.cumpleAnios();
  persona2.saluda();
  delete persona2;
  return 0;  
}
La salida del programa anterior es la siguiente:
¡Hola! me llamo Verónica, tengo 16 años, peso 60 kilos y mido 1.6 metros
Un año después:
¡Hola! me llamo Verónica, tengo 17 años, peso 60.1 kilos y mido 1.7 metros
¡Hola! me llamo Vianey, tengo 22 años, peso 65 kilos y mido 1.65 metros
Un año después:
¡Hola! me llamo Vianey, tengo 23 años, peso 65 kilos y mido 1.65 metros
Vianey: ¡Adiós!
Verónica: ¡Adiós!
Pregunta: ¿Por qué se despide primero Vianey que Verónica?

La respuesta es muy simple, Vianey es la persona que manejamos con un apuntador, por lo que antes de salir de la función la destruimos usando delete, en ese momento se despide. Por otro lado Verónica es destruida automáticamente al salir de la función en la que fue creada, por eso se despide después de Vianey.

Nótese que ambos objetos son declarados dentro de la misma función, si declaramos a Verónica en un bloque separado podremos apreciar como es destruida al salir de él:

#include <iostream>
#include "Persona.h" // Para usar la clase Persona
using namespace std;

int main(){ // Función principal  
// Persona 1 (declaración en un bloque)
  {
    Persona persona1("Verónica",16,60,1.60);
    persona1.saluda();
    cout << "Un año después:" << endl;
    persona1.cumpleAnios();
    persona1.saluda();
  }
// En este punto persona1 ya fue destruida
// Persona 2
  Persona* persona2;
  persona2 = new Persona("Vianey",22,65,1.65);
  persona2 -> saluda();
  cout << "Un año después:" << endl;
  persona2.cumpleAnios();
  persona2.saluda();
  delete persona2;
  return 0;  
}

Esta vez obtenemos una salida distinta, Verónica es destruida primero que Vianey:

¡Hola! me llamo Verónica, tengo 16 años, peso 60 kilos y mido 1.6 metros
Un año después:
¡Hola! me llamo Verónica, tengo 17 años, peso 60.1 kilos y mido 1.7 metros
Verónica: ¡Adiós!
¡Hola! me llamo Vianey, tengo 22 años, peso 65 kilos y mido 1.65 metros
Un año después:
¡Hola! me llamo Vianey, tengo 23 años, peso 65 kilos y mido 1.65 metros
Vianey: ¡Adiós!

Conclusión

Con lo estudiado en esta nota ya podemos usar objetos de nuestras clases, cuidando siempre los detalles referentes a los constructores y destructores. En las siguientes notas revisaremos y aplicaremos algunos conceptos referentes a la organización de las clases.

4 comments

  1. Excelente post Victor, desde hace tiempo tenía la duda sobre la diferencia de instanciar con New y sin new en C++.
    Una explicación simple pero detallada y muy ejemplificada del tema.
    Muchas gracias me fue de mucha ayuda 😉

    1. Hola Sebastián
      Gracias por leernos y dejar tu comentario. Efectivamente la serie aún no está terminada, espero pronto estarle dando continuación a la publicación de las siguientes notas.

      ¡Saludos!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *