Taller de Lenguaje C PDF
Taller de Lenguaje C PDF
Eduardo Grosclaude
2014-12-09
[2016/03/16, 13:11:47- Material en preparación, se ruega no imprimir mientras aparezca esta nota]
2
Índice general
3
ÍNDICE GENERAL ÍNDICE GENERAL
4
Capítulo 1
Introducción al Lenguaje C
El lenguaje de programación C fue creado en 1972 por investigadores de Bell Telephone Laboratories,
con el objetivo de reescribir un sistema operativo, el UNIX, en un lenguaje de alto nivel, para poder adaptarlo
(es decir, portarlo) a diferentes arquitecturas. Por este motivo sus creadores se propusieron metas de diseño
especiales, tales como:
Poder utilizar todos los recursos del hardware (“acceso al bajo nivel”).
Obtener código generado eficiente en uso de memoria y en tiempo de ejecución (programas pequeños
y veloces).
Paradigma procedural
El lenguaje C no es un lenguaje orientado a objetos, sino que adhiere al paradigma tradicional
de programación imperativa o procedural. No soporta la orientación a objetos propiamente dicha, al
no proporcionar herramientas fundamentales, como la herencia. Sin embargo, algunas características del
lenguaje permiten que un proyecto de programación se beneficie, de todas maneras, con la aplicación de
algunos principios de la orientación a objetos, tales como el ocultamiento de información y el encapsula-
miento de responsabilidades. El lenguaje C++, orientado a objetos, no es una versión más avanzada del
lenguaje o un compilador de C con más capacidades, sino que se trata de un lenguaje completamente
diferente.
5
1.1. CARACTERÍSTICAS DEL LENGUAJE CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C
Minimalidad
Un programa en C es, por lo general, más sintético que en otros lenguajes procedurales; la idea central
que atraviesa todo el lenguaje es la minimalidad. La definición del lenguaje consta de muy pocos elementos,
y tiene muy pocas palabras reservadas.
Como rasgo distintivo, en C no existen, rigurosamente hablando, funciones o procedimientos de uso
general del programador. Por ejemplo, no tiene funciones de entrada/salida; la definición del lenguaje
apenas alcanza a las estructuras de control y los operadores. La idea de definir un lenguaje sin funcio-
nes es, por un lado, hacer posible que el compilador sea pequeño, fácil de escribir e inmediatamente
portable; y por otro, permitir que sea el usuario quien defina sus propias funciones cuando el problema de
programación a resolver tenga requerimientos especiales. El usuario puede escribir sus propios procedimien-
tos (llamados funciones aunque no devuelvan valores). Aunque existe la noción de bloque de sentencias
(sentencias encerradas entre llaves), el lenguaje se dice no estructurado en bloques porque no pueden
definirse funciones dentro de otras.
Versatilidad, a un precio
El lenguaje entrega completamente el control de la máquina subyacente al programador, no realizando
controles en tiempo de ejecución. Es decir, no verifica condiciones de error comunes como overflow de
variables, errores de entrada/salida, o consistencia de argumentos en llamadas a funciones. Ofrece
una gran libertad sintáctica al programador. No es fuertemente tipado. Cuando es necesario, se realizan
conversiones automáticas de tipo en las asignaciones, a veces con efectos laterales inconvenientes si
no se tiene precaución. Una función que recibe determinados parámetros formales puede ser invocada con
argumentos reales de otro tipo.
Se ha dicho que estas características “liberales” posibilitan la realización de proyectos complejos con
más facilidad que otros lenguajes como Pascal o Ada, más estrictos; aunque al mismo tiempo, así resulta
más difícil detectar errores de programación en tiempo de compilación. Como resultado, es frecuente que
el principiante, y aun el experto, cometan errores de programación que no se hacen evidentes enseguida,
ocasionando problemas y costos de desarrollo. En este sentido, según los partidarios de la tipificación
estricta, C no es un buen lenguaje. Gran parte del esfuerzo de desarrollo del estándar ANSI se dedicó a
dotar al C de elementos para mejorar esta deficiencia.
Una característica especial del lenguaje C es que el pasaje de argumentos a funciones se realiza
siempre por valor. ¿Qué ocurre cuando una función debe modificar datos que recibe como argumentos?
La única salida es pasarle –por valor– la dirección del dato a modificar. Las consecuencias de este hecho son
más fuertes de lo que parece a primera vista, ya que surge la necesidad de todo un conjunto de técnicas de
manejo de punteros que no siempre son bien comprendidas por los programadores poco experimentados,
y abre la puerta a sutiles y escurridizos errores de programación. Quizás este punto, junto con el de la
ausencia de comprobaciones en tiempo de ejecución, sean los que le dan al C fama de “difícil de aprender”.
Portabilidad
Los tipos de datos no tienen un tamaño determinado por la definición del lenguaje, sino que diferentes
implementaciones pueden adoptar diferentes convenciones. Paradójicamente, esta característica obedece al
objetivo de lograr la portabilidad de los programas en C. El programador está obligado a no hacer ninguna
suposición sobre los tamaños de los objetos de datos, ya que lo contrario haría al software dependiente
de una arquitectura determinada (“no portable”).
6
CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C 1.2. EVOLUCIÓN DEL LENGUAJE
Biblioteca Standard
Pese a no estar formalmente definidas funciones de entrada/salida en el lenguaje, se ha establecido un
conjunto mínimo de funciones, llamado la Biblioteca Standard del lenguaje C, que todos los compiladores
proveen, a veces con agregados. La filosofía de la Biblioteca Standard es la portabilidad, es decir, casi
no incluye funciones que sean específicas de un sistema operativo determinado. Aquellas que sí incluye
están orientadas a la programación de sistemas, y a veces no resultan suficientes para el programador
de aplicaciones. No provee, por ejemplo, la capacidad de manejo de archivos indexados, ni funciones de
entrada/salida interactiva por consola que sean seguras (“a prueba de usuarios”). Estas deficiencias se
remedian utilizando bibliotecas de funciones “de terceras partes” (creadas por el usuario u obtenidas de
otros programadores).
Las funciones de la Biblioteca Standard no tienen ningún privilegio sobre las del usuario y sus nom-
bres no son palabras reservadas; el usuario puede reemplazarlas por sus propias funciones simplemente
dándoles el mismo nombre.
Figura 1.1: Brian Kernighan y Dennis Ritchie, los creadores del lenguaje C
Tipos long long int y unsigned long long int de al menos 64 bits
Tipo boolean
7
1.3. EL CICLO DE COMPILACIÓN CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C
Compilador
El compilador acepta un archivo fuente, posiblemente relacionado con otros (una unidad de tra-
ducción), y genera con él un módulo objeto. Este módulo objeto contiene porciones de código
ejecutable mezclado con referencias, aún no resueltas, a variables o funciones cuya definición no
figura en los fuentes de entrada. Estas referencias quedan en forma simbólica en el módulo objeto
8
CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C 1.4. UN PRIMER EJEMPLO
Si ocurren errores en esta fase, se deberán a problemas de sintaxis (el código escrito por el programador
no respeta la definición del lenguaje).
Si ocurren errores en esta fase, se deberán a que existen variables o funciones cuya definición no
ha sido dada (no se encuentran en las unidades de traducción procesadas, ni en ninguna biblioteca
conocida por el linker).
Bibliotecario
El bibliotecario es un programa administrador de módulos objeto. Su función es reunir módulos objeto
en archivos llamados bibliotecas, y luego permitir la extracción, borrado, reemplazo y agregado de
módulos. El conjunto de módulos en una biblioteca se completa con una tabla de información sobre
sus contenidos para que el linker pueda encontrar rápidamente aquellos módulos donde se ha definido
una variable o función, y así extraerlos durante el proceso de linkedición.
El bibliotecario es utilizado por el usuario cuando desea mantener sus propias bibliotecas. La creación
de bibliotecas propias del usuario ahorra tiempo de compilación y permite la distribución de software
sin revelar la forma en que se han escrito los fuentes y protegiéndolo de modificaciones.
Una vez que el código ha sido compilado y vinculado, obtenemos un programa ejecutable. Los errores
que pueden producirse en la ejecución ya no corresponden a problemas de compilación, sino que se deben
a aspectos de diseño del programa que deben ser corregidos por el programador.
#include <stdio.h>
/* El primer programa! */
main()
{
printf("Hola, gente!\n");
}
9
1.4. UN PRIMER EJEMPLO CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C
Standard. Los prototipos se incluyen para advertir al compilador de los tipos de las funciones y de
sus argumentos.
Entre los pares de caracteres especiales /* y */se puede insertar cualquier cantidad de líneas de
comentarios.
La función main() es el cuerpo principal del programa (es por donde comenzará la ejecución). Todas
las funciones en C están delimitadas por un par de llaves. Terminada la ejecución de main(), terminará
el programa.
La función printf() imprimirá la cadena entre comillas, que es una constante string terminada
por un carácter de nueva línea (la secuencia especial “\n”).
1. Copiar el programa con cualquier editor de textos y guardarlo en un archivo llamado hola.c en el
directorio de trabajo del usuario.
2. Sin cambiar de directorio, invocar al compilador ejecutando el comando gcc hola.c -o hola. Por
defecto, el compilador gcc invocará al vinculador ld para generar el ejecutable a partir del archivo
objeto intermedio generado.
Notar el punto y barra del principio al ejecutar el programa. El punto y barra le indican al shell que
debe buscar el programa en el directorio activo.
Otra manera
1. Como antes, copiar el programa, o usar el mismo archivo fuente de hace un momento.
2. Sin cambiar de directorio, ejecutar el comando make hola en una consola o terminal.
La diferencia es que en el primer caso invocamos directamente al compilador gcc, mientras que en el
segundo caso utilizamos la herramienta make, que nos asiste en la compilación de proyectos. En el ejemplo,
le decimos al compilador que procese el archivo fuente hola.c, y que el ejecutable de salida (opción -o,
de output) reciba el nombre hola.
El comando make
Cuando damos un comando como make hola, utilizamos el comando make para compilar y vincular
el programa hola.c. El comando make contiene la inteligencia para:
ver que no existe en el directorio activo un programa ejecutable llamado hola, o que, si existe, su
fecha de última modificación es anterior a la del fuente;
10
CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C 1.5. MAPA DE MEMORIA DE UN PROGRAMA
razonar que, por lo tanto, es necesario compilar el fuente hola.c para producir el ejecutable hola;
e invocar con una cantidad de opciones por defecto al compilador gcc, y renombrar la salida con el
nombre hola. Éste será el ejecutable que deseamos producir.
Si se invoca al comando make una segunda vez, éste comprobará, en base a las fechas de modificación
de los archivos fuente y ejecutable, que no es necesaria la compilación (ya que el ejecutable es posterior
al fuente). Si editamos el fuente para cambiar algo en el programa, invocar nuevamente a make ahora
repetirá la compilación (porque ahora el fuente es posterior al ejecutable).
11
1.5. MAPA DE MEMORIA DE UN PROGRAMA CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C
Texto del programa La región de texto contendrá el código del programa, es decir, la versión ejecutable
de las instrucciones que escribió el programador, traducidas por el compilador al lenguaje de la
máquina. En general, el programa fuente C se compondrá de funciones, que serán replicadas a nivel de
máquina por subrutinas en el lenguaje del procesador subyacente. Algunas instrucciones C resultarán
en última instancia en invocaciones a funciones del sistema (por ejemplo, cuando necesitamos escribir
en un archivo).
Datos estáticos La región de datos estáticos es un lugar de almacenamiento para datos del programa
que quedan definidos al momento de la compilación. Se trata de datos cuya vida o instanciación
no depende de la invocación de las funciones. Son las variables estáticas, definidas en el cuerpo
del programa que es común a todas las funciones. A su vez, esta zona se divide en dos: la de
datos estáticos inicializados explícitamente por el programa (zona a veces llamada bss por motivos
históricos) y la zona de datos estáticos sin inicializar (a veces llamada data), que será llenada con
ceros binarios al momento de la carga del programa.
Stack El stack, o pila, aloja las variables locales de las funciones a medida que esas funciones son invocadas.
Cada función que declare variables locales obtendrá espacio de almacenamiento para esas variables
en el stack. Al terminar la función, como el ámbito de sus variables desaparece, esas variables son
desalojadas y el espacio que ocupaban vuelve a quedar disponible.
Heap Un programa C puede utilizar estructuras de datos dinámicas, como listas o árboles, que vayan
creciendo al agregárseles elementos. El programa puede “pedir” memoria cada vez que necesite alojar
un nuevo elemento de estas estructuras dinámicas, o para crear buffers temporarios para cualquier
uso que sea necesario. El heap es la zona de donde el programa obtiene esos trozos de memoria,
solicitada en forma dinámica al sistema operativo. El límite del heap se irá desplazando hacia las
direcciones superiores.
Este modelo será útil en varias ocasiones para explicar algunas cuestiones especiales del lenguaje C.
Ejemplo 1.1
Analicemos en qué lugares quedarán alojados los elementos del programa siguiente.
int a = 1;
int b;
int fun()
{
int c;
c = a + b;
}
12
CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C 1.6. EJERCICIOS
main()
{
fun();
}
Las variables a y b corresponden a la zona de datos estáticos. La variable a está inicializada con 1, pero como b no
está inicializada, recibirá un valor 0.
La variable c aparecerá en el stack cuando se ejecute la función fun().
Las instrucciones de máquina procedentes de la compilación del programa (funciones main() y fun()) serán almace-
nadas en la región de texto.
1.6 Ejercicios
1. ¿Qué nombres son adecuados para los archivos fuente C?
4. ¿Qué pasaría si un programa en C no contuviera una función main()? Haga la prueba modificando
hola.c.
5. Edite el programa hola.c y modifíquelo según las pautas que siguen. Interprete los errores de compi-
lación. Identifique en qué etapa del ciclo de compilación ocurren los errores. Si resulta un programa
ejecutable, observe qué hace el programa y por qué.
13
1.7. PREGUNTAS PARA EL CAPÍTULO 1 CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C
2. La primera definición oficial del lenguaje fue dada por Kernighan y Ritchie en
A. 1975. B. 1978. C. 1983. D. 1988.
4. La Biblioteca Standard de C
A. Provee funciones para todas las necesidades. B. Está escrita por el usuario. C. No provee
funciones para todas las necesidades.
5. El lenguaje C
A. No realiza recolección de basura pero sí controles de tiempo de ejecución. B. No realiza controles
de tiempo de ejecución pero sí recolección de basura. C. Realiza ambas cosas. D. Ninguna de las
dos cosas.
14
CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C 1.7. PREGUNTAS PARA EL CAPÍTULO 1
Respuestas al Capítulo 1
1. D 2. B 3. B 4. C 5. D 6. B 7. A 8. B 9. B 10. D 11. D 12. A
15
1.7. PREGUNTAS PARA EL CAPÍTULO 1 CAPÍTULO 1. INTRODUCCIÓN AL LENGUAJE C
16
Capítulo 2
El preprocesador
El compilador C tiene un componente auxiliar llamado preprocesador, que actúa en la primera etapa
del proceso de compilación. Su misión es buscar, en el texto del programa fuente entregado al compilador,
ciertas directivas que le indican realizar alguna tarea a nivel de texto. Por ejemplo, inclusión de otros
archivos, o sustitución de ciertas cadenas de caracteres (símbolos o macros) por otras.
El preprocesador cumple estas directivas en forma similar a como podrían ser hechas interactivamente
por el usuario, utilizando los comandos de un editor de texto (“incluir archivo” o “reemplazar texto”), pero
en forma automática. Una vez cumplidas todas estas directivas, el preprocesador entrega el texto resultante
al resto de las etapas de compilación, que terminarán dando por resultado un módulo objeto. Un archivo
fuente, junto con todos los archivos que incluya, es llamado una unidad de traducción.
17
2.1. DIRECTIVAS DE PREPROCESADOR CAPÍTULO 2. EL PREPROCESADOR
facilitando su mantenimiento. Si una variable o función se utiliza en varios archivos fuente, es posible aislar
su declaración, colocándola en un único archivo aparte que será incluido al tiempo de compilación en los
demás fuentes. Esto facilita toda modificación de elementos comunes en los fuentes de un proyecto. Por
otro lado, si una misma constante o expresión aparece repetidas veces en un texto, y es posible que su
valor deba cambiarse más adelante, es muy conveniente definir esa constante con un símbolo y especificar
su valor sólo una vez, mediante un símbolo o macro.
Símbolos y macros
Una de las funciones del preprocesador es sustituir símbolos, o cadenas de texto dadas, por otras (Fig.
2.1). La directiva define establece la relación entre los símbolos y su expansión, o cadena a sustituir. Los
símbolos indicados con una directiva de definición define se guardan en una tabla de símbolos durante
el preprocesamiento.
Habitualmente se llama símbolos a aquellas cadenas que son directamente sustituibles por una expre-
sión, reservándose el nombre de macros para aquellos símbolos cuya expansión es parametrizable (es decir,
llevan argumentos formales y reales como en el caso de las funciones). La cadena de expansión puede ser
cualquiera, no necesariamente un elemento sintácticamente válido de C.
Ejemplo 2.1
La Fig. 2.2 muestra el programa ejemplo hola.c escrito usando directivas de inclusión de archivos, símbolos y macros, y las
sucesivas transformaciones que hará el preprocesador.
El texto del programa una vez preprocesado quedará idéntico al del programa hola.c anteriormente presentado, y listo
para ingresar a la etapa de compilación propiamente dicha.
Headers
Las directivas para incluir archivos suelen darse al principio de los programas, ya que en general se
desea que su efecto alcance a todo el archivo fuente. Por esta razón los archivos preparados para ser
incluidos se denominan headers o archivos de cabecera. La implementación de la Biblioteca Standard que
viene con un compilador posee sus propios headers, uno por cada módulo de la biblioteca, que declaran
funciones y variables de uso general. Estos headers contienen texto legible por humanos, y están en algún
subdirectorio predeterminado (llamado /usr/include en UNIX, y dependiendo del compilador en otros
sistemas operativos). El usuario puede escribir sus propios headers, y no necesita ubicarlos en el directorio
reservado del compilador; puede almacenarlos en el directorio activo durante la compilación.
En el párrafo anterior, nótese que decimos declarar funciones, y no definirlas; la diferencia es im-
portante y se verá en profundidad más adelante. Recordemos por el momento que en los headers de la
Biblioteca Standard no aparecen definiciones -es decir, textos- de funciones, sino solamente declaraciones
18
CAPÍTULO 2. EL PREPROCESADOR 2.2. DEFINICIÓN DE SÍMBOLOS
o prototipos, que sirven para anunciar al compilador detalles como los tipos y cantidad de los argumentos
de las funciones.
No se considera buena práctica de programación colocar la definición de una función de uso frecuente
en un header. Esto obligaría a recompilar siempre la función cada vez que se la utilizara. Por el contrario, lo
ideal sería compilarla una única vez, produciendo un módulo objeto (y posiblemente incorporándolo a una
biblioteca). Esto ahorraría el tiempo correspondiente a su compilación, ocupando sólo el necesario para la
vinculación.
a = 2 * 3.14159 * 20.299322;
#define PI 3.14159
#define RADIO 20.299322
a = 2 * PI * RADIO;
19
2.3. DEFINICIÓN DE MACROS CAPÍTULO 2. EL PREPROCESADOR
#include <stdio.h>
#include "aux.h"
#define MAXITEM 100
#define DOBLE(X) 2*X
Se incluye el header de biblioteca standard stdio.h, que contiene declaraciones necesarias para poder
utilizar funciones de entrada/salida standard (hacia consola y hacia archivos).
Se incluye un header aux.h escrito por el usuario. Al indicar el nombre del header entre ángulos,
como en la línea anterior, especificamos que la búsqueda debe hacerse en los directorios reservados
del compilador. Al indicarlo entre comillas, nos referimos al directorio actual.
Se define una macro DOBLE(X) que deberá sustituirse por la cadena 2*(argumento de la llamada a
la macro).
a=MAXITEM;
b=DOBLE(45);
a=100;
b=2*45;
el resultado será b=2*40+5; y no b=2*45, ni b=2*(40+5), que presumiblemente es lo que desea el progra-
mador.
Este problema puede solucionarse redefiniendo la macro así:
Ahora la expansión de la macro será la deseada. En general, es saludable rodear las apariciones de los
argumentos de las macros entre paréntesis, para obligar a su evaluación al tiempo de ejecución con la
precedencia debida, y evitar efectos laterales.
20
CAPÍTULO 2. EL PREPROCESADOR 2.4. COMPILACIÓN CONDICIONAL
Ejemplo 2.1
Con las directivas condicionales:
Definimos una macro CARTEL que equivaldrá a invocar a una función “imprimir”, pero sólo si el símbolo DEBUG ha sido
definido. En otro caso, equivaldrá a la cadena vacía.
#ifdef DEBUG
#define CARTEL(x) imprimir(x)
#else
#define CARTEL(x)
#endif
El segmento siguiente muestra un caso con lógica inversa pero equivalente al ejemplo anterior.
#ifndef DEBUG
#define CARTEL(x)
#else
#define CARTEL(x) imprimir(x)
#endif
En el caso siguiente, se incluirá uno u otro header dependiendo del valor del símbolo SISTEMA. Tanto DEBUG como
SISTEMA pueden tomar valores al momento de compilación, si se dan como argumentos para el compilador. De esta
manera se puede modificar el comportamiento del programa sin necesidad de editarlo.
#if SISTEMA==MS_DOS
#include "header1.h"
#elif SISTEMA==UNIX
#include "header2.h"
#endif
2.5 Observaciones
A veces puede resultar interesante, para depurar un programa, observar cómo queda el archivo inter-
medio generado por el preprocesador después de todas las sustituciones, inclusiones, etc. La mayoría
de los compiladores cuentan con una opción que permite generar este archivo intermedio y detener
allí la compilación, para poder estudiarlo.
Otra opción relacionada con el preprocesador que suelen ofrecer los compiladores es aquella que
permite definir, al tiempo de la compilación y sin modificar los fuentes, símbolos que se pondrán a la
vista del preprocesador. Así, la estructura final de un programa puede depender de decisiones tomadas
al tiempo de compilación. Esto permite, por ejemplo, aumentar la portabilidad de los programas, o
generar múltiples versiones de un sistema sin diseminar el conocimiento reunido en los módulos fuente
que lo componen.
Finalmente, aunque el compilador tiene un directorio default donde buscar los archivos de inclusión,
es posible agregar otros directorios para cada compilación con argumentos especiales si es necesario.
21
2.6. EJERCICIOS CAPÍTULO 2. EL PREPROCESADOR
2.6 Ejercicios
1. Dé ejemplos de directivas de preprocesador:
2. Proponga un método para incluir un conjunto de archivos en un módulo fuente con una sola directiva
de preprocesador.
6. Edite el programa hello.c del ejemplo del capítulo 1 reemplazando la cadena "Hola, mundo!\n" por
un símbolo definido a nivel de preprocesador.
8. Escriba una macro que imprima su argumento usando la función printf(). Aplíquela para reescribir
hello.c de modo que funcione igual que antes.
9. ¿Cuál es el resultado de preprocesar las líneas que siguen? Es decir, ¿qué recibe exactamente el
compilador luego del preprocesado?
#define ALFA 8
#define BETA 2*ALFA
#define PROMEDIO(x,y) (x+y)/2
a=ALFA*BETA;
b=5;
c=PROMEDIO(a,b);
22
CAPÍTULO 2. EL PREPROCESADOR 2.7. PREGUNTAS PARA EL CAPÍTULO 2
b. #define 3.14 PI
2. El preprocesador promueve
A. La legibilidad. B. La redundancia. C. La rapidez de ejecución. D. Todas las anteriores.
3. El preprocesador facilita
A. El mantenimiento. B. La legibilidad. C. La expresividad. D. Todas las anteriores.
6. Los headers
A. Son escritos por el usuario. B. Vienen con el compilador. C. Todas las anteriores. D. Nin-
guna de las anteriores.
8. ¿Cuál es la directiva de preprocesador correcta si queremos definir un símbolo ALFA con valor 1?
A. #ALFA = 1 B. #define ALFA = 1 C. #define ALFA 1 D. #define 1 ALFA
10. La directiva correcta para crear una macro que devuelva el doble de su argumento es
A. #DOBLE(x)2*x B. #define DOBLE 2*x C. #define DOBLE(x)2*(x) D. #define
DOBLE(x)2*(x); E. #define DOBLE(x)2 * (x)
23
2.7. PREGUNTAS PARA EL CAPÍTULO 2 CAPÍTULO 2. EL PREPROCESADOR
11. ¿Cuál es la directiva correcta para incluir un header llamado beta.h situado en el directorio donde se
está realizando la compilación?
A. #define <beta.h> B. #include <beta.h> C. #include "beta.h"
24
CAPÍTULO 2. EL PREPROCESADOR 2.7. PREGUNTAS PARA EL CAPÍTULO 2
Respuestas al Capítulo 2
1. B 2. A 3. D 4. C 5. B 6. C 7. C 8. C 9. C 10. C 11. C 12. A 13. B 14. B 15. D
25
2.7. PREGUNTAS PARA EL CAPÍTULO 2 CAPÍTULO 2. EL PREPROCESADOR
26
Capítulo 3
char a;
int alfa,beta;
float x1,x2;
Los tipos enteros (char, int y long) admiten los modificadores signed (con signo) y unsigned (sin
signo). Un objeto de datos unsigned utiliza todos sus bits para representar magnitud; un signed utiliza un
bit para signo, en representación complemento a dos.
El modificador signed sirve sobre todo para explicitar el signo de los chars. El default para un int es
signed; el default para char puede ser signed o unsigned, dependiendo del compilador.
27
3.2. TAMAÑOS DE LOS OBJETOS DE DATOS CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES
short i;
unsigned short k;
Cuando en una declaración aparece sólo el modificador unsigned o short, y no el tipo, se asume int.
El tipo entero se supone el tipo básico manejable por el procesador, y es el tipo por omisión en varias otras
situaciones. Por ejemplo, cuando no se especifica el tipo del valor devuelto por una función.
El modificador long puede aplicarse también a float y a double. Los tipos resultantes pueden tener más
precisión que los no modificados.
long float e;
long double pi;
28
CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES 3.3. OPERACIONES CON DISTINTOS TIPOS
Cuando una operación sobre una variable provoca overflow, no se obtiene ninguna indicación de error.
El valor sufre truncamiento a la cantidad de bits que pueda alojar la variable.
Así, en una implementación donde los ints son de 16 bits, si tenemos en una variable entera el máximo
valor positivo:
int a;
a=32767; /* a=0111111111111111 binario */
a=a+1;
Al calcular el nuevo valor de a, aparece un 1 en el bit más significativo, lo cual, según la representación
de los enteros, lo transforma en un negativo (el menor negativo que soporta el tipo de datos, -32768).
Si el int es sin signo:
unsigned a;
a=65535; /* máximo valor de unsigned int */
a=a+1;
Truncamiento en asignaciones
Para empezar, una asignación de una expresión de un tipo dado a una variable de un tipo “menor” (en
el sentido del tamaño en bits de cada objeto de datos), no sólo es permitida en C, sino que la conversión
se hace en forma automática y generalmente sin ningún mensaje de tiempo de compilación ni de ejecución.
Por ejemplo,
int a;
float b;
29
3.3. OPERACIONES CON DISTINTOS TIPOS CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES
...
a=b;
a=19.27 * b;
a contendrá los sizeof(int) bits menos significativos del resultado de evaluar la expresión de la derecha,
truncada sin decimales.
a=3/2;
se tiene que tanto la constante 3 como la constante 2 son vistas por el compilador como ints; el resultado
de la división será también un entero (la parte entera de 3/2, o sea 1). Aun más llamativo es el hecho de
que si declaramos previamente:
float a;
el resultado es casi el mismo: a contendrá finalmente el valor float 1.0, porque el problema de truncamiento
se produce ya en la evaluación del miembro derecho de la asignación.
Operador cast
En el ejemplo anterior, ¿cómo recuperar el valor correcto, con decimales, de la división? Declarar a la
variable a como float es necesario, pero no suficiente. Para que la expresión del miembro derecho sea float
es necesario que al menos uno de sus operandos sea float. Hay dos formas de lograr esto; la primera
es escribir cualquiera de las subexpresiones como constante en punto flotante (con punto decimal, o en
notación exponencial):
a=3./2;
La segunda consiste en forzar explícitamente una conversión de tipo, con un importante operador
llamado cast, de la siguiente manera.
a=(float)3/2;
30
CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES 3.4. OBSERVACIONES
El operador cast es la aclaración, entre paréntesis, del tipo al cual queremos convertir la expresión
(en este caso, la subexpresión 3). Da lo mismo aplicarlo a cualquiera de las constantes. Sin embargo, lo
siguiente no funcionará:
a=(float)(3/2);
Aquí el operador cast se aplica a la expresión ya evaluada como entero, con lo que volvemos a tener
un valor 1.0, float, en a.
3.4 Observaciones
Nótese que no existen tipos boolean ni string. Más adelante veremos cómo manejar datos de estas
clases.
El tipo char, pese a su nombre, no está restringido a la representación de caracteres. Por el contrario,
un char tiene entidad aritmética. Almacena una cantidad numérica y puede intervenir en operaciones
matemáticas. En determinadas circunstancias, y sin perder estas propiedades, puede ser interpretado como
un carácter (el carácter cuyo código ASCII contiene).
En general, en C es conveniente habituarse a pensar en los datos separando la representación (la
forma como se almacena un objeto) de la presentación (la forma como se visualiza). Un mismo patrón de
bits almacenado en un objeto de datos puede ser visto como un número decimal, un carácter, un número
hexadecimal, octal, etc. La verdadera naturaleza del dato es la representación de máquina, el patrón de
bits almacenado en el objeto de datos.
31
3.5. UNA HERRAMIENTA: PRINTF() CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES
%d entero, decimal
%u entero sin signo, decimal
%l long, decimal
%c carácter
%s cadena
%f float
%lf double
%x entero hexadecimal
Ejemplo 3.1
Este programa escribe algunos valores con dos especificaciones de formato distintas.
main() {
int i,j;
for(i=65, j=1; i<70; i++, j++)
printf("vuelta no. %d: i= %d, i= %c\n",j,i,i);
}
32
CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES 3.6. EJERCICIOS
Ejemplo 3.2
El programa siguiente escribe el mismo valor en doble precisión pero con diferentes modificadores del campo correspondiente,
para incluir una cierta cantidad de decimales o alinear la impresión.
main() {
double d;
d=3.141519/2.71728182;
printf("d= %lf\n",d);
printf("d= %20lf\n",d);
printf("d= %20.10lf\n",d);
printf("d= %20.5lf\n",d);
printf("d= %.10lf\n",d);
}
d=1.156126
d= 1.156126
d= 1.1561255726
d= 1.15613
d=1.1561255726
3.6 Ejercicios
1. ¿Cuáles de entre estas declaraciones contienen errores?
33
3.6. EJERCICIOS CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES
7. ¿Qué hace falta corregir para que la variable r contenga la división exacta de a y b?
int a, b;
float r;
a = 5;
b = 2;
r = a / b;
int a, b, c, d;
a = 1;
b = 2;
c = a / b;
d = a / c;
9. ¿Cuál es el resultado del siguiente fragmento de código? Anticipe su respuesta en base a lo dicho en
esta unidad y luego confírmela mediante un programa.
10. Escribir un programa que multiplique e imprima 100000 ∗ 100000. ¿De qué tamaño son los ints en
su sistema?
11. Convertir una moneda a otra sabiendo el valor de cambio. Dar el valor a dos decimales.
12. Escriba y corra un programa que permita saber si los chars en su sistema son signed o unsigned.
13. Escriba y corra un programa que asigne el valor 255 a un char, a un unsigned char y a un signed
char, y muestre los valores almacenados. Repita la experiencia con el valor -1 y luego con ’\377’.
Explicar el resultado.
main() {
double x;
int i;
i = 1400;
x = i; /* conversion de int a double */
printf("x = %10.6le\ti = %d\n",x,i);
x = 14.999;
i = x; /* conversion de double a int */
printf("x = %10.6le\ti = %d\n",x,i);
x = 1.0e+60;
34
CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES 3.7. PREGUNTAS PARA EL CAPÍTULO 3
i = x;
printf("x = %10.6le\ti = %d\n",x,i);
}
15. Escriba un programa que analice la variable v conteniendo el valor 347 y produzca la salida:
3 centenas
4 decenas
7 unidades
16. Sumando los dígitos de un entero escrito en notación decimal se puede averiguar si es divisible por 3
(se constata si la suma de los dígitos lo es). ¿Esto vale para números escritos en otras bases? ¿Cómo
se puede averiguar esto?
18. Aplicar operador cast donde sea necesario para obtener resultados apropiados:
b. long a;
int b,c;
b = 1000; c = 1000;
a = b * c;
35
3.7. PREGUNTAS PARA EL CAPÍTULO 3 CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES
8. El signo default de las variables de tipo int y de tipo char es, respectivamente,
A. signed y unsigned. B. signed y dependiente de la implementación. C. unsigned y signed.
D. dependiente de la implementación y unsigned. E. dependiente de la implementación en ambos
casos.
10. ¿Cuál es la regla verdadera para los tamaños de los objetos de datos?
A. Un long es mayor que un short. B. Un int es menor que un long. C. Un short no es
menor que un long. D. Un short no es mayor que un long.
15. Si a y b son enteros, para obtener el valor de su cociente con decimales se debe escribir
A. a % b; B. a / b; C. (float) a / b; D. float (a)/ b; E. (float)(a / b);
36
CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES 3.7. PREGUNTAS PARA EL CAPÍTULO 3
17. Si a y b son chars que tienen el máximo valor posible para los chars, el tipo de la expresión a * b es:
A. unsigned char B. unsigned C. int D. char
37
3.7. PREGUNTAS PARA EL CAPÍTULO 3 CAPÍTULO 3. TIPOS DE DATOS Y EXPRESIONES
Respuestas al Capítulo 3
1. D 2. B 3. C 4. C 5. C 6. A 7. D 8. B 9. B 10. D 11. B 12. C 13. D 14. C 15. C
16. A 17. D
38
Capítulo 4
Constantes
Constantes enteras
10, -1 son constantes decimales.
’A’ es una constante de carácter. En una computadora que sigue la convención del código ASCII,
equivale al decimal 65, hexadecimal 0x41, etc.
Constantes long
Una constante entera puede indicarse como long agregando una letra L mayúscula o minúscula: 0L,
43l. Si bien numéricamente son equivalentes a 0 y a 43 int, el compilador, al encontrarlas, manejará
constantes de tamaño long (construirá objetos de datos sobre la cantidad de bits correspondientes a un
long). Esto puede ser importante en ciertas ocasiones: por ejemplo, al invocar a funciones con argumentos
formales long, usando argumentos reales que caben en un entero.
Constantes unsigned
Para hacer más claro el propósito de una constante positiva, o para forzar la promoción de una expresión,
puede notársela como unsigned. Esto tiene que ver con las reglas de promoción expresadas en el capítulo
3. Constantes unsigned son, por ejemplo, 32u y 298U.
39
4.2. CONSTANTES STRING CAPÍTULO 4. CONSTANTES
El compilador reserva una zona de memoria en la imagen del programa que está construyendo, del
tamaño del string más uno.
Se copian los caracteres entre comillas en esa zona, agregando al final un byte conteniendo un
cero (’\0’).
Se reemplaza la referencia a la constante string en el texto del programa por la dirección donde
quedó almacenada.
La cadena registrada por el compilador será almacenada al momento de ejecución en la zona del
programa correspondiente a datos estáticos inicializados (zona a veces llamada “bss”). Así, una constante
string equivale a, o se evalúa a, una dirección de memoria: la dirección donde está almacenado su primer
carácter.
El cero final
El carácter ’\0’ impuesto por el compilador al final de la secuencia de caracteres señaliza el fin
de la cadena, y tiene la importante misión de funcionar como protocolo o señal de terminación para
aquellas funciones de la Biblioteca Standard que manejan strings (copia de cadenas, búsqueda de caracteres,
comparación de cadenas, etc.). Debido a esta representación interna, algunas veces se las ve mencionadas
con el nombre de cadenas ASCIIZ (caracteres ASCII seguidos de cero).
Gracias a esta representación con el cero final, las cadenas ASCIIZ en C no tienen una longitud
máxima. El final de la cadena simplemente ocurre donde aparezca un carácter cero.
Al construir o manipular cadenas ASCIIZ, es necesario cumplir con el protocolo de terminarlas con su
cero final para poder utilizar las funciones que trabajan con strings. De lo contrario, esas funciones
no encontrarán el final del string.
La primera constante, ’\0’, es un entero. Su valor aritmético es 0. Todos los bits del objeto de datos
donde está representada son 0.
La segunda, ’0’, es una constante de carácter. Ocupa un objeto de datos de 8 bits de tamaño. Su
valor es 48 decimal en aquellas computadoras cuyo juego de caracteres esté basado en ASCII, pero
puede ser diferente en otras.
La tercera, "0", es una constante string, y se evalúa a una dirección. Es decir, en cualquier expresión
donde figure, su valor aritmético es una dirección de memoria dentro del espacio de direcciones del
programa. Ocupa un objeto de datos del tamaño de una dirección (frecuentemente 16 o 32 bits),
además del espacio de memoria ubicado a partir de esa dirección y ocupado por los caracteres del
string. Ese espacio de memoria está ocupado por un byte igual a ’0’ (el primer y único carácter
del string), que como hemos visto equivale a 48 decimal en computadoras que adoptan ASCII, y a
continuación viene un byte ’\0’, o sea, 0 (señal de fin del string).
40
CAPÍTULO 4. CONSTANTES 4.3. CONSTANTES DE CARÁCTER
Ejemplo 4.1
Si tenemos las declaraciones y asignaciones siguientes, donde las tres variables declaradas son char:
char a,b,c;
a=’\0’;
b=’0’;
c="0";
\t tabulador, ASCII 9
Una forma alternativa de escribir constantes de carácter es mediante su código ASCII: ’\033’,
’\x1B’ Aquí representamos el carácter cuyo código ASCII es 27 decimal, en dos bases. La barra
invertida (backslash) muestra que el contenido de las comillas simples debe ser interpretado como
el código del carácter. Si el carácter siguiente al backslash es x o X, el código está en hexadecimal;
si no, está en octal. Para representar el carácter backslash, sin su significado como modificador de
secuencias de otros caracteres, lo escribimos dos veces seguidas.
Estas constantes de carácter pueden ser también escritas respectivamente como las constantes numéri-
cas 033, 27 o 0x1B, ya que son aritméticamente equivalentes; pero con las comillas simples indicamos que
el programador “ve” a estas constantes como caracteres, lo que puede agregar expresividad a un segmento
de programa.
41
4.4. CONSTANTES ENUMERADAS CAPÍTULO 4. CONSTANTES
Por ejemplo, 0 es claramente una constante numérica; pero si escribimos ’\0’ (que es numéricamente
equivalente), ponemos en evidencia que estamos pensando en el carácter cuyo código ASCII es 0. El carácter
’\0’ (ASCII 0) es distinto de ’0’ (ASCII 48). La expresión de las constantes de carácter mediante backslash
y algún otro contenido se llama una secuencia de escape.
Ejemplo 4.1
Aquí los valores de las constantes son ENE = 1, FEB = 2, MAR = 3, etc.
enum meses {
ENE = 1, FEB, MAR, ABR, MAY, JUN,
JUL,AGO, SEP, OCT, NOV, DIC
};
Ejemplo 4.2
4.5 Ejercicios
1. Indicar si las siguientes constantes están bien formadas, y en caso afirmativo indicar su tipo y dar su
valor decimal.
a. "ABC\bU\tZ"
b. "\103B\x41"
a. "\0BA"
b. "\\0BA"
c. "BA\0CD"
printf("0\r1\r2\r3\r4\r5\r6\r7\r8\r9\r");
5. Escribir una macro que devuelva el valor numérico de un carácter correspondiente a un dígito en base
decimal.
6. La cadena ’ABC0x25’ es
A. Una constante decimal. B. Una constante hexadecimal. C. Una constante string. D. Una
constante de carácter. E. Ninguna de las anteriores.
43
4.6. PREGUNTAS PARA EL CAPÍTULO 4 CAPÍTULO 4. CONSTANTES
Respuestas al Capítulo 4
1. A 2. C 3. B 4. A 5. D 6. E
44
Capítulo 5
Las variables tienen diferentes propiedades según que sean declaradas dentro o fuera de las funciones,
y según ciertos modificadores utilizados al declararlas. Entre las propiedades de las variables distinguimos:
Liga o linkage (en qué forma puede ser manipulada por el linker)
Las reglas que determinan, a partir de la declaración de una variable, cuáles serán sus propiedades,
son bastante complejas. Estas reglas son tan interdependientes, que necesariamente la discusión de las
propiedades de las variables será algo reiterativa.
Ejemplo 5.1
La variable m declarada al principio es externa, y puede ser vista desde fun1() y fun2(). Sin embargo, fun1() declara su
propia variable m local, y toda operación con m dentro de fun1() se referirá a esta última. Por otro lado, la variable n es
también externa, pero es visible sólo por fun2(), porque todo uso de las variables debe estar precedido por su declaración.
Si apareciera una referencia a la variable n en fun1(), se dispararía un error de compilación.
int m;
int fun1()
{
int m;
m=1;
...
}
int n;
int fun2()
45
5.2. VIDA DE LAS VARIABLES CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
{
m=1;
...
}
Ejemplo 5.1
Cada vez que fun2() asigna el resultado de fun1() a j, está utilizando el mismo objeto de datos de la misma variable j,
porque ésta es externa; pero cada invocación de fun1() crea un nuevo objeto de datos para la variable k, el cual se destruye
al terminar esta función.
int j;
int fun1()
{
int k;
...
}
int fun2()
{
j=fun1();
}
Ejemplo 5.2
La diferencia del siguiente ejemplo con el anterior es que ahora k es declarada con el modificador static. Esto hace que k
tenga las mismas propiedades de vida que una variable externa. A cada invocación de fun1(), ésta utiliza el mismo objeto de
datos, sin modificarlo, para la variable k. Si lee su valor, encontrará el contenido que pueda haberle quedado de la invocación
anterior. Si le asigna un valor, la invocación siguiente de fun1() encontrará ese valor en k.
Este ejemplo muestra que alcance y vida no son propiedades equivalentes en C.
int j;
int fun1()
{
static int k;
...
}
int fun2()
{
j=fun1();
}
La propiedad que diferencia ambas instancias de k es la clase de almacenamiento; en el primer caso, k es local y automática;
en el segundo, k es local pero estática.
46
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.3. CLASES DE ALMACENAMIENTO
almacenamiento. Una variable externa tiene clase de almacenamiento estática. Una variable local tiene
-salvo indicación contraria- clase de almacenamiento automática. Una tercera clase de almacenamiento es
la llamada registro. La clase de almacenamiento determina, como se vio recién, la vida de las variables.
Variables estáticas Las variables estáticas comienzan su vida al tiempo de carga del programa, es decir,
aun antes de que se inicie la ejecución de la función main(). Existen durante todo el tiempo de
ejecución del programa. Son inicializadas con ceros binarios, salvo que exista otra inicialización
explícita. Son las variables externas y las locales declaradas static.
Variables automáticas Esta clase abarca exclusivamente las variables, declaradas localmente a una fun-
ción, que no sean declaradas static. El objeto de datos de una variable automática inicia su exis-
tencia al entrar el control a la función donde está declarada, y muere al terminar la función. No son
inicializadas implícitamente, es decir, contienen basura salvo que se las inicialice explícitamente.
Variables registro Una variable registro no ocupará memoria, sino que será mantenida en un registro del
procesador.
Ejemplo 5.1
int m;
int fun()
{
int j;
register int k;
static int l;
...
}
Una declaración register debe tomarse solamente como una recomendación hecha por el programador
al compilador, ya que no hay garantías de que, al tiempo de ejecución, resulte posible utilizar un registro
para esa variable. Más aún, el mismo programa, compilado y corrido en diferentes arquitecturas, podrá
utilizar diferentes cantidades de registros para sus variables.
Una variable register tendrá un tiempo de acceso muy inferior al de una variable en memoria, porque
el acceso a un registro de CPU es mucho más rápido. La motivación de la clase de almacenamiento registro
se hace evidente en el Cuadro 5.1 (tomado de Systems Performance: Enterprise and the Cloud, Brendan
Gregg, Prentice Hall, 2014).
En general resulta interesante que las variables más frecuentemente accedidas sean las declaradas como
register; típicamente, los índices de arrays, variables de control de lazos, etc. Sin embargo, la declaración
register es quizás algo anacrónica, ya que los compiladores modernos ejecutan una serie de optimizaciones
que frecuentemente utilizan registros para mantener las variables, aun cuando no haya indicación alguna
por parte del programador.
47
5.3. CLASES DE ALMACENAMIENTO CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
La clase de almacenamiento automática es la natural para las variables locales; ¿cuál es la idea de declarar variables locales que
sean estáticas? Generalmente se desea aprovechar la capacidad de “recordar la historia” de las variables estáticas, utilizando
el valor al momento de la última invocación para producir uno nuevo.
La inicialización (implícita o explícita) de una variable estática se produce una única vez, al momento de carga del
programa. Por el contrario, la inicialización (explícita) de una variable automática se hace al crear cada instancia de la misma
(al momento de la entrada del control a la función).
Usando variables estáticas, una función puede, por ejemplo, contar la cantidad de veces que ha sido llamada. En el código
siguiente, el ciclo while se ejecuta 50 veces y la variable vez asume cada vez un valor dependiente del anterior, pese a ser local
a la función veces().
int veces()
{
static int vez=0;
return ++vez;
}
int fun()
{
while(veces() <= 50) {
...
}
}
Las variables estáticas (las externas, y las locales cuando son declaradas static) se alojan en la zona
de datos estáticos. Esta zona no cambia de tamaño ni pierde sus contenidos, y queda inicializada
al momento de carga del programa.
A medida que una función invoca a otras, las variables locales van apareciendo en el stack, y a medida
que las funciones terminan, el stack se va desalojando en orden inverso a como aparecieron las variables.
Cada función, al recibir el control, toma parte del stack, con los contenidos que hubieran quedado allí de
ejecuciones previas, para alojar allí sus variables. A esto se debe que el programa las vea inicializadas con
basura.
Ejemplo 5.3
Con el código de la Fig. 5.1, el estado del stack en momentos sucesivos será:
48
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.4. LIGA
Ejemplo 5.4
Con el código de la Fig. 5.2, que invoca a dos funciones secuencialmente, el estado del stack en momentos sucesivos será:
1. Antes de entrar a fun1() se tiene el stack vacío.
2. Al entrar a fun1() se disponen sus variables locales en el stack, en orden de aparición.
3. Al terminar fun1() se desmantela el stack.
4. Al entrar en fun2() se dispone en el stack su variable local, cuyo objeto de datos tendrá basura debida al valor de a
dejado por fun1().
5. Al salir de fun2() se desmantela su stack completamente y se vuelve al estado inicial.
5.4 Liga
La liga es la propiedad que determina si las variables y funciones definidas en una unidad de traducción
serán o no visibles por el linker. Una vez que un conjunto de unidades de traducción pasa exitosamente
la compilación, tenemos un conjunto de módulos objeto. Cada módulo objeto puede contener, en forma
simbólica, pendiente de resolución, referencias a variables o funciones definidas en otros módulos. La
propiedad de las variables y funciones que permite que el linker encuentre la definición de un objeto para
aparearlo con su referencia es la liga externa. Tienen liga externa por defecto las variables externas y
las funciones, de modo que todas éstas pueden ser referenciadas desde otras unidades de traducción.
El concepto de liga externa es importante cuando el proyecto de desarrollo abarca varias unidades
de traducción que deben dar lugar a un ejecutable. Aprovechando la propiedad de liga externa de las
49
5.4. LIGA CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
funciones, se puede ubicar cada definición de función, o un conjunto de ellas, en un archivo separado. Esto
suele facilitar el mantenimiento y aportar claridad a la estructura de un proyecto de desarrollo.
La excepción a la regla de liga externa se produce cuando las variables externas o funciones son
declaradas con el modificador static. Este modificador cambia el tipo de los objetos a liga interna. Un
objeto que normalmente sería de liga externa, declarado como static, pasa a ser visible únicamente dentro
de la unidad de traducción donde ha sido declarado.
Esta particularidad permite realizar, en cierta medida, ocultamiento de información. Si una unidad de
traducción utiliza variables externas o funciones de su uso privado, que no deben hacerse visibles desde
afuera, puede declarárselas static, con lo cual se harán inaccesibles a toda otra unidad de traducción. El
caso típico se presenta cuando se desea hacer opacas las funciones que implementan un tipo de datos
abstracto, haciéndolas de liga interna mientras que las funciones públicas (las de interfaz) se dejan con liga
externa.
Finalmente, las variables locales, al ser visibles únicamente dentro de su función, se dice que no tienen
liga (el linker nunca llega a operar con ellas).
Ejemplo 5.1
En el ejemplo dado en el Cuadro 5.2, fun1(), fun2() y fun3() están definidas en unas unidades de traducción distintas
de la de main(). El fuente alfa.c es capaz de dar origen a un programa ejecutable (porque contiene el punto de entrada al
programa), pero solamente si al momento de linkedición se logra que el linker resuelva las referencias pendientes a fun1() y
a fun2() (que no están definidas en alfa.c). Por motivos similares, las referencias en gamma necesitan de las definiciones en
beta al momento de linkedición.
En la práctica logramos esto de varias maneras.
1. O bien, con:
50
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.5. DECLARACIONES Y DEFINICIONES
que significa “compilar separadamente los tres fuentes, linkeditarlos juntos y al ejecutable resultado renombrarlo como
alfa”;
2. o bien con:
gcc -c alfa.c
gcc -c beta.c
gcc -c gamma.c
gcc alfa.o beta.o gamma.o -o alfa
Ejemplo 5.2
El ejemplo del Cuadro 5.3 es casi idéntico al anterior, salvo que la función fun3() ahora está declarada static, y por este
motivo no podrá ser vista por el linker para resolver la referencia pendiente de fun2() en lambda.c. La función fun3() tiene
liga interna. Las tres unidades de traducción jamás podrán satisfacer la compilación.
Una definición consiste en una sentencia de creación de dicha variable o función, que a partir de la
ejecución de esa sentencia comienza a ser una entidad viva del programa.
51
5.6. MODIFICADORES ESPECIALES CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
Ejemplo 5.1
int a;
float b, c;
int fun()
{
...
}
Cuando la declaración de una variable cualquiera aparece precedida del modificador extern, ésta indica
el nombre y tipo asociado, pero no habilita al compilador para crear el objeto de datos; se trata de una
variable cuya definición puede ser encontrada más adelante, o aun en otra unidad de traducción. La
declaración extern tan sólo enuncia el tipo y nombre de la variable para que el compilador los tenga en
cuenta.
Ejemplo 5.2
Las declaraciones siguientes no crean objetos de datos y por lo tanto no son definiciones.
extern int a;
extern float b, c;
int fun();
Una variable externa es visible desde todas las funciones de la unidad de traducción, y además puede
ser utilizada desde otras. Esto se debe a la propiedad de liga externa de las variables externas: son visibles
al linker como candidatos para resolver referencias pendientes.
El requisito para poder utilizar una variable definida en otra unidad de traducción es declararla con el
modificador extern en aquella unidad de traducción donde se va a utilizar.
Ejemplo 5.3
En el Cuadro 5.4, el texto delta.c es una unidad de traducción que declara dos variables externas y dos funciones, pero hace
opacas a la variable n y a la función fun2() con el modificador static.
La función fun1() puede utilizar a todas ellas por estar dentro de la misma unidad de traducción, pero fun3(), que
está en otra, sólo puede referenciar a m y a fun1(), que son de liga externa. Para ello debe declarar a m como extern,
o de lo contrario no superará la compilación (“todo uso debe ser precedido por una declaración”).
Si, además, eta.c declarara una variable extern int n, con la intención de referirse a la variable n definida en delta.c,
la referencia no podría ser resuelta a causa de la condición de liga interna de n.
Los usos de funciones (como fun1() en eta.c) pueden aparecer sin declaración previa, pero en este caso el compilador
asumirá tipos de datos default para los argumentos y para el tipo del valor devuelto por la función (int en todos los
casos).
52
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.6. MODIFICADORES ESPECIALES
delta.c eta.c
int m;
static int n;
int fun1()
{ extern int m;
n=fun2(); int fun3()
... {
} m=fun1();
static int fun2() }
{
...
}
El modificador const también permite expresar, en el prototipo de una función, que un argumento
no podrá ser modificado por la función, aun cuando sea pasado por referencia.
Volatile Los compiladores modernos aplican una cantidad de pasos de optimización cuando ven instruccio-
nes aparentemente redundantes o sin efectos, porque su desplazamiento o eliminación puede implicar
ventajas en tiempo de ejecución o espacio de almacenamiento. Esto es especialmente así si las ins-
trucciones sospechosas se encuentran dentro de ciclos. El modificador volatile sirve para advertir al
compilador de que una variable será modificada asincrónicamente con la ejecución del programa (por
ejemplo, por efecto de una rutina de atención de interrupciones) y por lo tanto el optimizador no
puede inferir correctamente su utilidad dentro del programa. Esto evitará que el compilador aplique
la lógica de optimización a las instrucciones que involucran a esta variable.
Ejemplo 5.1
El ciclo while del Cuadro 5.5 podría ser reescrito por un optimizador, extrayendo del ciclo la asignación a=beta en el
entendimiento de que beta no cambiará en ninguno de los pasos del ciclo.
Sin embargo, si esperamos que la variable beta cambie por acción de algún agente externo a la rutina en cuestión, con la
declaración previa volatile int beta el compilador se abstendrá de optimizar las líneas donde intervenga beta.
53
5.7. EJERCICIOS CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
5.7 Ejercicios
1. Copie, compile y ejecute el siguiente programa. Posteriormente agregue un modificador static sobre
la variable a y repita la experiencia.
int fun()
{
int a;
a = a + 1;
return a;
}
main()
{
printf(" %d\n", fun());
printf(" %d\n", fun());
}
int alfa;
int fun()
{
int alfa;
alfa = 1;
return alfa;
}
main()
{
alfa = 2;
printf(" %d\n",fun());
printf(" %d\n",alfa);
}
int alfa;
int fun(int alfa)
{
alfa = 1;
return alfa;
}
main()
{
alfa = 2;
printf(" %d\n",fun(alfa));
printf(" %d\n",alfa);
}
4. Copie y compile, juntas, las unidades de traducción que se indican abajo. ¿Qué hace falta para que
54
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.7. EJERCICIOS
int a; main()
int fun1(int x) {
{ a = 1;
return 2 * x; printf("d\n", fun1(a));
} }
5. ¿Qué ocurre si un fuente intenta modificar una variable externa, declarada en otra unidad de traduc-
ción como const? Prepare, compile y ejecute un ejemplo.
7. Denotemos esquemáticamente que un módulo objeto prueba.o contiene un objeto de datos x y una
función fun(), ambos de liga externa, de esta manera:
prueba.o
x
fun()
Si se tiene un conjunto de archivos y unidades de traducción que se compilarán para formar los
respectivos módulos objeto, ¿cómo se aplicaría la notación anterior al conjunto de módulos objeto
resultantes? Hacer el diagrama para los casos que aparecen en el Cuadro 5.6. ¿Hay colisión de
nombres? ¿Hay referencias que el linker no pueda resolver? Cada grupo de fuentes, ¿puede producir
un ejecutable?
55
5.8. PREGUNTAS PARA EL CAPÍTULO 5 CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
Habrá una función para consultar, y otra para establecer, la cantidad de aviones esperando
aterrizar o despegar por cada pista.
Habrá una función para consultar la cantidad de aviones en tierra y otra para consultar la
cantidad de aviones en el aire.
No deberá ser posible que un programa modifique el estado de las estructuras de datos sino a
través de las funciones dichas.
9. ¿Cuáles pueden ser los “agentes externos” al programa que sean capaces de cambiar el valor de una
variable volatile?
6. Si una función declara una variable local con el mismo nombre que una externa, los usos de esa variable
dentro de la función se referirán a
A. la variable local. B. la variable externa. C. depende de la implementación.
56
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.8. PREGUNTAS PARA EL CAPÍTULO 5
b)
extern int c; #include "hdr1.h" int a, b, c=1; main()
extern int int fun1(int x) int fun2(int x) {
fun1(int p), { { int d;
fun2(int p); return return x-1; d = fun1(3);
fun2(x)+1; } }
}
c)
int fun1(int x) int fun3(int x) main()
{ { {
return x+1; return x+3; int a;
} } a = fun1(a);
int fun2(int x) static int }
{ fun2(int x)
return x+2; {
} return x+4;
}
d)
int fun1(int x) static int int b;
{ a = 1; main()
extern int b; static int {
x = b-fun2(x); b = 1; b = 2;
} int fun2(int x) printf(" %d",
{ fun1(3));
return x-a; }
}
57
5.8. PREGUNTAS PARA EL CAPÍTULO 5 CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
58
CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES 5.8. PREGUNTAS PARA EL CAPÍTULO 5
Respuestas al Capítulo 5
1. C 2. A 3. C 4. B 5. C 6. A 7. B 8. A 9. A 10. B 11. B 12. A 13. B 14. B 15. B
16. D 17. A 18. C 19. D 20. B
59
5.8. PREGUNTAS PARA EL CAPÍTULO 5 CAPÍTULO 5. PROPIEDADES DE LAS VARIABLES
60
Capítulo 6
Operadores
% (operador módulo)
No existe operador de división entera, opuesto a la división entre números reales, como en Pascal.
Sin embargo, la división entre enteros da un entero: el resultado se trunca debido a las reglas de
evaluación de expresiones.
Ejemplo 6.1
Aquí a y b reciben respectivamente el cociente y el resto de dividir un VALOR por 256. Imprimiéndolos podemos ver cómo
una CPU, en la cual un unsigned short int tuviera un tamaño de 16 bits, almacenaría ese VALOR sobre dos bytes.
Ejemplo 6.2
float c;
int j,k;
j=3;
k=2;
c=j/k;
Ejemplo 6.3
61
6.2. OPERADORES DE RELACIÓN CAPÍTULO 6. OPERADORES
Los operadores de incremento y decremento (++ y −−) equivalen a las sentencias del tipo a = a+1 o b = b-1. Suman o
restan una unidad a su argumento, que debe ser un tipo entero. Se comportan diferentemente según que se apliquen en forma
prefija o sufija.
Sentencias a b
a=5; b=a++; 6 5
a=5; b=++a; 6 6
a=3; b=a--; 2 3
a=3; b=--a; 2 2
Aplicados como prefijos, el valor devuelto por la expresión es el valor incrementado o decrementado.
Aplicados como sufijos, el incremento o decremento se realiza como efecto lateral, pero el valor devuelto por la expresión
es el anterior al incremento o decremento.
Abreviaturas
Existe una forma de abreviar la notación en las expresiones del tipo a = a*b y a = a+b. Podemos
escribir, respectivamente:
a *= b;
a += b;
!= (distinto de)
Es un error muy común sustituir el operador de relación == por el de asignación =. Este error lógico no
provocará error de compilación ni de ejecución, sino que será interpretado como una asignación, que en C
puede ser hecha en el mismo contexto que una comparación. La sentencia:
if(a = 38)
...
es sintácticamente válida, pero en lugar de comparar a con 38, asigna el valor 38 a la variable a.
La expresión a=38 es legal en C como expresión lógica. Su valor de verdad es siempre TRUE, indepen-
dientemente del valor que tuviera a anteriormente. Si el programador desea obtener el valor lógico de la
comparación de a con 38, debe utilizar la expresión if(a == 38).
Ejemplo 6.1
62
CAPÍTULO 6. OPERADORES 6.4. OPERADORES DE BITS
si es F .
Ejemplo 6.2
a - b, que es una expresión aritmética, es también una expresión lógica. Será F cuando a sea igual a b.
a como expresión lógica será V sólo cuando a sea distinto de 0 .
a = 8 es una expresión de asignación. Su valor aritmético es el valor asignado, por lo cual, como expresión lógica, es
V , ya que 8 es distinto de 0.
Constantes lógicas
Generalmente los compiladores definen dos símbolos o macros, FALSE y TRUE, en el header stdlib.h,
y a veces también en otros. Su definición suele ser la siguiente:
#define FALSE 0
#define TRUE !FALSE
Ejemplo 6.1
El operador de negación de bits es unario y provoca el complemento a uno de su operando. Los operadores and, or y or
exclusivo de bits son binarios.
63
6.5. OPERADORES ESPECIALES CAPÍTULO 6. OPERADORES
unsigned char a;
a = ~255; /* a <-- 0 */
a = 0xf0 ^ 255; /* a <-- 0x0f */
a = 0xf0 & 0x0f; /* a <-- 0x00 */
a = 0xf0 | 0x0f; /* a <-- 0xff */
Ejemplo 6.2
Los operadores >> y << desplazan los bits de un objeto de tipo char, int o long, una cantidad dada de posiciones.
= (operador de asignación)
= (operador de inicialización)
?: (operador ternario)
Inicialización
Una inicialización es la operación que permite asignar un valor inicial a una variable en el momento de
su definición:
int a=1;
Asignación
Una asignación es la operación que permite asignar un valor a una variable que ha sido definida
anteriormente:
int a;
a=1;
Operador ternario
El operador ternario comprueba el valor lógico de una expresión, y, según este valor, se evalúa a una u
otra de las restantes expresiones. Supongamos tener la expresión siguiente.
64
CAPÍTULO 6. OPERADORES 6.6. PRECEDENCIA Y ORDEN DE EVALUACIÓN
Ejemplo 6.1
La expresión
c = b + (a < 0) ? -a : a;
a = w * x / ++y;
b = a + z / y;
6.7 Resumen
El Cuadro 6.1 ilustra las relaciones de precedencia entre los diferentes operadores. La tabla está ordenada
con los operadores de mayor precedencia a la cabeza. Los que se encuentran en el mismo renglón tienen la
misma precedencia.
6.8 Ejercicios
1. ¿Qué valor lógico tienen las expresiones siguientes?
65
6.8. EJERCICIOS CAPÍTULO 6. OPERADORES
Precedencia Operador
1 () Llamada a función
[] Indexación de arreglo
-> . Acceso a miembros de estructuras
2 ! ~ Negación y complemento a uno
++ -- Incremento y decremento
* Indirección
& Dirección de
() Conversión forzada de tipo (cast)
sizeof Tamaño de variables o tipos
+ unario
- unario
3 * / % Multiplicación, división y módulo
4 + - Suma y resta
5 << >> Desplazamiento de bits
6 < <= >= > Operadores de relación
7 == != Operadores lógicos de igualdad
8 & AND de bits
9 ^ XOR de bits
10 | OR de bits
11 && AND lógico
12 || OR lógico
13 ?: Operador ternario
= Asignación
14 += -= *= /= %= Aritméticos y lógicos abreviados
&= ^= |=
<<= >>=
15 , Operador coma
66
CAPÍTULO 6. OPERADORES 6.8. EJERCICIOS
2. Escriba una macro IDEM(x,y) que devuelva el valor lógico TRUE si x e y son iguales, y FALSE en caso
contrario. Escriba NIDEM(x,y) que devuelva TRUE si las expresiones x e y son diferentes y FALSE si
son iguales.
3. Escriba una macro PAR(x) que diga si un entero es par. Muestre una versión usando el operador %,
una usando el operador >>, una usando el operador & y una usando el operador |.
4. Escriba macros MIN(x,y) y MAX(x,y) que devuelvan el menor y el mayor elemento entre x e y.
Usando las anteriores, escriba macros MIN3(x,y,z) y MAX3(x,y,z) que devuelvan el menor y el
mayor elemento entre tres expresiones.
6. Utilice el resultado anterior para escribir una macro DOSALA(x) que calcule 2 elevado a la x-ésima
potencia.
a. a = a + 1;
b. b = b * 2;
c. b = b - 1;
d. c = c - 2;
e. d = d % 2;
f. e = e & 0x0F;
g. a = a + 1;
h. b = b + a;
i. a = a - 1;
j. c = c * a;
main()
{
int a = 1;
int b;
b = a || 12;
printf(" %d\n",b);
}
67
6.9. PREGUNTAS PARA EL CAPÍTULO 6 CAPÍTULO 6. OPERADORES
c = 1;
a = c++;
c = 1;
a = ++c;
c = 1;
a = --c;
a += c++;
a = 1;
b = 2;
if(a = b)
b = a;
a = 1;
b = 0;
if(a = b)
b = a;
68
CAPÍTULO 6. OPERADORES 6.9. PREGUNTAS PARA EL CAPÍTULO 6
12. Dada la declaración unsigned char a=1; la operación a <<= a tiene como resultado
A. 0. B. 1. C. 2. D. 255. E. 127.
69
6.9. PREGUNTAS PARA EL CAPÍTULO 6 CAPÍTULO 6. OPERADORES
Respuestas al Capítulo 6
1. C 2. B 3. C 4. E 5. B 6. C 7. A 8. D 9. C 10. D 11. B 12. C 13. D 14. C
70
Capítulo 7
Estructuras de control
Las estructuras de control de C no presentan, en conjunto, grandes diferencias con las del resto de los
lenguajes estructurados. En los esquemas siguientes, donde figura una sentencia puede reemplazarse por
varias sentencias encerradas entre llaves (un bloque).
if(expresión)
sentencia;
if(expresión) {
sentencias
...
}
if(expresión)
sentencia;
else
sentencia;
Ejemplo 7.1
El formato en C es libre, y las llaves sólo son necesarias cuando las sentencias de ejecución condicional son más de una.
if(a == 8) c++;
if(d)
c++;
else
c += 2;
71
7.2. ESTRUCTURAS REPETITIVAS CAPÍTULO 7. ESTRUCTURAS DE CONTROL
En las estructuras anidadas, la cláusula else se aparea con el if más interno, salvo que las llaves expresen otra cosa.
En el Cuadro 7.1, las llaves del Listado 1 muestran cómo están asociadas las estructuras. En el Listado 2, se han quitado,
con lo cual la semántica cambia, pero se mantiene la misma indentación, lo que puede dar lugar a confusión. El Listado 2 en
realidad es equivalente al Listado 3, que utiliza llaves aunque en forma redundante, y además tiene la indentación correcta.
En general, el estilo de programación debe sugerir la estructura, usando indentación; pero es, por
supuesto, la ubicación de las llaves, y no la indentación, la que determina la estructura.
Estructura while
while(expresión)
sentencia;
Estructura do-while
do {
sentencias;
} while(expresión);
Estructura for
La iteración es un caso particular de lazo while donde necesitamos que un bloque de sentencias se repita
una cantidad previamente conocida de veces. Estos casos implican la inicialización de variables de control,
el incremento o decremento de las mismas, y la comprobación por valor límite. Estas tareas administrativas
se pueden hacer más cómoda y expresivamente con un lazo for.
El esquema es:
72
CAPÍTULO 7. ESTRUCTURAS DE CONTROL 7.2. ESTRUCTURAS REPETITIVAS
Donde:
inicialización es una o más sentencias, separadas por comas, que se ejecutan una única vez al entrar
al lazo.
condición_mientras es una expresión lógica, que se comprueba al principio de cada iteración. Mien-
tras resulte verdadera, se continúa ejecutando el cuerpo.
incremento es una o más sentencias, separadas por comas, que se realizan al final de cada ejecución
del cuerpo de la iteración.
inicialización;
while(condición_mientras) {
sentencia;
incremento;
}
Aunque el uso más común de las sentencias de incremento es hacer avanzar o retroceder un contador
de la cantidad de iteraciones, nada impide que se utilice esa sección para cualquier otro fin.
Cualesquiera de las secciones inicialización, condición_mientras o incremento pueden estar vacías. En
particular, la sentencia:
for( ; ; )
es un lazo infinito.
Ejemplo 7.1
Si se quiere asegurar que la variable a tiene un valor inicial cero, se puede escribir:
Aprovechando la propiedad del corto circuito en las expresiones lógicas, se puede introducir el cuerpo del lazo for en
la comprobación (aunque no es recomendable si complica la lectura):
Nótese que el cuerpo de este último for es la sentencia nula. A propósito: es un error muy común utilizar un signo ";"de
más, así:
Esta estructura llevará la variable i desde 1 hasta 10 sin ejecutar ningún otro trabajo (lo que se repite es la sentencia
nula) y después incrementará a, una sola vez, en el valor de la última iteración de i.
Una clásica estructura de control para un proceso consumidor de objetos:
73
7.3. ESTRUCTURA DE SELECCIÓN CAPÍTULO 7. ESTRUCTURAS DE CONTROL
a=leercarácter();
while( a != ’\033’ ) {
procesar(a);
a = leercarácter();
}
La propiedad de que toda asignación en C tiene un valor como expresión (el valor asignado) permite reescribir la
estructura de control anterior como:
Las expresiones conectadas por los operadores lógicos se evalúan de izquierda a derecha, y la evaluación se detiene
apenas alcanza a determinarse el valor de verdad de la expresión (propiedad “del corto circuito”). Así, si suponemos
que procesar() siempre devuelve un valor distinto de cero, la sentencia siguiente equivale a los lazos anteriores.
do {
if((a=leercarácter()) != ’\033’)
procesar(a);
} while(a != ’\033’);
switch(expresión_entera) {
case expresión_constante1:
sentencias;
break;
case expresión_constante2:
sentencias;
break;
default:
sentencias;
}
Al entrar al switch, se comprueba el valor de la expresión entera, y si coincide con alguna de las constantes
propuestas en los rótulos case, se deriva el control directamente allí. La sección default no es obligatoria.
Sirve para derivar allí todos los casos que no se contemplen explícitamente. En las expresiones_constantes
no se permite la aparición de variables ni funciones. Un ejemplo válido con expresiones_constantes sería:
74
CAPÍTULO 7. ESTRUCTURAS DE CONTROL 7.4. TRANSFERENCIA INCONDICIONAL
#define ARRIBA 10
#define ABAJO 8
switch(valor(tecla)) {
case 127+ARRIBA:
arriba();
break;
case 127+ABAJO:
abajo();
break;
}
La sentencia break es necesaria aquí porque, al contrario que en Pascal, el control no se detiene al llegar
al siguiente rótulo.
Ejemplo 7.1
Esta estructura recibe como entrada las variables m y a (mes y año) y da como salida d (la cantidad de días del mes).
switch(m) {
case 2:
d=28 + bisiesto(a) ? 1 : 0;
break;
case 4:
case 6:
case 9:
case 11:
d= 30;
break; default:
d= 31;
}
Si m vale 4, 6, 9, u 11, la variable d recibe el valor 30. Al no haber un break intermedio, el control cae hasta la asignación
d = 30.
La estructura switch tiene algunas limitaciones con respecto a sus análogos en otros lenguajes. A saber, no se puede
comparar la expresión de selección con expresiones no constantes, ni utilizar rangos (el concepto de rango no está definido en
C).
Sentencia continue
Utilizada dentro de un lazo while, do-while o for, hace que el control salte directamente a la compro-
bación de la condición de iteración. Así:
75
7.5. OBSERVACIONES CAPÍTULO 7. ESTRUCTURAS DE CONTROL
En este lazo, si la función no_procesar() devuelve valor distinto de cero, no se ejecuta el resto del
lazo (la función procesar() y otras, si las hubiera, hasta la llave final del lazo). Se comprueba la validez de
la expresión i<100, y si corresponde se inicia una nueva iteración.
Sentencia break
La sentencia break, por el contrario, hace que el control abandone definitivamente el lazo:
while(expresión) {
if(ya_no_procesar())
break;
procesar();
}
seguir();
Sentencia goto
Un rótulo es un nombre, seguido del carácter ":", que se asocia a un segmento de un programa.
La sentencia goto transfiere el control a la instrucción siguiente a un rótulo. Aunque no promueve la
programación estructurada, y se sabe que su abuso es perjudicial, goto es útil para resolver algunas
situaciones. Por ejemplo: anidamiento de lazos con salida forzada.
Aquí se podría implementar una estrategia estructurada, en base a break, pero el control quedaría
retenido en el lazo exterior y se requeriría más lógica para resolver este problema. Se complicaría la legibilidad
del programa innecesariamente.
Los rótulos a los que puede dirigirse un goto tienen un espacio de nombres propio. Es decir, no hay
peligro de conflicto entre un rótulo y una variable del mismo nombre. Además, el ámbito de un rótulo es
local a la función (una sentencia goto sólo puede acceder a los rótulos dentro del texto de la función donde
aparece).
Sentencia return
Permite devolver un valor a la función llamadora. Implica una transferencia de control incondicional
hasta el punto de llamada de la función que se esté ejecutando.
7.5 Observaciones
Hay errores de programación típicos, relacionados con estructuras de control en C, que vale la pena
enumerar:
76
CAPÍTULO 7. ESTRUCTURAS DE CONTROL 7.6. EJERCICIOS
Confundir el significado de un lazo do-while tomando la condición de mientras como si fuera una
condición de hasta (por analogía con repeat de Pascal).
7.6 Ejercicios
1. Reescribir estas sentencias usando while en vez de for:
c. for( ; ; )
a++;
2. Si la función quedanDatos() devuelve el valor lógico que sugiere su nombre, ¿cuál es la estructura
preferible?
a. while(quedanDatos()) {
procesar();
}
b. do {
procesar();
} while(quedanDatos());
77
7.7. PREGUNTAS PARA EL CAPÍTULO 7 CAPÍTULO 7. ESTRUCTURAS DE CONTROL
7. Imprimir la tabla de los diez primeros números primos (sólo divisibles por sí mismos y por la unidad).
a = 1;
if(a == 1)
a = 2;
a = 1;
if(3)
a = 2;
a = 1;
if(b == 2)
a = 2;
a = 1;
if(b == 2);
a = 2;
78
CAPÍTULO 7. ESTRUCTURAS DE CONTROL 7.7. PREGUNTAS PARA EL CAPÍTULO 7
a = 1;
if(b = 0)
a=2;
b = 3;
if(b == 1)
a=2;
else
if(b == 2)
a=3;
else
a=4;
A. 2. B. 3. C. 4. D. No está definido.
switch(c) {
case 1: a = a+d;
break;
case 2: a = a-d;
break;
}
A. 1. B. 2. C. 3.
switch(c) {
case 1: a = a+d;
case 2: a = a-d;
}
A. 1. B. 2. C. 3.
switch(c) {
case 1: b = b+d;
case 2: b = b-d;
default: b = 0;
}
A. 0 B. 1 C. 2 D. 3
switch(c) {
79
7.7. PREGUNTAS PARA EL CAPÍTULO 7 CAPÍTULO 7. ESTRUCTURAS DE CONTROL
case 1: b = b+d;
break;
case 2: b = b-d;
break;
default: b = 0;
}
A. 0 B. 1 C. 2 D. 3
11. ¿Qué resultado final tiene la variable b si inicialmente b, c y d valen 3?
switch(c) {
case 1: b = b+d;
break;
case 2: b = b-d;
break;
default: b = 0;
}
A. 0 B. 1 C. 2 D. 3
12. ¿Qué resultado final tiene la variable c?
c = 1;
for(i=0; i<5; i++);
for(j=0; j<2; j++)
c++;
A. 1 B. 2 C. 3 D. 6 E. 13
13. ¿Cuántas X imprimen estas líneas?
c = 3;
do {
printf("X");
} while(c--);
A. 1 B. 2 C. 3 D. 4
14. ¿Cuántas X imprimen estas líneas?
c = 3;
do {
printf("X");
} while(--c);
A. 1 B. 2 C. 3 D. 4
15. ¿Cuántas X imprimen estas líneas?
c = 3;
while(c--)
printf("X");
80
CAPÍTULO 7. ESTRUCTURAS DE CONTROL 7.7. PREGUNTAS PARA EL CAPÍTULO 7
A. 1 B. 2 C. 3 D. 4
c = 3;
while(--c)
printf("X");
A. 1 B. 2 C. 3 D. 4
81
7.7. PREGUNTAS PARA EL CAPÍTULO 7 CAPÍTULO 7. ESTRUCTURAS DE CONTROL
Respuestas al Capítulo 7
1. B 2. B 3. C 4. B 5. A 6. C 7. B 8. A 9. A 10. C 11. A 12. C 13. D 14. C 15. C
16. B
82
Capítulo 8
Funciones
Una unidad de traducción en C contiene un conjunto de funciones. Si entre ellas existe una con el
nombre especial main, entonces esa unidad de traducción puede dar origen a un programa ejecutable, y el
comienzo de la función main será el punto de entrada al programa.
Ejemplo 8.1
El caso especial de una función que no desea devolver ningún valor se especifica con el modificador
void, y en tal caso un return, si lo hay, no debe tener argumento. Los paréntesis son necesarios aunque la
función no lleve parámetros, y en ese caso es recomendable indicarlo con un parámetro void:
Ejemplo 8.2
void procesar(int k)
{
...
}
int hora(void)
83
8.2. PROTOTIPOS DE FUNCIONES CAPÍTULO 8. FUNCIONES
{
...
}
Una función puede ser declarada de un tipo cualquiera y sin embargo no contar con una instrucción
return. En ese caso su valor de retorno queda indeterminado. Además, la función que llama a otra puede
utilizar o ignorar el valor devuelto, a voluntad, sin provocar errores.
Ejemplo 8.3
En el caso siguiente se recoge basura en la variable a, ya que fun2 no devuelve ningún valor pese a ser declarada como de tipo
entero.
int fun2(int x)
{
...
return;
}
...
a=fun2(1);
Ejemplo 8.4
La variable v declarada dentro del bloque interno opaca a la declarada al principio de la función.
int fun3()
{
int j,k,v;
84
CAPÍTULO 8. FUNCIONES 8.3. REDECLARACIÓN DE FUNCIONES
main()
{
double a;
a=exp(5);
}
Nada permite al compilador suponer que la función exp() debe devolver algo distinto de un entero (el
hecho de que se esté asignando su valor a un double no es informativo, dada la conversión automática de
expresiones que hace el C). Además, el argumento 5 puede tomarse a primera vista como int, pudiendo ser
que en la definición real de la función se haya especificado como double, o alguna otra elección de tipo.
En cualquier caso, esto es problemático, porque la comunicación de parámetros entre funciones, normal-
mente, se hace mediante el stack del programa, donde los objetos se almacenan como sucesiones de bytes.
La función llamada intentará recuperar del stack los bytes necesarios para “armar” los objetos que necesita,
mientras que la función que llama le ha dejado en el mismo stack menos información de la esperada. El
programa compilará correctamente pero los datos pasados a y desde la función serán truncamientos de los
valores deseados.
En el caso particular de las funciones de biblioteca standard, cada grupo de funciones cuenta con su
header conteniendo estas declaraciones, que podemos utilizar para ahorrarnos tipeo. Para las matemáticas,
utilizamos el header math.h:
#include <math.h>
main()
{
double a;
a=exp(5);
}
Un problema más serio que el de la redeclaración de funciones es cuando una función es compilada en
una unidad de traducción separada A y luego se la utiliza, desde una función en otra unidad de traducción
B, pero con una declaración incorrecta, ya sea porque se ha suministrado un prototipo erróneo o porque
no se ha suministrado ningún prototipo explícito. y el implícito, que puede inferir el compilador, no es el
correcto. En este caso la compilación y la linkedición tendrán lugar sin errores, pero la conducta al momento
de ejecución depende de la diferencia entre ambos prototipos, el real y el inferido.
85
8.4. RECURSIVIDAD CAPÍTULO 8. FUNCIONES
8.4 Recursividad
Las funciones en C pueden ser recursivas, es decir, pueden invocarse a sí mismas directa o indirecta-
mente. Siguiendo el principio de que las estructuras de programación deben replicar la estructura de los
datos, la recursividad de funciones es una manera ideal de tratar las estructuras de datos recursivas, como
árboles, listas, etc.
int factorial(int x)
{
if(x==0)
return 1;
return x * factorial(x-1);
}
8.5 Ejercicios
1. Escribir una función que reciba tres argumentos enteros y devuelva un entero, su suma.
2. Escribir una función que reciba dos argumentos enteros y devuelva un long, su producto.
3. Escribir una función que reciba dos argumentos enteros a y b, y utilice a las dos anteriores para
calcular:
(a * b + b * 5 + 2) * (a + b + 1)
4. Escribir un programa que utilice la función anterior para realizar el cálculo con a=7 y b=3.
b. void f2(int k)
{
return k + 3;
}
c. int f3(long k)
{
return (k < 0) ? -1 : 1;
}
printf(" %d\n",f3(8));
86
CAPÍTULO 8. FUNCIONES 8.6. PREGUNTAS PARA EL CAPÍTULO 8
6. Escribir una función que reciba dos argumentos, uno de tipo int y el otro de tipo char. La función
debe repetir la impresión del char tantas veces como lo diga el otro argumento. Escribir un programa
para probar la función.
int fun(int a) {
a = 2 * b;
return b;
}
3. ¿Cuál sería el prototipo más plausible para la función q() si su uso legal es como el siguiente?
float p, r;
int s;
r = q(p,s) / 2;
A. float q(float x, int y); B. float q(int x, int y); C. int q(float x, float
y);
4. ¿Cuál sería el prototipo más plausible para la función t() si su uso legal es como el siguiente?
double w;
w = t(5e1, 2L);
A. long t(); B. double t(int x, int y); C. double t(double x, long y); D. long
t(double x, double y);
5. Con el prototipo:
87
8.6. PREGUNTAS PARA EL CAPÍTULO 8 CAPÍTULO 8. FUNCIONES
A. x B. y C. g D. h
6. Con el prototipo:
A. a B. b C. c D. d
88
CAPÍTULO 8. FUNCIONES 8.6. PREGUNTAS PARA EL CAPÍTULO 8
Respuestas al Capítulo 8
1. B 2. C 3. A 4. C 5. A 6. D 7. E
89
8.6. PREGUNTAS PARA EL CAPÍTULO 8 CAPÍTULO 8. FUNCIONES
90
Capítulo 9
Variables estructuradas
Como casi todos los lenguajes, el C provee varias maneras de estructurar, o combinar, variables simples
en alguna forma de agregación. Las variables estructuradas permiten manejar un conjunto de datos como
una sola entidad.
9.1 Arreglos
La declaración
tipo nombre[cant_elementos];
define un bloque llamado nombre de cant_elementos objetos consecutivos de tipo tipo, lo que
habitualmente recibe el nombre de vector, array o arreglo. Sus elementos se acceden indexando el
bloque con expresiones enteras entre corchetes.
Ejemplo 9.1
int dias[12];
dias[0] = 31;
enero = dias[0];
febrero = dias[1];
a = dias[6 * b - 1];
double saldo[10];
for(i=0; i<10; i++)
saldo[i] = entradas[i] - salidas[i];
91
9.1. ARREGLOS CAPÍTULO 9. VARIABLES ESTRUCTURADAS
Inicialización de arreglos
Al ser declarados, los arreglos pueden recibir una inicialización, que es una lista de valores del tipo
correspondiente, indicados entre llaves. Esta inicialización puede ser completa o incompleta. Si se omite la
dimensión del arreglo, el compilador la infiere por la cantidad de valores de inicialización.
Ejemplo 9.2
/* inicialización completa */
int dias[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
/* inicialización incompleta */
double saldo[10] = { 150.40, 170.20 };
Ejemplo 9.3
La última instrucción equivale a acceder a un elemento fuera de los límites del arreglo. Este programa, aunque erróneo,
compilará correctamente.
int tabla[5];
Asignación de arreglos
Es frecuente confundir las operaciones de inicialización y de asignación. La inicialización sólo es válida
en el momento de la declaración: no es legal asignar un arreglo. La asignación debe forzosamente
hacerse elemento por elemento.
Ejemplo 9.4
/* Inicializacion */
int tabla[5] = { 1, 3, 2, 3, 4 }; /* correcto */
/* Asignacion */
int tabla[5];
tabla[] = { 1, 3, 2, 3, 4 }; /* incorrecto */
92
CAPÍTULO 9. VARIABLES ESTRUCTURADAS 9.2. ARREGLOS MULTIDIMENSIONALES
tabla[0] = 1;
tabla[1] = 3; ...etc
int matriz[3][4];
expresa un arreglo de tres posiciones cuyos elementos son arreglos de cuatro posiciones. Una declaración
con inicialización podría escribirse así:
int matriz[3][4] = {
{1, 2, 5, 7},
{3, 0, 0, 1},
{2, 8, 5, 4}};
y correspondería a una matriz de tres filas por cuatro columnas. La primera dimensión de un arreglo
multidimensional puede ser inferida:
int matriz[][4] = {
{1, 2, 5, 7},
{3, 0, 0, 1},
{2, 8, 5, 4}};
El recorrido de toda una matriz implica necesariamente un lazo doble, a dos variables:
93
9.3. ESTRUCTURAS Y UNIONES CAPÍTULO 9. VARIABLES ESTRUCTURADAS
Ejemplo 9.1
Las declaraciones siguientes no definen variables, con espacio de almacenamiento, sino que simplemente enuncian un nuevo
tipo que puede usarse en nuevas declaraciones de variables.
struct persona {
long DNI;
char nombre[40];
int edad;
};
struct cliente {
int num_cliente;
struct persona p;
double saldo;
};
El nombre o tag dado a la estructura es el nombre del nuevo tipo. En las instrucciones siguientes se utilizan los tags definidos
anteriormente y se acceden a los diferentes miembros de las estructuras.
c1.num_cliente = 1001;
c1.p.DNI = 14233326; /* acceso anidado */
c1.p.edad=40;
struct cliente c3 = {
1002,
{17698735, "Juan Perez", 30},
150.25 };
Ejemplo 9.2
Es legal declarar una variable struct junto con la enunciación de su tipo, con o sin el tag asociado y con o sin inicialización.
94
CAPÍTULO 9. VARIABLES ESTRUCTURADAS 9.3. ESTRUCTURAS Y UNIONES
Ejemplo 9.3
struct punto {
int x, y;
};
Uniones
En una estructura, el compilador calcula la dirección de inicio de cada uno de los miembros dentro de
la estructura sumando los tamaños de los elementos de datos. Una unión es un caso especial de estructura
donde todos los miembros “nacen” en el mismo lugar de origen de la estructura.
union intchar {
int i;
char a[sizeof(int)];
};
Este ejemplo de unión contiene dos miembros: un entero y un arreglo de tantos caracteres como bytes
contiene un int en la arquitectura destino. Ambos miembros, por la propiedad fundamental de los unions,
quedan “superpuestos” en memoria. El resultado es que podemos asignar un campo por un nombre y
acceder por el otro.
Ejemplo 9.4
En este caso particular, podemos conocer qué valores recibe cada byte de los que forman un int.
union intchar k;
k.i = 30541;
b = k.a[2];
Campos de bits
Se pueden definir estructuras donde los miembros son agrupaciones de bits. Esta construcción es espe-
cialmente útil en programación de sistemas donde se necesita la máxima compactación de las estructuras
de datos. Cada miembro de un campo de bits es un unsigned int que lleva explícitamente un “ancho”
indicando la cantidad de bits que contiene.
struct disp {
unsigned int encendido : 1;
unsigned int
online : 1,
estado : 4;
};
95
9.4. EJERCICIOS CAPÍTULO 9. VARIABLES ESTRUCTURADAS
Ejemplo 9.5
Imaginemos, en base a la declaración anterior, un dispositivo mapeado en memoria con el cual comunicarnos en base a un
protocolo, también imaginario. Implementamos con un campo de bits un registro de control encendido, que permite encenderlo
o apagarlo, consultar su disponibilidad (online o no), y hacer una lectura de un valor de estado de cuatro bits (que entonces
puede tomar valores entre 0 y 15). Todo el registro de comunicación cabe en un byte.
Podríamos encender nuestro dispositivo imaginario, esperar a que se ponga online, tomar el promedio de diez lecturas de
estado y apagarlo, con las instrucciones siguientes. Como se ve, no hay diferencia sintáctica de acceso con las estructuras.
struct disp {
unsigned int encendido : 1;
unsigned int
online : 1,
estado : 4;
};
struct disp d;
d.encendido = 1;
while(!d.online);
for(p=0, i=0; i<10; i++)
p += d.estado;
p /= 10;
d.encendido = 0;
9.4 Ejercicios
1. Escribir una declaración con inicialización de un arreglo de diez elementos double, todos inicialmente
iguales a 2,25.
4. Escribir una función que devuelva la posición donde se halla el menor elemento de un arreglo de
floats.
5. Dado un vector de diez elementos, escribir todos los promedios de cuatro elementos consecutivos.
6. Declarar una estructura punto conteniendo coordenadas x e y de tipo double. Dar ejemplos de
inicialización y de asignación.
7. Declarar una estructura segmento conteniendo dos estructuras punto. Dar ejemplos de inicialización
y de asignación. Dar una función que calcule su longitud. Dar una función que reciba un segmento
S y un punto p, y devuelva TRUE si p ∈ S.
a. struct alfa {
int a, b;
};
alfa = { 10, 25 };
96
CAPÍTULO 9. VARIABLES ESTRUCTURADAS 9.5. PREGUNTAS PARA EL CAPÍTULO 9
b. struct alfa {
int a, b;
};
alfa d = { 10, 25 };
c. union dato {
char dato_a[4];
long dato_n;
} xdato = { "ABC", 1000 };
5. Con la declaración del arreglo que sigue, ¿cuál de las sentencias es incorrecta?
long trece[12] = {1, 5, 20L, 35};
A. trece[1]++; B. trece[12]--; C. trece[1] = trece[0]; D. trece[0]; E. trece[11]
= 20L; F. 20L;
97
9.5. PREGUNTAS PARA EL CAPÍTULO 9 CAPÍTULO 9. VARIABLES ESTRUCTURADAS
Respuestas al Capítulo 9
1. B 2. C 2. E 3. C 4. A 5. B 5. D 5. F 6. B
98
Capítulo 10
Apuntadores y Direcciones
El tema de esta unidad es el más complejo del lenguaje C y por este motivo se han separado los
contenidos en dos partes (llamadas 10 y 10b).
La memoria del computador está organizada como un vector o arreglo unidimensional. Los índices en este
arreglo son las direcciones de memoria. Este arreglo puede accederse indexando a cada byte individualmente,
y en particular a cada estructura de datos del programa, mediante su dirección de comienzo.
Para manipular direcciones se utilizan en C variables especiales llamadas apuntadores o punteros, que
son aquellas capaces de contener direcciones. En la declaración de un apuntador se especifica el tipo de los
objetos de datos cuya dirección contendrá.
La notación:
char *p;
es la declaración de una variable puntero a carácter. El contenido de la variable p puede ser, en principio,
cualquiera dentro del rango de direcciones de la máquina subyacente al programa. Una vez habiendo recibido
un valor, se dice que la variable p apunta a algún objeto en memoria.
Esquemáticamente representamos la situación de una variable que contiene una dirección (y por lo tanto
“apunta a esa dirección”) según el diagrama siguiente (mas_información). La posición 1 de la memoria
aloja un puntero que actualmente apunta a la posición 5.
El tema de apuntadores (o punteros) y direcciones es crucial en la programación en C, y parece ser el
origen más frecuente de errores. Programas con mala lógica de acceso a memoria pueden ser declarados
válidos por el compilador: su compilación puede ser exitosa y sin embargo ser completamente erróneos en
ejecución. Esta es una de las críticas más frecuentes al lenguaje C, aunque en rigor de verdad, el problema no
es del lenguaje, sino del programador con una mala comprensión de las cuestiones del lenguaje relacionadas
con memoria.
Es fundamental, para no cometer estos errores, comprender en profundidad los conceptos de direcciones
y punteros, así como la sintaxis de las declaraciones de punteros, para asegurarnos de que escribimos lo
que se pretende lograr.
p = &a;
99
10.2. ARITMÉTICA DE PUNTEROS CAPÍTULO 10. APUNTADORES Y DIRECCIONES
a = *p;
Ejemplo 10.1
Las instrucciones
int a, *p;
p = &a;
*p = 1;
equivalen a
a = 1;
100
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.3. PUNTEROS Y ARREGLOS
Resta de punteros
El sentido de una resta de punteros (o, equivalentemente, de una diferencia de direcciones) es obtener
el tamaño del área de memoria comprendida entre los objetos apuntados por ambos punteros. La resta
tendrá sentido únicamente si se hace entre variables que apuntan a objetos del mismo tipo.
Nuevamente se aplica la lógica del punto anterior: el resultado obtenido quedará expresado en unidades
del tamaño del objeto apuntado. Es decir, si una diferencia entre punteros a long da 3, debe entenderse el
resultado como equivalente a 3 longs, y por lo tanto a 3*sizeof(long) bytes.
int *p;
int a[10];
p = &a[0];
Habilitan al programador para acceder a cada elemento del arreglo a mediante aritmética sobre el
puntero p. Como el nombre de un arreglo se evalúa a su dirección inicial, la última sentencia también puede
escribirse simplemente así:
p = a;
Ejemplo 10.1
int alfa[] = { 2, 4, 6, 7, 4, 2, 3, 1 };
int *p, *q;
int b;
*p = 3; /* equivalente a alfa[0] = 3 */
*(p+2) = 4; /* equivalente a alfa[2] = 4 */
b = *p; /* equiv. a b = alfa[0] */
*(p+3) = *(p+6); /* sobreescribe el 7 con el 3 */
Los dos segmentos de código siguientes hacen exactamente la misma tarea pero de maneras diferentes.
101
10.4. PUNTEROS Y CADENAS DE TEXTO CAPÍTULO 10. APUNTADORES Y DIRECCIONES
Ejemplo 10.1
Ejemplo 10.2
La inicialización de s y la asignación de t cargan a ambas variables con las direcciones del primer carácter, o direcciones
iniciales, de las cadenas respectivas.
Ejemplo 10.3
O bien, equivalentemente:
imprimen:
Cadena de prueba
de prueba
Ejemplo 10.4
102
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.5. PASAJE POR REFERENCIA
Una función que recorre una cadena ASCIIZ buscando un carácter y devuelve la primera dirección donde se lo halló, o bien el
puntero nulo (NULL).
main()
{
char *cadena = "Buscando exactamente esto";
char *s;
s = donde(cadena, ’e’);
if(s != NULL)
printf(" %s\n", s);
}
exactamente esto
El pasaje por referencia permite que una función pueda modificar un objeto que es local a otra
función. Un pasaje por referencia de un objeto implica entregar a la función la dirección del objeto.
Ejemplo 10.1
Modificación de un objeto externo a una función. La función f2() debe poner a cero una variable entera que es externa a ella,
por lo cual el argumento formal h debe ser la dirección de un entero.
int f1()
{
int j,k;
int *p;
p = &j;
f2(p); /* le pasamos una direccion */
f2(&k); /* y tambien aqui */
}
103
10.6. PUNTEROS Y ARGUMENTOS DE FUNCIONES
CAPÍTULO 10. APUNTADORES Y DIRECCIONES
Ejemplo 10.2
La función swap(), que podría ser usada por un algoritmo de ordenamiento para intercambiar los valores de dos variables,
está incorrectamente escrita, ya que los valores que intercambia son los de sus argumentos, que vienen a estar al nivel de
variables locales. El uso de la función swap() no tendrá efecto en el exterior de la misma. La versión correcta debe escribirse
con pasaje por referencia:
La invocación de swap() debe hacerse con las direcciones de los objetos a intercambiar:
int a, b;
swap(&a, &b);
Ejemplo 10.1
La función que busca un carácter en una cadena, vista más arriba, puede escribirse correctamente así, cambiando el tipo del
argumento formal. El uso es exactamente el mismo que antes, sin cambios en la función que llama.
Nótese que dentro del cuerpo de la función podemos seguir utilizando la notación de punteros si lo deseamos, aun con la
declaración del argumento formal como arreglo.
104
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.7. EJERCICIOS
return p;
return NULL;
}
Del mismo modo, si quisiéramos, podríamos representar los argumentos como punteros y manipular los
datos con indexación. Todo esto se debe, por un lado, a que las notaciones *p y p[], para argumentos
formales, expresan únicamente que el argumento es una dirección; y por otro lado, a la equivalencia entre
las formas de acceso mediante apuntadores y mediante índices de arreglos.
¡Esto no quiere decir que punteros y arreglos sean lo mismo! Véanse las observaciones en la próxima
unidad.
10.7 Ejercicios
1. Dado el programa siguiente, ¿a dónde apunta k1?
main()
{
int k;
int *k1;
}
int *m1;
main()
{
...
}
3. ¿Cuánto espacio de almacenamiento ocupa un arreglo de diez enteros? ¿Cuánto espacio de almace-
namiento ocupa un puntero a entero?
4. Declarar variables long llamadas a, b y c, y punteros a long llamados p, q y r; y dar las sentencias en
C para realizar las operaciones siguientes. Para cada caso, esquematizar el estado final de la memoria.
5. Compilar y ejecutar:
a. main()
{
char *a = "Ejemplo";
printf(" %s\n",a);
}
105
10.7. EJERCICIOS CAPÍTULO 10. APUNTADORES Y DIRECCIONES
b. main()
{
char *a;
printf(" %s\n",a);
}
c. main()
{
char *a = "Ejemplo";
char *p;
p = a;
printf(" %s\n", p);
}
a. char *s = "ABCDEFG";
b. printf(" %s\n", s);
c. printf(" %s\n", s + 0);
d. printf(" %s\n", s + 1);
e. printf(" %s\n", s + 6);
f. printf(" %s\n", s + 7);
g. printf(" %s\n", s + 8);
7. ¿Son correctas estas sentencias? Bosqueje un diagrama del estado final de la memoria para aquellas
que lo sean.
a. char *a = "Uno";
b. char *a, b; a = "Uno"; b = "Dos";
c. char *a,*b ; a = "Uno"; b = a;
d. char *a,*b ; a = "Uno"; b = *a;
e. char *a,*b ; a = "Uno"; *b = a;
f. char *a = "Dos"; *a = ’T’;
g. char *a = "Dos"; a = "T";
h. char *a = "Dos"; *(a + 1)= ’i’; *(a + 2)= ’a’;
i. char *a, *b ; b = a;
9. Escribir una función para reemplazar en una cadena todas las ocurrencias de un carácter dado por
otro, suponiendo:
106
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.8. ERRORES MÁS FRECUENTES
a. Que no interesa conservar la cadena original, sino que se reemplazarán los caracteres sobre la
misma cadena.
b. Que se pretende obtener una segunda copia, modificada, de la cadena original, sin destruirla.
a. Rellenar una cadena con un carácter dado, hasta que se encuentre el 0 final, o hasta alcanzar
n iteraciones.
b. Pasar una cadena a mayúsculas o minúsculas.
11. Reescriba dos de las funciones escritas en 8 y dos de las escritas en 10 usando la notación opuesta
(cambiando punteros por arreglos).
Ejemplo 10.1
Si bien sintácticamente correctas, las líneas del ejemplo presentan un problema muy común en C. Declaran un puntero a entero,
p, y almacenan un valor entero en la dirección apuntada por el mismo.
/* Incorrecto */
int *p;
*p = 8;
Como p es un puntero, su contenido será interpretado como una dirección de memoria. El problema es que el contenido
de p es impredecible:
Si la variable p es local, su clase de almacenamiento es auto y por lo tanto contiene basura, salvo inicialización explícita.
Si p es externa, será inicializada a 0.
En el primer caso, esa dirección es aleatoria. En el segundo caso, será cero, que en la mayoría de los sistemas operativos
conocidos es una dirección inaccesible para los procesos no privilegiados.
En cualquier caso, el programa compilará pero se encontrará con problemas de ejecución. Lo que falta es hacer que p
apunte a alguna zona válida.
Direcciones válidas, que pueden ser manipuladas mediante punteros, son las de los objetos conocidos por el programa:
variables, estructuras, arreglos, funciones, bloques de memoria asignados dinámicamente, son todos objetos cuya dirección
ha sido obtenida legítimamente, ya sea al momento de carga o al momento de ejecución.
Si un puntero no está explícitamente inicializado a alguna dirección válida dentro del espacio del
programa, se estará escribiendo en alguna dirección potencialmente prohibida. En el mejor de los
casos, el programa intentará escribir en el espacio de memoria de otro proceso y el sistema operativo
lo terminará. En casos más sutiles, el programa continuará funcionando pero luego de corromper
algún área de memoria impredecible dentro del espacio del proceso.
Este problema es por lo general difícil de detectar, porque el efecto puede no mostrar ninguna relación
con el origen del problema. El error puede estar en una instrucción que se ejecuta pero corrompe la
memoria, y sin embargo manifestarse recién cuando se accede a esa zona de memoria corrupta.
Para este momento, el control del programa puede estar en un punto arbitrariamente lejano de la
instrucción que causó el problema.
107
10.8. ERRORES MÁS FRECUENTES CAPÍTULO 10. APUNTADORES Y DIRECCIONES
Ejemplo 10.2
La solución al problema de los punteros sin inicializar es asegurarse, siempre, que los punteros apuntan a lugares válidos del
programa, asignándoles direcciones de objetos conocidos.
/* Correcto */
int a;
int *p;
p = &a;
*p = 8;
Aquí usamos como equivalentes a un arreglo y a un puntero, porque de cualquiera de las dos
maneras estamos expresando una dirección en la invocación a printf().
Ambas formas son válidas, porque lo único que estamos expresando es que un argumento es una
dirección; y, como se ha dicho, tanto punteros como nombres de arreglos los representan. Además,
108
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.8. ERRORES MÁS FRECUENTES
cualquiera de las funciones escritas puede usarse en cualquiera de las dos maneras siguientes, pasando
punteros o arreglos en la llamada:
3. Comparten operadores.
Se pueden aplicar los mismos operadores de acceso a ambos; a saber, se puede dereferenciar un
arreglo (igual que un puntero) para acceder a un elemento y se puede indexar un puntero (como un
arreglo) para acceder a una posición dentro del espacio al que apunta.
En muchas formas, entonces, punteros y arreglos pueden ser intercambiados porque son dos maneras
de acceder a direcciones de memoria, pero un arreglo no es un puntero, y ninguno de ellos es una
dirección (aunque las representan), porque:
Un arreglo tiene memoria asignada para todos sus elementos (desde la carga del programa, para
arreglos globales o static, y desde la entrada a la función donde se lo declara, para los arreglos
locales).
Un puntero, en cambio, solamente contiene una dirección, que puede ser o no válida en el sentido
de apuntar o no a un objeto existente en el espacio direccionable por el programa. La validez de la
dirección contenida en un puntero es responsabilidad del programador.
109
10.8. ERRORES MÁS FRECUENTES CAPÍTULO 10. APUNTADORES Y DIRECCIONES
Al escribir una expresión, o hacer una asignación, o proveer argumentos reales para una función, etc., es
necesario que los niveles de indirección de los elementos que componen la expresión sean consistentes,
es decir, que el resultado final de cada subexpresión tenga el nivel de indirección adecuado.
El concepto de consistencia de niveles de indirección es análogo al que se usa al escribir una ecuación
con magnitudes físicas, donde ambos miembros de la ecuación deben tener el mismo sentido físico.
Por ejemplo, si V = velocidad, E = espacio, T = tiempo, no tiene sentido en Física escribir la
igualdad V = E ∗ T , simplemente porque si multiplicamos metros por segundo no obtenemos m/s,
que son las unidades de V . Del mismo modo podemos verificar la consistencia de las expresiones en
C preguntándonos qué nivel de indirección debe tener cada subexpresión.
Ejemplo 10.3
Este tipo de análisis es sumamente útil para prevenir errores de programación. Conviene utilizarlo para dar una segunda mirada
crítica a las expresiones que escribimos.
char *s = "cadena";
char *t;
char u;
t = s + 2; /* CORRECTO */
u = s; /* INCORRECTO */
u = *s; /* CORRECTO */
t = &u; /* CORRECTO */
La asignación t = s + 2 es correcta porque la suma de una dirección más un entero está definida y devuelve una dirección;
con lo cual la expresión mantiene el mismo nivel de indirección en ambos miembros (puntero = dirección).
En cambio, la asignación u = s intenta asignar una dirección (la dirección contenida en s) a un char. No se respeta el
mismo nivel de indirección en ambos miembros de la asignación (puntero = char), de modo que ésta es incorrecta y será
rechazada por el compilador.
En las dos últimas asignaciones se usan los operadores de dirección y de indirección para hacer consistentes los niveles de
indirección de ambos miembros:
u = *s;
110
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.9. ARREGLOS DE PUNTEROS
t = &u;
puntero a char = direccion de(char);
direccion de char = direccion de char;
Ejemplo 10.1
Aquí el tipo de los elementos del arreglo mes es puntero a carácter. Cada elemento se inicializa en la declaración a una constante
string.
printf("Mes: %s\n",mes[6]);
Ejemplo 10.1
struct persona p;
p.DNI = 14233326;
p.edad = 40;
En forma indirecta:
struct itemlista {
double dato;
struct itemlista *proximoitem;
}
111
10.12. CONSTRUCCIÓN DE TIPOS CAPÍTULO 10. APUNTADORES Y DIRECCIONES
struct nodoarbol {
int valor;
struct nodoarbol *hijoizquierdo;
struct nodoarbol *hermanoderecho;
}
Ejemplo 10.1
Las declaraciones del ejemplo anterior se podrían reescribir más claramente de la forma que sigue.
Entonces, el tipo de un argumento de una función podría quedar expresado sintéticamente como nodop:
La construcción con typedef no es indispensable, pero aporta claridad al estilo de programación, y, bien manejada,
promueve la portabilidad.
Ejemplo 10.1
112
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.14. PUNTEROS A FUNCIONES
En lugar de hacer que p apunte a un objeto existente al momento de compilación, solicitamos tanta memoria como sea necesaria
para alojar un entero y ponemos a p apuntando allí. Luego podemos hacer la asignación. Luego del uso se debe liberar la zona
apuntada por p.
/* Correcto */
int *p;
p = malloc(sizeof(int));
*p = 8;
free(p);
Para ser completamente correcto, el programa del ejemplo anterior debería verificar que malloc() no
devuelva NULL por ser imposible satisfacer el requerimiento de memoria. El símbolo NULL corresponde
a la dirección 0, o, equivalentemente, al puntero nulo, y nunca es una dirección utilizable.
Ejemplo 10.2
/* ‘Mejor! */
int *p;
if(p = (int *)malloc(sizeof(int))) {
*p = 8;
free(p);
}
Aplicamos el operador cast al resultado de malloc() para que ambos miembros de la asignación sean consistentes.
Ejemplo 10.3
La propiedad de poder aplicar indexación a los punteros hace que, virtualmente, el C sea capaz de proporcionarnos arreglos
dimensionables en tiempo de ejecución. En efecto:
double *tabla;
tabla = malloc(k);
tabla[50] = 15.25;
Estas líneas son virtualmente equivalentes a un arreglo de k elementos double, donde k, por supuesto, puede ser calculado
en tiempo de ejecución.
Una variante de malloc() es la función calloc(), que solicita una cantidad dada de elementos de memoria, de un tamaño
también dado, y además garantiza que todo el bloque de memoria concedido esté inicializado con ceros binarios.
float *lista;
int i;
lista = calloc(k, sizeof(float));
for(i=0; i<k; i++)
lista[i] = fun(i);
113
10.15. APLICACIÓN DE PUNTEROS A FUNCIONES
CAPÍTULO 10. APUNTADORES Y DIRECCIONES
La declaración de un puntero a función tiene una sintaxis algo complicada: debe indicar el tipo devuelto
por la función y los tipos de los argumentos. El nombre del puntero, con el signo de “puntero” incluido,
debe estar entre paréntesis en la declaración.
Ejemplo 10.1
Definición de un puntero llamado p, a una función que recibe dos enteros y devuelve un entero:
o también:
Los paréntesis alrededor de *p son importantes: sin ellos, se define una función que devuelve un puntero a entero, que
no es lo que se pretende.
Asignación del puntero p:
Uso del puntero p para invocar a la función fun cuya dirección tiene asignada:
a = (*p)(k1, 20 - k2);
Si Devolver
*p1 < *p2 un número menor que cero
*p1 == *p2 cero
*p1 < *p2 un número mayor que cero
Ejemplo 10.1
La declaración de argumentos formales como void * expresa que esos argumentos pueden tratarse de direcciones de objetos
de cualquier tipo.
#include <stdlib.h>
struct p {
...
char nombre[40];
114
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.16. PUNTEROS A PUNTEROS
double salario;
...
} lista[100];
Con estas definiciones, la tabla lista se puede ordenar por uno u otro campo de la estructura con las sentencias:
Ejemplo 10.1
115
10.18. EJERCICIOS CAPÍTULO 10. APUNTADORES Y DIRECCIONES
La función gets() debe recibir la dirección de un área de memoria legal para el programa y donde
no haya riesgo de sobreescribir contenidos. Es importante comprender la teoría dada en este capítulo para
evitar el uso de un puntero no inicializado, una situación de error frecuente cuando se utilizan gets()
y funciones similares.
Ejemplo 10.1
Creamos un área de memoria utilizable por gets() simplemente definiendo un arreglo de caracteres del tamaño deseado.
main()
{
char arreglo[100];
gets(arreglo);
}
El puntero del siguiente ejemplo no está inicializado. Por ser una variable local, contiene basura, y por lo tanto apunta a
un lugar impredecible.
main()
{
char *s;
gets(s); /* Mal!!! */
}
Podemos corregir el ejemplo anterior proveyendo espacio legítimo a donde apunte s, reservándolo estáticamente mediante
la declaración del arreglo.
main()
{
char arreglo[100];
char *s;
s = arreglo;
gets(s); /* ‘Ahora sí! */
}
En el tercer ejemplo usamos asignación dinámica y liberación de memoria. En un sentido estricto, en este caso particular
no es necesaria la liberación, porque la terminación del programa devuelve todas las estructuras creadas dinámicamente; pero
es útil habituarse a la disciplina de aparear cada invocación de malloc() con el correspondiente free().
main()
{
char *s;
s = malloc(100); /* ‘Ahora sí! */
gets(s);
printf("ingresado: %s\n",s);
free(s);
}
10.18 Ejercicios
1. ¿Qué objetos se declaran en las sentencias siguientes?
116
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.19. PREGUNTAS PARA EL CAPÍTULO 10
e. int pi[3];
f. long *beta[3];
g. int *(gamma[3]);
h. int (*delta)[3];
i. void (*eta[5])(int *rho);
j. int *mu(long delta);
2. Construir una función que reciba un arreglo de punteros a string A y un string B, y busque a B en
el array A, devolviendo su índice en el array, o bien −1 si no se halla.
3. Construir una función iterativa que imprima una cadena en forma inversa. Muestre una versión y una
recursiva.
4. Construir un programa que lea una sucesión de palabras y las busque en un pequeño diccionario. Al
finalizar debe imprimir la cuenta de ocurrencias de cada palabra en el diccionario.
5. Construir un programa que lea una sucesión de palabras y las almacene en un arreglo de punteros a
carácter.
2. La expresión a = *p es equivalente a
A. multiplicar a por p. B. hacer a igual a lo apuntado por p. C. almacenar en a la dirección de p.
D. a = p.
117
10.19. PREGUNTAS PARA EL CAPÍTULO 10 CAPÍTULO 10. APUNTADORES Y DIRECCIONES
14. Con la declaración char *a[] = {"alfa", "beta", "gamma" };, se tiene que a[1] equivale a:
A. la dirección de la cadena "beta". B. la dirección de la cadena "alfa". C. la letra ’l’ dentro
de la cadena "alfa". D. la letra ’b’ dentro de la cadena "beta". E. Ninguna de las anteriores.
15. Con la declaración char *a[] = {"alfa", "beta", "gamma" }; se tiene que *a[1] equivale a:
A. la dirección de la cadena "beta". B. la dirección de la cadena "alfa". C. la letra ’l’ dentro
de la cadena "alfa". D. la letra ’b’ dentro de la cadena "beta". E. Ninguna de las anteriores.
16. Con la declaración char *a[] = {"alfa", "beta", "gamma" };, la letra ’t’ dentro de la cadena
"beta" se puede escribir como:
A. a[1][2]. B. *(a[1]+2). C. *(*(a+1)+2). D. Todas las anteriores. E. Ninguna de las
anteriores.
118
CAPÍTULO 10. APUNTADORES Y DIRECCIONES 10.19. PREGUNTAS PARA EL CAPÍTULO 10
Respuestas al Capítulo 10
1. B 2. B 3. D 4. C 5. C 6. B 7. A 8. B 9. C 10. D 11. B 12. C 13. B 14. A 15. D
16. D
119
10.19. PREGUNTAS PARA EL CAPÍTULO 10 CAPÍTULO 10. APUNTADORES Y DIRECCIONES
120
Capítulo 11
Entrada/Salida Standard
Funciones de Entrada/Salida
E/S standard (??) Sobre archivos (??)
De caracteres (??) ANSI C (??) POSIX (??)
De líneas (??) De caracteres (??) De acceso directo (??)
Con formato (??) De líneas (??)
Sobre strings (??) Con formato (??)
121
11.2. E/S STANDARD DE CARACTERES CAPÍTULO 11. ENTRADA/SALIDA STANDARD
o la posibilidad de organizar la salida en pantalla. Por ejemplo, no hay una forma canónica de borrar
la pantalla en C, ya que ésta es una función que depende fuertemente de la plataforma y del ambiente
donde se ejecute el programa. Las características faltantes en la E/S standard se compensan recurriendo a
bibliotecas de terceras partes.
Ejemplo 11.1
Un programa que copia la entrada en la salida, carácter a carácter. Puede usarse, con redirección, para crear o copiar archivos,
como un clon del comando cat de UNIX.
#include <stdio.h>
main()
{
int a;
while((a = getchar()) != EOF)
putchar(a);
}
Ejemplo 11.1
El mismo programa, pero orientado a copiar un stream de texto línea por línea. La constante BUFSIZ está definida en stdio.h
y es el tamaño del buffer de estas funciones. Se puede sugerir esta elección para el buffer del programa, salvo que haya motivos
para proporcionar otro tamaño.
#include <stdio.h>
main()
{
char area[BUFSIZ];
while(gets(area) != NULL)
puts(area);
}
La función gets() elimina el \n final con que termina la línea antes de almacenarla en su buffer. La
función puts() lo repone.
122
CAPÍTULO 11. ENTRADA/SALIDA STANDARD 11.4. E/S STANDARD CON FORMATO
Como el tamaño del buffer no es argumento para la función gets(), ésta no conoce los límites del
área de memoria de que dispone para dejar los resultados de una operación de entrada, y por lo
tanto no puede hacer verificación en tiempo de ejecución. Podría ocurrir que una línea de entrada
superara el tamaño del buffer: entonces esta entrada corromperá algún otro contenido del espacio del
programa.
Esta condición se conoce como buffer overflow, y el comportamiento en este caso queda indefinido;
dando lugar, inclusive, a problemas de seguridad. Por este motivo gets() no es utilizada en programas
de producción.
Ejemplo 11.1
Leer de entrada standard un valor entero y un long, e imprimirlos. Controlar que efectivamente se haya logrado leer ambos
valores antes de imprimir las variables.
main()
{
int a, long b;
if(scanf(" %d %ld", &a, &b) != 2)
exit(1);
printf("a= %d, b= %ld\n", a, b);
}
El valor devuelto por scanf() es la cantidad de datos leídos y convertidos exitosamente, y debería
siempre comprobarse que es igual a lo esperado.
El uso de scanf() es con frecuencia problemático. La función scanf() consumirá toda la entrada
posible, pero se detendrá al encontrar un error (una entrada que no corresponda a lo descripto por la
especificación de conversión) y dejará el resto de la entrada sin procesar, en el buffer de entrada. Si
luego otra función de entrada intenta leer, se encontrará con esta entrada no consumida, lo cual puede dar
origen a problemas de ejecución difíciles de diagnosticar. El error parecerá producirse en una instrucción
posterior a la invocación de scanf() culpable.
Por esta razón suele ser difícil combinar el uso de scanf() con el de otras funciones de entrada/salida.
Además, no hay manera directa de validar que la entrada quede en el rango del tipo de datos destino.
123
11.5. E/S STANDARD SOBRE STRINGS CAPÍTULO 11. ENTRADA/SALIDA STANDARD
El uso más recomendable de scanf() es cuando se la utiliza para leer, mediante redirección, un flujo
generado automáticamente por otro programa (y que, por lo tanto, tiene una gramática rigurosa y
conocida).
Ejemplo 11.1
main()
{
char area[1024];
int a; long b;
1: -6534 1273632
2: -6534 1273632
3: -6534 1273632
124
Capítulo 12
Diferentes sistemas operativos tienen diferentes sistemas de archivos y diferentes modelos de ar-
chivos. Los sistemas de archivos son los conjuntos de funciones particulares que cada sistema ofrece para
acceder a los archivos y a la estructura de directorios que soporta. Los modelos de archivos son aquellas
convenciones de formato u organización que son particulares de un sistema operativo o plataforma.
Por ejemplo, tanto DOS como UNIX soportan la noción de archivo de texto, pero con algunas diferencias
a nivel de modelo de archivos. En ambos, un archivo de texto es una secuencia de líneas de texto, donde
cada línea es una secuencia de caracteres terminado en fin de línea; pero en DOS, la convención de fin de
línea es un par de caracteres (CR, LF) (equivalentes a ASCII 13 y 10, respectivamente) mientras que para
UNIX, un fin de línea equivale solamente a LF (ASCII 10). Además DOS, a diferencia de UNIX, almacena
un carácter especial (EOF o ASCII 26) al final de los archivos de texto para señalar el fin del archivo.
Por otro lado, diferentes sistemas de archivo proveen diferentes vistas sobre diferentes implementaciones.
Un sistema operativo puede soportar o no la noción de directorio, o la de links múltiples; o puede fijar
determinadas condiciones sobre los nombres de archivos, todo esto en función de la organización íntima de
sus estructuras de datos.
125
12.2. FUNCIONES ANSI C DE E/S SOBRE ARCHIVOS CAPÍTULO 12. E/S SOBRE ARCHIVOS
solicitada por una instrucción del programa no se efectiviza inmediatamente sino que se realiza sobre un
buffer intermedio, administrado por las funciones de biblioteca standard y con su política propia de flushing
o descarga al dispositivo.
Las funciones de entrada/salida bufferizada reciben argumentos a imprimir y los van depositando en
un buffer o zona de memoria intermedia. Cuando el buffer se llena, o cuando aparece un carácter de fin
de línea, el buffer se descarga al dispositivo, escribiéndose los contenidos del buffer en pantalla, disco, etc.
En cambio, las funciones POSIX hacen E/S directa a los dispositivos (o al menos, al sistema de E/S del
sistema operativo) y por esto son las preferidas para la programación de drivers, servidores, etc., donde la
performance y otros detalles finos deban ser controlados más directamente por el programa.
Como en la entrada/salida standard, para manejar archivos tenemos funciones para E/S de caracteres,
de líneas y con formato.
126
CAPÍTULO 12. E/S SOBRE ARCHIVOS
12.3. FUNCIONES ANSI C DE CARACTERES SOBRE ARCHIVOS
#include <stdio.h>
main()
{
FILE *fp1, *fp2;
int a;
if(((fp1 = fopen("ejemplo.txt","r")) == NULL) ||
((fp2 = fopen("copia.txt","w")) == NULL))
exit(1);
while((a = fgetc(fp1)) != EOF)
fputc(a, fp2);
fclose(fp1);
fclose(fp2);
}
Hay otras funciones dentro de esta categoría, como ungetc(), que devuelve un carácter al flujo de
donde se leyó.
char area[BUFSIZ];
...
while(fgets(area, BUFSIZ, fp1) != NULL)
fputs(area, fp2);
Ejemplo 12.1
#include <stdio.h>
main()
{
FILE *fp1, *fp2;
int a; long b;
if(((fp1 = fopen("ejemplo.txt","r")) == NULL) ||
((fp2 = fopen("copia.txt","w")) == NULL))
exit(1);
127
12.6. FUNCIONES ANSI C DE ACCESO DIRECTO CAPÍTULO 12. E/S SOBRE ARCHIVOS
Ejemplo 12.1
En el ejemplo suponemos que se han grabado en el archivo varios registros cuyo formato está representado por la estructura
de la variable datos. La función fseek() posiciona el puntero de lectura en el offset 10 * sizeof(...) (que debe ser un
long), significando que necesitamos acceder al registro lógico número 10 del archivo. A continuación se leen tantos bytes
como mide un elemento de datos.
Cambiando el tercer argumento de fread() podemos leer en un solo acceso un vector completo de estas estructuras en
lugar de un elemento individualmente. Luego de cambiar un valor del registro se lo vuelve a grabar, esta vez en un offset
distinto (correspondiente al registro lógico 5).
struct registro {
int dato1;
long dato2;
} datos;
...
fseek(fp, 10L * sizeof(struct registro), SEEK_SET);
fread(&datos, sizeof(struct registro), 1, fp);
datos.dato1 = 1;
fseek(fp, 5L * sizeof(struct registro), SEEK_SET);
fwrite(&datos, sizeof(struct registro), 1, fp);
128
CAPÍTULO 12. E/S SOBRE ARCHIVOS 12.7. RESUMEN DE FUNCIONES ANSI C DE E/S
Constantes de posicionamiento
Para posicionar el puntero de lectura/escritura en un offset determinado, existe la función fseek(). El
origen del desplazamiento se expresa con las constantes SEEK_SET, SEEK_END y SEEK_CUR.
La constante SEEK_SET indica que el posicionamiento solicitado debe entenderse como absoluto a
partir del principio del archivo. La constante SEEK_CUR indica posicionamiento a partir del offset actual, y
SEEK_END, a partir del fin del archivo. El offset proporcionado puede ser negativo.
Sincronización de E/S
Una restricción importante de la E/S en ANSI C es que no se pueden mezclar instrucciones de entrada
y de salida sin que intervenga una operación intermedia de posicionamiento. Es decir, una sucesión de
invocaciones a fwrite() puede ser seguida de uno o más fread(), pero únicamente luego de un fseek()
entre ambas. La operación de posicionamiento resincroniza ambos punteros y su ausencia puede hacer que
se recupere basura.
Este posicionamiento puede ser nulo, como por ejemplo en fseek(fp, 0L, SEEK_CUR) que no varía
en absoluto la posición de los punteros, pero realiza la sincronización buscada.
129
12.8. FUNCIONES POSIX DE E/S SOBRE ARCHIVOS CAPÍTULO 12. E/S SOBRE ARCHIVOS
Apertura de archivos
Las funciones POSIX que operan sobre archivos lo hacen a través de descriptores de archivos. Estos
pertenecen a una tabla de archivos abiertos que tiene cada proceso o programa en ejecución y se corres-
ponden con estructuras de datos del sistema operativo para manejar la escritura y la lectura en los archivos
propiamente dichos.
La tabla de archivos abiertos de un proceso se inicia con tres archivos abiertos (correspondientes a los
streams de entrada standard, salida standard y salida standard de errores), que reciben los descriptores
números 0, 1 y 2 respectivamente. Estos archivos son automáticamente provistos por el ambiente donde
se ejecuta el proceso, y al comenzar a ejecutarse los encuentra ya abiertos.
Además de los tres descriptores standard abiertos por defecto, los programas abren otros archivos usando
la función open(). Los archivos que se abran subsiguientemente irán ocupando los siguientes descriptores
de archivo libres en esta tabla, y por lo tanto reciben números de descriptor de 3 en adelante. Cada nueva
operación de apertura exitosa de un archivo devuelve un nuevo descriptor. Este número de descriptor se
utiliza durante el resto de la actividad sobre el archivo.
Tanto las funciones ANSI C para archivos como las funciones POSIX de archivos manejan referencias
obtenidas mediante la apertura y utilizadas durante toda la relación del programa con el archivo, pero las
referencias son diferentes. La referencia al bloque de control utilizada por las funciones ANSI es de tipo
FILE *, mientras que el descriptor de archivo POSIX es un int. Por este motivo no se pueden mezclar las
llamadas a funciones de uno y otro grupo.
Sin embargo, sí es cierto que las estructuras de tipo FILE, referenciadas por un FILE *, como se dijo
antes, se apoyan en funcionalidad aportada por funciones POSIX, y por lo tanto contienen un descriptor
de archivo. Si se tiene un archivo abierto mediante una función POSIX, es posible, dado su descriptor,
obtener directamente el stream bufferizado correspondiente para manipularlo con funciones ANSI. Esto se
logra con la función fdopen().
Flags
Las funciones de E/S POSIX permiten controlar varios aspectos de la entrada/salida sobre dispositivos
o archivos. Existe un conjunto de flags, u opciones, que modifican la conducta de cada operación. Los flags
se expresan como nombres simbólicos para constantes de bits, y se especifican en la apertura del archivo
como segundo argumento de la función open(). Todas las combinaciones válidas de flags pueden expresarse
como un OR de bits (operador |) que acumula las constantes de bits correspondientes. Resumimos los flags
más importantes existentes para este segundo argumento de open() en el Cuadro ??.
Ejemplo 12.1
Este programa copia un archivo ejemplo.txt sobre otro llamado copia.txt, usando funciones POSIX read() y write()
aplicadas a los descriptores obtenidos con open().
Abre el primer archivo en modo de sólo lectura con el flag, u opción, O_RDONLY.
Como necesita escribir sobre el segundo, utiliza el flag O_WRONLY.
Además, para el segundo archivo, especifica otros flags que van agregados al primero, y que son O_CREAT (si no existe,
se lo crea) y O_TRUNC (si ya existe, se borran todos sus contenidos).
130
CAPÍTULO 12. E/S SOBRE ARCHIVOS 12.8. FUNCIONES POSIX DE E/S SOBRE ARCHIVOS
#include <unistd.h>
#include <fcntl.h>
main()
{
char area[1024];
int fd1, fd2, bytes;
if((fd1 = open("ejemplo.txt", O_RDONLY)) < 0)
exit(1);
if((fd2 = open("copia.txt", O_WRONLY|O_CREAT|O_TRUNC, 0660)) < 0)
exit(1);
while(bytes = read(fd1, area, sizeof(area)))
write(fd2, area, bytes);
close(fd1);
close(fd2);
}
Ejemplo 12.2
Si el archivo prueba.dat no existe, se lo crea; se abre para lectura y escritura y con todos los permisos para el creador, pero
sólo con permiso de lectura para el grupo del dueño. El resto de los usuarios no tiene ningún permiso sobre el archivo.
131
12.9. EJERCICIOS CAPÍTULO 12. E/S SOBRE ARCHIVOS
Posicionamiento en archivos
Para posicionar el puntero de lectura/escritura en un offset determinado, existe la función lseek(). El
origen del desplazamiento se expresa, como en las funciones ANSI C de acceso directo, con las constantes
SEEK_SET, SEEK_END y SEEK_CUR (ver ??).
Ejemplo 12.3
Repetimos el ejemplo dado para las funciones ANSI donde se lee el registro lógico número 10 y tras una modificación se lo
copia en el registro lógico 5, esta vez con funciones POSIX.
12.9 Ejercicios
1. Escribir una función que copie la entrada en la salida pero eliminando las vocales.
2. Escribir una función que reemplace los caracteres no imprimibles por caracteres punto.
5. Construir un programa que cuente la cantidad de palabras de un archivo, separadas por blancos,
tabuladores o fin de línea.
7. Construir un programa que permita eliminar de un archivo las líneas que contengan una cadena dada.
8. Escribir una función que reciba como argumento dos enteros y devuelva un string de formato conte-
niendo una máscara de formato apropiada para imprimir un número en punto flotante. Por ejemplo,
si se le dan como argumentos 7 y 2, deberá devolver el string " %7.2f". Aplicar la función para
imprimir números en punto flotante.
9. Escribir sobre un archivo una variable int con valor 1 y una variable long con valor 2. Hacerlo
primero con funciones de E/S con formato, y luego con funciones de acceso directo. Examinar en
cada caso el resultado visualizando el archivo y opcionalmente con un comando como od -bc.
10. (*) Defina una estructura básica simple para un registro, a su gusto. Puede ser un registro de informa-
ción personal, bibliográfica, etc. Construya funciones para leer estos datos del teclado e imprimirlos
en pantalla. Luego, usando funciones ANSI C, construya funciones para leer y escribir una de estas
estructuras en un archivo, dado un número de registro lógico determinado.
11. (*) Repita el ejercicio anterior reemplazando las funciones ANSI C por funciones POSIX.
132
CAPÍTULO 12. E/S SOBRE ARCHIVOS 12.9. EJERCICIOS
12. Construya programas que utilicen las funciones anteriores, ANSI C o POSIX, y ofrezcan un menú de
operaciones de administración: cargar un dato en el archivo, imprimir los datos contenidos en una
posición determinada, listar la base generada completa, eliminar un registro, etc.
13. Construir una función que pida por teclado datos personales (nombre, edad, ...) y los almacene en
una estructura. Construir una función que imprima los valores recogidos por la función anterior.
14. Construir un programa que lea una cantidad de datos en lote y luego los imprima utilizando las
funciones del ejercicio anterior. Generar un archivo de datos usando redirección.
15. Realizar el mismo programa del punto anterior pero efectuando toda la E/S sobre archivos. El pro-
grama deberá poder leer el archivo de datos del punto anterior.
16. Construir un programa que organice la salida de la función anterior para obtener un clon del comando
od de UNIX.
17. Construir un programa que lea el listado de un directorio en formato largo (la salida del comando
ls -l) y devuelva la cantidad total de bytes ocupados.
18. Construya programas que utilicen las funciones ANSI C o POSIX de los ejercicios anteriores marcados
con un asterisco, y que ofrezcan un menú de operaciones de administración: cargar un dato en el
archivo, imprimir los datos contenidos en una posición determinada, listar la base generada completa,
eliminar un registro, etc.
133
12.9. EJERCICIOS CAPÍTULO 12. E/S SOBRE ARCHIVOS
134
Capítulo 13
Entendemos por comunicación con el ambiente todas aquellas formas posibles de intercambiar datos
entre el programa y el entorno, ya sea el sistema operativo, el shell del usuario, u otro programa que lo
haya lanzado. Una necesidad evidente de comunicación será recibir parámetros, argumentos u opciones de
trabajo. Otras necesidades serán generar archivos con resultados, o comunicar una condición de error a la
entidad que puso en marcha el programa.
135
13.2. VARIABLES DE AMBIENTE CAPÍTULO 13. COMUNICACIÓN CON EL AMBIENTE
Para poder aprovechar estas capacidades solamente se requiere un protocolo común entre los programas
que se comunicarán. Un medio para lograrlo, en aquellos programas que no son naturalmente cooperativos,
es a veces construir adaptadores a nivel de shell. Estos son scripts generalmente sencillos que transforman
un formato de datos en otro, facilitando la flexibilidad que no da el C por tratarse de un lenguaje compilado.
Los scripts, siendo interpretados, pueden ejecutarse directamente sin compilación. Pueden modificarse
y probarse más rápidamente que los programas compilables, y la programación suele ser más flexible y
poderosa. El costo asociado con el scripting es una menor velocidad de ejecución, lo que propone un
estudio de cada caso, para optar entre scripting o programación ad hoc. Ambientes como el moderno
UNIX ofrecen numerosas herramientas y varios intérpretes de lenguajes de scripting, cada cual con mayores
ventajas en un área determinada. Herramientas que es útil conocer son grep, sed, diff, comm, etc. El
shell de usuario es normalmente una buena elección para scripting de tareas simples, poseyendo un lenguaje
completo con manejo de variables, estructuras de control, arreglos, etc. Sin embargo, otros como awk,
Perl o Python tienen mejores capacidades de manejo de cadenas, esencial para el trabajo que describimos,
además de una sintaxis sumamente sintética y poderosa.
Ejemplo 13.1
Estos comandos a nivel de shell colocan una variable y su valor en el ambiente. El comando export la hace visible a los
procesos hijos.
$ DIR=/usr/local/programa
$ export DIR
char directorio[50];
strcpy(directorio, getenv("DIR"));
136
CAPÍTULO 13. COMUNICACIÓN CON EL AMBIENTE 13.4. SALIDA DEL PROGRAMA
Los elementos del segundo parámetro son punteros a cadenas, terminadas en ’\0’, representando
cada argumento recibido (incluyendo el nombre del programa).
Ejemplo 13.1
El primer parámetro representa la cantidad total de argumentos en la línea de comandos, incluido el nombre del programa.
$ programa Alicia 26
Nombre: Alicia Edad: 26
Ejemplo 13.1
137
13.5. OPCIONES CAPÍTULO 13. COMUNICACIÓN CON EL AMBIENTE
13.5 Opciones
Es muy común encontrar comandos del sistema operativo que aceptan un conjunto, a veces muy vasto,
de opciones. Las opciones, si están presentes, se reconocen por comenzar con guiones, y deben ser los
primeros argumentos dados al programa.
La convención usual en UNIX de expresar las opciones con un signo guión y letras, y opcionalmente
argumentos numéricos, ha llevado a definir funciones de Biblioteca Standard para manejar conjuntos de
opciones.
Ejemplo 13.1
#include <getopt.h>
#include <unistd.h>
extern char *optarg;
extern int optind, opterr, optopt;
int debug;
opterr=0;
while((c=getopt(argc, argv, optstring)) != EOF)
switch(c) {
case ’v’:
case ’V’:
debug=atoi(optarg);
printf("Nivel de debugging: %d\n",debug);
break;
case ’:’:
printf("Falta valor numérico\n");
exit(1);
break;
case ’R’:
case ’r’:
printf("Recibiendo\n");
recibir(argv[optind]);
break;
case ’T’:
case ’t’:
printf("Transmitiendo\n");
transmitir(argv[optind]);
break;
case ’?’:
printf("Mal argumento\n");
break;
}
El programa podría usarse tanto para transmitir como para recibir archivos, observando un nivel de salida de debugging
conveniente. Podría invocarse como:
$ transferir -v 2 -T archivo.txt
La función getopt() es quien va recogiendo las opciones vistas en la línea de comandos y devolviéndolas
138
CAPÍTULO 13. COMUNICACIÓN CON EL AMBIENTE 13.6. EJERCICIOS
como caracteres separados. La variable string optstring contiene las opciones válidas. Para aquellas
opciones (como V en el ejemplo) que pueden asumir un modificador numérico, se ubica un símbolo “dos
puntos” a continuación en el string optstring. El valor para la opción numérica se recibe en la variable
optarg.
Si ocurre un error sintáctico en el procesamiento de las opciones, la rutina devuelve el carácter ’?’ y
emite un mensaje de error por salida de errores standard. Si no se desea emitir este mensaje, se hace
opterr=0. Las funciones recibir() y transmitir() obtienen el nombre del archivo del arreglo de
argumentos argv[], indexándolo con la variable optind, que queda apuntando al siguiente elemento
en la línea de comandos.
13.6 Ejercicios
1. Escribir un programa que imprima una secuencia de números consecutivos donde el valor inicial, el
valor final y el incremento son dados como argumentos.
2. Mismo ejercicio pero donde los parámetros son pasados como variables de ambiente.
3. Mismo ejercicio pero donde los parámetros son pasados como opciones.
4. Programar una calculadora capaz de resolver cálculos simples como los siguientes:
$ casio 3 + 5
8
$ casio 20 * 6
120
$ casio 5 / 3
1
$ casio -d2 5 / 3
1.66
6. Manteniendo la capacidad anterior, agregar la posibilidad de leer una variable de ambiente que
establezca la precisión default. Si no se da la precisión como opción, se tomará la establecida por la
variable de ambiente, pero si se la especifica, ésta será la adoptada. Si no hay definida una precisión
se tomará 0. Ejemplo:
$ casio 10 / 7
1
$ PRECISION_CASIO=5
$ export PRECISION_CASIO
$ casio 10 / 7
1.42857
$ casio -d2 10 / 7
1.42
139
13.6. EJERCICIOS CAPÍTULO 13. COMUNICACIÓN CON EL AMBIENTE
práctica) puede recibir opciones o variables de ambiente especificando cuáles serán los separadores
entre palabras.
140
Capítulo 14
La Biblioteca Standard
La Biblioteca Standard no forma parte del lenguaje C, estrictamente hablando, pero todos los com-
piladores contienen una implementación de ella, a veces con agregados o pequeñas variantes. Desde la
oficialización del ANSI C, los contenidos de la Biblioteca Standard se han estabilizado y se puede contar
con el mismo conjunto de funciones en todas las plataformas.
Las funciones de la Biblioteca Standard están agrupadas en varias categorías, cada una representada en
un header, según el Cuadro ??. Una implementación de C puede, sin embargo, aportar muchísimos otros
headers, más específicos.
Para poder utilizar cualquiera de las funciones de cada categoría, es necesario incluir en el fuente
el header asociado con la categoría. Esto no implica incluir los textos de las funciones en la unidad de
traducción, sino simplemente incorporar los prototipos de las funciones de la biblioteca. Es decir, incluir un
header de Biblioteca Standard no define las funciones que se van a usar, sino que las declara. La resolución
de las referencias a las funciones o variables de Biblioteca Standard quedan pendientes hasta la linkedición.
Ya hemos visitado la mayoría de las funciones de la categoría de funciones de E/S. Nos quedan por
explorar algunas otras de importancia. Con esta información no pretendemos reemplazar al manual del
compilador, sino orientar a los primeros pasos en el uso de la Biblioteca Standard.
141
14.2. LISTAS DE ARGUMENTOS VARIABLES CAPÍTULO 14. LA BIBLIOTECA STANDARD
Categoría Header
Macros de diagnóstico de errores <assert.h>
Macros de clasificación de caracteres <ctype.h>
Variables y funciones relacionadas con condiciones de error <errno.h>
Características de la representación en punto flotante <float.h>
Rangos de tipos de datos, dependientes de la plataforma <limits.h>
Definiciones relacionadas con el idioma y lugar de uso <locale.h>
Funciones matemáticas <math.h>
Saltos no locales <setjmp.h>
Manejo de señales <signal.h>
Listas de argumentos variables <stdarg.h>
Definiciones de algunos tipos y constantes comunes <stddef.h>
Entrada/salida <stdio.h>
Varias funciones útiles <stdlib.h>
Operaciones sobre cadenas <string.h>
Funciones de fecha y hora <time.h>
142
CAPÍTULO 14. LA BIBLIOTECA STANDARD 14.3. TRATAMIENTO DE ERRORES
Ejemplo 14.1
#include <stdarg.h>
int sumar(int cuantos, ...)
{
va_list ap;
int suma=0;
va_start(ap, cuantos);
for(i=0; i<cuantos; i++)
suma += va_arg(ap, int);
va_end(ap);
return suma;
}
main()
{
printf("Resultado 1: %d\n", sumar(3, 4, 5, 6));
printf("Resultado 2: %d\n", sumar(2, 100, 2336));
}
Ejemplo 14.1
#include <errno.h>
if(open("noexiste", O_RDONLY) < 0)
perror("Error en apertura");
La macro assert() sirve para detener la ejecución cuando se alcanza un estado imposible para la lógica
del programa. Para usarla adecuadamente es necesario identificar invariantes en el programa (condiciones
que jamás deban ser falsas). Si al evaluarse la macro resulta que su condición argumento es falsa, assert()
aborta el programa indicando nombre del archivo fuente y línea donde estaba originalmente la llamada. Un
programa en producción no debería fallar debido a assert().
Ejemplo 14.2
#include <assert.h>
...
assert(restantes >= 0);
143
14.4. FUNCIONES DE FECHA Y HORA CAPÍTULO 14. LA BIBLIOTECA STANDARD
struct tm {
int tm_sec, /* segundos 0..59 */
tm_min, /* minutos 0..59 */
tm_hour, /* horas 0..23 */
tm_mday, /* día del mes 1..31 */
tm_mon, /* meses desde enero 0..11 */
tm_year, /* años desde 1900 */
tm_wday, /* días desde el domingo 0..6 */
tm_yday, /* días desde enero 0..365 */
tm_isdst; /* flag de ahorro diurno de luz */
};
Por otro lado, existe un segundo formato de representación interna de fechas, time_t, que es simple-
mente un entero conteniendo la cantidad de segundos desde el principio de la era UNIX (“the epoch”),
acaecido el 1/1/1970 a la hora 0 UTC. Este formato es el usado por la función time() que da la hora
actual. Este formato entero puede convertirse a struct tm y viceversa con funciones definidas en esta
zona de la BS, como mktime() y gmtime().
El contenido de una estructura tm se puede imprimir en una gran variedad de formatos, con la función
strftime(), que acepta una cantidad de especificaciones al estilo de printf(). Las funciones ctime() y
asctime() son más sencillas. Devuelven una cadena conteniendo una fecha en formato normalizado (como
el que aparece en los mensajes de correo electrónico). La primera recibe un puntero a time_t; la segunda,
un puntero a struct tm.
Ejemplo 14.1
time_t t;
struct tm *stm;
char area[100];
stm = gmtime(&t); /* convierte t a struct tm */
strftime(area,sizeof(area)," %A %b %d %H",stm); /* prepara string según formato del usuario */
printf(" %s\n",area); /* lo imprime */
Una versión de precisión limitada, donde se agrega una f al nombre de la función, que recibe argu-
mentos float, y devuelve un valor float. Por ejemplo, sinf(x).
144
CAPÍTULO 14. LA BIBLIOTECA STANDARD 14.6. FUNCIONES UTILITARIAS
Una versión de precisión extendida, donde se agrega una l al nombre de la función, que recibe
argumentos long double, y devuelve un valor long double. Por ejemplo, sinl(x).
En el Cuadro ?? se enumeran las versiones básicas. Aunque sólo se ejemplifica para sin(x), todas las
funciones matemáticas de la Biblioteca Standard presentan las tres versiones. Como siempre, se recomienda
consultar los manuales de estas funciones.
Para vincular la biblioteca matemática con un programa es necesario incluir la directiva de compilación
-lm.
Funciones de conversión: las funciones atoi(), atol(), atof(), strtol(), strtod(), strtoul(),
toman cadenas representando números y generan los elementos de datos del tipo correspondiente.
145
14.7. CLASIFICACIÓN DE CARACTERES CAPÍTULO 14. LA BIBLIOTECA STANDARD
Macro Devuelve
isalnum(c) TRUE cuando isalpha(c) o isdigit(c) son TRUE
isalpha(c) TRUE cuando isupper(c) o islower(c) son TRUE
iscntrl(c) TRUE cuando c es un carácter de control
isdigit(c) TRUE cuando c es un dígito decimal
isgraph(c) TRUE cuando c es un carácter imprimible y no isspace(c)
islower(c) TRUE cuando c es una letra minúscula
isprint(c) TRUE cuando c es un carácter imprimible,
incluyendo el caso en que isspace(c) es TRUE
ispunct(c) TRUE cuando c es imprimible pero no espacio, letra ni dígito
isspace(c) TRUE cuando c es espacio, fin de línea, tabulador
isupper(c) TRUE cuando c es letra mayúscula
isxdigit(c) TRUE cuando c es dígito hexadecimal
tolower(c) c en minúscula
toupper(c) c en mayúscula
Aquí también se declaran las funciones de asignación de memoria como malloc(), calloc(),
realloc(), free().
Para manejar datos en memoria con eficiencia se puede recurrir a qsort() y bsearch(), que ordenan
una tabla y realizan búsqueda binaria en la tabla ordenada.
14.8 Ejercicios
1. Utilizar la función de cantidad variable de argumentos definida más arriba para obtener los promedios
de los 2, 3, ..., n primeros elementos de un arreglo.
2. Construir una función de lista variable de argumentos que efectúe la concatenación de una cantidad
arbitraria de cadenas en una zona de memoria provista por la función que llama.
3. Construir una función de cantidad variable de argumentos que sirva para imprimir, con un formato
especificado, mensajes de debugging, conteniendo nombres y valores de variables.
4. Construir un programa que separe la entrada standard en palabras, usando las macros de clasificación
de caracteres. Debe considerar como delimitadores a los caracteres espacio, tabulador, signos de
puntuación, etc.
5. Dadas dos fechas y horas del día, calcular su diferencia. Utilizar las funciones de BS para convertir a
tipos de datos convenientes e imprimir la diferencia en años, meses, días, horas, etc.
146