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.

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:

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.

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:



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']
segunda pasada 1 '5' ['4', '5']
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:

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.

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.

  1. 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?
  2. 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?
  3. 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.

#C #Python

👉 Sigue a micromáquina en @gabriel@micromaquina.com 👉 Sigue a Gabriel Viso en @gabriel@fedi.gvisoc.com