INTRODUCCION a C++ • El Lenguaje de Programación C++ se diseñó para : • mejorar el lenguaje C • apoyar la abstracción de datos • apoyar la programación orientada a objetos • Un lenguaje apoya un estilo de programación cuando contiene elementos que facilitan la programación con esa técnica. • C++ es un superconjunto de C. C++ hereda de C lo siguiente: funciones, aritméticas, selección y construcciones cÃ−clicas, operaciones de E/S, manejo de punteros • El mÃ−nimo programa en C++ es : main () {} al igual que en C, todo programa en C++ debe tener una función main y el programa comienza ejecutando esa función ALGUNAS MEJORAS MENORES DEL C Comentarios : // permite introducir comentarios hasta el fin de lÃ−nea Nombres de datos enumerados : el nombre de una enumeración es un nombre de tipo, por lo tanto simplifica la escritura de un programa : enum { a, b, c=0 } enum { d, e, f=e+2 } enum color { rojo, amarillo, verde=20, azul } color col = rojo color* cp = &col if(*cp == azul) // … Nombres de clases y estructuras : los nombres de clases y estructuras son nombres de tipos en C++. Las clases no existen en C. En C++ no se necesita colocar el calificador struct o class delante de un nombre de estructura o clase. Declaraciones en bloques : C++ permite la introducción de declaraciones de variables dentro de bloques y antes de sentencias ejecutables. Esto permite declarar un identificador más cerca de su punto de aplicación. for (int k=0; k<10; k++) cout << “el valor de k es : ” << k << `\n' ; 1 Operador de alcance : el operador de alcance :: es un nuevo operador y permite resolver conflictos de nombre. Por ejemplo si se tiene una función local con una variable llamada vector y también se tiene una variable global llamada vector, el calificador ::vector permite acceder a los valores de la variable global. Lo contrario no se puede hacer. Especificador const: sirve para : fijar el valor de una entidad dentro de su alcance, fijar el dato apuntado por una variable puntero, el valor de la dirección del puntero o ambos a la vez (puntero y valor). Conversiones explÃ−citas de tipo : se puede emplear un tipo predefinido o tipo definido por el programador como una función para convertir datos de un tipo a otro. Bajo ciertas circunstancias se puede emplear la conversión explÃ−cita de tipo como una alternativa a una conversión por asignación. Sobrecarga de funciones : En C++ varias funciones pueden utilizar los mismos nombres de función, cada una de las funciones sobrecargadas puede distinguirse gracias al número y tipo de parámetros. Valores por omisión de los parámetros de una función : Podemos asignar valores por omisión a los parámetros finales de las funciones de C++. De modo tal que se puede llamar a una función con menos parámetros de los definidos. Funciones con número no especificado de parámetros : Empleando … se pueden definir funciones con un número no definido de parámetros. No se chequean los tipos de los parámetros utilizados para permitir flexibilidad en el uso. Parámetros por referencia en una función : Utilizando el operador &, podemos declarar un parámetro de función formal como un parámetro por referencia : void incremento (int& valor) { valor++; } int i; incremento(i); Cuando se llama a la función incremento se asigna a la dirección de valor la dirección de i. En incremento se incrementa el valor de i y se devuelve a la función que lo llamó. No es necesario pasar la dirección de i como ocurrÃ−a en C, con lo que simplifica la codificación. Operadores new y delete : Los operadores new y delete son introducidos en C++ para asignar y desasignar memoria dinámicamente. MEJORAS IMPORTANTES RESPECTO a C Estas mejoras están directamente relacionadas con la Programación Orientada a Objetos (POO) Constructores de clases y encapsulamiento de datos : Una clase puede contener en su definición las 2 declaraciones de datos, los valores iniciales y el conjunto de operaciones (métodos) para la abstracción de datos. Se crean objetos a partir de una clase dada. Entre objetos se envÃ−an mensajes. Cada objeto puede contener un conjunto público y privado de datos. Estructura de clases : Una struct en C++ es un subconjunto de una definición de clase, con todos sus miembros públicos. La struct puede contener datos y funciones. Constructores y destructores : se emplean para inicializar objetos de una clase determinada. Cuando se declara un objeto se activa el constructor. Los destructores desasignan la memoria del objeto involucrado. Esto se puede hacer explÃ−citamente o automáticamente cuando se sale del ámbito de declaración del objeto. Mensajes : En C++ los mensajes se envÃ−an con un mecanismo similar al llamado de una función. Por lo general se invoca a una función miembro del objeto especificado pasando los parámetros definidos para esa función. mi_objeto.mi_metodo(5); Sobrecarga de operadores : C++ podemos redefinir el conjunto de operadores suministrados por el compilador, de modo tal que puedan ser adaptados a los tipos definidos por el usuario. Esto permitirÃ−a definir operadores (+,*,/, etc.) para que operen con un tipo definido por el usuario del mismo modo que los tipos fundamentales. Clases derivadas : una clase derivada es una subclase de una clase. Este es lo que permite implementar herencia entre los objetos. Un objeto puede heredar el todo o un parte de la superclase. Las subclases heredan la parte pública de la clase madre (superclase), pero no puede heredar la parte privada. Polimorfismo : es la capacidad que tienen los objetos de responder de distinto modo a un mismo mensaje. En C++ la especificación de las clases derivadas, la sobrecarga de funciones y operadores permiten implementar esta importante caracterÃ−stica de la POO. Facilidades para las E/S : Las entradas y salidas pueden ser rápidamente definidas para ser adaptadas a los distintos objetos definidos por el usuario. cin, cout y cerr, forman una jerarquÃ−a de clases y objetos que pueden ser empleados fácilmente. Elementos del Lenguaje Un programa C++ está compuesto por una secuencia de componentes léxicos. Existen cinco componentes léxicos: identificadores, palabras claves, operadores, constantes y otro separadores. Identificadores : en general los nombres que asigna un programador a los: objetos, funciones, enumerador, tipo, miembro de clase, patrón, un valor ó un rótulo y o subprogramas (funciones) Palabras claves : palabras reservadas que el programador no puede utilizar de ninguna otra manera que no sea la asignada por el lenguaje. asm continue float new signed try auto default for operator sizeof typedef break delete friend private static union 3 case do goto protected struct unsigned catch double if public switch virtual char else inline register template void class enum int return this volatile const extern long short throw while Los identificadores con doble subrayado _ _ son empleados por algunos compiladores de C++ y bibliotecas estándar por lo que se recomienda no usarlos. Operadores : tienen una función especÃ−fica asignada por el programa y van acompañados de identificadores, literales, otros operadores, etc.. Los siguientes caracteres simples se emplean como operadores o signos de puntuación: !%^&*()-+={} |[]\;`:“<>?, ./ también se emplea la siguiente combinación de caracteres como operadores: -> ++ -- .* ->* << >> <= >= == != && || *= /= %= += -= <<= >>= &= ^= |= :: Literales : son los que por lo general se conocen como “constantes” y pueden ser : enteras, de caracteres, flotante (decimal ó real) cadena de caracteres. numérica : 123456 de caracteres : nueva lÃ−nea: \n tabulador horizontal: \t tabulador vertical: \v retroceso: \b retorno de carro: \r avance de página: \f alerta: \a diagonal invertida: \\ interrogación: \? apóstrofe: \' comillas: \” flotantes : 0.1233, 1233.e-04 cadena de caracteres : encerrada entre comillas “abcde” TIPOS FUNDAMENTALES DE DATOS char 4 unsigned short int long float double long double OPERACIONES SOBRE TIPOS sizeof : tamaño de new : asignar memoria del tipo delete : liberar memoria del tipo int main() { int *p = new int; cout << “tamaño de : ” << sizeof (p) << `\n' ; delete(p); } En C++ el nombre de tipo se emplea para la conversión explÃ−cita de un tipo a otro : float f; char *p; // … long k = long(p); // convertir p a un long int l = int (f) ; // convertir f a un int TIPOS DERIVADOS Por medio de los operadores de declaración se pueden derivar otros tipos : 5 * Puntero & Referencia [] Arreglo () Función además podemos definir estructuras (registros) por medio de la palabra struct int *a ; // puntero a un entero float v[10]; //arreglo 10 posiciones para nros. reales char *p[20]; // arreglo de 20 punteros a caracteres void f(int); struct estr{ short longitud; char *p); OPERADORES ARITMETICOS + sumar * multiplicar % residuo - restar / dividir En C++ en las operaciones de asignación o aritméticas las conversiones entre los tipos básicos se realizan automáticamente, esto implica que los tipos se pueden mezclar libremente OPERADORES DE COMPARACION = = igual que < menor que <= menor o igual que != distinto de > mayor que >= mayor o igual que DECLARACIONES Antes de que un nombre sea utilizado este debe haberse declarado y además debe haberse definido su tipo : char car; int cuenta = 1; char *nombre=”Juan”; struct complejo {float re, im}; complejo varcom; extern complejo sqrt (complejo); 6 extern int numero_error; const double pi=3.1415926535897932385; enum perro{Bulldog, Terrier, Pekines}; struct usuario; La mayor parte de estas declaraciones son también definiciones definen la entidad a la que se referirá el nombre car, cuenta, nombre, varcom un lugar en la memoria con un valor asociado struct usuario no son definiciones extern complejo sqrt (complejo); son declaraciones extern int numero_error; de nombres las entidades a las que se refieren se definirán en otro lugar ALCANCE Una declaración tiene un alcance determinado un nombre se puede emplear en un determinado lugar del programa locales : se emplean dentro de una función especÃ−fica globales : no pertenecen a una función, su alcance va desde donde se declaró hasta el fin de archivo La declaración de una variable local OCULTA a la de la variable global int x; // x global void f() { int x; // x local oculta a x global x = 1; // asignación a x local { int x; // oculta a la primera x local 7 x = 2; // asignación a la segunda x local } x = 3; // asignación a la primera x local } int *p = &x; // toma la dirección de x global En programas grandes la ocultación de nombres es inevitable su uso debe evitarse por ser frecuente fuente de errores Es posible emplear el nombre de una variable global dentro del ámbito local ( operador alcance :: ) int x; void f2() { int x = 2; // oculta a x global ::x = 3; // asigna el valor 3 a x global } No hay modo de utilizar un nombre local oculto NOMBRES • Identificador : secuencia de letras y dÃ−gitos • 1er. caracter debe ser una letra • subrayado bajo _ se considera una letra • C++ no impone lÃ−mite a los nombres pero las implementaciones si lo hacen (Borland C++ los 32 primeros son significativos) • Se pueden admitir ASCII extendidos limitan la portabilidad • Mayúsculas y minúsculas son distintas Identificadores válidos : hola este_es_un_nombre_largo DEFINIDO fo0 bAr var0 var10 CLASS _class Identificadores no_válidos : 012 un gato $sist class 3 var 8 num-cta dir~emp .nombre if TIEMPO DE VIDA (ámbito o visibilidad) Un objeto se crea cuando se llega a su definición Se destruye cuando se sale de su alcance (ámbito) Los objetos globales se crean e inicializan una sola vez, se destruyen cuando el programa termina Cuatro posibilidades de visibilidad : • bloque • función • archivo (static) • programa (extern) Objetos definidos con la palabra clave static se crean una sola vez y se destruyen al final del programa. Se inicializan la primera vez que el programa pasa por la declaración # include <iostream.h> int a void f() { int b=1; // se inicializa cada vez que se llama a f static int c=a; // inicializado una sola vez cout << “ a = ” << a++ << “ b = ” << b++ << “ c = ” << c++ << `\n' ; } int main() { while (a<4) f(); } Salida del programa : 9 a = 1 b= 1 c=1 a = 2 b= 1 c=2 a = 3 b= 1 c=3 El programador puede controlar el tiempo de vida de los objetos que crea con los operadores new y delete PUNTEROS T : Tipo fundamental de datos T* : puntero a un objeto del tipo T int *pi; // puntero a un entero char **aac; // puntero a un puntero de tipo char Para arreglos y funciones se tiene : int (*vp)[10]; // puntero a un arreglo de 10 enteros int (*fp)(char, char *); // puntero a una función que // recibe argumentos tipo char y char* y devuelve int Operación indirección : hace referencia al objeto que apunta el puntero char c1 = `a'; char *p = &c1; // p tiene la dirección de c1 char c2 = *p; // a c2 se le asigna el valor `a' Es posible realizar operaciones aritméticas con los punteros : int strlen (char *p) // calcula la longitud en caracteres { // de una cadena que termina en `\0' int i=0; // sin contar el cero final while (*p++) i++; return i; } ARREGLOS tipo T[tam] : especifica el arreglo de nombre T de tipo tipo 10 de tamaño tam, indizado de 0 a tam-1 float v[3]; // arreglo de tres float, v[0], v[1] y v[2] int a[2][5]; // matriz de enteros de 2 filas y 5 colum. char* vpc[32];// arreglo punteros char 32 posiciones Ejemplo : #include <string.h> char alfa[]=”abcdefghijklmnñopqrstuvwxyz”; main() { int tam=strlen(alfa); for (int i=0; i<tam; i++) { char car = alfa[i]; cout << car <<” = “<< int(car) <<'\n”; } } Salida : a = 97 b = 98 c = 99 …………………. No hace falta especificar el tamaño del arreglo alfa. El compilador asigna como tamaño la cadena especificada. Es el único caso en que se puede emplear el operador de asignación para una cadena de caracteres. char v[10]; v = “una cadena”; // error !!!!!!!! strcpy(v, “una cadena”); Para inicializar arreglos de otro tipo se necesita otra notación 11 int v1[] = {1, 2, 3, 4} int v2[] = {`a', `b', `c', `d'} char v3[]={1,2,3,4} char v4[]={`a', `b', `c', `d'} v3 y v4 son arreglos caracteres de 4 elementos que no tienen la marca de fin de cadena posibilidad de cometer errores PUNTEROS Y ARREGLOS El nombre de un arreglo puntero al primer elemento #include <string.h> char alfa[]=”abcdefghijklmnñopqrstuvwxyz”; main() { char *p=alfa, car; // otra alternativa char *p=&alfa[0] while(car = *p++) cout<<car<<” = “<< int(car) <<'\n”; } Cuando un arreglo se pasa como argumento a una función siempre se pasa por referencia (puntero al primer elemento del arreglo) #include <string.h> main() { char v[]= “Alejandra”; char *p= v; strlen(p); // en ambas llamadas se pasa el mismo valor strlen(v);// a strlen } Operaciones sobre punteros : 12 p apunta a un elemento del tipo T p+1 apunta al siguiente elemento p-1 apunta al elemento anterior Resta de punteros permitida cuando se apunta a elementos del mismo arreglo = al nro. de elemento que existen entre los punteros. Se pueden sumar o restar valores enteros a un puntero si se sale de los lÃ−mites resultado imprevisible void f() { int v1[10]; int v2[10]; int i=&v1[5]-&v1[3]; // resultado =2 i=&v1[5]-&v2[3]; // resultado imprevisible int *p= v2+2; // p=&v2[2] p=v2-3; //*p no definido } La mayorÃ−a de los compiladores C++ no chequean los lÃ−mites de los arreglos. ESTRUCTURAS Arreglo : agregado de elementos del mismo tipo Estructura â ¡ Registro : agregado de elementos de â tipo struct domicilio{ char *nombre; char *calle; long numero; char* ciudad; char estado[2]; int cod_post; }; // este es uno de los pocos lugares donde el usuario 13 // además de la llave debe poner un punto y coma Ahora se pueden declarar variables del tipo domicilio : domicilio js; //constructor para estructuras js.nombre= “Juan Samora”; js.numero= 61; También se puede declarar un arreglo de estructuras : domicilio SantaFe[100]; SantaFe[1].nombre= “Alberto Sosa”; Otro modo de inicializar una estructura es : domicilio js = { “Juan Samora”; “Avenida de los Naranjos”, 61 “Buenos Aires”,{`N', `L'},1021 }; Las estructuras se pueden asignar, pasar como argumento de función y devolver como resultado de función : domicilio actual; domicilio fijar_actual(domicilio siguiente) { domicilio previo=actual; actual = siguiente; return previo; } Operaciones como igualdad o diferencia no están definidas, por lo tanto no se deben utilizar, si se puede definir una función que lo haga. Tamaño de la estructura â de la suma de los tipos individuales sizeof(domicilio) El nombre de una estructura está disponible inmediatamente después de haberla declarado : 14 struct lista_doble{ int num; lista_doble* siguiente; lista_doble* previo; }; No es posible declarar un objeto de una estructura que no se ha definido completamente todavÃ−a : struct muestra{ muestra nueva; // error!! muestra no está }; // totalmente definida todavÃ−a Es posible reservar un nombre para que sea empleado más adelante : struct lista; struct nodo { nodo* next; nodo* previo; lista* nuevo; }; struct lista { nodo *tope; }; Ahorro de espacio Existen dos modos de exprimir (en el sentido de tratar de aprovechar al máximo) espacio en la memoria disponible : • Campos : colocar un objeto pequeño en un byte • Uniones : utilizar el mismo espacio para contener objetos diferentes en momentos distintos Estos recursos no son portables, por lo tanto, se debe pensar bien antes de usarlos. Campos Cuando se quiere emplear una variable binaria (0-1, verdadero-falso) se emplea generalmente un char que ocupa un byte, pero se pueden reunir una o más unidades pequeñas como campos de una struct. Un campo 15 de esta struct se especifica por medio del nombre seguido de la cantidad de bits que ocupa. struct regest { unsigned habilitar :1; unsigned pagina : 3; unsigned : 1; // no se usa sirve para mejorar la // disposición de bits unsigned modo : 2; unsigned : 4; unsigned acceso : 1; unsigned longitud : 1; }; Los campos se emplean como cualquier otra variable entera, pero no es posible obtener su dirección. Emplear campos no siempre ahorra espacio, porque por lo general se incrementa el tamaño del código requerido para manejar variables. Se hace referencia a un campo de la siguiente forma: struct regest reg1; reg1.acceso = 0; if (reg1.longitud == 0).... Uniones Supongamos una tabla de entrada que contiene nombre y valor, y el valor es una cadena de caracteres o un entero : struct entrada { char* nombre; char tipo; char* valor_cadena; int valor_entero; } void imprimir_entrada(entrada *p) { 16 switch(p->tipo) { case `c': printf(“%s”, p->valor_cadena) ; break; case `e': printf(“%d”, p->valor_entero) ; break; default : printf(“tipo corrompido\n”); break; } } Como no se puede emplear valor_cadena y valor_entero al mismo tiempo, se desperdiciará espacio, especificando que ambos son miembros de una unión se ahorra espacio : struct entrada { char* nombre; char tipo; union { char* valor_cadena; // utilizado si tipo ='c' int valor_entero; // utilizado si tipo = `e' }; }; El código que se escribió anteriormente sigue inalterado, al asignar un valor a una entrada, valor_entero y valor_cadena tienen la misma dirección los miembros de una unión ocupan el espacio requerido por el miembro más grande.FUNCIONES Y ARCHIVOS Por lo general, un programa está compuesto por varias unidades compiladas en forma independiente y que se encuentran en diferentes archivos. Esto facilita la legibilidad, modificación y/ o corrección del código. 17 Calificadores extern y static A no ser que se especifique lo contrario un nombre que no es local respecto de una función o clase debe referirse al mismo tipo, valor, función u objeto en todas las partes de un programa compiladas individualmente un programa sólo puede existir un tipo, valor, función u objeto no local con ese nombre // arch1.c int a=1; int f() {/* hacer algo */} // arch2.c extern int a; int f(); void g() { a = f(); } • La variable a y la función f() empleadas por g() en arch2.c son las que se definieron en arch1.c. • La palabra clave extern indica que la declaración de a en arch2.c es solo eso una declaración y no una definición. • Si se hubiera inicializado a se habrÃ−a ignorado la palabra extern porque una declaración con una inicialización es una definición. Un objeto se debe definir una y solo una vez en un programa, se puede declarar muchas veces pero los tipos deben concordar con exactitud // arch1.c int a=1; int b=1; extern int c; // arch2.c int a; // error !! a se define dos veces extern double b; // error!! b se declara dos veces con tipos // diferentes extern int c; // error!! c se declara dos veces pero no se // define Estos errores no los detecta el compilador (mira un archivo por vez), el ensamblador es el que lo hace Con la declaración static es posible hacer que un 18 nombre sea local a un archivo // arch1.c static int a=6; static int f() {/* …*/}; // arch2.c static int a=7 ; static int f() { /* …*} Cada archivo tiene su variable a y su función f() ARCHIVOS DE ENCABEZADO Los tipos de todas las declaraciones del mismo objeto o función deben ser consistentes. Un método para facilitar esto es la inclusión de archivos de encabezado que contienen código fuente y/o definiciones de datos. Directiva include sirve para poner fragmentos de un programa en un solo archivo. #include “archivo.cpp” se reemplaza esta lÃ−nea por el contenido de archivo.cpp (archivo fuente con código C++) #include <iostream.h> // búsqueda en el directorio estándar #include “iostream.h” // búsqueda en el directorio actual Cà MO ORGANIZAR UN ARCHIVO DE ENCABEZADO Definiciones de tipos struct punto{int c,y;}; Patrones template<class T> class V{…} Declaraciones de funciones extern int strlen (const char*); Definiciones de funciones en lÃ−nea inline char obt(){return *p++;} Declaraciones de datos extern int a; Definiciones de constantes const float pi=3.141593; Enumeraciones enum bool {falso, verdadero}; 19 Declaraciones de nombres class Matriz; Directivas de inclusión #include <signal.h> Comentarios //comprobar si abrió el archivo Un archivo de encabezado no deberÃ−a incluir : Definición de funciones ordinarias char obt(){return *p++;} Definición de datos int a; Por convención los archivos de encabezado llevan la extensión .h y los que tienen definiciones de funciones o datos llevan la extensión .c, .cpp, .cc, .cxx FUNCIONES Declaración - nombre de la función - valor devuelto (si lo hay) • tipo del/los argumento/s de llamada extern double sqrt(double); extern char* strcpy(char* a, const char* de); extern void exit(int); El compilador ignora los nombres de los argumentos que se ponen en la declaración Paso de argumentos : por valor : cuando se pasa el argumento se realiza una copia del mismo por referencia : la función emplea el argumento que se está pasando void f(int val, int &ref) { val++; ref++; } void g() { int i; 20 int j; f(i,j); } Llamadas por referencia : • pueden dificultar la lectura del programa • son útiles cuando se quieren pasar argumentos largos • si se quiere evitar que la función modifique el valor se los puede pasar como argumento const void f(const grande &arg) { // no se puede alterar el valor de arg // sin emplear una conversión explÃ−cita de tipo } Arreglos como argumentos Siempre se pasan por referencia no por valor. El tamaño no está disponible para la función de llamada. Si en la función de llamada necesitamos trabajar con las dimensiones del arreglo, se debe conocer la dimensión del mismo. void imprimir_matriz34(int m[3][4]) { for (int i=0; i<3; i++) for (int j=0; j<4; j++) cout << ` ` << m[i] [j] << `\n'; } // no hay problemas porque las dimensiones se conocen en // tiempo de compilación void imprimir_matriz34(int m[][4], int dim1) { for (int i=0; i<dim1; i++) for (int j=0; j<4; j++) cout << ` ` << m[i] [j] << `\n'; } // no hay problemas porque la 2da. dimensión se conoce en 21 // tiempo de compilación se puede calcular la ubicación // de un elemento void imprimir_matriz34(int m[][], int dim1, int dim2) //error { for (int i=0; i<dim1; i++) for (int j=0; j<dim2; j++) cout << ` ` << m[i] [j] << `\n'; } //PROBLEMAS porque las dimensiones NO SE CONOCEN // en tiempo de compilación Una posible solución para esto es : void imprimir_matriz34(int** m, int dim1, int dim2) { for (int i=0; i<dim1; i++) for (int j=0; j<dim2; j++) cout << ` ` << ((int*)m)[i*dim2+j] << `\n'; } Nombres de función sobrecargados Sobrecarga : mismo nombre de función para realizar tareas diferentes, generalmente manipulan objetos de distinto tipo Ejemplo : solo hay un nombre para la suma, +, pero puede manipular objetos del tipo entero, punto flotante y punteros void imprimir(int); // para imprimir un entero void imprimir(const char*) // para imprimir una cadena de // caracteres void imprimir(double); void imprimir(long); void f() { 22 imprimir(1L); //imprimir(long) imprimir(1.0); //imprimir(double) imprimir(1); //imprimir(int) } El compilador determina la función que debe llamar de acuerdo con los argumentos de llamada a la misma. Para ello determina las siguientes reglas de concordancia : REGLAS DE CONCORDANCIA DE ARGUMENTOS 1) Concordancia exacta : verifica que los argumentos concuerden exactamente sin emplear conversiones o haciéndolo con sólo las inevitables (nombre de arreglo a puntero, nombre de funciónpuntero a función, T a const T) 2) Concordancia empleando promociones integrales : char a int, short a int y sus contrapartes unsigned, float a double. 3) Concordancia empleando conversiones estándar : int a double, derivado* a base*, unsigned int a int. 4) Concordancia empleando conversiones definidas por el usuario 5) Concordancia empleando … en declaración de la función void imprimir(int); void imprimir(const char*) void imprimir(double); void imprimir(long); void imprimir(char); void h(char c, int i, short s, float f) { imprimir(c); // concordancia exacta imprimir(i); // concordancia exacta imprimir(s); // promoción integral imprimir(int) 23 imprimir(f); // promoción integral imprimir(double) imprimir(`a'); // concordancia exacta imprimir(49); // concordancia exacta imprimir(“a”); // concordancia exacta imprimir(const // char*) } Argumentos por omisión Se emplean cuando se necesitan más argumentos en el caso general que en el caso más simple que es el más frecuente • Solo es posible incluir argumentos al final de la lista de argumentos • Los argumentos que pueden omitirse deben tener su valor inicializado • El argumento se fija en la llamada a la función void imprimir(int valor, int base=10) void f() { imprimir(31); imprimir(31,10); imprimir(31,16); imprimir(31,2); } int f(int, int=0, char* =0); int g(int = 0; int = 0; char*) // error!! se debe fijar un valor // por omisión Número no-especificado de argumentos Se emplea cuando no es posible especificar el número y tipo de todos los argumentos de una llamada, se termina la declaración de una función de este tipo con … int printf(const char* …) // printf debe tener al menos un //argumento que es una cadena de caracteres printf(“Hola todo el mndo \n”); 24 printf(“Mi nombre es %s %s \n”, nombre, apellido); printf(“%d + %d = %d \n”,2,3,5); La sentencia : printf (“Mi nombre es %s %s \n”, 2); se compilará bien, pero tendrá una salida rara, en tiempo de compilación no es posible verificar los argumentos que tendrá. Un programa bien diseñado no deberÃ−a necesitar funciones de este tipo. Las funciones sobrecargadas y los argumentos por omisión hace que se tengan alternativas que haga que no sea necesario emplear este recurso Punteros a funciones Dos cosas se pueden hacer con una función : • llamarla • obtener su dirección El puntero de una función puede servir para llamarla, pero se debe poner el operador indirección * encerrado entre paréntesis porque el operador llamada a función () tiene mayor precedencia que el indirección void error (char*p) {…} void (*pointf) (char); // puntero a función void f() { pointf = &error; // pointf apunta error (*pointf) (“error”) // llama a la función error } si escribiéramos *pointf(“error”) *(pointf(“error”)) nos darÃ−a un error de tipo Cuando se declara un puntero a una función se debe tener en cuenta los tipos de los argumentos. Debe haber una concordancia exacta. void (*pf) (char*); void f1(char*); int f2(char*); 25 int f3(char*); void f() { pf = &f1; // correcto pf = &f2; // error!! tipo devuelto incorrecto pf = &f3 // error de tipo de argumento (*pf) (“asfd”); // correcto (*pf) (1); // error de tipo de argumento int i=(*pf) (“qwer”); // error!! void asignado a int } SOBRECARGA DE OPERADORES Tipos básicos definidas operaciones manejo fácil, cómodo, breve, convencional Las clases nos permiten especificar objetos no primitivos además de un conjunto de operaciones que se pueden llevar a cabo con esos objetos class complejo { double re, im; public: complejo(double r, double i) { re=r; im=i; } friend complejo operator+(complejo, complejo); friend complejo operator*(complejo, complejo); }; La definición de operator+ y operator* da al + y * un significado especial. Dados dos complejos a y b, a+b significa por definición operator+(a,b) void f() { complejo a = complejo(1,3.1) 26 complejo b = complejo(1.2, 2) complejo c = a; a=b+c; b = b+c*a; c = a*b + complejo(1,2); } Valen las mismas reglas de precedencia que con cualquier otro operador. Se pueden definir funciones para los siguientes operadores : +-*%^&|~! = < > += -= *= /= %= ^= /= << >> >>= <<= == != <= >= && || ++ -- ->* , -> [] () new delete No es posible : • alterar el orden de precedencia • no se puede modificar la sintaxis (ej.: no se puede utilizar un operador unario como binario o viceversa) • no es posible definir nuevos operadores solo se pueden redefinir los que están Una función operador es la que se define utilizando la palabra clave operator seguida del operador (ej.: operator<<). Se pueden invocar como cualquier otra función. Cuando se emplea el operador únicamente estamos empleando una abreviatura del operador. void f(complejo a, complejo b) { complejo c = a + b; // abreviado complejo d = operator+ (a,b) // llamada explÃ−cita } Operadores Binarios y Unarios Para cualquier operador genérico @ tenemos : si es binario puede ser implementado como una función miembro que recibe un argumento ó una función global que recibe dos argumentos aa@bb aa.operator@(bb) 27 operator@(aa,bb) si se definen ambas, la concordancia de argumentos le determinará que función le corresponde si el operador es unario y prefijo @aa aa.operator@() operator@(aa) si es posfijo : aa@ aa.operator@() operator@(aa) class X{ // miembros con apuntador this implÃ−cito X* operator&(); //&(dirección de) unario prefijo X operator&(X); //&(and) binario X operator++(int); // incremento posfijo X operator&(X,X); //error!! ternario X operator/(); // error!! unario }; // funciones globales (con frecuencia amigas); X* operator-(X); //- prefijo unario X operator-(X,X); //- binario X operator --(X&,int); // decremento posfijo X operator-(); //error!! falta operando X operator-(X,X,X); // error!! ternario Asignación e inicialización struct cadena{ char* p; int tamaño; // del vector que apunta p cadena(int tam) {p=new char[tamaño=tam];} ~cadena(){delete p;} 28 }; Cadena puntero a vector de caracteres y tamaño vector void f() { cadena c1(10); cadena c2(20); c1=c2; // problemas porque al salir de f se llama al //destructor de c1 y de c2, además } // c1 tiene â tamaño que c2 Redefiniendo el operador = struct cadena { char* p; int tamaño; cadena(int tam) {p = new char[tamaño=tam];} ~cadena() {delete p;} cadena & operator = (const cadena &); }; cadena& cadena::operator=(const cadena& a) { if (this != &a) { // tener en cuenta a=a delete p; p = new char[tamaño = a.tamaño]; strcpy(p,a.p); } return *this; } 29 Con esto se mejora la situación anterior pero no se evita lo siguiente : void f() { cadena c1(10); cadena c2=c1; // inicialización no es asignación } El constructor construye una cadena pero destruye dos. El operador asignación no se aplica a un objeto no inicializado. struct cadena { char* p; int tamaño; cadena(int tam) {p = new char[tamaño=tam];} ~cadena() {delete p;} cadena & operator = (const cadena &); cadena(const cadena&); //constructor de copia }; cadena::cadena(const cadena& a) { p = new char[tamaño = a.tamaño]; strcpy(p,a.p); } Para un tipo X, el constructor de copia X(const X&) se ocupa de la inicialización con un objeto del mismo tipo X. Un constructor debe poseer la máxima cantidad de funciones posibles : class X{ //… X(algo); // constructor de nuevos objetos 30 X(const X&); // constructor de copia operator=(const X&) //asignación : limpieza y copia ~X(); //destructor : limpieza }; SubÃ−ndices Una función operator[] sirve para asignar subÃ−ndices a objetos de clase, el segundo argumento (el subÃ−ndice de una función operator[], puede ser de cualquier tipo. class asoc{ struct pareja{ char *nombre int val; }; pareja* vec; int max; int libre; asoc(const asoc&); // evitar la copia asoc& operator=(const asoc&); // evitar la copia public: asoc(int); int &operator[](const char*); void imprimir_todo(); }; asoc es un vector de objetos pareja de tamaño max. El constructor de copia y el operador de asignación se mantienen privados para evitar la copia de arreglos asoc. asoc::asoc(int s) { max = (s<16)?s:16; 31 libre=0; vec=new pareja[max]; } #include <string.h> int& asoc::operator[](const char*p) /* administrador de objetos del tipo “pareja” : • buscar p • devolver una referencia a la parte entera de pareja • crear una nueva “pareja” si no se encuentra p */ { register pareja* pp; for (pp=&vec[libre-1]; vec<=pp; pp--) if strcmp(p,pp->nombre==0) return pp->val; if(libre==max) { // desborde amplair arreglo pareja* nvec= new pareja[max*2] for (int i=0; i<max; i++) nvec[i] = vec [i]; delete vec; vec=nvec; max=2*max; } pp=&vec[libre++]; pp->nombre=new char[strlen(p)+1] strcpy(pp->nombre,p); pp->val=0; //valor inicial=0 return pp->val; } 32 Conversiones de tipo Ejemplo de números complejos : class complejo { double re,im public: complejo(double r, double i) {re=r; im=i;} friend complejo operator+(complejo, complejo); friend complejo operator+(complejo, double); friend complejo operator+(double, complejo); friend complejo operator-(complejo, complejo); friend complejo operator-(complejo, double); friend complejo operator-(double, complejo); complejo operator-(); //unario friend complejo operator*(complejo, complejo); friend complejo operator*(complejo, double); friend complejo operator*(double, complejo); } void f() { complejo a(1,1), b(2,2), c(3,3), d(4,4),e(5,5) a= -b-c; b=c*2.0*c; c=d+e*a; } Problema : tedioso escribir una función para combinación complejo - double Solución : constructor que dado un double cree un complejo 33 class complejo { //… complejo(double r) {re r, im=0) }; esto especifica como crear un complejo a partir de un double Operadores incremento y decremento Son los únicos en C++ que se pueden especificar tanto como operadores prefijos como postfijos class VerifyPointer{ T* p; T* arreglo; int tamaño; public: // encadenar con arreglo `a' de tamaño `t' valor inicial `p' VerifyPointer(T* p, T* a, int T); // encadenar con un solo objeto valor inicial `p' VerifyPointer(T* p); T* operator++(); // prefijo T* operator++(int); //posfijo T* operator--(); // prefijo T* operator--(int); //posfijo T& operator*(); //prefijo } El argumento int sirve para indicar que la función debe llamar a la función posfija de ++. El argumento no se usa solo sirve para distinguir la implementación prefija de la posfija. void f3(T a) { T v[200]; 34 VerifyPointer p(&v[0],v,200) p.operator-(1); p.operator*()=a; //error `p' fuera del intervalo p.operator++(); p.operator*()=a; // correcto } Un excesivo empleo de la sobrecarga de operadores puede dar como resultado programas incomprensibles Su uso deberÃ−a limitarse para imitar el empleo convencional de los operadores, cuando esto no es posible, el empleo de llamadas a función es el mecanismo más adecuado Universidad Tecnológica Nacional - Santa Fe - Departamento Sistemas Curso : Desarrollos de Programación en C++ para representar valores enteros para representar valores reales 35