Capitulo 2: Tipos, Operadores y Expresiones

Las variables y las constantes son los objetos de datos basicos que se manipu­lan en un programa. Las declaraciones muestran las variables que se van a utilizar y establecen el tipo que tienen y algunas veces cuales son sus valores ini­ciales. Los operadores especifican lo que se hara con las variables. Las expresio­nes combinan variables y constantes para producir nuevos valores. El tipo de un objeto determina el conjunto de valores que puede tener y que operaciones se pue­den realizar sobre el. Estos son los temas de este capitulo.

El estandar ANSI ha hecho muchos pequenos cambios y agregados a los ti­pos basicos y a las expresiones. Ahora hay formas signed y unsigned de todos los tipos enteros, y notaciones para constantes sin signo y constantes de caracter hexadecimales. Las operaciones de coma flotante pueden hacerse en precision sen­cilla; tambien hay un tipo long double para precision extendida. Las constantes de cadena pueden concatenarse al tiempo de comilacion. Las enumeraciones son ya parte del lenguaje, formalizando una caracteristica pendiente por mucho tiempo. Los objetos pueden ser declarados const, lo que impide que cambien. Las reglas para conversion automatica entre tipos aritmeticos fueron aumentadas pa­ra manejar el conjunto de tipos mas rico actual.

2.1 Nombres de variables

Aunque no lo mencionamos en el capitulo 1, existen algunas restricciones en los nombre de las variables y de las constantes simbolicas. Los nombres se componen de letras y digitos; el primer caracter debe ser una letra. El caracter de sub­rayado "_" cuenta como una letra; algunas veces es util para mejorar la legibili­dad de nombres largos de variables. Sin embargo, no se debe comenzar los nombres de variables con este caracter, puesto que las rutinas de biblioteca con frecuencia usan tales nombres. Las letras mayusculas y minusculas son distintas, de tal manera que x y X son dos nombres diferentes. La practica tradicional de C es usar letras minusculas para nombres de variables, y todo en mayusculas para constantes simbolicas.

Al menos los primeros 31 caracteres de un nombre interno son significativos, para nombres de funciones y variables externas el numero puede ser menor que 31, puesto que los nombres externos los pueden usar los ensambladores y los car­gadores, sobre los que el lenguaje no tiene control. Para nombres externos, el es­tandar garantiza distinguir solo 6 caracteres (y sin diferenciar mayusculas de minusculas). 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 minusculas.

Es conveniente elegir nombres que esten relacionados con el proposito de la variable, que no sea probable confundirlos tipograficamente. Por estilo, nosotros tende­mos a utilizar nombres cortos para variables locales (especialmente indices de ite­raciones), y nombres mas largos para variables externas.

2.2 Tipos y tamanos de datos

Hay unos cuantos tipos de datos basicos en C:

char un solo byte, capaz de contener un caracter del conjunto de caracteres local.
int un entero, normalmente del tamano natural de los enteros en la maquina en la que se ejecuta.
float punto flotante de precision normal.
double punto flotante de doble precision.

Ademas, existen algunos calificadores que se aplican a estos tipos basicos, short y long se aplican a enteros:

short int sh;
long int counter;

La palabra int puede omitirse de tales declaraciones, lo que tipicamente se hace.

La intencion es que short y long puedan proporcionar diferentes longitudes de enteros donde sea practico; int sera normalmente el tamano natural para una maquina 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 tamanos apropiados para su propio hardware, sujeto solo a la restriccion 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 numeros unsigned son siempre positivos o cero y obedecen las leyes de la aritmetica modulo 2", donde n es el numero de bits en el tipo. Asi, 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 maquina de complemento a dos). El hecho de que los chars ordinarios sean con signo o sin el depende de la maquina, pero los caracteres que se pueden imprimir son siempre positivos.

El tipo long double especifica coma flotante de precision extendida. Igual que con los enteros, los tamanos de objetos de coma flotante se definen en la implantacion; float, double y long double pueden representar uno, dos o tres tamanos distintos.

Los archivos de encabezado headers estandar <limits.h> y <float.h> con­tienen constantes simbolicas para todos esos tamanos, junto con otras propie­dades de la maquina y del compilador, los cuales se discuten en el apendice B.

  • Ejercicio 2-1. Escriba un programa para determinar los rangos de variables char, short, int y long, tanto signed como unsigned, imprimiendo los valores apropia­dos de los headers estandar y por calculo directo. Es mas dificil si los calcula: de­termine 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 tambien sera tomado como long. Las constantes sin sig­no 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 ex­ponente (1e-2) o ambos; su tipo es double, a menos que tengan sufijo. Los sufi­jos 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 lu­gar de decimal. Un 0 (cero) al principio de una constante entera significa octal; 0x o 0X al principio significa hexadecimal. Por ejemplo, el decimal 31 puede es­cribirse como 037 en octal y 0x1f o 0x1F en hexadecimal. Las constantes octales y hexadecimales tambien 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 caracter es un entero, escrito como un caracter dentro de apos­trofos, tal como 'x'. El valor de una constante de caracter es el valor numerico del caracter en el conjunto de caracteres de la maquina. Por ejemplo, en el conjunto de caracteres ASCII el caracter constante '0' tiene el valor de 48, el cual no esta relacionado con el valor numerico 0. Si escribimos '0' en vez de un valor numerico tal como 48 (que depende del conjunto de caracteres), el programa resulta independiente del valor particular y sera mas facil de leer. Las constantes de caracter participan en ope­raciones numericas tal como cualesquier otros enteros, aunque se utilizan mas co­munmente en comparaciones con otros caracteres.

Ciertos caracteres pueden ser representados en constante de caracter y de cadena, Por medio de secuencias de escape como \n (caraceter nueva linea); esas secuencias se ven como dos caracteres, pero representan solo uno. Ademas, un patron de bits ar­bitrario de tamano de un byte puede ser especificado por

'\ooo'

en donde ooo son de uno a tres digitos octales (0...7) o por

'\xhh'

en donde hh son uno o mas digitos hexadecimales (0...9, a...f, A...F). Asi podria­mos escribir

#define VTAB '\013'   /* tab vertical ASCII */
#define BELL '\007'   /* caracter campana ASCII */

o, en hexadecimal,

#define VTAB '\xb'    /* tab vertical ASCII */
#define BELL '\x7'    /* caracter campana ASCII */

La constante de caracter '\0' representa el caracter nulo (con valor cero). '\0' a menudo se escribe en vez de 0 para enfatizar la naturaleza de caracter de algunas expresiones, pero el valor numerico es precisamente 0.

El conjunto completo de secuencias de escape es

Secuencia de Escape Caracter ASCII
\a caracter de alarma/campana (BELL)
\b retroceso, BKSP
\f avance de hoja, FF
\n nueva linea, LF
\r regreso de carro, CR
\t tabulador horizontal, TAB
\v tabulador vertical, VTAB
\0 caracter nulo, NULL
\\ barra invertida **
\? signo de interrogacion ?
\' apostrofo '
\" comillas "
\ooo Numero octal
\xhh Numero hexadecimal

Una expresion constante es una expresion que solo inmiscuye constantes. Ta­les expresiones pueden ser evaluadas durante la compilacion en vez de que se haga en tiempo de ejecucion, 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 anos 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 mas ca­racteres encerrados entre comillas, como en

"Soy una cadena"

o

""/* la cadena vacia */

Las comillas no son parte de la cadena, solo sirven para delimitarla. Las mismas secuencias de escape utilizadas en constantes de caracter se aplican en cadenas; \" representa el caracter comillas. Las constantes de cadena pueden ser concate­nadas en tiempo de compilacion:

"Viva " "Peron!"

es equivalente a

"Viva Peron!"

Esto es util para separar cadenas largas entre varias lineas de codigo fuente.

Tecnicamente, una constante de cadena es un arreglo de caracteres. La represen­tacion interna de una cadena tiene un caracter nulo '\0' al final, de modo que el almacenamiento fisico requerido es uno mas del numero de caracteres escritos entre las comillas. Esta representacion significa que no hay limite en cuanto a que tan larga puede ser una cadena, pero los programas deben leer completamente una cadena para determinar su longitud. La funcion strlen(s) de la biblioteca estandar regresa la longitud de su argumento s de tipo cadena de caracteres, ex­cluyendo el '\0' del final. Aqui esta nuestra version:

/* 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 estan declaradas en el header estandar <string.h>.

Se debe ser cuidadoso al distinguir entre una constante de caracter y una cadena que contiene un solo caracter: 'x' no es lo mismo que "x". El primero es un ente­ro, utilizado para producir el valor numerico de la letra x en el conjunto de carac­teres de la maquina. El ultimo es un arreglo de caracteres que contiene un caracter (el caracter x) y un caracter '\0' al final.

Existe otra clase de constante, la constante de enumeracion. Una enumeracion 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 asi sucesivamente, a menos que sean especificados valores explicitos. Si no son es­pecificados todos los valores, los valores no especificados continuan la progresion a partir del ultimo 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 estan en enumeraciones diferentes deben ser distintos. Los va­lores no necesitan ser distintos d entro de la misma enumeracion.

Las enumeraciones proporcionan una manera conveniente de asociar valores constantes con nombres, una alternativa a #define con la ventaja de que los valo­res 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 alma­cenar en tal variable es un valor valido para la enumeracion. No obstante, las va­riables de enumeracion ofrecen la oportunidad de revisarlas (y a menudo tal cosa es mejor que los #define). Ademas, un depurador puede ser capaz de impri­mir los valores de variables de enumeracion en su forma simbolica.

2.4 Declaraciones

Todas las variables deben ser declaradas antes de su uso, aunque ciertas decla­raciones pueden ser hechas en forma implicita por el contexto. Una declaracion especifica un tipo, y contiene una lista de una o mas 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 podria igualmente ser escrita como

int  inferior;
int  superior;
int  paso;
char c;
char line [1000];

Esta ultima forma ocupa mas espacio, pero resulta conveniente para agregar un co­mentario a cada declaracion o para realizar subsecuentes modificaciones.

Una variable tambien puede ser inicializada en su declaracion. Si el nombre es seguido por un signo de igual y una expresion, la expresion sirve como un inicializador, como en

char  esc = '\\';
int   i = 0;
int   limit = MAXLINE + 1;
float eps = l.0e—5;

Si la variable en cuestion no es automatica, la inicializacion es efectuada solo una vez, conceptualmente antes de que el programa inicie su ejecucion, y el inicializador debe ser una expresion constante. Una variable automatica explicitamente inicializada es inicializada cada vez que se entra a la funcion o bloque en que se encuentra; el inicializador puede ser cualquier expresion. Las variables estati­cas y externas son inicializadas en cero por omision. Las variables automaticas para las que no hay un inicializador explicito tienen valores indefinidos (esto es, basura).

El calificador const puede aplicarse a la declaracion de cualquier variable pa­ra especificar que su valor no sera cambiado. Para un arreglo, el calificador const indica que los elementos no seran alterados.

const double e = 2.71828182845905;
const char msg[] = "precaucion: ";

La declaracion const tambien se puede utilizar con argumentos de tipo arreglo, para indicar que la funcion no cambia ese arreglo:

int strlen(const char[]);

Si se efectua un intento de cambiar un const, el resultado esta definido por la implantacion.

2.5 Operadores aritmeticos

Los operadores aritmeticos binarios son +, -, *, y /, y el operador modulo %. La division entera trunca cualquier parte fraccionaria. La expresion

x % y

produce el residuo cuando x es dividido entre y, por lo que es cero cuando y divi­de a x exactamente. Por ejemplo, un ano es bisiesto si es divisible entre 4 pero no entre 100, excepto aquellos anos que si son divisibles entre 400, que si son bisies­tos. Por lo tanto

if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
    printf("%d es un ano bisiesto\n", year);
else
    printf("%d no es un ano bisiesto\n", year);

El operador % no puede aplicarse a operandos float o double. La direccion de truncamiento para / y el signo del resultado de % son dependientes de la maquina para operandos negativos, asi como la accion 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 aritmeticos se asocian de zquierda a derecha.

La tabla 2-1 que se encuentra al final de este capitulo, resume la precedencia y asociatividad para todos los operadores.

2.6 Operadores de relacion y logicos

Los operadores de relacion son

> Mayor que
>= Mayor o igual que
< Menor que
<= Menor o igual que

Todos ellos tienen la misma precedencia. Precisamente bajo ellos en precedencia estan los operadores de igualdad:

== Exactamente igual a
=! Exactamente no igual a

Los operadores de relacion tienen precedencia inferior que los operadores arit­meticos, asi que una expresion como i < lim -1 se toma como i < (lim -1 ), como se esperaria.

Mas interesantes son los operadores logicos && y ||. Las expresiones conecta­das por && o || son evaluadas de izquierda a derecha, y la evaluacion se detiene tan pronto como se conoce el resultado verdadero o falso. La mayoria de los programas en C descansan sobre esas propiedades. Por ejemplo, aqui esta un ci­clo de la funcion de entrada getline que escribimos en el capitulo 1:

for (i=0; i < lim-1 && (c=getchar()) != '\n' && c != EOF; ++i)
    s[i] = c;

Antes de leer un nuevo caracter es necesario verificar que hay espacio para alma­cenarlo en el arreglo s, asi que la prueba i < lim -1 debe hacerse primero. Ade­mas, si esta prueba falla, no debemos seguir y leer otro caracter.

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 asignacion deben ocurrir antes de que se pruebe el caracter c.

La precedencia de && es mas alta que la de ||, y ambas son menores que los operadores de relacion y de asignacion, asi que expresiones como

i < lim-1 && (c=getchar()) != '\n' && c != EOF

no requieren de parentesis adicionales. Pero puesto que la precedencia de != es superior que la asignacion, los parentesis se necesitan en

(c=getchar()) != '\n'

para obtener el resultado deseado de asignacion a c y despues comparacion con '\n'.

Por definicion, el valor numerico de una expresion de relacion o logica es 1 si la relacion es verdadera, y 0 si la relacion es falsa.

El operador unario de negacion ! convierte a un operando que no es cero en 0, y a un operando cero en 1. Un uso comun de ! es en construcciones como

if (!nvalido)

en lugar de

if (valido == 0)

Es dificil generalizar acerca de cual es la mejor. Construcciones como !nvalido se leen en forma agradable ("si es invalido" ), pero otras mas complicadas pueden ser dificiles de entender.

  • Ejercicio 2-2. Escriba un ciclo equivalente a la iteracion for anterior sin usar && o !!.

2.7 Conversiones de tipo

Cuando un operador tiene operandos de tipos diferentes, estos se convierten a un tipo comun de acuerdo con un reducido num ero de reglas. En general, las unicas conversiones automaticas son aquellas que convierten un operando “an­gosto” en uno “amplio” sin perdida de informacion, tal como convertir un ente­ro a coma flotante en una expresion como f + i. Las expresiones que no tienen sentido, como utilizar un float como subindice, no son permitidas. Las expresio­nes que podrian perder informacion, como asignar un tipo mayor a uno mas cor­to, o un tipo de coma flotante a un entero, pueden producir una advertencia, pero no son ilegales.

Un char solo es un entero pequeno, por lo que los chars se pueden utilizar li­bremente en expresiones aritmeticas. Esto permite una flexibilidad considerable en ciertas clases de transformacion de caracteres. Una es ejemplificada con esta in­genua implantacion de la funcion atoi, que convierte una cadena de digitos en su equivalente numerico.

/* 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 discutio en el capitulo 1, la expresion

s[i] - '0'

da el valor numerico del caracter almacenado en s[i], debido a que los valores de '0', 1, etc., forman una secuencia ascendente contigua.

Otro ejemplo de conversion de char a int es la funcion lower, que conviene un caracter simple a minuscula en el conjunto de caracteres ASCII. Si el caracter no es una letra mayuscula, lower lo regresa sin cambio.

/* lower: convierte c a minuscula; 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 mayusculas y minusculas se encuentran a una distancia fija como valores numericos y cada alfabeto es contiguo (no hay sino letras entre A y Z). Sin embargo, esta ultima observa­cion no es cierta para el conjunto de caracteres EBCDIC, asi que este codigo podria convertir algo mas que solo letras en EBCDIC.

El header estandar <ctype.h>, que se describe en el apendice B, define una familia de funciones que proporcionan pruebas y conversiones independientes de los juegos de caracteres. Por ejemplo, si c es una mayuscula, la funcion tolower(c) regresa el valor de la letra minuscula de c (de modo que tolower es un reemplazo transportable para la funcion 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 conversion de caracteres a enteros.' El len­guaje no especifica si las variables de tipo char son valores con o sin signo. Cuan­do un char se convierte a int, ¿puede producir alguna vez un entero negativo?

La respuesta varia de una maquina a otra, reflejando diferencias en la arquitectu­ra. En algunas maquinas un char cuyo bit mas a la izquierda es 1 se convertira a un entero negativo (“extension de signo”). En otras, un char resulta promovido a un int agregando ceros del lado izquierdo, asi que siempre es positivo.

La definicion de C garantiza que ningun caracter que este en el conjunto estandar de caracteres de impresion de la maquina sera negativo, de modo que esos caracteres siempre seran cantidades positivas en las expresiones. Pero hay patrones arbitrarios de bits almacenados en variables de tipo caracter que pueden aparecer como negativos en algunas maquinas, aunque sean positivos en otras - por razones de portabilidad - se debe especificar signed o unsigned si se van a almace­nar datos que no son caracteres en variables tipo char.

Las expresiones de relacion como i > j y las expresiones logicas conectadas por && y || estan definidas para tener un valor de 1 siendo verdaderas, y 0 al ser falsas. De este modo, la asignacion

d = c >= '0' && c <= '9'

vuelve 1 a d si c es un digito, 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 validacion de if, while, for, etc., “verdadero” solo significa “diferente de cero”, por lo que esto no hace diferencia.

Las conversiones aritmeticas implicitas 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 operacion proceda. El resultado es el del tipo mayor. La seccion 6 del apendice A establece las reglas de conversion en forma precisa. Si no hay operandos unsigned, sin embargo, el siguiente conjunto informal de reglas bas­tara:

  • Siendo cualquier operando long double, conviertase el otro a long double.
  • De otra manera, siendo cualquier operando double, conviertase el otro a double.
  • De otra manera, siendo cualquier operando float, conviertase el otro a float.
  • De otra manera, conviertase char y short a int.
  • Luego, siendo cualquier operando long, conviertase el otro a long.

Notese que los floats que estan en una expresion no se convierten automatica­mente a double; esto es a resultas de una alteracion a la definicion original. En general, las fun­ciones matematicas como las de <math.h> utilizaran doble precision. La razon principal para usar float es ahorrar espacio de almacenamiento en arreglos gran­des o - con menor frecuencia - ahorrar tiempo en procesamiento en maquinas donde la aritme­tica de doble precision resulta particularmente costosa.

Cuando hay operandos unsigned las reglas de conversion son mas complicadas. El problema es que las comparaciones de valores con signo y sin signo son dependientes de la maquina, debido a su dependencia de los tamanos de los varios tipos de enteros. Por ejemplo, supongase 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 asi parece ser un gran numero positivo.

Las conversiones tambien 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 caracter es convertido a un entero, tenga o no extension de signo, como se describio anteriormente.

Los enteros mas largos son convertidos a cortos o a char desechando el exceso de bits de mas alto orden. Asi 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 ex­tension de signo. Sin embargo, el invertir el orden de las asignaciones podria producir perdida de informacion.

Si x es float e i es int, entonces x = i e i = x produciran 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 implantacion.

Puesto que un argumento de la llamada a una funcion es una expresion, tambien suceden conversiones de tipo cuando se pasan argumentos a funciones. En ausencia del prototipo de una funcion, char y short pasan a ser int, y float se hace double. Esta es la razon por la que se han declarado los argumentos a funcio­nes como int y double, aun cuando la funcion se llama con char y float.

Finalmente, la conversion explicita de tipo puede ser forzada (“coacciona­da” ) en cualquier expresion, con un operador unario llamado cast. En la construccion

(nombre-de-tipo) expresion

la expresion es convertida al tipo nombrado, segun las reglas de conversion anterio­res. El significado preciso de un cast es como si la expresion fuera asignada a una variable del tipo especificado, que se utiliza entonces en lugar de la construc­cion completa. Por ejemplo, la rutina de biblioteca sqrt espera un argumento de doble precision double, y si maneja inadvertidamente algo diferente producira resultados sin sentido (sqrt esta declarado en <math.h>). Asi, si n es un entero, pode­mos usar

sqrt((double) n)

para convertir el valor de n a doble antes de pasarlo a sqrt. Notese que la conver­sion forzosa produce el valor de n en el tipo apropiado; n en si no se altera. El operador cast tiene la misma alta precedencia que otros operadores unarios, co­mo se resume en la tabla del final de este capitulo.

Si un prototipo de funcion declara argumentos, como debe ser normalmente, la declaracion produce conversion forzada automatica de los argumentos cuando la funcion es llamada. Asi, dado el prototipo de la funcion 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 ningun cast.

La biblioteca estandar incluye una implantacion transportable de un genera­dor de numeros pseudoalealorios, y una funcion 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 funcion htoi(s), que convierte una cadena de digitos hexadecimales (incluyendo Ox o OX en forma optativa) en su valor entero equiva­lente. Los digitos permitidos son del 0 al 9, de la a a la f, y de la A a la F. □

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 disminucion -- le resta 1. Hemos usado frecuentemen­te ++ para incrementar variables, como en

if (c == '\n')
   ++nl;

El aspecto poco comun es que ++ y pueden ser utilizado como prefijos (antes de la variable, como en + + n ), o como postfijos (despues de la variable: (11 ++). En ambos casos, el efecto es incrementar n. Pero la expresion ++ n in­crementa a n antes de que su valor se utilice, en tanto que n++ incrementa a n despues de que su valor se ha empleado. Esto significa que en un contexto don­de el valor esta siendo utilizado, y no solo 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 solo pueden aplicarse a variables; una expresion com o (i + j)+ + es ilegal.

Dentro de un contexto en donde no se desea ningun valor, sino solo el efecto de incremento, como en

if (c == '\n')
    nl++;

prefijos y postfijos son iguales. Pero existen situaciones en donde se requiere es­pecificam ente unou otro. Por ejemplo, considerese la funcion squeeze(s,c), que elimina todas las ocurrencias del caracter 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, este se copia en la posicion actual j, y solo entonces j es incrementada para prepararla para el siguiente carac­ter. Esto es exactamente equivalente a

if (s[i] != c) {
    s[j] = s[i];
    j++;
}

Otro ejemplo de construccion semejante viene de la funcion getline que escri­bimos en el capitulo 1, en donde podemos reemplazar

if (c == '\n') {
    s[i] = c;
    ++i;
}

por algo mas compacto como

if (c == '\n')
    s[i++] = c;

Como un tercer ejemplo, considerese que la funcion estandar strcat(s,t), que concatena la cadena t al final de la cadena s. strcat supone que hay suficiente es­pacio en s para almacenar la combinacion. Como la habiamos escrito, strcat no regresaba un valor; en cambio la version de la biblioteca estandar 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 caracter es copiado de t a s, el ++ postfijo se aplica tanto a i como a j para estar seguros de que ambos estan en posicion para la siguiente iteracion.

  • Ejercicio 2-4. Escriba una version alterna de squeeze(sl,s2) que borre cada caracter de s1 que coincida con cualquier caracter de la cadena s2. □
  • Ejercicio 2-5. Escriba la funcion any(sl,s2), que regresa la primera posicion de la cadena s1 en donde se encuentre cualquier caracter de la cadena s2, o -1 si s1 no contiene caracteres de s2. (La funcion de biblioteca estandar strpbrk hace el mismo trabajo pero regresa un apuntador a la posicion encontrada.) □

2.9 Operadores para manejo de bits

El lenguaje C proporciona seis operadores para manejo de bits; solo 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 algun con­junto 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 posicion en donde sus ope­randos tienen bits diferentes, y cero en donde son iguales.

Se deben distinguir los operadores de bits & y | de los operadores logicos && y ||, que implican evaluacion 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 izquier­da y a la derecha de su operando que esta a la izquierda, el numero de posicio­nes de bits dado por el operando de la derecha, el cual debe ser positivo. Asi x << 2 desplaza el valor de x a la izquierda dos posiciones, llenando los bits vacantes con cero; esto es equivalente a una multiplicacion 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 llenara con bits de signo (“corrimiento aritme­tico”) en algunas maquinas y con bits 0 (“corrimiento logico” ) en otras.

El operador unario ~ da el complemento a uno de un entero; esto es, con­vierte cada bit 1 en un bit 0 y viceversa. Por ejemplo,

x = x & ~077

fija los ultimos seis bits de x en cero. Notese 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 expresion constante que pue­de ser evaluada en tiempo de compilacion.

Como ilustracion de algunos de los operadores de bits, considere la fun­cion getbits(x,p,n) que regresa el campo de n bits de x (ajustado a la derecha) que principia en la posicion p. Se supone que la posicion del bit 0 esta en el borde derecho y que n y p son valores positivos adecuados. Por ejemplo, getbits(x,4,3) regresa los tres bits que estan en la posicion 4, 3 y 2, ajustados a la derecha.

/* getbits: obtiene n bits desde la posicion p */
unsigned getbits(unsigned x, int p, int n)
{
    return (x >> (p+1-n)) & ~(~0 << n);
}

La expresion x >> (p+1-n) mueve el campo deseado al borde derecho de la pa­labra. ~0 es todos los bits en 1; corriendo n bits hacia la izquierda con ~0<<n coloca ceros en los n bits mas a la derecha; complementado con ~ hace una mascara de unos en los n bits mas a la derecha.

  • Ejercicio 2-6. Escriba una funcion setbits(x,p,n,y) que regresa x con los n bits que principian en la posicion p iguales a los n bits mas a la derecha de y, dejando los otros bits sin cambio. □
  • Ejercicio 2-7. Escriba una funcion invert(x,p,n) que regresa x con los n bits que

principian en la posicion p invertidos (esto es, 1 cambiado a 0 y viceversa), dejando los otros sin cambio. □

  • Ejercicio 2-8. Escriba una funcion rightrot(x,n) que regresa el valor del entero x rotado a la derecha n posiciones de bits. □

2.10 Operadores de asignacion 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 asignacion.

La mayoria de los operadores binarios (operadores como + que tienen un operando izquierdo y otro derecho) tienen un correspondiente operador de asig­nacion 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 solo una vez. Notense los parentesis alrededor de expr2:

x *= y + 1

significa

x = x * (y + 1)

Y no

x = x * y + 1

Como ejemplo, la funcion bitcount cuenta el numero de bits en 1 en su argu­mento 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 dere­cha, los bits vacantes se llenaran con ceros, no con bits de signo, sin importar la maquina en la que se ejecute el programa.

Muy aparte de su concision, los operadores de asignacion 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, agregale 2, despues pon el resultado de nuevo en i". Asi la expresion i += 2 es preferible a i = i + 2. Ademas, para una expresion complicada como

yyval[yypv[p3+p4] + yypv[p1]] += 2

el operador de asignacion hace al codigo mas facil de entender, puesto que el lec­tor no tiene que verificar arduamente que dos expresiones muy largas son en realidad iguales, o preguntarse por que no lo son, y un operador de asignacion puede incluso ayudar al compilador a producir codigo mas eficiente.

Ya hemos visto que la proposicion de asignacion tiene un valor y puede estar dentro de expresiones; el ejemplo mas comun es

while ((c = getchar()) != EOF)
    ...

Los otros operadores de asignacion (+=, —=, etc.) tambien pueden estar dentro de expresiones, aunque esto es menos frecuente.

En todas esas expresiones, el tipo de una expresion de asignacion es el tipo de su operando del lado izquierdo, y su valor es el valor despues de la asignacion.

  • Ejercicio 2-9. En un sistema de numeros de complemento a dos, x &= (x-1) bo­rra el bit 1 de mas a la derecha en x. Explique el porque. Utilice esta observacion para escribir una version mas rapida de bitcount. □

2.11 Expresiones condicionales

Las proposiciones

if (a > b)
    z = a;
else
    z = b;

calculan en z el maximo de a y b. La expresion condicional, escrita con el opera­dor ternario "?:" proporciona una forma alternativa para escribir esta y otras construcciones semejantes. En la expresion

expr1 ? expr2 : expr3

la expresion expr1 es evaluada primero. Si es diferente de cero (verdadero), en­tonces la expresion expr2 es evaluada, y ese es el valor de la expresion condicio­nal. De otra forma, se evalua expr3, y ese es el valor. Solo se evalua una de entre expr2 y expr3. Asi, para hacer z el maximo de a y b,

z = (a > b) ? a : b;    /* z = max(a, b) */

Se debe notar que la expresion condicional es en si una expresion, y se puede utilizar en cualquier lugar donde otra expresion pueda. Si expr2 y expr3 son de tipos diferentes, el tipo del resultado se determina por las reglas de conver­sion discutidas anteriormente en este capitulo. Por ejemplo, si f es un float y n es un int, entonces la expresion

(n > 0) ? f : n

es de tipo float sea n positivo o no.

Los parentesis no son necesarios alrededor de la primera expresion de una ex­presion condicional, puesto que la precedencia de ?: es muy baja, solo arriba de la asignacion. De cualquier modo son recomendables, puesto que hacen mas facil de ver la parte de condicion de la expresion.

La expresion condicional frecuentemente lleva a un codigo conciso. Por ejem­plo, este ciclo imprimen elementos de un arreglo, 10 por linea, con cada columna separada por un caracter en blanco, y con cada linea (incluida la ultima) terminada por un caracter nueva linea.

for (i = 0; i < n; i++)
    printf("%6d%c", a[i], (i%10==9 || i==n-1) ? '\n' : ' ');

Se imprime un caracter nueva linea despues de cada diez elementos, y despues del n-esimo. Todos los otros elementos son seguidos por un espacio en blanco. Esto podria parecer complicado, pero es mas 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 funcion lower, que convierte letras mayusculas e minusculas, con una expresion condicional en vez de un if-else. □

2.12 Precedencia y orden de evaluacion

La tabla 2-1 resume las reglas de precedencia y asociatividad de todos los ope­radores, incluyendo aquellos que aun no se han tratado. Los operadores que estan en la misma linea tienen la misma precedencia; los renglones estan en orden de precedencia decreciente, asi, por ejemplo, %, /, y * tienen todos la misma pre­cedencia, la cual es mas alta que la de + y - binarios. El “operador” () se refiere a la llamada a una funcion. Los operadores -> y . son utilizados para tener acce­so a miembros de estructuras; seran cubiertos en el capitulo 6, junto con sizeof (tamano de un objeto). En el capitulo 5 se discuten * (indireccion a traves de un apuntador) y & (direccion de un objeto), y en el capitulo 3 se trata al operador , (coma).

Los +, -, y * unarios, tienen mayor precedencia que las formas binarias.

Notese que la precedencia de los operadores de bits &, ^, y | estan por debajo de == y != . Esto implica que las expresiones de prueba de bits como

if ((x & MASK) == 0) ...

deben ser completamente colocadas entre parentesis para dar los resultados apro­piados.

Como muchos lenguajes, C no especifica el orden en el cual los operandos de un operador seran 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 va­riable de la que la otra depende, x puede depender del orden de evaluacion. Se pueden almacenar resultados intermedios en variables temporales para asegurar una secuencia particular.

De manera semejante, el orden en el que se evaluan los argumentos de una funcion no esta especificado, de modo que la proposicion

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 solucion, por supuesto, es escribir

++n;
printf("%d %d\n", n, power(2, n));

Las llamadas a funciones, proposiciones de asignacion anidadas, y los opera­dores de incremento y decremento provocan “efectos colaterales” — alguna varia­ble resulta modificada como producto de la evaluacion de una expresion. En cualquier expresion que involucra efectos colaterales, pueden existir sutiles dependencias del orden en que las variables involucradas en la expresion se actualizan. La in­fortunada situacion es tipificada por la proposicion

a[i] = i++;

La pregunta es si el subindice 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 interpretacion. El estandar deja intencionalmente sin especificacion la mayoria de estos aspectos. Cuando existen efectos colaterales (asignacion a variables) dentro de una expresion, se deja a la prudencia del compilador, puesto que el orden mas eficiente depende mayormente de la arquitectura de la maquina. (El estandar si especifica que todos los efectos colaterales sobre argumentos suce­dan antes de que la funcion sea llamada, pero eso podria no ayudar en la llamada a printf mostrada anteriormente).

La moraleja es que escribir un codigo dependiente del orden de evaluacion es una mala practica de programacion en cualquier lenguaje. Naturalmente, es nece­sario conocer que cosas evitar, pero si no sabe como varias maquinas resuelven las cosas, no debe intentar sacar provecho de una implantacion 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

Continuar: Capitulo 3