Capitulo 1: Introduccion General
Comencemos con una introduccion rapida a C. Nuestro objetivo es mostrar los elementos esenciales del lenguaje en programas reales, pero sin perdernos en detalles, reglas o excepciones. Por el momento, no intentamos ser completos ni precisos (exceptuando en los ejemplos, que si lo son). Deseamos llevarlo tan rapido com o sea posible al punto en donde pueda escribir programas utiles, y para hacerlo tenemos que concentrarnos en las bases: variables y constantes, aritmetica, control de flujo, funciones y los rudimentos de entrada y salida. Hemos dejado intencionalmente fuera de este capitulo las caracteristicas de C que son importantes para escribir programas mas grandes. Esas caracteristicas incluyen apuntadores, estructuras, la mayor parte del rico conjunto de operadores de C, varias proposiciones para control de flujo y la biblioteca estandar.
Este enfoque tiene sus inconvenientes. Lo mas notorio es que aqui no se encuentra la descripcion completa de ninguna caracteristica particular del lenguaje, y la introduccion, por su brevedad, puede tambien resultar confusa. Y debido a que los ejemplos no utilizan la potencia completa de C, no son tan concisos y elegantes como podrian serlo. Hemos tratado de aminorar esos efectos, pero tenga cuidado. Otro inconveniente es que los capitulos posteriores necesariamente repetiran algo de lo expuesto en este. Esperamos que la repeticion, mas que molestar, ayude.
En cualquier caso, los programadores con experiencia deben ser capaces de extrapolar del material que se encuentra en este capitulo a sus propias necesidades de programacion. Los principiantes deben complementarlo escribiendo pequenos programas semejantes a los aqui expuestos. Ambos grupos pueden utilizar este capitulo como un marco de referencia sobre el cual asociar las descripciones mas detalladas que comienzan en el capitulo 2.
1.1 Comencemos
La unica forma de aprender un nuevo lenguaje de programacion es escribiendo programas con el. El primer programa por escribir es el mismo para todos los lenguajes:
Imprime las palabras
Viva Peron!
Este es el gran obstaculo; para librarlo debe tener la habilidad de crear el texto del programa de alguna manera, compilarlo con exito, cargarlo, ejecutarlo y descubrir a donde fue la salida. Con el dominio de estos detalles mecanicos, todo lo demas es relativamente facil.
En C, el programa para escribir "Viva Peron!
" es
#include <stdio.h>
main()
(
printf("Viva Peron!\n");
}
La forma de ejecutar este programa dependera del sistema que se este
utilizando. Como un ejemplo especifico, en el sistema operativo UNIX se
debe crear el programa en un archivo cuyo nombre termine con
".c
" , como vivaperon.c
, y despues compilarlo
con la orden
cc vivaperon.c
Si no se ha cometido algun error, como la omision de un caracter o
escribir algo en forma incorrecta, la compilacion se hara sin emitir
mensaje alguno, y creara un archivo ejecutable llamado
a.out
. Si se ejecuta a.out
escribiendo la
orden
a.out
se presentara:
Viva Peron!
En otros sistemas, las reglas seran diferentes, consultelo con un experto.
Ahora algunas explicaciones acerca del programa en si. Un programa en
C, cualquiera que sea su tamano, consta de funciones y
variables. Una funcion contiene proposiciones que
especifican las operaciones de calculo que se van a realizar, y las
variables almacenan los valores utilizados durante los calculos. Las
funciones de C son semejantes a las subrutinas y funciones de Fortran o
a los procedimientos y funciones de Pascal. Nuestro ejemplo es una
funcion llamada main
. Normalmente se tiene la libertad de
dar cualquier nombre que se desee, pero “main
” es especial
— el programa comienza a ejecutarse al principio de main
.
Esto significa que todo programa debe tener un main
en
algun sitio.
Por lo comun main
llamara a otras funciones que ayuden a
realizar su trabajo, algunas que usted ya escribio, y otras de
bibliotecas escritas previamente. La primera linea del programa.
#include <stdio.h>
indica al compilador que debe incluir informacion acerca de la biblioteca estandar de entrada/salida; esta linea aparece al principio de muchos archivos de codigo fuente de lenguaje C. La biblioteca estandar esta descrita en el capitulo 7 y en el apendice B.
Un metodo para comunicar datos entre las funciones es que la funcion
que llama proporcione una lista de valores, llamados
argumentos, a la funcion que esta invocando. Los parentesis que
estan despues del nombre de la funcion encierran a la lista de
argumentos. En este ejemplo, main
esta definido para ser
una funcion que no espera argumentos, lo cual esta indicado por la lista
vacia ()
.
El primer programa en C
#include <stdio.h> /* Incluye informacion acerca de la biblioteca estandar */
main() /* Define una funcion llamada main que no recibe valores de argumentos */
{ /* Las proposiciones de main estan encerradas entre llaves */
printf("Viva Peron!\n"); /* main llama a la funcion de biblioteca printf para escribir esta secuencia de caracteres; \n representa el caracter nueva linea. */
}
Las proposiciones de una funcion estan encerradas
entre llaves {...}
. La funcion main
solo
contiene una proposicion, printf ("Viva Peron!\n");
. Una
funcion se invoca al nombrarla, seguida de una lista de argumentos entre
parentesis; de esta manera se esta llamando a la funcion
printf
con el argumento "Viva Peron!\n"
.
printf
es una funcion de biblioteca que presenta la salida,
en este caso la cadena de caracteres que se encuentra entre comillas. A
una secuencia de caracteres entre comillas, como
"Viva Peron!\n"
, se le llama cadena de caracteres
o constante de cadena. Por el momento, nuestro unico uso de
cadenas de caracteres sera como argumentos para printf
y
otras funciones.
La secuencia \n
en la cadena representa el caracter
nueva linea en la notacion de C, y hace avanzar la impresion al
margen izquierdo de la siguiente linea. Si se omite el \n
(un experimento que vale la pena hacer), encontrara que no hay avance de
linea despues de la impresion. Se debe utilizar \n
para
incluir un caracter de nueva linea en el argumento de
printf
; si se intenta algo como
printf("Viva Peron!
");
...el compilador de C producira un mensaje de error.
printf
nunca proporciona una nueva linea
automaticamente, de manera que se pueden utilizar varias llamadas para
construir una linea de salida en etapas.
Nuestro primer programa tambien pudo haber sido escrito de la siguiente manera:
#include <stdio.h>
main()
{
printf ("Viva");
printf ("Peron!");
printf ("\n");
}
...produciendose una salida identica.
Notese que \n
representa un solo caracter. Una secuencia
de escape como \n
proporciona un mecanismo general y
extensible para representar caracteres invisibles o dificiles de
escribir. Entre otros que C proporciona estan \t
para
tabulacion, \b
para retroceso, \"
para
comillas, y \\
para la barra diagonal invertida. Hay una
lista completa en la seccion 2.3.
- Ejercicio 1-1. Ejecute el programa "vivaperon" en su sistema. Experimente con la omision de partes del programa, para ver que mensajes de error se obtienen. □
- Ejercicio 1-2. Experimente el descubrir que pasa
cuando la cadena del argumento de
printf
contiene\c
, en dondec
es algun caracter no puesto en lista anteriormente. □
1.2 Variables y Expresiones Aritmeticas
El siguiente programa utiliza la formula °C =(5/9) x (°F-32) para imprimir la siguiente tabla de temperaturas Fahrenheit y sus equivalentes centigrados o Celsius:
0 -17
20 -6
40 4
60 15
80 26
100 37
120 38
140 60
160 71
180 82
200 93
220 104
240 115
260 126
280 137
300 148
En si el programa aun consiste de la definicion de una unica funcion
llamada main
. Es mas largo que el que imprime
Viva peron!
, pero no es complicado. Introduce varias ideas
nuevas, incluyendo comentarios, declaraciones, variables, expresiones
aritmeticas, ciclos y salida con formato.
#include < std io .h >
/* imprime Tabla Fahrenheit-Celsius para fahr = 0, 20, ..., 300 */
main()
{
int fahr, celsius;
int inferior, superior, paso;
inferior = 0; /* limite inferior de la tabla de temperaturas */
superior = 300; /* limite superior */
paso = 20; /* tamano del incremento */
fahr = inferior;
while (fahr < = superior) {
celsius = 5 * (fahr-32) / 9;
printf("%d\t%d\n", fahr, celsius);
fahr = fahr + paso;
}
}
Las dos lineas
/* imprime la tabla Fahrenheit-Celsius
para fahr = 0, 20, ..., 300 */
...son un comentario, que en este caso explica brevemente lo
que hace el programa. Cualesquier caracteres entre /*
y
*/
son ignorados por el compilador, y pueden ser utilizados
libremente para hacer a un programa mas facil de entender. Los
comentarios pueden aparecer en cualquier lugar donde puede colocarse un
espacio en blanco, un tabulador o nueva linea.
En C, se deben declarar todas las variables antes de su uso, generalmente al principio de la funcion y antes de cualquier proposicion ejecutable. Una declaracion notifica las propiedades de una variable; consta de un nombre de tipo y una lista de variables, como
int fahr, celsius;
int inferior, superior, paso;
El tipo int
significa que las variables de la lista son
enteros, en contraste con float
, que significa punto
flotante, esto es, numeros que pueden tener una parte fraccionaria. El
rango tanto de int
como de float
depende de la
maquina que se esta utilizando; los int
de 16 bits, que
estan comprendidos entre el -32768
y +32767
,
son comunes, como lo son los int
de 32 bits. Un numero
float
tipicamente es de 32 bits, por lo menos con seis
digitos significativos y una magnitud generalmente entre 10 a la -38 y
10 a la +38.
Ademas de int
y float
, C proporciona varios
tipos de datos basicos, incluyendo:
char |
caracter —un solo byte |
short |
entero corto |
long |
entero largo |
double |
punto flotante de doble precision |
Los tamanos de estos objetos tambien dependen de la maquina. Tambien existen arreglos, estructuras y uniones de estos tipos basicos, apuntadores a ellos y funciones que regresan valores con esos tipos, todo lo cual se vera en el momento oportuno.
Los calculos en el programa de conversion de temperaturas principian con las proposiciones de asignacion.
inferior = 0 ;
superior = 300;
paso = 20 ;
...que asignan a las variables sus valores iniciales. Las
proposiciones individuales se terminan con punto y coma
;
.
Cada linea de la tabla se calcula de la misma manera por lo que se
utiliza una iteracion que se repite una vez por cada linea de salida;
este es el proposito del ciclo while
:
while (fahr <= superior) {
...
}
El ciclo while
funciona de la siguiente manera: se
prueba la condicion entre parentesis. De ser verdadera (si
fahr
es menor o igual que superior
), el cuerpo
del ciclo (las tres proposiciones entre llaves) se ejecuta. Luego la
condicion se prueba nuevamente, y si es verdadera, el cuerpo se ejecuta
de nuevo. Cuando la prueba resulta falsa (fahr
excede a
superior
) la iteracion termina, y la ejecucion continua en
la proposicion que sigue al ciclo. No existe ninguna otra proposicion en
este programa, de modo que termina.
El cuerpo de un while
puede tener una o mas
proposiciones encerradas entre llaves, como en el convertidor de
temperaturas, o una sola proposicion sin llaves, como en:
while (i < j)
i = 2 + i;
En cualquier caso, siempre se sangra la proposicion controlada por el
while
con una tabulacion (lo que se presenta aqui a cuatro
espacios) para poder apreciar de un vistazo cuales proposiciones estan
circunscriptas dentro del ciclo. El sangrado enfatiza la
estructura logica del programa. Aunque a los compiladores de C no les
importa la apariencia del programa, un sangrado y espaciamiento
adecuados son muy importantes para hacer programas faciles de leer.
Recomendamos escribir una sola proposicion por linea y utilizar espacios
en blanco alrededor de los operadores para dar claridad al agrupamiento.
La posicion de las llaves es menos importante, aunque los programadores
del pueblo sostienen credos pasionales al respecto. Se eligio uno de los
varios estilos populares. Escoja un estilo que le satisfaga y sea
consistente en su uso.
La mayor parte del trabajo se realiza en el cuerpo del ciclo. La temperatura Celsius se calcula y se asigna a la variable celsius por la proposicion.
celsius = 5 * (fahr—32) / 9;
La razon de multiplicar por 5 y despues dividir entre 9 en lugar de solamente multiplicar por 5/9 es que en C - como en muchos otros lenguajes - la division de enteros trunca el resultado: cualquier parte fraccionaria se descarta. Puesto que 5 y 9 son enteros, 5/9 seria truncado a cero y asi todas las temperaturas Celsius se reportarian como cero.
Este ejemplo tambien muestra un poco mas acerca de como funciona
printf
. En realidad, printf
es una funcion de
proposito general para dar formato de salida, que se describira con
detalle en el capitulo 7. Su primer
argumento es una cadena de caracteres que seran impresos, con cada
%
indicando en donde uno de los otros (segundo, tercero,
...) argumentos va a ser sustituido, y en que forma sera impreso. Por
ejemplo, %d
especifica un argumento entero, de modo que la
proposicion
printf("%d\t%d\n", fahr, celsius);
hace que los valores de los dos enteros fahr
y
celsius
sean escritos, con una tabulacion (\t
)
entre ellos.
Cada construccion %
en el primer argumento de
printf
esta asociada con el correspondiente segundo
argumento, tercero, etc., y deben corresponder apropiadamente en numero
y tipo, o se tendran soluciones incorrectas.
Con relacion a esto, printf
no es parte del lenguaje C;
no existe propiamente una entrada o salida definida en C.
printf
es solo una util funcion de la biblioteca estandar
de funciones que esta accesible normalmente a los programas en C. Sin
embargo, el comportamiento de printf
esta definido en el
estandar ANSI, por lo que sus propiedades deben ser las mismas en
cualquier compilador o biblioteca que se apegue a el.
Para concentrarnos en C, no hablaremos mucho acerca de la entrada y
la salida hasta el capitulo 7. En
particular, pospondremos el tema de la entrada con formato hasta
entonces. Si se tiene que darle entrada a numeros, lease la discusion de
la funcion scanf en la seccion
7.4. La funcion scanf
es como printf
,
exceptuando que lee de la entrada en lugar de escribir a la salida.
Existen un par de problemas con el programa de conversion de temperaturas.
El mas simple es que la salida no es muy estetica debido a que los
numeros no estan justificados hacia su derecha. Esto es facil de
corregir; si aumentamos a cada %d
de la proposicion
printf
una amplitud, los numeros impresos seran
justificados hacia su derecha dentro de sus campos. Por ejemplo, podria
decirse:
printf("%3d %6d\n", fahr, celsius);
para escribir el primer numero de cada linea en un campo de tres digitos de ancho, y el segundo en un campo de seis digitos, como esto:
0 -17
20 -6
40 4
60 15
80 26
100 37
...
El problema mas grave es que debido a que se ha utilizado aritmetica de enteros, las temperaturas Celsius no son muy precisas; por ejemplo, 0ºF es en realidad aproximadamente —17.8°C, no —17ºC. Para obtener soluciones mas precisas, se debe utilizar aritmetica de punto flotante en lugar de entera. Esto requiere de algunos cambios en el programa. Aqui esta una segunda version:
#include <stdio.h>
/* imprime la tabla Fahrenheit-Celsius
para fahr = 0, 20, ..., 300; version de coma flotante */
main()
{
float fahr, celsius;
float inferior, superior, paso;
inferior = 0; /* limite inferior de escala de temperatura */
superior = 300; /* limite superior */
paso = 20; /* tamano de paso */
fahr = inferior;
while (fahr <= superior) {
celsius = (5.0/9.0) * (fahr-32.0);
printf("%3.0f %6.1f\n", fahr, celsius);
fahr = fahr + paso;
}
}
Esto es muy semejante a lo anterior, excepto que fahr
y
celsius
estan declarados como float
, y la
formula de conversion esta escrita en una forma mas natural. No pudimos
utilizar 5/9
en la version anterior debido a que la
division entera lo truncaria a cero. Sin embargo, un punto decimal en
una constante indica que esta es de punto flotante, por lo que
5.0/9.0
no se trunca debido a que es una relacion de dos
valores de punto flotante.
Si un operador aritmetico tiene operandos enteros, se ejecuta una
operacion entera. Si un operador numerico tiene un operando de punto
flotante y otro entero, este ultimo sera convertido a punto flotante
antes de hacer la operacion. Si se hubiera escrito
(fahr-32)
, el 32 seria convertido automaticamente a punto
flotante. Escribir constantes de punto flotante con puntos decimales
explicitos, aun cuando tengan valores enteros, destaca su naturaleza de
punto flotante para los lectores humanos.
Las reglas detalladas de cuando los enteros se convierten a punto flotante se encuentran en el capitulo 2. Por ahora, notese que la asignacion
fahr = inferior;
y la prueba
while (fahr <= superior)
tambien trabajan en la forma natural (el int
se
convierte a float
antes de efectuarse la operacion).
La especificacion de conversion %3.0f
del
printf
indica que se escribira un numero de punto flotante
(en este caso fahr
) por lo menos con tres caracteres de
ancho, sin punto decimal y sin digitos fraccionarios; %6.1f
describe a otro numero (celsius
) que se escribira en una
amplitud de por lo menos 6 caracteres, con 1 digito despues del punto
decimal. La salida tendra el siguiente aspecto:
0 -17.8
20 -6.7
40 4.4
...
La amplitud y la precision pueden omitirse de una especificacion:
%6f
indica que el numero es por lo menos de seis caracteres
de ancho; %.2f
indica dos caracteres despues del punto
decimal, pero el ancho no esta restringido; y %f
unicamente
indica escribir el numero como punto flotante.
%d |
Escribe como entero decimal |
%6d |
escribe como entero decimal, por lo menos con 6 caracteres de amplitud. |
%f |
escribe como punto flotante |
%6f |
escribe como punto flotante, por lo menos con 6 caracteres de amplitud |
%.2f |
escribe como punto flotante, con 2 caracteres despues del punto decimal. |
%6.2f |
escribe como punto flotante, por lo menos con 6 caracteres, y con dos caracteres despues del punto decimal. |
Entre otros, printf
tambien reconoce %o
para octal, %x
para hexadecimal, %c
para
caracter, %s
para cadena de caracteres y %%
para denotar el caracter %
en si.
-
Ejercicio 1-3. Modifique el programa de conversion de temperaturas de modo que escriba un encabezado sobre la tabla. □
-
Ejercicio 1-4. Escriba un programa que imprima la tabla correspondiente Celsius a Fahrenheit. □
1.3 La proposicion for
Existen suficientes formas distintas de escribir un programa para una tarea en particular. Intentemos una variacion del programa de conversion detem peraturas.
#include <stdio.h>
/* imprime la tabla Fahrenheit-Celsius */
main()
{
int fahr;
for (fahr = 0; fahr <= 300; fahr = fahr + 20)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
Este produce los mismos resultados, pero ciertamente se ve diferente.
Un cambio importante es la eliminacion de la mayoria de las variables;
solo permanece fahr
y la hemos convertido en
int
. Los limites inferior y superior y el tamano del paso
solo aparecen como constantes dentro de la proposicion for
,
que es una nueva construccion, y la expresion que calcula la temperatura
Celsius ahora aparece como el tercer argumento de printf
en
vez de una proposicion de asignacion separada.
Este ultimo cambio ejemplifica una regla general — en cualquier
contexto en el que se permita utilizar el valor de una variable de algun
tipo, es posible usar una expresion mas complicada de ese tipo. Puesto
que el tercer argumento de printf
debe ser un valor de
punto flotante para coincidir con %6.1f
, cualquier
expresion de punto flotante puede ocurrir aqui.
La proposicion for
es un ciclo, una forma generalizada
del while
. Si se compara con el while
anterior, su operacion debe ser clara. Dentro de los parentesis existen
tres secciones, separadas por punto y coma ;
. La primera,
la inicializacion
fahr = 0
se ejecuta una vez, antes de entrar propiamente al ciclo. La segunda seccion es la condicion o prueba que controla el ciclo:
fahr <= 300
Esta condicion se evalua; si es verdadera, el cuerpo del ciclo (en
este caso un simple printf
) se ejecuta. Despues el
incremento de avance
fahr = fahr + 20
se ejecuta y la condicion se vuelve a evaluar. El ciclo termina si la
condicion se hace falsa. Tal como con el while
, el cuerpo
del ciclo puede ser una proposicion sencilla o un grupo de proposiciones
encerradas entre llaves. La inicializacion, la condicion y el incremento
pueden ser cualquier expresion.
La seleccion entre while
y for
es
arbitraria, y se basa en aquello que parezca mas claro. El
for
es por lo general apropiado para ciclos en los que la
inicializacion y el incremento son proposiciones sencillas y logicamente
relacionadas, puesto que es mas compacto que el while
y
mantiene reunidas en un lugar a las proposiciones que controlan al
ciclo.
- Ejercicio 1-5. Modifique el programa de conversion de temperaturas de manera que escriba la tabla en orden inverso, esto es, desde 300 grados hasta 0. □
1.4 Constantes simbolicas
Una observacion final antes de dejar definitivamente el tema de la
conversion de temperaturas. Es una mala practica poner “numeros magicos”
tales como 300
y 20
en un programa, ya que le
proporcionan muy poca informacion a quien tenga que leer el programa, y
son dificiles de modificar de forma sistematica. Una manera de tratar a
esos numeros magicos es darles nombres significativos. Una linea
#define
define un nombre simbolico o constante
simbolica como una cadena de caracteres particularmente
especial:
#define nombre texto de reemplazo
A partir de esto, cualquier ocurrencia de nombre (que no este entre comillas ni como parte de otro nombre) se sustituira por el texto de reemplazo correspondiente. El nombre tiene la misma forma que un nombre de variable: una secuencia de letras y digitos que comienza con una letra. El texto de reemplazo puede ser cualquier secuencia de caracteres; no esta limitado a numeros.
#include <stdio.h>
#define INFERIOR 0 /* limite inferior de la tabla »/
#define SUPERIOR 300 /* limite superior */
#define PASO 20 /* tamano del paso de incremento */
/* imprime la tabla Fahrenheit-Celsius */
main()
{
int fahr;
for (fahr = INFERIOR; fahr <= SUPERIOR; fahr = fahr + PASO)
printf("%3d %6.1f\n", fahr, (5.0/9.0)*(fahr-32));
}
Las cantidades INFERIOR
, SUPERIOR
y
PASO
son constantes simbolicas, no variables, por lo que no
aparecen entre las declaraciones. Por convencion, los nombres de
constantes simbolicas, se escriben con letras mayusculas, de forma tal
que puedan distinguirse facilmente de los nombres de variables (escritos
con minusculas). Notese que no hay punto y coma al final de una linea
#define
.
1.5 Entrada y salida de caracteres
Ahora vamos a considerar una familia de programas relacionados para el procesamiento de datos de tipo caracter. Se encontrara que muchos programas solo son versiones ampliadas de los prototipos que se tratan aqui.
El modelo de entrada y salida manejado por la biblioteca estandar es muy simple. La entrada y salida de texto, sin importar donde fue originada o hacia donde se dirige, se tratan como flujos ("streams") de caracteres. Un flujo de texto es una secuencia de caracteres divididos entre lineas, cada una de las cuales consta de cero o mas caracteres seguidos de un caracter nueva linea. La biblioteca es responsable de hacer que cada secuencia de entrada o salida este de acuerdo con este modelo; el programador de C que utiliza la biblioteca no necesita preocuparse de como estan representadas las lineas fuera del programa.
La biblioteca estandar proporciona varias funciones para leer o
escribir de a un caracter a la vez, de las cuales getchar
y
putchar
son las mas simples. Cada vez que es invocada,
getchar
lee el siguiente caracter de entrada de
una secuencia de texto y lo devuelve como su valor. Esto es, despues
de
c = getchar()
la variable c
contiene el siguiente caracter de entrada.
Los caracteres provienen normalmente del teclado; la entrada de archivos
se trata en el capitulo 7.
La funcion putchar
escribe un caracter cada vez que se
invoca:
putchar(c);
escribe el contenido de la variable entera c
como un
caracter, generalmente en la pantalla; Las llamadas a
putchar
y a printf
pueden estar alternadas; la
salida aparecera en el orden en que se realicen las llamadas.
1.5.1 Copia de archivos
Con getchar
y putchar
se puede escribir una
cantidad sorprendente de codigo util sin saber nada mas acerca de
entrada y salida. El ejemplo mas sencillo es un programa que copia la
entrada en la salida, un caracter a la vez:
lee un caracter
while (caracter no es indicador de fin de archivo)
manda a la salida el caracter recien leido
lee un caracter
Al convertir esto en C se obtiene
#include <stdio.h>
/* copia la entrada a la salida; la. version */
main()
{
int c;
c = getchar();
while (c != EOF) {
putchar(c);
c = getchar();
}
}
El operador de relacion !=
significa "no igual a".
Lo que aparece como un caracter en el teclado o en la pantalla es,
por supuesto, como cualquier otra cosa, almacenado internamente como un
patron de bits. El tipo char
tiene la funcion especifica de
almacenar ese tipo de dato, pero tambien puede ser usado cualquier tipo
de entero. Usamos int
por una sutil pero importante
razon.
El problema es distinguir el fin de la entrada de los datos validos.
La solucion es que getchar devuelve un valor distintivo cuando no hay
mas a la entrada, un valor que no puede ser confundido con ningun otro
caracter. Este valor se llama EOF
, por "end-of-file" ("fin
de archivo"). Se debe declarar c
con un tipo que sea lo
suficientemente grande para almacenar cualquier valor que le regrese
getchar
. No se puede utilizar char
puesto que
c
debe ser suficientemente grande como para mantener a
EOF
ademas de cualquier otro caracter. Por lo tanto, se
emplea int
.
EOF
es un entero definido en
<stdio.h>
, pero el valor numerico especifico no
importa mientras que no sea el mismo que ningun valor tipo
char
. Utilizando la constante simbolica, hemos asegurado
que nada en el programa depende del valor numerico especifico.
El programa para copiar podria escribirse de modo mas conciso por programadores experimentados de C. En lenguaje C, cualquier asignacion, tal como
c = getchar();
es una expresion y tiene un valor: el del lado izquierdo luego de la asignacion. Esto significa que una asignacion puede aparecer como parte de una expresion mas larga. Si la asignacion de un caracter a c se coloca dentro de la seccion de prueba de un ciclo while, el programa que copia puede escribirse de la siguiente manera:
#include <stdio.h>
/* copia la entrada a la salida; 2a. version */
main()
{
int c;
while ((c = getchar()) != EOF)
putchar(c);
}
El while
obtiene un caracter, lo asigna a
c
, y entonces prueba si el caracter fue la senal de fin de
archivo. De no serlo, el cuerpo del while
se ejecuta,
escribiendo el caracter; luego se repite el while
. Luego,
cuando se alcanza el final de la entrada, el while
termina
y tambien lo hace main
.
Esta version centraliza la entrada — ahora hay solo una referencia a
getchar
— y reduce el programa. El programa resultante es
mas compacto y mas facil de leer una vez que se domina el truco. Usted
vera seguido este estilo. (Sin embargo, es posible descarriarse y crear
codigo impenetrable, una tendencia que trataremos de reprimir.)
Los parentesis que estan alrededor de la asignacion dentro de la
condicion son necesarios. La precedencia de !=
es
mas alta que la de =
, lo que significa que en ausencia de
parentesis la prueba de relacion !=
se realizaria antes de
la asignacion =
. De esta manera, la proposicion
c = getchar() != EOF
es equivalente a
c = (getchar() != EOF)
Esto tiene el efecto indeseable de hacer que c
sea
0
o 1
, dependiendo de si la llamada de
getchar
encontro fin de archivo. (En el capitulo 2 se trata este tema con mas
detalle).
- Ejercicio 1-6. Verifique que la expresion
getchar() != EOF
es0
o1
. □ - Ejercicio 1-7. Escriba un programa que imprima el
valor de
EOF
. □
1.5.2 Conteo de caracteres
El siguiente programa cuenta caracteres y es semejante al programa que copia.
#include <stdio.h>
/* cuenta los caracteres de la entrada; la. version */
main()
{
long nc;
nc = 0;
while (getchar() != EOF)
++nc;
printf("%ld\n", nc);
}
La proposicion
++nc;
presenta un nuevo operador, ++
, que significa incrementa
en uno. Es posible escribir nc = nc + 1
, pero
++nc
es mas conciso y muchas veces mas eficiente. Hay un
operador correspondiente --
para disminuir en
1
. Los operadores ++
y --
pueden
ser tanto operadores prefijos (++nc
) como
postfijos (nc++
); esas dos formas tienen
diferentes valores dentro de las expresiones, como se demostrara en el
capitulo 2, pero ambos ++nc
y nc++
incrementan a nc
. Por el momento
adoptaremos la forma de prefijo.
El programa para contar caracteres acumula su cuenta en una variable
long
en lugar de una int
. Los enteros
long
son por lo menos de 32 bits. Aunque en algunas
maquinas int y long son del mismo tamano, en otras un
int es de 16 bits, con un valor maximo de 32767
, y
tomaria relativamente poca lectura a la entrada para desbordar un
contador int. La especificacion de conversion %ld
indica a printf
que el argumento correspondiente es un
entero long.
Seria posible tener la capacidad de trabajar con numeros mayores
empleando un double
(float de doble precision).
Tambien se utilizara una proposicion for
en lugar de un
while
, para demostrar otra forma de escribir el ciclo.
#include <stdio.h>
/* cuenta los caracteres de la entrada; 2a. version */
main()
{
double nc;
for (nc = 0; gechar() != EOF; ++nc)
;
printf("%.0f\n", nc);
}
printf
utiliza %f
tanto para float
como para double; %.0f
suprime la impresion del
punto decimal y de la parte fraccionaria, que es cero.
El cuerpo de este ciclo for
esta vacio, debido a que
todo el trabajo se realiza en las secciones de prueba e incremento. Pero
las reglas gramaticales de C requieren que una proposicion
for
tenga un cuerpo. El ;
aislado se llama
proposicion nula, y esta aqui para satisfacer este requisito.
Lo colocamos en una linea aparte para que sea visible.
Antes de abandonar el programa para contar caracteres, observese que
si la entrada no contiene caracteres, la prueba del while
o
del for
no tiene exito desde la primera llamada
getchar
, y el programa produce cero, el resultado correcto.
Esto es importante. Uno de los aspectos agradables acerca del
while
y del for
es que hacen la prueba al
inicio del ciclo, antes de proceder con el cuerpo. Si no hay nada que
hacer, nada se hace, aun si ello significa no pasar a traves del cuerpo
del ciclo. Los programas deben actuar en forma inteligente cuando se les
da una entrada de longitud cero. Las proposiciones while
y
for
ayudan a asegurar que los programas realizan cosas
razonables con condiciones de frontera.
1.5.3 Conteo de lineas
El siguiente programa cuenta lineas a la entrada. Como se menciono anteriormente, la biblioteca estandar asegura que una secuencia de texto de entrada parezca una secuencia de lineas, cada una terminada por un caracter nueva linea.
Por lo tanto, contar lineas es solamente contar caracteres nueva linea:
#include <stdio.h>
/* cuenta las lineas de la entrada */
main()
{
int c, nl;
nl = 0;
while ((c = getchar()) != EOF)
if (c == '\n')
++nl;
printf("%d\n", nl);
}
El cuerpo del while
consiste ahora en un
if
, el cual a su vez controla el incremento
++n1
. La proposicion if
prueba la condicion
que se encuentra entre parentesis y, si la condicion es verdadera,
ejecuta la proposicion (o grupo de proposiciones entre llaves) que le
sigue. Hemos sangrado nuevamente para mostrar lo que controla cada
elemento.
El doble signo de igualdad ==
es la notacion de C para
expresar "igual a" (como el =
simple de Pascal o el
.EQ.
de Fortran). Este simbolo se emplea para distinguir la
prueba de igualdad del =
simple que utiliza C para la
asignacion. Un mensaje de alerta: los principiantes de C ocasionalmente
escriben =
cuando en realidad deben usar ==
.
Como se vera en el capitulo 2, el resultado es por lo general una
expresion legal, de modo que no se obtendra ninguna advertencia.
Un caracter escrito entre apostrofos '...'
representa un
valor entero igual al valor numerico del caracter en el conjunto de
caracteres de la maquina. Esto se llama una constante de
caracter, aunque solo es otra forma de escribir un pequeno entero.
Asi, por ejemplo 'A'
es una constante de caracter; en el
conjunto ASCII de caracteres su valor es 65
(esta es la
representacion interna del caracter A
). Por supuesto
'A'
es preferible que 65
: su significado es
obvio, y es independiente de un conjunto de caracteres en
particular.
Las secuencias de escape que se utilizan en constantes de cadena
tambien son legales en constantes de caracter; asi, '\n'
significa el valor del caracter nueva linea, el cual es 10 del
codigo ASCII. Se debe notar cuidadosamente que '\n'
es un
caracter simple, y en expresiones es solo un entero; por otro lado,
'\n'
es una constante de cadena que contiene solo un
caracter. En el capitulo 2 se trata el
tema de cadenas versus caracteres.
- Ejercicio 1-8. Escriba un programa que cuente espacios en blanco, tabuladores y nuevas lineas. □
- Ejercicio 1-9. Escriba un programa que copie su entrada a la salida, reemplazando cada cadena de uno o mas blancos por un solo blanco. □
- Ejercicio 1-10. Escriba un programa que copie su
entrada a la salida, reemplazando cada tabulacion por \t , cada
retroceso por
\b
y cada diagonal invertida por\\
. Esto hace que las tabulaciones y los espacios sean visibles sin confusiones. □
1.5.4 Conteo de palabras
El cuarto en nuestra serie de programas utiles cuenta las lineas, palabras y caracteres, usando la definicion de que una palabra es cualquier secuencia de caracteres que no contiene espacio en blanco ni tabulacion ni nueva linea. Esta es una version reducida del programa wc de UNIX.
#include <stdio.h>
#define DENTRO 1 /* en una palabra */
#define FUERA 0 /* fuera de una palabra */
/* cuenta lineas, palabras, y caracteres de la entrada */
main()
{
int c, nl, nw, nc, state;
state = OUT;
nl = nw = nc = 0;
while ((c = getchar()) != EOF) {
++nc;
if (c == '\n')
++nl;
if (c == ' ' || c == '\n' || c = '\t')
state = FUERA;
else if (state == FUERA) {
state = IN;
++nw;
}
}
printf("%d %d %d\n", nl, nw, nc);
}
Cada vez que el programa encuentra el primer caracter de una palabra,
contabiliza una palabra mas. La variable state
registra si
actualmente el programa esta o no sobre una palabra; al iniciar es “no
esta sobre una palabra”, por lo que se asigna el valor
DENTRO
. Es preferible usar las constantes simbolicas
DENTRO
y FUERA
que los valores literales
1
y 0
, porque hacen el programa mas legible.
En un programa tan pequeno como este, la diferencia es minima, pero en
programas mas grandes el incremento en claridad bien vale el esfuerzo
extra que se haya realizado para escribir de esta manera desde el
principio. Tambien se descubrira que es mas facil hacer cambios
extensivos en programas donde los numeros magicos aparecen solo como
constantes simbolicas.
La linea
n1 = nw = nc = 0;
inicializa a las tres variables en cero. Este no es un caso especial sino una consecuencia del hecho de que una asignacion es una expresion con un valor, y que las asignaciones se asocian de derecha a izquierda. Es como si se hubiese escrito
n1 = (nw = (nc = 0));
El operador ||
significa "OR", por lo que la linea
if (c == ' ' || c == '\n' || c = '\t')
dice "si c
es un caracter en blanco o
c
es nueva linea, o c
es un
tabulador". (Recuerde que la secuencia de escape
\t
es una representacion visible del caracter
tabulador). Existe un correspondiente operador
&&
para AND; su precedencia es mas alta
que la de ||
. Las expresiones conectadas por
&&
o ||
se evaluan de izquierda a
derecha, y se garantiza que la evaluacion terminara tan pronto como se
conozca la verdad o falsedad. Si c
es un caracter en
blanco, no hay necesidad de probar si es una nueva linea o un
tabulador, de modo que esas pruebas no se hacen. Esto no es de
particular importancia en este caso, pero es significativo en
situaciones mas complicadas, como se vera mas adelante.
El ejemplo muestra tambien un else
, el cual especifica
una accion alternativa si la condicion de una proposicion
if
es falsa. La forma general es
if (expresion)
proposicion1
else
proposicion2
Una y solo una de las dos proposiciones asociadas con un
if-else
se realiza. Si la expresion es verdadera,
se ejecuta proposicion1 si no lo es, se ejecuta
proposicion2. Cada proposicion puede ser una
proposicion sencilla o varias entre llaves. En el programa para contar
palabras, la que esta despues del else
es un
if
que controla dos proposiciones entre llaves.
- Ejercicio 1-11. ¿Como probaria el programa para contar palabras? ¿Que clase de entrada es la mas conveniente para descubrir errores si estos existen? □
- Ejercicio 1-12. Escriba un programa que imprima su entrada una palabra por linea. □
1.6 Arreglos
Escribamos un programa para contar el numero de ocurrencias de cada digito, de caracteres espaciadores (caracter en blancos, tabuladores, nueva linea), y de todos los otros caracteres. Esto es artificioso, pero nos permite ilustrar varios aspectos de C en un programa.
Existen doce categorias de entrada, por lo que es conveniente utilizar un arreglo para mantener el numero de ocurrencias de cada digito, en lugar de tener diez variables individuales. Esta es una version del programa:
#include <stdio.h>
/* cuenta digitos, espacios blancos, y otros */
main()
{
int c, i, nwhite, nother;
int ndigit[10];
nwhite = nother = 0;
for (i = 0; i < 10; ++i)
ndigit[i] = 0;
while ((c = getchar()) != EOF)
if (c >= '0' && c <= '9')
++ndigit[c-'0'];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;
printf("digitos =");
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
printf(", espacio en blanco = %d, otro = %d\n",
nwhite, nother);
}
La salida de este programa al ejecutarlo sobre si mismo es
digitos = 9 3 0 0 0 0 0 0 0 1, espacios en blancos = 123, otros = 345
La declaracion
int ndigit [ 1 0 ];
declara ndigit
como un arreglo de 10 enteros. En C, los
subindices de arreglos comienzan en cero, por lo que los elementos son
ndigit[0]
, ndigit[1]
, ndigit[9]
.
Esto se refleja en los ciclos for
que inicializan e
imprimen el arreglo.
Un subindice puede ser cualquier expresion entera, lo que incluye a
variables enteras como i
, y constantes enteras.
Este programa en particular se basa en las propiedades de la representacion de los digitos como caracteres. Por ejemplo, la prueba
if (c >= '0' && c <= '9')
determina si el caracter en c
es un digito. Si lo es, el
valor numerico del digito es
c - '0'
Esto solo funciona si '0', '1', ..., '9' tienen valores consecutivos ascendentes. Por fortuna, esto es asi en todos los conjuntos de caracteres.
Por definicion, los char
son solo pequenos enteros, por
lo que las variables y las constantes char
son identicas a
las int
en expresiones aritmeticas. Esto es natural y
conveniente; por ejemplo, c - '0'
es una expresion entera
con un valor entre 0 y 9, correspondiente a los caracteres '’0’' a ’'9’'
almacenados en c
, por lo que es un subindice valido para el
arreglo ndigit
.
La decision de si un caracter es digito, espacio en blanco u otra cosa se realiza con la secuencia
if (c >= '0' && c <= '9')
++ndigit[c-'0'];
else if (c == ' ' || c == '\n' || c == '\t')
++nwhite;
else
++nother;
El patron
if (condicion 1)
proposicion 1
else if (condicion 2)
proposicion2
...
...
else
proposicion_n,
se encuentra frecuentemente en programas como una forma de expresar
una decision multiple. Las condiciones se evaluan en orden
desde el principio hasta que se satisface alguna condicion; en
ese punto se ejecuta la proposicion correspondiente, y la
construccion completa termina. (Cualquier proposicion puede
constar de varias proposiciones entre llaves.) Si no se satisface
ninguna de las condiciones, se ejecuta la proposicion que
esta despues del else
final (si es que esta existe).
Cuando se omiten el else
y la proposicion finales,
tal como se hizo en el programa para contar palabras, no se lleva a cabo
ninguna accion. Puede haber cualquier numero de grupos de
else if (condicion)
proposicion
entre el if
inicial y el else
final.
Se recomienda, por estilo, escribir esta construccion tal como se ha
mostrado; si cada if
estuviese sangrado despues del
else
anterior, una larga secuencia de decisiones podria
rebasar el margen derecho de la pagina.
La proposicion switch
, que se tratara en el capitulo 3, proporciona otra forma de
escribir una decision multiple, que es particularmente apropiada cuando
la condicion es determinar si alguna expresion entera o de caracter
corresponde con algun miembro de un conjunto de constantes. Para
contrastar, se presentara una version de este programa, usando
switch
, en la seccion 3.4.
- Ejercicio 1-13. Escriba un programa que imprima el histograma de las longitudes de las palabras de su entrada. Es facil dibujar el histograma con las barras horizontales; la orientacion vertical es un reto mas interesante. □
- Ejercicio 1-14. Escriba un programa que imprima el histograma de las frecuencias con que se presentan diferentes caracteres leidos a la entrada. □
1.7 Funciones
En lenguaje C, una funcion es el equivalente a una subrutina o funcion en Fortran, o a un procedimiento o funcion en Pascal. Una funcion proporciona una forma conveniente de encapsular algunos calculos, que se pueden emplear despues sin preocuparse de su implantacion. Con funciones disenadas adecuadamente, es posible ignorar como se realiza un trabajo; basta con saber que hace. El lenguaje C hace que el uso de funciones sea facil, conveniente y eficiente; es comun ver una funcion corta definida y empleada una sola vez, unicamente porque eso esclarece alguna parte del codigo.
Hasta ahora solo se han utilizado funciones como printf
,
getchar
y putchar
, que nos han sido
proporcionadas; ya es el momento de escribir unas pocas nosotros mismos.
Dado que C no posee un operador de exponenciacion como el
**
de Fortran, ilustremos el mecanismo de la definicion de
una funcion al escribir la funcion power(m,n)
, que eleva un
entero m
a una potencia entera y positiva n
.
Esto es, el valor de power(2,5)
es 32. Esta funcion no es
una rutina de exponenciacion practica, puesto que solo maneja potencias
positivas de enteros pequenos, pero es suficiente para ilustracion (la
biblioteca estandar contiene una funcion pow(x,y)
que
calcula x^y).
A continuacion se presenta la funcion power
y un
programa main
para utilizarla, de modo que se vea la
estructura completa de una vez.
#include <stdio.h>
int power(int m, int n);
/* prueba la funcion power */
main()
{
int i;
for (i = 0; i < 10; ++i)
printf("%d %d %d\n", i, power(2,i), power(-3,i));
return 0;
}
/* power: eleva la base a la n-esima potencia; n >= 0 */
int power(int base, int n)
{
int i, p;
p = 1;
for (i = 1; i <= n; ++i)
p = p * base;
return p;
}
Una definicion de funcion tiene la forma siguiente:
tipo-de-retorno nombre-de-funcion (declaracion de parametros, si los hay)
{
declaraciones
proposiciones
}
Las definiciones de funcion pueden aparecer en cualquier orden y en uno o varios archivos fuente, pero una funcion no puede separarse en archivos diferentes. Si el programa fuente aparece en varios archivos, tal vez se tengan que especificar mas cosas al compilar y cargarlo que si estuviera en uno solo, pero eso es cosa del sistema operativo, no un atributo del lenguaje. Por ahora supondremos que ambas funciones estan en el mismo archivo y cualquier cosa que se haya aprendido acerca de como ejecutar programas en C, aun funcionaran.
La funcion power
es invocada dos veces por
main
, en la linea
printf("%d %d %d\n", i, power(2,i), power(-3,i));
Cada llamada pasa dos argumentos a power
, que cada vez
regresa un entero, al que se pone formato y se imprime. En una
expresion, power(2,i)
es un entero tal como lo son
2
e i
. (No todas las funciones producen un
valor entero; lo que se vera en el capitulo
4).
La primera linea de la funcion power
,
int power(int base, int n)
declara los tipos y nombres de los parametros, asi como el tipo de
resultado que la funcion devuelve. Los nombres que emplea
power
para sus parametros son locales a la funcion y son
invisibles a cualquier otra funcion: otras rutinas pueden utilizar los
mismos nombres sin que exista problema alguno. Esto tambien es cierto
para las variables i
y p
: la i
de
power
no tiene nada que ver con la i
de
main
.
Generalmente usaremos parametro para una variable nombrada en la lista entre parentesis de la definicion de una funcion, y argumento para el valor empleado al hacer la llamada de la funcion. Los terminos argumento formal y argumento real se emplean en ocasiones para hacer la misma distincion.
El valor que calcula power
se regresa a
main
por medio de la proposicion return
, a la
cual le puede seguir cualquier expresion:
return expresion;
Una funcion no necesita regresar un valor; una proposicion
return
sin expresion hace que el control regrese al
programa, pero no devuelve algun valor de utilidad, como se haria al
“caer al final” de una funcion al alcanzar el caracter }
de
llave cerrada que opera de terminacion. Ademas, la funcion que
llama puede ignorar el valor que regresa una funcion.
Probablemente haya notado que hay una proposicion return
al final de main
. Puesto que main
es una
funcion como cualquier otra, tambien puede regresar un valor a quien la
invoca, que es en efecto el medio ambiente en el que el programa se
ejecuta. Tipicamente, un valor de regreso cero implica una terminacion
normal; los valores diferentes de cero indican condiciones de
terminacion no comunes o erroneas. En busca de la simplicidad, se han
omitido hasta ahora las proposiciones return
de las
funciones main
, pero se incluiran mas adelante, como un
recordatorio de que los programas deben regresar su estado final a su
medio ambiente.
La declaracion
int power(int m, int n);
precisamente antes de main
, indica que
power
es una funcion que espera dos argumentos
int
y regresa un int
. Esta declaracion, a la
cual se le llama funcion prototipo, debe coincidir con la
definicion y uso de power
. Es un error el que la definicion
de una funcion o cualquier uso que de ella se haga no corresponda con su
prototipo.
Los nombres de los parametros no necesitan coincidir; de hecho, son opcionales en el prototipo de una funcion, de modo que para el prototipo se pudo haber escrito
int power(int, int);
No obstante, unos nombres bien seleccionados son una buena documentacion, por lo que se emplearan frecuentemente.
Una nota historica: La mayor modificacion entre ANSI C y las
versiones anteriores es como estan declaradas y definidas las funciones.
En la definicion original de C, la funcion power
se pudo
haber escrito de la siguiente manera:
/* power: eleva la base a n-esima potencia; n >= 0 */
/* (version en estilo antiguo) */
power(base, n)
int base, n;
{
int i, p;
p = 1 ;
for (i = 1 ; i < = n; + + i)
p — p * base;
return p;
}
Los parametros se nombran entre los parentesis y sus tipos se
declaran antes del caracter {
de llave abierta; los
parametros que no se declaran se toman como int
. {El cuerpo
de la funcion es igual a la anterior).
La declaracion de power
al inicio del programa pudo
haberse visto como sigue:
int power();
No se permitio ninguna lista de parametros, de modo que el compilador
no pudo revisar con facilidad que power
fuera llamada
correctamente. De hecho, puesto que por omision se podia suponer que
power
regresaba un entero int
, toda la
declaracion podria haberse omitido.
La nueva sintaxis de los prototipos de funciones permite que sea mucho mas facil para el compilador detectar errores en el numero o tipo de argumentos. El viejo estilo de declaracion y definicion aun funciona en ANSI C, al menos por un periodo de transicion, pero se recomienda ampliamente utilizar la nueva forma si se tiene un compilador que le de soporte.
- Ejercicio 1-15. Escriba de nuevo el programa de conversion de temperatura de la seccion 1.2, de modo que utilice una funcion para la conversion.
1.8 Argumentos -- llamadas por valor
Hay un aspecto de las funciones de C que puede parecer poco familiar a los programadores acostumbrados a otros lenguajes, particularmente Fortran. En C, todos los argumentos de una funcion se pasan “por valor” . Esto significa que la funcion que se invoca recibe los valores de sus argumentos en variables temporales y no en las originales. Esto conduce a algunas propiedades diferentes a las que se ven en lenguajes con “llamadas por referencia” como Fortran o con parametros var en Pascal, en donde la rutina que se invoca tiene acceso al argumento original, no a una copia local.
La diferencia principal es que en C la funcion que se invoca no puede alterar directamente una variable de la funcion que hace la llamada; solo puede modificar su copia privada y temporal.
Sin embargo, la llamada por valor es una ventaja, no una desventaja.
Por lo general, esto conduce a elaborar programas mas compactos con
pocas variables extranas, puesto que los parametros se tratan en la
funcion invocada como variables locales convenientemente inicializadas.
Por ejemplo, he aqui una version de power
que utiliza esta
propiedad.
/* power: eleva la base a la n-esima potencia; n > = 0 ; version 2 */
int power(int base, int n)
{
int p;
for (p = 1; n > 0; --n)
p = p * base;
return p;
}
El parametro n
se utiliza como una variable temporal, y
se decrementa (un ciclo for
que se ejecuta hacia atras)
hasta que llega a cero; ya no es necesaria la variable i
.
Cualquier cosa que se le haga a n
dentro de
power
no tiene efecto sobre el argumento con el que se
llamo originalmente power
.
Cuando sea necesario, es posible hacer que una funcion modifique una variable dentro de una rutina invocada. La funcion que llama debe proporcionar la direccion de la variable que sera cambiada (tecnicamente un apuntador a la variable), y la funcion que se invoca debe declarar que el parametro sea un apuntador y tenga acceso a la variable indirectamente a traves de el. Los apuntadores se trataran en el capitulo 5.
La historia es diferente con los arreglos. Cuando el nombre de un arreglo se emplea como argumento, el valor que se pasa a la funcion es la localizacion o la direccion del principio del arreglo — no hay copia de los elementos del arreglo. Al colocarle subindices a este valor, la funcion puede tener acceso y alterar cualquier elemento del arreglo. Este es el tema de la siguiente seccion.
1.9 Arreglos de caracteres
El tipo de arreglo mas comun en C es el de caracteres. Para ilustrar el uso de arreglos de caracteres y funciones que los manipulan, escriba un programa que lea un conjunto de lineas de texto e imprima la de mayor longitud. El pseudocodigo es bastante simple:
while (hay otra linea)
if (es mas larga que la anterior mas larga)
guardala
guarda su longitud
imprime la linea mas larga
Este pseudocodigo deja en claro que el programa se divide naturalmente en partes. Una trae una nueva linea, o trae la prueba y el resto controla el proceso.
Puesto que la division de las partes es muy fina, lo correcto sera
escribirlas de ese modo. Asi pues, escribamos primero un a funcion
getline
para extraer la siguiente linea de la entrada.
Trataremos de hacer a la funcion util en otros contextos. Al menos,
getline
tiene que regresar una senal acerca de la
posibilidad de un fin de archivo; un diseno de mas utilidad debera
retornar la longitud de la linea, o cero si se encuentra el fin de
archivo. Cero es un regreso de fin de archivo aceptable debido a que
nunca es una longitud de linea valida. Cada linea de texto tiene al
menos un caracter; incluso una linea que solo contenga un caracter nueva
linea, tiene longitud 1.
Cuando se encuentre una linea que es mayor que la anteriormente mas
larga, se debe guardar en algun lugar. Esto sugiere una segunda funcion
copy
, para copiar la nueva linea a un lugar seguro.
Finalmente, se necesita un programa principal para controlar
getline
y copy
.
El resultado es el siguiente:
#include <stdio.h>
#define MAXLINE 1000 /* tamano maximo de la linea de entrada */
int getline(char line[], int maxline);
void copy(char to[], char from[]);
/* imprime la linea de entrada mas larga */
main()
{
int len; /* longitud actual de la linea */
int max; /* maxima longitud vista hasta el momento */
char line[MAXLINE]; /* linea de entrada actual */
char longest[MAXLINE]; /* la linea mas larga se guarda aqui */
max = 0 ;
while ((len = getline(line, MAXLINE)) > 0)
if (len > max) {
max = len;
copy(longest, line);
}
if (max > 0 ) /* hubo una linea */
printf("%s", longest);
return 0 ;
}
/* getline: lee una linea en s, regresa su longitud */
int getline(char s[], int lim)
{
int c, i;
for (i=0; i < lim-1 && (c=getchar())!=EOF && c!='\n'; ++i)
s[i] = c;
if (c == '\n') {
s[i] = c;
++i;
}
s[i] = '\0';
return i;
}
s[i] = '\0';
return i;
}
/* copy: copia 'from' en 'to'; supone que to es suficientemente grande */
void copy(char to[], char from[])
{
int i;
i = 0;
while ((to[i] = from[i]) != '\0')
++i;
}
Las funciones getline
y copy
estan
declaradas al principio del programa, que se supone esta contenido en un
archivo. main
y getline
se comunican a traves
de un par de argumentos y un valor de retorno. En getline
los argumentos se declaran por la linea
int getline(char s[], int lim);
que especifica que el primer argumento, s
, es un
arreglo, y el segundo, lim
, es un entero. El proposito de
proporcionar el tamano de un arreglo es fijar espacio de almacenamiento
contiguo. La longitud del arreglo s
no es necesaria en
getline
, puesto que su tamano se fija en main
.
En getline
se utiliza return
para regresar un
valor a quien lo llama, tal como hizo la funcion power
.
Esta linea tambien declara que getline
regresa un int;
puesto que int es el valor de retorno por omision, puede suprimirse.
Algunas funciones regresan un valor util; otras, como
copy
, se emplean unicamente por su efecto y no regresan un
valor. El tipo de retorno de copy
es void, el cual
establece explicitamente que ningun valor se regresa.
En getline
se coloca el caracter \0
(caracter nulo, cuyo valor es cero) al final del arreglo que
esta creando, para marcar el fin de la cadena de caracteres. Esta
convencion tambien se utiliza por el lenguaje C; cuando una constante de
caracter como
"hola\n"
aparece en un programa en C, se almacena como un arreglo que contiene
los caracteres de la cadena y termina con un \0
para marcar
el fin.
h |
o |
l |
a |
\n |
\0 |
La especificacion de formato %s
dentro de
printf
espera que el argumento correspondiente sea una
cadena representada de este modo; copy
' tambien se basa en
el hecho de que su argumento de entrada se termina con \0
,
y copia este caracter dentro del argumento de salida.
Todo esto implica que
\0
no es parte de un texto normal.
Es util mencionar de paso que aun un programa tan pequeno como este
presenta algunos problemas de diseno. Por ejemplo, ¿que debe hacer
main
si encuentra una linea que es mayor que su limite?
getline
trabaja en forma segura, en ese caso detiene la
recopilacion cuando el arreglo esta lleno, aunque no encuentre el
caracter nueva linea. Probando la longitud y el ultimo caracter
devuelto, main
puede determinar si la linea fue demasiado
larga, y entonces realiza el tratamiento que se desee. Por brevedad,
hemos ignorado el asunto.
No existe forma para un usuario de getline
de saber con
anticipacion cuan larga podra ser una linea de entrada, por lo que
getline
revisa un posible desbordamiento ("overflow"). Por
otro lado, el usuario de copy
ya conoce (o lo puede
averiguar) cual es el tamano de la cadena, por lo que decidimos no
agregar comprobacion de errores en ella.
- Ejercicio 1-16. Corrija la rutina principal del programa de la linea mas larga de modo que imprima correctamente la longitud de lineas de entrada arbitrariamente largas, y tanto texto como sea posible. □
- Ejercicio 1-17. Escriba un programa que imprima todas las lineas de entrada que sean mayores de 80 caracteres. □
- Ejercicio 1-18. Escriba un programa que elimine los blancos y los tabuladores que esten al final de cada linea de en trad a, y que borre completamente las lineas en blanco. □
- Ejercicio 1-19. Escriba una funcion
reverse(s)
que invierta la cadena de caracteress
. Usela para escribir un programa que invierta su entrada, linea a linea. □
1.10 Variables externas y alcance
Las variables que estan en main
, tal como
line
, longest
, etc., son privadas o locales a
ella. Debido a que son declaradas dentro de main
, ninguna
otra funcion puede tener acceso directo a ellas. Lo mismo tambien es
valido para variables de otras funciones; por ejemplo, la variable
i
en getline
no tiene relacion con la
i
que esta en copy
. Cada variable local de una
funcion comienza a existir solo cuando se llama a la funcion, y
desaparece cuando la funcion termina. Esto es por lo que tales variables
son conocidas como variables automaticas, siguiendo la
terminologia de otros lenguajes. Aqui se utilizara en adelante el
termino automatico para hacer referencia a esas variables locales. (En
el capitulo 4 se discute la categoria de
almacenamiento static
("estatica"), en la que las variables
locales si conservan sus valores entre llamadas.)
Puesto que las variables locales aparecen y desaparecen con la invocacion de funciones, no retienen sus valores entre dos llamadas sucesivas, y deben ser inicializadas explicitamente en cada entrada. De no hacerlo, contendran “basura” .
Como una alternativa a las variables automaticas, es posible definir
variables que son externas a todas las funciones, esto es,
variables a las que toda funcion puede tener acceso por su nombre. (Este
mecanismo es parecido al COMMON
de Fortran o a las
variables de Pascal declaradas en el bloque mas exterior). Debido a que
es posible tener acceso global a las variables externas, estas pueden
ser usadas en lugar de listas de argumentos para comunicar datos entre
funciones. Ademas, puesto que las variables externas se mantienen
permanentemente en existencia, en lugar de aparecer y desaparecer cuando
se llaman y terminan las funciones, mantienen sus valores aun despues de
que regresa la funcion que los fijo.
Una variable externa debe definirse, exactamente una vez,
fuera de cualquier funcion; esto fija un espacio de almacenamiento para
ella. La variable tambien debe declararse en cada funcion que
desee tener acceso a ella; esto establece el tipo de la variable. La
declaracion debe ser una proposicion extern
explicita, o
bien puede estar implicita en el contexto. Para concretar la discusion,
reescribamos el programa de la linea mas larga con line
,
longest
y max
como variables externas. Esto
requiere cambiar las llamadas, declaraciones y cuerpos de las tres
funciones.
#include <stdio.h>
#define MAXLINE 1000 /* maximo tamano de una linea de entrada */
int max; /* maxima longitud vista hasta el momento */
char line [MAXLINE]; /* linea de entrada actual */
char longest [MAXLINE]; /* la linea mas larga se guarda aqui */
int getline(void);
void copy(void);
/* imprime la linea de entrada mas larga; version especializada */
main()
{
int len;
extern int max;
extern char longest[];
max = 0;
while ((len = getline()) > 0)
if (len > max) {
max = len;
copy();
}
if (max > 0) /* hubo una linea */
printf("%s", longest);
return 0;
}
/* getline: version especializada */
int getline(void)
{
int c, i;
extern char line[];
for (i = 0; i < MAXLINE - 1
&& (c=getchar)) != EOF && c != '\n'; ++i)
line[i] = c;
if (c == '\n') {
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}
/* copy: version especializada */
void copy(void)
{
int i;
extern char line[], longest[];
i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}
Las variables externas de main
, getline
y
copy
estan definidas en las primeras lineas del ejemplo
anterior, lo que establece su tipo y causa que se les asigne espacio de
almacenamiento. Desde el punto de vista sintactico, las definiciones
externas son exactamente como las definiciones de variables locales,
pero puesto que ocurren fuera de las funciones, las variables son
externas. Antes de que una funcion pueda usar una variable externa, se
debe hacer saber el nombre de la variable a la funcion. Una forma de
hacer esto es escribir una declaracion extern
dentro de la
funcion; la declaracion es la misma que antes, excepto por la palabra
reservada extern
.
Bajo ciertas circunstancias, la declaracion extern
puede
omitirse. Si la definicion de una variable externa ocurre dentro del
archivo fuente antes de su uso por una funcion en particular, entonces
es necesario usar una declaracion extern
dentro de la
funcion. La declaracion extern
en main
,
getline
y copy
es - por tanto - redundante. De
hecho, una practica comun consiste en colocar las definiciones de todas
las variables externas al principio del archivo fuente y luego omitir
todas las declaraciones extern
.
Si el programa esta conformado por varios archivos de codigo fuente y
una variable se define en archivo1
y tambien se recurre a
ella en archivo2
y archivo3
, entonces es
necesario realizar las declaraciones extern
en
archivo2
y archivo3
para conectar las
ocurrencias de la variable. La practica comun es reunir dichas
declaraciones de variables y funciones extern
en un archivo
separado - historicamente denominado header.h
(el sufijo
.h
se usa por convencion para nombres de header) -
los cuales son incluido por medio de #include
al principio
de cada archivo fuente. Las funciones de la biblioteca estandar, por
ejemplo, estan declaradas en headers como <stdio.h>
.
Este tema se trata ampliamente en el capitulo
4, y la biblioteca en el capitulo 7 y
en el apendice B.
Puesto que las versiones especializadas de getline
y
copy
no tienen argumentos, la logica sugeriria que sus
prototipos al principio del archivo deben ser getline()
y
copy()
. Pero para mantener compatibilidad con programas de
C anteriores, el estandar considera una lista vacia como una declaracion
del estilo antiguo, y suspende toda revision de listas de argumentos;
para una lista explicitamente vacia debe emplearse la palabra
void
. Esto se discutira en el capitulo 4.
Se debe notar que en esta seccion empleamos cuidadosamente las palabras definicion y declaracion cuando nos referimos a variables externas. La palabra “definicion” se refiere al lugar donde se crea la variable o se le asigna un lugar de almacenamiento; “declaracion” se refiere al lugar donde se establece la naturaleza de la variable pero no se le asigna espacio.
A proposito, existe una tendencia a convertir todo en variables
extern
, debido a que aparentemente simplifica las
comunicaciones — las listas de argumentos son cortas y las variables
siempre estan alli, cuando se las necesita. Pero las variables externas
existen siempre, aun cuando no hacen falta. Descansar en la dependencia
de las variables externas resulta peligroso, puesto que lleva a
programas cuyas conexiones entre datos no son absolutamente obvias — las
variables pueden alterarse de manera inadvertida e inesperada, y dichos
programas son dificiles de modificar. La segunda version del programa de
la linea mayor es inferior a la primera, en parte por las anteriores
razones y en parte porque destruye la generalidad de dos utiles
funciones, introduciendo en ellas los nombres de las variables que
manipula.
Hasta este punto hemos descrito lo que podria llamarse los fundamentos convencionales de C. Con estos fundamentos, le sera posible escribir programas utiles de tamano considerable, y probablemente seria una buena idea hacer una pausa suficientemente grande para realizarlos. Estos ejercicios sugieren programas de complejidad algo mayor que los presentados anteriormente en este capitulo.
- Ejercicio 1-20. Escriba un programa
detab
que reemplace tabuladores de la entrada con el numero apropiado de caracteres en blancos para espaciar hasta el siguiente paro de tabulacion. Considere un conjunto fijo de paros de tabulacion, digamos cada n columnas. ¿Debe ser n una variable o un parametro simbolico? □ - Ejercicio 1-21. Escriba un programa
entab
que reemplace cadenas de blancos por el minimo numero de tabuladores y blancos para obtener el mismo espaciado. Considere los paros de tabulacion de igual manera que paradetab
. Cuando un tabulador o un simple espacio en blanco fuese suficiente para alcanzar un paro de tabulacion, ¿a cual se le debe dar preferencia? □ - Ejercicio 1-22. Escriba un programa para "dividir" lineas grandes de entrada en dos o mas lineas mas cortas despues del ultimo caracter no blanco que ocurra antes de la n-esima columna de entrada. Asegurese de que su programa se comporte apropiadamente con lineas muy largas, y de que no existan caracteres en blancos o tabuladores antes de la columna especificada. □
- Ejercicio 1-23. Escriba un programa para eliminar todos los comentarios de un programa en C. No olvide manejar apropiadamente las cadenas entre comillas y las constantes de caracter. Los comentarios de C no se anidan. □
- Ejercicio 1-24. Escriba un programa para revisar los errores de sintaxis rudimentarios de un programa en C, como parentesis, llaves y corchetes no alineados. No olvide las comillas ni los apostrofos, las secuencias de escape y los comentarios. (Este programa es dificil si se hace completamente general). □
Continuar: Capitulo 2