===== 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 seremos 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 //proposició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 separador, 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 equivalentes 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 encontrar 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 verdadera, se ejecuta la //proposición// asociada con ella, y esto termina toda la cadena. Como 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 casos 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 binaria 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 posició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 encuentra 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 medio ''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 control 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 como ''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 alguna. 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 terminar 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 direcció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 expresió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 analogí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 componentes del ''for'' son expresiones arbitrarias, sus ciclos no están restringidos a progresiones 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 actividades, 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 ordenamiento - 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 ordenamientos 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 gradual 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 evalú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 paralelo. Esto se ilustra en la función ''reverse(s)'', que invierte a la cadena ''s'' en el mismo 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 declaraciones, 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 expresió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 ejecuta 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 equivalente 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 debe 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 representació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 convertido debe agregársele caracteres en blanco a la izquierda para hacerlo lo suficientemente 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'' proporciona 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 inmediatamente.
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 proposició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 lugar. La más común es abandonar el procesamiento en alguna estructura profundamente 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 interno. 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 errores 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úsqueda 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ódigo 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]]