Qué es un constructor
En la página anterior definiste clases y les diste variables miembro. Pero los miembros de un objeto recién creado contienen la basura que hubiera en esa memoria, a menos que tú les des valor. Un constructor soluciona eso: es una función miembro especial que se ejecuta automáticamente en el momento en que se crea un objeto, y su única tarea es dejar el objeto en un estado válido y completamente inicializado.
Un constructor tiene el mismo nombre que la clase y no tiene tipo de retorno, ni siquiera void. Nunca lo llamas directamente; el compilador lo llama por ti cada vez que un objeto cobra existencia.
El Counter() sin parámetros se llama constructor por defecto: es el que se usa cuando creas un objeto sin pasar ningún argumento.
Constructores parametrizados
Un constructor que no recibe argumentos está bien, pero normalmente quieres crear un objeto con valores concretos. Un constructor parametrizado acepta argumentos y los usa para inicializar los miembros.
Una clase puede tener más de un constructor, siempre que sus listas de parámetros difieran: esto es la sobrecarga de funciones habitual aplicada a los constructores. Aquí Point se puede crear con o sin coordenadas:
Un error habitual: Point p(); no crea un objeto; el compilador lo lee como la declaración de una función llamada p que devuelve un Point. Para llamar al constructor por defecto, escribe Point p; (sin paréntesis) o Point p{}; con llaves.
Listas de inicialización de miembros
Hasta ahora los ejemplos asignaban a los miembros dentro del cuerpo del constructor. Eso funciona para tipos simples, pero es la herramienta equivocada. Cuando se ejecuta el cuerpo, cada miembro ya ha sido construido por defecto; el cuerpo lo descarta y asigna encima. Una lista de inicialización de miembros inicializa cada miembro directamente, antes del cuerpo, en un solo paso.
La sintaxis es dos puntos después de la lista de parámetros, seguidos de parejas member(value):
Para un miembro string esto también evita construir una cadena vacía y luego asignar: la lista de inicialización la construye correctamente al primer intento.
La lista de inicialización no es solo una optimización; es obligatoria en tres casos, porque el cuerpo solo puede asignar, no inicializar:
- Miembros
const: no puedes asignar a unconstuna vez que existe. - Miembros de referencia: una referencia debe quedar ligada en el momento de nacer.
- Miembros cuyo tipo no tiene constructor por defecto.
class Sensor {
const int id; // miembro const
int& slot; // miembro de referencia
public:
Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
// Intentar asignar id o slot en el cuerpo no compilaría.
};
Un detalle que conviene conocer: los miembros se inicializan en el orden en que se declaran en la clase, no en el orden en que los listas en la lista de inicialización. Si el inicializador de un miembro lee a otro, lo que importa es el orden de declaración; confundir ambos es una fuente clásica de usar un valor que aún no se ha inicializado.
Argumentos por defecto y constructores delegantes
No siempre necesitas sobrecargas separadas. Los argumentos por defecto permiten que un único constructor cubra varios casos: omite un argumento y el valor por defecto lo rellena.
Ten cuidado al combinar un constructor parametrizado con valores por defecto con un constructor por defecto Point() aparte: el compilador no puede saber a cuál llamar para Point p; y reportará una ambigüedad. Elige un solo enfoque.
Cuando tienes varios constructores que comparten una configuración común, un constructor delegante (C++11) permite que un constructor llame a otro en lugar de repetir la lógica. "Delegas" poniendo el otro constructor en la lista de inicialización:
El constructor de copia
Cuando creas un objeto como copia de otro (al pasarlo por valor, devolverlo o escribir Foo b = a;), se ejecuta el constructor de copia. Su firma recibe una referencia const al mismo tipo:
ClassName(const ClassName& other);
Si no escribes uno, el compilador genera un constructor de copia por defecto que copia cada miembro. Para clases que solo contienen valores (ints, strings, vectors), eso es exactamente lo correcto y no deberías escribir el tuyo.
El gran problema vive en el siguiente capítulo sobre memoria: si tu clase posee un puntero crudo a memoria del heap, el constructor de copia por defecto copia el puntero, no los datos, así que dos objetos acaban apuntando a la misma memoria y ambos intentarán liberarla. Ese es el bug de la doble liberación. La regla práctica es la regla de tres/cinco: si escribes un destructor personalizado, es casi seguro que también necesitas un constructor de copia personalizado (y la asignación por copia). En el C++ moderno la solución más limpia es usar un std::vector o un puntero inteligente, para que la copia generada por el compilador simplemente funcione.
Ten en cuenta también que recibir el parámetro por referencia es obligatorio, no opcional: un constructor de copia que recibiera su argumento por valor tendría que copiar el argumento para poder llamarse a sí mismo, una recursión infinita que ni siquiera compilaría.
A continuación: destructores
Un constructor monta un objeto; un destructor lo desmonta. Cuando un objeto sale de ámbito o se elimina, su destructor se ejecuta automáticamente: el lugar perfecto para liberar archivos, conexiones de red o la memoria del heap que el objeto tenía. La siguiente página explica cómo funcionan los destructores, cuándo se ejecutan exactamente y cómo se emparejan con los constructores para dar a C++ su potente patrón RAII.
Preguntas frecuentes
¿Qué es un constructor en C++?
Un constructor es una función miembro especial que tiene el mismo nombre que la clase y no tiene tipo de retorno. Se ejecuta automáticamente cuando se crea un objeto, y su tarea es dejar el objeto en un estado válido y completamente inicializado antes de que cualquier otro código lo use.
¿Cuál es la diferencia entre un constructor por defecto y uno parametrizado?
Un constructor por defecto no recibe argumentos y se usa cuando creas un objeto sin proporcionar valores (Point p;). Un constructor parametrizado recibe argumentos para que quien lo llame pueda inicializar el objeto con valores concretos (Point p(3, 4);). Una clase puede tener ambos, porque los constructores se sobrecargan según sus listas de parámetros.
¿Por qué debería usar una lista de inicialización de miembros en C++?
Una lista de inicialización de miembros (: name(n), age(a)) inicializa los miembros directamente, antes de que se ejecute el cuerpo del constructor. Es obligatoria para miembros const, referencias y miembros sin constructor por defecto, y evita el costoso construir-por-defecto-y-luego-asignar que ocurre cuando asignas dentro del cuerpo.