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 las 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 sino 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(s1);
gets(s2);

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

EntradaBuffer antesInstrucciónBuffer después
20\n20\nscanf("%d", &i1);\n
30\n\n30\nscanf("%d", &i2);\n
\n\ngets(s1); 
pablo\npablo\ngets(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.

Si quieres seguir aprendiendo C, te recomiendo el MOOC Introducción a la programación en C de la Universidad Autónoma de Madrid.

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!