Capitulo 2: Tipos, Operadores y Expresiones
Las variables y las constantes son los objetos de datos basicos 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 cuales son sus valores iniciales. Los operadores especifican lo que se hara 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 que operaciones se pueden realizar sobre el. Estos son los temas de este capitulo.
El estandar ANSI ha hecho muchos pequenos cambios y agregados a los
tipos 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 sencilla;
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
para 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 subrayado "_
"
cuenta como una letra; algunas veces es util para mejorar la legibilidad
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 cargadores, sobre los que el lenguaje no tiene
control. Para nombres externos, el estandar 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 tendemos a utilizar nombres cortos para variables locales (especialmente indices de iteraciones), 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
short
s e int
s son, por lo menos - de 16 bits,
los long
s 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 char
s 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>
contienen
constantes simbolicas para todos esos tamanos, junto con otras
propiedades 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
ylong
, tantosigned
comounsigned
, imprimiendo los valores apropiados de los headers estandar y por calculo directo. Es mas dificil 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
tambien sera
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
o
0X
al principio significa hexadecimal. Por ejemplo, el
decimal 31
puede escribirse 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 apostrofos, 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 operaciones numericas tal
como cualesquier otros enteros, aunque se utilizan mas comunmente 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
arbitrario 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
podriamos 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. Tales 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 caracteres 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 concatenadas 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
representacion 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, excluyendo 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 entero, utilizado para
producir el valor numerico de la letra x en el conjunto de caracteres 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 especificados 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 valores 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 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 valido para la enumeracion. No obstante, las
variables de enumeracion ofrecen la oportunidad de revisarlas (y a
menudo tal cosa es mejor que los #define
). Ademas, un
depurador puede ser capaz de imprimir 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 declaraciones 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 comentario 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 estaticas 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 para 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
divide 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 bisiestos. 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 aritmeticos, 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 conectadas
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 ciclo 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 almacenarlo en el arreglo s
, asi que la prueba
i < lim -1
debe hacerse primero. Ademas, 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 “angosto” en uno “amplio” sin perdida de
informacion, tal como convertir un entero 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 expresiones que podrian perder informacion, como asignar
un tipo mayor a uno mas corto, 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
char
s se pueden utilizar libremente en expresiones
aritmeticas. Esto permite una flexibilidad considerable en ciertas
clases de transformacion de caracteres. Una es ejemplificada con esta
ingenua 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 observacion
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 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 varia de una maquina a otra, reflejando diferencias en
la arquitectura. 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 almacenar 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 bastara:
- Siendo cualquier operando
long double
, conviertase el otro along double
. - De otra manera, siendo cualquier operando
double
, conviertase el otro adouble
. - De otra manera, siendo cualquier operando
float
, conviertase el otro afloat
. - De otra manera, conviertase
char
yshort
aint
. - Luego, siendo cualquier operando
long
, conviertase el otro along
.
Notese que los float
s que estan en una expresion no se
convierten automaticamente a double
; esto es a resultas de
una alteracion a la definicion original. En general, las funciones
matematicas como las de <math.h
> utilizaran doble
precision. La razon principal para usar float
es ahorrar
espacio de almacenamiento en arreglos grandes o - con menor frecuencia -
ahorrar tiempo en procesamiento en maquinas donde la aritmetica 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 extension 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 funciones como int
y double
,
aun cuando la funcion se llama con char
y
float
.
Finalmente, la conversion explicita de tipo puede ser forzada (“coaccionada” ) 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 anteriores. 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
construccion 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,
podemos usar
sqrt((double) n)
para convertir el valor de n
a doble antes de pasarlo a
sqrt
. Notese que la conversion 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, como 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 generador 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 (incluyendoOx
oOX
en forma optativa) en su valor entero equivalente. Los digitos permitidos son del0
al9
, de laa
a laf
, y de laA
a 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
disminucion --
le resta 1. Hemos usado frecuentemente
++
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
incrementa 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 donde 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 especificam 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
caracter. 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 escribimos 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 espacio 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 des1
que coincida con cualquier caracter de la cadenas2
. □ - Ejercicio 2-5. Escriba la funcion
any(sl,s2)
, que regresa la primera posicion de la cadenas1
en donde se encuentre cualquier caracter de la cadenas2
, o-1
sis1
no contiene caracteres des2
. (La funcion de biblioteca estandarstrpbrk
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 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
posicion en donde sus operandos 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 izquierda y a la
derecha de su operando que esta a la izquierda, el numero de posiciones
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 aritmetico”) en algunas maquinas y con bits 0
(“corrimiento logico” ) 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 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 puede ser
evaluada en tiempo de compilacion.
Como ilustracion de algunos de los operadores de bits, considere la
funcion 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 palabra. ~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 regresax
con los n bits que principian en la posicionp
iguales a los n bits mas a la derecha dey
, dejando los otros bits sin cambio. □ - Ejercicio 2-7. Escriba una funcion
invert(x,p,n)
que regresax
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 enterox
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 asignacion 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 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 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 lector 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)
borra el bit 1 de mas a la derecha enx
. Explique el porque. Utilice esta observacion para escribir una version mas rapida debitcount
. □
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
operador 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), entonces la expresion expr2 es evaluada, y
ese es el valor de la expresion condicional. 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 conversion 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 expresion 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 ejemplo, 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 unif-else
. □
2.12 Precedencia y orden de evaluacion
La tabla 2-1 resume las reglas
de precedencia y asociatividad de todos los operadores, 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 precedencia, 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
acceso 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 apropiados.
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
variable 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 operadores de incremento y decremento provocan “efectos colaterales” — alguna variable 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 infortunada 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 sucedan 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 necesario 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