Página principal del curso de C++
Otros cursos y tutoriales: comercio electrónico, WAP, Webmaster
Página principal del grupo GeNeura

Curso de programación avanzada en C++

Normas de estilo en C++

Igual que escribir en un idioma tiene una serie de normas de estilao, que, si se cumplen, hacen el texto más comprensible y elegante, igual los lenguajes de programación tienen una serie de normas de estilo, para hacer su código más elegante, comprensible e incluso fácil de depurar. En C++, las normas de estilo parten del diseño correcto de las clases, de la utilización correcta de la herencia y de la encapsulación, y del aprovechamiento de todas las capacidades de C++.

Simultáneamente, veremos como los conceptos fundamentales de la programación orientada a objetos, tales como la herencia y encapsulación se implementan en C++.

Forma canónica

La forma canónica es la forma ortodoxa de declarar una clase en C++. Permite evitar problemas de programación, y permite que una clase declarada de esta forma se pueda usar de la misma manera que cualquier tipo tradicional de C.

En la forma canónica, evidentemente, tienen que entrar el constructor y el destructor.

class miClase {
    public:
    /// Constructor
    miClase() {};

    /// Destructor
    ~miClase() {};
}

Habitualmente se cae en la tentación de no declarar ni el constructor ni el destructor si se utilizan los constructores por defecto; de hecho, los compiladores generan automáticamente el código para ellos; sin embargo, siempre es bueno hacerlo, aunque sea sólo para guardarles sitio, porque más adelante podría interesar cambiarlos por otro tipo de constructor y no sabríamos donde meterlos. Los comentarios también son esenciales, aunque sean redundantes. En este caso, se usan la convencion del doc++, que genera automáticamente la documentación; en caso de que no se incluyeran los comentarios, el usuario podría pensar mirando a la documentación que no existe el constructor, o que se nos ha olvidado incluirlo. Otra razón por la que se debe incluir un constructor vacío es para que se puedan declarar contenedores de las STL que contengan ese tipo; algunos compiladores, tales como el Visual C++, lo exige.

No solamente estos elementos son esenciales. El constructor de copia también lo es. El constructor de copia, aunque no lo parezca, se usa mucho. Por ejemplo, se está llamando al constructor de copia cuando se hace

miClase zape;
miClase zipi=zape;
o cuando se llama a un procedimiento o función por valor, en vez de por referencia (o usando punteros o referencias).
void unMetodo( miClase _zipi ) {
    // hacer lo que sea con _zipi
}

miClase pantuflo;
unMetodo(pantuflo);
El constructor de copia también lo genera el compilador por defecto, pero a veces puede que lo que haga ese constructor generado no sea lo que nosotros pretendemos que haga. Por ejemplo, si el objeto incluye punteros a objetos asignados por el mismo, ese constructor generado copiará los punteros, no a lo que apuntan (copia superficial). Incluyendo el constructor de copia. la forma canónica quedaría así
class miClase {
public:
    /// Constructor
    miClase() {};

    /// Constructor de copia
    miClase( const miClase& _c ) {
        // Inicializar las variables de instancia
    }

    /// Destructor
    ~miClase() {};
}
El objeto que se va a copiar se pasa como una referencia constante. El usar referencias para pasar parámetros a procedimientos y funciones es una buena costumbre: evita tener que enviar muchos bytes a la pila en caso de que se trate de un objeto complejo; además, la referencia tiene que ser constante como una promesa de que no se le va a hacer nada al objeto dentro de esa función.

Todavía la forma canónica está incompleta. ¿Qué ocurre si queremos copiar un objeto ya construido sobre otro ya construido? Hará falta el operador de asignación. Vamos a por él

class miClase {
public:
    /// Constructor
    miClase() {};

    /// Constructor de copia
    miClase( const miClase& _c ) {
        // Inicializar las variables de instancia
    }

    /// Operador de asignación
    const miClase& operator=( const miClase& _c ) {
        // Comprobación si no es uno mismo
        if ( _c !=*this) {
            // Copiar variables de instancia
        }
        return *this;
    }

    /// Destructor
    ~miClase() {};
}

El operador=devuelve una referencia al objeto para que se puedan hacer cosas tales como

miClase zape, zipi, pantuflo;
zape=( zipi=pantuflo );

Pero para que no se pueda hacer (zipi=pantuflo)=zape la referencia se hace constante. Dentro del operador de asignación se tiene que comprobar si se está asignando el propio objeto sobre sí mismo; sobre todo si hay que destruir y asignar memoria dinámica, y luego copiarla.

En algunos casos puede que no se quiera usar alguno de estos elementos, o que no tenga sentido. En tal caso, deberían declararse protected o bien private.

En todo esto, ¿dónde deberían ir las variables de instancia? No en cualquier sitio, no. Aquí es donde entra la encapsulación. Encapsulación de un objeto significa regular el acceso a su interior a través de un interface. Si ponemos tales variables en el interface (es decir, la ponemos en la parte public o protected), la regulación del acceso se va claramente a tomar por saco. Si está en la parte public, porque cualquiera puede cambiar su valor, y si está en la parte protected, porque puede cambiarlo cualquier objeto de la jerarquía de clases.

class miClase {
public:
    /// Constructor
    miClase(): repVariableInstancia1(),repVariableInstancia2(){};

    /// Constructor de copia
    miClase( const miClase& _c )
    : repVariableInstancia1( _c.repVariableInstancia1),
    repVariableInstancia2( _c.repVariableInstancia2) {
        // Inicializar las otras variables de instancia
    }

    /// Operador de asignación
    const miClase& operator=( const miClase& _c ) {
        // Comprobación si no es uno mismo
	if ( _c !=*this) {
	    // Copiar variables de instancia
	}
	return *this;
    }

    /// Destructor
    ~miClase() {};

private:
    tipoVariable repVariableInstancia1;
    otroTipoVariable repVariableInstancia2;
}

Aparte de introducir las variables de instancia, hemos introducido su inicialización en los dos constructores que tenemos a mano, y lo hemos hecho usando la lista de inicialización. Esta forma es mucho más eficiente, y evita la construcción de variables temporales (sobre todo en el constructor de copia). Pero algunas veces puede uno querer cambiar los valores de estas variables. ¡Así no se puede! Bueno, en general, las clases deberían diseñarse de forma que no se necesitara acceder o siquiera saber el nombre de las variables de instancia (el ideal en programación dirigida a objetos es separar totalmente el interfaz de la implementación), pero si alguna clase derivada contumaz quiere acceder a ellas, se deberían declarar funciones dentro del interfaz protegido.

class miClase {
public:
    /// Constructor
    miClase() {};

    /// Constructor de copia
    miClase( const miClase& _c ) {
    // Inicializar las variables de instancia
    }

    /// Operador de asignación
    const miClase& operator=( const miClase& _c ) {
        // Comprobación si no es uno mismo
	if ( _c !=*this) {
	    // Copiar variables de instancia
	}
	return *this;
    }

    /// Destructor
    ~miClase() {};

protected:
    tipoVariable& variableInstancia1() {
        return repVariableInstancia1;
    }

    const tipoVariable& variableInstancia1() const {
    return repVariableInstancia1;
    }

private:
    tipoVariable repVariableInstancia1;
    otroTipoVariable repVariableInstancia2;
}

El lector avezado podrá pensar que estas dos declaraciones son iguales; y efectivamente lo son. Para ser exactos, la función variableInstancia1 está sobrecargada, es decir, que hace dos funciones diferentes dependiendo del contexto en el que sea invocada. ¿Cuál es en este caso el contexto? El contexto viene determinado por el const delante y detrás de la segunda declaración. Ese const indica que, aparte de devolver una referencia constante, es decir, que no se puede asignar un valor a lo que devuelva esa función, no altera el contenido del objeto (el segundo const), con lo cual podemos llamar a esta función desde aquellas otras funciones a las que se les pase un objeto const.

Nunca se deben devolver referencias o punteros no constantes a las variables de instancia, ni siquiera en el interfaz protegido, porque si no la encapsulación se va a hacer puñeta: cualquiera podría asignarles un valor desde fuera, o alterar un puntero. Las referencias o punteros que se devuelvan deben ser constantes, a no ser que efectivamente queramos que le asignen valor desde fuera, algo bastante poco aconsejable.

Ya tenemos una clase completa declarada; hay que fijarse en el orden de la declaración. Pensando en los clientes, sobre todo, la parte pública debe ir la primera, porque ahí es donde mirarán los programadores para ver qué es lo que pueden usar; otra categoría de programadores mirará a la parte protected, si le interesa heredar de la clase; y los listillos, por último, mirarán a la parte privada pensando: "Hum, este nombre de variable no me convence", o bien "Si pusiera esto en la parte pública, me ahorraría muchas cosas". ¡Pues no!

También hemos hecho algo que no tiene porqué sentarle bien a todo el mundo: incluir la definición de varias funciones al lado de la declaración, en el fichero de cabecera. Aunque algunas guías de estilo lo indiquen así, en otros casos, como en casi todos los ejemplos de las STL, aparecen como arriba, y en todo caso es mucho más cómodo.


 
 

El interfaz de una clase

El interfaz de una clase es lo más importante de la misma, casi me atrevería a decir que más importante que su implementación. Cuando se piensa en un programa, se debe pensar en los objetos que hay en el mismo y en sus interfaces; la implementación vendrá luego. Incluso, con un interfaz bien diseñado, la mitad de la implementación está hecha. Por eso es tan importante.

Como ya hemos visto, el interfaz tiene dos partes: la pública y la protegida. La pública es lo que la clase ofrece a los clientes, y la protegida es la que le ofrece a los clientes y los descendientes. La regla que hay que seguir es que el interfaz debe ser mínimo y completo, mínimo porque debe incluir sólo lo necesario, y completo, porque debe incluir todas las operaciones necesarias para que se pueda usar la clase como nosotros pretendemos (y sólo como nosotros pretendemos: no olvidemos que si algo se puede hacer, se hará). Una clase con 2 o 3 funciones aparte de las de la forma canónica es el ideal.Esta regla se aplica a cada uno de los interfaces. En la parte privada, uno puede meter todo lo que le dé la gana, que para eso es privada.

Algo que hay que tener en cuenta aquí es que las clases friend forman también parte del interfaz de una clase, puesto que pueden acceder a sus variables privadas de instancia. Por ello, caben dos opciones. La primera es no usar nunca los friends: meter todo lo necesario directamente dentro de la declaración de la clase. Segunda, usarlos aplicando la norma anterior: mantener el interfaz mínimo, es decir, dejar en las clases amigas alguna parte del interfaz que por sus características deba estar aparte.

Las funciones constantes (const) se deben decidir desde el principio del diseño. Si una función no altera ninguna variable de instancia de un objeto, debe ser declarada constante. Estas serán las únicas funciones que se pueden que invocar desde variables declaradas como punteros o referencias constantes a un objeto. Así, además, controlas claramente el acceso a tu clase: sólo aquellos métodos declarados como no-constantes alterarán al objeto, y tomaremos las medidas pertinentes.

void unaFuncion( const miClase&_param ) {
    tipoVariable unaVar=_param.variableInstancia1(); // Correcto
    tipoVariable otraVar;
    _param.variableInstancia1()=otraVar; // Error de compilación
}

En este caso, en la primera invocación del método variableInstancia1() se está llamando a la versión constante del mismo, y por lo tanto no hay problema; en este caso se deduce que es la versión constante por el contexto: no se está haciendo nada para alterar el contenido del objeto. Sin embargo, en el segundo caso, indicado en rojo, se está llamando al método no-constante, el que devuelve una referencia a repVariableInstancia1, lo cual el compilador deduce una vez más por el contexto: se le está tratando de asignar un valor. Como _param está declarado como una referencia constante, el compilador detecta un error. Los métodos constantes, a su vez, son los únicos que se pueden llamar desde otros métodos constantes.

Ejercicio: diseñar una clase Polinomio que permita acceder a cada uno de sus coeficientes, imprimirlo como polinomio, y hacer alguna operación aritmética tal como suma y resta.

De tal palo, tal astilla

Ya hemos visto como se construye una clase de forma que la encapsulación se respete, y el interfaz esté bien diseñado; pero el otro aspecto que hace de C++ un lenguaje orientado a objetos (o dirigido a objetos) es la herencia: la posibilidad de reusar las clases ya definidas, en todo o en parte, para hacer nuevas clases. Al grupo de todas las clases que descienden de una clase principal se le denomina jerarquía de clases. A la clase madre o padre se le puede llamar también superclase o clase base, mientras que a las clases descencientes se les llama también subclases o clases derivadas. Por tanto, una subclase hereda de una superclase, y ambas juntas (y todas las demás) constituyen una jerarquía de clases.

En general, la herencia en C++ expresa la relación es-un. Una subclase es-una superclase, pero con algunas diferencias. Lo que no queda claro es que es esa relación. En otros lenguajes está un poco más claro. Por ejemplo, en Objective-C hay dos tipos de herencia: herencia de código, y herencia de interfaz (cuando un objeto implementa un interfaz que se ha declarado anteriormente). En Java, e incluso en Visual Basic, se pueden declarar interfaces como objetos de pleno derecho, y la herencia de tales interfaces (habitualmente denominada implementación, va por un lado diferente que la herencia de código. En C++, la herencia es una mezcla de las dos: se hereda a veces interfaces, a veces implementación, a veces ambos. En el siguiente ejemplo:

class miClase {
    // Dejar todo lo demás, y añadir alguna función
    /// Método puesto solo para heredar
    void metodo( void ) const; {
    // Una implementación del método
    }
}
/// Esta subclase es una subclase tonta que no hace nada en realidad
class miSubclase: public miClase {
    /// Método puesto solo para heredar
        void metodo( void ) const; {
    }
}

¿Qué está heredando, en realidad, miSubclase de miClase? Algunas cosas. Está heredando el interfaz, puesto que tiene una función con el mismo nombre que la clase base, pero no la implementación, puesto que está redefiniendo el codigo de un método (apropiadamente llamado metodo) definido en la clase base. Aparte de eso, hereda bien poco: ninguno de los constructores ni destructores. Sí toma de la clase base parte del código: las funciones protegidas y las variables de instancia, aunque no puede acceder a éstas porque están declaradas como privadas.

Sin embargo, esta declaración puede causar problemas a la hora de trabajar con punteros o referencias a objetos de la clase:

miClase& refZipi;
miSubclase& refZape;
miSubclase zipi;
refZipi = zipi;
refZipi.metodo();  // Llama a miClase::metodo
refZape = zipi;
refZape.metodo();  // Llama a miSublase::metodo

Con punteros sucederia algo similar, pero los punteros causarían aún más problemas a la hora de destrur el objeto al que apuntan:

miClase* pZipi = new miSubclase();  // Conversion
automática de puntero de clase derivada a clase base
delete pZipi; // Correcto, pero llamaría a miClase::~miClase()

Para resolver todos estos problemas se inventaron las funciones virtuales. En C++, una funcón declarada como virtual se asocia al código en concreto que va a ejecutar en tiempo de ejecución, no en tiempo de compilación como ocurre con el resto de las funciones. Para ello, cada objeto perteneciente a una clase con funciones virtuales contiene una estructura de datos denominada vptr que contiene punteros a todas las implementaciones de funciones virtuales a lo largo y ancho de la jerarquía de clases. Todo esto, por supuesto, supone un aumento del espacio de almacenamiento para los objetos de la clase; y además supone el gasto de un direccionamiento indirecto adicional cada vez que se llama a un método virtual. Algo a tener en cuenta, si la velocidad y el espacio son esenciales. Que no deberían serlo: para solucionar esos problemas está el hardware.

Usando funciones virtuales, y declarando la subclase también en forma canónica, las dos clases anteriores quedarían así:

class miClase {
// eliminados los comentarios y código para dejarlo todo más claro
public:
    miClase() {};
    miClase( const miClase& _c );
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
protected:
    tipoVariable& variableInstancia1();
    const tipoVariable& variableInstancia1()
const
private:
    tipoVariable repVariableInstancia1;
    otroTipoVariable repVariableInstancia2;
}

/// Esta subclase es una subclase tonta que no hace nada en realidad
class miSubclase: public miClase {
public:
    miSubclase(): miClase(), varInstSubclase() {};
    miSubclase( const miSubclase& _c )
        :miClase( _c ), varInstSubclase( _c.varInstSubclase) {};
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
// resto del codigo
private:
    tipoVar varInstSubclase;

}

En este ejemplo, aparte de declarar el destructor de la clase derivada y el método redefinido como virtual, se han añadido una serie de novedades que indican la forma canónica de inicializar la clase base y las variables de instancia: se deben inicializar por ese orden, y si es posible, usando la lista de inicialización (es decir, tal como se ha hecho arriba: dos puntos detrás del nombre del constructor, y las variables y superclases a inicializar separadas por comas). El usar la lista de inicialización permite que se genere código más eficiente, y permite detectar en tiempo de compilación los errores en el orden de inicialización de las variables.

Además, hemos convertido en virtuales todos los destructores y el metodo. De hecho, todas las funciones virtuales de una jerarquía de clase constituyen el interfaz de la jerarquía, y no el interfaz de una clase en particular dentro de la jerarquía; por ello hay que prestar especial cuidado al diseño de este interfaz, tanta o más que al diseño del interfaz de cada clase. Aunque es casi imposible diseñar una jerarquía de clases que solamente tenga funciones virtuales, se puede intentar, concentrando todas las diferencias entre miembros de la jerarquía en el constructor.

No se debe usar la herencia para expresar relaciones tales como es parte de: en tal caso debería usarse herencia privada; ni tampoco la relación usa-un, en cuyo caso deberá ponerse el objeto usado como variable de instancia. Por ejemplo, un coche es-un vehículo, y un motor es-parte-de un coche; por lo tanto, la clase coche debería heredar de la clase vehílo y debería contener una variable de instance de la clase motor.

Aunque en este cuadro aparezcan las dos clases juntas, lo más conveniente es que vayan separadas cada una en su fichero; y si es posible, el interfaz separado también de la implementación, cada uno en su fichero (incluso aunque se trate de templates). Lo más universal es nombrar los ficheros de cabecera con la extensión .h y los que contienen la implementación con .cpp; la mayoría de los compiladores entienden estas extensiones, aunque para que funcionen correctamente los Makefiles en Unix deberá añadirse una orden. Los ficheros tendrán el mismo nombre de la clase, incluso teniendo en cuenta mayúsculas y minúsculas; los tiempo en que tenia uno que poner nombres de ficheros con 8 caracteres están afortunadamente periclitados, y cualquier sistema operativo decente admite nombres largos (aunque algunos supuestamente decentes, como Windows NT, todavía confundan mayúsculas y minúsculas.

Las funciones virtuales sirven para una cosa más: dado que definen el interfaz de una clase, existen en C++ las funciones virtuales puras que sólo definen un interfaz, sin aportar (habitualmente) ningún código, y obligando además a las clases derivadas a implementar ese código. Un caso clásico de estas funciones o métodos virtuales puros es el método que imprime la clase a un canal de salida:

class miClase {
// eliminados los comentarios y código para dejarlo todo más claro
public:
    miClase() {};
    miClase( const miClase& _c );
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
    virtual void printSelf(
ostream& _o) const = 0; // método virtual puro
protected:
    tipoVariable& variableInstancia1();
    const tipoVariable& variableInstancia1()
const
private:
    tipoVariable repVariableInstancia1;
    otroTipoVariable repVariableInstancia2;
}

/// Esta subclase es una subclase tonta que no hace nada en realidad
class miSubclase: public miClase {
public:
    miSubclase(): miClase(), varInstSubclase() {};
    miSubclase( const miSubclase& _c )
        :miClase( _c ), varInstSubclase( _c.varInstSubclase) {};
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
    virtual void printSelf(
ostream& _o) const; // El código iría en otro lado
private:
    tipoVar varInstSubclase;
}

Los métodos virtuales puros convierten a nuestra archiconocida clase base en una clase base abstracta (CBA). En cierto modo, una CBA declara un interfaz solamente, y deja a las clases derivadas la tarea de definir implementaciones. Este hecho se puede usar en conjunción con los templates, para definir qué tipo de clases pueden instanciar un template determinado. Habitualmente, cuando se escribe un template se debe de especificar claramente cual es el interfaz que debe tener la clase para que sea posible instanciarlo; sin embargo, esto da lugar a descripciones más bien largas y puede ser problemático. En vez de eso, es mejor declarar una CBA que tenga ese interfaz, e indicar simplemente que ese template se puede instanciar solamente con clases que desciendan de esa CBA. Por ejemplo

/** Este comentario sigue la sintaxis del doc++
Esta funcion solo puede instanciarse con aquellos objetos que
tengan los métodos a() y b( foo& ) */
template<class T> void baz<T>( const T& _t){
    _t.a();// y más cosas
    foo miFoo;
    _t.b( miFoo );
}

// Pero es más fácil hacerlo así
class miCBA{
public:
    void a() = 0;
    void b( foo& ) = 0;
}
/** T debe descender de la clase miCBA */
template<class T> void baz<T>( const T& _t){
    _t.a();// y más cosas
    foo miFoo;
    _t.b( miFoo );
}

Más formas de construir

Aunque un constructor propio, con más o menos sofisticación es la forma habitual de construir una clase, hay clases que no tienen porqué saber como construirse. O, en algunos casos, toda la complejidad del constructor de un objeto hay que encapsularla en otro objeto, que sabrá como construirlo. Por ejemplo, en entornos de aplicaciones tales como COM (Common Object Model, de Microsoft), es necesario diseñar factorías para todos los objetos, de forma que los clientes que los usen no tengan que saber como construir cada objeto específico.

En algunos casos también, objetos complejos, compuestos de referencias y punteros a otros objetos, ni siquiera pueden construirse a sí mismos, porque no pueden asignar valores a las referencias que usan; tampoco deberían asignar memoria a sus propios punteros, pues en ese caso no quedaría claro quién tendría que desasignarlo, si el propio objeto o quien ha creado los punteros. En esta sección incluiremos una serie de patrones para construir objetos de diferente forma.

En algunos casos es necesario tener un constructor de copia virtual. Ninguno de los constructores se heredan, ni siquiera el operador de asignación, pero cuando estamos usando referencias o punteros a una clase base, y se quiere copiar el objeto, no se pueden usar los constructores de copia, que copiarín el objeto de la clase base o bien simplemente el puntero. Se puede incluir el constructor de copia virtual de la forma siguiente

class miClase {
// eliminados los comentarios y código para dejarlo todo más claro
public:
    miClase() {};
    miClase( const miClase& _c );
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
    /** Ctor virtual de copia; devuelve un puntero constante para que no
    se pueda usar como operador de asignación. Sin implemtación:
    Un objeto que no se puede instanciar no sepuede copiar
    */
    virtual const miClase* clone() const = 0; 
    virtual void printSelf(
ostream& _o) const = 0; 
protected:
    tipoVariable& variableInstancia1();
    const tipoVariable& variableInstancia1()
const
private:
    tipoVariable repVariableInstancia1;
    otroTipoVariable repVariableInstancia2;
}

class miSubclase: public miClase {
public:
    miSubclase(): miClase(), varInstSubclase() {};
    miSubclase( const miSubclase& _c )
        :miClase( _c ), varInstSubclase( _c.varInstSubclase) {};
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
    virtual const miClase*
clone() const; {
        return new
miSubclase(*this)
    }
    virtual void printSelf(
ostream& _o) const; 
private:
    tipoVar varInstSubclase;
}

La función cloneactuaría como un constructor de copia virtual, produciendo nuevas copias del objeto dondequiera que esté en la jerarquía de clases. Por ejemplo, se pueden hacer cosas así:

class miClase {
// eliminados los comentarios y código para dejarlo todo más claro
public:
    miClase() {};
    miClase( const miClase& _c );
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
    /** Ctor virtual de copia; devuelve un puntero constante para que no
se pueda usar como operador de asignación. Sin implemtación:
    Un objeto que no se puede instanciar no sepuede copiar
    */
    virtual const miClase* clone() const = 0; 
    virtual void printSelf(
ostream& _o) const = 0; 
protected:
    tipoVariable& variableInstancia1();
    const tipoVariable& variableInstancia1()
const
private:
    tipoVariable repVariableInstancia1;
    otroTipoVariable repVariableInstancia2;
}

class miSubclase: public miClase {
public:
    miSubclase(): miClase(), varInstSubclase() {};
    miSubclase( const miSubclase& _c )
        :miClase( _c ), varInstSubclase( _c.varInstSubclase) {};
    const miClase& operator=( const miClase& _c );
    virtual ~miClase() {};
    virtual void metodo( void ) const;
    virtual const miClase*
clone() const; {
        return new
miSubclase(*this)
    }
    virtual void printSelf(
ostream& _o) const; 
private:
    tipoVar varInstSubclase;
}

Lo que permite usar esta función de la forma siguiente:

miClase& pMiClase;
miClase zipi;
miSubclase zape;
pMiClase = zipi;
miClase* nuevoZipi = pMiClase.clone(); // Llama a miClase::clone()
pMiClase = zape;
miClase* nuevoZape = pMiClase.clone(); // Llama a miSubclase::clone()

La última versión del C++ permite incluso declarar la función clone así:

    virtual const miSubclase* clone() const; {
        return new miSubclase(*this)
    }

Es decir, aquellas funciones que devuelvan punteros a la clase base pueden ser sustituidas en la clase derivada por funciones que devuelvan punteros a la misma. Lo mismo ocurre con las referencias.
Página principal del curso de C++
Otros cursos y tutoriales: comercio electrónico, WAP, Webmaster
Tutorial de doc++
Página principal del grupo GeNeura