Original article: The C Beginner's Handbook: Learn C Programming Language basics in just a few hours

Este manual sigue la regla 80/20. Aprenderás 80% del lenguaje de programación C en 20% del tiempo.

Este enfoque te dará una visión amplia del lenguaje.

Este manual no trata de cubrir todo lo relacionado con C, sino que se enfoca en el núcleo del lenguaje, tratando de simplificar los temas más complejos.

Puedes tener la versión PDF y ePub de este manual en inglés acá (hecha por el autor original)

¡Disfruta!

Tabla de Contenidos

  1. Introducción a C
  2. Variables y tipos
  3. Constantes
  4. Operadores
  5. Condicionales
  6. Bucles,
  7. Arréglos
  8. Strings (Cadenas de texto)
  9. Punteros
  10. Funciones
  11. Entrada y Salida
  12. Alcance de las variables
  13. Variables estáticas
  14. Variables Globales
  15. Definiciones de tipos
  16. Tipos enumerados
  17. Estructuras
  18. Parámetros de línea de comandos
  19. Archivos de Cabecera
  20. Preprocesador
  21. Conclusión

Introducción a C

C es probablemente el lenguaje de programación más conocido. Es usado como un lenguaje de referencia para cursos de programación alrededor de todo el mundo, y es probablemente el lenguaje más aprendido por las personas junto con Python y Java.

Recuerdo que fue mi segundo lenguaje de programación después de Pascal.

C no es sólo algo que usan los estudiantes para aprender programación. No es un lenguaje académico. Y diría que no es el lenguaje más fácil, porque C es un lenguaje de programación de bajo nivel.

Al día de hoy, C es ampliamente usado en dispositivos integrados, y alimenta la mayoría de los servidores de internet, los cuales están construidos usando Linux. El Kernel de Linux está construido usando C, esto significa también que alimenta el núcleo de todos los dispositivos Android. Podríamos decir que justo en este momento, el código C se ejecuta en un gran número de aplicaciones en el mundo entero. Lo cual es bastante notable.

Cuando fue creado, C se consideraba un lenguaje de alto nivel, porque era portable entre máquinas. Ahora damos por sentado que podemos ejecutar un programa escrito en una Mac, Windows o Linux, usando tal vez Node.js o Python.

Hace algún tiempo, esto simplemente no era el caso. Lo que C trajo a la mesa era un lenguaje simple de implementar y que tenía un compilador que podía ser fácilmente portado a diferentes máquinas.

Dije un compilador: C es un lenguaje compilado, como Go, Java, Swift o Rust. Otros lenguajes de programación populares como Python, Ruby o JavaScript son interpretados. La diferencia consiste en: un lenguaje compilado genera un archivo binario que puede ser directamente ejecutado y distribuido.

C no tiene recolección de basura. Esto significa que tenemos que manejar la memoria por nuestra cuenta. Es una tarea compleja, y una que requiere mucha de nuestra atención para prevenir bugs, pero esto hace también que C sea ideal para escribir programas para dispositivos integrados como Arduino.

C no esconde la complejidad y capacidades de la máquina debajo. Tienes mucho poder, una vez que sabes qué puedes hacer.

Quiero presentarte el primer programa C, el cual llamaremos ¡"Hola Mundo!"

hola. c

#include <stdio.h>

int main(void) {
    printf("Hola Mundo!");
}

Describamos el código del programa: primero importamos la librería stdio (el nombre significa librería estándar de entradas y salidas).

Esta librería nos da acceso a las funciones I/O, es decir de entrada y salida (input/output).

C es un lenguaje muy pequeño en su núcleo, y cualquier cosa que no sea parte del núcleo se provee a través de librerías. Algunas de estas librerías están construidas por programadores normales que las habilitan para que otros los usen. Algunas librerías están integradas en el compilador. Como stdio y otras.

stdio es la librería que provee la función printf().

Esta función está envuelta en la función main(), que es el punto de entrada de cualquier programa en C.

Pero entonces, ¿Qué es una función?

Una función es una rutina que toma uno o más argumentos, y devuelve un valor.

En el caso de la función main() no recibe argumentos, y retorna un entero. Identificamos esto usando la palabra clave void para el argumento, y la palabra clave int para el valor de retorno.

La función tiene un cuerpo, el cual está entre los paréntesis de llave. Dentro del cuerpo tenemos todo el código que la función necesita para realizar sus operaciones.

La función printf() está escrita de forma diferente, como podrás ver. Esta no tiene un valor de retorno definido, y le pasamos una cadena de texto entre comillas dobles. Nosotros no especificamos el tipo de argumento.

Esto es porque esta es una invocación de función. En algún lugar, dentro de la librería stdio, la función printf está definida como:

int printf(const char *format, ... );

No necesitas entender qué significa esto por ahora, pero, en resumen, esta es la definición. Y cuando llamamos a printf("Hola Mundo!"); ahí es donde se ejecuta la función.

La función main() que definimos arriba:

#include <stdio.h>

int main(void) {
    printf("¡Hola Mundo!");
}

será ejecutada por el sistema operativo cuando a su vez, el programa sea ejecutado.

¿Cómo ejecutamos un programa C?

Como mencioné, C es un lenguaje compilado. Para ejecutar el programa debemos compilarlo primero. Cualquier computador con Linux o MacOS ya viene con un compilador C integrado. Para Windows, puedes usar el subsistema de Windows para Linux (WSL).

Cuando abras la terminal puedes escribir gcc, y este comando debería retornar un error diciendo que no especificaste un archivo:

imagen-1
Linux Ubuntu Terminal

Esto está bien. Significa que el compilador está ahí, y podemos comenzar a usarlo.

Ahora escribiremos el programa de arriba en el archivo hola.c. Puedes usar cualquier editor, para mantener la simplicidad usaré el editor nano en la línea de comandos:

imagen-2

Escribe el programa:

imagen-3

Ahora presiona ctrl-x para salir:

imagen-4

Confirma presionando la tecla S, y luego presiona enter para confirmar el nombre del archivo:

imagen-5

Esto de debería traer de vuelta a la terminal:

imagen-6

Ahora escribe

gcc hola.c -o hola
imagen-7

Esto debería haber generado un ejecutable hola. Ahora escribe ./hola para ejecutarlo:

imagen-8

Antepongo ./ al nombre del programa para decirle al terminal que el comando es para el directorio actual.

¡Impresionante!

Ahora si invocas ls -al hola, verás que el programa es de tan solo 16KB:

imagen-9

Este es uno de los pros de C: está altamente optimizado, esta también, es una de las razones por las que es bueno para sistemas integrados que tienen una cantidad limitada de recursos.

Variables y tipos

C es un lenguaje de tipo estático.

Esto significa que cualquier variable tiene un tipo asociado, y si tipo es conocido al momento de la compilación.

Esto es muy distinto a como trabajamos con variables en Python, JavaScript, PHP y otros lenguajes interpretados.

Cuando creas una variable en C, tienes que especificar el tipo de variable en la declaración.

En este ejemplo iniciamos una variable edad con el tipo int:

int edad;

Un nombre de variable puede tener mayúsculas, minúsculas, puede contener dígitos y el guion bajo, pero no puede empezar con un dígito. EDAD y Edad10 son nombres de variables válidos, 1edad no lo es.

También puedes iniciar la declaración de una variable especificando su valor inicial:

int edad = 37;

Una vez que declaras una variable podrás usarla en el código de tu programa. Puedes cambiar su valor en cualquier momento, usando el operador =, por ejemplo en edad=100; (siempre que el nuevo valor sea del mismo tipo).

En este caso:

#include <stdio.h>

int main(void) {
    int edad = 0;
    edad = 37.2;
    printf("%u", edad);
}

El compilador lanzará una advertencia en tiempo de compilación, y convertirá el número decimal en un número entero.

Los tipos de datos integrados en C son int, char, short, long, float, double y long double. Aprendamos más sobre ellos.

Números enteros

C nos provee los siguientes tipos para definir valores enteros:

  • char
  • int
  • short
  • long

La mayoría del tiempo es probable que uses int para almacenar enteros. Pero en algunos casos podrías querer usar una de las otras 3 opciones.

El tipo char es comúnmente usado para almacenar letras de la tabla ASCII, pero puede ser usado para guardar pequeños enteros desde -128 a 127. Esto usa al menos 1 byte.

int usa al menos 2 bytes. short usa al menos 2 bytes. long usa al menos 4 bytes.

Como puedes ver, no se nos garantiza los mismos valores para diferentes entornos. Sólo tenemos una indicación. El problema es que el número exacto de bits que puede ser almacenado en cada tipo de dato depende de la implementación y arquitectura.

Tenemos garantizado que short no es más grande que int. Y tenemos garantizado que long no es más pequeño que int.

El estándar de especificaciones ANSI C determina los valores mínimos de cada tipo, y gracias a este, podemos al menos saber cuál es el valor mínimo que podemos esperar tener a nuestra disposición.

Si estás programando C en un Arduino, diferentes placas tendrán diferentes límites.

En una placa Arduino Uno, int almacena un valor de 2 byte, con rangos desde -32,768 a 32,767. En Arduino MKR 1010 int almacena un valor de 4 bytes, con rangos desde -2,147,483,648 a 2,147,483,647. Una gran diferencia.

En todas las placas Arduino, short almacena valores de 2 bytes, con rangos desde -32,768 a 32,767. long almacena 4 bytes, con rangos desde -2,147,483,648 a 2,147,483,647.

Enteros sin signo

Para todos los tipos de datos arriba, podemos anteponer unsigned para comenzar el rango desde 0, en vez de números negativos. Esto podría tener sentido en muchas formas.

  • unsigned char tiene un rango de 0 hasta al menos 255
  • unsigned int tiene un rango de 0 hasta al menos 65,535
  • unsigned short tiene un rango de 0 hasta al menos 65,535
  • unsigned long tiene un rango de 0 hasta al menos 4,294,967,295

Problema con el desbordamiento

Dados todos estos límites puede surgir una duda: ¿Cómo podemos estar seguros de que nuestros números no excederán los límites? Y ¿Qué pasa si excedimos estos límites?

Si tienes un número de tipo unsigned int en 255, y lo incrementas, tendrás 256 de retorno, como podrías esperar. Si tienes un número de tipo unsigned char en 255, y lo incrementas, obtendrás un 0 de retorno. Este se resetea empezando desde el posible valor inicial.

Si tienes un número de tipo unsigned char en 255 y luego sumas 10 a él, obtendrás el número 9:

#include <stdio.h>

int main(void) {
    unsigned char j = 255;
    j = j + 10:
    printf("%u", j); /*9*/
}

Si no tienes un valor con signo, el comportamiento es indefinido. Esto básicamente te dará un número gigante que puede variar, como en este caso:

#include <stdio.h>

int main(void) {
    char j = 127;
    j = j + 10;
    printf("%u", j); /* 4294967177 */
}

En otras palabras, C no te protege de salirte de los límites de un tipo. Tienes que hacerte cargo por ti mismo.

Advertencias cuando declaras el tipo equivocado

Cuando declaras una variable y la inicializas con el valor equivocado, el compilador gcc (el que probablemente estés usando) debería advertirte:

#include <stdio.h>

int main(void) {
    char j = 1000;
}
imagen-10

Y también te advierte en asignaciones directas:

#include <stdio.h>

int main(void) {
    char j;
    j = 1000;
}
imagen-11

Pero no te advertirá si aumentas el número usando +=, por ejemplo:

#include <stdio.h>

int main(void) {
    char j = 0;
    j += 1000;
}

Números de punto flotante

Los tipos de punto flotante (float) pueden representar un conjunto de valores mucho más grandes de lo que pueden hacer los enteros (int), también pueden representar fracciones, algo que los enteros no pueden hacer.

Al usar números de punto flotante, representamos lo números como números decimales multiplicados por potencias de 10.

Tal vez veas números de punto flotante escritos como:

  • 1.29e-3
  • -2.3e+5

y de otras formas aparentemente extrañas.

Los siguientes tipos:

  • float
  • double
  • long double

son usados para representar números con punto decimal (tipos de punto flotante). Todos ellos pueden representar números positivos y negativos.

Los requerimientos mínimos para cualquier implementación en C es que float puede representar un rango entre 10^-37 y 10^+37, y típicamente es implementado usando 32 bits. double puede representar conjuntos más grandes de números. long double puede tener incluso más números.

Las cifras exactas, al igual que con los números enteros, dependen de la implementación.

En una Mac moderna, un float es representado en 32 bits, y tiene una precisión de 24 bits significativos, de los cuales 8 bits son usados para codificar el exponente.

Un número double se representa en 64 bits, con una precisión de 53 bits significativos, 11 bits de los cuales son usados para codificar el exponente.

El tipo long double es representado en 80 bits, tiene una precisión de 64 bits significativos. 15 bits son usados para codificar el exponente.

En tu equipo, ¿cómo puedes determinar el tamaño específico de los tipos? Puedes escribir un programa para hacer eso:

#include <stdio.h>

int main(void) {
  printf("char size: %lu bytes\n", sizeof(char));
  printf("int size: %lu bytes\n", sizeof(int));
  printf("short size: %lu bytes\n", sizeof(short));
  printf("long size: %lu bytes\n", sizeof(long));
  printf("float size: %lu bytes\n", sizeof(float));
  printf("double size: %lu bytes\n", 
    sizeof(double));
  printf("long double size: %lu bytes\n", 
    sizeof(long double));
}

En mi sistema, Windows moderno, imprime:

char size: 1 bytes
int size: 4 bytes
short size: 2 bytes
long size: 8 bytes
float size: 4 bytes
double size: 8 bytes
long double size: 16 bytes

Constantes

Hablemos ahora de las constantes:

Una constante se declara de forma similar a las variables, excepto que anteponemos la palabra reservada const y siempre tienes que especificar un valor.

De la siguiente forma:

const int age = 37;

Esto es C perfectamente válido, aunque es común declarar las constantes con mayúsculas, así:

const int AGE = 37;

Esta es sólo una convención, pero una que puede ayudarte mucho mientras escribes o lees un programa C, ya que mejora su legibilidad. Los nombres en mayúscula significan constantes, nombres en minúsculas significas variables.

Un nombre de constante sigue las mismas reglas que los nombres de variables: pueden contener letras mayúsculas o minúsculas, pueden contener dígitos y el guión bajo (_), pero no pueden comenzar con un dígito. EDAD y Edad10 son nombres de variables válidos, 1EDAD no lo es.

Otra forma de definir constantes y mediante el uso de la siguiente sintaxis:

#define EDAD 37

En este caso, tu no necesitas agregar el tipo, y tampoco necesitas agregar el signo igual =, y omites el punto y coma al final.

El compilador C inferirá el tipo del valor especificado en tiempo de compilación.

Operadores

C ofrece una amplia variedad de operadores que podemos usar para operar en los datos.

En particular, podemos definir varios grupos de operadores:

  • operadores aritméticos
  • operadores de comparación
  • operadores lógicos
  • operadores de asignación compuestos.
  • operadores bitwise
  • operadores de punteros
  • operadores de estructura
  • operadores misceláneos

En esta sección voy a detallar todos ellos, usando dos variables imaginarias a y b como ejemplos.

Mantendré los operadores bitwise, operadores de estructura y punteros fuera de esta lista por simplicidad

Operadores aritméticos

En este macro-grupo separaré operadores binarios y unarios.

Los operadores binarios utilizan dos operandos:

Operator Name Example
= Assignment a = b
+ Addition a + b
- Subtraction a - b
* Multiplication a * b
/ Division a / b
% Modulo a % b

Los operadores unarios toman sólo un operando:

Operator Name Example
+ Unary plus +a
- Unary minus -a
++ Increment a++ or ++a
-- Decrement a-- or --a

La diferencia entre a++ y ++a es que a++ incrementa la variable a después de usarla. ++a incrementa la variable antes de usarla.

Por ejemplo:

int a = 2; 
int b; 
b = a++ /* b es 2, a es 3*/ 
b = ++a /* b es 4, a es 4*/
Ejemplo

Lo mismo aplica para el operador decremento.

Operadores de comparación

Operator Name Example
== Equal operator a == b
!= Not equal operator a != b
> Bigger than a > b
< Less than a < b
>= Bigger than or equal to a >= b
<= Less than or equal to a <= b

Operadores lógicos

  • ! No (NOT, ejemplo: !a)
  • && Y (AND, ejemplo: a && b)
  • || O (OR, ejemplo: a || b)

Estos operadores son geniales cuando trabajamos con valores booleanos.

Operadores de asignación compuestos

Estos operadores son útiles para realizar asignaciones y a la vez realizar cálculos aritméticos:

Operator Name Example
+= Addition assignment a += b
-= Subtraction assignment a -= b
*= Multiplication assignment a *= b
/= Division assignment a /= b
%= Modulo assignment a %= b

Operador Ternario

El operador ternario es el único operador en C que trabaja con 3 operandos, y es una forma corta de expresar condicionales.

Así luce:

<condición> ? <expresión> : <expresión>

Ejemplo:

a ? b : c

Si a retorna true, entonces se ejecuta b, de otra forma se ejecuta c.

El operador ternario es funcionalmente igual que un condicional if/else, excepto que su expresión es más corta y que se puede insertar en una expresión.

sizeof

El operador sizeof retorna el tamaño del operando que pases cómo argumento. Puedes pasar una variable, o incluso un tipo.

Ejemplos de uso:

#include <stdio.h> 
int main(void) { 
	int edad = 37; 
    print("%ld\n", sizeof(edad)); 
    print("%ld", sizeof(int)); 
}

Precedencia de operadores

Con todos estos operadores (y más, que no he cubierto en este artículo, incluyendo bitwise, operadores de estructura, y operadores de puntero), debemos poner atención cuando los usamos juntos en una única expresión.

Supón que tienes esta operación:

int a = 2; 
int b = 4; 
int c = b + a * a / b - a;

¿Cuál es el valor de c? ¿Acaso se ejecuta la adición antes que la multiplicación y la división?

Hay un conjunto de reglas que nos ayudarán a resolver este acertijo.

En orden de menor a mayor precedencia, tenemos:

  • el operador de asignación =
  • los operadores binarios + y -
  • los operadores * y /
  • los operadores unarios + y -

Los operadores también tienen la regla de asociatividad, la cual siempre es de izquierda a derecha, excepto por los operadores unarios y el operador de asignación.

En:

   int c = b + a * a / b - a

Primero ejecutamos a * a / b, del cual, dada la regla de izquierda-a-derecha, podemos separarlo en a * a y el resultado dividirlo entre /b: 2* 2 = 4, 4 / 4 = 1.  

Luego podemos realizar la suma y la sustracción: 4 + 1 - 2. El valor de c es 3.

En cualquier caso, sin embargo, querrás asegurarte de darte cuenta de que puedes usar paréntesis para hacer que cualquier expresión sea más fácil de leer y de comprender.

Los paréntesis tienen prioridad más alta sobre cualquier cosa.

La expresión de más arriba puede ser reescrita como:

int c = b + ((a * a) / b) - a;

Ahora no tenemos que pensar tanto en ella.

Condicionales

Cualquier lenguaje de programación brinda al programador opciones y la habilidad de realizar tomar decisiones.

Queremos hacer X en algunos casos, Y en otros casos.

Queremos verificar datos, y tomar decisiones en base al estado de esos datos.

C nos ofrece dos formas de hacerlo.

Primero está la declaración if, con su ayudante else, y la segunda es la declaración switch.

if

En una declaración if, puedes verificar si una confición es verdadera, y luego ejecutar el código que está entre los paréntesis de llave:

int a = 1; 
if (a == 1) { 
  /* haz algo*/ 
}

Puedes agregar un bloque else para ejecutar código distinto, si en la condición original el argumento se evalúa como falso:

int a = 1; 
if (a == 2) { 
   /* haz algo */ 
} else { 
   /* haz esto otro */ 
}

Debes tener cuidado con una causa común de errores: siempre usa el operador de comparación == cuando compares, y no el operador de asignación =. Si no lo haces, el condicional if siempre será verdad, a menos que el argumento sea 0, por ejemplo si haces:

int a = 0; 
if (a = 0 ) { 
  /* nunca será invocado*/ 
}

¿Porqué pasa esto? Porque el operador condicional buscará un resultado booleano (el resultado de la comparación), y el número 0 siempre equivale a un valor falso. Todos los demás números equivalen a verdadero, incluyendo los números negativos.

Puedes tener múltiples bloques else agrupando varias sentencias if:

int a = 1; 
if (a == 2) { 
  /* haz algo */ 
} else if ( a == 1 ) { 
  /* es otra cosa */ 
} else { 
  /* es esta otra cosa */ 
}

switch

Si necesitas hacer muchos bloques if/else/if para realizar una verificación, tal vez necesites revisar el valor exacto de una variable, entonces switch puede ser muy útil para ti.

Puedes proveer variables como condiciones, y una serie de puntos de entrada para cada caso (case) esperado:

int a = 1; 
switch (a) { 
  case 0: 
    /* hace algo si a = 0 */ 
  	break; 
  case 1: 
    /* hace otra cosa si a = 1 */ 
    break; 
  case 2: 
    /* hace otra cosa si a = 2 */ 
    break; 
}

Necesitamos la palabra reservada break al final de cada caso para evitar que el siguiente caso sea ejecutado cuando el anterior haya terminado. Este efecto "cascada" puede ser útil en algunas formas creativas.

Puedes agregar un "comodín" al final, etiquetado como default:

default: int a = 1; 
switch (a) { 
  case 0: 
    /* hace algo si a = 0 */ 
    break; 
  case 1: 
    /* hace otra cosa si a = 1 */ 
    break; 
  case 2: 
    /* hace otra cosa si a = 2 */ 
    break; 
  default: 
    /* Aquí se manejan todos los otros casos */ 
    break; 
 }

Bucles

C nos ofrece tres formas de ejecutar bucles o también conocidos como ciclos: ciclo for, ciclo while y ciclo do while. Ellos te permiten iterar sobre arrays, pero con algunas diferencias. Veámoslas en detalle.

Ciclo for

El primero y probablemente la forma más común de realizar un ciclo es el ciclo for.

Al usar la palabra reservada for podemos definir las reglas del bucle por adelantado, y luego definimos el bloque de código que se ejecutará repetidamente.

Algo como esto:

for (int i = 0 ; i <= 10 ; i++){ 
  /* instrucciones que serán repetidas */ 
}

El bloque (int i = 0; i<= 10; i++) contiene 3 partes de los detalles del bucle:

  • La condición inicial (int i = 0)
  • La prueba (i <= 10)
  • El incremento (i++)

Primero definimos la variable del bucle, en este caso i.  i es un nombre de variable común usado en los bucles, junto con j para los ciclos anidados (un bucle dentro de un bucle), lo cual es sólo una convención.

La variable es inicializada en el valor 0, y se realiza la primera iteración. Luego esta es incrementada según dice la parte del incremento (i++ en este caso, incrementando por 1), y todo el ciclo se repite hasta que tienes el número 10.

Dentro del bloque principal del bucle, podemos acceder a la variable i para saber en qué iteración vamos. Este programa imprimirá en pantalla 0 1 2 3 4 5 6 7 8 9 10:

for (int i = 0; i <= 10; i++) { 
  /* Instrucciones a repetir */ 
  printf("%u ", i); 
}

Los ciclos también pueden empezar desde un número más grande y descender a uno más pequeño, como este:

for (int i = 10; i > 0; i--) { 
  /* instrucciones a repetir */ 
}

Además puedes incrementar la variable de iteración por 2 o cualquier otro valor :

for (int i = 0; i < 1000; i = i+30){ 
  /* instrucciones a repetir */ 
}

Ciclo While

El ciclo while es más simple de escribir que un ciclo for, porque requiere un poco más de trabajo de tu parte.

En vez de definir toda la información del loop arriba, cuando inicias el loop, como lo harías en un ciclo for, al usar while sólo verificas una condición:

while ( i < 10){ 
  /* hace algo cuando i es menor que 10 */ 
}

Esto asume que i ya está definido e inicializado con un valor.

Y este bucle será un ciclo infinito a menos que incrementes la variable i en algún punto dentro del ciclo. Un ciclo infinito es malo porque este bloqueará el programa, sin permitir que ocurran otras cosas.

Esto es lo necesario para un ciclo while "correcto":

int i = 0; 
while ( i < 10 ) { 
  /* haz algo */ 
  i++; 
}

Hay una excepción a esto, la veremos en un minuto. Antes déjame presentarte do while

Ciclo Do while

Los ciclos while son geniales, pero existirán veces en las que necesitarás hacer una cosa particular: Tú quieres que siempre se ejecute un bloque, y luego tal vez repetirlo.

Esto se hace usando la palabra clave do while. De una forma muy similar al ciclo while, pero con una leve diferencia:

int i = 0; 
do { 
  /* hace algo */ 
  i++; 
} while (i < 10);

El bloque que contiene el comentario /* hace algo*/, siempre será ejecutado, a lo menos una vez, sin importar si cumple la condición de abajo.

Luego, mientras i sea menor que 10, repetiremos el bloque.

Saliendo de un bucle usando la palabra reservada break

En C todos los bucles tienen una forma salir, es decir de detener las repeticiones en cualquier momento, inmediatamente, sin importar las condiciones definidas para el bucle.

Esto lo logramos usando la palabra clave break.

Esto es útil en muchos casos. Tal vez quieras verificar el valor de una variable, por ejemplo:

for (int i = 0; i <= 10; i++) { 
 if ( i == 4 && algunaVariable == 10) { 
   break; 
 } 
}

Tener esta opción de salida de un bucle es particularmente interesante para el ciclo while (y también para el do while), porque podemos crear bucles aparentemente infinitos que terminarán cuando ocurra una condición. Define esto dentro del bloque del ciclo:

int i = 0; 
while (1) { 
  /* haz algo */ 
  i++; 
  if ( i == 10) 
    break; 
}

Es bastante común tener este tipo de bucles en C.

Arréglos

Un arréglo o array es una variable que almacena múltiples valores.

Todo valor en el array (en C), debe tener el mismo tipo (type). Esto significa que tendrás arreglos de valores int, arreglos con valores double, y más.

Puedes definir un array con valores int así:

int precios[5];

Siempre debes especificar el tamaño del array. C no provee arreglos dinámicos listos para usar. (tienes que usar una estructura de datos como una lista enlazada para hacer eso).

Puedes usar constantes para definir el tamaño:

const int TAMANO = 5; 
int precios[TAMANO];

Puedes inicializar un array al momento de su definición así:

int precios[5] = {1, 2, 3, 4, 5 };

También puedes asignar valores después de su definición, de esta forma:

int precios[5]; 
precios[0] = 1; 
precios[1] = 2; 
precios[2] = 3; 
precios[3] = 4; 
precios[4] = 5;

O de forma más práctica usando un ciclo:

int precios[5]; 
for (int i = 0; i<5; i++) { 
  precios[i] = i + 1; 
}

Puedes referenciar el item de un array usando paréntesis cuadrados después del nombre de la variable array, agregando un entero para determinar el valor del índice, así:

precios[0]; /* valor del elemento del array: 1 */ 
precios[1]; /* valor del elemento del array: 2 */

Los índices de un array parten desde 0, por lo que un array con 5 elementos, como el arreglo precios de más arriba tendrá elementos que parten desde precios[0] hasta precios[4].

La parte interesante sobre los arreglos en C es que todos los elementos del array son guardados secuencialmente, uno justamente después de otro. No es algo que suceda normalmente en lenguajes de programación de más alto nivel.

Otra cosa interesante es que: el nombre de variable precios del array en el ejemplo anterior, es un puntero al primer elemento del array. Y como tal, se puede usar como un puntero normal.

Veremos pronto los punteros.

Strings

En C, los strings (cadenas de texto) son un tipo especial de array: un string es un array de valores char:

char name[6];

Introduje el tipo char cuando toccamos el tema de los tipos, este se usa comúnmente para almacenar letras de la tabla ASCII.

char name[6] = { "M","a","u","r","o"};

O de forma más conveniente con una cadena de texto literal (también llamada constante string), una secuencia de caracteres entre comillas dobles:

char name[6] = "Mauro"‌

Puedes imprimir un string vía printf() usando %s:

printf("%s", name);

Notaste que "Mauro" tiene en total 5 caracteres, pero definí el array con 6? ¿porqué? Esto es porque el último carácter de una cadena debe ser el valor 0, el terminador de la cadena, y debemos dejar espacio para él.

Es importante mantener esto en mente, especialmente cuando manipulas strings.

Cuando hablamos de manipular strings, existe una librería estándar importante que provee C: string.h.

Esta librería es esencial porque abstrae muchos de los detalles de bajo nivel al trabajar, y provee un grupo de herramientas útiles.

Puedes cargar la librería agregando la siguiente línea de código en la parte superior de tu programa:

#include <string.h>

Una vez que haces esto, tienes acceso a:

  • strcpy() para copiar un string sobre otro string
  • strcat() para anexar un string a otro string
  • strcmp() para comparar igualdad entre dos strings
  • strncpm() para comparar los primeros n caracteres de dos strings
  • strlen() para calcular el largo de un string

Y muchos más

Punteros

Los punteros son una de las partes más confusas/desafiantes de C, en mi opinión. Especialmente si eres nuevo en los lenguajes de programación, pero también si vienes de lenguajes de programación de alto nivel como Python o JavaScript.

En esta sección quiero introducirlos de la forma más sencilla, pero completa posible.

Un puntero es la dirección de un bloque de memoria que contiene una variable.

Cuando declaras un número entero como este:

  int edad = 37;

Podemos usar el operador & para obtener el valor de la dirección en memoria de la variable:

printf("%p", &edad ); /* 0x7ffe3425bcac */

Usé el formato %p especificado en printf() para imprimir el valor de la dirección.

Podemos asignar la dirección a una variable:

int *direccion = &edad;

Usando int *direccion en la declaración, no estamos declarando una variable tipo entero, sino el puntero a un entero.

Podemos usar el operador de puntero * para obtener el valor de una variable a la que apunta una dirección:

int edad = 37; 
int *direccion = &edad; 
printf("%u", *direccion); /* 37 */

Esta vez estamos usando el operador de puntero otra vez, pero como no es una declaración, esta vez significa "El valor de la variable a la que apunta el puntero".

En este ejemplo, nosotros declaramos una variable edad y usamos un puntero para inicializar el valor:

int edad; 
int *direccion = &edad; 
*direccion = 37; 
printf("%u"; *direccion);

Cuando trabajes con C, te darás cuenta que muchas cosas están construidas sobre este simple concepto. Así que debes asegurarte de estar familiarizado con él, lo cual se te facilitará al realizar los ejemplos por tu cuenta.

Los punteros son una gran oportunidad de aprendizaje, porque nos obligan a pensar en las direcciones de memoria y cómo están organizados los datos.

Los arrays son un ejemplo, cuando declaras un array:

int precios[3] = { 5, 4, 3 };

La variable precios es de hecho, un puntero al primer elemento del array. Puedes obtener el valor del primer item usando la funciónprintf()  de la siguiente forma:

printf("%u", *precios);  /* 5 */

Lo genial es que podemos obtener el segundo item sumando 1 al puntero precios:

printf("%u%", *(precios + 1); /* 4 */

De la misma forma podemos obtener los valores que siguen.

También podemos hacer muchas manipulaciones y operaciones de strings, dado que internamente, son arreglos de caracteres.

También tenemos muchas otras formas de aplicarlo, incluyendo pasar la referencia a un objeto o una función para evitar consumir más recursos al copiarlo.

Funciones

Las funciones son formas en las que podemos estructurar nuestro código en subrutinas a las cuales podremos:

  1. Darles un nombre
  2. Llamarlas (invocarlas) cuando lo necesitemos

Al comenzar con tu primer programa ("Hola Mundo"), inmediatamente comenzaste a usar las funciones de C:

#include <stdio.h> 
int main(void) { 
   printf("Hola Mundo"); 
}

La función main() es una muy importante, y es el punto de entrada a un programa en C.

Aquí hay otra función:

void hazAlgo(int valor) { 
   printf("%u", valor); 
}

Las funciones tienen 4 aspectos importantes:

  1. Tienen un nombre, para que podamos invocarlas("llamarlas") en el momento que la necesitemos.
  2. Especifican un valor de retorno
  3. Pueden tener argumentos
  4. Tienen un cuerpo envuelto en paréntesis de llave ({})

El cuerpo de la función es el conjunto de instrucciones que se ejecutará cada vez que se invoca la función.

Si la función no tiene valor de retorno, puedes usar la palabra reservada void antes del nombre de la función. De otra forma debes especificar el tipo de valor que retornará (int para enteros, float para decimales de punto flotante, const char * para un string, etc).

No puedes retornar más de un valor desde una función.

Una función puede tener argumentos. Estos son opcionales. Si no tiene argumentos, debes colocar la palabra reservada void, de la siguiente forma:

void hazAlgo(void) { /* ... */ }

En este caso, cuando invocamos la función, la llamaremos sin elementos dentro del paréntesis:

hazAlgo();

Si tenemos un parámetro, especificamos el tipo y el nombre del parámetro así:

void hazAlgo(int valor) { /* ... */ }

Cuando invocamos la función, le pasaremos ese parámetro entre paréntesis de la siguiente forma:

hazAlgo(3);

Podemos tener múltiples parámetros. De ser así, debemos separarlos usando una coma, tanto en la declaración como en la invocación:

void hazAlgo(int valor1, int valor2) { 
   /* ... */ 
} 
hazAlgo(3, 4);

Los parámetros son pasados a la función como copias, esto significa que si modificas el valor1, este valor sólo se modificará localmente, es decir dentro del cuerpo de la función. El valor afuera, que fue pasado al momento de la invocación no cambiará.

Si pasas un puntero como parámetro, podrás modificar el valor de la variable, esto porque ahora podrás acceder directamente a su dirección en memoria.

No puedes definir un valor por defecto para un parámetro. C++ puede hacerlo ( y por lo tanto los programas en lenguaje Arduino), pero C no puede.

Asegúrate de definir la función antes de llamarla, o el compilador enviará un mensaje de advertencia:

➜  ~ gcc hola.c -o hola; ./hola 
hola.c:14:3: warning: implicit declaration of ç
     function 'hazAlgo' is invalid in C99 
     [-Wimplicit-function-declaration] 
   hazAlgo(3, 4); 
   ^
hola.c:17:6: error: conflicting types for
      'hazAlgo'
void hazAlgo(int valor1, char valor2) {
     ^
hola.c:13:3: note: previous implicit declaration
      is here
  hazAlgo(3, 4);
  ^
1 warning and 1 error generated.

Cómo he mencionado anteriormente la alerta que hemos recibido tiene que ver con el orden.

El error se trata de otra cosa relacionada. Ya que C no puede "ver" la declaración de la función antes de la invocación entonces debe hacer suposiciones. Y asume que la función retorna un int, pero la función retorna void y a esto se debe el error.

Si cambias la definición de la función a:

int hazAlgo(int valor1, int valor2) {
  printf("%d %d\n", valor1, valor2);
  return 1;
}

Sólo obtenemos una advertencia y no el error:

➜  ~ gcc hello.c -o hello; ./hello
hello.c:14:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
1 warning generated.

En cualquier caso, asegúrate de declarar la función antes de usarla. Ya sea moviendo la función hacia arriba, o agregando el prototipo de función en un archivo de cabecera.

Dentro de una función puedes declarar variables.

void hasAlgo(int valor) { 
   int dobleValor = valor * 2; 
}

Una variable es creada al momento de la invocación y se destruye cuando la función termina su ejecución, la variable no será visible desde el exterior.

Dentro de una función, puedes llamar a la función en sí misma. Esto se llama recursión y es algo que ofrece oportunidades peculiares.

Entradas y salidas (Input Output - IO)

C es un lenguaje pequeño, y el "núcleo" de C no incluye funcionalidades de entrada y salida, "Input/Output" (I/O).

Por supuesto, esto no es algo único de C. Es común para el núcleo de un lenguaje ser agnóstico de I/O.

En el caso de C, la librería estándar nos provee Input/Output vía un conjunto de funciones definidas en el archivo de cabecera stdio.h

Puedes importar esta librería usando include <stdio.h> en la parte superior de tu archivo C:

#include <stdio.h>

Esta librería nos provee diversas funciones:

  • printf()
  • scanf()
  • sscanf()
  • fgets()
  • fprintf()

Antes de describir qué hacen estas funciones, quiero tomarme un minuto para habler sobre los flujos I/O (I/O streams) [Flujos de entrada y salida].

Tenemos 3 tipos de flujos de E/S en C:

  • stdin (standard input) [entrada estandar]
  • stdout (standard output) [salida estandar]
  • stderr (standar error) [error estandar]

Con las funciones I/O siempre trabajaremos con flujos. Un flujo es una interfaz de alto nivel que representa un dispositivo o un archivo. Desde el punto de vista de C, no tenemos ninguna diferencia al leer desde un archivo o leer desde la línea de comandos: ambos son un flujo I/O en cualquier caso.

Esto es algo para tener presente.

Algunas funciones están diseñadas para trabajar en un flujo específico, como printf(), el cual usamos para imprimir caracteres al stdout. Al usar su contraparte más general fprintf(), podemos específicar a cual flujo escribiremos.

Ya que empecé hablando de printf(), hagámos su introducción ahora.

printf() es una de las primeras funciones que usarás al aprender a programar en C.

En su forma de uso más simple le pasamos una cadena de texto literal:

printf("hola!");

... y el programa mostrará en consola el contenido de la cadena de texto.

También puedes imprimir el valor de una variable. Pero es algo complicado ya que necesitas agregar un carácter especial, un carácter de relleno que cambia dependiendo del tipo de la variable. Por ejemplo, usamos %d para un dígito decimal entero con signo:

int edad = 37;

printf("Mi edad es %d", edad);

Podemos imprimir más de una variable usando comas:

int edad_ayer = 37;
int edad_hoy = 36;

printf("Ayer mi edad era %d y hoy es %d", edad_ayer, edad_hoy);

Hay otros especificadores de formato como %d:

  • %c para caracteres
  • %s para caracteres
  • %f para números de punto flotante
  • %p para punteros

... y varios más.

Podemos usar caracteres de escape en printf(), como \n que podemos usar para crear una nueva línea en el resultado de salida.

scanf()

printf() se utiliza cómo una función de salida. Ahora quiero presentarte una función de entrada para que podamos realizar todas las acciones I/O (de entrada y salida): scanf().

Esta función se utiliza para obtener un valor de parte del usuario mediante la consola.

Debemos definir una variable que tomará el valor que obtuvimos desde la entrada:

int edad;

Luego la llamamos scanf() pasándole 2 argumentos (el formato de la variable y la dirección de variable):

scanf("%d", &edad);

Si queremos obtener un string como entrada, recuerda que el nombre de un string es un puntero a el primer carácter, por lo que no necesitas usar el carácter al inicio &:

char nombre[20];
scanf("%s", nombre);

Aquí un pequeño programa que utiliza ambos printf() y scanf():

#include <stdio.h>

int main(void) {
  char nombre[20];
  printf("Escribe tu nombre: ");
  scanf("%s", nombre);
  printf("te llamas %s", nombre);
}

Alcance de las variables

Cuando defines una variable en un programa C, dependiendo de dónde lo declares, va a tener un alcance distinto (scope).

Esto significa que estará disponible en algunos lugares pero no en otros.

La ubicación de dónde es declarada, determina los 2 tipo de variables:

  • variables globales
  • variables locales

Esta es la diferencia: una variable declarada dentro de una función es una variable local, así:

int main(void) {
  int edad = 37;
}

Las variables locales sólo son accesibles desde adentro de la función y cuando la función termina, dejan de existir, es decir, son liberadas de la memoria, con algunas excepciones.

Una variable definida fuera de una función se conoce como variable global cómo en el siguiente ejemplo:

int edad = 37;

int main(void) {
  /* ... */
}

Las variables globales son accesibles desde cualquier función del programa y están disponibles por toda la ejecución del programa hasta que este concluye.

Mencioné que las variables locales no están disponibles luego de finalizar la función que las contenga.

La razón de esto es que las variables locales por defecto, son declaradas en la pila (stack), a menos que las asignes manualmente al almacenamiento dinámico (heap) usando punteros. Pero entonces tendrás que administrar el uso de memoria tú mismo.

Variables estáticas

Dentro de una función puedes inicializar una variable estática usando la palabra reservada static. He dicho "dentro de una función" ya que las variables globales son estáticas por defecto, así que no hay necesidad de agregar la palabra clave static.

¿Qué es una variable estática? Una variable estática es inicializada a 0 si no se le asigna ningún valor y retiene su valor en todas las llamadas o invocaciones de función.

Considera esta función:

int incrementarEdad() {
  int edad = 0;
  edad++;
  return edad;
}

Si llamamos incrementarEdad() vamos a obtener 1 cómo valor de retorno, si la seguimos invocando, simpre obtendremos 1, debido a que edad es una variable local y se reinicia su valor a 0 en cada llamada o invocación.

Si cambiamos la función a:

int incrementarEdad() {
  static int edad = 0;
  edad++;
  return edad;
}

Ahora cada vez que llamemos a esta función, vamos a obtener un valor incremental:

printf("%d\n", incrementarEdad());
printf("%d\n", incrementarEdad());
printf("%d\n", incrementarEdad());

nos arrojará

1
2
3

También podemos omitir la inicialización de edad a 0 en la línea static int edad = 0 y sólo escribir static int edad; dado que las variables estáticas reciben automáticamente el valor 0 al ser creadas.

También podemos usar arreglos estáticos, en este caso cada ítem individual en el arreglo es inicializado a 0 automáticamente:

int incrementarEdad() {
  static int edades[3];
  edades[0]++;
  return edades[0];
}

Variables globales

En esta sección quiero hablar más acerca de la diferencia entre variables globales y locales.

Una variable local se define dentro de una función y sólo se encuentra disponible dentro de esa función.

Así:

#include <stdio.h>

int main(void) {
  char j = 0;
  j += 10;
  printf("%u", j); //10
}

j no está disponible en cualquier lugar fuera de la función main.

Una variable global se define fuera de las funciones de la siguente forma:

#include <stdio.h>

char i = 0;

int main(void) {
  i += 10;
  printf("%u", i); //10
}

Se puede tener acceso a una variable global desde cualquier función en el programa, además no se limita a la lectura del valor que almacena sino que este valor puede ser modificado desde cualquier función.

Debido a esto, las variables globales son una de las formas en que podemos compartir los mismos datos entre funciones.

La principal diferencia con las variables locales es que el espacio de memoria que se asigna a una variable local se libera una vez que la función termina. En cambio, el espacio de memoria de las variables locales sólo es liberado cuando termina el programa.

Definiciones de tipos

En C, la palabra reservada typedef nos permite definir nuevos tipos de datos.

Tomando los tipos incluidos en C como base, podemos crear nuestros propios tipos de datos usando la siguiente sintaxis:

typedef existingtype NUEVOTIPO

Por convención, es común utilizar mayúsculas para definir un nuevo tipo, esto para poder distinguirlo y reconocerlo como un tipo de dato con mayor facilidad.

Por ejemplo, podemos definir un nuevo tipo de dato NUMERO como int:

typedef int NUMERO

y una vez que lo hayas hecho podrás definir variables de tipo NUMERO:

NUMERO uno = 1;

¿Ahora bien, te preguntarás porqué hacer esto? ¿Porque no sólo utilizar el tipo int en su lugar?

Bueno, typedef se vuelve muy útil cuando se le acompaña de dos cosas: tipos enumerados y estructuras.

Tipos enumerados

Usando las palabras reservadas typedef y podemos definir un tipo que puede tener uno u otro valor. Es uno de los más importantes usos de la palabra reservada enumtypedef.

Esta es la sintaxis de la declaración un tipo enumerado:

typedef enum {
  //...valores
} NOMBRETIPO;

El tipo enumerado que creemos es usualmente por convención definido en mayúsculas .

Aquí, un ejemplo sencillo:

typedef enum {
  true,
  false
} BOOLEAN;

Aunque C ya cuenta con un tipo bool, lo anterior nos sirve como ejemplo explicativo.

Otro ejemplo son los días de la semana como tipo de datos:

typedef enum {
  lunes,  
  martes,
  miercoles,
  jueves,
  viernes,
  sabado,
  domingo
} DIASEMANA;

Y aquí un programa simple que utiliza este tipo enumerado:

#include <stdio.h>

typedef enum {
  lunes,  
  martes,
  miercoles,
  jueves,
  viernes,
  sabado,
  domingo
} DIASEMANA;

int main(void) {
  DIASEMANA dia = lunes;

  if (day == lunes) {
    printf("Es lunes!"); 
  } else {
    printf("No es lunes"); 
  }
}

Internamente cada ítem en una definición de un enum, se empareja con un entero. Por lo que en este ejemplo lunes es 0, es y martes es 1 así sucesivamente.

Lo que quiere decir que la condición la pudimos haber escrito así: if (dia==0) y no así: if (dia == lunes), sin embargo, es una sintaxis más conveniente, legible y fácil de razonar para nosotros humanos usar palabras en lugar de números.

Estructuras

Usando la palabra reservada struct podemos crear estructuras de datos complejas usando los tipos básicos de C.

Una estructura es una colección de valores de tipos distintos. Los arreglos en C están limitados a un sólo tipo de datos, por lo que las estructuras pueden ser muy útiles en muchos casos.

Esta es la sintaxis para definir una estructura:

struct <nombredeestructura> {
  //...variables
};

Ejemplo:

struct persona {
  int edad;
  char *nombre;
};

Puedes declarar variables que sean del tipo de la estructura que has definido, al especificar el nombre de la variable luego de la llave que cierra y antes del punto y coma, así:

struct persona {
  int edad;
  char *nombre;
} flavio;

O para múltiples variables, así:

struct persona {
  int edad;
  char *nombre;
} flavio, gente[20];

En este caso declaron una sóla variable flavio de tipo persona y un arreglo gente de 20 personas.

También podemos declarar las variables usando esta sintaxis:

struct persona {
  int edad;
  char *nombre;
};

struct persona flavio;

Podemos inicializar una estructura en la misma línea en que hemos declarado la variable:

struct persona {
  int edad;
  char *nombre;
};

struct persona flavio = { 37, "Flavio" };

y una vez que hayamos definido una estructura, podemos tener acceso a los valores que contiene usando un punto (notació  de punto):

struct persona {
  int edad;
  char *nombre;
};

struct persona flavio = { 37, "Flavio" };
printf("%s, edad %u", flavio.nombre, flavio.edad);

Usando el punto también podemos editar los valores:

struct persona {
  int edad;
  char *nombre;
};

struct persona flavio = { 37, "Flavio" };

flavio.edad = 38;

Las estructuras son muy útiles porque podemos moverlas de un lado a otro o pasarlas como parámetros o valores de retorno en funciones o embedir variables y cada variable tiene una etiqueta.

Es importante mencionar que las estructuras se "pasan como copia", a menos que por supuesto pasemos tambien un puntero, en tal caso "pasa como referencia".

Al trabajar con estructuras, podemos simplificar el código usando la palabra reservada typedef.

Veamos un ejemplo:

typedef struct {
  int edad;
  char *nombre;
} PERSONA;

El nombre de la estructura que creamos usando typedef es usualmente y por convención, escrita en mayúsculas.

Ahora podemos declarar nuevas variables persona así:

PERSONA flavio;

y  las podemos inicializar en la misma línea en que fueron declaradas, así:

PERSONA flavio = { 37, "Flavio" };

Parámetros de línea de comandos

En tus programas escritos en C podrías necesitar aceptar parámetros desde la línea de comandos cuando arrancas su ejecución.

Para casos de uso simples, lo único que vas a necesitar es modificar la firma de función main() de:

int main(void)

a:

int main (int argc, char *argv[])

argc es un número entero que representa el número de parámetros que fueron proporcionados mediante la línea de comandos.

argv es un arreglo de strings (cadenas de caracteres).

Al arrancar el programa se tiene acceso a los argumentos de los 2 parámetros.

Cabe mencionar que siempre habrá por lo menos un ítem en el arreglo argv: el nombre del programa.

Tomemos el ejemplo del compilador C que usamos para ejecutar nuestros problemas, así:

gcc hola.c -o hola

Si este fuera nuestro programa, argc sería equivalente a 4 y argv una matriz (arreglo) que contiene

  • gcc
  • hello.c
  • -o
  • hello

Escribamos un programa que imprima los argumentos que recibe:

#include <stdio.h>

int main (int argc, char *argv[]) {
  for (int i = 0; i < argc; i++) {
    printf("%s\n", argv[i]);
  }
}

Si el nombre de nuestro programa es hello y lo ejecutamos así: ./hello, obtendríamos esto como salida:

./hello

Si pasamos algunos parámetros aleatorios, como este: obtendríamos esta salida a la terminal:./hello a b c

./hello
a
b
c

Este sistema funciona muy bien para casos de uso simples. Para necesidades más complejas, existen paquetes de uso común como getopt.

Archivos de cabecera

Los programas simples se pueden poner en un solo archivo. Pero cuando tu programa crezca, será imposible mantenerlo todo en un solo archivo.

Puede mover partes de un programa a archivos independientes. A continuación, se crea un archivo de cabecera.

Un archivo de cabecera se parece a un archivo C normal, excepto que termina con en .h lugar de .c. En lugar de las implementaciones de las funciones y las otras partes de un programa, contiene las declaraciones.

Ya usaste archivos de encabezado cuando usaste la función printf(), o alguna otra función de E/S, y para usarlo tuviste que escribir:

#include <stdio.h>

#include es una directiva de preprocesador.

El preprocesador va y busca el archivo stdio.h en la biblioteca estándar porque usó corchetes alrededor de él. Para incluir tus propios archivos de encabezado, usarás comillas, como esta:

#include "hola.h"
_

Lo anterior buscará hola.h en la carpeta actual.

También puedes utilizar estructura de carpetas para bibliotecas:

#include "proyecto-hola/hola.h"

Veamos un ejemplo. Este programa calcula los años transcurridos desde un año determinado:

#include <stdio.h>

int calcularEdad(int anio) {
  const int ANIO_ACTUAL = 2020;
  return ANIO_ACTUAL - anio;
}

int main(void) {
  printf("%u", calcularEdad(1983));
}

Supongamos que quiero mover la función calcularEdad a un archivo separado.

Creo un archivo:calcular_edad.c

int calcularEdad(int anio) {
  const int ANIO_ACTUAL = 2020;
  return ANIO_ACTUAL - anio;
}

Y un archivo calcular_edad.h donde pongo el prototipo de la función, que es la misma que la función en el archivo con .c, excepto el cuerpo de la función:

int calculateAge(int year);

Ahora en el archivo .c principal podemos ir y eliminar la definición de la función calculateAge(), y luego podemos importar el archivo calculate_age.h, lo que hará que la función calculateAge() esté disponible:

#include <stdio.h>
#include "calculate_age.h"

int main(void) {
  printf("%u", calculateAge(1983));
}

No olvides que, para compilar un programa compuesto por varios archivos, debes enumerarlos todos (pasarlos como parámetros) en la línea de comandos, así:

gcc -o main main.c calculate_age.c

Para configuraciones más complejas, es necesario un Makefile para decirle al compilador cómo compilar el programa.

El preprocesador

El preprocesador es una herramienta que nos ayuda mucho a la hora de programar con C. Es parte del estándar C, al igual que el lenguaje, el compilador y la biblioteca estándar.

El preprocesador analiza nuestro programa y se asegura de que el compilador obtenga todas las cosas que necesita antes de continuar con el proceso.

¿Qué hace en la práctica?

Por ejemplo, busca todos los archivos de encabezado que se incluyen con la directiva #include.

También examina todas las constantes que definió usando #define y las sustituye por su valor real.

Eso es solo el comienzo. Mencioné esas 2 operaciones porque son las más comunes. El preprocesador puede hacer mucho más.

¿Te diste cuenta de que #include y #define tienen un # un al principio? Esto es común en todas las directivas del preprocesador. Si una línea comienza con #, el preprocesador se encarga de ello.

Condicionales

Una de las cosas que podemos hacer es usar condicionales para cambiar la forma en que se compilará nuestro programa, dependiendo del valor de una expresión.

Por ejemplo, podemos comprobar si la constante DEBUG es 0:

#include <stdio.h>

const int DEBUG = 0;

int main(void) {
#if DEBUG == 0
  printf("No estoy depurando\n");
#else
  printf("Estoy depurando\n");
#endif
}

Constantes simbólicas

Podemos definir una constante simbólica:

#define VALUE 1
#define PI 3.14
#define NAME "Flavio"

Cuando usamos NAME o PI o VALUE en nuestro programa, el preprocesador reemplaza su nombre con el valor antes de ejecutar el programa.

Las constantes simbólicas son muy útiles porque podemos dar nombres a los valores sin crear variables en el momento de la compilación.

Macros

Con #define también podemos definir una macro. La diferencia entre una macro y una constante simbólica es que una macro puede aceptar un argumento y normalmente contiene código, mientras que una constante simbólica es un valor:

#define POTENCIA(x) ((x) * (x))

Observe los paréntesis alrededor de los argumentos: esta es una buena práctica para evitar problemas cuando se reemplaza la macro en el proceso de pre-compilación.

Luego podemos usarlo en nuestro código de la siguiente manera:

printf("%u\n", POTENCIA(4)); //16

La gran diferencia con las funciones es que las macros no especifican el tipo de sus argumentos o valores devueltos, lo que puede ser útil en algunos casos.

Las macros, sin embargo, se limitan a definiciones de una línea.

If defined

Podemos comprobar si se define una constante simbólica o una macro usando #ifdef:

#include <stdio.h>
#define VALOR 1

int main(void) {
#ifdef VALOR
  printf("Valor está definido\n");
#else
  printf("Valor no está definido\n");
#endif
}

También tenemos #ifndev comprobar lo contrario (macro no definida).

También podemos usar #if defined y #if !defined para realizar la misma tarea.

Es común envolver algún bloque de código en un bloque como este:

#if 0

#endif

para evitar temporalmente que se ejecute o para usar una constante simbólica DEBUG:

#define DEBUG 0

#if DEBUG
  //este código sólo se envía al compilador
  //si DEBUG no es igual a 0
#endif

Constantes simbólicas predefinidas que puedes usar

El preprocesador también define una serie de constantes simbólicas que puede utilizar, identificadas por los 2 guiones bajos que encuentras antes y después del nombre, entre las que se incluyen:

  • __LINE__ se traduce a la línea actual en el archivo de código fuente
  • __FILE__ se traduce en el nombre del archivo
  • __DATE__ se traduce a la fecha de compilación, en el formatoMmm gg aaaa
  • __TIME__ se traduce en el tiempo de compilación, en el formato hh:mm:ss.

Conclusión

¡Muchas gracias por leer este manual!

Espero que te inspire a saber más sobre C.

Para obtener más tutoriales, consulte mi blog flaviocopes.com.

Sígueme en Twitter @flaviocopes.