Materia
Ejercicios


anterior | siguiente

Datos de una persona

El siguiente programa le pide al usuario ingresar su nombre completo, su rut y su fecha de nacimiento.

Como salida, se muestra la edad del usuario.

#include <stdio.h>
#include <stdlib.h>

#define ANNO_ACTUAL 2012
#define LARGO_NOMBRE 50
#define LARGO_RUT 10

struct fecha {
    int dia;
    int mes;
    int anno;
};

struct persona {
    char nombre[LARGO_NOMBRE + 1];
    char rut[LARGO_RUT + 1];
    struct fecha fecha_nacimiento;
};


int fecha_es_valida(struct fecha f) {
    int dias_mes[] = {0, 31, 28, 31, 30,
                         31, 30, 31, 31,
                         30, 31, 30, 31};

    if (f.mes < 1 || f.mes > 12)
        return 0;
    if (f.dia < 1 || f.dia > dias_mes[f.mes])
        return 0;
    return 1;
}


int main() {

    struct persona p;

    printf("Nombre completo: ");
    scanf("%[^\n]", p.nombre);

    printf("Rut: ");
    scanf("%s", p.rut);

    printf("Fecha de nacimiento (dia mes anno): ");
    scanf("%d", &p.fecha_nacimiento.dia);
    scanf("%d", &p.fecha_nacimiento.mes);
    scanf("%d", &p.fecha_nacimiento.anno);

    if (!fecha_es_valida(p.fecha_nacimiento)) {
        fprintf(stderr, "Fecha es invalida\n");
        exit(1);
    }

    printf("\n");
    printf("%s tiene %d annos.\n",
            p.nombre, ANNO_ACTUAL - p.fecha_nacimiento.anno);

    exit(0);
}

Además, el programa verifica que la fecha de nacimiento sea válida, revisando que el mes esté entre 1 y 12, y que el día tenga sentido para ese mes. Para simplificar, nos hemos echado los años bisiestos al bolsillo.

Macros de preprocesador

La primera cosa extraña que vemos en este programa son las líneas que comienzan con #define. Estas líneas son instrucciones para el preprocesador, que es un componente del compilador que hace algunas sustituciones en el código antes de que comience realmente a ser compilado.

Estas sustituciones se llaman macros, y son definidas por el programador usando la instrucción #define. Cada vez que aparece la macro en el código, el preprocesador la reemplaza literalmente por lo que aparezca a su derecha en el #define.

Es común usar macros para definir una única vez al principio del programa los largos de los arreglos. Estos valores suelen aparecer muchas varias veces durante el programa; por ejemplo, en las declaraciones y en los ciclos que los recorren.

En nuestro programa, hemos definido las macros LARGO_NOMBRE y LARGO_RUT, que son los largos de los strings. Si más adelante uno quisiera modificar el programa para que alguno de estos strings tenga un largo diferente, bastaría con modificar la macro asociada para que automáticamente el programa siga estando correcto.

Hay que tener muy claro que las macros no son variables. Son sólo abreviaciones que son reemplazadas tal cual cada vez que aparecen en el código. Para distinguirlas de las variables, se sigue la convención de ponerle a las macros nombres en mayúsculas.

En la línea de comandos, usted puede usar el programa cpp para ver cómo queda el código después de ser preprocesado:

$ cpp personas.c

Estructuras

Una estructura es un tipo de datos que agrupa varios valores en uno solo.

A diferencia de los arreglos, los componentes de una estructura pueden ser de tipos diferentes.

Las estructuras en C se usan para lo mismo que las tuplas en Python: para agrupar datos que, por su naturaleza, deben ser tratados como un único valor.

El ejemplo típico es crear una estructura para almacenar una fecha:

struct fecha {
    int dia;
    int mes;
    int anno;
};

Esta definición crea un nuevo tipo de datos llamado struct fecha, que contiene tres valores enteros. El punto y coma después de la llave es obligatorio; un error muy común es omitirlo.

Una variable de tipo struct fecha debe ser declarada de la misma forma que las demás variables:

struct fecha f;

Una vez declarada la variable f, sus miembros pueden ser accedidos poniendo su nombre después de un punto:

f.dia = 21;
f.mes = 5;
f.anno = 1879;

Note que las estructuras no se desempaquetan como las tuplas de Python. No es necesario ya que se puede acceder a los campos a través de su nombre, y no por su posición.

Los campos de una estructura pueden ser de cualquier tipo, incluso arreglos u otra estructura. En el ejemplo, la estructura persona está compuesta de dos strings y una estructura fecha.

Inicialización de arreglos y estructuras

La función fecha_es_valida utiliza el arreglo dias_mes para tener a la mano cuántos días tiene cada mes.

Para que el mes m esté asociado al elemento m del arreglo, dejamos un valor de relleno en la posición 0, que no corresponderá a ningún mes.

En vez de llenar el arreglo elemento por elemento:

dias_mes[1] = 31;
dias_mes[2] = 28;
dias_mes[3] = 31;
/* ... */

podemos usar la siguiente sintaxis para inicializarlo:

int dias_mes[] = {0, 31, 28, 31, /* ... */ };

Al inicializar el arreglo de esta manera no es necesario especificar su tamaño. En nuestro programa, el arreglo dias_mes será de largo trece.

La misma sintaxis se puede usar para inicializar los elementos de una estructura:

struct fecha hoy = {29, 2, 2011};

La sintaxis de inicialización sólo puede ser usada en la misma declaración, no más adelante en el programa:

int a[5];
a = {900, 100, 600, 300, 200};  /* Esto es ilegal. */

Leer una línea completa

El descriptor de formato %s indica a la función scanf que debe leer un string. Lo que hace la función es leer texto hasta encontrarse con el primer caracter en blanco (como un espacio o un salto de línea).

Esto no resulta útil cuando el string que interesa sí tiene espacios entre medio. En el caso de nuestro programa, necesitamos un nombre completo, en que el nombre y el apellido están separados por un espacio.

Para leer el nombre completo del usuario, usamos el descriptor de formato %[^\n]. Esto significa literalmente «leer todos los caracteres que no sean saltos de línea».

Salidas estándar y de error

Cada vez que uno imprime cosas usando la función printf, lo que realmente ocurre es que el texto es enviado a un flujo de datos denominado salida estándar. Podemos pensar en la salida estándar como un canal de comunicación entre nuestro programa y la consola.

En todos los programas en C, la salida estándar está disponible para escribir cosas en ella. Pero además los programas tienen también otro flujo de datos, llamado salida de error, que está destinada exclusivamente para escribir en ella mensajes de error.

Los nombres de las salidas estándar y de error en un programa son, respectivamente, stdin y stderr.

En nuestro programa, usamos la salida de error para imprimir un mensaje antes de abortar el programa cuando se ha ingresado una fecha inválida. Para esto, usamos la función fprintf. Esta función es muy parecida a printf, salvo que recibe como primer parámetro el flujo de datos en que se escribirá el mensaje. Más adelante utilizaremos fprintf para escribir datos en archivos.

Por omisión, ambas salidas están conectadas con la consola, por lo que los mensajes impresos en ambas aparecen mezclados unos con otros, sin distinción. La gracia es que es posible redirigir por separado a cualquiera de ellas hacia otros mecanismos de salida, como un archivo, o de frentón suprimirlos. Por lo tanto, es una buena práctica escribir los mensajes de error en stderr.

Ejercicios

¿Qué imprime el siguiente programa? Pruebe el programa y explique el resultado.

#include <stdio.h>
#define DOS    1 + 1
#define SIETE  3 + 4

int main() {
    int m = DOS * SIETE;
    printf("%d\n", m);
    return 0;
}