Buscar..


Introducción

Esta sección analiza algunos de los errores comunes que un programador de C debería conocer y debería evitar cometer. Para más información sobre algunos problemas inesperados y sus causas, consulte Comportamiento indefinido

Mezclar enteros con signo y sin signo en operaciones aritméticas

Por lo general no es una buena idea mezclar signed y unsigned enteros en las operaciones aritméticas. Por ejemplo, ¿cuál será la salida del siguiente ejemplo?

#include <stdio.h>

int main(void)
{ 
    unsigned int a = 1000;
    signed int b = -1;

    if (a > b) puts("a is more than b");
    else puts("a is less or equal than b"); 

    return 0;
}  

Dado que 1000 es más de -1, se esperaría que la salida fuera a is more than b , pero ese no será el caso.

Las operaciones aritméticas entre diferentes tipos integrales se realizan dentro de un tipo común definido por las llamadas conversiones aritméticas habituales (consulte la especificación del lenguaje, 6.3.1.8).

En este caso, el "tipo común" es unsigned int , porque, como se indica en las conversiones aritméticas habituales ,

714 De lo contrario, si el operando que tiene un tipo entero sin signo tiene un rango mayor o igual al rango del tipo del otro operando, entonces el operando con tipo entero con signo se convierte al tipo del operando con tipo entero sin signo.

Esto significa que int operando b se convertirá a unsigned int antes de la comparación.

Cuando -1 se convierte en un unsigned int el resultado es el máximo valor de unsigned int posible, que es mayor que 1000, lo que significa que a > b es falso.

Escribir erróneamente = en lugar de == al comparar

El operador = se usa para la asignación.

El operador == se utiliza para la comparación.

Uno debe tener cuidado de no mezclar los dos. A veces uno escribe erróneamente

/* assign y to x */
if (x = y) {
     /* logic */
}

cuando lo que realmente se quería es:

/* compare if x is equal to y */
if (x == y) {
    /* logic */
}

El primero asigna un valor de y a x y verifica si ese valor no es cero, en lugar de hacer una comparación, que es equivalente a:

if ((x = y) != 0) {
    /* logic */
}

Hay ocasiones en las que se pretende probar el resultado de una asignación y se usa comúnmente, porque evita tener que duplicar el código y tener que tratar la primera vez especialmente. Comparar

while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
        switch (c) {
        ...
        }
}

versus

c = getopt_long(argc, argv, short_options, long_options, &option_index);
while (c != -1) {
        switch (c) {
        ...
        }
        c = getopt_long(argc, argv, short_options, long_options, &option_index);
}

Los compiladores modernos reconocerán este patrón y no avisarán cuando la asignación esté dentro de paréntesis como arriba, pero pueden advertir sobre otros usos. Por ejemplo:

if (x = y)         /* warning */

if ((x = y))       /* no warning */
if ((x = y) != 0)  /* no warning; explicit */

Algunos programadores utilizan la estrategia de poner la constante a la izquierda del operador (comúnmente llamadas condiciones de Yoda ). Debido a que las constantes son valores, este estilo de condición hará que el compilador arroje un error si se usa el operador incorrecto.

if (5 = y) /* Error */

if (5 == y) /* No error */

Sin embargo, esto reduce considerablemente la legibilidad del código y no se considera necesario si el programador sigue las buenas prácticas de codificación en C, y no ayuda cuando se comparan dos variables, por lo que no es una solución universal. Además, muchos compiladores modernos pueden dar advertencias cuando el código está escrito con las condiciones de Yoda.

Uso incauto de punto y coma

Ten cuidado con los puntos y coma. Siguiente ejemplo

if (x > a);
   a = x;

en realidad significa:

if (x > a) {}
a = x;

lo que significa que x se asignará a a en cualquier caso, que podría no ser lo que querías originalmente.

A veces, faltar un punto y coma también causará un problema que no se nota:

if (i < 0) 
    return
day = date[0];
hour = date[1];
minute = date[2];

Se omite el punto y coma detrás de la devolución, por lo que day = date [0] se devolverá.

Una técnica para evitar este y otros problemas similares es usar llaves en condicionales y bucles de varias líneas. Por ejemplo:

if (x > a) {
    a = x;
}

Olvidarse de asignar un byte extra para \ 0

Cuando esté copiando una cadena en un búfer malloc ed, recuerde siempre agregar 1 a strlen .

char *dest = malloc(strlen(src)); /* WRONG */
char *dest = malloc(strlen(src) + 1); /* RIGHT */

strcpy(dest, src);

Esto se debe a que strlen no incluye el final \0 en la longitud. Si adopta el enfoque WRONG (como se muestra arriba), al llamar a strcpy , su programa invocará un comportamiento indefinido.

También se aplica a situaciones en las que estás leyendo una cadena de longitud máxima conocida desde la stdin o alguna otra fuente. Por ejemplo

#define MAX_INPUT_LEN 42

char buffer[MAX_INPUT_LEN]; /* WRONG */
char buffer[MAX_INPUT_LEN + 1]; /* RIGHT */

scanf("%42s", buffer);  /* Ensure that the buffer is not overflowed */

Olvidando liberar memoria (fugas de memoria)

Una buena práctica de programación es liberar cualquier memoria que haya sido asignada directamente por su propio código, o implícitamente llamando a una función interna o externa, como una API de biblioteca como strdup() . Si no se puede liberar memoria, se puede producir una pérdida de memoria, que podría acumularse en una cantidad sustancial de memoria desperdiciada que no está disponible para su programa (o el sistema), lo que posiblemente provoque bloqueos o un comportamiento indefinido. Es más probable que ocurran problemas si la fuga se incurre repetidamente en un bucle o función recursiva. El riesgo de fallo del programa aumenta cuanto más se ejecuta un programa con fugas. A veces los problemas aparecen al instante; otras veces, los problemas no se verán durante horas o incluso años de funcionamiento constante. Las fallas en el agotamiento de la memoria pueden ser catastróficas, dependiendo de las circunstancias.

El siguiente bucle infinito es un ejemplo de una fuga que eventualmente agotará la fuga de memoria disponible al llamar a getline() , una función que asigna implícitamente una nueva memoria, sin liberar esa memoria.

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    /* The loop below leaks memory as fast as it can */

    for(;;) { 
        getline(&line, &size, stdin); /* New memory implicitly allocated */

        /* <do whatever> */

        line = NULL;
    }

    return 0;
}

En contraste, el código a continuación también usa la función getline() , pero esta vez, la memoria asignada se libera correctamente, evitando una fuga.

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    char *line = NULL;
    size_t size = 0;

    for(;;) {
        if (getline(&line, &size, stdin) < 0) {
            free(line);
            line = NULL;

            /* Handle failure such as setting flag, breaking out of loop and/or exiting */
        }

        /* <do whatever> */

        free(line);
        line = NULL;

    }

    return 0;
}

La pérdida de memoria no siempre tiene consecuencias tangibles y no es necesariamente un problema funcional. Si bien las "mejores prácticas" exigen que se libere rigurosamente la memoria en puntos y condiciones estratégicas, para reducir la huella de la memoria y reducir el riesgo de agotamiento de la memoria, puede haber excepciones. Por ejemplo, si un programa está limitado en duración y alcance, el riesgo de falla en la asignación podría considerarse demasiado pequeño para preocuparse. En ese caso, eludir la desasignación explícita podría considerarse aceptable. Por ejemplo, la mayoría de los sistemas operativos modernos liberan automáticamente toda la memoria consumida por un programa cuando finaliza, ya sea debido a un fallo del programa, a una llamada del sistema a exit() , a la finalización del proceso o al final de main() . Liberar explícitamente la memoria en el momento de la finalización inminente del programa podría ser redundante o introducir una penalización de rendimiento.

La asignación puede fallar si no hay suficiente memoria disponible, y las fallas en el manejo deben contabilizarse en los niveles apropiados de la pila de llamadas. getline() , que se muestra arriba, es un caso de uso interesante porque es una función de biblioteca que no solo asigna la memoria que deja al llamante gratis, sino que puede fallar por varias razones, todas las cuales deben tenerse en cuenta. Por lo tanto, es esencial al usar una API de C, leer la documentación (página de manual) y prestar especial atención a las condiciones de error y al uso de la memoria, y tener en cuenta qué capa de software tiene la responsabilidad de liberar la memoria devuelta.

Otra práctica común en el manejo de la memoria es establecer constantemente los punteros de la memoria en NULL inmediatamente después de que se libere la memoria a la que hacen referencia dichos punteros, de modo que se pueda probar la validez de los punteros en cualquier momento (por ejemplo, se verificó si no tenía NULL). puede llevar a problemas graves, como obtener datos de basura (operación de lectura) o corrupción de datos (operación de escritura) y / o un bloqueo del programa. En la mayoría de los sistemas operativos modernos, liberar la ubicación de memoria 0 ( NULL ) es un NOP (por ejemplo, es inofensivo), como lo exige el estándar C, por lo que al establecer un puntero en NULL, no hay riesgo de duplicar la memoria si el puntero Se pasa a free() . Tenga en cuenta que la doble liberación de la memoria puede llevar a fallas de tiempo, confusas y difíciles de diagnosticar .

Copiando demasiado

char buf[8]; /* tiny buffer, easy to overflow */

printf("What is your name?\n");
scanf("%s", buf); /* WRONG */
scanf("%7s", buf); /* RIGHT */

Si el usuario introduce una cadena de más de 7 caracteres (- 1 para el terminador nulo), la memoria detrás de la memoria intermedia buf será sobrescrito. Esto resulta en un comportamiento indefinido. Los piratas informáticos malintencionados a menudo explotan esto para sobrescribir la dirección de retorno y cambiarla a la dirección del código malicioso del pirata informático.

Olvidando copiar el valor de retorno de realloc en un temporal

Si falla realloc , devuelve NULL . Si asigna el valor del búfer original al valor de retorno de realloc , y si devuelve NULL , el búfer original (el puntero antiguo) se pierde, lo que provoca una pérdida de memoria . La solución consiste en copiar en un puntero temporal, y si eso no es NULL temporal, luego copia en el buffer real.

char *buf, *tmp;

buf = malloc(...);
...

/* WRONG */
if ((buf = realloc(buf, 16)) == NULL)
    perror("realloc");

/* RIGHT */
if ((tmp = realloc(buf, 16)) != NULL)
    buf = tmp;
else
    perror("realloc");

Comparando números de punto flotante

Los tipos de punto flotante ( float , double y double long double ) no pueden representar con precisión algunos números porque tienen una precisión finita y representan los valores en un formato binario. Al igual que hemos repetido decimales en la base 10 para fracciones como 1/3, hay fracciones que no se pueden representar finamente en binario (como 1/3, pero también, lo que es más importante, 1/10). No compare directamente los valores de punto flotante; usa un delta en su lugar.

#include <float.h> // for DBL_EPSILON and FLT_EPSILON
#include <math.h>  // for fabs()

int main(void)
{
    double a = 0.1; // imprecise: (binary) 0.000110...

    // may be false or true
    if (a + a + a + a + a + a + a + a + a + a == 1.0) {
        printf("10 * 0.1 is indeed 1.0. This is not guaranteed in the general case.\n");
    }

    // Using a small delta value.
    if (fabs(a + a + a + a + a + a + a + a + a + a - 1.0) < 0.000001) {
        // C99 5.2.4.2.2p8 guarantees at least 10 decimal digits
        // of precision for the double type.
        printf("10 * 0.1 is almost 1.0.\n");
    }

    return 0;
}

Otro ejemplo:

gcc -O3   -g   -I./inc   -std=c11   -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes  -Wold-style-definition       rd11.c -o rd11 -L./lib -lsoq 
#include <stdio.h>
#include <math.h>

static inline double rel_diff(double a, double b)
{
    return fabs(a - b) / fmax(fabs(a), fabs(b));
}

int main(void)
{
    double d1 = 3.14159265358979;
    double d2 = 355.0 / 113.0;

    double epsilon = 1.0;
    for (int i = 0; i < 10; i++)
    {
        if (rel_diff(d1, d2) < epsilon)
            printf("%d:%.10f <=> %.10f within tolerance %.10f (rel diff %.4E)\n",
                   i, d1, d2, epsilon, rel_diff(d1, d2));
        else
            printf("%d:%.10f <=> %.10f out of tolerance %.10f (rel diff %.4E)\n",
                   i, d1, d2, epsilon, rel_diff(d1, d2));
        epsilon /= 10.0;
    }
    return 0;
}

Salida:

0:3.1415926536 <=> 3.1415929204 within tolerance 1.0000000000 (rel diff 8.4914E-08)
1:3.1415926536 <=> 3.1415929204 within tolerance 0.1000000000 (rel diff 8.4914E-08)
2:3.1415926536 <=> 3.1415929204 within tolerance 0.0100000000 (rel diff 8.4914E-08)
3:3.1415926536 <=> 3.1415929204 within tolerance 0.0010000000 (rel diff 8.4914E-08)
4:3.1415926536 <=> 3.1415929204 within tolerance 0.0001000000 (rel diff 8.4914E-08)
5:3.1415926536 <=> 3.1415929204 within tolerance 0.0000100000 (rel diff 8.4914E-08)
6:3.1415926536 <=> 3.1415929204 within tolerance 0.0000010000 (rel diff 8.4914E-08)
7:3.1415926536 <=> 3.1415929204 within tolerance 0.0000001000 (rel diff 8.4914E-08)
8:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000100 (rel diff 8.4914E-08)
9:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000010 (rel diff 8.4914E-08)

Haciendo escala adicional en aritmética de punteros.

En la aritmética de punteros, el número entero que se agrega o se resta al puntero se interpreta no como un cambio de dirección sino como el número de elementos a mover.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = ptr + sizeof(int) * 2; /* wrong */
    printf("%d %d\n", *ptr, *ptr2);
    return 0;
}

Este código realiza una escala adicional al calcular el puntero asignado a ptr2 . Si sizeof(int) es 4, lo que es típico en entornos modernos de 32 bits, la expresión significa "8 elementos después de la array[0] ", que está fuera de rango, e invoca un comportamiento indefinido .

Para tener el punto ptr2 en lo que es 2 elementos después de la array[0] , simplemente debe agregar 2.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = ptr + 2;
    printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
    return 0;
}

La aritmética de punteros explícita con operadores aditivos puede ser confusa, por lo que el uso de subíndices de matriz puede ser mejor.

#include <stdio.h>

int main(void) {
    int array[] = {1, 2, 3, 4, 5};
    int *ptr = &array[0];
    int *ptr2 = &ptr[2];
    printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
    return 0;
}

E1[E2] es idéntico a (*((E1)+(E2))) ( N1570 6.5.2.1, párrafo 2), y &(E1[E2]) es equivalente a ((E1)+(E2)) ( N1570 6.5.3.2, nota 102).

Alternativamente, si se prefiere la aritmética de punteros, la conversión del puntero para direccionar un tipo de datos diferente puede permitir el direccionamiento de bytes. Sin embargo, tenga cuidado: el endianness puede convertirse en un problema, y ​​la conversión a tipos que no sean 'puntero a carácter' conduce a problemas estrictos de aliasing .

#include <stdio.h>

int main(void) {
    int array[3] = {1,2,3};  // 4 bytes * 3 allocated
    unsigned char *ptr = (unsigned char *) array;  // unsigned chars only take 1 byte
    /*
     * Now any pointer arithmetic on ptr will match
     * bytes in memory.  ptr can be treated like it
     * was declared as: unsigned char ptr[12];
     */

    return 0;
}

Las macros son simples reemplazos de cadena

Las macros son simples reemplazos de cadena. (Estrictamente hablando, trabajan con tokens de preprocesamiento, no con cadenas arbitrarias).

#include <stdio.h>

#define SQUARE(x) x*x

int main(void) {
    printf("%d\n", SQUARE(1+2));
    return 0;
}

Usted puede esperar que este código para imprimir 9 ( 3*3 ), pero en realidad 5 será impreso porque la macro se expandirá a 1+2*1+2 .

Debería envolver los argumentos y toda la expresión de la macro entre paréntesis para evitar este problema.

#include <stdio.h>

#define SQUARE(x) ((x)*(x))

int main(void) {
    printf("%d\n", SQUARE(1+2));
    return 0;
}

Otro problema es que no se garantiza que los argumentos de una macro se evalúen una vez; pueden no ser evaluados en absoluto, o pueden ser evaluados varias veces.

#include <stdio.h>

#define MIN(x, y) ((x) <= (y) ? (x) : (y))

int main(void) {
    int a = 0;
    printf("%d\n", MIN(a++, 10));
    printf("a = %d\n", a);
    return 0;
}

En este código, la macro se expandirá a ((a++) <= (10) ? (a++) : (10)) . Como a++ ( 0 ) es menor que 10 , a++ se evaluará dos veces y hará que el valor de a y lo que se devuelva desde MIN difiera de lo que puede esperar.

Esto se puede evitar mediante el uso de funciones, pero tenga en cuenta que los tipos serán corregidos por la definición de la función, mientras que las macros pueden ser (también) flexibles con los tipos.

#include <stdio.h>

int min(int x, int y) {
    return x <= y ? x : y;
}

int main(void) {
    int a = 0;
    printf("%d\n", min(a++, 10));
    printf("a = %d\n", a);
    return 0;
}

Ahora el problema de la doble evaluación está solucionado, pero esta función min no puede tratar con datos double sin truncar, por ejemplo.

Las directivas macro pueden ser de dos tipos:

#define OBJECT_LIKE_MACRO     followed by a "replacement list" of preprocessor tokens
#define FUNCTION_LIKE_MACRO(with, arguments) followed by a replacement list

Lo que distingue estos dos tipos de macros es el carácter que sigue al identificador después de #define : si es un lparen , es una macro similar a una función; de lo contrario, es una macro similar a un objeto. Si la intención es escribir una macro similar a una función, no debe haber ningún espacio en blanco entre el final del nombre de la macro y ( . Verifique esto para obtener una explicación detallada.

C99

En C99 o posterior, puede usar static inline int min(int x, int y) { … } .

C11

En C11, podría escribir una expresión 'genérica de tipo' para min .

#include <stdio.h>

#define min(x, y) _Generic((x), \
                        long double: min_ld, \
                        unsigned long long: min_ull, \
                        default: min_i \
                        )(x, y)

#define gen_min(suffix, type) \
    static inline type min_##suffix(type x, type y) { return (x < y) ? x : y; }

gen_min(ld, long double)
gen_min(ull, unsigned long long)
gen_min(i, int)

int main(void)
{
    unsigned long long ull1 = 50ULL;
    unsigned long long ull2 = 37ULL;
    printf("min(%llu, %llu) = %llu\n", ull1, ull2, min(ull1, ull2));
    long double ld1 = 3.141592653L;
    long double ld2 = 3.141592652L;
    printf("min(%.10Lf, %.10Lf) = %.10Lf\n", ld1, ld2, min(ld1, ld2));
    int i1 = 3141653;
    int i2 = 3141652;
    printf("min(%d, %d) = %d\n", i1, i2, min(i1, i2));
    return 0;
}

La expresión genérica podría extenderse con más tipos, como double , float , long long , unsigned long , long , unsigned , y las gen_min macro gen_min apropiadas escritas.

Errores de referencia indefinidos al enlazar

Uno de los errores más comunes en la compilación ocurre durante la etapa de vinculación. El error se parece a esto:

$ gcc undefined_reference.c 
/tmp/ccoXhwF0.o: In function `main':
undefined_reference.c:(.text+0x15): undefined reference to `foo'
collect2: error: ld returned 1 exit status
$

Así que echemos un vistazo al código que generó este error:

int foo(void);

int main(int argc, char **argv)
{
    int foo_val;
    foo_val = foo();
    return foo_val;
}

Aquí vemos una declaración de foo ( int foo(); ) pero ninguna definición de ella (función real). Por lo tanto, le proporcionamos al compilador el encabezado de la función, pero no se definió esa función en ninguna parte, por lo que la etapa de compilación pasa, pero el vinculador sale con un error de Undefined reference no definido.
Para corregir este error en nuestro pequeño programa, solo tendríamos que agregar una definición para foo:

/* Declaration of foo */
int foo(void);

/* Definition of foo */
int foo(void)
{
    return 5;
}

int main(int argc, char **argv)
{
    int foo_val;
    foo_val = foo();
    return foo_val;
}

Ahora este código se compilará. Surge una situación alternativa donde la fuente de foo() está en un archivo fuente separado foo.c (y hay un encabezado foo.h para declarar foo() que se incluye tanto en foo.c como en undefined_reference.c ). Luego, la solución es vincular tanto el archivo objeto de foo.c como undefined_reference.c , o compilar ambos archivos fuente:

$ gcc -c undefined_reference.c 
$ gcc -c foo.c
$ gcc -o working_program undefined_reference.o foo.o
$

O:

$ gcc -o working_program undefined_reference.c foo.c
$

Un caso más complejo es donde están involucradas las bibliotecas, como en el código:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(int argc, char **argv)
{
    double first;
    double second;
    double power;

    if (argc != 3)
    {
        fprintf(stderr, "Usage: %s <denom> <nom>\n", argv[0]);
        return EXIT_FAILURE;
    }

    /* Translate user input to numbers, extra error checking
     * should be done here. */
    first = strtod(argv[1], NULL);
    second = strtod(argv[2], NULL);

    /* Use function pow() from libm - this will cause a linkage 
     * error unless this code is compiled against libm! */
    power = pow(first, second);

    printf("%f to the power of %f = %f\n", first, second, power);

    return EXIT_SUCCESS;
}

El código es sintácticamente correcto, la declaración para pow() existe desde #include <math.h> , por lo que tratamos de compilar y vincular pero obtenemos un error como este:

$ gcc no_library_in_link.c -o no_library_in_link
/tmp/ccduQQqA.o: In function `main':
no_library_in_link.c:(.text+0x8b): undefined reference to `pow'
collect2: error: ld returned 1 exit status
$

Esto sucede porque no se encontró la definición de pow() durante la etapa de enlace. Para arreglar esto, tenemos que especificar que queremos vincular con la biblioteca matemática llamada libm especificando el indicador -lm . (Tenga en cuenta que hay plataformas como macOS donde no se necesita -lm , pero cuando obtiene la referencia no definida, se necesita la biblioteca).

Así que ejecutamos la etapa de compilación nuevamente, esta vez especificando la biblioteca (después de los archivos de origen o de objeto):

$ gcc no_library_in_link.c -lm -o library_in_link_cmd
$ ./library_in_link_cmd 2 4
2.000000 to the power of 4.000000 = 16.000000
$

¡Y funciona!

Malentendido decaimiento de la matriz

Un problema común en el código que utiliza matrices multidimensionales, matrices de punteros, etc. es el hecho de que Type** y Type[M][N] son tipos fundamentalmente diferentes:

#include <stdio.h>

void print_strings(char **strings, size_t n)
{
    size_t i;
    for (i = 0; i < n; i++)
        puts(strings[i]);
}

int main(void)
{
    char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
    print_strings(s, 4);
    return 0;
}

Ejemplo de salida del compilador:

file1.c: In function 'main':
file1.c:13:23: error: passing argument 1 of 'print_strings' from incompatible pointer type [-Wincompatible-pointer-types]
         print_strings(strings, 4);
                       ^
file1.c:3:10: note: expected 'char **' but argument is of type 'char (*)[20]'
     void print_strings(char **strings, size_t n)

El error indica que la matriz s en la función main se pasa a la función print_strings , que espera un tipo de puntero diferente del que recibió. También incluye una nota que expresa el tipo que se espera por print_strings y el tipo que se le pasó desde main .

El problema se debe a algo llamado decaimiento de la matriz . Lo que sucede cuando se pasa s con su tipo char[4][20] (matriz de 4 matrices de 20 caracteres) a la función es que se convierte en un puntero a su primer elemento como si hubiera escrito &s[0] , que tiene el tipo char (*)[20] (puntero a 1 matriz de 20 caracteres). Esto ocurre para cualquier matriz, incluida una matriz de punteros, una matriz de matrices de matrices (matrices en 3D) y una matriz de punteros a una matriz. A continuación se muestra una tabla que ilustra lo que sucede cuando una matriz se descompone. Los cambios en la descripción del tipo se resaltan para ilustrar lo que sucede:

Antes de la decadencia Despues de la decadencia
char [20] matriz de (20 caracteres) char * puntero a (1 char)
char [4][20] matriz de (4 matrices de 20 caracteres) char (*)[20] puntero a (1 matriz de 20 caracteres)
char *[4] matriz de (4 punteros a 1 carácter) char ** puntero a (1 puntero a 1 carácter)
char [3][4][20] matriz de (3 matrices de 4 matrices de 20 caracteres) char (*)[4][20] puntero a (1 matriz de 4 matrices de 20 caracteres)
char (*[4])[20] matriz de (4 punteros a 1 matriz de 20 caracteres) char (**)[20] puntero a (1 puntero a 1 matriz de 20 caracteres)

Si una matriz se puede descomponer en un puntero, se puede decir que un puntero puede considerarse una matriz de al menos 1 elemento. Una excepción a esto es un puntero nulo, que apunta a nada y, en consecuencia, no es una matriz.

La desintegración de la matriz solo ocurre una vez. Si una matriz se ha degradado a un puntero, ahora es un puntero, no una matriz. Incluso si tiene un puntero a una matriz, recuerde que el puntero podría considerarse una matriz de al menos un elemento, por lo que ya se ha producido una caída de la matriz.

En otras palabras, un puntero a una matriz ( char (*)[20] ) nunca se convertirá en un puntero a un puntero ( char ** ). Para arreglar la función print_strings , simplemente haga que reciba el tipo correcto:

void print_strings(char (*strings)[20], size_t n)
/* OR */
void print_strings(char strings[][20], size_t n)

Surge un problema cuando desea que la función print_strings sea ​​genérica para cualquier conjunto de caracteres: ¿qué print_strings si hay 30 caracteres en lugar de 20? O 50? La respuesta es agregar otro parámetro antes del parámetro de matriz:

#include <stdio.h>

/*
 * Note the rearranged parameters and the change in the parameter name
 * from the previous definitions:
 *      n (number of strings)
 *   => scount (string count)
 *
 * Of course, you could also use one of the following highly recommended forms
 * for the `strings` parameter instead:
 *
 *    char strings[scount][ccount]
 *    char strings[][ccount]
 */
void print_strings(size_t scount, size_t ccount, char (*strings)[ccount])
{
    size_t i;
    for (i = 0; i < scount; i++)
        puts(strings[i]);
}

int main(void)
{
    char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
    print_strings(4, 20, s);
    return 0;
}

Compilarlo no produce errores y da como resultado el resultado esperado:

Example 1
Example 2
Example 3
Example 4

Pasar matrices no adyacentes a funciones que esperan matrices multidimensionales "reales"

Cuando se asignan matrices multidimensionales con malloc , calloc y realloc , un patrón común es asignar las matrices internas con múltiples llamadas (incluso si la llamada solo aparece una vez, puede estar en un bucle):

/* Could also be `int **` with malloc used to allocate outer array. */
int *array[4];
int i;

/* Allocate 4 arrays of 16 ints. */
for (i = 0; i < 4; i++)
    array[i] = malloc(16 * sizeof(*array[i]));

La diferencia en bytes entre el último elemento de una de las matrices internas y el primer elemento de la siguiente matriz interna puede no ser 0 como lo sería con una matriz multidimensional "real" (por ejemplo, int array[4][16]; ) :

/* 0x40003c, 0x402000 */
printf("%p, %p\n", (void *)(array[0] + 15), (void *)array[1]);

Teniendo en cuenta el tamaño de int , obtiene una diferencia de 8128 bytes (8132-4), que es 2032 elementos de matriz de tamaño int , y ese es el problema: una matriz multidimensional "real" no tiene espacios entre los elementos.

Si necesita usar una matriz asignada dinámicamente con una función que espera una matriz multidimensional "real", debe asignar un objeto de tipo int * y usar la aritmética para realizar cálculos:

void func(int M, int N, int *array);
...

/* Equivalent to declaring `int array[M][N] = {{0}};` and assigning to array4_16[i][j]. */
int *array;
int M = 4, N = 16;
array = calloc(M, N * sizeof(*array));
array[i * N + j] = 1;
func(M, N, array);

Si N es una macro o un literal entero en lugar de una variable, el código simplemente puede usar la notación de matriz 2-D más natural después de asignar un puntero a una matriz:

void func(int M, int N, int *array);
#define N 16
void func_N(int M, int (*array)[N]);
...

int M = 4;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;

/* Cast to `int *` works here because `array` is a single block of M*N ints with no gaps,
   just like `int array2[M * N];` and `int array3[M][N];` would be. */
func(M, N, (int *)array);
func_N(M, array);
C99

Si N no es una macro o un literal entero, la array apuntará a una matriz de longitud variable (VLA). Esto todavía se puede usar con func al lanzar int * y una nueva función func_vla reemplazaría func_N :

void func(int M, int N, int *array);
void func_vla(int M, int N, int array[M][N]);
...

int M = 4, N = 16;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;
func(M, N, (int *)array);
func_vla(M, N, array);
C11

Nota : los VLA son opcionales a partir de C11. Si su implementación es compatible con C11 y define la macro __STDC_NO_VLA__ a 1, está atascado con los métodos anteriores a C99.

Usando constantes de caracteres en lugar de cadenas literales, y viceversa

En C, las constantes de caracteres y los literales de cadena son cosas diferentes.

Un carácter rodeado de comillas simples como 'a' es una constante de carácter . Una constante de carácter es un entero cuyo valor es el código de carácter que representa el carácter. La forma de interpretar constantes de caracteres con varios caracteres como 'abc' está definida por la implementación.

Cero o más caracteres rodeados por comillas dobles como "abc" es una cadena literal . Un literal de cadena es una matriz no modificable cuyos elementos son tipo char . La cadena entre las comillas dobles y el carácter nulo que termina son los contenidos, por lo que "abc" tiene 4 elementos ( {'a', 'b', 'c', '\0'} )

En este ejemplo, se usa una constante de caracteres donde se debe usar un literal de cadena. Esta constante de caracteres se convertirá en un puntero de una manera definida por la implementación y hay pocas posibilidades de que el puntero convertido sea válido, por lo que este ejemplo invocará un comportamiento indefinido .

#include <stdio.h>

int main(void) {
    const char *hello = 'hello, world'; /* bad */
    puts(hello);
    return 0;
}

En este ejemplo, se usa un literal de cadena donde se debe usar una constante de caracteres. El puntero convertido de la cadena literal será convertido a un número entero de una manera definida por la implementación, y se puede convertir en char de una manera definida por la implementación. (Cómo convertir un entero a un tipo con signo que no puede representar el valor a convertir es definido por la implementación, y si char se firmó también es definido por la implementación.) La salida será algo sin sentido.

#include <stdio.h>

int main(void) {
    char c = "a"; /* bad */
    printf("%c\n", c);
    return 0;
}

En casi todos los casos, el compilador se quejará de estas confusiones. Si no es así, debe usar más opciones de advertencia del compilador, o se recomienda que use un compilador mejor.

Ignorar los valores de retorno de las funciones de la biblioteca

Casi todas las funciones en la biblioteca estándar de C devuelven algo en el éxito, y algo más en el error. Por ejemplo, malloc devolverá un puntero al bloque de memoria asignado por la función en caso de éxito y, si la función no pudo asignar el bloque de memoria solicitado, un puntero nulo. Por lo tanto, siempre debe verificar el valor de retorno para una depuración más fácil.

Esto es malo:

char* x = malloc(100000000000UL * sizeof *x);
/* more code */
scanf("%s", x); /* This might invoke undefined behaviour and if lucky causes a segmentation violation, unless your system has a lot of memory */

Esto es bueno:

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    char* x = malloc(100000000000UL * sizeof *x);
    if (x == NULL) {
        perror("malloc() failed");
        exit(EXIT_FAILURE);
    }

    if (scanf("%s", x) != 1) {
        fprintf(stderr, "could not read string\n");
        free(x);
        exit(EXIT_FAILURE);
    }

    /* Do stuff with x. */

    /* Clean up. */
    free(x);

    return EXIT_SUCCESS;
}

De esta manera, sabrá de inmediato la causa del error; de lo contrario, podría pasar horas buscando un error en un lugar completamente incorrecto.

El carácter de nueva línea no se consume en una llamada a scanf () típica

Cuando este programa

#include <stdio.h>
#include <string.h>

int main(void) {
    int num = 0;
    char str[128], *lf;

    scanf("%d", &num);
    fgets(str, sizeof(str), stdin);

    if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
    printf("%d \"%s\"\n", num, str);
    return 0;
}

se ejecuta con esta entrada

42
life

la salida será de 42 "" lugar de las 42 "life" esperadas 42 "life" .

Esto se debe a que un carácter de nueva línea después de 42 no se consume en la llamada a scanf() y es consumido por fgets() antes de leer la life . Entonces, fgets() deja de leer antes de leer la life .

Para evitar este problema, una forma que es útil cuando se conoce la longitud máxima de una línea, cuando se resuelven problemas en el sistema de jueces en línea, por ejemplo, es evitar usar scanf() directamente y leer todas las líneas a través de fgets() . Puede usar sscanf() para analizar las líneas leídas.

#include <stdio.h>
#include <string.h>

int main(void) {
    int num = 0;
    char line_buffer[128] = "", str[128], *lf;

    fgets(line_buffer, sizeof(line_buffer), stdin);
    sscanf(line_buffer, "%d", &num);
    fgets(str, sizeof(str), stdin);

    if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
    printf("%d \"%s\"\n", num, str);
    return 0;
}

Otra forma es leer hasta que golpeas un carácter de nueva línea después de usar scanf() y antes de usar fgets() .

#include <stdio.h>
#include <string.h>

int main(void) {
    int num = 0;
    char str[128], *lf;
    int c;

    scanf("%d", &num);
    while ((c = getchar()) != '\n' && c != EOF);
    fgets(str, sizeof(str), stdin);

    if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
    printf("%d \"%s\"\n", num, str);
    return 0;
}

Añadiendo un punto y coma a un #define

Es fácil confundirse con el preprocesador de C y tratarlo como parte de C, pero eso es un error porque el preprocesador es solo un mecanismo de sustitución de texto. Por ejemplo, si escribes

/* WRONG */
#define MAX 100;
int arr[MAX];

el código se expande a

int arr[100;];

que es un error de sintaxis. El remedio es eliminar el punto y coma de la línea #define . Es casi invariablemente un error terminar un #define con un punto y coma.

Los comentarios multilínea no pueden ser anidados

En C, comentarios multilínea, / * y * /, no se anidan.

Si anota un bloque de código o función utilizando este estilo de comentario:

/*
 * max(): Finds the largest integer in an array and returns it.
 * If the array length is less than 1, the result is undefined.
 * arr: The array of integers to search.
 * num: The number of integers in arr.
 */
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

No podrás comentarlo fácilmente:

//Trying to comment out the block...
/*

/*
 * max(): Finds the largest integer in an array and returns it.
 * If the array length is less than 1, the result is undefined.
 * arr: The array of integers to search.
 * num: The number of integers in arr.
 */
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

//Causes an error on the line below...
*/

Una solución es utilizar comentarios de estilo C99:

// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

Ahora todo el bloque se puede comentar fácilmente:

/*

// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}

*/

Otra solución es evitar deshabilitar el código usando la sintaxis de los comentarios, utilizando las directivas de preprocesador #ifdef o #ifndef lugar. Estas directivas hacen nido, por lo que podrá comentar su código en el estilo que prefiera.

#define DISABLE_MAX /* Remove or comment this line to enable max() code block */

#ifdef DISABLE_MAX
/*
 * max(): Finds the largest integer in an array and returns it.
 * If the array length is less than 1, the result is undefined.
 * arr: The array of integers to search.
 * num: The number of integers in arr.
 */
int max(int arr[], int num)
{
    int max = arr[0];
    for (int i = 0; i < num; i++)
        if (arr[i] > max)
            max = arr[i];
    return max;
}
#endif

Algunas guías llegan al punto de recomendar que las secciones de código nunca deben ser comentadas y que si el código se desactiva temporalmente, se podría recurrir al uso de una directiva #if 0 .

Vea #if 0 para bloquear secciones de código .

Superando los límites de la matriz

Las matrices se basan en cero, es decir, el índice siempre comienza en 0 y termina con la longitud de la matriz de índice menos 1. Por lo tanto, el siguiente código no generará el primer elemento de la matriz y generará basura para el valor final que imprima.

#include <stdio.h>

int main(void)
{
    int x = 0;
    int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements

    for(x = 1; x <= 5; x++) //Looping from 1 till 5.
       printf("%d\t", myArray[x]);

    printf("\n");
    return 0;
}

Salida: 2 3 4 5 GarbageValue

A continuación se muestra la forma correcta de lograr el resultado deseado:

#include <stdio.h>

int main(void)
{
    int x = 0;
    int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements

    for(x = 0; x < 5; x++) //Looping from 0 till 4.
       printf("%d\t", myArray[x]);

    printf("\n");
    return 0;
}

Salida: 1 2 3 4 5

Es importante conocer la longitud de una matriz antes de trabajar con ella, ya que de lo contrario puede dañar el búfer o provocar un fallo de segmentación al acceder a las ubicaciones de memoria que están fuera de los límites.

Función recursiva - perdiendo la condición base

Calcular el factorial de un número es un ejemplo clásico de una función recursiva.

Falta la condición de base:

#include <stdio.h>

int factorial(int n)
{
       return n * factorial(n - 1);
}

int main()
{
    printf("Factorial %d = %d\n", 3, factorial(3));
    return 0;
}

Salida típica: Segmentation fault: 11

El problema con esta función es que se produciría un bucle infinito, lo que provocaría un fallo de segmentación: necesita una condición base para detener la recursión.

Condición de base declarada:

#include <stdio.h>

int factorial(int n)
{
    if (n == 1) // Base Condition, very crucial in designing the recursive functions.
    {
       return 1;
    }
    else
    {
       return n * factorial(n - 1);
    }
}

int main()
{
    printf("Factorial %d = %d\n", 3, factorial(3));
    return 0;
}

Salida de muestra

Factorial 3 = 6

Esta función terminará tan pronto como llegue a la condición n es igual a 1 (siempre que el valor inicial de n sea ​​lo suficientemente pequeño - el límite superior es 12 cuando int es una cantidad de 32 bits).

Reglas a seguir:

  1. Inicializa el algoritmo. Los programas recursivos a menudo necesitan un valor semilla para empezar. Esto se logra ya sea utilizando un parámetro pasado a la función o proporcionando una función de puerta de enlace que no sea recursiva pero que configure los valores semilla para el cálculo recursivo.
  2. Compruebe si los valores actuales que se procesan coinciden con el caso base. Si es así, procese y devuelva el valor.
  3. Redefinir la respuesta en términos de un subproblema o subproblemas más pequeños o más simples.
  4. Ejecutar el algoritmo en el sub-problema.
  5. Combina los resultados en la formulación de la respuesta.
  6. Devuelve los resultados.

Fuente: Función Recursiva.

Comprobando la expresión lógica contra 'verdadero'

El estándar C original no tenía un tipo booleano intrínseco, por lo que bool , true y false no tenía un significado inherente y a menudo los definían los programadores. Normalmente, true se definiría como 1 y false se definiría como 0.

C99

C99 agrega el tipo _Bool y el encabezado <stdbool.h> que define bool (expandiéndose a _Bool ), false y true . También le permite redefinir bool , true y false , pero observa que esta es una característica obsolescente.

Más importante aún, las expresiones lógicas tratan todo lo que se evalúa como cero como falso y cualquier evaluación que no sea cero como verdadera. Por ejemplo:

/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
    if ((bitField & 0x80) == true)  /* Comparison only succeeds if true is 0x80 and bitField has that bit set */
    {
        return true;
    }
    else
    {
        return false;
    }
}

En el ejemplo anterior, la función está intentando verificar si el bit superior está establecido y devuelve true si lo está. Sin embargo, al verificar explícitamente contra true , la instrucción if solo tendrá éxito si (bitfield & 0x80) evalúa a lo que se define como true , que generalmente es 1 y muy raramente 0x80 . O bien compruebe explícitamente contra el caso que espera:

/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
    if ((bitField & 0x80) == 0x80) /* Explicitly test for the case we expect */
    {
        return true;
    }
    else
    {
        return false;
    }
}

O evalúe cualquier valor distinto de cero como verdadero.

/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
    /* If upper bit is set, result is 0x80 which the if will evaluate as true */
    if (bitField & 0x80)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Los literales de punto flotante son de tipo doble por defecto.

Se debe tener cuidado al inicializar variables de tipo float a valores literales o al compararlas con valores literales, porque los literales de punto flotante regulares como 0.1 son de tipo double . Esto puede llevar a sorpresas:

#include <stdio.h>
int main() {
    float  n;
    n = 0.1;
    if (n > 0.1) printf("Wierd\n");
    return 0;
}
// Prints "Wierd" when n is float

Aquí, n se inicializa y se redondea a precisión simple, lo que da como resultado un valor de 0.10000000149011612. Luego, n se vuelve a convertir en doble precisión para compararse con 0.1 literal (lo que equivale a 0.1000000000000000001), lo que resulta en una falta de coincidencia.

Además de los errores de redondeo, la mezcla de variables float con literales double dará como resultado un rendimiento deficiente en plataformas que no tienen soporte de hardware para la precisión doble.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow