14.5. Funciones que no se heredan automáticamente

No todas las funciones se heredan automáticamente de la clase base a la clase derivada. Los constructores y destructores se ocupan de la creación y la destrucción de un objeto, y sólo saben qué hacer con los aspectos del objeto que corresponden a su clase concreta, de modo que hay que llamar a todos los constructores y destructores de la jerarquía que hay por debajo de ellos. Así, los constructores y destructores no se heredan y deben crearse específicamente para cada clase derivada.

Además, operator= tampoco se hereda porque realiza una tarea parecida a la de un constructor. Esto es, el hecho de saber cómo asignar todos los miembros de un objeto situado a la izquierda del = a partir de un objeto situado a la derecha no significa que la asignación vaya a seguir teniendo el mismo significado después de la herencia.

En lugar de heredarse, el compilador sintetiza estas funciones si no las crea usted mismo. (En el caso de los constructores, para que el compilador sintetice el constructor por defecto y el constructor de copia, no debe crear ningún constructor.) Esto se describió brevemente en el Capítulo 6. Los constructores sintetizados utilizan inicialización miembro a miembro y el operator= sintetizado utiliza asignación miembro a miembro. He aquí un ejemplo de las funciones que sintetiza el compilador:

//: C14:SynthesizedFunctions.cpp
// Functions that are synthesized by the compiler
#include <iostream>
using namespace std;

class GameBoard {
public:
  GameBoard() { cout << "GameBoard()\n"; }
  GameBoard(const GameBoard&) { 
    cout << "GameBoard(const GameBoard&)\n"; 
  }
  GameBoard& operator=(const GameBoard&) {
    cout << "GameBoard::operator=()\n";
    return *this;
  }
  ~GameBoard() { cout << "~GameBoard()\n"; }
};

class Game {
  GameBoard gb; // Composition
public:
  // Default GameBoard constructor called:
  Game() { cout << "Game()\n"; }
  // You must explicitly call the GameBoard
  // copy-constructor or the default constructor
  // is automatically called instead:
  Game(const Game& g) : gb(g.gb) { 
    cout << "Game(const Game&)\n"; 
  }
  Game(int) { cout << "Game(int)\n"; }
  Game& operator=(const Game& g) {
    // You must explicitly call the GameBoard
    // assignment operator or no assignment at 
    // all happens for gb!
    gb = g.gb;
    cout << "Game::operator=()\n";
    return *this;
  }
  class Other {}; // Nested class
  // Automatic type conversion:
  operator Other() const {
    cout << "Game::operator Other()\n";
    return Other();
  }
  ~Game() { cout << "~Game()\n"; }
};

class Chess : public Game {};

void f(Game::Other) {}

class Checkers : public Game {
public:
  // Default base-class constructor called:
  Checkers() { cout << "Checkers()\n"; }
  // You must explicitly call the base-class
  // copy constructor or the default constructor
  // will be automatically called instead:
  Checkers(const Checkers& c) : Game(c) {
    cout << "Checkers(const Checkers& c)\n";
  }
  Checkers& operator=(const Checkers& c) {
    // You must explicitly call the base-class
    // version of operator=() or no base-class
    // assignment will happen:
    Game::operator=(c);
    cout << "Checkers::operator=()\n";
    return *this;
  }
};

int main() {
  Chess d1;  // Default constructor
  Chess d2(d1); // Copy-constructor
//! Chess d3(1); // Error: no int constructor
  d1 = d2; // Operator= synthesized
  f(d1); // Type-conversion IS inherited
  Game::Other go;
//!  d1 = go; // Operator= not synthesized 
           // for differing types
  Checkers c1, c2(c1);
  c1 = c2;
} ///:~

Listado 14.10. C14/SynthesizedFunctions.cpp


Los constructores y el operator= de GameBoard y Game se anuncian a sí mismos, de modo que puede ver cuándo los utiliza el compilador. Además, operator Other() realiza una conversión automática de tipo de un objeto Game a un objeto de la clase anidada Other. La clase Chess simplemente hereda de Game y no crea ninguna función (para ver cómo responde el compilador). La función f() recibe un objeto Other para probar la función de conversión automática de tipo.

En main() se llama al constructor por defecto y al constructor de copia sintetizados de la clase derivada Chess. Las versiones de Game de estos constructores se llaman como parte de la jerarquía de llamadas a constructores. Aunque parezca herencia, en realidad el compilador sintetiza constructores nuevos. Como cabría esperar, no se crea automáticamente ningún constructor con argumentos, porque eso es demasiado para que el compilador lo intuya.

El operator= también se sintetiza como una nueva función en Chess usando asignación miembro a miembro (por lo que se llama a la versión de la clase base), porque esa función no se escribió explícitamente en la nueva clase. Y, por supuesto, el compilador sintetizó automáticamente el destructor.

Con todas estas reglas sobre la reescritura de las funciones que se encargan de la creación de objetos, al principio puede parecer un poco extraño que el operador de conversión automática de tipo sí se herede. Pero no es tan descabellado: si en Game hay piezas suficientes para construir un objeto Other, esas piezas siguen estando en cualquier cosa derivada de Game, y el operador de conversión de tipo sigue siendo válido (aunque, de hecho, puede que quiera redefinirlo).

El operator= sólo se sintetiza para asignar objetos del mismo tipo. Si desea asignar un tipo a otro, siempre deberá escribir ese operator= usted mismo.

Si mira con más detenimiento Game, observará que el constructor de copia y el operador de asignación contienen llamadas explícitas al constructor de copia y al operador de asignación del objeto miembro. Normalmente querrá hacer esto porque, si no, en el caso del constructor de copia se utilizará en su lugar el constructor por defecto del objeto miembro, y en el caso del operador de asignación, ¡no se realizará ninguna asignación de los objetos miembro!

Por último, fíjese en Checkers, que escribe explícitamente el constructor por defecto, el constructor de copia y el operador de asignación. En el caso del constructor por defecto, se llama automáticamente al constructor por defecto de la clase base, y eso es normalmente lo que se desea. Pero, y esto es importante, en cuanto decide escribir su propio constructor de copia y su propio operador de asignación, el compilador asume que sabe lo que está haciendo y no llama automáticamente a las versiones de la clase base, como sí hace en las funciones sintetizadas. Si quiere que se llame a las versiones de la clase base (y normalmente querrá), debe llamarlas explícitamente usted mismo. En el constructor de copia de Checkers, esta llamada aparece en la lista de inicialización del constructor:

Checkers(const Checkers& c) : Game(c) {

En el operador de asignación de Checkers, la llamada a la clase base es la primera línea del cuerpo de la función:

Game::operator=(c);

Estas llamadas deberían formar parte de la forma canónica que utilice siempre que herede de una clase.

14.5.1. Herencia y métodos estáticos

Las funciones miembro estáticas se comportan igual que las funciones miembro no estáticas:

  1. Se heredan en la clase derivada.

  2. Si redefine un miembro estático, se ocultan todas las demás funciones sobrecargadas de la clase base.

  3. Si cambia el prototipo de una función de la clase base, se ocultan todas las versiones de la clase base con ese nombre de función (en realidad, esto es una variante del punto anterior).

Sin embargo, las funciones miembro estáticas no pueden ser virtuales (un tema que se trata a fondo en el Capítulo 15).