===== Capítulo 3: Control de Flujo ===== Las proposiciones de control de flujo de un lenguaje especifican el orden en que se realiza el procesamiento. Ya hemos visto la mayoría de las construcciones de control de flujo en ejemplos anteriores; aquí completaremos el conjunto, y sere­mos más precisos acerca de las discutidas con anterioridad. ==== 3.1 Proposiciones y bloques ==== Una expresión como ''x = 0'' ó ''i++'' o ''printf(...)'' se convierte en una //proposi­ción// cuando va seguida de un punto y coma '';'', como en x = 0; i++; printf(...); En C, el punto y coma '';'' es un terminador de proposición, en lugar de un separa­dor, como lo es en un lenguaje tipo Pascal. Las llaves ''{'' y ''}'' se emplean para agrupar declaraciones y proposiciones dentro de una //proposición compuesta// o //bloque//, de modo que son sintácticamente equi­valentes a una proposición sencilla. Las llaves que encierran las proposiciones de una función son un ejemplo obvio; otros ejemplos son las llaves alrededor de proposiciones múltiples después de un ''if'', ''else'', ''while'' o ''for''. (Pueden declararse variables dentro de cualquier bloque; esto se expondrá en el [[#capitulo 4|capítulo 4]]). No hay punto y coma después de la llave derecha que termina un bloque. ==== 3.2 If-Else ==== La proposición ''if-else'' se utiliza para expresar decisiones. Formalmente, la sintaxis es if (expresión) proposición1 else proposición2 donde la parte del ''else'' es optativa. La expresión se evalúa; si es verdadera (esto es, si la expresión tiene un valor diferente de cero), la //proposición1// se ejecuta. Si es falsa (//expresión// es cero) y si existe una parte de ''else'', se ejecuta en su lugar la //proposición2//. Puesto que un ''if'' simplemente prueba el valor numérico de una expresión, son posibles ciertas abreviaciones de código. Lo más obvio es escribir if (expresión) en lugar de if (expresión != 0) Algunas veces esto es claro y natural; otras puede ser misterioso. Debido a que la parte ''else'' de un ''if-else'' es optativa, existe una ambigüedad cuando un ''else'' se omite de una secuencia ''if'' anidada. Esto se resuelve al asociar el ''else'' con el ''if'' sin ''else'' anterior más cercano. Por ejemplo, en if (n > 0) if (a > b) z = a; else z = b; el ''else'' va con el ''if'' más interno, como se muestra con el sangrado. Si eso no es lo que se desea, se deben utilizar llaves para forzar la asociación correcta: if (n > 0) { if (a > b) z = a; } else z = b; 4 La ambigüedad es especialmente perniciosa en situaciones como esta: if (n > 0) for (i = 0; i < n; i++) if (s[i] > 0) { printf("..."); return i; } else /* EQUIVOCADO */ printf("error -- n es negativo\n"); El sangrado muestra en forma inequívoca lo que se desea, pero el compilador no entiende el mensaje y asocia el ''else'' con el ''if'' más interno. Puede ser difícil encon­trar esta clase de errores; es una buena idea utilizar llaves cuando hay varios ''if'' anidados. A propósito, nótese que hay un punto y coma '';'' después de ''z = a'' en if (a > b) z = a; else z = b; Esto se debe a que gramaticalmente al ''if'' le sigue una //proposición//, y una expresión como "''z = a;''" siempre se termina con punto y coma '';''. ==== 3.3 Else-if ==== La construcción if (expresión) proposición else if (expresión) proposición else if (expresión) proposición else if (expresión) proposición else proposición ocurre de modo tan frecuente que bien vale una pequeña discusión aparte. Esta secuencia de proposiciones ''if'' es la forma más general de escribir una decisión múltiple. Las //expresiones// se evalúan en orden; si cualquier //expresión// es verdade­ra, se ejecuta la //proposición// asociada con ella, y esto termina toda la cadena. Co­mo siempre, el código para cada //proposición// es una proposición simple o un grupo dentro de llaves. La parte del último ''else'' maneja el caso “ninguno de los anteriores” o //caso por omisión// cuando ninguna de las otras condiciones se satisface. En algunos ca­sos no hay una acción explícita para la omisión; en ese caso el último else proposición puede omitirse, o puede utilizarse para detección de errores al atrapar una condición “imposible” . Para ilustrar una decisión de tres vías, se muestra a continuación una función de búsqueda bi­naria que decide si un valor particular de ''x'' se encuentra en el arreglo ordenado ''v'' - Los elementos de ''v'' deben estar en orden ascendente. La función regresa la po­sición (un número entre ''0'' y ''n-1'') si ''x'' ocurre en ''v'', y ''-1'' si no es así. La búsqueda binaria primero compara el valor de entrada ''x'' con el elemento medio del arreglo ''v''. Si ''x'' es menor que el valor del medio, la búsqueda se enfoca sobre la mitad inferior de la tabla; de otra manera lo hace en la mitad superior, cualquier caso, el siguiente paso es comparar a ''x'' con el elemento medio de la mitad seleccionada. Este proceso de dividir en dos continúa hasta que se en­cuentra el valor o ya no hay elementos. /* binsearch: encuentra x en v[0] <= v[1] <= ... <= v[n-1] */ { int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low+high)/2; if (x < v[mid]) high = mid + 1; else if (x > v[mid]) low = mid + 1; else /* el elemento fue encontrado */ return mid; } return -1; /* no fue encontrado */ } La decisión fundamental es si ''x'' es menor que, mayor que o igual al elemento me­dio ''v[mid]'' en cada paso; esto es un ''else-if'' natural. * **Ejercicio 3-1**. Nuestra búsqueda binaria realiza dos pruebas dentro del ciclo, cuando una podría ser suficiente (al precio de más pruebas en el exterior). Escriba una versión con sólo una prueba dentro del ciclo y mida la diferencia en tiempo de ejecución. □ ==== 3.4 Switch ==== La proposición ''switch'' es una decisión múltiple que prueba si una expresión coincide con uno de un número de valores //constantes// enteros, y traslada el con­trol adecuadamente. switch (expresión) { case exp-const: proposiciones case exp-const: proposiciones default: proposiciones ) Cada ''case'' se etiqueta con uno o más valores constantes enteros o expresiones constantes enteras. Si un ''case'' coincide con el valor de la expresión, la ejecución comienza allí. Todas las expresiones ''case'' deben ser diferentes. El que está etiquetado co­mo ''default'' se ejecuta sí ninguno de los otros se satisface. El ''default'' es optativo; si no está y ninguno de los casos coincide, no se toma acción algu­na. Las cláusulas ''case'' y ''default'' pueden ocurrir en cualquier orden. En el [[#capitulo 1|capítulo 1]] se escribió un programa para contar las ocurrencias de cada dígito, espacio en blanco y todos los demás caracteres, usando una secuencia de ''if ... else if ... else''. Aquí está el mismo programa con un ''switch'': #include main() /* cuenta dígitos, espacios blancos, y otros */ { int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getchar()) != EOF) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ndigit[c-'0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; } } printf("digits ="); for (i = 0; i < 10; i++) printf(" %d", ndigit[i]); printf(", espacio en blanco = %d, otros = %d\n", nwhite, nother); return 0; } La proposición ''break'' provoca una salida inmediata del ''switch''. Puesto que los ''case'' sirven sólo como etiquetas, después de que se ejecuta el código para Uno, la ejecución //pasa al siguiente//, a menos que se tome una acción específica para terminar el ''switch''. Las formas más comunes de dejar un ''switch'' son ''break'' y ''return''. Una proposición ''break'' también se puede emplear para forzar una salida inmediata de los ciclos ''while'', ''for'' y ''do'', como se verá más adelante en este capítulo. //Pasar a través// de los ''case'' es en parte bueno y en parte no. Por el lado positivo, esto permite conectar varios ''case'' a una acción simple, como con los dígitos de este ejemplo. Pero eso también implica que cada case normalmente debe termi­nar con un ''break'' para prevenir pasar al siguiente. Pasar de un ''case'' a otro no es una práctica muy sólida y es susceptible a la desintegración cuando se modifica el programa. Con la excepción de etiquetas múltiples para un cálculo simple, lo anterior se debe utilizar con cautela y emplear comentarios. Como formalidad, coloque un ''break'' después del último ''case'' (en este caso el ''default'') aun si desde ek punto de vista lógico resulta innecesario. Algún día - cuando se agregue otro ''case'' al final - esta práctica de programación defensiva lo salvará. * **Ejercicio 3-2**. Escriba una función ''escape(s,t)'' que convierte caracteres como //nueva línea// y //tabulación// en secuencias de escape visibles como ''\n'' y ''\t'' mientras copia la cadena ''t'' a ''s''. Utilice un ''switch''. Escriba también una función para la di­rección inversa, convirtiendo secuencias de escape en caracteres reales. □ ==== 3.5 Ciclos - While y For ==== Ya hemos encontrado los ciclos ''while'' y ''for''. En while (expresión) proposición la expresión se evalúa. Si es diferente de cero, se ejecuta la //proposición// y se reevalúa la //expresión//. Este ciclo continúa hasta que la //expresión// se hace cero, punto en el cual se suspende la ejecución para continuar después de la //proposición//. La proposición ''for'' for (expr1; expr2; expr3) proposición es equivalente a expr1; while (expr2) { proposición expr3; } excepto por el comportamiento de ''continue'' que se describe en la [[#3.7|sección 3.7]]. Gramaticalmente, las tres componentes de un ciclo ''for'' son expresiones. Por lo común, //expr1// y //expr3// son asignaciones o llamadas a función y //expr2// es una ex­presión de relación. Cualquiera de las tres partes se puede omitir, aunque deben permanecer los punto y coma '';''. Si se omite //expr1// o //expr3//, sólo se desecha de la expansión. Si la prueba //expr2// no está presente, se toma como permanentemente verdadera, así que for (;;) { ... } es una iteración “infinita”, que presumiblemente será interrumpida por otros medios, como un ''break'' o un ''return''. El usar ''while'' o ''for'' es principalmente cuestión de preferencia personal. Por ejemplo, en while ((c = getchar()) == ' ' || c == '\n' || c = '\t') ; /* ignora caracteres espaciadores */ no hay inicialización o reinicialización, por lo que el ''while'' es más natural. El ''for'' se prefiere cuando existe una inicialización simple e incrementos, puesto que mantiene las proposiciones de control del ciclo juntas y visibles al principio del mismo. Esto es más obvio en for (i = 0; i < n; i++) ... que es la forma característica de procesar los primeros //n// elementos de un arreglo en C, lo análogo al ciclo ''DO'' de Fortran o al ''for'' de Pascal. Sin embargo, la analo­gía no es perfecta puesto que tanto el índice como el límite de un ciclo ''for'' en C pueden ser alterados desde dentro del ciclo, y la variable del Índice retiene su valor cuando por cualquier razón terminan las iteraciones. Puesto que las componen­tes del ''for'' son expresiones arbitrarias, sus ciclos no están restringidos a progresio­nes aritméticas. Por otra parte, considere que es un mal estilo incluir en las secciones de inicialización e incremento operaciones no relacionadas con esas ac­tividades, que más bien se reservan para acciones de control del ciclo. Como un ejemplo más amplio, aquí está otra versión de ''atoi'' para convertir una cadena a su equivalente numérico. Esta es ligeramente más general que la del [[#capitulo 2|capítulo 2]]; trata también los espacios en blanco previos al número, y los signos ''+'' o ''-''. (El [[#capitulo 4|capitulo 4]] muestra ''atof'', que realiza la misma conversión para números de punto flotante). La estructura del programa refleja la forma de la entrada: * ignora espacios en blanco, si los hay * toma el signo, si lo hay * toma la parte entera y conviértela Cada paso realiza su parte, y deja las cosas en forma clara para el siguiente. La totalidad del proceso termina con el primer carácter que no pueda ser parte de un número. #include /* atoi: convierte s a entero; versión 2 */ int atoi(char s[]) { int i, n, sign; for (i = 0; isspace(s[i]); i++) /* ignora espacio en blanco * / ; sign = (s[i] == '-') ? -1 : 1; if (s[i] == '+' || s[i] == '-') /* ignora el signo */ i++; for (n = 0; isdigit(s[i]); i++) n = 10 * n + (s[i] - '0'); return sign * n; } La biblioteca estándar proporciona una función más elaborada, ''strtol'', para la conversión de cadenas a enteros largos; véase la [[#apendice b#seccion 5|sección 5 del apéndice B]]. Las ventajas de mantener centralizado el control del ciclo son aún más obvias cuando existen ciclos anidados. La siguiente función es una clasificación Shell para ordenar un arreglo de enteros. La idea básica de este algoritmo de ordena­miento - inventado en 1959 por D.L. Shell - es que en las primeras etapas sean comparados elementos lejanos (en lugar de los adyacentes, como en los or­denamientos de intercambio más simples). Esto tiende a eliminar rápidamente gran cantidad de desorden, así que los estados posteriores tienen menos trabajo por hacer. El intervalo entre los elementos comparados disminuye en forma gra­dual hasta uno, punto en el que el ordenamiento viene a ser efectivamente un mé­todo adyacente de intercambio. /* shellsort: ordena v[0]...v[n-1] en orden ascendente */ void shellsort(int v[], int n) { int gap, i, j, temp; for (gap = n/2; gap > 0; gap /= 2) for (i = gap; i < n; i++) for (j=i-gap; j>=0 && v[j]>v[j+gap]; j-=gap) { temp = v[j]; v[j] = v[j+gap]; v[j+gap] = temp; } } Existen tres ciclos anidados. El más externo controla el espacio entre los elementos comparados, reduciéndolo desde ''n/2'' por un factor de dos en cada paso hasta que llega a cero. El ciclo intermedio recorre los elementos. El ciclo más interno compara cada pareja de elementos que está separada por el espacio ''gap'' e invierte a las que estén desordenadas. Puesto que ''gap'' finalmente se reduce a uno, todos los elementos se ordenan correctamente. Nótese cómo la generalidad del ''for'' hace que el ciclo más externo coincida con la forma de los otros, aun cuando no es una progresión aritmética. Un último operador de C es la coma '','' que frecuentemente encuentra uso en la proposición ''for''. Una pareja de expresiones separadas por una coma se eva­lúa de izquierda a derecha, y el tipo y valor del resultado son el tipo y valor del operando derecho. Así, en una proposición ''for'' es posible colocar expresiones múltiples en las diferentes partes, por ejemplo, para procesar dos índices en para­lelo. Esto se ilustra en la función ''reverse(s)'', que invierte a la cadena ''s'' en el mis­mo lugar. #include /* reverse: invierte la cadena s en el mismo lugar */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } Las comas que separan a los argumentos de una función, las variables en declara­ciones, etc., __no son__ operadores coma, y no garantizan evaluación de izquierda a derecha. Los operadores coma deberán utilizarse poco. Los usos más adecuados son en construcciones fuertemente relacionadas una con la otra, como en el ciclo ''for'' de ''reverse'', y en macros en donde un cálculo de paso múltiple debe ser una expre­sión simple. Una expresión coma podría también ser apropiada para el intercambio de elementos en ''reverse'', donde el intercambio puede ser a través de una operación simple: for (i = 0, j = strlen(s)-1; i < j; i++, j--) c = s[i], s[i] = s[j], s[j] = c; * **Ejercicio 3-3**. Escriba la función ''expand(sl,s2)'' que expande notación abreviada como ''a-z'', que viene en la cadena ''s1'', en la lista equivalente completa ''abc...xyz'' en ''s2''. Permita letras mayúsculas y minúsculas, así como dígitos, y esté preparado para manejar casos como ''a-b-c'' y ''a-z0-9'' y ''—a—z''. Haga que los guiones ''-'' al inicio o al final se tomen literalmente. □ ==== 3.6 Ciclos - do-while ==== Como ya se expuso en el [[#capitulo 1|capítulo 1]], los ciclos ''while'' y ''for'' verifican al principio la condición de término. En contraste, el tercer ciclo en C, el ''do-while'', prueba al final __después__ de realizar cada paso a través del cuerpo del ciclo, el cual se ejecu­ta siempre por lo menos una vez. La sintaxis del do es do proposición while (expresión); La //proposición// se ejecuta y después se evalúa la //expresión//. Si es verdadera, la //proposición// se evalúa de nuevo, y así sucesivamente. Cuando la expresión se hace falsa, el ciclo termina. Excepto por el sentido de la prueba, el ''do-while'' es equiva­lente a la proposición ''repeat-until'' de Pascal. La experiencia demuestra que el ''do-while'' es mucho menos utilizado que el ''while'' y el ''for''. Aunque de cuando en cuando es valioso, como en la siguiente función ''itoa'', que convierte un número a una cadena de caracteres (lo inverso de ''atoi''). El trabajo es ligeramente más complicado de lo que podría pensarse en un principio, debido a que los métodos fáciles para generar dígitos los generan en el orden incorrecto. Hemos elegido generar la cadena al revés y después invertirla. /* itoa: convierte n a caracteres en s */ void itoa(int n, char s[]) { int i, sign; if ((sign = n) < 0) /* registra el signo */ n = -n; /* hace a n positivo */ i = 0; do { /* genera digitos en orden inverso */ s[i++] = n % 10 + '0'; /* obtiene siguiente dígito */ } while ((n /= 10) > 0); /* lo borra */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; reverse(s); } El ''do-while'' es necesario, o al menos conveniente, puesto que por lo menos se de­be instalar un carácter en el arreglo ''s'', aun si ''n'' es cero. También empleamos llaves alrededor de la proposición simple que hace el cuerpo del do-while, aunque son innecesarias, y así el lector apresurado no confundirá la sección del ''while'' con el //comienzo// de un ciclo ''while''. * **Ejercicio 3-4**. En una representación de números en complemento a dos, nuestra versión de ''itoa'' no maneja el número negativo más grande, esto es, el valor de ''n'' igual a ''-( 2^tamañopalabra-1)''. Explique por qué. Modifíquelo para imprimir el valor correctamente, sin importar la máquina en que ejecute. □ * **Ejercicio 3-5**. Escriba la función ''itob(n,s,b)'' que convierte al entero ''n'' en una re­presentación de caracteres con base ''b'' dentro de la cadena ''s''. En particular, ''itob(n,S,16)'' da formato a ''s'' como un entero hexadecimal en ''s''. □ * **Ejercicio 3-6**. Escriba una versión de ''itoa'' que acepte tres argumentos en lugar de dos. El tercer argumento es un ancho mínimo de campo; si es necesario, al número converti­do debe agregársele caracteres en blanco a la izquierda para hacerlo lo suficiente­mente ancho. □ ==== 3.7 Break y Continue ==== Algunas veces es conveniente tener la posibilidad de abandonar un ciclo de otra manera que no sea probando al inicio o al final. La proposición ''break'' pro­porciona una salida anticipada de un ''for'', ''while'' y ''do'', tal como lo hace el ''switch''. Un ''break'' provoca que el ciclo o ''switch'' más interno que lo encierra termine inme­diatamente. La siguiente función, ''trim'', elimina espacios blancos, tabuladores y nuevas lí­neas al final de una cadena, utilizando un ''break'' para salir de un ciclo cuando se encuentra el no-blanco, no-tabulador o no-nueva línea de más a la derecha. /* trim: elimina blancos, tabuladores y nueva linea al final */ int trim(char s[]) { int n; for (n = strlen(s)-1; n >= 0; n--) if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') break; s[n+1] = '\0'; return n; } ''strlen'' regresa la longitud de la cadena. El ciclo ''for'' inicia al final y rastrea hacia atrás, buscando el primer carácter que no sea blanco o tabulador o nueva linea. El ciclo se interrumpe cuando se encuentra alguno o cuando //n// se hace negativa (esto es, cuando se ha rastreado toda la cadena. Se deberá verificar que este comportamiento es correcto, aun cuando la cadena esté vacía o sólo contiene espacios en blanco. La proposición ''continue'' está relacionada con el ''break'', pero se utiliza menos; provoca que inicie la siguiente iteración del ciclo ''for'', ''while'' o ''do'' que la contiene. Dentro de ''while'' y ''do'', esto significa que la parte de la prueba se ejecuta inmediatamente; en el ''for'', el control se traslada al paso de incremento. La propo­sición continué se aplica solamente a ciclos, no a ''switch''. Un ''continue'' dentro de un ''switch'' que está a su vez en un ciclo, provoca la siguiente iteración del ciclo. Como un ejemplo, el siguiente fragmento procesa sólo los elementos no negativos que están en el arreglo a; los valores negativos son ignorados. for (i = 0; i < n; i++) if (a[i] < 0) /* ignora elementos negativos */ continue; ... /* trabaja elementos positivos */ La proposición ''continue'' se emplea a menudo cuando la parte del ciclo que sigue es complicada, de modo que invertir la prueba y sangrar otro nivel podría anidar profundamente el programa. ==== 3.8 Goto y Etiquetas ==== C proporciona la infinitamente abusable proposición ''goto'', y etiquetas para saltar hacia ellas. Formalmente, el ''goto'' nunca es necesario, y en la práctica es casi siempre más fácil escribir código sin él. En este libro no se ha usado ''goto'' alguno. Sin embargo, hay algunas situaciones donde los ''goto'' pueden encontrar un lu­gar. La más común es abandonar el procesamiento en alguna estructura profun­damente anidada, tal como salir de dos o más ciclos a la vez. La proposición ''break'' no se puede utilizar directamente, puesto que sólo sale del ciclo más inter­no. Así: for ( ... ) for ( ... ) { ... if (desastre) goto error; } ... error: /* arregla el desorden /* Esta organización es útil si el código de manejo de error no es trivial y si los erro­res pueden ocurrir en varios lugares. Una etiqueta tiene la misma forma que un nombre de variable y es seguida por dos puntos '':''. Puede ser adherida a cualquier proposición de la misma función en la que está el ''goto''. El alcance de una etiqueta es toda la función. Como otro ejemplo, considérese el problema de determinar si dos arreglos, ''a'' y ''b'', tienen un elemento en común. Una posibilidad es for (i = 0; i < n; i++) for (j = 0; j < m; j++) if (a[i] == b[j]) goto encontrado; /* no se encontró ningún elemento común */ ... encontrado: /* se encontró uno: a[i] == b[j] */ ... El código que involucra un ''goto'' siempre puede escribirse sin él, aunque tal vez al precio de algunas pruebas repetidas o variables extra. Por ejem plo, la bús­queda en los arreglos quedará encontrado = 0; for (i = 0; i < n && !encontrado; i++) for (j = 0; j < m && !encontrado; j++) if (a[i] == b[j]) encontrado = 1; if (encontrado) /* Se encontró uno: a[i-1] == b[j-1] */ ... else /* no se encontró ningún elemento común */ ... Con pocas excepciones, como las citadas aquí, el código que se basa en proposiciones ''goto'' es generalmente más difícil de entender y de mantener que el códi­go sin ellas. Aunque no somos dogmáticos acerca del asunto, se ve que las proposiciones ''goto'' deben ser utilizadas raramente, si acaso. __**Continuar:**__ [[el_lenguaje_de_programacion_c_-_capitulo_4|Capítulo 4]]