domingo, 19 de enero de 2014

El lenguaje C II

Puesto que el lenguaje C nació y creció en los tiempos en los que las computadoras eran artículos de lujo a los cuales no quedaba otra opción más que conectarse mediante una de varias terminales (pantalla y escritorio) y en donde la única interfaz visual eran pantallas monocromáticas (blanco y negro) sin capacidades gráficas que sólo podían poner caracteres de texto en la pantalla en una ventana de comandos de línea, lo cual se sostuvo en los primeros años en que se comercializaron las computadoras caseras con los sistemas operativos PC-DOS y MS-DOS, pudiera suponerse que al hacer su aparición la interfaz visual con una rica variedad de íconos, gráficas, ventanas y colores el lenguaje C debería de haber expirado y debería haber pasado a formar parte de los libros de la historia de computación como el lenguaje Forth y el lenguaje Modula-2. Sin embargo, pese a la rápida evolución de los recursos de hardware, el lenguaje C lejos de ser abandonado fue mejorado de varias maneras, de modo tal que con dicho lenguaje era posible escribir programas ya sea para las viejas ventanas DOS de líneas de comandos o para los sistemas operativos con interfaz de ventanas tipo Windows. ¿Por qué la razón de aferrarse a C a lo largo de varias décadas? Precisamente por tratarse de un lenguaje que puede darnos acceso a los recursos internos del hardware de cualquier máquina sin tener que meter las manos en lenguajes ensambladores que no son portátiles hacia computadoras con distintos tipos de arquitecturas pero que pueden ser endiabladamente difíciles de escribir y depurar de errores. Por esto mismo, cualquiera interesado en las ciencias de la computación que dedique algo de su tiempo al aprendizaje del lenguaje C no terminará decepcionado como aquellos que dedicaron toda su vida al dominio de lenguajes como FORTRAN o PL/1 que hoy se encuentran virtualmente extintos. Cuando el lenguaje C ha sido reemplazado por otras alternativas como C++ o como C#, estas alternativas no hacen a un lado la estructura general de C, por el contrario la van ampliando con una nueva variedad de funciones y nuevas palabras reservadas. Por otro lado, hay millones de líneas de código escritas en lenguaje C conteniendo algoritmos bastante útiles que pueden ser (y son) reciclados, evitándole al programador la frustante perspectiva de tener que volver a inventar la rueda. Es así como tenemos muchos libros como C Tools for Scientists and Engineers y More C Tools ambos de Louis Baker que contienen programas completos con algoritmos para todo tipo de cosas como el cálculo de las Tranformadas Discretas de Fourier, la solución numérica de sistemas de ecuaciones diferenciales, el ajuste de datos a fórmulas empíricas y el método Newton-Rapson para resolver problemas de ecuaciones no-lineares. Se trata de código que puede ser compilado de inmediato a programas ejecutables, que se puede adaptar sin muchas dificultades al tipo de sistema operativo que se esté utilizando.

En los tiempos en los que el sistema operativo UNIX era rey en las entonces grandes y voluminosas computadoras alquiladas por prestigiosas universidades y organizaciones gubernamentales (eran demasiado caras para poder ser compradas, tenían que ser rentadas), para elaborar un programa en lenguaje C se requería un editor de texto llano (los editores de texto no forman parte del lenguaje C, no hay especificación alguna de C que indique que se tenga que usar algún editor de texto en particular), no un procesador de palabras como WordStar o WordPerfect capaces de crear efectos especiales como letras negritas o itálicas y maniobrar sobre letras de distintos tamaños y estilos de fuentes tipográficas. La redacción de los programas solía hacerse a través de terminales con pantallas monocromáticas conectadas a la gran computadora mediante una interfaz visual sencilla tipo UNIX (la cual inspiró la interfaz visual del DOS) que solo podía producir en la pantalla caracteres de texto (Windows no existía, y los monitores así como las tarjetas adaptadoras para producir gráficos en las pantallas no habían sido desarrollados). Hoy en día, para elaborar un programa en lenguaje C básico se puede recurrir a un editor de texto como el Bloc de Notas (Notepad) de Windows, y se sigue requiriendo de algún tipo de pantalla que pueda interactuar con el usuario mediante una ventana sencilla de líneas de comando textuales. Y si bien aún sigue habiendo editores de texto instalados en casi todas las computadoras en existencia, la interfaz visual de líneas de comandos parece haber desaparecido. ¿Entonces cómo habremos de elaborar y probar un programa escrito en lenguaje C? La respuesta es que hoy en día se usan entornos integrados de desarrollo IDE (Integrated Development Environment) que lo incluyen todo: el editor de texto, el compilador, el simulador del programa ejecutable, un depurador de errores (debuger) para probar programas y eliminar los errores que haya en ellos, una interfaz visual de líneas de comandos, y los demás recursos requeridos (archivos de ayuda, bibliotecas de funciones, enlazadores, etc.)

En los tiempos en los que UNIX era rey, el costo de elaboración de un compilador para algún lenguaje de alto nivel como FORTRAN era sumamente elevado, situado en los cientos de miles de dólares, porque no había muchas máquinas en las que fuera a ser utilizado. Pero con la fabricación masiva de computadoras caseras, hicieron su aparición los primeros compiladores en lenguajes como BASIC o C que podían ser vendidos en cien dólares, esto ayudado por el hecho de que el mercado potencial de compradores era considerable. Así fue como aparecieron los primeros compiladores C tales como Turbo C  y se llegó a los entornos integrados de desarrollo que incluyen todo lo que se requiere. Aquí hablaremos de uno que consolidó la punta de la tecnología en lo que se refiere a entornos integrados de desarrollo.

El entorno Borland C++ en su versión 4.0 ofreció la posibilidad de poder compilar código escrito en C para ser ejecutado en tres tipos de ambientes:

(1) La clásica ventana DOS de comandos de línea.
(2) La interfaz visual Windows de 16 bits (Windows 3.1)
(3) La interfaz visual Windows de 32 bits (Windows 95, Windows 98, Windows XP, etc.)

Es importante señalar que para poder elaborar programas ejecutables para ser lanzados dentro de un sistema operativo Windows de 16 bits, el entorno Borland C++ requería echar mano de un programa compilador capaz de compilar código de 16 bits, mientras que para elaborar programas ejecutables para ser lanzados dentro de un sistema operativo Windows de 32 bits, el entorno requiería echar mano de otro programa compilador capaz de compilar código de 32 bits. De este modo, un buen entorno de programación puede incluír no un solo compilador sino varios compiladores, dependiendo del sistema operativo que sea el objetivo final del programa ya compilado.

Al abrir el entorno Borland C++, se presenta algo como esto (se recomienda ampliar la ventana para poder apreciar mejor los detalles):




Como podemos ver, hay una ventana grande que corresponde precisamente a la de un documento MDI (Multiple Document Interface) de Windows (se dió una introducción a la manera en la cual se puede crear un documento MDI en una de las entradas de “En entorno Visual Basic”) en la cual en la parte superior hay una línea de menú y debajo de la misma hay una barra de herramientas. Solo es posible abrir una ventana MDI como esta. Sin embargo, dentro de dicha ventana se pueden abrir uno o varios documentos hijo del mismo tipo de acuerdo a la convención SDI (Single Document Interface) de Windows. Al seleccionar en la línea del menú la opción “File” y de ella seleccionar la sub-opción “New”, se abre una ventana nueva dentro de la ventana MDI:




El documento que se abre, una ventana en blanco, es el espacio que usamos como editor de texto para escribir dentro de la misma el código fuente de un programa elaborado en C. Todos los entornos usados para elaborar programas ejecutables en C empiezan con el editor de texto en el cual se irán escribiendo las instrucciones del programa C que estamos construyendo. Al ser una ventana SDI abierta dentro de una ventana MDI, podemos tener abiertas al mismo tiempo varias ventana SDI, lo cual significa que al usar el entorno podemos estar trabajando en varios proyectos al mismo tiempo, cada uno en su propia ventana.

Al elaborar un programa C dentro del editor de texto de un entorno visual, es común que conforme se va escribiendo el código el editor de texto vaya resaltando de color varias porciones de texto para distinguir comandos, y en el caso de los comentarios que no afectarán el desempeño del programa ejecutable (todos los comentarios son removidos por el compilador C al construírse el programa ejecutable) tal vez serán distinguidos con otro tipo de letra, por ejemplo letras itálicas.

Para el lector que haya echado un vistazo a las entradas previas que tratan los detalles del entorno Visual Basic, será obvio que en este entorno de programación C no hay una “caja de herramientas” (Toolbox) en donde haya controles (objetos) como botones de opción, cajas de texto, cajas de imagen y cajas de etiquetas. Esto en virtud de que no se trata de un entorno de <i>programación visual</i>. Esto no significa que en un programa C no se puedan definir botones de opción, cajas de texto y cajas de imagen, se puede hacer tal cosa, pero la sintaxis de C no contempla tales cosas y es necesario proporcionar funciones de graficado y funciones de biblioteca para que tales cosas ocurran.

Escribiremos dentro del editor de texto del entorno Borland C++ el siguiente programa introductorio que ya habíamos visto en la entrada previa.


   /* Programa para ser compilado en Borland C++ 4.0 */

   #include <stdio.h>

   main()

      {

         printf("Hola, mundo!");

      }


Una vez que hemos hecho lo anterior, tendremos algo como esto:




A continuación, tenemos la opción de hacer una ejecución simulada, yéndonos al menú y seleccionando de la opción “Debug” la sub-opción “Run”. Cuando estamos trabajando en un programa nuevo de nuestra propia invención, siempre es recomendable llevar a cabo estas ejecuciones simuladas antes de convertir el código C en un programa ejecutable, en virtud de que en esta etapa podemos obtener retroalimentación del entorno C sobre los errores que pueda tener el programa (y el cometer errores al llevar a cabo una programación es el pan de cada día inclusive en los programadores más avezados usando los entornos más sofisticados de que puedan echar mano):




El sencillo programa que estamos usando debe poder ejecutar bien al ser puesto en marcha desde una ventana de líneas de comandos DOS. Excepto que el entorno C con el que estamos trabajando fue abierto en una máquina con Windows XP (32 bits) instalado en ella, usándose la interfaz visual propia de Windows con la amplia variedad de opciones para graficados que ofrece un monitor de colores con una resolución de 1024x768. ¿Cómo nos presentará entonces el entorno C el resultado de la ejecución simulada? ¿Abrirá la ventana DOS que incluye el mismo sistema operativo windows? Nada de esto. En entorno C abre su propia ventana que hace las veces de una ventana DOS, excepto que dicha ventana tiene un fondo blando y no un fondo negro. El resultado de la simulación es precisamente el resultado que esperaríamos del programa, la impresión (en la ventana tipo DOS del entorno C) de la hilera “Hola, mundo!”:




Naturalmente, no se trata de una ventana DOS con la que podamos interactuar una vez que haya terminado la ejecución del programa simulado.

Una vez que estamos satisfechos con los resultados obtenidos, podemos llevar a cabo la compilación del programa regresando a la línea del menú, y de la opción “Project” podemos seleccionar la opción “Compile” para iniciar el proceso de compilación, esto es, la conversión del código fuente en C a un código binario ejecutable:




Como resultado del proceso de compilación, se produce en el interior del entorno C una ventana que nos indica que el proceso de compilación ha resultado exitoso:




La ventana nos dice que el resultado del proceso de compilación fue exitoso (Success). Sin embargo, nos indica una advertencia. Esto puede parecer desconcertante. Si el proceso de compilación fue exitoso, ¿por qué entonces la advertencia? ¿Se hizo algo mal que puede ocasionar problemas? En realidad, en este caso no. Si cerramos la ventana del resultado de compilación oprimiento su botón OK, veremos que queda al descubierto otra ventana en el inferior del entorno C, la cual nos da mensajes de advertencia:




Podemos hacer que se elimine el mensaje de advertencia haciendo el siguiente cambio en el programa:


   /* Programa para ser compilado en Borland C++ 4.0 */

   #include <stdio.h>

   void main()

      {

         printf("Hola, mundo!");

      }


Con el cambio, al anteponer la palabra reservada void al nombre de la función principal que es main(), estamos declarando formalmente que no esperamos que dicha función regrese valor alguno de ningún tipo. Pero tratándose de la función principal que encapsula todo el programa, ¿no es esto un rigorismo innecesario de parte de C? Después de todo, si no asignamos ningún tipo de retorno a la función principal el programa se ejecutará correctamente si es invocado desde una ventana de líneas de comandos. Sin embargo, podemos hacer que cualquier función en C, y esto no excluye a la función principal, regrese algún valor de algún tipo. Y de hecho, la función principal puede regresar un valor que será tomado en cuenta por el sistema operativo DOS cuando haya terminado la ejecución del programa. La omisión de void no es crítica en este caso, pero en otros programas puede ser importante, y el compilador C nos está dando la voz de alerta para que limpiemos el programa al máximo eliminando fuentes potenciales de error.

¿Y qué del caso en el que incurrimos en un error dentro del mismo programa, por ejemplo al omitir algún corchete o cometer un error ortográfico u olvidar declarar previamente una variable? Entonces, desde antes de que el programa pueda ser compilado, se nos dará una advertencia de errores fatales.

Veamos otro ejemplo de código C en el cual sí le damos un valor de retorno a la función main(). En dicho programa invocamos la función gettime() la cual, por no formar parte de la lista de funciones cuyos prototipos están dentro del archivo de cabecera STDIO.H, requiere la inclusión adicional del archivo de cabecera DOS.H en donde se encuentra dicha función:


   /* Ejemplo de gettime() */ 

   #include   
   #include   

   int main(void)
   {
      struct  time t;

      gettime(&t);
      printf("The current time is: %2d:%02d:%02d.%02d\n",
             t.ti_hour, t.ti_min, t.ti_sec, t.ti_hund);
      return 0;
   }



Supóngase que en el programa anterior se cometió una omisión involuntaria, en la cual al escribir la palabra reservada struct omitimos la primera letra de dicha palabra. Si tratamos de compilar el programa, el entorno C se negará a tal cosa indicándonos que hay errores que deben ser atendidos (en realidad, hay un solo error que debe ser atendido, pero dicho error desencadena otros errores; el programador es el que tiene que descubrir cuál de todos los errores entre una lista de 50 es el que está provocando los 49 restantes):




Al cerrar la ventana obtendremos la confirmación de que hay una palabra no-definida, truct:




con el siguiente mensaje resaltado:

Undefined symbol 'truct'

Un problema que suele suscitarse al importar rutinas escritas en lenguaje C es que al haber varios dialectos de C el compilador que se use puede negarse a efectuar su labor al encontrar incompatibilidades que no está preparado para resolver. La mayoría de los compiladores C modernos ofrecen opciones para poder procesar código en el dialecto C empleado. En el entorno Borland, por ejemplo, en la sub-opción “Project...” accesible en la línea de menú bajo la opción “Options”, al escoger dentro de la barra “Compiler” la opción “Source” encontramos una lista que nos permite escoger bajo “Language Compliance” el dialecto a ser usado:




teniendo en la lista el ANSI C, el C usado en el sistema operativo UNIX V, y el C especificado en el estándard de Kernighan y Ritchie.

El entorno de desarrollo Borland C++, al igual que casi todos los demás entornos de desarrollo IDE usados en la actualidad, tiene otro tipo de ayudas posibles, incluída documentación en-línea, definiciones de términos, maquinaria de búsqueda, etcétera. En realidad, una vez que se ha trabajado con un entorno de desarrollo IDE, ya se sabe la manera en la que trabajarán todos los demás entornos; quien ha visto uno ya ha visto todos, aunque de cualquier modo cada uno tendrá sus propias idiosincrasias. La ventaja de un entorno es que al reunir en un mismo sitio todas las herramientas requeridas presentándolas de una manera visual le ahorra al programador mucho tiempo.

El entorno Visual C++ elaborado por la empresa Microsoft es presentado a continuación en una captura de imagen para fines comparativos con el entorno de Borland que hemos estado viendo arriba:




En la ventana superior derecha se puede apreciar la ventana del editor de texto en donde se elabora el programa C, en la ventana superior izquierda puede apreciarse la ventana para selección de paquetes de clases, y la ventana larga inferior corresponde a la ventana que nos dá los mensajes de error, la cual es  usada para fines de depuración de errores. El entorno ofrecido por Microsoft tiene una línea de menú muy parecida a la que ofrece el entorno IDE de Borland y esto ha cambiado poco con el paso del tiempo, pudiendo apreciarse configuraciones similares en entornos como Android Studio usado para la programación (en lenguaje Java) de las aplicaciones apps usadas en los teléfonos inteligentes y las tabletas electrónicas.

Habiendo visto la manera en la cual usamos un entorno de desarrollo típico para elaborar un programa en lenguaje C, continuaremos ahora nuestro estudio del lenguaje C.

Vimos ya en la entrada anterior que todos los enunciados en C tienen que ser terminados con un semicolon (;). En C, el semicolon es un delimitador, un terminador de enunciados. Es lo que marca el final de una entidad lógica. (Aquellos que tengan alguna familiaridad con el lenguaje Pascal tienen que ejercer aquí precaución, ya que mientras que el semicolon en Pascal es un separador de enunciados, en C es un terminador de enunciados. En C, un bloque es un conjunto de enunciados conectados lógicamente, que está dentro de corchetes de apertura y cierre que siempre ocurren en pares. Si se considera un bloque como un grupo de enunciados, tiene sentido el que un bloque no sea seguido por un semicolon (o puesto de otra manera, el uso del semicolon de terminación a la derecha de un corchete de cierre es innecesario). Podemos anidar una expresión entre corchetes apareados cuantas veces queramos, aunque el uso de corchetes adicionales sea superfluo, por ejemplo:

   #include <stdio.h>

   main()

      {
         {
            {
               printf("Hola, mundo!");
            }
         }
      }



C no reconoce el final de una línea de texto como terminador. Esto significa que no hay ninguna limitación en la posición o acomodo que le demos a los enunciados. El compilador C, en efecto, se “traga” los espacios en blanco y los saltos de línea. Esto hace más fácil agrupar o separar enunciados como ayuda visual para mayor legibilidad. Esto significa que podemos tomar un fragmento como el siguiente:

   a = b;

   b = b + 1;

   mul(a,b);

y compactarlo de la siguiente manera si con ello resaltamos algún propósito lógico del fragmento:

   a = b;
   b = b + 1;
   mul(a,b);

Formalizaremos ahora el concepto de las variables y las constantes en C.

Las variables y las constantes son manipuladas con operadores para formar expresiones, y son el sostén básico del lenguaje C. A diferencia de otros lenguajes de programación como BASIC, que tienen una visión muy simple y limitada para lo que son las variables, los operadores y las expresiones, C proporciona mucho más potencia e importancia a estos elementos. El lenguaje C define los nombres que son usados para referenciar variables, funciones, etiquetas y otros objetos definidos por el usuario como identificadores. Un identificador en C puede constar de uno o varios caracteres. El primer caracter puede ser una letra o un guión bajo, con caracteres subsecuentes que pueden ser letras, números, o el guión bajo. En C los siguientes nombres son correctos:

conteo   ,   _cbasic   ,   prueba510   ,   _TEMPERATURA

mientras que los siguientes nombres son incorrectos:

5conteo   ,   ofertas!   ,   apellido..paterno   ,   lista base
 
En C todas las variables tienen que ser declaradas antes de ser usadas, y al ser declaradas se tiene que especificar el tipo de las mismas. Esto se requiere para que el compilador pueda reservar suficiente espacio de memoria para los datos a ser usados en un programa según su tipo. Hay cinco tipos básicos de datos en C que son: caracter, entero, punto flotante, doble punto flotante, y “sin valor alguno”. Las palabras reservadas usadas para declarar variables de estos tipo son char, int, float, double y void. En las primeras computadoras caseras basadas en los procesadores Intel con un compilador como Turbo C instalado en ellas, el tamaño y rango de cada tipo de dato era como lo que se muestra a continuación:


Tipo  Extension
en bits
Rango
 char 8  -128 a 127
 int 16  -32768 a 32767
 float 32  3.4E-38 a 3.4E+38
 double 64  1.7E-308 a 1.7E+308 
 void 0  Sin valor alguno


En cualquier tiempo, la extensión en bits asignada a cada uno de los tipos de datos usados en C ha ido cambiando conforme han ido evolucionando las capacidades de almacenamiento de las máquinas, en ciertos casos permanece invariable, mientras que en otras entornos manifiesta ciertas diferencias. A modo de ejemplo, cuando C ya se había popularizado enormemente en 1987, podíamos encontrar las siguientes especificaciones para los sistemas mostrados:


 DEC PDP-11
16 bits
 DEC VAX
32 bits
Procesador
 Intel 80386
32 bits
IBM PC
 (Microsoft)
16 bits)
 char 8 8 8 8
 int 16 32 32 16
 short 16 16 16 16
 long 32 32 32 32
 float 32 32 32 32
 double 64 64 64 64
 rango
 exponente
 (double)
±38 ±38 -307 a 308 -307 a 308


Las variables de tipo char son usadas para almacenar caracteres ASCII de 8 bits tales como “A”, “B” y “C” así como cualquier otra cosa que pueda ser almacenada con un byte. Las variables de tipo int pueden almacenar cantidades enteras que no requieren un componente fraccional; las variables de este tipo son usadas frecuentemente para controlar bucles y enunciados condicionales. Las variables de tipos float y double son usadas cuando se requiere de una componente fraccional o cuando se requieren números muy grandes o muy pequeños en una aplicación. La diferencia entre una variable float y una variable double es la magnitud del número mayor así como del número menor que puede contener. Como puede verse en la penúltima tabla dada arriba, un double puede almacenar un número muchas veces mayor que un número float.

Con la excepción de void, los tipos básicos de datos pueden tener varios modificadores que los preceden. Un modificador puede ser usado para alterar el significado de algún tipo de dato para enfrentar de manera más precisa algún tipo de situación. Los modificadores esenciales son: signed, unsigned, long y short. Los modificadores signed, unsigned, long y short pueden ser aplicados a los tipos de base usados para caracteres y enteros. Asimismo, long también puede ser aplicado a double como lo resume la siguiente tabla con varias combinaciones posibles para una computadora basada en los procesadores Intel 8086/8088:


Tipo  Extension
en bits
Rango
 char 8  -128 a 127
 unsigned char 8  0 a 255
 signed char 8  -128 a 127
 int 16  -32768 a 32767
 unsigned int 16  0 a 65535
 signed int 16  -32768 a 32767
 short int 16  -32768 a 32767
 unsigned short int  16  0 a 65535
 signed short int 16  -32768 a 32767
 long int 32  -2147483648 a
 2147483647
 signed long int 32  -2147483648 a
 2147483647
 unsigned long int 32  0 a 4294967295 
 float 32  3.4E-38 a 3.4E+38
 double 64  1.7E-308 a 1.7E+308 
 long double 80  3.4E-4932 a
 1.1E+4932


Aunque es permitido, el uso de signed para enteros es redundante porque el entero predeterminado supone un número con signo. La diferencia entre los enteros con signo y los enteros sin signo es la manera en la cual el bit de orden máximo (bit más significativo) es interpretado. Si se especifica un entero con signo, el compilador generará código que supone que el bit más significativo debe ser usado como una bandera de signo. Si el bit de la bandera de signo es igual a 0, entonces el número es positivo; si el bit de bandera es 1, entonces el número es negativo. Los números negativos son representados usando el método de dos complemento, en el cual todos los bits en el número exceptuando la bandera del signo son invertidos y se le añade 1 al número. Finalmente, la bandera de signo es puesta a 1. Los enteros con signo son importantes en muchos algoritmos, pero tienen únicamente la mitad de la magnitud que pueden tener los enteros sin signo. Tómese por ejemplo el entero 32,767 dado en el sistema binario (ocupando dos bytes):

01111111 11111111

Si el bit de orden alto (el que está más a la izquierda) es puesto a 1, entonces el entero sería interpretado como:

-1

Sin embargo, si el número ha sido declarado como un unsigned int,  entonces el número se vuelve 65,535.

El siguiente programa C resalta la diferencia entre enteros con signo y enteros sin signo:


   #include <stdio.h>

   main()
   {
      int i;   /* entero con signo */
      unsigned int j; /* entero sin signo */

      j = 60000;
      i = j;
      printf("%d %u", i, j);
   }


Si se compila y ejecuta el programa anterior, se obtendrá (esto puede variar de sistema a sistema, dependiendo de la extensión en bits que se les dé a los tipos básicos):

-5536  60000

La razón por la diferencia en los dos resultados es que el patrón binario de bits que representa a 60000 como un entero sin signo es interpretado como -5536 cuando se le toma como un entero con signo, en virtud de que %d le indica a la función printf() que imprima el código en formato decimal, mientras que el código de formato %u que introducimos aquí ordena que se imprima un entero sin signo, esto es, un unsigned int.

El lenguaje C permite una notación abreviada para la declaración de enteros unsigned, short y long; podemos escribir simplemente unsigned, short o long prescindiendo de int, como se muestra aquí:

   unsigned A;

   unsigned int A;

Ambas expresiones declaran una variable A como un entero sin signo.

Las variables del tipo char pueden ser usadas para almacenar también un entero “pequeño” en el rango entre -128 y 127, y pueden ser usadas en lugar de un entero numérico en una situación que no requiere de grandes números. A modo de ejemplo, el siguiente programa usa una variable char para controlar un bucle que imprime el alfabeto en la pantalla:


   #include <stdio.h>

   main()
   {
      char letra;

      for(letra = 'A'; letra <= 'Z'; letra ++)
         printf("%c ", letra);
   }


Si este bucle for puede parecer desconcertante, recuérdese que un caracter como “A” es representado dentro de la computadora como un número (véase el anexo puesto al final de esta obra titulado “El código ASCII”), y los valores desde “A” hasta la “Z” son secuenciales en orden ascendente.

La sintaxis de la forma general usada para la declaración de una variable en C es:


tipo</i>  lista_de_variables;

en donde el tipo debe ser uno de los tipos de datos válidos en C y la lista_de_variables puede consistir de uno o de más nombres identificadores separados con comas. He aquí algunos ejemplos de declaraciones:

   int x, y, z;

   short int entero_corto;

   unsigned int enero_sin_signo;

   float velocidad, temperatura, presion;

A diferencia de lo que ocurre en otros lenguajes (como BASIC, en donde una variable llamada A$ es necesariamente una variable de hilera), el nombre de una variable no tiene nada que ver con el tipo de la variable.

El lugar en donde declaramos una variable en un programa C tiene un efecto importante en cómo la variable será usada en otras partes del programa. Las reglas que determinan cómo será usada una variable, dependiendo del lugar en donde es declarada, son llamadas las reglas de alcance del lenguaje. Hay tres lugares en un programa C en donde las variables pueden ser declaradas. El primer lugar es fuera de todas las funciones que haya en el programa C incluída la función principal main(). Este tipo de variable tiene un alcance global y puede ser accesada desde cualquier parte del programa (desde cualquier función). El segundo lugar en donde una variable puede ser declarada es dentro de una función. Las variables que son declaradas de este modo son llamadas variables locales porque tienen un alcance local, y solo son accesibles por enunciados que se encuentran puestos dentro de la misma función. En esencia, una variable local es conocida únicamente por el código puesto dentro de una función, y es desconocida por completo fuera de la función.

El último lugar en donde las variables pueden ser declaradas es en la declaración de los parámetros formales de la función (esto es, los parámetros usados para recibir argumentos cuando la función es invocada). Además de cumplir la tarea especial de recibir la información que es pasada a una función, estos parámetros actúan como cualesquier otra variable local. A continuación tenemos un programa que lleva a cabo una suma de números desde 0 hasta 9:


   #include <stdio.h>

   int suma;    /* ESTA ES UNA VARIABLE GLOBAL */

   /* funcion para sumar cumulativamente al total */

   total(int x)   /* VARIABLE DE PARAMETRO FORMAL */
   {
      suma = x + suma;
   }

   mostrar()
   {
      int conteo;   /* ESTA ES UNA VARIABLE LOCAL */
      /* esta variable conteo es diferente de la
            variable conteo puesta main() */

      for(conteo=0; conteo<3; conteo++) printf(".");
      printf("la suma actual es %d\n", suma);
   }


   main()
   {
      int conteo;   /* ESTA ES UNA VARIABLE LOCAL */ 
      /* esta variable conteo es diferente de la
            variable contedo puesta en mostrar() */

      suma = 0;  /* inicializar */

      for(conteo=0; conteo<10; conteo++) {
         total(conteo);
         mostrar();
      }
   }


Si se ejecuta el programa anterior, se imprimirá lo siguiente:
...la suma actual es 0
...la suma actual es 1
...la suma actual es 3
...la suma actual es 6
...la suma actual es 10
...la suma actual es 15
...la suma actual es 21
...la suma actual es 28
...la suma actual es 36
...la suma actual es 45
Obsérvese que hemos puesto aquí la función principal main() al final, después de haber declarado y definido las dos funciones total() y mostrar(). ¿Por qué? Por una sencilla razón que suele desconcertar a muchos novatos que son introducidos a los entornos integrados de desarrollo C: en muchos compiladores C, si la función principal main() hace una invocación a otras funciones, y tales funciones son declaradas y definidas después de la función principal main(), el compilador marcará un error diciendo que hay funciones no definidas. En los programas que hemos visto previamente en los cuales hay por lo menos otra función además de la función main(), se había puesto la función principal main() al principio por razones meramente didácticas. Hay algunos cuantos compiladores C que buscarán primero a la función main() en dondequiera que se encuentre, ya sea al principio o al final, y una vez que la encuentran y hallan que dentro de la misma se invocan otras funciones, entonces repasan todo el código fuente hasta ir localizando cada una de dichas funciones, y solo si no encuentran alguna de ellas entonces marcarán como error fatal una función (o más) no definida. Pero no todos los compiladores hacen tales operaciones de búsqueda y cotejación, muchos compiladores requieren que las funciones que serán usadas por la función principal main() sean definidas previamente, de lo contrario no llevarán a cabo el proceso de compilación y marcarán errores fatales (Borland C++ es uno de los entornos que hace tal cosa).

De hecho, si en cualquiera de los programas en los cuales hemos usado la función printf() se borra la línea con la que se ordena al compilador incluír el archivo de cabecera STDIO.H:

#include <stdio.h>

el compilador marcará una omisión grave indicando que se ha encontrado una función indefinida, la función printf(), porque la declaración de esta función se encuentra precisamente en el archivo de cabecera STDIO.H, que dicho sea de paso por ser declarado al principio de cualquier programa C garantizará que la función aludida no sea tomada como una función indefinida (esta es la razón por la cual todos los archivos de cabecera tienen que ser puestos al principio de todo programa C).

Por fines puramente didácticos, seguiremos poniendo al principio la función <main(), habido el hecho de que el análisis de cualquier programa elaborado en lenguaje C empieza a partir de la función principal main(), este es el punto de partida. No podemos entender bien un programa elaborado en un lenguaje como C viéndolo en forma “panorámica”. Tenemos que entrar dentro del código empezando por la función principal main() y seguirlo en el mismo orden en el cual la computadora debe llevar a cabo la ejecución de una secuencia de instrucciones. Hay que “jugar a ser la computadora”, tomando incluso cada instrucción y cada rutina o función tal y como esperamos que lo haga la computadora, siguiendo un orden estricto. Pero si se quiere obtener un programa que pueda ser compilado sin errores, entonces las funciones usadas por main() tendrán que ser relocalizadas con el editor de texto y puestas al principio del programa C.

Como puede verse en el programa C que se acaba de dar, la variable global suma puede ser accesada por cualquier función que forme parte del programa, precisamente por ser una variable global. Sin embargo, la variable conteo declarada dentro de la función main() no puede ser accesada directamente por la función total() y debe ser pasada como argumento. Esto es necesario porque una variable local solo puede ser usada por código en la misma función en donde es declarada, y nadie más fuera de la función puede tener acceso a ella. Finalmente, obsérvese que la variable conteo usada en la función mostrar() es completamente diferente de la variable conteo usada en la función main(), De nueva cuenta, porque una variable local solo es conocida por la función dentro de la cual es declarada, C trata a conteo en mostrar() como una variable completamente separada e independiente de aquella en main().

Hay algunos detalles importantes que hay que tener presentes. En primer lugar, no puede haber dos variables globales que puedan tener el mismo nombre. Si las hubiera, el compilador no sabría cuál de las dos usar. En la gran mayoría de los compiladores C, intentar declarar dos variables globales con el mismo nombre producirá un mensaje de error. En segundo lugar, una variable local en una función puede tener el mismo nombre que las variables locales usadas en otras funciones, sin que ello ocasione conflicto alguno. La razón de esto es que el código y los datos dentro de una función son completamente independientes de aquello usado en otra función. Puesto de otro modo, los enunciados dentro de una función no tienen conocimiento alguno de los enunciados usados dentro de otra función, y cada función es tratada como una “caja negra”. Naturalmente, dentro de una función tampoco puede haber dos variables locales con el mismo nombre.

En lo que toca a constantes, en C esto se refiere a valores fijos que no podrán ser alterados en ninguna parte del programa. Las constantes pueden ser declaradas en cualquiera de los tipos básicos de datos. La manera en la cual es declarada cada constante depende de su tipo. Las constantes de caracter son encerradas entre comillas sencillas, de modo tal que 'W' y '%' ambas son constantes de caracter. Las constantes de enteros son especificadas como números sin componentes fraccionarias (o sea, nada que involucre el uso del punto decimal), mientras que las constantes de punto flotante son números que requieren el uso de un punto decimal.

Como posiblemente el lector ya lo sabe, en programación a veces es más fácil usar un sistema numérico en otra base distinta de la base decimal, un sistema numérico base 8 ó base 16 en lugar de base 10. El sistema numérico basado en 8 es el sistema octal porque usa los dígitos 0 a 7; en octal el número 10 es lo mismo que el número 8 en sistema decimal. Y el sistema numérico base 16 es llamado hexadecimal porque usa los dígitos 0 a 9 más las letras A a la F que representan 10, 11, 12, 13, 14 y 15 en el sistema decimal; el número hexadecimal 10 equivale al número 16 en decimal. En virtud de la frecuencia con la cual estos sistemas numéricos son usados por los programadores profesionales, C permite especificar constantes enteras en sistema hexadecimal u octal en lugar de sistema decimal. Una constante hexadecimal debe comenzar siempre con “0X” (un cero seguido de una equis) seguido de la constante en su forma hexadecimal. Una constante octal debe comenzar con un cero, como en los ejemplos que se muestran:

   palabra = 0xF3A5;   /* 62373 en decimal */

   registro = 022552;   /* 9578 en decimal  */

El hecho de que C interprete automáticamente un número precedido por un cero como un número octal no debe causar preocupación de que números decimales puedan ser tomados como números octales, en virtud de que el clásico “cero a la izquierda” usado en chistes sobre cosas que carecen de valor no es algo que se acostumbre utilizar en números reales consignados en documentos.

Además de los tipos predefinidos de datos, hay otros tipos de constantes a las cuales C proporciona soporte, entre las cuales se encuentra la hilera. Una hilera es un conjunto de caracteres encerrado entre comillas dobles. De este modo, “esta es una hilera de caracteres” es una hilera. Ya hemos visto algunos ejemplos de hileras encerradas entre comillas dobles en algunos de los enunciados usados con printf() en los programas de muestra.

Es importante no confundir los caracteres con las hileras. Una constante de caracter sencillo es encerrada entre comillas sencillas, por ejemplo 'f', mientras que “f” es una hilera que contiene una sola letra.

Encerrar todas las constantes de caracter entre comillas sencillas es algo que funciona sin problema alguno en los caracteres que se pueden imprimir en una impresora. Sin embargo, hay algunos cuantos caracteres no-imprimibles (los cuales forman parte del código ASCII) los cuales es imposible meter desde un teclado dentro de una hilera. Por este motivo, C proporciona como algo especial las constantes de caracter de diagonal inversa (backlash character constants). Los códigos utilizados se muestran en la tabla siguiente:


 Codigo  Significado
\b  Backspace (atrás)
\f  Form feed (alimentación de página)
\n  Newline (línea nueva)
\r  Carriage return (regreso de carro)
\t  Horizontal tab (tabulador horizontal) 
\'  Single quote (comila sencilla)
\"  Double quote (comilla doble)
\0  Null (nulo)
\\  Backlash (diagonal inversa)
\v  Vertical tab (tabulador vertical)
\a  Bell (campana de alerta)
\N  Constante N octal
\xN  Constante N hexadecimal


De este modo, con la diagonal inversa podemos enviar a la pantalla (o a la impresora) para su reproducción símbolos como las comillas dobles que de otra manera no sería posible reproducir por estar reservados para propósitos especiales en C, o producir ciertos efectos (como el tabulador o el regreso hacia atrás del cursor de impresión de caracteres) para los cuales no hay otra forma de pedirlos a un programa. Ya hemos estado usando la diagonal inversa en la función printf() al “imprimir” como parte del final de una hilera el caracter “\n” que es tomado como una orden de mover el cursor hacia la siguiente línea. De este modo, con la asignación:

tabulador = '\t';

podemos invocar a la constante tabulador para pedir un desplazamiento en varios espacios de texto hacia la derecha para imprimir algo.

Cuando son declaradas, podemos asignarle a la mayoría de las variables en C un valor poniendo un signo de igualadad y una constante después del nombre de la variable. La sintaxis de inicialización de variables es la siguiente:

tipo  nombre = constante;

Algunos ejemplos de inicialización son:

   char letraH = 'H';

   int semilla = 1;

   float humedad = 67.75;

Cuando se inicializa una variable, es importante aparear el tipo de la constante con el tipo de la variable. En los siguientes casos:

   int manzanas = 250;

   int duraznos = 120.450;

la primera inicialización es correcta mientras que la segunda inicialización es incorrecta.

Las variables globales son inicializadas únicamente al principio de un programa, mientras que las variables locales son inicializadas cada vez que la función en la cual son declaradas es utilizada. En forma predeterminada, todas las variables globales son inicializadas a cero si no se hace una asignación al momento en que se les declara; mientras que las variables locales que no son inicializadas contendrán valores desconocidos antes de que se les haga la primera asignación.

La ventaja principal de inicializar variables es que ello reduce ligeramente la cantidad de código usada en un programa. A modo de ejemplo, tomaremos el programa dado arriba para suma cumulativa de números del 0 al 9, y lo convertiremos en un programa que le da al usuario la opción de introducir un número a su antojo; el programa sumará los números desde 1 hasta el número dado por el usuario (recuérdese que para que el programa pueda ser compilado sin errores en la gran mayoría de los compiladores C, la función main() tiene que ser puesta después de la definición de la función total()):


   #include <stdio.h>

   main()
   {
      int t;

      printf("Dame un numero: ");
      scanf("%d", &t);
      total(t);
   }

   total(int x)
   {
      int suma = 0, i, conteo;

      for(i=0; i<x; i++) {
         suma = suma + i;
         for(conteo = 0; conteo < 3; conteo++) printf(".");
         printf("la suma actual es is %d\n",suma);
      }
   }


Una cosa en la que C es generoso es la cantidad de operadores disponibles. Definimos un operador como un símbolo que le instruye al compilador que lleve a cabo ciertas manipulaciones matemáticas o lógicas. Hay tres tipos de operadores: matemáticos, lógicos, y de bits (bitwise). La siguiente tabla nos dá un resumen de los operadores aritméticos en C:


 Operador  Acción
+  Suma
-  Resta,
 menos unario
*  Multiplicación
/  División
%  División modular
++  Incremento
--  Decremento



Los operadores de adición y resta producen la misma acción que los que encontramos en la aritmética escolar. El operador de resta también es usado como menos unario para representar números negativos como los siguientes:

-125   ,   -562.45   ,   -.3235

El operador de división modular, %, produce el residuo de una división entre números enteros cuando esta se lleva a cabo, y no puede ser utilizado en operaciones que involucran números del tipo float y double. A continuación se tiene un programa que calcula el cociente y el residuo de dos enteros proporcionados por un usuario (los números enteros introducidos por el usuario son separados con un espacio en blanco):


   #include <stdio.h>

   main()
   {
     int x, y;

     printf("dame un dividendo y un divisor: ");
     scanf("%d%d", &x, &y);

     printf("cociente %d\n", x/y);
     printf("residuo %d ", x%y);

   }


Además del operador de división modular, C introduce dos operadores útiles que no son encontrados en otros lenguajes, los operadores de incremento y decremento, los cuales son de mucha utilidad en operaciones de conteo en bucles. La operación de incremento añade 1 al operando, mientras que la operación de decremento le resta 1 al operando. Las siguientes dos operaciones:

   A++;

   B--;

representan en C lo mismo que:

   A = A + 1;

   B = B - 1;

Tanto el operador de incremento como el operador de decremento se pueden poner antes y después de un operando. Por ejemplo:

   A = A + 1;

puede ser escrito ya sea como:

   ++A;

en lo que se conoce como modo prefijo o como:

   A++;

en lo que se conoce como modo postfijo. Existe una diferencia importante en el modo prefijo y el modo postifjo, o sea en el orden en el cual sean puestos los operadores de incremento o decremento al lado de un operando. Cuando un operador de incremento o decremento precede al operando (modo prefijo), C llevará a cabo primero la operación de incremento o decremento antes de usar el valor del operando, mientras que cuando el operador está puesto después del operando (modo postfijo) C usará el valor del operando antes de aplicar el incremento o decremento. Considérese el siguiente código:

   A = 5;
   B = ++A;

En este caso, A es primero incrementada de 5 a 6, y el nuevo valor (6) es asignado a B. Por otro lado, con el código:

   A = 5:
   B = A++;

primero se le asigna 5 a B, y una vez hecho esto A es incrementada de 5 a 6. En ambos casos, A sube de valor de 5 a 6, pero la diferencia está en <i>cuando</i> ocurre; en el primer caso la variable B termina con el valor 6 mientras que en el segundo caso la variable B termina con el valor 5. Podemos resumir la acción del siguiente modo:


 ++A   Incrementar el operando A en 1,
 y luego usarlo
 A++   Usar el valor del operando A,
 y luego incrementarlo


De este modo, en la siguiente operación en la cual se usa el modo prefijo:

   Q = 2*++A;

primero se incrementa la variable A en una unidad, y después se multiplica con el factor 2, mientras que en la siguiente operación en la cual se usa el modo postfijo:

   Q = 2*A++;

primero se multiplica la variable A por el factor 2, y el producto resultante es incrementado en una unidad. Obviamente, los resultados obtenidos en ambas operaciones serán diferentes.

El origen de este tipo curioso de operadores de incremento y decremento se debe a que una instrucción en C como:

   i++;

tenía un equivalente directo en el conjunto de instrucciones del procesador utilizado por la computadora PDP-11 a su vez empleada en el desarrollo del lenguaje C. Así pues, la sintaxis de C como un lenguaje ensamblador de alto nivel refleja sus orígenes.

En lo que toca a los operadores relacionales y lógicos, la siguiente tabla resume los operadores de este tipo que C tiene disponibles:


Operadores relacionales
 Operador  Significado
>  Mayor que
>=  Mayor o igual que
<  Menor que
<=  Menor o igual que
= =  Igual
!=  No igual
Operadores lógicos
Operador Significado
&&  AND
||  OR
!  NOT


El siguiente programa muestra el uso de los operadores relacionales:


   #include <stdio.h>

   main()
   {
     int i, j;

     printf("dame dos numeros enteros: ");
     scanf("%d%d", &i, &j);

     printf("%d == %d es %d\n", i, j, i==j);
     printf("%d != %d es %d\n", i, j, i!=j);
     printf("%d <= %d es %d\n", i, j, i<=j);
     printf("%d >= %d es %d\n", i, j, i>=j);
     printf("%d < %d es %d\n", i, j, i<j);
     printf("%d > %d es %d\n", i, j, i>j);

   }


Los operadores relacionales siempre regresan un valor de 1 (verdadero) ó 0 (falso) dependiendo del resultado de la prueba relacional. Pueden ser aplicados a cualquiera de los tipos básicos de datos. El siguiente fragmento reproduce el mensaje “mayor que” porque en el equivalente numérico de la secuencia de caracteres ASCII, una 'F' es mayor que una 'B':

   car1 = 'B';
   car2 = 'F';
   if(car2 > car1) printf("mayor que");

Los operadores lógicos son usados para apoyar las operaciones lógicas OR, AND y NOT de acuerdo a la siguiente tabla de verdad en donde 1 es usado para verdadero y 0 es usado para falso.


 p  p AND q   p OR q   NOT p 
0 0 0 0 1
0 1 0 1 1
1 0 0 1 0
1 1 1 1 0


El siguiente programa demuestra el uso de los operadores lógicos:


   #include <stdio.h>

   main()
   {
     int i, j;

     printf("dame dos numeros ya sea 0 o 1: ");
     scanf("%d%d", &i, &j);

     printf("%d AND %d es %d\n", i, j, i && j);
     printf("%d OR %d es %d\n", i, j, i || j);
     printf("NOT %d es %d\n", i, !i);
   }


En C, al igual que en otros lenguajes de programación, existe el concepto de la precedencia de operadores. Tanto los operadores relacionales como operadores lógicos tienen menor precedencia que los operadores aritméticos. En lo que toca a los operadores aritméticos, una expresión como:

20 > 10 + 5

es evaluada como:

20 > (10 + 5)   

y no como:

(20 > 10) + 5

En pocas palabras, primero se efectúa la suma, obteniéndose 15, y después se lleva a cabo la operación de comparación lógica, dando como resultado verdadero (1).

Usualmente, C no garantiza cómo serán evaluadas las partes de una expresión. En el enunciado:

aviones = (3 + 7) * (2 + 9);

la suma 3+7 puede ser evaluada antes que la suma 2+9, o bien puede ser al revés (sin embargo, las prioridades de los operadores garantizan que ambas serán evaluadas antes de que se lleve a cabo la multiplicación). Esta ambigüedad es intencional en el lenguaje C para permitirle a los constructores de programas compiladores efectuar la selección más eficiente para cierto sistema en particular. Sin embargo, en ausencia de paréntesis, C garantiza que todas las expresiones lógicas siempre serán evaluadas de izquierda a derecha, una consideración importante en virtud de que se pueden combinar varias expresiones lógicas en una sola expresión como en el siguiente ejemplo:

25>10 && !(20<10) || 7<=8

para el cual el orden de evaluación es el mismo que el siguente fijado por paréntesis:

( (25>10) && !(20<10) ) || (7<=8)

PROBLEMA: ¿Cuál es el valor lógico de la expresión anterior?

El valor lógico de la expresión es 1 (verdadero).

PROBLEMA: ¿Cómo es interpretada la siguiente expresión?:

a > b || b > c && b > d

El orden de evaluación es el mismo que en:

( (a > b) || (b > c) ) && (b >d)

La siguiente tabla nos proporciona la precedencia relativa de los operadores relacionales y lógicos:


 Orden de precedencia
(de mayor a menor)
! (mayor)
>    >=    <    <=
= =    !=
&&
|| (menor)


Al igual que como ocurre con las expresiones aritméticas en otros lenguajes de programación, se usan paréntesis para alterar el orden natural de evaluación en una expresión relacional y/o lógica. La siguiente expresión, por ejemplo:

1 && !0 || 1

es evaluada como cierta (1) después de que el primer AND es evaluado como cierto de acuerdo a la regla que fue dada arriba. Sin embargo, si a la misma expresión se le añaden paréntesis en la manera en que se muestra, el resultado será falso (0):

1 && !(0 || 1)

En virtud de que (0||1) es evaluado como cierto, el NOT inversor lo cambia a falso, y ocasiona que el AND resulte falso.

Todas las expresiones relacionales y lógicas producen un valor que es 1 ó 0. De este modo, el siguiente programa imprime un 1 en la pantalla:


   #include <stdio.h>

   main()
   {
     int x;

     x = 100;
     printf("%d", x>10);
   }


Los operadores lógicos y relacionales son usados para apoyar enunciados de control del programa incluyendo todos los bucles así como el enunciado if. El siguiente programa usa dicho enunciado para imprimir los números pares entre 1 y 100 (inclusive):


   #include <stdio.h>

   main()
   {
     int i;
     for(i=1; i<=100; i++)
       if(!(i%2)) printf("%d ", i);
   }


En este ejemplo, la operación de división modular (i%2) produce un resultado igual a cero (falso) cuando se usa con un entero par. Este resultado es invertido con el operador lógico NOT.

El operador de asignación en C es el signo de igualdad. A diferencia de otros lenguajes de programación, C permite que el operador de asignación pueda ser usado también en expresiones que involucran operadores relacionales y lógicos. Considérese el enunciado if usado en el siguiente programa:


   #include <stdio.h>

   main()
   {
     int x, y, producto;

     printf("dame dos numeros: ");
     scanf("%d%d", &x, &y);

     if( (producto=x*y) < 0 )
       printf("un numero es negativo\n");
     else
       printf("el producto positivo es: %d", producto);
   }


En el programa, el usuario dá dos números enteros separados con un espacio en blanco. Si uno de ellos es negativo, se imprime el mensaje “un número es negativo”. Si los dos son positivos, se imprime el mensaje “el producto es: ” dándose también el producto de ambos números.

Obsérvese la expresión usada en if. En primer lugar, a producto se le asigna el valor x*y. A continuación, la expresión de asignación entre paréntesis es probada contra cero. Este tipo de código es perfectamente válido y es común encontrar enunciados de este tipo en código C escrito por profesionales. En C podemos ver al operador de asignación haciendo dos cosas. La primera es que asigna el valor en el lado derecho a la variable que se encuentra en el lado izquierdo. Sin embargo, cuando se usa como parte de una expresión más grande, el operador de asignación produce el valor del lado derecho de la expresión. Por lo tanto, la parte de la expresión (producto=x*y) le asigna a producto el valor de x*y así como regresar dicho valor. Este es el valor que es probado contra el cero lógico en la expresión if. Los paréntesis son necesarios porque el operador de asignación tiene menor precedencia que los operadores relacionales.