Definición y Propiedades de Clases en C++

Clase

Las clases son una expansión del concepto de estructuras de datos: como las estructuras de datos, pueden contener miembros de datos, pero también pueden contener funciones como miembros.

Un objeto es una instanciación de una clase. En términos de variables, una clase sería el tipo, y un objeto sería la variable.

Las clases se definen usando ya sea la palabra clave class o la palabra clave struct, con la siguiente sintaxis:

class nombre_clase {
  especificador_acceso_1:
    miembro1;
  especificador_acceso_2:
    miembro2;
  ...
} nombres_objetos;


Donde nombre_clase es un identificador válido para la clase, nombres_objetos es una lista opcional de nombres para objetos de esta clase. El cuerpo de la declaración puede contener miembros, que pueden ser declaraciones de datos o funciones, y opcionalmente especificadores de acceso.

Las clases tienen el mismo formato que las estructuras de datos simples, excepto que también pueden incluir funciones y tener estas nuevas llamadas especificadores de acceso. Un especificador de acceso es uno de los siguientes tres keywords: private, public o protected. Estos especificadores modifican los derechos de acceso para los miembros que les siguen:

los miembros privados de una clase son accesibles solo desde dentro de otros miembros de la misma clase (o desde sus "amigos"). los miembros protegidos son accesibles desde otros miembros de la misma clase (o desde sus "amigos"), pero también desde miembros de sus clases derivadas. Finalmente, los miembros públicos son accesibles desde cualquier lugar donde el objeto sea visible.

Por defecto, todos los miembros de una clase declarada con la palabra clave class tienen acceso privado para todos sus miembros. Por lo tanto, cualquier miembro que se declare antes de cualquier otro especificador de acceso tiene acceso privado automáticamente. Por ejemplo:

class Rectangulo {
    int ancho, alto;
  public:
    void establecer_valores (int,int);
    int area (void);
} rect;


Declara una clase (es decir, un tipo) llamada Rectangulo y un objeto (es decir, una variable) de esta clase, llamado rect. Esta clase contiene cuatro miembros: dos miembros de datos de tipo int (miembro ancho y miembro alto) con acceso privado (porque private es el nivel de acceso por defecto) y dos funciones miembro con acceso público: las funciones establecer_valores y area, de las cuales por ahora solo hemos incluido su declaración, pero no su definición.

Nota la diferencia entre el nombre de la clase y el nombre del objeto: En el ejemplo anterior, Rectangulo era el nombre de la clase (es decir, el tipo), mientras que rect era un objeto de tipo Rectangulo. Es la misma relación que tienen int y a en la siguiente declaración:

int a;


donde int es el nombre del tipo (la clase) y a es el nombre de la variable (el objeto).

Después de las declaraciones de Rectangulo y rect, cualquiera de los miembros públicos del objeto rect puede ser accedido como si fueran funciones o variables normales, simplemente insertando un punto (.) entre el nombre del objeto y el nombre del miembro. Esto sigue la misma sintaxis que para acceder a los miembros de estructuras de datos simples. Por ejemplo:

rect.establecer_valores (3,4);
miarea = rect.area(); 


Los únicos miembros de rect que no pueden ser accedidos desde fuera de la clase son ancho y alto, ya que tienen acceso privado y solo pueden ser referenciados desde dentro de otros miembros de esa misma clase.

Aquí está el ejemplo completo de la clase Rectangulo:

// ejemplo de clases
#include <iostream>
using namespace std;

class Rectangulo {
    int ancho, alto;
  public:
    void establecer_valores (int,int);
    int area() {return ancho*alto;}
};

void Rectangulo::establecer_valores (int x, int y) {
  ancho = x;
  alto = y;
}

int main () {
  Rectangulo rect;
  rect.establecer_valores (3,4);
  cout << "area: " << rect.area();
  return 0;
}
//area: 12


Este ejemplo reintroduce el operador de ámbito (::, dos puntos), visto en capítulos anteriores en relación con los namespaces. Aquí se usa en la definición de la función establecer_valores para definir un miembro de una clase fuera de la clase misma.

Nota que la definición de la función miembro area se ha incluido directamente dentro de la definición de la clase Rectangulo debido a su extrema simplicidad. Por el contrario, establecer_valores solo se declara con su prototipo dentro de la clase, pero su definición está fuera de ella. En esta definición externa, el operador de ámbito (::) se usa para especificar que la función que se está definiendo es un miembro de la clase Rectangulo y no una función regular no miembro.

El operador de ámbito (::) especifica la clase a la que pertenece el miembro que se está definiendo, otorgando exactamente las mismas propiedades de ámbito como si esta definición de función se incluyera directamente dentro de la definición de la clase. Por ejemplo, la función establecer_valores en el ejemplo anterior tiene acceso a las variables ancho y alto, que son miembros privados de la clase Rectangulo, y por lo tanto solo accesibles desde otros miembros de la clase, como esta.

La única diferencia entre definir una función miembro completamente dentro de la definición de la clase o solo incluir su declaración en la función y definirla más tarde fuera de la clase, es que en el primer caso la función es automáticamente considerada una función miembro inline por el compilador, mientras que en el segundo es una función miembro de clase normal (no inline). Esto no causa diferencias en el comportamiento, sino solo en posibles optimizaciones del compilador.

Los miembros ancho y alto tienen acceso privado (recuerda que si no se especifica nada, todos los miembros de una clase definida con la palabra clave class tienen acceso private). Al declararlos private, no se permite el acceso desde fuera de la clase. Esto tiene sentido, ya que ya hemos definido una función miembro para establecer valores para esos miembros dentro del objeto: la función miembro establecer_valores. Por lo tanto, el resto del programa no necesita tener acceso directo a ellos. Quizás en un ejemplo tan simple como este, es difícil ver cómo restringir el acceso a estas variables puede ser útil, pero en proyectos más grandes puede ser muy importante que los valores no puedan ser modificados de inesperada manera (inesperada desde el punto de vista del objeto).

La propiedad más importante de una clase es que es un tipo, y como tal, podemos declarar múltiples objetos de ella. Por ejemplo, continuando con el ejemplo anterior de la clase Rectangulo, podríamos haber declarado el objeto rectb además del objeto rect:

// ejemplo: una clase, dos objetos
#include <iostream>
using namespace std;

class Rectangulo {
    int ancho, alto;
  public:
    void establecer_valores (int,int);
    int area () {return ancho*alto;}
};

void Rectangulo::establecer_valores (int x, int y) {
  ancho = x;
  alto = y;
}

int main () {
  Rectangulo rect, rectb;
  rect.establecer_valores (3,4);
  rectb.establecer_valores (5,6);
  cout << "area de rect: " << rect.area() << endl;
  cout << "area de rectb: " << rectb.area() << endl;
  return 0;
}
//area de rect: 12
//area de rectb: 30  


En este caso particular, la clase (tipo de los objetos) es Rectangulo, de la cual hay dos instancias (es decir, objetos): rect y rectb. Cada uno tiene sus propias variables miembro y funciones miembro.

Nota que la llamada a rect.area() no da el mismo resultado que la llamada a rectb.area(). Esto es porque cada objeto de la clase Rectangulo tiene sus propias variables ancho y alto, ya que -de alguna manera- también tienen sus propias funciones miembro establecer_valor y area que operan sobre las variables miembro del propio objeto.

Las clases permiten programar usando paradigmas orientados a objetos: Los datos y las funciones son ambos miembros del objeto, reduciendo la necesidad de pasar y llevar manejadores u otras variables de estado como argumentos a las funciones, porque son parte del objeto cuyo miembro se está llamando. Nota que no se pasaron argumentos en las llamadas a rect.area o rectb.area. Esas funciones miembro usaron directamente los datos miembro de sus respectivos objetos rect y rectb.

Constructores

¿Qué pasaría en el ejemplo anterior si llamáramos a la función miembro area antes de haber llamado a establecer_valores? Un resultado indeterminado, ya que los miembros ancho y alto nunca habían sido asignados un valor.

Para evitar eso, una clase puede incluir una función especial llamada su constructor, que es llamado automáticamente siempre que se crea un nuevo objeto de esta clase, permitiendo que la clase inicialice variables miembro o asigne almacenamiento.

Esta función constructor se declara como una función miembro regular, pero con un nombre que coincide con el nombre de la clase y sin ningún tipo de retorno; ni siquiera void.

La clase Rectangulo anterior puede mejorarse fácilmente implementando un constructor:

// ejemplo: constructor de clase
#include <iostream>
using namespace std;

class Rectangulo {
    int ancho, alto;
  public:
    Rectangulo (int,int);
    int area () {return (ancho*alto);}
};

Rectangulo::Rectangulo (int a, int b) {
  ancho = a;
  alto = b;
}

int main () {
  Rectangulo rect (3,4);
  Rectangulo rectb (5,6);
  cout << "area de rect: " << rect.area() << endl;
  cout << "area de rectb: " << rectb.area() << endl;
  return 0;
}
//area de rect: 12
//area de rectb: 30 


Los resultados de este ejemplo son idénticos a los del ejemplo anterior. Pero ahora, la clase Rectangulo no tiene la función miembro establecer_valores, y en su lugar tiene un constructor que realiza una acción similar: inicializa los valores de ancho y alto con los argumentos pasados a él.

Nota cómo estos argumentos se pasan al constructor en el momento en que se crean los objetos de esta clase:

Rectangulo rect (3,4);
Rectangulo rectb (5,6);


Los constructores no pueden ser llamados explícitamente como si fueran funciones miembro regulares. Solo se ejecutan una vez, cuando se crea un nuevo objeto de esa clase.

Nota que ni el prototipo del constructor (dentro de la clase) ni la posterior definición del constructor, tienen valores de retorno; ni siquiera void: Los constructores nunca retornan valores, simplemente inicializan el objeto.

Sobrecarga de constructores

Como cualquier otra función, un constructor también puede ser sobrecargado con diferentes versiones tomando diferentes parámetros: con un número diferente de parámetros y/o parámetros de diferetnes tipos. El compilador automáticamente llamará al que sus parámetros coinciden con los argumentos:

// sobrecarga de constructores de clase
#include <iostream>
using namespace std;

class Rectangulo {
    int ancho, alto;
  public:
    Rectangulo ();
    Rectangulo (int,int);
    int area (void) {return (ancho*alto);}
};

Rectangulo::Rectangulo () {
  ancho = 5;
  alto = 5;
}

Rectangulo::Rectangulo (int a, int b) {
  ancho = a;
  alto = b;
}

int main () {
  Rectangulo rect (3,4);
  Rectangulo rectb;
  cout << "area de rect: " << rect.area() << endl;
  cout << "area de rectb: " << rectb.area() << endl;
  return 0;
}
//area de rect: 12
//area de rectb: 25  


En el ejemplo anterior, se construyen dos objetos de la clase Rectangulo: rect y rectb. rect se construye con dos argumentos, como en el ejemplo anterior.

Pero este ejemplo también introduce un tipo especial de constructor: el constructor por defecto. El constructor por defecto es el constructor que no toma parámetros, y es especial porque se llama cuando se declara un objeto pero no se inicializa con ningún argumento. En el ejemplo anterior, el constructor por defecto se llama para rectb. Nota cómo rectb ni siquiera se construye con un conjunto vacío de paréntesis - de hecho, los paréntesis vacíos no pueden usarse para llamar al constructor por defecto:

Rectangulo rectb;   // ok, constructor por defecto llamado
Rectangulo rectc(); // ups, constructor por defecto NO llamado 


Esto se porque el conjunto vacío de paréntesis haría de rectc una declaración de función en lugar de una declaración de objeto: Sería una función que no toma argumentos y retorna un valor de tipo Rectangulo.

Inicialización uniforme

La forma de llamar a los constructores encerrando sus argumentos entre paréntesis, como se muestra arriba, se conoce como forma funcional. Pero los constructores también pueden ser llamados con otras sintaxis:

Primero, los constructores con un solo parámetro pueden ser llamados usando la sintaxis de inicialización de variable (un signo igual seguido del argumento):

class_name object_name = initialization_value;


Más recientemente, C++ introdujo la posibilidad de que los constructores puedan ser llamados usando inicialización uniforme, que es esencialmente la misma que la forma funcional, pero usando llaves ({}) en lugar de paréntesis (()):

class_name object_name { value, value, value, ... }


Opcionalmente, esta última sintaxis puede incluir un signo igual antes de las llaves.

Aquí hay un ejemplo con cuatro formas de construir objetos de una clase cuyo constructor toma un solo parámetro:

#include <iostream>
using namespace std;

class Circulo {
    double radio;
  public:
    Circulo(double r) { radio = r; }
    double circunferencia() {return 2*radio*3.14159265;}
};

int main () {
  Circulo foo (10.0);   // forma funcional
  Circulo bar = 20.0;   // inicialización por asignación
  Circulo baz {30.0};   // inicialización uniforme
  Circulo qux = {40.0}; // estilo POD

  cout << "circunferencia de foo: " << foo.circunferencia() << '\n';
  return 0;
}
//circunferencia de foo: 62.8319


Una ventaja de la inicialización uniforme sobre la forma funcional es que, a diferencia de los paréntesis, las llaves no pueden confundirse con declaraciones de función, y por lo tanto pueden usarse para llamar explícitamente a constructores por defecto:

Rectangulo rectb;   // constructor por defecto llamado
Rectangulo rectc(); // declaración de función (constructor por defecto NO llamado)
Rectangulo rectd{}; // constructor por defecto llamado 


La elección de la sintaxis para llamar a constructores es en gran medida una cuestión de estilo. La mayoría del código existente actualmente usa la forma funcional, y algunas guías de estilo más recientes sugieren elegir la inicialización uniforme sobre las otras, aunque también tiene sus posibles riesgos por su preferencia por initializer_list como su tipo.

Inicialización de miembros en constructores

Cuando un constructor se usa para inicializar otros miembros, estos otros miembros pueden ser inicializados directamente, sin recurrir a declaraciones en su cuerpo. Esto se hace insertando, antes del cuerpo del constructor, un dos puntos (:) y una lista de inicializaciones para miembros de la clase. Por ejemplo, considere una clase con la siguiente declaración:

class Rectangulo {
    int ancho,alto;
  public:
    Rectangulo(int,int);
    int area() {return ancho*alto;}
};


El constructor para esta clase podría definirse, como es habitual, como:

Rectangulo::Rectangulo (int x, int y) { ancho=x; alto=y; }


Pero también podría definirse usando inicialización de miembros como:

Rectangulo::Rectangulo (int x, int y) : ancho(x) { alto=y; }


O incluso:

Rectangulo::Rectangulo (int x, int y) : ancho(x), alto(y) { }


Nota cómo en este último caso, el constructor no hace nada más que inicializar sus miembros, por lo tanto tiene un cuerpo de función vacío.

Para miembros de tipos fundamentales, no hay diferencia en cualquiera de las formas anteriores en que el constructor se define, porque no son inicializados por defecto, pero para objetos miembro (aquellos cuyo tipo es una clase), si no se inicializan después de los dos puntos, son construidos por defecto.

Construir por defecto todos los miembros de una clase puede o no ser conveniente: en algunos casos, esto es un desperdicio (cuando el miembro luego se reinicializa de otra manera en el constructor), pero en otros casos, la construcción por defecto ni siquiera es posible (cuando la clase no tiene un constructor por defecto). En estos casos, los miembros deben ser inicializados en la lista de inicialización de miembros. Por ejemplo:

// inicialización de miembros
#include <iostream>
using namespace std;

class Circulo {
    double radio;
  public:
    Circulo(double r) : radio(r) { }
    double area() {return radio*radio*3.14159265;}
};

class Cilindro {
    Circulo base;
    double altura;
  public:
    Cilindro(double r, double h) : base (r), altura(h) {}
    double volumen() {return base.area() * altura;}
};

int main () {
  Cilindro foo (10,20);

  cout << "volumen de foo: " << foo.volumen() << '\n';
  return 0;
}
//volumen de foo: 6283.19


En este ejemplo, la clase Cilindro tiene un objeto miembro cuyo tipo es otra clase (el tipo de base es Circulo). Porque los objetos de la clase Circulo solo pueden ser construidos con un parámetro, el constructor de Cilindro necesita llamar al constructor de base, y la única forma de hacerlo es en la lista de inicialización de miembros.

Estas inicializaciones también pueden usar la sintaxis de inicialización uniforme, usando llaves {} en lugar de paréntesis ():

Cilindro::Cilindro (double r, double h) : base{r}, altura{h} { }


Punteros a clases

Los objetos también pueden ser apuntados por punteros: Una vez declarada, una clase se convierte en un tipo válido, por lo que puede ser usado como el tipo apuntado por un puntero. Por ejemplo:

Rectangulo * prect;


es un puntero a un objeto de la clase Rectangulo.

Similarmente que con estructuras de datos simples, los miembros de un objeto pueden ser accedidos directamente desde un puntero usando el operador de flecha (->). Aquí hay un ejemplo con algunas combinaciones posibles:

// ejemplo de puntero a clases
#include <iostream>
using namespace std;

class Rectangulo {
  int ancho, alto;
public:
  Rectangulo(int x, int y) : ancho(x), alto(y) {}
  int area(void) { return ancho * alto; }
};


int main() {
  Rectangulo obj (3, 4);
  Rectangulo * foo, * bar, * baz;
  foo = &obj;
  bar = new Rectangulo (5, 6);
  baz = new Rectangulo[2] { {2,5}, {3,6} };
  cout << "area de obj: " << obj.area() << '\n';
  cout << "area de *foo: " << foo->area() << '\n';
  cout << "area de *bar: " << bar->area() << '\n';
  cout << "area de baz[0]:" << baz[0].area() << '\n';
  cout << "area de baz[1]:" << baz[1].area() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}
//area de obj: 12
//area de *foo: 12
//area de *bar: 30
//area de baz[0]:10
//area de baz[1]:18


Este ejemplo hace uso de varios operadores para operar sobre objetos y punteros (operadores *, &, ., ->, []). Pueden interpretarse como:

expresión puede leerse como
*x apuntado por x
&x dirección de x
x.y miembro y del objeto x
x->y miembro y del objeto apuntado por x
(*x).y miembro y del objeto apuntado por x (equivalente al anterior)
x[0] primer objeto apuntado por x
x[1] segundo objeto apuntado por x
x[n] (n+1)-ésimo objeto apuntado por x

La mayoría de estas expresiones han sido introducidas en capítulos anteriores. En particular, el capítulo sobre arreglos introdujo el operador de desplazamiento ([]) y el capítulo sobre estructuras de datos simples introdujo el operador de flecha (->).

Clases definidas con struct y union

Las clases pueden definirse no solo con la palabra clave class, sino también con las palabras clave struct y union.

La palabra clave struct, generalmente usada para declarar estructuras de datos simples, también puede usarse para declarar clases que tienen funciones miembro, con la misma sintaxis que con la palabra clave class. La única diferencia entre ambas es que los miembros de las clases declaradas con la palabra clave struct tienen acceso público por defecto, mientras que los miembros de las clases declaradas con la palabra clave class tienen acceso privado por defecto. Para todos los demás propósitos, ambas palabras clave son equivalentes en este contexto.

Por el contrario, el concepto de union es diferente del de las clases declaradas con struct y class, ya que las uniones solo almacenan un miembro de datos a la vez, pero nevertheless también son clases y por lo tanto pueden también contener funciones miembro. El acceso por defecto en clases union es público.

Sobrecarga de operadores

Las clases, esencialmente, definen nuevos tipos a ser usados en código C++. Y los tipos en C++ no solo interactúan con el código mediante construcciones y asignaciones. También interactúan mediante operadores. Por ejemplo, tome la siguiente operación en tipos fundamentales:

int a, b, c;
a = b + c;


Aquí, diferentes variables de un tipo fundamental (int) se les aplica el operador de suma, y luego el operador de asignación. Para un tipo aritmético fundamental, el significado de tales operaciones es generalmente obvio y sin ambigüedad, pero puede no serlo para ciertos tipos de clase. Por ejemplo:

struct mioclase {
  string producto;
  float precio;
} a, b, c;
a = b + c;


Aquí, no es obvio qué resultado tiene la operación de suma en b y c. De hecho, este código solo causaría un error de compilación, ya que el tipo mioclase no tiene un comportamiento definido para sumas. Sin embargo, C++ permite que la mayoría de los operadores sean sobrecargados para que su comportamiento pueda ser definido para casi cualquier tipo, incluyendo clases. Aquí hay una lista de todos los operadores que pueden ser sobrecargados:

Operadores sobrecargables
3863142427

Los operadores son sobrecargados mediante funciones de operador, que son funciones regulares con nombres especiales: su nombre comienza con la palabra clave operator seguida del signo del operador que se está sobrecargando. La sintaxis es:

type operator sign (parametros) { /*... cuerpo ...*/ }


Por ejemplo, los vectores cartesianos son conjuntos de dos coordenadas: x y y. La operación de suma de dos vectores cartesianos se define como la suma de ambas coordenadas x juntas, y ambas coordenadas y juntas. Por ejemplo, sumar los vectores cartesianos (3,1) y (1,2) resultaría en (3+1,1+2) = (4,3). Esto podría implementarse en C++ con el siguiente código:

// ejemplo de sobrecarga de operadores
#include <iostream>
using namespace std;

class CVector {
  public:
    int x,y;
    CVector () {};
    CVector (int a,int b) : x(a), y(b) {}
    CVector operator + (const CVector&);
};

CVector CVector::operator+ (const CVector& param) {
  CVector temp;
  temp.x = x + param.x;
  temp.y = y + param.y;
  return temp;
}

int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector resultado;
  resultado = foo + bar;
  cout << resultado.x << ',' << resultado.y << '\n';
  return 0;
}
//4,3


Si se confunde con tantas apariciones de CVector, considere que algunas de ellas se refieren al nombre de la clase (es decir, el tipo) CVector y otras son funciones con ese nombre (es decir, constructores, que deben tener el mismo nombre que la clase). Por ejemplo:

CVector (int, int) : x(a), y(b) {}  // nombre de función CVector (constructor)
CVector operator+ (const CVector&); // función que retorna un CVector  


La función operator+ de la clase CVector sobrecarga el operador de suma (+) para ese tipo. Una vez declarada, esta función puede ser llamada implícitamente usando el operador, o explícitamente usando su nombre funcional:

c = a + b;
c = a.operator+ (b);


Ambas expresiones son equivalentes.

Las sobrecargas de operadores son simplemente funciones regulares que pueden tener cualquier comportamiento; de hecho no hay requisito de que la operación realizada por esa sobrecarga tenga relación con el significado matemático o habitual del operador, aunque es fuertemente recomendado. Por ejemplo, una clase que sobrecarga operator+ para realmente restar o que sobrecarga operator== para llenar el objeto con ceros, es perfectamente válida, aunque usar tal clase podría ser un desafío.

El parámetro esperado para una función miembro de sobrecarga para operaciones como operator+ es naturalmente el operando al lado derecho del operador. Esto es común a todos los operadores binarios (aquellos con un operando a su izquierda y un operando a su derecha). Pero los operadores pueden venir en diversas formas. Aquí tiene una tabla con un resumen de los parámetros necesarios para cada uno de los diferentes operadores que pueden ser sobrecargados (por favor, reemplace @ por el operador en cada caso):

Expresión Operador Función miembro Función no miembro
@a + - * & ! ~ ++ -- A::operator@() operator@(A)
a@ ++ -- A::operator@(int) operator@(A,int)
a@b + - * / % ^ & | < > == != <= >= << >> && || , A::operator@(B) operator@(A,B)
a@b = += -= *= /= %= ^= &= |= <<= >>= [] A::operator@(B) -
a(b,c...) () A::operator()(B,C...) -
a->b -> A::operator->() -
(TIPO) a TIPO A::operator TIPO() -

Donde a es un objeto de la clase A, b es un objeto de la clase B y c es un objeto de la clase C. TIPO es solo cualquier tipo (que la sobrecarga de operadores convierte al tipo TIPO).

Nota que algunos operadores pueden ser sobrecargados en dos formas: ya como función miembro o como función no miembro: El primer caso ha sido usado en el ejemplo anterior para operator+. Pero algunos operadores también pueden ser sobrecargados como funciones no miembro; En este caso, la función de operador toma un objeto de la clase apropiada como primer argumento.

Por ejemplo:

// sobrecargas de operador no miembro
#include <iostream>
using namespace std;

class CVector {
  public:
    int x,y;
    CVector () {}
    CVector (int a, int b) : x(a), y(b) {}
};


CVector operator+ (const CVector& lhs, const CVector& rhs) {
  CVector temp;
  temp.x = lhs.x + rhs.x;
  temp.y = lhs.y + rhs.y;
  return temp;
}

int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector resultado;
  resultado = foo + bar;
  cout << resultado.x << ',' << resultado.y << '\n';
  return 0;
}
//4,3


La palabra clave this

La palabra clave this representa un puntero al objeto cuya función miembro se está ejecutando. Se usa dentro de una función miembro de una clase para referirse al objeto mismo.

Uno de sus usos puede ser para verificar si un parámetro pasado a una función miembro es el objeto mismo. Por ejemplo:

// ejemplo de this
#include <iostream>
using namespace std;

class Dummy {
  public:
    bool esyo (Dummy& param);
};

bool Dummy::esyo (Dummy& param)
{
  if (&param == this) return true;
  else return false;
}

int main () {
  Dummy a;
  Dummy* b = &a;
  if ( b->esyo(a) )
    cout << "si, &a es b\n";
  return 0;
}
//si, &a es b


También se usa frecuentemente en funciones miembro operator= que retornan objetos por referencia. Continuando con los ejemplos de vector cartesiano vistos antes, su función operator= podría haberse definido como:

CVector& CVector::operator= (const CVector& param)
{
  x=param.x;
  y=param.y;
  return *this;
}


De hecho, esta función es muy similar al código que el compilador genera implícitamente para esta clase para operator=.

Miembros estáticos

Una clase puede contener miembros estáticos, ya sean datos o funciones.

Un miembro de datos estático de una clase también es conocido como una "variable de clase", porque hay solo una variable común para todos los objetos de esa misma clase, compartiendo el mismo valor: es decir, su valor no es diferente de un objeto de esta clase a otro.

Por ejemplo, puede ser usado para una variable dentro de una clase que puede contar un contador con el número de objetos de esa clase que están actualmente asignados, como en el siguiente ejemplo:

// miembros estáticos en clases
#include <iostream>
using namespace std;

class Dummy {
  public:
    static int n;
    Dummy () { n++; };
};

int Dummy::n=0;

int main () {
  Dummy a;
  Dummy b[5];
  cout << a.n << '\n';
  Dummy * c = new Dummy;
  cout << Dummy::n << '\n';
  delete c;
  return 0;
}
//6
//7


De hecho, los miembros estáticos tienen las mismas propiedades que las variables no miembro pero disfrutan del ámbito de la clase. Por esa razón, y para evitar que sean declarados varias veces, no pueden ser inicializados directamente en la clase, pero necesitan ser inicializados en algún lugar fuera de ella. Como en el ejemplo anterior:

int Dummy::n=0;


Porque es un valor de variable común para todos los objetos de la misma clase, puede ser referenciado como un miembro de cualquier objeto de esa clase o incluso directamente por el nombre de la clase (por supuesto esto solo es válido para miembros estáticos):

cout << a.n;
cout << Dummy::n;


Estas dos llamadas anteriores se refieren a la misma variable: la variable estática n dentro de la clase Dummy compartida por todos los objetos de esta clase.

Nuevamente, es como una variable no miembro, pero con un nombre que requiere ser accedido como un miembro de una clase (o un objeto).

Las clases también pueden tener funciones miembro estáticas. Estas representan lo mismo: miembros de una clase que son comunes a todos los objetos de esa clase, actuando exactamente como funciones no miembro pero siendo accedidas como miembros de la clase. Porque son como funciones no miembro, no pueden acceder a miembros no estáticos de la clase (ni variables miembro ni funciones miembro). Tampoco pueden usar la palabra clave this.

Funciones miembro const

Cuando un objeto de una clase es calificado como un objeto const:

const MiClase miobjeto;


El acceso a sus miembros de datos desde fuera de la clase está restringido a solo lectura, como si todos sus miembros de datos fueran const para aquellos que los acceden desde fuera de la clase. Nota sin embargo, que el constructor todavía se llama y se le permite inicializar y modificar estos miembros de datos:

// constructor en objeto const
#include <iostream>
using namespace std;

class MiClase {
  public:
    int x;
    MiClase(int val) : x(val) {}
    int obtener() {return x;}
};

int main() {
  const MiClase foo(10);
// foo.x = 20;            // no válido: x no puede ser modificado
  cout << foo.x << '\n';  // ok: miembro de datos x puede ser leído
  return 0;
}
//10


Las funciones miembro de un objeto const solo pueden ser llamadas si ellas mismas están especificadas como miembros const; en el ejemplo anterior, el miembro obtener (que no está especificado como const) no puede ser llamado desde foo. Para especificar que un miembro es un miembro const, la palabra clave const debe seguir el prototipo de la función, después del paréntesis de cierre para sus parámetros:

int obtener() const {return x;}


Nota que const puede usarse para calificar el tipo retornado por una función miembro. Este const no es el mismo que el que especifica un miembro como const. Ambos son independientes y están ubicados en diferentes lugares en el prototipo de la función:

int obtener() const {return x;}        // función miembro const
const int& obtener() {return x;}       // función miembro retornando un const&
const int& obtener() const {return x;} // función miembro const retornando un const& 


Las funciones miembro especificadas como const no pueden modificar miembros de datos no estáticos ni llamar a otras funciones miembro no const. En esencia, los miembros const no deben modificar el estado de un objeto.

Los objetos const están limitados a acceder solo a funciones miembro marcadas como const, pero los objetos no const no están restringidos y por lo tanto pueden acceder tanto a funciones miembro const como no const.

Puede pensar que de todos modos rara vez va a declarar objetos const, y por lo tanto marcar todos los miembros que no modifican el objeto como const no vale la pena, pero los objetos const son en realidad muy comunes. La mayoría de las funciones que toman clases como parámetros realmente las toman por referencia const, y por lo tanto, estas funciones solo pueden acceder a sus miembros const:

// objetos const
#include <iostream>
using namespace std;

class MiClase {
    int x;
  public:
    MiClase(int val) : x(val) {}
    const int& obtener() const {return x;}
};

void imprimir (const MiClase& arg) {
  cout << arg.obtener() << '\n';
}

int main() {
  MiClase foo (10);
  imprimir(foo);

  return 0;
}
//10


Si en este ejemplo, obtener no estuviera especificado como miembro const, la llamada a arg.obtener() en la función imprimir no sería posible, porque los objetos const solo tienen acceso a funciones miembro const.

Las funciones miembro pueden ser sobrecargadas por su constancia: es decir, una clase puede tener dos funciones miembro con idénticas firmas excepto que una es const y la otra no: en este caso, la versión const se llama solo cuando el objeto es const, y la versión no const se llama cuando el objeto es no const.

// sobrecarga de miembros por constancia
#include <iostream>
using namespace std;

class MiClase {
    int x;
  public:
    MiClase(int val) : x(val) {}
    const int& obtener() const {return x;}
    int& obtener() {return x;}
};

int main() {
  MiClase foo (10);
  const MiClase bar (20);
  foo.obtener() = 15;         // ok: obtener() retorna int&
// bar.obtener() = 25;        // no válido: obtener() retorna const int&
  cout << foo.obtener() << '\n';
  cout << bar.obtener() << '\n';

  return 0;
}
//15
//20


Plantillas de clase

Al igual que podemos crear plantillas de función, también podemos crear plantillas de clase, permitiendo que las clases tengan miembros que usen parámetros de plantilla como tipos. Por ejemplo:

template <class T>
class miPar {
    T valores [2];
  public:
    miPar (T primero, T segundo)
    {
      valores[0]=primero; valores[1]=segundo;
    }
};


La clase que acabamos de definir sirve para almacenar dos elementos de cualquier tipo válido. Por ejemplo, si quisiéramos declarar un objeto de esta clase para almacenar dos valores enteros de tipo int con los valores 115 y 36 escribiríamos:

miPar<int> miobjeto (115, 36);


Esta misma clase también podría usarse para crear un objeto para almacenar cualquier otro tipo, como:

miPar<double> misFlotantes (3.0, 2.18); 


El constructor es la única función miembro en la plantilla de clase anterior y ha sido definida inline dentro de la definición de la clase misma. En caso de que una función miembro se defina fuera de la definición de la plantilla de clase, debe precederse con el prefijo template <...>:

// plantillas de clase
#include <iostream>
using namespace std;

template <class T>
class miPar {
    T a, b;
  public:
    miPar (T primero, T segundo)
      {a=primero; b=segundo;}
    T obtenermax ();
};

template <class T>
T miPar<T>::obtenermax ()
{
  T retval;
  retval = a>b? a : b;
  return retval;
}

int main () {
  miPar <int> miobjeto (100, 75);
  cout << miobjeto.obtenermax();
  return 0;
}
//100


Nota la sintaxis de la definición de la función miembro obtenermax:

template <class T>
T miPar<T>::obtenermax () 


¿Confundido con tantas T's? Hay tres T's en esta declaración: La primera es el parámetro de la plantilla. La segunda T se refiere al tipo retornado por la función. Y la tercera T (la que está entre corchetes angulares) también es un requisito: Especifica que el parámetro de plantilla de esta función también es el parámetro de plantilla de la clase.

Especialización de plantillas

Es posible definir una implementación diferente para una plantilla cuando se pasa un tipo específico como argumento de plantilla. Esto se llama una especialización de plantilla.

Por ejemplo, supongamos que tenemos una clase muy simple llamada miContenedor que puede almacenar un elemento de cualquier tipo y que tiene solo una función miembro llamada aumentar, que aumenta su valor. Pero我们发现 que cuando almacena un elemento de tipo char sería más conveniente tener una implementación completamente diferente con una función miembro mayusculas, así que decidimos declarar una especialización de plantilla de clase para ese tipo:

// especialización de plantilla
#include <iostream>
using namespace std;

// plantilla de clase:
template <class T>
class miContenedor {
    T elemento;
  public:
    miContenedor (T arg) {elemento=arg;}
    T aumentar () {return ++element;}
};

// especialización de plantilla de clase:
template <>
class miContenedor <char> {
    char elemento;
  public:
    miContenedor (char arg) {elemento=arg;}
    char mayusculas ()
    {
      if ((elemento>='a')&&(elemento<='z'))
      elemento+='A'-'a';
      return elemento;
    }
};

int main () {
  miContenedor<int> miint (7);
  miContenedor<char> michar ('j');
  cout << miint.aumentar() << endl;
  cout << michar.mayusculas() << endl;
  return 0;
}
//8
//J


Esta es la sintaxis usada para la especialización de plantilla de clase:

template <> class miContenedor <char> { ... };


Primero de todo, nota que precedemos el nombre de la clase con template<> , incluyendo una lista de parámetros vacía. Esto es porque todos los tipos son conocidos y no se requieren argumentos de plantilla para esta especialización, pero todavía, es la especialización de una plantilla de clase, y por lo tanto requiere ser notada como tal.

Pero más importante que este prefijo, es el parámetro de especialización después del nombre de la plantilla de clase. Este parámetro de especialización en sí identifica el tipo para el cual la plantilla de clase está siendo especializada (char). Nota las diferencias entre la plantilla de clase genérica y la especialización:

template <class T> class miContenedor { ... };
template <> class miContenedor <char> { ... };


La primera línea es la plantilla genérica, y la segunda es la especialización.

Cuando declaramos especializaciones para una plantilla de clase, también debemos definir todos sus miembros, incluso aquellos idénticos a la plantilla de clase genérica, porque no hay "herencia" de miembros de la plantilla genérica a la especialización.

Miembros especiales

[NOTA: Este capítulo requiere un entendimiento adecuado de memoria asignada dinámicamente]

Las funciones miembro especiales son funciones miembro que se definen implícitamente como miembros de clases bajo ciertas circunstancias. Hay seis:

Función miembro forma típica para la clase C:
Constructor por defecto C::C();
Destructor C::~C();
Constructor de copia C::C (const C&);
Asignación por copia C& operator= (const C&);
Constructor de movimiento C::C (C&&);
Asignación por movimiento C& operator= (C&&);

Examinemos cada una de estas:

Constructor por defecto

El constructor por defecto es el constructor llamado cuando se declaran objetos de una clase, pero no se inicializan con ningún argumento.

Si una definición de clase no tiene constructores, el compilador asume que la clase tiene un constructor por defecto implícitamente definido. Por lo tanto, después de declarar una clase como esta:

class Ejemplo {
  public:
    int total;
    void acumular (int x) { total += x; }
};


El compilador asume que Ejemplo tiene un constructor por defecto. Por lo tanto, objetos de esta clase pueden ser construidos simplemente declarándolos sin ningún argumento:

Ejemplo ex;


Pero tan pronto como una clase tiene algún constructor tomando cualquier número de parámetros explícitamente declarado, el compilador ya no proporciona un constructor por defecto implícito, y ya no permite la declaración de nuevos objetos de esa clase sin argumentos. Por ejemplo, la siguiente clase:

class Ejemplo2 {
  public:
    int total;
    Ejemplo2 (int valor_inicial) : total(valor_inicial) { };
    void acumular (int x) { total += x; };
};


Aquí, hemos declarado un constructor con un parámetro de tipo int. Por lo tanto la siguiente declaración de objeto sería correcta:

Ejemplo2 ex (100);   // ok: llama al constructor 


Pero la siguiente:

Ejemplo2 ex;         // no válido: no hay constructor por defecto 


No sería válida, ya que la clase ha sido declarada con un constructor explícito tomando un argumento y eso reemplaza al constructor por defecto implícito tomando ninguno.

Por lo tanto, si los objetos de esta clase necesitan ser construidos sin argumentos, el constructor por defecto apropiado también debe ser declarado en la clase. Por ejemplo:

// clases y constructores por defecto
#include <iostream>
#include <string>
using namespace std;

class Ejemplo3 {
    string datos;
  public:
    Ejemplo3 (const string& str) : datos(str) {}
    Ejemplo3() {}
    const string& contenido() const {return datos;}
};

int main () {
  Ejemplo3 foo;
  Ejemplo3 bar ("Ejemplo");

  cout << "contenido de bar: " << bar.contenido() << '\n';
  return 0;
}
//contenido de bar: Ejemplo


Aquí, Ejemplo3 tiene un constructor por defecto (es decir, un constructor sin parámetros) definido como un bloque vacío:

Ejemplo3() {}


Esto permite que los objetos de la clase Ejemplo3 sean construidos sin argumentos (como foo fue declarado en este ejemplo). Normalmente, un constructor por defecto como este es definido implícitamente para todas las clases que no tienen otros constructores y por lo tanto no se requiere una definición explícita. Pero en este caso, Ejemplo3 tiene otro constructor:

Ejemplo3 (const string& str);


Y cuando cualquier constructor es explícitamente declarado en una clase, no se proporciona implícitamente ningún constructor por defecto.

Destructor

Los destructores cumplen la funcionalidad opuesta a los constructores: Son responsables de la limpieza necesaria requerida por una clase cuando su vida útil termina. Las clases que hemos definido en capítulos anteriores no asignaban ningún recurso y por lo tanto no realmente requerían ninguna limpieza.

Pero ahora, imaginemos que la clase en el último ejemplo asigna memoria dinámica para almacenar la cadena que tenía como miembro de datos; en este caso, sería muy útil tener una función llamada automáticamente al final de la vida útil del objeto en cargo de liberar esta memoria. Para hacer esto, usamos un destructor. Un destructor es una función miembro muy similar a un constructor por defecto: toma ningún argumento y no retorna nada, ni siquiera void. También usa el nombre de la clase como su propio nombre, pero precedido con un signo tilde (~):

// destructores
#include <iostream>
#include <string>
using namespace std;

class Ejemplo4 {
    string* ptr;
  public:
    // constructores:
    Ejemplo4() : ptr(new string) {}
    Ejemplo4 (const string& str) : ptr(new string(str)) {}
    // destructor:
    ~Ejemplo4 () {delete ptr;}
    // acceder al contenido:
    const string& contenido() const {return *ptr;}
};

int main () {
  Ejemplo4 foo;
  Ejemplo4 bar ("Ejemplo");

  cout << "contenido de bar: " << bar.contenido() << '\n';
  return 0;
}
//contenido de bar: Ejemplo


En la construcción, Ejemplo4 asigna almacenamiento para una cadena. Almacenamiento que es liberado más tarde por el destructor.

El destructor para un objeto se llama al final de su vida útil; en el caso de foo y bar esto sucede al final de la función main.

Constructor de copia

Cuando a un objeto se le pasa un objeto nombrado de su propio tipo como argumento, su constructor de copia es invocado para construir una copia.

Un constructor de copia es un constructor cuyo primer parámetro es de tipo referencia a la clase misma (posiblemente calificado const) y que puede ser invocado con un solo argumento de este tipo. Por ejemplo, para una clase MiClase, el constructor de copia puede tener la siguiente firma:

MiClase::MiClase (const MiClase&);


Si una clase no tiene constructores de copia ni de movimiento personalizados definidos (o asignaciones), se proporciona implícitamente un constructor de copia. Este constructor de copia simplemente realiza una copia de sus propios miembros. Por ejemplo, para una clase como:

class MiClase {
  public:
    int a, b; string c;
};


Se define implícitamente un constructor de copia. La definición asumida para esta función realiza una copia superficial, aproximadamente equivalente a:

MiClase::MiClase(const MiClase& x) : a(x.a), b(x.b), c(x.c) {}


Este constructor de copia por defecto puede satisfacer las necesidades de muchas clases. Pero las copias superficiales solo copian los miembros de la clase mismos, y esto probablemente no es lo que esperamos para clases como la clase Ejemplo4 que definimos arriba, porque contiene punteros de los cuales maneja su almacenamiento. Para esa clase, realizar una copia superficial significa que el valor del puntero es copiado, pero no el contenido mismo; Esto significa que ambos objetos (la copia y el original) estarían compartiendo un único objeto cadena (ambos estarían apuntando al mismo objeto), y en algún momento (en la destrucción) ambos objetos intentarían eliminar el mismo bloque de memoria, probablemente causando que el programa se cuelgue en tiempo de ejecución. Esto puede solucionarse definiendo el siguiente constructor de copia personalizado que realiza una copia profunda:

// constructor de copia: copia profunda
#include <iostream>
#include <string>
using namespace std;

class Ejemplo5 {
    string* ptr;
  public:
    Ejemplo5 (const string& str) : ptr(new string(str)) {}
    ~Ejemplo5 () {delete ptr;}
    // constructor de copia:
    Ejemplo5 (const Ejemplo5& x) : ptr(new string(x.contenido())) {}
    // acceder al contenido:
    const string& contenido() const {return *ptr;}
};

int main () {
  Ejemplo5 foo ("Ejemplo");
  Ejemplo5 bar = foo;

  cout << "contenido de bar: " << bar.contenido() << '\n';
  return 0;
}
//contenido de bar: Ejemplo


La copia profunda realizada por este constructor de copia asigna almacenamiento para una nueva cadena, que es inicializada para contener una copia del objeto original. De esta manera, ambos objetos (copia y original) tienen copias distintas del contenido almacenadas en diferentes ubicaciones.

Asignación por copia

Los objetos no solo se copian en la construcción, cuando se inicializan: También pueden ser copiadas en cualquier operación de asignación. Vea la diferencia:

MiClase foo;
MiClase bar (foo);       // inicialización de objeto: constructor de copia llamado
MiClase baz = foo;       // inicialización de objeto: constructor de copia llamado
foo = bar;               // objeto ya inicializado: asignación por copia llamada 


Nota que baz se inicializa en la construcción usando un signo igual, pero esto no es una operación de asignación! (aunque pueda parecer una): La declaración de un objeto no es una operación de asignación, es simplemente otra de las sintaxis para llamar a constructores de un solo argumento.

La asignación en foo es una operación de asignación. No se está declarando ningún objeto aquí, sino que se está realizando una operación en un objeto existente; foo.

El operador de asignación por copia es una sobrecarga de operator= que toma un valor o referencia de la clase misma como parámetro. El valor de retorno es generalmente una referencia a *this (aunque esto no es requerido). Por ejemplo, para una clase MiClase, la asignación por copia puede tener la siguiente firma:

MiClase& operator= (const MiClase&);


El operador de asignación por copia también es una función especial y también se define implícitamente si una clase no tiene asignaciones por copia ni de movimiento personalizadas (ni constructor de movimiento) definidas.

Pero nuevamente, la versión implícita realiza una copia superficial que es adecuada para muchas clases, pero no para clases con punteros a objetos que manejan su almacenamiento, como es el caso en Ejemplo5. En este caso, no solo la clase incurre en el riesgo de eliminar el objeto apuntado dos veces, sino que la asignación crea fugas de memoria al no eliminar el objeto apuntado por el objeto antes de la asignación. Estos problemas podrían solucionarse con una asignación por copia que elimine el objeto anterior y realice una copia profunda:

Ejemplo5& operator= (const Ejemplo5& x) {
  delete ptr;                      // eliminar cadena actualmente apuntada
  ptr = new string (x.contenido());  // asignar espacio para nueva cadena, y copiar
  return *this;
}


O incluso mejor, ya que su miembro cadena no es constante, podría reutilizar el mismo objeto cadena:

Ejemplo5& operator= (const Ejemplo5& x) {
  *ptr = x.contenido();
  return *this;
}


Constructor y asignación por movimiento

Similar a la copia, el movimiento también usa el valor de un objeto para establecer el valor en otro objeto. Pero, a diferencia de la copia, el contenido realmente se transfiere de un objeto (la fuente) al otro (el destino): la fuente pierde ese contenido, que es tomado por el destino. Este movimiento solo ocurre cuando la fuente del valor es un objeto sin nombre.

Los objetos sin nombre son objetos que son temporales por naturaleza, y por lo tanto ni siquiera han sido dados un nombre. Ejemplos típicos de objetos sin nombre son los valores de retorno de funciones o las conversiones de tipo.

Usar el valor de un objeto temporal como estos para inicializar otro objeto o para asignar su valor, realmente no requiere una copia: el objeto nunca va a ser usado para nada más, y por lo tanto, su valor puede ser movido al objeto destino. Estos casos activan el constructor de movimiento y las asignaciones por movimiento:

El constructor de movimiento se llama cuando un objeto se inicializa en la construcción usando un temporal sin nombre. Del mismo modo, la asignación por movimiento se llama cuando un objeto se le asigna el valor de un temporal sin nombre:

MiClase fn();            // función que retorna un objeto MiClase
MiClase foo;             // constructor por defecto
MiClase bar = foo;       // constructor de copia
MiClase baz = fn();      // constructor de movimiento
foo = bar;               // asignación por copia
baz = MiClase();         // asignación por movimiento 


Tanto el valor retornado por fn como el valor construido con MiClase son temporales sin nombre. En estos casos, no hay necesidad de hacer una copia, porque el objeto sin nombre es de muy corta duración y puede ser adquirido por el otro objeto cuando es una operación más eficiente.

El constructor de movimiento y la asignación por movimiento son miembros que toman un parámetro de tipo referencia rvalue a la clase misma:

MiClase (MiClase&&);             // constructor de movimiento
MiClase& operator= (MiClase&&);  // asignación por movimiento 


Una referencia rvalue se especifica siguiendo el tipo con dos ampersands (&&). Como parámetro, una referencia rvalue coincide con argumentos de temporales de este tipo.

El concepto de movimiento es más útil para objetos que manejan el almacenamiento que usan, como objetos que asignan almacenamiento con new y delete. En tales objetos, copiar y mover son realmente operaciones diferentes:

  • Copiar de A a B significa que se asigna nueva memoria a B y luego todo el contenido de A se copia a esta nueva memoria asignada para B.
  • Mover de A a B significa que la memoria ya asignada a A se transfiere a B sin asignar ningún almacenamiento nuevo. Implica simplemente copiar el puntero.

Por ejemplo:

// constructor/assignación por movimiento
#include <iostream>
#include <string>
using namespace std;

class Ejemplo6 {
    string* ptr;
  public:
    Ejemplo6 (const string& str) : ptr(new string(str)) {}
    ~Ejemplo6 () {delete ptr;}
    // constructor de movimiento
    Ejemplo6 (Ejemplo6&& x) : ptr(x.ptr) {x.ptr=nullptr;}
    // asignación por movimiento
    Ejemplo6& operator= (Ejemplo6&& x) {
      delete ptr; 
      ptr = x.ptr;
      x.ptr=nullptr;
      return *this;
    }
    // acceder al contenido:
    const string& contenido() const {return *ptr;}
    // suma:
    Ejemplo6 operator+(const Ejemplo6& rhs) {
      return Ejemplo6(contenido()+rhs.contenido());
    }
};


int main () {
  Ejemplo6 foo ("Exam");
  Ejemplo6 bar = Ejemplo6("ple");   // construcción por movimiento
  
  foo = foo + bar;                  // asignación por movimiento

  cout << "contenido de foo: " << foo.contenido() << '\n';
  return 0;
}
//contenido de foo: Example


Los compiladores ya optimizan muchos casos que formalmente requieren una llamada a constructor de movimiento en lo que se conoce como Optimización de Valor de Retorno. En particular, cuando el valor retornado por una función se usa para inicializar un objeto. En estos casos, el constructor de movimiento puede realmente nunca ser llamado.

Nota que aunque las referencias rvalue pueden ser usadas para el tipo de cualquier parámetro de función, rara vez es útil para usos otros que el constructor de movimiento. Las referencias rvalue son complicadas, y usos innecesarios pueden ser la fuente de errores bastante difíciles de rastrear.

Miembros implícitos

Las seis funciones miembro especiales descritas arriba son miembros implícitamente declarados en clases bajo ciertas circunstancias:

Función miembro implícitamente definida: definición por defecto:
Constructor por defecto si no hay otros constructores no hace nada
Destructor si no hay destructor no hace nada
Constructor de copia si no hay constructor de movimiento ni asignación por movimiento copia todos los miembros
Asignación por copia si no hay constructor de movimiento ni asignación por movimiento copia todos los miembros
Constructor de movimiento si no hay destructor, ni constructor de copia, ni asignación por copia ni por movimiento mueve todos los miembros
Asignación por movimiento si no hay destructor, ni constructor de copia, ni asignación por copia ni por movimiento mueve todos los miembros

Nota cómo no todas las funciones miembro especiales son implícitamente definidas en los mismos casos. Esto es en gran medida debido a la comptaibilidad con estructuras C y versiones anteriores de C++, y de hecho algunos incluyen casos obsoletos. Afortunadamente, cada clase puede seleccionar explícitamente cuáles de estos miembros existen con su definición por defecto o cuáles están eliminados usando las palabras clave default y delete, respectivamente. La sintaxis es una de las siguientes:

declaracion_funcion = default;
declaracion_funcion = delete;


Por ejemplo:

// default y delete miembros implícitos
#include <iostream>
using namespace std;

class Rectangulo {
    int ancho, alto;
  public:
    Rectangulo (int x, int y) : ancho(x), alto(y) {}
    Rectangulo() = default;
    Rectangulo (const Rectangulo& otro) = delete;
    int area() {return ancho*alto;}
};

int main () {
  Rectangulo foo;
  Rectangulo bar (10,20);

  cout << "area de bar: " << bar.area() << '\n';
  return 0;
}
//area de bar: 200


Aquí, Rectangulo puede ser construido ya sea con dos argumentos int o ser construido por defecto (sin argumentos). Sin embargo, no puede ser construido por copia desde otro objeto Rectangulo, porque esta función ha sido eliminada. Por lo tanto, asumiendo los objetos del último ejemplo, la siguiente declaración no sería válida:

Rectangulo baz (foo);


Podría, sin embargo, hacerse explícitamente válida definiendo su constructor de copia como:

Rectangulo::Rectangulo (const Rectangulo& otro) = default;


Lo cual sería esencialmente equivalente a:

Rectangulo::Rectangulo (const Rectangulo& otro) : ancho(otro.ancho), alto(otro.alto) {}


Nota que, la palabra clave default no define una función miembro igual al constructor por defecto (es decir, donde constructor por defecto significa constructor sin parámetros), sino igual al constructor que sería definido implícitamente si no estuviera eliminado.

En general, y para compatibilidad futura, las clases que definen explícitamente un constructor/operador de copia o un constructor/operador de movimiento pero no ambos, son alentadas a especificar ya sea delete o default en las otras funciones miembro especiales que no definen explícitamente.

Amistad e herencia

Funciones amigas

En principio, los miembros privados y protegidos de una clase no pueden ser accedidos desde fuera de la misma clase en la que están declarados. Sin embargo, esta regla no se aplica a "amigos".

Los amigos son funciones o clases declaradas con la palabra clave friend.

Una función no miembro puede acceder a los miembros privados y protegidos de una clase si es declarada como amiga de esa clase. Esto se hace incluyendo una declaración de esta función externa dentro de la clase, y precediéndola con la palabra clave friend:

// funciones amigas
#include <iostream>
using namespace std;

class Rectangulo {
    int ancho, alto;
  public:
    Rectangulo() {}
    Rectangulo (int x, int y) : ancho(x), alto(y) {}
    int area() {return ancho * alto;}
    friend Rectangulo duplicar (const Rectangulo&);
};

Rectangulo duplicar (const Rectangulo& param)
{
  Rectangulo res;
  res.ancho = param.ancho*2;
  res.alto = param.alto*2;
  return res;
}

int main () {
  Rectangulo foo;
  Rectangulo bar (2,3);
  foo = duplicar (bar);
  cout << foo.area() << '\n';
  return 0;
}
//24


La función duplicar es amiga de la clase Rectangulo. Por lo tanto, la función duplicar puede acceder a los miembros ancho y alto (que son privados) de diferentes objetos de tipo Rectangulo. Nota sin embargo que ni en la declaración de duplicar ni en su posterior uso en main, la función duplicar es considerada un miembro de la clase Rectangulo. ¡No lo es! Simplemente tiene acceso a sus miembros privados y protegidos sin ser miembro.

Los casos de uso típicos de funciones amigas son operaciones que se realizan entre dos clases diferentes accediendo a miembros privados o protegidos de ambas.

Clases amigas

Similar a las funciones amigas, una clase amiga es una cuyos miembros tienen acceso a los miembros privados o protegidos de otra clase:

// clase amiga
#include <iostream>
using namespace std;

class Cuadrado;

class Rectangulo {
    int ancho, alto;
  public:
    int area ()
      {return (ancho * alto);}
    void convertir (Cuadrado a);
};

class Cuadrado {
  friend class Rectangulo;
  private:
    int lado;
  public:
    Cuadrado (int a) : lado(a) {}
};

void Rectangulo::convertir (Cuadrado a) {
  ancho = a.lado;
  alto = a.lado;
}
  
int main () {
  Rectangulo rect;
  Cuadrado cuad (4);
  rect.convertir(cuad);
  cout << rect.area();
  return 0;
}
//16


En este ejemplo, la clase Rectangulo es amiga de la clase Cuadrado permitiendo que las funciones miembro de Rectangulo accedan a los miembros privados y protegidos de Cuadrado. Más concretamente, Rectangulo accede a la variable miembro Cuadrado::lado, que describe el lado del cuadrado.

Hay otra cosa nueva en este ejemplo: al principio del programa, hay una declaración vacía de la clase Cuadrado. Esto es necesario porque la clase Rectangulo usa Cuadrado (como parámetro en miembro convertir), y Cuadrado usa Rectangulo (declarándola amiga).

Las amistades nunca son correspondidas a menos que se especifique: En nuestro ejemplo, Rectangulo es considerada una clase amiga por Cuadrado, pero Cuadrado no es considerada amiga por Rectangulo. Por lo tanto, las funciones miembro de Rectangulo pueden acceder a los miembros protegidos y privados de Cuadrado pero no al revés. Por supuesto, Cuadrado también podría ser declarada amiga de Rectangulo, si se necesitara, concediendo tal acceso.

Otra propiedad de las amistades es que no son transitivas: El amigo de un amigo no es considerado un amigo a menos que se especifique explícitamente.

Herencia entre clases

Las clases en C++ pueden ser extendidas, creando nuevas clases que retienen características de la clase base. Este proceso, conocido como herencia, involucra una clase base y una clase derivada: La clase derivada hereda los miembros de la clase base, sobre los cuales puede agregar sus propios miembros.

Por ejemplo, imaginemos una serie de clases para describir dos tipos de polígonos: rectángulos y triángulos. Estos dos polígonos tienen ciertas propiedades comunes, como los valores necesarios para calcular sus áreas: ambos pueden ser descritos simplemente con una altura y un ancho (o base).

Esto podría representarse en el mundo de las clases con una clase Poligono de la cual derivaríamos las otras dos: Rectangulo y Triangulo:

La clase Poligono contendría miembros que son comunes para ambos tipos de polígono. En nuestro caso: ancho y alto. Y Rectangulo y Triangulo serían sus clases derivadas, con características específicas que son diferentes de un tipo de polígono al otro.

Las clases que se derivan de otras heredan todos los miembros accesibles de la clase base. Eso significa que si una clase base incluye un miembro A y derivamos una clase de ella con otro miembro llamado B, la clase derivada contendrá tanto el miembro A como el miembro B.

La relación de herencia de dos clases se declara en la clase derivada. Las definiciones de clases derivadas usan la siguiente sintaxis:

class nombre_clase_derivada: public nombre_clase_base
{ /*...*/ };


Donde nombre_clase_derivada es el nombre de la clase derivada y nombre_clase_base es el nombre de la clase sobre la cual se basa. El especificador de acceso public puede ser reemplazado por cualquiera de los otros especificadores de acceso (protected o private). Este especificador de acceso limita el nivel más accesible para los miembros heredados de la clase base: Los miembros con un nivel más accesible son heredados con este nivel en su lugar, mientras que los miembros con un nivel igual o más restrictivo mantienen su nivel restrictivo en la clase derivada.

// clases derivadas
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    void establecer_valores (int a, int b)
      { ancho=a; alto=b;}
 };

class Rectangulo: public Poligono {
  public:
    int area ()
      { return ancho * alto; }
 };

class Triangulo: public Poligono {
  public:
    int area ()
      { return ancho * alto / 2; }
  };
  
int main () {
  Rectangulo rect;
  Triangulo trgl;
  rect.establecer_valores (4,5);
  trgl.establecer_valores (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}
//20
//10


Los objetos de las clases Rectangulo y Triangulo cada uno contiene miembros heredados de Poligono. Estos son: ancho, alto y establecer_valores.

El especificador de acceso protected usado en la clase Poligono es similar a private. Su única diferencia ocurre en realidad con la herencia: Cuando una clase hereda de otra, los miembros de la clase derivada pueden acceder a los miembros protegidos heredados de la clase base, pero no a sus miembros privados.

Al declarar ancho y alto como protected en lugar de private, estos miembros también son accesibles desde las clases derivadas Rectangulo y Triangulo, en lugar de solo desde miembros de Poligono. Si fueran public, podrían ser accedidos desde cualquier lugar.

Podemos resumir los diferentes tipos de acceso según qué funciones pueden acceder a ellos de la siguiente manera:

Acceso public protected private
miembros de la misma clase
miembros de clase derivada no
no miembros no no

Donde "no miembros" representa cualquier acceso desde fuera de la clase, como desde main, desde otra clase o desde una función.

En el ejemplo anterior, los miembros heredados por Rectangulo y Triangulo tienen los mismos permisos de acceso que tenían en su clase base Poligono:

Poligono::ancho           // acceso protected
Rectangulo::ancho         // acceso protected

Poligono::establecer_valores()    // acceso public
Rectangulo::establecer_valores()  // acceso public  


Esto es porque la relación de herencia ha sido declarada usando la palabra clave public en cada una de las clases derivadas:

class Rectangulo: public Poligono { /* ... */ }


Esta palabra clave public después de los dos puntos (:) denota el nivel más accesible que tendrán los miembros heredados de la clase que sigue (en este caso Poligono) desde la clase derivada (en este caso Rectangulo). Como public es el nivel más accesible, al especificar esta palabra clave la clase derivada heredará todos los miembros con los mismos niveles que tenían en la clase base.

Con protected, todos los miembros públicos de la clase base son heredados como protected en la clase derivada. Por el contrario, si se especifica el nivel de acceso más restrictivo (private), todos los miembros de la clase base son heredados como private.

Por ejemplo, si hija fuera una clase derivada de madre que definiéramos como:

class Hija: protected Madre;


Esto establecería protected como el nivel menos restrictivo para los miembros de Hija que heredó de madre. Es decir, todos los miembros que eran públicos en Madre se convertirían en protected en Hija. Por supuesto, esto no restringiría a Hija para declarar sus propios miembros públicos. Ese nivel menos restrictivo de acceso solo se establece para los miembros heredados de Madre.

Si no se especifica un nivel de acceso para la herencia, el compilador asume private para clases declaradas con la palabra clave class y public para aquellas declaradas con struct.

De hecho, la mayoría de los casos de uso de herencia en C++ deberían usar herencia pública. Cuando se necesitan otros niveles de acceso para clases base, generalmente pueden ser mejor representados como variables miembro en su lugar. ¿Qué se hereda de la clase base? En principio, una clase derivada públicamente hereda acceso a cada miembro de una clase base excepto:

  • sus constructores y su destructor
  • sus miembros operador de asignación (operator=)
  • sus amigos
  • sus miembros privados

Aunque el acceso a los constructores y destructor de la clase base no se hereda como tal, son automáticamente llamados por los constructores y destructor de la clase derivada.

A menos que se especifique lo contrario, los constructores de una clase derivada llaman al constructor por defecto de sus clases base (es decir, el constructor que no toma argumentos). Llamar a un constructor diferente de una clase base es posible, usando la misma sintaxis usada para inicializar variables miembro en la lista de inicialización:

nombre_constructor_derivado (parametros) : nombre_constructor_base (parametros) {...}


Por ejemplo:

// constructores y clases derivadas
#include <iostream>
using namespace std;

class Madre {
  public:
    Madre ()
      { cout << "Madre: sin parámetros\n"; }
    Madre (int a)
      { cout << "Madre: parámetro int\n"; }
};

class Hija : public Madre {
  public:
    Hija (int a)
      { cout << "Hija: parámetro int\n\n"; }
};

class Hijo : public Madre {
  public:
    Hijo (int a) : Madre (a)
      { cout << "Hijo: parámetro int\n\n"; }
};

int main () {
  Hija kelly(0);
  Hijo bud(0);
  
  return 0;
}
//Madre: sin parámetros
//Hija: parámetro int

//Madre: parámetro int
//Hijo: parámetro int


Nota la diferencia entre qué constructor de Madre se llama cuando se crea un nuevo objeto Hija y cuándo es un objeto Hijo. La diferencia se debe a las diferentes declaraciones de constructores de Hija y Hijo:

Hija (int a)          // nada especificado: llamar constructor por defecto
Hijo (int a) : Madre (a)  // constructor especificado: llamar este constructor específico 


Herencia múltiple

Una clase puede heredar de más de una clase simplemente especificando más clases base, separadas por comas, en la lista de clases base de una clase (es decir, después de los dos puntos). Por ejemplo, si el programa tuviera una clase específica para imprimir en pantalla llamada Salida, y quisiéramos que nuestras clases Rectangulo y Triangulo también heredaran sus miembros además de los de Poligono podríamos escribir:

class Rectangulo: public Poligono, public Salida;
class Triangulo: public Poligono, public Salida; 


Aquí está el ejemplo completo:

// herencia múltiple
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    Poligono (int a, int b) : ancho(a), alto(b) {}
};

class Salida {
  public:
    static void imprimir (int i);
};

void Salida::imprimir (int i) {
  cout << i << '\n';
}

class Rectangulo: public Poligono, public Salida {
  public:
    Rectangulo (int a, int b) : Poligono(a,b) {}
    int area ()
      { return ancho*alto; }
};

class Triangulo: public Poligono, public Salida {
  public:
    Triangulo (int a, int b) : Poligono(a,b) {}
    int area ()
      { return ancho*alto/2; }
};
  
int main () {
  Rectangulo rect (4,5);
  Triangulo trgl (4,5);
  rect.imprimir (rect.area());
  Triangulo::imprimir (trgl.area());
  return 0;
}
//20
//10 


Polimorfismo

Antes de profundizar más en este capítulo, debería tener un entendimiento adecuado de punteros y herencia de clases. Si no está realmente seguro del significado de alguna de las siguientes expresiones, debería revisar las secciones indicadas:

Declaración: Explicada en:
int A::b(int c) { } Clases
a->b Estructuras de datos
class A: public B {}; Amistad e herencia

Punteros a clase base

Una de las características clave de la herencia de clases es que un puntero a una clase derivada es compatible en tipo con un puntero a su clase base. El polimorfismo es el arte de aprovechar esta simple pero poderosa y versátil característica.

El ejemplo sobre las clases rectángulo y triángulo puede ser reescrito usando punteros tomando esta característica en cuenta:

// punteros a clase base
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    void establecer_valores (int a, int b)
      { ancho=a; alto=b; }
};

class Rectangulo: public Poligono {
  public:
    int area()
      { return ancho*alto; }
};

class Triangulo: public Poligono {
  public:
    int area()
      { return ancho*alto/2; }
};

int main () {
  Rectangulo rect;
  Triangulo trgl;
  Poligono * ppoly1 = &rect;
  Poligono * ppoly2 = &trgl;
  ppoly1->establecer_valores (4,5);
  ppoly2->establecer_valores (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}
//20
//10


La función main declara dos punteros a Poligono (nombrados ppoly1 y ppoly2). Estos son asignadas las direcciones de rect y trgl, respectivamente, que son objetos de tipo Rectangulo y Triangulo. Tales asignaciones son válidas, ya que tanto Rectangulo como Triangulo son clases derivadas de Poligono.

Dereferenciar ppoly1 y ppoly2 (con *ppoly1 y *ppoly2) es válido y nos permite acceder a los miembros de sus objetos apuntados. Por ejemplo, las siguientes dos declaraciones serían equivalentes en el ejemplo anterior:

ppoly1->establecer_valores (4,5);
rect.establecer_valores (4,5);


Pero porque el tipo de ppoly1 y ppoly2 es puntero a Poligono (y no puntero a Rectangulo ni puntero a Triangulo), solo los miembros heredados de Poligono pueden ser accedidos, y no los de las clases derivadas Rectangulo y Triangulo. Por eso el programa anterior accede a los miembros area de ambos objetos usando rect y trgl directamente, en lugar de los punteros; los punteros a la clase base no pueden acceder a los miembros area.

El miembro area podría haber sido accedido con los punteros a Poligono si area fuera un miembro de Poligono en lugar de un miembro de sus clases derivadas, pero el problema es que Rectangulo y Triangulo implementan diferentes versiones de area, por lo tanto no hay una única versión común que podría ser implementada en la clase base.

Miembros virtuales

Un miembro virtual es una función miembro que puede ser redefinida en una clase derivada, mientras preserva sus propiedades de llamada a través de referencias. La sintaxis para que una función se vuelva virtual es preceder su declaración con la palabra clave virtual:

// miembros virtuales
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    void establecer_valores (int a, int b)
      { ancho=a; alto=b; }
    virtual int area ()
      { return 0; }
};

class Rectangulo: public Poligono {
  public:
    int area ()
      { return ancho * alto; }
};

class Triangulo: public Poligono {
  public:
    int area ()
      { return (ancho * alto / 2); }
};

int main () {
  Rectangulo rect;
  Triangulo trgl;
  Poligono poly;
  Poligono * ppoly1 = &rect;
  Poligono * ppoly2 = &trgl;
  Poligono * ppoly3 = &poly;
  ppoly1->establecer_valores (4,5);
  ppoly2->establecer_valores (4,5);
  ppoly3->establecer_valores (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  cout << ppoly3->area() << '\n';
  return 0;
}
//20
//10
//0



En este ejemplo, las tres clases (Poligono, Rectangulo y Triangulo) tienen los mismos miembros: ancho, alto, y funciones establecer_valores y area.

La función miembro area ha sido declarada como virtual en la clase base porque es redefinida más tarde en cada una de las clases derivadas. Los miembros no virtuales también pueden ser redefinidos en clases derivadas, pero los miembros no virtuales de clases derivadas no pueden ser accedidos a través de una referencia de la clase base: es decir, si virtual se elimina de la declaración de area en el ejemplo anterior, las tres llamadas a area retornarían cero, porque en todos los casos, se habría llamado la versión de la clase base.

Por lo tanto, esencialmente, lo que hace la palabra clave virtual es permitir que un miembro de una clase derivada con el mismo nombre que uno en la clase base sea llamado apropiadamente desde un puntero, y más precisamente cuando el tipo del puntero es un puntero a la clase base que está apuntando a un objeto de la clase derivada, como en el ejemplo anterior.

Una clase que declara o hereda una función virtual se llama una clase polimórfica.

Nota que a pesar de la virtualidad de uno de sus miembros, Poligono era una clase regular, de la cual incluso se instanció un objeto (poly), con su propia definición del miembro area que siempre retorna 0.

Clases base abstractas

Las clases base abstractas son algo muy similar a la clase Poligono en el ejemplo anterior. Son clases que solo pueden ser usadas como clases base, y por lo tanto se les permite tener funciones miembro virtuales sin definición (conocidas como funciones virtuales puras). La sintaxis es reemplazar su definición por =0 (un signo igual y un cero):

Una clase base abstracta Poligono podría verse así:

// clase base abstracta CPoligono
class Poligono {
  protected:
    int ancho, alto;
  public:
    void establecer_valores (int a, int b)
      { ancho=a; alto=b; }
    virtual int area () =0;
};


Nota que area no tiene definición; esto ha sido reemplazado por =0, lo que la convierte en una función virtual pura. Las clases que contienen al menos una función virtual pura son conocidas como clases base abstractas.

Las clases base abstractas no pueden ser usadas para instanciar objetos. Por lo tanto, esta última versión abstracta de la clase base Poligono no podría ser usada para declarar objetos como:

Poligono mipoligono;   // no funciona si Poligono es clase base abstracta 


Pero una clase base abstracta no es totalmente inútil. Puede ser usada para crear punteros a ella, y aprovechar todas sus capacidades polimórficas. Por ejemplo, las siguientes declaraciones de punteros serían válidas:

Poligono * ppoly1;
Poligono * ppoly2;


Y pueden realmente ser dereferenciadas cuando apuntan a objetos de clases derivadas (no abstractas). Aquí está el ejemplo completo:

// clase base abstracta
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    void establecer_valores (int a, int b)
      { ancho=a; alto=b; }
    virtual int area (void) =0;
};

class Rectangulo: public Poligono {
  public:
    int area (void)
      { return (ancho * alto); }
};

class Triangulo: public Poligono {
  public:
    int area (void)
      { return (ancho * alto / 2); }
};

int main () {
  Rectangulo rect;
  Triangulo trgl;
  Poligono * ppoly1 = &rect;
  Poligono * ppoly2 = &trgl;
  ppoly1->establecer_valores (4,5);
  ppoly2->establecer_valores (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  return 0;
}
//20
//10


En este ejemplo, objetos de tipos diferentes pero relacionados son referidos usando un único tipo de puntero (Poligono*) y la función miembro apropiada es llamada cada vez, simplemente porque son virtuales. Esto puede ser realmente útil en algunas circunstancias. Por ejemplo, es incluso posible para un miembro de la clase base abstracta Poligono usar el puntero especial this para acceder a los miembros virtuales apropiados, aunque Poligono mismo no tiene implementación para esta función:

// miembros virtuales puros pueden ser llamados
// desde la clase base abstracta
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    void establecer_valores (int a, int b)
      { ancho=a; alto=b; }
    virtual int area() =0;
    void imprimirarea()
      { cout << this->area() << '\n'; }
};

class Rectangulo: public Poligono {
  public:
    int area (void)
      { return (ancho * alto); }
};

class Triangulo: public Poligono {
  public:
    int area (void)
      { return (ancho * alto / 2); }
};

int main () {
  Rectangulo rect;
  Triangulo trgl;
  Poligono * ppoly1 = &rect;
  Poligono * ppoly2 = &trgl;
  ppoly1->establecer_valores (4,5);
  ppoly2->establecer_valores (4,5);
  ppoly1->imprimirarea();
  ppoly2->imprimirarea();
  return 0;
}
//20
//10


Los miembros virtuales y las clases abstractas otorgan a C++ características polimórficas, más útiles para proyectos orientados a objetos. Por supuesto, los ejemplos anteriores son casos de uso muy simples, pero estas características pueden ser aplicadas a arreglos de objetos o objetos asignados dinámicamente.

Aquí hay un ejemplo que combina algunas de las características en los últimos capítulos, como memoria dinámica, inicializadores de constructor y polimorfismo:

// asignación dinámica y polimorfismo
#include <iostream>
using namespace std;

class Poligono {
  protected:
    int ancho, alto;
  public:
    Poligono (int a, int b) : ancho(a), alto(b) {}
    virtual int area (void) =0;
    void imprimirarea()
      { cout << this->area() << '\n'; }
};

class Rectangulo: public Poligono {
  public:
    Rectangulo(int a,int b) : Poligono(a,b) {}
    int area()
      { return ancho*alto; }
};

class Triangulo: public Poligono {
  public:
    Triangulo(int a,int b) : Poligono(a,b) {}
    int area()
      { return ancho*alto/2; }
};

int main () {
  Poligono * ppoly1 = new Rectangulo (4,5);
  Poligono * ppoly2 = new Triangulo (4,5);
  ppoly1->imprimirarea();
  ppoly2->imprimirarea();
  delete ppoly1;
  delete ppoly2;
  return 0;
}
//20
//10


Nota que los punteros ppoly:

Poligono * ppoly1 = new Rectangulo (4,5);
Poligono * ppoly2 = new Triangulo (4,5);


son declarados siendo de tipo "puntero a Poligono", pero los objetos asignados han sido declarados teniendo el tipo de clase derivado directamente (Rectangulo y Triangulo).

Referencias

[1] Clases I [2] Clases II [3] Miembros especiales [4] Amistad e herencia [5] Polimorfismo

  • Registro de cambios
Tiempo Lugar Modificador Notas
2020-10-03 Yulin PatrickLee La primera versión de esta nota

Etiquetas: clases C++ Programación Orientada a Objetos herencia polimorfismo Constructores

Publicado el 6-28 23:46