Un primer programa en C
La mayoría de los libros acerca de lenguajes de programación empiezan analizando un ejemplo sencillo, que consiste en escribir el mensaje “Hello, world!”, “¡Hola, mundo!”, en la pantalla.
Éste sería el famoso programa:
#include <stdio.h>
int main ()
{
printf("Hello, world!\n");
return(0);
}
Sin embargo, en este artículo no vamos a discutir este programa, porque ya hemos visto conceptos mucho más avanzados en artículos anteriores. Lo que vamos a hacer es analizar una versión rudimentaria en C del programa paridad.py
del artículo “Un primer programa en Python para entender cómo funciona un ordenador”, originalmente escrito en Python, comparando un poco ambos lenguajes.
El programa tomaba un número entero de la entrada estándar y nos devolvía un mensaje que expresaba si el dato de entrada era par o impar.
Por comodidad vamos a reproducir el programa paridad.py
aquí, pero esta vez ya omitiendo los comentarios:
#!/usr/bin/python3
import sys
num = int(input())
if num % 2 == 0:
print("par")
else:
print ("impar")
En C, el programa paridad.c
es el siguiente:
#include <stdio.h>
#include <stdlib.h>
/* Prorama paridad.c. Toma como
dato de entrada un entero, de
la entrada estándar, y escribe
"par" o "impar" por la salida
estándar según sea su paridad */
/* Función principal */
int main()
{
char input[10], ch;
int count = 0;
do {
ch = getchar();
input[count] = ch;
count = count + 1;
}
while (ch != '\n'
&& ch != EOF
&& count < 10);
// conversión a entero
int num = atoi(input);
if (num % 2 == 0) {
printf("par\n");
}
else {
printf("impar\n");
}
return 0;
}
Las diferencias son varias, pero vamos primero a ejecutar ambos programas para ver si son equivalentes.
Ejecutando el programa en C
Abre tu entorno de trabajo, configurado de acuerdo a las instrucciones de al artículo “Prepara tu ordenador para lo que viene”, copia el código anterior en un fichero llamado paridad.c
, y ejecuta el siguiente comando en el terminal (sólo lo que está tras $
):
$ gcc paridad.c -o paridad
Este comando ha compilado el listado en C anterior en un archivo binario llamado paridad
, a secas, que el sistema puede ejecutar directamente desde el terminal; ha traducido el listado en C a código máquina que es directamente ejecutable por el microprocesador (relativamente, como veremos en las conclusiones. Pero si lo intentamos ejecutar, recibiremos un error:
$ ./paridad
zsh: permission denied: ./paridad
Ahí están ocurriendo varias cosas.
- En primer lugar, habrás notado que, igual que en artículos anteriores, estoy anteponiendo
./
al nombre del programa:./paridad
. Esto se debe a que el sistema nunca va a presuponer que hay programas ejecutables en el directorio donde tú estás, sino en una serie de directorios predefinidos. Al anteponer./
, estamos diciendo al sistema que el programaparidad
está en el directorio actual, dondequiera que estemos trabajando, y no en la colección de directorios predefinidos. - Lo segundo, vemos que hay un ente llamado
zsh
.zsh
, leído “zosh”, es un shell (o intérprete de comandos) presente en muchos sistemas UNIX, y es el intérprete de comandos por defecto de macOS. - Por último, ese tal Zosh dice que no tenemos permiso para ejecutar
./paridad
.
Para poder ejecutarlo, debemos otorgarle permisos de ejecución. Esto se hace con el siguiente comando:
$ chmod +x paridad
Los detalles de la herramienta chmod
, que significa change file access modes, “cambia modos de acceso a ficheros”, lo veremos más adelante.
Ahora ya, por fin, podemos ejecutar la versión en C, paridad
:
$ ./paridad
45
impar
$ ./paridad
22
par
$ echo 67 | ./paridad | tr a-z A-Z
IMPAR
Ejecutando el programa en Python 3
Repitamos el ejercicio con el programa en Python, para comparar los resultados.
Guarda el listado del principio del artículo en un fichero llamado paridad.py
, y repite el proceso (recuerda: los comandos son los que empiezan por $
, pero no copies el $
):
$ chmod +x paridad.py
$ ./paridad.py
45
impar
$ ./paridad.py
22
par
$ echo 67 | ./paridad.py | tr a-z A-Z
IMPAR
Como ves, el funcionamiento es aparentemente idéntico, porque operan de igual forma con la entrada y salida estándares. Sin embargo, las apariencias engañan, como veremos a lo largo del artículo.
Diferencias de comportamiento entre los dos programas
En realidad, hay diferencias otras diferencias fáciles de experimentar, con las que puedes jugar en tu ordenador:
- El programa en C, tal y como está escrito, es menos potente y sólo interpretará los primeros 10 caracteres, ignorando todos los demás. En Python podrías introducir todos los dígitos que quisieras, hasta provocar un error de desbordamiento, que es lo que ocurre cuando el microprocesador y el propio lenguaje de programación no soporta números tan largos. Eso ocurre con muchos (muchos) más dígitos que 10. En cualquier caso, esa simplificación es consciente, en aras de mantener el ejemplo simple y no meternos en charcos de gestión de memoria en C a las primeras de cambio, que es algo que Python hace automáticamente.
- Ambas versiones del programa (Python y C) se comportarán de forma diferente si introducimos algún carácter no numérico. El programa en C ignorará todo a partir del primer carácter no numérico, proporcionando un resultado de aquello que ha logrado convertir, de forma “grácil”. El programa en Python, sin embargo, arrojará un error al no ser capaz de convertir la cadena enter a aun valor numérico entero. Esto se debe a decisiones diferentes que los respectivos programadores de
atoi()
, en el caso de C, eint()
en el caso de Python, han tomado.
Estos dos ejemplos diferencias de comportamiento que, un ejercicio serio de reescritura de un programa entre diferentes lenguajes, tendría que haber tenido en cuenta para conseguir un comportamiento idéntico incluso a la hora de procesar y gestionar los errores. Hay técnicas estándares de ingeniería del software para llevar esto a cabo y proporcionar una conversión sin fisuras, que algún día discutiremos en Micromáquina —¡no en este artículo!
Análisis del código en C
Vamos a centrarnos en el programa en C, y analizarlo paso a paso, para observar más diferencias fundamentales entre Python y C. Volvamos al listado:
#include <stdio.h>
#include <stdlib.h>
/* Programa paridad.c. Toma como
dato de entrada un entero, de
la entrada estándar, y escribe
"par" o "impar" por la salida
estándar según sea su paridad */
/* Función principal */
int main()
{
char input[10], ch;
int count = 0;
do {
ch = getchar();
input[count] = ch;
count = count + 1;
}
while (ch != '\n'
&& ch != EOF
&& count < 10);
// conversión a entero
int num = atoi(input);
if (num % 2 == 0) {
printf("par\n");
}
else {
printf("impar\n");
}
return 0;
}
Las dos primeras líneas incluyen dos librerías. De stdio.h
nos interesa las funciones getchar()
y printf()
, mientras que de stdlib.h
nos interesa la función atoi()
. Es interesante que, en C, #
no representa un comentario sino una macro de pre-procesador. No entraré en muchos detalles, pero básicamente, antes de compilar el programa, un pre-procesador sustituye las sentencias #include <librería.h>
por el contenido de librería.h
.
En C, los comentarios son las líneas envueltas en /* ... */
y, aunque se introdujeron realmente con C++, también son comentarios aquellas líneas que empiezan con //
. Las líneas siguientes son comentarios:
/* Programa paridad.c. Toma como
dato de entrada un entero, de
la entrada estándar, y escribe
"par" o "impar" por la salida
estándar según sea su paridad */
/* Función principal */
El primer bloque es un comentario multilínea, y el segundo un comentario de una sóla línea. Para comentarios mutlilínea, muchos programadores y entornos de programación aplican un estilo más elaborado con los asteriscos, como el siguiente, por legibilidad, pero no es técnicamente necesario.
/**
* Programa paridad.c. Toma como
* dato de entrada un entero, de
* la entrada estándar, y escribe
* "par" o "impar" por la salida
* estándar según sea su paridad
*/
La línea // conversión a entero
es, también, un comentario de una sola línea. El marcado de estilo //
no permite hacer comentarios multilínea: si tenemos mucho que decir, debemos incluír //
al principio de cada línea, o si no la compilación fallará.
int main()
es la función principal del programa y es el punto que el sistema operativo va a utilizar para entrar en nuestro programa. Cualquier código en lenguaje C tiene que estar escrito dentro de una función, así que, como mínimo, un programa en C consta de la función main()
como en el ejemplo.
La función main()
declara un tipo de datos de salida de tipo entero: int main()
, y tiene su contenido entre llaves. Al declarar int
como tipo de datos devuelto, debemos devolver un valor entero, que en el programa hacemos con la sentencia return 0;
del final.
Con los valores devueltos por main
estamos comunicando al sistema operativo si el resultado fue correcto o si, por el contrario, el programa no pudo realizar su trabajo por alguna condición errónea. Por convenio, si devolvemos 0 estamos declarando una ejecución sin errores, y si devolvemos valores diferentes de 0, estamos comunicando algún error. En nuestro ejemplo no hemos realizado ningún tipo de gestión de errores, y por lo tanto devolvemos 0 en cualquier caso.
En este punto hemos discutido la estructura básica de un programa en C ejecutable. El siguiente listado es un programa en C que no hace nada, y termina correctamente (“no hace nada, correctamente” en lugar del muy negativo “no hace nada correctamente” 😬):
int main() {
return 0;
}
Aunque en este artículo, main()
no toma parámetros, los puede tomar como veremos más adelante.
Las dos siguientes líneas declaran variables para poder trabajar:
char input[10], ch;
int count = 0;
La primera de ellas, char input[10], ch;
declara dos variables de tipo char
, carácter, en la misma línea.
- La primera de las variables,
input[10]
nombra una zona de memoria de 10 posiciones en total, cada una de ellas de tipochar
. Esta formación se suele llamar en inglés array (“formación” o “arreglo”). Funciona de tal forma que si escribimosinput
en el programa nos referimos a la formación entera, pero si escribimosinput[0]
al primer elemento,input[1]
al segundo,... einput[9]
al último; las posiciones de los arrays empiezan en 0 y terminan en la longtud declarada menos 1. - La segunda de las variables,
ch
, es un único carácter.
Podríamos haber declarado cada variable en una línea:
char input[10];
char ch;
Pero es una práctica común declarar variables similares en una misma línea.
La siguiente variable declarada es de tipo entero, int
, y está inicializada: le damos el valor inicial 0 en el momento de declararla.
int count = 0;
Es una buena práctica inicializar las variables, siempre, a un valor que no represente información. Por ejemplo, a un valor con el que podamos detectar un error de funcionamiento en el código que la va a utilizar; de esa manera, si el programa fracasa, lo sabremos porque veremos que el valor de la variable no ha cambiado. Además, si no inicializamos las variables corremos el riesgo de escribir programas con funcionamiento errático en algunos casos especiales. Más adelante, en Micromáquina, veremos técnicas e información que nos ayudarán a decidir valores adecuados para inicializar variables.
A estas alturas, probablemente habrás observado que, salvo la estructura de código que demarca la función main
propiamente dicha, todas las líneas de código terminan en ;
. Esto es obligatorio en C para las sentencias de programa, pero no para las macros de pre-procesador (#include...
). En Python no es obligatorio, pero no hace daño: se puede usar si, con ello, nos sentimos mejor.
Vamos ahora con esta construcción:
do {
ch = getchar();
input[count] = ch;
count = count + 1;
}
while (ch != '\n'
&& ch != EOF
&& count < 10);
Esto es un bucle llamado “do... while” y lo que hace es realizar las operaciones que van entre llaves en el miembro do {...}
mientras que se cumplan las condiciones que van entre paréntesis en el miembro while (...)
. Lo que va entre las llaves de do {...}
se llama cuerpo del bucle.
Traduciéndolo al español y explicando el significado de las sentencias, esto es lo que hace el bucle:
- haz lo siguiente:
- asigna a
ch
el resultado de obtener un carácter de la entrada estándar ( que es lo que hacegetchar()
) - asigna al elemento número
count
del arrayinput
el valor dech
- incrementa el valor de
count
en una unidad (count = count + 1;
)
- asigna a
- mientras al terminar cada pasada se cumpla que:
- el valor de
ch
sea diferente a '\n', que representa la tecla intro (⏎), - y el valor de
ch
sea diferente aEOF
, que representa la condición de fin de fichero (que explicaremos a continuación), - y el valor de
count
sea estrictamente menor que 10 (de forma que evitamos salirnos del arrayinput
).
- el valor de
Así pues, si cuando ejecutamos ./paridad
escribimos “45⏎”, y sabiendo que ⏎ en C se escribe “\n”, ésta es la sucesión de operaciones de ese bucle:
count |
ch |
input |
¿Seguimos? | |
---|---|---|---|---|
primera pasada | 0 | '4' |
['4'] |
Sí |
segunda pasada | 1 | '5' |
['4', '5'] |
Sí |
tercera pasada | 2 | '\n' |
['4', '5', '\n' |
No ('\n' ) |
Es importante mencionar que el bucle “do ... while” se ejecuta siempre, al menos, una vez.
Vamos con EOF
:
EOF
, o fin de fichero, es una condición frecuente a la hora de procesar datos en sistemas tipo Unix (como macOS y GNU/Linux), y aunque se refiere claramente a ficheros, también está presente en la entrada estándar.- En UNIX existe un principio de diseño que establece que “todo” se debe comportar como si fuera un fichero.
- Siguiendo este principio, la entrada estándar se ha diseñado de tal forma que se comporta como si fuera un fichero.
En nuestro caso, que procesamos la entrada estándar, este principio ayuda a gestionar condiciones muy variopintas de la misma forma, sin cambiar nuestro programa, y sin tener que escribir catedrales de código sólo para poder recibir datos. Por ejemplo, nuestro programa funciona igual tanto si un programa vuelca un fichero a la entrada estándar, como si un usuario teclea, o como si hacemos un pipe desde la salida estándar de otro programa (echo 5 | ./paridad
). El código en C y en Python va a ser el mismo, y eso ayuda.
Como curiosidad, originalmente (cuando todo esto se inventó), y en la vasta mayoría de entornos actuales, se puede producir EOF
con el teclado pulsando CONTROL + D. Sin embargo, no siempre funciona de la misma manera porque hay distintas aplicaciones de terminal que pueden estar usando esa misma combinación de teclas para otras cosas.
Vamos con la siguiente sentencia del programa:
// conversión a entero
int num = atoi(input);
Como menciona el comentario, atoi
toma un array de caracteres y devuelve un número entero. Lo hace interpretando las posiciones del array que tienen caracteres numéricos, y se detiene cuando encuentra el primer carácter no numérico. En nuestra ejecución, sustituyendo las variables por sus valores, ésto es lo que ocurre:
atoi(['4', '5', '\n']) → 45
Además, en la línea int num = atoi(input);
estamos declarando una variable de tipo entero y nombre num
. En C, las variables se pueden declarar en la misma sentencia que se utilizan por primera vez, aunque no es muy elegante. Es mejor, y más legible, declarar las variables primero y lo más arriba posible, e inicializarlas. Lo incluyo en el ejemplo porque, seguro, te encontrarás con muchas construcciones así.
La programación no es una competición para ver quién resuelve un problema con menos líneas de código. Un programa bien escrito, y legible, demuestra preocupación por compartir conocimientos, hacer la vida más fácil a quien herede tu código, y en general contribuye a hacer del mundo un lugar mejor. Aunque veas que a tu alrededor mucha gente se lo tome así, no te dejes llevar por ello.
A continuación, y para terminar el programa, tenemos la sentencia de control “if... else” y la operación “resto de la división entera” que discutimos en el artículo anterior. Es lo suficientemente parecida a la versión en Python como para no entrar en demasiados detalles. Lo único que comentaré es que, cuando el cuerpo de las ramas del if
y del else
sólo tienen una sentencia, es decir, una sóla línea como en el ejemplo, las llaves se pueden omitir. Podríamos haber escrito lo siguiente:
if (num % 2 == 0)
printf("par\n");
else
printf("impar\n");
El resultado es más compacto. En estos casos, usar o no las llaves es una mera cuestión de estilo que no tiene ninguna implicación técnica ni de rendimiento.
Y con esto terminamos el análisis del programa.
Conclusiones y algunas otras curiosidades
Python es un lenguaje interpretado, mientras que C es compilado. Esto puede parecer un detalle menor, que afecta solamente a cómo trabajamos con ellos, pero entre muchas otras implicaciones, tiene una que es fundamental: en C se pueden programar sistemas operativos, y en Python no.
- La razón es que el proceso de compilación de lenguajes como C producen archivo que contienen directamente instrucciones de microprocesador. En Python, al no tener compilación, necesitamos siempre un intérprete que se instala en un sistema operativo, en general.
- Eso no quiere decir que exista una forma práctica de ejecutar la versión en lenguaje C de
paridad
, o el “Hello, world!” en un ordenador sin sistema operativo; la forma de programar, construir y compilar sistemas operativos es algo que no se parece a lo que hemos visto en este artículo: que no se hace igual. Sin embargo, la capacidad está ahí, y la inmensa mayoría de los sistemas operativos modernos están escritos en C o en sus sucesores (como C++ y Objective C). - Recientemente han salido al mercado ciertos dispositivos con orientación educativa que admiten código en Python directamente, pero el secreto que tienen es que incorporan un intérprete de Python en un chip de memoria de sólo lectura. Este es el mismo enfoque que encontrábamos con el lenguaje BASIC, también interpretado, en microordenadores de los años 1970 y 1980 como los Spectrum y los Commodore. Hablaré de estos temas en otros artículos, porque tengo mucha nostalgia de aquella época 👴.
Python y C tienen sintaxis parecidas; el código de uno recuerda al otro, y de hecho Python está influído fuertemente por C. Pero, al mismo tiempo, tienen diferencias grandes. Por ejemplo, algo que no hemos comentado en ninguno de los dos artículos es la decisión entre usar llaves en C y la indentación, o sangrado, de las líneas en Python. En Python es importante hasta el punto que el intérprete de Python se negará a ejecutar el código si éste está mal sangrado. El C, sin embargo, es un lenguaje delimitado. El usar llaves para delimitar cuerpos, y el punto y coma mara marcar el fin de las sentencias, resuelve cualquier ambigüedad al compilador, en tanto en cuanto a si una sentencia pertenece a un bucle, o a los miembros “if” o “else”, o a cualquier otro cuerpo. Por lo tanto, en C el sangrado del código es totalmente opcional y una cuestión puramente estética y de decencia; el siguiente programa en C es absolutamente ilegible, pero compila y se ejecuta perfectamente:
#include <stdio.h>
#include <stdlib.h>
int main() { char input[10], ch; int count = 0;do {ch = getchar();input[count] = ch;count = count + 1;}while (ch != '\n' && ch != EOF && count < 10);int num = atoi(input);if ( num % 2 == 0) {printf("par\n");} else {printf("impar\n");}return 0;}
Si intentas hacer lo mismo en Python, no funcionará de ninguna manera. Aunque en C se pueda omitir la mayoría de reglas y buenas prácticas de estilo, no se debe hacer, como ya se discutió antes en este artículo.
Ejercicios
Envía tu respuestas, si quieres, escribiendo en Mastodon a @gabriel@fedi.gvisoc.com y @gabriel@micromaquina.com, con un enlace a este artículo y tus respuestas. Te contestaré en privado.
- Ejecuta
./paridad
y, sin introducir ningún dato, pulsa intro. ¿Cómo explicas la salida del programa en C?, ¿y la del programa en Python? - Experimenta con
./paridad.py
y./paridad
y trata de encontrar todos las diferencias en su comportamiento. ¿Hay alguna forma práctica de hacer que la versión en C,./paridad
, falle por un error interno que le impida terminar su trabajo? - Usando sentencias de control “if... else” y las variables que consideres oportunas, trata de conseguir reducir las diferencias entre ambas versiones (implementaciones) todo lo posible.
👉 Sigue a micromáquina en @gabriel@micromaquina.com 👉 Sigue a Gabriel Viso en @gabriel@fedi.gvisoc.com