Ciclos en C - AlgoritmosyEstructurasDeDatos/Laboratorio_JSL GitHub Wiki

Existen muchas situaciones en las que necesitamos repetir operaciones, como por ejemplo, para realizar una sumatoria, recorrer los elementos uno a uno de un arreglo o realizar una búsqueda por los datos de una lista. Existen dos alternativas, principalmente, que son la recursión (en donde buscamos un "punto de partida" y luego construimos una función en base a mecanismos que nos permitan llegar a dicho punto de partida [próximamente habrá un artículo sobre ello]) y las iteraciones o loops. En C, las iteraciones se realizan con tres instrucciones básicas: for, while y do...while.

Para realizar un loop, necesitamos especificar, por regla general, tres elementos:

  • Inicialización: el punto de partida del loop, el estado inicial.
  • Expresión de control: la expresión que permite determinar si se reiteran las sentencias o no.
  • Ajuste: la finalización de cada iteración, que permite cambiar el estado de los elementos que deben cambiar en el loop.

No todo ciclo requiere todos estos elementos (por ejemplo, si la inicialización fuera el resultado de un proceso previo, el ciclo no tenga razón de detenerse o simplemente está esperando a que ocurra "algo", como en la programación orientada a eventos, en cuyo caso, no habría ajuste).

Ciclos for

El ciclo for es un loop de un tipo conocido como top-driven: primero evalúa, luego ejecuta, si la evaluación dice que se debe continuar. Su estructura incluye buena parte de la lógica de un loop en sí misma:

for( [expresión 1]; [expresión 2]; [expresión 3])
    declaración

(Los corchetes señalan expresiones opcionales)

La característica del ciclo for en C es que cada una de esas expresiones representa uno de los tres elementos a especificar con un ciclo:

  • expresión 1 corresponde a la inicialización, que se evalúa una única vez antes de realizar cualquier operación.
  • expresión 2 corresponde a la expresión de control y se evalúa una vez antes de cada ciclo. La repetición termina cuando esta se evalúa a falso (que, en C, por regla general, es igual a 0).
  • expresión 3 es el ajuste, que se realiza al final de cada ciclo, pero antes de la nueva evaluación de expresión 2.

Por ejemplo, para poner todos los punteros de un arreglo de punteros a NULL, podemos hacer:

// Para la definición de NULL
#include <stdlib.h>
#define LENGTH 1000

int *array[LENGTH];
for(int i = 0; i < LENGTH; i++)
    array[i] = NULL;

La sentencia a repetir puede ser simple (una sola) o compuesta (varias dispuestas secuencialmente), en cuyo caso, se agrupan en un entorno mediante llaves:

for(int i = 0; i < LENGTH; i++){
    array[i] = NULL;
    if (i%100 == 0)
        printf("%d entradas procesadas", i);
}

Las declaraciones como la de i en estos loops son posibles desde el estándar ANSI C99. Es una declaración local para el entorno del ciclo for y no existe fuera de él.

Omisión de las expresiones en for

En un ciclo for, cualquiera de las expresiones puede ser omitida. Por ejemplo, podemos hacer un loop infinito que no hace nada más que iterar y mantener ocupado al procesador con:

for(;;);

Simplemente no hay inicialización, la carencia de expresión de control se interpreta como que esta es siempre verdadera y tampoco hay ajuste. Entonces itera per sæcula sæculōrum.

Cualquier combinación de expresiones omitidas sería válida, produciendo diferentes efectos:

  • Omitir la inicialización hace que se utilicen los valores previos a la iteración.
  • Omitir la expresión de control, como se dijo, hace que el ciclo sea infinito.
  • Omitir el ajuste hace que cambie ningún estado después del ciclo y antes de la evaluación de la expresión de control.

Por ejemplo, en el siguiente fragmento:

// Obtiene i desde el usuario
int i = get_from_user();
// MAX está definida en algún sitio como constante
for(; i < MAX;){
    // Alguna operación sobre i
    operation_on_i(i);
    // Pide una nueva versión de i al usuario
    i = get_from_user();
}

Se ejecuta el ciclo para i < MAX, pero no se inicializa y su valor se actualiza dentro del ciclo, en lugar de en la expresión 3 (aunque podría hacerlo).

Operador Coma (,)

Dentro de las expresiones de inicialización (expresión 1) y ajuste (expresión 3), podemos utilizar el operador coma para asignar y ajustar varios valores a la vez. Por ejemplo, podemos invertir un string mediante su uso con un for:

// Biblioteca requerida
#include <string.h>

// Invierte el string s
// Notar que, como es un puntero, sí modifica sus 
// valores
void str_reverse(char* s){
    char ch;
    /* La inicialización define i y j, poniendo sus 
     * valores como el primer índice y el último, respectivamente.
     * 
     * La expresión de control es continuar hasta el medio del string.
     * 
     * La expresión de ajuste actualiza i aumentándolo y j, disminuyéndolo.
     */
   for(size_t i=0, j=strlen(s)-1; i < j; i++, j--){
       ch = s[i];
       s[i] = s[j];
       s[j] = ch;
    }
}

Sentencia while

La sentencia while repite las declaraciones que le siguen tantas veces como la expresión de control sea verdadera. Su estructura básica es:

while (expresión)
    sentencias

Es un tipo de loop de tipo top-driven, como for, pues parte por la evaluación de la expresión de control. Si esta es verdadera, se ejecuta el cuerpo del ciclo (representado arriba como declaraciones) y, tras ello, la expresión de control se evalúa de nuevo:

char* s = "abcdefghijkl";

// Inicialización
int i = 0;
size_t length = strlen(s);

while (i < length){
    // Sentencias a repetir
    putc(s[i]);
    putc('\n');
    // Ajuste
    i++;
}

Al igual que en el caso de for, la sentencia puede ser simple (una sola) o compuesta (un bloque de sentencias), sin embargo, su diferencia principal radica en que las expresiones de inicialización y ajuste del ciclo no son parte de la sintaxis, sino que externas a esta, resultando en que el ajuste, en particular, sea parte de las acciones a ejecutar en el ciclo.

En el caso de while, un mecanismo para generar un ciclo infinito e inútil es el siguiente:

// Macros para trabajar con booleanos
#include <stdbool.h>

// Ciclo infinito
while(true);
// Alternativa
while(1);

Una característica de C es que las sentencias for y while son totalmente intercambiables, por lo que hay más de una forma de escribir el mismo ciclo. Por ejemplo, el ciclo para mostrar los caracteres del string s de antes podemos reescribirlo:

for(int i = 0; i < length; i++){
    putc(s[i]);
    putc('\n');
}

Esta libertad se da gracias a que, a diferencia de otros lenguajes, for, en C, determina una expresión de control, en lugar de recorrer un objeto con un largo determinado. Como norma general, for es preferible a while cuando hay contadores que se inicializan para el ciclo y se actualizan con cada iteración.

Sentencia do...while

Como alternativa a while, C define la sentencia do...while como un loop de tipo botton-driven, lo que significa que "ejecuta primero y pregunta después":

do sentencia while(expresión);

Esto implica que primero se ejecuta la sentencia (simple o bloque) y después se evalúa la expresión de control, de modo que permite asegurar que una iteración, al menos, se ejecute siempre. Si la expresión de control se evalúa como verdadera, entonces se ejecuta la sentencia una vez más, terminando el ciclo en otro caso.

Edgerunner

Un ejemplo de aplicación es la copia de strings: como los strings son arreglos de caracteres terminado por el caracter nulo ('\0'), para copiar de uno a otro, hemos de copiar al menos dicho caracter:

// definidos ambos más arriba y con memoria reservada
char* s1;
char* s2;

// Resto del programa, con las definiciones y demases
...;

// La copia
// Inicialización
int i = 0;
do
    // Copia el caracter actual
    s1[i] = s2[i];
// Si solo copia el caracter nulo, termina
while (s2[i++] != '\0');

Conviene recordar que el operador unario ++ aumenta el valor de la variable que sucede después de evaluarla (en otras palabras, resulta equivalente a i = i+1;), por lo que se comprueba el carácter actual antes de pasar al siguiente.

Como dato adicional, este procedimiento es el que realiza la función strcpy del módulo string.h.

Ciclos anidados

Debido a que el cuerpo puede ser una sentencia, es posible insertar un ciclo dentro de otro, lo que se conoce como ciclos anidados. En caso de que se interrumpa el ciclo con alguna instrucción afín, el ciclo que alberga la instrucción es el que se termina, por lo que, de interrumpirse el interno, el externo sigue ejecutándose. Podemos anidar tantos ciclos (y otras sentencias, como condicionales) como haga falta, sin embargo, debemos considerar factores como la legibilidad del código o si tiene "sentido".

Un ejemplo de ciclo anidado es el algoritmo del ordenamiento burbuja:

void swap(float *a, float *b){
    float temp = *a;
    *a = *b;
    *b = temp;
}

// Necesitamos un arreglo para ordenar y su tamaño,
// que es un entero sin signo
void bubble_sort(float array[], unsigned length){
    bool is_sorted = false;
    do{
        // Suponemos que está ordenado
        is_sorted = true;
        // Intercambiamos cada posición
        for(int i = 0; i < len; i++){
            if(array[i] > array[i+1]){
                // Si es mayor el previo, los intercambiamos
                swap(array+i, array+i+1);
                is_sorted = false;
            }
        }
    // Seguimos mientras hayamos encontrado algo que mover
    } while(!is_sorted);
} // Fin de la función
⚠️ **GitHub.com Fallback** ⚠️