Constructores (C++)

Para personalizar cómo inicializa una clase sus miembros o para invocar funciones cuando se crea un objeto de la clase, defina un constructor. Un constructor tiene el mismo nombre que la clase y no devuelve ningún valor. Puede definir tantos constructores sobrecargados como sea necesario para personalizar la inicialización de varias maneras. Normalmente, los constructores tienen accesibilidad pública para que el código que no pertenece a la jerarquía de herencia ni a la definición de la clase pueda crear objetos de la clase. Pero también puede declarar un constructor como protected o private.

Opcionalmente, los constructores pueden tomar una lista de inicializadores de miembros. Es una manera más eficaz de inicializar los miembros de la clase que asignar valores en el cuerpo del constructor. En el ejemplo siguiente, se muestra una clase Box con tres constructores sobrecargados. Los dos últimos utilizan listas de inicializadores de miembros:

class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    explicit Box(int i) : m_width(i), m_length(i), m_height(i) // member init list
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

    int Volume() { return m_width * m_length * m_height; }

private:
    // Will have value of 0 when default constructor is called.
    // If we didn't zero-init here, default constructor would
    // leave them uninitialized with garbage values.
    int m_width{ 0 };
    int m_length{ 0 };
    int m_height{ 0 };
};

Al declarar una instancia de una clase, el compilador elige qué constructor invocar en función de las reglas de resolución de sobrecargas:

int main()
{
    Box b; // Calls Box()

    // Using uniform initialization (preferred):
    Box b2 {5}; // Calls Box(int)
    Box b3 {5, 8, 12}; // Calls Box(int, int, int)

    // Using function-style notation:
    Box b4(2, 4, 6); // Calls Box(int, int, int)
}
  • Los constructores se pueden declarar como inline, explicit, friend o constexpr.
  • Un constructor puede inicializar un objeto que se ha declarado como const, volatile o const volatile. El objeto se convierte en const después de que finalice el constructor.
  • Para definir un constructor en un archivo de implementación, asígnele un nombre completo como a cualquier otra función miembro: Box::Box(){...}.

Listas de inicializadores de miembros

Opcionalmente, un constructor puede tener una lista de inicializadores de miembros, que inicializa los miembros de la clase antes de que se ejecute el cuerpo del constructor. (Una lista de inicializadores de miembros no es lo mismo que una lista de inicializadores de tipo std::initializer_list<T>).

Son preferibles las listas de inicializadores de miembros antes que la asignación de valores en el cuerpo del constructor. Una lista de inicializadores de miembros inicializa directamente los miembros. En el ejemplo siguiente, se muestra la lista de inicializadores de miembros, que consta de todas las expresiones identifier(argument) después de los dos puntos:

    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

El identificador debe hacer referencia a un miembro de la clase; se inicializa con el valor del argumento. El argumento puede ser uno de los parámetros del constructor, una llamada de función o std::initializer_list<T>.

Los miembros const y los miembros del tipo de referencia se deben inicializar en la lista de inicializadores de miembros.

Para asegurarse de que las clases base se hayan inicializado completamente antes de que se ejecute el constructor derivado, llame a cualquier constructor de clase base con parámetros en la lista de inicializadores.

Constructores predeterminados

Los constructores predeterminados normalmente no tienen parámetros, pero pueden tener parámetros con valores predeterminados.

class Box {
public:
    Box() { /*perform any required default initialization steps*/}

    // All params have default values
    Box (int w = 1, int l = 1, int h = 1): m_width(w), m_height(h), m_length(l){}
...
}

Los constructores predeterminados son una de las funciones miembro especiales. Si no se declara ningún constructor en una clase, el compilador proporciona un constructor predeterminado inline implícito.

#include <iostream>
using namespace std;

class Box {
public:
    int Volume() {return m_width * m_height * m_length;}
private:
    int m_width { 0 };
    int m_height { 0 };
    int m_length { 0 };
};

int main() {
    Box box1; // Invoke compiler-generated constructor
    cout << "box1.Volume: " << box1.Volume() << endl; // Outputs 0
}

Si se basa en un constructor predeterminado implícito, asegúrese de inicializar los miembros en la definición de la clase, como se muestra en el ejemplo anterior. Sin esos inicializadores, los miembros no se inicializarían y la llamada a Volume() produciría un valor de elemento no utilizado. En general, se recomienda inicializar los miembros de esta manera incluso cuando no se basan en un constructor predeterminado implícito.

Puede impedir que el compilador genere un constructor predeterminado implícito si lo define como eliminado:

    // Default constructor
    Box() = delete;

Un constructor predeterminado generado por el compilador se definirá como eliminado si algún miembro de la clase no se puede construir de manera predeterminada. Por ejemplo, todos los miembros del tipo de clase y sus miembros de tipo de clase deben tener un constructor predeterminado y destructores a los que se pueda acceder. Todos los miembros de datos del tipo de referencia y todos los miembros const deben tener un inicializador de miembros predeterminado.

Al llamar a un constructor predeterminado generado por el compilador e intentar usar paréntesis, se emite una advertencia:

class myclass{};
int main(){
myclass mc();     // warning C4930: prototyped function not called (was a variable definition intended?)
}

Esta instrucción es un ejemplo del problema "Análisis más acuciante". Puede interpretar myclass md(); como una declaración de función o como la invocación de un constructor predeterminado. Dado que los analizadores de C++ favorecen las declaraciones sobre otras cosas, la expresión se trata como una declaración de función. Para obtener más información, consulte Análisis más acuciante.

Si se declaran constructores no predeterminados, el compilador no proporcionará un constructor predeterminado:

class Box {
public:
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height){}
private:
    int m_width;
    int m_length;
    int m_height;

};

int main(){

    Box box1(1, 2, 3);
    Box box2{ 2, 3, 4 };
    Box box3; // C2512: no appropriate default constructor available
}

Si una clase no tiene ningún constructor predeterminado, no se puede crear una matriz de objetos de esa clase únicamente mediante una sintaxis de corchetes. Por ejemplo, dado el bloque de código anterior, no se puede declarar una matriz de objetos Box de esta manera:

Box boxes[3]; // C2512: no appropriate default constructor available

Sin embargo, puede utilizar un conjunto de listas de inicializadores para inicializar una matriz de objetos Box:

Box boxes[3]{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };

Para obtener más información, consulte Inicializadores.

Constructores de copias

Un constructor de copia inicializa un objeto copiando los valores de los miembros de un objeto del mismo tipo. Si los miembros de la clase son todos tipos simples, como valores escalares, el constructor de copia generado por el compilador es suficiente y no es necesario definir el suyo propio. Si la clase requiere una inicialización más compleja, debe implementar un constructor de copia personalizado. Por ejemplo, si un miembro de clase es un puntero, debe definir un constructor de copia para asignar nueva memoria y copiar los valores del objeto al que apunta el otro. El constructor de copia generado por el compilador simplemente copia el puntero, por lo que el nuevo puntero sigue apuntando a la ubicación de memoria del otro.

Un constructor de copia puede tener una de estas firmas:

    Box(Box& other); // Avoid if possible--allows modification of other.
    Box(const Box& other);
    Box(volatile Box& other);
    Box(volatile const Box& other);

    // Additional parameters OK if they have default values
    Box(Box& other, int i = 42, string label = "Box");

Al definir un constructor de copia, también debe definir un operador de asignación de copia (=). Para obtener más información, consulte Asignación y Constructores de copia y operadores de asignación de copia (C++).

Puede evitar que el objeto se copie definiendo el constructor de copia como eliminado:

    Box (const Box& other) = delete;

Al intentar copiar el objeto, se produce el error C2280: se intenta hacer referencia a una función eliminada.

Constructores de movimiento

Un constructor de movimiento es una función miembro especial que mueve la propiedad de los datos de un objeto existente a una nueva variable sin copiar los datos originales. Toma una referencia rvalue como primer parámetro y los parámetros posteriores deben tener valores predeterminados. Los constructores de movimiento pueden aumentar significativamente la eficacia del programa al pasar objetos grandes.

Box(Box&& other);

El compilador elige un constructor de movimiento cuando otro objeto del mismo tipo inicializa el objeto, si el otro objeto está a punto de ser destruido y ya no necesita sus recursos. En el ejemplo siguiente, se muestra un caso cuando se selecciona un constructor de movimiento mediante la resolución de sobrecarga. En el constructor que llama a get_Box(), el valor devuelto es un valor xvalue (valor que va a expirar). No está asignado a ninguna variable y, por lo tanto, está a punto de salir del ámbito. Para proporcionar motivación para este ejemplo, vamos a proporcionar a Box un vector grande de cadenas que representan su contenido. En lugar de copiar el vector y sus cadenas, el constructor de movimiento lo "roba" del valor "box" que va a expirar para que el vector ahora pertenezca al nuevo objeto. La llamada a std::move es todo lo que se necesita porque las clases vector y string implementan sus propios constructores de movimiento.

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;

class Box {
public:
    Box() { std::cout << "default" << std::endl; }
    Box(int width, int height, int length)
       : m_width(width), m_height(height), m_length(length)
    {
        std::cout << "int,int,int" << std::endl;
    }
    Box(Box& other)
       : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        std::cout << "copy" << std::endl;
    }
    Box(Box&& other) : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        m_contents = std::move(other.m_contents);
        std::cout << "move" << std::endl;
    }
    int Volume() { return m_width * m_height * m_length; }
    void Add_Item(string item) { m_contents.push_back(item); }
    void Print_Contents()
    {
        for (const auto& item : m_contents)
        {
            cout << item << " ";
        }
    }
private:
    int m_width{ 0 };
    int m_height{ 0 };
    int m_length{ 0 };
    vector<string> m_contents;
};

Box get_Box()
{
    Box b(5, 10, 18); // "int,int,int"
    b.Add_Item("Toupee");
    b.Add_Item("Megaphone");
    b.Add_Item("Suit");

    return b;
}

int main()
{
    Box b; // "default"
    Box b1(b); // "copy"
    Box b2(get_Box()); // "move"
    cout << "b2 contents: ";
    b2.Print_Contents(); // Prove that we have all the values

    char ch;
    cin >> ch; // keep window open
    return 0;
}

Si una clase no define un constructor de movimiento, el compilador genera uno implícito si no hay ningún constructor de copia declarado por el usuario, operador de asignación de copia, operador de asignación de movimiento ni destructor. Si no se define ningún constructor de movimiento explícito o implícito, las operaciones que usarían de otro modo un constructor de movimiento usan el constructor de copia en su lugar. Si una clase declara un constructor de movimiento o un operador de asignación de movimiento, el constructor de copia declarado implícitamente se define como eliminado.

Un constructor de movimiento declarado implícitamente se define como eliminado si alguno de los miembros que son tipos de clase carece de un destructor o si el compilador no puede determinar qué constructor usar para la operación de movimiento.

Para obtener más información sobre cómo escribir un constructor de movimiento no trivial, consulte Constructores de movimiento y operadores de asignación de movimiento (C++).

Constructores explícitamente establecidos como predeterminados y eliminados

Puede establecer explícitamente constructores de copia predeterminados, constructores predeterminados, constructores de movimiento, operadores de asignación de copia, operadores de asignación de movimiento y destructores. Puede eliminar explícitamente todas las funciones miembro especiales.

class Box2
{
public:
    Box2() = delete;
    Box2(const Box2& other) = default;
    Box2& operator=(const Box2& other) = default;
    Box2(Box2&& other) = default;
    Box2& operator=(Box2&& other) = default;
    //...
};

Para obtener más información, consulte Funciones establecidas como valor predeterminado y eliminadas explícitamente.

Constructores constexpr

Un constructor se puede declarar como constexpr si:

  • Se declara como predeterminado o satisface todas las condiciones de las funciones constexpr en general.
  • La clase no tiene clases base virtuales.
  • Cada uno de los parámetros es un tipo literal.
  • El cuerpo no es una función try-block.
  • Se han inicializado todos los miembros de datos no estáticos y los subobjetos de clase base.
  • Si la clase es (a) una unión que tiene miembros variantes o (b) tiene uniones anónimas, solo se inicializa uno de los miembros de la unión.
  • Todos los miembros de datos no estáticos del tipo de clase y todos los subobjetos de clase base tienen un constructor constexpr.

Constructores de lista de inicializadores

Si un constructor toma std::initializer_list<T> como parámetro y cualquier otro parámetro tiene argumentos predeterminados, se selecciona ese constructor en la resolución de sobrecarga cuando se crea una instancia de la clase mediante la inicialización directa. Puede usar el elemento initializer_list para inicializar cualquier miembro que pueda aceptarlo. Por ejemplo, supongamos que la clase Box (mostrada anteriormente) tiene un miembro m_contents de tipo std::vector<string>. Puede proporcionar un constructor de esta manera:

    Box(initializer_list<string> list, int w = 0, int h = 0, int l = 0)
        : m_contents(list), m_width(w), m_height(h), m_length(l)
{}

Y, a continuación, crear objetos Box de esta forma:

    Box b{ "apples", "oranges", "pears" }; // or ...
    Box b2(initializer_list<string> { "bread", "cheese", "wine" }, 2, 4, 6);

Constructores explícitos

Si una clase tiene un constructor con un solo parámetro, o si todos los parámetros excepto uno tienen un valor predeterminado, el tipo de parámetro se puede convertir implícitamente en el tipo de clase. Por ejemplo, si la clase Box tiene un constructor como este:

Box(int size): m_width(size), m_length(size), m_height(size){}

Se puede inicializar un objeto Box de esta manera:

Box b = 42;

O pasar un valor int a una función que toma un objeto Box:

class ShippingOrder
{
public:
    ShippingOrder(Box b, double postage) : m_box(b), m_postage(postage){}

private:
    Box m_box;
    double m_postage;
}
//elsewhere...
    ShippingOrder so(42, 10.8);

Estas conversiones pueden ser útiles en algunos casos, pero lo más habitual es que provoquen errores sutiles, pero graves, en el código. Como regla general, es conveniente usar la palabra clave explicit en un constructor (y los operadores definidos por el usuario) para evitar esta clase de conversión de tipos implícita:

explicit Box(int size): m_width(size), m_length(size), m_height(size){}

Cuando el constructor es explícito, esta línea provoca un error del compilador: ShippingOrder so(42, 10.8);. Para obtener más información, consulte Conversiones de tipo definidas por el usuario (C++).

Orden de construcción

Un constructor realiza su trabajo en este orden:

  1. Llama a los constructores miembros y de clase base en el orden en que se declararon.

  2. Si la clase se deriva de clases base virtuales, inicializa los punteros base virtuales del objeto.

  3. Si la clase tiene o hereda funciones virtuales, inicializa los punteros de funciones virtuales del objeto. Los punteros de funciones virtuales apuntan a la tabla de funciones virtuales de la clase para permitir el enlace correcto de las llamadas de funciones virtuales al código.

  4. Ejecuta cualquier código en el cuerpo de su función.

En el ejemplo siguiente se muestra el orden en el que los constructores miembros y de clase base se llaman en el constructor para una clase derivada. En primer lugar, se llama al constructor base. A continuación, se inicializan los miembros de clase base en el orden en el que aparecen en la declaración de clase. Por último, se llama al constructor derivado.

#include <iostream>

using namespace std;

class Contained1 {
public:
    Contained1() { cout << "Contained1 ctor\n"; }
};

class Contained2 {
public:
    Contained2() { cout << "Contained2 ctor\n"; }
};

class Contained3 {
public:
    Contained3() { cout << "Contained3 ctor\n"; }
};

class BaseContainer {
public:
    BaseContainer() { cout << "BaseContainer ctor\n"; }
private:
    Contained1 c1;
    Contained2 c2;
};

class DerivedContainer : public BaseContainer {
public:
    DerivedContainer() : BaseContainer() { cout << "DerivedContainer ctor\n"; }
private:
    Contained3 c3;
};

int main() {
    DerivedContainer dc;
}

Esta es la salida:

Contained1 ctor
Contained2 ctor
BaseContainer ctor
Contained3 ctor
DerivedContainer ctor

Un constructor de clase derivada siempre llama a un constructor de clase base, por lo que se pueden usar clases base completamente construidas antes de realizar cualquier trabajo adicional. Los constructores de clase base se llaman en orden de derivación: por ejemplo, si ClassA se deriva de ClassB, que se deriva de ClassC, se llama primero al constructor de ClassC, después al constructor de ClassB y, por último, al constructor de ClassA.

Si una clase base no tiene un constructor predeterminado, debe proporcionar los parámetros del constructor de la clase base en el constructor de la clase derivada:

class Box {
public:
    Box(int width, int length, int height){
       m_width = width;
       m_length = length;
       m_height = height;
    }

private:
    int m_width;
    int m_length;
    int m_height;
};

class StorageBox : public Box {
public:
    StorageBox(int width, int length, int height, const string label&) : Box(width, length, height){
        m_label = label;
    }
private:
    string m_label;
};

int main(){

    const string aLabel = "aLabel";
    StorageBox sb(1, 2, 3, aLabel);
}

Si un constructor inicia una excepción, el orden de destrucción es el inverso al orden de la construcción:

  1. El código del cuerpo de la función de constructor se desenredará.

  2. Los objetos miembro y de la clase base se destruirán en el orden inverso de la declaración.

  3. Si el constructor no delega, se destruirán todos los miembros y objetos de clase base totalmente construidos. Sin embargo, dado que el objeto en sí no está totalmente construido, el destructor no se ejecuta.

Constructores derivados e inicialización de agregado extendida

Si el constructor de una clase base no es público, pero es accesible para una clase derivada, no puede usar llaves vacías para inicializar un objeto del tipo derivado en modo /std:c++17 y versiones posteriores en Visual Studio 2017 y versiones posteriores.

En el ejemplo siguiente se muestra el comportamiento correspondiente de C++14:

struct Derived;

struct Base {
    friend struct Derived;
private:
    Base() {}
};

struct Derived : Base {};

Derived d1; // OK. No aggregate init involved.
Derived d2 {}; // OK in C++14: Calls Derived::Derived()
               // which can call Base ctor.

En C ++ 17, Derived ahora se considera un tipo de agregado. Eso significa que la inicialización de Base mediante el constructor privado predeterminado se produce directamente como parte de la regla de inicialización de agregados extendida. Anteriormente, se llamaba al constructor privado Base mediante el constructor Derived y se realizaba correctamente debido a la declaración friend.

En el ejemplo siguiente, se muestra el comportamiento de C++17 en Visual Studio 2017 y versiones posteriores en el modo /std:c++17:

struct Derived;

struct Base {
    friend struct Derived;
private:
    Base() {}
};

struct Derived : Base {
    Derived() {} // add user-defined constructor
                 // to call with {} initialization
};

Derived d1; // OK. No aggregate init involved.

Derived d2 {}; // error C2248: 'Base::Base': can't access
               // private member declared in class 'Base'

Constructores de clases que tienen herencia múltiple

Si una clase se deriva de varias clases base, los constructores de clase base se invocan en el orden en el que se enumeran en la declaración de la clase derivada:

#include <iostream>
using namespace std;

class BaseClass1 {
public:
    BaseClass1() { cout << "BaseClass1 ctor\n"; }
};
class BaseClass2 {
public:
    BaseClass2() { cout << "BaseClass2 ctor\n"; }
};
class BaseClass3 {
public:
    BaseClass3() { cout << "BaseClass3 ctor\n"; }
};
class DerivedClass : public BaseClass1,
                     public BaseClass2,
                     public BaseClass3
                     {
public:
    DerivedClass() { cout << "DerivedClass ctor\n"; }
};

int main() {
    DerivedClass dc;
}

Debería esperar los siguientes resultados:

BaseClass1 ctor
BaseClass2 ctor
BaseClass3 ctor
DerivedClass ctor

Constructores de delegación

Un constructor de delegación llama a otro constructor de la misma clase para realizar algunas de las tareas de inicialización. Esta característica es útil cuando tiene varios constructores que todos tienen que realizar un trabajo similar. Puede escribir la lógica principal en un constructor e invocarla desde otros. En el siguiente ejemplo trivial, Box(int) delega su trabajo en Box(int,int,int):

class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    Box(int i) :  Box(i, i, i)  // delegating constructor
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}
    //... rest of class as before
};

El objeto creado por los constructores se inicializa totalmente en cuanto finaliza cualquiera de los constructores. Para obtener más información, consulte Constructores de delegación.

Constructores heredados (C++11)

Una clase derivada puede heredar los constructores de una clase base directa mediante una declaración using, como se muestra en el ejemplo siguiente:

#include <iostream>
using namespace std;

class Base
{
public:
    Base() { cout << "Base()" << endl; }
    Base(const Base& other) { cout << "Base(Base&)" << endl; }
    explicit Base(int i) : num(i) { cout << "Base(int)" << endl; }
    explicit Base(char c) : letter(c) { cout << "Base(char)" << endl; }

private:
    int num;
    char letter;
};

class Derived : Base
{
public:
    // Inherit all constructors from Base
    using Base::Base;

private:
    // Can't initialize newMember from Base constructors.
    int newMember{ 0 };
};

int main()
{
    cout << "Derived d1(5) calls: ";
    Derived d1(5);
    cout << "Derived d1('c') calls: ";
    Derived d2('c');
    cout << "Derived d3 = d2 calls: " ;
    Derived d3 = d2;
    cout << "Derived d4 calls: ";
    Derived d4;
}

/* Output:
Derived d1(5) calls: Base(int)
Derived d1('c') calls: Base(char)
Derived d3 = d2 calls: Base(Base&)
Derived d4 calls: Base()*/

Visual Studio 2017 y versiones posteriores: la instrucción using en modo /std:c++17 y versiones posteriores incluye en el ámbito a todos los constructores de la clase base, excepto los que tienen una firma idéntica a la de los constructores de la clase derivada. En general, es mejor usar constructores que heredan cuando la clase derivada no declara ningún constructor ni miembro de datos nuevo.

Una plantilla de clase puede heredar todos los constructores de un argumento de tipo si dicho tipo especifica una clase base:

template< typename T >
class Derived : T {
    using T::T;   // declare the constructors from T
    // ...
};

Una clase derivada no puede heredar de varias clases base si esas clases base tienen constructores con una firma idéntica.

Constructores y clases compuestas

Las clases que contienen miembros de tipo de clase se conocen como clases compuestas. Cuando se crea un miembro de tipo de clase compuesta, se llama al constructor antes que al propio constructor de la clase. Si una clase contenida carece de un constructor predeterminado, debe utilizar una lista de inicializaciones en el constructor de la clase compuesta. En el ejemplo anterior de StorageBox, si cambia el tipo de la variable miembro m_label a una nueva clase Label, debe llamar al constructor de la clase base e inicializar la variable m_label en el constructor StorageBox:

class Label {
public:
    Label(const string& name, const string& address) { m_name = name; m_address = address; }
    string m_name;
    string m_address;
};

class StorageBox : public Box {
public:
    StorageBox(int width, int length, int height, Label label)
        : Box(width, length, height), m_label(label){}
private:
    Label m_label;
};

int main(){
// passing a named Label
    Label label1{ "some_name", "some_address" };
    StorageBox sb1(1, 2, 3, label1);

    // passing a temporary label
    StorageBox sb2(3, 4, 5, Label{ "another name", "another address" });

    // passing a temporary label as an initializer list
    StorageBox sb3(1, 2, 3, {"myname", "myaddress"});
}

En esta sección

Consulte también

Clases y estructuras