El asombroso poder de C corta en ambos sentidos. Esto es lo que debe tener en cuenta y cómo mantener sus programas C en línea recta y estrecha.
por Serdar Yegulalp
Pocos lenguajes de programación pueden igualar a C por pura velocidad y potencia a nivel de máquina. Esta afirmación era cierta hace 50 años y sigue siendo verdad hoy. Sin embargo, hay una razón por la que los programadores acuñaron el término “pistolero” para describir el tipo de poder de C. Si no tiene cuidado, C puede volarle los dedos de los pies o los de otra persona.

Aquí hay cuatro de los errores más comunes que puede cometer con C y cinco pasos que puede seguir para prevenirlos.
Error común de C: no liberar la memoria malloc -ed (o liberarla más de una vez)
Este es uno de los grandes errores en C, muchos de los cuales involucran la administración de la memoria. La memoria asignada (realizada con la malloc
función) no se elimina automáticamente en C. Es el trabajo del programador deshacerse de esa memoria cuando ya no se usa. Si no libera solicitudes de memoria repetidas, terminará con una pérdida de memoria. Intente utilizar una región de la memoria que ya se haya liberado y su programa se bloqueará o, lo que es peor, cojeará y se volverá vulnerable a un ataque mediante ese mecanismo.
Tenga en cuenta que una pérdida de memoria solo debe describir situaciones en las que se supone que la memoria debe liberarse, pero no lo está. Si un programa sigue asignando memoria porque la memoria es realmente necesaria y se usa para el trabajo, entonces su uso de la memoria puede ser ineficiente , pero estrictamente hablando no es una fuga.
Error común de C: leer una matriz fuera de los límites
Aquí tenemos otro de los errores más comunes y peligrosos en C. Una lectura más allá del final de una matriz puede devolver datos basura. Una escritura más allá de los límites de una matriz podría corromper el estado del programa, bloquearlo por completo o, lo peor de todo, convertirse en un vector de ataque de malware.
Entonces, ¿por qué la carga de verificar los límites de una matriz se deja al programador? En la especificación oficial de C, leer o escribir una matriz más allá de sus límites es un “comportamiento indefinido”, lo que significa que la especificación no tiene voz en lo que se supone que debe suceder. El compilador ni siquiera está obligado a quejarse de ello.
C ha favorecido durante mucho tiempo dar energía al programador incluso bajo su propio riesgo. Una lectura o escritura fuera de límites no suele ser atrapada por el compilador, a menos que habilite específicamente las opciones del compilador para protegerse contra ella. Es más, es posible que se exceda el límite de una matriz en tiempo de ejecución de una manera que ni siquiera una verificación del compilador puede evitar.
Error C común: no comprobar los resultados de malloc
malloc y calloc (para la memoria previamente puesta a cero) son las funciones de la biblioteca C que obtienen la memoria asignada al montón del sistema. Si no pueden asignar memoria, generan un error. En los días en que las computadoras tenían relativamente poca memoria, existía una gran posibilidad de que una llamada malloc no tuviera éxito.
A pesar de que las computadoras de hoy tienen gigabytes de RAM para distribuir malloc, siempre existe la posibilidad de que falle, especialmente bajo alta presión de memoria o cuando se asignan grandes bloques de memoria a la vez. Esto es especialmente cierto para los programas C que “asignan en bloque”, un gran bloque de memoria del sistema operativo primero y luego lo dividen para su propio uso. Si esa primera asignación falla porque es demasiado grande, es posible que pueda atrapar ese rechazo, reducir la asignación y ajustar la heurística de uso de memoria del programa en consecuencia. Pero si la asignación de memoria falla sin trabas, todo el programa podría fallar.
Error común de C: uso void* de punteros genéricos a la memoria
Usar void* para señalar la memoria es un hábito antiguo y malo. Los punteros a la memoria debe ser siempre char*, unsigned char* o uintpr t*. Los conjuntos de compiladores de C modernos deben proporcionar uintpr_t como parte de stdint.h.
Cuando se etiqueta de una de estas formas, está claro que el puntero se refiere a una ubicación de memoria en abstracto en lugar de a algún tipo de objeto indefinido. Esto es doblemente importante si está realizando cálculos matemáticos con punteros. Con uintpr_t* y similares, el elemento de tamaño al que se apunta y cómo se utilizará no son ambiguos. Con void*, no mucho.
5 consejos para evitar errores comunes en C
¿Cómo se evitan estos errores tan comunes cuando se trabaja con memoria, matrices y punteros en C? Tenga en cuenta estos cinco consejos:
Estructura el programa en C para que la propiedad de la memoria se mantenga clara
Si recién está iniciando una aplicación C, vale la pena pensar en la forma en que se asigna y libera la memoria como uno de los principios organizativos del programa. Si no está claro dónde se libera una asignación de memoria determinada o bajo qué circunstancias, está buscando problemas. Haga un esfuerzo adicional para que la propiedad de la memoria sea lo más clara posible. Te harás un favor a ti mismo (y a los futuros desarrolladores).
Esta es la filosofía detrás de lenguajes como Rust. Rust hace que sea imposible escribir un programa que se compile correctamente a menos que exprese claramente cómo se posee y se transfiere la memoria. C no tiene tales restricciones, pero es aconsejable adoptar esa filosofía como guía siempre que sea posible.
Utilice las opciones del compilador de C que protegen contra problemas de memoria
Muchos de los problemas descritos en la primera mitad de este artículo se pueden marcar utilizando opciones estrictas del compilador. Las ediciones recientes de gcc, por ejemplo, proporcionan herramientas como AddressSanitizer (“ASAN”) como una opción de compilación para verificar errores comunes de administración de memoria.
Tenga cuidado, estas herramientas no captan absolutamente todo. Son barandillas; no agarran el volante si sales de la carretera. Además, algunas de estas herramientas, como ASAN, imponen costos de compilación y tiempo de ejecución, por lo que deben evitarse en las versiones de lanzamiento.
Utilice Cppcheck o Valgrind para analizar el código C en busca de pérdidas de memoria
Cuando los propios compiladores se quedan cortos, otras herramientas intervienen para llenar el vacío, especialmente cuando se trata de analizar el comportamiento del programa en tiempo de ejecución.
Cppcheck ejecuta un análisis estático en el código fuente de C para buscar errores comunes en la administración de la memoria y comportamientos indefinidos (entre otras cosas).
Valgrind proporciona un caché de herramientas para detectar errores de memoria y subprocesos en la ejecución de programas C. Esto es mucho más poderoso que usar el análisis en tiempo de compilación, ya que puede derivar información sobre el comportamiento del programa cuando está realmente activo. La desventaja es que el programa se ejecuta a una fracción de su velocidad normal. Pero esto generalmente está bien para probar.
Estas herramientas no son soluciones mágicas y no atraparán todo. Pero funcionan como parte de una estrategia defensiva general contra la mala gestión de la memoria en C.
Automatice la gestión de la memoria C con un recolector de basura
Dado que los errores de memoria son una fuente evidente de problemas de C, aquí hay una solución sencilla: no administre la memoria en C manualmente. Utilice un recolector de basura.
Sí, esto es posible en C. Puede usar algo como el recolector de basura Boehm-Demers-Weiser para agregar administración automática de memoria a los programas C. Para algunos programas, el uso del colector Boehm puede incluso acelerar las cosas. Incluso se puede utilizar como mecanismo de detección de fugas.
La principal desventaja del recolector de basura Boehm es que no puede escanear ni liberar memoria que usa el malloc predeterminado. Utiliza su propia función de asignación, y solo funciona en la memoria que le asigna específicamente.
No uses C cuando otro idioma sea suficiente
Algunas personas escriben en C porque realmente lo disfrutan y lo encuentran fructífero. Sin embargo, en general, es mejor usar C solo cuando sea necesario, y luego con moderación, para las pocas situaciones en las que realmente es la opción ideal.
Si tiene un proyecto en el que el rendimiento de la ejecución se verá limitado principalmente por la E/S o el acceso al disco, es poco probable que escribirlo en C lo haga más rápido de la manera que importa, y probablemente solo lo hará más propenso a errores y difícil de escribir. mantener. El mismo programa bien podría estar escrito en Go o Python.
Otro enfoque es usar C solo para las partes de la aplicación que realmente requieren un alto rendimiento, y un lenguaje más confiable, aunque más lento, para otras partes. Nuevamente, Python se puede usar para empaquetar bibliotecas C o código C personalizado, lo que lo convierte en una buena opción para los componentes más repetitivos como el manejo de opciones de línea de comandos.