De los TADs a los Objetos Andrés Becerra Sandoval 28 de julio de 2014 1 TAD frac Los Tipos Abstractos de Datos modelan conjuntos de datos, sus invariantes y las operaciones permitidas sobre ellos. Un ejemplo es el TAD frac para representar números racionales: TAD frac num den invariante: num y den son enteros, den6=0 crear: entero x entero . frac suma: frac x frac . frac producto: frac x frac . frac comparar: frac x frac . entero Su cabecera o interfaz, escrita en C, se encuentra a continuación: frac.h #i f n d e f FRAC H #d e f i n e FRAC H typedef struct f r a c f r a c ; struct f r a c { int num ; int den ; }; f r a c c r e a r ( int num , int den ) ; void p r i n t F r a c ( f r a c f ) ; f r a c suma ( f r a c a , f r a c b ) ; f r a c producto ( f r a c a , f r a c b ) ; int comparar ( f r a c a , f r a c b ) ; #endif Y su implementación es: 1 frac.c #include<f r a c . h> #include<s t d i o . h> int e u c l i d e s ( int a , int b ) { i f ( a%b==0) return b ; else return e u c l i d e s ( b , a%b ) ; } f r a c c r e a r ( int num , int den ) { frac res ; int mdc ; mdc = e u c l i d e s (num , den ) ; r e s . num = num/mdc ; r e s . den = den /mdc ; return r e s ; } void p r i n t F r a c ( f r a c f ) { p r i n t f ( ”%d/%d” , f . num , f . den ) ; } f r a c suma ( f r a c a , f r a c b ) { int num , den ; num = a . num ∗ b . den + a . den ∗ b . num ; den = a . den ∗ b . den ; return c r e a r (num , den ) ; } f r a c producto ( f r a c a , f r a c b){ int num , den ; num = a . num ∗ b . num ; den = a . den ∗ b . den ; return c r e a r (num , den ) ; } int comparar ( f r a c a , f r a c b ) { int x , y ; x = a . num ∗ b . den ; y = a . den ∗ b . num ; i f ( x<y ) return −1; i f ( x==y ) return 0 ; i f ( x>y ) return 1 ; } 2 Una pequeña prueba del TAD frac es: main.c #include <s t d i o . h> #include <f r a c . h> int main ( ) { f r a c uno , dos , t r e s ; p r i n t f ( ”Empezando\n” ) ; uno = c r e a r ( 2 , 3 ) ; dos = c r e a r ( 1 , 4 ) ; t r e s = suma ( uno , dos ) ; p r i n t F r a c ( uno ) ; p r i n t f ( ”+” ) ; p r i n t F r a c ( dos ) ; p r i n t f ( ”=” ) ; p r i n t F r a c ( t r e s ) ; t r e s = p r o d u c t o ( uno , dos ) ; p r i n t f ( ” \n” ) ; p r i n t F r a c ( uno ) ; p r i n t f ( ”∗ ” ) ; p r i n t F r a c ( dos ) ; p r i n t f ( ”=” ) ; p r i n t F r a c ( t r e s ) ; p r i n t f ( ” \n” ) ; p r i n t f ( ”%d” , comparar ( uno , dos ) ) ; return 0 ; } 2 Empezando a usar C++ El lenguaje C++ expande las capacidades lenguaje C. El TAD frac puede declararse e implementarse en C++ como sigue: 3 frac.h #i f n d e f FRAC H #d e f i n e FRAC H using namespace s t d ; int e u c l i d e s ( int a , int b ) { i f ( a%b==0) return b ; else return e u c l i d e s ( b , a%b ) ; } struct f r a c { int num ; int den ; // c o n s t r u c t o r f r a c ( int num , int den ) { int mdc = e u c l i d e s (num , den ) ; this−>num = num/mdc ; this−>den = den /mdc ; } void p r i n t F r a c ( ) { c o u t << num << ” / ” << den ; } f r a c suma ( f r a c o ) { int n = this−>num ∗ o . den + den ∗ o . num ; int d = this−>den ∗ o . den ; return f r a c ( n , d ) ; } f r a c producto ( f r a c o ){ int num , den ; num = this−>num ∗ o . num ; den = this−>den ∗ o . den ; return f r a c (num , den ) ; } bool comparar ( f r a c o ) { int x , y ; x = num ∗ o . den ; y = den ∗ o . num ; return x==y ; } }; #endif 4 Observe que en C++: • las operaciones pueden estar adentro del struct • las operaciones se llaman con la sintaxis v.operacion(argumentos), donde v es una variable del tipo del struct. Esto se verá en la implementación mas adelante • En las operaciones suma, producto y comparar no usamos dos parámetros frac, porque la variable actual, de tipo frac, ya tiene un numerador y un denominador. Este se opera con el otro frac que llega como parámetro. • Cuando se define suma con un solo parámetro o, se toma el numerador y denominador de quien esté haciendo el llamado y se operan con o.num y o.den. • this es un puntero que, dentro de una operación en un struct, hace referencia a quien esté haciendo el llamado • hay una operación frac, con el mismo nombre del struct (un constructor!) • existen espacios de nombres (using namespace std;) • la salida se hace con cout, no con print • existe el tipo de dato bool La prueba, escrita en C++ queda ası́: 5 main.cc #include <i o s t r e a m > #include <f r a c . h> using namespace s t d ; int main ( ) { c o u t << ”Empezando” << e n d l ; f r a c uno ( 2 , 3 ) ; f r a c dos ( 1 , 4 ) ; frac tres (0 ,1); t r e s = uno . suma ( dos ) ; uno . p r i n t F r a c ( ) ; c o u t << ”+” ; dos . p r i n t F r a c ( ) ; c o u t << ”=” ; t r e s . p r i n t F r a c ( ) ; c o u t << e n d l ; t r e s = uno . p r o d u c t o ( dos ) ; uno . p r i n t F r a c ( ) ; c o u t << ” ∗ ” ; dos . p r i n t F r a c ( ) ; c o u t << ”=” ; t r e s . p r i n t F r a c ( ) ; c o u t << e n d l ; c o u t << ”Es ” ; uno . p r i n t F r a c ( ) ; c o u t << ” i g u a l a ” ; dos . p r i n t F r a c ( ) ; c o u t << ” ? ” << e n d l ; c o u t << uno . comparar ( dos ) ; return 0 ; } Observe que: • cuando se declara una variable de tipo frac, se llama la operación frac (el constructor) • todas las variables de tipo frac tienen adentro todas las operaciones. Cuando se llama uno.suma(dos) la variable uno usa su operación suma, con el argumento dos. Este argumento va a reemplazar el parámetro o, en la definición de suma. • Cuando se llama a uno.suma(dos), el puntero this hace referencia la variable uno. 3 Separando el contrato de la implementación Nuestro TAD frac queda ahora separado en: 6 frac.h #i f n d e f FRAC H #d e f i n e FRAC H struct f r a c { int num ; int den ; f r a c ( int num , int den ) ; void p r i n t F r a c ( ) ; f r a c suma ( f r a c o ) ; f r a c producto ( f r a c o ) ; bool comparar ( f r a c o ) ; }; #endif La implementación, en la siguiente página, usa el operador :: que identifica a que struct pertenece una función declarada en una cabecera. Este operador es el de resolución de ámbito. Aparte del uso del operador ::, el código es el mismo que en la versión anterior. 7 frac.cc #include<f r a c . h> #include<i o s t r e a m > using namespace s t d ; int e u c l i d e s ( int a , int b ) { i f ( a%b==0) return b ; else return e u c l i d e s ( b , a%b ) ; } f r a c : : f r a c ( int num , int den ) { int mdc ; mdc = e u c l i d e s (num , den ) ; this−>num = num/mdc ; this−>den = den /mdc ; } void f r a c : : p r i n t F r a c ( ) { c o u t << num << ” / ” << den ; } f r a c f r a c : : suma ( f r a c o ) { int n = this−>num ∗ o . den + den ∗ o . num ; int d = this−>den ∗ o . den ; return f r a c ( n , d ) ; } f r a c f r a c : : producto ( f r a c o ){ int num , den ; num = this−>num ∗ o . num ; den = this−>den ∗ o . den ; return f r a c (num , den ) ; } bool f r a c : : comparar ( f r a c o ) { int x , y ; x = num ∗ o . den ; y = den ∗ o . num ; return x==y ; } 8 4 Mas detalles de C++ • Un struct es casi idéntico a una clase en C++. • Algunos elementos de una clase pueden declararse públicos y otros privados, esto permite el encapsulamiento • Una operación puede definirse de varias formas, variando los parámetros, esto permite el polimorfismo (un nombre, varias implementaciones) • A los operadores del lenguaje como a << y == se le pueden dar nuevos significados por medio de operaciones, esto permite la sobrecarga frac.h #i f n d e f FRAC H #d e f i n e FRAC H #i n c l u d e <i o s t r e a m > class frac { private : int num ; int den ; public : frac (); f r a c ( int n ) ; f r a c ( int num , int den ) ; friend s t d : : ostream& operator<<(s t d : : ostream& os , const f r a c & f ) ; f r a c suma ( f r a c o ) ; f r a c producto ( f r a c o ) ; bool operator== ( const f r a c& o ) const ; }; #endif La implementación: • sobrecarga los operadores antes mencionados • usa ostream, un flujo de salida • utiliza el operador de paso por referencia (&), una caracterı́stica que no existe en el lenguaje C 9 frac.cc #include<f r a c . h> #include<i o s t r e a m > using namespace s t d ; int e u c l i d e s ( int a , int b ) { i f ( a%b==0) return b ; else return e u c l i d e s ( b , a%b ) ; } frac : : frac (){ num = 0 ; den = 1 ; } f r a c : : f r a c ( int n ) { num = n ; den = 1 ; } f r a c : : f r a c ( int num , int den ) { int mdc ; mdc = e u c l i d e s (num , den ) ; this−>num = num/mdc ; this−>den = den /mdc ; } s t d : : ostream& operator<<(s t d : : ostream& os , const f r a c & f ) { o s << f . num << ” / ” << f . den ; return o s ; } f r a c f r a c : : suma ( f r a c o ) { int n = this−>num ∗ o . den + den ∗ o . num ; int d = this−>den ∗ o . den ; return f r a c ( n , d ) ; } f r a c f r a c : : producto ( f r a c o ){ int num , den ; num = this−>num ∗ o . num ; den = this−>den ∗ o . den ; return f r a c (num , den ) ; } bool f r a c : : operator== ( const f r a c& o ) 10 const { int x , y ; x = num ∗ o . den ; y = den ∗ o . num ; return x==y ; } La prueba del TAD frac ahora permite que variables de tipo frac puedan ser operadas con los operadores << y ==. Además usa las tres versiones del constructor con 0, 1 y 2 argumentos. main.cc #include <i o s t r e a m > #include <f r a c . h> using namespace s t d ; int main ( ) { c o u t << ”Empezando” << e n d l ; f r a c uno ( 1 , 4 ) ; f r a c dos ( 2 ) ; frac tres (); c o u t << ”Es ” << uno << ” i g u a l a ” << dos << ” ? ” << ( uno==dos ) ; return 0 ; } 5 No se preocupe Si no ha comprendido bien la implementación del TAD frac en C++, vamos a tener tiempo durante todo el semestre. Lo que si debe hacer es compilar y ejecutar las diferentes implementaciones del TAD para las secciones anteriores y dejar que la curiosidad lo invada! El código está en: http://cic.javerianacali.edu.co/~abecerra/files/tadFrac.tar.gz sección carpeta 1 frac 2 frac++0 3 frac++1 4 frac++2 También, ejecute la implementación del TAD frac en python (probado con la versión 2.7, no con python 3). Allı́ el parámetro self hace las veces del puntero this en C++. 11 fraccion.py # −∗− c o d i n g : u t f −8 −∗− class frac : def i n i t ( s e l f , num , den =1): s e l f . num = num s e l f . den = den str ( self ): def return ”%d/%d” % ( s e l f . num , s e l f . den ) def mul ( s e l f , otro ) : i f type ( o t r o ) == type ( 5 ) : otro = frac ( otro ) return f r a c ( s e l f . num ∗ o t r o . num , s e l f . den ∗ o t r o . den ) rmul = mul #suma de f r a c e s def add ( s e l f , otro ) : i f type ( o t r o ) == type ( 5 ) : otro = frac ( otro ) return f r a c ( s e l f . num ∗ o t r o . den + s e l f . den ∗ o t r o . num , s e l f . den ∗ o t r o . den ) radd = add def i n i t ( s e l f , num , den =1): g=e u c l i d e s (num , den ) s e l f . num=num / g s e l f . den=den / g def cmp ( s e l f , otro ) : d i f = ( s e l f . num ∗ o t r o . den − o t r o . num ∗ s e l f . den ) return d i f d e f e u c l i d e s (m, n ) : i f m%n==0: return n else : return e u c l i d e s ( n ,m%n ) a = frac (8 ,4) b = frac (2 ,3) print a print b p r i n t a+b p r i n t a ∗b p r i n t 5∗ a p r i n t b∗5 p r i n t b+2 12 Conceptos valor: un patrón de bits que representa algo del mundo real. Puede ser primitivo en el lenguaje de programación o creado por el programador. Ej: 5, 2.1 tipo: un tipo abstracto de datos modela un conjunto de valores, sus invariantes y las operaciones permitidas. Además tiene soporte en el lenguaje de programación. Hay tipos primitivos y creados por el programador. Ej: int, frac. clase: tipo definido por el programador. Ej: frac. objeto: valor cuyo tipo es una clase. variable: un valor, cargado en la memoria, al que se puede acceder mediante un identificador (nombre). Tienen un tipo primitivo o creado por el programador. polimorfismo: usar un nombre único para acceder a varias operaciones diferentes. encapsulamiento: proteger la estructura interna de los objetos. sobrecarga: dar varios significados a una misma entidad. 13