Errores más frecuentes P4

Ha habido tres problemas importantes durante la realización de la práctica 4. El primero ha sido decidir cuando se utiliza un bucle for y cuando un while (o en su versión do-while). Aunque cualquier bucle se puede implementar de las tres formas, ya hemos motivado múltiples veces la necesidad de seguir un estilo correcto.

Los primeros se denominan bucles definidos ya que a priori se puede saber el número de iteraciones que va a realizar el bucle, mientras que los segundos son indefinidos ya que en cada ejecución pueden tener un número de iteraciones distintas. Manuel García-Herranz me comentó una metáfora que seguro te va a ayudar a recordarlo: si tienes que pintar todas las habitaciones de tu casa ¿cuantas habitaciones tienes que recorrer? Todas, es decir, un número definido de habitaciones y siempre el mismo. En cambio, si tienes que buscar tus zapatillas en casa ¿cuantas habitaciones tienes que recorrer? Depende de dónde las hayas dejado, es decir, un número indefinido ya que cada día puedas haberlas dejado en un sitio distinto. Si tuviéramos que programar nuestro recorrido de la casa, el primero lo haríamos con un bucle for, y el segundo con un bucle while. Nos queda una duda adicional y es cuando escoger while o do-while. El primero se ejecuta cero o más veces, mientras que el segundo se ejecuta al menos una vez. Así, por ejemplo, los bucles para mostrar los menús se han implementado con do-while, ya que al menos se tenía que presentar una vez el menú al usuario.

El segundo problema que me he encontrado es la definición del bucle. De nuevo es una cuestión de estilo, pero importante para que nuestro código sea fácil y rápidamente comprensible.

En un bucle for toda la información relativa a la definición del mismo debería encontrarse dentro de los (). Por ejemplo,

for (i=0; i < sesion.maxUsuarios; i++) {
    /* Recorrer la tabla sesion.usuarios y
       realizar operación correspondiente para cada usuario de la tabla */
}

Detalles importantes de la definición anterior:

  • Si la variable contador no tiene un significado especial se elige i,j,k…
  • La variable i se inicializa dentro de los ().
  • El bucle comienza en 0 y se ejecuta mientras que sea estrictamente menor que el número de elementos. En el caso de la práctica, el máximo número de elementos viene marcado por sesion.maxUsuarios, no por la constante U. Este último valor marca el máximo número de usuarios que puede haber en la tabla, que, en general, no coincidirá con el número usuarios dados de alta. Cualquier operación sobre los usuarios tiene que ser sobre los que realmente tenemos datos válidos.

En un bucle while, se recomienda que toda la información sobre las condiciones de mantenimiento se encuentre entre los (). Así, para buscar si un nombre está repetido dentro de la tabla usuario lo expresaríamos tal que:

i = 0;
while (i < sesion.maxUsuarios &&
       strcmp(sesion.usuarios[i].nombre, usuario.nombre) != 0) {
    i++;
}

Nótese que la condición i < sesion.maxUsuarios tiene que ir antes que la comparación con strcmp. En caso contrario, i podría valer sesion.maxUsuarios que no es una posición válida de la tabla, y la comparación podría resultar errónea.

Ahora bien, algunas veces os podéis encontrar con estructuras de control que no siguen las reglas anteriores pero que terminan siendo también muy utilizadas. Por ejemplo, cuando se busca un elemento en una tabla es habitual encontrarlo también expresado empleando un bucle for:

for (i=0; i < sesion.maxUsuarios; i++) {
    if (strcmp(sesion.usuarios[i].nombre, usuario.nombre) == 0) {
        break;
    }
}

Estas formas alternativas pueden funcionar siempre que el bucle no sea muy largo y que tenga un único punto de ruptura. En caso contrario, es más fácil de entender el funcionamiento del mismo si toda las condiciones que afectan al bucle se encuentran en un único sitio.

Otro punto importante es inicializar en la definición del bucle todas las variables que afectan al mismo:

for (i=0, numUsuarios=0; i < sesion.maxUsuarios; i++) {
    if (strcmp(sesion.usuarios[i].nombre, usuario.nombre) == 0) {
        numUsuarios ++;
    break;
    }
}

En la primera parte de la definición, se pueden incluir tantas inicializaciones como queramos separándolas por comas. Como regla general recomendaría inicializar siempre las variables el sitio más cercano a su primer uso, dado que evita tener que volver hacia atrás para comprobar si han sido correctamente inicializadas.

El tercer problema es más sutil de entender. Cuando es empieza a programar es normal terminar haciendo bloques muy largos de código donde se mezclan partes de código que realmente son candidatas a separarse. Esto se ve más claro una vez que se domina como se usan las funciones. En los casos concretos que me he encontrado, el error ha sido mezclar dentro del bucle tanto la búsqueda del elemento como la decisión a tomar en caso de encontrarlo o no.

El siguiente ejemplo

i = 0;
while (i < sesion.maxUsuarios) {
    if (strcmp(sesion.usuarios[i].nombre, usuario.nombre) != 0) {
        printf(“Usuario encontrado”);
    }
    else if (i+1 == sesion.maxUsuarios) {
        printf(“Usuario NO encontrado”);
    }
    i++;
}

incorpora dentro del bucle ambos procesos (buscar y decidir). Una solución más clara sería:

i = 0;
while (i < sesion.maxUsuarios &&
       strcmp(sesion.usuarios[i].nombre, usuario.nombre) != 0) {
    i++;
}

/* si el indice es menor que el máximo de usuario de la tabla es que está */
if (i < sesion.maxUsuarios) {
    printf(“Usuario encontrado”);
}
else {
    printf(“Usuario NO encontrado”);
}

El problema de mezclar ambos procesos es que complica el código. En ejemplos tan sencillos como estos la diferencia es sutil. Ahora bien, supongamos que la parte del código que toma decisiones tiene que hacerse más compleja, si la hemos incluido dentro del bucle afectaría a la comprensión del mismo, cuando la complejidad de buscar un elemento en un tabla tendría que ser independiente de lo que se quiera hacer con él una vez encontrado.