la35.net blog para alumnos de la ET Nº35

Condicionales y loops en C

Los programas que vimos hasta ahora ejecutaban cada uno de sus enunciados de manera secuencial. Uno atrás del otro, en el orden que aparecían en el código. La mayoría de los programas reales pueden variar la cantidad de veces que ejecutan un enunciado o el orden de los mismos.

Las estructuras de control de un lenguaje de programación son el mecanismo que nos permite alterar el orden y la cantidad de veces que ejecutamos un conjunto de enunciados.

La estructura condicional nos permite ejecutar un grupo de enunciados u otro en base a una condición. Los ciclos o loops nos permiten repetir un grupo de enunciados, dependiendo de que se cumpla una condición dada.

Contenidos

  1. Condicionales
  2. Loops
  3. Ciclos anidados
  4. Aplicaciones

Condicionales

Supongamos que queremos ejecutar un enunciado o enunciados solo si se cumple una condición lógica. Por ejemplo si queremos calcular el valor absoluto de un número entero tenemos que cambiar el signo del número solo si es menor a cero.

Un enunciado if (<expresión>) { <enunciados> } es el mecanismo que C nos da para hacer esto. Lo que aparece entre <> es una plantilla, se reemplaza por alguna expresión o enunciados válidos. El fragmento de código que corresponde al ejemplo del valor absoluto quedaría como sigue.

// x es una variable de tipo int
if (x < 0) { x = -x; }
// podemos omitir las llaves si hay un solo enunciado
if (x < 0) x = -x;

La condición que sigue a if es una expresión que puede ser verdadera o falsa. En C esto se puede hacer con los operadores relacionales como <, >, ==, <=, >= y !=. También podemos usar variables de tipo bool con 0 representando falso y 1 verdadero.

Los condicionales pueden llevar también un else, de manera general:

if (<condicion>) {
  <enunciados si es verdadera>
} else {
  <enunciados si es falsa>
}

El siguiente ejemplo ilustra este uso de los condicionales en un programa que simula tirar una moneda e imprime “Cara” o “Ceca” usando la función rand() para generar un número aleatorio. Nuevamente, las llaves son necesarias cuando tenemos más de un enunciado.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
  srand(time(NULL));
  if ((rand() % 2) < 1)
    printf("Cara\n");
  else
    printf("Ceca\n");
  return 0;
}

También podemos encadenar condicionales agregando un nuevo if ... else después de un else.

if (<condicion 1>) {
  <enunciados 1>
} else if (<condicion 2>) {
  <enunciados 2>
} else if (<condicion 3>) {
  <enunciados 3>
} else {
  <enunciados 4>
}

En la estructura de arriba si la primera condición se cumple se ejecutan los primeros enunciados y solo esos. Si no se evalúa la segunda condición y si fuera verdadera se ejecuta el segundo grupo de enunciados y así. Si ninguna de las condiciones fuera verdadera se ejecuta el grupo de enunciados después del último else, en este caso <enunciados 4>.

Loops

Un ciclo o loop es una estructura de control que permite repetir uno o más enunciados si se cumple una condición. Una forma de escribir un loop es usando while en C. Un ciclo con while tiene la siguiente estructura.

while (<condicion>) {
  <enunciados>
}

El funcionamiento es como sigue. Se evalúa la condición, si es verdadera se ejecutan los enunciados dentro de las llaves. Después de ejecutar el último enunciado se vuelve a evaluar la condición, si es verdadera se repite el proceso. Si es falsa la ejecución sigue debajo de la llave de cierre del while, saliendo del loop.

Como primer ejemplo tenemos un programa que imprime un mensaje tres veces.

#include <stdio.h>

#define TIMES 3

int main(void) {
  int i = 1;
  while (i <= TIMES) {
    printf("%dº vuelta\n", i);
    i = i + 1;
  }
  return 0;
}
$ gcc loop.c
$ ./a.out
1º vuelta
2º vuelta
3º vuelta

Cambiando la definición de la constante TIMES podemos repetir el código dentro del while la cantidad de veces que querramos. La variable i funciona como contador, dentro del loop incrementamos en uno el valor de i. Tal como está arriba el ciclo se repite para los valores 1, 2 y 3 de i. Cuando i vale 4 la condición del while es falsa y el programa sigue su ejecución fuera del ciclo y termina.

Si bien podemos hacer lo mismo escribiendo tres enunciados con printf() no sería práctico si tuviéramos que repetir una operación un número de veces muy elevado. Y eso a menudo es deseable y necesario. Por ejemplo si queremos imprimir una tabla de valores.

El siguiente programa imprime una tabla con las potencias de 2, desde $2^0$ hasta $2^n$, siendo $n$ un número ingresado como argumento por el usuario.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  int n = atoi(argv[1]);
  int power = 1;
  int i = 0;
  while (i <= n) {
    printf("2^%d\t%d\n", i, power);
    power *= 2;  // equivalente a power = power * 2;
    i++;         // equivalente a i = i + 1;
  }
  return 0;
}
$ gcc powers.c
$ ./a.out 5
2^0     1
2^1     2
2^2     4
2^3     8
2^4     16
2^5     32

Ciclos anidados

Como próximo ejemplo consideremos un programa donde tenemos dos ciclos, uno dentro del otro. El siguiente programa imprime un patrón usando asteriscos. La salida de este programa se puede interpretar como una tabla de doble entrada. Cada fila $i$ y cada columna $j$ corresponde a un número natural. Si $i$ es divisible por $j$ o $j$ es divisible por $i$ entonces corresponde imprimir un asterisco, de lo contrario no imprimimos nada, o mejor dicho, un espacio.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  int n = atoi(argv[1]);
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
      if ((i % j == 0) || (j % i == 0))
        printf("* ");
      else
        printf("  ");
    }
    printf("%d\n", i);
  }
  return 0;
}

La tabla tiene $n$ filas y columnas, el valor de $n$ es provisto como argumento en la línea de comandos.

En vez de usar el ciclo while usamos el ciclo for, que es otra forma de escribir ciclos en C. Aunque la sintaxis es distinta, son prácticamente equivalentes.

// ciclo while que imprime "1 2 3 4 5"
int i = 1;            // 1) inicializar contador
while (i <= 5) {      // 2) condicion
  printf("%d ", i);
  i++;                // 3) incrementar contador
}
// lo mismo escrito con for
for (int i = 1; i <= 5; i++) {
  printf("%d ", i);
}

Los pasos 1, 2 y 3 que señalo en el while con los comentarios, en el ciclo for se escriben dentro de los paréntesis separados por punto y coma. La sintaxis del for es útil cuando sabemos de antemano cuántas veces tenemos que repetir el conjunto de enunciados dentro del loop.

Volviendo al ejemplo de los ciclos anidados. Para cada vuelta del loop exterior, el loop interior se ejecuta $n$ veces. En total el condicional dentro del loop interior se ejecuta $n \times n = n^2$ veces. Este tipo de construcción con un ciclo dentro de otro es natural cuando tenemos que repetir un proceso que a su vez debe repetirse una cantidad de veces. El ejemplo más obvio es cuando procesamos datos que están en forma de tabla. Para cada fila de la tabla debemos ejecutar algo que se repite para cada columna. Pero tenemos que tener cuidado y no abusar de este tipo de construcciones, porque a medida que aumentamos el valor de $n$, la cantidad de instrucciones crece en el orden de $n^2$, que para grandes valores de $n$ puede ser muy costoso.

Aplicaciones

Veamos algunas aplicaciones prácticas usando estas estructuras. En primer lugar consideremos una suma finita. El $n$-ésimo número armónico está definido como la suma de los primeros $n$ recíprocos de los números naturales:

\[H_n = \sum_{k=1}^n \frac{1}{k}=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}\]

El siguiente programa sirve para calcular el valor de dichos números. Usamos la variable i para controlar la cantidad de repeticiones y la variable sum para ir acumulando el resultado final. Al salir del loop mostramos en pantalla el valor final de la suma.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  int n = atoi(argv[1]);
  double sum = 0.0;
  for (int i = 1; i <= n; i++) {
    sum += 1.0 / i;       // i se convierte a double automaticamente
  }
  printf("%f\n", sum);
  return 0;
}

El ejemplo ilustra perfectamente el tipo de cálculos repetitivos que aparecen en la matemática y que las computadoras pueden resolver de manera tan eficiente.

$ gcc harmonic.c
$ ./a.out 1000
7.485471
$ ./a.out 1000000
14.392727

Consideremos ahora un programa que imprime la representación en binario de un número $n$ ingresado como argumento. Este programa toma en cuenta el hecho de que un número en binario se puede pensar como una suma de potencias de dos. Por ejemplo el número $ 10011_2 = 19_{10}$ (el subíndice indica la base), se puede representar como $19 = 16 + 2 + 1$.

El primer ciclo calcula la potencia de dos más grande que “entra” en el número $n$. Por ejemplo para el 19 es el 16. Para 255 es el 128. El segundo while tiene un if ... else dentro. Si la potencia es mayor al número entonces esa potencia no está en la representación en binario, y se imprime un 0. Caso contrario, si la potencia es menor o igual al número entonces si está y se imprime un uno. Luego le restamos al número $n$ la potencia que estabamos viendo si imprimimos un uno. Y en cualquiera de los dos casos dividimos la potencia por dos, para repetir el proceso con la próxima potencia de dos en orden decreciente. El código completo queda así:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  int n = atoi(argv[1]);
  int power = 1;
  while (power <= n / 2)
    power *= 2;
  while (power > 0) {
    if (n < power) {
      printf("0");
    } else {
      printf("1");
      n -= power;
    }
    power /= 2;
  }
  printf("\n");
  return 0;
}

Conviene examinar el funcionamiento del programa con una tabla con los valores de las variables. La tabla para el primer while, con $n=19$:

n n/2 power power <= n/2
19 9 1 true
19 9 2 true
19 9 4 true
19 9 8 true
19 9 16 false

El enunciado power *= 2 dentro del while se ejecuta 4 veces. Cuando power vale 16 ya no vuelve a entrar al loop. Con este valor de power comienza el segundo while. La tabla sería así:

power n printf() power > 0 n < power
16 19 “1” true false
8 3 “0” true true
4 3 “0” true true
2 3 “1” true false
1 1 “1” true false
0 0   false  

La última fila muestra los valores finales de power y de n. Como no entramos al loop en ese caso dejo vacías las celdas correspondientes a la condición del if ... else y del printf(). El resultado se puede leer en la columna de printf() de arriba hacia abajo. Ojo que el valor final de power es 0 y no 0.5 porque es una variable de tipo int, si no nunca podríamos salir del ciclo porque por más que sigamos dividiendo por dos nunca llegaríamos a cero.