¡Esta es una revisión vieja del documento!
El lenguaje de Programación C
Este es un ejemplar amorosamente wikificado por ~peron de El Lenguaje de Programación C, 2da Edición, de B. Kerningham y D. Ritchie, adaptado para hackers del Tercer Mundo.
Introducción
C es un lenguaje de programación de propósito general que ha sido estrechamente asociado con el sistema UNIX en donde fue desarrollado puesto que tanto el sistema como los programas que corren en él están escritos en lenguaje C. Sin embargo, este lenguaje no está ligado a ningún sistema operativo ni a ninguna máquina, y aunque se le llama “lenguaje de programación de sistemas” debido a su utilidad para escribir compiladores y sistemas operativos, se utiliza con igual eficacia para escribir importantes programas en diversas disciplinas.
Muchas de las ideas importantes de C provienen del lenguaje BCPL, desarrollado por Martin Richards. La influencia de BCPL sobre C se continuó indirectamente a través del lenguaje B, el cual fue escrito por Ken Thompson en 1970 para el primer sistema UNIX de la DEC PDP-7.
BCPL y B son lenguajes “carentes de tipos”. En contraste, C proporciona una variedad de tipos de datos. Los tipos fundamentales son caracteres, enteros y números de punto flotante de varios tamaños. Además, existe una jerarquía de tipos de datos derivados, creados con apuntadores, arreglos, estructuras y uniones. Las expresiones se forman a partir de operadores y operandos; cualquier expresión, incluyendo una asignación o una llamada a función, puede ser una proposición. Los apuntadores proporcionan una aritmética de direcciones independiente de la máquina.
C proporciona las construcciones fundamentales de control de flujo que se requieren en programas bien estructurados: agrupación de proposiciones, toma de decisiones (if-else), selección de un caso entre un conjunto de ellos (switch), iteración con la condición de paro en la parte superior (while, for) o en la parte inferior (do), y terminación prematura de ciclos (break).
Las funciones pueden regresar valores de tipos básicos, estructuras, uniones o apuntadores. Cualquier función puede ser llamada recursivamente. Las variables locales son normalmente “automáticas”, o creadas de nuevo con cada invocación. La definición de una función no puede estar anidada, pero las variables pueden estar declaradas en una modalidad estructurada por bloques. Las funciones de un program a en C pueden existir en archivos fuente separados, que se compilan de m anera separada. Las variables pueden ser internas a una función, externas pero conocidas sólo dentro de un archivo fuente, o visibles al programa completo.
Un paso de preprocesamiento realiza substitución de macros en el texto del programa, inclusión de otros archivos fuente y compilación condicional.
C es un lenguaje de relativo “bajo nivel”. Esta caracterización no es peyorativa, simplemente significa que C trata con el mismo tipo de objetos que la mayoría de las computadoras, llámense caracteres, números y direcciones. Estos pueden ser combinados y cambiados de sitio con los operadores aritméticos y lógicos implantados por máquinas reales.
C no proporciona operaciones para tratar directamente con objetos compuestos, tales como cadenas de caracteres, conjuntos, listas o arreglos. No existen operaciones que manipulen un arreglo o una cadena completa, aunque las estructuras pueden copiarse como una unidad. El lenguaje no define ninguna facilidad para asignación de almacenamiento que no sea la de definición estática y la disciplina de pilas provista por las variables locales de funciones; no emplea heap ni recolector de basura. Finalmente, C en sí mismo no proporciona capacidades de entrada/salida; no hay proposiciones READ o WRITE, ni métodos propios de acceso a archivos. Todos esos mecanismos de alto nivel deben ser proporcionados por funciones llamadas explícitamente.
De manera semejante, C solamente ofrece un control de flujo franco, y lineal: condiciones, ciclos, agrupamientos y subprogramas, pero no multiprogramación, operaciones paralelas, sincronización ni co-rutinas.
Aunque la ausencia de alguna de esas capacidades puede parecer como una grave deficiencia (“¿significa que se tiene que llamar a una función para comparar dos cadenas de caracteres?”), el mantener al lenguaje de un tamaño modesto tiene beneficios reales. Puesto que C es relativamente pequeño, se puede describir en un pequeño espacio y aprenderse con rapidez. Un programador puede razonablemente esperar conocer, entender y utilizar en verdad la totalidad del lenguaje.
Por muchos años, la definición de C fue el manual de referencia de la primera edición de El lenguaje de programación C. En 1983, el American National Standards Institute (ANSI) estableció un comité para proporcionar una moderna y comprensible definición de C. La definición resultante, el estándar ANSI o “ANSI C” , se esperaba fuera aprobada a fines de 1988. La mayoría de las características del estándar ya se encuentran soportadas por compiladores modernos.
El estándar está basado en el manual de referencia original. El lenguaje ha cambiado relativamente poco; uno de los propósitos del estándar fue asegurar que la mayoría de los programas existentes pudiesen permanecer válidos o, al menos, que los compiladores pudieran producir mensajes de advertencia acerca del nuevo comportamiento.
Para la mayoría de los programadores, el cambio más importante es una nueva sintaxis para declarar y definir funciones. Una declaración de función ahora puede incluir una descripción de los argumentos de la función; la sintaxis de la definición cambia para coincidir. Esta información extra permite que los compiladores detecten más fácilmente los errores causados por argumentos que no coinciden; de acuerdo con nuestra experiencia, es una adición muy útil al lenguaje.
Existen otros cambios de menor escala en el lenguaje. La asignación de estructuras y enumeraciones, que ha estado ampliamente disponible, es ahora parte oficial del lenguaje. Los cálculos de punto flotante pueden ahora realizarse con precisión sencilla. Las propiedades de la aritmética, especialmente para tipos sin signo, están esclarecidas. El preprocesador es más elaborado. La mayor parte de esos cambios sólo tendrán efectos secundarios para la mayoría de los programadores.
Una segunda contribución significativa dei estándar es la definición de una biblioteca que acompañe a C. Esta especifica funciones para tener acceso al sistema operativo (por ejemplo, leer de archivos y escribir en ellos), entrada y salida con formato, asignación de memoria, manipulación de cadenas y otras actividades semejantes. Una colección de encabezadores (headers) estándar proporcionan un acceso uniforme a las declaraciones de funciones y tipos de datos. Los programas que utilizan esta biblioteca para interactuar con un sistema anfitrión están asegurados de un com portam iento com patible. La m ayor parte de la biblioteca está estrecham ente modelada con base en la “biblioteca E/S estándar“ del sistema UNIX. Esta biblioteca se describió en la primera edición y ha sido también ampliamente utilizada em otros sistemas. De nuevo, la m ayoría de los programadores no notarán mucho el cambio.
Debido a que los tipos de datos y estructuras de control provistas por C son manejadas directamente por la mayoría de las computadoras, la biblioteca de ejecución (run-time) requerida para implantar programas autocontenidos es pequeña. Las funciones de la biblioteca estándar únicamente se llam an en form a explícita, de manera que se pueden evitar cuando no se necesitan. La mayor parte puede escribirse en C, y excepto por detalles ocultos del sistema operativo, ellas mismas son portátiles.
Aunque C coincide con las capacidades de muchas com putadoras, es independiente de cualquier arquitectura. Con un poco de cuidado es fácil escribir programas portátiles, esto es, programas que puedan correr sin cambios en una variedad de máquinas. El estándar explica los problemas de la transportabilidad, y prescribe un conjunto de constantes que caracterizan a la máquina en la que se ejecuta el programa.
C no es un lenguaje fuertemente tipificado, sino que, al evolucionar, su verificación de tipos ha sido reforzada. La definición original de C desaprobó - pero permitió - el intercambio de apuntadores y enteros; esto se ha eliminado y el estándar ahora requiere la adecuada declaración y la conversión explícita que ya ha sido obligada por los buenos compiladores. La nueva declaración de funciones es otro paso en esta dirección. Los compiladores advertirán de la mayoría de los errores de tipo, y no hay conversión automática de tipos de datos incompatibles.
Sin embargo, C mantiene la filosofía básica de que los programadores saben lo Que están haciendo; sólo requiere que establezcan sus intenciones en forma explícita.
Como cualquier otro lenguaje, C tiene sus defectos. Algunos de los operadores tienen la precedencia equivocada; algunos elementos de la sintaxis pueden ser mejores. A pesar de todo, C ha probado ser un lenguaje extremadamente efectivo y expresivo p ara una am plia variedad de program as de aplicación.
El libro está organizado com o sigue. El capítulo 1 es una introducción orientada a la parte central de C. El propósito es hacer que el lector se inicie tan pronto como le sea posible, puesto que creemos firmemente que la forma de aprender un nuevo lenguaje es escribir program as en él. La introducción supone un conocimiento práctico de los elementos básicos de la programación; no hay una explicación de computadoras, de compilación, ni del significado de una expresión como n = n + 1. Aunque hemos tratado de mostrar técnicas útiles de programación en donde fue posible, la intención del libro no es la de ser un texto de consulta sobre estructuras de datos y algoritmos; cuando nos vimos forzados a hacer una elección, nos hemos concentrado en el lenguaje.
En los capítulos del 2 al 6 se discuten varios aspectos de C en mayor detalle y más formalmente de lo que se hace en el capítulo 1, aunque el énfasis está aún en los ejemplos de programas completos, más que en fragmentos aislados. El capítulo 2 trata de los tipos básicos de datos, operaciones y expresiones. El capítulo 3 trata sobre control de flujo: if-else, switch, while, for, etc. En el capítulo 4 se cubren funciones y la estructura de un programa —variables externas, reglas de alcance, archivos fuente múltiples y otros aspectos— y también abarca al preprocesador. El capítulo 5 discute sobre apuntadores y aritmética de direcciones.
El capítulo 6 cubre estructuras y uniones.
El capítulo 7 describe la biblioteca estándar, la cual proporciona una interfaz común con el sistema operativo. Esta biblioteca está definida por el estándar ANSI y se intenta que se tenga en todas las máquinas que manejan C; así, los programas que la usen para entrada, salida y otros accesos al sistema operativo se puedan transportar de un sistema a otro sin cambios.
El capítulo 8 describe una interfaz entre los programas en C y el sistema operativo UNIX, concentrándose en entrada/salida, el sistema de archivos y la asignación de memoria. Aunque algo de este capítulo es específico de sistemas UNIX, los programadores que usen otros sistemas de todas maneras encontrarán aquí material de utilidad, incluyendo alguna comprensión acerca de cómo está implantada una versión de la biblioteca estándar, así como sugerencias para obtener un código portátil.
El apéndice A contiene un manual de consulta del lenguaje. El informe oficial de la sintaxis y la semántica de C es en sí el estándar ANSI. Ese documento, sin embargo, está principalmente pensado para quienes escriben compiladores. El manual de consulta de este libro transmite la definición del lenguaje en una forma más concisa y sin el mismo estilo legalista. El apéndice B es un resumen de la biblioteca estándar, de nuevo más para usuarios que para implantadores. El apéndice C es un breve resumen de los cambios del lenguaje original. A unque, en caso de duda, el estándar y el compilador en uso quedan como las autoridades finales sobre el lenguaje.
Capitulo 2: Tipos, Operadores y Expresiones
Las variables y las constantes son los objetos de datos básicos que se manipulan en un programa. Las declaraciones muestran las variables que se van a utilizar y establecen el tipo que tienen y algunas veces cuáles son sus valores iniciales. Los operadores especifican lo que se hará con las variables. Las expresiones combinan variables y constantes para producir nuevos valores. El tipo de un objeto determina el conjunto de valores que puede tener y qué operaciones se pueden realizar sobre él. Estos son los temas de este capítulo.
El estándar ANSI ha hecho muchos pequeños cambios y agregados a los tipos básicos y a las expresiones. Ahora hay formas signed y unsigned de todos los tipos enteros, y notaciones para constantes sin signo y constantes de carácter hexadecimales. Las operaciones de coma flotante pueden hacerse en precisión sencilla; también hay un tipo long double para precisión extendida. Las constantes de cadena pueden concatenarse al tiempo de comilación. Las enumeraciones son ya parte del lenguaje, formalizando una característica pendiente por mucho tiempo. Los objetos pueden ser declarados const, lo que impide que cambien. Las reglas para conversión automática entre tipos aritméticos fueron aumentadas para manejar el conjunto de tipos más rico actual.
2.1 Nombres de variables
Aunque no lo mencionamos en el capítulo 1, existen algunas restricciones en los nombre de las variables y de las constantes simbólicas. Los nombres se componen de letras y dígitos; el primer carácter debe ser una letra. El carácter de subrayado ”_“ cuenta como una letra; algunas veces es útil para mejorar la legibilidad de nombres largos de variables. Sin embargo, no se debe comenzar los nombres de variables con este carácter, puesto que las rutinas de biblioteca con frecuencia usan tales nombres. Las letras mayúsculas y minúsculas son distintas,
de tal manera que x y X son dos nombres diferentes. La práctica tradicional de C es usar letras minúsculas para nombres de variables, y todo en mayúsculas para constantes simbólicas.
Al menos los primeros 31 caracteres de un nombre interno son significativos, para nombres de funciones y variables externas el número puede ser menor que 31, puesto que los nombres externos los pueden usar los ensambladores y los cargadores, sobre los que el lenguaje no tiene control. Para nombres externos, el estándar garantiza distinguir sólo 6 caracteres (y sin diferenciar mayúsculas de minúsculas). Las palabras clave como if; else, int, float, etc., se encuentran reservadas: no se pueden utilizar como nombres de variables. Todas ellas deben escribirse con minúsculas.
Es conveniente elegir nombres que estén relacionados con el propósito de la variable, que no sea probable confundirlos tipográficamente. Por estilo, nosotros tendemos a utilizar nombres cortos para variables locales (especialmente índices de iteraciones), y nombres más largos para variables externas.
2.2 Tipos y tamaños de datos
Hay unos cuantos tipos de datos básicos en C:
char | un solo byte, capaz de contener un carácter del conjunto de caracteres local. |
int | un entero, normalmente del tamaño natural de los enteros en la máquina en la que se ejecuta. |
float | punto flotante de precisión normal. |
double | punto flotante de doble precisión. |
Además, existen algunos calificadores que se aplican a estos tipos básicos, short y long se aplican a enteros:
short int sh; long int counter;
La palabra int puede omitirse de tales declaraciones, lo que típicamente se hace.
La intención es que short y long puedan proporcionar diferentes longitudes de enteros donde sea práctico; int será normalmente el tamaño natural para una máquina en particular. A menudo short es de 16 bits y long de 32; int es de 16 o de 32 bits. Cada compilador puede seleccionar libremente los tamaños apropiados para su propio hardware, sujeto sólo a la restricción de que los shorts e ints son, por lo menos - de 16 bits, los longs son por lo menos de 32 bits y el short no es mayor que int, el cual a su vez no es mayor que long.
El calificador signed o unsigned puede aplicarse a char o a cualquier entero.
Los números unsigned son siempre positivos o cero y obedecen las leyes de la aritmética módulo 2”, donde n es el número de bits en el tipo. Así, por ejemplo, si los char son de 8 bits, las variables unsigned char guardan valores entre 0 y 255, en tanto que las variables signed char guardan valores entre -128 y 127 (en una máquina de complemento a dos). El hecho de que los chars ordinarios sean con signo o sin él depende de la máquina, pero los caracteres que se pueden imprimir son siempre positivos.
El tipo long double especifica coma flotante de precisión extendida. Igual que con los enteros, los tamaños de objetos de coma flotante se definen en la implantación; float, double y long double pueden representar uno, dos o tres tamaños distintos.
Los archivos de encabezado headers estándar <limits.h> y <float.h> contienen constantes simbólicas para todos esos tamaños, junto con otras propiedades de la máquina y del compilador, los cuales se discuten en el apéndice B.
- Ejercicio 2-1. Escriba un programa para determinar los rangos de variables
char,short,intylong, tantosignedcomounsigned, imprimiendo los valores apropiados de los headers estándar y por cálculo directo. Es más difícil si los calcula: determine los rangos de los varios tipos de punto flotante. □
2.3 Constantes
Una constante entera como 1234 es un int. Una constante long se escribe con una 1 (ele) o L terminal, como en 123456789L; un entero demasiado grande para caber dentro de un int también será tomado como long. Las constantes sin signo se escriben con una u o U final, y el sufijo ul o UL denota unsigned long.
Las constantes de punto flotante contienen un punto decimal (123.4) o un exponente (1e-2) o ambos; su tipo es double, a menos que tengan sufijo. Los sufijos f o F indican una constante float; l o L indican un long double.
El valor de un entero puede especificarse en forma octal o hexadecimal en lugar de decimal. Un 0 (cero) al principio de una constante entera significa octal; 0x ó 0X al principio significa hexadecimal. Por ejemplo, el decimal 31 puede escribirse como 037 en octal y 0x1f ó 0x1F en hexadecimal. Las constantes octales y hexadecimales también pueden ser seguidas por L para convertirlas en long y U para hacerlas unsigned: OXFUL es una constante unsigned long con valor de 15 en decimal.
Una constante de carácter es un entero, escrito como un carácter dentro de apóstrofos, tal como 'x'. El valor de una constante de carácter es el valor numérico del carácter en el conjunto de caracteres de la máquina. Por ejemplo, en el conjunto de caracteres ASCII el carácter constante '0' tiene el valor de 48, el cual no está relacionado con el valor numérico 0. Si escribimos '0' en vez de un valor numérico tal como 48 (que depende del conjunto de caracteres), el programa resulta independiente del valor particular y será más fácil de leer. Las constantes de carácter participan en operaciones numéricas tal como cualesquier otros enteros, aunque se utilizan más comúnmente en comparaciones con otros caracteres.
Ciertos caracteres pueden ser representados en constante de carácter y de cadena, Por medio de secuencias de escape como \n (caraceter nueva línea); esas secuencias se ven como dos caracteres, pero representan sólo uno. Además, un patrón de bits arbitrario de tamaño de un byte puede ser especificado por
'\ooo'
en donde ooo son de uno a tres dígitos octales (0…7) o por
'\xhh'
en donde hh son uno o más dígitos hexadecimales (0…9, a…f, A…F). Así podríamos escribir
#define VTAB '\013' /* tab vertical ASCII */ #define BELL '\007' /* carácter campana ASCII */
o, en hexadecimal,
#define VTAB '\xb' /* tab vertical ASCII */ #define BELL '\x7' /* carácter campana ASCII */
La constante de carácter '\0' representa el carácter nulo (con valor cero). '\0' a menudo se escribe en vez de 0 para enfatizar la naturaleza de carácter de algunas expresiones, pero el valor numérico es precisamente 0.
El conjunto completo de secuencias de escape es
| Secuencia de Escape | Caracter ASCII |
|---|---|
\a | carácter de alarma/campana (BELL) |
\b | retroceso, BKSP |
\f | avance de hoja, FF |
\n | nueva línea, LF |
\r | regreso de carro, CR |
\t | tabulador horizontal, TAB |
\v | tabulador vertical, VTAB |
\0 | caracter nulo, NULL |
\\ | barra invertida \ |
\? | signo de interrogación ? |
\' | apóstrofo ' |
\" | comillas “ |
\ooo | Número octal |
\xhh | Número hexadecimal |
Una expresión constante es una expresión que sólo inmiscuye constantes. Tales expresiones pueden ser evaluadas durante la compilación en vez de que se haga en tiempo de ejecución, y por tanto pueden ser utilizadas en cualquier lugar en que pueda encontrarse una constante, como en
#define MAXLINE 1000 char line[MAXLINE+1];
o
#define BISIESTO 1 /* en años bisiestos */ int days[31+28+BISIESTO+31+30+31+30+31+31+30+31+30+31];
Una constante de cadena o cadena literal, es una secuencia de cero o más caracteres encerrados entre comillas, como en
"Soy una cadena"
o
""/* la cadena vacía */
Las comillas no son parte de la cadena, sólo sirven para delimitarla. Las mismas secuencias de escape utilizadas en constantes de carácter se aplican en cadenas; \" representa el carácter comillas. Las constantes de cadena pueden ser concatenadas en tiempo de compilación:
"Viva " "Peron!"
es equivalente a
"Viva Peron!"
Esto es útil para separar cadenas largas entre varias líneas de código fuente.
Técnicamente, una constante de cadena es un arreglo de caracteres. La representación interna de una cadena tiene un carácter nulo '\0' al final, de modo que el almacenamiento físico requerido es uno más del número de caracteres escritos entre las comillas. Esta representación significa que no hay límite en cuanto a qué tan larga puede ser una cadena, pero los programas deben leer completamente una cadena para determinar su longitud. La función strlen(s) de la biblioteca estándar regresa la longitud de su argumento s de tipo cadena de caracteres, excluyendo el '\0' del final. Aquí está nuestra versión:
/* strlen: /* regresa la longitud de s */ int strlen(char s[]) { int i; while (s[i] != '\0') ++i; return i; }
strlen y otras funciones para cadenas están declaradas en el header estándar <string.h>.
Se debe ser cuidadoso al distinguir entre una constante de carácter y una cadena que contiene un sólo carácter: 'x' no es lo mismo que "x". El primero es un entero, utilizado para producir el valor numérico de la letra x en el conjunto de caracteres de la máquina. El último es un arreglo de caracteres que contiene un carácter (el caracter x) y un caracter '\0' al final.
Existe otra clase de constante, la constante de enumeración. Una enumeración es una lista de valores enteros constantes, como en
enum boolean {NO, SI};
El primer nombre en un enum tiene valor 0, el siguiente 1, y así sucesivamente, a menos que sean especificados valores explícitos. Si no son especificados todos los valores, los valores no especificados continúan la progresión a partir del último valor que si lo fue, como en el segundo de esos ejemplos:
enum escapes { BELL = '\a', RETROCESO = '\b', TAB = '\t', NVALIN = '\n', VTAB = '\v' , RETURN = '\r'}; enum months { ENE = 1, FEB, MAR, ABR, MAY, JUN, JUL, AGO, SEP, OCT, NOV, DIC}; /* FEB es 2, MAR es 3, etc. */
Los nombres que están en enumeraciones diferentes deben ser distintos. Los valores no necesitan ser distintos d entro de la misma enumeración.
Las enumeraciones proporcionan una manera conveniente de asociar valores constantes con nombres, una alternativa a #define con la ventaja de que los valores pueden ser generados por uno mismo. Aunque las variables de tipos enum pueden declararse, los compiladores no necesitan revisar que lo que se va a almacenar en tal variable es un valor válido para la enumeración. No obstante, las variables de enumeración ofrecen la oportunidad de revisarlas (y a menudo tal cosa es mejor que los #define). Además, un depurador puede ser capaz de imprimir los valores de variables de enumeración en su forma simbólica.
2.4 Declaraciones
Todas las variables deben ser declaradas antes de su uso, aunque ciertas declaraciones pueden ser hechas en forma implícita por el contexto. Una declaración especifica un tipo, y contiene una lista de una o más variables de ese tipo, como en
int inferior, superior, paso; char c, line [1000];
Las variables pueden ser distribuidas entre las declaraciones en cualquier forma; la lista de arriba podría igualmente ser escrita como
int inferior; int superior; int paso; char c; char line [1000];
Esta última forma ocupa más espacio, pero resulta conveniente para agregar un comentario a cada declaración o para realizar subsecuentes modificaciones.
Una variable también puede ser inicializada en su declaración. Si el nombre es seguido por un signo de igual y una expresión, la expresión sirve como un inicializador, como en
char esc = '\\'; int i = 0; int limit = MAXLINE + 1; float eps = l.0e—5;
Si la variable en cuestión no es automática, la inicialización es efectuada sólo una vez, conceptualmente antes de que el programa inicie su ejecución, y el inicializador debe ser una expresión constante. Una variable automática explícitamente inicializada es inicializada cada vez que se entra a la función o bloque en que se encuentra; el inicializador puede ser cualquier expresión. Las variables estáticas y externas son inicializadas en cero por omisión. Las variables automáticas para las que no hay un inicializador explícito tienen valores indefinidos (esto es, basura).
El calificador const puede aplicarse a la declaración de cualquier variable para especificar que su valor no será cambiado. Para un arreglo, el calificador const indica que los elementos no serán alterados.
const double e = 2.71828182845905; const char msg[] = "precaución: ";
La declaración const también se puede utilizar con argumentos de tipo arreglo, para indicar que la función no cambia ese arreglo:
int strlen(const char[]);
Si se efectúa un intento de cambiar un const, el resultado está definido por la implantación.
2.5 Operadores aritméticos
Los operadores aritméticos binarios son +, -, *, y /, y el operador módulo %. La división entera trunca cualquier parte fraccionaria. La expresión
x % y
produce el residuo cuando x es dividido entre y, por lo que es cero cuando y divide a x exactamente. Por ejemplo, un año es bisiesto si es divisible entre 4 pero no entre 100, excepto aquellos años que si son divisibles entre 400, que si son bisiestos. Por lo tanto
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) printf("%d es un año bisiesto\n", year); else printf("%d no es un año bisiesto\n", year);
El operador % no puede aplicarse a operandos float o double. La dirección de truncamiento para / y el signo del resultado de % son dependientes de la máquina para operandos negativos, así como la acción que se toma en caso de sobreflujo o subflujo.
Los operadores binarios + y - tienen la misma precedencia, la cual es menor que la precedencia de *, /, y %, que a su vez es menor que + y - unarios. Los operadores aritméticos se asocian de zquierda a derecha.
La tabla 2-1 que se encuentra al final de este capítulo, resume la precedencia y asociatividad para todos los operadores.
2.6 Operadores de relación y lógicos
Los operadores de relación son
> | Mayor que |
>= | Mayor o igual que |
< | Menor que |
<= | Menor o igual que |
Todos ellos tienen la misma precedencia. Precisamente bajo ellos en precedencia están los operadores de igualdad:
== | Exactamente igual a |
=! | Exactamente no igual a |
Los operadores de relación tienen precedencia inferior que los operadores aritméticos, así que una expresión como i < lim -1 se toma como i < (lim -1 ), como se esperaría.
Más interesantes son los operadores lógicos && y ||. Las expresiones conectadas por && o || son evaluadas de izquierda a derecha, y la evaluación se detiene tan pronto como se conoce el resultado verdadero o falso. La mayoría de los programas en C descansan sobre esas propiedades. Por ejemplo, aquí está un ciclo de la función de entrada getline que escribimos en el capítulo 1:
for (i=0; i < lim-1 && (c=getchar()) != '\n' && c != EOF; ++i) s[i] = c;
Antes de leer un nuevo carácter es necesario verificar que hay espacio para almacenarlo en el arreglo s, así que la prueba i < lim -1 debe hacerse primero. Además, si esta prueba falla, no debemos seguir y leer otro carácter.
De manera semejante, seria desafortunado si c fuese probada contra EOF antes de que se llame a getchar; por lo tanto, la llamada y la asignación deben ocurrir antes de que se pruebe el carácter c.
La precedencia de && es más alta que la de ||, y ambas son menores que los operadores de relación y de asignación, así que expresiones como
i < lim-1 && (c=getchar()) != '\n' && c != EOF
no requieren de paréntesis adicionales. Pero puesto que la precedencia de != es superior que la asignación, los paréntesis se necesitan en
(c=getchar()) != '\n'
para obtener el resultado deseado de asignación a c y después comparación con '\n'.
Por definición, el valor numérico de una expresión de relación o lógica es 1 si la relación es verdadera, y 0 si la relación es falsa.
El operador unario de negación ! convierte a un operando que no es cero en 0, y a un operando cero en 1. Un uso común de ! es en construcciones como
if (!nvalido)
en lugar de
if (valido == 0)
Es difícil generalizar acerca de cuál es la mejor. Construcciones como !nvalido se leen en forma agradable (“si es inválido” ), pero otras más complicadas pueden ser difíciles de entender.
- Ejercicio 2-2. Escriba un ciclo equivalente a la iteración
foranterior sin usar&&o!!.
2.7 Conversiones de tipo
Cuando un operador tiene operandos de tipos diferentes, éstos se convierten a un tipo común de acuerdo con un reducido núm ero de reglas. En general, las únicas conversiones automáticas son aquellas que convierten un operando “angosto” en uno “amplio” sin pérdida de información, tal como convertir un entero a coma flotante en una expresión como f + i. Las expresiones que no tienen sentido, como utilizar un float como subíndice, no son permitidas. Las expresiones que podrían perder información, como asignar un tipo mayor a uno más corto, o un tipo de coma flotante a un entero, pueden producir una advertencia, pero no son ilegales.
Un char sólo es un entero pequeño, por lo que los chars se pueden utilizar libremente en expresiones aritméticas. Esto permite una flexibilidad considerable en ciertas clases de transformación de caracteres. Una es ejemplificada con esta ingenua implantación de la función atoi, que convierte una cadena de dígitos en
su equivalente numérico.
/* atoi: convierte s en entero */ int atoi(char s[]) { int i, n; n = 0; for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i) n = 10 * n + (s[i] - '0'); return n; }
Tal com o se discutió en el capítulo 1, la expresión
s[i] - '0'
da el valor numérico del carácter almacenado en s[i], debido a que los valores de '0', 1, etc., forman una secuencia ascendente contigua.
Otro ejemplo de conversión de char a int es la función lower, que conviene un carácter simple a minúscula en el conjunto de caracteres ASCII. Si el carácter no es una letra mayúscula, lower lo regresa sin cambio.
/* lower: convierte c a minúscula; solamente ASCII */ int lower(int c) { if (c >= 'A' && c <= 'Z') return c + 'a' - 'A'; else return c; }
Esto funciona en ASCII puesto que las correspondientes letras mayúsculas y minúsculas se encuentran a una distancia fija como valores numéricos y cada alfabeto es contiguo (no hay sino letras entre A y Z). Sin embargo, esta última observación no es cierta para el conjunto de caracteres EBCDIC, así que este código podría convertir algo más que sólo letras en EBCDIC.
El header estándar <ctype.h>, que se describe en el apéndice B, define una familia de funciones que proporcionan pruebas y conversiones independientes de los juegos de caracteres. Por ejemplo, si c es una mayúscula, la función tolower© regresa el valor de la letra minúscula de c (de modo que tolower es un reemplazo transportable para la función lower mostrada antes). De modo semejante, la
prueba
c >= '0' && c <= '9'
puede reemplazarse por
isdigit(c)
Nosotros utilizaremos las funciones de <ctype.h> en adelante. Existe un sutil punto acerca de la conversión de caracteres a enteros.' El lenguaje no especifica si las variables de tipo char son valores con o sin signo. Cuando un char se convierte a int, ¿puede producir alguna vez un entero negativo?
La respuesta varía de una máquina a otra, reflejando diferencias en la arquitectura. En algunas máquinas un char cuyo bit más a la izquierda es 1 se convertirá a un entero negativo (“extensión de signo”). En otras, un char resulta promovido a un int agregando ceros del lado izquierdo, así que siempre es positivo.
La definición de C garantiza que ningún carácter que esté en el conjunto estándar de caracteres de impresión de la máquina será negativo, de modo que esos caracteres siempre serán cantidades positivas en las expresiones. Pero hay patrones arbitrarios de bits almacenados en variables de tipo carácter que pueden aparecer como negativos en algunas máquinas, aunque sean positivos en otras - por razones de portabilidad - se debe especificar signed o unsigned si se van a almacenar datos que no son caracteres en variables tipo char.
Las expresiones de relación como i > j y las expresiones lógicas conectadas por && y || están definidas para tener un valor de 1 siendo verdaderas, y 0 al ser falsas. De este modo, la asignación
d = c >= '0' && c <= '9'
vuelve 1 a d si c es un dígito, y 0 si no lo es. Sin embargo, las funciones como isdigit pueden regresar cualquier valor diferente de cero como verdadero. En el componente de validación de if, while, for, etc., “verdadero” sólo significa “diferente de cero”, por lo que esto no hace diferencia.
Las conversiones aritméticas implícitas trabajan como se espera. En general, si un operador como -f o * que toma dos operandos (operador binario) tiene operandos de diferentes tipos, el tipo “menor” es promovido al tipo “superior” antes de que la operación proceda. El resultado es el del tipo mayor. La sección 6 del apéndice A establece las reglas de conversión en forma precisa. Si no hay operandos unsigned, sin embargo, el siguiente conjunto informal de reglas bastará:
- Siendo cualquier operando
long double, conviértase el otro along double. - De otra manera, siendo cualquier operando
double, conviértase el otro adouble. - De otra manera, siendo cualquier operando
float, conviértase el otro afloat. - De otra manera, conviértase
charyshortaint. - Luego, siendo cualquier operando
long, conviértase el otro along.
Nótese que los floats que están en una expresión no se convierten automáticamente a double; esto es a resultas de una alteración a la definición original. En general, las funciones matemáticas como las de <math.h> utilizarán doble precisión. La razón principal para usar float es ahorrar espacio de almacenamiento en arreglos grandes o - con menor frecuencia - ahorrar tiempo en procesamiento en máquinas donde la aritmética de doble precisión resulta particularmente costosa.
Cuando hay operandos unsigned las reglas de conversión son más complicadas. El problema es que las comparaciones de valores con signo y sin signo son dependientes de la máquina, debido a su dependencia de los tamaños de los varios tipos de enteros. Por ejemplo, supóngase que int es de 16 bits y long de 32. Entonces -1L < 1U, puesto que 1U, que es un unsigned int, es promovido a signed long. Pero -1L > 1UL, puesto que —1L es promovido a unsigned long. Y así parece ser un gran número positivo.
Las conversiones también tienen lugar en las asignaciones; el valor del lado derecho es convertido al tipo de la izquierda, el cual es el tipo del resultado.
Un carácter es convertido a un entero, tenga o no extensión de signo, como se describió anteriormente.
Los enteros más largos son convertidos a cortos o a char desechando el exceso de bits de más alto orden. Así en
int i; char c; i = c; c = i;
el valor de c no cambia. Esto es verdadero ya sea que se inmiscuya o no la extensión de signo. Sin embargo, el invertir el orden de las asignaciones podría producir pérdida de información.
Si x es float e i es int, entonces x = i e i = x producirán conversiones; de float a int provoca el truncamiento de cualquier parte fraccionaria. Cuando double se convierte a float, el que se redondee o trunque el valor es dependiente de la implantación.
Puesto que un argumento de la llamada a una función es una expresión, también suceden conversiones de tipo cuando se pasan argumentos a funciones. En ausencia del prototipo de una función, char y short pasan a ser int, y float se hace double. Esta es la razón por la que se han declarado los argumentos a funciones como int y double, aun cuando la función se llama con char y float.
Finalmente, la conversión explícita de tipo puede ser forzada (“coaccionada” ) en cualquier expresión, con un operador unario llamado cast. En la construcción
(nombre-de-tipo) expresión
la expresión es convertida al tipo nombrado, según las reglas de conversión anteriores. El significado preciso de un cast es como si la expresión fuera asignada a una variable del tipo especificado, que se utiliza entonces en lugar de la construcción completa. Por ejemplo, la rutina de biblioteca sqrt espera un argumento de doble precisión double, y si maneja inadvertidamente algo diferente producirá resultados sin sentido (sqrt está declarado en <math.h>). Así, si n es un entero, podemos usar
sqrt((double) n)
para convertir el valor de n a doble antes de pasarlo a sqrt. Nótese que la conversión forzosa produce el valor de n en el tipo apropiado; n en sí no se altera. El operador cast tiene la misma alta precedencia que otros operadores unarios, como se resume en la tabla del final de este capítulo.
Si un prototipo de función declara argumentos, como debe ser normalmente, la declaración produce conversión forzada automática de los argumentos cuando la función es llamada. Así, dado el prototipo de la función sqrt:
double sqrt(double)
la llamada
root2 = sqrt(2)
obliga al entero 2 a ser el valor double, esto es 2.0, sin necesidad de ningún cast.
La biblioteca estándar incluye una implantación transportable de un generador de números pseudoalealorios, y una función para inicializar la semilla; lo primero ilustra un cast:
unsigned long int next = 1; /* rand: regresa un entero pseudoaleatorio en 0..32767 */ int rand(void) { next = next * 1103515245 + 12345; return (unsigned int)(next/65536) % 32768; } /* srand: set seed for rand() */ void srand(unsigned int seed) { next = seed; }
- Ejercicio 2-3. Escriba la función
htoi(s), que convierte una cadena de dígitos hexadecimales (incluyendoOxoOXen forma optativa) en su valor entero equivalente. Los dígitos permitidos son del0al9, de laaa laf, y de laAa laF. □
2.8 Operadores de incremento y decremento
El lenguaje C proporciona dos operadores poco comunes para incrementar y decrementar variables. El operador de aumento ++ agrega 1 a su operando, en tanto que el operador de disminución – le resta 1. Hemos usado frecuentemente ++ para incrementar variables, como en
if (c == '\n') ++nl;
El aspecto poco común es que ++ y — pueden ser utilizado como prefijos (antes de la variable, como en + + n ), o como postfijos (después de la variable: (11 ++). En ambos casos, el efecto es incrementar n. Pero la expresión ++ n incrementa a n antes de que su valor se utilice, en tanto que n++ incrementa a
n después de que su valor se ha empleado. Esto significa que en un contexto donde el valor está siendo utilizado, y no sólo el efecto, ++n y n++ son diferentes.
Si n es 5, entonces
x = n++;
asigna 5 a x, pero
x = ++n;
hace que x sea 6. En ambos casos, n se hace 6. Los operadores de incremento y decremento sólo pueden aplicarse a variables; una expresión com o (i + j)+ +
es ilegal.
Dentro de un contexto en donde no se desea ningún valor, sino sólo el efecto de incremento, como en
if (c == '\n') nl++;
prefijos y postfijos son iguales. Pero existen situaciones en donde se requiere específicam ente unou otro. Por ejemplo, considérese la función squeeze(s,c), que elimina todas las ocurrencias del carácter c de una cadena s.
/* squeeze: borra todas las c de s */ void squeeze(char s[], int c) { int i, j; for (i = j = 0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[j] = '\0'; } }
Cada vez que se encuentra un valor diferente de c, éste se copia en la posición actual j, y sólo entonces j es incrementada para prepararla para el siguiente carácter. Esto es exactamente equivalente a
if (s[i] != c) { s[j] = s[i]; j++; }
Otro ejemplo de construcción semejante viene de la función getline que escribimos en el capítulo 1, en donde podemos reemplazar
if (c == '\n') { s[i] = c; ++i; }
por algo más compacto como
if (c == '\n') s[i++] = c;
Como un tercer ejemplo, considérese que la función estándar strcat(s,t), que concatena la cadena t al final de la cadena s. strcat supone que hay suficiente espacio en s para almacenar la combinación. Como la habíamos escrito, strcat no regresaba un valor; en cambio la versión de la biblioteca estándar regresa un apuntador a la cadena resultante.
/* strcat: concatena t al final de s; s debe ser suficientemente grande *1 void strcat(char s[], char t[]) { int i, j; i = j = 0; while (s[i] != '\0') /* encontrar fin de s */ i++; while ((s[i++] = t[j++]) != '\0') /* copiar t */ ; }
Como cada carácter es copiado de t a s, el ++ postfijo se aplica tanto a i como a j para estar seguros de que ambos están en posición para la siguiente iteración.
- Ejercicio 2-4. Escriba una versión alterna de
squeeze(sl,s2)que borre cada carácter des1que coincida con cualquier carácter de la cadenas2. □ - Ejercicio 2-5. Escriba la función
any(sl,s2), que regresa la primera posición de la cadenas1en donde se encuentre cualquier carácter de la cadenas2, o-1sis1no contiene caracteres des2. (La función de biblioteca estándarstrpbrkhace el mismo trabajo pero regresa un apuntador a la posición encontrada.) □
2.9 Operadores para manejo de bits
El lenguaje C proporciona seis operadores para manejo de bits; sólo pueden ser aplicados a operandos integrales, esto es, char, short, int, y long, con o sin signo.
& | AND de bits |
| | OR inclusivo de bits |
^ | OR exclusivo de bits |
<< | corrimiento a la izquierda |
>> | corrimiento a la derecha |
~ | complemento a uno (unario) |
El operador AND de bits & a menudo es usado para enmascarar algún conjunto de bits; por ejemplo,
n = n & 0177;
hace cero todos los bits de n, menos los 7 de menor orden.
El operador OR de bits | es empleado para encender bits:
x = x | SET_ON;
fija en uno a todos los bits de x que son uno en SET_ON.
El operador OR exclusivo ^ pone un uno en cada posición en donde sus operandos tienen bits diferentes, y cero en donde son iguales.
Se deben distinguir los operadores de bits & y | de los operadores lógicos && y ||, que implican evaluación de izquierda a derecha de un valor de verdad. Por ejemplo, si x es 1 y y es 2, entonces x & y es cero en tanto que x && y es uno.
Los operadores de corrimiento << y >> realizan corrimientos a la izquierda y a la derecha de su operando que está a la izquierda, el número de posiciones de bits dado por el operando de la derecha, el cual debe ser positivo. Así x << 2 desplaza el valor de x a la izquierda dos posiciones, llenando los bits vacantes con cero; esto es equivalente a una multiplicación por 4. El correr a la derecha una cantidad unsigned siempre llena los bits vacantes con cero. El correr a la derecha una cantidad signada llenará con bits de signo (“corrimiento aritmético”) en algunas máquinas y con bits 0 (“corrimiento lógico” ) en otras.
El operador unario ~ da el complemento a uno de un entero; esto es, convierte cada bit 1 en un bit 0 y viceversa. Por ejemplo,
x = x & ~077
fija los últimos seis bits de x en cero. Nótese que x & ~077 es independiente de la longitud de la palabra, y por lo tanto, es preferible a, por ejemplo, x & 0177700, que supone que x es una cantidad de 16 bits. La forma transportable no involucra un costo extra, puesto que ~077 es una expresión constante que puede ser evaluada en tiempo de compilación.
Como ilustración de algunos de los operadores de bits, considere la función getbits(x,p,n) que regresa el campo de n bits de x (ajustado a la derecha) que principia en la posición p. Se supone que la posición del bit 0 está en el borde derecho y que n y p son valores positivos adecuados. Por ejemplo, getbits(x,4,3) regresa los tres bits que están en la posición 4, 3 y 2, ajustados a la derecha.
/* getbits: obtiene n bits desde la posición p */ unsigned getbits(unsigned x, int p, int n) { return (x >> (p+1-n)) & ~(~0 << n); }
La expresión x >> (p+1-n) mueve el campo deseado al borde derecho de la palabra. ~0 es todos los bits en 1; corriendo n bits hacia la izquierda con ~0<<n coloca ceros en los n bits más a la derecha; complementado con ~ hace una máscara de unos en los n bits más a la derecha.
- Ejercicio 2-6. Escriba una función
setbits(x,p,n,y)que regresaxcon los n bits que principian en la posiciónpiguales a los n bits más a la derecha dey, dejando los otros bits sin cambio. □ - Ejercicio 2-7. Escriba una función
invert(x,p,n)que regresaxcon los n bits que
principian en la posición p invertidos (esto es, 1 cambiado a 0 y viceversa), dejando los otros sin cambio. □
- Ejercicio 2-8. Escriba una función
rightrot(x,n)que regresa el valor del enteroxrotado a la derecha n posiciones de bits. □
2.10 Operadores de asignación y expresiones
Las expresiones tales como
i = i + 2
en las que la variable del lado izquierdo se repite inmediatamente en el derecho, pueden ser escritas en la forma compacta
i += 2
El operador += se llama operador de asignación.
La mayoría de los operadores binarios (operadores como + que tienen un operando izquierdo y otro derecho) tienen un correspondiente operador de asignación op= , en donde op es uno de
+ | - | * | / | % | « | » | & | * | | |
Si expr1 y expr2 son expresiones, entonces
expr1 op— expr2
es equivalente a
expr1 = (expr1) op (expr2)
exceptuando que expr1 se calcula sólo una vez. Nótense los paréntesis alrededor de expr2:
x *= y + 1
significa
x = x * (y + 1)
Y no
x = x * y + 1
Como ejemplo, la función bitcount cuenta el número de bits en 1 en su argumento entero.
/* bitcount: cuenta bits 1 en x */ int bitcount(unsigned x) { int b; for (b = 0; x != 0; x >>= 1) if (x & 01) b++; return b; }
Declarar al argumento x como unsigned asegura que cuando se corre a la derecha, los bits vacantes se llenarán con ceros, no con bits de signo, sin importar la máquina en la que se ejecute el programa.
Muy aparte de su concisión, los operadores de asignación tienen la ventaja de que corresponden mejor con la forma en que la gente piensa. Decimos “suma 2 a i” o “incrementa i en 2” , no “toma i, agrégale 2, después pon el resultado de nuevo en i”. Así la expresión i += 2 es preferible a i = i + 2. Además, para una expresión complicada como
yyval[yypv[p3+p4] + yypv[p1]] += 2
el operador de asignación hace al código más fácil de entender, puesto que el lector no tiene que verificar arduamente que dos expresiones muy largas son en realidad iguales, o preguntarse por qué no lo son, y un operador de asignación puede incluso ayudar al compilador a producir código más eficiente.
Ya hemos visto que la proposición de asignación tiene un valor y puede estar dentro de expresiones; el ejemplo más común es
while ((c = getchar()) != EOF) ...
Los otros operadores de asignación (+=, —=, etc.) también pueden estar dentro de expresiones, aunque esto es menos frecuente.
En todas esas expresiones, el tipo de una expresión de asignación es el tipo de su operando del lado izquierdo, y su valor es el valor después de la asignación.
- Ejercicio 2-9. En un sistema de números de complemento a dos,
x &= (x-1)borra el bit 1 de más a la derecha enx. Explique el porqué. Utilice esta observación para escribir una versión más rápida debitcount. □
2.11 Expresiones condicionales
Las proposiciones
if (a > b) z = a; else z = b;
calculan en z el máximo de a y b. La expresión condicional, escrita con el operador ternario “?:” proporciona una forma alternativa para escribir ésta y otras construcciones semejantes. En la expresión
expr1 ? expr2 : expr3
la expresión expr1 es evaluada primero. Si es diferente de cero (verdadero), entonces la expresión expr2 es evaluada, y ése es el valor de la expresión condicional. De otra forma, se evalúa expr3, y ése es el valor. Sólo se evalúa una de entre expr2 y expr3. Asi, para hacer z el máximo de a y b,
z = (a > b) ? a : b; /* z = max(a, b) */
Se debe notar que la expresión condicional es en sí una expresión, y se puede utilizar en cualquier lugar donde otra expresión pueda. Si expr2 y expr3 son de tipos diferentes, el tipo del resultado se determina por las reglas de conversión discutidas anteriormente en este capítulo. Por ejemplo, si f es un float y n es un int, entonces la expresión
(n > 0) ? f : n
es de tipo float sea n positivo o no.
Los paréntesis no son necesarios alrededor de la primera expresión de una expresión condicional, puesto que la precedencia de ?: es muy baja, sólo arriba de la asignación. De cualquier modo son recomendables, puesto que hacen más fácil de ver la parte de condición de la expresión.
La expresión condicional frecuentemente lleva a un código conciso. Por ejemplo, este ciclo imprimen elementos de un arreglo, 10 por línea, con cada columna separada por un caracter en blanco, y con cada línea (incluida la última) terminada por un caracter nueva línea.
for (i = 0; i < n; i++) printf("%6d%c", a[i], (i%10==9 || i==n-1) ? '\n' : ' ');
Se imprime un carácter nueva línea después de cada diez elementos, y después del n-ésimo. Todos los otros elementos son seguidos por un espacio en blanco. Esto podría parecer complicado, pero es más compacto que el if-else equivalente. Otro buen ejemplo es
printf("Tiene %d elementos%s.\n", n, n==1 ? "" : "s");
- Ejercicio 2-10. Reescriba la función
lower, que convierte letras mayúsculas e minúsculas, con una expresión condicional en vez de unif-else. □
2.12 Precedencia y orden de evaluación
La tabla 2-1 resume las reglas de precedencia y asociatividad de todos los operadores, incluyendo aquellos que aún no se han tratado. Los operadores que están en la misma línea tienen la misma precedencia; los renglones están en orden de precedencia decreciente, así, por ejemplo, %, /, y * tienen todos la misma precedencia, la cual es más alta que la de + y - binarios. El “operador” () se refiere a la llamada a una función. Los operadores -> y . son utilizados para tener acceso a miembros de estructuras; serán cubiertos en el capitulo 6, junto con sizeof (tamaño de un objeto). En el capitulo 5 se discuten * (indirección a través de un apuntador) y & (dirección de un objeto), y en el capítulo 3 se trata al operador , (coma).
Los +, -, y * unarios, tienen mayor precedencia que las formas binarias.
Nótese que la precedencia de los operadores de bits &, ^, y | están por debajo de == y != . Esto implica que las expresiones de prueba de bits como
if ((x & MASK) == 0) ...
deben ser completamente colocadas entre paréntesis para dar los resultados apropiados.
Como muchos lenguajes, C no especifica el orden en el cual los operandos de un operador serán evaluados. (Las excepciones son &&, ||, ?: y ,.) Por ejemplo, en proposiciones como
x = f() + g();
f puede ser evaluada antes de g o viceversa; de este modo si f o g alteran una variable de la que la otra depende, x puede depender del orden de evaluación. Se pueden almacenar resultados intermedios en variables temporales para asegurar una secuencia particular.
De manera semejante, el orden en el que se evalúan los argumentos de una función no está especificado, de modo que la proposición
printf("%d %d\n", ++n, power(2, n)); /* EQUIVOCADO */
puede producir resultados diferentes con distintos compiladores, dependiendo de si n es incrementada antes de que se llame a power. La solución, por supuesto, es escribir
++n; printf("%d %d\n", n, power(2, n));
Las llamadas a funciones, proposiciones de asignación anidadas, y los operadores de incremento y decremento provocan “efectos colaterales” — alguna variable resulta modificada como producto de la evaluación de una expresión. En cualquier expresión que involucra efectos colaterales, pueden existir sutiles dependencias del orden en que las variables involucradas en la expresión se actualizan. La infortunada situación es tipificada por la proposición
a[i] = i++;
La pregunta es si el subíndice es el viejo o el nuevo valor de i. Los compiladores pueden interpretar esto en formas diferentes, y generar diferentes respuestas de pendiendo de su interpretación. El estándar deja intencionalmente sin especificación la mayoría de estos aspectos. Cuando existen efectos colaterales (asignación a variables) dentro de una expresión, se deja a la prudencia del compilador, puesto que el orden más eficiente depende mayormente de la arquitectura de la máquina. (El estándar sí especifica que todos los efectos colaterales sobre argumentos sucedan antes de que la función sea llamada, pero eso podría no ayudar en la llamada a printf mostrada anteriormente).
La moraleja es que escribir un código dependiente del orden de evaluación es una mala práctica de programación en cualquier lenguaje. Naturalmente, es necesario conocer qué cosas evitar, pero si no sabe cómo varias máquinas resuelven las cosas, no debe intentar sacar provecho de una implantación particular.
Tabla 2-1: Precedencia y asociatividad de operadores
| Operadores | Asociatividad |
|---|---|
() [] -> . | izquierda a derecha |
! ~ ++ – + - * (tipo) sizeof | derecha a izquierda |
* / % | izquierda a derecha |
+ - | izquierda a derecha |
« » | izquierda a derecha |
< <= > >= | izquierda a derecha |
== != | izquierda a derecha |
& | izquierda a derecha |
^ | izquierda a derecha |
| | izquierda a derecha |
&& | izquierda a derecha |
|| | izquierda a derecha |
?: | derecha a izquierda |
= += -= *= /= %= &= ^= |= <<= >>= | derecha a izquierda |
Capítulo 3: Control de Flujo
Las proposiciones de control de flujo de un lenguaje especifican el orden en que se realiza el procesamiento. Ya hemos visto la mayoría de las construcciones de control de flujo en ejemplos anteriores; aquí completaremos el conjunto, y seremos más precisos acerca de las discutidas con anterioridad.
3.1 Proposiciones y bloques
Una expresión como x = 0 ó i++ o printf(…) se convierte en una proposición cuando va seguida de un punto y coma ;, como en
x = 0; i++; printf(...);
En C, el punto y coma ; es un terminador de proposición, en lugar de un separador, como lo es en un lenguaje tipo Pascal.
Las llaves { y } se emplean para agrupar declaraciones y proposiciones dentro de una proposición compuesta o bloque, de modo que son sintácticamente equivalentes a una proposición sencilla. Las llaves que encierran las proposiciones de una función son un ejemplo obvio; otros ejemplos son las llaves alrededor de proposiciones múltiples después de un if, else, while o for. (Pueden declararse variables dentro de cualquier bloque; esto se expondrá en el capítulo 4). No hay punto y coma después de la llave derecha que termina un bloque.
3.2 If-Else
La proposición if-else se utiliza para expresar decisiones. Formalmente, la sintaxis es
if (expresión) proposición1 else proposición2
donde la parte del else es optativa. La expresión se evalúa; si es verdadera (esto es, si la expresión tiene un valor diferente de cero), la proposición1 se ejecuta. Si es falsa (expresión es cero) y si existe una parte de else, se ejecuta en su lugar la proposición2.
Puesto que un if simplemente prueba el valor numérico de una expresión, son posibles ciertas abreviaciones de código. Lo más obvio es escribir
if (expresión)
en lugar de
if (expresión != 0)
Algunas veces esto es claro y natural; otras puede ser misterioso.
Debido a que la parte else de un if-else es optativa, existe una ambigüedad cuando un else se omite de una secuencia if anidada. Esto se resuelve al asociar el else con el if sin else anterior más cercano. Por ejemplo, en
if (n > 0) if (a > b) z = a; else z = b;
el else va con el if más interno, como se muestra con el sangrado. Si eso no es lo que se desea, se deben utilizar llaves para forzar la asociación correcta:
if (n > 0) { if (a > b) z = a; } else z = b;
La ambigüedad es especialmente perniciosa en situaciones como esta:
if (n > 0) for (i = 0; i < n; i++) if (s[i] > 0) { printf("..."); return i; } else /* EQUIVOCADO */ printf("error -- n es negativo\n");
El sangrado muestra en forma inequívoca lo que se desea, pero el compilador no entiende el mensaje y asocia el else con el if más interno. Puede ser difícil encontrar esta clase de errores; es una buena idea utilizar llaves cuando hay varios if anidados.
A propósito, nótese que hay un punto y coma ; después de z = a en
if (a > b) z = a; else z = b;
Esto se debe a que gramaticalmente al if le sigue una proposición, y una expresión como “z = a;” siempre se termina con punto y coma ;.
3.3 Else-if
La construcción
if (expresión) proposición else if (expresión) proposición else if (expresión) proposición else if (expresión) proposición else proposición
ocurre de modo tan frecuente que bien vale una pequeña discusión aparte. Esta secuencia de proposiciones if es la forma más general de escribir una decisión múltiple. Las expresiones se evalúan en orden; si cualquier expresión es verdadera, se ejecuta la proposición asociada con ella, y esto termina toda la cadena. Como siempre, el código para cada proposición es una proposición simple o un grupo dentro de llaves.
La parte del último else maneja el caso “ninguno de los anteriores” o caso por omisión cuando ninguna de las otras condiciones se satisface. En algunos casos no hay una acción explícita para la omisión; en ese caso el último
else
proposición
puede omitirse, o puede utilizarse para detección de errores al atrapar una condición “imposible” .
Para ilustrar una decisión de tres vías, se muestra a continuación una función de búsqueda binaria que decide si un valor particular de x se encuentra en el arreglo ordenado v - Los elementos de v deben estar en orden ascendente. La función regresa la posición (un número entre 0 y n-1) si x ocurre en v, y -1 si no es así.
La búsqueda binaria primero compara el valor de entrada x con el elemento medio del arreglo v. Si x es menor que el valor del medio, la búsqueda se enfoca sobre la mitad inferior de la tabla; de otra manera lo hace en la mitad superior, cualquier caso, el siguiente paso es comparar a x con el elemento medio de la mitad seleccionada. Este proceso de dividir en dos continúa hasta que se encuentra el valor o ya no hay elementos.
/* binsearch: encuentra x en v[0] <= v[1] <= ... <= v[n-1] */ { int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low+high)/2; if (x < v[mid]) high = mid + 1; else if (x > v[mid]) low = mid + 1; else /* el elemento fue encontrado */ return mid; } return -1; /* no fue encontrado */ }
La decisión fundamental es si x es menor que, mayor que o igual al elemento medio v[mid] en cada paso; esto es un else-if natural.
- Ejercicio 3-1. Nuestra búsqueda binaria realiza dos pruebas dentro del ciclo, cuando una podría ser suficiente (al precio de más pruebas en el exterior). Escriba una versión con sólo una prueba dentro del ciclo y mida la diferencia en tiempo de ejecución. □
3.4 Switch
La proposición switch es una decisión múltiple que prueba si una expresión coincide con uno de un número de valores constantes enteros, y traslada el control adecuadamente.
switch (expresión) { case exp-const: proposiciones case exp-const: proposiciones default: proposiciones )
Cada case se etiqueta con uno o más valores constantes enteros o expresiones constantes enteras. Si un case coincide con el valor de la expresión, la ejecución comienza allí. Todas las expresiones case deben ser diferentes. El que está etiquetado como default se ejecuta sí ninguno de los otros se satisface. El default es optativo; si no está y ninguno de los casos coincide, no se toma acción alguna. Las cláusulas case y default pueden ocurrir en cualquier orden.
En el capítulo 1 se escribió un programa para contar las ocurrencias de cada dígito, espacio en blanco y todos los demás caracteres, usando una secuencia de if … else if … else. Aquí está el mismo programa con un switch:
#include <stdio.h> main() /* cuenta dígitos, espacios blancos, y otros */ { int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getchar()) != EOF) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ndigit[c-'0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; } } printf("digits ="); for (i = 0; i < 10; i++) printf(" %d", ndigit[i]); printf(", espacio en blanco = %d, otros = %d\n", nwhite, nother); return 0; }
La proposición break provoca una salida inmediata del switch. Puesto que los case sirven sólo como etiquetas, después de que se ejecuta el código para Uno, la ejecución pasa al siguiente, a menos que se tome una acción específica para terminar el switch. Las formas más comunes de dejar un switch son break y return. Una proposición break también se puede emplear para forzar una salida inmediata de los ciclos while, for y do, como se verá más adelante en este capítulo.
Pasar a través de los case es en parte bueno y en parte no. Por el lado positivo, esto permite conectar varios case a una acción simple, como con los dígitos de este ejemplo. Pero eso también implica que cada case normalmente debe terminar con un break para prevenir pasar al siguiente. Pasar de un case a otro no es una práctica muy sólida y es susceptible a la desintegración cuando se modifica el programa. Con la excepción de etiquetas múltiples para un cálculo simple, lo anterior se debe utilizar con cautela y emplear comentarios.
Como formalidad, coloque un break después del último case (en este caso el default) aun si desde ek punto de vista lógico resulta innecesario. Algún día - cuando se agregue otro case al final - esta práctica de programación defensiva lo salvará.
- Ejercicio 3-2. Escriba una función
escape(s,t)que convierte caracteres como nueva línea y tabulación en secuencias de escape visibles como\ny\tmientras copia la cadenatas. Utilice unswitch. Escriba también una función para la dirección inversa, convirtiendo secuencias de escape en caracteres reales. □
3.5 Ciclos - While y For
Ya hemos encontrado los ciclos while y for. En
while (expresión) proposición
la expresión se evalúa. Si es diferente de cero, se ejecuta la proposición y se reevalúa la expresión. Este ciclo continúa hasta que la expresión se hace cero, punto en el cual se suspende la ejecución para continuar después de la proposición.
La proposición for
for (expr1; expr2; expr3) proposición
es equivalente a
expr1; while (expr2) { proposición expr3; }
excepto por el comportamiento de continue que se describe en la sección 3.7.
Gramaticalmente, las tres componentes de un ciclo for son expresiones. Por lo común, expr1 y expr3 son asignaciones o llamadas a función y expr2 es una expresión de relación. Cualquiera de las tres partes se puede omitir, aunque deben permanecer los punto y coma ;. Si se omite expr1 o expr3, sólo se desecha de la expansión. Si la prueba expr2 no está presente, se toma como permanentemente verdadera, así que
for (;;) { ... }
es una iteración “infinita”, que presumiblemente será interrumpida por otros medios, como un break o un return.
El usar while o for es principalmente cuestión de preferencia personal. Por ejemplo, en
while ((c = getchar()) == ' ' || c == '\n' || c = '\t') ; /* ignora caracteres espaciadores */
no hay inicialización o reinicialización, por lo que el while es más natural.
El for se prefiere cuando existe una inicialización simple e incrementos, puesto que mantiene las proposiciones de control del ciclo juntas y visibles al principio del mismo. Esto es más obvio en
for (i = 0; i < n; i++) ...
que es la forma característica de procesar los primeros n elementos de un arreglo en C, lo análogo al ciclo DO de Fortran o al for de Pascal. Sin embargo, la analogía no es perfecta puesto que tanto el índice como el límite de un ciclo for en C pueden ser alterados desde dentro del ciclo, y la variable del Índice retiene su valor cuando por cualquier razón terminan las iteraciones. Puesto que las componentes del for son expresiones arbitrarias, sus ciclos no están restringidos a progresiones aritméticas. Por otra parte, considere que es un mal estilo incluir en las secciones de inicialización e incremento operaciones no relacionadas con esas actividades, que más bien se reservan para acciones de control del ciclo.
Como un ejemplo más amplio, aquí está otra versión de atoi para convertir una cadena a su equivalente numérico. Esta es ligeramente más general que la del capítulo 2; trata también los espacios en blanco previos al número, y los signos + o -. (El capitulo 4 muestra atof, que realiza la misma conversión para números de punto flotante).
La estructura del programa refleja la forma de la entrada:
- ignora espacios en blanco, si los hay
- toma el signo, si lo hay
- toma la parte entera y conviértela
Cada paso realiza su parte, y deja las cosas en forma clara para el siguiente. La totalidad del proceso termina con el primer carácter que no pueda ser parte de un número.
#include <ctype.h> /* atoi: convierte s a entero; versión 2 */ int atoi(char s[]) { int i, n, sign; for (i = 0; isspace(s[i]); i++) /* ignora espacio en blanco * / ; sign = (s[i] == '-') ? -1 : 1; if (s[i] == '+' || s[i] == '-') /* ignora el signo */ i++; for (n = 0; isdigit(s[i]); i++) n = 10 * n + (s[i] - '0'); return sign * n; }
La biblioteca estándar proporciona una función más elaborada, strtol, para la conversión de cadenas a enteros largos; véase la sección 5 del apéndice B.
Las ventajas de mantener centralizado el control del ciclo son aún más obvias cuando existen ciclos anidados. La siguiente función es una clasificación Shell para ordenar un arreglo de enteros. La idea básica de este algoritmo de ordenamiento - inventado en 1959 por D.L. Shell - es que en las primeras etapas sean comparados elementos lejanos (en lugar de los adyacentes, como en los ordenamientos de intercambio más simples). Esto tiende a eliminar rápidamente gran cantidad de desorden, así que los estados posteriores tienen menos trabajo por hacer. El intervalo entre los elementos comparados disminuye en forma gradual hasta uno, punto en el que el ordenamiento viene a ser efectivamente un método adyacente de intercambio.
/* shellsort: ordena v[0]...v[n-1] en orden ascendente */ void shellsort(int v[], int n) { int gap, i, j, temp; for (gap = n/2; gap > 0; gap /= 2) for (i = gap; i < n; i++) for (j=i-gap; j>=0 && v[j]>v[j+gap]; j-=gap) { temp = v[j]; v[j] = v[j+gap]; v[j+gap] = temp; } }
Existen tres ciclos anidados. El más externo controla el espacio entre los elementos comparados, reduciéndolo desde n/2 por un factor de dos en cada paso hasta que llega a cero. El ciclo intermedio recorre los elementos. El ciclo más interno compara cada pareja de elementos que está separada por el espacio gap e invierte a las que estén desordenadas. Puesto que gap finalmente se reduce a uno, todos los elementos se ordenan correctamente. Nótese cómo la generalidad del for hace que el ciclo más externo coincida con la forma de los otros, aun cuando no es una progresión aritmética.
Un último operador de C es la coma , que frecuentemente encuentra uso en la proposición for. Una pareja de expresiones separadas por una coma se evalúa de izquierda a derecha, y el tipo y valor del resultado son el tipo y valor del operando derecho. Así, en una proposición for es posible colocar expresiones múltiples en las diferentes partes, por ejemplo, para procesar dos índices en paralelo. Esto se ilustra en la función reverse(s), que invierte a la cadena s en el mismo lugar.
#include <string.h> /* reverse: invierte la cadena s en el mismo lugar */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } }
Las comas que separan a los argumentos de una función, las variables en declaraciones, etc., no son operadores coma, y no garantizan evaluación de izquierda a derecha.
Los operadores coma deberán utilizarse poco. Los usos más adecuados son en construcciones fuertemente relacionadas una con la otra, como en el ciclo for de reverse, y en macros en donde un cálculo de paso múltiple debe ser una expresión simple. Una expresión coma podría también ser apropiada para el intercambio de elementos en reverse, donde el intercambio puede ser a través de una operación simple:
for (i = 0, j = strlen(s)-1; i < j; i++, j--) c = s[i], s[i] = s[j], s[j] = c;
- Ejercicio 3-3. Escriba la función
expand(sl,s2)que expande notación abreviada comoa-z, que viene en la cadenas1, en la lista equivalente completaabc…xyzens2. Permita letras mayúsculas y minúsculas, así como dígitos, y esté preparado para manejar casos comoa-b-cya-z0-9y—a—z. Haga que los guiones-al inicio o al final se tomen literalmente. □
3.6 Ciclos - do-while
Como ya se expuso en el capítulo 1, los ciclos while y for verifican al principio la condición de término. En contraste, el tercer ciclo en C, el do-while, prueba al final después de realizar cada paso a través del cuerpo del ciclo, el cual se ejecuta siempre por lo menos una vez.
La sintaxis del do es
do proposición while (expresión);
La proposición se ejecuta y después se evalúa la expresión. Si es verdadera, la proposición se evalúa de nuevo, y así sucesivamente. Cuando la expresión se hace falsa, el ciclo termina. Excepto por el sentido de la prueba, el do-while es equivalente a la proposición repeat-until de Pascal.
La experiencia demuestra que el do-while es mucho menos utilizado que el while y el for. Aunque de cuando en cuando es valioso, como en la siguiente función itoa, que convierte un número a una cadena de caracteres (lo inverso de atoi). El trabajo es ligeramente más complicado de lo que podría pensarse en un
principio, debido a que los métodos fáciles para generar dígitos los generan en el orden incorrecto. Hemos elegido generar la cadena al revés y después invertirla.
/* itoa: convierte n a caracteres en s */ void itoa(int n, char s[]) { int i, sign; if ((sign = n) < 0) /* registra el signo */ n = -n; /* hace a n positivo */ i = 0; do { /* genera digitos en orden inverso */ s[i++] = n % 10 + '0'; /* obtiene siguiente dígito */ } while ((n /= 10) > 0); /* lo borra */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; reverse(s); }
El do-while es necesario, o al menos conveniente, puesto que por lo menos se debe instalar un carácter en el arreglo s, aun si n es cero. También empleamos llaves alrededor de la proposición simple que hace el cuerpo del do-while, aunque son innecesarias, y así el lector apresurado no confundirá la sección del while con el comienzo de un ciclo while.
- Ejercicio 3-4. En una representación de números en complemento a dos, nuestra versión de
itoano maneja el número negativo más grande, esto es, el valor denigual a-( 2^tamañopalabra-1). Explique por qué. Modifíquelo para imprimir el valor correctamente, sin importar la máquina en que ejecute. □ - Ejercicio 3-5. Escriba la función
itob(n,s,b)que convierte al enteronen una representación de caracteres con basebdentro de la cadenas. En particular,itob(n,S,16)da formato ascomo un entero hexadecimal ens. □ - Ejercicio 3-6. Escriba una versión de
itoaque acepte tres argumentos en lugar de dos. El tercer argumento es un ancho mínimo de campo; si es necesario, al número convertido debe agregársele caracteres en blanco a la izquierda para hacerlo lo suficientemente ancho. □
3.7 Break y Continue
Algunas veces es conveniente tener la posibilidad de abandonar un ciclo de otra manera que no sea probando al inicio o al final. La proposición break proporciona una salida anticipada de un for, while y do, tal como lo hace el switch.
Un break provoca que el ciclo o switch más interno que lo encierra termine inmediatamente.
La siguiente función, trim, elimina espacios blancos, tabuladores y nuevas líneas al final de una cadena, utilizando un break para salir de un ciclo cuando se encuentra el no-blanco, no-tabulador o no-nueva línea de más a la derecha.
/* trim: elimina blancos, tabuladores y nueva linea al final */ int trim(char s[]) { int n; for (n = strlen(s)-1; n >= 0; n--) if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') break; s[n+1] = '\0'; return n; }
strlen regresa la longitud de la cadena. El ciclo for inicia al final y rastrea hacia atrás, buscando el primer carácter que no sea blanco o tabulador o nueva linea.
El ciclo se interrumpe cuando se encuentra alguno o cuando n se hace negativa (esto es, cuando se ha rastreado toda la cadena. Se deberá verificar que este comportamiento es correcto, aun cuando la cadena esté vacía o sólo contiene espacios en blanco.
La proposición continue está relacionada con el break, pero se utiliza menos; provoca que inicie la siguiente iteración del ciclo for, while o do que la contiene. Dentro de while y do, esto significa que la parte de la prueba se ejecuta inmediatamente; en el for, el control se traslada al paso de incremento. La proposición continué se aplica solamente a ciclos, no a switch. Un continue dentro de un switch que está a su vez en un ciclo, provoca la siguiente iteración del ciclo.
Como un ejemplo, el siguiente fragmento procesa sólo los elementos no negativos que están en el arreglo a; los valores negativos son ignorados.
for (i = 0; i < n; i++) if (a[i] < 0) /* ignora elementos negativos */ continue; ... /* trabaja elementos positivos */
La proposición continue se emplea a menudo cuando la parte del ciclo que sigue es complicada, de modo que invertir la prueba y sangrar otro nivel podría anidar profundamente el programa.
3.8 Goto y Etiquetas
C proporciona la infinitamente abusable proposición goto, y etiquetas para saltar hacia ellas. Formalmente, el goto nunca es necesario, y en la práctica es casi siempre más fácil escribir código sin él. En este libro no se ha usado goto alguno.
Sin embargo, hay algunas situaciones donde los goto pueden encontrar un lugar. La más común es abandonar el procesamiento en alguna estructura profundamente anidada, tal como salir de dos o más ciclos a la vez. La proposición break no se puede utilizar directamente, puesto que sólo sale del ciclo más interno. Así:
for ( ... ) for ( ... ) { ... if (desastre) goto error; } ... error: /* arregla el desorden /*
Esta organización es útil si el código de manejo de error no es trivial y si los errores pueden ocurrir en varios lugares.
Una etiqueta tiene la misma forma que un nombre de variable y es seguida por dos puntos :. Puede ser adherida a cualquier proposición de la misma función en la que está el goto. El alcance de una etiqueta es toda la función.
Como otro ejemplo, considérese el problema de determinar si dos arreglos, a y b, tienen un elemento en común. Una posibilidad es
for (i = 0; i < n; i++) for (j = 0; j < m; j++) if (a[i] == b[j]) goto encontrado; /* no se encontró ningún elemento común */ ... encontrado: /* se encontró uno: a[i] == b[j] */ ...
El código que involucra un goto siempre puede escribirse sin él, aunque tal vez al precio de algunas pruebas repetidas o variables extra. Por ejem plo, la búsqueda en los arreglos quedará
encontrado = 0; for (i = 0; i < n && !encontrado; i++) for (j = 0; j < m && !encontrado; j++) if (a[i] == b[j]) encontrado = 1; if (encontrado) /* Se encontró uno: a[i-1] == b[j-1] */ ... else /* no se encontró ningún elemento común */ ...
Con pocas excepciones, como las citadas aquí, el código que se basa en proposiciones goto es generalmente más difícil de entender y de mantener que el código sin ellas. Aunque no somos dogmáticos acerca del asunto, se ve que las proposiciones goto deben ser utilizadas raramente, si acaso.
Capitulo 4: Funciones y la estructura del programa
Las funciones dividen tareas grandes de computación en varias más pequeñas, y permiten la posibilidad de construir sobre lo que otros ya han hecho, en lugar de comenzar desde cero. Las funciones apropiadas ocultan los detalles de operación de las partes del programa que no necesitan saber acerca de ellos, así que dan claridad a la totalidad y facilitan la penosa tarea de hacer cambios.
El lenguaje C se diseñó para hacer que las funciones fueran eficientes y fáciles de usar; los programas escritos en C se componen de muchas funciones pequeñas en lugar de sólo algunas grandes. Un programa puede residir en uno o más archivos fuente, los cuales pueden compilarse por separado y cargarse junto con funciones de biblioteca previamente compiladas. No trataremos aquí tales procesos, puesto que los detalles varían de un sistema a otro.
La declaración y definición de funciones es el área donde el estándar ANSI ha hecho los cambios más visibles a C. Tal como mencionam os en el capítulo 1, ahora es posible declarar los tipos de los argumentos cuando se declara una función. La sintaxis de la definición de funciones también cambia, de modo que las declaraciones y las definiciones coincidan. Esto hace posible que el compilador pueda detectar muchos más errores de lo que podía anteriormente. Además, cuando los argumentos se declaran con propiedad, se realizan automáticamente las conversiones convenientes.
El estándar clarifica las reglas sobre el alcance de los nombres; en particular, requiere que sólo haya una definición de cada objeto externo. La inicialización es más general: los arreglos y las estructuras automáticas ahora se pueden inicializar.
El preprocesador de C también se ha mejorado. Las nuevas facilidades del Procesador incluyen un conjunto más completo de directivas para la compilación condicional, una forma de crear cadenas entrecomilladas a partir de argumentos de macros y un mejor control sobre el proceso de expansión de macros.
4.1 Conceptos básicos de funciones
Para comenzar, diseñemos y escribamos un programa que imprim a cada línea de su entrada que contenga un “patró n” o cadena de caracteres en particular.
(Este es un caso especial del programa grep de UNIX.) Por ejem plo, al buscar el patrón de letras “ould.” en el conjunto de líneas
Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire, Would not we shatter it to bits -- and then Re-mould it nearer to the Heart's Desire!
producirá la salida
Ah Love! could you and I with Fate conspire Would not we shatter it to bits -- and then Re-mould it nearer to the Heart's Desire!
El trabajo se ajusta ordenadamente en tres partes:
while {hay otra línea) if {la línea contiene el patrón) imprímela
Aunque ciertamente es posible poner el código de todo esto en main, una mejor forma es aprovechar la estructura haciendo de cada parte una función separada. Es más fácil trabajar con tres piezas pequeñas que con una grande, debido a que los detalles irrelevantes se pueden ocultar dentro de las funciones, y minimizar así el riesgo de interacciones no deseadas. Los fragmentos incluso se pueden emplear en otros programas.
“Mientras hay otra línea” es getline, función que ya escribimos en el capítulo 1, e “imprímela” es printf, que alguien ya nos proporcionó. Esto significa que sólo necesitamos escribir una rutina para decidir si la línea contiene una ocurrencia del patrón.
Podemos resolver ese problema escribiendo una función strindex(s,t), que regresa la posición o índice en la cadena s en donde comienza la cadena t, o -1 si s no contiene t. Debido a que los arreglos en C principian en la posición cero, los índices serán cero o positivos, y así un valor negativo como -1 es conveniente para señalar una falla. Cuando posteriormente se necesite una coincidencia de patrones más elaborada, sólo se debe reemplazar strindex; el resto del código puede permanecer igual. (La biblioteca estándar provee una función strstr que es semejante a strindex, excepto en que regresa un apuntador en lugar de un índice.)
Una vez definido todo este diseño, llenar los detalles del programa es simple. Aquí está en su totalidad, de modo que se puede ver cómo las piezas quedan juntas. Por ahora, el patrón que se buscará es una cadena literal, lo cual no es el mecanismo más general. Regresaremos en breve a una discusión sobre cómo inicializar arreglos de caracteres, y en el capítulo 5 mostraremos cómo hacer que el patrón de caracteres sea un parámetro fijado cuando se ejecuta el programa.
También hay una versión ligeramente diferente de getline, que se podrá comparar con la del capítulo 1.
# include <stdio.h> #define MAXILINE 1000 /* longitud máxima por línea de entrada */ int getline(char line[], int max) int strindex(char source[], char searchfor[]); char pattern[] = "ould"; /* patrón a buscar */ /* encontrar todas las líneas que coincidan con el patrón */ main() { char line[MAXLINE]; int found = 0; while (getline(line, MAXLINE) > 0) if (strindex(line, pattern) >= 0) { printf("%s", line); found++; } return found; }
/* getline: pone linea en s, retorna longitud */ int getline(char s[], int lim) { int c, i; i = 0; while (--lim > 0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if (c == '\n') s[i++] = c; s[i] = '\0'; return i; }
/* strindex: retorna el índice de t en s, -1 si no hay */ int strindex(char s[], char t[]) { int i, j, k; for (i = 0; s[i] != '\0'; i++) { for (j=i, k=0; t[k]!='\0' && s[j]==t[k]; j++, k++) ; if (k > 0 && t[k] == '\0') return i; } return -1; }
Cada definición de función tiene la forma
tipo-regresado nombre-de-función(declaraciones de argumentos) { declaraciones y proposiciones }
Varias partes pueden omitirse; una función mínima “dummy” es
nada() {}
que no hace ni regresa nada. Una función hacer-nada, como ésta, es algunas veces útil para reservar lugar al desarrollar un programa. Si el tipo que regresa se omite, se supone int.
Un programa es sólo un conjunto de definiciones de variables y funciones. La comunicación entre funciones es por argumentos y valores regresados por las funciones, y a través de variables externas. Las funciones pueden presentarse en cualquier orden dentro del archivo fuente, y el programa fuente se puede dividir en varios archivos, mientras las funciones no se dividan.
La proposición return es el mecanismo para que la función que se llama regrese un valor a su invocador. Al return le puede seguir cualquier expresión:
return expresión
La expresión se convertirá al tipo de retorno de la función si es necesario. Con frecuencia se utilizan paréntesis para encerrar la (expresión), pero son optativos.
La función que llama tiene la libertad de ignorar el valor regresado. Incluso, no hay necesidad de un a expresión después de return; en tal caso, ningún valor regresa al invocador. También el control regresa, sin valor, cuando la ejecución “cae al final” de la función al alcanzar la llave cerrada derecha }. No es ilegal,
aunque probablemente un signo de problemas, el que una función regrese un valor desde un lugar y ninguno desde otro. En cualquier caso, si una función no regresa explícitamente un valor, su “valor” es ciertamente basura.
El programa de búsqueda del patrón regresa un estado desde main, el número de coincidencias encontradas. Este valor está disponible para ser empleado por el medio ambiente que llamó al programa.
El mecanismo de cómo compilar y cargar un programa en C que reside en varios archivos fuente varía de un sistema a otro. En el sistema UNIX, por ejemplo, la orden cc mencionada en el capítulo 1 hace el trabajo. Suponiendo que las tres funciones se almacenan en tres archivos llamados main.c, getline.c, y strindex.c. Entonces la orden
cc main.c getline.c strindex.c
compila los tres archivos, sitúa el código objeto resultante en los archivos main.o, getline.o, y strindex.o, y después los carga todos dentro de un archivo ejecutable llamado a.out. Si existe un error - digamos en main.c - dicho archivo puede volverse a compilar por sí mismo y el resultado cargado con los archivos objeto previos, con la orden.
cc main.c getline.o strindex.o
cc emplea la convención “.c” contra “.o” para distinguir los archivos fuente de los archivos objeto.
- Ejercicio 4-1. Escriba la función
strrindex(s,t), que regresa la posición de la ocurrencia de más a la derecha detens, o-1si no hay alguna.
4.2 Funciones que regresan valores no enteros
Basta ahora los ejemplos de funciones han regresado o ningún valor (void) o un int. ¿Qué pasa si una función debe regresar algo de otro tipo? Muchas funciones numéricas como sqrt, sin y cos regresan double; otras funciones especializadas regresan tipos diferentes. Para ilustrar cómo tratar con esto, escribamos y usemos la función atof(s), que convierte la cadena s a su valor equivalente de punto flotante de doble precisión. La función atoi es una extensión de atoi, de la que mostramos versiones en los capítulos 2 y 3. Maneja signo y punto decimal optativos, y presencia o ausencia de parte entera o fraccionaria. Nuestra versión
no es una rutina de conversión de alta calidad; tomaría más espacio del que podemos dedicarle. La biblioteca estándar incluye un atof; el header <math.h> la declara.
Primero, atof por sí misma debe declarar el tipo del valor que regresa, puesto que no es int. El nombre del tipo precede al nombre de la función:
#include <ctype.h> /* atof: convierte la cadena s a double */ double atof(char s[]) { double val, power; int i, sign; for (i = 0; isspace(s[i]); i++) /* saltea espacio en blanco */ ; sign = (s[i] == '-') ? -1 : 1; if (s[i] == '+' || s[i] == '-') i++; for (val = 0.0; isdigit(s[i]); i++) val = 10.0 * val + (s[i] - '0'); if (s[i] == '.') i++; for (power = 1.0; isdigit(s[i]); i++) { val = 10.0 * val + (s[i] - '0'); power *= 10; } return sign * val / power; }
Segundo, e igualmente importante, la rutina que llama debe indicar que atof regresa un valor que no es int. Una forma de asegurar esto es declarar atof explícitamente en la rutina que la llama. La declaración se muestra en esta primitiva calculadora (apenas adecuada para un balance de chequera), que lee un número por
línea, precedido en forma optativa por un signo, y lo acumula, imprimiendo la suma actual después de cada entrada:
#include <stdio.h> #define MAXLINE 100 /* calculadora rudimentaria * / main() { double sum, atof(char []); char line[MAXLINE]; int getline(char line[], int max); sum = 0; while (getline(line, MAXLINE) > 0) printf("\t%g\n", sum += atof(line)); return 0; }
La declaración
double sum, atof(char []);
señala que sum es una variable double, y que atof es una función que toma un argumento char[] y regresa un double.
La función atof se debe declarar y definir consistentemente. Si atof en sí misma y la llamada a ella en main tienen tipos inconsistentes dentro del mismo archivo fuente, el error será detectado por el compilador. Pero si (como es probable) atof fuera compilada separadamente, la falta de consistencia no se detectaría, atof regresaría un valor double que main trataría como int, y se producirían resultados incongruentes.
A la luz de lo que hemos mencionado acerca de cómo deben coincidir las declaraciones con las definiciones, esto podría ser sorprendente. La razón de que ocurra una falta de coincidencia es que, si no existe el prototipo de una función, ésta es declarada implícitam ente la primera vez que aparece en una expresión, como
sum += atof(line)
Si en una expresión se encuentra un nombre que no ha sido declarado previamente y está seguido por paréntesis izquierdo, se declara por contexto, de modo que se supone que es el nombre de una función que regresa un int, y nada se supone acerca de sus argumentos. Aún más, si la declaración de una función no incluye argumentos como en
double atof();
también es tomada de modo que no se supone nada acerca de los argumentos de atof; se desactiva toda revisión de parámetros. Este significado especial de la lista de argumentos vacía se hace para permitir que los programas en C viejos se compilen con los nuevos compiladores. Pero es una mala táctica usar esto con programas nuevos. Si la función toma argumentos, declárelos; si no los toma, use void.
Dado atof, propiamente declarado, podemos escribir atoi (convierte una cade
na a int) en términos de él:
/* atoi: convierte la cadena s a entero usando atof */ int atoi(char s[]) { double atof(char s[]); return (int) atof(s); }
Nótese la estructura de las declaraciones y la proposición return. El valor de la expresión en
return expresión;
se convierte al tipo de la función antes de que se tome el return. Por lo tanto, el valor de atof, un double, se convierte automáticamente a int cuando aparece en este return, puesto que la función atoi regresa un int. Sin embargo, esta operación potencialmente descarta información, de manera que algunos compiladores lo previenen. El cast establece explícitamente lo que la operación intenta y suprime las advertencias.
- Ejercicio 4-2. Extienda
atofpara que maneje notación científica de la forma123.45e-6donde un número de punto flotante puede ir seguido poreoEy opcionalmente un exponente con signo. □
4.3 Variables Externas
4.4 Reglas y Alcance
4.5 Archivo de encabezamiento header
4.6 Variables estáticas
4.7 Variables tipo registro
4.8 Estructura de bloques
4.9 Inicialización
4.10 Recursividad
4.11 El preprocesador de C
4.1.1 Inclusión de archivos
4.1.2 Substitución de macros
4.1.3 Inclusión condicional
Capitulo 5: Apuntadores y arreglos
5.1 Apuntadores y Direcciones
5.2 Apuntadores y argumentos de funciones
5.3 Apuntadores y arreglos
5.4 Aritmética de direcciones
5.5 Apuntadores a caracteres, y funciones
5.6 Arreglos de apuntadores; apuntadores a apuntadores
5.7 Arreglos muitidimensionales
5.8 Inicialización de arreglos de apuntadores
5.9 Apuntadores vs. arreglos muitidimensionales
5.10 Argumentos en la línea de órdenes
5.11 Apuntadores a funciones
5.12 Declaraciones complicadas
Capítulo 6: Estructuras
Una estructura es una colección de una o más variables, de tipos posiblemente diferentes, agrupadas bajo un solo nombre para manejo conveniente. (Las estructuras se conocen como “ records” en algunos otros lenguajes, principalm ente Pascal.) Las estructuras ayudan a organizar datos complicados, en particular dentro de programas grandes, debido a que permiten que a un grupo de variables relacionadas se les trate como una unidad en lugar de como entidades separadas.
Un ejemplo tradicional de estructura es el registro de una nómina: un empleado está descrito por un conjunto de atributos, como nombre, domicilio, número del seguro social, salario, etc. Algunos de estos atributos pueden, a su vez, ser estructuras: un nombre tiene varios componentes, como los tiene un domicilio y aún un salario. Otro ejemplo, más típico para C, procede de las gráficas: un punto es un par de coordenadas, un rectángulo es un par de puntos, y otros casos semejantes.
El principal cambio realizado por el estándar ANSI es la definición de la asignación de estructuras: las estructuras se pueden copiar y asignar, pasar a funciones y ser regresadas por funciones. Esto ha sido manejado por muchos compiladores durante varios años, pero las propiedades están ahora definidas en forma precisa. Las estructuras y los arreglos automáticos ahora también se pueden inicializar.
6.1 Conceptos básicos sobre estructuras
6.2 Estructuras y funciones
6.3 Arreglos de estructuras
6.4 Apuntadores a estructuras
6.5 Estructuras autorreferenciadas
6.6 Búsqueda en tablas
6.7 Typedef
6.8 Uniones
6.9 Campos de bits
Capítulo 7:
Las operaciones de entrada y salida no son en si parle del lenguaje C, por lo que hasta ahora no las hemos destacado. Sin embargo, los programas interactúan con su medio ambiente en formas mucho más complicadas de las que hemos mostrado antes. En este capítulo describiremos la biblioteca estándar, un conjunto de funciones que proporcionan entrada y salida, manipulación de cadenas, manejo de memoria, rutinas matemáticas y una variedad de otros servicios para programas en C, aunque haremos hincapié en la entrada y salida.
El estándar ANSI define de manera precisa estas funciones de biblioteca, de modo que pueden existir en forma compatible en cualquier sistema en donde exista C. Los programas que restringen su interacción con el sistema a las facilidades provistas por la biblioteca estándar pueden ser llevados de un sistema a otro sin cambios.
Las propiedades de las funciones de biblioteca están especificadas en más de una docena de headers; ya hemos visto algunos, incluyendo <stdio.h>, <string.h> y <ctype.h>. No presentaremos aquí la totalidad de la biblioteca, puesto que estamos más interesados en escribir programas en C que los usan. La biblioteca se describe en detalle en el apéndice B.
7.1 Entrada y salida estándar
7.2 Salida con formato - printf
Tabla 7-1. Conversiones básicas de Printf
7.3 Listas de argumentos de longitud variable
7.4 Entrada con formato — scanf
Tabla 7-2 - Conversiones básicas de scanf
| Caracter | Dato de entrada:tipo de argumento |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
|
7.5 Acceso a archivos
7.6 Manejo de errores—stderr y exit
7.7 Entrada y salida de líneas
7.8 Otras funciones
La biblioteca estándar proporciona una amplia variedad de funciones. Esta sección es una breve sinopsis de las más útiles. En el apéndice B pueden encontrarse más detalles y muchas otras funciones.
7.8.1 Operaciones sobre cadenas
7.8.2 Prueba y conversión de clases de caracteres
7.8.3 Ungete
7.8.4 Ejecución de órdenes
7.8.5 Administración del almacenamiento
7.8.6 Funciones matemáticas
7.8.7 Generación de números aleatorios
Capítulo 8: La interfaz con el sistem a UNIX
El sistema operativo UNIX proporciona sus servicios a través de un conjunto de llamadas al sistema, que consisten en funciones que están dentro del sistema operativo y que pueden ser invocadas por programas del usuario. Este capitulo describe cómo emplear algunas de las más importantes llamadas al sistema desde programas en C. Si el lector usa UNIX, esto debe serle directamente útil, debido a que algunas veces es necesario emplear llamadas al sistema para tener máxima eficiencia, o para tener acceso a alguna facilidad que no esté en la biblioteca.
Incluso, si se emplea C en un sistema operativo diferente el lector debería ser capaz de adentrarse en la programación estudiando estos ejemplos; aunque los detalles varían, se encontrará un código semejante en cualquier sistema. Puesto que la biblioteca de C ANSI está en muchos casos modelada con base en las facilidades de UNIX , este código puede ayudar también a su entendimiento.
El capítulo está dividido en tres partes fundamentales: entrad a/salida, sistema de archivos y asignación de almacenamiento. Las primeras dos partes suponen una modesta familiaridad con las características externas de los sistemas UNIX.
El capítulo 7 tuvo que ver con una interfaz de entrada/salida uniforme entre sistemas operativos. En cualquier sistema las rutinas de la biblioteca estándar se tienen que escribir en términos de las facilidades proporcionadas por el sistema anfitrión. En las secciones de este capítulo describiremos las llamadas al sistema UNIX para entrada y salida, y mostraremos cómo puede escribirse parte de la biblioteca estándar con ellas.
8.1 Descriptores de Archivos
8.2 E/S de bajo nivel — read y write
8.3 Open, creat, close, unlink
8.4 Acceso aleatorio — lseek
8.5 Ejemplo - Una realización de fopen y getc
8.6 Ejemplo — listado de directorios
8.7 Ejemplo - Asignador de memoria
Apéndice A: Manual de Referencia
A1: Introducción
Este manual describe al lenguaje C tal como se especifica en Draft Proposed American National Standard for Information Systems — Programming Language C, documento número X3J11/88-001, con fecha 11 de enero de 1988. Este borrador no es el estándar final, y todavía es posible que ocurran algunos cambios en el lenguaje. Así pues, este manual no describe la definición final del lenguaje. Más aún es una interpretación del borrador propuesto del estándar, no el estándar en sí, aunque se ha tenido cuidado de hacerlo una guía confiable.
En su mayor parte, este manual sigue la línea amplia del borrador estándar, que a su vez sigue la de la primera edición de este libro, aunque la organización difiere en el detalle. Excepto por renombrar algunas producciones y porque no se formalizan las definiciones de los componentes léxicos o del preprocesador, la gramática dada aquí para el lenguaje es equivalente a la del borrador actual.
En este manual, el material comentado se encuentra sangrado y escrito en un tipo más pequeño, como este. A menudo estos comentarios resaltan las formas en las que el estándar ansí de C difiere del lenguaje definido por la primera edición de este libro, o de refinamientos introducidos posteriormente en varios compiladores.
Apéndice B: Biblioteca Estándar
Apéndice C: Resúmen de Modificaciones
Desde la publicación de la primera edición de este libro, la definición del lenguaje C ha sufrido modificaciones. Casi todas fueron extensiones al lenguaje original, y fueron diseñadas cuidadosamente para permanecer compatibles con la práctica existente; algunas repararon ambigüedades de la descripción original, y otras representan modificaciones de la práctica existente. Muchas de las nuevas características se anunciaron en los documentos que acompañan a los compiladores disponibles de AT&T, y posteriormente se han adoptado por otros proveedores de compiladores del lenguaje C. Recientemente, el comité ANSI incorporó más de esos cambios estandarizando el lenguaje, y también introdujo otras modificaciones significativas. Su reporte fue en parte anticipado por algunos compiladores comerciales aún antes de la publicación del estándar formal.
Este apéndice resume las diferencias entre el lenguaje definido por la primera edición de este libro, y lo esperado como la definición del estándar final. Trata solamente al lenguaje en sí, no a su entorno ni a su biblioteca; aunque esas son partes importantes del estándar, hay poco con qué compararlas, puesto que en la primera edición no se intentó definirlas.
- El preprocesamiento está definido más cuidadosamente en el Estándar que en la primera edición, y está extendido: está explícitamete basado en tokens (símbolos); existen nuevos operadores para la concatenación de tokens (# # ) y creación de cadenas (#); hay nuévas líneas de control como #elif y #pragm a; está explícitamente permitida la redeclaración de macros por la misma secuencia de tokens; ya no se reemplazan los parámetros que están dentro de cadenas. La separación de líneas por \ está permitida en cualquier lugar, no sólo en definiciones de cadenas y macros. Véase §A12.
- El significado mínimo el más pequeño de todos los identificadores internos se incrementó a 31 caracteres; permitido para identificadores con liga externo permanece en 6 letras, sin importar sin son mayúsculas o minúsculas (muchas implantaciones proporcionan más).
- Las secuencias trigráficas introducidas por ?? permiten la representación de caracteres que no se encuentran en algunos conjuntos. Están definidos los escapes para
#\'[']{}¡\. Véase §A12.1. Obsérvese que la introducción de trigrafos puede cambiar el significado
de cadenas que contengan la secuencia ??.
- Se introdujeron nuevas palabras reservadas (void, const, volatile, signed. enum). La palabra reservada entry, que nunca se puso en uso, fue retirada.
- Se definen nuevas secuencias de escape para uso dentro de constantes de carácter y cadenas literales. El efecto de seguir
\con un carácter que no sea parte de una secuenciade escape aprobada está indefinido. Véase §A2.5.2.
