domingo, 18 de noviembre de 2012

Operadores y Máscaras de Bits

Las computadoras almacenan datos en forma de 0s y 1s. La razón de ésto es que el binario es el sistema de numeración posicional más simple. Un bit es la menor cantidad de información que se puede almacenar. Un conjunto de 8 bits recibe el nombre de palabra o byte. El lenguaje de programación C es un lenguaje de nivel medio. Ésto, entre otras cosas, significa que es posible realizar operaciones a nivel de bits. Ésta es una interacción directa con el hardware y por lo tanto es mucho más rápida que la programación a alto nivel. Por ejemplo, es posible determinar el tamaño en bytes de cualquier tipo de datos con
=====================================================================================
El operador unario sizeof()
=====================================================================================
Vamos a considerar el siguiente programa

/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 *                                                         +
 * Este programa imprime los tamanios en bits de variables +
 * tipo char, int, double, etc.                            + 
 *+++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

#include 
#define TAMANO 2

int main()
{  /* Abre main*/
char c;
int x;
int Arreglo[TAMANO];
double y;

printf("\nEl tamanio en bytes de una variable char es: %d\n", sizeof(c));
printf("El tamanio en bytes de una variable int es: %d\n", sizeof(x));
printf("El tamanio en bytes de una arreglo int de %d localidades es: %d", TAMANO, sizeof(Arreglo));
printf("\nEl tamanio en bytes de una variable double es: %d\n", sizeof(y));

return 0;
}  /* Cierra main*/

La ejecución del mismo produce

El tamanio en bytes de una variable char es: 1
El tamanio en bytes de una variable int es: 4
El tamanio en bytes de una arreglo int de 2 localidades es: 8
El tamanio en bytes de una variable double es: 8
Cuando el operador sizeof se aplica a una variable char, el resultado es 1 porque 1 byte (8 bits) es el tamaño necesario para almacenar un carácter cualquiera de los 256 que forman el código ASCII. Así que por esa razón el tamaño de una variable tipo carácter es estándar. El tamaño de un entero en C, depende de la máquina en la que se ejecuta; para ésta máquina en particular es de 4 bytes (32 bits) (véase la entrada La representación en complemento a 2 para más detalles.) También se ha definido un arreglo de 2 localidades enteras; el número de bytes que ocupa dicho arreglo es 8, cuatro para cada localidad entera. Adicionalmente aparece una variable tipo double, que ocupa 8 bytes (64 bits).
=====================================================================================
& El Operador AND de Bits
=====================================================================================
El operador & (NO confundir con el operador lógico &&) es un operador lógico a nivel de bits. Compara un par de cadenas de bits bit por bit, el resultado para cada comparación es 1 si los dos bits son 1 y 0 en otro caso. Como ejemplo, vamos a considerar las siguientes cadenas:
Una máscara de bits
Empezando por la derecha, el primer bit de la primera cadena, tiene un 1, y la segunda cadena tiene también un 1, por lo tanto, 1 & 1 = 1. Aplicando & a los bits de la siguiente posición: 0 & 0 = 0, los siguientes: 1 & 0 = 0; después 0 & 1 = 0; los bits de la 5ta posición: 1 & 1 = 1, y los últimos dan 0, 1 & 0 = 0.
Con el operador & se puede ocultar un conjuto de bits que no son relevantes en determinada situación. Ésto constituye una máscara de bits. Vamos a considerar un pequeño problema y elaborar una solución: En un ciclo controlado por una variable llamada TAMANO, es necesario imprimir el valor del contador sólo desde 0 hasta 15, y comenzar después en 0, aún cuando TAMANO se siga incrementando. Con algunas instrucciones condicionales dentro del ciclo es posible realizar esta tarea, pero con una máscara de bits, se puede escribir un programa como el siguiente:

/*+++++++++++++++++++++++++++++++++++++++++++++
 *                                            +
 * Este programa imprime ciclicamente valores +
 * de una variable por debajo de un limite,   +
 * por medio de una mascara de bits.          +
 * +++++++++++++++++++++++++++++++++++++++++++*/

#include 
#define TAMANO 200
#define LIMITE 15

int  main()

{ /* Abre main*/
int i = 0;

for ( i = 0; i < TAMANO; i++ )
{
 printf("%d\n", i & LIMITE);
}

return 0;
} /* Cierra main*/

La salida de este programa es: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, ...
Se ha mantenido la impresión por debajo de la variable LIMITE. La línea relevante, para los fines de esta entrada, es la siguiente:

printf("%d\n", i & LIMITE);

la función printf imprime un entero, el entero que se produce de la comparación de los bits de el primer número, el contador i, con el número LIMITE. Es importante mencionar que éste y los siguientes operadores son aplicables únicamente a variables de tipo entero, como char, int, long, short, con los calificativos signed y unsigned. En éste caso, la variable límite funciona como una máscara, que oculta todos los bits a la izquierda de el último 1 a la izquierda.
Máscara de bits para controlar un ciclo.
Ésta figura presenta los primeros 6 bits de los los números LIMITE (15) y un valor de TAMANO (25), recuerde que, en nuestra máquina, los enteros tienen tìpicamente 32 bits, o sea que hacia la izquierda de cada renglón se encuentran muchos ceros que no se han dibujado por comodidad. Aplicando el operador & bit por bit a ambas representaciones obtenemos el renglón de abajo, el cual es el número 9 en el sistema de numeración binario. Observe que más allá del último 1 (el de la extrema izquierda)de la máscara (15) todos los bits del resultado se vuelven 0, y si hubiera algún 0 antes del último 1 ocurriría una ambigüedad, ya que 0 ó 1 en TAMANO produciría 0. ÉSta es la razón por la cual éste método funciona solamente con números que en su representación binaria tengan sólo 1s, como 3 (11), 7 (111), 15 (1111), 31 (11111), etc.
=====================================================================================
<< El Operador de desplazamiento izquierdo de Bits
=====================================================================================
Vamos a considerar el siguiente programa.

/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*  Este programa hace uso de el operador de desplazamiento *
*  hacia la izquierda <<                                   *
*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

#include 
using namespace::std;

int main()
{   /* Abre main*/
int x = 1;
// Los operadores de bits solo se aplican a variables unsigned
cout <<"\nEl tamanio de una variable int es: "<< sizeof(int)<< endl;

cout <<"\nEl valor inicial de x: "<< x << endl;

for (unsigned i = 1; i <= 8; i++ ) 
{
  
unsigned desplazado = x << i;

cout <<"\nEl numero: " << desplazado << endl;
}

cout <<"\nEl valor final de x: " << x << endl;
}   /* Cierra main*/


La ejecución es la siguiente:

El tamanio de una variable int es: 4

El valor inicial de x: 1
El numero: 2
El numero: 4
El numero: 8
El numero: 16
El numero: 32
El numero: 64
El numero: 128
El numero: 256
El valor final de x: 1

Al principio se define una variable llamada x, la cual se inicializa con el valor 1. Un ciclo for, de 1 a 8 incluidos, utiliza el operador de desplazamiento izquierdo para recorrer los bits hacia la izquierda, lo cual, en el sistema posicional binario, hace que el valor se incremente en potencias de 2, tal como se muestra en la figura siguiente:

Observe que el valor de x, fuera del ciclo for, sigue siendo de 1.

=====================================================================================
>> El operador de desplazamiento derecho de bits
=====================================================================================
De manera similar, el lenguaje C proporciona el operador de desplazamiento derecho de bits >>. El siguiente programa muestra cómo opera:

/*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*  Este programa hace uso de el operador de desplazamiento *
*  hacia la derecha  >>                                    *
*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

#include 
using namespace::std;

int main()
{   /* Abre main*/
int x = 128;

cout <<"\nEl tamanio de una variable int es: "<< sizeof(int)<< endl;

cout <<"\nEl valor inicial de x: "<< x << endl;

for (unsigned i = 1; i <= 8; i++ ) 
{
  
unsigned desplazado = x >> i;

cout <<"\nEl numero: " << desplazado << endl;
}

cout <<"\nEl valor final de x: " << x << endl;
}   /* Cierra main*/

La salida es la siguiente:

El tamanio de una variable int es: 4

El valor inicial de x: 128

El numero: 64
El numero: 32
El numero: 16
El numero: 8
El numero: 4
El numero: 2
El numero: 1
El numero: 0
El valor final de x: 128

Se inicia con una variable llamada x, la cual se inicializa en 128, en un ciclo for se va disminuyendo el valor de dicha variable por medio del operador de desplazamiento derecho. La variable adquiere sucesivamente los valores 128, 64, 32, 16, 8, 4, 2, 1. Una representación esquemática del acarreo de bits es la siguiente:


martes, 6 de noviembre de 2012

100 000 visitas y mi otro Blog

Bueno, supongo que comparado con muchas páginas ésta cifra no es nada. El caso es que en éstos días, éste blog ha alcanzado las 100000 páginas vistas, lo cual me da mucho gusto. Sobre todo si tengo en cuenta que cuando inicié, hace poco más de dos años, recibía algunas decenas de visitantes al mes. Con el paso del tiempo, de manera lenta pero continua, las visitas se han incrementado. Como dije, esto me da mucho gusto, pero también me hace pensar que la mayoría de los que aquí entran vienen con la intención de resolver un problema o de encontrar información útil; por ésto he cambiado la orientación que tenía éste blog. Al principio fue, y lo sigue siendo, una forma de almacenar la información que yo tenía. No se me ocurrió nada mejor para guardar mis programas que hacerlos públicos en ésta página; cualquiera que haya intentado almacenar información por mucho tiempo en un disco duro se habrá dado cuenta qué tan peligrosa es esa práctica; el blog ha sido para mi un espacio más estable. Pero como decía, los visitantes no pueden leer mi mente, y casi todo el código que tengo aquí está muy poco explicado, a eso me refería con el cambio en la orientación. Ahora trato de escribir no sólo para mí, sino para las personas que ocasionalmente los visitan. También estoy reescribiendo muchas entradas, cambiando el formato (ahora simplemente no tengo formato) y tratando de ponerme en los zapatos de quien entra aquí por primera vez y no sabe siquiera qué es un blog. A lo largo de éste tiempo he tenido diversas plantillas, de las estándar de blogger a la presente, que me ha resultado más atractiva. He tenido ya varias generaciones de lectores y algunos de ellos incluso han regresado y han dejado comentarios útiles e interesantes.
La intención original de éste blog sigue siendo la misma: resolver problemas y ejercicios de programación. En particular los correspondientes a los libros de Deitel (Java y C++) y los de Kernighan y Ritchie (algunos de ellos bastante difíciles desde el enunciado mismo). Si no he podido terminarlos ha sido por una mezcla de circunstancias entre las que sobresale mi habitual pereza. Sin embargo, la idea sigue tan válida como en el principio.
Por otro lado, hace ya algún tiempo, decidí crear otro blog, en wordpress, para continuar con algo que empecé en éste: mis entradas relativas a Unix - Linux. Resulta que desde hace mucho he venido usando exclusivamente linux y, como no sabía absolutamente nada, he llevado un registro de las cosas que voy aprendiendo en el camino. Por eso no han aparecido más entradas clasificadas en la etiqueta Linux. Ahora están en Aprendiendo a Usar Linux
Aprendiendo A Usar Linux
Algunas de las entradas de Ejercicios Resueltos hacen referencia a Aprendiendo a Usar Linux, además de que algunos de los comentarios aquí son contestados con el perfil de wordpress. Estoy seguro de que muchos de los visitantes de éstos blogs tienen intereses comunes. La razón por la cual estas entradas no aparecen juntas es, entre otras cosas, porque blogspot y wordpress tienen ventajas y desventajas. Wordpress es más "estable" aunque ofrece menos servicios; blogspot ofrece mayores libertades.
En ambos casos, bloguear me ha sigo doblemente útil y divertido, por un lado los blogs sirven de archivos que están disponibles en culquier momento y por otro, el explicar algo para otra persona enseña bastante: una idea que parece bastante clara, resulta no serlo tanto cuando se tiene que explicar. También puedo añadir una tercera ventaja de tener un blog: los visitantes. Sean principiantes, avanzados o expertos en el tema, algunos dejan comentarios muy interesantes y educativos. Esta entrada es un agradecimiento a ellos.

miércoles, 10 de octubre de 2012

Deitel_C++_5.18 (Construyendo la Computadora Simpletron)

SECCIÓN ESPECIAL: CONSTRUYA SU PROPIA COMPUTADORA
_____________________________________________________________________________________
En los siguientes problemas nos desviaremos temporalmente del mundo de la programación en lenguajes de alto nivel para "abrir de par en par" una computadora y ver su estructura interna. Presentaremos la programación en lenguaje máquina y escribiremos varios programas en este lenguaje. Para que ésta sea una experiencia valiosa, crearemos también una computadora (mediante la técnica de la simulación basada en software) en la que pueda ejecutar sus programas en lenguaje máquina.
_____________________________________________________________________________________
5.18 (Programación en Lenguaje Máquina) Crearemos una computadora a la que llamaremos Simpletron. Como su nombre lo indica, es una máquina simple, pero poderosa. Simpletron sólo ejecuta programas escritos en el único lenguaje que entiende directamente: el lengaje máquina de Simpletron, o LMS.
Simpletron contiene un acumulador, un registro especial en el cual se coloca la información antes de que Simpletron la utilice en los cálculos, o que la analice de distintas maneras. Toda la información dentro de Simpletron se manipula en términos de palabras. Una palabra es un número decimal con signo de cuatro dígitos, como +3364, -1293, +0007 y -0001. Simpletron está equipada con una memoria de 100 palabras, y se hace referencia a estas palabras mediante sus números de ubicación 00, 01, ..99.
Antes de ejecutar un programa LMS debemos cargar, o colocar, el programa en la memoria. La primera instrucción de cada programa LMS se coloca siempre en la ubicación 00. El simulador empezará a ejecutarse en esta ubicación. Cada instrucción escrita en LMS ocupa una palabra de la memoria de Simpletron (y, por lo tanto, las instrucciones son números decimales de cuatro dígitos con signo). Supondremos que el signo de una instrucción LMS siempre será positivo, pero el signo de una palabra de información puede ser positivo o negativo. Cada una de las ubicaciones en la memoria de Simpletron puede contener una instrucción, un valor de datos utilizado por un programa o un área no utilizada (y, por lo tanto, indefinida) de memoria. Los primeros dos dígitos de cada instrucción LMS son el código de operación que especifica la operación a realizar. Los códigos de operación de LMS se sintetizan en la figura siguiente.

_________________________________________________________________________________________________________
CÓDIGO DE OPERACIÓN                       SIGNIFICADO
_________________________________________________________________________________________________________

Operaciones de entrada/salida
_________________________________________________________________________________________________________

final int LEE = 10;                       Lee una palabra desde el teclado y la introduce en una 
                                          ubicación específica de memoria.

final int Escribe = 11:                   Escribe una palabra de una ubicación específica de memoria
                                          y la imprime en la pantalla.
_________________________________________________________________________________________________________

Operaciones de carga/almacenamiento
_________________________________________________________________________________________________________
final int CARGA = 20;                     Carga una palabra de una ubicación específica de memoria y 
                                          la coloca en el acumulador.

final int ALMACENA = 21;                  Almacena una palabra del acumulador dentro de una ubicación
                                          específica de memoria.
_________________________________________________________________________________________________________
Opoeraciones aritméticas
_________________________________________________________________________________________________________
final int SUMA = 30;                      Suma una palabra de una ubicación específica de memoria a la
                                          palabra en el acumulador (deja el resultado en el acumulador).

final int RESTA = 31;                     Suma una palabra de una ubicación específica de memoria a la
                                          palabra en el acumulador (deja el resultado en el acumulador).

final int DIVIDE = 32;                    Divide una palabra de una ubicación específica de memoria a la
                                          palabra en el acumulador (deja el resultado en el acumulador).

final int MULTIPLICA                      Multiplica una palabra de una ubicación específica de memoria a la
                                          palabra en el acumulador (deja el resultado en el acumulador).
_________________________________________________________________________________________________________

Operaciones de transferencia de control
_________________________________________________________________________________________________________
final int BIFURCA = 40;                   Bifurca hacia una ubicación específica de memoria.

final int BIFURCANEG = 41;                Bifurca hacia una ubicación específica de memoria si el 
                                          acumulador es negativo.

final int BIFURCACERO = 42;               Bifurca hacia una ubicación específica de memoria si el 
                                          acumulador es cero.

const int ALTO = 43;                      Alto. El programa completó su tarea.
__________________________________________________________________________________________________________

Los dos últimos dígitos de una instrucción LMS son el operando (la dirección de la ubicación en memoria que contiene la palabra a la cual se aplica la operación). Consideremos varios programas simples en LMS.

El primer programa en LMS, figura siguiente, lee dos números del teclado, cualcula e imprime su suma. La instrucción +1007 lee el primer número del teclado y lo coloca en la ubicación 07 (que se ha inicializado con 0). Después, la instrucción +1008 lee el siguiente número y lo coloca en la ubicación 08. La instrucción carga, +2007, coloca el primer número en el acumulador y la instrucción suma, +3008, suma el segundo número al número en el acumulador. Todas las instrucciones LMS aritméticas dejan sus resultados en el acumulador. La instrucción almacena, +2109, coloca el resultado de vuelta en la ubicación de memoria 09, desde la cual la instrucción escribe, +1109, toma el número y lo imprime (como un número decimal de cuatro dígitos con signo). La instrucción alto, +4300, termina la ejecución.

Ubicación         Número               Instrucción
00                +1007                (Lee A)
01                +1008                (Lee B)
02                +2007                (Carga A)
03                +3008                (Suma B)
04                +2109                (Almacena C)
05                +1109                (Escribe C)
06                +4300                (Alto)
07                +0000                (Variable A)
08                +0000                (Variable B)
09                +0000                (Resultado C)

El segundo programa en LMS (figura siguiente) lee dos números desde el teclado, determina e imprime el valor más grande. Observe el uso de la instrucción +4107 como una transferencia de control condicional, en forma muy similar a la instrucción if de Java.

Ubicación         Número               Instrucción
00                +1009                (Lee A)
01                +1010                (Lee B)
02                +2009                (Carga A)
03                +3110                (Resta B)
04                +4107                (Bifurcación negatuva a 07)
05                +1109                (Escribe A)
06                +4300                (Alto)
07                +1110                (Escribe B)
08                +4300                (Alto)
09                +0000                (Variable A)
10                +0000                (Variable B)

Ahora escriba un programa en LMS para realizar cada una de las siguientes tareas:

a) Usar un ciclo controlado por centinela para leer 10 números positivos. Calcular e imprimir la suma.
b) Usar un ciclo controlado por contador para leer siete números, algunos positivos y otros negativos, y calcular e imprimir su promedio.
c) Leer una serie de números, determinar e imprimir el número más grande. El primer número leído indica cuántos números deben procesarse.
_____________________________________________________________________________________
SOLUCIÓN:
a) Usar un ciclo controlado por centinela para leer 10 números positivos. Calcular e imprimir la suma.

Ubicacion         Numero               Instruccion
00                +1015                Lee el primer numero y lo almacena en la localidad 15
01                +2015                Carga el numero almacenado en 15
02                +4004                Bifurcacion positiva a la localidad 04
03                +4300                Para
04                +1016                Lee un segundo numero y lo almacena en la localidad 16
05                +2016                Carga el numero de la localidad 16
06                +4009                Bifurcacion positiva a 09
07                +1115                Imprime el numero de la localidad 15
08                +4300                Para
09                +3015                Suma el numero de la localidad 15
10                +2115                Almacena la suma en la localidad 15
11                +1016                Lee otro numero y lo almacena en la localidad 16
12                +4009                Bifurcacion positiva a 09
13                +1115                Imprime el numero almacenado en 15
14                +4300                Para
15                +0000                Variable
16                +0000                Variable

1) Recibe un número y para si es negativo (00 a 03) Si el número es positvivo, se guarda en la variable Suma.
2) Recibe un segundo número y si es negativo, se imprime Suma y se para (04-08) Si el número es positivo, se suma a la variable Suma.
3) Se realiza el ciclo hasta que se recibe un número negativo (09-12)
4) Se imprime la suma y se detiene la ejecución (13-14)
5) Variables (15 y 16)

b) Usar un ciclo controlado por contador para leer siete números, algunos positivos y otros negativos, y calcular e imprimir su promedio.

Ubicacion         Numero               Instruccion
00                +1020                Lee un numero y lo almacena en la localidad 20
01                +2020                Carga el numero almacenado en 20
02                +4100                Bifurcacion negativa a la localidad 00
03                +2020                Carga el numero almacenado en 20
04                +4200                Bifurcacion 0 a la localidad 00

05                +1024                Lee un numero y lo almacena en la localidad 24
06                +2024                Carga el valor almacenado en la localidad 24
07                +3023                Suma el valor de la localidad 23 al acumulador
08                +2123                Almacena la suma en la localidad 23

09                +2121                Carga el valor almacenado en 21
10                +2022                Suma el valor de la localidad 22
11                +2121                Almacena la suma en la localidad 21

12                +2021                Se carga el valor de la localidad 21
13                +3120                Se resta el valor almacenado en la localidad 20
14                +4005                Si la diferencia es positiva, bifurca a la localidad 05

15                +2021                Carga la variable de la localidad 21
16                +3223                Se divide el contenido de 18 entre el contenido en el acumulador
17                +2125                Se almacena el resultado en la localidad 25
18                +1125                Se Imprime el resultado
19                +4300                Para


20                +0000                Limite
21                +0000                Contador (Inicializado en 0)
22                +0000                Uno (Inicializado en uno)
23                +0000                Suma (Inicializado en 0)
24                +0000                Variable

Éste es el programa. No recibe 7 números, sino cualquiera que el usuario introduzca al principio. Por lo demás, el procedimiento es igual para 7 o para cualquier número N. Se requiere una variable que haga las veces de contador, en la localidad 21, inicializada en 0. Esta variable se incrementa cada vez que se realiza el ciclo. El incremento es siempre en uno, y se hace sumando la constante uno almacenada en la localidad 22. También es necesaria otra variable más, llamada suma, localizada en la localidad 23, la cual se inicia con el valor 0 y se incrementa cada vez con el valor del número recibido.
El programa consta de 6 partes, separadas por espacios para hacer más visible esta división. Éstas partes son:
1) Recepción del límite ( de la localidad 00 a la 04) Verifica que el primer número (el que indica cuántos enteros se recibirán) sea positivo. Para ésto, primero se revisa que no sea negativo; si lo es, el control se regresa al inicio, a la localidad 00. Después de ésto, se verifica que el número sea distinto de 0. Si lo es, de nuevo se regresa el control a la localidad 00.
2) Suma del número. (De las localidades 05 a 08) Recibe un número y lo suma al contenido de la variable suma, la cual, inicialmente tiene el valor 0.
3) Incremento del contador (De las localidades 09 a 11) Para incrementar la viariable Contador, almacenada en la localidad 21, se hace uso de la constante Uno, almacenada en la localidad 22.
4) Verificación de la condición para verificar que Contador no haya superado a Límite, se resta el primero del segundo. En caso de que el resultado no sea positivo (esto incluye 0 o un número negativo), se termina el ciclo, de lo contrario, se transfiere el control a 05.
5) Impresión de los resultados (de la localidad 15 a la 19) En estas instrucciones se divide la Suma entre el Límite y se presenta el resultado.
6) Variables (Localidades 20 a 25) En esta sección se almacenan las variables a las que se hace referencia en el programa.

c) Leer una serie de números, determinar e imprimir el número más grande. El primer número leído indica cuántos números deben procesarse.

Ubicacion         Numero               Instruccion
00                +1026                Lee un numero y lo almacena en la localidad 26
01                +2026                Carga el numero almacenado en 26
02                +4100                Bifurcacion negativa a la localidad 00
03                +2026                Carga el numero almacenado en 26
04                +4200                Bifurcacion 0 a la localidad 00

05                +1024                Lee un numero y lo almacena en la localidad 24

06                +2025                Carga el valor almacenado en la localidad 25
07                +3126                Resta el valor almacenado en 26
08                +4221                Bifurcacion cero (transferencia de control) a la localidad 21

09                +1023                Recibe un número y lo almacena en 23

10                +2024                Carga el numero de la localidad 24
11                +3123                Resta el número de la localidad 23
12                +4015                Bifurca positivo a la localidad 15

13                +2023                Carga el número almacenado en la localidad 23
14                +2124                Almacena el número en la localidad 24

15                +2025                Carga la variable de la localidad 25
16                +3027                Suma el contenido de la localidad 27
17                +2125                Se almacena el resultado en la localidad 25

18                +2025                Se carga el contenido de la localidad 25
19                +3126                Se resta el contenido de la localidad 26
20                +4109                Bifurca negativo a la lodalidad 09

21                +1124                Imprime la localidad 24
22                +4300                Para

23                +0000                Numero
24                +0000                Mayor
25                +0000                Contador (Inicializado a 1)
26                +0000                Limite
27                +0000                Uno (Inicializado a 1) (Inicializado en 0)

También éste programa ha sido dividido en partes para su mejor comprensión.
1) Recepción del límite ( 00 a 04) Ésta variable indica cuántos números se recibirán. No se aceptan negativos ni 0.
2) Recepción del primer número (05) Éste número, por ser el primero, se almacena en la variable Mayor, localidad 24.
3) Primera verificación del límite (06-08) En caso de que el límite sea sólo 1 número, éste condicional lleva el control a la localidad 21.
4) Recepción de número (09) Si el límite es 2 o más, se recibe el siguiente número y se almacena en la variable Número (localidad 23).
5) ¿Es más grande Mayor o Número? (10 a 12) Si Mayor - Número > 0, entonces el control se transfiere a la localidad 15, de lo contrario se lleva a cabo las instrucciones siguientes.
6) Mayor = Número (13-14) Estas líneas asignan un nuevo valor a la variable Mayor.
7) Incremento del contador (15-17) Éste incremento hace uso de la variable Uno, la cual se inicializa en 1.
8) Verificación del contador (18- 20) Si Contador - limite es negativo, se transfiere el control a la localidad 9. De lo contrario (una diferencia igual a 0), se realiza la siguiente instrucción.
9) Se imprime el número Mayor y se termina el programa (21-22) Esta instrucción se realiza cualesquiera que sean las transferencias de control anteriores.
10) Variables (23-27) La variable Número almacena los sucesivos números que se introducen. La variable Mayor almacena siempre el mayor, Contador debe ser inicializado a 0, Límite se recibe del usuario al principio, y Uno debe inicializarse a 1.

sábado, 29 de septiembre de 2012

¿Qué es un Algoritmo?

Los algoritmos están en la base, y son uno de los conceptos más importantes, de la computación. Un algoritmo es una solución por pasos a un problema de cómputo. Éstos pasos individuales pueden ser repetidos una y otra vez, pero no deben ser ambigüos y, eventualmente, tienen que terminar. Una analogía muy usada para dejar claro qué es un algoritmo, son las recetas de cocina. Éstas incluyen una serie de pasos a realizar en los que el orden es importante y que eventualmente terminan. Por ejemplo, para cocinar huevos con jamón, una receta típica diría:

Paso 1) Pique y fría en mantequilla el jamón hasta que adquiera
        un tono dorado
Paso 2) Agregue uno por uno los huevos y revuelva hasta obtener
        una mezcla homogénea 
Paso 3) Añada una pizca de sal.
Paso 4) Revuelva de vez en cuando hasta que la mezcla esté bien 
        cocida
Paso 5) Sirva con pan tostado.

La tarea es cocinar huevos revueltos, pero ésta ha sido dividida en pasos más pequeños en los cuales el orden es importante. Por ejemplo, vamos a suponer que la cocinera no realiza los pasos en el órden 1) 2) 3) 4) 5), sino en ésta variante: 1), 3), 4), 2), 5). Todos los pasos se ha realizado, pero en éste caso desafortunado, en vez de tener el desayuno servido, tendremos pedazos salados de jamón revueltos con huevos crudos. Éstas instrucciones presentan ambigüedades, por lo cual no se pueden considerar propiamente un algoritmo: hay personas para las que "una pizca de sal" es una cucharada cafetera, y "un tono dorado" puede significar "fría los jamones hasta que se deshidraten tanto que queden crujientes". Desde luego, cualquier cocinera buena evitaría hacer semejantes desastres, pero no debemos confiar mucho en que las cocineras harán un buen desayuno a base de una mala receta, de la misma manera en que no debemos confiar mucho en que el poder ciego de las computadoras va a ejecutar un buen programa a base de un mal algoritmo.
LLevando más lejos incluso la comparación, podemos decir que escribir programas de computación es bastante parecido, al menos conceptualmente, a escribir recetas de cocina. Una receta inicia con una lista de ingredientes y continúa con las instrucciones necesarias para elaborar el platillo. Un programa de cómputo inicia con una declaración de variables que hacen las veces de ingredientes y continúa con instrucciones en las que éstos datos se manipulan para dar un resultado.
Los algoritmos son el grial de la computación. Su importancia se resume en ésta frase, tomada del libro Introducción al diseño y análisis de algoritmos, Un enfoque estratégico, de Lee, Tseng, Chang y Tsai:

"Si usted es rico, y no conoce mucho sobre algoritmos, tal vez no esté preparado para competir con alguien pobre que sepa mucho de algoritmos"

La frase apunta a un hecho bastante común: suele pensarse que para obtener grandes velocidades de cómputo, basta un procesador que realice una gran cantidad de ciclos de instrucción por segundo. Desde luego ésto no es cierto. Se estudian algoritmos para resolver problemas de cómputo de manera eficiente, aún cuando se disponga de los mejores procesadores. Tampoco es cierto que el estudio de los algoritmos haya surgido con la cibernética. Por poner un ejemplo, La Criba de Eratóstenes, un algoritmo para encontrar números primos, fue desarrollado hace 2300 años. Y, finalmente, los algoritmos constituyen la base de la computación no solamente en el software, sino en el hardware. Las operaciones aritméticas llevadas a cabo por la Unidad Aritmética, están basadas en algoritmos, y se ha construído un hardware optimizado cuando se han descubierto algoritmos más eficientes para realizar operaciones aritméticas a nivel de bits.
En las siguientes entradas veremos un par de métodos que los estudiosos de la computación usan para representar algorimos: El Seudocódigo y Los Diagramas de Flujo.
_________________________________________________________________________________________
_________________________________________________________________________________________

Esta entrada forma parte del Curso de C con Programas Explicados Línea por Línea
Índice
Anterior
Siguiente


jueves, 20 de septiembre de 2012

Las virtudes del programador, según Larry Wall


Larry Wall es el creador del lenguaje de programación Perl y en alguno de sus libros publicó las que para él son tres virtudes básicas del programador. Supongo que estas características son necesarias y útiles no sólo en programación, sino en cualquier actividad. "hibris" es una palabra griega antigua que significa orgullo y confianza excesiva en uno mismo:

1 Pereza - Es la cualidad que te hace realizar un gran esfuerzo para reducir el total del gasto energético. Te hace escribir programas que ahorren trabajo y que otras personas encuentren útil, y documentar lo que escribes para no tener que responder muchas preguntas sobre él. Por lo tanto, la primera gran virtud de un programador. Y también por lo tanto, este libro. Ver también impaciencia e hibris.
2 Impaciencia - La ira que se siente cuando el ordenador se está volviendo perezoso. Esto te hace escribir programas que no solo reaccionan a tus necesidades, sino que se anticipen a ellas. O al menos lo pretendan. Por lo tanto, la segunda virtud de un programador. Ver también pereza e hibris.
3 Hibris - Orgullo excesivo, la suerte de cosas que Zeus te arrebata. También la cualidad que te hace escribir (y mantener) programas de los cuales otras personas no puedan decir cosas malas. Por lo tanto, la tercera virtud de un programador. Ver también pereza e impaciencia.

Unos programas básicos escritos en Perl, explicados paso a paso, se encuentran en Mi Otro Blog

domingo, 16 de septiembre de 2012

¿Qué es la memoria RAM?

De los dispositivos de almacenamiento de información, la memoria RAM es la más importante para la ejecución de un programa. Una memoria consta de un número (generalmente) muy grande de localidades y un nombre para cada una de ellas. La memoria RAM es la unidad de almacenamiento en la que se guardan los datos y las instrucciones que lleva a cabo la Unidad de Proceso (PU). Sea por ejemplo la operación aritmética

7 + 3 = 10

A la hora de ejecutar el programa, esos datos, los sumandos y la suma, necesitan ser almacenados. Vamos a considerar una memoria RAM en miniatura como la que se presenta en la siguiente figura:

Ésta memoria tiene 12 localidades, cada una de las cuales ha sido etiquetada con un nombre del 0 al 11. En cada una de esas celdas es posible almacenar información. Esta disposición recuerda un conjunto de lockers como los que están disponibles en muchas universidades para guardar útiles. En los espacios de memoria, en vez de tener libros, tenemos tipos de datos. ¿Cuántos bits hay en cada una de las celdas? La gran mayoría de las computadoras tienen unidades independientes de memoria (llamadas espacio de direccionamiento) de 8 bits. Cada grupo de 8 bits recibe el nombre de byte. Las computadoras son direccionables por bytes porque en 8 bits caben todos los caracteres del teclado en código ASCII. Recuerde que los caracteres del código ASCII van del 00 al 255, exactamente los 256 números que se pueden almacenar en 8 bits. Dado que lo menos que tiene que hacer una computadora personal es recibir instrucciones del teclado, ese es el tamaño mínimo direccionable necesario. Una memoria RAM de 2 GB contiene 2000 000 000 de bytes o 16 000 000 000 de bits. Modernamente, para realizar cálculos numéricos, se han creado memorias direccionables por 64 bits.
Nuestra memoria de juguete puede ser direccionable hasta por 4 bits, aunque eso no es importante. Estamos considerando que el número 7 fue almacenado en el espacio 1; el número 3, en el 4. Para acceder a las localidades de memoria, es necesario guardar en el Registro de Dirección de Memoria (MAR) la dirección de la localidad a la cual se va a tener acceso; el resultado de la lectura de dicha localidad se almacena en el Registro de Datos de Memoria (MDR). La PU realiza la operación y guarda el resultado (10) en la dirección de memoria marcada con el número 10. Como puede verse de esta explicación, el tiempo de acceso a los datos almacenados repercute en el tiempo de ejecución del programa. Imaginemos que se tiene que recorrer una por una las direcciones de memoria antes de obtener la necesaria (este tipo de búsqueda es llamada acceso secuencial). En una memoria con miles de millones de espacios de direccionamiento, esto no es eficiente. En lugar de eso, sólo se busca la dirección almacenada en la MAR, este tipo de búsqueda es conocida como acceso aleatorio. De aquí viene el nombre RAM (Random Acces Memory, o memoria de acceso aleatorio).

La RAM tiene estas características:
1) Es volátil. Lo cual significa que la información almacenada estará disponible sólo mientras la computadora está encendida. La vida de las variables de un programa es incluso menor que eso. Duran sólo lo que dura el tiempo de ejecución. Incluso en el lapso de dicha ejecución, una dirección de memoria, identificada con un nombre y tipo de variable, puede cambiar el contenido almacenado, de la misma manera que en el caso de los lockers, con el tiempo cambia el material guardado, aunque la ubicación del locker siga siendo la misma.
2) Es de fácil acceso. Ésto quiere decir que para el procesador es fácil (rápido) extraer y almacenar información en ella, comparado con el acceso y escritura en los dispositivos de almacenamiento como los discos duros.
3) Es imprescindible. No puede echarse a andar una computadora sin memoria RAM.

Cada lenguaje de programación define el tamaño para los tipos de datos enteros y flotantes. Dependiendo del tipo, se reservan una o más localidades de memoria para almacenar datos. Un arreglo es un conjunto contiguo de espacios de direccionamiento. Las variables tipo puntero no guardan números sino direcciones. Una máquina con poca memoria RAM será necesariamente más lenta que otra con mucha; incluso, como en nuestro caso hipotético, podría no poder realizar ciertas operaciones (como sumar números que excedan el almacenamiento de la memoria), sin embargo, ya que, al final de cuentas, lo que hacen es ejecutar programas, si se les da tiempo y memoria, todas las computadoras pueden hacer las mismas cosas.

viernes, 14 de septiembre de 2012

Kernighan_Ritchie_2.10 (Convertir un texto a minúsculas)

___________________________________________________________________________________
2.10 Reescriba la función lower, que convierte letras mayúsculas en minúsculas, con una expresión condicional en vez de un if-else.
___________________________________________________________________________________
SOLUCIÓN: La modificación es directa. El programa está escrito de tal manera que se puede recibir como entrada un archivo de texto y convertirlo a minúsculas. La razón por la que éste programa sólo funciona con el código ASCII, es porque la línea principal de la función lower es la siguiente:

x = (c >= 'A' && c <= 'Z')? c + 'a' - 'A': c;

básicamente hace uso del hecho que en ASCII, las letras mayúsculas aparecen antes que las minúsculas, y la distancia entre una mayúscula y su correspondiente minúscula es de 32 posiciones. Así, el valor de A es 65, y el de a es 97; el de S es 83, y el de s 115. Por tal motivo, en la instrucción de asignación, primero se verifica que el carácter sea mayúscula, si lo es, entonces se cambia a el correspondiente valor más 32 ( escrito como c + 'a' -'A'), de lo contrario, se deja igual.


#include <stdio.h>

int lower(int s);


////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////

int main()
{ /*Abre main*/
int z, l;

printf("\nPor favor, introduzca un texto. ");
printf("Se imprimira en minusculas.\n");

while( (z = getchar()) != EOF)
{ /* Abre while */  
l = lower(z);
putchar(l);
}/*Cierra while */

return 0;
} /*Cierra main*/

/*//////////////////////////////////////////////
// LOWER
//////////////////////////////////////////////*/

int lower(int c)
{  /*Abre lower*/
int x;

x = (c >= 'A' && c <= 'Z')? c + 'a' - 'A': c;

return x;
}  /*Cierra lower*/


La ejecución con éste texto como entrada

ERASE UN HOMBRE A UNA NARIZ PEGADO,
ERASE UNA NARIZ SUPERLATIVA
UNASE UNA NARIZ ZAYON Y ESCRIBA,
ERASE UN PEJE ESPADA MUY BARBADO.

produce:

Por favor, introduzca un texto. Se imprimira en minusculas.

erase un hombre a una nariz pegado,
erase una nariz superlativa
unase una nariz zayon y escriba,
erase un peje espada muy barbado.

Kernighan_Ritchie_2.5 (Primera posición de la coincidencia de dos cadenas)

___________________________________________________________________________________
2.5 Escriba la función any(s1,s2), que regresa la primera posición de la cadena s1 en donde se encuentre cualquier carácter de la cadena s2, o -1 si s1 no contiene caracteres de s2. (La función de biblioteca estándar strpbrk hace el mismo trabajo pero regresa un apuntador a la posición encontrada).
___________________________________________________________________________________
Solución: El código siguiente presenta la solución del problema. Se ha puesto un par de cadenas, las cuales se pueden cambiar. Lo importante es la función any. La ejecución del programa, tal como está, produce -1 como salida, ya que la cadena "xxz" no tiene ningún elemento en común con "La felicidad esta en lugares mas o menos lejanos".

#include<stdio.h>
enum {TAMANO1 = 100, TAMANO2 = 50};

int any(char s1[], int n, char s2[], int m);
void Imprimir( char cadena[]);

/*//////////////////////////////////
// MAIN
//////////////////////////////////*/

int main()
{ /* Abre main */
int y;

char cadena1[TAMANO1] = "La felicidad esta en lugares mas o menos lejanos";
char cadena2[TAMANO2] = "xxz";
/*La cadena1 */
Imprimir(cadena1);
printf("\n");
/*La cadena 2*/
Imprimir(cadena2);
printf("\n");

y = any(cadena1, TAMANO1, cadena2, TAMANO2);

if (TAMANO1 != y)
printf("\n%d\n", y);
else 
printf("-1\n");
return;
} /* Cierra main*/

/*//////////////////////////////////
// ANY
//////////////////////////////////*/

int any(char s1[], int n, char s2[], int m)

{ /* Abre any*/
int i, j, x = TAMANO1; 

for (i = 0; s2[i] !=  '\0'; i++)   
for (j = 0; s1[j] !=  '\0'; j++)
{ /*Abre for*/
if (s1[j] == s2[i])
{ /* Abre if */
x = (j < x)?  j:x;
break; /*Salimos del for anidado*/
} /* Cierra if*/
} /* Cierra for*/

return x;
} /* Cierra any*/


/*///////////////////////////////////
// IMPRIMIR
///////////////////////////////////*/

void Imprimir(char cadena[])

{ /*  Abre Imprimir */
int i = 0; 
for (i = 0; cadena[i] != '\0'; i++)
putchar(cadena[i]); 

return;
} /* Abre Imprimir */

La ejecución de éste programa es la siguiente:

Cadena 1:
La felicidad esta en lugares mas o menos lejanos
Cadena 2:
xxz
-1

__________________________________________________________________________________________
Esta entrada es parte de los problemas resueltos del libro El Lenguaje de Programación C de B. Kernighan y D. Ritchie
Entrada anterior
Entrada siguiente
__________________________________________________________________________________________

jueves, 13 de septiembre de 2012

Kernighan_Ritchie_2.4 (Suprimir las coincidencias de dos cadenas)

___________________________________________________________________________________
2.4 Escriba una versión alterna de squeeze(s1,s2) que borre cada carácter de s1 que coincida con cualquier carácter de la cadena s2.
___________________________________________________________________________________
Solución:
La función squeeze a la que hace referencia el enunciado aparece en el texto y es la siguiente:


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';
}

Esta función comprime la cadena s1 por medio de eliminar la variable c. La variable i recorre el arreglo que contiene s1 y la variable j recorre el mismo arreglo siempre que la variable leída no coincida con c. Al final, para indicar que la cadena ha terminado, se escribe explícitamente el carácter de terminación de cadena '\0'.
A continuación presento mi versión de ésta problema tal como lo pide el libro. He puesto un par de cadenas, la primera que dice: "La felicidad esta en lugares mas o menos lejanos, como Londres" y la segunda: "Hola". Lo que hace el programa, y concretamente la función squeeze, es recibir las dos cadenas (supone que la primera es mayor que la segunda) y recorrer todo el primer arreglo (el mayor) desde el elemento 0 hasta el último en busca de coincidencias. Al mismo tiempo va sustituyendo las variables encontradas en el mismo arreglo, sólo si no coinciden con alguna de las entradas de la segunda cadena. Para esto, se recorren también todos los elementos del segundo arreglo hasta encontrar el elemento terminal '\0'.

/*************************************************************************
*                                                                        *
* Este programa tiene como datos un par de cadenas: cadena y cadena2     *
* Suprime todas las coincidencias de cadena2 en cadena e imprime cadena  *
*                                                                        *
**************************************************************************/
#include<stdio.h>
#define TAMANO 100

void squeeze( char s[], char t[],  int c);
void Imprime(char cl[], int n);

/*//////////////////////////////////////
// MAIN
//////////////////////////////////////*/ 

int main()
{  /* Abre main */

char cadena[TAMANO] = "La felicidad esta en lugares mas o menos lejanos," " como Londres";
char cadena2[TAMANO] = "Hola";
/*Las cadenas se pueden concatenar en tiempo de complilacion*/

/*cadena antes de llamar a la funcion squeeze*/
printf("\nCadena 1: \n");
Imprime(cadena, TAMANO);
printf("\nCadena 2:\n");
Imprime(cadena2, TAMANO);

printf("\n");

squeeze (cadena, cadena2, TAMANO);
 
/* Cadena despues de llamar a la funcion squeeze */ 
Imprime(cadena, TAMANO);

printf("\n");

return 0; 
}  /* Cierra main */

/*/////////////////////////////////////
// SQUEEZE
/////////////////////////////////////*/

void squeeze( char s[], char f[], int c)
{  /* Abre squeeze */
int i, j, k;

for (k = 0; k < c; k++) 
{ /*Abre for*/
for (i = j = 0; s[i] != '\0'; i++)
if ( s[i] != f[k]) 
s[j++] = s[i];

s[j] = '\0';
} /*Cierra for*/
}  /* Cierra squeeze*/

/*//////////////////////////////////////
//  IMPRIME
//////////////////////////////////////*/

void Imprime(char cl[], int n)
{  /* Abre Imprime */
for (n = 0; cl[n] != '\0'; n++ )
putchar(cl[n]);

}  /* Cierra Imprime */

La ejecución de éste programa produce la salida:

Cadena 1: 
La felicidad esta en lugares mas o menos lejanos, como Londres
Cadena 2:
Hola
L feicidd est en ugres ms  mens ejns, cm Lndres

que es, efectivamente, la cadena original sin los elementos de la segunda.

__________________________________________________________________________________________
Esta entrada es parte de los problemas resueltos del libro El Lenguaje de Programación C de B. Kernighan y D. Ritchie
Entrada Anterior
Entrada Siguiente
__________________________________________________________________________________________

viernes, 8 de junio de 2012

Aritmética en el Lenguaje de Programación C

Antes de hablar de aritmética en el lenguaje C, vamos a hablar simplemente de la aritmética.
¿Cuál es el resultado de la siguiente operación?

5 + 4 x 6 = ?

Muchas personas responden de inmediato que el resultado es 54, porque si se suma 5 + 4 el resultado es 9; si ahora se multiplica 9 x 6, el resultado es 54. Otras contestan que el resultado es 29, porque 4 x 6 = 24, y 5 + 24 = 29.
Este ejemplo es ilustrativo, porque muestra claramente que en aritmética es importante el orden en el cual los operadores actúan. Esta importancia es más grande todavía en programación. La respuesta correcta es la segunda,

5 + 4 x 6 = 29

La razón de ésto es que por convención, la multiplicación tiene un orden de precedencia mayor que la suma. Las operaciones NO se realizan en el orden de aparición de los operadores. La norma es, primero se realizan las multiplicaciones y divisiones y luego sumas y restas. Cuando en una expresión aritmética sólo aparecen sumas (restas) o sólo multiplicaciones (divisiones), el orden en el que se realicen estas operaciones sí es indiferente. El lector tal vez recuerda estos resultados como las leyes asociativas y conmutativas de la suma(resta) y la multiplicación (división): el orden de los sumandos no altera la suma y el orden de los factores no altera el producto.
Los operadores aritméticos binarios del lenguaje c son

_______________________________________________________________
|OPERADORES ARITMETICOS BINARIOS DEL LENGUAJE DE PROGRAMACION C|
_______________________________________________________________
|SIMBOLO EN C   | EQUIVALENTE MATEMATICO | EJEMPLO             |
________________________________________________________________
|      *        |            x           |  5*7 = 35           |
________________________________________________________________
|      /        |            /           |  15/3 = 5           |
_______________________________________________________________
|      +        |            +           |  7 + 4 = 11         |
________________________________________________________________
|      -        |            -           |  14 - 8 = 6         |
________________________________________________________________
|      %        |            %           |  9%2 = 1            |
_______________________________________________________________


Los operadores aritméticos de suma, resta y división son los mismos en C y en matemáticas. No así el operador de multiplicación, que en C es *. La razón de ésto, es que el lenguaje toma cualquier letra, como x ó X, como nombres de variables y no como operadores. El signo * está reservado para ser el operador de multiplicación. El lector está familiarizado con dichas operaciones, y no será necesario abundar en ellas. Probablemente el operador de módulo % no sea tan familiar. Éste operador recibe dos enteros y devuelve el residuo de la que queda al realizar la división de tales números. En el ejemplo presentado en la tabla de arriba, el cociente (entero) de 9/2 = 4, y el residuo es 1.
Para alterar el orden natural de ejecución de los operadores es necesario usar paréntesis. Si en el caso de la suma presentada al principio, usamos un par de paréntesis como éstos, que rodean al 5 y al 4

(5 + 4) x 6 = 54

obtendremos como resultado 54. Los paréntesis indican que lo contenido dentro de ellos, debe considerarse como un sólo número. Primero deben hacerse pues, las operaciones indicadas ahí, y el resultado final multiplicarlo por 6. Casos más complicados pueden aparecer cuando hay más de un par de paréntesis y algunos de ellos están anidados

((4 - 6) - 7*(2 + (-8)) = ?

4 pares de paréntesis aparecen aquí. El primero, externo, rodea a toda la expresión. Ese podría ser suprimido sin ningún problema. Sin embargo, muchas veces, cuando escriba programas, verá que es útil rodear una expresión aritmética con paréntesis externos. El siguiente par rodea a 4 - 6. Esto quiere decir, como en el caso anterior, que hay que considerar esa expresión como un sólo número; ese número es -2. El siguiente par de paréntesis rodea a 2 + (-8). El otro par de paréntesis rodea a -8. En realidad también es redundante, ya que podría haberse escrito 2 - 8, pero la literal también es válida. El resultado de esa expresión es -6. Así que, suprimiendo los paréntesis internos, la expresión queda como

-2 - 7*(-6) = ?

Aquí sólo aparece un par de paréntesis, rodeando a -6. Primero se realiza la multiplicación, -7*(-6) = 42. -2 + 42 = 40. El resultado es 40.
Para la potenciación, logaritmo y raíz cuadrada, el lenguaje C incluye funciones preestablecidas. Éstas funciones están empacadas en el encabezado <stdio.h>

Sin embargo, hay que tener en cuenta que en C los números reales son almacenados en espacios de memoria, y el resultado de las operaciones aritméticas que con ellos se realizan, también. Las operaciones entre números enteros (int) producen un int; las operaciones entre float produce números float. Sin embargo, no es posible realizar, por ejemplo, la suma de un tipo int con un tipo float. En caso de ocurrir una operación de ese tipo, el compilador convierte el tipo int a tipo float de manera implícita, y el resultado es un float. En general la regla es que los tipos menores se convierten a los tipos mayores, lo cual ocurre sin pérdida de precisión. En general, y a pesar de dicha conversión, no es conveniente permitir que ocurran tales casos. El almacenamiento de un tipo float (producto de una operación aritmética) en un tipo int no es posible, y el intento produce un mensaje de error.
Los operadores en una expresión aritmética podrían ser funciones de variables. Por ejemplo:

int a, s, t;

a = f(s) + g(t)

En este fragmento de programa, a la variable entera a se le asigna la suma de los valores de retorno (tipos int) de las funciones f (que depende del parámetro entero s) y g (dependiente, a su vez, del parámetro entero t). En casos como éste, el estándar de C no indica qué función se evalúa primero. Un compilador, en una computadora podría evaluar primero g(t) y luego f(s), en tanto que otro compilador (o incluso el mismo) en una computadora distinta, puede hacer la evaluación al revés. Esto es especialmente grave si las funciones dependen del mismo parámetro y lo modifican en los cuerpos de instrucciones.
También es necesario cuidar la precedencia cuando uno escribe segmentos de código como

a[i] = i++;

Los operadores de incremento y decremento pueden producir sutiles errores cuando se usa la variable incrementada o decrementada en la misma expresión. En este ejemplo, el estándar no dice qué se hace primero, si incrementar el contador o localizar el espacio de memoria en el arreglo. Como recomendación contra éstos errores, es necesario evitar expresiones en las que el resultado depende el orden de evaluación de los elementos de dichas expresiones.
Vale aquí una digresión que no está del todo fuera de lugar. El procesador de una computadora para fines teóricos es llamado unidad de proceso (PU por sus siglas en inglés). Una PU consta de dos partes: la unidad lógica aritmética (ALU) y la unidad de control (CU). La primera es capaz de realizar sumas, restas, etc. y llevar a cabo operaciones lógicas del tipo igual que, mayor o igual que, etc; en tanto que la segunda coordina la realización de esas tareas aritméticas y lógicas. La ALU sólo puede llevar a cabo una operación a la vez, por lo cual es importante el papel jugado por la CU al asignarle las tareas mediante las instrucciones adecuadas. Estas instrucciones se realizan con un orden determinado y en conjunto se les conoce como ciclo de instrucción. Éstos pasos son:
Buscar y traer (Fetch) La PU busca y trae la siguiente instrucción del programa que está ejecutando.
Decodificar la instrucción LA PU tiene que saber qué tipo de instrucción va a realizar la ALU (suma, multiplicación, división, etc.)
Ejecutar la instrucción Esta es la tarea realizada por la ALU.
Almacenar el resultado El resultado de la operación aritmética o lógica realizada por la ALU es almacenada en memoria para su posterior utilización.
Todas las computadoras realizan este ciclo de instrucción, y todo lo que saben hacer las computadoras es solamente esto. Es sorprendente que audio, video,comunicación remota y todas las tareas para las que sirven las computadoras sean realizadas llevando a cabo sólo operaciones lógicas y aritméticas, pero así es. Tan importante es este ciclo, que los procesadores valen por la cantidad de ciclos de instrucción que pueden hacer en un segundo. Un procesador de 3 GHz realiza 3 x 1000000000 ciclos por segundo.
Ninguna tarea tiene control absoluto del procesador. Todas las tareas se enlistan y se efectúan con un orden que depende de la prioridad que tiene la tarea. Los sistemas operativos se crearon para administrar las tareas que hará el procesador y el tiempo asignado a ellas.

miércoles, 30 de mayo de 2012

Las Funciones Getchar y Putchar

La función getchar recibe un carácter, mientras que la función putchar imprime un carácter. Este par de funciones son más útiles de lo que parece. Permiten manipular de distintas maneras archivos y caracteres. El siguiente programa es un clásico. Recibe carácter por carácter la entrada del usuario (puede ser un número, una cadena, un archivo) con la función getchar y lo imprime con la función putchar.


/*+++++++++++++++++++++++++++++++++++++++++
  * ESTE PROGRAMA LEE E IMPRIME UNA CADENA + 
  * CARACTER POR CARACTER                  +
  * +++++++++++++++++++++++++++++++++++++++*/

 /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  *                                                       +
  *                   ALGORITMO                           +
  * Solicitar una cadena                                  +
  *                                                       +
  + Recibe un caracter                                    +
  * Mientras (el caracter recibido no sea fin de archivo) +
  * { // Abre mientras                                    +
  * Imprime el siguiente caracter                         +
  * Incrementa en uno el numero de caracteres             +
  * Recibe el siguiente caracter                          +
  * }  // Cierra mientras                                 +
  *                                                       +
  *++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

  #include <stdio.h>
  
  int main()
  {  /* Abre main */
  int c;

  printf("\nEste programa recibe e imprime una cadena de caracteres ");
  printf("caracter por caracter.\nPor favor introduzca la cadena: \n");

  while(EOF != (c = getchar()))
  putchar(c);
  
  return 0;
  } /* Cierra main */

_____________________________________________________________________________________________
Ahora hagamos un análisis del programa línea por línea para revisar cada instrucción

/*+++++++++++++++++++++++++++++++++++++++++
  * ESTE PROGRAMA LEE E IMPRIME UNA CADENA + 
  * CARACTER POR CARACTER                  +
  * +++++++++++++++++++++++++++++++++++++++*/

 /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++
  *                                                       +
  *                   ALGORITMO                           +
  * Solicitar una cadena                                  +
  *                                                       +
  + Recibe un caracter                                    +
  * Mientras (el caracter recibido no sea fin de archivo) +
  * { // Abre mientras                                    +
  * Imprime el siguiente caracter                         +
  * Incrementa en uno el numero de caracteres             +
  * Recibe el siguiente caracter                          +
  * }  // Cierra mientras                                 +
  *                                                       +
  *++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

Este par de comentarios indican de qué va el programa.

#include <stdio.h>

Un encabezado que indica que se usará entrada y salida estándar. Además de eso, en este programa se hace uso de otro contenido de este encabezado: la definición del valor del carácter que indica el fin de archivo EOF (End of File)

int main()

Recuerde que en C la unidad básica de programación es la función. El nombre main es también un buen nombre para esta función. main se llamaba la función principal en el lenguaje de programación B, del cual evolucionó el lenguaje C.

{  /* Abre main */

Los corchetes se utilizan en cualquier parte donde pueda ponerse una instrucción simple (salvo en el operador ternario). En el caso de la función main (en realidad en cualquier definición de función) las llaves son obligatorias. NO es correcto escribir un programa como éste:

int main()

return 0;

Un programa con una función main vacía debe escribirse así

main()
{

}

Éste es el programa más corto que puede escribirse en el lenguaje C. Sólo consta de 8 caracteres. Sobra decirlo: el programa no hace nada, pero sintácticamente está bien. Es prescindible el encabezado stdio.h, la instrucción return y el tipo int. No son prescindibles el nombre main, los paréntesis después del nombre, ni las llaves. Todos los cuerpos de las funciones deben estar contenidos en llaves.

int c;

Esta declaración de variable reserva espacio para un tipo entero. La variable, c, será utilizada para almacenar temporalmente cada uno de los caracteres. El tipo de datos char es el tipo adecuado para almacenar caracteres. ¿Por qué se utiliza entonces el tipo int? Porque c también debe guardar el valor de EOF, el fin de archivo, el cual generalmente es -1 (véase este programa). Dado que los tipos char son enteros sin signo, al declarar a la variable c como tipo char, no se podría distinguir el fin de archivo.

printf("\nEste programa recibe e imprime una cadena de caracteres ");
printf("caracter por caracter.\nPor favor introduzca la cadena: \n");

Este par de líneas printf son un mensaje para el usuario.

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

Esta es una forma muy compacta de escribir código. Dentro de la condición de while ocurren varias cosas, y ellas tienen que ver con el orden de precedencia de los operadores. Lo que está dentro de los paréntesis externos, sea lo que sea; se evalúa como una expresión booleana. Si no es una expresión booleana, entonces se convierte explícitamente a una, esto quiere decir que si el valor de dicha expresión es 0, entonces se toma como "falso" y no se realiza el cuerpo de if y por otra parte, el compilador tomará como "verdadero" todo valor distinto de 0, y en ese caso se ejecuta el cuerpo de la instrucción while. Lo primero que "ve" el compilador dentro de los paréntesis externos es una condición:
EOF != (expresion) ¿Qué es EOF? EOF es el caracter que indica el fin del archivo. En los sistemas Unix el fin de archivo es Ctrl D, en tanto que en los sistemas windows es Ctrl C. Si usa un sistema distinto, deberá averiguar cuál es la combinación de teclas que indican EOF. EOF tiene un valor determinado por la plataforma en la que programe. Este valor tiene que ser tal que no se pueda confundir con cualquier carácter válido (como a, b, c, \t, etc). Generalmente se trata de un número negativo (-1). La condición dentro de los paréntesis de while dice "mientras el carácter leído sea distinto de EOF, realiza este cuerpo de instrucciones". Pero ¿qué hay a la derecha del operador relacional !=? Este operador es un operador "izquierdo", toma el valor de la izquierda y lo compara con el de la derecha. A su derecha, y entre paréntesis aparece la expresión x = getchar() ¿Por qué aparece esta expresión entre paréntesis? Porque la precedencia del operador relacional != es superior a la del operador de asignación =. De no haber paréntesis, el compilador primero compararía el valor de c con EOF y luego trataría de asignar el valor de getchar() a la cantidad que se encuentra a la izquierda de =, lo cual provocará un error en la compilación; en este caso los paréntesis son necesarios. Por lo tanto, después de evaluar el valor de EOF, el compilador evalúa la expresión c = getchar(), y aquí se encuentra una expresión que contiene al operador = de asignación. La asociatividad de = es de derecha a izquierda, primero se evalúa la expresión a la derecha y eso se le asigna a la variable c. Pero la expresión de la derecha es una invocación de la función getchar(), definida en el encabezado stdio.h Esa función lo que hace es recibir un carácter de la entrada. La sintaxis general de la función getchar es la siguiente:

int getchar(void)

La función retorna un entero (el valor del carácter leído) y no recibe ningún parámetro. Este valor entero se asigna a la variable c, que se ha declarado previamente como tipo int. Y es ese valor, el de c, el que se compara con EOF. Si el carácter leído es distinto a EOF, entonces se lee el siguiente y así. Se trata pues de un ciclo while controlado por centinela.

putchar(c);

El cuerpo de while consta de una sola instrucción: la función putchar, la cual recibe un entero y retorna también un entero. Esos int son los valores de un carácter. La forma general de la función putchar es la siguiente:

int putchar(int caracter);

En este programa, putchar(c) simplemente envía a la salida estándar el carácter que getchar() acaba de leer.

return 0;

La instrucción return 0; como en todos los programas, indica que se ha terminado el programa satisfactoriamente.

} /* Cierra main */

Esta llave indica el fin del cuerpo de main. Muchas veces le va a ocurrir que hay cuerpos de instrucciones que contienen llaves sin cerrar. Para evitar esto, créese el buen hábito de cerrar el cuerpo antes de escribir las instrucciones. También acostúmbrese a indicar, en cada caso, a qué instrucción corresponden las llaves.
_______________________________________________________________________________________________
Lo que aprendió:
El significado de EOF
La función getchar()
La función putchar()
_______________________________________________________________________________________________
Esta entrada forma parte del Curso de C con Programas Explicados Línea por Línea
Índice
Entrada Anterior
Entrada Siguiente
Related Posts Plugin for WordPress, Blogger...