FAQ de C - Errores comunes de programación

 

  1. Una condición de un if, o de un while, tiene que ir siempre entre paréntesis. Nada puede quedar fuera de los paréntesis. En general, es mejor poner siempre más paréntesis de la cuenta: mejor pasarse que quedarse corto, aunque seamos redundantes.

    Ejemplo:

    if !condición => incorrecto
    if (!condición) => correcto

  2. En C los strings se escriben entre comillas dobles. En cambio, los caracteres van entre comillas simples. Las comillas dobles son las que hay en la tecla del 2, mientras que las simples son las de la tecla del lado del 0.

    Ejemplo:

    printf ("Cadena de texto\n");
    printf ("%c\n", 'c');

  3. El signo de comparación booleana en C es el doble igual, es decir "==". Un solo signo igual es una asignación. Debemos tener mucho cuidado porque aunque confundamos los signos, es posible que el compilador no permita hacerlo sin mostrar errores ni avisos (warning's).

    Ejemplo:

    if (a==b) => correcto
    if (a=b) => incorrecto

    En este último caso estaríamos realizando una asignación. Si el valor asignado es cero, la condición será falsa; si es diferente de cero, será cierta.

  4. A veces, al ejecutar un programa no aparece ninguna salida por pantalla (o sale incompleta), y se tiene que acabar la aplicación cerrando la ventana MS-DOS. Esto suele ser indicio de que existe un bucle infinito, es decir, un bucle donde nunca se cumple la condición de salida. Hay que revisar la condición del bucle, la correcta inicialización de la variable de control del bucle, comprobar que dicha variable se incrementa o decrementa convenientemente a cada iteración, etc...

  5. Los grupos de instrucciones en C que forman parte de un mismo bloque (una rama de un if, el cuerpo de un bucle o similares) se deben agrupar entre llaves {}. Cuando el "bloque" de instrucciones está formado por una sola instrucción no es necesario poner las llaves, y simplemente se pone un signo ";" al final de esta:

    Ejemplo:

    while (condición)
    {
    instrucción1; } instrucción2;

    Se puede escribir también así:

    while (condición)
    instrucción1;
    instrucción2;

    En los dos casos, la instrucción2 está fuera del bucle. Acostumbra a ser motivo de error mezclar las dos opciones, y poner el signo ";" seguido del bloque entre llaves {}:

    Ejemplo:

    while (condición);
    {
    instrucción1;
    }

    Este error tiene consecuencias nefastas, ya que el signo ";" cierra el bucle, al ser interpretado como una instrucción vacía. El bloque de instrucciones entre llaves se ejecutaría después del bucle (si es que no se queda iterando indefinidamente) y lo haría una sola vez.

  6. El compilador de C (y, en general, todos los compiladores) utilizan un conjunto de símbolos que les permiten no "perderse" en el proceso de interpretación (llamado "parsing") de las instrucciones del código fuente. Estos símbolos son, básicamente, los punto-y-coma (;), las llaves de los bucles y los if, los paréntesis, etc... Un error muy típico es el de "olvidarse" de poner uno de estos símbolos (el clásico punto-y-coma, por ejemplo). Entonces, el compilador se pierde y no puede precisar mucho el error. Acostumbra a mostrar un mensaje del tipo: "parse error before 'variable'", "parse error at end of input" o similar. La solución pasa, pues, por buscar el símbolo perdido en la línea que nos indica (o bien una o dos líneas antes).

  7. El lenguaje C es un lenguaje case sensitive (distingue mayúsculas y minúsculas). Un error muy clásico es declarar una variable o función de una manera y después utilizarla con un pequeño cambio de mayúscula/minúscula. Eso provoca mensajes del compilador del tipo undeclared symbol 'variable' o similares.

  8. En lenguaje C los parámetros de entrada/salida son, en realidad, referencias a variables; es decir, direcciones de memoria donde se encuentra el valor real de la variable. En una cabecera de una función, un parámetro de e/s se indica con el símbolo *. Desde dentro del cuerpo de esta función, se accede al valor precediendo el símbolo * al nombre de la variable. En cambio, en el llamamiento, hay que pasar la dirección de la variable, por lo cual se utiliza el símbolo &.

    Ejemplo:

    void incrementar(int *a) {
    *a = *a + 1; }

    Desde el programa principal o función que invoca, para llamar a la acción pasándole una variable b declarada como int, hace falta hacer:

    incrementar (&b);

    Y si tuviésemos una tupla llamada t con un campo llamado c, para acceder al campo c de t en una función donde t está pasada por referencia, haríamos:

    (*t).c, o bien, t->c

    Si t fuera un parámetro de entrada, en cambio, sería:

    t.c

    A continuación se muestra el uso de los parámetros de entrada/salida en comparación con los de sólo entrada y sólo salida:

    void f1(int *par_sal) {
    	/* se ha aplicado directamente la tabla de conversión.                  */
    	/* Dentro de una acción los parámetros de salida se deben utilizar      */
    	/* con el * delante.                                                    */ 
    
    	*par_sal = 1;

    	/* -- ¡¡¡entender esto es importante!!! --                              */
    	/* También se puede aplicar directamente la tabla de conversión.        */ 
    	/* Dentro de una acción los parámetros de salida se deben utilizar      */ 
    	/* con el * delante. Pero además, como es un parámetro                  */ 
    	/* de entrada/salida hay que poner & delante de la llamada, es decir,   */ 
    	/* f2(&(*par_sal));                                                     */ 
    	/* Ahora Bien, &(*(p)) == p, por lo tanto                               */ 
    
    	f2(par_sal);
    }
    void f2(int *par_ent_sal) {
    	/* se ha aplicado directamente la tabla de conversión.                  */
    	/* Dentro de una acción los parámetros de entrada/salida se han         */ 
    	/* de utilizar con el * delante.                                        */ 
    
    	*par_ent_sal = *par_ent_sal + 1;
    	f3(*par_ent_sal);
    }
    void f3(int par_ent) {
    	printf("Número: %d\n", par_ent);
    	par_ent = 3; /* Esta línea no sirve de nada: el valor del parámetro no se modifica */
    }
    main() {
    	int n;

    /* Se ha aplicado directamente la tabla de conversión. */ /* Parámetro de salida */ f1(&n);

    /* Se ha aplicado directamente la tabla de conversión. */ /* Parámetro de entrada/salida */ f2(&n);

    /* Se ha aplicado directamente la tabla de conversión. */ /* Parámetro de entrada */ f3(n);
    }

    Los errores de compilación más comunes referentes a los parámetros de e/s son:

    warning passing arg N of 'función' incompatible pointer type o bien incompatible type for argumentN of 'función': Se está intentando pasar algo que no es una dirección como parámetro de entrada/salida. Es posible que falte el símbolo & en la llamada.

    subscripted value is neither array nor pointer: Se está haciendo uso de los corchetes [] sobre una variable que no es un vector o un puntero (dirección de memoria o parámetro de e/s).

    assignment makes pointer from integer without a cast: Se intenta asignar un entero a una referencia a entero. Es un error típico de llamadas que necesitan un parámetro por referencia y no hemos puesto el símbolo &.

    Los parámetros de entrada/salida acostumbran a ser fuente de los errores más graves, ya que internamente juegan con direcciones de memoria y un acceso a una dirección equivocada o no definida puede hacer detener de forma abrupta el programa. Errores típicos de ejecución son una ventana de Windows con el mensaje

    "Practica.exe provocó un error en MSVCRT.DLL. Practica.exe se cerrará. Si continúan los problemas, pruebe de nuevo despuéss de reiniciar el equipo", o bien, "Este programa ha efectuado una operación no válida y se apagará".

    La solución pasa por repasar todos los casos de paso de parámetros de e/s, porque muy posiblemente faltará/sobrará algún * o algún &.

  9. Se debe declarar toda acción o función que queramos utilizar antes de su codificación. A esto se le llama pre-declaración de funciones, y consiste en poner la cabecera de la función con el nombre, parámetros que necesita y tipo de retorno. Hay dos maneras de declarar funciones:

    Se recomienda utilizar la primera opción. De hecho, cuando hacemos un include en realidad estamos añadiendo cabeceras de funciones que están incluidas en librerías que se enlazarán finalmente (en el proceso de enlazado o link) a nuestro código para crear el ejecutable final. Avisos y errores relaciones con la pre-declaración de funciones pueden ser:

    warning: previus implicit declaration of 'función': Intentamos utilizar una función que no hemos declarado. En este caso, el compilador muestra un aviso y considera que la función devuelve un entero (tipo por defecto).

    warning: 'función' was previously implicitly declared to return 'int': Se intenta recoger el valor de retorno de una función no declarada en una variable que no es de tipo entero.

    warning: type mismatch with previous implicit declaration: Informa de un conflicto de tipo.

    C:\WINDOWS\TEMP\cclua9fb.o(.text+0x6d2):pract.c: undefined reference to 'función': Es un error de enlazado o link. No se ha encontrado una función que utilizamos y de la cual no se ha encontrado la implementación. Podría ser un error de mayúsculas/minúsculas.

  10. A menudo en situaciones de mal funcionamiento del programa donde se produce un acceso ilegal a memoria o, simplemente, se entra en un bucle infinito, interesa saber qué ha fallado. Para eso, están las técnicas de debug. Aunque Dev-C++ incorpora un debugger, este es demasiado complejo y poco intuitivo, y se recomienda el uso de printf para consultar el valor de las variables en puntos del código estratégicamente escogidos. Por ejemplo, en un caso en que el programa no acaba nunca, es interesante hacer printf antes y después de cada bucle, para saber en cuál de ellos se queda iterando indefinidamente. Si un cálculo se realiza mal, estaría bien poner printf's para mostrar los cálculos parciales, para saber cuál de ellos es el causante del problema.

    No obstante, hay ocasiones en que encontrar el error puede ser más costoso. Por ejemplo, el problema del mensaje "operación no válida" es más difícil saber de dónde proviene. Normalmente se debe a que el código está mal y accede a una posición incorrecta de memoria. Este problema está relacionado con los punteros. Hay que mirar que se hayan puesto todos los símbolos & y * necesarios y sólo los necesarios. Dado que es difícil mirarse todo el código es más fácil utilizar "chivatos" mediante printf's para saber por dónde pasa la ejecución del programa y ver en qué punto (más o menos) se produce el error.

    Por ejemplo:

    printf("Chivato 1\n");
    scanf("%c", c);
    printf("Chivato 2\n");

    Durante la ejecución del programa el error se producirá después del "Chivato 1" y antes del "Chivato 2", eso quiere decir que el error está allí. Una vez localizado es cuestión de encontrar el error, en este caso es la falta de un símbolo & ante la c. Es decir, debería ser:

    scanf("%c", &c);
  11. En C una acción y una función sólo se diferencian en el tipo de retorno. Una función tiene un tipo diferente de void como retorno, y necesita una instrucción return al final del cuerpo de la función. Una acción, en cambio, tiene void como tipo de retorno, y no debe incluir return.

    Un error común es olvidarse de poner el return en una función, lo cual genera un mensaje como warning: control reaches end of non-void function. Acostumbra a ser un aviso, no un error, aunque si no se corrige puede provocar un funcionamiento totalmente impredecible del programa.

    De igual manera, otro error es poner return en una acción. Entonces, el aviso tiene el aspecto de

    warning: `return' with a value, in function returning void

    Además, si desde el programa principal se intenta llamar a una acción y "recoger" su "valor de retorno" en una variable, obtendremos un mensaje de error como:

    void value not ignored as it ought to be

  12. El compilador de C distingue dos tipos de anomalías en la interpretación del código fuente: errores y warnings. Los errores son equivocaciones en la codificación que no pueden ser pasadas por alto. Se tienen que corregir porque, si no, es imposible construir un ejecutable. En cambio, un warning es un aviso. Se trata, posiblemente, de un error por nuestra parte, pero que no impide poder construir un ejecutable.

    Ejemplo:

    Utilizar una variable no declarada => error
    Utilizar una variable no inicializada => warning

  13. Los ficheros de include son ficheros que contienen, generalmente, pre-declaraciones de funciones y acciones. De aquí que se llamen ficheros de cabeceras o headers, y por este motivo tengan extensión ".h".

    Una pregunta que a menudo se hace un programador es dónde se encuentra una función, qué include hay que poner para poder utilizarla, etc. Normalmente, para aplicaciones de tipo consuela (con ventana MS-DOS, por decirlo de alguna forma), necesitan únicamente stdio.h. No obstante, si tenemos que hacer llamadas como system("pause"), también hará falta stdlib.h, y si necesitamos alguna funcionalidad de cadenas de caracteres, también string.h.

    Un problema añadido es la coexistencia de ficheros include para C y para C++. Incluir un fichero de cabecera de C++ en un programa de C puede provocar una cantidad de errores enorme, que sólo se resuelven eliminando dicho include. Se debe vigilar especialmente de no incluir el fichero iostream.h (propio de C++) en un programa C.

  14. Propagación de errores. Cuando se produce un error durante una compilación, a menudo aparecen más errores derivados del primero. Por ejemplo, un error en la definición de un tipo de datos (un typedef) puede provocar que el tipo no quede definido y que todas las variables que intentamos declarar después de este tipo fallen. Entonces, el mensaje del compilador sería

    warning: data definition has no type or storage class

    Solucionando el error en la definición del tipo desaparecerán los que se derivan de este.

  15. En C las tablas siempre empiezan en el elemento que ocupa la posición 0, por lo tanto el rango por una tabla de N elementos será 0..N-1. Por ejemplo, el programa siguiente intenta calcular el cuadrado de los 10 primeros números naturales y almacenarlos en una tabla:

    ...
    int i;
    int t[10];
    ...

    precio (i=1; i<=10; i++) { t[i]=i*i;
    }

    ...

    La manera correcta de hacerlo sería:

    ...

    int i;
    int t[10];

    precio (i=1; i<=10; i++) { t[i-1]=i*i; /* el primer elemento (i=1) ocupa la posición 0 */ } ...
  16. El orden en el cual se ponen los elementos en los tipos enumerados es importante.

    Ejemplo:

    typedef enum {FALSE,TRUE} bool; => correcto
    typedef enum {TRUE,FALSE} bool; => incorrecto

    Eso es debido a la forma interna en que C trata los tipos enumerados (los elementos son enteros). Además las relaciones de orden se establecen de forma que el primer elemento es más pequeño que el segundo, etc. Y cómo habéis visto en lenguaje algorítmico, por convenio, en el tipo booleano FALSO < CIERTO.

  17. Cuidado con la traducción del "para" en for. El siguiente código:

    para indice:=1 hasta 10 paso 1 hacer 
    	escribirEntero(indice);
    fpara

    Se traduce por:

    for (indice=1; indice<=10; indice++)
    {
    printf("%d", indice);
    }
  18. El scanf no devuelve el valor leído del teclado como si fuera una función. A diferencia del lenguaje algorítmico, el valor se guarda en una variable de entrada/salida.

    Ejemplo:

    ...
    e: entero;

    e=leerEntero();
    ...

    Se traduce en:

    ...
    int e;

    scanf("%d",&e);
    ...

    Y no así:

    ...
    int e;
    
    e=scanf("%d",&e); incorrecto.
    ...
    Atención porque en C eso último no genera ningún error, pero tampoco hace lo que nosotros esperamos, que es asignar a la variable e el valor leído del teclado.