Estructura General de Un Programa en C

March 16, 2018 | Author: lkinpark2007 | Category: Control Flow, Compiler, Computer Programming, Computer Program, Programming Language


Comments



Description

Estructura general de un programa en C/* Comentarios de un parrafo completo comprendidos entre /*.....*/, sirven para aclarar qué el programa o una parte del programa */ // Comentarios de 1 sola línea // Zona de ficheros de cabecera de las librerías #include <....... . h> // h de Head #include <....... . h> // Zona de prototipos de funciones int Potencia (int x,y) // Zona de variables globales int valor; float media_total; void main (void) // Prog. ppal. típico de Turbo C { // llave de inicio del programa // codigo del programa ....... ....... ....... // fin del programa } // Desarrollo del código de las funciones anteriores 1 Entornos de programación C Suele decirse que los programadores de verdad trabajan con un simple editor de texto y un compilador. Aunque también existen programas que nos hacen la, en muchas ocasiones ardua, tarea de programar más cómoda y sencilla. Veamos alguno de ellos. Dev-C++ es uno de estos programas, desarrollado por Bloodshed Software y sin una nueva versión desde 2005, nos ofrece un entorno cómodo para la realización de nuestros proyectos tanto en lenguaje C como en C++. Permite la incorporación de añadidos y librerías que expandan las funcionalidades del programa y utiliza un compilador basado en GCC: Mingw. Es un programa sencillo, rápido e intuitivo de utilizar, además incorpora un depurador para facilitar la optimización de nuestros códigos. Code::Blocks es un programa similar a Dev-C++, no es tan sencillo de configurar como es el anteriormente citado, pero admite la posibilidad de añadir numerosos compiladores como Digital Mars, Microsoft Visual C++, Borland C++ o Watcom. Además Code::Blocks incorpora, además de la posibilidad de añadir diversas librerías, herramientas para la creación de interfaces gráficas de usuario. Para finalizar, Microsoft Visual C++ es un programa muy completo, orientado sobre todo al desarrollo de aplicaciones para Windows, ofrece herramientas para la programación utilizando librerías de DirectX o de .Net Framework. Existe una versión de pago que ofrece funcionalidad para muchos más lenguajes y numerosas herramientas más y también existe una versión gratuita llamada Express. 2     Siguiendo la terminología anterior, es el banco de trabajo del programador Da soporte a las actividades de la fase de codificación (preparación del código y prueba de unidades) Los mismos productos sirven también para el diseño detallado y para las pruebas de integración. Se sitúa, por tanto, en la parte central del ciclo de desarrollo Funciones de un Entorno de Programación Como se ha dicho, la misión de un Entorno de Programación es dar soporte a la preparación de programas, es decir, a las actividades de codificación y pruebas.   Las tareas esenciales de la fase de codificación son: o Edición (creación y modificación) del código fuente o Proceso/ejecución del programa  Interpretación directa (código fuente)  Compilación (código máquina) - montaje - ejecución  Compilación (código intermedio) - interpretación Otras funciones: o Examinar (hojear) el código fuente o Analizar consistencia, calidad, etc. o Ejecutar en modo depuración o Ejecución automática de pruebas o Control de versiones o Generar documentación, reformar código o ... y otras muchas más ... 3 Tipos de Entornos de Programación Un entorno de programación puede estar concebido y organizado de maneras muy diferentes. A continuación se mencionan algunas de ellas.  En las primeras etapas de la informática la preparación de programas se realizaba mediante una cadena de operaciones tales como la que se muestra en la figura para un lenguaje procesado mediante compilador. Cada una de las herramientas debía invocarse manualmente por separado. En estas condiciones no puede hablarse propiamente de un entorno de programación   El editor es un editor de texto simple El compilador traduce cada fichero de código fuente a código objeto El montador (linker / builder / loader) combina varios ficheros objeto para generar un fichero ejecutable o El depurador maneja información en términos de lenguaje de máquina Un entorno de programación propiamente dicho combina herramientas como éstas, mejoradas y mejor integradas. A veces se nombra con las siglas IDE (Integrated Development Environment). Los componentes cuya evolución ha sido más aparente son los que realizan la interacción con el usuario: o El editor ya no es un simple editor de texto, sino que tiene una clara orientación al lenguaje de programación usado (reconoce y maneja determinados elementos sintácticos) o El depurador no presenta información en términos del lenguaje de máquina, sino del lenguaje fuente o El editor está bien integrado con las demás herramientas (se posiciona directamente en los puntos del código fuente en los que hay errores de compilación, o que se están ejecutando con el depurador en un momento dado. No es fácil establecer una clasificación dentro de la variedad de entornos de programación existentes. En algún momento se describieron las siguientes clases de entornos, no excluyentes: o Entornos centrados en un lenguaje o Entornos orientados a estructura o Entornos colección de herramientas o o o 4   Compilación        Existen muchos compiladores de C. El cc es el compilador estándar de Sun. El compilador GNU de C es gcc, el cual es bastante popular y esta disponible en varias plataformas. Existen también compiladores equivalentes de C++ los cuales usualmente son nombrados como CC. Por ejemplo, Sun provee CC y GNU GCC. El compilador de GNU es también denotado como g++. Existen otros compiladores menos comunes de C y C++. En general todos los compiladores mencionados operan esencialmente de la misma forma y comparten muchas opciones comunes en la línea de opciones. Más adelante se listan y se dan ejemplos de opciones comunes de los compiladores. Sin embargo, la mejor referencia de cada compilador es a través de las páginas en línea, del manual del sistema. Por ejemplo: man gcc. Para compilar el programa usaremos el comando gcc. El comando deberá ser seguido por el nombre del programa en C que se quiere compilar. Un determinado número de opciones del compilador pueden ser indicadas también. Por el momento no haremos uso de estas opciones todavía, se irán comentando algunas más esenciales. Por lo tanto, el comando básico de compilación es: gcc programa.c donde programa.c es el nombre del archivo. Si hay errores obvios en el programa (tales como palabras mal escritas, caracteres no tecleados u omisiones de punto y coma), el compilador se detendrá y los reportará. Podría haber desde luego errores lógicos que el compilador no podrá detectar. En el caso que esta fuera la situación se le estará indicando a la computadora que haga las operaciones incorrectas. Cuando el compilador ha terminado con éxito, la versión compilada, o el ejecutable, es dejado en un archivo llamado a.out, o si la opción -o es usada con el compilador, el nombre después de -o es el nombre del programa compilado.        Se recomienda y es más conveniente usar la opción -o con el nombre del archivo ejecutable como se muestra a continuación: gcc -o programa programa.c el cual pone el programa compilado en el archivo del programa señalado, en éste caso en programa, en vez del archivo a.out. 5 Ejecución del programa El siguiente estado es correr el programa ejecutable. Para correr un ejecutable en UNIX, simplemente se escribe el nombre del archivo que lo contiene, en este caso programa (o a.out). Con lo anterior, se ejecuta el programa, mostrando algún resultado en la pantalla. En éste estado, podría haber errores en tiempo de ejecución (run-time errors), tales como división por cero, o bien, podrían hacerse evidentes al ver que el programa no produce la salida correcta. Si lo anterior sucede, entonces se debe regresar a editar el archivo del programa, recompilarlo, y ejecutarlo nuevamente. Estructuras En C una estructura es una colección de variables que se referencian bajo el mismo nombre. Una estructura proporciona un medio conveniente para mantener junta información que se relaciona. Una definición de estructura forma una plantilla que se puede usar para crear variables de estructura. Las variables que forman la estructura son llamados elementos estructurados. Generalmente, todos los elementos en la estructura están relacionados lógicamente unos con otros. Por ejemplo, se puede representar una lista de nombres de correo en una estructura. Mediante la palabra clave struct se le indica al compilador que defina una plantilla de estructura. struct direc { char nombre[30]; char calle[40]; char ciudad[20]; char estado[3]; unsigned int codigo; }; Con el trozo de código anterior no ha sido declarada ninguna variable, tan sólo se ha definido el formato. Para declarar una variable, se hará como sigue: struct direc info_direc; Se pueden declarar una o más variables cuando se define una estructura entre ) y ;. Por ejemplo: struct direc { 6 char nombre[30]; char calle[40]; char ciudad[20]; char estado[3]; unsigned int codigo; } info_direc, binfo, cinfo; observar que direc es una etiqueta para la estructura que sirve como una forma breve para futuras declaraciones. Como en este última declaración se indican las variables con esta estructura, se puede omitir el nombre de la estructura tipo. Las estructuras pueden ser también preinicializadas en la declaración: struct direc info_direc={"Vicente Fernandez","Fantasia 2000","Dorado","MMX",12345}; Para referenciar o accesar un miembro (o campo) de una estructura, C proporciona el operador punto ., por ejemplo, para asignar a info_direc otro código, lo hacemos como: info_direc.codigo=54321; 7 Tipos de datos o Los tipos de datos definen los métodos de almacenamiento disponibles para representar información, junto con la manera en que dicha información ha de ser interpretada. Los tipos de datos son indispensables para la declaración de variables. En C tenemos diferentes tipos de datos: o o o 6. Tipos de datos (I) Tipo Bytes Desde Hasta void Es nulo (NULL) no retorna nada signed char 1 -128 127 unsigned char 1 0 255 signed short 2 -32768 32767 unsigned short 2 0 65535 signed int 2 -32768 32767 7. Tipos de datos (II) Tipo Bytes Desde Hasta unsigned int 2 0 65535 signed long 4 -2147483648 2147483647 unsigned long 4 0 4294967295 float 4 3,4x10 -38 3,4x10 38 double 8 1,7x10 -308 1,7x10 308 long double 10 3,4x10 -4932 3,4x10 4932 8. Nota sobre los tipos de datos: si omitimos las palabras “signed” o “unsigned” al declarar un tipo de dato, el compilador automáticamente asume por default que es un tipo “signed”; es decir que si al declarar el tipo de dato simplemente colocamos, por ejemplo, “int”, entonces el compilador asumirá que hemos declarado un “signed int” 9. Para declarar variables globales: //llamado a las cabeceras Tipo_dato1 variable1, variable2, … , variablen; Tipo_dato2 variable3, variable4, … , variablem; //declaración de funciones{…} Ejemplo: #include<stdio.h> char caracter; float iva,total_pagar,descuento; unsigned long pvc,tcd; void main(){ … //instrucciones; ... } Estas variables van a servir en cualquier parte del programa 10. Para declarar variables locales: //llamado a las cabeceras //declaración de una función{ Tipo_dato1 variable1, variable2, … , variablen; Tipo_dato2 variable3, variable4, … , variablem; } Ejemplo: #include<stdio.h> void main(){ int numero,edad,cantidad; float iva,total_pagar,descuento; unsigned long pvc,tcd; //instrucciones; ... } Estas variables van a servir solo en la función main() 11. ¿Qué son las constantes? o Son aquellos valores que, una vez compilado el programa, no pueden ser cambiados. o Al definir las constantes, debemos tomar en cuenta las siguientes REGLAS DE CONVERSIÓN DE TIPOS DE DATOS: 12. o Reglas de conversión de tipos de datos I o Una constante entera (sin parte decimal) es tomada como tal, a menos que se la añadan las letras F ó L (mayúsculas ó minúsculas) ejemplos : 1 : tomada como entera (int) 12f : tomada como flotante (float) 456L : tomada como doble larga (long double) 8 o Una variable con parte decimal es tomada siempre como DOUBLE, salvo que se la siga de la letra F ó L 2.0 : tomada como doble (double) 3.56F : tomada como flotante (float) 1.007L : tomada como flotante larga (long float) 13. Reglas de conversión de tipos de datos II Si en cualquiera de los casos anteriores agregamos la letra U ó u la constante queda calificada como UNSIGNED: 86u : tomada como entera sin signo (unsigned int) 32.44632UL : tomada como doble larga sin signo (unsigned long double) o Una variable numérica que comienza con &quot;0&quot; (cero) es tomado como OCTAL asi : 012 equivale a 10 unidades en numeración decimal o Una variable numérica que comienza con &quot;0x&quot; ó &quot;0X&quot; (cero – equis) es tomada como HEXADECIMAL asi : 0x16 equivale a 22 unidades en numeración decimal 14. Para declarar constantes: //llamado a las cabeceras #define constante1 valor1; #define constante2 valor2; … //declaración de una función{ } Ejemplo: #include<stdio.h> #define PI 3.141592 //constante double #define OCTAL 017 //constante octal #define FLOTANTE 14F //constante flotante Void main(){ … //instrucciones; ... } Estas constantes sirven en cualquier parte del programa 15. Operadores Aritméticos en C Operador Nombre Ejemplo + Suma a+b - Resta a-b * Multiplicación a*b / División a/b % Residuo entero de la división a%b ++ Incremento en 1 a++ -- Decremento en 1 a-16. Operadores Relacionales en C Operador Nombre Ejemplo > Mayor que a>b < Menor que a<b >= Mayor o igual que a>=b <= Menor o igual que a<=b == Igual (Equivalente) a==b != Diferente (No es igual) a!=b 17. Operadores Lógicos en C Operador Nombre Ejemplo Devuelve cierto si: && Y (and) (exp1)&& (exp2) ambas son verdaderas || O (or) (exp1)|| (exp2) Una o ambas es verdadera ! No (not) !(exp1) Cambia el valor de la expresión 18. Notas sobre los Operadores en C o Los operadores aritméticos y relacionales pueden trabajar con variables o constantes de cualquier tipo numérico, como por ejemplo int, double, float, etc o En los Operadores lógicos, exp1 y exp2 corresponden a EXPRESIONES LÓGICAS (Expresiones que pueden tomar los valores de verdadero o falso). Ejemplo: (7<2)||(4>3) 19. Caracteres de conversión más usados de scanf(); y printf(); (I) Carácter Significado %c El dato es carácter %d El dato es entero %e El dato es valor en coma flotante %f El dato es valor en coma flotante %g El dato es valor en coma flotante %h El dato es entero corto 20. Caracteres de conversión más usados de scanf(); y printf(); (II) Carácter Significado %i El dato es entero decimal, octal o hexadecimal %o El dato es octal %s El dato es cadena de caracteres, seguido de espacio en blanco y del carácter fin de línea () %u El dato es entero decimal sin signo %x El dato es entero hexadecimal 21. Caracteres de conversión más usados de scanf(); y printf(); (III) o o 9 Ejemplo: int a,b; char letra; float area; printf(“%c ,%i, %f, %i”,letra,a,area,b); 22. Secuencias de Escape (I) Carácter Código Valor ASCII Campana (alerta) a 007 Retroceso (espacio atrás) 008 Tabulador horizontal 009 Nueva línea 010 Tabulador vertical v 011 Nueva página f 012 23. Secuencias de Escape (II) Carácter Código Valor ASCII Retorno de carro 013 Comillas (“) ” 034 Interrogación (?) ? 039 Barra invertida 063 Fin de línea 092 Número octal ooo 000 o o o o o 10 Enteros Los enteros son el tipo de dato más primitivo en C. Se usan para representar números enteros. Pero siempre se pueden encontrar otras aplicaciones para los números enteros. En general se pueden usar para representar cualquier variable discreta. Los tipos de datos enteros son: short, int, long y long long. Es decir que para el lenguaje C existen diferentes tamaños de números enteros que, según el compilador y la plataforma de hardware, pueden tener desde 1 byte hasta 8 bytes (para más detalles busca en la referencia). Además, el lenguaje C hace la distinción de si el entero es con signo o sin signo (signed o unsigned). La forma de declarar un entero es con uno de los tipos de datos que sean enteros según el tamaño que se quiera. En caso de que no se declare si es con signo o sin signo, se toma con signo. Algunos ejemplos de declaraciones de enteros: int a; unsigned int a; signed long a; signed long long a = 10000000; Todos los números son representados en memoria mediante una cadena de bits. En el caso de los números con signo, el bit más significativo es el que se usa para representar el signo. La representación de los números negativos se realiza mediante el complemento a dos, que es una técnica que permite operar con los números negativos de forma lógica. Sólo a modo de ejemplo, la representación en memoria de un -8 en una variable de 2 bytes, entera, con signo sería la siguiente: 1111111111111000 Nótese que no se ha tenido en cuenta el endianness de la arquitectura. 11 Flotantes Se denomina flotantes a los tipos de datos que representan a los números reales, ya que utilizan un sistema de representación basado en la técnica de coma flotante, que permite operar con números reales de diversas magnitudes, mediante un número decimal llamado mantisa y un exponente que indica el orden de magnitud. El tipo de dato flotante en lenguaje C sólo tiene dos tamaños: el float y el double, que son 4 bytes y 8 bytes respectivamente. Se los puede utilizar tanto para representar números decimales, como para representar números enteros con un orden de magnitud muy grande. La forma de declarar una variable flotante es escribiendo en una línea uno de los tipos de datos flotantes y a continuación el nombre de la variable y tal vez algún valor que se les quiera dar. Algunos ejemplos: float a; double a = 1e23; double a = 3.1416; float a = 4e-9; double a = -78; Hay que tener en cuenta que aunque los valores flotantes son más convenientes para algunas aplicaciones, hay casos en los que se prefieren los enteros. Esto se debe a que los números flotantes no necesariamente tienen soporte de hardware, en particular en las plataformas integradas. Una alternativa que se utiliza en estas situaciones es interpretar los enteros como decimales de forma que 150 se interprete como 1.5 y 2345 como 23.45. Para el caso de los flotantes de 4 bytes, se utiliza 1 bit para el signo, 8 bits para el exponente y 23 bits para el valor del número. El procedimiento para almacenar un número en una variable flotante es el siguiente: 1. Se convierte a binario la parte entera. 2. Se coloca el signo en el bit más significativo de la misma manera que en los enteros (1 para el - y 0 para el +). 3. Se mueve la coma (en la representación binaria de la parte entera) hasta que esté a la derecha del primer uno y éste se descarta (el uno más significativo). El valor del exponente será el número de posiciones que se movió la coma. El exponente usa la representación de un entero con complemento a dos. 4. Se convierte en binario la parte decimal del número. Esto usando el peso de los bits. el bit decimal más significativo vale 1/2, el siguiente vale 1/4, el 12 otro 1/8, el otro 1/16 y así hasta completar lo que falta para los 23bits del valor. 5. Se concatena todo y ese es el valor flotante representado en memoria. 13 Caracteres Los caracteres se representan utilizando el tipo char, que tiene sólo 1 byte de tamaño. Este tipo se utiliza para representar los 256 caracteres de la tabla de caracteres del sistema. El tipo char es también un tipo entero, ya que puede tomar valores de 0 a 255. En cuanto a la forma de declarar variables de tipo char es la misma forma que con los otros tipos. char a; char a = 's'; char a = 48; Como puedes ver, se le puede asignar un número a una variable char, ya que se trata de un tipo entero. En algunas situaciones particulares se utiliza el tipo char para contadores, porque permite que ocupen sólo un byte en memoria. Es importante notar que con la llegada de la codificación UTF-8, los caracteres de los diversos idiomas pueden ocupar 1, 2, 3 o 4 bytes, de modo que el tipo char ya no alcanza para la representación de todos los caracteres. Por ello, el estándar C99 introduce el tipo wchar que puede ocupar más de 1 byte, según sea necesario para la codificación utilizada por el sistema. 14 Caracteres y Cadenas en C 1. Caracteres y Cadenas 2. Conceptos Básicos o Caracteres  Valor entero representado como caracter entre comillas simples. Por ejemplo: 'z' representa al valor entero de z  Internamente se representa como un tipo de dato enumerado usando el código ASCII ( código estándar americano para el intercambio de información ). o Cadenas  Es un arreglo de caracteres que:  Puede incluir letras, dígitos y caracteres especiales (*, /, $)  Tiene un puntero al primer caracter  Cuyo valor de la cadena es la dirección de memoria del primer elemento. 3. o Los códigos para los caracteres que representan dígitos del 0 al 9 son consecutivos. o Las letras en el alfabeto están divididos en dos rangos: uno para las mayúsculas (A-Z) y otro para las minúsculas (a-z). Sin embargo dentro de cada rango los valores ASCII son consecutivos. Propiedades Importantes del Código ASCII 4. Constantes de Tipo Caracter o Es un estándar para referirse a un carácter específico en C. o Para referirse al código ASCII de la letra A, se especifica „A‟ , el cual es el 65. o Para referirse al código del carácter 9, de forma similar, „9‟. CUIDADO: El referirse al carácter, no es lo mismo que referirse al valor entero. El 9 es diferente del „9‟. 5. Operaciones con Caracteres o Se puede: o Sumar un entero a un carácter o Restar un entero de un caracter o Restar un caracter de otro o Comparar dos caracteres entre sí 15 CUIDADO: Al sumar o restar el resultado no debe salirse del rango de representación ASCII 6. Manejo de Cadenas o Definición  Como un arreglo de caracteres o una variable de tipo char *  char color[] = &quot;blue&quot;;  char *colorPtr = &quot;blue&quot;;  Recuerde que una cadena se representa como un arreglo de caracteres y termina con ''  color tiene 5 elementos o Lectura  Utilizando scanf  scanf(&quot;%s&quot;, cadena);  Copia la entrada en el arreglo cadena[]  No se necesita el & (porque una cadena es un puntero)  Recuerde dejar espacio en el arreglo para el fin de cadena '„ o Escritura  Utilizando printf  printf(“%s”,cadena); 7. Ejemplos o char RandomLetra(void) o { o return (RandomInteger („A‟, „Z‟)); o } o bool esMayuscula (char ch) o { o return (ch >= „A‟ && ch <=„Z‟); o } bool esDigito (char ch) { return (ch >= „0‟ && ch <=„9‟); } bool esMinuscula (char ch) { return (ch >= „a‟ && ch <=„z‟); } 8. Interfaces útiles 9. La interfaz ctype.h o Contiene un gran número de funciones para determinar el tipo de carácter, entre las principales tenemos: o islower(ch) retorna TRUE si el carácter ch es minúscula o isupper(ch) retorna TRUE si el carácter ch es mayúscula o isalpha(ch) retorna TRUE si ch es un valor alfabético o isdigit(ch) retorna TRUE si ch es un dígito o isalnum(ch) retorna TRUE si ch es un valor alfanumérico o ispunct(ch) retorna TRUE si ch es un símbolo de puntuación o isspace(ch) retorna TRUE si ch es un carácter en blanco 10. ctype.h: Librería de manejo de caracteres 11. Stdlib.h: Librería de funciones de conversión 16 o Convierte cadenas de dígitos a enteros y valores de punto flotante. 12. stdio.h 13. String.h: Librería de manipulación de cadenas o Incluye funciones para:  Manipular cadenas  Búsqueda en cadenas  Manejo de tokens  Determine la longitud de cadenas 14. Funciones de comparación de cadenas o int strcmp( const char *s1, const char *s2 );  Compara string s1 con s2  Retorna:  Un número negativo si s1 < s2  Cero, si s1 == s2  Un número positivo si s1 > s2 o int strncmp(const char *s1,const char *s2,size_t n);  Compara n caracteres de s1 en s2  Retorna valores como los anteriores 15. Funciones de Búsqueda 17 VARIABLES, Y CONSTANTES EN C ¿Qué es una Variable?    Es solo un nombre para identificar posiciones de memoria. Este nombre de la variable debe ser un identificador válido. En las variables (posiciones de memoria) se guardan los datos usados por el programa durante su ejecución. TODA variable debe ser DECLARADA antes de poder ser utilizada.  constante o función           Para que un identificador sea válido debe: Iniciar con una letra del alfabeto inglés, o con el signo (_) No debe contener caracteres especiales, tales como @, $, # Después de la primera letra puede contener más letras del alfabeto inglés, números, o el carácter (_) NO DEBE haber espacios en blanco en los identificadores C diferencia mayúsculas de minúsculas, entonces no es lo mismo declarar la variable numero que Numero o NuMeRo Existen palabras propias del lenguaje (palabras reservadas) que no pueden ser usadas como identificadores ej: if, do numero @hola _hola Peso neto Peso_neto 1radio radio1 if si 18 Tipos de variables  Variables globales: son las que se declaran después del llamado a las cabeceras, pero antes de cualquier función, y son útiles para cualquier parte del programa. Variables locales: son las que se declaran dentro de una función, y solo sirven para ser usadas dentro de esa función.  19 Entrada y salida (E/S) stdio.h En este capítulo se verán varias formas de entrada y salida (E/S). Se han mencionado brevemente algunas formas y ahora se revisarán con un poco más de detalle. Los programas que hagan uso de las funciones de la biblioteca de E/S deben incluir la cabecera, esto es: #include <stdio.h> Reportando errores En muchas ocasiones es útil reportar los errores en un programa de C. La función de la biblioteca estándar perror es la indicada para hacerlo. Es usada conjuntamente con la variable errno y frecuentemente cuando se encuentra un error se desea terminar el programa antes. Además se revisa la función exit() y errno, que en un sentido estricto no son parte de la biblioteca stdio.h, para ver como trabajan con perror. perror() El prototipo de la funcion perror es: void perror(const char *s); La función perror() produce un mensaje que va a la salida estándar de errores, describiendo el último error encontrado durante una llamada al sistema o a ciertas funciones de biblioteca. La cadena de caracteres s que se pasa como argumento, se muestra primero, luego un signo de dos puntos y un espacio en blanco; por último, el mensaje y un salto de línea. Para ser de más utilidad, la cadena de caracteres pasada como argumento debería incluir el nombre de la función que incurrió en el error. El código del error se toma de la variable externa errno, que toma un valor cuando ocurre un error, pero no es puesta a cero en una llamada no errónea. errno A la variable especial del sistema errno algunas llamadas al sistema (y algunas funciones de biblioteca) le dan un valor entero, para indicar que ha habido un error. Esta variable esta definida en la cabecera #include <errno.h> y para ser usada dentro de un programa debe ser declarada de la siguiente forma: extern int errno; 20 El valor sólo es significativo cuando la llamada devolvió un error (usualmente -1), algunas veces una función también puede devolver -1 como valor válido, por lo que se debe poner errno a cero antes de la llamada, para poder detectar posibles errores. exit La función exit tiene el siguiente prototipo de acuerdo a la cabecera #include <stdlib.h>: void exit(int status); La función produce la terminacón normal del programa y la devolución de status al proceso padre o al sistema operativo. El valor de status es usado para indicar como ha terminado el programa: o sale con un valor EXIT_SUCCESS en una terminación o sale con un valor EXIT_FAILURE en una terminación Por lo tanto cuando se encuentre un error se llamará a la función como exit(EXIT_FAILURE) para terminar un programa con errores. Flujos Los flujos son una forma flexible y eficiente para leer y escribir datos. Existe una estructura interna de C, FILE, la cual representa a todas los flujos y esta definida en stdio.h. Por lo tanto simplemente se necesita referirse a la estructura para realizar entrada y salida de datos. Para usar los flujos entonces se debe declarar una variable o apuntador de este tipo en el programa. No se requiere conocer más detalles acerca de la definición. Se debe abrir un flujo antes de realizar cualquier E/S, después se puede accesar y entonces se cierra. El flujo de E/S usa un BUFFER, es decir, un pedazo fijo de área temporal de la memoria (el buffer) es leído o escrito a un archivo. Lo siguiente se muestra en la figura 17.1. Observar que el apuntador del archivo actualmente apunta a éste buffer. 21 Figura 16.1: Modelo de entrada salida usando un buffer. Esto conduce a un uso eficiente de E/S pero se debe tener cuidado: los datos escritos a un buffer no aparecen en un archivo (o dispositivo) hasta que el buffer es escrito (con \n se puede hacer). Cualquier salida anormal del código puede causar problemas. Flujos predefinidos En UNIX se tienen predefinidos 3 flujos (en stdio.h): stdin, stdout y stderr. Todas ellas usan texto como método de E/S. Los flujos stdin y stdout pueden ser usadas con archivos, programas, dispositivos de E/S como el teclado, la consola, etc. El flujo stderr siempre va a la consola o la pantalla. La consola es el dispositivo predefinido para stdout y stderr. El teclado lo es para stdin. Los flujos predefinidos son automáticamente abiertas. 22 Redireccionamiento Lo siguiente no es parte de C, pero depende del sistema operativo. Se hace redireccionamiento desde la línea de comandos con: > que redirecciona stdout a un archivo. Por lo tanto, si se tiene un programa llamado salida, que usualmente muestra en pantalla algo, entonces: salida > archivo_sal mandará la salida al archivo archivo_sal < redirecciona stdin desde un archivo a un programa. Por lo tanto, si se espera entrada desde el teclado para un programa llamado entrada, se puede leer en forma similar la entrada desde un archivo. entrada < archivo_ent Con | entubamiento o pipe se coloca stdout de un programa en stdin de otro, prog1 | prog2 Por ejemplo, mandar la salida (usualmente a consola) de un programa directamente a la impresora out | lpr E/S Basica Hay un par de funciones que dan las facilidades básicas de E/S. Quizás las más comunes son: getchar() y putchar(). Están definidas y son usadas como sigue:   int getchar(void) -- lee un caracter de stdin int putchar(char ch) -- escribe un caracter a stdout y regresa el caracter escrito. main() { int ch; ch = getchar(); (void) putchar((char) ch); } 23 Otras funciones relacionadas son: int getc(FILE *flujo); int putc(char ch, FILE *flujo); E/S formateada Se han visto ya algunos ejemplos de como C usa la E/S formateada. En esta sección se revisarán con más detalle. printf El prototipo de la función esta definido como: int printf( const char *formato, lista arg ...); que muestra en stdout la lista de argumentos de acuerdo al formato especificado. La función devuelve el número de caracteres impresos. La cadena de formateo tiene dos tipos de objetos:   caracteres ordinarios -- estos son copiados a la salida. especificadores de conversión -- precedidos por % y listados en la tabla 16.1. 24 Tabla 16.1: Caracteres de format printf/scanf Especificador (%) Tipo c i,d o x,X char int int int Resultado un sólo caracter número base diez número base ocho número base dieciseis notación minús/mayús u s int char * entero sin signo impresión de cadena terminada por nulo f e,E double/float formato -m.ddd ... " Notación Científica -1.23e002 g,G " e ó f la que sea más compacta % caracter % Entre el % y el caracter de formato se puede poner:     - signo menos para justificar a la izquierda. número entero para el ancho del campo. m.d en donde m es el ancho del campo, y d es la precisión de dígitos después del punto decimal o el número de caracteres de una cadena. Por lo tanto: 25 printf("%-2.3f\n",17.23478); la salida en pantalla será: 17.235 y printf("VAT=17.5%%\n"); genera la siguiente salida: VAT=17.5% scanf La función esta definida como sigue: int scanf( const char *formato, lista arg ...); Lee de la entrada estándar (stdin) y coloca la entrada en la dirección de las variables indicadas en lista args. Regresa el número de caracteres leídos. La cadena de control de formateo es similar a la de printf. Importante: se requiere la dirección de la variable o un apuntador con scanf. Por ejemplo: scanf("%d", &i); Para el caso de un arreglo o cadena sólo se requiere el nombre del mismo para poder usar scanf ya que corresponde al inicio de la dirección de la cadena. char cadena[80]; scanf("%s",cadena); Archivos Los archivos son la forma más común de los flujos. Lo primero que se debe hacer es abrir el archivo. La función fopen() hace lo siguiente: FILE *fopen(const char *nomb, const char *modo); 26 fopen regresa un apuntador a un FILE. En la cadena nomb se pone el nombre y la trayectoria del archivo que se desea accesar. La cadena modo controla el tipo de acceso. Si un archivo no puede ser accesado por alguna razón un apuntador NULL es devuelto. Los modos son:    ``r'' lectura; ``w'' escritura; y ``a'' Para abrir un archivo se debe tener un flujo (apuntador tipo archivo) que apunte a la estructura FILE. Por lo tanto, para abrir un archivo denominado miarch.dat para lectura haremos algo como lo siguiente; FILE *flujo; /* Se declara un flujo */ flujo = fopen("miarch.dat","r"); es una buena práctica revisar si un archivo se pudo abrir correctamente if ( (flujo = fopen("miarch.dat","r")) == NULL ) { printf("No se pudo abrir %s\n","miarch.dat"); exit(1); } ..... Lectura y escritura de archivos Las funciones fprintf y fscanf son comúnmente empleadas para accesar archivos. int fprintf(FILE *flujo, const char *formato, args ... ); int fscanf(FILE *flujo, const char *formato, args ... ); Las funciones son similares a printf y scanf excepto que los datos son leídos desde el flujo, el cual deberá ser abierto con fopen(). El apuntador al flujo es automáticamente incrementado con todas las funciones de lectura y escritura. Por lo tanto, no se debe preocupar en hacer lo anterior. 27 char *cadena[80]; FILE *flujo; if ( (flujo = fopen( ... )) != NULL) fscanf(flujo,"%s",cadena); Otras funciones para archivos son: int getc(FILE *flujo) int putc(char ch, FILE *s) Estas son parecidas a getchar y putchar. getc esta definida como una macro del preprocesador en stdio.h. fgetc es una función de la biblioteca de C. Con ambas se consigue el mismo resultado. Para el volcado de los datos de los flujos a disco, o bien, para disasociar un flujo a un archivo, haciendo previamente un volcado, usar: int fflush(FILE *flujo); int fclose(FILE *flujo); También se puede tener acceso a los flujos predeterminados con fprintf, etc. Por ejemplo: fprintf(stderr,"¡¡No se puede calcular!!\n"); fscanf(stdin,"%s",string); int fgetc(FILE *flujo) int fputc(char ch, FILE *s) sprintf y sscanf Son parecidas a fprintf y fscanf excepto que escriben/leen una cadena. int sprintf(char *cadena, char *formato, args ... ) int sscanf(char *cadena, cahr *formato, args ... ) Por ejemplo: float tanque_lleno = 47.0; float kilometros = 400; char km_por_litro[80]; /* litros */ sprintf( km_por_litro, "Kilometros por litro = %2.3f", kilometros/tanque_lleno); 28 Petición del estado del flujo Existen unas cuantas funciones útiles para conocer el estado de algún flujo y que tienen los prototipos siguientes: int feof(FILE *flujo); int ferror(FILE *flujo); void clearerr(FILE *flujo); int fileno(FILE *flujo);       feof() devuelve verdadero si el flujo indicado esta en el fin del archivo. Por lo tanto para leer un flujo, fp, línea a línea se podría hacer algo como: while ( !feof(fp) ) fscanf(fp,"%s",linea); ferror() inspecciona el indicador de error para el flujo indicado, regresando verdadero si un error ha ocurrido. clearerr() limpia los indicadores de fin-de-fichero y error para el flujo indicado. fileno() examina el argumento flujo y devuelve su descriptor de fichero, como un entero. E/S de bajo nivel o sin almacenamiento intermedio Esta forma de E/S es sin buffer -cada requerimiento de lectura/escritura genera un acceso al disco (o dispositivo) directamente para traer/poner un determinado número de bytes. No hay facilidades de formateo -ya que se están manipulando bytes de información. Lo anterior significa que se estan usando archivos binarios (y no de texto). En vez de manejar apuntadores de archivos, se emplea un manejador de archivo de bajo nivel o descriptor de archivo, el cual da un entero único para identificar cada archivo. Para abrir un archivo usar: int open(char* nomb, int flag); que regresa un descriptor de archivo ó -1 si falla. flag controla el acceso al archivo y tiene los siguientes macros definidas en fcntl.h: 29       O_APPEND el archivo se abrirá en modo de sólo añadir O_CREAT si el archivo no existe, será creado. O_EXCL abrir en forma exclusiva. O_RDONLY sólo lectura O_RDWR lectura y escritura O_WRONLY sólo escritura para ver otras opciones usar man. La función: int creat(char* nomb, int perms); puede también ser usada para crear un archivo. Otras funciones son: int close(int fd); int read(int fd, char *buffer, unsigned longitud); int write(int fd, char *buffer, unsigned longitud); que pueden ser usadas para cerrar un archivo y leer/escribir un determinado número de bytes de la memoria/hacia un archivo en la localidad de memoria indicada por buffer. La función sizeof() es comúnmente usada para indicar la longitud. Las funciones read y write regresan el número de bytes leídos/escritos o -1 si fallan. Se tiene a continuación dos aplicaciones que usan algunas de las funciones indicadas: #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> main(int argc, char **argv) {float buffer[]={23.34,2.34,1112.33}; int df; int bytes_esc; int num_flot; /* Primeramente se crea el archivo */ if ( (df = open(argv[1], O_CREAT, S_IRUSR | S_IWUSR) ) == -1) 30 { /* Error, archivo no abierto */ perror("Archivo datos, apertura"); exit(1);} else printf("Descriptor de archivo %d\n",df); /* Despues se abre para solamente escribir */ if ( (df = open(argv[1], O_WRONLY) ) == -1) { /* Error, archivo no abierto */ perror("Archivo datos, apertura"); exit(1);} else printf("Descriptor de archivo %d\n",df); /* En primer lugar se escribe el n´umero de flotantes que seran escritos */ num_flot = 3; if ( (bytes_esc = write(df, &num_flot, sizeof(int)) ) == -1) { /* Error en la escritura */ perror("Archivo datos, escritura"); exit(1);} else printf("Escritos %d bytes\n",bytes_esc); /* Se escribe el arreglo de flotantes */ if ( (bytes_esc = write(df, buffer, num_flot*sizeof(float)) ) == -1) { /* Error en la escritura */ perror("Archivo datos, escritura"); exit(1);} else printf("Escritos %d bytes\n",bytes_esc); close(df);} Ejemplo de lectura del ejemplo anterior: /* Este programa lee una lista de flotantes de un archivo binario. */ /* El primer byte del archivo es un entero indicando cuantos */ /* flotantes hay en el archivo. Los flotantes estan despues del */ /* entero, el nombre del archivo se da en la linea de comandos. */ #include <stdio.h> #include <fcntl.h> 31 main(int argc, char **argv) {float buffer[1000]; int fd; int bytes_leidos; int num_flot; if ( (fd = open(argv[1], O_RDONLY)) == -1) { /* Error, archivo no abierto */ perror("Archivo datos"); exit(1);} if ( (bytes_leidos = read(fd, &num_flot, sizeof(int))) == -1) { /* Error en la lectura */ exit(1);} if ( num_flot > 999 ) { /* arch muy grande */ exit(1); } if ( (bytes_leidos = read(fd, buffer, num_flot*sizeof(float))) == -1) { /* Error en la lectura */exit(1);} 32 Tipos de Operadores y Expresiones Nombres de las variables Letra (Letra o Dígito)* -> 31 caracteres Las mayúsculas son diferentes de las minúsculas Es práctica común de C, denotar con - MAYUSCULAS las constantes - minúsculas las variables Tipos char int -> (CALIFICADORES: short, long, unsigned) float double Constantes Simbólicas (Preprocesamiento) Ej. #define PI 3.14159265359 #define MAXLINE 1000 char line [MAXLINE+1] #define FORMFEED '\014' Variables constantes: Ej. const float pi=3.14159265359; Alfanuméricas: Se evalúan en tiempo de compilación, no de ejecución. Ejs. 3.14159265359 123.456e-7 0.12E3 '0' -> ASCII 48, EBCDIC 240 Declaraciones: Se pueden declarar varias variables en pocos renglones: int lower,upper,step; char c, line[1000]; También se pueden escribir más explicitamente, para agregar comentarios: int lower; /* algunos comentarios */ int upper; int step; char c; char line[1000]; Inicialización de variables: char backslash='\\'; int i=0; 33 Operadores aritméticos: + - * / %(módulo) Ej. (uso del módulo) if (year%4==0 && year%100!=0 || year%400==0) es un año bisiesto; else no lo es; Operadores relacionales y lógicos: > >= <= < == != El operador que convierte en 0 algo distinto de 0, y en 1, el cero, es !. Ej. if (!inword) if (inword==0) Conversiones de Tipos Se hacen conversiones automáticas cuando tiene sentido. Ej. float + int -> float "char" e "int" se manejan indiscrimidamente. Ej. int atoi(char s[]) /* convierte s a int */ { int i,n; n=0; for (i=0; s[i]>='0' && s[i]<='9';++i) n=10*n+s[i]-'0'; return (n); } Conversión coaccionada "cast" Se puede forzar la conversión de un tipo en otro. El formato es: (nombre-de-tipo) expresión por ejemplo: sqrt( (double) n) Operadores de incremento y decremento si n=5, entonces x=n++; -> x=5 n=6 x=++n; -> x=6 n=6 Operadores lógicos para manejo de bits: & AND lógico | OR lógico ^ XOR lógico << desplazamiento a la izquierda >> desplazamiento a la derecha ~ complemento a uno (unario) 34 Ejs c = n & '\077'; x = x | MASK; x = x & ~'\077'; Ej. Función para tomar n bits a partir de la posición p getbits(unsigned int x, p, n) { return ((x>>(p+1-n)) & ~(~0 << n)); } Nota: (~0 << n) es 11..100..0 con n ceros a la derecha ~(~0 << n) es 00..011..1 con n unos a la derecha Operadores y expresiones de asignación La expresión e1 op= e2 es idéntico a e1 = e1 op e2 donde "op" es un operador como {],-,*,/,%,<<,>>,&,^,|} Por ejemplo: i=i+2; es idÚntico a i+=2; Expresiones condicionales Tienen el siguiente formato e1 ? e2 : e3; si e1 es verdadero se evalúa e2, si e1 es falso, se evalúa e3. z=(a>b) ? a : b; /* z=max(a,b) */ Precedencia y orden de evaluación 35 OPERADOR ASOCIATIVIDAD () [] -> . -> ! ~ ++ -- - (tipo) * & sizeof <* / % -> + - -> << >> -> < <= > >= -> == != -> & -> ^ -> | -> && -> || -> ? : <= += -= etc <0 -> ASOCIATIVIDAD -> izquierda a derecha <- derecha a izquierda i=i+2 i+=2 Expresiones En un programa, el tipo de un dato determina las operaciones que se pueden realizar con él. Por ejemplo, con los datos de tipo entero se pueden realizar operaciones aritméticas, tales como la suma, la resta o la multiplicación. Ejemplo 1: Algunos ejemplos son: 111 + 6 (operación suma) 19 - 72 (operación resta) 24 * 3 (operación multiplicación) Todas las operaciones del ejemplo constan de dos operandos (constantes enteras) y un operador. La mayoría de las veces es así, pero, también es posible realizar operaciones con distinto número de operadores y/u operandos. Ejemplo 2: Por ejemplo: 111 + 6 - 8 (tres operandos y dos operadores) -( ( +19 ) + 72 ) (dos operandos y tres operadores) -( -72 ) (un operando y dos operadores) 36 En las operaciones del ejemplo se puede observar que los caracteres más (+) y menos (-) tienen dos usos: 1. Operadores suma y resta. 2. Signos de un número (también son operadores). Los operadores de signo más (+) y menos (-) son operadores monarios, también llamados unarios, ya que, actúan, solamente, sobre un operando. Los caracteres abrir paréntesis "(" y cerrar paréntesis ")" se utilizan para establecer la prioridad de los operadores, es decir, para establecer el orden en el que los operadores actúan sobre los operandos. Un operador indica el tipo de operación a realizar sobre los operandos (datos) que actúa. Los operandos pueden ser:     Constantes (expresadas por su valor o con un nombre (identificador)). Variables. Llamadas a funciones. Elementos de formaciones (arrays). En este apartado se van a tratar operaciones en donde sólo aparecen constantes y variables. Cuando se combinan uno o más operadores con uno o más operandos se obtiene una expresión. De modo que, una expresión es una secuencia de operandos y operadores escrita bajo unas reglas de sintaxis. Ejemplo 3: Dadas las siguientes declaraciones de constantes y variables: #define PI 3.141592 int numero = 2; float radio_circulo = 3.2; Algunos ejemplos de expresiones son: 2 * PI * radio_circulo ( PI * PI ) numero * 5 37 De sus evaluaciones se obtienen los valores: 20.106189 (valor real) ( 2 * 3.141592 * 3.2 ) 9.869600 (valor real) ( 3.141592 * 3.141592 ) 10 (valor entero) ( 2 * 5 ) Un operador siempre forma parte de una expresión, en la cual, el operador siempre actúa sobre al menos un operando. Por el contrario, un operando sí puede aparecer solo en una expresión. En programación, de la evaluación de una expresión siempre se obtiene un valor. Dicho valor puede ser de tipo: entero, real, lógico, carácter o cadena. Por consiguiente, una expresión puede ser:     Aritmética (devuelve un número entero o real). Lógica (devuelve un valor lógico: verdadero o falso) De carácter (devuelve un carácter representable por el ordenador). De cadena (devuelve una cadena). A continuación, vamos a estudiar los operadores y las expresiones en lenguaje C, en comparación con los operadores y expresiones en psedocódigo CEE utilizado por el autor en el libro EMPEZAR DE CERO A PROGRAMAR EN LENGUAJE C. 38 Estructuras de control e iteraciones Sentencias de decisión Estructura de control IF DEFINICIÓN Las sentencias de decisión o también llamadas de CONTROL DE FLUJO son estructuras de control que realizan una pregunta la cual retorna verdadero o falso (evalúa una condicion) y selecciona la siguiente instrucción a ejecutar dependiendo la respuesta o resultado. En algún momento dentro de nuestros algoritmos, es preciso cambiar el flujo de ejecución de las instrucciones, es decir, el orden en que las instrucciones son ejecutadas. Muchas de las veces tenemos que tomar una decisión en cuanto a que se debe ejecutar basándonos en una respuesta de verdadero o falso (condicion). La ejecución de las instrucciones incluyendo una estructura de control como el condicional funcionan de esta manera: • Las instrucciones comienzan a ejecutarse de forma secuencial (en orden) y cuando se llega a una estructura condicional, la cual esta asociada a una condicion, se decide que camino tomar dependiendo siempre del resultado de la condicion siendo esta falsa o verdadera. • Cuando se termina de ejecutar este bloque de instrucciones se reanuda la ejecución en la instrucción siguiente a la de la condicional. Sentencia if La instrucción if es, por excelencia, la más utilizada para construir estructuras de control de flujo. SINTAXIS 39 Primera Forma Ahora bién, la sintaxis utilizada en la programación de C++ es la siguiente: if (condicion) { Set de instrucciones } siendo "condicion" el lugar donde se pondrá la condicion que se tiene que cumplir para que sea verdadera la sentencia y así proceder a realizar el "set de instrucciones" o código contenido dentro de la sentencia. Segunda Forma Ahora veremos la misma sintaxis pero ahora le añadiremos la parte "Falsa" de la sentencia: if (condicion) { Set de instrucciones } else { Set de instrucciones 2 //Parte FALSA } La forma mostrada anteriormente muestra la union de la parte "VERDADERA" con la nueva secuencia la cual es la parte "FALSA" de la sentencia de decision "IF" en la cual esta compuesta por el: else { Set de instrucciones 2 //Parte FALSA } 40 //PARTE VERDADERA la palabra "else" o "De lo contrario" indica al lenguaje que de lo contrario al no ser verdadera o no se cumpla la parte verdadera entonces realizara el "set de instrucciones 2". EJEMPLOS DE SENTENCIAS IF... Ejemplo 1: if(numero == 0) //La condicion indica que tiene que ser igual a Cero { cout<<"El Numero Ingresado es Igual a Cero"; } Ejemplo 2: if(numero > 0) // la condicion indica que tiene que ser mayor a Cero { cout<<"El Numero Ingresado es Mayor a Cero"; } Ejemplo 3: if(numero < 0) // la condicion indica que tiene que ser menor a Cero { cout<<"El Numero Ingresado es Menor a Cero"; } Ahora uniremos todos estos ejemplos para formar un solo programa mediante la utilización de la sentencia "Else" e introduciremos el hecho de que se puede escribir en este espacio una sentencia if ya que podemos ingresar cualquier tipo de código dentro de la sentencia escrita después de un Else. Ejemplo 4: if(numero == 0) //La condicion indica que tiene que ser igual a Cero { cout<<"El Numero Ingresado es Igual a Cero"; 41 } else { if(numero > 0) // la condicion indica que tiene que ser mayor a Cero { cout<<"El Numero Ingresado es Mayor a Cero"; } else { if(numero < 0) // la condicion indica que tiene que ser menor a Cero { cout<<"El Numero Ingresado es Menor a Cero"; } } } Sentencia switch switch es otra de las instrucciones que permiten la construcción de estructuras de control. A diferencia de if, para controlar el flujo por medio de una sentencia switch se debe de combinar con el uso de las sentencias case y break. Notas: cualquier número de casos a evaluar por switch así como la sentencia default son opcionales. La sentencia switch es muy útil en los casos de presentación de menus. Sintaxis: switch (condición) { case primer_caso: 42 bloque de instrucciones 1 break; case segundo_caso: bloque de instrucciones 2 break; case caso_n: bloque de instrucciones n break; default: bloque de instrucciones por defecto } Ejemplo 1 switch (numero) { case 0: cout << "numero es cero"; } Ejemplo 2 switch (opcion) { case 0: cout << "Su opcion es cero"; break; case 1: cout << "Su opcion es uno"; break; case 2: cout << "Su opcion es dos"; } 43 Ejemplo 3 switch (opcion) { case 1: cout << "Su opcion es 1"; break; case 2: cout << "Su opcion es 2"; break; case 3: cout << "Su opcion es 3"; break; default: cout << "Elija una opcion entre 1 y 3"; } Operador condicional ternario ?: En C, existe el operador condicional ( ?: ) el cual es conocido por su estructura como ternario. El comportamiento de dicho operador es el mismo que una estructura if - then - else del lenguaje BASIC (y de la función IIf de Visual Basic). El operador condicional ?: es útil para evaluar situaciones tales como: Si se cumple tal condición entonces haz esto, de lo contrario haz esto otro. Sintaxis: ( (condicion) ? proceso1 : proceso2 ) En donde, condicion es la expresión que se evalua, proceso1 es la tarea a realizar en el caso de que la evaluación resulte verdadera, y proceso2 es la tarea a realizar en el caso de que la evaluación resulte falsa. Ejemplo 1 int edad; cout << "Cual es tu edad: "; cin >> edad; cout << ( (edad < 18) ? "Eres joven aun" : "Ya tienes la mayoría de edad" ); El ejemplo anterior podría escribirse de la siguiente manera: int edad; cout << "Cual es tu edad: "; 44 cin >> edad; if (edad < 18) cout << "Eres joven aun"; else cout << "Ya tienes la mayoría de edad"; Ejemplo 2 Vamos a suponer que deseamos escribir una función que opere sobre dos valores numéricos y que la misma ha de regresar 1 (true) en caso de que el primer valor pasado sea igual al segundo valor; en caso contrario la función debe retornar 0 (false). int es_igual( int a, int b) { return ( (a == b) ? 1 : 0 ) } Sentencias de iteración DEFINICIÓN Las Sentencias de Iteración o Ciclos son estructuras de control que repiten la ejecución de un grupo de instrucciones. Básicamente, una sentencia de iteración es una estructura de control condicional, ya que dentro de la misma se repite la ejecución de una o más instrucciones mientras o hasta que una a condición especifica se cumpla. Muchas veces tenemos que repetir un número definido o indefinido de veces un grupo de instrucciones por lo que en estos casos utilizamos este tipo de sentencias. en C++ los ciclos o bucles se construyen por medio de las sentencias for, while y do - while. La sentencia for es útil para los casos en donde se conoce de antemano el número de veces que una o más sentencias han de repetirse. Por otro lado, la sentencia while es útil en aquellos casos en donde no se conoce de antemano el número de veces que una o más sentencias se tienen que repetir. Sentencias For for(contador; final; incremento) { Codigo a Repetir; 45 } donde: 1. 2. 3. contador es una variable numérica final es la condición que se evalua, o sea, el valor final para contador incremento es el valor que se suma o resta al contador Ejemplo 1: for(i=1; i<=10; i++) { cout<<"Hola Mundo"; } Esto indica que el contador "i" inicia desde 1 y finaliza cuando el contador "i" sea menor o igual a 10 ( en este caso llegará hasta 10) e "i++" realiza la sumatoria por unidad lo que hace que el for y el contador se sumen. repitiendo 10 veces "HOLA MUNDO" en pantalla. Ejemplo 2: for(i=10; i>=0; i--) { cout<<"Hola Mundo"; } Este ejemplo hace lo mismo que el primero, salvo que el contador se inicializa a 10 en lugar de 1; y por ello cambia la condición que se evalua así como como que el contador se decrementa en lugar de ser incrementado. Sentencia while while(condicion) { código a Repetir 46 } donde: 1. condicion es la expresión a evaluar Ejemplo 1: int contador = 0; while(contador<=10) { contador=contador+1; cout<<"Hola Mundo"; } El contador Indica que hasta que este llegue a el total de 10 entonces se detendrá y ya no se realizará el código contenido dentro de la sentencia while, de lo contrario mientras el "contador" sea menor a 10 entonces el código contenido se ejecutará desplegando hasta 10 veces "Hola Mundo" en pantalla. Sentencia do - while La sentencia do es usada generalmente en cooperación con while para garantizar que una o más instrucciones se ejecuten al menos una vez. Por ejemplo, en la siguiente construcción no se ejecuta nada dentro del ciclo while, el hecho es que el contador inicialmente vale cero y la condición para que se ejecute lo que está dentro del while es "mientras el contador sea mayor que diez". Es evidente que a la primera evaluación hecha por while la condición deja de cumplirse. int contador = 0; while(contador > 10) { contador ++; cout<<"Hola Mundo"; } 47 Al modificar el segmento de código anterior usando do tenemos: int contador = 0; do { contador ++; cout<<"Hola Mundo";} while(contador > 10); Observe cómo en el caso de do la condición es evaluada al final en lugar de al principio del bloque de instrucciones y, por lo tanto, el código que le sigue al do se ejecuta al menos la primera vez. Sentencias break y continue En la sección (Sentencia switch) vimos que la sentencia break es utilizada con el propósito de forzar un salto dentro del bloque switch hacia el final del mismo. En esta sección volveremos a ver el uso de break, salvo que esta ocasión la usaremos junto con las sentecias for y la sentencia while. Además, veremos el uso de la sentencia continue. break La sentencia break se usa para forzar un salto hacia el final de un ciclo controlado por for o por while. Ejemplo: En el siguiente fragmento de código la sentencia break cierra el ciclo for cuando la variable ( i ) es igual a 5. La salida para el mismo será: 01234 for (i=0; i<10; i++) { if (i == 5) break; cout << i << " ";} continue 48 La sentencia continue se usa para ignorar una iiteración dentro de un ciclo controlado por for o por while. Ejemplo: En el siguiente fragmento de código la sentencia continue ignora la iteración cuando la variable ( i ) es igual a 5. La salida para el mismo será: 012346789 for (i=0; i<10; i++) { if (i == 5) continue; cout << i << " ";} Uso de break y continue junto con while Los dos ejemplos anteriores se presentan en seguida, salvo que en lugar de for se hace uso de while. Nota: no deje de observar que la construcción del ciclo while para el caso de la sentencia continue es diferente, esto para garantizar que el ciclo no vaya a caer en una iteración infinita. break int i = 0; while (i<10) { if (i == 5) break; cout << i << " "; i++;} continue int i = -1; while (i<10) { i++; if (i == 5) continue; cout << i << " ";} 49 Tipos Estructurados Conceptos: Estructura de datos, Array, Vector, Matriz, Índice, String, Cadena, Record, Campo, Set, Conjunto. Resumen: A diferencia de los datos de tipo simple que sólo pueden almacenar un valor, los datos estructurados o estructuras de datos pueden recolectar varios valores simultáneamente. Se hace una primera introducción a los datos estructurados destacando en primer lugar que se les asigna una cantidad fija de memoria durante la ejecución del programa cuando se declara una variable de un determinado tipo estructurado. El primer tipo estructurado es el tipo array que permite agrupar otros datos más simples de igual tipo bajo un mismo identificador. Este tipo de estructuras permiten definir vectores, matrices, tablas y estructuras multidimensionales. TurboPascal incorpora un tipo especial de array: el tipo string. Se define como una secuencia de caracteres cuya longitud puede variar entre 1 y 255. El tipo record está compuesto de elementos de diferentes tipos a cada uno de los cuales se les asocia un identificador. Finalmente se analiza el tipo estructurado set equivalente al concepto de conjunto matemático y otros tipos de datos no simples. Objetivos específicos. Al finalizar el tema, el alumno deberá ser capaz de: a) Describir los tipos de datos estructurados en el lenguaje de programación Turbopascal, su formato de representación y las operaciones más características que pueden realizarse con ellos (Conocimiento) b) Escribir la declaración de variables de cualquiera de los tipos de datos estructurados (Comprensión) c) Escribir el código necesario para acceder a un elemento o conjunto de elementos de una estructura de datos (Comprensión) d) Seleccionar la estructura de datos más adecuada para una aplicación determinada (Aplicación) e) Codificar una tarea sencilla convenientemente especificada, utilizando datos estructurados (Aplicación) Datos estructurados 63 6.1. INTRODUCCIÓN Los tipos estructurados de datos se componen de otros tipos de datos más simples previamente declarados o predefinidos en el lenguaje TurboPascal. Los tipos de datos estructurados en TurboPascal son los siguientes: a) Array b) String c) Record d) Set e) File f) Text g) Object 50 Existen otros dos tipos de datos que, aunque no son estrictamente una composición de otros datos más simples también se van a describir en este capítulo: el tipo Pointer y el tipo Procedimiental. 6.2. Tipo Un dato de tipo array es, en realidad, un conjunto o estructura de datos que engloba una colección de datos del mismo tipo. Pueden ser unidimensionales, denominados también vectores o listas, o multidimensionales, denominados matrices o tablas. Los números o valores que identifican a cada elemento particular del Array se llaman índices. Sintaxis: Type ident = Array [TSub1,...,TSubn] of Tipo; donde TSub1,...,TSubn es una sucesión de tipos de dato ordinales (¡no pueden ser variables y no valen tipos de dato reales!: sólo enteros, lógico, carácter, enumerado o subrangos de los anteriores) separados por comas y que especifican, según su producto cartesiano, el número de elementos de la estructura. TSubi es un identificador de un tipo de dato ordinal o un subrango de éste: lim_inf_i..lim_sup_i Ej.: TYPE vector1 = Array [1..4] of Char; matriz1 = Array [1..10, 1..10] of Integer; matriz2 = Array [Boolean, 1..10] of Boolean; color = (blanco, amarillo, negro, rojo); ciudad = (Al,Ca,Co,Gr,Ja,Hu,Ma,Se); estacion = (prim,ver,oto,inv); cestacion = array[estacion] of string[9]; CONST est : cestacion = (‟primavera‟,‟verano‟,‟otoño‟,‟invierno‟); VAR v : Integer; vect1,vect2 : vector1; matriz : matriz1; qt : matriz2; raza : Array [1..40] of color; grados : Array [ciudad] of real; begin vect1[1] := 't'; vect1 := vect2; matriz[2,5] := 6; matriz[1,9] := matriz[2,5]; qt[true,4] := false; raza[15] := amarillo; Fundamentos de programación - A. García-Beltrán, R. Martínez y J.A. Jaén 64 grados[Al] := 18.5; ... En el ejemplo anterior, se observa como puede accederse a cualquier elemento de la estructura Array referenciando el/los subíndice/s entre corchetes. Asimismo, pueden realizarse asignaciones entre datos Array del mismo tipo. Variable vect1 vect1[1] vect1[2] vect1[3] vect1[4] 51 Figura 19. Espacio de memoria reservado para una variable array vect1de tipo vector1 El tamaño reservado en memoria para una variable de tipo Array es igual al número total de elementos por el tamaño del elemento, en bytes. Así, mientras la variable vect1 del ejemplo anterior ocupa 4 elementos x 1 byte = 4 bytes, la variable matriz ocupa 10 x 10 elementos x 2 bytes = 200 bytes en la memoria durante la ejecución del programa. Cuando se trabaja con datos de tipo Array (especialmente si son multidimensionales) hay que tener cuidado con la cantidad de memoria que hay que reservar ya que se podría sobrepasar la memoria disponible. En principio, TurboPascal sólo permite tipos de dato estructurados con un tamaño máximo de 65520 bytes. Las dos siguientes declaraciones de tipos de dato son, por lo tanto, incorrectas: type vector = array[1..65536] of byte; vector2 = array[1..32800] of integer; Con los tipos de dato Array sólo pueden utilizarse los operadores de asignación y no pueden emplearse, como estructura completa con los procedimientos de entrada y salida de datos: Read/ReadLn o Write/WriteLn. Esto es independiente de las operaciones que puedan realizarse con cada uno de los elementos que componen la variable array, si lo permite el tipo de dato correspondiente. 6.3. Tipo String Este tipo de dato predefinido en el lenguaje TurboPascal permite representar una secuencia o cadena de caracteres correspondientes al código ASCII de un tamaño máximo de 255 (por defecto). Si se desea especificar un tamaño menor de 255 se utilizarán corchetes para delimitar un entero que especifica el tamaño máximo de la cadena de caracteres. Una variable de este tipo ocupa en memoria tantos bytes como caracteres tenga más uno; en este byte se guarda la longitud real de la cadena almacenada en la variable. A esta longitud se le denomina tamaño ó longitud lógica. Puede accederse a cada uno de los caracteres que forman la secuencia de caracteres como si fueran datos de tipo Array unidimensionales de caracteres. Sintaxis: TYPE identificador : String[J]; { donde 1≤J≤255 } Ej.: CONST LineLin = 79; TYPE Nombre = String [10]; Linea = String [LineLin]; VAR n : nombre; comentario : linea; Datos estructurados 65 Para la variable n se reserva un espacio de 11 bytes en la memoria durante la ejecución del programa. Figura 20. Espacio de memoria reservado para una variable n tipo nombre En las expresiones y sentencias que manipulan datos de tipo String, el valor o la constante literal correspondiente va encerrado entre comillas simples. Pueden manipularse datos de tipo String con operaciones de asignación (:=), comparaciones (operadores de relación) 52 y concatenaciones (+). Ej.: n := 'mario'; Tiene como resultado... Figura 21. Asignacuión de valores a la variable n tipo nombre La variable n, en este caso, almacena una cadena de cinco caracteres. La función estándar Length devuelve la longitud de la cadena almacenada en una variable de tipo String. Tras la asignación anterior, la llamada a la función Length(n) devolvería el valor entero 5. Como puede accederse a cada carácter de forma independiente, el mismo valor también podría obtenerse del espacio en memoria que se emplea para guardar el tamaño de la cadena que se almacena en la variable, con la llamada a la función Ord(n[0]). También pueden emplearse datos de cualquier tipo cadena con los procedimientos estándar de entrada y salida de datos Read/ReadLn y Write/WriteLn para asignar valores a variables de tipo cadena y visualizar datos de tipo cadena por la pantalla. A diferencia del tipo de dato Char, NO es un tipo de dato ordinal, ya que no es un conjunto “finito” de datos. Aunque sí se puede establecer el orden entre dos valores de tipo cadena. Éste se obtiene por el orden entre los valores de tipo carácter que componen las cadenas según las posiciones respectivas de los caracteres11: 'a' < 'anterior' < 'antes' < 'despues' < 'fuego' < 'luego' Por otro lado, 'casas' se considera mayor que 'casa', ya que en dos cadenas de distinto tamaño, cada carácter en la cadena de mayor tamaño sin el correspondiente carácter en la menor supone un valor superior. También puede asignarse a una variable de tipo cadena una constante cadena de caracteres vacía. Ej.: n := ''; Según muestra la Tabla 18 el operador suma es el único de este tipo. Es un operador binario que actúa sobre dos operandos de tipo Char o String. Da como resultado un valor de tipo String. 11 Es importante tener en cuenta que, en la tabla de caracteres ASCII, los caracteres alfabéticos mayúsculas van antes que las minúsculas n[0] n[1] n[2] ... n[10] #5 'm' 'a''r' 'i' 'o' Fundamentos de programación - A. García-Beltrán, R. Martínez y J.A. Jaén 66 Tabla 18. Operadores de cadena Operador Descripción Ejemplo de expresión Resultado del ejemplo + Suma de cadenas 'coche' + 'azul' 'cocheazul' 6.4. Tipo Record Un tipo record o registro permite definir una estructura que almacena un conjunto de datos del mismo o de distintos tipos (excepto File). Los datos individuales se conocen como campos del registro y se declaran como variables cuando se define el tipo de registro. A cada uno de los campos se le asigna un identificador al realizar la declaración, no pudiendo existir dos identificadores de campo iguales dentro del mismo registro. 53 Sintaxis: TYPE Tiporegistro = Record lista ident1 : tipo1; lista ident2 : tipo2; { ... } lista identn : tipon end; Ej.: Type meses = (En,Fb,Mr,Ab,My,Jn,Jl,Ag,Sp,Oc,Nv,Dc); fecha = record dia : 1..31; mes : meses; anno : 1900..2000 end; Un campo de un tipo registro puede ser de otro tipo registro (registros anidados). En el siguiente ejemplo, el tipo ficha_personal incluye un campo de tipo fecha. Type ficha_personal = record nombre, ape1, apel2 : string [20]; fecha_nacimiento : fecha; profesion : string [40]; telefono : integer end; Var cumple : fecha; individuo : ficha_personal; El acceso, para entradas o salidas, a los campos de registro, se realiza con el identificador del registro, un punto y el identificador del campo. Sintaxis: IdentificadorRegistro.IdentificadorCampo Ej.: cumple.dia := 15; write(cumple.dia); individuo.fecha_nacimiento.mes := Fb; O pueden manipularse los campos de un dato tipo Record determinado con la estructura With: Ej.: with cumple do begin dia:=27; mes:=En; anno:=1993; write(dia) end; Datos estructurados 67 with individuo.fecha_nacimiento do begin dia:=2; mes:=My; anno:=1953 end; 54 El tamaño de una variable de tipo Record es la suma de los tamaños de sus campos. Así, la variable cumple del ejemplo anterior ocupa 1 + 1 + 2 = 4 bytes en memoria durante la ejecución del programa. La estructura tipo Record permite la introducción de campos variantes, que aparecen o no en una variable de ese tipo, en función del valor de un cierto campo. En general, los registros variantes tendrán una parte fija, que se declara en primer lugar, y otra variante. La principal ventaja de este tipo de estructura es el ahorro de memoria, ya que el espacio ocupado por una variable de este tipo es la suma del tamaño de la parte fija y el tamaño de la parte variante más grande. El campo de selección puede ser cualquier variable de tipo ordinal. Sintaxis: TYPE Tiporegistro = Record lista ident1 : tipo1; { ... } lista identn : tipon; case campo_selector:tipo of valor1 : (lista ident1b); { ... } valorn : (lista identnb) end; Ej.: TYPE ficha = Record nombre: String[20]; dni : String[8]; CASE alumno : Boolean OF False : (prof : Boolean; dpt : String[20]); True : (mat : String[5]; curso : 1..6) END; 6.5. Tipo Set Un dato de tipo Set corresponde a la definición matemática de conjunto. Es una parte de un conjunto universal, de un tipo de dato base ordinal ya definido y tiene un máximo de 256 elementos. Aunque sus elementos deben pertenecer a un mismo tipo ordinal, dentro del conjunto no están ordenados. Los valores ordinales de todos los elementos deben estar dentro del intervalo [0-255]. La definición del tipo Set se realiza de la siguiente manera: Sintaxis: TYPE TipoSet = Set of tipo; Ej.: type dia = (lu,ma,mi,ju,vi,sa,dm); Frutas = (limon,naranja,uva,pera,platano); conj_caract = Set of Char; digitos = Set of 0..9; dias = Set of dia; clase_fruta = Set of frutas; A continuación pueden declararse variables de tipo Set: Ej.: var laborable : dias; Fundamentos de programación - A. García-Beltrán, R. Martínez y J.A. Jaén 68 55 letras : conj_caract; conj_num : digitos; La sintaxis de asignación de datos de tipo Set es: identificador := [valor_i, .., valor_j]; Ej.: laborable := [lu, ma, mi, ju, vi]; o bien, de forma más condensada: laborable := [lu..vi]; letras :=['A','C','T','m']; conj_num := []; { se le asigna el conjunto vacio } conj_num := conj_num + [2, 3]; El tamaño reservado en memoria para una variable de tipo Set es, en bytes, el cociente entero mas uno del número máximo de elementos posibles del conjunto menos uno dividido entre ocho. Por ejemplo, una variable de tipo conjunto que pueda albergar, como máximo, entre 1 y 8 elementos, ocupará 1 byte en memoria; entre 9 y 16 elementos, 2 bytes,... entre 249 y 256 elementos, 32 bytes. No se pueden utilizar estos tipos de dato, los conjuntos o los elementos de un conjunto, con los procedimientos de entrada y salida de datos: Read/ReadLn o Write/WriteLn. Las operaciones que pueden realizarse con datos de tipo Set pertenecen al álgebra de conjuntos. Los operadores de conjuntos definidos en TurboPascal son binarios y se resumen en la Tabla 19. Tabla 19. Operadores de conjuntos Operador Descripción Ejemplo de expresión Resultado del ejemplo + Unión [2,3] + [3,6] [2,3,6] * Intersección (conjunto de elementos que estén a la vez en dos conjuntos) [2,3] * [3,6] [3] - Diferencia (conjunto de elementos pertenecientes a un primero que no están en un segundo conjunto) [2,3] - [3,6] [2] = Igualdad [2,3] = [3,6] false <> Desigualdad [2,3] <> [3,6] true <= Inclusión (de un conjunto en otro) [2,3] <= [3,6] false => Inclusión inversa (de un segundo conjunto en un primero) [2,3] => [3] true in Pertenencia (Nota: el primer operando es del tipo de dato correspondiente al elemento del conjunto) 3 in [3,6] true 6.6. Tipo File 56 El tipo predefinido file permite utilizar una estructura de datos que se emplea cuando es necesario manipular grandes cantidades de datos y deben almacenarse en un sistema de almacenamiento masivo (habitualmente, como archivo o fichero en el disco duro del ordenador). Un archivo es una secuencia lineal de valores de datos de un cierto tipo. Esta secuencia no tiene longitud fija, ni predefinida. Si no se especifica el tipo de componentes será un fichero sin tipo (indefinido). Ej.: TYPE Fich_numeros = File of Integer; Fichero = File of Ficha; Archivo = File; Este tipo de dato se verá con más detenimiento en el capítulo Archivos. 6.7. Tipo Text El tipo predefinido text permite utilizar una estructura de datos de tipo archivo que contiene caracteres (datos tipo Char) organizados por líneas o filas. Ej.: VAR fichero_texto : Text; Este tipo de dato se verá con más detenimiento en el capítulo Archivos. 6.8. Tipo Pointer Los punteros representan o almacenan direcciones de memoria en las que se almacenan datos de tipo dinámico. Los punteros no tienen porqué ser datos de tipo dinámico pueden ser datos estáticos que apuntan a datos dinámicos. Este tipo de dato se verá con más detenimiento en el capítulo Punteros y Variables Dinámicas. 6.9. Tipo Procedural o Procedimental Los procedimientos y funciones, también llamados genéricamente rutinas, son módulos o conjuntos independientes de sentencias de un programa que pueden ejecutarse a través de una llamada. Admiten parámetros en función de los cuales pueden ejecutarse. Estos parámetros pueden ser de cualquiera de los tipos vistos anteriormente pero, incluso, pueden ser otros procedimientos o funciones. Para permitir esto, deben declararse tipo procedurales o procedimentales que definan un tipo de procedimiento o función. La sintaxis de definición del tipo procedural o procedimental es el siguiente: En el caso de un tipo procedimiento: Type TipoProc = Procedure(Parametros); o bien en el caso de un tipo función: Type TipoFunc = Function(Parametros):id_tipo; id_tipo hace referencia al tipo de dato devuelto por la función. Ej.: Type Proced = Procedure; AsignaP = Procedure(var a:integer); FuncionUni = Function(x:real):real; FuncionBi = Function(x,y:real):real; 57 MaxFun = Function(a,b:real; f:FuncionUni):real; 6.10. Tipo Object Este tipo de dato, que no entra dentro del alcance de este curso, permite trabajar con la metodología de Programación Orientada a Objetos en TurboPascal. Fundamentos de programación - A. García-Beltrán, R. Martínez y J.A. Jaén 70 Bibliografía básica • García-Beltrán, A., Martínez, R. y Jaén, J.A. Métodos Informáticos en TurboPascal, Ed. Bellisco, 2ª edición, Madrid, 2002 • Joyanes, L. Fundamentos de programación, Algoritmos y Estructuras de Datos, McGrawHill, Segunda edición, 1996 • Aho, A.H., Hopcroft, J.E. y Ullman, J.D. Estructuras de Datos y Algoritmos, AddisonWesley Iberoamericana, 1988 • Kruse, R. Estructuras de Datos y Diseño de Programas, Prentice-Hall, 1988 58 Funciones Una función es un conjunto de líneas de código que realizan una tarea específica y puede retornar un valor. Las funciones pueden tomar parámetros que modifiquen su funcionamiento. Las funciones son utilizadas para descomponer grandes problemas en tareas simples y para implementar operaciones que son comúnmente utilizadas durante un programa y de esta manera reducir la cantidad de código. Cuando una función es invocada se le pasa el control a la misma, una vez que esta finalizó con su tarea el control es devuelto al punto desde el cual la función fue llamada. <tipo> [clase::] <nombre> ( [Parámetros] ) { cuerpo; } Ejemplo de una función Para comenzar, vamos a considerar el caso en el cual se desea crear la función cuadrado(), misma que deberá volver el cuadrado de un número real (de punto flotante), es decir, cuadrado() aceptará números de punto flotante y regresará una respuesta como número flotante. Nota: aunque para la función que veremos el tipo de retorno coincide con el tipo de parámetro pasado, algunas veces las cosas pueden cambiar, es decir, no es obligatorio que una función reciba un parámetro de un tipo y que tenga que regresar una respuesta de dicho tipo. // regresar el cuadrado de un número double cuadrado(double n) { return n*n; } 59 Parámetros Normalmente, las funciones operan sobre ciertos valores pasados a las mismas ya sea como constantes literales o como variables, aunque se pueden definir funciones que no reciban parámetros. Existen dos formas en C++ de pasar parámetros a una función; por referencia o por valor. El hecho es que si en una declaración de función se declaran parámetros por referencia, a los mismos no se les podrá pasar valores literales ya que las referencias apuntan a objetos (variables o funciones) residentes en la memoria; por otro lado, si un parámetro es declarado para ser pasado por valor, el mismo puede pasarse como una constante literal o como una variable. Los parámetros pasados por referencia pueden ser alterados por la función que los reciba, mientras que los parametros pasados por valor o copía no pueden ser alterados por la función que los recibe, es decir, la función puede manipular a su antojo al parámetro, pero ningún cambio hecho sobre este se reflejará en el parámetro original. Parametros por valor La función cuadrado() (ver arriba) es un clásico ejemplo que muestra el paso de parámetros por valor, en ese sentido la función cuadrado() recibe una copia del parámetro n. En la misma función se puede observar que se realiza un calculo ( n*n ), sin embargo el parámetro original no sufrirá cambio alguno, esto seguirá siendo cierto aún cuando dentro de la función hubiera una instrucción parecida a n = n * n; o n*=n;. Parametros por referencia Para mostrar un ejemplo del paso de parámetros por referencia, vamos a retomar el caso de la función cuadrado, salvo que en esta ocasión cambiaremos ligeramente la sintaxis para definir la misma. Veamos: // regresar el cuadrado de un número double cuadrado2(double &n) { n *= n; return n; } Al poner a prueba las funciones cuadrado() y cuadrado2() se podrá verificar que la primera de estas no cambia el valor del parámetro original, mientras que la segunda sí lo hace. 60 Parámetros constantes Los parámetros usados por una función pueden declararse como constantes ( const ) al momento de la declaración de la función. Un parámetro que ha sido declarado como constante significa que la función no podrá cambiar el valor del mismo ( sin importar si dicho parámetro se recibe por valor o por referencia ). Ejemplo: int funcionX( const int n ); void printstr( const char *str ); Parámetros con valor por defecto Los parámetros usados por una función pueden declararse con un valor por defecto. Un parámetro que ha sido declarado con valor por defecto es opcional a la hora de hacer la llamada a la función. Ejemplo: Dada la función: void saludo( char* mensaje = "Hola sudafrica 2010" ); la misma puede ser invocada como: saludo(); // sin parámetro saludo("Sea usted bienvenido a C++"); // con parámetro Para ver un ejemplo más, vamos a considerar el caso de la función binstr() del programa funciones01. Ahora, vamos modificar dicha función, salvo que esta ocasión nos interesa que la misma sirva para convertir números decimales en cadenas numéricas y cuya base de conversión sea pasada como parámetro. Es decir, la función de la que estamos hablando podrá convertir números decimales a: binario, octal, decimal, hexadecimal, etc.; y la única condición será que la base indicada esté entre el 2 y el 36, inclusive. Nota: Ya que la función servirá para convertir números a cualquier representación la nombraremos como numstr() en lugar de binstr(). Si la función es invocada sin el parámetro base regresará una cadena de digitos decimales. #include <iostream> #include <stdlib.h> using namespace std; // declaración de prototipo char *numstr(unsigned int, const int base = 10); // punto de prueba 61 int main() { int n = 128; cout << "decimal = " << n << ", binario = " << numstr(n, 2) << endl; cout << "decimal = " << n << ", octal.. = " << numstr(n, 8) << endl; cin.get(); } // definición de función numstr() // nota: esta funcion requiere de la librería stdlib.h char *numstr(unsigned int n, const int base) { static char buffer[65]; itoa(n, buffer, base); return buffer; } Parámetros de tipo puntero Anteriormente se mencionó que en C++ los parámetros a una función pueden pasarse por valor o por referencia, al respecto, podemos agregar que los parámetros también pueden pasarse como punteros. El paso de parámetros de punteros es bastante parecido al paso de parámetros por referencia, salvo que el proceso de los datos dentro de la función es diferente. Por ejemplo, las funciones: void referencia( int &X ) { X = 100; } void puntero( int *X ) { *X = 100; } ambas reciben un puntero o referencia a un objeto de tipo entero, por lo tanto cualquiera de las funciones del ejemplo puede cambiar el valor de la variable entera apuntada por X, la diferencia radica en la forma en que cada una de las mismas lleva cabo la tarea. Si en la función puntero() en lugar de usar *X = 100; se usara X = 100; se le asignaría 100 al puntero X, más no al objeto apuntado por X, y esto podría ser la causa de que el programa se terminara de manera abrupta. 62 Parámetros estructurados Al igual que cualquier otro tipo los parámetros de tipo estruturado pueden pasarse por valor o por referencia, sin embargo, podría ser que si una estructura es pasada por valor el compilador mostrara una advertencia ( warning ) indicando que se pasado por valor una estructura, puesto que el paso de estructuras por valor es permitido usted puede ignorar la advertencia, pero lo mejor es pasar estructuras por referencia. Si una estructura es pasada por valor y si esta es muy grande podria ser que se agotara la memoria en el segmento de pila ( Stack Segment ), aparte de que la llamada a la función sería más lenta. Para ver un ejemplo, consideremos el caso del siguiente tipo estructurado: struct empleado { char nombre[32]; int edad; char sexo; }; Ahora, pensemos que deseamos escribir una función para imprimir variables del tipo empleado. Así, la función puede escribirse de las tres maneras siguientes: void ImprimeEmpleadoV( empleado e) { cout << "Nombre: " << e.nombre << endl; cout << "Edad: " << e.edad << endl; cout << "Sexo: " << e.sexo << endl; } // Parametro empleado pasado por referencia void ImprimeEmpleadoR( empleado &e ) { cout << "Nombre: " << e.nombre << endl; cout << "Edad: " << e.edad << endl; cout << "Sexo: " << e.sexo << endl; } 63 // Parametro empleado pasado como puntero void ImprimeEmpleadoP( empleado *e ) { cout << "Nombre: " << e->nombre << endl; cout << "Edad: " << e->edad << endl; cout << "Sexo: " << e->sexo << endl; } 64 Recursividad Se dice que algo es recursivo si se define en función de sí mismo o a sí mismo. También se dice que nunca se debe incluir la misma palabra en la definición de ésta. El caso es que las definiciones recursivas aparecen con frecuencia en matemáticas, e incluso en la vida real. Un ejemplo: basta con apuntar una cámara al monitor que muestra la imagen que muestra esa cámara. El efecto es verdaderamente curioso, en especial cuando se mueve la cámara alrededor del monitor. En matemáticas, tenemos múltiples definiciones recursivas: - Números naturales: (1) 1 es número natural. (2) el siguiente número de un número natural es un número natural - El factorial: n!, de un número natural (incluido el 0): (1) si n = 0 entonces: 0! = 1 (2) si n > 0 entonces: n! = n · (n-1)! Asimismo, puede definirse un programa en términos recursivos, como una serie de pasos básicos, o paso base (también conocido como condición de parada), y un paso recursivo, donde vuelve a llamarse al programa. En un computador, esta serie de pasos recursivos debe ser finita, terminando con un paso base. Es decir, a cada paso recursivo se reduce el número de pasos que hay que dar para terminar, llegando un momento en el que no se verifica la condición de paso a la recursividad. Ni el paso base ni el paso recursivo son necesariamente únicos. Por otra parte, la recursividad también puede ser indirecta, si tenemos un procedimiento P que llama a otro Q y éste a su vez llama a P. También en estos casos debe haber una condición de parada. Existen ciertas estructuras cuya definición es recursiva, tales como los árboles, y los algoritmos que utilizan árboles suelen ser en general recursivos. Un ejemplo de programa recursivo en C, el factorial: int factorial(int n) { if (n == 0) return 1; return n * factorial(n-1); } 65 Como se observa, en cada llamada recursiva se reduce el valor de n, llegando el caso en el que n es 0 y no efectúa más llamadas recursivas. Hay que apuntar que el factorial puede obtenerse con facilidad sin necesidad de emplear funciones recursivas, es más, el uso del programa anterior es muy ineficiente, pero es un ejemplo muy claro. A continuación se expone un ejemplo de programa que utiliza recursión indirecta, y nos dice si un número es par o impar. Al igual que el programa anterior, hay otro método mucho más sencillo de determinar si un número es par o impar, basta con determinar el resto de la división entre dos. Por ejemplo: si hacemos par(2) devuelve 1 (cierto). Si hacemos impar(4) devuelve 0 (falso). /* declaracion de funciones, para evitar errores */ int par(int n); int impar(int n); int par(int n) { if (n == 0) return 1; return impar(n-1); } int impar(int n) { if (n == 0) return 0; return par(n-1); } En Pascal se hace así (notar el uso de forward): function impar(n : Integer) : Boolean; forward; function par(n : Integer) : Boolean; forward; function par(n : Integer) : Boolean; begin if n = 0 then par := true else par := impar(n-1) end; function impar(n : Integer) : Boolean; begin if n = 0 then impar := false else impar := par(n-1) end; 66 Ejemplo: si hacemos la llamada impar(3) hace las siguientes llamadas: par(2) impar(1) par(0) -> devuelve 1 (cierto) Por lo tanto 3 es un número impar. ¿Qué pasa si se hace una llamada recursiva que no termina? Cada llamada recursiva almacena los parámetros que se pasaron al procedimiento, y otras variables necesarias para el correcto funcionamiento del programa. Por tanto si se produce una llamada recursiva infinita, esto es, que no termina nunca, llega un momento en el que no quedará memoria para almacenar más datos, y en ese momento se abortará la ejecución del programa. Para probar esto se puede intentar hacer esta llamada en el programa factorial definido anteriormente: factorial(-1); Por supuesto no hay que pasar parámetros a una función que estén fuera de su dominio, pues el factorial está definido solamente para números naturales, pero es un ejemplo claro. ¿Cuándo utilizar la recursión? Para empezar, algunos lenguajes de programación no admiten el uso de recursividad, como por ejemplo el ensamblador o el FORTRAN. Es obvio que en ese caso se requerirá una solución no recursiva (iterativa). Tampoco se debe utilizar cuando la solución iterativa sea clara a simple vista. Sin embargo, en otros casos, obtener una solución iterativa es mucho más complicado que una solución recursiva, y es entonces cuando se puede plantear la duda de si merece la pena transformar la solución recursiva en otra iterativa. Posteriormente se explicará como eliminar la recursión, y se basa en almacenar en una pila los valores de las variables locales que haya para un procedimiento en cada llamada recursiva. Esto reduce la claridad del programa. Aún así, hay que considerar que el compilador transformará la solución recursiva en una iterativa, utilizando una pila, para cuando compile al código del computador. Por otra parte, casi todos los algoritmos basados en los esquemas de vuelta atrás y divide y vencerás son recursivos, pues de alguna manera parece mucho más natural una solución recursiva. 67 Aunque parezca mentira, es en general mucho más sencillo escribir un programa recursivo que su equivalente iterativo. Si el lector no se lo cree, posiblemente se deba a que no domine todavía la recursividad. Se propondrán diversos ejemplos de programas recursivos de diversa complejidad para acostumbrarse a la recursión. Ejercicio La famosa sucesión de Fibonacci puede definirse en términos de recurrencia de la siguiente manera: (1) Fib(1) = 1 ; Fib(0) = 0 (2) Fib(n) = Fib(n-1) + Fib(n-2) si n >= 2 ¿Cuantas llamadas recursivas se producen para Fib(6)?. Codificar un programa que calcule Fib(n) de forma iterativa. Nota: no utilizar estructuras de datos, puesto que no queremos almacenar los números de Fibonacci anteriores a n; sí se permiten variables auxiliares. Ejemplos de programas recursivos - Dados dos números a (número entero) y b (número natural mayor o igual que cero) determinar a^b. int potencia(int a, int b) { if (b == 0) return 1; else return a * potencia(a, b-1); } La condición de parada se cumple cuando el exponente es cero. Por ejemplo, la evaluación de potencia(-2, 3) es: potencia(-2, 3) -> (-2) · potencia(-2, 2) -> (-2) · (-2) · potencia(-2, 1) -> (-2) · (-2) · (-2) · potencia(-2, 0) -> (-2) · (-2) · (-2) · 1 y a la vuelta de la recursión se tiene: (-2) · (-2) · (-2) · 1 /=/ (-2) · (-2) · (-2) · potencia(-2,0) < (-2) · (-2) · (-2) /=/ (-2) · (-2) · potencia(-2, 1) < (-2) · 4 /=/ (-2) · potencia(-2,2) < -8 /=/ potencia(-2,3) en negrita se ha resaltado la parte de la expresión que se evalúa en cada llamada recursiva. 68 - Dado un array constituido de números enteros y que contiene N elementos siendo N >= 1, devolver la suma de todos los elementos. int sumarray(int numeros[], int posicion, int N) { if (posicion == N-1) return numeros[posicion]; else return numeros[posicion] + sumarray(numeros, posicion+1, N); } ... int numeros[5] = {2,0,-1,1,3}; int N = 5; printf("%d\n",sumarray(numeros, 0, N)); Notar que la condición de parada se cumple cuando se llega al final del array. Otra alternativa es recorrer el array desde el final hasta el principio (de derecha a izquierda): int sumarray(int numeros[], int posicion) { if (posicion == 0) return numeros[posicion]; else return numeros[posicion] + sumarray(numeros, posicion-1); } ... int numeros[5] = {2,0,-1,1,3}; int N = 5; printf("%d\n",sumarray(numeros, N-1)); - Dado un array constituido de números enteros, devolver la suma de todos los elementos. En este caso se desconoce el número de elementos. En cualquier caso se garantiza que el último elemento del array es -1, número que no aparecerá en ninguna otra posición. int sumarray(int numeros[], int posicion) { if (numeros[posicion] == -1) return 0; else return numeros[posicion] + sumarray(numeros, posicion+1); } ... int numeros[5] = {2,4,1,-3,-1}; printf("%d\n",sumarray(numeros, 0)); La razón por la que se incluye este ejemplo se debe a que en general no se conocerá el número de elementos de la estructura de datos sobre la que se trabaja. En ese caso se introduce un centinela -como la constante -1 de este ejemplo o la constante NULO para punteros, u otros valores como el mayor o menor entero que la máquina pueda representar- para indicar el fin de la estructura. 69 - Dado un array constituido de números enteros y que contiene N elementos siendo N >= 1, devolver el elemento mayor. int mayor(int numeros[], int posicion) { int aux; if (posicion == 0) return numeros[posicion]; else { aux = mayor(numeros, posicion-1); if (numeros[posicion] > aux) return numeros[posicion]; else return aux; } } ... int numeros[5] = {2,4,1,-3,-1}; int N = 5; printf("%d\n", mayor(numeros, 4)); - Ahora uno un poco más complicado: dados dos arrays de números enteros A y B de longitud n y m respectivamente, siendo n >= m, determinar si B está contenido en A. Ejemplo: A = {2,3,4,5,6,7,-3} B = {7,-3} -> contenido; B = {5,7} -> no contenido; B = {3,2} -> no contenido Para resolverlo, se parte del primer elemento de A y se compara a partir de ahí con todos los elementos de B hasta llegar al final de B o encontrar una diferencia. A = {2,3,4,5}, B = {3,4} -2,3,4,5 3,4 ^ En el caso de encontrar una diferencia se desplaza al segundo elemento de A y así sucesivamente hasta demostrar que B es igual a un subarray de A o que B tiene una longitud mayor que el subarray de A. 3,4,5 3,4 70 Visto de forma gráfica consiste en deslizar B a lo largo de A y comprobar que en alguna posición B se suporpone sobre A. Se han escrito dos funciones para resolverlo, contenido y esSubarray. La primera devuelve cierto si el subarray A y el array B son iguales; tiene dos condiciones de parada: o que se haya recorrido B completo o que no coincidan dos elementos. La segunda función es la principal, y su cometido es ir 'deslizando' B a lo largo de A, y en cada paso recursivo llamar una vez a la función contenido; tiene dos condiciones de parada: que el array B sea mayor que el subarray A o que B esté contenido en un subarray A. int contenido(int A[], int B[], int m, int pos, int desp) { if (pos == m) return 1; else if (A[desp+pos] == B[pos]) return contenido(A,B,m, pos+1, desp); else return 0; } int esSubarray(int A[], int B[], int n, int m, int desp) { if (desp+m > n) return 0; else if (contenido(A, B, m, 0, desp)) return 1; else return esSubarray(A, B, n, m, desp+1); } ... int A[4] = {2, 3, 4, 5}; int B[3] = {3, 4, 5}; if (esSubarray(A, B, 4, 5, 0)) printf("\nB esta contenido en A"); else printf("\nB no esta contenido en A"); Hay que observar que el requisito n >= m indicando en el enunciado es innecesario, si m > n entonces devolverá falso nada más entrar en la ejecución de esSubarray. Este algoritmo permite hacer búsquedas de palabras en textos. Sin embargo existen algoritmos mejores como el de Knuth-Morris-Prat, el de Rabin-Karp o mediante autómatas finitos; estos algoritmos som más complicados pero mucho más efectivos. - Dado un array constituido de números enteros y que contiene N elementos siendo N >= 1, devolver el elemento mayor. En este caso escribir un procedimiento, es decir, que el elemento mayor devuelto sea una variable que se pasa por referencia. 71 void mayor(int numeros[], int posicion, int *m) { if (posicion == 0) *m = numeros[posicion]; else { mayor(numeros, posicion-1, m); if (numeros[posicion] > *m) *m = numeros[posicion]; } } ... int numeros[5] = {2,4,1,-3,-1}; int M; mayor(numeros, 5-1, &M); printf("%d\n", M); Hay que tener cuidado con dos errores muy comunes: el primero es declarar la variable para que se pase por valor y no por referencia, con lo cual no se obtiene nada. El otro error consiste en llamar a la función pasando en lugar del parámetro por referencia una constante, por ejemplo: mayor(numeros, 5-1, 0); en este caso además se producirá un error de compilación. - La función de Ackermann, siendo n y m números naturales, se define de la siguiente manera: Ackermann(0, n) = n + 1 Ackermann(m, 0) = A(m-1, 1) Ackermann(m, n) = A(m-1, A(m, n-1)) Aunque parezca mentira, siempre se llega al caso base y la función termina. Probar a ejecutar esta función con diversos valores de n y m... ¡que no sean muy grandes!. En Internet pueden encontrarse algunas cosas curiosas sobre esta función y sus aplicaciones. Ejercicios propuestos Nota: para resolver los ejercicios basta con hacer un único recorrido sobre el array. Tampoco debe utilizarse ningún array auxiliar, pero si se podrán utilizar variables de tipo entero o booleano. - Dado un array constituido de números enteros y que contiene N elementos siendo N >= 1, escribir una función que devuelva la suma de todos los elementos mayores que el último elemento del array. - Dado un array constituido de números enteros y que contiene N elementos siendo N >= 1, escribir una función que devuelva cierto si la suma de la primera mitad de los enteros del array es igual a la suma de la segunda mitad de los enteros del array. 72 - Dados dos arrays A y B de longitud n y m respectivamente, n >= m cuyos elementos estén ordenados y no se repiten, determinar si todos los elementos de B están contenidos en A. Recordar que los elementos están ordenados, de esta manera basta con realizar un único recorrido sobre cada array. Conclusiones En esta sección se ha pretendido mostrar que la recursividad es una herramienta potente para resolver múltiples problemas. Es más, todo programa iterativo puede realizarse empleando expresiones recursivas y viceversa. Apuntadores Los apuntadores son una parte fundamental de C. Si usted no puede usar los apuntadores apropiadamente entonces esta perdiendo la potencia y la flexibilidad que C ofrece básicamente. El secreto para C esta en el uso de apuntadores. C usa los apuntadores en forma extensiva. ¿Porqué?    Es la única forma de expresar algunos cálculos. Se genera código compacto y eficiente. Es una herramienta muy poderosa. C usa apuntadores explícitamente con:    Es la única forma de expresar algunos cálculos. Se genera código compacto y eficiente. Es una herramienta muy poderosa. C usa apuntadores explícitamente con:    Arreglos, Estructuras y Funciones Definición de un apuntador Un apuntador es una variable que contiene la dirección en memoria de otra variable. Se pueden tener apuntadores a cualquier tipo de variable. El operador unario o monádico & devuelve la dirección de memoria de una variable. El operador de indirección o dereferencia * devuelve el ``contenido de un objeto apuntado por un apuntador''. 73 Para declarar un apuntador para una variable entera hacer: int *apuntador; Se debe asociar a cada apuntador un tipo particular. Por ejemplo, no se puede asignar la dirección de un short int a un long int. Para tener una mejor idea, considerar el siguiente código: main() { int x = 1, y = 2; int *ap; ap = &x; y = *ap; x = ap; *ap = 3; } Cuando se compile el código se mostrará el siguiente mensaje: warning: assignment makes integer from pointer without a cast. Con el objetivo de entender el comportamiento del código supongamos que la variable x esta en la localidad de la memoria 100, y en 200 y ap en 1000. Nota: un apuntador es una variable, por lo tanto, sus valores necesitan ser guardados en algún lado. int x = 1, y = 2; int *ap; ap = &x; 100 x 1 200 y 2 1000 ap 100 74 Las variables x e y son declaradas e inicializadas con 1 y 2 respectivamente, ap es declarado como un apuntador a entero y se le asigna la dirección de x (&x). Por lo que ap se carga con el valor 100. y = *ap; 100 x 1 200 y 1 1000 ap 100 Después y obtiene el contenido de ap. En el ejemplo ap apunta a la localidad de memoria 100 -- la localidad de x. Por lo tanto, y obtiene el valor de x -- el cual es 1. x = ap; 100 200 1000 ap 100 x 100 y 1 Como se ha visto C no es muy estricto en la asignación de valores de diferente tipo (apuntador a entero). Así que es perfectamente legal (aunque el compilador genera un aviso de cuidado) asigna el valor actual de ap a la variable x. El valor de ap en ese momento es 100. *ap = 3; 100 x 3 200 y 1 1000 ap 100 Finalmente se asigna un valor al contenido de un apuntador (*ap). Importante: Cuando un apuntador es declarado apunta a algún lado. Se debe inicializar el apuntador antes de usarlo. Por lo que: main() { int *ap; *ap = 100; } puede generar un error en tiempo de ejecución o presentar un comportamiento errático. 75 El uso correcto será: main() { int *ap; int x; ap = &x; *ap = 100; } Con los apuntadores se puede realizar también aritmética entera, por ejemplo: main() { float *flp, *flq; *flp = *flp + 10; ++*flp; (*flp)++; flq = flp; } NOTA: Un apuntador a cualquier tipo de variables es una dirección en memoria -la cual es una dirección entera, pero un apuntador NO es un entero. La razón por la cual se asocia un apuntador a un tipo de dato, es por que se debe conocer en cuantos bytes esta guardado el dato. De tal forma, que cuando se incrementa un apuntador, se incrementa el apuntador por un ``bloque'' de memoria, en donde el bloque esta en función del tamaño del dato. Por lo tanto para un apuntador a un char, se agrega un byte a la dirección y para un apuntador a entero o a flotante se agregan 4 bytes. De esta forma si a un apuntador a flotante se le suman 2, el apuntador entonces se mueve dos posiciones float que equivalen a 8 bytes. 76 Apuntadores y Funciones Cuando C pasa argumentos a funciones, los pasa por valor, es decir, si el parámetro es modificado dentro de la función, una vez que termina la función el valor pasado de la variable permanece inalterado. Hay muchos casos que se quiere alterar el argumento pasado a la función y recibir el nuevo valor una vez que la función ha terminado. Para hacer lo anterior se debe usar una llamada por referencia, en C se puede simular pasando un puntero al argumento. Con esto se provoca que la computadora pase la dirección del argumento a la función. Para entender mejor lo anterior consideremos la función swap() que intercambia el valor de dos argumentos enteros: void swap(int *px, int *py); main() { int x, y; x = 10; y = 20; printf("x=%d\ty=%d\n",x,y); swap(&x, &y); printf("x=%d\ty=%d\n",x,y); } void swap(int *px, int *py) { int temp; temp = *px; /* guarda el valor de la direccion x */ *px = *py; /* pone y en x */ *py = temp; /* pone x en y */ } 77 Apuntadores y arreglos Existe una relación estrecha entre los punteros y los arreglos. En C, un nombre de un arreglo es un índice a la dirección de comienzo del arreglo. En esencia, el nombre de un arreglo es un puntero al arreglo. Considerar lo siguiente: int a[10], x; int *ap; ap = &a[0]; x = *ap; /* ap apunta a la direccion de a[0] */ /* A x se le asigna el contenido de ap (a[0] en este caso) */ *(ap + 1) = 100; /* Se asigna al segundo elemento de 'a' el valor 100 usando ap*/ Como se puede observar en el ejemplo la sentencia a[t] es idéntica a ap+t. Se debe tener cuidado ya que C no hace una revisión de los límites del arreglo, por lo que se puede ir fácilmente más alla del arreglo en memoria y sobreescribir otras cosas. C sin embargo es mucho más sútil en su relación entre arreglos y apuntadores. Por ejemplo se puede teclear solamente: ap = a; en vez de ap = &a[0]; y también *(a + i) en vez de a[i], esto es, &a[i] es equivalente con a+i. Y como se ve en el ejemplo, el direccionamiento de apuntadores se puede expresar como: a[i] que es equivalente a *(ap + i) Sin embargo los apuntadores y los arreglos son diferentes:   Un apuntador es una variable. Se puede hacer ap = a y ap++. Un arreglo NO ES una variable. Hacer a = ap y a++ ES ILEGAL. Este parte es muy importante, asegúrese haberla entendido. Con lo comentado se puede entender como los arreglos son pasados a las funciones. Cuando un arreglo es pasado a una función lo que en realidad se le esta pasando es la localidad de su elemento inicial en memoria. 78 Por lo tanto: strlen(s) es equivalente a strlen(&s[0]) Esta es la razón por la cual se declara la función como: int strlen(char s[]); y una declaración equivalente es int strlen(char *s); ya que char s[] es igual que char *s. La función strlen() es una función de la biblioteca estándar que regresa la longitud de una cadena. Se muestra enseguida la versión de esta función que podría escribirse: int strlen(char *s) { char *p = s; while ( *p != '\0' ) p++; return p - s; } Se muestra enseguida una función para copiar una cadena en otra. Al igual que en el ejercicio anterior existe en la biblioteca estándar una función que hace lo mismo. void strcpy(char *s, char *t) { while ( (*s++ = *t++) != '\0' ); } En los dos últimos ejemplos se emplean apuntadores y asignación por valor. Nota: Se emplea el uso del caracter nulo con la sentencia while para encontrar el fin de la cadena. Arreglos de apuntadores En C se pueden tener arreglos de apuntadores ya que los apuntadores son variables. A continuación se muestra un ejemplo de su uso: ordenar las líneas de un texto de diferente longitud. Los arreglos de apuntadores son una representación de datos que manejan de una forma eficiente y conveniente líneas de texto de longitud variable. 79 ¿Cómo se puede hacer lo anterior? 1. Guardar todas las líneas en un arreglo de tipo char grande. Observando que \n marca el fin de cada línea. Ver figura 8.1. 2. Guardar los apuntadores en un arreglo diferente donde cada apuntador apunta al primer caracter de cada línea. 3. Comparar dos líneas usando la función de la biblioteca estándar strcmp(). 4. Si dos líneas están desacomodadas -- intercambiar (swap) los apuntadores (no el texto). Figura 8.1: Arreglos de apuntadores (Ejemplo de ordenamiento de cadenas). Con lo anterior se elimina:   el manejo complicado del almacenamiento. alta sobrecarga por el movimiento de líneas. 80 Arreglos multidimensionales y apuntadores Un arreglo multidimensional puede ser visto en varias formas en C, por ejemplo: Un arreglo de dos dimensiones es un arreglo de una dimensión, donde cada uno de los elementos es en sí mismo un arreglo. Por lo tanto, la notación a[n][m] nos indica que los elementos del arreglo están guardados renglón por renglón. Cuando se pasa una arreglo bidimensional a una función se debe especificar el número de columnas -- el número de renglones es irrelevante. La razón de lo anterior, es nuevamente los apuntadores. C requiere conocer cuantas son las columnas para que pueda brincar de renglón en renglón en la memoria. Considerando que una función deba recibir int a[5][35], se puede declarar el argumento de la función como: f( int a[][35] ) { ..... } o aún f( int (*a)[35] ) { ..... } En el último ejemplo se requieren los parénteis (*a) ya que [ ] tiene una precedencia más alta que *. Por lo tanto: int (*a)[35]; declara un apuntador a un arreglo de 35 enteros, y por ejemplo si hacemos la siguiente referencia a+2, nos estaremos refiriendo a la dirección del primer elemento que se encuentran en el tercer renglón de la matriz supuesta, mientras que int *a[35]; declara un arreglo de 35 apuntadores a enteros. Ahora veamos la diferencia (sutil) entre apuntadores y arreglos. El manejo de cadenas es una aplicación común de esto. 81 Considera: char *nomb[10]; char anomb[10][20]; En donde es válido hacer nomb[3][4] y anomb[3][4] en C. Sin embargo: anomb es un arreglo verdadero de 200 elementos de dos dimensiones tipo char. El acceso de los elementos anomb en memoria se hace bajo la siguiente fórmula 20*renglon + columna + dirección_base En cambio nomb tiene 10 apuntadores a elementos. NOTA: si cada apuntador en nomb indica un arreglo de 20 elementos entonces y solamente entonces 200 chars estarán disponibles (10 elementos). Con el primer tipo de declaración se tiene la ventaja de que cada apuntador puede apuntar a arreglos de diferente longitud. Considerar: char *nomb[] = { "No mes", "Ene", "Feb", "Mar", .... }; char anomb[][15] = { "No mes", "Ene", "Feb", "Mar", ... }; 82 Lo cual gráficamente se muestra en la figura 8.2. Se puede indicar que se hace un manejo más eficiente del espacio haciendo uso de un arreglo de apuntadores y usando un arreglo bidimensional. Figura 8.2: Arreglo de 2 dimensiones VS. arreglo de apuntadores. Inicialización estática de arreglos de apuntadores La inicialización de arreglos de apuntadores es una aplicación ideal para un arreglo estático interno, por ejemplo: func_cualquiera() { static char *nomb[] = { "No mes", "Ene", "Feb", "Mar", .... }; } Recordando que con el especificador de almacenamiento de clase static se reserva en forma permanente memoria el arreglo, mientras el código se esta ejecutando. 83 Apuntadores y estructuras Los apuntadores a estructuras se definen fácilmente y en una forma directa. Considerar lo siguiente: main() { struct COORD { float x,y,z; } punto; struct COORD *ap_punto; punto.x = punto.y = punto.z = 1; ap_punto = &punto; /* Se asigna punto al apuntador */ ap_punto->x++; ap_punto->y+=2; ap_punto->z=3; } /* Con el operador -> se accesan los miembros */ /* de la estructura apuntados por ap_punto */ Otro ejemplo son las listas ligadas: typedef struct { int valor; struct ELEMENTO *sig; } ELEMENTO; ELEMENTO n1, n2; n1.sig = &n2; La asignación que se hace corresponde a la figura 8.3 Figura 8.3: Esquema de una lista ligada con 2 elementos. Nota: Solamente se puede declarar sig como un apuntador tipo ELEMENTO. No se puede tener un elemento del tipo variable ya que esto generaría una definición recursiva la cual no esta permitida. Se permite poner una referencia a un apuntador ya que los los bytes se dejan de lado para cualquier apuntador. 84 Fallas comunes con apuntadores A continuación se muestran dos errores comunes que se hacen con los apuntadores.     No asignar un apuntador a una dirección de memoria antes de usarlo int *x *x = 100; lo adecuado será, tener primeramente una localidad física de memoria, digamos int y; int *x, y; x = &y; *x = 100;  Indirección no válida Supongamos que se tiene una función llamada malloc() la cual trata de asignar memoria dinámicamente (en tiempo de ejecución), la cual regresa un apuntador al bloque de memoria requerida si se pudo o un apuntador a nulo en otro caso. char *malloc() -- una función de la biblioteca estándar que se verá más adelante. Supongamos que se tiene un apuntador char *p Considerar: *p = (char *) malloc(100): *p = 'y'; /* pide 100 bytes de la memoria */ Existe un error en el código anterior. ¿Cuál es? El * en la primera línea ya que malloc regresa un apuntador y *p no apunta a ninguna dirección. El código correcto deberá ser: p = (char *) malloc(100); 85 Ahora si malloc no puede regresar un bloque de memoria, entonces p es nulo, y por lo tanto no se podrá hacer: *p = 'y'; Un buen programa en C debe revisar lo anterior, por lo que el código anterior puede ser reescrito como: p = (char *) malloc(100): /* pide 100 bytes de la memoria */ if ( p == NULL ) { printf("Error: fuera de memoria\n"); exit(1); } *p = 'y'; 86 Bibliografía Estructura general de un programa en C mimosa.pntic.mec.es/~flarrosa/lengc.pdf Entornos de programación C http://www.malavida.com/blog/355/entornos-de-programacion-c Tipos de Entornos de Programación http://www.fismat.umich.mx/mn1/manual/node8.html Compilación http://www.fismat.umich.mx/mn1/manual/node8.html Ejecución del programa http://www.fismat.umich.mx/mn1/manual/node8.html Tipos de datos http://www.slideshare.net/javi2401/variables-constantes-y-tipos-de-datos-en-cpresentation Enteros http://www.fismat.umich.mx/mn1/manual/node8.html Flotantes http://www.fismat.umich.mx/mn1/manual/node8.html Caracteres y Cadenas en C http://www.slideshare.net/javi2401/caracteres-y-cadenas-en-c-presentation Variables y Constantes en C http://www.slideshare.net/javi2401/variables-constantes-y-tipos-de-datos-en-cpresentation Entrada y salida http://mimosa.pntic.mec.es/~flarrosa/lengc.pdf 87 Tipos de operadores y expresiones http://jtortone.com.ar/utnconfluencia/recursividadypractica.pdf Tipos estructurados http://ocw.upm.es/ciencia-de-la-computacion-e-inteligencia-artificial/fundamentosprogramacion/contenidosteoricos/ocwfundamentosprogramaciontema6.pdf Funciones http://www.algoritmia.net/articles.php?id=11 http://es.wikibooks.org/wiki/Programaci%C3%B3n_en_C%2B%2B/Funciones Apuntadores http://www.fismat.umich.mx/mn1/manual/node9.html 88
Copyright © 2024 DOKUMEN.SITE Inc.