Tamaño de tipos de datos en C: sizeof(), strlen() uso y errores comunes

Para saber el tamaño de los tipos de datos en C tenemos un operador incorporado sizeof que nos permite conocer el tamaño en bytes. En los tipos de datos básicos tenemos por definición que el tipo char tiene un tamaño de 1 byte (C define el tamaño de 1 byte en términos de char). Para otros tipos de datos básicos su tamaño puede variar entre diferentes arquitecturas.

Vamos a ver como mostrar el tamaño de algunos tipos de datos básicos:

/* Mostrar los tamaños de tipos básicos son sizeof 
*/ 
#include <stdio.h> 
#include <stdlib.h> 
 
int  
main (int argc, char *argv[])  
{ 
    printf ("El Tipo <char> tiene un tamaño de %zd bytes\n", sizeof (char)); 
    printf ("El Tipo <short int> tiene un tamaño de %zd bytes\n", sizeof (short int)); 
    printf ("El Tipo <int> tiene un tamaño de %zd bytes\n", sizeof (int)); 
    printf ("El Tipo <float> tiene un tamaño de %zd bytes\n", sizeof (float)); 
    printf ("El Tipo <long> tiene un tamaño de %zd bytes\n", sizeof (long)); 
    printf ("El Tipo <long long> tiene un tamaño de %zd bytes\n", sizeof (long long)); 
    printf ("El Tipo <double> tiene un tamaño de %zd bytes\n", sizeof (double)); 
    printf ("El Tipo <long double> tiene un tamaño de %zd bytes\n", sizeof (long double)); 
     
    exit (EXIT_SUCCESS); 
} 

Vemos que he utilizado el especificador %zd en la función printf. sizeof puede devolver un tipo variable, normalmente unsigned o unsigned long, por lo que tendríamos que usar %u o %lu. Desde C99 esta definido el especificador %zd para mostrar los datos devueltos por sizeof (o strlen que veremos en un momento).

Si ejecutamos el código tendremos algo como esto:

$ ./tvar 
El Tipo <char> tiene un tamaño de 1 bytes
El Tipo <short int> tiene un tamaño de 2 bytes
El Tipo <int> tiene un tamaño de 4 bytes
El Tipo <float> tiene un tamaño de 4 bytes
El Tipo <long> tiene un tamaño de 8 bytes
El Tipo <long long> tiene un tamaño de 8 bytes
El Tipo <double> tiene un tamaño de 8 bytes
El Tipo <long double> tiene un tamaño de 16 bytes

De igual forma podemos obtener el tamaño de datos más complejos como una estructura:

/* Mostrar el tamaño de una estructura 
*/ 
#include <stdio.h> 
#include <stdlib.h> 
 
int  
main (int argc, char *argv[])  
{ 
    struct mis_datos_s { 
        char dat_char[32]; 
        short int dat_sint[20]; 
        int dat_int[10]; 
        long dat_long; 
        double dat_double[10];  
    }; 
 
    struct mis_datos_s primer_dato = {}; 
    struct mis_datos_s varios_datos[10] = {}; 
     
    printf ("<mis_datos_s> tiene un tamaño de %zd bytes\n", sizeof (struct mis_datos_s)); 
    printf ("<primer_dato> tiene un tamaño de %zd bytes\n", sizeof (primer_dato));  
    printf ("<varios_datos[0]> tiene un tamaño de %zd bytes\n", sizeof (varios_datos[0]));  
    printf ("<varios_datos> tiene un tamaño de %zd bytes\n", sizeof (varios_datos));  
     
    printf ("Numero de elementos en <varios_datos>: %zd\n",  
            sizeof (varios_datos) / sizeof (varios_datos[0])); 
 
    exit (EXIT_SUCCESS); 
} 

Si ejecutamos este código tendríamos un resultado similar a este:

$ ./sizestruct
<mis_datos_s> tiene un tamaño de 200 bytes
<primer_dato> tiene un tamaño de 200 bytes
<varios_datos[0]> tiene un tamaño de 200 bytes
<varios_datos> tiene un tamaño de 2000 bytes
Numero de elementos en <varios_datos>: 10

Como vemos nos muestra correctamente el tamaño del tipo struct mis_datos_s con un tamaño de 200 bytes, una instancia de esta que hemos llamado primer_dato de igual forma con un tamaño de 200 bytes, y un array de 10 estructuras mis_datos_s con un tamaño de 2000 bytes (10 estructuras x 200 bytes cada una) llamada varios_datos.

Podemos ver también el tamaño de un solo elemento de la estructura con varios_datos[0], lo que nos permite conocer el numero de elementos de la estructura con sizeof (varios_datos) / sizeof (varios_datos[0]).

Es muy frecuente utilizar sizeof para calcular el tamaño en la asignación dinámica de memoria, por lo que en su uso se deben extremar las precauciones para evitar errores graves en la gestión de memoria (desbordamientos de buffer en head). Vamos a ver un ejemplo reservando nuestra estructura struct mis_datos del ejemplo anterior usando memoria dinámica:

/* Mostrar el tamaño de una estructura (Punteros) 
*/ 
#include <stdio.h> 
#include <stdlib.h> 
 
int  
main (int argc, char *argv[])  
{ 
    struct mis_datos_s { 
        char dat_char[32]; 
        short int dat_sint[20]; 
        int dat_int[10]; 
        long dat_long; 
        double dat_double[10];  
    }; 
     
    int numStruct = 10; 
     
    struct mis_datos_s primer_dato = {}; 
    struct mis_datos_s *varios_datos = NULL; 
     
    printf ("<mis_datos_s> tiene un tamaño de %zd bytes\n", sizeof (struct mis_datos_s)); 
    printf ("<primer_dato> tiene un tamaño de %zd bytes\n", sizeof (primer_dato));  
     
    /* Reservo espacio para numStruct estructuras <mis_datos_s> */ 
    varios_datos = calloc (numStruct, sizeof(struct mis_datos_s)); 
    if (varios_datos == NULL) // fallo al asignar la memoria  
        exit (EXIT_FAILURE);  
     
    printf ("Puntero a <varios_datos> tiene un tamaño de %zd bytes\n", sizeof (varios_datos));  
    printf ("<varios_datos> tiene un tamaño de %zd bytes\n", sizeof (*varios_datos) * numStruct);  
     
    free (varios_datos);  
    varios_datos = NULL; 
    exit (EXIT_SUCCESS); 
} 

Tenemos la misma estructura que en el ejemplo anterior y hacemos una reserva dinámica de memoria (inicializando a 0) de 10 estructuras con calloc (numStruct, sizeof(struct mis_datos_s).

Este es el resultado de ejecutar este programa:

$ ./sizestructs
<mis_datos_s> tiene un tamaño de 200 bytes
<primer_dato> tiene un tamaño de 200 bytes
Puntero a <varios_datos> tiene un tamaño de 8 bytes
<varios_datos> tiene un tamaño de 2000 bytes

Lo más relevante que podemos ver es que si ejecutamos sizeof (varios_datos) obtenemos el tamaño del puntero que apunta a la estructura y no el tamaño de la estructura.

Cuando tenemos un puntero a una estructura o a una matriz no podemos obtener el tamaño del dato al que apunta son sizeof: CERT C Secure Coding Standard "ARR01-C. No aplique el operador sizeof a un puntero cuando tome el tamaño de una matriz"

Para saber el tamaño del dato al que apunta el putero tenemos que saber el tipo de dato y el número de elementos.

Utilizar sizeof con punteros a estructuras o matrices es un error que podemos encontrar en algunas ocasiones. Este, por ejemplo, es un código que se puede encontrar en Internet:

/* Programa con error en el uso de sizeof 
*/ 
#include <stdio.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/mman.h> 
 
static void* create_shared_memory(size_t size) { 
    // Our memory buffer will be readable and writable: 
    int protection = PROT_READ | PROT_WRITE; 
 
    // The buffer will be shared (meaning other processes can access it), but 
    // anonymous (meaning third-party processes cannot obtain an address for it), 
    // so only this process and its children will be able to use it: 
    int visibility = MAP_ANONYMOUS | MAP_SHARED; 
 
    // The remaining parameters to `mmap()` are not important for this use case, 
    // but the manpage for `mmap` explains their purpose. 
    return mmap(NULL, size, protection, visibility, -1, 0); 
} 
 
int main() { 
    char* parent_message = "hello"; // parent process will write this message 
    char* child_message = "goodbye"; // child process will then write this one 
 
    void* shmem = create_shared_memory(128); 
 
    memcpy(shmem, parent_message, sizeof(parent_message)); 
 
    int pid = fork(); 
 
    if (pid == 0) { 
    printf("Child read: %s\n", (char *) shmem); 
    memcpy(shmem, child_message, sizeof(child_message)); 
    printf("Child wrote: %s\n", (char *) shmem); 
 
    } else { 
    printf("Parent read: %s\n", (char *) shmem); 
    sleep(1); 
    printf("After 1s, parent read: %s\n", (char *) shmem); 
    } 
} 

El programa crea un proceso con fork() y utiliza mmap() para comunicarse entre procesos. Si ejecutamos el programa, funciona como se espera:

$ ./mem_err 
Parent read: hello
Child read: hello
Child wrote: goodbye
After 1s, parent read: goodbye

Esta es la definición de la variable parent_message:

char* parent_message = "hello"; // parent process will write this message 

Esta variable es un puntero a literal, y esta es la forma que usan para medir el tamaño de la cadena:

memcpy(shmem, parent_message, sizeof(parent_message)); 

Como hemos comentado es erronea. Lo que devuelve sizeof en este caso es el tamaño del puntero (8 bytes) que como es suficiente para una cadena de 6 bytes (5 caracteres más final de cadena) hace que el programa funcione. Si utilizamos una cadema más larga comenzaremos a ver los problemas.

Si compilamos con -Wall o al menos con -Wsizeof-pointer-memaccess tendremos una aviso de gcc donde nos alerta del uso incorrecto de sizeof:

$ gcc -Wall mem_err.c -o mem_err
mem_err.c: In function ‘main’:
mem_err.c:28:39: warning: argument to ‘sizeof’ in ‘memcpy’ call is the same expression as the source; did you mean to provide an explicit length? [-Wsizeof-pointer-memaccess]
28 | memcpy(shmem, parent_message, sizeof(parent_message));
| ^
mem_err.c:34:40: warning: argument to ‘sizeof’ in ‘memcpy’ call is the same expression as the source; did you mean to provide an explicit length? [-Wsizeof-pointer-memaccess]
34 | memcpy(shmem, child_message, sizeof(child_message));
| ^

Para poder saber la longitud de una cadena se utiliza la función strlen(), que calcula la longitud de la cadena en caracteres. Vamos a ver el uso de strlen() con un ejemplo:

/* Mostrar el tamaño de una cadena como array, como un puntero a literal 
 * y como un puntero a memoria dinámica  
*/ 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
 
int  
main (int argc, char *argv[])  
{ 
    char cadena1[] = "0123456789"; 
    char cadena2[] = { '0', '1', '2', '3', '4', '5','6', '7', '8', '9', '\0' };  
    char *cadena3 = "0123456789"; 
    char *cadena4 = NULL;  
     
    printf ("<cadena1> tiene un tamaño de %zd bytes\n", sizeof (cadena1)); 
    printf ("<cadena2> tiene un tamaño de %zd bytes\n", sizeof (cadena2));  
    printf ("<cadena3> tiene un tamaño de %zd bytes\n", sizeof (cadena3));  
    printf ("<cadena4> tiene un tamaño de %zd bytes\n", sizeof (cadena4)); 
 
    printf ("<cadena1> tiene un tamaño de %zd caracteres\n", strlen (cadena1)); 
    printf ("<cadena2> tiene un tamaño de %zd caracteres\n", strlen (cadena2));  
    printf ("<cadena3> tiene un tamaño de %zd caracteres\n", strlen (cadena3)); 
         
    size_t num_car_cadena3 = strlen (cadena3); 
    cadena4 = (char *) malloc (num_car_cadena3 + 1); // + 1 para el final de cadena 
    if (cadena4 == NULL) // fallo al asignar la memoria  
        exit (EXIT_FAILURE);  
    memset(cadena4, 0, num_car_cadena3 + 1); // Inicializamos la memoria a 0  
    strncpy (cadena4, cadena3, num_car_cadena3 + 1); // copia de cadena3 incluido el fin de cadena 
    *(cadena4 + num_car_cadena3) = '\0'; // nos aseguramos que cadena4 termina con fin de cadena 
          
    printf ("<cadena4> tiene un tamaño de %zd caracteres\n", strlen (cadena4)); 
     
    free (cadena4); 
    cadena4 = NULL; 
    exit (EXIT_SUCCESS); 
} 

Tenemos en cadena1[] y cadena2[] dos formas de definir un array a caracteres (en ambas se añade el caracter nulo fin de cadena '\0' aunque solo es visible en la segunda). Luego tenemos *cadena3 que es un puntero a literal y *cadena4 que definimos como un puntero inicializado a NULL y al que apuntaremos a la reserva de memoria que haremos com malloc().

Como es de esperar sizeof devuelve correctamente el tamaño de los array de caracteres y el tamaño de las variables punteros en el caso de los punteros (y no el tamaño de los datos a los que apuntan):

$ ./sizepchar 
<cadena1> tiene un tamaño de 11 bytes
<cadena2> tiene un tamaño de 11 bytes
<cadena3> tiene un tamaño de 8 bytes
<cadena4> tiene un tamaño de 8 bytes
<cadena1> tiene un tamaño de 10 caracteres
<cadena2> tiene un tamaño de 10 caracteres
<cadena3> tiene un tamaño de 10 caracteres
<cadena4> tiene un tamaño de 10 caracteres

Usando strlen() obtenemos el número de carcteres en todos los casos. Pero no cuenta el caracter nulo final '\0' por lo que si queremos reservar memoria para copiar un array de caracteres tendremos que añadirle 1 a lo que nos devuelve strlen() del array:

size_t num_car_cadena3 = strlen (cadena3); 
cadena4 = (char *) malloc (num_car_cadena3 + 1); // + 1 para el final de cadena 

Debemos recordar siempre que strlen() no cuenta el caracter nulo fin de cadena '\0': CERT C Secure Coding Standard "STR31-C. Garantiza que el almacenamiento de la cadenas tiene suficiente espacio para los datos de caracteres y el terminador null"

De igual forma tenemos que tenerlo en cuenta si queremos inicializar la memoria reservada con memset():

memset(cadena4, 0, num_car_cadena3 + 1); // Inicializamos la memoria a 0 

O al hacer la copia del valor del array incluyendo el nulo final con strncpy():

strncpy (cadena4, cadena3, num_car_cadena3 + 1); // copia de cadena3 incluido el fin de cadena 

Aunque en este caso esta claro que se incluye el nulo final en la copia, la función strncpy() no garantiza que la copia resultante finalice con '\0', es decir si el último caracter de la copia no es '\0' la cadena resultante no acabará en '\0'. Podemos asegurarnos añadiendo el nulo final:

*(cadena4 + num_car_cadena3) = '\0'; // nos aseguramos que cadena4 termina con fin de cadena 

En este caso no sumamos 1 a strlen() ya que los array comienzan desde 0.

En el estandar C11 se definieron unas funciones llamadas seguras entre las que se encuentra strncpy_s() que si añade el nulo final, pero estas fucniones no estan incluidas en gcc

También tenemos que tener en cuenta que cuando pasamos un array o estructura a una función, esta siempre se pasa como puntero.

Vamos a ver un ejemplo:

/* Paso de estructuras o arrays como parametros 
*/ 
#include <stdio.h> 
#include <stdlib.h> 
 
struct mis_datos_s { 
    char dat_char[32]; 
    short int dat_sint[20]; 
    int dat_int[10]; 
    long dat_long; 
    double dat_double[10];  
}; 
 
static void 
parametro_struct_arr (struct mis_datos_s dato_fun[]) // es como struct mis_datos_s *dato_fun 
{ 
    /*** ERROR: no podemos obtener el número de elementos de un puntero ***/  
    printf ("Numero de elementos en <dato_fun>: %zd\n",  
            sizeof (dato_fun) / sizeof (dato_fun[0]));  
} 
 
static void 
parametro_struct_pun (struct mis_datos_s *dato_fun, size_t elementos)  
{ 
    printf ("Para saber que <dato_fun> tiene %zd elementos\n", elementos);  
} 
 
int  
main (int argc, char *argv[])  
{ 
    struct mis_datos_s primer_dato = {}; 
    struct mis_datos_s varios_datos[10] = {}; 
     
    printf ("<mis_datos_s> tiene un tamaño de %zd bytes\n", sizeof (struct mis_datos_s)); 
    printf ("<primer_dato> tiene un tamaño de %zd bytes\n", sizeof (primer_dato));  
    printf ("<varios_datos[0]> tiene un tamaño de %zd bytes\n", sizeof (varios_datos[0]));  
    printf ("<varios_datos> tiene un tamaño de %zd bytes\n", sizeof (varios_datos));  
     
    size_t num_ele_varios_datos = sizeof (varios_datos) / sizeof (varios_datos[0]); 
    printf ("Numero de elementos en <varios_datos>: %zd\n", num_ele_varios_datos); 
             
    parametro_struct_arr (varios_datos); 
    parametro_struct_pun (varios_datos, num_ele_varios_datos); 
 
    exit (EXIT_SUCCESS); 
} 

Cuando pasamos un array a una función, siempre se pasa como un puntero. De esta forma tanto parametro_struct_arr como parametro_struct_pun, pasan un puntero al array de estructuras y por tanto no podemos usar sizeof para obtener el tamaño dentro de la función.

Para terminar voy a añadir otro caso diferente a la hora de calcular el tamaño y es en el uso de los caracteres multibyte. Para procesar los caracteres de un conjunto de caracteres grande, un programa puede representar cada carácter como un carácter ancho. Estos generalmente ocupan más espacio que un carácter ordinario. La mayoría de las implementaciones eligen 16 o 32 bits para representar un carácter ancho.

Para calcular el número de caracteres de una cadena ancha no podemos usar strlen(), pero contamos con una función similar wcslen().

Vamos a ver un ejemplo:

/* Uso de wcslen en caracteres anchos 
*/ 
#include <stdio.h> 
#include <stdlib.h> 
#include <wchar.h> 
 
int  
main (int argc, const char *argv[])  
{ 
    wchar_t wide_str1[] = L"0123456789"; 
    wchar_t *wide_str2 = NULL;  
    wchar_t *wide_str3 = L"0123456789"; 
     
    wide_str2 = (wchar_t *) malloc ( 
                (wcslen (wide_str1) + 1) * sizeof (wchar_t) ); 
 
    if (wide_str2 == NULL) // fallo al asignar la memoria  
        exit (EXIT_FAILURE);  
     
    wmemcpy (wide_str2, wide_str1, (wcslen (wide_str1) + 1)); 
 
    printf ("sizeof (wchar_t) = %zd \n", sizeof (wchar_t));  
    printf ("sizeof (char) = %zd \n", sizeof (char));  
    printf ("wcslen (wide_str1) + 1 = %zd \n", (wcslen (wide_str1) + 1)); 
    printf ("Tamaño wide_str1 = %zd \n", (wcslen (wide_str1) + 1) * sizeof (wchar_t)); 
    printf ("Tamaño wide_str2 = %zd \n", (wcslen (wide_str2) + 1) * sizeof (wchar_t)); 
    printf ("Tamaño wide_str3 = %zd \n", (wcslen (wide_str3) + 1) * sizeof (wchar_t)); 
 
    free (wide_str2); 
    wide_str2 = NULL; 
    exit (EXIT_SUCCESS); 
} 

Podemos ver como al calcular la memoria para copiar un array de caracteres anchos, al igual que en el el caso de strlen(), wcslen() no cuenta el caracter nulo final '\0' por lo que para reservar memoria tendremos que añadirle 1 a lo que nos devuelve wcslen() del array. Además tenemos que muntiplicar por sizeof (wchar_t) ya que como vemos wchar_t ocupa 4 bytes, no como char que como hemos dicho, por definición es siempre 1.

 $ ./sizewchar 
sizeof (wchar_t) = 4
sizeof (char) = 1
wcslen (wide_str1) + 1 = 11
Tamaño wide_str1 = 44
Tamaño wide_str2 = 44
Tamaño wide_str3 = 44
Modificado por última vez enViernes, 14 Agosto 2020 19:28
(2 votos)
Etiquetado como :

Deja un comentario

Asegúrese de introducir toda la información requerida, indicada por un asterisco (*). No se permite código HTML.