Regla 0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr)
Memoria, punteros y tipos (0x30XX)
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr)¶
Enunciado normativo¶
Toda reserva dinámica DEBE calcular su tamaño con el operador
sizeof, DEBE incluir la cantidad de elementos cuando corresponda, y DEBE preferir la formasizeof(*ptr)sobresizeof(tipo).
¿Por qué existe esta regla?¶
El problema¶
Un tamaño escrito a mano (4, 8, 100) es una suposición sobre la plataforma que
el compilador no puede verificar. sizeof obtiene el tamaño del tipo en la
unidad de traducción actual y se mantiene correcto aunque el tipo cambie.
La forma sizeof(*ptr) es superior a sizeof(tipo) porque ata el tamaño al
puntero que se está reservando. Si mañana ptr cambia de int * a long *, o
de nodo_t * a nodo_v2_t *, la reserva sigue siendo correcta sin editar la
expresión. Con sizeof(tipo) hay que recordar actualizar los dos lados.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Desbordamiento de heap | Un tamaño menor al necesario corrompe memoria al escribir. |
| Desperdicio | Un tamaño mayor reserva de más y oculta errores de cálculo. |
| Bug silencioso | El código funciona en una plataforma y falla al portarlo. |
| Mantenibilidad | Cambiar el tipo del puntero obliga a rastrear todos los sizeof. |
Fundamento en el estándar y en la cátedra¶
C11 §6.5.3.4 define sizeof como una expresión constante cuando el tipo es
completo. La cátedra adopta sizeof(*ptr) como forma idiomática porque combina
verificación de tipos y resistencia a refactorizaciones, y complementa a
0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado, que prohíbe específicamente usar el tamaño del puntero en lugar
del apuntado.
Alcance y excepciones¶
Aplica a malloc, calloc, realloc y a arreglos de longitud calculada.
No obliga a sizeof cuando el tamaño proviene de un cálculo semántico ya
expresado correctamente (por ejemplo strlen(s) + 1), aunque ese cálculo debe
ser correcto. calloc recibe la cantidad y el tamaño; la misma preferencia
aplica al segundo argumento.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Constante mágica en lugar de sizeof¶
int *valores = malloc(40);
if (valores == NULL)
{
return -1;
}
valores[0] = 1;Por qué falla: 40 asume sizeof(int) == 4, lo que no es universal. En una
plataforma con int de 2 bytes se reservan 20 enteros cuando el programa cree
tener diez; en otra con int de 8, sólo cinco. La cantidad de elementos y el
tipo deben expresarse, no adivinarse.
❌ Contraejemplo 2 — sizeof(tipo) desincronizado tras el cambio de tipo¶
long *valores = malloc(10 * sizeof(int));Por qué falla: la reserva usa sizeof(int) pero el puntero es long *. Si
long ocupa más que int, se reservan menos bytes de los que se escribirán:
desbordamiento de heap. La forma sizeof(*valores) habría seguido al tipo sin
intervención.
✅ Ejemplo conforme 1 — Forma idiomática sizeof(*ptr)¶
int *valores = malloc(10 * sizeof(*valores));
if (valores == NULL)
{
return -1;
}La cantidad (10) expresa la intención y sizeof(*valores) el tamaño unitario.
Si el tipo del puntero cambia, la reserva permanece correcta. La verificación de
0x3001h: Siempre verificá la asignación exitosa de memoria dinámica completa el patrón.
✅ Ejemplo conforme 2 — Cadena con terminador y struct dinámico¶
char *copia = malloc(strlen(origen) + 1);
if (copia == NULL)
{
return NULL;
}
strcpy(copia, origen);Se reserva strlen más el byte nulo. El + 1 es parte del cálculo semántico y
debe estar presente; omitirlo es 0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr). Para un struct con
arreglo flexible, sumá además n * sizeof(elemento), como advierte
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr).
⚠️ Casos límite¶
Desbordamiento del producto:
n * sizeof(*ptr)puede excedersize_t; validánantes de multiplicar.calloc: usacalloc(n, sizeof(*p)), que además inicializa en cero y detecta el desbordamiento internamente.Tipos incompletos:
sizeof(*ptr)es válido con punteros a tipos completos; en un tipo opaco no se puede aplicar (el cliente no lo conoce).
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | malloc/calloc/realloc con tamaño literal o sin sizeof. |
gcc / clang | gcc -Wall -Wextra -std=c11 ... | No detecta la constante mágica; -fanalyzer sí el desbordamiento. |
cppcheck | cppcheck --enable=all archivo.c | Allocation size mismatch entre tipo y sizeof. |
Checklist de autocontrol¶
¿Usé
sizeofen todas las reservas?¿Preferí
sizeof(*ptr)sobresizeof(tipo)?¿Incluí la cantidad de elementos?
¿Sumé el byte nulo en cadenas y el arreglo flexible en structs?
¿Validé el desbordamiento del producto?
Reglas relacionadas¶
0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado — prohibición específica de
sizeof(ptr)en lugar desizeof(*ptr).0x3001h: Siempre verificá la asignación exitosa de memoria dinámica — la reserva se verifica siempre contra
NULL.0x3016h: Orden incorrecto o sospechoso de argumentos en llamadas a memset — el error simétrico con los argumentos de
memset.0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos — un tamaño correcto no excusa validar los índices de acceso.
Antipatrón: Pointer decay en sizeof de arreglo parámetro¶
Síntoma en el código del estudiante¶
La función recibe un arreglo e intenta calcular su cantidad con sizeof:
void imprimir(int vec[])
{
size_t n = sizeof(vec) / sizeof(vec[0]);
for (size_t i = 0; i < n; i++) {
printf("%d\n", vec[i]);
}
}El estudiante cree que sizeof(vec) mide el arreglo, pero vec es un
parámetro y el compilador ya lo trató como puntero.
Diagnóstico¶
Mecanismo del defecto¶
En casi toda expresión, el nombre de un arreglo decae (decay) a un puntero
a su primer elemento (§6.3.2.1p3). Al declarar un parámetro int vec[], el
compilador lo ajusta a int *vec (§6.7.6.3p7). Por eso sizeof(vec) devuelve
sizeof(int *) —8 bytes en 64 bits— y no el tamaño del arreglo original.
El cociente sizeof(vec) / sizeof(vec[0]) da entonces 8 / 4 = 2, sin importar
cuántos elementos se hayan pasado. El lazo recorre solo 2 elementos y el resto
queda sin procesar. En otros tipos el cociente puede dar 1 o directamente
truncarse.
Consecuencia observable¶
Recorrido incompleto y silencioso del arreglo.
Si el cociente se calcula mal, puede accederse fuera de rango o no accederse nunca.
El bug no produce advertencia del compilador porque la expresión es sintácticamente válida.
Fundamento en el estándar C11¶
C11 §6.3.2.1p3: salvo en
sizeof,_Alignofo&, un arreglo se convierte en puntero a su primer elemento.C11 §6.7.6.3p7: los parámetros declarados como
tipo arreglo[]se ajustan atipo *.C11 §6.5.3.4p1:
sizeofde un puntero mide el puntero, no aquello a lo que apunta.La cátedra exige pasar la cantidad de elementos de forma explícita y usar
size_t(regla 0x3010h).
Corrección idiomática¶
❌ Código con el antipatrón¶
void imprimir(int vec[])
{
size_t n = sizeof(vec) / sizeof(vec[0]);
for (size_t i = 0; i < n; i++) {
printf("%d\n", vec[i]);
}
}✅ Código refactorizado — cantidad explícita¶
void imprimir(const int *vec, size_t n)
{
for (size_t i = 0; i < n; i++) {
printf("%d\n", vec[i]);
}
}✅ El sizeof que sí funciona — dentro del alcance del arreglo¶
int main(void)
{
int datos[5] = {1, 2, 3, 4, 5};
size_t n = sizeof(datos) / sizeof(datos[0]); /* 5, correcto */
imprimir(datos, n);
return 0;
}datos no decae aquí porque sizeof es una de las excepciones; el largo se
calcula en el mismo ámbito donde el arreglo es un arreglo.
⚠️ Casos límite¶
Con VLA (prohibidos por la regla 0x5001h) el
sizeofen el ámbito local sí conoce la dimensión, pero la cátedra no los admite.Un parámetro
int (*vec)[5](puntero a arreglo) sí conserva el tamaño; es una alternativa, aunque menos legible.El mismo error aparece con
char buf[]ystrlenmal usado.
Errores típicos al compilar o ejecutar¶
# Compila sin advertencia; el error es de lógica:
$ ./programa
1
2
# se imprimen solo 2 elementos de 5
$ gcc -Wall -Wextra -std=c11 programa.c
# sin diagnóstico: el decay es legalChecklist de verificación¶
¿Toda función que recibe un arreglo recibe además su cantidad (
size_t)?¿El
sizeofse calcula donde el arreglo sigue siendo arreglo?¿Usé
size_tpara índices y cantidades (regla 0x3010h)?¿Evité recalcular el largo con
sizeofsobre un parámetro?
Reglas relacionadas¶
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) — reglas de
sizeofen reserva dinámica.0x3010h: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t — usar
size_tpara tamaños e índices.0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado — otro mal uso de
sizeofsobre un puntero.0x3016h: Orden incorrecto o sospechoso de argumentos en llamadas a memset — misma raíz de decay.
Antipatrón: Reserva de buffer con malloc(strlen(s)) sin espacio para byte nulo¶
Síntoma en el código del estudiante¶
El buffer para copiar una cadena se reserva con el largo exacto que reporta
strlen:
char *dup = malloc(strlen(s));
strcpy(dup, s);El + 1 que debería acompañar a strlen no está.
Diagnóstico¶
Mecanismo del defecto¶
strlen cuenta los caracteres hasta, sin incluir, el byte nulo '\0'. Una
cadena "dato" tiene strlen igual a 4, pero ocupa 5 bytes: d, a, t,
o y '\0'. Al reservar strlen(s) bytes, falta lugar para el terminador.
strcpy copia los 4 caracteres y el '\0', es decir 5 bytes en un bloque
de 4. Es un off-by-one de escritura: se escribe un byte fuera del bloque
reservado. Ese byte pisa metadatos del asignador o el comienzo de otro bloque.
Además, la cadena resultante queda sin terminador si por casualidad el byte
siguiente no era cero.
Consecuencia observable¶
Corrupción de heap de a un byte, muy difícil de rastrear.
La cadena puede quedar sin terminar y provocar lecturas fuera de límites en
printf,strcmpo cualquier función de cadenas.heap-buffer-overflowbajo AddressSanitizer, señalado exactamente en elstrcpy.
Fundamento en el estándar C11¶
C11 §7.24.6.3p3:
strlendevuelve el número de caracteres que preceden al terminador.C11 §7.24.2.3p2:
strcpycopia la cadena incluido el carácter nulo final.C11 §7.22.3p1:
mallocreserva exactamente los bytes pedidos. No hay margen implícito.La regla 0x300Bh exige dimensionar la reserva con
sizeof; para cadenas, el patrón correcto es(strlen(s) + 1) * sizeof(*dup).
Corrección idiomática¶
❌ Código con el antipatrón¶
char *duplicar(const char *s)
{
char *dup = malloc(strlen(s));
if (dup == NULL) {
return NULL;
}
strcpy(dup, s);
return dup;
}✅ Código refactorizado¶
char *duplicar(const char *s)
{
char *dup = malloc((strlen(s) + 1) * sizeof(*dup));
if (dup == NULL) {
return NULL;
}
strcpy(dup, s);
return dup;
}El + 1 es para el terminador; sizeof(*dup) deja el cálculo preparado para
cualquier tipo de carácter.
✅ Alternativa robusta con memcpy¶
size_t largo = strlen(s) + 1;
char *dup = malloc(largo * sizeof(*dup));
if (dup == NULL) {
return NULL;
}
memcpy(dup, s, largo);⚠️ Casos límite¶
Si
sesNULL,strlen(s)es UB; hay que validar antes.Cadenas vacías:
strlen("") == 0, por lo que la reserva correcta es 1 byte.snprintfcon el tamaño correcto evita elstrcpy, pero no exime del+ 1en la reserva.En lectura con
fgets, el buffer debe contemplar terminador y eventual salto de línea.
Errores típicos al compilar o ejecutar¶
# Con AddressSanitizer:
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 1 at 0x... thread T0
#0 0x... in __interceptor_strcpy
#1 0x... in duplicar programa.c:7
# Con glibc:
$ ./programa
malloc(): corrupted top size
Aborted (core dumped)Checklist de verificación¶
¿Toda reserva de cadena suma
+ 1por el terminador?¿Usé
sizeof(*ptr)y no un número fijo?¿Validé que la cadena de origen no sea
NULL?¿La copia deja lugar para el
'\0'?
Reglas relacionadas¶
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) — dimensionar con
sizeof.0x5004h: Todas las operaciones con cadenas deben ser seguras — operaciones seguras con cadenas.
0x5006h: Preferí fgets sobre gets y scanf para leer cadenas — preferir
fgetssobregets/scanf.0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) — otro cálculo de tamaño incompleto.
Antipatrón: Cálculo erróneo de tamaño para struct dinámico con miembro flexible¶
Síntoma en el código del estudiante¶
La reserva de una estructura con arreglo flexible suma la cantidad de elementos sin multiplicar por el tamaño del elemento:
malloc(sizeof(struct vector) + 10);El 10 parece contar elementos, pero malloc cuenta bytes.
Diagnóstico¶
Mecanismo del defecto¶
Un miembro de arreglo flexible (FAM) se declara como último campo con corchetes vacíos:
struct vector {
size_t cantidad;
int datos[];
};sizeof(struct vector) incluye todo menos el arreglo flexible. Para n
elementos hay que sumar n * sizeof(elemento). Si el elemento mide más de un
byte —int mide 4, un puntero mide 8—, sizeof(struct) + n reserva de menos:
+ 10 suma 10 bytes en lugar de 40.
El resultado es un desbordamiento de heap al escribir el elemento 3 o 4 (a partir de los 10 bytes). Se pisan los metadatos del asignador o el bloque siguiente, con el desenlace habitual: corrupción y aborto tardío.
Consecuencia observable¶
Los primeros elementos entran; a partir del byte 10, escritura fuera de rango.
malloc(): corrupted top sizeal liberar o al reservar de nuevo.AddressSanitizer reporta
heap-buffer-overflowen la escritura del elemento.
Fundamento en el estándar C11¶
C11 §6.7.2.1p18: el miembro flexible se ignora en
sizeofde la estructura y debe ser el último.C11 §7.22.3p1:
mallocreserva bytes; el cálculo del tamaño es responsabilidad del programador.La regla 0x300Bh exige expresar el tamaño con
sizeof, no con números sueltos. La expresión idiomática essizeof(*p) + n * sizeof(p->datos[0]).
Corrección idiomática¶
❌ Código con el antipatrón¶
struct vector {
size_t cantidad;
int datos[];
};
struct vector *crear(size_t n)
{
struct vector *v = malloc(sizeof(struct vector) + n);
if (v == NULL) {
return NULL;
}
v->cantidad = n;
for (size_t i = 0; i < n; i++) {
v->datos[i] = 0; /* escribe fuera de rango */
}
return v;
}✅ Código refactorizado¶
struct vector *crear(size_t n)
{
struct vector *v = malloc(sizeof(*v) + n * sizeof(v->datos[0]));
if (v == NULL) {
return NULL;
}
v->cantidad = n;
for (size_t i = 0; i < n; i++) {
v->datos[i] = 0;
}
return v;
}El tamaño queda expresado en función de sizeof, así que sigue siendo correcto
si el tipo de datos cambia.
⚠️ Casos límite¶
Si el elemento fuera
char,sizeof(elemento) == 1y la fórmula errónea coincidiría por casualidad; no hay que confiar en esa coincidencia.n == 0requiere solosizeof(struct), peromallocde un tamaño mínimo puede devolverNULLsin ser error.Conviene usar
sizeof(v->datos[0])en lugar desizeof(int)para sobrevivir a refactorizaciones.
Errores típicos al compilar o ejecutar¶
# Con AddressSanitizer:
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 4 at 0x... thread T0
#0 0x... in crear programa.c:15
# Con glibc:
$ ./programa
malloc(): corrupted top size
Aborted (core dumped)Checklist de verificación¶
¿La reserva del FAM multiplica
nporsizeof(elemento)?¿Usé
sizeof(*v)para la parte fija de la estructura?¿El miembro flexible es el último campo?
¿El cálculo sigue siendo correcto si cambio el tipo del elemento?
Reglas relacionadas¶
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) — dimensionar con
sizeof.0x3001h: Siempre verificá la asignación exitosa de memoria dinámica — verificar el retorno de
malloc.0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos — validar límites antes de acceder a elementos.
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) — otro tamaño incompleto por un elemento.