El lenguaje de programación C (Explicación de las transparencias)

Anuncio
El lenguaje de programación C
(Explicación de las transparencias)
(C) Javier Miranda 1996-2004
Reservados todos los derechos
Esta colección de notas es un material complementario de la
colecci’on de transparencias. Forma parte del contenido
del libro “Diseño de Software con C y UNIX (Volumen 1)”,
J.Miranda (1996), con ISBN 84-87526-45-4.
18 de febrero de 2004
1
18 de febrero de 2004
Notas 1
4.
4.1.
El lenguaje de programación C
Historia de C
El nacimiento de C está muy ligado al nacimiento de Unix en los laboratorios Bell.
En 1968 Thompson creó un grupo para investigar alternativas al sistema operativo
Multics y desarrolló Unix sobre un computador DEC PDP-7 (8k-18bit palabras de
memoria, sin software disponible). Escribió la versión original de Unix mediante macros
y un ensamblador cruzado (cargando el ejecutable mediante una cinta). De esta forma
desarrolló: el núcleo primitivo de Unix, un editor básico, un ensamblador sencillo que
generaba siempre el ejecutable en el fichero “a.out”, un interprete de comandos básico y
los comandos básicos del S.O. (rm, cat, cp). A partir de este momento ya pudo escribir
los programas directamente en Unix.
En 1969 Thompson intenta utilizar FORTRAN para escribir el S.O., pero lo abandona rápidamente y crea el lenguaje B (simplificación del lenguaje BCPL). B es un
lenguaje para programación de sistemas, pequeño, con descripción compacta, fácilmente traducible y cercano a la máquina. Sin embargo, Thompson comprobó que B no
era adecuado para reescribir Unix y sus utilidades.
En 1970 Thompson continuó el desarrollo de Unix sobre DEC PDP-11 (24k), y reescribió Unix en ensamblador del PDP-11 utilizando 12k para el núcleo de Unix y 12k
como disco RAM.
En 1971 Unix comenzó a tener usuarios (que programaban en ensamblador con rutinas
de biblioteca). De esta forma Steve Johnson creó yacc. Este mismo año Thompson
modificó B y creó el lenguaje NB (introduciendo básicamente los tipos de datos int,
char, los punteros y los arrays).
En 1972 introdujeron en el lenguaje un preprocesador y realizaron pequeñas modificaciones en la evaluación de las expresiones e introdujeron nuevos operadores.
En 1973 se creó C, Thompson reescribió Unix en C e instaló C en otras arquitecturas
(Honeywell 635, IBM 360/370) y Lesk desarrolló un módulo de E/S estándar.
En 1978 Kernighan y Ritchie publicaron el libro The C programming language. (Kernighan
se encargó de la exposición de C y Ritchie de la interfaz con Unix y el apéndice —
manual de referencia técnica de C—) y Steve Johnson desarrolló pcc (un compilador
de C fácilmente transportable).
En 1979 Johnson crea lint adaptando pcc.
En 1983 se crea el comite X2J11 para la creación del estándar de C.
En 1989 C se convierte en estándar ISO/IEC 9899-1990
A partir de C se han desarrollado los siguientes lenguajes: Objetive C [Cox, 86], C++
[Stroustrup, 86], C Concurrente[Gehani, 89], C* [Thinking, 90], C Concurrente Tolerante a fallos [Cmelik, Gehani, Roome].
18 de febrero de 2004
Notas 2
4.2.
El compilador de C
El compilador de C consta internamente de 4 programas: el preprocesador, el compilador, un ensamblador y un enlazador. El preprocesador se encarga de eliminar los
comentarios del programa y de interpretar sus comandos. Tras el preprocesador se ejecuta el compilador, que genera un fichero equivalente en ensamblador. Este fichero se
ensambla para generar el fichero objeto (.o). Finalmente el enlazador combina todos
los ficheros objeto especificados en la lı́nea de comando (incluyendo los ficheros objeto
de las bibliotecas) y genera el fichero ejecutable.
Ejemplos:
1. Llamar al preprocesador, compilador, ensamblador, enlazador y generar el ejecutable en el fichero a.out.
cc ejemplo.c
2. Llamar al preprocesador, compilador y ensamblador para generar los ficheros
objeto asociados a los ficheros main.c, getline.c e index.c. A continuación llamar
al enlazador pasandole los tres ficheros objeto para generar el fichero ejecutable
myprog.
cc main.c getline.c index.c -o myprog
3. Llamar al preprocesador, compilador y ensamblador para generar el fichero objeto
asociado al fichero main.c. A continuación llamar al enlazador pasandole los tres
ficheros objeto para generar el fichero ejecutable.
cc main.c getline.o index.o
18 de febrero de 2004
Notas 3
4.3.
El preprocesador
El preprocesador de C es un procesador de macros simple que conceptualmente procesa
el fichero fuente de un programa C antes de su compilación. Se controla mediante lı́neas
de comandos (lı́neas del programa fuente C) que pueden ir en cualquier parte del fichero
fuente y deben cumplir las siguientes reglas:
1. Deben comenzar comienzan con el carácter #.
2. Cada orden debe ocupar una lı́nea.
4.3.1.
Macros
El comando #define introduce una definición de macro. Una macro asocia un identificador a una cadena (string). El preprocesador sustituye las ocurrencias de dicho
identificador por la cadena de reemplazo (sustituyendo los parámetros formales que
aparezcan en el cuerpo de la macro por el valor de los parámetros proporcionado en
el momento de la invocacion de la macro). El ámbito de las macros va desde el punto
de su definición hasta el final del fichero. Las macros pueden declararse desde la lı́nea
comandos (cuando se invoca el compilador). Por ejemplo:
cc -DM68000=1 file.c
. . . define la macro M68000 con el valor 1 antes de compilar el fichero file.c.
El comando #undef borra la definición de una macro previamente definida.
18 de febrero de 2004
Notas 4
4.3.2.
Compilación condicional
La compilación condicional se utiliza para incorporar u omitir de forma selectiva una
secuencia de sentencias. Por convenio los ficheros que se incluyen tienen el sufijo .h. Se
utiliza para incluir sentencias de depuración de los programas C y para aumentar la
portabilidad de los programas.
4.3.3.
Inclusión de ficheros
El comando #include hace que el contenido del fichero especificado se procese como
si estuviese ubicado en el lugar del comando. Existen dos formas de nombrar el fichero,
y difieren en donde está almacenado el fichero especificado.
#include "fichero.h" /* Busca en el directorio actual */
#include <fichero.h> /* Busca el sitio estandar /usr/include */
Este comando se utiliza generalmente para acceder a grupos de macros. De esta forma,
varios ficheros ven las mismas constantes. Sin embargo, debe tenerse especial cuidado
con los posibles bucles y duplicaciones de macros. La forma correcta de evitar estos
problemas consiste en utilizar el comando #ifndef de la siguiente forma:
#ifndef MAX
#define MAX 50
/* Resto del fichero de definiciones */
#endif
18 de febrero de 2004
Notas 5
4.4.
Introducción a C
Veamos con un ejemplo algunas de las caracterı́sticas del lenguaje C:
1. C es un lenguaje de formato libre (excepto los comandos del preprocesador).
2. Los comentarios pueden ponerse en cualquier sitio donde pueda ponerse un espacio
en blanco. Comienzan mediante /* y terminan con */.
3. Es un lenguaje sensible a mayúsculas y minúsculas. Por convenio se suele programar utilizando MAYUSCULAS para las CONSTANTES y minúsculas para las
variables.
4. Los identificadores de C estan formados por una secuencia de letras, digitos y .
El primer carácter debe ser una letra o .
5. Las palabras reservadas de C son:
PALABRAS CLAVE (en minuscula)
----------------------------auto
double int
struct
break
else
long
switch
case
enum
register typedef
char
extern return
union
const
float
short
unsigned
continue for
signed
void
default goto
sizeof
volatile
do
if
static
while
- Algunas implementaciones tambien reservan fortran
y asm.
- const, signed y volatile son palabras nuevas de
C-ANSI
- enum y void son nuevas con respecto a la primera
version de C.
6. Los programas C se estructuran mediante funciones.
Los parámetros se pasan siempre por valor.
La función main es necesaria en todo programa (ya que es la primera en
ejecutarse) y puede tener parámetros (los veremos en la sección 4.13.5).
7. Las variables pueden inicializarse en la declaración.
8. La finalización del programa ocurre cuando:
Se finaliza main.
Se ejecuta una sentencia exit().
Ocurre una interrupción del programa (externa, interna).
18 de febrero de 2004
Notas 6
4.5.
Tipos de datos
C tiene cuatro tipos de datos básicos: carácter (char), entero (int), flotante de simple
precisión (float) y flotante de doble precisión (double). Además tiene tres cualificadores
que pueden aplicarse a los tipos básicos para ajustar la precisión de los tipos básicos:
short, long y unsigned (aritmética módulo 2N , siendo N el número de bits). Los rangos
están definidos por la implementación y son especificados en el fichero <limits.h>. Si
se utiliza un cualificador (short, long, unsigned) y se omite el tipo, se presupone que
la variable es de tipo int.
long int factorial;
unsigned int natural;
signed char c; /* -127..127 */
short int dia,mes;
4.6.
Constantes
Constantes tipo carácter.
ascii \’{A}’
octal ’\101’
hex
’\x41’
Constantes tipo string. Son una secuencia de caracteres entre comillas dobles.
Las strings adyacentes son automáticamente concatenadas en una única string.
C utiliza el carácter null (’\0’) como terminador de strings.
Constantes tipo entero. El tipo de una constante entera depende de su forma,
valor y sufijo1 .
decimal
octal (comienzan por 0)
hex
1234
015
0x4f
12L
015L
0x4f3
12U
015U
Constantes tipo float. Su sintaxis es: int.fracción [ E [+|-]int[F|L], significando
los sufijos F y L float y long double respectivamente. Puede omitirse:
• La parte entera o la parte fraccionaria (pero no ambas).
• El punto decimal o el exponente (no ambos).
1
Si no tiene sufijo adopta el primero de los siguientes tipos: si es decimal adopta int, long int, unsigned
long int; si es octal o hex adopta int, unsigned int, long int, unsigned long int.
18 de febrero de 2004
Notas 7
4.7.
Operadores y expresiones
Aritméticos: Debe tenerse en cuenta que la división entera trunca, y que el resto
(operador módulo) adopta el mismo signo que el dividendo. Toda la aritmética
entera se realiza al menos con rango int (short int es convertido automáticamente
en int, lo que se denomina promoción integral). En expresiones mixtas se realiza
conversión automática al mayor tamaño.
Relacionales. Adoptan los valores 0 para falso y 1 para verdadero.
Lógicos.
Autoincremento, autodecremento. Existen cuatro posibilidades:
++variable; Preincremento;
--variable: Predecremento
variable++: Postincremento;
variable--: Postdecremento
Cuidado con los efectos laterales.
Asignación. Existen dos tipos de asignación:
1. Asignación múltiple. Operador asociativo por la izquierda que realiza conversiones de tipo implı́citas.
a=b=c+d
===> a=(b=(c+d))
2. Asignación compuesta (a op= b). Puede utilizarse con:
• Cualquiera de los operadores aritméticos.
• Todos los operadores de nivel de bit excepto el complemento a 1.
La asignación compuesta tiene las siguientes ventajas:
a) Concisa.
b) Se corresponde mejor con la forma en que piensa la gente. Decimos suma
2 a i o incrementa i en 2, no toma i, súmale 2 y vuelve a colocar el
resultado en i.
c) En expresiones complicadas hace el código más fácil de comprender, ya
que el lector no necesita comprobar si dos expresiones complicadas son
realmente la misma. Por ejemplo:
yyval[yypv[p3+p4] + yypv[p1+p2]] += 2
d ) Puede ayudar al compilador a producir un código más eficiente.
Expresión condicional: Evalúa la expresión e1. Si es cierto, evalúa la expresión e2
y toma el resultado como el valor de la expresión condicional; si es falso, evalúa
la expresión e3 y toma este resultado como el valor de la expresión condicional.
Operador coma: Evalúa la primera expresión (comenzando por la izquierda) y
descarta el resultado; a continuación evalúa la siguiente expresión y toma este
valor como el resultado de la expresión.
Operador sizeof(): Operador unitario que devuelve un entero igual al tamaño en
bytes de cualquier objeto.
18 de febrero de 2004
Notas 8
Precedencia de operadores (ver tabla). Puede obviarse si no se evitan los paréntesis.
Conversión de tipos. Se puede forzar la conversión explı́cita del tipo çoaccionada”de
una expresión mediante una construcción denominada cast. El operador cast tiene
la misma precedencia que cualquier otro operador unitario2 .
4.7.1.
Orden de evaluación de expresiones
C no especifica en que orden se evalúan los operandos de un operador. Por ejemplo,
en una sentencia como x = f ()+g(), f puede ser evaluado antes que g o viceversa.
Entonces, si f o g alteran alguna variable externa, de la cual depende la otra
función, x puede depender del orden de evaluación.
Tampoco especifica el orden de evaluación de los argumentos de una función esta
especificado. Asi, la sentencia:
printf ("%d %d\n", ++n, f(n) );
puede producir diferentes resultados en distintas máquinas, según que n sea incrementado antes o despues de la llamada a f (). La solución consiste en escribir
++n;
printf ("%d %d\n", n, f(n) );
2
Existe un punto oscuro en la conversión de caracteres a enteros. El lenguaje no especifica si las variables
de tipo char tienen signo o no. De esta forma, dependiendo de las caracterı́sticas de la arquitectura de cada
máquina puede producir un entero negativo o no.
18 de febrero de 2004
Notas 9
4.8.
Control de flujo
Todas las sentencias de control de flujo de C estan asociadas a una única sentencia. Si
se desea asociarlas a un bloque debe crearse un nuevo ámbito (mediante { }).
Sentencias condicionales
1. IF: El resultado de evaluar la expresión logica es un entero donde cualquier
valor diferente de 0 se interpreta como verdadero. Se asocia else al if más
interno.
2. SWITCH: Todas las alternativas deben ser diferentes. Debe combinarse con
la sentencia BREAK.
Sentencias iterativas
1. WHILE.
2. DO-WHILE.
3. FOR.
for (expr1; expr2; expr3)
sentencia;
===
expr1;
while (expr2) {
sentencia;
expr3;
}
Puede omitirse cualquiera de las expresiones.
Sentencias de bifurcación
1. BREAK: Sale de la sentencia SWITCH, WHILE o FOR más interna.
2. CONTINUE: Salta a reevaluar la condición del bucle más interno.
3. GOTO: Salto incondicional.
18 de febrero de 2004
Notas 10
4.9.
Funciones
Un programa C consta de 1 o más funciones. Un programa completo C tiene una
función denominada main().
C no permite definir funciones dentro de funciones.
La finalización de la función ocurre cuando se llega al final de la función (}) o se
ejecuta una sentencia return().
Los parámetros se pasan siempre por valor.
Retornan por defecto un valor de tipo entero. Las funciones que no devuelven
nada se definen de tipo void.
Las funciones C pueden ser recursivas.
18 de febrero de 2004
Notas 11
4.10.
Variables
Las variables de C pueden ser:
Externas: Son las que se definen fuera de cualquier función y, por tanto, son
potencialmente utilizables por muchas funciones. El campo de validez de una
variable externa abarca desde el punto de su declaración en un archivo fuente
hasta el fin del archivo. Si se ha de hacer referencia a una variable externa antes
de su definición o si está definida en un archivo fuente que no es aquel en que se
usa, es obligatoria una declaración extern.
Automáticas: Son las definidas dentro de las funciones.
Estáticas: Las variables estáticas (static) pueden ser internas o externas:
• Las variables estáticas internas son locales a una función en la misma forma
que las automáticas pero, a diferencia de ellas, su existencia es permanente,
en lugar de aparecer y desaparecer al activar la función. Esto significa que
las variables estáticas internas proporcionan un medio de almacenamiento
permanente y privado a una función.
• Las variables estáticas externas son accesibles en el resto del archivo fuente en
el que está declarada, pero no en otro. Por tanto, el almacenamiento estático
externo proporciona un medio de almacenamiento permanente y privado a
un archivo (de hecho, no habrá conflictos con los mismos nombres en otros
archivos del mismo programa).
Registro: Una declaración register avisa al compilador que la variable será muy
usada. Cuando es posible se colocan en los registros de la máquina, lo que produce
programas más cortos y rápidos.
Existen algunas restricciones en las variables registro, que reflejan la realidad del
hardware subyacente:
• Pocas variables de cada función se pueden mantener en registros.
• Sólo se permiten algunos tipos.
• La palabra register se ignora si hay declaraciones excesivas o no permitidas.
• No es posible tomar la dirección de una variable registro.
Las variables de C pueden definirse en una forma estructurada en bloques. Las declaraciones de variables (incluyendo inicializaciones) se colocan despues de la llave de apertura que introduce cualquier sentencia compuesta, no solamente al comienzo de una
función. Las variables declaradas ası́ se solapan con cualquier variable del mismo nombre en bloques externos, y permanecen hasta que se alcanza la llave de cierre.
18 de febrero de 2004
Notas 12
4.10.1.
Reglas de inicialización de variables
En ausencia de una inicializacion explı́cita, se garantiza que las variables externas
y estáticas tendran inicialmente el valor cero. Las variables automáticas y registro
tienen valores indefinidos (”basura”).
Las variables externas y estáticas sólo pueden inicializarse mediante una expresión
constante; la inicialización se realiza una sola vez, conceptualmente antes de que
comience la ejecución del programa.
Las variables automáticas y registro pueden inicializarse a valores no constantes
(cualquier expresión, incluyendo llamadas a funciones), y se inicializan cada vez
que se entra en la función o bloque.
18 de febrero de 2004
Notas 13
4.11.
Entrada/salida
La entrada/salida básica de caracteres se realiza mediante dos macros definidas en
<stdio.h>. Son:
putchar(char c)
int getchar()
La entrada/salida de strings se realiza mediante las funciones:
gets(char *linea)
puts(const char *linea)
La entrada/salida formateada se realiza mediante las funciones:
printf(char *formato, arg1, arg2, ...)
scanf(char *formato, arg1, arg2, ...)
La conversión de strings a tipos básicos y viceversa se realiza mediante las funciones:
sprintf(char *s, const char *format, ...)
sscanf(char *s, const char *format, ...)
18 de febrero de 2004
Notas 14
4.12.
4.12.1.
Estructuras de datos
Sinónimo (alias)
C permite crear sinónimos de tipos de datos. Las principales razones para utilizar este
tipo de declaraciones son:
1. Facilitar la documentación (y legibilidad) de un programa.
2. Asociar nombres más cortos a identificadores largos.
3. Parametrizar un programa contra problemas de portabilidad. Si se utiliza typedef
con los tipos de datos que pueden ser dependientes de la instalación, sólo se
tendrán que cambiar los typedef cuando se lleve el programa a otro computador.
Una práctica común es emplear typedef para las cantidades enteras y luego hacer
las elecciones apropiadas de short, int y long en cada computadora.
4. Utilizar algún programa que aproveche la informacion contenida en las declaraciones typedef para realizar comprobaciones de tipos en el programa (por ejemplo,
lint).
4.12.2.
Enumerado
A menos que se le asignen valores explı́citos, el compilador asigna valores enteros constantes sucesivos comenzando por cero. La declaración de un enumerado tiene las siguientes ventajas frente a las declaraciones equivalentes mediante macros:
1. El compilador puede detectar errores en las expresiones y asignaciones.
2. El depurador puede imprimir el valor simbólico (en vez del valor entero).
18 de febrero de 2004
Notas 15
4.12.3.
Array
Declaración: Existen dos formas de declaración de arrays:
1. Explı́cita: cuando se especifica el número de elementos del array.
2. Implı́cita: cuando el número de elementos del array lo calcula el compilador.
Los arrays multidimensionales se declaran mediante notación de vectores (no matricial) y se almacenan por filas.
Acceso. El ı́ndice de acceso del array es una expresión entera. Es responsabilidad
del programador garantizar que el valor del ı́ndice está dentro de los lı́mites del
array.
18 de febrero de 2004
Notas 16
4.12.4.
Estructura (Registro)
Declaración. Una estructura es un conjunto de una o más variables, posiblemente
de tipos diferentes, agrupadas bajo un mismo nombre para hacer más eficiente el
manejo. Las estructuras se denominan registros en otros lenguajes; por ejemplo,
en Pascal.
• Opcionalmente puede seguir un nombre a la palabra clave struct; se lo denomina nombre de la estructura y se puede emplear en declaraciones posteriores
como una abreviatura de la estructura.
• La llave de cierre que termina la lista de miembros puede ir seguida de una
lista de variables.
• Si la declaración de una estructura no va seguida de la lista de variables, no
se reserva memoria alguna; en este caso se está describiendo una plantilla de
la estructura.
• Se puede inicializar una estructura externa o estática añadiendo a su definición la lista de inicializadores de los componentes.
• Las estructuras se pueden anidar.
• Podemos declarar arrays de estructuras.
Acceso. Para referenciar un miembro de una estructura en una expresión, se
emplea la notación punto.
nombre_de_la_estructura.miembro
4.12.5.
Restricciones de las estructuras
Las estructuras no pueden compararse; las únicas operaciones que se pueden realizar
con una estructura son copiarlas o asignarlas como un todo, tomar su dirección (mediante &), y acceder a uno de sus miembros.
18 de febrero de 2004
Notas 17
4.12.6.
Unión
Una unión es una estructura donde todos los miembros tienen desplazamiento cero. La
estructura es lo suficientemente grande como para contener el mayor de los miembros,
y la alineación es la apropiada para todos los tipos de la unión. Es responsabilidad del
programador recordar cual es el tipo que hay en la unión. Las uniones pueden aparecer
dentro de estructuras y arreglos, o viceversa. La notación para acceder a un miembro
de una unión dentro de una estructura (o viceversa) es idéntica a la empleada con
estructuras anidadas.
4.12.7.
Campos de bit
La forma usual de manejar bits consiste en definir un conjunto de máscaras (en potencias de 2) que corresponden a las posiciones de los bits. El acceso a los bits se convierte
en un juego de operaciones de desplazamiento, enmascaramiento y complementación.
C posee capacidad para definir y acceder directamente a campos definidos en el interior
de una palabra, en lugar de hacerlo mediante máscaras. Un campo es un conjunto de
bits adyacentes dentro de un int. La sintaxis de la definición de los campos se basa en
las estructuras. Los campos sin nombre (dos puntos y un tamaño solamente) sirven de
relleno.
18 de febrero de 2004
Notas 18
4.13.
Punteros
Un puntero es una variable que contiene la dirección de otra variable. Los punteros se
utilizan con abundancia en C, debido a que:
A veces son la única manera de expresar un cálculo.
Con ellos puede obtenerse un código más compacto y eficiente.
C proporciona dos operadores especiales para punteros:
El operador unitario * toma su operando como una dirección y accede a ella para
obtener su contenido.
El operador unitario & devuelve la dirección de un objeto. Sólo puede aplicarse
a variables y a elementos de un arreglo; construcciones como &(x + 1) y &3 son
ilegales. Tambien es ilegal obtener la dirección de una variable de tipo register.
La declaración del puntero p1 se entiende como un nemotécnico; se quiere indicar que
la combinación ∗p1 equivale a una variable de tipo int. C permite declarar punteros
dobles (punteros a punteros).
En general, un puntero se puede inicializar como cualquier otra variable, aunque normalmente los únicos valores significativos son cero (NULL) o una expresión en que
aparezcan direcciones de objetos del tipo apropiado.
Los punteros pueden aparecer en expresiones. Los operadores unitarios ∗ y & tienen
mayor precedencia que los operadores aritméticos, por lo que al evaluar y = ∗p1 + 1
la expresión toma el valor del objeto al que apunta p1, se le suma 1, y el resultado se
asigna a y. También pueden aparecer referencias a punteros en la parte izquierda de
una asignación (∗p1 = 0; ∗p1+ = 1).
La expresión (∗p1) + + necesita los paréntesis; sin ellos incrementarı́a el valor del
puntero p1 y no el objeto al que apunta, ya que los operadores unitarios ∗ y ++ se
evalúan de derecha a izquierda.
Los punteros se pueden comparar entre ellos o con cero (NULL).
Como los punteros son variables, se pueden manipular igual que cualquier otra variable
(p2 = p1).
Como los parámetros de las funciones se pasan siempre por valor, cuando queremos
que la función devuelva valores mediante los parámetros tenemos que pasar punteros.
18 de febrero de 2004
Notas 19
4.13.1.
Punteros y arrays
En C existe una estrecha relación entre punteros y arrays, ya que cualquier operación
que se pueda realizar mediante la indexación de un array se puede realizar también con
punteros. La versión con estos puede ser más rápida pero, más difı́cil de entender.
Si ptr apunta a un elemento particular de un array, entonces ptr − i apunta al elemento
que está i elementos antes de ptr, y ptr + i apunta al elemento que está i elementos
después. Si ptr apunta a a[0], entonces ∗(p1 + i) se refiere al contenido de a[i]. Esto es
cierto independientemente del tipo de variables del array. La definición de sumar 1 a
un puntero, y, por extensión, toda la aritmética de punteros establece que el incremento
se adecúa al tamaño en memoria del objeto apuntado. En pa + i, i se multiplica por el
tamaño de los objetos a los que apunta pa antes de ser sumado a p1.
Cuando se pasa el nombre de un array a una función, se pasa la dirección del comienzo
del array (un puntero). El paso de arrays multidimensionales a una función no requiere
especificar la primera dimension (es irrelevante porque lo que se trasmite realmente es,
como antes, un puntero). En el ejemplo de la transparencia, la última declaración de la
derecha indica que el parámetro es un puntero a un array de 4 enteros. Los paréntesis
son necesarios porque los corchetes [ ] tienen mayor precedencia que el operador ∗; sin
paréntesis. La declaración int *tabla[4]; serı́a un array de 4 punteros a enteros.
4.13.2.
Punteros y estructuras: notación − >
Si p es un apuntador a una estructura, los miembros de la estructura se pueden referenciar mediante (*pd).miembro (hacen falta los paréntesis porque la precedencia del
operador de miembro de estructuras es mayor que la de *). Sin embargo, como son tan
frecuentes los apuntadores a estructuras se ha introducido en el lenguaje la notación
abreviada − >, que es equivalente.
4.13.3.
Punteros a funciones
En C es posible definir un puntero a una función, que puede ser manipulado, pasado
a funciones, colocado en arrays, etc. La declaración de un puntero a función tiene la
siguiente sintaxis:
tipo (*nombre_puntero)()
Es necesaria la primera pareja de paréntesis. Sin ellos estarı́amos haciendo una declaración
de una función que devuelve un puntero a un entero. La llamada se realiza mediante:
(*nombre_puntero)(parametros)
18 de febrero de 2004
Notas 20
4.13.4.
Resumen de aritmética de punteros
En resumen, las operaciones aritméticas que podemos realizar con los punteros de C
son las siguientes:
Pueden inicializar como cualquier otra variable, aunque normalmente los únicos
valores significativos son 0 (NULL) o una expresión en que aparezcan direcciones
de objetos del tipo apropiado.
Puede sumarse o restarse un entero a un puntero.
Pueden compararse.
Pueden restarse punteros.
No se permite sumar, restar, multiplicar, dividir, rotar, enmascarar
punteros, ni sumarles valores float o double.
18 de febrero de 2004
Notas 21
4.13.5.
Parámetros de la lı́nea de comandos
Cuando se invoca main al comienzo de la ejecución, se llama con dos parámetros.
1. El primero (llamado argc por convenio) es el número de argumentos en la lı́nea
de comandos con que se invocó al programa.
2. El segundo (argv) es un puntero a un array de cadenas de caracteres que contienen
los argumentos, uno por cadena.
Por convenio, argv[0] es el nombre con el que se invoco el programa; por tanto, argc es
como mı́nimo 1 (el primer argumento real es argv[1] y el último es argv[argc-1]).
Debido a que argv un puntero a un array de punteros, existen varias maneras de
manipular los parámetros.
18 de febrero de 2004
Notas 22
4.14.
Tratamiento de ficheros
Para manejar ficheros con formato en C necesitamos punteros a una estructura de datis
definida en el fichero <stdio.h> denominada FILE. Las principales funciones C para
tratamiento de ficheros son:
1. Apertura, cierre, renombrado y borrado:
FILE *fopen(char *nombre, char *modo)
El modo de apertura puede ser:
• r: modo lectura.
• w: modo escritura (crea el fichero si no existe y descarta su contenido en
caso de que ya existiese).
• a: modo añadir (si no existe lo crea).
Los modos r+, w+ y a+ tienen el mismo significado que sus homólogos r, w,
a, pero se permite tanto la lectura como la escritura. Si el modo incluye una
b (rb, r+b), significa que es un fichero binario. En caso de error retorna un
puntero NULL.
int fclose(FILE *f)
int rename(const char *antiguo nombre, const char *nuevo nombre)
int remove(const char *nombre fichero)
2. Transferencia:
int fgetc(FILE *f)
int fputc(int c, FILE *f)
char *fgets(char *linea, int max linea, FILE *f)
int fputs(char *linea, FILE *fp)
int fscanf(FILE *f, char *format, ....)
int fprintf(FILE *f, char *format, ....)
3. Posicionamiento:
int fseek(FILE *f, long despl, int posicion)
long ftell(FILE *f)
void rewind(FILE *f);
4. Funciones de error:
int ferror(FILE *f)
int feof(FILE *f)
Existen tres punteros de tipo FILE que son estándar y están definidos en <stdio.h>:
stdin, puntero a la entrada estándar, stdout, puntero a la salida estándar y stderr,
puntero a la salida estándar de errores. Las funciones getchar() y putchar() son macros
definidas en <stdio.h> de la siguiente forma:
#define getchar() getc(stdin)
#define putchar(x) putc(x,stdout)
18 de febrero de 2004
Notas 23
4.15.
Errores frecuentes de los programadores de C
En este último apartado se presentan los errores que comenten con más frecuencia los
nuevos programadores de C.
18 de febrero de 2004
Notas 24
18 de febrero de 2004
Notas 25
18 de febrero de 2004
Notas 26
18 de febrero de 2004
Notas 27
4.16.
Programación de estructuras de datos dinámicas con C
La combinación de estructuras (registros) y punteros permiten crear estructuras cuya
forma y tamaño no sea fija: estructuras de datos dinámicas. La unidad básica de las
estructuras dinámicas es el nodo (registros que son enlazados mediante punteros para
formar la estructura deseada). En términos generales las estructuras se pueden agrupar
en tres clases: estructuras lineales, estructuras jerárquicas, y grafos3 .
La declaración de un nodo se realiza mediante una declaración recursiva (una estructura
que se autoreferencia —ver transparencia—). El miembro next es una variable de tipo
puntero que contendrá la dirección de otro nodo.
Para utilizar las estructuras dinámicas es necesario solicitar memoria (mediante malloc()) y liberar memoria (mediante free()). Para ello es necesario utilizar las rutinas
definidas en el fichero stdio.h.
En las siguientes transparencias veremos cómo podemos implementar en C las siguientes estructuras dinámicas:
Pilas.
Colas.
Listas simplemente encadenadas.
Listas doblemente encadenadas.
Arboles.
3
Un grafo es una generalización de una estructura dinámica en la que cada nodo puede tener varios enlaces,
y pueden existir bucles
18 de febrero de 2004
Notas 28
4.16.1.
Pila (LIFO)
Una pila es una estructura de datos que consta de dos operaciones: push() para insertar
un elemento y pop() para extraer el último elemento insertado. Por esta razón las pilas
también se denominan estructuras LIFO (del inglés Last In First Out).
Las pilas pueden declararse mediante arrays. Esto tiene el inconveniente de que necesitamos reservar el espacio máximo necesario para ella. En la transparencia se presenta
la implementación en C de una pila dinámica de datos de tipo entero. Utilizamos la
variable global Top para apuntar a la cima de la pila. El algoritmo de las funciones
push() y pop() es el siguiente:
Push():
• Creamos un nuevo nodo.
• Rellenamos la información del nuevo nodo.
• Actualizamos el puntero de cima de pila al nuevo nodo.
Pop():
• Si la pila está vacı́a retornamos un código de error.
• Guardamos en una variable auxiliar la información del nodo que está en la
cima de la pila.
• Guardamos en un puntero auxiliar la dirección del nodo que está en la cima
de la pila.
• Actualizamos el puntero de cima de pila al siguiente nodo.
• Liberamos el nodo que estaba en la cima de la pila.
• Retornamos la información que contenı́a el nodo que hemos liberado.
18 de febrero de 2004
Notas 29
4.16.2.
Cola
Una cola es una estructura de datos que consta de dos operaciones: una para insertar
un elemento al final de la cola y otra para para extraer el elemento que está al principio
de la cola. Por esta razón las pilas también se denominan estructuras FIFO (del inglés
First In First Out).
En esta implementación utilizamos las variables globales principio y final para apuntar
al primer y último elemento de la cola. El algoritmo de las funciones insertar() y
extraer() es el siguiente:
Insertar()
• Creamos un nuevo nodo.
• Rellenamos la información del nuevo nodo.
• Si la cola está vacı́a actualizamos los punteros primero y siguiente al nuevo
nodo.
• Si la cola no está vacı́a insertamos el nuevo nodo al final de la cola.
Extraer()
• Si la cola está vacı́a retornamos un error.
• Guardamos en variables auxiliares la información del primer elemento de la
cola (el nodo que vamos a liberar) y la dirección del segundo elemento de la
cola (que va a pasar a ser el nuevo primer elemento de la cola).
• Liberamos la memoria del primer elemento.
• Actualizamos el puntero al nuevo primer elemento de la cola.
• Si la cola está vacı́a (el primer elemento es NULL), actualizamos el puntero
al último elemento de la cola a NULL.
• Retornamos la información del nodo que hemos extraido.
18 de febrero de 2004
Notas 30
4.16.3.
Lista simplemente encadenada
La implementación de una lista simplemente encadenada es similar a cola, pero se
diferencia en que los elementos se insertan y extraen de forma ordenada.
18 de febrero de 2004
Notas 31
.
Lista simplemente encadenada (cont.)
La función de busqueda recibe un parámetro de entrada (el dato a buscar —n—) y
retorna dos parámetros de salida:
1. El nombre de la función retorna un puntero al nodo donde se encontró el dato.
2. El parámetro de salida anterior es un puntero al nodo que hay antes (siguiendo
el orden de la lista) del nodo donde se encontró el dato.
De esta forma podemos utilizar la función buscar con dos fines:
1. Saber la información solicitada está en la lista (ya que la función retorna NULL
cuando la clave no está en la lista).
2. Facilitar la extracción del nodo (ya que la función retorna las direcciones del nodo
anterior al buscado y la dirección del nodo buscado, con lo que solamente necesitamos actualizar los punteros —observese la función Borrar en la transparencia—).
Si anterior vale NULL significa que la clave se encontró en el primer nodo de la
lista.
18 de febrero de 2004
Notas 32
4.16.4.
Lista doblemente encadenada
En una lista doblemente encadenada cada nodo tiene dos punteros: un puntero al
siguiente nodo (en orden) de la lista, y un puntero al nodo anterior. Como puede
apreciarse en la transparencia, la función de inserción es similar a la función de inserción
en una lista simplemente encadenada (sólo hay que tener cuidado en la actualización
del nuevo puntero).
18 de febrero de 2004
Notas 33
4.16.5.
Arbol binario
El arbol binario es probablemente la estructura dinámica más utilizada. Cada nodo
de la estructura tiene un predecesor (padre o ancestro) y uno o dos sucesores (hijo o
descendientes).
La transparencia contiene el pseudocódigo de un algoritmo de recorrido in-order iterativo del arbol binario. El algoritmo consiste en comenzar profundizando en el arbol
sólamente por la rama izquierda de los nodos a la vez que almacenamos en la pila la
dirección de estos nodos (porque aún no los hemos procesado).
Al llegar a lo más profundo del a’rbol por su rama más izquierda, en la cima de la
pila tenemos el primer nodo que debemos procesar. Por lo tanto, extraemos el nodo
de la pila, lo procesamos, e intentamos profundizar a partir de su hijo derecho. Si no
tiene hijo derecho, sacamos otro nodo de la pila (el padre), lo procesamos e intentamos
profundizar por su hijo derecho. Este proceso se repite hasta procesar el arbol completo.
El algoritmo recursivo es quizás más fácil de entender. Es básicamente el mismo algorimo, lo que en este caso la pila se implementa de forma automática mediante las
llamadas recursivas.
18 de febrero de 2004
Notas 34
.
Arbol binario (cont.)
La inserción en el arbol binario sigue el siguiente algoritmo:
Si el arbol está vacı́o, la raiz es el nuevo nodo que ibamos a insertar.
Si el arbol no está vacı́o, buscamos la posición en el árbol donde debemos insertar
el nodo y actualizar el puntero correspondiente.
Descargar