C++
Funciones especiales para miembros
Buscar..
Destructores virtuales y protegidos.
Una clase diseñada para ser heredada se llama clase base. Se debe tener cuidado con las funciones especiales para miembros de dicha clase.
Una clase diseñada para ser utilizada polimórficamente en tiempo de ejecución (a través de un puntero a la clase base) debe declarar el destructor virtual
. Esto permite que las partes derivadas del objeto se destruyan adecuadamente, incluso cuando el objeto se destruye a través de un puntero a la clase base.
class Base {
public:
virtual ~Base() = default;
private:
// data members etc.
};
class Derived : public Base { // models Is-A relationship
public:
// some methods
private:
// more data members
};
// virtual destructor in Base ensures that derived destructors
// are also called when the object is destroyed
std::unique_ptr<Base> base = std::make_unique<Derived>();
base = nullptr; // safe, doesn't leak Derived's members
Si la clase no necesita ser polimórfica, pero aún necesita permitir que su interfaz sea heredada, use un destructor protected
no virtual.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
Tal clase nunca puede ser destruida a través de un puntero, evitando fugas silenciosas debido al corte en rodajas.
Esta técnica se aplica especialmente a las clases diseñadas para ser clases base private
. Una clase de este tipo podría usarse para encapsular algunos detalles de implementación comunes, al tiempo que proporciona métodos virtual
como puntos de personalización. Este tipo de clase nunca debe utilizarse polimórficamente, y un destructor protected
ayuda a documentar este requisito directamente en el código.
Finalmente, algunas clases pueden requerir que nunca se usen como clase base. En este caso, la clase se puede marcar como final
. Un destructor público no virtual normal está bien en este caso.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Movimiento implícito y copia
Tenga en cuenta que declarar un destructor impide que el compilador genere constructores de movimiento implícito y operadores de asignación de movimiento. Si declara un destructor, recuerde agregar también las definiciones apropiadas para las operaciones de movimiento.
Además, la declaración de operaciones de movimiento suprimirá la generación de operaciones de copia, por lo que también deben agregarse (si se requiere que los objetos de esta clase tengan copia semántica).
class Movable {
public:
virtual ~Movable() noexcept = default;
// compiler won't generate these unless we tell it to
// because we declared a destructor
Movable(Movable&&) noexcept = default;
Movable& operator=(Movable&&) noexcept = default;
// declaring move operations will suppress generation
// of copy operations unless we explicitly re-enable them
Movable(const Movable&) = default;
Movable& operator=(const Movable&) = default;
};
Copiar e intercambiar
Si está escribiendo una clase que administra recursos, debe implementar todas las funciones especiales para miembros (consulte la Regla de tres / cinco / cero ). El enfoque más directo para escribir el constructor de copia y el operador de asignación sería:
person(const person &other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
person& operator=(person const& rhs) {
if (this != &other) {
delete [] name;
name = new char[std::strlen(other.name) + 1];
std::strcpy(name, other.name);
age = other.age;
}
return *this;
}
Pero este enfoque tiene algunos problemas. Falla la fuerte garantía excepción - si es new[]
tiros, que ya hemos aclarado los recursos propiedad de this
y no se puede recuperar. Estamos duplicando gran parte de la lógica de la construcción de copias en la asignación de copias. Y tenemos que recordar la verificación de autoasignación, que generalmente solo agrega gastos generales a la operación de copia, pero aún es crítica.
Para satisfacer la fuerte garantía de excepción y evitar la duplicación de código (doble con el operador de asignación de movimiento subsiguiente), podemos utilizar el lenguaje de copia e intercambio:
class person {
char* name;
int age;
public:
/* all the other functions ... */
friend void swap(person& lhs, person& rhs) {
using std::swap; // enable ADL
swap(lhs.name, rhs.name);
swap(lhs.age, rhs.age);
}
person& operator=(person rhs) {
swap(*this, rhs);
return *this;
}
};
¿Por qué funciona esto? Considera lo que pasa cuando tenemos
person p1 = ...;
person p2 = ...;
p1 = p2;
Primero, copiamos y construimos rhs
desde p2
(que no tuvimos que duplicar aquí). Si esa operación se produce, no hacemos nada en el operator=
y p1
permanece intacto. A continuación, intercambiamos los miembros entre *this
y rhs
, y luego rhs
queda fuera del alcance. Cuando operator=
, eso limpia de forma implícita los recursos originales de this
(a través del destructor, que no tuvimos que duplicar). La autoasignación también funciona: es menos eficiente con la copia e intercambio (implica una asignación adicional y una desasignación), pero si ese es el escenario improbable, no ralentizamos el caso de uso típico para tenerlo en cuenta.
La formulación anterior funciona tal como está para la asignación de movimiento.
p1 = std::move(p2);
Aquí, movemos-construimos rhs
desde p2
, y todo lo demás es igual de válido. Si una clase es movible pero no puede copiarse, no es necesario eliminar la asignación de copia, ya que este operador de asignación simplemente tendrá una forma incorrecta debido al constructor de copia eliminado.
Constructor predeterminado
Un constructor predeterminado es un tipo de constructor que no requiere parámetros cuando se le llama. Se nombra después del tipo que construye y es una función miembro de él (como lo son todos los constructores).
class C{
int i;
public:
// the default constructor definition
C()
: i(0){ // member initializer list -- initialize i to 0
// constructor function body -- can do more complex things here
}
};
C c1; // calls default constructor of C to create object c1
C c2 = C(); // calls default constructor explicitly
C c3(); // ERROR: this intuitive version is not possible due to "most vexing parse"
C c4{}; // but in C++11 {} CAN be used in a similar way
C c5[2]; // calls default constructor for both array elements
C* c6 = new C[2]; // calls default constructor for both array elements
Otra forma de satisfacer el requisito de "sin parámetros" es que el desarrollador proporcione valores predeterminados para todos los parámetros:
class D{
int i;
int j;
public:
// also a default constructor (can be called with no parameters)
D( int i = 0, int j = 42 )
: i(i), j(j){
}
};
D d; // calls constructor of D with the provided default values for the parameters
En algunas circunstancias (es decir, el desarrollador no proporciona constructores y no hay otras condiciones de descalificación), el compilador proporciona implícitamente un constructor predeterminado vacío:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Tener algún otro tipo de constructor es una de las condiciones de descalificación mencionadas anteriormente:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
Para evitar la creación implícita del constructor predeterminado, una técnica común es declararla como private
(sin definición). La intención es provocar un error de compilación cuando alguien intenta usar el constructor (esto puede resultar en un error de Acceso a privado o un error de vinculador, según el compilador).
Para asegurarse de que se defina un constructor predeterminado (funcionalmente similar al implícito), un desarrollador podría escribir uno vacío explícitamente.
En C ++ 11, un desarrollador también puede usar la palabra clave delete
para evitar que el compilador proporcione un constructor predeterminado.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Además, un desarrollador también puede ser explícito acerca de querer que el compilador proporcione un constructor predeterminado.
class C{
int i;
public:
// does have automatically generated default constructor (same as implicit one)
C() = default;
C( int i ) : i(i){}
};
C c1; // default constructed
C c2( 1 ); // constructed with the int taking constructor
Puede determinar si un tipo tiene un constructor predeterminado (o es un tipo primitivo) usando std::is_default_constructible
de <type_traits>
:
class C1{ };
class C2{ public: C2(){} };
class C3{ public: C3(int){} };
using std::cout; using std::boolalpha; using std::endl;
using std::is_default_constructible;
cout << boolalpha << is_default_constructible<int>() << endl; // prints true
cout << boolalpha << is_default_constructible<C1>() << endl; // prints true
cout << boolalpha << is_default_constructible<C2>() << endl; // prints true
cout << boolalpha << is_default_constructible<C3>() << endl; // prints false
En C ++ 11, todavía es posible usar la versión no funcional de std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
Incinerador de basuras
Un destructor es una función sin argumentos que se llama cuando un objeto definido por el usuario está a punto de ser destruido. Se nombra después del tipo que destruye con un prefijo ~
.
class C{
int* is;
string s;
public:
C()
: is( new int[10] ){
}
~C(){ // destructor definition
delete[] is;
}
};
class C_child : public C{
string s_ch;
public:
C_child(){}
~C_child(){} // child destructor
};
void f(){
C c1; // calls default constructor
C c2[2]; // calls default constructor for both elements
C* c3 = new C[2]; // calls default constructor for both array elements
C_child c_ch; // when destructed calls destructor of s_ch and of C base (and in turn s)
delete[] c3; // calls destructors on c3[0] and c3[1]
} // automatic variables are destroyed here -- i.e. c1, c2 and c_ch
En la mayoría de las circunstancias (es decir, un usuario no proporciona un destructor y no hay otras condiciones de descalificación), el compilador proporciona un destructor predeterminado de manera implícita:
class C{
int i;
string s;
};
void f(){
C* c1 = new C;
delete c1; // C has a destructor
}
class C{
int m;
private:
~C(){} // not public destructor!
};
class C_container{
C c;
};
void f(){
C_container* c_cont = new C_container;
delete c_cont; // Compile ERROR: C has no accessible destructor
}
En C ++ 11, un desarrollador puede anular este comportamiento impidiendo que el compilador proporcione un destructor predeterminado.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Además, un desarrollador también puede ser explícito acerca de querer que el compilador proporcione un destructor predeterminado.
class C{
int m;
public:
~C() = default; // saying explicitly it does have implicit/empty destructor
};
void f(){
C c1;
} // C has a destructor -- c1 properly destroyed
Puede determinar si un tipo tiene un destructor (o es un tipo primitivo) usando std::is_destructible
de <type_traits>
:
class C1{ };
class C2{ public: ~C2() = delete };
class C3 : public C2{ };
using std::cout; using std::boolalpha; using std::endl;
using std::is_destructible;
cout << boolalpha << is_destructible<int>() << endl; // prints true
cout << boolalpha << is_destructible<C1>() << endl; // prints true
cout << boolalpha << is_destructible<C2>() << endl; // prints false
cout << boolalpha << is_destructible<C3>() << endl; // prints false