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.

Errores más frecuentes P3

Uno de los errores que más se ha repetido es la separación de la condición de año bisiesto en dos condiciones if distintas. De esta manera se dificultad la comprensión del código ya que se espera que cada condición compruebe una única parte de la validación de la fecha.

En relación a esta última también ha sido habitual duplicar información dentro de las condiciones. Por ejemplo ha sido habitual encontrar código organizado del siguiente modo:

if ((fecha.anyo == enero) && (fecha.dia > 31)) {
    /* código */
} else if ((fecha.anyo == marzo) && (fecha.dia > 31)) {
    /* código */
}

cuando la condición que comprueba los meses de 31 días tendría que estar agrupada.

En algunos casos, no se ha incluido el else final en el ejercicio de comparación de fechas quedándose alguna de las posibilidades sin responder correctamente. Tal como os comenté en clase es importante incluir este else incluso cuando no sea necesario, ya que permite descubrir errores en el código que de otra manera pueden ser difíciles de detectar. Lo mismo se aplicara a la opción default del switch.

De nuevo han seguido apareciendo “números mágicos” en vez de constantes, sobre todo en los switch-case que en vez de utilizarse las constantes que representan a las opciones en los enumerados, se han empleado directamente los números. Las constantes permiten abstraer el código aportando significado a los números y facilitando la lectura. Así,

switch(opcion) {
...
    case nuevaSesion:
}

queda mas reconocible que hace esa parte del código que:

switch(d) {
...
    case 2:
}

(la d la he puesto a propósito ya que me he encontrado alguna combinación de este estilo)

Un error complicado de detectar es cuando se emplea el operador asignación en vez de la comparación dentro de un if

if (i = 5)

Lo tenéis explicado en un bocado de C.

Finalmente, recordar que a medida que el código se está haciendo cada vez más largo es fundamental que lo comentéis correctamente. Por un lado, aquellos símbolos que añadís de cosecha propia (constantes, variables, enumerados, estructuras…) son susceptibles de acompañarlos con un comentario, dado que son ajenos al que lee el código. Desde luego siempre es mejor un buen nombre que un buen comentario, pero no hay que desaprovechar la oportunidad de incluir información adicional cuando sea necesario. En especial en aquellas variables/constantes que sean claves para la comprensión del código. Por otro lado, los comentarios ayudan a seguir y comprender el flujo del programa, y a matizar porque se han tomado ciertas decisiones de programación. Ahora bien, tan importante es comentar el código como no pasarse comentando. Cada vez que se incluye un comentario de alguna manera se está introduciendo una duplicidad de información. Esto implica que a la hora de realizar cambios en el código puede que haya que tocar en dos sitios. Es fundamental que el comentario esté actualizado, sino confunde más que ayuda. En este sentido, hay que tener en consideración que el que lee vuestro código se supone que sabe C. Así que comentarios como los siguientes no tienen sentido a menos que estéis haciendo material docente:

printf("%s\n", name); /* imprimimos el nombre en una línea separada */
fclose(fd);  /* cerramos el fichero */ 

Diferencia entre scanf, gets y fgets

La biblioteca estándar de C provee de varias funciones para introducir datos a nuestros programas. Estas son parte del módulo stdio.h. Vamos a ver la diferencia que existe entre tres de ellas (scanf, gets y fgets). Las tres nos permiten leer cadenas de caracteres introducidas por el usuario pero con importantes diferencias.

scanf es la más versátil de la tres dado que puede leer distintos tipos de datos (cadenas, enteros, reales…) según el formato especificado. En cambio, gets y fgets sólo leen cadenas de caracteres (nótese la ‘s’ final del nombre que hace referencia en este caso a string).

Veamos como leen cada una de ellas la entrada del usuario. Cuando se utiliza la consola, el programa no lee directamente el texto tal cual lo introduce el usuario si no que éste se almacena en una tabla intermedia que llamaremos buffer. Cada vez que el usuario pulsa el retorno de carro, se llena el buffer con la línea introducida (incluyendo el carácter '\n'). Las diferencias radican en cómo leen las tres funciones del buffer. Vamos a verlo con el siguiente ejemplo:

    int i1, i2;
    char s1[30], s2[30];

    scanf("%d", &i1);
    scanf("%d", &i2);

    gets("%s", s1);
    gets("%s", s2);

Supongamos que el usuario introduce “20\n30\npablo\n”, la secuencia de lectura sería la siguiente:

Entrada Buffer antes Instrucción Buffer después
20\n 20\n scanf("%d", &i1); \n
30\n \n30\n scanf("%d", &i2); \n
\n \n gets("%s", &s1);
pablo\n pablo\n gets("%s", &s2);

El primer scanf tiene que leer del buffer hasta que encuentra un número (%d). En cuanto encuentra el 20 termina de leer, y deja en el buffer un '\n'. El segundo scanf, tras la entrada del usuario, se encuentra con \n30\n, y tiene que realizar la misma tarea que el anterior, leer un número. Se salta el primer '\n', y lee 30, dejando de nuevo un '\n' en el buffer. La función gets es más simple, y lo único que hace es leer todo lo que haya en el buffer hasta que encuentre un '\n', y lo copia en la variable correspondiente. Así, el primer gets se encuentra un \n, lo consume pero no copia nada en s1. El segundo gets se encuentra pablo\n, así que lee todo lo que hay en el buffer y lo guarda en s2. Para permitir que pablo se copie en s1, una posibilidad es incluir una llamada a getchar() antes de emplear gets para leer s1. Esta función lee un carácter del buffer, consumiendo así el '\n' que impedía rellenar s1 con pablo.

Ahora bien, gets es una función insegura tal como lo indica el compilador (warning: this program uses gets(), which is unsafe.). El problema es que no puedes controlar el número de caracteres que introduce el usuario pudiendo ocurrir que se copien en la cadena más caracteres que los permitidos por su tamaño máximo. Por ejemplo, en el código anterior que el usuario introdujera más de 29 caracteres. Este comportamiento puede producir que el programa termine abruptamente. Su uso también puede producir fallos graves de seguridad al habilitar la posibilidad de ejecutar código malicioso.

La alternativa segura de gets es fgets que si permite establecer el máximo de caracteres que pueden leerse. Un ejemplo de uso sería:

    char s[30];

    fgets(s, 30, stdin);

Primero se establece donde se quiere copiar la línea leída. A continuación el máximo número de caracteres incluyendo el '' que se pueden leer. En el ejemplo, fgets lee del buffer hasta que encuentra un '\n' o hasta que haya copiado en s un máximo de 29 caracteres. La propia fgets se encarga de incluir el '' para finalizar la cadena. El última parámetro hace referencia de donde se obtiene los datos. Al igual que otras funciones como fprintf, fgets se puede emplear para leer de la consola, indicándolo con stdin (standard input), o de un fichero. Otra diferencia importante con gets es que el retorno de carro se copia también en la cadena.

Errores más frecuentes P2.2

En esta segunda parte de la práctica 2 los errores relacionados con la legibilidad del código que he encontrado son:

Comentarios

Es importante que el código esté correctamente comentado de cara al que vaya a leerlo. Cuando definimos una “palabra” nueva en el programa suele ser necesario que también incluyamos un comentarios con su significado. Esto ocurre cuando definimos una constante, por ejemplo. Las constantes tienen nombres asignados por nosotros que no tienen que coincidir con el nombre que definiría otro programador. Así, es básico ponerle un nombre que refleje claramente que representa la constante, y recomendable, también, añadir un comentario que ayude a explicar el significado. Un ejemplo lo podemos encontrar en la biblioteca math.h

#define MATH_ERRNO        1    /* errno set by math functions.  */

Dar formato a las estructuras

Cuando se define una estructura se lee más claro si los campos se encuentran sangrados a la derecha:

Incorrecto:

typedef struct {
int dia;
int mes;
int anyo;
}Fecha;

Correcto:

typedef struct {
    int dia;
    int mes;
    int anyo;
} Fecha;

Por otro lado, vamos a seguir el convenio de nombrar los nuevos tipos de datos que definamos con la primera en mayúscula. Así definimos Fecha en vez de fecha.

Castellano o inglés

A la hora de decidir las nuevas “palabras” de vuestro programa (nombres de constantes y de variables, por el momento) es preciso mantener coherencia en el idioma que se utiliza. No mezclarlos.

Inicializar las variables

En el último ejercicio el campo numSesion contabilizaba el número de usuarios válidos que se había introducido. Este campo es preciso inicializarlo a cero para que al incrementarlo funcione correctamente en todas las plataformas. Algunos veces puede que las variables no inicializadas se le asigne el valor 0, pero no podemos confiar en que esa así.

Errores más frecuentes P2.1

Uso de constantes en la definición del tamaño de las tablas

En esta práctica he encontrado un número elevado de soluciones que no usaban constantes para  los tamaños de las tablas. Por ejemplo, la definición del tamaño de las matrices del ejercicio 2.3 se parecía más a esto:

int m1[3][3];
int m2[3][3];

que a esto:

#define DIM 3

int m1[DIM][DIM];
int m2[DIM][DIM];

Hay que evitar utilizar número mágicos en el código, y sustituirlos por constante con nombres significativos. Cuando empecéis a trabajar con bucles veréis más claro su utilidad.

Variables innecesarias

Otro error, aunque menos común, ha sido la definición de variables innecesarias. Por ejemplo, incluir una variable para guardar el resultado de la resta de uno al mes introducido por el usuario:

int mes, mes2;

/* más código*/

mes2 = mes - 1;

Cada variable adicional que se defina y que no sea realmente útil, por un lado, entorpece la comprensión del código a otro programador, por otro, ocupa memoria de manera innecesaria. Durante el curso iremos dando consejos para elegir convenientemente las variables auxiliares de tu programa. Un consejo muy importante para grabarse a fuego, nunca dejes declaradas variables que no se empleen.

Definición de variables en cualquier parte del código

Aunque el compilador que emplea Netbeans está configurado para permitir declarar las variables en cualquier parte del código, tenéis que declararlas en los puntos permitidos por el estándar ANSI C (aunque existe dos estándares C89, y C99, cuando se hace referencia a ANSI C es la primera versión). Es habitual que los compiladores acepten código que realmente no está permitido dentro de ese estándar. El problema es que no todos los compiladores aceptan las mismas extensiones, de manera que un código fuente que compila en uno, puede dejar de compilar en otro. Para evitar ese problema se recomienda altamente que el código se ajuste al estándar ANSI C.

En ANSI C, las variables se pueden declarar al comienzo de un bloque, esto es, después de un “{“.

Correcto:

int main {

    int edad;

Incorrecto:

int main {

    printf("Hola\n");

    int edad;

Hay que tener en mente que esta restricción es particular de ANSI C. otros lenguajes permiten declarar variables en medio del código. Incluso, el estándar C99 también lo permite, pero no se encuentra tan extendido como ANSI C.

Errores más frecuentes y mejoras en la práctica 1

Legibilidad

Es importante que el código fuente sea claro de entender para otras personas que tenga que revisarlo o modificarlo, o por ti mismo cuando tengas que retomar el programa. Cuando más fácil sea leer y entender menos costoso será mejorarlo en un futuro. Durante el curso iremos trabajando el estilo de programación. Aunque cada uno termina desarrollando un estilo propio, existen recomendaciones básicas que hay que seguir para que cualquier pueda entender rápidamente tu código.

En este sentido, la primera es elegir nombres de variables que reflejen el contenido que almacenan. En un número no despreciable de soluciones el nombre de las variables se elegía empleando el abecedario: a, b, c…

int a;
int b, c, d;

no dice nada sobre el significado de la variable que se almacena, mientras

int opcion;
int dia, mes, anyo;

son nombres mucho más ilustrativos.

Otra recomendación básica es el tamaño de las líneas de código. Algunos habéis implementado los primeros ejercicios en una sola línea. Algo así:

printf("********************\n**                **\n**     TUENTI     **\n**
**\n********************\n");

Aunque el programa funciona perfectamente, es mucho más claro de entender si se incluye un printf por cada línea. En general, se recomienda que el ancho de las líneas no superen los 80 caracteres (si os fijáis Netbeans os marca la frontera con una línea vertical roja que sirve de guía). El motivo de elegir 80 es un tema que viene de lejos.

Bocados de C

Bocados de C son pequeños trocitos  para alimentar mentes curiosas que quieran conocer mejor el lenguaje C. Iré dejando preguntas que espero susciten debate, y generan nuevas preguntas entre aquellos que participéis, lo cual es totalmente voluntario.

Comenzado las prácticas de PROG1 2013-14

Este primer post podéis encontrar información básica sobre la organización de los grupos 1161, 1162, y 1101

Contactar conmigo:

Si queréis hablar conmigo personalmente lo más sencillo es enviarme un correo (Pablo.Haya@uam.es) para que acordemos día y hora. Trabajo en el IIC, que se encuentra en la quita planta del edificio B, pero las tutorías las realizo en mi despacho de la tercera planta (B-342) bajo cita previa.

Para dudas o consultas rápidas los canales más rápidos son:

Foro de la asignatura:  el primer sitio donde hay que plantearse responder una duda. Al ser un espacio público todo el mundo puede ver la respuesta, todo el mundo aprende, y no hay que repetirse constantemente.

Twitter: @phayauam Consultas cortas que se puedan responder en pocas palabras. Mismas ventajas que los foros pero más ágil.

Correo-e: Pablo.Haya@uam.es La última opción. En este caso, por favor, incluir en el asunto del mensaje [PROG1] y el motivo del mismo para que se puedan filtrar y reconocer rápidamente . El tono del mensaje, algo intermedio entre la retórica del siglo XVIII (Ilustrísimo Sr. Profesor) y el Whatsapp (ola cndo se nos da la notas!???).

En cualquier caso, siempre hay que ser precisos, claros y educados.

Material y actividades adicionales:

El lenguaje de programación que vamos a utilizar durante el curso es C. Aún habiendo pasado más de cuarenta año desde su invención, sigue siendo uno de los lenguajes de programación más usados en la industria, y uno de los que más ha influenciado en el diseño de lenguajes de programación modernos. Es un lenguaje muy potente pero también con muchas aristas y detalles que pueden pasar muy fácilmente desapercibidos.

Para aquellas mentes curiosas e interesadas durante este curso voy a llevar a cabo una actividad dirigida a conocer mejor el lenguaje C. Iré dejando preguntas que espero susciten debate, y generan nuevas preguntas entre aquellos que participéis, lo cual es totalmente voluntario. Aquí os dejo la primera:  ¡Hola Mundo!

Para poder participar hay que registrarse en github, que es un popular gestor de versiones que más tarde o más temprano terminareis utilizando en vuestra vida profesional.

¡Bienvenido!