You are on page 1of 12

Programación Modular. ETSIT. 1o C.

Apuntes del profesor Juan Falgueras.


Curso 2001/02
versión: 28 de abril de 2003

5
Programación orientada a objetos

Contenido
5. Programación orientada a objetos 1
5.1. Introducción a C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
5.2. Diferencias entre C y C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
5.3. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
5.4. Definición de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
5.5. Métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
5.6. Constructores y Destructores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.7. Sobrecarga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
5.8. Entrada y salida en C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

5. Programación orientada a objetos

5.1. Introducción a C++


El lenguaje C++ fue desarrollado por Bjarne Stroustrup1 de AT&T Bell Laboratories durante
los 80. El autor expandió enormemente el lenguaje C para dar soporte a principios de diseño más
modernos. La diferencia más importante entre C y C++ está en el soporte para clases, pero son
fundamentales también:
1. Sobrecarga de operadores, que hace posible dar significados añadidos a los operadores tradi-
cionales.
2. Plantillas (templates), que permiten escribir pre-código muy reutilizable sin concretar aún
elementos variables.
3. Gestión de excepciones, uniformando dando soporte a la detección y respuesta a los errores
de ejecución.

Sin embargo uno de los objetivos que se propuso el autor fue el de mantener aún compatibilidad con
el lenguaje germinal C en lo posible. Ası́ todas las posibilidades del estándard C están igualmente
presentes en C++. Esto, sin embargo no significa que todos los programas escritos en C compilen
en C++; existen restricciones en C++ respecto a C sobre todo en aras de una mayor seguridad.

5.2. Diferencias entre C y C++


Además de las grandes novedades relativas a la programación orientada a objetos (clases,
sobrecarga, derivación, funciones virtuales, plantillas y gestión de excepciones), C++ añade a C
pequeñas diferencias que conviene conocer.
1 http://www.research.att.com/~bs/homepage.html
5.2 Diferencias entre C y C++ 2

Comentarios C++ tiene comentarios de ámbito limitado a una sóla lı́nea, frente a C que no acaba
el comentario al terminar la lı́nea. Como ya habremos visto, los comentarios de C++ empiezan con
// y terminan con el final de la lı́nea. Esto no quiere decir, por otra parte, que no se puedan
utilizar también la técnica de comentarios de C.

Etiquetas frente a nombres de tipo Los identificadores de estructuras, uniones y enumerados


bastan para definir en C++ el propio tipo sin necesidad de crear un tipo con typedef. Ası́ en vez
de
typedef struct {int numerador, denominador;} Fracc;
basta (en C++) escribir:
struct Fracc {int numerador, denominador;};
pudiéndose desde entonces definir estructuras de tipo Fracc;.

Funciones sin argumentos No hay necesidad de usar la palabra void cuando se definen fun-
ciones sin argumentos:
int leeInt(void); // en C
int leeInt(); // es suficiente en C++

Argumentos con valor por defecto C++ permite que un argumento tome un valor por defecto
cuando no se especifique un valor real en la llamada. Por ejemplo:

void wrLn(int n = 1) { // tiene un parámetro que toma por defecto el valor 1


while (n-- > 0) putchar(’\n’);
}

puede ser llamado con o sin parámetros

wrLn(3); // escribirı́a tres saltos de lı́nea


wrLn(); // escribirı́a uno

Si se define la función int a(int a=1, int b=2, int c), ¿qué significa a(4,5)? Podrı́a significar
varias cosas con lo que los argumentos con valores por defecto siempre deben estar juntos al final
de la lista de argumentos formales. int a(int c, int a=1, int b=2) hace que a(1), ó a(1,2)
ó a(1,2,3) sean todos no-ambiguos.2

Funciones inline El lenguaje C explotó enormemente el uso del preprocesado léxico. El prepro-
cesador actúa antes que el compilador substituyendo sintácticamente los elementos #define’dos
por su equivalente, sin que el preprocesador interprete el significado sino sólo los tokens léxicos que
intervienen. Esta idea provenı́a de los antiguos lenguajes ensambladores de los que C era cercano
heredero. La #define’ción de sı́mbolos es sólo el mecanismo más simple; cpp, el preprocesador,
previo a C, permite la expansión de macros. Una macro es un sı́mbolo con argumentos. Entonces
se sustituye el sı́mbolo por la expresión y los argumentos se intercalan en la expresión allı́ dónde
se indique.
La potencia sintáctica del preprocesado de macros de C tiene sin embargo algunos puntos
oscuros. Imaginémonos un macro
#define cuadrado(x) (x*x)
2 En otros lenguajes es posible poner nombres a los argumentos en la propia llamada, a(1, c=>3) permitirı́a que

b tomase libremente su valor por defecto. Esto ayuda a manejar funciones con muchos argumentos superfluos.
5.2 Diferencias entre C y C++ 3

que significa que allı́ en el fuente en C dónde aparezca cuadrado(algo) lo debe substituir por
cuadrado(algo*algo). Insistimos, el preprocesador no entra en interpretar lo que signifique nin-
guna de las expresiones anteriores, eso se lo deja al compilador que recoje el resultado del prepro-
cesado. Veámosla en acción en diversas ocasiones como:

void f(double d, int i)


{
r = cuadrado(d); // bien
r = cuadrado(i++); // mal: significa (i++*i++)
r = cuadrado(d+1); // mal: significa (d+1*d+1); que es, (d+d+1)
// ...
}

El problema de “d+1” se resuelve mediante el añadido de paréntesis en el macro:


#define cuadrado(x) ((x)*(x)) /* mejor */
pero el de “i++” no tiene solución en C. C++ añade un tipo de “funciones” que se expanden
(sintácticamente) en el momento de ser utilizadas. Ası́ no es un preprocesador, el preprocesador de
C, que no tiene en cuenta la semántica de las operaciones C, el que se encarga de expandir estas
construcciones sintácticas sino el propio compilador en C++ el que expande de manera correcta
estos macros, teniendo sobre todo en cuenta la manipulación correcta de los argumentos.
inline int icuadrado(int x) {return x * x;}
El hecho de que sea el propio compilador el que expanda el cuerpo de la función obliga, sin embargo,
a que el tipo del argumento sea conocido, algo que C++ no puede resolver excepto con el uso de
plantillas, que son, sin embargo, inadecuadas para este propósito.

Paso de parámetros por referencia En C todos los parámetros siempre son pasados por
valor, esto es, copiando el parámetro que se ponga en la llamada, parámetro actual, a la variable
receptora en el parámetro formal ya como variable local del procedimiento. Esto nos obliga a
utilizar a poner las direcciones de las variables (& en los parámetros de la llamada y a apuntar
mediante esas direcciones (*) dentro del procedimiento para poder modificar los valores de las
variables pasadas.

void intercambia(int *a, int *b) {


int temp= *a;
*a = *b;
*b = temp;
}

y es llamado mediante intercambia(&i, &j);. O la tan famosa scanf que require referencias a
los elementos a leer. Por ejemplo:

for (i=0; i<N; i++)


scanf("%d", &a[i]);

Esta técnica es incómoda e insegura3 . C++ detalla y permite un mayor control del paso de
parámetros añadiendo la posibilidad del paso de parámetros por referencia. En este caso, el paráme-
tro actual no necesita llevar el indicador de que se va modificar (&) sino que es en el parámetro
3 Aunque esta técnica es incómoda, también hay que decir que es más explı́cita en cuanto a la peligrosidad de

uso de este tipo de modificaciones no locales. Es más explı́cita porque manifiesta el cambio mediante el signo *
en el procedimiento. Sin embargo respecto a los arrays el tema es más oscuro y el cambio se produce siempre ya
que el propio nombre del array es la dirección del bloque de datos del mismo. Recordemos que en C los paréntesis
cuadrados [] actúan como operadores de indirección siempre: x[i] equivale *(x+i)
5.2 Diferencias entre C y C++ 4

formal de la declaración del procedimiento donde se indica que se va a recibir al parámetro por
referencia. Esto es mucho más cómo en muchos casos y es preferido por la mayorı́a de los pro-
gramadores. Es un mecanismo añadido. El compilador se encarga de los aspectos ‘sucios’ de la
modificación del parámetro actual. No hay que usar & cada vez que se llame en el parámetro
formal ni * en la función cada vez que se quiera acceder al valor del parámetro. En el caso del
procedimiento de intercambio anterior escribiremos:

void intercambia(int& a, int& b) {


int temp= a;
a = b;
b = temp;
}

y es llamado mediante intercambia(i, j);. Nótese que aparece una nueva notación: se declara
un parámetro que es una referencia a un tipo de elemento. C carece de las posibilidades de la
referencia y por ello usa los punteros siempre.

Operadores new y delete En vez de las funciones malloc, calloc, realloc y free, aunque
C++ las puede utilizar, C++ introduce un operador (y por lo tanto una sintaxis diferente) para
adquirir nuevos bloques de memoria y/o crear nuevos objetos en ella. Por ejemplo:
int *pint, *pin2, *aint;
pint = new int; // adquiere memoria adecuada para un entero
pin2 = new int(33); // .. e inicializa su contenido a 33
aint = new int[10]; // ı́dem para un array de 10 enteros
Como es de esperar, new devolverá un puntero nulo (0 ó NULL) cuando no pueda encontrar un
espacio suficiente de memoria. El tamaño del espacio lo calcula el propio compilador según el
objeto a ubicar. En esto está la mayor diferencia con C. Por otro lado
delete pint; // libera la memoria conseguida
delete[] aint; // requiere [] con arrays
El hecho de que sea un operador hace más cómodo su uso ya que tampoco hace falta el incluir
<stdlib.h> con el prototipo (interfaz) de malloc, etc. sino que el propio compilador expande el
operador al estilo de un macro:

pa = new int; // (int *) malloc(sizeof(int))


es equivalente al macro:
#define new(X) (X *) malloc(sizeof(X))

encargándose el compilador de C++ de estos detalles.

Conversión de tipos El operador de conversión de tipos de C se ve ampliado en C++ y además


se usa con una sintaxis un poco diferente aún en el caso sencillo de adaptación de tipos sencilla
común:
int num = 33;
char c1 = (char) num; // estilo C
char c2 = char (num); // estilo C++
pero existen en C++ otros conversores de tipo como son conversión de atributo const, reinterpretar
el significado de los bytes (sin cambio de valores) y conversores entre clases, de una semántica más
compleja de la que se pretende cubrir en esta introducción.
5.3 Clases 5

5.3. Clases
La mayor diferencia entre C y C++ está en soporte que el último ofrece para las clases 4 .
Esencialmente una clase es un tipo abstracto de datos: colección de datos junto a operadores para
acceder y modificarlos. Con las clases podremos, pues construir nuevos tipos de datos, que siendo
cuidadosos en su construcción, serán equivalentes en su trato, con los tipos predefinidos por el
lenguaje. Ası́ podrı́amos definir una clase Complejo y variables
Complejo c1, c2, c3;
y, mediante la sobrecarga de operadores que C++ permite, operar correctamente con nuestros
propios operadores de multiplicación, etcétera
c3 = c1 * c2;
además del resto de operaciones que queramos definir sobrecargando los operadores propios del
lenguaje.
Pero el uso de las clases es aún más interesante desde el punto de vista de la programación
orientada a objetos. Si estamos desarrollando una interfaz de usuario, probáblemente tengamos
un objeto panel que puede siembre abrirse, cerrarse, ocultarse, etc. Pero después veremos que
un diálogo es un panel más otra serie de operaciones como la de interactuar con el teclado. Una
ventana será otro tipo de panel que tendrá barra de desplazamiento, etc. Luego serán todos objetos
derivados del primero con comportamientos heredados de aquél más particularidades propias. Si
lo que estamos desarrollando es una aplicación de contabilidad, podremos tener una clase Cuenta
con operaciones comunes como deposito, retiro, etc.
Las clases facilitan la programación de aplicaciones complejas haciendolas más legibles y con-
trolables. Sin embargo el precio es una mayor dificultad de aprendizaje del lenguaje. La progra-
mación orientada a objetos es adecuada en aplicaciones medianas a grandes, no tanto en pequeñas
aplicaciones.

5.4. Definición de clases


Definir una clase en C++ es muy parecido a definir una structura en C. En su forma más
simple, la el aspecto de una clase es idéntico al de una estructura, usando la palabra class en vez
de struct:
class Complejo {
float parteReal;
float parteImaginaria;
};
diciéndose que parteReal y parteImaginaria son miembros de la clase Complejo. La convención
de comenzar los nombres de los tipos con mayúsculas es adecuada también aquı́, pero por supuesto
depende de las normas de estilo del grupo de programadores, no del lenguaje.
En una terminologı́a más general en programación orientada a objetos, los datos miembros
son los atributos de la clase.
Una vez definida la clase, su nombre se puede usar para declarar variables como se hacı́a con
las estructuras:
Complejo c1, c2;
Nótese que el identificador (la etiqueta) sirve en C++ como nombre del tipo y no es necesario
escribir class Complejo c1;.
A las variables c1 y c2 se les llama instancias de la clase Complejo. Una instancia de
cualquier clase se denomina objeto.
Los miembros de una estructura son accedidos mediante los operadores . y ->. En una clase,
por otro lado, los miembros están ocultos, mientras no se indique lo contrario, o sea, que por
defecto son inaccesibles, de manera que las siguientes acciones son ilegales:
4 El nombre original de C++ era C with Classes: C + C.
5.5 Métodos 6

c1.parteReal = 0.0; // ilegal, no accesible


im = c2.parteImaginaria; // ilegal, no accesible
Decimos que parteReal y parteImaginaria son miembros privados de la clase Complejo.
Pero si ası́ lo queremos, podemos hacer accesibles los miembros de una clase declarándolos
public:
class Complejo {
public:
float parteReal;
float parteImaginaria;
};
Podemos mezclar partes públicas y privadas de una clase:
class Complejo {
public:
float parteReal;
private:
float parteImaginaria;
};
Nótese el uso de private para comenzar una sección de declaraciones ocultas. Al principio de la
declaración si no se indica otra cosa se supone implı́citamente la palabra private, por lo que lo
anterior es equivalente a:
class Complejo {
float parteImaginaria;
public:
float parteReal;
};

5.5. Métodos
Si no son visibles los miembros privados de una clase, ¿cómo se pueden modificar o ver sus
valores? La respuesta es ingenua: las funciones que necesiten acceder a los datos miembros de una
clase deben estar declarados dentro de la misma clase. Son las funciones miembro o métodos.
Esta es la técnica que C++ utiliza para ver/modificar sus datos miembro. La diferencia entre
las clases y las estructuras empieza realmente aquı́, para controlar el uso de la información que
determina el estado de un objeto se deben usar métodos que son las funciones, vı́as, de acceso
seguras para controlar y presentar la información oculta de manera adecuada. Ası́ un objeto tiene
un estado interno reconocible por su comportamiento ante las llamadas a sus métodos.
Ası́:
class Complejo {
public:
void crear(int preal int pimag);
void print();
private:
float parteReal;
float parteImaginaria;
};
tienen los métodos crear y print que son miembros públicos, accesibles desde fuera de la clase.
Para acceder a los métodos de los objetos se utiliza como para acceder sus datos miembro, la
notación “punto”:
c1.crear(0.0, 1.0); // c1 contiene el número complejo 0 + 1 i
c1.print(); // imprimirı́a 0 + 1 i
5.5 Métodos 7

Respecto al estilo empleado en C, estas llamadas son extrañas ya que se conoce al objeto al que
se llama, no poniéndolo como parámetro, sino anteponiéndolos como prefijo.
No todos los métodos de una clase tienen que ser públicos. Por ejemplo:
class Fraccion {
public:
void crear(int num, int denom);
void print();
private:
void reduce();
int numerador, denominador;
La filosofı́a subyacente es la de ofertar (hacer público) sólo lo necesario e imprescindible para,
por un lado no confundir innecesariamente al usuario del objeto y, por otro, evitar errores en la
manipulación de los mismos.
Hasta ahora sólo hemos definido los métodos de las clases y objetos de aquéllas, pero ¿dónde
están los algoritmos que realizan tales métodos?
Una posibilidad es definir cada método más adelante, fuera de la definición de la clase. Por
ejemplo, en el caso de la función crear de las fracciones:

void Fraccion.crear(int num, int denom)


{
numerador = num;
denominador = denom;
reduce();
}

Nótese cómo Fraccion:: precede el nombre de la función. Esta es la notación que C++ emplea
para distinguir funciones ordinarias de los métodos propios de una clase. Observar que crear tiene
acceso directo a los datos miembros de la clase propia. En general esto es ası́ ya sean los datos
públicos o privados.
En vez de definir posteriormente los métodos de una clase, se pueden resolver inline sobre la
marcha en la propia definición de la clase:
class Fraccion {
public:
void crear(int num, int denom)
{numerador = num; denominador = denom; reduce();};
void print();
private:
void reduce();
int numerador, denominador;
esto es conveniente, sólo si el cuerpo del método es corto.
Añadamos el método mul para multiplicar modificando ésta por otra fracción:
class Fraccion {
public:
void crear(int num, int denom);
void print();
Fraccion mul(Fraccion f); // método para multiplicar
private:
void reduce();
int numerador, denominador;
Después tendrı́amos que escribir la definición (antes escribimos la declaración) de la función
mul:
5.6 Constructores y Destructores 8

Fraccion Fraccion::mul(Fraccion f)
{
Fraccion res;

res.numerador = numerador * f.numerador;


res.denominador = denominador * f.denominador;
res.reduce();
return res;
}

Inicialmente la función mul puede parecer algo misteriosa: está claro cuál es uno de los mun-
tiplicandos, el argumento f, pero ¿dónde está el otro? La respuesta está en la forma en la que mul
es llamado:
f3 = f1.mul(f2);

5.6. Constructores y Destructores


Para asegurar que las instancias de una clase se inicializan adecuadamente la clase puede tener
unos funciones especiales denominadas constructores. Ası́mismo la clase puede tener destruc-
tores, funciones que arreglan y dejan las cosas como estaban antes de eliminarse el objeto. Lo
interesante en principio respecto a los constructores y destructores es que son llamados automáti-
camente sin una llamada explı́cita. Hay que tener cuidado con esto.
Nosotros en nuestro ejemplo, hasta ahora, hemos usado una llamada explı́cita crear para
asignar valores a los atributos. Dada la importancia de la inicialización y la limpieza posterior C++
soporta el uso de funciones contructores y destructores automáticamente llamados en el momento
de la creación de una nueva instancia de la misma y en el momento de su destrucción, en el
momento en que la instancia deja de existir, generalmente esto último ocurrirá cuando la función
en la que está el objeto local deje de existir.
La sintaxis elegida en C++ es de que toda función con el mismo nombre de la clase que
no devuelve nada (ni void siquiera) es un constructor. Mientras que toda función con el mismo
nombre de la clase antecedido de una tilde ~ es un destructor.
En nuestro ejemplo:
class Fraccion {
public:
Fraccion(int num, int denom)
{numerador = num; denominador = denom; reduce(); }
...
};
Notemos que el constructor va en la parte pública de la clase. Los constructores pueden ser llama-
dos explı́citamente como los otros métodos pero lo más interesante de ellos es que son llamados
implı́citamente en el momento de crear una nueva instancia del objeto:
Fraccion f(7,3); // declara e inicializa f a 7/3
Pero ¿qué ocurrirı́a con la declaración?
Fraccion muchasFracs[3];
El compilador se quejarı́a inmediatamente de que no estamos poniendo los argumentos exigidos
en la llamada implı́cita de construcción de cada instancia de fracción.
Una primera solución a esto (como veremos en §5.7) serı́a crear un constructor más sobrecar-
gando el anterior pero sin parámetros. Como veremos esta es una solución factible, pero la más
adecuada es la de usar parámetros por defecto:
5.6 Constructores y Destructores 9

class Fraccion {
public:
Fraccion(int num=0, int denom=1)
{numerador = num; denominador = denom; reduce(); }
...
};
que, como sabemos permite mucha mayor flexibilidad en las llamadas. Podrı́amos crear:
Fraccion f(7,3); // 7/3
Fraccion f(7); // 7/1
Fraccion f; // 0/1
Fraccion f[10]; // 10 fracciones f[0]..f[9] inicializadas a 0/1
Los constructores y destructores son especialmente importantes cuando los objetos necesitan
adquirir dinámicamente memoria del sistema para almacenar sus datos (atributos) mediante new
y delete.
Veamos un ejemplo. La clase Cadena puede contener cadenas de caracteres de cualquier lon-
gitud. La podemos definir:
class Cadena {
..
private:
char *texto;
int long;
};
O sea un puntero a la ristra de caracteres y, optimizando mucho su cálculo, una variable que recoja
su longitud.
La manera más adecuada de usar estos objetos serı́a inicializándolos con un valor y posterior-
mente modificándolos. Tendrı́amos:
class Cadena {
public:
Cadena(const char *s); // constructor
private:
char *texto;
int long;
}
Y entonces:

Cadena::Cadena(const char *s)


{
long = strlen(s);
text = new char[long+1];
strcpy(text, s);
}

Después de calcular la longitud de la cadena a la que apunta s el constructor llama al operador


new para conseguir espacio de memoria suficiente para copiar la cadena; finalmente el constructor
copia la cadena en el espacio recién conseguido.
Al construir objetos de tipo Cadena harı́amos:
Cadena c1("Hola"), c2("y adiós.");
Para que esta clase tenga algún interés habrı́a lógicamente que añadirle métodos para modificar y
leer la cadena. Ası́mismo podrı́amos tener un constructor con valor por defecto nulo:
5.7 Sobrecarga 10

class Cadena {
public:
Cadena(const char *s = 0); // constructor
...
}
y después:

Cadena::Cadena(const char *s)


{
if (s == 0) {
long = 0;
text = 0;
} else {
long = strlen(s);
text = new char[long+1];
strcpy(text, s);
}
}

La cuestión viene cuando este objeto deje de existir, ya sea porque era local a una función
o porque se acabe el programa, etc. Entonces habrı́a que eliminar la memoria conseguida con
new mediante el delete adecuado. C++ aporta un destructor por defecto que si se dice nada lo
único que hace es liberar la memoria que ocupaban los miembros de la instancia de la clase. Esto
es lo usual también con variables estáticas. Sin embargo C++ es incapaz de invertir el algoritmo
de construcción empleado para almacenar la cadena de caracteres en el objeto y ası́ liberar la
memoria que usaba. En este caso es pues necesario añadir un desctructor explı́cito a la clase que
será llamado automáticamente por C++ cuando el objeto desaparezca:
class Cadena {
public:
Cadena(const char *s = 0);
~Cadena() { delete[] text; } // destructor
...
}

5.7. Sobrecarga
En C++ dos o más funciones dentro del mismo ámbito pueden compartir el mismo nombre.
Cuando las funciones están sobrecargadas es el compilador el que decide en base a la firma
(signature) de la función cuál es la que debe ser llamada. Sólo en caso de ambigüedad el compi-
lador no podrı́a decidir. La firma está compuesta del nombre, argumentos, tipo y modo de uso de
los argumentos y tipo y modo del posible valor devuelto.
Ası́ podrı́amos tener:

void intercambiar(int& a, int &b);


void intercambiar(char& a, char &b); // sobrecargada
y después
intercambiar(a_int, b_int); // llamarı́a a la primera
intercambiar(letraa, letrab); // llamarı́a a la segunda
Las funciones miembro de una clase también pueden ser sobrecargadas (lo cual es el uso más
frecuente de la sobrecarga). En el caso de Cadena es más fácil utilizar la sobrecarga que considerar
aparte el caso de no tener parámetros el constructor:
5.8 Entrada y salida en C++ 11

class Cadena {
public:
Cadena(const char *s = 0);
Cadena() {text = 0; long =0; } // sobrecargada
...
}
Además de admitir la sobrecarga de funciones, C++ admite la sobrecarga de operadores. De
hecho esto ya es habitual en el caso de los tipos simples predefinidos. Por ejemplo, podemos hacer
3*2 llamando ası́ al operador multiplicación de enteros o 3.1*2.0 que llama a un operador distinto,
el multiplicador de números reales. La sobrecarga de operadores es particularmente interesante
para redefinir los operadores usuales ampliando sus posibles argumentos con los nuevos objetos. El
resultado es un aspecto mucho más natural de las operaciones que reutilizan nombres u operadores
conocidos en vez de tener que recordar multitud de nombres nuevos para cada tipo u objeto nuevo.
En el caso de Fraccion serı́a mucho más fácil el uso de la multiplicación reemplazando el
método mul por el operador *. Hacer esto es tan fácil como substituir el nombre mul por operator*
en la declaración y definición del método:
class Fraccion {
public:
...
Fraccion operator*(Fraccion f); // método para multiplicar
private:
...
La implementación de operator* es idéntica que la de mul.
Cuando un operador se define como método de una clase uno de sus operandos está siempre
implı́cito. Ası́, el operador que acabamos de definir es un operador binario, no unario y cuando
escribimos sentencias como:
f1 * f2;
el compilador nota que f1 es un objeto Fraccion y mira en la clase Fraccion buscando un operador
operator* para convertir la expresión en:
f1.operator*(f2);

5.8. Entrada y salida en C++


Aunque los programas en C++ pueden utilizar perfectamente <stdio.h>, C++ dispone de una
alternativa a la clásica librerı́a de entrada y salida. La cabecera más importante de la nueva librerı́a
es <iostream.h> que define varias clases, incluyendo istream (canal de entrada, teclado, etc.) y
ostream (canal de salida, pantalla, etc.). Los programas simples que toman su entrada del teclado
y presentan sus resultados en la pantalla usan el objeto cin para la entrada y el objeto cout para
la salida. cin es una instancia de la clase istream y cout es una instancia de la clase ostream.
Las clases istream y ostream tienen muchos operadores sobrecargados para permitir la salida
y entrada de los tipos estándard. La clase istream sobrecarga >> mientras que la clase ostream
sobrecarga a <<. De esta forma una sesión interactiva serı́a:
cout << "Introducir un número: ";
cin >> n;
cout << "su cuadrado es: " << n*n << endl;

Recordemos que estamos llamando a los operadores ostream::operator<< de la instancia cout


de la clase ostream y al operador istream::operator>> de la instancia cin de la clase istream.
Estos están fuertemente recargados, para argumentos de tipo cadena, número, etcétera.
Entre las ventajas de usar la librerı́a iostream están:
5.8 Entrada y salida en C++ 12

Podemos ampliar la entrada y salida usándo los mismos operadores para las clases que
definamos.
Es más segura ya que no se producen nunca incoherencias entre el tipo de datos esperado en
el formato de printf o scanf con el de los parámetros pasados. Estas incoherencias llevan
fácilmente, en el caso de scanf a graves fallos de funcionamiento.
Es más rápida ya que la decisión de qué algoritmo de salida para el tipo hay que usar se toma
en el momento de compilar y no hay que interpretar el formato en momento de la ejecución.
Hay que decir también que printf y scanf son más flexibles y potentes.
La jerarquı́a de clases de la que deriva <iostream> se esquematiza en la Figura 1:

ios_base

ios

istream ostream

iostream
ifstream ofstream

istringstream ostringstream

fstream stringstream

Figura 1: Jerarquı́a de la que derivan las clases para manipulación de la entrada y salida estándares
<iostream> y de ficheros fstream.

Juan Falgueras
Dpto. Lenguajes y Ciencias de la Computación
Universidad de Málaga
Despacho 3.2.32