¿Qué hay de malo con este código C?

Leamos su código línea por línea:

Línea 3: le pide al sistema operativo que asigne 5 bytes de memoria y almacene el puntero al primer byte en la variable a .

Línea 4: intenta leer la memoria 5 bytes después del byte apuntado a la dirección contenida en a variable. ¡Pero debes recordar cómo funcionan los índices en C! Cuando accede a la tab[0] , donde la pestaña representa una matriz (o un puntero, realmente no nos importa porque en C se pueden rastrear de la misma manera), en realidad está accediendo al primer elemento de la matriz correspondiente. Al preguntar por el valor de a[5] , en realidad está preguntando por el valor del sexto elemento de su matriz declarada dinámicamente. Pero en realidad, no reservó espacio para este elemento, porque en la línea 3 asignó 5 bytes, o una matriz de 5 elementos, no 6. La lectura / escritura fuera de la memoria que asignó tiene un comportamiento indefinido, lo que significa que básicamente todo puede suceder: nada o una falla de segmentación. Copié / pegué el código (agregué los encabezados correspondientes para que funcionara) y para mí funcionó como se esperaba, pero aún así había escrito en un punto de memoria no asignado.

Eres verdadero cuando dices que debes free algo que malloc . En realidad, no permanecerá en su computadora hasta que reinicie, porque los sistemas operativos modernos pueden rastrear toda la memoria utilizada por un programa determinado y liberar cada espacio utilizado por un programa después de que se detiene. Sin embargo, cada asignación no liberada es memoria desperdiciada para la ejecución actual del programa, por lo que sigue siendo muy importante liberar la memoria asignada dinámicamente justo cuando ya no la necesite.

Ahora, hablemos de lo que quieres lograr. Por lo que entendí, estás tratando de:

  • Asignar memoria dinámicamente
  • Escribir en esa memoria asignada dinámicamente
  • Leyendo una cuerda

Acabamos de hablar sobre la asignación dinámica de memoria y la lectura / escritura en memoria, y creo que todo debería estar claro en este momento. Para resumir, debe asignar memoria tanto como la necesite, y debe asegurarse, como programador, de nunca tener que leer fuera de ese espacio dado.

Ahora, intenta imprimir esa cadena, con printf . Como habrás notado, cuando imprimes la cadena, nunca escribes en ningún lado cuánto dura la cadena para imprimir. La función printf no sabe cuánto dura tu matriz de caracteres, incluso si declaraste tu variable como char a[10]; (en este nivel, no puede tener un nivel de introspección tan grande como para que la variable sea una cadena de longitud 9 (sí, ¡dije 9! sigue leyendo)). Es por eso que debe poner un \0 al final de la cadena, ya que como no tenemos la longitud de la cadena, la función printf aún debe saber cuándo dejar de leer el siguiente elemento de la matriz (como un ¡array es una especie de puntero y, por lo tanto, no tiene una longitud!). Esta restricción de ese carácter nulo final implica dos cosas:

  • Nunca olvide agregar el carácter nulo; de lo contrario, printf u otras funciones basadas en cadenas nunca dejarán de repetir, siempre leyendo e imprimiendo el siguiente elemento de la matriz a menos que intente leer fuera de la memoria reservada del programa (¡lo que provocará un bloqueo! )
  • Cuando asigne un número fijo de bytes para una cadena, no olvide asignar un byte adicional para ese carácter nulo. Por ejemplo, si desea tener una cadena de 5 caracteres imprimibles (para almacenar la cadena de hello , 5 caracteres imprimibles), tendrá que asignar 6 bytes, debido a ese carácter nulo final.

En el futuro, cuando tenga algunos programas en C y no sepa por qué se bloquea (o al menos, no sabe en qué parte del código se bloquea), use Valgrind. Le indica en qué función se produjo un error de lectura / escritura de memoria y tiene muchas otras herramientas de seguimiento de errores. Incluso no necesita un tutorial completo para saber cómo usarlo, simplemente ejecute valgrind y le dará todo lo que necesita en la salida.

Algunos enlaces sobre lo que hablamos aquí:

¿Qué pasa REALMENTE cuando no liberas después de Malloc?

¿Qué tan peligroso es acceder a una matriz fuera de los límites?

Valgrind

Oh, mi querido compañero Quoran, estás viendo al hombre del saco de la programación en C: el comportamiento indefinido.

Verá, su código (supongo que eso es todo lo que escribió), realmente se ejecuta en mi computadora sin ningún error. Pero eso es lo que ocurre con el comportamiento indefinido: puede producir cualquier resultado , incluso uno exitoso. Incluso puede funcionar a veces y bloquearse en otros. La conclusión es que el comportamiento indefinido es algo que desea evitar a toda costa (consulte la lectura recomendada a continuación).

Repasemos su código línea por línea y le mostraré dónde se encuentra el hombre del saco:

char * a = malloc (5);

Aquí esencialmente estás haciendo dos cosas:

  1. Asignación de memoria de forma dinámica con malloc: dado que pasa 5 como parámetro, asigna 5 bytes de memoria.
  2. Está declarando un puntero de caracteres llamado a, y lo está haciendo apuntar a la memoria asignada recientemente.

a [5] = ‘\ 0’;

¡Oh, oh, alerta de comportamiento indefinido! ¿Recuerdas que asignaste 5 bytes de memoria? Bueno, amigo mío, debes recordar que las computadoras siempre cuentan desde 0. Eso significa que cuando escribes un [5] , en realidad te estás refiriendo al sexto elemento de la matriz. En otras palabras, está jugando con la memoria que no asignó. ¡Es un comportamiento clásico indefinido! No quieres hacer eso, nunca. Sin embargo, eso es fácil de solucionar: asigne un byte adicional para ese carácter nulo. Haz malloc (6) en la línea anterior y estarás bien. =)

size_t i = 0;
mientras que (i <5) a [i ++] = '';
printf (“‘% s’ \ n”, a);

¡Estás usando el operador postincremento (i ++) aquí muy bien! El efecto de esta porción de código es establecer su conjunto de caracteres (a [0] a a [4]) en el carácter de espacio en blanco (”). Luego, imprimirá 5 caracteres de espacio en blanco, seguidos de una nueva línea. Todo esto es correcto.

libre (a);

Esto también es correcto. Debe pasar un puntero a la función libre, que apunta a un fragmento de memoria previamente asignado, y su variable se ajusta a los criterios. =)

Espero que esta respuesta te ayude de alguna manera, y que evites comportamientos indefinidos tanto como puedas. Finalmente, me gustaría agregar un comentario a esta declaración tuya:

o ocuparía memoria en su computadora hasta que la reinicie.

Una vez que el programa termine de ejecutarse, el sistema operativo liberará toda la memoria del programa (incluso la parte que no asignó sin cargo). Afortunadamente, los sistemas operativos son más inteligentes de lo que piensas. Dicho esto, los programadores no debemos descuidarnos: ¡liberemos siempre la memoria que asignamos! Para programas pequeños, la pérdida de memoria no es un problema, pero cuando se trata de programas que son enormes y muy importantes, las pérdidas de memoria pueden ser una verdadera pesadilla.

EDITAR (9 de febrero) : como señaló mi muy inteligente amigo Thiago Martins, la línea a continuación también podría ser problemática:

printf (“‘% s’ \ n”, a);

Aquí está utilizando el especificador de formato de cadena (% s), que es bueno. Pero recuerde que las cadenas C tienen terminación nula. Teniendo en cuenta que su carácter NULL está situado en la memoria que no asignó (a [5]), este fragmento de código terminará leyendo la memoria no asignada (comportamiento indefinido).

¡Chico, rastrear el comportamiento indefinido puede ser una tarea difícil! Pero al final, supongo que es una de las alegrías de ser un programador en C. =)

——————————

Lectura recomendada:

Una guía para el comportamiento indefinido en C y C ++, Parte 1

C Asignación de memoria con malloc (), calloc (), free () y realloc ()

Almacenamiento para cadenas en C – GeeksforGeeks

En la línea 4, asigna cinco bytes de memoria y asigna el puntero a un,
que se declara como un puntero a char.

Si considera estos cinco bytes como una matriz de caracteres,
entonces los elementos de esta matriz son a [0], a [1, a [2], a [3] y a [4].

Como el tamaño de la matriz es solo cinco, no existe tal cosa como:

  • “A [5]”

Tampoco hay ninguna cosa tal como un [7] o una [123454679] ni [-37] !!!
El tamaño de la matriz es 5 y no hay elementos de matriz válidos.
con subíndices que no sean 0, 1, 2, 3, 4, 5.

En la línea 4, intenta asignar un valor a un [5], que no existe.
A partir de este punto (de acuerdo con el estándar del idioma),
el comportamiento es indefinido o “depende del procesador”
(lo que significa que no puedes predecir lo que hace).


En realidad, la mayoría de los procesadores asignarán cantidades impares de memoria
comenzando en límites convenientes, que generalmente son poderes de dos.

Teóricamente, es posible que esta indexación excesiva de la matriz
resultará en golpear lo que sea que se encuentre en el próximo byte,
como algunos datos no relacionados o tal vez una instrucción o tal vez una
instrucción que, cuando su código de operación se aprieta, permitirá al hacker
quién hizo esto para tomar el control de su sistema operativo, unidades de disco,
conexiones de red, etc., etc.) Por lo tanto, esta sobre indexación de la matriz
Es probablemente inofensivo.

El resto de su código llena los elementos (válidos) de la matriz con espacios en blanco e intenta imprimir una cadena (terminada en nulo) que comienza en a . Obviamente, el codificador asumió que habría un valor NULL (‘\ 0’ ‘) en el byte de memoria (no asignado) después de los cinco bytes solicitados en la línea 3, pero (nuevamente, de acuerdo con el estándar), no hay garantía de que el el byte que sigue a la matriz de cinco bytes (es decir, el byte que reside en la dirección ilegal “a [5]”) se ha asignado a NULL

La línea 7 intenta imprimir la cadena (terminada en nulo).

Después de liberar la memoria asignada (bloque de cinco bytes de) en la línea 9,
no está claro qué pasará con el SEXTO byte, ” a [5] “,
¡pero es muy probable que este byte de memoria se libere junto con los otros cinco!

En la mayoría de los procesadores, esto funcionará como se esperaba (solo imprime cinco espacios en blanco),
pero de nuevo no hay “garantía” ya que este programa no cumple
a la norma Sin embargo, el comportamiento es impredecible y es posible.
que algunos procesadores harán esto inesperadamente, como:

  • colgar mientras intenta imprimir una cadena que nunca termina.
  • saliendo de este programa pero dejando un byte no asignado en la memoria que fue asignado a ‘\’. (Es posible que este byte liberado sea asignado por otro programa, posiblemente afectando su comportamiento).
  • lanzando una excepción en tiempo de ejecución.
  • ejecución continua pero haciendo otra cosa, porque el comportamiento del procesador no está definido cuando se viola el estándar (por ejemplo, al indexar en exceso una matriz).

  1. int main ()
  2. {
  3. char * a = malloc (5); // ASIGNAR CINCO BYTES DE MEMORIA
  4. a [5] = ‘\ 0’; // ¡ESTA DECLARACIÓN ES ILEGAL! No hay un [5]
  5. size_t i = 0;
  6. mientras que (i <5) a [i ++] = '';
  7. printf (“‘% s’ \ n”, a);
  8. libre (a); // GRATIS CINCO BYTES DE MEMORIA ASIGNADA
  9. devuelve 0;
  10. }

La declaración:

a [5] = ‘\ 0’;

se almacena más allá del final de la matriz que asignó en la declaración anterior.

Está asignando un bloque de exactamente 5 bytes (5 caracteres, en este caso). Recuerde que las matrices (incluso las matrices asignadas dinámicamente como esta) tienen una indexación basada en cero, por lo que los únicos índices de matriz de valores en este caso son 0, 1, 2, 3 y 4. a [5} es un carácter más allá del final del bloque asignado. No tienes ese byte. Usted solo posee los cinco bytes que asignó, un [0] a un [4].

Esto lleva a un comportamiento indefinido. El comportamiento real del programa variará, dependiendo del compilador específico, la implementación del montón, los cambios del compilador, etc. Si bien el comportamiento variará, una posibilidad es la corrupción del montón, porque está escribiendo un byte en un área del montón que no usa ‘pueblo. Cuando eso sucede, el problema puede no manifestarse hasta que llame gratis, llame a otro malloc para obtener otro bloque, etc.

Para solucionar esto, establezca un [4] en su carácter de terminación nula y cambie su condición while a i <4:

int main ()
{
char * a = malloc (5);
a [4] = ‘\ 0’; // último byte que poseemos en el bloque
size_t i = 0;
mientras que (i <4) a [i ++] = ''; // rellena 0, 1, 2, 3 con espacios en blanco
printf (“‘% s’ \ n”, a);
libre (a);
devuelve 0;
}

Ahora, con respecto a su mención de la llamada gratuita:

  1. Sí, cada vez que asigne un bloque, debe liberarlo de nuevo al montón cuando haya terminado con él, o podría terminar con una pérdida de memoria.
  2. En un programa como el que ha presentado aquí, el programa finalizará después de que llame de todos modos, por lo que todo se limpiará. Sin embargo, es una buena práctica liberar siempre lo que sea que malloc.

Definitivamente tiene que liberar un puntero que apunte a la memoria asignada en el montón (en este caso, asignada usando malloc). ¡Pero esto rara vez es una razón para que un programa se bloquee!

El problema principal es de naturaleza diferente pero relacionada. Primero, asigna 5 bytes en el montón:

índice: 0 1 2 3 4
byte: [b0 | b1 | b2 | b3 | b4]

Entonces hay 5 bytes, ¡pero en C comienzas a contar en 0! Esto se debe a que la variable de puntero ‘a’ realmente contiene la dirección de memoria del primer byte asignado. Puede hacer aritmética con esto: a + 1 será la dirección del próximo byte; es lo mismo que un [1]. Del mismo modo, a [0] es la dirección a + 0 = a.

Ahora en su código, hay 5 bytes. El primero tiene índice 0, el último tiene índice … 4. ¡NO 5! Pero en su código intenta escribir en un [5], que escribirá en algún lugar en la memoria que no ha asignado y eso irritará el sistema operativo o la MMU, haciendo que su programa se bloquee.

Por lo general, un programa que se ejecuta en un sistema operativo moderno nunca tendrá acceso (incluso solo lectura) a la memoria que no se ha asignado a sí mismo; el sistema operativo siempre intervendrá.

En su ciclo while, sobrescribe el 4to (último) byte con un espacio. Tienes que arreglar eso también.

El resto del código está bien, aunque un método principal sin argumentos a veces está mal visto. Me parece aceptable para tal programa. Sin embargo, ¡podrías intentar tomar argumentos y escribirlos en a!

Todos los demás ya han señalado el error de desbordamiento del búfer que golpeó el encabezado del siguiente bloque de memoria encadenado en el montón.

He tenido que perseguir tales errores muchas veces en sistemas embebidos con herramientas mínimas. El mal uso del puntero es una fuente importante de error en C, que los lenguajes más modernos se esfuerzan por evitar. La experiencia y el entrenamiento reducen en gran medida sus posibilidades de equivocarse con los punteros, pero nunca a cero.

Si está utilizando Linux con glibc 2.2 o posterior, tiene algunas herramientas de depuración que pueden ayudarlo. La función mcheck_check_all () realiza una comprobación completa de la coherencia de la memoria. Si eres nuevo en la programación de Linux o C, esta no es una herramienta fácil de usar. Para ser efectivo, generalmente requiere cargar paquetes de información de depuración y usar gdb para leer los volcados del núcleo. Este no es el lugar para un tutorial, pero puede “man mcheck” en Linux para comenzar. Incluyo un programa de muestra aquí para detectar su clase de error de desbordamiento.

Esto solo funciona para desbordamientos que corrompen las áreas de almacenamiento de memoria. Ese es un modo de error, pero otros son igualmente probables. Por ejemplo, si asigna una matriz de estructuras (que requiere una conversión del puntero malloc devuelto), la indexación excesiva en 1 puede bloquear algo cientos de bytes en el siguiente fragmento malloc, lo que puede causar errores impredecibles que no se manifiestan hasta mucho después de la invasión. Las funciones mcheck () no pueden detectar esto, ni un compilador o enlazador.

Moral: C fue diseñado para permitir el acceso ilimitado del programador a los punteros. Si fuera fácil prevenir o diagnosticar errores de puntero en C, no habría tanto escándalo por evitar punteros sin restricciones en los lenguajes modernos.

#include
#include
#include

En t
main (int argc, char * argv [])
{
char * p;

if (mcheck (NULL)! = 0) {
fprintf (stderr, “mcheck () falló \ n”);
salir (EXIT_FAILURE);
}

p = malloc (1000);
p [1000] = ‘x’; // ¡Desbordamiento de búfer!

mcheck_check_all ();

salir (EXIT_SUCCESS);
}

Todos los demás te dicen por qué este código falla: estás escribiendo más allá del final de la memoria. Alguien seguramente habrá dicho que la memoria que no liberas se libera para ti al final de la vida útil del proceso (en máquinas razonables), y no solo cuando reinicias, también. Pero cada vez que su software falla, no puede esperar correr a Quora.

Entonces responderé la pregunta que deberías haber hecho:

¿Cómo encontraría el error en este programa?

¡Bien, me alegra que lo hayas preguntado! Hay una serie de herramientas que puede usar, además de una técnica genérica. Comencemos con cómo depurar:

  1. Caracteriza el problema. (Muy simple, aquí, ya que este es un pequeño programa).
  2. Adivine, una hipótesis, cuál podría ser el problema.
  3. Haz una predicción.
  4. Prueba la predicción.
  5. Arreglar el problema.

Eso es más o menos eso. Es posible que necesite pasar de (4) a (2) varias veces, y su predicción puede involucrar una solución candidata, por lo que (5) podría terminar entre (2) y (3) en su lugar.

Entonces, ¿cómo caracterizamos el problema? Básicamente, eso es encontrar más información sobre cómo y cuándo se bloquea. Aquí es donde entran las herramientas. Repasemos las opciones básicas para los programadores de C / C ++:

Depuradores

Un depurador clásico, como gdb, detectará el bloqueo en el momento en que ocurre y le dará información, y le permitirá examinar las variables del programa y otros estados, para tratar de adivinar por qué. En este caso, los resultados pueden ser sorprendentes: podría bloquearse en la línea 4, que, como sabemos, es la línea realmente defectuosa. Pero podría bloquearse en la lectura en la línea 7, o tal vez en la línea 8, dependiendo de lo que esté roto.

Pero cuando lo compilé y lo ejecuté, no se bloqueó en absoluto … Más sobre por qué más tarde.

Análisis estático

Esto toma su código y, generalmente en lugar de compilarlo, lo examina con gran detalle para ver si puede encontrar errores lógicos y otros errores.

Tiendo a usar el analizador estático CLang localmente, pero para proyectos de código abierto también uso Coverity donde puedo. Podría encontrar el error en la línea 4 … Pero cuando intenté CLang no lo hizo.

Valgrind

Hay, según tengo entendido, otros verificadores de memoria de tiempo de ejecución, pero, sinceramente, cualquiera que no use valgrind en la programación C o C ++ necesita su examen de la cabeza. Lo que hace valgrind es crear una CPU sintética, con memoria sintética, y luego ejecutar su programa, sin modificaciones, en el simulador resultante.

Esto le permite observar el comportamiento con mucha precisión, para que pueda ver el momento en que algo sale mal, incluso si no se produce un bloqueo. De valgrind, obtengo esto:

> valgrind ./quora
== 7538 == Memcheck, un detector de errores de memoria
== 7538 == Copyright (C) 2002-2015, y GNU GPL’d, por Julian Seward et al.
== 7538 == Usando Valgrind-3.11.0 y LibVEX; vuelva a ejecutar con -h para obtener información de copyright
== 7538 == Comando: ./quora
== 7538 ==
== 7538 == Escritura inválida de tamaño 1
== 7538 == en 0x4005D4: principal (tmp.c: 7)
== 7538 == La dirección 0x5203045 es 0 bytes después de un bloque de tamaño 5 asignado
== 7538 == en 0x4C2DB8F: malloc (en /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
== 7538 == por 0x4005C7: principal (tmp.c: 6)
== 7538 ==
== 7538 == Lectura inválida del tamaño 1
== 7538 == en 0x4E88CC0: vfprintf (vfprintf.c: 1632)
== 7538 == por 0x4E8F898: printf (printf.c: 33)
== 7538 == por 0x400613: principal (tmp.c: 10)
== 7538 == La dirección 0x5203045 es 0 bytes después de un bloque de tamaño 5 asignado
== 7538 == en 0x4C2DB8F: malloc (en /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
== 7538 == por 0x4005C7: principal (tmp.c: 6)
== 7538 ==

== 7538 ==
== 7538 == RESUMEN DEL HEAP:
== 7538 == en uso en la salida: 0 bytes en 0 bloques
== 7538 == uso total de almacenamiento dinámico: 2 asignaciones, 2 liberaciones, 1.029 bytes asignados
== 7538 ==
== 7538 == Todos los bloques del montón fueron liberados – no hay fugas posibles
== 7538 ==
== 7538 == Para recuentos de errores detectados y suprimidos, vuelva a ejecutar con: -v
== 7538 == RESUMEN DE ERRORES: 2 errores de 2 contextos (suprimido: 0 de 0)

Ahora, he tenido que agregar tres líneas en la parte superior para encabezados y otras cosas, para que tmp.c: 7 sea su línea a [5] = ‘\ 0’. Un claro error de escritura en la memoria, justo después del final. Tres líneas más tarde, dentro de printf, hay un claro error de lectura que coincide, ya que el byte NUL se vuelve a leer, pero no causa un bloqueo porque aún termina bien.

Sorprendentemente, la llamada free () no genera una mala lectura, porque el bloque de 5 bytes se redondea, probablemente a 8, lo que significa que no es * realmente * un problema aquí. (Por eso sospecho que esto es realmente un problema de tarea, por cierto).

Esto también significa que libera con éxito la memoria, por lo que valgrind puede informar que no tiene fugas. Valgrind puede hacer otras comprobaciones, como subprocesos y todo tipo, pero la instalación de memcheck es simplemente increíble, como puede ver.

¿Ahora que?

Entonces, ahora que sabemos exactamente lo que está sucediendo, podemos continuar: casi cada mala escritura es un punto de colgar (claramente no es el caso) o un desbordamiento. Si es un desbordamiento (y valgrind nos ha dicho que lo es), entonces el aumento del tamaño del búfer debería solucionarlo. Entonces, aumentamos el malloc en uno, y predecimos que valgrind permanecerá en silencio.

Todos estos bits son bastante triviales en este caso, en gran parte porque valgrind ha hecho todo el trabajo por nosotros y el caso fue muy trivial; sin embargo, normalmente, hay un poco más de ida y vuelta, tratando de ver cuál es la causa raíz.

Sin embargo, en este caso, podemos probarlo y ver cómo la predicción se hace realidad de inmediato.

Error solucionado, toma un descanso para el té.

bueno, primero comentemos esto:

char * a = malloc (5);

Creo que muchos compiladores lanzarán advertencias sobre el lanzamiento, esta ortografía es más agradable:

char * a = (char *) malloc (5);

Ahora “a” apunta a un segmento de memoria contigua de 5 bytes que malloc le asignó.

C le permite acceder a los datos del puntero como una matriz, pero no tiene forma de conocer el tamaño de los datos. Sabe que el tipo almacena en la dirección de “a” es un char, sizeof (char) == 1.

Por lo tanto, a [0] son ​​los datos de tipo char que comienzan en el primer byte de a, y tienen un tamaño de un byte. El tipo de un [0] es un char.

La forma en que calcula el desplazamiento en su memoria en la que se encuentra su char es básicamente: (dirección de la variable) + sizeof (tipo de puntero de a) * índice;

a [5] = ‘\ 0’;

Calculemos dónde está el carácter en el índice 5 de su memoria:

(dirección de a) + sizeof (char) * 5

Si imaginamos que ‘a’ apunta a la dirección 0, esto significa que está buscando asignar el char (byte) a la dirección 0 + 1 * 5 = 5. C confía en usted y a [5] apuntará al siguiente byte DESPUÉS del último byte de su memoria asignada (el último es un [4].

Por lo tanto, está escribiendo un valor en un lugar que puede contener absolutamente CUALQUIER COSA y no es para que lo reescriba. Esto se llama corrupción de memoria.

size_t i = 0;
mientras que (i <5) a [i ++] = '';

Ahora cada carácter en tu memoria desde un [0] a un [4] contiene ”

printf (“‘% s’ \ n”, a);

Will lleva el puntero a “a”, mencionando el primer byte de tu memoria. Estás “diciéndole” a printf que los datos se imprimirán como una cadena (conjunto de caracteres nulo terminado por \ 0).

No tiene forma de saber dónde termina su cadena, excepto repitiéndola hasta llegar a \ 0.

la memoria en “a” contiene 4 bytes de ” seguido de algo que probablemente sea su ‘\ 0’ a menos que haya sido reescrito por otra cosa. Por lo tanto, no hay forma de saber dónde se detendrá realmente la impresión. Este es un comportamiento incierto.

libre (a);

Dado que asignó “a” usando malloc, el tamaño de esta asignación se registra internamente y se asocia con la dirección de a en una tabla de la que no tiene que preocuparse.

free usará esa tabla para saber cuánta memoria está realmente liberada (en su mayoría, simplemente elimine la dirección de a de la tabla de memoria asignada. En este punto, es probable que todavía contenga sus datos, esperando ser reescrito por otra cosa.

Su código no se bloquea debido a que es “gratuito” sino porque escribe demasiado en la memoria señalado por un.

Ahora no sé si su código tiene algún propósito, así que como un pensamiento para pensar, lo dejaré con una versión más corta con el mismo resultado:

int main ()
{
char * a = “”; // a se le asignará el tamaño de 4 + 1 caracteres
// Su matriz de caracteres tiene terminación nula automática y es de tamaño 5
printf (“‘% s’ \ n”, a);
devuelve 0;
}

Estás escribiendo más allá de los límites de la matriz.

El recuento de elementos de matriz comienza en [math] 0. [/ Math]

La cadena de CA debe tener un valor nulo al final para que sea válida en las operaciones de cadena normales.

La notación de índice x[n] es una conveniencia para el programador. El sistema traduce
en *(x + n) donde n es un múltiplo del tamaño del tipo de x.

Si x es una matriz int , n reenvía la dirección n*4 direcciones,
porque int es un valor de 4 bytes.

Entonces n debe ser 1 menos que la capacidad asignada de a.

En cuanto al hardware, la dirección justo después del LSB del último elemento de una matriz, probablemente sea un byte no asignado o el comienzo de otra variable. La mayoría de las computadoras asignan objetos a lo largo de los límites de bytes y si el objeto no llena exactamente todo el bloque de bytes, algunos bytes vacíos quedan sin asignar. La siguiente variable comienza en un límite de bytes después de un área en blanco.

Es por eso que a veces se obtiene basura pero no hay falla de memoria y otras veces se produce una falla de memoria. En su caso, está almacenando char , que es 1 byte. Si hubiera asignado la matriz de caracteres estáticamente, como

char s [5];

la asignación de s[5] = '\0' probablemente habría tenido éxito, porque la siguiente variable estaría en (s + 8) independientemente. printf habría impreso los espacios y se habría detenido en '\0'. (Todavía no está bien).

Pero tienes esos 5 bytes con malloc .
malloc sigue muy de cerca sus asignaciones y fallará incluso por 1 byte adicional. Esa es una buena cosa. El monitor de montón (o lo que sea) detectó correctamente una violación del espacio de montón e hizo lo que tenía que hacer y generó un evento de seguridad, que es un evento fatal, por lo que el programa se bloqueó.

C tiene un tiempo de ejecución muy pequeño, muy pequeño que habla con el sistema operativo sobre la memoria en vivo y verifica que el sistema operativo dirija su funcionamiento. Cuando el sistema operativo dice morir, detiene la operación e intenta hacer una limpieza mínima, como cerrar los descriptores de archivos abiertos y generar un volcado de núcleo si tiene ese código compilado, y emitir algunos códigos de error al shell. No mucho, pero lo suficiente como para rescatar y no llevar consigo el sistema operativo, por lo general.

Entonces, el programa obtiene su memoria de almacenamiento dinámico del asignador del sistema operativo, por lo que cuando solicita el valor en una dirección, confía en que el sistema operativo vaya allí y lo devuelva, y si el sistema operativo emite un error, no hay nada que el programa pueda hacer porque intentó algo que el sistema no le permitió hacer.

No tiene idea cuando solicita memoria a través de malloc qué memoria se está asignando. El sistema operativo tiene muchos procesos que solicitan memoria de almacenamiento dinámico, por lo que no puede permitir que un programa se demore en exceder sus límites de bloqueo de memoria o la corrupción podría resultar no solo en el programa, sino en otros programas en ejecución o el sistema operativo en sí.

¿Qué es un pequeño byte extra, aunque? Bueno, es un byte extra demasiado.

Así que esto es algo por lo que todos pasan con los lenguajes orientados al hardware.
Es un rito de iniciación para cualquier programador de C, y volverá a suceder, demasiadas veces.
Lo mejor que puedes hacer es entenderlo lo mejor que puedas y aprender a minimizar este pequeño insecto que surge en todas partes.

El problema está en la segunda línea de código:

char * a = malloc (5);
a [5] = ‘\ 0’;

Al principio, cuando se ejecuta la primera línea, la invocación malloc devuelve un valor de puntero de tipo void* que apunta a un fragmento de memoria de tamaño (al menos, pero no se garantiza que sea más de) cinco bytes. Entonces este valor se asigna a la variable a. a es de tipo char* , al que void* se convierte implícitamente (como es el caso con todos los demás tipos de puntero).

Cuando llegas a la segunda línea, las cosas comienzan a ponerse más interesantes. Cuando se evalúa la expresión a a[5] , el compilador la trata como la expresión *(a + 5) en la aritmética del puntero. Esto simplemente significa que se agregan cinco bytes a la dirección de memoria contenida en a , haciendo que a[5] haga referencia al sexto byte desde el comienzo del fragmento ( a[0] referencia al primero ). La nueva dirección a a + 5 es en realidad un byte más allá del último byte del fragmento, que desciende al espacio de memoria que no ha asignado explícitamente (recuerde que asignó solo cinco bytes con malloc ). No importa el valor que asigne al byte a a[5] , está fuera de su reino asignado, por lo que no debe meterse con él.

Una forma directa (y con suerte significativa) de corregir este error es disminuir el índice de a[5] , convirtiéndolo en a[4] . Ahora esta es una posición de byte válida en el fragmento asignado (apunta al quinto byte del fragmento).

Puede encontrar útil el siguiente artículo como su primera parada para leer más sobre el tema: Asignación de memoria dinámica C – Wikipedia.

No he leído las otras 100+ respuestas (!), Pero voy a entrar de todos modos. Mucho de esto podría duplicar algunas de las otras respuestas.

En primer lugar, liberar o no liberar el puntero no es el problema.

El mayor problema con el código C en su pregunta es que no es su código real .

Llama a malloc y free , que se declaran en el encabezado estándar , y printf , que se declara en el encabezado estándar . En las últimas versiones del estándar C (1999 y 2011), llamar a una función sin una declaración visible requiere un diagnóstico del compilador, pero muchos compiladores no se ajustan completamente por defecto. La versión 1989/1990 de C le permite llamar a funciones sin una declaración visible, pero hace suposiciones sobre tales funciones que ninguna de malloc , free e printf satisfacen, por lo que las tres llamadas tendrían un comportamiento indefinido.

Pero el regalo real es el uso de size_t , un tipo que se define en varios encabezados estándar. Ningún compilador de C le permitirá referirse a size_t sin una declaración visible, que normalmente obtendría al incluir uno de los encabezados requeridos. El código en su pregunta no se compilará, por lo que no hay forma de que se bloquee en tiempo de ejecución.

Asumiré que realmente tienes estas líneas en la parte superior de tu archivo fuente real:

#include
#include

Aparentemente, usted decidió que esas líneas no eran importantes para aquellos de nosotros que tratamos de ayudarlo, pero como no sabe cuál es el problema, por definición no puede saber de manera confiable qué información es segura para omitir. Así que no dejes nada. Siempre copie y pegue el código fuente exacto que realmente está compilando.

(Esto probablemente suena duro. No se entiende de esa manera. Por favor, trate todo esto como una crítica constructiva).

int main ()

Esto debería ser:

int main (nulo)

Esto no es un gran problema, y ​​hay algún argumento sobre si int main() es realmente correcto, pero no hay duda de que int main(void) es correcto (es una de las formas especificadas en el estándar C). Es más fácil hacerlo bien que averiguar si puede salirse con la suya de manera diferente.

char * a = malloc (5);
printf (“‘% s’ \ n”, a);

Esto intenta asignar un fragmento de memoria de 5 bytes e inicializa a para señalar el byte inicial. Tenga en cuenta que dije “intentos”. Es casi seguro que una asignación pequeña como esta tenga éxito, pero nunca debe suponer que malloc ha tenido éxito; siempre prueba el resultado. Algo como esto:

char * a = malloc (5);
if (a == NULL)
{
fprintf (stderr, “malloc (5) falló \ n”);
salir (EXIT_FAILURE);
}

Lo más probable es que nunca veas ese mensaje de error.

a [5] = ‘\ 0’;

Como sospecho que más de 90 de las otras respuestas le han dicho, esto asigna un valor al sexto elemento de la matriz asignada. No posee esa memoria, por lo que asignarla tiene un comportamiento indefinido. Podría bloquear su programa. O, lo que es peor, podría comportarse tal como lo espera (lo que significa que tiene un error no detectado).

size_t i = 0;
mientras que (i <5) a [i ++] = '';

Esta parte está en su mayoría bien (aunque personalmente encontraría un bucle for más claro). Asigna un carácter de espacio a cada una de las 5 ranuras sucesivas en su matriz asignada. Esos son los 5 elementos, por lo que en realidad no hay espacio para el carácter nulo que termina la cadena; ya intentó asignar el carácter nulo, pero está más allá del final de la memoria que posee. (There’s nothing wrong with having a character array with no null character at the end of it, as long as you don’t try to treat its contents as a string — which you do a couple of lines down so yes, you do need that null character.)

You now have space characters in a[0] , a[1] , a[2] , a[3] , and a[4] , and you’ve tried to store a null character in a[5] . If you had (successfully) called malloc(6) rather than malloc(5) , you’d be in good shape.

printf(“‘%s’\n”, a);

This prints the contents of the memory pointed to by a , assuming it contains a string. You did carefully null-terminate it, so if you were able to access that byte just past the end of the allocated memory, this would work, printing a single quote, 5 spaces, another single quote, and a newline.

In fact, when I compiled and ran your code on my system (after adding the required #include directives), that’s exactly what it did. Typically malloc will allocate a little more memory than it actually needs. Memory tends to be managed in chunks whose sizes are powers of 2, so if you call malloc(5) the underlying system is likely to allocate at least 8 bytes. I’m a little surprised that your program crashed on your system. Perhaps you’re using an implementation that explicitly checks for this kind of thing (a very useful feature, but it can hurt performance). Some systems have debug modes where they perform extra checks, and production modes where the checks are omitted and the code can run faster.

free(a);

Esto es bueno. As you say, it’s good practice to free any memory that you allocate when you’re done with it. But on almost all systems, all allocated memory is automatically released when the program finished. (Otherwise misbehaving programs could damage the entire running system.) So if you omitted this line, it very probably wouldn’t make any difference.

devuelve 0;

No problem here, but as of the 1999 version of the C standard it’s not strictly necessary. The language was changed so that reaching the terminating } of the main function does an implicit return 0; . But doing it explicitly is harmless, and it makes your code more portable to older implementations.

Now that we’ve gone through your code line by line, let’s look at some larger issues.

You’re using malloc to allocate a single very small chunk of memory. In a real-world production program, this wouldn’t make any sense, and you should just define an array object of the same size.

The program as a whole simply prints a single line of output. It’s far more complicated than it needs to be for that task. Here’s a simpler program that does exactly the same thing:

#include
int main (nulo) {
puts(“‘ ‘”);
}

And here’s an even shorter way that doesn’t require C:

echo “‘ ‘”

But these are silly objections. Presumably your real goal isn’t just to print that line of text; it’s to learn how to write C code. Programs that make meaningful use of dynamic memory allocation are going to be a lot more complicated and harder to get right. It makes sense to start with simple examples like this one.

Is this a test for me, or help for you? 🙂

OK, this code overwrites the byte immediately following in memory the end of the malloc’d memory. Valid indices for array “a” are 0, 1, 2, 3, and 4. a[5] does not belong to the malloc’d array, and may cause behavior anywhere from no bad effect, to a system crash, to (and this is the worst) no direct indication of anything wrong except that some “down the line” result (program output or result) is wrong – without giving any other indications of a bug.

In addition, the pointer returned from malloc isn’t checked for success/failure. If the malloc failed then “a” would point to memory location zero – and to go ahead and use it may well result in undefined behavior (and not the kind you want). This is exactly the problem I found recently when taking over some legacy code for an embedded system. The previous developer didn’t check the value returned from malloc and went ahead and used it anyway for a communications buffer. Well, the malloc always failed (heap set too small), but the “failure” was sometimes not detected, and at other times various different “modes” of bad behavior would result – depending on how many bytes had been received in the communications buffer as what had happened is that writing to the buffer overwrote the IVT (Interrupt Vector Table). Fun stuff. 🙂

So why do you ask?

Quora User has given you an excellent, to-the-point answer. Now let me tell you why this happens.

You need to understand how the heap allocator works. This is different according to the implementation, but I will give you a general account.

The kernel gives your program a slice of memory, for the sake of simplicity let’s say it is one page. The standard C library initializes this memory by declaring it as one big free chunk of memory, but it adds some book keeping information like the size of the chunk in bytes, and probably some magic number to check for memory corruption, and whether this chunk is free or allocated. This chunk would look similar to:

+—–+———-+——–+
El | hdr | free mem | footer |
+—–+———-+——–+

When you ask malloc to allocate memory, it does the following (also differs by implementation):

  • Calculate total bytes required = requested size + padding to account for correct alignment
  • Add size of header + footer to total bytes

It then searches the heap for a chunk that is free and is at least the size required. There are different algorithms to determine this like first fit, best fit and worst fit.

When it finds a suitable chunk, it marks it as reserved, updates the header and footer, and if needed, split it in two chunks if it is more than needed. It then returns a pointer to the start of free memory chunk, not to the chunk header.

Now when you access the memory right before or after your pointer (like you did), you will most probably overwrite the header or footer of the adjacent chunk. When the standard library tries to access that chunk for whatever reason, it finds a corrupt header/footer. This will probably lead to an invalid pointer, which ultimately results in a segmentation fault.

Everyone has rightly pointed out that you asked for five bytes, but then did something that assumes you have six instead. The scary part is that this is liable to run, because heap routines typically hand out maximally-aligned hunks of memory, and maximally-aligned typically means eight-byte (for 64-bit integers or double precision floating point).

That said, there’s another issue here as well. In this particular case, since it’s the very first thing you’re doing (aside from what’s done in the code leading up to calling main()), you’re not likely to trip over it, but… malloc() can fail. (If you’re writing for embedded systems that typically have as little memory as they can get away with, you’re more likely to trip over this—but even on a big system, maybe you forgot to free up space after you’re done with it, so that eventually the heap will be used up.) If the heap can’t give you the space you’re asking for, it will return NULL, and then, even if you didn’t try to use more than you asked for, you’ll be using more than you actually got.

Pregunta original

What is wrong with this C code?

int main () {
char *a = malloc(5);
a[5] = ‘\0’;
size_t i = 0;
while (i < 5)
a[i++] = ‘ ‘;
printf(“‘%s’\n”, a);
free(a);
devuelve 0;
}

Before I get started, I want to state that I am no means extremely well versed in C or C++. I am still in the learning stages of these lower level languages. Although, my knowledge of C# should apply with the problem solving aspect.
I am actually getting a compiler error with how you are using malloc. So I have done a slight change to the code so I don’t generate a compiler with Visual Studio.

#include “stdafx.h”
#include
#include

int main ()
{
char * buffer;
buffer = (char *)malloc(5);
buffer[5] = ‘\0’;
size_t i = 0;
while (i < 5) buffer[i++] = ' ';
printf(“‘%s’\n”, buffer);
free(buffer);
devuelve 0;
}

Notice the only real change is the addition of buffer and replacing a with it.

However, the overall issue is with the malloc size. With a size of 5, it buffers outside of the bounds set. Changing it to 6 will allow the program to run appropriately.

The breakpoint of the program explicitly states:

CRT detected that the application wrote to memory after end of heap buffer.

Which means that the buffer size is too small.

Let’s fix this.

#include “stdafx.h”
#include
#include

int main ()
{
char * buffer;
buffer = (char *)malloc(6);
buffer[5] = ‘\0’;
size_t i = 0;
while (i < 5) buffer[i++] = ' ';
printf(“‘%s’\n”, buffer);
free(buffer);
devuelve 0;
}

¡Hola!

“I read somewhere that when you malloc a pointer, you must free it somewhere, or it would take memory on your computer until you restart it.”

It won’t take memory on your computer until you restart it, but it will take memory on your computer until the process exits. This wouldn’t be much of a biggie with your program because you’re only malloc’ing 5 bytes of memory, but if you did that on some long running process (think, OS-related processes or antiviruses or even Steam if you’re into gaming) it will eventually chug up all of your machine’s RAM and make it run slowly or even DDoS it.

Now I assume the crash you’re getting here is a seg fault (your error message should contain a core dump memory map if I’m right, which should assist you in debugging, but not right now) or a stack overflow (see where that comes from now, eh?) which basically means your program is trying to access memory outside of its scope. That’s a security measure to ensure your program doesn’t overwrite random memory blocks on your system, not previously borrowed by your program, which would cause unpredictable results in the execution of other programs.

So, why does that happen you may wonder? Can’t tell for sure, but if that’s all the code in your program, you want to include the libraries to allow you to do stuff like printf and malloc :

#include

#include

Also, in the future, please post the crashing message, it would help a lot with having an idea.

Saludos,

Gabriel

Try either char *a = malloc(6) or a[4] = ‘\0’. In the second line, you are trying to assign value to an un-allocated memory. When you do malloc(5), you get only 5 bytes of space allocated from the heap. Accessing the sixth byte could be disastrous depending on the significance of that particular memory location( present in the heap) during you program’s run time. If you are on a linux command line, you could run your application with gdb to find the exact root cause, like so:

  1. gdb
  2. After the gdb starts, type ‘r’ and press the enter key.
  3. As soon as the program crashes, do a back trace by typing ‘bt’ on the gdb command line. This would give a fair idea as to what causes the program to crash. The very first entry of the back trace is certainly the root cause of your crash.

    Note: Ensure you are compiling the application with the debug options enabled, usually selected by using the -g flag in gcc.
    Using and Porting the GNU Compiler Collection (GCC): Invoking GCC

    ¡Espero eso ayude!

There are three fundamental mistakes in your code.

Firstly , you must, at the beginning of your program type #include

#include Is a statement which tells the compiler to insert the contents of stdio at that particular place.
In C/C++ we use some functions like printf(), scanf(), cout,cin.
Do we define it? No.
These functions are not defined by the programmer. These functions are already defined inside the language library. To use these kinds of predefined functions, we have to include the corresponding header file.

Secondly , you must include the header file

We must include the header file at the beginning of the program. Just like the inbuilt functions printf() and scanf() are a part of the header file, malloc(), calloc(), free() and realloc() are member functions of the header file. Hence to use those functions, we must include the header file at the beginning of our program.

Thirdly , attempt to assign void pointer type value to a char pointer.

Line 3 of your code

char *a = malloc(5);

is and attempt to assign value of a void pointer type to a char pointer type.

The default return data type of the malloc() function is void pointer which must be converted to char type pointer in order to be able to point towards a memory location which stores char type data.

The above can be done by explicit type conversion.

char *a=(char *)malloc(5);

Hence, the debugged code is:

And yes, if you don’t free the memory after using malloc() function, it does not just lie there until the next restart. Just about every modern operating system will recover all the allocated memory space after a program exits.

I haven’t codded in C from quite some time now but think you have the following 2 problems:

  1. The allocation using malloc is made like this :

ptr = (cast-type*) malloc(byte-size);

or even better:

ptr = (cast-type*) malloc(n * sizeof(cast-type));

where n is the actual size you want your array to have. In the particular case of type ‘char’ , malloc ( n ) will allocate memory for an array with length n because sizeof(char) is 1 , but in case of an ‘int’ for example malloc( n ) usually(depending on compiler) will allocate memory for an array with the length n/4 (32 bit system) or n/8 ( 64 bit system ) because the size of int is 4 or 8 bytes(depending of system). So for an array of int with length 5 is better to allocate like this

int* ptr = (int*) malloc(5 * sizeof(int)) ;

*More readable and less prone to errors than :

int* ptr = (int*) malloc(20);

Edit : As Anoop Saxena shared, even if not incorrect , casting the result of malloc it’s unnecessary because void * is automatically and safely promoted to any other pointer type in this case. So , i think this is how you should use malloc.

int* ptr = malloc(5 * sizeof(int))

2. In almost all programming languages the index of an array starts from 0, so for an array ‘a’ , with length 5 the last element will be a [4] . So when you put a value in a[5], you actually set a value in some not allocated memory.

You can read more about this :

C Memory Allocation Using malloc(), calloc(), free() & realloc()

C Programming Pointers and Arrays

Arrays in C

This program allocates a region for 5 bytes and at line 4, there is an assignment to the 6-th byte. There is an issue (pick one of the following):

  • the variable a should have 6 bytes

O

  • the assignment should be done in the 5-th byte on line 4 (a[4] = ‘\0’) and the while look should be not further than 4

About the alloc/free, several observations:

  • Your program frees the memory after using it. As such there is no issue, your program is correct from a memory allocation perspective. You still have the bug mentioned above (writing in a memory not allocated).
  • If your program consumes too much memory and/or if there is memory leaks (non-freed memory), the operating system will free it when killing the process. The memory will be allocated/lost while the program is running. However, when killing the program the OS will reclaim all the memory allocated for this process. No need to restart your machine – we are in 2016, your OS has some good memory management mechanisms 🙂

When trying to debug your program, it can be useful to use valgrind – it will report many issues in your program. Valgrind Home