Apendice A. El lenguaje de programación C.

A.1 Introducción

C es un lenguaje de programación de alto nivel desarrollado por Dennis Ritchie para codificar el sistema operativo UNIX. Las primeras versiones de UNIX se escribieron en ensamblador, pero a partir de 1973 comenzaron a escribirse en C. Solo la pequeña parte del núcleo de UNIX relacionada directamente con el hardware sigue escrita en ensamblador, todas las ordenes y aplicaciones estándar que acompañan al sistema están escritas en C. Por lo cual este lenguaje es la forma natural de comunicarse con el sistema. El lenguaje se puede clasificar dentro de los lenguajes de alto nivel, sin embargo, también le ofrece al programados posibilidades que solo están presentes en los lenguajes de bajo nivel. Así, por ejemplo, en C se pueden manipular bits y aritmética de direcciones. C también permite el desarrollo de la programación estructurada y modular.

A.2 Ciclo de creación de un programa

A la hora de crear un programa empezaremos por la edición de un fichero que contendrá el código fuente. Este fichero, se nombra por convenio añadiéndole la extensión .c. Si nos valemos del editor vi, la forma de editar el programa será:

$ vi programa.c

El compilador cc es el encargado de generar el fichero ejecutable a partir del fichero fuente. Para invocarlo debemos escribir:

$ cc programa.c

Esta línea de ordenes genera el fichero a.out, que ya es ejecutable. Si queremos que nuestro ejecutable tenga otro nombre, compilaremos con la orden:

$ cc -o programa programa.c

lo que haria que el fichero ejecutable se llame programa en lugar de a.out. También podemos compilar un programa utilizando la orden make. Si queremos crear el programa ejemplo a partir del fichero fuente ejemplo.c, bastara con escribir:

$ make ejemplo

A.2.1 Componentes léxicos del lenguaje

Existen 6 clases de componentes léxicos: identificadores, palabras reservadas, constantes, cadenas de caracteres, operadores y otros separadores.

  • Identificador. Es una secuencia de dígitos y letras donde el primer elemento debe de ser una letra o los caracteres _ y $. Las letras mayúsculas y minúsculas se consideran distintas. En toda implementación deben ser significativos al menos los 32 primeros caracteres de un identificador.
  • Palabras reservadas. Las siguientes palabras no se pueden utilizar como identificadores, ya que tienen un significado especial para el compilador:
    auto, default, float, register, struct, volatile, break, do, for, return, switch, while, case, double, goto, short, typedef, char, else, if, signet, union, const, enum, int, sizeof, unsignet, extern, long, static, void, continue
  • Constantes. Consideramos 4 tipos: enteras, de carácter, flotantes y de enumeración.
  • Cadenas de caracteres. Secuencia de caracteres alfanuméricos entre comillas dobles.
  • Otros separadores: {, }, [, ], (, ), ;, ->, .
  • Comentarios. Secuencia de caracteres que se inicia con /* y termina con */. Los comentarios no se pueden anidar y son ignorados por el compilador. Los comentarios de línea se inician con // y se extienden hasta el final de la línea.

A.2.2 Estructura de un programa C

Aunque las formas de un programa C pueden ser muy variadas y no hay formas fijas, suele ser común estructurar un programa como sigue:

  1. #directrices para el preprocesador.
  2. Declaración de variables y funciones externas.
  3. Declaración de variables globales y prototipos de las funciones.
  4. Funciones, la función main debe aparecer en un módulo, y sólo en uno.

Las directrices del preprocesador son órdenes que ejecuta el preprocesador -cpp- para generar el fichero con extensión .i con el que posteriormente trabajará el compilador. Hay dos directrices que se emplean masivamente: include y define.

  • include se emplea para indicar los ficheros de cabecera donde es´ta definidos tipos derivados y los prototipos de las funciones.
  • definne se emplea para declarar identificadores sinónimos de otros identificadores o constantes. También se emplea para declarar macros.

A.3 Tipos de datos

En C se consideran dos bloques de datos: los que suministra el lenguaje -tipos fundamentales- y los que define el programador -tipos derivados-.

A.3.1 Tipos fundamentales

Tipos enteros

Para declarar variables de alguno de los tipos enteros emplearemos las palabras reservadas char, short, int, long y enum.

  • char define un número entero de 8 bits. Su rango es [-128,127]. Tambien se emplea para representar el conjunto de caracteres ASCII.
  • int define un numero entero de 16 o 32 bits dependiendo del bus del procesador.
  • long define un numero entero de 32 o 64 bits.
  • short define un numero entero menor o igual que int.

Estos cuatro tipos pueden ir precedidos del modificador unsigned para indicar que el tipo solo representa números positivos o el cero.

enum se utiliza para definir un subconjunto dentro del conjunto de los numeros enteros.

Tipos reales

Para declarar variables de alguno de los tipos reales emplearemos las palabras reservadas float y double.

  • float define un numero en coma flotante de precisión simple, el tamaño de este tipo suele ser 4 bytes.
  • double define un número en coma flotante de precisión doble, el tamaño de este tipo suele se 8 bytes. el tipo double puede ir precedido del modificador long, con lo que su tamaño pasa a ser 10 byetes.

A.3.2 Operador sizeof

Para determinar el tamaño en bytes tanto de un tipo fundamental como derivado, podemos usar el operador sizeof.

A.3.3 Tipos derivados

Los arrays son bloques de elementos del mismo tipo. El tipo base puede ser fundamental o derivado. Los elementos individuales del array son accesibles mediante una secuencia de índices. Los índices deben ser variables o constantes de tipo entero. Se define la dimensión de un array como el total de índices que necesitamos para acceder a un elemento en particular.

A los arrays unidimensionales se les llama vectores, y los bidimensionales matrices. Los arrays unidimensionales de tipo char se llaman cadenas de caracteres, y los arrays de cadenas de caracteres - matrices de caracteres- se llaman tablas.

A.3.4 Punteros

Los punteros son variables que almacenan direcciones de memoria. Se definen también en base a un tipo fundamental o a un tipo derivado. La declaración de un puntero es como sigue:

tipo_base *puntero;

Para trabajar con punteros hay definidos dos operadores unarios: & y *. El operador & da la dirección de memoria asociada a una variable y se utiliza para inicializar un puntero. El operador * se utiliza para referirse al contenido de una dirección de memoria.

Los punteros admiten las operaciones de incremento, decremento, suma de una constante entera y diferencia de punteros. Al realizar estas operaciones, realmente estamos modificando la dirección a la que referencia el puntero.

Un array no es lo mismo que un puntero. Mientras que un array es una constante, un puntero es una variable.

Unos de los inconvenientes que se plantea en el uso de arrays es que sus dimensiones deben ser conocidas por el compilador. los punteros nos ayudan a solucionar este problema, ya que posibilita el uso de arrays dinámicos -arrays que se dimensionan en tiempo de ejecución-.

Estructuras

Una estructura es un agregado de tipos fundamentales o derivados y se compone de varios campos.

Puesto que el tipo de cada campo de una estructura puede ser un tipo fundamental o derivado, también puede ser otra estructura. Tendremos así declaradas estructuras dentro de estructuras. Uniones

Las uniones se definen de forma parecida a las estructuras y se emplean para almacenar en un mismo espacio de memoria variables de distintos tipos.

El tamaño de una unión no es igual a la suma de cada uno de sus campos, como ocurre con las estructuras, sino que es igual al tamaño del mayor de sus campos.

Campos de bits

C da la posibilidad de definir variables cuyo campo en bits puede no coincidir con un múltiplo de 8.

A.3.5 Alias para los nombres de tipo.

Para hacer que un identificador sea considerado el nombre de un nuevo tipo, tenemos que emplear la palabra clave typedef.

A.4 Expresiones y operadores

Una expresión esta formada por operadores y operandos. Los operadores establecen la relación entre los operandos, y los operandos pueden ser variables, constantes u otras expresiones. Los paréntesis también pueden formar parte de una expresión y se emplean para modificar la precedencia de los operadores.

A.4.1 Operadores aritméticos.

Hay 5 operadores aritméticos:

  • + Suma
  • - Resta
  • * Multiplicación
  • / División. La división de números enteros produce un truncamiento del cociente.
  • % Resto de la división entera.

Las expresiones aritméticas se evalúan de izquierda a derecha. Si en una expresión aritmética intervienen variables o constantes de diferentes tipos, el tipo del resultado coincidirá con el tipo mayor que aparezca en la expresión.

A.4.2 Operadores de relación y lógicos

Los operadores de relación y los lógicos se emplean para formar expresiones booleanas. Una expresión booleana solo puede tomar dos valores: VERDADERO o FALSO. En C se considera una expresión booleana FALSA cuando su resultado es 0 y VERDAD cuando es distinto de 0.

Los operadores de relación son:

  • > Mayor
  • >= Mayor o igual
  • < Menor
  • <= Menor o igual
  • == Igual, hay que tener cuidado de no confundir el operador de asignació = con el de comparación ==.
  • ! = Distinto

Los operadores lógicos son:

  • && And lógica.
  • || Or lógica.
  • ! Negación lógica.

A.4.3 Operadores para el manejo de bits

C también implementa operadores para manipular los bits de las variables o contantes enteras. Estos operadores son:

  • & And a nivel de bits.
  • | Or a nivel de bits.
  • ^ Or exclusiva --- XOR --- a nivel de bits.
  • ~ Negación a nivel de bits o complemento a 1.
  • << Desplazamiento hacia la izquierda.
  • >> Desplazamiendo hacia la derecha haciendo una extensión del signo.

A.4.4 Expresiones abreviadas

En esta tabla se muestra una relación entre expresiones abreviadas y sus equivalentes:

Tabla de relacciones entre expresiones y equivalentes

A.5 Sentencias de control de flujo

A.5.1 Proposiciones y bloques

Una proposición es una expresión seguida de ;. Una proposición compuesta o bloque es un cojunto de declaraciones y proposiciones agrupadas entre llaves { }.

A.5.2 Selección (if - else)

Su sintaxis es:
if (expresión)
proposición1;
else
proposición2;

Si expresión es verdadera se ejecuta proposición1; en caso contrario, se ejecuta proposicion2.

A.5.3 Selección múltiple (else-if)

Su sintaxis es:
if (expr1)
(proposicion1);
else if (expr2);
(proposicion2);
...
else if (expr n)
(proposicion n)
else
(proposicion m)

A.5.4 Selección por casos (switch)

La proposición switch es una decisión múltiple que prueba si una expresión coincide con alguno de entre un número de valores constantes enteros y traslada el control adecuadamente. Su sintaxis es:

switch (expresión) {
case exp_const1: proposiciones1;
case expr_const2: proposiciones2;
...
case exp_const n: proposiciones n;
default: proposiciones;
}

A.5.5 Bucle for

Su sintaxis es:

for (inicialización; expresión; progresión)
proposición;

Mientras la expresión sea verdadera, se estará ejecutando proposición; inicialización es una expresión, o conjunto de expresiones, para inicializar las variables de control que intervienen en expresión; y progresión es una o conjunto de expresiones que indica como evolucionan las variables de control.

A.5.6 Bucle (while)

Su sintaxis es;

while (expresion)
proposición;
donde proposición se estará ejecutando mientras expresión sea verdadera.

A.5.7 Bucle (do-while)

Su sintaxis es:

do {
proposición;
} while (expresión);
donde proposición se estará ejecutando mientras expresión sea verdadera, la primera vez siempre se ejecuta.

A.5.8 break y continue

La proposición break produce una salida anticipada de un bucle for, while o do - while, y también produce la salida de la sentencia switch.

La proposición continue provoca que se inicie la siguiente iteración del bucle for, while o do - while que la contiene.

A.6 Funciones

Las funciones se utilizan para agrupar bajo un identificador una serie de proposiciones concebidas para realizar alguna tarea específica. La organización de un programa grande en funciones sencillas hará que el programa sea estructurado además de fácil de depurar y mantener. Una función puede ser de cualquier tipo, excepto del tipo función o array. Si no se especifica nada, el tipo es int.

Las variables locales y los parámetros de la función se reservan en la pila de usuario del programa, por lo que al entrar en la función se reserva espacio para ellos, pero al salir de la función desaparecen. Este tipo de almacenamiento se conoce como automático, en contraposicion al estático. Si una variable local va precedida del calificador static, su almacenamiento será estático y ocupara una zona de memoria reservada a las variables globales; además, esa variable existirá durante todo el tiempo de ejecución del programa.

A.6.1 Paso de parámetros por valor y por referencia

Es importante tener en cuenta que los parámetros de una función sólo se pueden modificar a nivel local.

Para modificar un parámetro de forma permanente hay que trabajar con un puntero al mismo.

El paso de arrays como parámetros de funciones siempre se realiza por referencia. Las estructuras se pueden pasar por valor o por referencia. Si pasamos una estructura por valor, las modificaciones de sus campos serán locales mientras que pasándolas por referencia, las modificaciones serán permanentes.

El hecho de que los parámetros pasados por valor sólo sufran modificaciones a nivel local se debe a que el parámetro es una copia en la pila de usuario de la variable a que se refiere. Así, las modificaciones se hacen sobre la copia de la variable que hay en la pila y no sobre la propia variable.

Bajate esta documentación en un archivo Acrobat Reader

Imagen que hace referencia a un archivo para Acrobat Reader