Después de todos estos años, el mundo todavía funciona con la programación C
Publicado: 2022-03-11Muchos de los proyectos C que existen hoy en día se iniciaron hace décadas.
El desarrollo del sistema operativo UNIX comenzó en 1969 y su código se reescribió en C en 1972. El lenguaje C en realidad se creó para mover el código del núcleo UNIX del ensamblador a un lenguaje de nivel superior, que haría las mismas tareas con menos líneas de código. .
El desarrollo de la base de datos Oracle comenzó en 1977 y su código se reescribió de ensamblador a C en 1983. Se convirtió en una de las bases de datos más populares del mundo.
En 1985 se lanzó Windows 1.0. Aunque el código fuente de Windows no está disponible públicamente, se ha dicho que su kernel está escrito principalmente en C, con algunas partes en ensamblaje. El desarrollo del kernel de Linux comenzó en 1991 y también está escrito en C. Al año siguiente, se lanzó bajo la licencia GNU y se usó como parte del sistema operativo GNU. El propio sistema operativo GNU se inició utilizando los lenguajes de programación C y Lisp, por lo que muchos de sus componentes están escritos en C.
Pero la programación en C no se limita a proyectos que comenzaron hace décadas, cuando no había tantos lenguajes de programación como hoy. Muchos proyectos C todavía se inician hoy; hay algunas buenas razones para eso.
¿Cómo funciona el mundo alimentado por C?
A pesar de la prevalencia de los lenguajes de alto nivel, C continúa empoderando al mundo. Los siguientes son algunos de los sistemas que son utilizados por millones y están programados en lenguaje C.
Microsoft Windows
El kernel de Windows de Microsoft está desarrollado principalmente en C, con algunas partes en lenguaje ensamblador. Durante décadas, el sistema operativo más utilizado del mundo, con alrededor del 90 por ciento de la cuota de mercado, ha funcionado con un núcleo escrito en C.
linux
Linux también está escrito principalmente en C, con algunas partes en ensamblador. Alrededor del 97 por ciento de las 500 supercomputadoras más poderosas del mundo ejecutan el kernel de Linux. También se utiliza en muchas computadoras personales.
Mac
Las computadoras Mac también funcionan con C, ya que el kernel OS X está escrito principalmente en C. Cada programa y controlador en una Mac, como en las computadoras con Windows y Linux, se ejecuta en un kernel con tecnología C.
Móvil
Los núcleos de iOS, Android y Windows Phone también están escritos en C. Son solo adaptaciones móviles de los núcleos existentes de Mac OS, Linux y Windows. Entonces, los teléfonos inteligentes que usa todos los días se ejecutan en un kernel C.
bases de datos
Las bases de datos más populares del mundo, incluidas Oracle Database, MySQL, MS SQL Server y PostgreSQL, están codificadas en C (las tres primeras en realidad tanto en C como en C++).
Las bases de datos se utilizan en todo tipo de sistemas: financiero, gobierno, medios, entretenimiento, telecomunicaciones, salud, educación, retail, redes sociales, web y similares.
Películas 3D
Las películas en 3D se crean con aplicaciones que generalmente están escritas en C y C++. Esas aplicaciones deben ser muy eficientes y rápidas, ya que manejan una gran cantidad de datos y hacen muchos cálculos por segundo. Cuanto más eficientes son, menos tiempo les toma a los artistas y animadores generar las tomas de la película, y más dinero ahorra la empresa.
Sistemas embebidos
Imagina que te levantas un día y vas de compras. La alarma que te despierta probablemente esté programada en C. Luego usas tu microondas o cafetera para preparar tu desayuno. También son sistemas integrados y, por lo tanto, probablemente estén programados en C. Enciende su televisor o radio mientras desayuna. Esos también son sistemas integrados, alimentados por C. Cuando abre la puerta de su garaje con el control remoto, también está utilizando un sistema integrado que probablemente esté programado en C.
Luego te subes a tu auto. Si tiene las siguientes características, también programado en C:
- transmisión automática
- sistemas de detección de presión de neumáticos
- sensores (oxígeno, temperatura, nivel de aceite, etc.)
- Memoria para ajustes de asientos y espejos.
- pantalla del tablero
- frenos antibloqueo
- control automático de estabilidad
- control de crucero
- control climatico
- cerraduras a prueba de niños
- entrada sin llave
- asientos con calefacción
- control de bolsas de aire
Llegas a la tienda, estacionas tu auto y vas a una máquina expendedora para comprar un refresco. ¿Qué lenguaje usaron para programar esta máquina expendedora? Probablemente C. Entonces compras algo en la tienda. La caja registradora también está programada en C. ¿Y cuando pagas con tu tarjeta de crédito? Lo has adivinado: el lector de tarjetas de crédito, de nuevo, probablemente esté programado en C.
Todos esos dispositivos son sistemas integrados. Son como pequeñas computadoras que tienen un microcontrolador/microprocesador adentro que ejecuta un programa, también llamado firmware, en dispositivos integrados. Ese programa debe detectar las pulsaciones de teclas y actuar en consecuencia, y también mostrar información al usuario. Por ejemplo, el despertador debe interactuar con el usuario, detectando qué botón está presionando el usuario y, a veces, cuánto tiempo lo está presionando, y programar el dispositivo en consecuencia, todo mientras muestra al usuario la información relevante. El sistema de frenos antibloqueo del automóvil, por ejemplo, debe ser capaz de detectar el bloqueo repentino de las llantas y actuar para liberar la presión sobre los frenos por un pequeño período de tiempo, desbloqueándolos y evitando así el derrape incontrolado. Todos esos cálculos son realizados por un sistema integrado programado.
Aunque el lenguaje de programación utilizado en los sistemas integrados puede variar de una marca a otra, normalmente se programan en lenguaje C, debido a las características de flexibilidad, eficiencia, rendimiento y cercanía del lenguaje con el hardware.
¿Por qué todavía se usa el lenguaje de programación C?
Hay muchos lenguajes de programación, hoy en día, que permiten a los desarrolladores ser más productivos que con C para diferentes tipos de proyectos. Hay lenguajes de nivel superior que proporcionan bibliotecas integradas mucho más grandes que simplifican el trabajo con JSON, XML, UI, páginas web, solicitudes de clientes, conexiones de bases de datos, manipulación de medios, etc.
Pero a pesar de eso, hay muchas razones para creer que la programación en C permanecerá activa durante mucho tiempo.
En los lenguajes de programación, una talla no sirve para todos. Aquí hay algunas razones por las que C es imbatible y casi obligatorio para ciertas aplicaciones.
Portabilidad y Eficiencia
C es casi un lenguaje ensamblador portátil . Está lo más cerca posible de la máquina, mientras que está disponible casi universalmente para las arquitecturas de procesadores existentes. Hay al menos un compilador de C para casi todas las arquitecturas existentes. Y hoy en día, debido a los binarios altamente optimizados generados por los compiladores modernos, no es una tarea fácil mejorar su salida con ensamblaje escrito a mano.
Tal es su portabilidad y eficiencia que “los compiladores, bibliotecas e intérpretes de otros lenguajes de programación a menudo se implementan en C”. Los lenguajes interpretados como Python, Ruby y PHP tienen sus implementaciones principales escritas en C. Incluso los compiladores lo utilizan para que otros lenguajes se comuniquen con la máquina. Por ejemplo, C es el lenguaje intermedio subyacente a Eiffel y Forth. Esto significa que, en lugar de generar código de máquina para cada arquitectura compatible, los compiladores de esos lenguajes solo generan código C intermedio, y el compilador de C maneja la generación de código de máquina.
C también se ha convertido en una lingua franca para la comunicación entre desarrolladores. Como dice Alex Allain, gerente de ingeniería de Dropbox y creador de Cprogramming.com:
C es un gran lenguaje para expresar ideas comunes en la programación de una manera que la mayoría de las personas se sienten cómodas. Además, muchos de los principios utilizados en C (por ejemplo,
argc
yargv
para parámetros de línea de comandos, así como construcciones de bucles y tipos de variables) aparecerán en muchos otros lenguajes que aprenda para que pueda hablar a las personas, incluso si no conocen C de una manera que sea común para ambos.
Manipulación de la memoria
El acceso a direcciones de memoria arbitrarias y la aritmética de punteros es una característica importante que hace que C se ajuste perfectamente a la programación de sistemas (sistemas operativos y sistemas integrados).
En el límite de hardware/software, los sistemas informáticos y los microcontroladores asignan sus periféricos y pines de E/S a direcciones de memoria. Las aplicaciones del sistema deben leer y escribir en esas ubicaciones de memoria personalizadas para comunicarse con el mundo. Por lo tanto, la capacidad de C para manipular direcciones de memoria arbitrarias es imprescindible para la programación del sistema.
Se podría diseñar un microcontrolador, por ejemplo, de modo que el byte en la dirección de memoria 0x40008000 sea enviado por el receptor/transmisor asíncrono universal (o UART, un componente de hardware común para comunicarse con periféricos) cada vez que se establece el bit número 4 de la dirección 0x40008001. a 1, y que después de establecer ese bit, el periférico lo desactivará automáticamente.
Este sería el código para una función C que envía un byte a través de ese UART:
#define UART_BYTE *(char *)0x40008000 #define UART_SEND *(volatile char *)0x40008001 |= 0x08 void send_uart(char byte) { UART_BYTE = byte; // write byte to 0x40008000 address UART_SEND; // set bit number 4 of address 0x40008001 }
La primera línea de la función se expandirá a:
*(char *)0x40008000 = byte;
Esta línea le dice al compilador que interprete el valor 0x40008000
como un puntero a un char
, luego elimine la referencia (entregue el valor señalado por) ese puntero (con el operador *
más a la izquierda) y finalmente asigne un valor de byte
a ese puntero sin referencia. En otras palabras: escriba el valor del byte
variable en la dirección de memoria 0x40008000
.

La siguiente línea se expandirá a:
*(volatile char *)0x40008001 |= 0x08;
En esta línea, realizamos una operación OR bit a bit en el valor de la dirección 0x40008001
y el valor 0x08
( 00001000
en binario, es decir, un 1 en el bit número 4) y guardamos el resultado en la dirección 0x40008001
. En otras palabras: configuramos el bit 4 del byte que está en la dirección 0x40008001. También declaramos que el valor en la dirección 0x40008001
es volátil . Esto le dice al compilador que este valor puede ser modificado por procesos externos a nuestro código, por lo que el compilador no hará suposiciones sobre el valor en esa dirección después de escribir en ella. (En este caso, este bit es desactivado por el hardware UART justo después de que lo configuramos por software). Esta información es importante para el optimizador del compilador. Si hiciéramos esto dentro de un ciclo for
, por ejemplo, sin especificar que el valor es volátil, el compilador podría asumir que este valor nunca cambia después de establecerse y omitir la ejecución del comando después del primer ciclo.
Uso determinista de recursos
Una característica común del lenguaje en la que no puede confiar la programación del sistema es la recolección de elementos no utilizados, o incluso la asignación dinámica para algunos sistemas integrados. Las aplicaciones integradas son muy limitadas en cuanto a tiempo y recursos de memoria. A menudo se usan para sistemas en tiempo real, donde no se puede permitir una llamada no determinista al recolector de basura. Y si la asignación dinámica no se puede utilizar debido a la falta de memoria, es muy importante contar con otros mecanismos de gestión de la memoria, como colocar datos en direcciones personalizadas, como lo permiten los punteros C. Los lenguajes que dependen en gran medida de la asignación dinámica y la recolección de basura no serían adecuados para los sistemas con recursos limitados.
Tamaño del código
C tiene un tiempo de ejecución muy pequeño. Y la huella de memoria para su código es más pequeña que para la mayoría de los otros lenguajes.
En comparación con C++, por ejemplo, un binario generado por C que va a un dispositivo integrado tiene aproximadamente la mitad del tamaño de un binario generado por un código C++ similar. Una de las principales causas de esto es el soporte de excepciones.
Las excepciones son una gran herramienta agregada por C++ sobre C y, si no se activan y se implementan de manera inteligente, prácticamente no tienen sobrecarga de tiempo de ejecución (pero a costa de aumentar el tamaño del código).
Veamos un ejemplo en C++:
// Class A declaration. Methods defined somewhere else; class A { public: A(); // Constructor ~A(); // Destructor (called when the object goes out of scope or is deleted) void myMethod(); // Just a method }; // Class B declaration. Methods defined somewhere else; class B { public: B(); // Constructor ~B(); // Destructor void myMethod(); // Just a method }; // Class C declaration. Methods defined somewhere else; class C { public: C(); // Constructor ~C(); // Destructor void myMethod(); // Just a method }; void myFunction() { A a; // Constructor aA() called. (Checkpoint 1) { B b; // Constructor bB() called. (Checkpoint 2) b.myMethod(); // (Checkpoint 3) } // b.~B() destructor called. (Checkpoint 4) { C c; // Constructor cC() called. (Checkpoint 5) c.myMethod(); // (Checkpoint 6) } // c.~C() destructor called. (Checkpoint 7) a.myMethod(); // (Checkpoint 8) } // a.~A() destructor called. (Checkpoint 9)
Los métodos de las clases A
, B
y C
se definen en otro lugar (por ejemplo, en otros archivos). Por lo tanto, el compilador no puede analizarlos y no puede saber si generarán excepciones. Por lo tanto, debe prepararse para manejar las excepciones lanzadas por cualquiera de sus constructores, destructores u otras llamadas a métodos. Los destructores no deberían lanzar (muy mala práctica), pero el usuario podría lanzar de todos modos, o podría lanzar indirectamente llamando a alguna función o método (explícita o implícitamente) que lanza una excepción.
Si alguna de las llamadas en myFunction
una excepción, el mecanismo de desenredado de la pila debe poder llamar a todos los destructores de los objetos que ya se construyeron. Una implementación para el mecanismo de desenredado de la pila utilizará la dirección de retorno de la última llamada de esta función para verificar el "número de punto de control" de la llamada que activó la excepción (esta es la explicación simple). Lo hace haciendo uso de una función auxiliar autogenerada (una especie de tabla de búsqueda) que se usará para desenrollar la pila en caso de que se produzca una excepción desde el cuerpo de esa función, que será similar a esto:
// Possible autogenerated function void autogeneratedStackUnwindingFor_myFunction(int checkpoint) { switch (checkpoint) { // case 1 and 9: do nothing; case 3: b.~B(); goto destroyA; // jumps to location of destroyA label case 6: c.~C(); // also goes to destroyA as that is the next line destroyA: // label case 2: case 4: case 5: case 7: case 8: a.~A(); } }
Si la excepción se lanza desde los puntos de control 1 y 9, ningún objeto necesita destrucción. Para el punto de control 3, b
y a
deben ser destruidos. Para el punto de control 6, c
y a
deben destruirse. En todos los casos se deberá respetar la orden de destrucción. Para los puntos de control 2, 4, 5, 7 y 8, solo se debe destruir el objeto a
.
Esta función auxiliar agrega tamaño al código. Esto es parte de la sobrecarga de espacio que C++ agrega a C. Muchas aplicaciones integradas no pueden permitirse este espacio adicional. Por lo tanto, los compiladores de C++ para sistemas integrados suelen tener un indicador para desactivar las excepciones. Deshabilitar las excepciones en C++ no es gratuito, porque la biblioteca de plantillas estándar depende en gran medida de las excepciones para informar errores. El uso de este esquema modificado, sin excepciones, requiere más capacitación para que los desarrolladores de C++ detecten posibles problemas o encuentren errores.
Y, estamos hablando de C++, un lenguaje cuyo principio es: “No pagas por lo que no usas”. Este aumento en el tamaño binario empeora para otros lenguajes que agregan una sobrecarga adicional con otras características que son muy útiles pero que los sistemas integrados no pueden permitirse. Si bien C no le brinda el uso de estas características adicionales, permite una huella de código mucho más compacta que los otros lenguajes.
Razones para aprender C
C no es un lenguaje difícil de aprender, por lo que todos los beneficios de aprenderlo serán bastante baratos. Veamos algunos de esos beneficios.
Lingua franca
Como ya se mencionó, C es una lingua franca para los desarrolladores. Muchas implementaciones de nuevos algoritmos en libros o en Internet están disponibles primero (o solo) en C por sus autores. Esto da la máxima portabilidad posible para la implementación. He visto a programadores luchando en Internet para reescribir un algoritmo C a otros lenguajes de programación porque no conocían conceptos muy básicos de C.
Tenga en cuenta que C es un lenguaje antiguo y generalizado, por lo que puede encontrar todo tipo de algoritmos escritos en C en la web. Por lo tanto, es muy probable que te beneficies de conocer este idioma.
Comprender la máquina (pensar en C)
Cuando discutimos el comportamiento de ciertas partes del código, o ciertas características de otros lenguajes, con colegas, terminamos "hablando en C:" ¿Esta parte pasa un "puntero" al objeto o copia todo el objeto? ¿Podría estar pasando algún “elenco” aquí? Y así.
Rara vez discutiríamos (o pensaríamos) acerca de las instrucciones de ensamblaje que una parte del código está ejecutando al analizar el comportamiento de una parte del código de un lenguaje de alto nivel. En cambio, cuando discutimos lo que está haciendo la máquina, hablamos (o pensamos) con bastante claridad en C.
Además, si no puede detenerse y pensar de esa manera sobre lo que está haciendo, puede terminar programando con algún tipo de superstición sobre cómo (mágicamente) se hacen las cosas.
Trabaje en muchos proyectos C interesantes
Muchos proyectos interesantes, desde grandes servidores de bases de datos o núcleos de sistemas operativos, hasta pequeñas aplicaciones integradas que incluso puede hacer en casa para su satisfacción personal y diversión, se realizan en C. No hay razón para dejar de hacer cosas que le gusten por la sola razón que no conoce un lenguaje de programación viejo y pequeño, pero fuerte y probado como C.
Conclusión
El lenguaje de programación C no parece tener fecha de caducidad. Su cercanía con el hardware, gran portabilidad y uso determinista de los recursos lo hace ideal para el desarrollo de bajo nivel para cosas tales como kernels de sistemas operativos y software integrado. Su versatilidad, eficiencia y buen desempeño lo convierten en una excelente opción para software de manipulación de datos de alta complejidad, como bases de datos o animación 3D. El hecho de que muchos lenguajes de programación hoy en día sean mejores que C para su uso previsto no significa que superen a C en todas las áreas. C sigue siendo insuperable cuando el rendimiento es la prioridad.
El mundo funciona con dispositivos alimentados por C. Usamos estos dispositivos todos los días, nos demos cuenta o no. C es el pasado, el presente y, por lo que podemos ver, sigue siendo el futuro de muchas áreas del software.