malloc responde una sola pregunta: dame esta cantidad de bytes. Dos compañeras en <stdlib.h> responden las preguntas que vienen después: dame esta cantidad de bytes, limpios (calloc) y necesito que el bloque que ya tengo sea más grande (realloc).
calloc: cantidad, tamaño y ceros
void *calloc(size_t count, size_t size);
Dos argumentos en lugar de uno, y el resultado queda lleno de bytes en cero.
Ese es el caso clásico: un histograma o un array de conteos donde cada casilla debe empezar en cero. Con malloc necesitarías un bucle; calloc lo hace como parte de la reserva, muchas veces sin costo porque el sistema operativo ya entrega páginas nuevas previamente puestas en cero.
El argumento del desbordamiento
La división en dos argumentos no es cosmética. Piensa en una cantidad leída de un archivo:
size_t n = huge_value_from_input;
int *a = malloc(n * sizeof *a); /* el producto puede dar la vuelta */
int *b = calloc(n, sizeof *b); /* obligada a detectar el desbordamiento */
Si n * sizeof *a supera lo que size_t puede contener, da la vuelta a un número pequeño, malloc tiene éxito con un bloque diminuto y cada escritura posterior se va muy por delante del final. El estándar obliga a calloc a fallar y devolver NULL en su lugar. Cuando un tamaño viene de fuera de tu programa, esa comprobación vale la pena.
Qué significa realmente "en cero"
calloc escribe bytes en cero. Para enteros y caracteres eso es el valor cero, que es lo que quieres. Para punteros y valores de punto flotante, todos los bits en cero equivalen a un puntero nulo y a 0.0 en toda plataforma habitual, pero el estándar de C no lo promete. El código que debe ser estrictamente portable los asigna de forma explícita.
calloc frente a malloc + memset
int *a = calloc(n, sizeof *a);
int *b = malloc(n * sizeof *b);
if (b != NULL) {
memset(b, 0, n * sizeof *b);
}
Producen el mismo resultado. Prefiere calloc: es una sola línea, hace la comprobación de desbordamiento y, para bloques grandes, puede evitar tocar la memoria por completo. Prefiere malloc a secas cuando de todas formas vas a sobrescribir cada byte: poner en cero un megabyte que vas a llenar de inmediato es puro desperdicio.
realloc: cambiar el tamaño
void *realloc(void *p, size_t newsize);
realloc devuelve un bloque de newsize bytes cuyo contenido coincide con el bloque antiguo hasta el menor de los dos tamaños. Puede extender el bloque donde está, o reservar uno nuevo, copiar y liberar el viejo. No puedes saber cuál de las dos cosas hizo, así que el único puntero que puedes usar después es el que devolvió.
El modismo del puntero tmp
Las tres líneas alrededor de tmp son toda la razón por la que esta función tiene mala fama. El atajo tentador está roto:
/* BUG: fuga el bloque original cuando realloc falla */
p = realloc(p, newsize);
if (p == NULL) {
return;
}
Cuando realloc falla devuelve NULL y deja el bloque original reservado y sin cambios. Asignar el resultado directamente a p sobrescribe el único puntero a ese bloque, así que nunca podrá liberarse: una fuga, y una que ocurre justo cuando la memoria ya escasea. Asigna a tmp, comprueba tmp y luego confirma:
void *tmp = realloc(p, newsize);
if (tmp == NULL) {
/* p sigue siendo válido; maneja el fallo como convenga a quien llama */
return 0;
}
p = tmp;
Dos comportamientos más que conviene conocer: realloc(NULL, n) se comporta exactamente como malloc(n), lo que permite que una función de crecimiento maneje su primera llamada sin un caso especial. Y al agrandar, los bytes nuevos quedan sin inicializar: solo se conserva el contenido antiguo.
Hacer crecer un array: el patrón estándar
Este es el patrón que usa casi todo contenedor en C: una longitud, una capacidad y duplicar cuando se encuentran.
Dos decisiones de diseño cargan con todo el peso aquí.
Duplicar, no sumar uno. Crecer en una cantidad fija hace que cada inserción copie el array entero, así que llenar n elementos cuesta aproximadamente n² copias de bytes. Duplicar hace que las copias sean lo bastante raras como para que cada inserción cueste tiempo constante en promedio. El arranque cap == 0 ? 4 : cap * 2 maneja la primera inserción sin una rama aparte, porque realloc(NULL, n) es simplemente malloc.
El struct es dueño de una sola reserva. v.data se libera exactamente una vez, por quien sea dueño del struct. Dejar esa regla de propiedad por escrito (en un comentario, o emparejando cada vec_init con un vec_free) es lo que evita que un contenedor que crece tenga fugas mientras pasa entre funciones.
Encoger
realloc con un tamaño menor recorta un bloque, lo cual resulta útil después de haber leído una cantidad desconocida de datos en un búfer sobredimensionado:
Un encogimiento fallido no es un error por el que valga la pena abortar: el bloque antiguo, más grande, sigue siendo válido y sigue siendo correcto. Este es el único caso donde ignorar el fallo es la decisión correcta, pero fíjate en que igual pasa por tmp, porque la regla de no sobrescribir el puntero vivo no cambia.
Cuál elegir
| Lo que quieres | Usa |
|---|---|
| Bytes, sin importar el contenido | malloc(n * sizeof *p) |
| Una cantidad de elementos, puestos en cero | calloc(n, sizeof *p) |
| Una cantidad que viene de una fuente no confiable | calloc, por la comprobación de desbordamiento |
| El bloque que ya tienes, más grande o más chico | realloc a través de un puntero tmp |
| La primera reserva en un bucle de crecimiento | realloc(NULL, n): sin caso especial |
Las cuatro se liberan con el mismo free, exactamente una vez.
Errores comunes
p = realloc(p, n): fuga cuando falla. Pasa siempre portmp.- Guardar un puntero antiguo al bloque. Después de
realloclos datos pueden haberse movido, así que todo puntero interior guardado o dirección derivada de un índice queda obsoleto. Los índices sobreviven a una mudanza; los punteros no. - Suponer que los bytes nuevos están en cero. Solo
callocpone ceros.reallocdeja la zona de crecimiento sin inicializar. calloc(n * size, 1): eso tira a la basura la comprobación de desbordamiento para la que existe la forma de dos argumentos.- Liberar el puntero antiguo tras un
reallocexitoso.reallocya lo liberó; un segundofreees una doble liberación.
Preguntas frecuentes
¿Cuál es la diferencia entre calloc y malloc en C?
Dos diferencias. calloc(n, size) recibe la cantidad y el tamaño de cada elemento por separado y comprueba que multiplicarlos no desborde, mientras que malloc(n * size) puede dar la vuelta en silencio. Y calloc rellena el bloque con ceros, mientras que malloc lo deja con lo que hubiera antes.
¿Cómo funciona realloc en C?
realloc(p, newsize) devuelve un bloque del nuevo tamaño con tu contenido actual conservado hasta el menor de los dos tamaños. Puede agrandar el bloque en su sitio o moverlo, así que el puntero devuelto es el único que puedes seguir usando: el antiguo quizá ya fue liberado.
¿Por qué hay que asignar realloc a un puntero temporal?
Porque p = realloc(p, n) sobrescribe p con NULL cuando la llamada falla, y el bloque original sigue reservado sin que nada apunte a él: una fuga. Asigna primero a un tmp, comprueba si es NULL y solo entonces escribe p = tmp.
¿Cuándo debo usar calloc en lugar de malloc?
Cuando realmente necesitas la memoria en cero (un array de contadores, un struct cuyos campos deben empezar vacíos, un búfer que llenarás parcialmente) o cuando el tamaño es una cantidad por un tamaño de elemento que podría ser grande. Si de todos modos vas a sobrescribir cada byte, malloc te ahorra el trabajo de poner ceros.