Autor Tema: Proyecto de Curso (Dictado - Notas): Programación en C (2013, por Argentinator)

0 Usuarios y 1 Visitante están viendo este tema.

03 Enero, 2013, 05:13 pm
Leído 51351 veces

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Proyecto de Curso (Dictado - Notas Teóricas): Programación en C

3/Enero/2013

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Bibliografía sugerida

\( \bullet \)  C standard documents

http://port70.net/~nsz/c/

(Una web con muy valiosa información sobre los documentos del estándar C, desde el original ANSI C hasta la actualidad).

\( \bullet \)  Programando con wxDevC++, archivo electrónico disponible en:

Programando con wxDevC++.zip

Programando con wxDevC++.pdf (archivo adjunto a este post)

(Manualcito para entender y utilizar el IDE wxDevC++).

\( \bullet \)  C Programming, 2nd. edition, K. N. King  (incluye estándares C89 y C99).

(Un completo y muy buen manual de referencia del lenguaje C, con información técnica ajustada a los estándares C89 y C99).

\( \bullet \)  El Lenguaje de Programación C, 2da. edición, B. W. Kernighan y D. M. Ritchie.

(Es el clásico libro de los creadores de C, Kernighan y Ritchie. No obstante, hay que recordar que ya es viejo, y que el estándar C se ha independizado de sus creadores originales).

\( \bullet \)  The C book, por: gbDirect, acceso online en:

The C Book

(Un manual de referencia de C de gbDirect Publications, cómodo, gratuito y disponible online).

\( \bullet \)  Final version of the C99 standard with corrigenda TC1, TC2, and TC3 included

Final version of the C99 standard with corrigenda TC1, TC2, and TC3 included, formatted as a draft

(Texto que define el lenguaje C, según el estándar C99 (ISO año 1999), versión corregida, año 2007).

\( \bullet \)  The New C Standard, D. M. Jones

The New C Standard

(Texto que comenta una a una las reglas del estándar C99, con valiosa información técnica y ejemplos interesantes).

\( \bullet \)  XL C/C++ V7.0 (for AIX), IBM (online)

Manual de C de IBM

(El C de IBM no es igual al estándar, pero aún así puede servirnos su manual para hallar información rápida sobre temas comunes de programación en C).


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Índice

Índice por áreas temáticas:

Información general, instalaciones, configuraciones, pruebas:

1.      Instalación, configuración, primeros pasos

2.      Explicando el HolaMundo.c

3.      Un sencillo Hola Mundo en Windows

4.      De la eficacia a la elegancia - Macros del Preprocesador (parte I)

5.      De la eficacia a la elegancia - Macros del Preprocesador (parte II)

6.      Etapas del Preprocesador

10.     Pare de sufrir: Alternativas a la consola CMD

11.     Configuración de opciones del Compilador. Compatibilidad con C99.

12.     Uso básico de la función printf().

30.     ¿Cómo lograr que todo funcione? (Parte I)

31.     ¿Cómo lograr que todo funcione? (Parte II)

Caracteres y strings:

7.      Caracteres (char) y Strings (char*)

33.     Caracteres en el lenguaje C (I)

34.     Caracteres en el lenguaje C (II)

35.     Caracteres en el lenguaje C (III)

36.     Caracteres en el lenguaje C (IV)

37.     Caracteres en el lenguaje C (V)

38.     Caracteres en el lenguaje C (VI)


Números enteros, de punto flotante y complejos:

8.      Enteros en C (parte I)

9.      Enteros en C (parte II)

12.     Uso básico de la función printf().

13.     Testeando tipos de datos enteros de C (parte I)

14.     Testeando tipos de datos enteros de C (parte II)

15.     Testeando tipos de datos enteros de C (parte III)

16.     Testeando tipos de datos enteros de C (parte IV)

17.     Números de punto flotante. Generalidades

18.     Números de punto flotante. Estándar IEEE 754

19.     Números de punto flotante. Estándar C99. Constantes

20.     Números de punto flotante. Redondeos y cambio de base

21.     Números de punto flotante. Valores fuera de rango

22.     Números de punto flotante. Estándar C99. FLOAT.H

23.     Números de punto flotante. Compilador GCC

24.     Números de punto flotante. Programas de Testeo (parte I)

25.     Números de punto flotante. Programas de Testeo (parte II)

26.     Números de punto flotante. Programas de Testeo (parte III)

27.     Números complejos en C. Preliminares

28.     Números complejos en C. Estándar C99. Conversión de tipos

29.     Números complejos en C. Programa de testeo.


(Índice por orden de edición)

1.      Instalación, configuración, primeros pasos

2.      Explicando el HolaMundo.c

3.      Un sencillo Hola Mundo en Windows

4.      De la eficacia a la elegancia - Macros del Preprocesador (parte I)

5.      De la eficacia a la elegancia - Macros del Preprocesador (parte II)

6.      Etapas del Preprocesador

7.      Caracteres (char) y Strings (char*)

8.      Enteros en C (parte I)

9.      Enteros en C (parte II)

10.     Pare de sufrir: Alternativas a la consola CMD

11.     Configuración de opciones del Compilador. Compatibilidad con C99.

13.     Testeando tipos de datos enteros de C (parte I)

14.     Testeando tipos de datos enteros de C (parte II)

15.     Testeando tipos de datos enteros de C (parte III)

16.     Testeando tipos de datos enteros de C (parte IV)

17.     Números de punto flotante. Generalidades

18.     Números de punto flotante. Estándar IEEE 754

19.     Números de punto flotante. Estándar C99. Constantes

20.     Números de punto flotante. Redondeos y cambio de base

21.     Números de punto flotante. Valores fuera de rango

22.     Números de punto flotante. Estándar C99. FLOAT.H

23.     Números de punto flotante. Compilador GCC

24.     Números de punto flotante. Programas de Testeo (parte I)

25.     Números de punto flotante. Programas de Testeo (parte II)

26.     Números de punto flotante. Programas de Testeo (parte III)

27.     Números complejos en C. Preliminares

28.     Números complejos en C. Estándar C99. Conversión de tipos

29.     Números complejos en C. Programa de testeo.

30.     ¿Cómo lograr que todo funcione? (Parte I)

31.     ¿Cómo lograr que todo funcione? (Parte II)

32.     

33.     Caracteres en el lenguaje C (I)

34.     Caracteres en el lenguaje C (II)

35.     Caracteres en el lenguaje C (III)

36.     Caracteres en el lenguaje C (IV)

37.     Caracteres en el lenguaje C (V)

38.     Caracteres en el lenguaje C (VI)



[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

03 Enero, 2013, 06:35 pm
Respuesta #1

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
1. Instalación, configuración, primeros pasos

3/Enero/2013

   El lenguaje C es un lenguaje de programación de computadoras, de alto nivel, estructurado e imperativo.
   No voy a explicar qué significan esos términos.  ::)
   A fines técnicos digamos que:

\( \bullet \)  Trabajaremos con el estándar C99.

Elección de un estándar
   El lenguaje C ha sido implementado con diversas variantes según las necesidades y las empresas que lo usaron. Naturalmente, esto es fuente de incompatibilidades, y por lo tanto se hace necesario establecer un estándar.

   Los estándares que podemos encontrar son los de ANSI (Estados Unidos) e ISO (internacional).
   Podemos esperar que, en lo referido al lenguaje C, ambos estándares coinciden.
   La historia de los estándares puede verse en:

http://es.wikipedia.org/wiki/ANSI_C#Compiladores_compatibles_con_ANSI_C

   Se aprecia que al pasar de los años el estándar C ha tenido modificaciones, nombradas como C89 (ANSI 1989), C90 (ISO 1990, coincide con C89), C99 (ANSI/ISO 1999) y C11 (ANSI/ISO 2011).

   Aunque los estándares están definidos en teoría, o sea en un papel firmado por un comité, no quiere decir que sea fácil hallar compiladores reales compatibles con esas versiones, ni bibliografía adecuada.

   Debido a estas complicaciones, elegiremos trabajar con el estándar C99, ya que el C11 es aún demasiado reciente, y se me ha complicado hallar compiladores adecuados y bibliografía.

[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Elección de un compilador

   Nosotros utilizaremos el compilador GCC, bajo la distribución MinGW.

   Para más información:  http://mingw.org/wiki/MinGW


¿Qué es un compilador y cuál elegir?

   Un lenguaje de programación es, en realidad, algo meramente teórico, está definido en un texto. Para que efectivamente podamos realizar programas en dicho lenguaje, necesitamos de un compilador, esto es, un programa que traduce código escrito según las reglas del lenguaje a instrucciones que la computadora pueda procesar (esto se llama lenguaje de máquina).

   Hay muchos compiladores disponibles para C, sin embargo debemos elegir uno que sea compatible con el estándar C99 que hemos elegido, y que además sea gratuito.
   Y más importante aún: que el compilador no tenga errores.

   Este último requisito quizá no siempre se cumpla, y para ello hay que ir pensando en la posibilidad de ponerse en contacto con la(s) personas que se ocupan del desarrollo del compilador que vayamos a usar.

   En este curso usaremos el compilador GCC bajo la distribución MinGW, que es un proyecto gratuito, serio, y aún en desrrollo.
   Por diversas razones en su historia, GCC tiene compatibilidad más bien con el compilador de Microsoft C, y por lo tanto no es compatible del todo con el C estándar.
   No obstante, el proyecto MinGW apunta a lograr una compatibilidad completa con el estándar C99.

   Por lo tanto, nosotros utilizaremos sólo aquellas herramientas de C99 que están implementadas en GCC/MinGW.

   La elección de esta distribución también nos queda cómoda, pues viene incluida en el IDE wxDevC++.

[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Elección de un IDE

   El IDE (Integrated development environment: entorno integrado de desarrollo) que vamos a utilizar es wxDevC++ 7. La versión a la fecha es 7.4.2.569.
   Incluye la distribución MinGW del compilador GCC (evitando instalar un compilador y un IDE, ambos por separado).

¿Qué es un IDE y cuál elegir?

   Además de un compilador, necesitamos un editor de texto especializado, que esté acondicionado para las tareas comunes de un programador, así como también que tenga opciones que enlacen con el compilador de nuestra preferencia. Este tipo de programas suelen conocerse como Entornos Integrados de Desarrollo, Entornos de Trabajo o IDEs.

   El IDE que voy a utilizar es el más popular, dentro de los gratuitos: wxDevC++.

   Originalmente el DevC++ fue desarrollado por Colin Laplace, Hongli Lai, Yiannis Mandravellos and Mike Berg, hasta la versión 4.9.9.2, y luego quedó trunco.
   A partir de ahí el proyecto fue retomado por dos vías actualmente disponibles: una es la de un equipo bajo el nombre de wxDevC++ (ver Desarrolladores de wxDevC++) y otro por un desarrollador independiente, creo que un tal Orwell (http://orwelldevcpp.blogspot.com.ar/2012/12/dev-c-5304-released.html).

   Voy a preferir wxDevC++ por dos razones. Una, porque al parecer el grupo de desarrollo está muy activo, lo que asegura continuidad y actualización del proyecto, y así nuestras herramientas de programación no quedan obsoletas.
   Otra razón es que parecen tomarse en serio el tema de las widgets, que son herramientas pensadas para la presentación visual de los programas, tan necesario en la informática de este siglo.

[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

¿Cómo instalar wxDevC++?

   Para poder escribir nuestros programas en C y compilarlos, hemos de descargar e instalar el IDE wxDevC++, que incluye el compilador MinGW.

   Para esto, basta ir a la siguiente página web:

Web de descarga de wxDevC++

   Allí seleccionamos la 1era opción (la que dice wxDev-C++ 7 Dynamic installer) y hacemos clic sobre ella.
   Nos descargará un muy pequeño programa instalador, que bajará enseguida.

   Buscamos este programa en la carpeta de descargas de nuestro navegador y lo ejecutamos.
   Al hacerlo, se ejecutará el programa instalador. Aceptemos todas las opciones que vayan apareciendo, y esperemos a que termine el proceso de descarga, descomprensión e instalación.

   Una vez finalizada la instalación tendremos en el menú Programas de nuestro sistema Windows un acceso directo a wxDevC++.

   La primera vez que corremos el IDE wxDevC++ nos hará algunas preguntas.
   Para nuestros futuros propósitos, da igual responder que Sí o que No. Responder que No nos permitirá comenzar más rápido.
   Lo importante es seleccionar nuestro idioma: Spanish, a fin de trabajar más cómodos.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   En las notas que siguen supondré que siempre tenemos instalado y abierto el IDE wxDEVC++.

   La primera vez que ejecutemos el IDE wxDevC++, deberíamos configurarlo para nuestros propósitos futuros.

   Los cambios de configuración que vamos a hacer son estos: ninguno;D

   Y lo digo muy en serio. Por favor no hacer cambios de configuración sólo porque se nos ocurre hacerlo.

   Vayamos al menú Herramientas, y luego clic en Opciones del Compilador.
   Veremos que está seleccionada la pestaña Compiler o Compilador.
   Encima de ella está seleccionado el compilador GCC (a mí me aparece como Default GCC Compiler).
   Se puede seleccionar allí otros compiladores, pero no lo haremos.

   En la pestaña se ve que aparece como Tipo de Compilador (o Compiler Type) el MinGW.
   Tampoco tocaremos esta opción.

   Nos interesa que nuestro compilador respete lo más posible el estándar C99. Pero considero que es mejor discutir esto más adelante, y allí les indicaré cómo configurar el compilador para esto.
   Si bien hemos de tener en cuenta que la compatibilidad de GCC con el estándar C99 no es completa, y está lejos de serlo.
   Para más información sobre este tema, consultar:

http://gcc.gnu.org/c99status.html

   Se ven varias pestañas allí: Compilador, Configuración, Directorios, etc.
   Les permito que las vayan mirando, pero no cambien ninguna configuración.

   Para salir de esta ventana, en la parte inferior de la ventana: clic en el botón Cancelar.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Antes de realizar nuestros programas, conviene ser ordenados y crear una carpeta para nuestros proyectos.

   Vamos por ejemplo en Windows a la carpeta  Mis Documentos y allí creamos la carpeta  Mis_Proyectos_C.
   A continuación debemos indicarle al IDE wxDevC++ que esa será nuestra carpeta de trabajo.

Para ello: vamos al
menú Herramientas,
clic en Opciones del Entorno,
clic en pestaña Directorios, y luego
vamos al campo Directorio del Programa,
o también User's Default Directory (en cualquier caso, es el primero de la lista de directorios).

   Allí hacemos clic en el ícono de la carpeta, y buscamos nuestra carpeta recién creada, hasta que encontramos por fin Mis_Proyectos_C, y la seleccionamos.
   Una vez seleccionada, clic en Aceptar.

Por las dudas, recomiendo en este momento cerrar el IDE wxDevC++, y luego reabrirlo, a fin de que las modificaciones en la configuración queden correctamente activadas.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   A fin de comprobar que todo funciona, hagamos un programa sencillo, el clásico "Hola Mundo".

   Vayamos al menú Archivo, clic en Nuevo, luego clic en Código Fuente.

   Nos aparece un cuadro de texto con el nombre SinNombre1.
   Para darle un nombre, y de paso ya tenerlo guardado en el disco duro, vamos al menú Archivo, clic en Guardar Como....
   Aparece un cuadro donde hemos de escribir un nombre para nuestro archivo.
   También aparece debajo un desplegable con opciones para el Tipo de Archivo.

   La opción por defecto para archivos nuevos es la C++ source files.
   Entonces abrimos el desplegable y seleccionamos la opción C source files

   Ahora en el Nombre de Archivo ponemos HolaMundo.c.

   Nos aseguramos de que va a guardar nuestro archivo en la carpeta Mis_Proyectos_C. Si no aparece, la buscamos hasta encontrarla.

   Por fin, hacemos clic en Guardar.

   Aún nuestro código está vacío. Escribimos las siguientes líneas de código:



#include <stdio.h>

int main(void) {   
   printf("Hola mundo!\n");
   getchar();                        /* Espera que se presione tecla ENTER */   
}



   Guardamos los cambios yendo al menú Archivo, clic en Guardar.

   En el futuro, cada vez que hagamos un cambio importante en nuestros proyectos, conviene inmediatamente guardar.

   Para poder correr finalmente nuestro programa, es necesario compilarlo.
   Para ello vamos al menú Ejecutar, y luego clic en Compilar.
   Esto traduce nuestro programa HolaMundo.c a un programa ejecutable, llamado HolaMundo.exe.

   Por último ejecutamos el programa compilado, yendo al menú Ejecutar, y luego clic en Ejecutar.

   En general conviene realizar estos dos pasos por separado, ya que la compilación se hace una sola vez, y luego podemos ejecutar el programa varias veces, sin tener que perder tiempo compilándolo de nuevo todas las veces.

   Por otra parte, el programa puede ejecutarse directamente en Windows, yendo a la carpeta donde guardamos nuestros proyectos, y haciendo doble clic directamente en el ejecutable HolaMundo.exe.

   Si todo está correcto, se ha de abrir la Línea de Comandos (una  ventana con fondo negro) y allí ha de aparecer la frase Hola Mundo!.
   Debajo estaría titilando el cursor, a la espera de que el usuario presione la tecla ENTER.
   Una vez presionada la tecla ENTER, el programa termina, y la ventana de la línea de comandos debiera cerrarse automáticamente (aunque esto puede depender de cómo tengamos configurada la ventana de la línea de comandos en nuestro sistema Windows).

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

03 Enero, 2013, 08:53 pm
Respuesta #2

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
2. Explicando el HolaMundo.c

   Nuestro primer programa, el HolaMundo.c, fue esto:

01 #include <stdio.h>
02
03 int main(void) {   
04    printf("Hola mundo!\n");
05    getchar();                        /* Espera que se presione tecla ENTER */   
06 }
07


   He agregado unos números de línea 01, 02, 03, ..., que en realidad no deben escribirse en el programa, sino que sólo están ahí para referencia.

   La línea 01 contiene la cláusula:

#include <stdio.h>

   El símbolo # se utiliza para indicar directivas del compilador.
   Otro día explicaré qué es eso.
   Basta saber que #include es una directiva del compilador.
   Lo que hace es indicarle al compilador que hay una lista de objetos previamente definidos (datos y procesos) en una determinada librería, y que estarán disponibles en el programa como si nosotros mismos las hubiéramos definido en nuestro programa.

   La librería que se incluye en este caso es el archivo <stdio.h>.

   Hay librerías estándar del lenguaje C, y librerías creadas por el usuario.
   Las librerías estándar se indican encerradas entre ángulos, como es el caso de <stdio.h>.
   Mientras que las librerías definidas o creadas por el usuario se indican entre comillas: "milibreria.h".

   La librería <stdio.h> tiene definidas funciones estándar de entrada y salida de datos.
   Allí aparecen pues la salida estándar (o pantalla), la entrada estándar (teclado), y otras funciones para el intercambio de datos con archivos en general.

   La he incluido porque necesitaba usar la función de salida estándar a pantalla printf(), y la función estándar de entrada por teclado getchar().

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Para que un programa C realmente haga algo, necesita que se declare una función con el nombre main().

   Una función en C tiene un nombre, una lista de parámetros de entrada, un parámetro de salida, un cuerpo o bloque de sentencias, y una sentencia de retorno.

   Por ejemplo, una función que suma dos números enteros tiene este aspecto:

int suma (int a, int b) {

   int s;
   s = a + b;

   return s;
}


   El nombre de la función sería suma, los parámetros de entrada serían int a, int b, el parámetro de salida o retorno sería un valor de tipo int, el cuerpo o bloque de instrucciones de la función es todo lo que aparece encerrado entre llaves { }, y finalmente la instrucción de retorno es return s;.

   Lo que hace esta función es recibir dos números enteros como entrada, sumarlos, y devolver el resultado al programa principal.
   Asi, al efectuar la llamada suma(5, 3), se obtiene como resultado 8.

   Ahora volvamos a la función main().
   Esta función es muy especial en C, es la más importante de todas.
   Es la única función que el compilador de C reconoce como cuerpo principal del programa.
La busca para comenzar a ejecutar instrucciones.
   Si main() no está presente, el programa no realiza ninguna acción.

   Lo importante aquí es que, si no nos interesa que main() reciba parámetros de entrada, podemos escribir void en su lista de parámetros.
   Por su parte, main() retorna siempre un valor de tipo int, cuestión que explicaremos en otro momento.
   Por ahora no nos interesa, y no retornaremos ningún valor específico.

   Entonces nos queda el encabezado así:

int main(void)

   Luego, encerramos entre llaves { } el cuerpo de la función main(), que será la lista de instrucciones que ejecutará nuestro programa.

   Vemos dos instrucciones. La primera es:

printf("Hola Mundo!\n");

   Lo que hace es invocar la función printf() de la librería <stdio.h>, para mostrar el mensaje Hola Mundo! en Línea de Comandos.
   Los caracteres \n lo que hacen es indicar que allí debe insertarse una nueva línea, con lo cual el cursor baja a la siguiente línea, y por eso aparecerá titilando debajo de la frase Hola Mundo! al ejecutar el programa.

   A fin de que la línea de comandos no se cierre sin que veamos la maravillosa frase, hemos de insertar algún tipo de pausa.
   Lo hemos hecho a través de la instrucción getchar(), que se encarga de esperar que el usuario ingrese información en línea de comandos desde el teclado.

   La función getchar() espera que el usuario ingrese caracteres con el teclado, seguido de la pulsación de la tecla ENTER.
   Hasta que no se presiona la tecla ENTER, el programa queda estancado a la espera de un dato.

   Tras esta instrucción, ya no hay más, y al alcanzar la llave de cierre } el programa termina su ejecución.
   Observamos que no hay una instrucción de retorno return, ya que en nuestro caso main() no devuelve ningún valor.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Una consideración importante aquí es que toda instrucción en C tiene que terminar con el signo de punto y coma: ;

   Esto es una regla del lenguaje, y es importante respetarla, porque si no se producen molestos errores de compilación, y el programa no ejecuta.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Observemos también que la línea 02 está vacía.
   En general, en el lenguaje C podemos intercalar libremente líneas en blanco, así como también es posible escribir muchos espacios en blanco, ya que muchos de ellos se cuentan como uno solo.

   Vemos en la línea 05 que, a la derecha de la sentencia getchar(), aparece una frase que dice:

/* Espera que se presione tecla ENTER */

   Todo texto que aparece encerrado entre los signos /* y */ se considera un comentario.

   Los comentarios son, para el lenguaje C, como espacios en blanco, es decir, no ejecutan instrucción alguna.
   Sin embargo, sirven de útiles indicaciones para quienes estén leyendo el código del programa y quieran entender qué hace una determina instrucción.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Finalmente digamos que el uso que hemos hecho de la función getchar() sería algo así como una mala práctica de programación.
   En efecto, la función getchar() está pensada para recibir datos de entrada, y luego guardarlos en alguna variable para su futura utilización.

   No hemos hecho nada de eso, sino que sólo la hemos utilizado para forzar al programa a hacer una pausa y terminar tras la presión de la tecla ENTER. Este uso es extraño y artificioso.

   Pero lo hicimos así porque es rápido, efectivo, simple, y además no involucra conceptos más complicados de la programación en C.
   Es un uso perfectamente apto para un programa inicial de testeo, tal como el HolaMundo.c.

   En particular, este uso irregular o extraño de la función getchar() es lo que ameritó el comentario aclaratorio que hemos puesto a la derecha en la línea 05.

   En programas sencillos, en los que tengamos el mismo inconveniente de querer frenar el programa en ejecución antes de que termine abruptamente, vamos a utilizar la misma técnica.
   Pero es sólo un truquillo para ahorrarnos tiempo, y no es parte de un aprendizaje serio de la programación.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

04 Enero, 2013, 05:19 am
Respuesta #3

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
3. Un sencillo Hola Mundo en Windows

   Los programas compilados y ejecutados desde GCC corren en el shell de comandos de Windows, esto es, una ventanita negra e inestética donde se muestran resultados de forma secuencial, en modo de texto exclusivamente. En realidad lo que se está corriendo es un programa llamado CMD.EXE, que sirve para ejecutar los comandos del sistema operativo. Se le puede decir también la consola, aunque considero que esta terminología no es técnicamente correcta.
   La ventana del sistema es donde C envía resultados por defecto. Es lo que se llama salida estándar. En cambio el teclado es la entrada estándar. Ambas en conjunto conforman la consola estándar.
   Esa ventanita es lo bastante fea y precaria como para que deseemos escapar de ella. Sin embargo, hacerlo supone aprender técnicas de programación bajo Windows, que quisiéramos evitar en un primer momento. Necesitamos que, al principio del curso, las cosas sean relativamente sencillas. Queremos estudiar el lenguaje C en la forma más pura posible.

   Sin embargo, para aquellos interesados, vamos a mostrar una posible alternativa (no muy sofisticada), que permitiría mostrar resultados sencillos mediante mensajes de Windows.

(Los detalles de esto son material opcional, y lo ponemos es spoiler: se discute sobretodo el tema de compilación condicional, con la idea de optar entre un mensaje mostrado en línea de comandos y un mensaje mostrado en una ventana de Windows).

(MessageBox(), compilación condicional)

   Vamos a utilizar la librería windows.h que provee GCC. En ella existe una función MessageBox() que muestra una ventanita con un mensaje de aviso.
   Nuestra versión de Hola Mundo para Windows será un nuevo programa llamado HolaMundoWin.c con el siguiente contenido:


#include <windows.h>

int main(void) {
    MessageBox(NULL,"\n\nEste es un Hola Mundo con una MessageBox.\n\n","Hola Mundo",0);
}


   Notar que la librería windows.h debe ser incluida en nuestro código.

   La función MessageBox() tiene este formato:


int WINAPI MessageBox(
  _In_opt_  HWND hWnd,
  _In_opt_  LPCTSTR lpText,
  _In_opt_  LPCTSTR lpCaption,
  _In_      UINT uType
);


   El 1er parámetro hWnd ha de especificar un manejador a la ventana dueña (o sea, la que se supone que proviene del programa principal que generó la ventanita de mensaje).
   Como estamos haciendo un uso casero de esta función, no necesitamos esto. Y por lo tanto no hay una ventana dueña o principal. Sólo tenemos la ventanita de mensaje, y por lo tanto especificaremos un manejador NULL (nulo).
   El 2do parámetro es el texto del mensaje que queremos mostrar.
   De nuevo, podemos usar \n para agregar saltos de línea, si queremos que el mensaje se vea un poco mejor.
   El 3er parámetro es el título de la ventanita de mensaje.
   El 4to parámetro es un número entero que indica qué botones adicionales han de aparecer.
   Nosotros usamos el valor 0, que por defecto nos pone solamente el botón OK (o Aceptar), que termina de mostrar el mensaje, cerrando la ventanita.

   Este tipo de funciones utilizan las herramientas de programación orientada a objetos de C++, o sea que me estoy saliendo del estándar C para hacer esto.
   Intentaré no abusar demasiado de esto, y utilizar estos trucos sólo para obtener una entrada/salida que sea más confortable.
   Más aún, los programitas que vayamos a hacer en el futuro tienen que poder funcionar tanto con MessageBox() de windows.h como con printf() de stdio.h para la consola estándar.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Podemos hacer algo más general como lo que sigue, agregando directivas del preprocesador. Podemos indicarle al compilador cuál de las dos versiones de "Hola Mundo" nos interesa compilar. Para ello, definir constantes del preprocesador, esto es, valores que sólo tienen sentido durante la compilación del programa.


#define Me_Conformo_con_CMD  0
#define Me_Gusta_mas_Windows 1


   La directiva #define sirve en este caso para definir expresiones que el compilador sustituirá por valores. Así, la expresión Me_Conformo_con_CMD se sustituirá por el valor 0, mientras que Me_Gusta_mas_Windows se sustituirá por el valor 1.
   En general, el uso de #define sería éste:

#define EXPRESION VALOR

   Lo cual indica que el compilador tomará toda ocurrencia de EXPRESION en nuestro código y la sustituirá automáticamente por VALOR.
   Es importante entender que se hace un reemplazo automático de este estilo, y así se entiende por qué no aparecen los famosos punto y coma terminadores de instrucción de C.
   La directiva #if permite indicar opciones al momento de realizar la compilación del programa.
   Esto quiere decir que el compilador elegirá distintos trozos de código para compilar, según las opciones que le indiquemos.
   La estructura genérica es ésta:

#if EVAL1
   /* Si el resultado de la expresión evaluada en EVAL1 es distinto de 0,
       entonces se compila este trozo de código,
       y se salta hasta la línea debajo de #endif.
       Si no, se sigue aquí debajo.
  */
#elif EVAL2
   /* Si el resultado de la expresión evaluada en EVAL2 es distinto de 0,
       entonces se compila este trozo de código,
       y se salta hasta la línea debajo de #endif.
       Si no, se sigue aquí debajo.
   */     
#else
   /* Si el resultado de todas las expresiones es 0,
       entonces se compila este trozo de código,
       y se salta hasta la línea debajo de #endif.
   */     
#endif


   Las cláusulas #elif y #else pueden faltar, pero no puede faltar #endif.
   Es importante respetar correctamente la sintaxis para que todo esto funcione.
   Ahora bien, con la combinación de estas cláusulas es posible indicarle al compilador distintos caminos en la compilación, pudiendo así tener varias versiones de un mismo programa dentro de un solo archivo.
   Definamos una constante del preprocesador llamada Creo_que para poner la información de qué es lo que preferimos, si el CMD o Windows, o ninguno.
   Nuestra función main() quedaría así:

int main(void) {

#if (Creo_que == Me_Conformo_con_CMD)
   printf("Hola mundo en Línea de Comandos.\n");
   getchar();                        /* Espera que se presione tecla ENTER */ 
#elif (Creo_que == Me_Gusta_mas_Windows)
   MessageBox(NULL,"\n\nHola Mundo con MessageBox.\n\n","Hola Mundo",0);
#else
   /* No se ha elegido una opción válida. El programa no hará nada. */
#endif
}


   Hemos comparado la constante Creo_que con cada una de las constantes Me_Conformo_con_CMD y Me_Gusta_mas_Windows.
   El operador == compara dos valores y determina si son iguales o no.
   Si son iguales, arroja el valor 1, que significa VERDADERO, y si son distintos, arroja 0, que significa FALSO.
   En general en C, todo valor distinto de 0 se sobreentiende como VERDADERO en una comparación. Pero esto lo veremos más adelante.
   La expresión EVAL1 fue, en nuestro caso, la comparación

Creo_que == Me_Conformo_con_CMD

   Además, esa expresión la rodeamos con paréntesis ( ).
   En general recomiendo esto de usar paréntesis alrededor de las expresiones EVAL1, EVAL2, pues el preprocesador podría confundirse creyendo que la expresión es sólo Creo_que y que lo que sigue a la derecha es la porción de código lista para ser compilada... Todo un lío.
   Falta asignarle un valor a la constante Creo_que. Ahí es cuando elegimos una de las dos opciones siguientes:

#define Creo_que Me_Conformo_con_CMD
#define Creo_que Me_Gusta_mas_Windows

   A continuación mostramos el programa terminado. Además, elegimos para compilar la opción de Windows, así que esa es la definición que daremos a Creo_que:

HolaMundo2.c

#define Me_Conformo_con_CMD  0
#define Me_Gusta_mas_Windows 1

#define Creo_que Me_Gusta_mas_Windows

#if (Creo_que == Me_Coformo_con_CMD)
   #include <stdio.h>
#elif (Creo_que == Me_Gusta_mas_Windows)
   #include <windows.h>
#else
   /* No se incluye ninguna de las dos librerías */
#endif

int main(void)
{

#if (Creo_que == Me_Conformo_con_CMD)
   printf("Hola mundo en Línea de Comandos.\n");
   getchar();                        /* Espera que se presione tecla ENTER */ 
#elif (Creo_que == Me_Gusta_mas_Windows)
   MessageBox(NULL,"\n\nHola Mundo con MessageBox.\n\n","Hola Mundo",0);
#else
   /* No se ha elegido una opción válida. El programa no hará nada. */
#endif
}


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Recordar:
La compilación condicional que tenemos gracias a las directivas #if produce como resultado un programa ejecutable, en este caso HolaMundo2.exe, tal que sólo aparecen compilados los trozos de código elegidos mediante #if.
   Así, si ponemos:

#define Creo_que Me_Conformo_con_CMD

el programa resultante HolaMundo2.exe será exactamente el mismo que nuestro ejemplo original HolaMundo.exe.
   Y si ponemos

#define Creo_que Me_Gusta_mas_Windows

el programa resultante HolaMundo2.exe será exactamente el mismo que el HolaMundoWin.exe (salvo por la frase que a propósito pusimos diferente en cada caso).
   El programa ejecutable HolaMundo2.exe jamás se entera de que en algún lugar se evaluaron condiciones de algún tipo, y no quedan en él rastros de ello.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Una última observación: Si ejecutamos la versión para CMD de este programita veremos que el texto "Línea" aparece mal escrito en la ventana de comandos CMD, mostrando incorrectamente el caracter especial de la i acentuada.
   Este desfasaje entre la codificación de caracteres especiales desde Windows a Línea de Comandos es algo común de esperar, y es por tanto una razón más para odiar CMD y que yo no quiera usarla.
   (El problema de los acentos tiene solución, que veremos en otro post).

[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

04 Enero, 2013, 02:47 pm
Respuesta #4

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
4. De la eficacia a la elegancia - Macros del Preprocesador (parte I)

   La programación estructurada consiste en tomar un problema informático y dividirlo en subproblemas, cada uno de ellos más fácil de entender y de resolver. A esto se le llama técnica de divide y vencerás.
   En C esto se lleva a cabo con las funciones: trozos de código con un determinado nombre que resumen una lista de tareas.
   Dado que nos falta analizar varios temas antes de introducir las funciones, no lo haremos en este post.
   Sin embargo, tenemos a mano una versión simplificada del comportamiento funcional: abreviar con un nombre a toda una porción de código. Esto puede hacerse en C con las directivas #define del preprocesador, tal como hemos hecho hasta ahora. Pero más todavía, se pueden usar parámetros a fin de "pasar información" al bloque de código.
   Así, si deseamos tener abreviada una expresión que nos dé el promedio de dos números, escribiríamos algo como esto:

#define PROMEDIO(A, B) ((A + B)/2.0)

   Esto lo que hace es que, cada vez que el compilador encuentra la palabra PROMEDIO en nuestro programa, se fija cuáles son los valores A, B, y luego reemplaza toda la expresión PROMEDIO(A, B) por el resultado de la operación (A + B)/2.0.
   ¿Por qué escribo 2.0 en vez de un simple 2?
   Es para que no se nos escapen resultados con decimales cuando A y B son enteros.
   Esta cuestión será analizada luego, al estudiar tipos de datos y conversiones de tipos.
   Además, se ha rodeado la operación con un juego adicional de paréntesis, otra vez para prevenir posibles desastres.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Macros en C.

   Esto que acabamos de definir se llama una macro en lenguaje C: Consiste en darle un nombre a una porción de código, y permitir que reciba parámetros que modifiquen su comportamiento.
   Ahora, podemos usarla para calcular el promedio de dos números cualesquiera, y mostrarlo luego en la línea de comandos:

printf("Promedio: %g\n", PROMEDIO(1.2, 3.14));

   El compilador reemplazará automáticamente la ocurrencia de PROMEDIO(1.2, 3.14) por la expresión ((1.2 + 3.14)/2.0), pues reconoce como parámetro A al valor 1.2 y como parámetro B al valor 3.14.
   Vemos que aparece un modificador %g en la sentencia printf(). Eso indica que justo en esa posición irá insertado el valor del 1er parámetro que aparezca después de la coma (en nuestro caso, el resultado de PROMEDIO(1.2, 3.14), que es el número 2.17), y que además se trata de un número con dígitos tras el punto decimal.
   Pronto estudiaremos en detalle todas las opciones de la función printf().

   Nota importante: Al invocar la macro PROMEDIO, es posible pasar como parámetros cualquier cosa que no sean números.
   O sea, la macro no tiene manera de adivinar qué tipo de datos se le están pasando.
   Sin embargo, el resultado de poner cualquier cosa será que el compilador intentará realizar operaciones inválidas entre tipos de datos distintos, y el programa no compilará. Entonces ni siquiera ejecutará.
   Aquí va finalmente el programa terminado y funcionando:

MacroPromedio.c:

#include <stdio.h>

#define PROMEDIO(A, B) ((A + B)/2.0)

int main(void) {
  printf("Promedio: %g\n", PROMEDIO(1.2, 3.14));
  getchar();                                  /* Presionar ENTER para terminar */ 
}


   Al correrlo, en línea de comandos ha de aparecer:


Promedio: 2.17


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   He continuado discutiendo este tema, pero me parece preferible ponerlo como material opcional, dentro del spoiler que sigue a continuación.
   Algunas temas que tocaremos en forma tangencial, pero que retomaremos con más seriedad en un futuro, son los siguientes: compilación condicional, operador coma, diferencias entre usar "coma" y "punto y coma" para separar acciones del programa, operador condicional.

(Continuación de la discusión...)

   Todo esto en realidad es pura conversación, ya aquí me interesa realizar una macro con intenciones más burdas.
   Estoy obsesionado con el "Hola Mundo" todavía.  :banghead: :banghead:
   Sería bueno tener una versión más elegante de nuestro programa HolaMundo2.c.
   Aprovechando que ahora sabemos hacer macros con parámetros, pondríamos como parámetro la frase de salutación "Hola Mundo" o cualquier otra información.
   Esto combinado con las directivas #if, nos daría una versión más resumida y elegante del programa, que vamos a llamar HolaMundo2bis.c.
   Queremos definir una macro llamada Mostrar_INFO(S), que toma un parámetro S, de manera que si estamos compilando una versión con salida para linea de comandos, entonces el mensaje contenido en S se imprima mediante printf(), mientras que si estamos compilando una versión con una ventanita de mensaje para Windows, entonces el mensaje contenido en S se muestre con MessageBox().

   Lo que haremos, en un primer intento, será definir una macro diferente para cada caso, en cada alternativa determinada por la directiva #if. Queremos resumir todo en una sola declaración #if.

HolaMundo2bis.c

#define Me_Conformo_con_CMD  0
#define Me_Gusta_mas_Windows 1

#define Creo_que Me_Gusta_mas_Windows

#if (Creo_que == Me_Conformo_con_CMD)
   #include <stdio.h>
   #define Mostrar_INFO(S) printf(S); getchar();   
#elif (Creo_que == Me_Gusta_mas_Windows)
   #include <windows.h>
   #define Mostrar_INFO(S) MessageBox(NULL,S,"Info",0);
#else
   /* Nada */
#endif

int main(void) {
   Mostrar_INFO("Hola Mundo!!\n\n")
}


   Para probar que las dos opciones funcionan, basta editar la línea que define la (macro) constante Creo_que, y ponerle alguno de los valores Me_Conformo_con_CMD  ó Me_Gusta_mas_Windows.
   En ambos casos hemos definido la macro Mostrar_INFO, pero hay que notar que la definición definitiva es completamente distinta, según qué camino tome el compilador en la directiva #if.
   Nota adicional: Tengamos en cuenta que la macro Mostrar_INFO(S) permite que el parámetro S sea cualquier cosa, y no hay manera de verificar si es una cadena de caracteres.
   Las macros no hacen verificaciones de tipos de datos, y entonces puede ser difícil descifrar donde ha surgido algún error con el compilador.
   Por ejemplo, si se pone en S un número, se pasará ese número finalmente en la sentencia printf(S), y esto dará un error de compilador, ya que se espera que el primer parámetro de printf() sea una cadena de caracteres.

   Aquí hay un cierto riesgo de programación, ya que hay que confiar en la buena intención del programador, que definirá la macro Mostrar_INFO en todos los casos, sin olvidarse de nada.
   Para evitar este riesgo se puede proceder de otra manera, pero ya estaríamos obligados a usar una nueva cláusula #if en el código, como en el siguiente programa, que habremos de llamar HolaMundo2bisbis.c.

   Definiremos prolijamente una macro para cada situación. Es decir, una macro Mostrar_INFO_CMD para el caso que compilemos para línea de comandos, y una macro Mostrar_INFO_WIN para el caso que compilemos para ventanita de Windows.

   Ahora querríamos colocar una cláusula #define Mostrar_INFO(S), y hacer que se reemplace por Mostrar_INFO_CMD(S) en el primer caso, y por Mostrar_INFO_WIN(S) en el 2do caso.
   Al parecer, estamos pensando en anidar una cláusula #if dentro de la cláusula #define.
   ¿Se pueden anidar directivas del preprocesador?  >:D
   En general NO: evitémoslo (detalles en el futuro).   ::)
   Dado que tendremos que evaluar varias veces las condiciones acerca de cuál de las dos opciones de compilación nos interesan, preferimos definir unas abreviaturas en forma de macros, para que el programa sea más legible:


#define Me_Conformo_con_CMD  0
#define Me_Gusta_mas_Windows 1

#define Creo_que Me_Conformo_con_CMD

#define CMD_OK (Creo_que == Me_Conformo_con_CMD)
#define WIN_OK (Creo_que == Me_Gusta_mas_Windows)


   Así que CMD_OK es VERDADERO (tiene valor 1), mientras que WIN_OK es FALSO (tiene valor 0).
   Si en la definición de Creo_que cambiamos el valor, también cambiarán los valores de CMD_OK y WIN_OK.
   Ahora seguimos con la definición de las macros Mostrar_INFO_CMD y Mostrar_INFO_WIN, lo cual se hace con un pequeño cambio en el código de HolaMundo2.c:


#if CMD_OK
   #include <stdio.h>
   #define Mostrar_INFO_CMD(S) ( printf(S), getchar(), 0 )
#elif WIN_OK
   #include <windows.h>
   #define Mostrar_INFO_WIN(S) ( MessageBox(NULL,S,"Info",0), 0 )
#else
   /* Nada */
#endif


   ¿Qué diablos hemos hecho ahí?  :o

   Prestemos atención a unos sutiles pero terroríficos cambios.
   En vez de punto y coma para separar sentencias, hemos puesto comas.   >:D
   ¿Se puede hacer esto? El signo de coma (,) tiene un significado muy distinto del punto y coma (;).
   Mientras el punto y coma es un terminador de instrucción, la coma es un operador cuyo resultado es, simplemente el valor que figura más a la derecha.
   Así, si escribimos la operación (9.11, 7.14), el resultado es 7.14, por ser el valor más a la derecha del operador coma;)
   Por otro lado, se supone que todas las funciones en C devuelven un cierto valor, de algún tipo, que puede ser número, cadena de caracteres, puntero, etc., etc., etc.
   Las funciones printf(), getchar() y MessageBox() devuelven un valor.
   Así que el lenguaje C lo que hace es llamar a esas funciones, digamos printf(), seguida de getchar, hace lo que cada una tenga que hacer, y luego el operador coma simplemente se dedica a retornar un valor, el cual nosotros no vamos a usar para nada.  >:D

   Estos trucos encierran algunos vicios de mala programación, que en su momento tendremos que ver cómo corregir.
   Por ahora nos ponemos contentos de nuestra astucia, y vemos que a fines prácticos nos resulta lo mismo escribir:

printf("Hola!"); getchar();

que

printf("Hola!"), getchar();

   Los valores que devuelven son compatibles, por ser todos números enteros.
   Pero resulta que no "me la quiero jugar" a que voy a tener la suerte de que esos valores sean compatibles. Quiero asegurarme que el resultado final del operador coma será siempre de tipos compatibles, para evitar efectos no deseados.
   La manera más sencilla de hacer esto es agregando un valor inútil, como un 0, así:

printf("Hola!"), getchar(), 0;

   Recién al final de la lista de funciones separadas por comas es que ponemos el terminador de instrucción punto y coma, que en alguna parte tiene que estar, si no, se nos desmadra el programa.
   Sin embargo, vemos que en nuestras macros no está ese dichoso punto y coma, y de hecho no está "a propósito", porque necesitamos diferir su colocación, ya veremos por qué.
   En realidad vamos a seguir jugando con los operadores de C, y mientras estamos evaluando fórmulas no podemos poner el "punto y coma" porque nos terminaría abruptamente la expresión, dando resultados erróneos.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Vamos a usar el operador de decisión ?:.
   Este operador toma una cierta expresión y se pregunta si es verdadera.
   En caso afirmativo evalúa la expresión que está inmediatamente a la derecha del signo ?, y en caso contrario salta hasta la expresión inmediatamente a la derecha del signo :.
   El resultado final del operador es la evaluación elegida de una de estas dos opciones.
   Un ejemplo sencillo sería éste:


(1 == 1)?"Hola": "Chau";
(1 == 2)?"Hola": "Chau";


   Aquí se pregunta si 1 es igual 1. Como es verdadero, el resultado de la operación será la frase "Hola".
   Luego se pregunta si 1 es igual a 2. Como es falso, el resultado de la operación será la frase "Chau".
   Lo que nosotros haremos será preguntar si CMD_OK es verdadero.
   Si lo es, evaluamos la expresión Mostrar_CMD(S), la cual incluye una lista de llamadas a funciones separadas por comas, y cuyo resultado final es un 0, que no usamos para nada.
   Si nos da falsa, entonces volveremos a hacer otra pregunta, otra vez con el operador ?:, esta vez indagando si WIN_OK es verdadera.
   En ese caso evaluamos la expresión Mostrar_WIN(S), la cual incluye una llamada a la función MessageBox().
   Por último, si esta consulta nos da falsa, no hacemos nada, y simplemente rellenamos con un 0, para que toda la expresión tenga algún valor como resultado.

   Todo esto se encierra con paréntesis para mayor seguridad en el código, y finalmente es posible colocar un terminador punto y coma.
   Nuestra macro tiene ahora esta forma:

#define Mostrar_INFO(S) ( CMD_OK? Mostrar_INFO_CMD(S) : (WIN_OK? Mostrar_INFO_WIN(S) :(0) ) )

   Se puede apreciar que es difícil de leer, y además rebasa la longitud del renglón.
   Para poder separar una línea del preprocesador en varios trozos, se lo puede hacer indicándolo con la barra invertida: \. La reescribimos, pues, así:


#define Mostrar_INFO(S) \
   (CMD_OK? Mostrar_INFO_CMD(S) : \
      ( WIN_OK? Mostrar_INFO_WIN(S) : (0) ) )


   ¿Por qué esto funciona?
   Puede parecer extraño que un operador del lenguaje C funcione bien con el preprocesador, sobretodo en esta situación en la cual aparecen funciones de librerías distintas.
   Por ejemplo, en la expresión anterior, si tenemos que CMD_OK es verdadero, esto significa que la librería windows.h no se ha cargado, y entonces la función MessageBox() queda inaccesible.
   ¿Cómo es que el programa compila igual?
   La respuesta está en el modo en que trabaja el preprocesador: Intentará evaluar todas las expresiones que involucren constantes, dejando como resultado final la expresión resultante a fin de cuentas.
   Como CMD_OK es una constante que el compilador conoce, pues la hemos definido con el preprocesador, el operador ?: inmediatamente reconoce que el valor es 1, por ejemplo, y pasa directamente a la expresión correspondiente, ignorando el trozo que contiene la llamada a MessageBox().
   Como todo esto sucede en la etapa de compilación, el programa final no se entera de una posible referencia a MessageBox().

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   El resultado final es el siguiente programa:

HolaMundo2bisbis.c:

#define Me_Conformo_con_CMD  0
#define Me_Gusta_mas_Windows 1

#define Creo_que Me_Conformo_con_CMD

#define CMD_OK (Creo_que == Me_Conformo_con_CMD)
#define WIN_OK (Creo_que == Me_Gusta_mas_Windows)

#if CMD_OK
   #include <stdio.h>
   #define Mostrar_INFO_CMD(S) ( printf(S), getchar(), 0)   
#elif WIN_OK
   #include <windows.h>
   #define Mostrar_INFO_WIN(S) ( MessageBox(NULL,S,"Info",0), 0)
#else
   /* Nada */
#endif

#define Mostrar_INFO(S) \
   (CMD_OK? Mostrar_INFO_CMD(S): \
      (WIN_OK? Mostrar_INFO_WIN(S): (0)) )

int main(void) {   
   Mostrar_INFO("Hola Mundo!!\n\n") ;
}


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Estos trucos funcionan sólo si el compilador tiene determinadas características. Ha de ocurrir que el preprocesador funciona realmente como hemos descripto: ignorando las partes de una expresión condicional en forma automática, cuando la condición puede saberse si es verdadera o falsa en la etapa misma de la compilación.
   Esto es posible en GCC, pero no es parte de la definición estándar de ISO C99.
   En general recomiendo no confiar en que el compilador actuará de este modo.

[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

04 Enero, 2013, 07:40 pm
Respuesta #5

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
5. De la eficacia a la elegancia - Macros del Preprocesador (parte II)

   Veamos lo que dice el comité de estandarización de C sobre algunas cuestiones del post anterior.
   Sobre el operador ,:

Citar

6.5.17 Comma operator

The left operand of a comma operator is evaluated as a void expression; there is a
sequence point after its evaluation. Then the right operand is evaluated; the result has its
type and value.

   Fuente: http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

Claramente especifica que en una expresión de la forma:

(A, B)

en que aparece el operador "coma", 1ero se evalúa la expresión A, se hacen vaya uno a saber qué gestiones (se da término a todos los llamados "efectos colaterales"), y recién cuando esto ha terminado se evalúa la expresión B.
   Por lo tanto, si escribimos alegremente una lista de acciones como:

printf("Hola"), getchar();

se ejecutará 1ro printf("Hola"), y sólo tras haber terminado esta acción se pasará a ejecutar getchar().
   En general, ¿qué ocurre cuando tenemos una lista con más de dos operandos separados por comas? ¿En qué orden se evalúan?
   Si tenemos una lista de 3 acciones como:

(A, B, C);

   ¿Qué dice el estándar?
   En primer lugar el operador , es asociativo de izquierda a derecha.
   Así, la terna (A, B, C) se asocia como ((A, B), C).
   Luego, debido a la regla de evaluación, tenemos que efectivamente se evalúa primero (A, B) hasta terminar, y luego se pasa a evaluar C.
   Pero para que termine de evaluarse (A, B), primero se debe terminar de evaluar A, y luego se pasa a B.
   Así que el orden de izquierda a derecha está asegurado para el operador ,.
   Notar que $reste efecto de ordenamiento de los efectos colaterales en general no se puede asegurar con los demás operadores asociativos del lenguaje C. El estándar lo deja sin especificar$R.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Pasemos a analizar el operador condicional ?:.
   Según el estándar, en la expresión

(A)? B: C

primero se evalúa A hasta terminar todos los efectos colaterales.
   Si A es no nulo (verdadero), entonces se pasa a evaluar B, y nunca se hace nada con la expresión C.
   En cambio, si A es nulo (falso), se pasa a evaluar C, y nunca se hace nada con B.

(En spoiler continuamos el spoiler del post anterior...)

(continúa la discusión del post anterior...)

   Ahora nos preguntamos qué ocurre si A es una constante en la etapa de compilación. Esto quiere decir que su valor está previamente definido, y de entrada, antes de la compilación misma, se puede saber qué expresión se ejecutará, si la B o la C.
   El compilador tiene el derecho a decidir eliminar la rama B ó C que nunca va a ejecutarse, y esto es lo que nos ha permitido (con el GCC invocado dentro de nuestro wxDevC++) directamente eliminar en la mera etapa de compilación aquella porción de instrucciones que no nos interesa mantener.
   Como esto ocurre antes de la compilación definitiva, el compilador no se las tiene que ver nunca con la aparición de una función MessageBox(), por ejemplo, sin que hayamos incluido la librería windows.h.
   Pero la clave de este asunto es que esta práctica no está avalada por el C estándar, sino que depende de la implementación específica de nuestro compilador.
   Con el siguiente experimento, podemos ver cómo funciona el compilador GCC que estamos usando:

ExperimentoBasura.c

#include <stdio.h>

int main(void) {
    int x = (('a' == 'a')? 1111: (int) (basura_sin_sentido())  )  ;

    printf("x = %d", x);
    getchar();
}


   Este programa ignorará la llamada a la función basura_sin_sentido(), que no está declarada en ninguna parte, y no dará mensaje de error en la compilación, debido al optimizado modo en que evalúa las expresiones con constantes.
   El compilador detecta que siempre la condición 'a' == 'a' es verdadera, y deja asignado el valor x = 1111.
   Lo demás, es como si nunca hubiera existido.
   Obviamente, esto es un efecto indeseado del compilador, y debemos procurar evitarlo, porque introduce errores indetectables a la vez que extraños en nuestros programas.

   Así, tendremos que investigar una manera diferente de realizar la compilación condicional que hicimos en HolaMundo2bisbis.c.
   Por ahora, como resultado de esta discusión, voy a retractarme del programa HolaMundo2bisbis.c, que sólo funciona para el compilador GCC, y en cambio voy a dejar una versión definitiva, que es correcta para cualquier compilador que respete el estándar:

HolaMundo2bisbisOk.c

#define Me_Conformo_con_CMD  0
#define Me_Gusta_mas_Windows 1

#define Creo_que Me_Conformo_con_CMD

#define CMD_OK (Creo_que == Me_Conformo_con_CMD)
#define WIN_OK (Creo_que == Me_Gusta_mas_Windows)

#if CMD_OK
   #include <stdio.h>
   #define Mostrar_INFO_CMD(S) ( printf(S), getchar(), 0)   
#elif WIN_OK
   #include <windows.h>
   #define Mostrar_INFO_WIN(S) ( MessageBox(NULL,S,"Info",0), 0)
#else
   /* Nada */
#endif

#if CMD_OK
   #define Mostrar_INFO(S) Mostrar_INFO_CMD(S)
#elif WIN_OK
   #define Mostrar_INFO(S) Mostrar_INFO_WIN(S)
#else
   /* Al parámetro S no se le hace nada, y lo dejamos pasar como resultado */
   #define Mostrar_INFO(S) S
#endif

int main(void) {   
   Mostrar_INFO("Hola Mundo!!\n\n") ;
}


[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

05 Enero, 2013, 12:12 am
Respuesta #6

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
6. Etapas del Preprocesador

   ¿Qué hace el preprocesador?
   ¿Podemos ver directamente lo que hace el preprocesador?

   El preprocesador transforma un programa en C en otro programa en C, pero con menos directivas de preprocesador que antes...
   Luego el preprocesador actúa tantas veces como sea necesario hasta lograr una versión definitiva sin cláusulas que contengan el símbolo # del preprocesador.

   Demos un ejemplo sencillo.

ObsPrepr.c


#define EJEMPLO_MACRO(X) (X + X)
#define DOBLE_1000 EJEMPLO_MACRO(1000)

int main(void) {

    return DOBLE_1000;   
}


   Es un programa que no hace nada, aunque retorna un número al sistema operativo, precisamente el valor 2000.

   Pero el preprocesador sí que hace cosas. Toma el código y reemplaza la macro EJEMPLO_MACRO en todas sus ocurrencias por la definición dada en la 1er línea.
   Tras esta traducción hecha por el preprocesador, se obtiene, en el aire, un nuevo programa en C, con este aspecto:

ObsPrepr.c (tras primera fase de traducción del preprocesador):


#define DOBLE_1000 (1000+1000)

int main(void) {

    return DOBLE_1000;
}


   Todavía quedan directivas de preprocesador, así que se realiza una nueva traducción, y nos queda:

ObsPrepr.c (tras segunda fase de traducción del preprocesador):


int main(void) {

    return (1000+1000);

}


   El compilador (GCC) toma la expresión (1000+1000), que contiene constantes, y las evalúa antes de finalizar la compilación, poniendo ahí directamente el valor 2000.
   La mayoría de compiladores hacen esta evaluación previa de expresiones constantes, sin embargo el estándar C no nos asegura que este cálculo se efectúe realmente en etapa de compilación.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Esas "traducciones" que hemos mencionado no hay modo de visualizarlas (en general) durante una compilación real.
   Sin embargo, es posible obtener alguna información de lo que ocurre con las acciones del preprocesador.
   Para ello usaremos el operador (de preprocesador) # , cuya acción es agregarle comillas a una expresión cualquiera.
   ¿Para qué sirve esto? Lo que hace es producir una cadena de caracteres que dice "lo mismo" que ponemos a la derecha del signo #.
   Con esto, tenemos disponible una cadena de caracteres que podemos usar luego para imprimir en pantalla, y así visualizar información de acciones del preprocesador.

   En el ejemplo anterior, podemos hacer algunos cambios para "depurar" más o menos cuál ha sido el resultado de los reemplazos del preprocesador.
   Vamos a definir una macro DEB_(A), que toma el argumento A y lo rodea con comillas a fin de convertirlo en una cadena de caracteres.
   Esto no nos sirve de mucho en principio, porque no nos da información de la traducción hecha por el procesador. Pero igual nos será útil luego.
   Su definición es así:

#define DEB_(A) #A

   Así, si invocamos la macro con algo como DEB_(Hola Mundo), nos genera la cadena entrecomillada "Hola Mundo".
   Esta cadena es una constante en la etapa de la compilación. O sea que en el programa finalmente compilado será un objeto constante, estático.

   Ahora daremos una macro ligeramente distinta, llamada DEB(X).
   Con ella es llamaremos a DEB_ con el argumento X, así:

#define DEB(X) DEB_(X)

   ¿Qué diablos hace esto?
   Cuando hacemos una invocación a la macro DEB(X), el preprocesador primero evalúa el valor de X, hasta "desenrollarlo" por completo, si es posible.
   Una vez terminado esto, realiza la "llamada" a la macro DEB_(X), pero con el valor de X ya sustituido apropidamente por su valor.

   Escribamos un programa y veamos el resultado que produce:

ObsPreprBis.c


#define DEB_(X) #X
#define DEB(X) DEB_(X)


#define DOBLE(X) (X + X)
#define N DOBLE(1000)

#include <stdio.h>

int main(void) {
   
    printf("%d\n", N);
    printf(DEB_(N) " = " DEB(N) "\n\n");
    printf("%d\n", DOBLE(17));
    printf(DEB_(DOBLE(17)) " = " DEB(DOBLE(17)) "\n\n");
    getchar(); 

}


   El resultado al ejecutar el programa es esto:


2000
N = (1000 + 1000)

34
DOBLE(17) = (17 + 17)


   El efecto de DEB_(N) ha sido simplemente agregarle comillas a N, quedando la cadena "N".
   En cambio, el efecto de DEB(N) ha sido expandir N a su definición: DOBLE(1000), que a su vez se traduce en (1000 + 1000).
   Luego de eso se invoca la macro DEB_ con (1000 + 1000) como argumento, produciendo la cadena entrecomillada "(1000 + 1000)".
   Algo análogo ha sucedido con DOBLE(17).

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Bien, el compilador (GCC) hace las traducciones del preprocesador en el orden en que hemos indicado.
   ¿Es esto así en el estándar C?
   La respuesta es afirmativa, como puede verse en la sección 6.10.3 de The New C Standard, libro de nuestra lista bibliográfica.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

05 Enero, 2013, 01:26 pm
Respuesta #7

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
7. Caracteres (char) y Strings (char*)

   Son muy comunes las tareas en las cuales se muestra información mediante mensajes de texto. Luego, es muy importante entender cómo se representan los caracteres en C, así como las cadenas de caracteresstrings).
   Por un lado está su representación en memoria, que se hace mediante bits (dígitos binarios 0 y 1), por otro lado está el modo en que el lenguaje C permite indicar caracteres y strings.
   Hay diversas vicisitudes, y la mayoría de ellas serán abordadas en este post.

Caracteres en C

Entendiendo los caracteres

   Primero veamos qué cosa es un caracter.
   En nuestra pantalla de la computadora vemos letras, números y signos de todo tipo.
   Todos ellos son caracteres, los cuales tienen un "dibujito" asociado y su significado es inteligible sólo por seres humanos.
   Cada caracter tiene una representación (o codificación) con bits (0s y 1s) a nivel interno en la máquina.

   Así, la codificación para el caracter 'A' es 01000001.
   Este código es un número escrito en base 2 (binaria), el cual tiene una equivalencia en base decimal (la que usamos los humanos, porque tenemos diez dedos para contar), que equivale a 65.

   Hay una tabla que asigna en forma estandarizada códigos binarios a caracteres, para los códigos que van del 00000000 (decimal 0) al 01111111 (decimal 127).
   Esa tabla es la famosa ASCII, y es compartida y respetada por una vastísima mayoría de entre todos los sistemas de computación. (Las excepciones pueden considerarse "rarezas").
   Para códigos del 128 en adelante ya no podemos estar seguros de nada.

http://es.wikipedia.org/wiki/ASCII

   Aún cuando los códigos del 10000000 (128) al 11111111 (255) no tengan caracteres ASCII asignados, podemos esperar (de nuevo, en la vasta mayoría de sistemas) que sigan teniendo algún tipo de caracter asignado. Son caracteres especiales, que varían según el sistema, la región o idioma, entre otros menjunjes.

   De cualquier manera, el lenguaje C reconoce como caracter a cualquiera de los códigos binarios que van del 0 al 255, por más que sean o no de la tabla ASCII.

   ¿Puede haber valores más allá del 255? . ¿El C los acepta? . Son temas que discutiremos mucho más adelante.

[cerrar]

   Si en el lenguaje C queremos referirnos a un caracter determinado, por ejemplo el @, no podemos simplemente escribir el caracter y esperar que todo funcione.
   Más concretamente, si quisiéramos verlo impreso en pantalla mediante, por ejemplo, la función printf(), no podemos escribir printf(@), porque eso estaría mal, y enseguida el compilador se nos quejaría de que eso no tiene sentido.
   La manera correcta de indicar en C una constante de caracter es rodeando al caracter con comillas simples, así: '@'.

   Los caracteres ASCII con códigos del 0 al 31 no son imprimibles, sino que se usan para realizar operaciones especiales, como saltos de línea, tabulaciones, marcas especiales en los archivos, etc.
   Se denominan caracteres de control. Excepcionalmente, el ASCII 127 también se considera un caracter de control (tiene código binario 01111111, suele ser no visible ni imprimible, y se lo asocia con la tecla de borrado hacia la derecha DEL ó DELETE).
   Como no se pueden imprimir, es difícil referirse a ellos en forma directa.
   Además, es posible que en distintos sistemas las mismas funciones se realicen por caracteres de control con distintos códigos.
   Para evitar esta ambigüedad, el lenguaje C permite que el programador se refiera a algunos caracteres de control (no a todos), mediante una notación especial, que luego el compilador se encarga de gestionar y traducir a los códigos verdaderos usados en el sistema.
   Se accede a ellos mediante una secuencia de escape, anteponiendo una barra invertida: \.
   He aquí la lista:

Secuencias de escape para caracteres especiales en C

\( \bullet \) \a   Sonido audible (sí, hay un caracter que hace un pitido :D )
\( \bullet \) \b   Retroceso (Corresponde a la tecla backspace, que borra un caracter hacia la izquierda)
\( \bullet \) \f   Salto de formato (formfeed  ??? )
\( \bullet \) \n   Salto de línea (linefeed)
\( \bullet \) \r   Retorno de carro (carriage return, se refiere a que el cursor vuelve al principio de la línea en pantalla, o bien que la impresora vuelve a comenzar desde la izquierda, pero sin bajar a la línea siguiente)
\( \bullet \) \t   Tabulación horizontal (Tab, simula un salto de varios espacios en blanco, mediante la tecla TAB)
\( \bullet \) \v   Tabulación vertical (V Tab  ??? )
\( \bullet \) \\   \ Barra invertida (backslash)
\( \bullet \) \'   ' Apóstrofo (comilla simple)
\( \bullet \) \"   " (comilla doble)
\( \bullet \) \?   ? (Interrogación)

   Así por ejemplo, si queremos indicar la constante de caracter "avance de línea", debiéramos escribir '\n'.
   Como es lógico, dado que el signo \ lo estamos usando con un significado especial en C, como secuencia de escape de caracteres especiales, necesitamos una manera de poder generarlo sin ambigüedad alguna.
   Esto se hace poniendo una doble barra: '\\', lo cual, al mostrarlo en pantalla dará el resultado: \, o sea, se muestra una sola barra.
   Un problema similar lo tenemos con las comillas simples, ya que ellas se usan para encerrar caracteres. Si queremos referirnos explícitamente al caracter "comilla simple" necesitamos usar una secuencia de escape, la siguiente: \', que con el entrecomillado queda así: '\''. Al correr el programa se verá tan solo: '
   Finalmente, las comillas dobles se usan, como hemos visto, para encerrar cadenas de caracteres: "Hola Mundo".
   Si necesitamos mostrar en pantalla el caracter "comilla doble", se presenta una situación ambigua, y tenemos que usar de nuevo una secuencia de escape, que en este caso es: \", la cual, con el entrecomillado queda así: '\"'. Al correr el programa, se verá en la pantalla tan sólo: "

   Se aprecia que hay también una secuencia de escape para el signo de interrogación: '\?'. Esto está relacionado con los trigrafos, tema que no estudiaremos por ahora.

[cerrar]

   Lo que nos va a interesar a nosotros es tener a mano una lista resumida de las secuencias de escape más utilizadas:

\( \bullet \) \n   Salto de Línea
\( \bullet \) \\   Caracter \
\( \bullet \) \'   Caracter '
\( \bullet \) \"   Caracter "

   El salto de línea lo que hace es bajar a la siguiente línea en la pantalla, y posicionar el cursor al principio de dicha línea.
   En la impresora, esto se consigue con un avance a la siguiente línea, y posicionando el cabezal de impresión al principio de dicha línea (a la izquierda de todo). (¡¡Bienvenidos a la prehistoria!! :P )
   Ahora bien. Desconocemos el código binario que los caracteres especiales, como el salto de línea '\n', tendrán en cada sistema. Si por alguna razón necesitamos trabajar directamente con los códigos numéricos de los caracteres de control, o de los caracteres ASCII, es aún posible acceder a ellos mediante el número que les corresponde, otra vez mediante secuencias de escape especiales.
   No obstante, estos códigos no están en números decimales, ni tampoco binarios, sino en bases 8 (octales) y 16 (hexadecimales). La explicación, en el Spoiler:

Secuencias de Escape Octales y Hexadecimales

   Los dígitos en base 8 son:
   
0  1  2  3  4  5  6  7

   Traducir de binario a octal es fácil, porque se toman ternas de dígitos binarios y se las traduce a su correspondiente octal, así:

000 ------- 0
001 ------- 1
010 ------- 2
011 ------- 3
100 ------- 4
101 ------- 5
110 ------- 6
111 ------- 7

   Por ejemplo, el caracter 't', cuyo código binario es 01110100, en octal es 164.
   (La traducción tiene que hacerse de derecha a izquierda para que el resultado sea el correcto, o sea, separar las ternas de dígitos así: 01-110-100).
   En decimal 't' es el ASCII 116.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Los números hexadecimales están en base 16, y sus dígitos son:

0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F

   Se usan las letras A, B, C, D, E, F, para denotar dígitos, porque la base 16 necesita 6 dígitos adicionales, y se estila usar esas letras para tal fin.
   Así, la A equivale al 10 en decimal, la B al 11 en decimal, etc.
   La traducción de binario a hexadecimal también es directa:

0000 ------- 0
0001 ------- 1
0010 ------- 2
0011 ------- 3
0100 ------- 4
0101 ------- 5
0110 ------- 6
0111 ------- 7
1000 ------- 8
1001 ------- 9
1010 ------- A
1011 ------- B
1100 ------- C
1101 ------- D
1110 ------- E
1111 ------- F

   Así, el caracter 't', cuyo código binario es 01110100, en hexadecimal es 74.
   Otro ejemplo: el caracter ']', cuyo código binario es 01011101, en hexadecimal es 5D.
   En decimal sería el ASCII 93.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Para expresar un caracter mediante su código en octal, se escribe:

   \***, donde *** son los tres (también pueden ser sólo uno o dos) dígitos en octal del código que corresponda.

   Por ejemplo, para 't' hacemos '\164'.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Para expresar un caracter mediante su código en hexadecimal, se escribe:

\x**, donde ** son los dos dígitos en hexadecimal del código que corresponda.

   Por ejemplo, para 't' hacemos '\x74', y para ']' hacemos '\x5D'.

   Si bien la x tiene que estar en minúsculas, los dígitos hexadecimales pueden estar tanto en mayúsculas como en minúsculas.
   O sea que es lo mismo poner '\x5d' en el último ejemplo.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Hay países donde ciertos caracteres están inaccesibles en el teclado. Así que en ese caso se usan convenciones que permiten generar esos caracteres mediante la combinación de otros tres, de fácil acceso. Se denominan trigrafos.
   También se pueden utilizar símbolos alternativos.
   No voy a detallar esto (por ahora).

   Para una consulta rápida, ver por ejemplo: http://c.conclase.net/curso/?cap=903

[cerrar]

   Todo esto de las secuencias de escape puede marearlo a uno.
   Pero para salir de dudas conviene siempre hacer un programita, para entender lo que pasa.
   Las secuencias de escape las escribe el programador, mientras que el caracter definitivo que le corresponde es lo que sale en la pantalla.
   Para verlo, utilizaremos la función putchar() de la biblioteca estándar <stdio.h>, la cual se encarga de enviar a la pantalla un caracter a la vez.
   El siguiente programa (en spoiler) muestra el uso de las comillas simples para denotar constantes de caracter, el uso de secuencias de escape octales y hexadecimales, y también el uso de la secuencia de escape '\n' para generar un salto de línea mediante putchar().

TestingChars.c

(programa TestingChars.c)


#include <stdio.h>

int main(void) {
   printf("Probando caracteres!!\n\n");
   
   printf("Primero los codigos octales: ");
   putchar('t');
   putchar('\164');
   putchar(']');
   putchar('\135');

   putchar('\n');
   
   printf("Ahora los codigos hexadecimales: ");
   putchar('t');
   putchar('\x74');
   putchar(']');
   putchar('\x5d');

   putchar('\n');
   
   getchar();   
}

[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   El tipo de datos que se usa  8^) en C para representar caracteres es char.
   (Nota técnica: esto no significa que los caracteres son constantes de tipo char. En realidad son de tipo int, pero esto será estudiado mucho más adelante en este curso).
   Una constante de tipo char necesita exactamente 1 byte (8 bits o dígitos binarios) de memoria para ser almacenado en la computadora.

   El lenguaje C permite operar directamente con los códigos numéricos asociados a cada caracter.
   Tal es así que el tipo de datos char se considera uno de los tantos tipos numéricos de C.
   Los números representados por char son enteros.

   No es siempre muy claro, y depende del compilador o el sistema, si los números char admiten signo o no.
   En el caso de que admitan signo, los números que representan van del -128 al 127.
   Mientras que si no admiten signo, el rango de números va del 0 al 255.
   (Nota ténica: estamos asumiendo bytes de 8 bits. Puede haber sistemas cuyos bytes tengan más de 8 bits).
   El bit de más a la izquierda se considera el "bit indicador de signo" para el caso de enteros con signo, o si sumar o no el número 128, en caso de enteros sin signo.

   En cualquier caso, los números del 0 al 127 siempre están disponibles en el rango de char.
   ¿Representan estos valores los códigos de los caracteres del estándar ASCII?
   En la mayoría de sistemas informáticos, podemos responder que sí. Pero como hay unos pocos casos en que esto no se puede saber de antemano, el estándar C no da nada por seguro.

   Si nos interesa realizar operaciones aritméticas con los char, y queremos estar seguros de si tiene o no tiene signo, y así controlar el rango de valores, podemos utilizar los tipos más específicos siguientes:

signed char      (con signo, de -128 a 127)
unsigned char    (sin singo, de 0 a 255)

   El char por sí solo podría ser cualquiera de esos dos, y no está especificado por el estándar.
   Puede que el compilador sí haya elegido cuál de las dos variantes (con o sin signo) elegir, pero a nosotros no nos interesa, porque:

   No debemos utilizar características que son dependientes de nuestro compilador, a fin de que nuestros programas sean portables a otros sistemas sin problemas.

   Para eso se inventaron, justamente, los estándares: para la portabilidad.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Strings: Cadenas de caracteres

   Una string o cadena de caracteres es una secuencia ordenada de caracteres.
   Si quisiéramos escribir la frase Hola Mundo, sería incómodo especificar uno a uno los caracteres que la componen:

'H' 'o' 'l' 'a' ' ' 'M' 'u' 'n' 'd' 'o'

   A fin de abreviar este tedio, se procede a poner todos los caracteres en cadena ordenada, y luego encerrar toda la cadena entre comillas dobles, así:

"Hola Mundo"

   El tipo de datos básico asociado en C a las strings es char*.
   Otro día les explico qué es ese asterisco: *::)

   Lo importante es que, aquellas funciones que desean mostrar o manipular cadenas de caracteres, tienen parámetros de tipo char*, que no es lo mismo que un simple char.
   Así, printf("Hola"); es correcto, porque printf acepta strings (de tipo char*).
   También printf("x"); es correcto, y muestra una x en pantalla.
   Pero printf('x'); es incorrecto, porque 'x' es una constante de tipo char, no una string.

   ¿Cómo hace C para diferenciar char de char*, y cómo almacena las char* en la memoria?[/b]

   En la memoria, se necesita indicarle al compilador de C que una cadena de caracteres termina en tal o cual lugar.
   En el lenguaje C se presupone que una string se almacena con sus caracteres colocados en posiciones contiguas de la memoria, y se toma la convención de que el final de la cadena de caracteres se alcanza sólo cuando se encuentra un caracter con código binario 00000000.
   Éste se llama caracter nulo, su código ASCII es el 0, y se puede especificar en octal como '\000' (o simplemente '\0'), o en hexadecimal como '\x00'.

   Las cadenas así formadas se denominan strings terminadas en nulo.

   Son típicas del lenguaje C, y no están presentes en general en otros lenguajes.
   Por lo tanto, la cadena entrecomillada "Hola Mundo", en realidad es la secuencia de caracteres siguiente:

'H' 'o' 'l' 'a' ' ' 'M' 'u' 'n' 'd' 'o' '\0'

   Esos caracteres se almacenan todos en posiciones contiguas de la memoria (en el ejemplo son 11 bytes, contando el byte ocupado por el caracter nulo), que han de estar reservados sólo para esa string.
   No se pueden usar para otra cosa, porque se producen violaciones de acceso de memoria RAM, lo cual repercute en errores de ejecución del programa.
   Lo más probable es que la computadora se bloquee.  :o :o :o :o

   Este es un ejemplo claro de que el lenguaje C permite situaciones problemáticas, que son responsabilidad del programador el evitarlas.
   El acceso indebido a ciertas posiciones de memoria puede ser un problema de aparición muy frecuente en C.
   Con las strings estamos expuestos ya a estas dificultades.  :banghead: :o

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Y ahora vemos por qué "x" es distinto de 'x'.
   En la memoria, "x" se almacena como la secuencia de caracteres:

'x' '\0'

la cual ocupa 2 bytes de memoria, mientras que 'x' es un solo caracter que ocupa 1 solo byte.

   Ahora bien, podemos tener también una cadena vacía, sin caracteres.
   Se la denota con "", y se ve que no hay nada entre ambas comillas.
   En memoria, la cadena vacía ocupa 1 byte, debido a que aún está presente el caracter nulo, que indica el punto donde termina la cadena, por más vacía que sea:

'\0'

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   En cualquier caso, el caracter nulo '\0' sólo está presente en la representación interna (o sea, en memoria RAM) de una string dada, pero los caracteres nulos no se muestran en pantalla, ni se imprimen (al menos mediante funciones que, como printf(), envían o manipulan strings).
   Otra curiosidad es que, una constante de cadena que intercale un caracter nulo en el medio de ella, quedará cortada en ese punto, desde el punto de vista del compilador de C.
   Así, si probamos con la sentencia:

printf("Hola\0 Mundo");

lo que se verá en la pantalla es sólo esto:

Hola

   Esto es así porque el C considera que el caracter nulo \0 marcó el fin de la cadena de caracteres, e ignora lo que quedó a la derecha de él (la palabra Mundo)
   ¿Y esto no sería un desperdicio de memoria RAM? La verdad es que sí. Pero bueno, el lenguaje C es así.

   Y nos hacemos una última pregunta:
   ¿Qué ocurre si deseamos utilizar cadenas de caracteres que contengan al caracter con código ASCII 0 (el caracter nulo) como parte de ellas?
   Bueno, simplemente nos aguantamos la mala suerte de no poder hacer tal cosa.  :(

   Sin embargo, es posible mediante putchar() o funciones similares, enviar caracteres en forma individual. En particular esto permitiría enviar caracteres nulos a un dispositivo de salida.

putchar('\0');

   (Eso en la pantalla se ve como un espacio en blanco...)

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Espacios en blanco:

   El espacio en blanco, ése que se genera presionando la tecla "barra espaciadora", tiene código ASCII 32, que en binario es 00100000, en octal es 040@, y en hexadecimal es 20.
   Para generarlo, tenemos pues estas alternativas:

' '       (poner el espacio en blanco entre comillas simples)
'\040'    (octal 040)
'\x20'    (hexadecimal 20)

Discusión sobre los espacios en blanco

   Hay otros caracteres que al mostrarlos generan el efecto visual de un espacio en blanco, pero internamente no son espacios en blanco.
   Uno de ellos es '\t' (tabulador), que genera (el efecto visual de) uno o más espacios en blanco.
   En las impresoras representa un salto a la siguiente marca (que suelen ser columnas múltiplos de 8).
   Otro es el ASCII 255 (binario 11111111, octal 377, hexadecimal FF), que tradicionalmente se usa para indicar un espacio en blanco de "no separación", ya que comunmente el ASCII 32 se usa para indicar separación de palabras.
   La diferencia está en que los espacios en blanco "comunes" (ASCII 32) suelen ser manipulados por los programas editores de texto de forma relajada y flexible, agregando o quitando espacios en blanco según convenga.
   En cambio, el ASCII 255 sería una versión de "espacio blanco rígido", que no admite esas manipulaciones.
   Por último, el '\0' se muestra como un espacio en blanco.

   Podemos agregar las siguientes líneas a nuestro programa TestingChars.c, a ver qué pasa:


   printf("\\t_\t_");
   printf("\n");
   printf("\\xFF_\xFF_");                /* '\xFF' es el ASCII 255 */
   printf("\n");

   printf("\\0_"); putchar('\0'); printf("_");    /* Caracter nulo */
   printf("\n");


   Lo que queremos decir con todo esto es que, visualmente, hemos perdido información.
   Se ven espacios en blanco, pero no son el caracter espacio en blanco con código ASCII 32.
[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

05 Enero, 2013, 04:35 pm
Respuesta #8

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
8. Enteros en C (parte I)

0. Números enteros, reales y complejos:

   Tenemos tres clases tipos de datos numéricos en C: enteros, reales y complejos.

Representación de números en C y en la computadora

   Los enteros son los que normalmente usamos para contar: 0, 1, 2, 3, 4, etc., y pueden ser positivos o negativos (-1, -2, -3, -4, etc.).
   Los reales son aquellos que admiten un punto y dígitos decimales, o parte fraccionaria, a la derecha. Por ejemplo: 3.141592, 1.414213, 0.25, -7.777777, etc.
   Los complejos están compuestos por dos componentes, llamadas parte real y parte imaginaria. Ambas partes son números reales, e \( i \) representa la unidad imaginaria, es decir la raíz cuadrada algebraica de \( -1 \):

\( i=\sqrt{-1}. \)

   Recordemos siempre: Los números en una computadora no se comportan como los de la matemática.

   La razón principal es que el procesador de una computadora tiene espacio limitado para alojar números.
   Esto obliga a que todo tipo de datos numérico tenga un rango de valores restringido.
   Asimismo, los números reales no pueden tener, un número arbitrariamente grande de dígitos. Así, sólo pueden pertenecer a un conjunto reducido de la clase de los racionales.
   Las últimas versiones del lenguaje C permiten incorporar números complejos.
   Estos se implementan simplemente como un par de números reales, y tienen las mismas limitaciones que ellos.

   Aunque los tipos aritméticos básicos de C tienen las limitaciones mencionadas, pueden remediarse con ciertas técnicas de programación avanzadas.

   ¿Para qué distinguir tipos enteros y reales? Es que las operaciones con números fraccionarios son proclives a producir errores de redondeo, mientras que los enteros nunca pierden exactitud.

   El tipo de datos estándar que se usa en C para trabajar con números reales son los llamados números de punto flotante.
[cerrar]

1. Constantes numéricas de tipo entero:

   Las constantes numéricas enteras se escriben en C en formato decimal (o sea, en base diez), en octal (base ocho) o en hexadecimal (base dieciséis).  ichas constantes pueden ser positivas o cero, y sólo están limitadas por el tamaño máximo que el compilador o el sistema subyacente puede interpretar. El signo - delante de una constante positiva ya no se considera parte de una constante, sino que es un operador de negación.)
   Si un número es demasiado grande, requiere más bytes en memoria RAM para poder ser representado. En el siguiente Spoiler mostramos una tabla con el máximo entero positivo que puede representarse, según la cantidad de bytes disponibles para un tipo dado:

1.1. Tabla de enteros positivos máximos

Suponiendo bytes de 8 bits, tenemos la siguiente tabla:

Bytes    Entero positivo máximo que puede representare

 1        \( 2^{8} - 1   = 255 \)
 2        \( 2^{16} - 1  = 65535 \)
 3        \( 2^{24} - 1  = 16'777215 \)
 4        \( 2^{32} - 1  = 4294'967295 \)
 5        \( 2^{40} - 1  = 1'099511'627775 \)
 6        \( 2^{48} - 1  = 281'474976'710655 \)
 7        \( 2^{56} - 1  = 72057'594037'927935 \)
 8        \( 2^{64} - 1  = 18'446744'073709'551615 \)
 9        \( 2^{72} - 1  = 4722'366482'869645'213695 \)
10        \( 2^{80} - 1  = 1'208925'819614'629174'706175 \)
11        \( 2^{88} - 1  = 309'485009'821345'068724'781055 \)
12        \( 2^{96} - 1  = 79228'162514'264337'593543'950335 \)

[cerrar]

   ¿Qué ocurre si escribimos enteros demasiado grandes? ¿El compilador los entiende? En principio, eso depende del compilador. Pero aún si el compilador sabe que se trata de una constante entera, no necesariamente "sabe cuánto vale".
   El C99 asegura la disponibilidad de constantes enteras positivas tan grandes como \( 2^{64}-1=18'446744'073709'551615 \), es decir, del orden de los 18 trillones (en nomenclatura inglesa, se diría "18 quintillones", lo cual es confuso...). Así que no vamos a dar por sentado la existencia de constantes enteras mayores que esa en nuestro compilador. De hecho, el compilador (GCC) reconoce ese número.

   Los tipos de datos de enteros sin signo (o sea positivos) en C son los siguientes (según C99):

unsigned char
unsigned short int          (declaración abreviada: unsigned short)
unsigned int                (declaración abreviada: unsigned)
unsigned long int           (declaración abreviada: unsigned long)
unsigned long long int      (declaración abreviada: unsigned long long)

   En todos ellos el mínimo entero representable es 0, pero el máximo en cada uno no está claramente definido en el estándar, dejándolo a criterio de los compiladores.

Rango de valores admitidos para los tipos enteros en C.
   El estándar C99 reclama que:

\( \bullet \)   unsigned char:
       Representa valores al menos 8 bits, con lo cual se asegura al menos el rango de 0 a \( 2^8-1=255 \).
\( \bullet \)   unsigned short int         
       Representa valores al menos en el rango de 0 a \( 2^{16}-1=65535 \).
       Además se exige que, en cualquier circunstancia, contenga el rango de unsigned char.
\( \bullet \)   unsigned int
       Representa valores al menos en el rango de 0 a \( 2^{16}-1=65535 \).
       Además se exige que, en cualquier circunstancia, contenga el rango de unsigned short int.
\( \bullet \)   unsigned long int
       Representa valores al menos en el rango de 0 a \( 2^{16}-1=65535 \).
       Además se exige que, en cualquier circunstancia, contenga el rango de unsigned int.
\( \bullet \)   unsigned long long int
       Representa valores al menos en el rango de 0 a \( 2^{64}-1=18'446744'073709'551615 \).
       Además se exige que, en cualquier circunstancia, contenga el rango de unsigned long.

   Los tipos char se consideran tipos numéricos enteros. Detalles en el spoiler:

1.2. Relación entre el tipo char y los tipos enteros
   Es posible hacer cálculos con valores de tipo char, igual que con cualquier otro tipo entero. Además, las constantes de caracteres, tales como 'm', '@', '&', etc., representan números enteros en C, aunque nos resulte extraño concebir esto así. Tiene sentido una operación como ésta: 'm' + 5 - '@', cuyo resultado es 50;)
   Para mostrar un número tipo char en pantalla, el programador elegirá el formato que prefiera, según el contexto: caracter o número.

   Digamos además que el tipo unsigned char en general siempre ocupa exactamente 8 bits en (casi) todas las implementaciones reales. Pero el estándar C99 no exige esto, y podría ocupar más bits, si hiciera falta. Sin embargo, su rango de valores no podrá ser mayor que el de un dato de tipo unsigned short int.
[cerrar]

   También debemos mencionar el tipo entero sin signo booleano (disponible desde el estándar C99):

_Bool   Ocupa (al menos) 1 byte, y es capaz de alojar los valores 0 y 1.

   El 0 se usa para indicar FALSO, y el 1 es VERDADERO.
   El tipo _Bool no ocupa más bits que el unsigned char.



   Ahora pasemos a estudiar los tipos de datos de enteros con signo. Para C99 son los siguientes:

signed char             
signed short int         (formas abreviadas: signed short, short int, short)
signed int                 (forma abreviada:   int)
signed long int          (formas abreviadas: signed long, long int, long)
signed long long int   (formas abreviadas: signed long long, long long int, long long)

   Las formas abreviadas son sinónimas.
   Lo típico en C es usar las formas abreviadas: short, int, long, long long.

Rangos de valores de los tipos enteros signados

   Los enteros con signo necesitan 1 bit para indicar el signo del número, influyendo en el rango de valores admisibles. C99 asegura los siguientes rangos de valores para los tipos signados:

\( \bullet \)  signed char:
       de \( -2^7=-127 \) a \( 2^7-1=127 \)
\( \bullet \)  signed short:
       al menos de \( -2^{15}=-32767 \) a \( 2^{15}-1=32767 \).
\( \bullet \)  signed int:
       al menos el mismo rango que signed short.
\( \bullet \)  signed long:
       al menos el mismo rango que signed int.
\( \bullet \)  signed long long:
       al menos de \( -2^{63}=-9223372036854775807 \) a \( 2^{63}-1=9223372036854775807 \).
       Además, el rango de valores debe ser al menos tan grande como el de signed long int.

2. Compatibilidad del tipo char:

   La forma aparentemente breve char no significa signed char>:(
   El tipo char siempre se considera distinto de unsigned char y signed char, aunque su rango de valores coincide siempre con alguno de los dos.
   Desde el estándar no se asegura si char tiene signo o no, pero claramente contiene siempre los valores del rango de 0 a 127.
   Además, los tres tipos de datos char, unsigned char y signed char, ocupan todos la misma cantidad de bytes.

   Nota técnica: Los tipos con igual denominación, en general ocupan la misma cantidad de bytes en sus versiones unsigned y signed. (Hay excepciones, pero es un tema complicado de abordar aquí).



3. Enteros de tamaño fijo.

   Según vimos, no es posible predecir la longitud exacta en bits que tiene cada tipo entero en C. La librería <stdint.h> del estándar C99 define nuevos tipos de enteros, que tienen una longitud fija medida en bits.
   Se definen los siguientes tipos enteros:

Sin signo con longitud fija en bits:
    uint8_t, uint16_t, uint32_t, uint64_t.
Con signo con longitud fija en bits:
    int8_t, int16_t, int32_t, int64_t.
Sin signo con longitud en bits mínima asegurada:
    uint_least8_t, uint_least16_t, uint_least32_t, uint_least64_t.
Con signo con longitud en bits mínima asegurada:
    int_least8_t, int_least16_t, int_least32_t, int_least64_t.
Sin signo, rápidos, con longitud en bits mínima asegurada:
    uint_fast8_t, uint_fast16_t, uint_fast32_t, uint_fast64_t.
Con signo, rápidos, con longitud en bits mínima asegurada:
    int_fast8_t, int_fast16_t, int_fast32_t, int_fast64_t.

   Los tipos con longitud fija tienen la cantidad de bits que su nombre indica. Ejemplo: uint8_t es un entero de 8 bits.
   La longitud mínima asegurada fija un mínimo de bits, pero la longitd real puede ser aún mayor.  Ejemplo: uint_least8_t es un tipo entero con al menos 8 bits, pero podría ser mayor en una implementación dada.
   Los tipos rápidos con longitud mínima asegurada también fijan un mínimo en bits para tipos enteros, y que además son rápidos. ¿Qué significa rápidos? El estándar no lo define ni lo sugiere. Pero se supone que un sistema dado puede aprovechar alguna característica interna que implemente versiones de enteros que permiten operar más rápido.

   También se definen los tipos de longitud máxima en bits que la implementación local detecta:

uintmax_t: Tipo entero sin signo de longitud máxima.
intmax_t:  Tipo entero con signo de longitud máxima.

   Dado que C99 asegura la existencia del tipo de 64 bits long long int, podemos asegurar que uintmax_t y intmax_t tienen al menos 64 bits.
   El estándar estipula además que es posible, aunque no obligatorio, definir los tipos intptr_t, uintptr_t. Son las versiones sin signo y con signo de un tipo de enteros portable a utilizar en operaciones con punteros (que involucran cálculos con posiciones en la memoria RAM).



4. Especificación de constantes enteras y establecimiento de tipos enteros en forma implícita.

   Si escribimos un número entero en nuestro programa, ¿de qué tipo de todos los anteriores es? Según las reglas de C99:

\( \bullet \)   Dada una constante numérica entera, escrita con números decimales, su tipo es el más pequeño en el que "aún" cabe el número que pretendemos representar, de entre:

int
long
long long

Un ejemplo en el Spoiler:

4.1. Ejemplo de asignación de tipos a constantes
   Supongamos que nuestro sistema admite int de 16 bits, long de 32 bits y long long de 64 bits.
   Números como -35, 0, 19, 126, 1848, 31009, serían considerados de tipo int.
   En cambio 32768 ya no cabe en nuestro int, y pasa a ser long. (El -32768 puede o no estar en el rango de int.)
   Números como 267543, -999999, 1012345678, serían aún de nuestro tipo long.
   En cambio 2147483648 ya no cabe, y es de tipo long long.
   En nuestro long long ya no cabe 9223372036854775808.
   ¿Qué ocurre con ese número tan grande, que no cabe ni siquiera en un long long?
   El estándar ya no especifica nada al respecto, y así la constante entera más grande que podemos asegurar que funciona  ;) en C es:

9223372036854775807  :aplauso: :aplauso: :aplauso:

   Las constantes escritas en base decimal nunca pueden adjudicarse a un tipo entero sin signo. Ver: Sección 7.1 del libro de King.

[cerrar]



5. Constantes enteras octales y hexadecimales:

   Si delante de un número anteponemos un 0, el compilador de C interpreta que el número está en base 8 (octal). Estos números sólo admiten los dígitos 0, 1, 2, 3, 4, 5, 6, 7, así que si aparecen los caracteres 8 ó 9, dará un error, por no ser un número octal válido.
   Ejemplos: 017, 033, 041234, -0167216.

   Atención:  :o :o No hay que confundirse con estos números. Si en C escribimos un 0 a la izquierda de un número, se lo considera octal, y así no es lo mismo 17 (en decimal) que 017, que está en octal, y que como número decimal equivale a 15.   :-* Así que hay que extremar las precauciones y, en general:

   No anteponer un 0 a la izquierda de una constante de número entero en un programa en C, a menos que a propósito queramos escribir un número en base octal.

   Es posible escribir números en base 16 (hexadecimal), anteponiendo el prefijo 0x delante del número. Por ejemplo, los siguientes son números hexadecimales:

0x34 (hexadecimal 34 = decimal 52), 0x7FC (hexadecimal 7FC = decimal 2044), etc.

   Los dígitos hexadecimales A, B, C, D, E, F, pueden escribirse en mayúsculas y/o en minúsculas, e incluso se pueden mezclar ambos estilos. Ejemplo: 0x1Aa (hexadecimal 1AA = decimal 426).
   En vez de 0x, se puede usar también mayúsculas poniendo 0X, por ejemplo: 0X4D (hexadecimal 4D = decimal 77).
   Observación: No hay que confundirse con el uso de la x al definir caracteres con código hexadecimal, ya que allí no está permitida la X mayúscula: '\x3F' está bien, pero '\X3F' está mal, mientras que para constantes numéricas, tanto 0x3F como 0X3F son ambas correctas.



   Para las constantes en octal y en hexadecimal existe una regla distinta de asignación de tipo entero:

\( \bullet \)   Dada una constante de número entero escrito en octal o hexadecimal, el tipo de datos entero que se le asigna es el primero de los siguientes, tal que el número puede representarse (o cabe) en dicho tipo:

int
unsigned int
long int
unsigned long int
long long int
unsigned long long int

   La máxima constante entera positiva que el estándar C99 nos asegura en un programa en C es \( 2^{63}-1 \), como ya vimos. En octal es: 0777777777777777777777. Y en hexadecimal es: 7FFFFFFFFFFFFFFF.



6. Forzando el tipo de una constante numérica.

   Es posible forzar el tipo de una constante numérica entera, mediante el uso de sufijos.

   Para forzar al compilador a interpretar una constante de número entero como unsigned (del tamaño que sea), se agrega como sufijo al número una U ó una u (es indistinto el uso de mayúsculas o minúsculas).
   Así, la constante 317, que normalmente sería un int, al escribir 317u pasa a ser un unsigned int.
   El compilador, al tomar un número entero con sufijo u, le asignará el primero de los siguientes tipos en que el valor del número "cabe":
   
unsigned int
unsigned long
unsigned long long

(Esto vale para números decimales, octales y hexadecimales).

   Otro sufijo es L ó l (de nuevo es indistinto el uso de mayúsculas o minúsculas), para indicar long.
   El efecto de agregar este sufijo es que el compilador trata de encajar la constante decimal en la más pequeña de las opciones: long ó long long.
   En cambio, para el sufijo L o l de una constante octal o hexadecimal, el compilador tratará de asignar el primero que sea posible de los siguientes tipos:

long int
unsigned long int
long long int
unsigned long long int

   También existe el sufijo LL ó ll (no se pueden mezclar mayúsculas y minúsculas como: Ll ó lL). Con este sufijo, una constante entera escrita en base decimal pasa a considerarse enseguida de tipo long long int.
   Mientras que si la constante es octal o hexadecimal, se intentará asignarle el primerlo de los tipos siguientes, que sea posible:

long long int
unsigned long long int


   Se pueden combinar los sufijos U con los L ó LL, para obtener UL (asegura unsigned long o mayor) y ULL (asegura tipo unsigned long long).
   Se pueden cambiar de orden o escribir en indistintamente en mayúsculas y minúsculas, y así todas estas opciones son válidas:

ul  uL  Ul  UL  ull  uLL  Ull  ULL  lu  Lu  lU  LU  llu  LLu  llU  LLU

   Todos los sufijos U, L, LL, UL, ULL, pueden colocarse detrás de constantes octales y hexadecimales: 0331413L, 0x35ULL.
   Al colocar el sufijo UL a una constante entera, el compilador intentará asignarle el primero que sea posible de la lista de tipos siguientes:

unsigned long int
unsigned long long int

(Esto vale para bases decimal, octal, hexadecimal).

   Si anteponemos el sufijo U a una constante más grande que el máximo valor aceptado en un signed long long int, pero que todavía está en el rango del tipo unsigned long long int. ¿Es una constante que el compilador debe aceptar?
   Debo revisar el estándar al respecto. (Revisar) En mi sistema, cuyo long long es de 64 bits, admite sin problemas la constante \( 2^{64}-1 \) seguida de sufijo U, como un unsigned long long int, escribiéndola así:
18446744073709551615U  ::)

(El estándar discute la posibilidad de tipos de datos extendidos, pero yo me restrinjo siempre a lo estrictamente asegurado por el estándar, y estos tipos extendidos no están especificados en el C99 estricto).



7. Constantes numéricas: tres caras de una misma moneda

   Podemos preguntar por las constantes numéricas desde 3 puntos de vista distintos. Ver spoiler:

(Discusión sobre las constantes enteras)
(i)  Constantes numéricas que hemos escrito, y que el compilador es capaz de reconocer como tales.
(ii) Constantes numéricas que el compilador reconoce como tales, y que a su vez fue capaz de asociarles correctamente un tipo numérico en C.
(iii) Valores en el rango admitido para un tipo de datos numérico dado.

   Esos 3 tipos de constantes son esencialmente diferentes.

   En general siempre se cumple (i) pues es fácil reconocer si algo es sintácticamente una constante entera.
   Si una constante cumple (ii), entonces también, siempre, cumple (i) y (iii).
   Si cumple (iii), ¿cumple (i)? En general sí. Si no, sería indicio de un mal compilador.

   Supongamos que nuestro compilador tiene tipos int de 16 bits, long de 32 bits, long long de 64 bits, y además no tiene tipos mayores que estos.
   Consideremos las constantes enteras 10 trillones y 100 trillones, escritas en decimal:

10000000000000000000 (\( 10^{19} \)), 100000000000000000000 (\( 10^{20} \)).

   El número 10 trillones no está en el rango de valores de signed long long int, pero sí de unsigned long long int.
   Mi compilador GCC actual avisa que sólo el estándar C90 acepta ese número como de tipo unsigned long long int (se siguen otras reglas). Pero con las reglas de C99 no se puede asignar un tipo entero.
   Sin embargo, 10 trillones es un valor correcto en el rango de unsigned long long int.
   Hemos encontrado un caso donde vale (iii), pero no (ii).

   El número 100 trillones no cabe en el rango de valores de unsigned long long int.
   En este ejemplo no valen ni (ii) ni (iii).
[cerrar]



Organización

Comentarios y Consultas

05 Enero, 2013, 04:35 pm
Respuesta #9

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
9. Enteros en C (parte II)

   El estándar C99 nos brinda las librerías <limits.h> y <stdint.h>, que ofrecen información útil de los tipos enteros.

¿Cómo especifica C el dominio de sus tipos enteros?

   Leyendo cuidadosamente el documento del estándar C99 así como el libro de comentarios The New C Standard, de nuestra lista bibliográfica, podemos concluir cosas bastante curiosas. Por ejemplo:

(i) El estándar no especifica el tamaño de los tipos enteros en bytes, ni siquiera en bits. Solamente indica rangos de valores numéricos que cada tipo entero está obligado a cubrir.
(ii) Las implementaciones particulares tienen el derecho de abarcar rangos mayores.
(iii) Hay sistemas, arquitecturas, y demás contextos informáticos, en los cuales se pueden ver ejemplos reales de tipos char de 9 bits, enteros de 36 bits, otro sistema con todos los tipos enteros de 6 bytes, etc., etc.

   El estándar es compatible con todas esas situaciones de apariencia exótica.

   En la práctica corriente, el bit de signo de un tipo signed int, por ejemplo, se aprovecha en un unsigned int para representar valores del doble de tamaño en el rango positivo respecto signed int.
   Sin embargo, esto no es obligatorio, y existen implementaciones en las que el máximo valor positivo en ambos casos se ha tomado como el mismo, y es un comportamiento admitido en el estándar.
   Otra observación:

(iv) El estándar indica valores mínimos y máximos numéricos esperados en cada tipo, pero esto no significa que esos valores puedan o deban expresarse directamente en forma literal en <limits.h>.

   A veces puede resultar enrevesado o incluso imposible escribir en forma directa los valores límites tolerados por ciertos tipos.
   Puede que a veces haga falta dar dichos valores a través de expresiones aritméticas quizá complejas, que involucran constantes.
   Se exige además (a la implementación) que dichas expresiones sean compatibles con el uso de la directiva #if.

[cerrar]

   En el siguiente spoiler hay una tabla con los valores obligatorios para cada constante definida en el archivo limits.h. También explicamos el significado de dichas constantes.

Constantes de limits.h y definición de los rangos de valores de los tipos enteros

CHAR_BIT        \( \geq 8 \)
MB_LEN_MAX      \( \geq 1 \)

SCHAR_MIN       \( \leq -127 \)
SCHAR_MAX       \( \geq 127 \)
UCHAR_MAX       \( \geq 255 \)
CHAR_MIN        0 si char se considera un tipo sin signo,
                ó: el mismo valor de SCHAR_MIN si char se considera con signo.
CHAR_MAX        UCHAR_MAX si char se considera un tipo sin signo,
                el mismo valor de SCHAR_MAX si char se considera con signo.

SHRT_MIN        \( \leq -32767 \)
SHRT_MAX        \( \geq 32767 \)
USHRT_MAX       \( \geq 65535 \)
INT_MIN         \( \leq -32767 \)
INT_MAX         \( \geq 32767 \)
UINT_MAX        \( \geq 65535 \)
LONG_MIN        \( \leq -2147483647 \)
LONG_MAX        \( \geq 2147483647 \)
ULONG_MAX       \( \geq 4294967295 \)
LLONG_MIN       \( \leq -9223372036854775807 \)
LLONG_MAX       \( \geq 9223372036854775807 \)
ULLONG_MAX      \( \geq 18446744073709551615 \)


   Expliquemos ahora como se usan las constates definidas en <limits.h>.

   La macro CHAR_BIT es una constante entera \( \geq 8 \) que indica la longitud en bits que van a tener los 3 tipos char: unsigned char, signed char y char.
   Esto es lo formalmente se llamará 1 byte.
   Como puede apreciarse, la cantidad de bits de un byte puede variar de un sistema a otro, y de hecho lo hace, admitiendo valores de 9, 16, etc. No entraremos en detalles.

   No hemos discutido aún los caracteres multibyte.
   Por ahora sólo diremos que la macro MB_LEN_MAX es una constante entera que indica el número de bytes (en el sentido dado en el párrafo previo) máximo que puede tener un caracter multibyte.

   El rango de valores que pueden tomar los distintos tipos enteros, quedan especificados así:


Tipo de entero ---------- Rango de valores válidos que puede albergar

signed char:              entre SCHAR_MIN y SCHAR_MAX
unsigned char:            entre 0 y UCHAR_MAX
char:                     entre SCHAR_MIN y SCHAR_MAX   (si char se considera "sin" signo)
                          ó: entre 0 y UCHAR_MAX        (si char se considera "con" signo)
signed short int:         entre SHRT_MIN y SHRT_MAX
unsigned short int:       entre USHRT_MIN y USHRT_MAX
signed int:               entre INT_MIN y INT_MAX
unsigned int:             entre 0 y UINT_MAX
signed long int:          entre LONG_MIN y LONG_MAX
unsigned long int:        entre 0 y ULONG_MAX
signed long long int:     entre LLONG_MIN y LONG_MAX
unsigned long long int:   entre 0 y ULLONG_MAX


   Supongamos como ejemplo que tenemos un sistema donde SHRT_MAX se ha elegido como 242143. Esto está permitido, porque SHRT_MAX ha de ser, según la primer tabla, un número \( \geq{}32768 \).
   Pero ahora, el rango de valores de un dato signed short está acotado por arriba, no pudiendo sobrepasar este valor estipulado de 242143.
   Análogas consideraciones valen para los otros tipos y sus rangos respectivos.
[cerrar]

   El estándar C99 obliga además a que se cumplan ciertas condiciones.

En particular, se entromete a fondo con la representación de los enteros a nivel de bits, y da indicaciones sobre el significado de esos bits, etc., etc. Esto no era así en los estándares previos a 1999 del lenguaje C.

   El análisis de las reglas en este Spoiler:

Representación estándar interna de los bits de datos de tipos enteros

   En general, un lenguaje de alto nivel no especifica ni se ocupa de cómo se han de representar sus tipos de datos a nivel de bits, ni dar indicaciones de cuántos bits debe ocupar un cierto tipo, etc.
   Sin embargo en C no es mala idea hacerlo, ya que existen operadores que trabajan bit a bit, y siendo así es mejor que haya reglas claras para mayor portabilidad.
   Desde 1999, muchos detalles a nivel de bits en C tienen un significado uniforme en todos los sistemas informáticos y todos los compiladores.

  • Se acostumbra  ;) ;) estipular que las versiones "con" signo y "sin" signo de un cierto tipo entero general tienen que ocupar la misma cantidad de bits.  Así, si tenemos un unsigned long int de 32 bits, también signed long int ocuparía 32 bits.

       Pero: el estándar no obliga a que esto sea así. Abajo iremos precisando un poco más.

  • Si la versión unsigned de un tipo entero ocupa \( N \) bits, entonces el rango de valores que tiene que representar va de 0 a \( 2^{N-1} \).
       Y más aún, debe representarse internamente en la máquina siguiendo una estricta escritura en base 2, con los bits contiguos respetando las potencias de 2.
       Este nivel tan detallado de especificación se ha establecido en el estándar C99, o sea que este tipo de cosas no eran obligatorias en el estándar anterior a 1999.

  • Para la versión signed de un tipo entero, tiene que haber exactamente 1 y sólo un bit de signo.

  • Para la versión signed de un tipo entero, en el caso de un bit usado para representar un valor (o sea, otro que el bit de signo), representará la misma potencia de 2 que el correspondiente bit en la versión unsigned.
       Esto implica que los mismos bits se usan en ambos tipos para las mismas potencias de 2, pero no quiere decir que se use obligatoriamente 0 ó 1 para indicar presencia o ausencia de un valor específico. Esto tiene que ver conque se permite utilizar distintas representaciones de los números negativos (complemento a 1, complemente a 2, ¿etc.?).
       Más aún, si la versión signed usa \( M \) bits para expresar valores, y la versión unsigned usa \( N \) bits, entonces \( M\leq N \).

  • Si el bit de signo es 0, entonces no afecta al número representado por los bits de valor.
       En consecuencia, en aquellos números no negativos que pertenecen al rango de valores de un tipo, en sus dos versiones signed y unsigned, el valor numérico representado coresponde al mismo valor matemático en ambos casos.

  • Si en un entero signed de \( N \) bits el bit de signo es 1 entonces el valor que representa es exactamente \( -2^N \) si para enteros con signo se usa formato complemento a 2, y \( -(2^N-1) \) si se usa complemento a 1.
       Esta última representación se entiende como una segunda versión del valor 0, cuando se usa complemento a 1.  Es el llamado 0 negativo. Se sigue considerando aritméticamente como el valor 0, pero es otra representación interna del mismo número.
       Además el estándar exige que sólo se produzca un 0 negativo bajo ciertas circunstancias, que aquí no voy a especificar.

    Nota: Desde hace varias décadas todos los sistemas informáticos usan complemento a 2.
    Comentario: Las representaciones complemento a 2 y complemento a 1 serán explicadas en otro momento.

  • No queda especificado el comportamiento de los operadores de bits en presencia de tipos signed con un valor de 0 negativo, aún en el caso que no se use complemento a 1.

       Los resultados de operaciones de bits con datos signed no están definidas por el estándar. Así, para mayor certeza, conviene trabajar con datos unsigned al hacer operaciones de bits.

  • Se denomina precisión de un tipo entero al número de bits usados para representar un valor (sin contar el bit de signo).
       Se denomina longitud (o ancho, en inglés: width) al número de bits usados para representar tanto el valor como el bit de signo.
       La longitud de un signed es siempre igual a su precisión más 1.

  • Cualesquiera sean los métodos empleados para representar valores enteros, las versiones signed y unsigned de un tipo dado tienen que representar el 0 de la misma forma, a saber, con todos los bits iguales a 0, incluido el bit de signo.

  • El estándar admite la posible presencia de unos bits extra, llamados padding bits.

    Nosotros asumiremos que en nuestro sistema no hay padding bits. (Muchos detalles y discusiones se esconden tras esto, y también las evitaremos...)

  • El valor de UCHAR_MAX tiene que ser exactamente igual a \( 2^{CHAR\underline{\ \  }BIT}-1 \).

  • En las reglas del estándar no queda demasiado claro cuánto debiera ser el mínimo valor entero de un tipo dado.
       Por ejemplo, SCHAR_MIN podría ser -128 o -127.
       Sin embargo, esto depende de si los números negativos se implementan como complemento a 2 o como complemento a 1.
       Podemos asegurar entonces, lo siguiente:
       \( \bullet \) Si se usa representación de complemento a 2, entonces: si el máximo valor que toma un tipo signed es \( M \), entonces el mínimo valor que admite es \( -(M+1) \).
       \( \bullet \) Si se usa representación de complemento a 1, entonces: si el máximo valor que toma un tipo signed es \( M \), entonces el mínimo valor que admite es \( -M \).

  • ¿El estándar asegura que el rango de valores admitido por un tipo signed está necesariamente centrado en 0?
       No he sido capaz de hallar esto en las reglas del estándar C99, así que no estoy seguro.
       Sin embargo, en la práctica corriente siempre ocurre que los valores de un signed están centrados en torno al 0.

  • Debido a que el tipo _Bool debe tener una longitud en bits menor o igual que el tipo unsigned char, podemos asegurar que a lo sumo ocupa un byte, sin importar cuántos bits tenga un byte.
       El número de bits de un _Bool es menor o igual que CHAR_BIT.

       A su vez, todos los datos en C tienen que ocupar la unidad mínima de almacenamiento: 1 byte.
       Como CHAR_BIT define el tamaño de 1 byte, resulta que _Bool ha de ocupar exactamente 1 byte.




   ¿Son iguales el número de bits de las versiones signed y unsigned de un tipo entero dado?

   En general, se acostumbra   ;) ;) que la versión signed de un tipo entero tenga la misma longitud que su compañera unsigned, y así la precisión será exactamente 1 menos que de la compañera unsigned.
   Pero el estándar no obliga a esto, y puede ocurrir que los tipos signed sean más pequeños.
   Lo que el estándar sí obliga es a que el rango de valores positivos y los bits que representan valores de la versión signed encajen a nivel de bits con la versión unsigned, de manera que representen el mismo valor matemático.

   Así, podría ocurrir que un signed int tenga 1 bit más que su compañero unsigned int, si ambos comparten los mismos valores positivos, ya que el signed int además debe contener el bit de signo. (Incluso hay ejemplo de un sistema de la vida real que trabaja así).
   Pero en todo otro caso, el número de bits de un signed será menor o igual :) que la de su compañero unsigned. Inclusive podría ocurrir que la versión unsigned tenga varios bits más.
 
Precaución: A nivel de bits, se puede dar el caso que los bits de valor sean de cierta forma, pero que su representación como objeto en memoria sea algo diferente, por más que en ambos casos se hable de bits bien ordenaditos y todo.
   Nosotros estamos suponiendo que ambas cosas son lo mismo, para no marearnos. Pero en algún momento tendríamos que estudiar la diferencia entre estos dos aspectos.
[cerrar]

   En el spoiler anterior no hemos indicado una sutileza importante: Cuando se definen las macros CHAR_BIT, SHRT_MIN, etc., del archivo limits.h, se debe hacer con expresiones numéricas constantes. Esto acarrea consecuencias internas que es bueno mencionar en este lugar. Como es un tema muy técnico, va escondido en un nuevo Spoiler:

Tipos de datos de las constantes de limits.h
   Estas expresiones no están obligadas a ser números escritos en decimal o hexadecimal, ni tener uno u otro formato.
   A su vez, el estándar exige que esas expresiones tengan como tipo resultante el mismo tipo de datos del cual habrán de definir el rango de valores.
   Por ejemplo, SHRT_MIN y SHRT_MAX tienen que ser constantes de tipo signed short int.
   Esta exigencia puede cumplirse en algunos casos, pero en otros es imposible, como podría ocurrir con un tipo short int, debido a que no hay sufijos adecuados que fuercen a que una constante decimal sea short, y además las reglas de conversión de tipos promueven directamente a int u otros.
   En este caso, el estándar permitirá que los valores SHRT_MIN, SHRT_MAX promuevan automáticamente a int, y que USHRT_MAX promueva a unsigned int.
   En principio no habría peligro de errores con esto, debido a que el rango de valores de un short int está contenido en el de un int, y análogamente ocurre con unsigned short int respecto unsigned int.
   Esta promoción automática a tipos int podría suponer un problema en contextos donde se espera que aparezca un tipo short. Sin embargo, si el valor es int pero está dentro del rango de valores de un short, y el contexto esperado es short, la conversión a short procedería automáticamente, y seguramente sin errores.

   ¿Qué ocurre con las constantes CHAR_BIT y MB_LEN_MAX?
   Son demasiado pequeñas como para traer inconvenientes. No analizaremos esto aquí.
[cerrar]

   Si ahora miramos el contenido del archivo limits.h seguro se entenderá.



Especificaciones para los tipos de datos enteros adicionales: librería <stdint.h>

   En el post anterior ya hemos estado estudiando los nuevos tipos enteros definidos en stdint.h. Detalles en el Spoiler.

Tipos de enteros definidos en stdint.h
   Ya hemos visto la mayoría de los tipos enteros definidos en stdint.h. Tenemos:
   
  • Tipos enteros de longitud prefijada (ya los hemos estudiamos):

      int8_t   int16_t   int32_t   int64_t 
      uint8_t  uint16_t  uint32_t  uint64_t
     

  • Tipos enteros de longitud mínima prefijada:

      int_least8_t   int_least16_t   int_least32_t   int_least64_t
      uint_least8_t  uint_least16_t  uint_least32_t  uint_least64_t.

  • Tipos enteros "rápidos" de longitud mínima prefijada:

      int_fast8_t    int_fast16_t    int_fast32_t    int_fast64_t
      uint_fast8_t   uint_fast16_t   uint_fast32_t   uint_fast64_t


   Tenemos también los tipos:

   intmax_t y uintmax_t: representan los tipos enteros de longitud máxima (en bits) que el compilador admite, y tienen que abarcar a todos los tipos enteros admitidos o definidos en el sistema. Por ejemplo, deben ser tan grandes como un long long.
   intptr_t y uintptr_t: se refieren a tipos de enteros compatibles con la operación de conversión de punteros a enteros.

   El estándar no obliga a una implementación a definir los tipos intptr_t y uintptr_t. Esos nombres existen para facilitar la portabilidad de los programas. (C99 exige que estos tipos se definan mediante una declaración que use typedef. No explicaremos ahora qué es esto.)  :'(
   Se puede inferir que los tipos intmax_t y uintmax_t, por ser los máximos tipos enteros, tienen que poder abarcar o contener a los tipos intptr_t y uintptr_t.
[cerrar]

   Estudiemos ahora los rangos de valores para los tipos enteros declarados en <stdint.h>. Allí se especifican macros con valores constantes indicando esos rangos. Detalles en el Spoiler:

Macros y rangos para los tipos enteros declarados en stdint.h

   En la siguiente tabla vemos que los valores mínimo y máximo de cada tipo se especifican con macros, las que están definidas en <stdint.h>, y deben cumplir unas condiciones que veremos después.


Tipo de entero --- Mínimo ---------- Máximo

int8_t             INT8_MIN          INT8_MAX
int16_t            INT16_MIN         INT16_MAX
int32_t            INT32_MIN         INT32_MAX
int64_t            INT64_MIN         INT64_MAX
uint8_t            0                 UINT8_MAX
uint16_t           0                 UINT16_MAX
uint32_t           0                 UINT32_MAX
uint64_t           0                 UINT64_MAX

int_least8_t       INT_LEAST8_MIN    INT_LEAST8_MAX
int_least16_t      INT_LEAST16_MIN   INT_LEAST16_MAX
int_least32_t      INT_LEAST32_MIN   INT_LEAST32_MAX
int_least64_t      INT_LEAST64_MIN   INT_LEAST64_MAX
uint_least8_t      0                 UINT_LEAST8_MAX
uint_least16_t     0                 UINT_LEAST16_MAX
uint_least32_t     0                 UINT_LEAST32_MAX
uint_least64_t     0                 UINT_LEAST64_MAX

int_fast8_t        INT_FAST8_MIN     INT_FAST8_MAX
int_fast16_t       INT_FAST16_MIN    INT_FAST16_MAX
int_fast32_t       INT_FAST32_MIN    INT_FAST32_MAX
int_fast64_t       INT_FAST64_MIN    INT_FAST64_MAX
uint_fast8_t       0                 UINT_FAST8_MAX
uint_fast16_t      0                 UINT_FAST16_MAX
uint_fast32_t      0                 UINT_FAST32_MAX
uint_fast64_t      0                 UINT_FAST64_MAX

intmax_t           INTMAX_MIN        INTMAX_MAX
uintmax_t          0                 UINTMAX_MAX

intptr_t           INTPTR_MIN        INTPTR_MAX
uintptr_t          0                 UINTPTR_MAX


Ahora especificamos las condiciones que deben cumplir las macros de la tabla anterior.

INT8_MIN          \( =-2^7 \)
INT8_MAX          \( =2^{7}-1 \)
UINT8_MAX         \( =2^{8}-1 \)
INT16_MIN         \( =-2^{15} \)
INT16_MAX         \( =2^{15}-1 \)
UINT16_MAX        \( =2^{16}-1 \)
INT32_MIN         \( =-2^{31} \)
INT32_MAX         \( =2^{31}-1 \)
UINT32_MAX        \( =-2^{32}-1 \)
INT64_MIN         \( =-2^{63} \)
INT64_MAX         \( =2^{63}-1 \)
UINT64_MAX        \( =2^{63}-1 \)

INT_LEAST8_MIN    \( \leq-2^7 \)
INT_LEAST8_MAX    \( \geq2^{7}-1 \)
UINT_LEAST8_MAX   \( \geq2^{8}-1 \)
INT_LEAST16_MIN   \( \leq-2^{15} \)
INT_LEAST16_MAX   \( \geq2^{15}-1 \)
UINT_LEAST16_MAX  \( \geq2^{16}-1 \)
INT_LEAST32_MIN   \( \leq-2^{31} \)
INT_LEAST32_MAX   \( \geq2^{31}-1 \)
UINT_LEAST32_MAX  \( \geq-2^{32}-1 \)
INT_LEAST64_MIN   \( \leq-2^{63} \)
INT_LEAST64_MAX   \( \geq2^{63}-1 \)
UINT_LEAST64_MAX  \( \geq2^{63}-1 \)

INT_FAST8_MIN     \( \leq-2^7 \)
INT_FAST8_MAX     \( \geq2^{7}-1 \)
UINT_FAST8_MAX    \( \geq2^{8}-1 \)
INT_FAST16_MIN    \( \leq-2^{15} \)
INT_FAST16_MAX    \( \geq2^{15}-1 \)
UINT_FAST16_MAX   \( \geq2^{16}-1 \)
INT_FAST32_MIN    \( \leq-2^{31} \)
INT_FAST32_MAX    \( \geq2^{31}-1 \)
UINT_FAST32_MAX   \( \geq-2^{32}-1 \)
INT_FAST64_MIN    \( \leq-2^{63} \)
INT_FAST64_MAX    \( \geq2^{63}-1 \)
UINT_FAST64_MAX   \( \geq2^{63}-1 \)

INTMAX_MIN        \( \leq-2^{63} \)
INTMAX_MAX        \( \geq2^{63}-1 \)
UINTMAX_MAX       \( \geq2^{63}-1 \)

INTPTR_MIN        \( \leq-2^{15} \)
INTPTR_MAX        \( \geq2^{15}-1 \)
UINTPTR_MAX       \( \geq2^{16}-1 \)

[/tt][/size]

[cerrar]

   Además, en <stdint.h> se definen rangos de valores para tipos enteros definidos en otras librerías.
   Vemos cuáles son en el spoiler:

(Otros tipos enteros)

   En <stddef.h> se declaran los siguientes tipos (también se declaran en otras librerías):

   size_t: Es un entero sin signo. Se utiliza para representar el tamaño en bits que ocupa un tipo de datos determinado. Esto se hace con el operador sizeof(), que explicaremos en otro momento.
   ptrdiff_t: Es un entero con signo. Se usa para representar la diferencia entre las posiciones de memoria de dos punteros de un mismo tipo.
   wchar_t: Se usa para albergar caracteres multibyte.

   En la librería <wchar.h> está definido el siguiente tipo entero:

   wint_t: Se usa para representar enteros que abarcan los caracteres multibyte representables en wchar_t, y al menos un valor adicional fuera de ese conjunto de valores.

   En la librería <signal.h> está definido el siguiente tipo entero:

sig_atomic_t: Se utiliza en el manejo de señales.  ??? ??? ??? Algún día les explicaré esto...

   Un detalle interesente es que un dato de tipo wint_t no se convierte automáticamente a otro tipo entero.
   El tipo wchar_t puede convertirse en wint_t si el contexto requiere un valor más grande... Pero estos detalles serán explicados en otro momento.

   Nota técnica: El estándar C99 no especifica si los tipos wchar_t, wint_t, sig_atomic_t, deben considerarse enteros con o sin signo. Cada compilador, pues, puede tener una representación distinta (es una situación análoga a char).
   En cualquier caso, en <stdint.h> deben figurar los límites para los rangos de valores de estos tipos de enteros. Los detalles en el Spoiler:

Límites para size_t, ptrdiff_t, wchar_t, wint_t, sig_atomic_t


Tipo de entero  ---------  Mínimo  ---------  Máximo

size_t              0                  SIZE_MAX
ptrdiff_t           PTRDIFF_MIN        PTRDIFF_MAX
wchar_t             WCHAR_MIN          WCHAR_MAX
wint_t              WINT_MIN           WINT_MAX
sig_atomic_t        SIG_ATOMIC_MIN     SIG_ATOMIC_MAX


Ahora estudiamos las condiciones que deben cumplir las macros correspondientes.

SIZE_MAX        debe ser \( \geq 2^{16}-1=65535 \).

PTRDIFF_MIN     debe ser \( \leq -(2^{16}-1)=-65535 \).
PTRDIFF_MAX     debe ser \( \geq 2^{16}-1=65535 \).

Si wchar_t se define como un entero con signo, entonces:
WCHAR_MIN       debe ser \( \leq -(2^{7}-1)=-127 \).
WCHAR_MAX       debe ser \( \geq  2^{7}-1 =127 \).

Si wchar_t se define como un entero sin signo, entonces:
WCHAR_MIN       debe ser \( =0 \).
WCHAR_MAX       debe ser \( \geq  2^{8}-1 =255 \).

Si wint_t se define como un entero con signo, entonces:
WINT_MIN        debe ser \( \leq -(2^{15}-1) \).
WINT_MAX        debe ser \( \geq  2^{15}-1  \).

Si wint_t se define como un entero sin signo, entonces:
WINT_MIN        debe ser \( =0 \).
WINT_MAX        debe ser \( \geq  2^{16}-1  \).

Si sig_atomic_t se define como un entero con signo, entonces:
SIG_ATOMIC_MIN  debe ser \( \leq -(2^{7}-1)=-127 \).
SIG_ATOMIC_MAX  debe ser \( \geq  2^{7}-1 =127 \).

Si sig_atomic_t se define como un entero sin signo, entonces:
SIG_ATOMIC_MIN  debe ser \( =0 \).
SIG_ATOMIC_MAX  debe ser \( \geq  2^{8}-1=255 \).


[cerrar]



Cómo forzar hacia los tipos de <stdint.h>

   Si queremos una constante entera de alguno de los tipos enteros definidos en <stdint.h>, no podemos forzar a mano mediante sufijos, como hacíamos con los tipos básicos.
   El C99 especifica que en <stdint.h> hay macros que permiten definir constantes adecuadas para estos tipos de enteros. Sin embargo estas macros no hacen magia, sino que "retocan" un poco las constantes numéricas para que encajen lo mejor posible en los tipos de <stdint.h>.

   Por ejemplo, la macro INT8_C(X) permitiría definir una constante numérica X como de tipo int8_t. Por ahora nos conformarmos con listarlas:

Macros para definir constantes de los tipos declarados en stdint.h

INT8_C(X)
INT16_C(X)
INT32_C(X)
INT64_C(X)
UINT8_C(X)
UINT16_C(X)
UINT32_C(X)
UINT64_C(X)

INT_LEAST8_C(X)
INT_LEAST16_C(X)
INT_LEAST32_C(X)
INT_LEAST64_C(X)
UINT_LEAST8_C(X)
UINT_LEAST16_C(X)
UINT_LEAST32_C(X)
UINT_LEAST64_C(X)

INT_FAST8_C(X)
INT_FAST16_C(X)
INT_FAST32_C(X)
INT_FAST64_C(X)
UINT_FAST8_C(X)
UINT_FAST16_C(X)
UINT_FAST32_C(X)
UINT_FAST64_C(X)

INTMAX_C(X)
UINTMAX_C(X)

[cerrar]



Más sobre tipos booleanos.

   Si bien el lenguaje C tiene definido un tipo booleano, el _Bool (para indicar valores VERDADERO o FALSO), también provee la librería stdbool.h, la cual vuelve a definir un tipo booleano, aunque con otro nombre.

En stdbool.h se definen las macros:

bool
true
false
__bool_true_false_are_defined


   La macro bool equivale a _Bool.
   No es un nuevo tipo de datos, sino otra forma de decir: _Bool.
   La macro true es igual a 1, y false igual a 0.
   La macro __bool_true_false_are_defined vale 1, y se usa para avisar que las 3 macros anteriores están definidas.

   Estas macros permiten programas más legibles. El tipo booleano tiene el feo nombre _Bool en vez de bool, para no colisionar con programas ya existentes, que definen su versión de bool.



Organización

Comentarios y Consultas

05 Enero, 2013, 04:36 pm
Respuesta #10

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
10. Pare de sufrir: Alternativas a la consola CMD

   Hemos dicho ya lo fea e incómodo que es el shell de comandos CMD de DOS/Windows.
   Sin embargo, nos gustaría seguir utilizando una consola así de sencilla, porque es mucho más fácil hacer programas en C que ejecuten directamente en la consola.

   En este post vamos a estudiar la alternativa que ofrecen los emuladores de consolas.
   Son programas que hacen el mismo trabajo que la consola de Windows: ejecutan comandos del sistema, muestran resultados por la entrada/salida estándar de los programas en C, etc.
   La diferencia con la consola usual estriba en que tienen muchas más opciones: se puede configurar el fondo de la ventana con mayor variedad de colores, se puede elegir el tipo de letra entre los que aparecen normalmente en las ventanas de Windows (las TrueType y OpenType), y hasta es posible agregar una imagen como fondo de pantalla.
   Una de las cosas que más nos interesará es justamente esto del fondo de pantalla...

   Vamos a instalar el emulador ConEmu, que significa: Console Emulator. He aquí la guía adecuada, paso a paso, para que nuestros programas C funcionen correctamente desde Dev-Cpp.

  • Vayamos a la página de descarga de ConEmu:
    Página de descarga de ConEmu

  • Nos aparece el enlace de descarga siguiente:

    ConEmuSetup.130827.exe

  • Hacemos clic allí, y descargamos el correspondiente ejecutable.

  • Buscamos el archivo recién descargado en nuestra carpeta de descargas, y lo ejecutamos (haciendo doble clic en él).

  • Se ejecuta entonces el asistente de la instalación del programa.
       Hacer clic en siguiente (o next) para ir aceptando cada una de las opciones.
       (En una de las primeras ventanas nos aparece un acuerdo de licencia. Hacer clic abajo en la cajita blanca o checkbox, para aceptar. Luego proseguir con siguiente o next.)
       Finalmente clic en Install, y cuando termine, en Finish.
  • El programa está instalado. Ahora lo ejecutamos por primera vez (buscarlo en el menú de programas), haciendo clic o doble clic en su ícono.

  • Aparece ahora una ventana de configuración rápida (ConEmu Fast configuration).
       Hacemos clic para que quede marcada la primer casilla, que dice Single instance mode. Esto quiere decir que todos los programas que necesiten consola, se ejecutarán en una sola copia de esta.
       (Evitemos múltiples consolas, que puede ser muy liado).

  • Se abre ahora la consola propiamente dicha, mostrándose en forma muy similar a la consola corriente de DOS/Windows. Hacemos clic en la flechita que está justo al de uno de los íconos superiores a la derecha, el que está en color verde y tiene un signo +.

  • Enseguida aparece una ventana para Settigs (selecciones de opciones del programa). Luego, clic en Main.

  • Allí aprovechamos a configurar cómo lucrirá la consola.
       Por ejemplo, en Font yo seleccoiné Arial Unicode MS con tamaño 16.
       En Antialising seleccioné la opción Clear Type.
       Click 2 veces en Monospace, para desactivar la opción que hace que las letras se amontonen.

  • En Main/Size & Pos ponemos otros valores para Width y Height.

  • Vayamos a Features/Colors. Allí vamos abajo, donde dice Schemes. Abrimos el desplegable y seleccionamos el esquema de colores que más nos guste.
       Yo les recomiendo elegir: <Solarized light>.

  • Ahora vamos Features/Integration, y allí en Force ConEmu as default terminal.
       Activamos esa opción haciendo clic en la cajita blanca. Debajo figura una lista de programas que se supone que son capaces de lanzar una ventana de comandos.
       En principio sólo figura el explorer.exe.
       Agregamos, separado por el signo barra vertical | al programa devcpp.exe, así:

    explorer.exe|devcpp.exe

  • Finalmente, click en Save Settings....  Esto cierra la ventana de configuración.

  • Ahora ejecutamos wxDev-C++ y abrimos nuestro programa HolaMundo.c.
       Aplicamos Compilar y ejecutar sobre él, y veremos cómo se ejecuta en una nueva subventana de ConEmu.
    Tras terminar el programa, presionamos ENTER, y se cierra dicha subventana.

   Normalmente, en nuestros programas poníamos un "freno", como el comando getchar(), antes del final, para poder visualizar los resultados del programa antes de que se cierre la ventana de comandos.
   Sin embargo, mientras estemos usando ConEmu, esto no será necesario, ya que la consola misma se encarga de poner un mensaje avisando de espera, que nos invita a presionar las teclas ESC o ENTER para terminar, y así cerrar la subventana correspondiente.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Ahora vamos a insertar una imagen de fondo en la consola ConEmu. :)
   Es posible insertar archivos de imágenes de cualquier formato, pero estamos particularmente interesados en los mapas de bits: BMP.
   Así que vamos a crear una imange BMP sencilla, y la vamos a cargar en la consola ConEmu como fondo de pantalla.
   Hagamos lo siguiente:

  • Abrir el programa de dibujo que tengamos más a mano, como por ejemplo el Paint.

  • En el menú Imagen/Atributos..., seleccionar Ancho=500, Alto=500, teniendo cuidado de seleccionar Píxeles.
       En la opción Colores, seleccionar Colores (y no Blanco y Negro).
       Esto nos dará, obviamente, un recuadro de imagen de 512x512 píxeles para poner nuestro dibujo.
       Esta medida que les sugiero (fuertemente), es para que no ocupe toda la ventana de ConEmu, sino sólo una pequeña porción manejable, distinguible del resto.

  • Pintemos el fondo del dibujo con un color suave (un gris pálido, o el que prefieran).

  • Hagamos un óvalo o círculo en el centro, relleno con color blanco.

  • En el centro del círculo pongamos una letra C bien grande, en un color oscuro (a mí me gusta el azul oscuro).
       Esto es para hacer honor al lenguaje C, que estamos estudiando.

  • Guardémoslo con el nombre outputc.bmp.
       Procuremos guardarlo en la misma carpeta en que tenemos nuestros documentos, o bien donde tenemos nuestros proyectos C, para que podamos encontrarlo fácilmente.

  • Ahora vamos de nuevo a la ventana de comandos de ConEmu, al menú Setup Tasks... (que aparece tras abrir la flechita al lado del iconito verde arriba a la derecha de todo).
       Allí vamos a la opción Main. En el panel a la derecha marcamos la casilla que dice Background image (bmp, jpg, png).
       Abajo a la derecha hacemos clic en los puntos suspensivos, y buscamos nuestro dibujo recién hecho, outputc.bmp.
       Importante: Este dibujo no debemos cambiarlo de lugar, porque si no, ConEmu no lo encontrará.

  • Debajo a la derecha, en donde dice Placement, abrimos el desplegable y elegimos la opción UpRight.
       Esto nos posicionará la imagen en el extremo superior derecho de la consola ConEmu.
  • Finalmente hacemos clic de nuevo en Save settings, abajo a la derecha.


   Veremos que nuestro dibujo aparece en la consola, y justo donde acabamos de decir: abajo a la derecha.
   Si hacemos modificaciones a nuestro dibujo veremos que, tras guardarlas en Paint, automáticamente se actualizan los cambios en la imagen mostrada en consola.

   En la consola común y corriente de Windows no podíamos darnos el lujo de poner imágenes de fondo como acá.
   Ahora en ConEmu sí podemos hacerlo.
   Pero lo más importante es que usaremos esa imagen en algunos de nuestros programas...
   Por eso he insistido con ella. ;)



CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

08 Enero, 2013, 01:07 pm
Respuesta #11

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
11. Configuración de opciones del Compilador. Compatibilidad con C99.

(Actualizado: 20/Diciembre/2013)

   Hay muchísimas opciones que configuran el comportamiento del compilador.
   Aquí vamos a configurar el IDE para que las cosas nos funcionen lo mejor posible, de acuerdo al ya famoso estándar C99 que hemos elegido.

   La opción -std=c99 es la que adopta las especificaciones del estándar C99, lo mejor que le es posible al compilador GCC en su actual versión (que en diciembre de 2013 es la 4.8.2).
   Esto en teoría tendría que configurar nuestra distribución MinGW para que funcione con el estándar C99.

   Para esto, en nuestro IDE wxDevC++ vamos al menú Herramientos, clic en Opciones del Compilador.
   Allí observamos en la pestaña Compilador, en la sección Paquete de Compilador a configurar, que nuestro paquete seleccionado es MinGW.
   Si no lo fuera, entonces abrimos el desplegable y buscamos MinGW. Lo seleccionamos.

   Ahora sigamos los pasos explicados en la siguiente imagen, y comentados más abajo.


Paso 1. Una vez que eso está listo, hacemos clic en el botón que se encuentra a la derecha, que sirve para reinicializar todas las opciones del compilador a su estado original, tal como cuando hicimos la instalación del IDE.

Cada vez que hayamos jugado demasiado con las opciones del compilador, podemos restaurarlo al estado original mediante ese botón.

Paso 2. Nos va a saltar inmediatamente un cartel pidiendo confirmación. Hacemos clic en Yes o .

Paso 3. Nos fijamos en la misma pestaña, en la sección Compiler Command Line, en la primer casilla, que dice Añadir los siguientes comandos cuando llama al compilador. Allí escribimos nuestra opción preferida:
   
-std=c99

Paso 4. Listo. Ahora terminamos haciendo clic en Aceptar.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   He podido comprobar que con esta opción funcionan bien los tipos enteros y sus rangos, tal como se espera con el C99.
   De cualquier manera, recordemos que MinGW y su compilador GCC, si bien han hecho un buen esfuerzo por adaptarse al C99, están lejos de lograr una compatibilidad completa.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Aunque la opción -std=c99 esté seleccionada, puede que el compilador GCC todavía haya elegido mantener algunas características propias que no son estrictamente correctas en el estándar C99.
   Los desarrolladores de GCC consideran que esas reglas estrictas son pedantes, :P  y por tal motivo, si uno las quiere, tiene que agregar al lado de la opción -std=c99 la opción

-pedantic-errors

   Yo voy a tener configurado mi compilador de esa manera, y para estar todos iguales en este curso, les recomiendo que hagan lo mismo.
   Pero no es esto estrictamente necesario.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Hay una característica especial del estándar C99, la de los caracteres universales, que pueden usarse en identificadores. Este tema será explicado mucho más adelante.
   No obstante, para tener mayor compatibilidad con C99, podemos habilitar una opción del compilador GCC que habilita estos identificadores:

-fextended-identifiers

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

08 Enero, 2013, 07:26 pm
Respuesta #12

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
12. Uso básico de la función printf()

Aún no voy a explicar la función printf(), sino que sólo voy a contar lo mínimo necesario para nuestros propósitos inmediatos.

   1ro. vamos a explicar cómo poner prolijos títulos explicativos a nuestros programas.
   2do. vamos a indicar algunas opciones especiales destinadas a visualizar valores de números enteros.

1. Uso casero de printf(). Concatenación natural de strings.

   Nos interesa tan sólo presentar algunos datos en forma sencilla y rudimentaria en la pantalla.
   Para ello utilizaremos, como hasta ahora, la función printf().
   Como printf() está definida en la librería <stdio.h>, es necesario que la incluyamos en nuestro programa escribiendo:

#include <stdio.h>

   La función printf() escribe una string (cadena de caracteres) en la pantalla (mejor dicho, en la ventana de comandos).
   Como ya hemos visto, una string se indica encerrando sus caracteres entre comillas: "Hola mundo".
   Sin embargo, es posible pegar varias strings, o sea, concatenarlas, simplemente poniéndolas una al lado de la otra: Por ejemplo, el efecto de escribir:

"<Todo junto> se escribe separado,"     " mientras que <separado> se escribe todo junto."   ".....\n\n"

es el mismo resultado que si escribiéramos todo en una sola cadena entrecomillada, así:

"<Todo junto> se escribe separado, mientras que <separado> se escribe todo junto......\n\n"

Un ejemplo más molesto:

"Ho" "la m" "und" "o."

es equivalente a "Hola mundo."

   Por otra parte, debemos utilizar el caracter '\n' cada vez que queramos indicarle a printf() que debe realizar un salto de línea.
   Es decir, si escribimos algo como esto:

"Esto está arriba. "
"Esto también."

El compilador va a entender esto: "Esto está arriba.Esto también".

   Así que, si queremos que realmente haya un salto de línea, bien visible para el usuario, tenemos que colocar un '\n', así:

"Esto está arriba.\n"
"Esto ya no."


   Bueno, el compilador de todos modos lo va a juntar en una sola línea: "Esto está arriba.\nEsto ya no."
   Pero lo importante es que ahora, si ponemos eso como parámetro de la función printf(), cuando corramos el programa vamos a obtener el resultado deseado:

Esto está arriba.
Esto ya no.


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

2. Escribir prolijamente varias líneas de texto con una sola sentencia printf().

   Supongamos que queremos escribir un minitutorial que explique la escala musical en la pantalla.
   Queremos que nuestro programa produzca el siguiente texto, que ponemos en spoiler:

Minitutorial de la escala musical en el pentagrama


             Tutorial básico de música
             ======== ====== == ======
            
Una octava musical occidental está compuesta por 8 notas:

Do, Re, Mi, Fa, Sol, La, Si, Do.

Las notas van subiendo de tonalidad, desde la más grave a la más aguda.
El último Do se encuentra a una distancia tonal de 12 semitonos del primer Do.
La posición de las notas en el pentagrama, en clave de Sol, es como sigue:

------------------------------------------------------------------------------

------------------------------------------------------------------------------
                                                             O  (DO)
---------------------------------------------------O-- (SI) ------------------
                                            O (LA)
----------------------------------O-- (SOL) ----------------------------------
                           O (FA)
------------------O-- (MI) ---------------------------------------------------
           O (RE)  
--O-- (DO)

Hemos dibujado una 'Redonda' (O), que equivale a 4 tiempos de 'Negra',
en cada posición del pentagrama.
Como se puede apreciar, las notas Do y Re caen fuera del pentagrama.
En particular, es necesario escribir un pequeño trazo horizontal, extendiendo
en un renglón hacia abajo el pentagrama, para anotar una nota Do ahí.



[cerrar]

   El primer inconveniente que vamos a encontrar es que las letras con acentos á é í ó ú, con diéresiss ü y las eñes ñ, no se van a ver correctamente, debido a que en la ventana de comandos Windows usa una codificación de caracteres distinta que en el Explorer.

   Una solución parcial a esto sería, por ejemplo, escribir la letra seguida de un apóstrofe: a' e' i' o' u'.
   Sería un entendimiento entre nosotros de que eso "viene a significar" letras acentuadas.
   Para la ñ podemos convenir en reemplazarla por ejemplo con n~, o bien n#.[/color]
   Eso no es lo ideal. Sin embargo, para obtener los caracteres correctamente acentuados, tendríamos que estudiar cuestiones más avanzadas de la codificación de caracteres, así como el uso de caracteres Unicode en C. Esto no es simple, y obviamente lo voy a postergar bastante.
   Una mejor solución sería cambiar la página de códigos como explicamos en la sección 10.

   Otra dificultad aparente es que el minitutorial de música parece respetar ciertas reglas visuales de alineación.
   Para que todo quede bien alineado, tendríamos que poder editar de forma cómoda el texto mientras hacemos el programa en C. Y esto dentro de la función printf().
   Bueno, acá aprovechamos que al lenguaje C no le importa si nosotros decidimos continuar nuestra sentencia en las líneas de más abajo.
   Eso sí, debemos cuidar el entrecomillado, porque C lee una string de corrido en un renglón, y no la continúa en el renglón de abajo. (En realidad esto puede depender del compilador, no sé).
   Por último, para "bajar a la línea siguiente", recordar que SIEMPRE se debe colocar un caracter de salto de línea '\n', porque printf() no puede adivinar que queremos "bajar" al renglón de abajo.  ::)
   Queda así (abrir Spoiler):

Programita Minitutorial de Música
MiniTutorialMusica.c


#include <stdio.h>

int main(void) {
  printf(
    "             Tutorial ba'sico de mu'sica\n"  
    "             ======== ======= == =======\n"
    "\n"
    "Una octava musical occidental esta' compuesta por 8 notas:\n"
    "\n"
    "Do, Re, Mi, Fa, Sol, La, Si, Do.\n"
    "\n"
    "Las notas van subiendo de tonalidad, desde la ma's grave a la ma's aguda.\n"
    "El u'ltimo Do se encuentra a una distancia tonal de 12 semitonos del primer Do.\n"
    "La posicio'n de las notas en el pentagrama, en clave de Sol, es como sigue:\n"
    "\n"
    "------------------------------------------------------------------------------\n"
    "\n"
    "------------------------------------------------------------------------------\n"
    "                                                             O  (DO)\n"
    "---------------------------------------------------O-- (SI) ------------------\n"
    "                                            O (LA)\n"
    "----------------------------------O-- (SOL) ----------------------------------\n"
    "                           O (FA)\n"
    "------------------O-- (MI) ---------------------------------------------------\n"
    "           O (RE)\n"
    "--O-- (DO)\n"
    "\n"
    "Hemos dibujado una 'Redonda' (O), que equivale a 4 tiempos de 'Negra',\n"
    "en cada posicio'n del pentagrama.\n"
    "Como se puede apreciar, las notas Do y Re caen fuera del pentagrama.\n"
    "En particular, es necesario escribir un pequen~o trazo horizontal, extendiendo\n"
    "en un renglo'n hacia abajo el pentagrama, para anotar una nota Do ahi'.\n"
   ); /* Paréntesis que cierra función printf( ... ) */
  
}



[cerrar]

   La gracia de este ejemplo está en que es posible escribir grandes cantidades de texto mediante un sola sentencia printf().
   La edición usada en el programa es natural y cómoda, y refleja fielmente lo que va a salir después en pantalla.

¿Ya logré vendérselos?  >:D

   Obsérverse que aquellas líneas que van completamente en blanco, se han indicado con una string que simplemente dice: "\n", o sea, un salto de línea, sin texto adicional.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

09 Enero, 2013, 09:13 am
Respuesta #13

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
13. Testeando tipos de datos enteros de C (parte I)

Visualización de datos numéricos con printf()

   Para visualizar números enteros con printf() hay varias posibilidades.
   Como siempre, escribiremos una string con una frase informativa, y en el punto exacto donde deseamos que se visualice el dato numérico, colocamos uno de los modificadores siguientes:

%i   adecuado para denotar un (signed) int.
%u   adecuado para denotar un unsigned int.
%ji  adecuado para denotar un intmax_t.
%ju  adecuado para denotar un uintmax_t.

Por ejemplo, si queremos mostrar el resultado de un cálculo sencillo, haríamos así:

printf("%i", 50+11*77-19*4 );

   Como el entero típico en C es un int, hemos usado %i.
   Si trabajamos a conciencia con un dato de tipo unsigned int, podemos usar %u.
   Por ejemplo, podemos convertir rápidamente un número hexadecimal a decimal, así:

printf("%u", 0x5DF4);

   Eso funciona porque el lenguaje C almacena en memoria a los números con un mismo formato, sin importar que nosotros se lo hayamos indicado en base decimal, octal o hexadecimal.
   Así que a C le da lo mismo si ponemos 0x5DF4 o sus equivalentes en otras bases.
   Luego, la opción %u le instruye a printf() que ese número lo muestre en formato decimal.
   Además, le indica que considere el dato como unsigned int.
   Así que si ponemos ahí un número negativo, puede dar resultados sin sentido.

   Ahora bien. Los indicadores de formato %i y %u se mueven en el rango de int y unsigned int.
   Nosotros estamos interesados en poder abarcar números tan grandes como el rango máximo de valores aceptado por el estándar C99.
   A estos les corresponden los tipos de datos intmax_t y uintmax_t.
   Para que printf() muestre correctamente este tipo de números hay que intercalar una j detrás del signo %. Ejemplos:

  printf("%ji",   9000000000000 );
  printf("%ju", 18000000000000u );

   Notemos el uso del sufijo u en el número 18 trillones. Esto es necesario, porque el compilador intenta encajar en un entero de tipo signed a una constante sin sufijos, y el 18 trillones es demasiado grande para encajar. En cambio, si le avisamos con el sufijo u que es unsigned, todo funcionará bien.
   Un último ejemplo: Imaginemos que queremos ver cuál es el valor numérico asociado al caracter '@', hacemos:

printf("%i", '@');

   Mágicamente aparecerá el número 64, código ASCII de '@'.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Macros especiales (sencillas) para mostrar información interna del compilador.

   Ahora quisiéramos mostrar el valor de una de las constantes declaradas en <limits.h>, por ejemplo INT_MAX.
   Pero no nos conformemos con algo tan simple como:

printf("%i", INT_MAX);

   Procuremos lograr algo más informativo. Inclusive, podríamos intentar ver cómo está definida la macro INT_MAX internamente en el archivo <limits.h.

   Recordemos nuestras macros funcionales DEB_(X) y DEB(X), que habíamos definido unos posts atrás.
   La macro DEB_(X) toma el parámetro X, y si X es una macro primero intenta "desenrollar" toda la definición de la misma. Al resultado lo pone entre comillas, para hacer de eso una string.
   Pero DEB_(X) no nos sirve de mucho si X es una macro tipo función.
   La macro DEB(X) primero expande la definición completa del parámetro X (asumiendo que X es una macro), y recién después se le agregan las comillas.

   En cualquier caso, obtenemos una string, que podemos poner "justo al lado" de otras strings, a fin de concatenarlas.
   Por ejemplo, si escribimos:

"Esto es una " DEB(frase entrecomillada) " aunque no lo parezca."

el compilador lo convertirá en esto:

"Esto es una " "frase entrecomillada" " aunque no lo parezca."

   A su vez, por concatenación, eso equivale a esta sola string:

"Esto es una frase entrecomillada aunque no lo parezca."

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   ¿Qué ocurriría si escribimos lo siguiente?

            "....................Tipo: " "signed int"    "\n"
            "..................Limite: " "superior"  "\n"
            "............Nombre macro: " "INT_MAX"      "\n"
            "...............Expande a: " DEB(INT_MAX) "\n"
            "...Lenguaje C interpreta: " "%ji" "\n"
            "\n"


   Si en nuestro sistema INT_MAX está definida en hexadecimal como 0x7FFF, entonces DEB(INT_MAX) es la string "0x7FFF".
   Luego de esto, todas las strings se concatenan, y dan esto:

"....................Tipo: signed int\n..................Limite: superior\n............Nombre macro: INT_MAX\n...............Expande a: 0x7FFF\n...Lenguaje C interpreta: %ji\n\n"

¿Para qué hacer esto, por Dios?  :o :o :o :o

   Bueno, la intención es poner eso en un printf(...).
   Ahora, el modificador %ji nos permitiría colocar ahí un valor int.
   Este valor será, claro está, la constante INT_MAX que estamos estudiando.
   Así, nuestra sentencia completa printf(), con todas las strings de arriba, y con el valor INT_MAX pasado como parámetro, tendrán este aspecto:

       printf(
            "....................Tipo: " "signed int"    "\n"
            "..................Limite: " "superior"  "\n"
            "............Nombre macro: " "INT_MAX"      "\n"
            "...............Expande a: " DEB(INT_MAX) "\n"
            "...Lenguaje C interpreta: " "%ji" "\n"
            "\n", INT_MAX
         );


   Si corremos un programa con esa sentencia, el resultado en pantalla será esto:

....................Tipo: signed int
..................Limite: superior
............Nombre macro: INT_MAX
...............Expande a: 0x7FFF
...Lenguaje C interpreta: 32767


   Primero se ha mostrado el tipo de entero que se pretende analizar.
   Luego se indica cuál es el límite que se analiza, si el inferior o el superior.
   (Si el rango de valores de un tipo es \( r\leq x \leq s \), \( r \) es el límite inferior, y \( s \) el límite superior).
   Por fin viene el nombre de la macro pertinente, en la que está declarado el valor del límite (inferior o superior) asociado al tipo entero analizado.
   Debajo viene la manera en que esa macro ha sido definida en las librerías, ya sea en <limits.h> o <stdint.h>.
   Por último, aparece por fin el valor numérico concreto de la macro estudiada.

   En el ejemplo, el 32767 apareció porque fue insertado en la posición donde se halla %ji en la cadena de formato para el printf().

Aclaración: Seguramente no aparecerá 0x7FFF al correr el programa, ni tampoco 32767. Lo que aparezca ahí depende enteramente de nuestro compilador y nuestro sistema.

   Al parecer, hemos obtenido información concreta e interesante de la macro INT_MAX, y además la hemos mostrado en pantalla de un modo bastante comprensible para el usuario.
   A propósito se ha separado el printf() en 6 líneas, a fin de hacer coincidir más o menos a ojo con las 6 líneas "de verdad" que esperamos obtener al ejecutar el programa.
   Sin embargo, si queremos repetir este proceso para todas las constantes de <limits.h>, o incluso también para las de <stdint.h>, resultaría muy engorroso intentar volver a escribir todos esos comandos printf(), con esas cadenas confusamente escritas.
   Lo que necesitamos urgentemente es una abreviatura clara, y correctamente escrita.
   Esto en C se puede llevar a cabo, claro está, con las famosas macros.
   Trataremos de hacer una macro prolija que nos resuelva el problema.

   Supongamos que nuestra macro se llamará ANALISIS_MACRO, y que tendrá varios parámetros: TIPO, LIMITE, M, FORMATO, los cuales representan esta información:

TIPO:    Es el tipo de entero a analizar. Se provee "a mano".
LIMITE:  Se refiere a si se presentarán datos del límite superior o inferior del tipo entero en cuestión.
M:       Es el nombre de la macro que corresponde al caso de estudio.
FORMATO: Indica si printf() debe asegurar un formato para tipo signed o unsigned.


   Ahora, en vez de signed int, superior, INT_MAX, y %ji, tenemos algo genérico, denotado con los parámetros TIPO, LIMITE, M, FORMATO, así:

El 1er renglón:

           "....................Tipo: " "signed int"    "\n"

tendríamos que escribirlo así:

           "....................Tipo: " TIPO    "\n"

El 2do renglón:

           "..................Limite: " "superior"  "\n"

pasa a ser ahora:

           "..................Limite: " LIMITE  "\n"

El 3er renglón:

           "............Nombre macro: " "INT_MAX"      "\n"

tendríamos que escribirlo así:

           "............Nombre macro: " #M   "\n"

Recordemos que el operador # realiza un "entrecomillado" del parámetro de una macro, convirtiéndolo en una string.

El 4to renglón seria esto:

           "...............Expande a: " DEB(M) "\n"

La 5ta línea es la que tiene el formato %ji.
   Ese formato es adecuado para mostrar enteros signed.
   Pero si queremos enteros unsigned necesitamos %ju.

   A fin de poder elegir en cada caso el formato que más nos convenga, lo hacemos a través del parámetro FORMATO, así:

           "...Lenguaje C interpreta: "  FORMATO    "\n"

   Finalmente, como las strings ocupan varias líneas, tenemos que usar el signo \ para indicarle a la macro ANALISIS_MACRO que la definición es larga, y continúa en la línea siguiente.
   El resultado final será esto:

#define ANALISIS_MACRO(TIPO, LIMITE, M, FORMATO)       \
            "....................Tipo: " TIPO     "\n" \
            "..................Limite: " LIMITE   "\n" \
            "............Nombre macro: " #M       "\n" \
            "...............Expande a: " DEB(M)   "\n" \
            "...Lenguaje C interpreta: " FORMATO  "\n" \
            "\n"

   Ahora, la invocación de la función printf() puede hacerse en forma más concisa y abreviada, usando la macro, así:

printf( ANALISIS_MACRO("signed int", "superior", INT_MAX,  "%ji" ), INT_MAX);

   Uff, eso es más conciso, y basta compilar el código para verificar que realmente hace el trabajo.
   Pero antes vamos a hacer un cambio más.

   Ciertamente, aunque sabemos qué son %ji y %ju, no se ven muy prolijos. De hecho, no resultan muy informativos.

   Si alguien mira nuestro programa de un golpe de vista, no podrá entender el significado de esos jeroglíficos, puestos en ese lugar.
   Se necesita algo más autoexplicativo.

En general, hacer programas autoexplicativos se considera parte de una buena documentación del programa en sí mismo.

   Así que antes definiremos un par de constantes con un nombre significativo:

#define FORMAT_unsigned "%ju"
#define FORMAT_signed   "%ji"

Ahora, la sentencia la haremos así:

printf( ANALISIS_MACRO("signed int", "superior", INT_MAX,  FORMAT_signed ), INT_MAX);

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Quisiera decirles que ya está todo listo para nuestro programa.
   Pero hay un terrible problema.  :-X

printf() va a mostrar erróneamente la mayoría de los números.  >:(  :banghead: :banghead:

   Esto se debe a que los modificadores % de la cadena de formato de printf() son muy quisquillosos, y sólo aseguran buenos resultados si uno pone el tipo de dato justo que espera ahí.
   Por ejemplo, %ji tiene que ser un intmax_t, y no otra cosa, porque no se sabe qué resultado puede dar.

   Para remediar esto hay dos caminos posibles:

(a) Explicar en profundidad los detalles de las cadena de formato de printf().
(b) Convertir todos los números a los mayores tipos enteros admisibles en el sistema, o sea, a intmax_t y uintmax_t.

   La opción (a) nos llevaría mucho tiempo, como para explicarlo en este justo momento.
   La opción (b) requiere introducir un poco de teoría que también corresponde a temas futuros. Pero es más simple.

   Para convertir unos tipos de datos en otros, a la fuerza, según la voluntad del programador, en C se realizan operaciones casting (moldear).
   Sin entrar en muchos detalles, digamos que vamos a utilizar dos castings, uno para el caso de tipos unsigned, y otro para tipos que son signed o que no se sabe a priori si son signed o no (como le ocurre al tipo char).

   Por ejemplo, para SHRT_MAX, que corresponde a signed short int, pondríamos:

((intmax_t) (SHRT_MAX))

   Para que esto no se vuelva demasiado tedioso, pondremos abreviaturas de nuevo, definiendo macros adecuadas, así:

#define SCAST(X) ((intmax_t) (X))
#define UCAST(X) ((uintmax_t) (X))

   Obviamente, la primera macro es para convertir enteros signed, y la 2da para enteros unsigned.

   Ahora, por fin, tenemos el modo definitivo en que vamos a obtener y/o generar información acerca del límite superior del tipo signed int:

printf( ANALISIS_MACRO("signed int", "superior", INT_MAX,  FORMAT_signed ), SCAST(INT_MAX) );

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Creo que la mejor explicación que puedo dar es el programa mismo.
   Basta copiarlo a vuestro IDE y correrlo ahí.
   El programa mismo explicará lo que va haciendo.
   Pueden copiar y pegar desde el siguiente spoiler, o bien descargarlo del archivo adjunto a este post.

Disculpa anticipada:

(1) El archivo es muuuuy monótono, y se hace largo en su monotonía.  :'( :'(
(2) Habrá quienes piensen que el programa TestingIntegers1.c está hecho por un cavernícola. :banghead:  :laugh:
      ¿Podría ser más breve, conciso, hecho con mejores herramientas, y con más inteligencia? La respuesta es sí a todas esas preguntas. Pero este programa tiene la virtud de que se entiende, y sólo utiliza los pocos conocimientos que tenemos hasta ahora.  8^)

Programa para testear rangos de tipos enteros en C


   
/* TestingIntegers1.c   */
/* ==================   */
/*                      */
/* Autor: Argentinator  */
/* 09/Enero/2013        */
/*                      */
/* Curso de C online    */
/*                      */
/* http://rinconmatematico.com/foros/index.php?topic=64835.msg260248#msg260248 */
/* --------------------------------------------------------------------------- */

#include <stdio.h>
#include <limits.h>
#include <stdint.h>

#define DEB_(X) #X
#define DEB(X) DEB_(X)

#define ANALISIS_MACRO(TIPO, LIMITE, M, FORMATO)       \
            "....................Tipo: " TIPO     "\n" \
            "..................Limite: " LIMITE   "\n" \
            "............Nombre macro: " #M       "\n" \
            "...............Expande a: " DEB(M)   "\n" \
            "...Lenguaje C interpreta: " FORMATO  "\n" \
            "\n"
  

#define FORMAT_unsigned "%ju"
#define FORMAT_signed   "%ji"

#define SCAST(X) ((intmax_t) (X))
#define UCAST(X) ((uintmax_t) (X))

int main (void) {
   printf(  "\n"
            "______________________________________________________________\n"
            "\n"
            "Ana'lisis de tipos de datos enteros (unsigned y signed).\n"
            "\n______________________________________________________________\n"
            "\n"
            "1ra. Parte: Tipos ba'sicos unsigned.\n"
            "===========\n"
            "\n"
        );  /* Cerramos paréntesis de printf(...) */
      
  
   printf( ANALISIS_MACRO("unsigned char",          "superior", UCHAR_MAX,  FORMAT_unsigned), UCAST(UCHAR_MAX) );
   printf( ANALISIS_MACRO("unsigned short int",     "superior", USHRT_MAX,  FORMAT_unsigned), UCAST(USHRT_MAX) );
   printf( ANALISIS_MACRO("unsigned int",           "superior", UINT_MAX,   FORMAT_unsigned), UCAST(UINT_MAX) );
   printf( ANALISIS_MACRO("unsigned long int",      "superior", ULONG_MAX,  FORMAT_unsigned), UCAST(ULONG_MAX) );
   printf( ANALISIS_MACRO("unsigned long long int", "superior", ULLONG_MAX, FORMAT_unsigned), UCAST(ULLONG_MAX) );
  
   getchar();
    
   printf(  "\n"
            "2da. Parte: Tipo char.\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("char", "inferior", CHAR_MIN,  FORMAT_signed), SCAST(CHAR_MIN) );
   printf( ANALISIS_MACRO("char", "superior", CHAR_MAX,  FORMAT_signed), SCAST(CHAR_MAX) );
  
   getchar();

   printf(  "\n"
            "3ra. Parte: Tipos ba'sicos signed.\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("signed char",          "inferior", SCHAR_MIN, FORMAT_signed), SCAST(SCHAR_MIN) );
   printf( ANALISIS_MACRO("signed char",          "superior", SCHAR_MAX, FORMAT_signed), SCAST(SCHAR_MAX) );
   printf( ANALISIS_MACRO("signed short int",     "inferior", SHRT_MIN,  FORMAT_signed), SCAST(SHRT_MIN) );
   printf( ANALISIS_MACRO("signed short int",     "superior", SHRT_MAX,  FORMAT_signed), SCAST(SHRT_MAX) );
   printf( ANALISIS_MACRO("signed int",           "inferior", INT_MIN,   FORMAT_signed), SCAST(INT_MIN) );
   printf( ANALISIS_MACRO("signed int",           "superior", INT_MAX,   FORMAT_signed), SCAST(INT_MAX) );
   printf( ANALISIS_MACRO("signed long int",      "inferior", LONG_MIN,  FORMAT_signed), SCAST(LONG_MIN) );
   printf( ANALISIS_MACRO("signed long int",      "superior", LONG_MAX,  FORMAT_signed), SCAST(LONG_MAX) );
   printf( ANALISIS_MACRO("signed long long int", "inferior", LLONG_MIN, FORMAT_signed), SCAST(LLONG_MIN) );
   printf( ANALISIS_MACRO("signed long long int", "superior", LLONG_MAX, FORMAT_signed), SCAST(LLONG_MAX) );

   getchar();
  
   printf(  "\n"
            "4ta. Parte: Tipos de longitud fija (unsigned).\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("uint8_t",  "superior", UINT8_MAX,  FORMAT_unsigned), UCAST(UINT8_MAX) );
   printf( ANALISIS_MACRO("uint16_t", "superior", UINT16_MAX, FORMAT_unsigned), UCAST(UINT16_MAX) );
   printf( ANALISIS_MACRO("uint32_t", "superior", UINT32_MAX, FORMAT_unsigned), UCAST(UINT32_MAX) );
   printf( ANALISIS_MACRO("uint64_t", "superior", UINT64_MAX, FORMAT_unsigned), UCAST(UINT64_MAX) );
  
   getchar();
  
   printf(  "\n"
            "5ta. Parte: Tipos de longitud fija (signed).\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("int8_t",  "inferior", INT8_MIN,  FORMAT_signed), SCAST(INT8_MIN) );
   printf( ANALISIS_MACRO("int8_t",  "superior", INT8_MAX,  FORMAT_signed), SCAST(INT8_MAX) );
   printf( ANALISIS_MACRO("int16_t", "inferior", INT16_MIN, FORMAT_signed), SCAST(INT16_MIN) );
   printf( ANALISIS_MACRO("int16_t", "superior", INT16_MAX, FORMAT_signed), SCAST(INT16_MAX) );
   printf( ANALISIS_MACRO("int32_t", "inferior", INT32_MIN, FORMAT_signed), SCAST(INT32_MIN) );
   printf( ANALISIS_MACRO("int32_t", "superior", INT32_MAX, FORMAT_signed), SCAST(INT32_MAX) );
   printf( ANALISIS_MACRO("int64_t", "inferior", INT64_MIN, FORMAT_signed), SCAST(INT64_MIN) );
   printf( ANALISIS_MACRO("int64_t", "superior", INT64_MAX, FORMAT_signed), SCAST(INT64_MAX) );
  
   getchar();
  
   printf(  "\n"
            "6ta. Parte: Tipos de longitud mi'nima prefijada (unsigned).\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("uint_least8_t",  "superior", UINT_LEAST8_MAX,  FORMAT_unsigned), UCAST(UINT_LEAST8_MAX) );
   printf( ANALISIS_MACRO("uint_least16_t", "superior", UINT_LEAST16_MAX, FORMAT_unsigned), UCAST(UINT_LEAST16_MAX) );
   printf( ANALISIS_MACRO("uint_least32_t", "superior", UINT_LEAST32_MAX, FORMAT_unsigned), UCAST(UINT_LEAST32_MAX) );
   printf( ANALISIS_MACRO("uint_least64_t", "superior", UINT_LEAST64_MAX, FORMAT_unsigned), UCAST(UINT_LEAST64_MAX) );
  
   getchar();
  
   printf(  "\n"
            "7ma. Parte: Tipos de longitud mi'nima prefijada (signed).\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("int_least8_t",  "inferior", INT_LEAST8_MIN,  FORMAT_signed), SCAST(INT_LEAST8_MIN) );
   printf( ANALISIS_MACRO("int_least8_t",  "superior", INT_LEAST8_MAX,  FORMAT_signed), SCAST(INT_LEAST8_MAX) );
   printf( ANALISIS_MACRO("int_least16_t", "inferior", INT_LEAST16_MIN, FORMAT_signed), SCAST(INT_LEAST16_MIN) );
   printf( ANALISIS_MACRO("int_least16_t", "superior", INT_LEAST16_MAX, FORMAT_signed), SCAST(INT_LEAST16_MAX) );
   printf( ANALISIS_MACRO("int_least32_t", "inferior", INT_LEAST32_MIN, FORMAT_signed), SCAST(INT_LEAST32_MIN) );
   printf( ANALISIS_MACRO("int_least32_t", "superior", INT_LEAST32_MAX, FORMAT_signed), SCAST(INT_LEAST32_MAX) );
   printf( ANALISIS_MACRO("int_least64_t", "inferior", INT_LEAST64_MIN, FORMAT_signed), SCAST(INT_LEAST64_MIN) );
   printf( ANALISIS_MACRO("int_least64_t", "superior", INT_LEAST64_MAX, FORMAT_signed), SCAST(INT_LEAST64_MAX) );
  
   getchar();
  
   printf(  "\n"
            "8va. Parte: Tipos ra'pidos de longitud mi'nima prefijada (unsigned).\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("uint_fast8_t",  "superior", UINT_FAST8_MAX,  FORMAT_unsigned), UCAST(UINT_FAST8_MAX) );
   printf( ANALISIS_MACRO("uint_fast16_t", "superior", UINT_FAST16_MAX, FORMAT_unsigned), UCAST(UINT_FAST16_MAX) );
   printf( ANALISIS_MACRO("uint_fast32_t", "superior", UINT_FAST32_MAX, FORMAT_unsigned), UCAST(UINT_FAST32_MAX) );
   printf( ANALISIS_MACRO("uint_fast64_t", "superior", UINT_FAST64_MAX, FORMAT_unsigned), UCAST(UINT_FAST64_MAX) );
  
   getchar();
  
   printf(  "\n"
            "9na. Parte: Tipos ra'pidos de longitud mi'nima prefijada (signed).\n"
            "===========\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("int_fast8_t",  "inferior", INT_FAST8_MIN,  FORMAT_signed), SCAST(INT_FAST8_MIN) );
   printf( ANALISIS_MACRO("int_fast8_t",  "superior", INT_FAST8_MAX,  FORMAT_signed), SCAST(INT_FAST8_MAX) );
   printf( ANALISIS_MACRO("int_fast16_t", "inferior", INT_FAST16_MIN, FORMAT_signed), SCAST(INT_FAST16_MIN) );
   printf( ANALISIS_MACRO("int_fast16_t", "superior", INT_FAST16_MAX, FORMAT_signed), SCAST(INT_FAST16_MAX) );
   printf( ANALISIS_MACRO("int_fast32_t", "inferior", INT_FAST32_MIN, FORMAT_signed), SCAST(INT_FAST32_MIN) );
   printf( ANALISIS_MACRO("int_fast32_t", "superior", INT_FAST32_MAX, FORMAT_signed), SCAST(INT_FAST32_MAX) );
   printf( ANALISIS_MACRO("int_fast64_t", "inferior", INT_FAST64_MIN, FORMAT_signed), SCAST(INT_FAST64_MIN) );
   printf( ANALISIS_MACRO("int_fast64_t", "superior", INT_FAST64_MAX, FORMAT_signed), SCAST(INT_FAST64_MAX) );
  
   getchar();
  
   printf(  "\n"
            "10ma. Parte: Tipos ma'ximos y asociados a punteros.\n"
            "============\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("uintmax_t", "superior", UINTMAX_MAX, FORMAT_unsigned), UCAST(UINTMAX_MAX) );
   printf( ANALISIS_MACRO("intmax_t",  "inferior", INTMAX_MIN,  FORMAT_signed),   SCAST(INTMAX_MIN) );
   printf( ANALISIS_MACRO("intmax_t",  "superior", INTMAX_MAX,  FORMAT_signed),   SCAST(INTMAX_MAX) );
   printf( ANALISIS_MACRO("uintptr_t", "superior", UINTPTR_MAX, FORMAT_unsigned), UCAST(UINTPTR_MAX) );
   printf( ANALISIS_MACRO("intptr_t",  "inferior", INTPTR_MIN,  FORMAT_signed),   SCAST(INTPTR_MIN) );
   printf( ANALISIS_MACRO("intptr_t",  "superior", INTPTR_MAX,  FORMAT_signed),   SCAST(INTPTR_MAX) );
  
   getchar();
  
   printf(  "\n"
            "11va. Parte: Tipos especiales.\n"
            "============\n"
            "\n"
        );  

   printf( ANALISIS_MACRO("size_t", "superior",       SIZE_MAX,       FORMAT_unsigned), UCAST(SIZE_MAX) );
   printf( ANALISIS_MACRO("ptrdiff_t", "inferior",    PTRDIFF_MIN,    FORMAT_signed),   SCAST(PTRDIFF_MIN) );
   printf( ANALISIS_MACRO("ptrdiff_t", "superior",    PTRDIFF_MAX,    FORMAT_signed),   SCAST(PTRDIFF_MAX) );
   printf( ANALISIS_MACRO("wchar_t", "inferior",      WCHAR_MIN,      FORMAT_signed),   SCAST(WCHAR_MIN) );
   printf( ANALISIS_MACRO("wchar_t", "superior",      WCHAR_MAX,      FORMAT_signed),   SCAST(WCHAR_MAX) );
   printf( ANALISIS_MACRO("wint_t", "inferior",       WINT_MIN,       FORMAT_signed),   SCAST(WINT_MIN) );
   printf( ANALISIS_MACRO("wint_t", "superior",       WINT_MAX,       FORMAT_signed),   SCAST(WINT_MAX) );
   printf( ANALISIS_MACRO("sig_atomic_t", "inferior", SIG_ATOMIC_MIN, FORMAT_signed),   SCAST(SIG_ATOMIC_MIN) );
   printf( ANALISIS_MACRO("sig_atomic_t", "superior", SIG_ATOMIC_MAX, FORMAT_signed),   SCAST(SIG_ATOMIC_MAX) );
  
}


[cerrar]

 ;D


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

10 Enero, 2013, 09:30 pm
Respuesta #14

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
14. Testeando tipos de datos enteros de C (parte II)

   Ahora vamos a hacer un programa que muestre información sobre los rangos de valores que toma cada tipo entero. Queremos que haga esto:

(i)   Por cada tipo de datos entero, mostrar el  rango de valores que toma en nuestra implementación local, o sea, el rango de valores admitido por el compilador en nuestro sistema.
(ii)  Luego mostrar el rango de valores que el estándar exige que siempre esté.
(iii) Finalmente mostrar una línea comparando ambos rangos, a fin de visualizar si nuestro compilador es compatible con el estándar.

Como nos quedará algo extenso, invertiremos más de un post en esto.

   Esta vez voy a preferir otro método de exposición. Voy a partir del resultado que se desea obtener, y a partir de ahí se irá armando el programa.
   De nuevo se usarán macros, esta vez un poco más complejas.
   Lo ideal es usar funciones, pero es algo que todavía no hemos visto en la teoría.
   En cambio, las macros permiten pensar trabajando al modo de una función, aunque de un modo algo tosco, y asumiendo el riesgo de obtener errores difíciles de descifrar:-\
   En particular, esto nos obliga a ser muy cuidadosos  :o :o :o :o :o con el uso de macros, y documentar en detalle cómo las definimos, con qué propósito, y dejar indicado con claridad cómo debe usarse para que funcione bien.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

1. Ejemplo de lo que se quiere lograr. Ejemplo unsigned int. Algunas pautas generales de programación.

En Spoiler:

Abrir para leer

Imaginemos que queremos exhibir la siguiente información del tipo entero unsigned int:

Rango de valores del tipo [unsigned int]:
___Compilador local: 0 <=  x  <= 4294967295.
______Esta'ndar C99: 0 <=  x  <= 65535.
_____Comparar ambos:
                 0 <=  x  <= 65535 <= 4294967295.

\( \bullet \)   En la 1er línea indicamos que vamos a analizar el tipo de datos unsigned int.
\( \bullet \)   En la 2da línea informamos cuál es el rango de valores que unsigned int admite en nuestra implementación local.
\( \bullet \)   La 3era línea informa acerca del rango de valores que, cuando menos, debe estar soportado en todo compilador, para el tipo unsigned int, acorde a las exigencias del estándar C99.
\( \bullet \)   La 4ta línea compara los rangos de ambos tipos: el primer número, 65535, corresponde al exigido por C99, mientras que el segundo número, 4294967295, corresponde al compilador local.
   Con el par de caracteres <= estamos expresando la frase menor o igual.

   Tengamos en cuenta que todo esto es información presentada en pantalla.
   Daría lo mismo si ponemos la frase explícita "menor o igual" en vez de los símbolos "<=".
   Lo importante es que la entienda el usuario que corre el programa.
   Se observa también que la información mostrada es muy escueta.
   La razón es que todavía estamos en la etapa en que nos conformamos con programas caseros para salir del paso.
   Un exceso de información o explicaciones puede resultar engorroso de leer en línea de comandos.  ::)

   Así que aquí tenemos ya una primera lección: hay que sopesar los pros y los contras a la hora de mostrar mucha o poca información, o que los estilos de presentación de los datos sean o no escuetos. ::)

   En general, conviene mantener las cosas breves, porque al usuario le molesta un exceso de palabrerío. >:( A lo sumo, se le puede dar la opción de obtener más información detallada, si así lo desea. :-*
   Incluso esta información puede estar fuera del programa, como en archivos PDF, páginas web, o darte el teléfono del tipo que te puede sacar de dudas.  ;)

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

En la 4ta línea de información además, se comparan los rangos:

                  0 <=  x  <= 65535 <= 4294967295.

   Y parece estar todo bien, ya que se ve claramente que el rango exigido por C99 está comprendido dentro del rango de valores admitidos por nuestro compilador.
   ¿Pero quién se dio cuenta de que estos rangos están correctos? ¿Nosotros o el programa?
   El programa no hizo ninguna verificación sobre esto, sino que nosotros "miramos ahí los númeritos"  :P y nos dimos cuenta de que todo funciona como se esperaba. :laugh:
   Por supuesto que podríamos ahorrarnos tener que mirar eso con nuestros propios ojos, y hacer que el programe tome los números y los compare. Pero esto requiere que estudiemos operadores booleanos y sentencias condicionales, que son temas que aún no abordaremos.  :-X
   De paso, quiero insistir en que se pueden hacer muchas cosas con un lenguaje, aún conociendo pocas herramientas de él.  Un programador diestro es aquel que está familiarizado con todas las posibilidades que le permite llevar a cabo una serie de herramientas del lenguaje de programación que usa.  8^)
   Estamos, pues, tirando de la cuerda, a ver cuánto podemos hacer sin más teoría. Saluden a Satán:  >:D

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Imaginemos una sentencia en C que nos muestre la información del rango de valores para el tipo unsigned int.
   Sería, simplemente, un printf() con strings concatenadas. Algo así:

  "Rango de valores del tipo [unsigned int]:         \n"
  "___Compilador local: 0 <=  x  <= 4294967295.      \n"
  "______Esta'ndar C99: 0 <=  x  <= 65535.           \n"
  "_____Comparar ambos:                              \n"
  "                 0 <=  x  <= 65535 <= 4294967295. \n"
  "\n"
);

   Como de costumbre, a fin de poder usar printf(), de la librería <stdio.h>, en nuestro programa, es necesario escribir esta línea al principio:

#include <stdio.h>

   Notemos que, por cada línea de texto, hemos usado una string, terminada con un salto de línea '\n', y seguimos en la siguiente línea, casi como si quisiéramos imitar el comportamiento real del programa.
   Es la misma técnica que usamos en el post anterior. Usar el printf() así, es más claro, porque representa con mucha mayor fidelidad lo que luego va a verse en pantalla al correr el programa.
   Además, pusimos todos los '\n' en la misma columna, para que se vean más nítidamente. Sin embargo eso puede que no sea siempre una buena idea.
   Por ahora es más claro nuestro printf() así presentado, pero como pronto se nos va arruinar, no podremos conservar a los saltos de línea '\n' bonitamente encolumnados.

[cerrar]

2. Macro para visualizar información de rango de valores de unsigned int. 1era. Versión. Casting Forzado.

En Spoiler:

Desarrollo de la macro PRINTF_INFO_UINT_MAX1

   Hay información que ya tenemos en las librerías acerca del rango de valores de unsigned int.
   La constante UINT_MAX de <limits.h> nos informa del máximo valor admitido para unsigned int en la implementación local.
   A fin de poder utilizar esa constante y otras parecidas, tendremos que poner al principio de nuestro programa la línea:

#include <limits.h>

Hagamos el intento de mostrar los datos mediante una macro:

#define PRINTF_INFO_UINT_MAX1 printf("Rango local admitido para unsigned int: 0 <= x <= %ju\n", UINT_MAX)

Si en nuestro programa escribimos la sentencia:

PRINTF_INFO_UINT_MAX1;

será equivalente a haber escrito el printf() indicado en la macro, y el resultado de la ejecución mostrará en pantalla:

Rango local admitido para unsigned int: 0 <= x <= 4294967295

   Hasta aquí no hay grandes misterios.
   Observemos además el uso del modificador "%ju" en printf(), utilizado para exhibir enteros unsigned, de tamaño arbitrario.
   Esto lo hacemos para abarcar, de una vez por todas, todos los casos posibles, ya que uintmax_t soporta hasta el más grande de todos los valores numéricos de todos los tipos enteros unsigned de la implementación local.
   Además, cuando se hace una conversión de tipos, automática o forzada, de una constante cuyo tipo es cualquiera de los enteros unsigned "hacia" el tipo uintmax_t, la conversión es siempre factible, y además correcta, en el sentido de que se conserva bien el valor numérico, tal como debiera ser.
   A pesar de todo esto, no quiere decir que necesariamente todo funcione bien cuando queramos exhibir con printf() constantes enteras cualesquiera (en este caso sólo enteros no signados).

   Para que printf() reciba y muestre enteros uintmax_t, requiere la especificación de conversión "%ju" en el punto exacto de la string (que se llama cadena de formato).

   El tipo de enteros que se espera con "%ju" es un uintmax_t.
   Vamos a suponer que nos restringimos por ahora a tipos unsigned solamente. ¿Puede haber algún error al intentar mostrar valores de tipos enteros de menor longitud?
   En ese caso hay que preguntarse si printf() interpretará y mostrará correctamente los valores numéricos.
   He releído con cuidado el documento sobre el estándar, y no parece claro al respecto. Lo único que puede sacarse en limpio es que el comportamiento de printf() queda indefinido, es decir, no especificado por el estándar. Así pues, si printf() funciona respetando los valores numéricos, como esperamos que haga, depende del compilador que nos toque en suerte.
   (En realidad hay razones más profundas, relativas a la lista de argumentos de printf(), cosa que analizaremos algún día).

   A fin de evitar riesgos en este sentido, y poder transportar nuestro programa a cualquier sistema, tendremos que tomar la precaución de hacer un casting forzado, tal como en el post anterior, mediante: ((uintmax_t)(UINT_MAX)).
   No obstante, el tipo entero uintmax_t no está disponible directamente, sino que se define en la biblioteca <stdint.h>. Así que al principio del programa debemos agregar la línea:

#include <stdint.h>

   Nuestra macro quedaría ahora así (en rojo marcamos los cambios):

#define PRINTF_INFO_UINT_MAX1 printf("Rango local admitido para unsigned int: 0 <= x <= %ju\n", ((uintmax_t)(UINT_MAX)) )

   Vemos también que es demasiado largo todo eso, para ponerlo en un solo renglón.
   Así que vamos a quebrar la declaración #define en varias líneas, mediante el símbolo \, el cual sirve, recordemos, para indicar que la definición de la macro continúa en la línea siguiente:

#define PRINTF_INFO_UINT_MAX1 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n",  \
      ((uintmax_t)(UINT_MAX))  \
   ) /* Cierre de printf( ... ) */

   Notemos que hemos puesto un paréntesis solitario para que se vea claramente dónde "termina" lo que corresponde al printf(). Por las dudas, hemos puesto ahí también un comentario indicando que ahí termina printf(), para que esto sea más visible dentro del programa.
[cerrar]

3. Macro para visualizar información del rango de valores de tipos enteros unsigned. 1era. versión.

En Spoiler:

Desarrollo de macro PRINTF_INFO_U1 (que tiene 2 parámetros)

   Como queremos mostrar esta información para todos los tipos enteros unsigned, tenemos que poner dos parámetros ahí: uno que indique el nombre del tipo, y otro que indique su máximo admitido por la implementación local. Nuestra macro quedaría ahora así:

#define PRINTF_INFO_U1(TIPO, MAX) \
   printf(  \
      "Rango local admitido para " TIPO ": 0 <= x <= %ju\n",  \
      ((uintmax_t)(MAX))  \
   ) /* Cierre de print( ... ) */

   El nombre de la macro ha cambiado a PRINTF_INFO_U1, algo más genérico, porque ahora nos dará información sobre el tipo de entero que le indiquemos.
   La U al final del nombre es por "UNSIGNED", ya que pretendemos usar esta macro sólo para tipos de enteros unsigned, por ahora.
   Tiene dos parámetros, TIPO y MAX.
   El parámetro TIPO representa un tipo entero, que nosotros le indicaremos.
   Fijémonos que, para que ese parámetre funcione correctamente, tiene que ser una string correctamente entrecomillada, ya que se encuentra entre dos strings, las cuales el compilador intentará concatenar.

   Aquí hay dos soluciones posibles. O bien forzamos el entrecomillado dentro de la macro misma, con el operador #, o bien documentamos la macro (con comentarios) y ponemos un aviso de que el parámetro TIPO requiere que le pasemos una string adecuadamente entrecomillada.
   A mí me parece más consecuente la 2da de estas posibilidades. Para entender por qué, analicemos el aspecto de estas dos posibles formas de usar la macro:

PRINTF_INFO_U1( unsigned int, UINT_MAX);
PRINTF_INFO_U1("unsigned int", UINT_MAX);


   En la 1ra forma, se pasa como argumento unsigned int, que no se sabe lo que es.
   A simple vista parece que estuviéramos pasando como parámetro ¡un tipo de datos!
   Pero en realidad la intención es que ese parámetro funcione como una string.
   En cambio, la 2da forma me parece más clara a la vista. No hay dudas de que eso es una string, y que si la macro va a mostrar ese dato, lo que mostrará será una string.
   No se confunde con ninguna otra cosa, y así sabemos que podemos usar la macro sin peligro de que su resultado final se convierta en algo extraño, impredecible.

   Hemos elegido la claridad 8^) por encima del formalismo absoluto  (que implicaría definir las cosas a prueba de errores de todo tipo, que no es fácil de lograr con las macros).

   En cuanto al parámetro MAX, la macro espera que allí aparezca una cierta constante, o una macro equivalente a una constante. De hecho, para que funcione correctamente, tiene que ser algún entero de los unsigned.
   Ahora, para volver a obtener la información relativa al tipo unsigned int, tendríamos que usar esa macro así:

PRINTF_INFO_U1("unsigned int", UINT_MAX);

[cerrar]

4. Macro exclusiva para el tipo unsigned int. 2da. versión.

En Spoiler:

Desarrollo de macro PRINTF_INFO_UINT_MAX2

   Agreguemos la información del rango de valores exigido por el estándar.
   Si bien a la constante UINT_MAX pudimos acceder, porque está en las librerías de C, resulta que el valor \( 2^{16}-1 \) (o sea 65535), que es el menor de los valores que el estándar exige para UINT_MAX, no es algo que pueda accederse a través de constantes en una librería de C::)  Esa información está solamente escrita en documentos, archivada en los cajones de las oficinas de ISO, en sobres lacrados y rubricados por las autoridades ¿competentes? del caso.  :-*
   ¿Cómo mostrar esa información en nuestro programa?
   La única solución es escribirla a mano.  :P

   Primero modifiquemos la macro PRINTF_INFO_UINT_MAX1:

#define PRINTF_INFO_UINT_MAX2 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= 65535\n",  \
      ((uintmax_t)(UINT_MAX))  \
   ) /* Cierre de printf( ... ) */

   Hemos agregado una 2da línea de información, acerca el rango del estándar.
   Y como se ve, pusimos "a mano" el valor 65535, como un pieza de información dentro de una string.
   El printf() ni se enteró de que ahí había un valor numérico que nos interesaba.

[cerrar]


5. Macro exclusiva para el tipo unsigned int. 3ra. versión.

En Spoiler:


Desarrollo de macro PRINTF_INFO_UINT_MAX3

   Si quisiéramos generalizar esto para aplicarlo a todos los tipos enteros, tendríamos que dejar que la macro acepte como parámetro el valor 65535.
   O sea que no puede figurar ahí en forma fija, puesta a mano, sino que mejor dejamos el "hueco" para poder poner un número cualquiera.
   Vamos a poner en ese lugar un especificador de conversión "%ju", lo cual reclamará que agreguemos un nuevo entero de tipo uintmax_t a la lista de argumentos de la función printf().
   Así que volvemos a redefinir la macro, así:

#define PRINTF_INFO_UINT_MAX3 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el estandar C99:     0 <= x <= %ju\n",  \
      ((uintmax_t)(UINT_MAX)), ((uintmax_t)(65535))   \
   ) /* Cierre de printf( ... ) */

   ¿Por qué no pusimos el número 65535 directamente, sino que usamos el cast forzado (uintmax_t)(65535)?
   Bueno, les dejo como ejercicio ver lo que pasa si no se usa el cast.
   Probando diferentes opciones, resulta que el printf() no me funcionó correctamente en este caso.
   Y el compilador no ha cometido pecado en esto, porque el estándar no obliga a nada, como ya hemos visto con este "asunto" del especificador "%ju".
   Sólo con el cast forzado tendremos la seguridad de que las cosas encajarán bien.

[cerrar]


6. Macro exclusiva para el tipo unsigned int. 4ta. versión. Macro para hacer casting forzado.

En Spoiler:

Macro PRINTF_INFO_UINT_MAX4. Macro de cast forzado U_CAST

   Ya vemos que en una misma sentencia repetimos dos veces el cast forzado.
   Dado que es una operación que se nos está haciendo muy común, quizás convenga abreviarla, por ejemplo mediante una macro (igual que en el post anterior).
   La ventaja de usar una macro, es que nos aseguramos de hacer la definición bien una única vez, y el resto de las veces sólo invocamos el nombre de la macro, y tendremos una declaración uniforme, sin errores que de otro modo surgen involuntariamente en el tipeo de un programa.

#define U_CAST(X)  ((uintmax_t)(X))

Volvemos a redefinir nuestra macro, ahora usando U_CAST, así:

#define PRINTF_INFO_UINT_MAX4 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= %ju\n",  \
      U_CAST(UINT_MAX), U_CAST(65535)   \
   ) /* Cierre de printf( ... ) */


El modo de usarla es muy sencillo:

PRINTF_INFO_UINT_MAX4;

Eso por sí solo hará el trabajo.

[cerrar]

7. Macro genérica para tipos enteros. 4ta. versión.

En Spoiler:

Macro PRINTF_INFO_U4 (que tiene 3 parámetros)

   Pero no es PRINTF_INFO_UINT_MAX4 la macro la que nos interesa, sino PRINTF_INFO_U1, que tenía parámetros.
   Necesitaremos agregarle como tercer parámetro el valor 65535, que en forma genérica será STD_MAX (que quiere decir standard maximum.

#define PRINTF_INFO_U4(TIPO, MAX, STD_MAX) \
   printf(  \
      "Rango local admitido para " TIPO ": 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99: 0 <= x <= %ju\n",  \
      U_CAST(MAX), U_CAST(STD_MAX)  \
   ) /* Cierre de print( ... ) */


   El modo de usarla con nuestro ejemplo, sería así:

PRINTF_INFO_U4("unsigned int", UINT_MAX, 65535);

   Aquí podemos preguntarnos si está bien poner el número 65535 así no más, o si conviene ponerle un sufijo u que indique unsigned, para que el cast forzado U_CAST(STD_MAX) se haga correctamente.
   Digamos por ahora tan solo que el cast forzado con (uintmax_t) arregla cualquier inconveniente que podamos plantear, siempre que los números sean positivos.
   (El tema de los castings lo estudiaremos más adelante.)

   Pero en caso de que no confiemos mucho en eso todavía, o simplemente nos hayamos olvidado de las reglas del juego, podemos poner los sufijos que queramos al 65535, y asegurarnos que es del tipo correcto. Por ejemplo: 65535ULL nos asegura que será un unsigned long long int.

[cerrar]


8. Macro exclusiva para unsigned int, 5ta. versión. Macro genérica para tipos enteros. 5ta. versión.

En Spoiler:

Macros PRINTF_INFO_UINT_MAX5 y PRINTF_INFO_U5

   Ahora sólo nos falta agregar una línea más, en la cual comparamos los valores MAX y STD_MAX de la macro. Pero esa comparación es meramente visual: la hace el usuario mientras mira la pantalla, si se le da la gana.  :laugh:
   Para el caso exclusivo de unsigned int, definimos la macro:

#define PRINTF_INFO_UINT_MAX5 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= %ju\n"  \
      "Comparacio'n de ambos rangos: 0 <= x <= %ju <= %ju\n",    \
      U_CAST(UINT_MAX), U_CAST(65535), U_CAST(65535), U_CAST(UINT_MAX)   \
   ) /* Cierre de printf( ... ) */

   La 3ra línea tiene dos especificadores de formato "%ju", por lo tanto el printf() requiere dos argumentos más.
   Pero esos argumentos son datos que ya tenemos disponibles.
   El primero de ellos es el 65535, y el segundo es UINT_MAX.
   Van en ese orden ahora, porque el valor 65535 proveído por el estándar C99 tiene que ser menor o igual que UINT_MAX (que contiene el valor definido por la implementación local).
   Llevemos esto a la macro genérica:

#define PRINTF_INFO_U5(TIPO, MAX, STD_MAX) \
   printf(  \
      "Rango local admitido para " TIPO ": 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99: 0 <= x <= %ju\n"  \
      "Comparacio'n de ambos rangos: 0 <= x <= %ju <= %ju\n",    \
      U_CAST(MAX), U_CAST(STD_MAX), U_CAST(STD_MAX), U_CAST(MAX) \
   ) /* Cierre de print( ... ) */
   

   Aquí la 3ra línea es la misma, mientras que la 4ta línea sólo pone los parámetros MAX y STD_MAX en los lugares que antes teníamos constantes concretas.
   El modo de uso es siempre el mismo:

PRINTF_INFO_U5("unsigned int", UINT_MAX, 65535);

[cerrar]

9. Macros para "casi" potencias de 2, y para potencias de 2 "exactas". Sobre el uso de constantes para obtener programas más sólidos.

En Spoiler:

Constantes para "casi" potencias de 2, y para potencias de 2 "exactas"

   Una cosa que podemos advertir acá, es que 65535 no es el único número de ese tipo que vamos a tener, sino que vamos a tener que repetir una y otra vez constantes que son "casi" potencias de 2.
   Para evitar errores involuntarios en la escritura de esas constantes, o incluso la ambigüedad de estarlas escribiendo a veces de un modo u otro distintos, podemos procurarlas mediante identificadores con un nombre específico. Por ejemplo:

#define CASI_2_a_la_16  65535

   ¿Qué ventaja ofrece definir una constante así?
   Si ponemos por error 655355, o 75535, el compilador no nos avisará de nada, porque no sabe que es un error, ya que no sabe lo que estamos tratando de hacer.
   Pero si definimos un identificador, como CASI_2_a_la_16, bastará usarlo para estar seguros de que siempre tendremos el valor correcto de 65535.
   El lector mordaz  >:D dirá que uno también puede equivocarse escribiendo mal el identificador...
   Pero acá es donde yo gano la discusión, porque esta vez el compilador sí avisa que hay un error, y esto nos obliga a escribir el identificador CASI_2_a_la_16 correctamente.  :P :P :P :P
   De la misma manera que en el post anterior, preferimos el método de usar números hexadecimales para especificar constantes de ese tipo. Declaramos las que vamos a usar:

#define CASI_2_a_la_7  0x7fULL
#define CASI_2_a_la_8  0xffULL
#define CASI_2_a_la_15 0x7fffULL
#define CASI_2_a_la_16 0xffffULL
#define CASI_2_a_la_31 0x7fffffffULL
#define CASI_2_a_la_32 0xffffffffULL
#define CASI_2_a_la_63 0x7fffffffffffffffULL
#define CASI_2_a_la_64 0xffffffffffffffffULL

   Los valores numéricos de esas constantes corresponden a:

\( 2^{7}-1, 2^{8}-1, 2^{15}-1, 2^{16}-1, 2^{31}-1, 2^{32}-1, 2^{63}-1, 2^{64}-1. \)

   Por eso llevan el calificativo de "CASI" por delante, ya que son potencias de 2 menos 1, es decir, "casi" potencias de 2.
   También anticipamos que vamos a usar algunas potencias exactas de 2, las siguientes:

#define EXACTO_2_a_la_7  (CASI_2_a_la_7  + 1ULL)
#define EXACTO_2_a_la_15 (CASI_2_a_la_15 + 1ULL)
#define EXACTO_2_a_la_31 (CASI_2_a_la_31 + 1ULL)
#define EXACTO_2_a_la_63 (CASI_2_a_la_63 + 1ULL)

   Corresponden a las potencias exactas de 2 siguientes:

\( 2^{7}, 2^{15}, 2^{31}, 2^{63}. \)

   Se obtuvieron sumando un 1 a las "casi" potencias. Parece un método extraño de obtenerlas, y ustedes pueden elegir otro método de definirlas.
   Pero aquí se requiere mucho cuidado, ya que si deseamos que todas esas constantes sean unsigned, y dado que algunos valores pueden volverse muy grandes, podemos tener problemas con las conversiones automáticas de los tipos enteros.
   Sea cual sea el método elegido para definir esas constantes, ha de ser uno que nos asegure el valor correcto, y en formato unsigned long long int.

[cerrar]

10. Macro genérica para tipos enteros. 6ta. versión.

   Llegamos por fin al final del post. Para entender de qué se habla abajo, no habrá más remedio que ir leyendo los spoilers de las secciones de arriba.
   Vamos a introducir un cambio en las frases del texto que se visualiza en pantalla. Vamos a dejar algo bien escueto, como pretendíamos al principio del post, así:

#define PRINTF_INFO_U6(TIPO, MAX, STD_MAX) \
   printf(  \
      "Rango del tipo [" TIPO "]:\n" \
      "___Compilador local: 0 <= x <= %ju\n"  \
      "______Esta'ndar C99: 0 <= x <= %ju\n"  \
      "_____Comparar ambos: 0 <= x <= %ju <= %ju\n" \
      "\n", U_CAST(MAX), U_CAST(STD_MAX), U_CAST(STD_MAX), U_CAST(MAX) \
   ) /* Cierre de print( ... ) */
 

   Su modo de uso es el de siempre, salvo que ahora utilizamos las constantes "CASI...":

PRINTF_INFO_U6("unsigned int", UINT_MAX, CASI_2_a_la_8);

   El resultado que mostrará el programa será esto:

Rango del tipo [unsigned int]:
___Compilador local: 0 <= x <= 4294967295
______Esta'ndar C99: 0 <= x <= 65535
_____Comparar ambos: 0 <= x <= 65535 <= 4294967295


 ;) ;) ;)

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

(Continuamos en el siguiente post...)

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

10 Enero, 2013, 10:37 pm
Respuesta #15

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
15. Testeando tipos de datos enteros de C (parte III)

Continuamos con la tarea del post anterior.

   La macro que terminamos de construir allí sólo sirve para estudiar los tipos unsigned.
Tenemos que hacer macros que nos permitan obtener resultados correctos con tipos de enteros signed.

11. Consideraciones generales. Macro exclusiva para signed int, 6ta. versión. Macro S_CAST de casting forzado a intmax_t.

En Spoiler:

Macro PRINTF_INFO_SINT6. Macro de casting forzado S_CAST

   Los tipos signed requerirán seguramente una macro distinta, ya que ellos tienen ahora dos extremos que considerar: un mínimo y un máximo.
   Basados en la experiencia de lo que hemos hecho hasta ahora, hagamos una macro exclusivamente para el tipo signed int.
   Primero que nada, el especificador de formato tendrá que ser "%ji", es decir que configuramos printf() para que acepte valores de tipo intmax_t, que es la versión signed de uintmax_t.
   En particular, necesitaremos una macro que haga el casting forzado, esta vez hacia el tipo intmax_t:

#define S_CAST(X)  ((intmax_t)(X))

   Se necesitan dos parámetros adicionales para la macro, uno que indique el mínimo del rango de valores de la implementación local, y otro que indique el mayor de los mínimos que admite el estándar C99. Nuestra macro sería así:

#define PRINTF_INFO_SINT6 \
   printf(  \
      "Rango del tipo [signed int]: \n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: %ji <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= %ji <= x <= %ji <= %ji\n",    \
      S_CAST(INT_MIN), S_CAST(INT_MAX), \
      -S_CAST(CASI_2_a_la_15), S_CAST(CASI_2_a_la_15),\
       S_CAST(INT_MIN), -S_CAST(CASI_2_a_la_15), S_CAST(CASI_2_a_la_15), S_CAST(INT_MAX)  \
   ) /* Cierre de printf( ... ) */


   Observemos que hay 8 especificadores "%ji" en el printf(), y debe haber por lo tanto 8 argumentos de tipo intmax_t para printf().
   Los hemos escrito separados en tres renglones, para mayor claridad.
   El 1er renglón de argumentos muestra con claridad cuáles son los dos argumentos de los primeros dos especificadores "%ji" de la línea que dice "Compilador local".
   El 2do renglón hace lo propio con "Estándar C99",  y finalmente el 3er renglón muestra los restantes 4 argumentos, en el orden correcto en que deben aparecer en la línea "Comparar ambos".

[cerrar]

12. Macro exclusiva para intmax_t, versiones 6ta. y 7ma. Problemas con valores negativos muy grandes.

En Spoiler:


Macros PRINTF_INFO_INTMAX_T6 y PRINTF_INFO_INTMAX_T6

   El enfoque de la macro esconde problemas potenciales con la conversión de tipos enteros, ya que recordemos que tenemos que manejar números grandes, tanto positivos como negativos.
   Para ver a lo que me refiero, intentemos hacer una macro que haga lo mismo, pero para el caso del tipo intmax_t:

#define PRINTF_INFO_INTMAX_T6 \
   printf(  \
      "Rango local admitido para intmax_t: %ji <= x <= %ji\n"  \
      "Rango exigido por el esta'ndar C99: %ji <= x <= %ji\n"  \
      "Comparacio'n de ambos rangos:  %ji <= %ji <= x <= %ji <= %ji\n",    \
      S_CAST(INTMAX_MIN), S_CAST(INTMAX_MAX), \
      -S_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63),\
       S_CAST(INTMAX_MIN), -S_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63), S_CAST(INTMAX_MAX)  \
   ) /* Cierre de printf( ... ) */


   El programa corre, pero el compilador me avisó de un error aritmético.

   El problema aquí es que la constante EXACTO_2_a_la_63 tiene el valor \( 2^{63} \), que es mayor que el máximo valor aceptado por intmax_t en mi implementación local. Ese valor necesita ser declarado como unsigned long long, y de hecho la macro EXACTO_2_a_la_63 lo hace así.
   Pero luego le hemos cambiado el signo anteponiendo un "-" a la izquierda, tratando de convertirlo a un número negativo.
   Las operaciones de conversión de tipos se pueden estar volviendo locas por dentro, sin que lo sepamos.
   Por ejemplo, el estándar no nos asegura que un número negativo tan grande esté aceptado como signed long long int. (Se está contemplando la posibilidad de una representación interna con complemento a 1, que quitaría del rango un valor negativo de \( -2^{63} \)).

   A mí me da un error de tipo Overflow.
   En general sencillamente hay riesgo de que el programa no sea portable, es decir, que en otras plataformas no pueda compilar.

   Para evitar estos riesgos, tendremos que tratar de rodear el problema.
   Primero, notemos que las constantes que provee el compilador, por muy negativas que sean, es seguro que pueden representarse correctamente como del tipo intmax_t, pues por mera definición es el tipo que soporta a todos los tipos enteros signed, junto con sus rangos de valores.
   Sólo un compilador mal hecho nos pondría en una situación errónea aquí.

   Por otro lado, la constante CASI_2_a_la_63, por ejemplo, la hemos proveído nosotros, y es unsigned long long int. Si pudiéramos dejarla trabajar como unsigned, no tendríamos los inconvenientes de que hablamos.
   Además, observemos que los límites del estándar para los "mínimos" de cada tipo entero, son siempre valores negativos. O sea que el signo - bien podríamos ponerlo a mano como un caracter más. Nadie nos obliga a obtenerlo mediante una expresión aritmética.  :-*

   En cambio, los extremos del lado derecho, tanto locales como del estándar, son números positivos, que tenemos la seguridad de que son representables en toda implementación.
   Por ejemplo, el valor estándar positivo más alto al que nos referiremos será \( 2^{63}-1 \), que es nuestra constante CASI_2_a_la_63.
   Así que no tendremos problemas con los extremos del lado derecho.

   La única precaución que tenemos que tomar es la del mayor de los valores mínimos exigido por el estándar, del cual tendremos que poner su signo negativo a mano, e indicar sólo su valor absoluto, y como unsigned.
   En particular esto nos obliga a cambiar la especificación de formato, en los puntos en donde usaremos ese valor, a "%ju".
   En consecuencia el cast para ese valor será U_CAST.

   A continuación mostramos la macro resultante:

#define PRINTF_INFO_INTMAX_T7 \
   printf(  \
      "\t\tRango del tipo [intmax_t]: \n" \
      "\n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: -%ju <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= -%ju <= x <= %ji <= %ji\n",    \
      S_CAST(INTMAX_MIN), S_CAST(INTMAX_MAX), \
      U_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63),\
       S_CAST(INTMAX_MIN), U_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63), S_CAST(INTMAX_MAX)  \
   ) /* Cierre de printf( ... ) */


   El código de esta macro es portable, compila pues sin errores, y ejecuta bien.

   En el primer renglón agregué dos tabuladores "\t\t" para intentar centrar un poco el título, además agregué una línea más en blanco con un "\n" entre el 1er y 2do renglón, por razones estéticas.

[cerrar]


13. Macro genérica para tipos signed, 7ma. versión.

En Spoiler:

Macro PRINTF_INFO_S7

   Ahora tenemos que generalizar a una macro que muestre datos del tipo entero (signed) que queramos.
   Necesitamos especificar una macro que acepte como parámetros una string que va a contener el nombre del tipo de entero signed que nos interesa, luego cuatro constantes numéricas que contengan la información de: el mínimo y el máximo del rango de valores del tipo indicado en la implementación local, el valor absoluto  :o :o :o :o :o del mayor de los mínimos exigido por el estándar C99, y finalmente el menor de los máximos del estándar.
   Especial cuidado ha de tenerse con el valor inferior del estándar, ya que de él deberemos especificar siempre su valor absoluto, por el modo en que programamos la macro. El resultado es esto:

#define PRINTF_INFO_S7(TIPO, MIN, MAX, ABS_STDMIN, STDMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]: \n" \
      "\n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: -%ju <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= -%ju <= x <= %ji <= %ji\n",    \
      S_CAST(MIN), S_CAST(MAX), \
      U_CAST(ABS_STDMIN), S_CAST(STDMAX),\
      S_CAST(MIN), U_CAST(ABS_STDMIN), S_CAST(STDMAX), S_CAST(MAX)  \
   ) /* Cierre de printf( ... ) */

   La S de PRINTF_INFO_S7 es de signed, y el 7 es porque estoy respetando el número de versión que traíamos de las macros anteriores (a macros "paralelas" les he puesto el mismo número de versión).
   Hagamos un experimento usándola con esta sentencia:

PRINTF_INFO_S7("intmax_t", INTMAX_MIN, INTMAX_MAX, EXACTO_2_a_la_63, CASI_2_a_la_63);

El resultado mostrado en mi pantalla es correcto:


                Rango del tipo [intmax_t]:

___Compilador local: -9223372036854775808 <= x <= 9223372036854775807
______Esta'ndar C99: -9223372036854775808 <= x <= 9223372036854775807
_____Comparar ambos: -9223372036854775808 <= -9223372036854775808 <= x <= 9223372036854775807 <= 9223372036854775807




   Sin embargo no caben todos esos números en la pantalla. Se puede arreglar la presentación, pero no lo voy a hacer, ya es demasiado engorro de programación hasta aquí.
[cerrar]

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

14. El caso de los tipos enteros "mixtos".

   Tenemos finalmente el caso de tipos de datos ambiguos como char, que el estándar no avisa si son signed o unsigned.

Detalles en el Spoiler:

Macro PRINTF_INFO_M7

   Usando nuestra experiencia con los casos unsigned y signed, podemos crear otra macro que trate este caso. Tendremos que poner especial cuidado de dónde y por qué poner especificadores "%ju" y "%ji".
   La mostramos aquí, sin más explicaciones, pues se entenderá:

#define PRINTF_INFO_M7(TIPO, MIN, MAX, STD_UMAX, ABS_STD_SMIN, STD_SMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]: \n" \
      "\n" \
      "___________________Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99 (caso unsigned): 0 <= x <= %ju\n"  \
      "_____________________Comparar ambos: \n" \
      "\n" \
      "\t %ji <= 0 <= x <= %ju <= %ji \n"    \
      "\n" \
      "______Esta'ndar C99 (caso   signed): -%ju <= x <= %ji\n"  \
      "_____________________Comparar ambos: \n" \
      "\n" \
      "\t %ji <= -%ju <= x <= %ji <= %ji\n",    \
      S_CAST(MIN), S_CAST(MAX), \
      U_CAST(STD_UMAX), \
      S_CAST(MIN), U_CAST(STD_UMAX), S_CAST(MAX),  \
      U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX),\
      S_CAST(MIN), U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX), S_CAST(MAX)  \
   ) /* Cierre de printf( ... ) */


   La M es por mixto (ambigüedad entre unsigned y signed).
   Los parámetros TIPO, MIN, MAX, son los de siempre.
   El parámetro STD_UMAX indica el valor superior del rango del estándar, en el caso de que el tipo indicado en TIPO sea unsigned.
   El parámetro ABS_STD_SMIN, se refiere al valor absoluto del valor inferior del estándar para el caso signed, mientras que STD_SMAX es el valor superior del estándar para el caso signed.
   El modo de usarla, con un ejemplo, sería así:

PRINTF_INFO_M7("wint_t", WINT_MIN, WINT_MAX, CASI_2_a_la_16, CASI_2_a_la_15, CASI_2_a_la_15);

El resultado mostrado es éste:


                Rango del tipo [wint_t]:

___________________Compilador local: 0 <= x <= 65535
______Esta'ndar C99 (caso unsigned): 0 <= x <= 65535
_____________________Comparar ambos:

         0 <= 0 <= x <= 65535 <= 65535

______Esta'ndar C99 (caso   signed): -32767 <= x <= 32767
_____________________Comparar ambos:

         0 <= -32767 <= x <= 32767 <= 65535




Nótese que se han hecho arreglos estéticos con ayuda del salto de línea '\n' y el tabulador '\t'.

[cerrar]


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC


15. Testeando todas las macros con un programa real.

   A continuación ponemos todas estas macros juntas en un programa, con sus respectivas sentencias de comprobación.
   Es sólo un programa que testea las macros, no uno que testea los rangos de los tipos enteros.
   Si bien hemos declarado allí la macro problemática PRINTF_INFO_INTMAX_T6, en lugar de "invocarla" hemos puesto un aviso con printf() que dice:
 
"No se muestra: PRINTF_INFO_INTMAX_T6\n\n"

   En particular, como esa macro no se "invoca", para el compilador es como si nunca la hubiéramos declarado, o sea que considera que no existe en nuestro programa.
   Esto muestra a las claras cuánto es que las macros difieren de las funciones.
   Por esa razón, no he obtenido mensajes de ningún tipo del compilador.
   ¡Una macro es sólo una regla de reemplazo de trozos de texto!

Para ver el programa, abrir Spoiler:

Programa TestingIntegerMacros.c

TestingIntegerMacros.c


#include <limits.h>
#include <stdint.h>
#include <stdio.h>

#define CASI_2_a_la_7  0x7fULL
#define CASI_2_a_la_8  0xffULL
#define CASI_2_a_la_15 0x7fffULL
#define CASI_2_a_la_16 0xffffULL
#define CASI_2_a_la_31 0x7fffffffULL
#define CASI_2_a_la_32 0xffffffffULL
#define CASI_2_a_la_63 0x7fffffffffffffffULL
#define CASI_2_a_la_64 0xffffffffffffffffULL

#define EXACTO_2_a_la_7  (CASI_2_a_la_7  + 1ULL)
#define EXACTO_2_a_la_15 (CASI_2_a_la_15 + 1ULL)
#define EXACTO_2_a_la_31 (CASI_2_a_la_31 + 1ULL)
#define EXACTO_2_a_la_63 (CASI_2_a_la_63 + 1ULL)

#define S_CAST(X)  ((intmax_t)(X))
#define U_CAST(X)  ((uintmax_t)(X))

#define PRINTF_INFO_UINT_MAX1 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n",  \
      ((uintmax_t)(UINT_MAX))  \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_UINT_MAX2 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= 65535\n",  \
      ( (uintmax_t)(UINT_MAX) )  \
   ) /* Cierre de printf( ... ) */
   
#define PRINTF_INFO_UINT_MAX3 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= %ju\n",  \
      ( (uintmax_t)(UINT_MAX) ), 65535ull   \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_UINT_MAX4 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= %ju\n",  \
      U_CAST(UINT_MAX), U_CAST(65535)   \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_UINT_MAX5 \
   printf(  \
      "Rango local admitido para unsigned int: 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99:     0 <= x <= %ju\n"  \
      "Comparacio'n de ambos rangos: 0 <= x <= %ju <= %ju\n",    \
      U_CAST(UINT_MAX), U_CAST(65535), U_CAST(65535), U_CAST(UINT_MAX)   \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_SINT6 \
   printf(  \
      "Rango del tipo [signed int]: \n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: %ji <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= %ji <= x <= %ji <= %ji\n",    \
      S_CAST(INT_MIN), S_CAST(INT_MAX), \
      -S_CAST(CASI_2_a_la_15), S_CAST(CASI_2_a_la_15),\
       S_CAST(INT_MIN), -S_CAST(CASI_2_a_la_15), S_CAST(CASI_2_a_la_15), S_CAST(INT_MAX)  \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_INTMAX_T6 \
   printf(  \
      "Rango del tipo [intmax_t]: \n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: %ji <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= %ji <= x <= %ji <= %ji\n",    \
      S_CAST(INTMAX_MIN), S_CAST(INTMAX_MAX), \
      -S_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63),\
       S_CAST(INTMAX_MIN), -S_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63), S_CAST(INTMAX_MAX)  \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_INTMAX_T7 \
   printf(  \
      "\t\tRango del tipo [intmax_t]: \n" \
      "\n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: -%ju <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= -%ju <= x <= %ji <= %ji\n",    \
      S_CAST(INTMAX_MIN), S_CAST(INTMAX_MAX), \
      U_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63),\
       S_CAST(INTMAX_MIN), U_CAST(EXACTO_2_a_la_63), S_CAST(CASI_2_a_la_63), S_CAST(INTMAX_MAX)  \
   ) /* Cierre de printf( ... ) */


#define PRINTF_INFO_U1(TIPO, MAX) \
   printf(  \
      "Rango local admitido para " TIPO ": 0 <= x <= %ju\n",  \
      ((uintmax_t)(MAX))  \
   ) /* Cierre de print( ... ) */
   
#define PRINTF_INFO_U4(TIPO, MAX, STD_MAX) \
   printf(  \
      "Rango local admitido para " TIPO ": 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99: 0 <= x <= %ju\n",  \
      U_CAST(MAX), U_CAST(STD_MAX)  \
   ) /* Cierre de print( ... ) */
   
#define PRINTF_INFO_U5(TIPO, MAX, STD_MAX) \
   printf(  \
      "Rango local admitido para " TIPO ": 0 <= x <= %ju\n"  \
      "Rango exigido por el esta'ndar C99: 0 <= x <= %ju\n"  \
      "Comparacio'n de ambos rangos: 0 <= x <= %ju <= %ju\n",    \
      U_CAST(MAX), U_CAST(STD_MAX), U_CAST(STD_MAX), U_CAST(MAX) \
   ) /* Cierre de print( ... ) */
   
#define PRINTF_INFO_U6(TIPO, MAX, STD_MAX) \
   printf(  \
      "Rango del tipo [" TIPO "]:\n" \
      "___Compilador local: 0 <= x <= %ju\n"  \
      "______Esta'ndar C99: 0 <= x <= %ju\n"  \
      "_____Comparar ambos: 0 <= x <= %ju <= %ju\n" \
      "\n", U_CAST(MAX), U_CAST(STD_MAX), U_CAST(STD_MAX), U_CAST(MAX) \
   ) /* Cierre de print( ... ) */
   
#define PRINTF_INFO_S7(TIPO, MIN, MAX, ABS_STDMIN, STDMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]: \n" \
      "\n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: -%ju <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= -%ju <= x <= %ji <= %ji\n",    \
      S_CAST(MIN), S_CAST(MAX), \
      U_CAST(ABS_STDMIN), S_CAST(STDMAX),\
      S_CAST(MIN), U_CAST(ABS_STDMIN), S_CAST(STDMAX), S_CAST(MAX)  \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_M7(TIPO, MIN, MAX, STD_UMAX, ABS_STD_SMIN, STD_SMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]: \n" \
      "\n" \
      "___________________Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99 (caso unsigned): 0 <= x <= %ju\n"  \
      "_____________________Comparar ambos: \n" \
      "\n" \
      "\t %ji <= 0 <= x <= %ju <= %ji \n"    \
      "\n" \
      "______Esta'ndar C99 (caso   signed): -%ju <= x <= %ji\n"  \
      "_____________________Comparar ambos: \n" \
      "\n" \
      "\t %ji <= -%ju <= x <= %ji <= %ji\n",    \
      S_CAST(MIN), S_CAST(MAX), \
      U_CAST(STD_UMAX), \
      S_CAST(MIN), U_CAST(STD_UMAX), S_CAST(MAX),  \
      U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX),\
      S_CAST(MIN), U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX), S_CAST(MAX)  \
   ) /* Cierre de printf( ... ) */


int main(void) {

    printf("Probando: PRINTF_INFO_UINT_MAX1\n\n");
    PRINTF_INFO_UINT_MAX1;
    getchar();
    printf("Probando: PRINTF_INFO_UINT_MAX2\n\n");
    PRINTF_INFO_UINT_MAX2;
    getchar();
    printf("Probando: PRINTF_INFO_UINT_MAX3\n\n");
    PRINTF_INFO_UINT_MAX3;
    getchar();
    printf("Probando: PRINTF_INFO_UINT_MAX4\n\n");
    PRINTF_INFO_UINT_MAX4;
    getchar();
    printf("Probando: PRINTF_INFO_UINT_MAX5\n\n");
    PRINTF_INFO_UINT_MAX5;
    getchar();
    printf("Probando: PRINTF_INFO_SINT6\n\n");
    PRINTF_INFO_SINT6;
    getchar();
    printf("No se muestra: PRINTF_INFO_INTMAX_T6\n\n");
    getchar();
    printf("Probando: PRINTF_INFO_INTMAX_T7\n\n");
    PRINTF_INFO_INTMAX_T7;
    getchar();
   
    printf("\n_________________________________________\n\n");
   
    printf("Probando: PRINTF_INFO_U1\n\n");
    PRINTF_INFO_U1("unsigned int", UINT_MAX);
    getchar();
    printf("Probando: PRINTF_INFO_U4\n\n");
    PRINTF_INFO_U4("unsigned int", UINT_MAX, CASI_2_a_la_16);
    getchar();
    printf("Probando: PRINTF_INFO_U5\n\n", CASI_2_a_la_16);
    PRINTF_INFO_U5("unsigned int", UINT_MAX, CASI_2_a_la_16);
    getchar();
    printf("Probando: PRINTF_INFO_U6\n\n");
    PRINTF_INFO_U6("unsigned int", UINT_MAX, CASI_2_a_la_16);
    getchar();
    printf("Probando: PRINTF_INFO_S7\n\n");
    PRINTF_INFO_S7("signed int", INT_MIN, INT_MAX, CASI_2_a_la_15, CASI_2_a_la_15);
    getchar();
    printf("Probando: PRINTF_INFO_M7\n\n");
    PRINTF_INFO_M7("wint_t", WINT_MIN, WINT_MAX, CASI_2_a_la_16, CASI_2_a_la_15, CASI_2_a_la_15);
    getchar();
   
}


[cerrar]


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

10 Enero, 2013, 11:31 pm
Respuesta #16

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
16. Testeando tipos de datos enteros de C (parte IV)

   En este post continuaremos y culminaremos el programa de testeo de rangos de tipos enteros que veníamos estudiando.
   Vamos a escribir un programa llamado TetingIntegers2.c.
   Lo único que tenemos que hacer es poner las versiones definitivas de los 3 tipos de macros que hemos ya desarrollado: una para tipos unsigned, una para tipos signed, y una para tipos "mixtos" (aquellos que el estándar no dice a priori si han de ser unsigned o signed).

   Además, a fin de aprovechar estas macros plenamente, las invocaremos una vez por cada uno de los tipos enteros que el estándar C99 exige que existan en una implementación.
   Nuestro programa también mostrará información adicional acerca de cómo deben comportarse los rangos de valores de tipos relacionados.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Sin embargo...

   Nos está faltando algo importante, a la vez que aburridísimo: la documentación.  :o :o :o :o

   Se debe especificar dentro del mismo archivo del programa cómo funcionan las macros.

   Dentro del programa TetingIntegers2.c, pondremos las versiones definitivas de las macros anteriores, y allí las documentaremos.
   La documentación de las macros se ha estructurado de la siguiente manera:

(1ro) Se colocó un comentario que dice: "He aquí las susodichas macros".  :P
(2do) Se colocaron las macros directamente.
(3ro) Debajo se inició un largo comentario que documentará las macros.
(4to) Se vuelve a nombrar las macros con su encabezamiento (nombre y parámetros sólo).
(5to) Se explica para qué fueron diseñadas las macros, y qué es lo que hacen.
(6to) Se explica el modo general de uso y primeras recomendaciones.
(7mo) Se dan ejemplos típicos de uso de cada una.
(8vo) Se muestra una salida típica de la ejecución real de esas macros.
(9no) Se explica en mayor detalle el significado de cada uno de los parámetros.
(10mo) Se indica con exactitud cómo deben usarse los parámetros para que no haya errores.
(11vo) Se explican los pormenores del diseño y las razones de por qué algunos detalles van de una forma y no de otra.
(12vo) Se informa sobre el buen funcionamiento en general de las macros, y los casos excepcionales que pueden producirse.

   De hecho, en el programa mismo se explica un caso excepcional factible, aunque improbable, en que la macro PRINTF_INFO_M puede fallar. Léanlo de ahí.  :-*

   Las macros son las mismas que las de TestingIntegerMacros.c, salvo que se les ha quitado el númerito de la versión, se han retocado los nombres de los parámetros para que coincidan adecuadamente, y se han hecho pequeños retoques estéticos en la salida por pantalla (saltos de línea y tabuladores).

   El programa que sigue analiza todos los tipos enteros estándar (excepto _Bool), y separa la presención en varias partes, con información adicional sobre las reglas que deben cumplir los rangos de valores.
   De los tipos que hemos llamado mixtos, basta conque esté correcto uno de los dos casos unsigned o signed.

   Además, el programa contiene comentarios sobre todas las macros y constantes allí definidas, y sobre su modo de uso. Esto, como parte de la documentación del programa conque tanto insistimos últimamente...

Van a ver que más de la mitad del programa es pura documentación.
Una buena documentación tiene muchas virtudes:

  • Ayuda a quien lee el programa a entender qué hemos hecho, para qué, y cómo.
  • Si alguien quiere usar partes de nuestro programa (macros, funciones, rutinas), tendrá claro cuáles son las especificaciones de las mismas, las limitaciones y las posibilidades.
  • Previenen de errores lógicos.
  • Nos ayudan a recordar qué diablos fue lo que hicimos al confeccionar el programa, cómo y parar qué, y también a recordar cuáles fueron los problemas que tuvimos, los errores que deben evitarse, y demás cuestiones técnicas.
  • Ayudan a ahorrar tiempo, que de otra forma uno malgastaría tratando de descifrar lo que dice el programa, o directamente tirándolo a la papelera para crear una versión nueva, y quizá no tan buena como la original.

Incluso, escribir lo que se supone que debe hacer una macro, una función, o una parte del programa, es mejor que el programa mismo, porque si hemos cometido un error al escribir el programa, esto ahora puede corregirse, ya que tenemos una especificación técnica clara de qué es lo que se supone que tiene que hacer.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

El programa se puede ver en el spoiler, y también está como archivo adjunto a este post, dentro de un archivo comprimido .ZIP.

Programa TestingIntegers2.c

TestingIntegers2.c


/* [[+D]] */

/* --------------------------------------------------------------------------- */
/* TestingIntegers2.c                                                          */
/* ==================                                                          */
/*                                                                             */
/* Autor: Argentinator                                                         */
/* 09/Enero/2013                                                               */
/*                                                                             */
/* Curso de C online                                                           */
/*                                                                             */
/* http://rinconmatematico.com/foros/index.php?topic=64835.msg260248#msg260248 */
/* --------------------------------------------------------------------------- */
/* [[-D]] */

#include <stdio.h>
#include <limits.h>
#include <stdint.h>

/* [[+D]] */
/* Potencias de 2 a las que se les restó 1 unidad. */
/* Tienen tipo unsigned long long int              */

#define CASI_2_a_la_7  0x7fULL
#define CASI_2_a_la_8  0xffULL
#define CASI_2_a_la_15 0x7fffULL
#define CASI_2_a_la_16 0xffffULL
#define CASI_2_a_la_31 0x7fffffffULL
#define CASI_2_a_la_32 0xffffffffULL
#define CASI_2_a_la_63 0x7fffffffffffffffULL
#define CASI_2_a_la_64 0xffffffffffffffffULL

/* Potencias de 2 exactas.  Tienen tipo unsigned long long int */

#define EXACTO_2_a_la_7  (CASI_2_a_la_7  + 1ULL)
#define EXACTO_2_a_la_15 (CASI_2_a_la_15 + 1ULL)
#define EXACTO_2_a_la_31 (CASI_2_a_la_31 + 1ULL)
#define EXACTO_2_a_la_63 (CASI_2_a_la_63 + 1ULL)

/* Macros que hacen cast forzado de su parámetro X a tipos enteros máximos */
/* Se recomienda que se respete la "signatura" del tipo, así: */
/* S_CAST para tipos signed que promocionan a intmax_t */
/* U_CAST para tipos unsigned que promocionan a uintmax_t */

#define S_CAST(X) ((intmax_t) (X))
#define U_CAST(X) ((uintmax_t) (X))

/* Macros para análisis de rangos de valores de tipos enteros (ver abajo) */

#define PRINTF_INFO_U(TIPO, MAX, STD_UMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]:\n" \
      "\n" \
      "___Compilador local: 0 <= x <= %ju\n"  \
      "______Esta'ndar C99: 0 <= x <= %ju\n"  \
      "_____Comparar ambos: 0 <= x <= %ju <= %ju\n" \
      "\n", U_CAST(MAX), U_CAST(STD_UMAX), U_CAST(STD_UMAX), U_CAST(MAX) \
   ) /* Cierre de print( ... ) */
   
#define PRINTF_INFO_S(TIPO, MIN, MAX, ABS_STD_SMIN, STD_SMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]: \n" \
      "\n" \
      "___Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99: -%ju <= x <= %ji\n"  \
      "_____Comparar ambos: %ji <= -%ju <= x <= %ji <= %ji\n"    \
      "\n", \
      S_CAST(MIN), S_CAST(MAX), \
      U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX),\
      S_CAST(MIN), U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX), S_CAST(MAX)  \
   ) /* Cierre de printf( ... ) */

#define PRINTF_INFO_M(TIPO, MIN, MAX, STD_UMAX, ABS_STD_SMIN, STD_SMAX) \
   printf(  \
      "\t\tRango del tipo [" TIPO "]: \n" \
      "\n" \
      "___________________Compilador local: %ji <= x <= %ji\n"  \
      "______Esta'ndar C99 (caso unsigned): 0 <= x <= %ju\n"  \
      "_____________________Comparar ambos: \n" \
      "\n" \
      "\t %ji <= 0 <= x <= %ju <= %ji \n"    \
      "\n" \
      "______Esta'ndar C99 (caso   signed): -%ju <= x <= %ji\n"  \
      "_____________________Comparar ambos: \n" \
      "\n" \
      "\t %ji <= -%ju <= x <= %ji <= %ji\n"   \
      "\n\n", \
      S_CAST(MIN), S_CAST(MAX), \
      U_CAST(STD_UMAX), \
      S_CAST(MIN), U_CAST(STD_UMAX), S_CAST(MAX),  \
      U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX),\
      S_CAST(MIN), U_CAST(ABS_STD_SMIN), S_CAST(STD_SMAX), S_CAST(MAX)  \
   ) /* Cierre de printf( ... ) */

/*

   Las macros
   
   PRINTF_INFO_U(TIPO, MAX, STD_UMAX)
   PRINTF_INFO_S(TIPO, MIN, MAX, ABS_STD_SMIN, STD_SMAX)
   PRINTF_INFO_M(TIPO, MIN, MAX, STD_UMAX, ABS_STD_SMIN, STD_SMAX)
   
    Reemplazan a la función printf(...) con el objetivo de mostrar
    información con un formato y un contenido muy específico,
    acerca de las constantes enteras del lenguaje C.
    Cualquier otro uso puede dar resultados sin sentido,
    errores de compilación y/o errores de ejecución.
   
    Estas macros muestran información del tipo de datos que se informa en
    el parámetro TIPO, indicando los valores mínimos y máximos admitidos por
    la implementación local, y los extremos exigidos por el estándar C99.
    También muestran una comparación entre ambos rangos de valores.
   
    La macro PRINTF_INFO_U está diseñada para el caso de los tipos unsigned.
    La macro PRINTF_INFO_S está diseñada para el caso de los tipos signed.
    La macro PRINTF_INFO_M está diseñada para el caso de los tipos "mixtos",
    es decir, aquellos casos que el estándar C99 no especifica a priori si son
    unsigned o signed (ejemplo: char).
   
    Se recomienda respetar el uso indicado para cada uno.
    En caso contrario, los resultados obtenidos pueden ser incorrectos.
   
    Ejemplo de uso:
    ===============
   
    Supóngase que localmente el rango de signed char es -32768 <= x <= 32767.
    El estándar exige el rango -127 <= x <= 127, cuando menos.
    La macro se usaría así:

    PRINTF_INFO_U("unsigned int", UINT_MAX, CASI_2_a_la_16);
    PRINTF_INFO_S("signed int", INT_MIN, INT_MAX, CASI_2_a_la_15, CASI_2_a_la_15);
    PRINTF_INFO_M("wint_t", WINT_MIN, WINT_MAX, CASI_2_a_la_16, CASI_2_a_la_15, CASI_2_a_la_15);
   
    La ejecución muestra en pantalla:
    =================================
       
    PRINTF_INFO_U:

                Rango del tipo [unsigned int]:
               
    ___Compilador local: 0 <= x <= 4294967295
    ______Esta'ndar C99: 0 <= x <= 65535
    _____Comparar ambos: 0 <= x <= 65535 <= 4294967295


    PRINTF_INFO_S:

                Rango del tipo [signed int]:

    ___Compilador local: -2147483648 <= x <= 2147483647
    ______Esta'ndar C99: -32767 <= x <= 32767
    _____Comparar ambos: -2147483648 <= -32767 <= x <= 32767 <= 2147483647

    PRINTF_INFO_M:

                Rango del tipo [wint_t]:

    ___________________Compilador local: 0 <= x <= 65535
    ______Esta'ndar C99 (caso unsigned): 0 <= x <= 65535
    _____________________Comparar ambos:

             0 <= 0 <= x <= 65535 <= 65535

    ______Esta'ndar C99 (caso   signed): -32767 <= x <= 32767
    _____________________Comparar ambos:

             0 <= -32767 <= x <= 32767 <= 65535

   
    Significado de los parámetros:
    ==============================
   
    TIPO: Representa el nombre del tipo entero que se analizará.
    MIN: Representa el mínimo valor del tipo TIPO en la implementación local.
    MAX: Representa el máximo valor del tipo TIPO en la implementación local.
    STD_UMAX: Representa el "menor de los valores máximos" aceptados por el
              estándar C99, para el caso de enteros unsigned.

    En todas las implementaciones, los valores x del estándar han de
    cumplir la siguiente relación para tipos enteros, cuando son unsigned:
                   
                      0 <= x <= STD_UMAX <= MAX
   
    ABS_STD_SMIN: Representa el VALOR ABSOLUTO del
                  "mayor de los valores mínimos" aceptados por el estándar C99,
                  para el caso de enteros signed.
    STD_SMAX: Representa el "menor de los valores máximos" aceptados por el
              estándar C99, para el caso de enteros signed.

    En todas las implementaciones, los valores x del estándar han de cumplir
    la siguiente relación para tipos enteros, cuando son signed:
                   
                    MIN <= - ABS_STD_SMIN <= x <= STD_SMAX <= MAX


    Especificaciones de los parámetros:
    ===================================

    TIPO: Este parámetro funcionará correctamente si se coloca allí una string.
          Ejemplo: "signed short int".

    MIN: Requiere una constante entera signed para funcionar como se espera.
         
    MAX: Requiere una constante entera POSITIVA para funcionar como se espera.
         Ha de ser unsigned para PRINTF_INFO_U,
         ha de ser sigbed para PRINTF_INFO_S y PRINTF_INFO_M.

    STD_UMAX, ABS_STD_SMIN, STD_SMAX: Requieren una constante entera de tipo
                                      unsigned long long int,
                                      para funcionar como se espera.
                           
    Nota: STD_SNAX es luego forzado a convertirse a signed long long int,
          por lo tanto el valor numérico de STD_MAX tiene que ser POSITIVO
          y no mayor que 0x7FFFFFFFFFFFFFFFull (\( 2^{63} - 1 \))         
                                     
    Si no se respetan los usos mencionados, no hay garantía de que las macros
    funcionen correctamente.
   
    Información sobre el diseño:
    ============================
   
    Las macros indicadas ejecutan una sentencia printf, con una
    cadena de formato en donde se insertarán números enteros.
    A fin de lograr macros generales, y que funcionen en forma uniforme
    manteniendo un estilo sencillo de programación,
    se procedió a utilizar especificadores de formato %ju y %ji.
    Estos especificadores indican que se esperan datos de
    tipos uintmax_t (caso unsigned) e intmaxt_t (caso signed), respectivamente,
    que son los enteros con máximo rango de valores de la implementación local.
   
    Los casos predeciblemente unsigned serán tratados con %ju,
    y los casos predeciblemente signed serán tratados con %ji.
   
    Pudieron ocurrir ambigüedades si es que aparecen enteros negativos de
    valor absoluto muy grande, razón por la cual el extremo inferior estándar
    de los tipos signados se trata aparte, poniendo un signo negativo (-)
    directamente "a mano" en la cadena de formato del printf,
    y además exigiendo que el valor pasado como parámetro sea el VALOR ABSOLUTO
    con tipo unsigned de dicho extremo inferior.   
    En ese caso, un especificador %ju fue necesario.
   
    A fin de asegurar que los argumentos del printf sean correctamente
    interpretados en la cadena de formato, se realizó un cast forzado "hacia"
    los tipos enteros máximos uintmax_t e intmax_t de la implementación local.
    Esto se lleva a cabo con ayuda de las macros U_CAST y S_CAST.
   
    En los parámetros STD_UMAX, ABS_STD_SMIN, STD_SMAX, se recomienda utilizar
    las constantes definidas más arriba, de nombres:
       
        CASI_2_a_la_N, con N = 7, 8, 15, 16, 31, 32, 63, 64.
        EXACTO_2_a_la_N, con N = 7, 15, 31, 63.
       
    (Son todas de de tipos unsigned long long int).
   
    Funcionamiento esperado:
    ========================
   
    Respetando todas las indicaciones anteriores es de esperar que las macros
    funcionen correctamente para todos los tipos enteros que figuran en forma
    explícita en el estándar C99.

    La única excepción a esta regla puede suceder en un caso improbable,
    aunque teóricamente factible, con los enteros "mixtos":
    Estos enteros tienen la intención en el estándar de tomar valores
    relativamente pequeños respecto unsigned/signed long long int.
    Pero si intmax_t tiene 64 bits en la implementación local (en particular
    coincidirá con signed long long int), mientras que uno de los tipos mixtos
    coincide justo con unsigned long long int,
    es casi seguro que fallará la macro PRINTF_INFO_M.
   
    No obstante, dada la ambigüedad de los tipos "mixtos",
    es muy probable que las implementaciones locales elijan darle un tipo,
    ya sea unsigned o signed, que de cualquier manera "quepa" en intmax_t.
   
*/
         
/* [[-D]] */

/* La macro PAUSE, frena la ejecución del programa y espera a que el usuario
   presione la tecla ENTER. Tras esto, el programa continúa.
*/
   
#define PAUSE getchar()

/* Nota: La macro PAUSE es sólo una solución casera. Puede fallar si antes se
         han hecho operaciones de lectura de datos con scanf, por ejemplo.
         Usar con precaución.
         Si no funciona, descomentar el siguiente trozo de código:
*/

/*

    #include <stdlib.h>
   
    #ifdef PAUSE
      #undef PAUSE
    #endif

   #define PAUSE system("PAUSE")
*/

/* Si se desea que el programa no haga pausas
   ni con getchar() ni con system("PAUSE"),
   descomentar el siguiente trozo de código:
*/

/*
    #ifdef PAUSE
      #undef PAUSE
    #endif

    #define PAUSE ;
*/   

     

int main (void) {
   printf( 
     "\n"
     "\n____________________________________________________________________\n"
     "\n"
     "Ana'lisis de rangos de tipos de datos enteros.\n"
     "\n____________________________________________________________________\n"
     "\n"
     "Si alguna comparacio'n entre el rango exigido por el esta'ndar y el\n"
     "rango del presente compilador resultara erro'nea,\n"
     "Por favor avise al creador de su compilador!!!!\n"
     "\n"
     "\n____________________________________________________________________\n"
     "\n\n"           
    );  /* Cerramos paréntesis de printf(...) */

    printf(           
            "1ra. Parte: Tipos ba'sicos (unsigned).\n"
            "===========\n"
            "\n"
        );
       
   PRINTF_INFO_U("unsigned char",          UCHAR_MAX,  CASI_2_a_la_8);   
   PRINTF_INFO_U("unsigned short int",     USHRT_MAX,  CASI_2_a_la_16);
   PRINTF_INFO_U("unsigned int",           UINT_MAX,   CASI_2_a_la_16);
   PRINTF_INFO_U("unsigned long int",      ULONG_MAX,  CASI_2_a_la_16);
   PRINTF_INFO_U("unsigned long long int", ULLONG_MAX, CASI_2_a_la_64);
   
   printf( "Adema's los rangos de los tipos que aparecen primero, deben\n"
           "estar contenidos en los rangos de los tipos que aparecen despues's.\n"
           "\n"
    );
   
   PAUSE;
   
   printf(  "\n"
            "2da. Parte: Tipos ba'sicos (signed).\n"
            "===========\n"
            "\n"
        ); 

   PRINTF_INFO_S("signed char",          SCHAR_MIN, SCHAR_MAX, CASI_2_a_la_7,  CASI_2_a_la_7);
   PRINTF_INFO_S("signed short int",     SHRT_MIN,  SHRT_MAX,  CASI_2_a_la_15, CASI_2_a_la_15);
   PRINTF_INFO_S("signed int",           INT_MIN,   INT_MAX,   CASI_2_a_la_15, CASI_2_a_la_15);
   PRINTF_INFO_S("signed long int",      LONG_MIN,  LONG_MAX,  CASI_2_a_la_15, CASI_2_a_la_15);
   PRINTF_INFO_S("signed long long int", LLONG_MIN, LLONG_MAX, CASI_2_a_la_63, CASI_2_a_la_63);
   
   printf( "Adema's los rangos de los tipos que aparecen primero, deben\n"
           "estar contenidos en los rangos de los tipos que aparecen despues's.\n"
           "\n"
    );

   PAUSE;
   
   printf(  "\n"
            "3ra. Parte: Tipo char.\n"
            "===========\n"
            "\n"
        ); 

   PRINTF_INFO_M("char", CHAR_MIN, CHAR_MAX, CASI_2_a_la_8, CASI_2_a_la_7, CASI_2_a_la_7);
   
   printf( "El rango de char debe coincidir con signed char, si es signed,\n"
           "y con unsigned char, si es unsigned.\n"
           "\n"
        );
   
   PAUSE;

   printf(  "\n"
            "4ta./5ta. Partes: Tipos de longitud fija.\n"
            "=================\n"
            "\n"
        ); 

   PRINTF_INFO_U("uint8_t",  UINT8_MAX,   CASI_2_a_la_8);
   PRINTF_INFO_U("uint16_t", UINT16_MAX,  CASI_2_a_la_16);
   PRINTF_INFO_U("uint32_t", UINT32_MAX,  CASI_2_a_la_32);
   PRINTF_INFO_U("uint64_t", UINT64_MAX,  CASI_2_a_la_64);
   
   PRINTF_INFO_S("int8_t",  INT8_MIN,  INT8_MAX,  EXACTO_2_a_la_7,  CASI_2_a_la_7);
   PRINTF_INFO_S("int16_t", INT16_MIN, INT16_MAX, EXACTO_2_a_la_15, CASI_2_a_la_15);
   PRINTF_INFO_S("int32_t", INT32_MIN, INT32_MAX, EXACTO_2_a_la_31, CASI_2_a_la_31);
   PRINTF_INFO_S("int64_t", INT64_MIN, INT64_MAX, EXACTO_2_a_la_63, CASI_2_a_la_63);
 
   PAUSE;
   
   printf(  "\n"
            "6ta. Parte: Tipos de longitud mi'nima prefijada (unsigned).\n"
            "===========\n"
            "\n"
        ); 

   PRINTF_INFO_U("uint_least8_t",  UINT_LEAST8_MAX,   CASI_2_a_la_8);
   PRINTF_INFO_U("uint_least16_t", UINT_LEAST16_MAX,  CASI_2_a_la_16);
   PRINTF_INFO_U("uint_least32_t", UINT_LEAST32_MAX,  CASI_2_a_la_32);
   PRINTF_INFO_U("uint_least64_t", UINT_LEAST64_MAX,  CASI_2_a_la_64);
   
   printf( "Adema's los rangos de los tipos que aparecen primero, deben\n"
           "estar contenidos en los rangos de los tipos que aparecen despues's.\n"
           "\n"
    );
   
   PAUSE;
   
   printf(  "\n"
            "7ma. Parte: Tipos de longitud mi'nima prefijada (signed).\n"
            "===========\n"
            "\n"
        ); 

   PRINTF_INFO_S("int_least8_t",  INT_LEAST8_MIN,  INT_LEAST8_MAX,  EXACTO_2_a_la_7,  CASI_2_a_la_7);
   PRINTF_INFO_S("int_least16_t", INT_LEAST16_MIN, INT_LEAST16_MAX, EXACTO_2_a_la_15, CASI_2_a_la_15);
   PRINTF_INFO_S("int_least32_t", INT_LEAST32_MIN, INT_LEAST32_MAX, EXACTO_2_a_la_31, CASI_2_a_la_31);
   PRINTF_INFO_S("int_least64_t", INT_LEAST64_MIN, INT_LEAST64_MAX, EXACTO_2_a_la_63, CASI_2_a_la_63);
   
   printf( "Adema's los rangos de los tipos que aparecen primero, deben\n"
           "estar contenidos en los rangos de los tipos que aparecen despues's.\n"
           "\n"
    );
   
   PAUSE;
   
   printf(  "\n"
            "8va. Parte: Tipos ra'pidos de longitud mi'nima prefijada (unsigned).\n"
            "===========\n"
            "\n"
        ); 

   PRINTF_INFO_U("uint_fast8_t",  UINT_FAST8_MAX,  CASI_2_a_la_8);
   PRINTF_INFO_U("uint_fast16_t", UINT_FAST16_MAX, CASI_2_a_la_16);
   PRINTF_INFO_U("uint_fast32_t", UINT_FAST32_MAX, CASI_2_a_la_32);
   PRINTF_INFO_U("uint_fast64_t", UINT_FAST64_MAX, CASI_2_a_la_64);
   
   printf( "Adema's los rangos de los tipos que aparecen primero, deben\n"
           "estar contenidos en los rangos de los tipos que aparecen despues's.\n"
           "\n"
    );
   
   PAUSE;
   
   printf(  "\n"
            "9na. Parte: Tipos ra'pidos de longitud mi'nima prefijada (signed).\n"
            "===========\n"
            "\n"
        ); 

   PRINTF_INFO_S("int_fast8_t",  INT_FAST8_MIN,  INT_FAST8_MAX,  EXACTO_2_a_la_7,  CASI_2_a_la_7);
   PRINTF_INFO_S("int_fast16_t", INT_FAST16_MIN, INT_FAST16_MAX, EXACTO_2_a_la_15, CASI_2_a_la_15);
   PRINTF_INFO_S("int_fast32_t", INT_FAST32_MIN, INT_FAST32_MAX, EXACTO_2_a_la_31, CASI_2_a_la_31);
   PRINTF_INFO_S("int_fast64_t", INT_FAST64_MIN, INT_FAST64_MAX, EXACTO_2_a_la_63, CASI_2_a_la_63);
   
   printf( "Adema's los rangos de los tipos que aparecen primero, deben\n"
           "estar contenidos en los rangos de los tipos que aparecen despues's.\n"
           "\n"
    );
   
   PAUSE;
   
   printf(  "\n"
            "10ma. Parte: Tipos ma'ximos y asociados a punteros.\n"
            "============\n"
            "\n"
        ); 

   PRINTF_INFO_U("uintmax_t", UINTMAX_MAX, CASI_2_a_la_64);
   PRINTF_INFO_S("intmax_t",  INTMAX_MIN, INTMAX_MAX,  EXACTO_2_a_la_63, CASI_2_a_la_63);
   PRINTF_INFO_U("uintptr_t", UINTPTR_MAX, CASI_2_a_la_16);
   PRINTF_INFO_S("intptr_t",  INTPTR_MIN, INTPTR_MAX,  EXACTO_2_a_la_15, CASI_2_a_la_15);
   
   printf( "Adema's los rangos de todos los tipos signados deben encajar en intmax_t,\n"
           "y los rangos de todos los tipos no signados deben encajar en uintmax_t.\n"
           "\n"
    );
   
   PAUSE;
   
   printf(  "\n"
            "11va. Parte: Tipos especiales.\n"
            "============\n"
            "\n"
        ); 

   PRINTF_INFO_U("size_t",    SIZE_MAX, CASI_2_a_la_16);
   PRINTF_INFO_S("ptrdiff_t", PTRDIFF_MIN, PTRDIFF_MAX, CASI_2_a_la_16, CASI_2_a_la_16);
   PRINTF_INFO_M("sig_atomic_t (caso unsigned)", SIG_ATOMIC_MIN, SIG_ATOMIC_MAX, CASI_2_a_la_8, CASI_2_a_la_7, CASI_2_a_la_7);
   PRINTF_INFO_M("wchar_t (caso unsigned)", WCHAR_MIN, WCHAR_MAX, CASI_2_a_la_8,  CASI_2_a_la_7,  CASI_2_a_la_7);
   PRINTF_INFO_M("wint_t (caso unsigned)",  WINT_MIN,  WINT_MAX,  CASI_2_a_la_16, CASI_2_a_la_15, CASI_2_a_la_15);
   
   printf( "Adema's el rango de wchar_t debe encajar en el de wint_t.\n"
           "\n"
    );
   
   PAUSE;

}



[cerrar]


 ;D

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

15 Enero, 2013, 04:56 am
Respuesta #17

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
17. Números de punto flotante. Generalidades

   Antes de abordar el tema de los números reales y complejos en C, necesitamos estudiar el estándar ISO/IEC/IEEE 60559:2011.

   ¿Otro estándar?  :'( :'( :'( ¡Oh, por Dios!  :banghead: :banghead: :banghead:

   Bueno, penas aparte, empecemos.

   Los números en punto flotante (o en coma flotante), en inglés: floating-point, son un tipo especial de números que se definen para uso en computación, que intentan representar el cálculo con números reales.

   De entrada digamos que las computadoras, sin importar cuál sea su arquitectura interna, son incapaces de albergar números cualesquiera con infinitos dígitos detrás del punto de fracción. Esto se debe a varias razones.
   Primero, si los números se albergan o procesan como datos codificados con una cantidad fija o máxima de bits, digamos \( N \), entonces no pueden representarse más que \( N \) dígitos binarios de un número cualquiera de punto flotante.
   Si se intenta sortear esta dificultad usando otros métodos de guardar números en una máquina, se requerirá seguramente un método para memorizar sus dígitos en algún dispositivo de memoria, y en todo momento la cantidad de memoria disponible de una máquina está acotada.

   En particular, habrá números reales que no podrán representarse, y así muchos cálculos darán resultados aproximados por redondeo.
   Al hacer esto, inevitablemente se introducen cálculos que son matemáticamente erróneos, porque ya son aproximaciones y no datos exactos.

   Asumiendo esta problemática, se debe pensar en situaciones de error típicas producidas por estos redondeos y cálculos con pequeños errores.
   Es así que se habla de la precisión de un número de punto flotante.

   Cuanto más dígitos puedan representarse en el conjunto de números de punto flotante de una máquina, se dirá que son más precisos.
   La precisión, pues, se refiere a la cantidad de dígitos que un dato numérico tiene asignado en el interior del sistema (hardware+software) que estemos utilizando.

   Sin embargo creo conveniente de aquí en más usar dos palabras distintas a este respecto, con un significado bien delimitado.
   La palabra precisión se referirá a la cantidad de dígitos significativos que un cierto formato de números es capaz de almacenar, mientras que la palabra exactitud se referirá a qué tan buenos son los valores aproximados a un determinado valor considerado exacto.

   Por otro lado, los números en punto flotante están usualmente estructurados en 3 partes bien especificadas:

Signo (\( \sigma \)), mantisa (\( \mu \)) y exponente (\( r \)).

   Así, un número de punto flotante tendrá la forma: \( \sigma \mu \cdot b^r \), en donde \( b \) representa la base de numeración utilizada (para las computadoras suele ser \( b=2 \), y para los humanos \( b=10 \)).
   La mantisa tiene esta forma: \( n.dddddd... \), en donde queremos expresar que hay un único dígito \( n \), que luego está seguido por un punto fraccionario, y detrás el resto de los dígitos.

   Es la típica notación científica o exponencial. El exponente \( r \) indica entre qué potencias sucesivas de la base se halla el número en cuestión.

   Así, para decir cuántos planetas de tamaño similar a la Tierra hay en la galaxia, lo indicamos con \( 1.7\cdot 10^{10}  \), que quiere decir que hay 10 dígitos aún detrás del 1, antes de llegar a la parte fraccionaria, con lo cual estamos hablando de un número en el orden de los 10 mil millones. (Precisamente, son 17 mil millones los del ejemplo).

   Si queremos expresar cuántos gramos pesa un electrón, decimos que son \( 9.10952\cdot 10^{-28} \), lo cual indica que detrás del punto fraccionario hay 27 ceros antes de llegar a los dígitos 910952 de nuestro número.

   La notación científica permite, pues, que podamos representar números de tamaños muy grandes o muy pequeños.
   El problema aquí es que, si bien la promesa es grande, el alcance de esto no lo es tanto.

   Supongamos que nuestro sistema tiene una precisión de 16 dígitos decimales, y que permite expresar números en notación científica tan grandes como los cincuentillones (del orden de \( 10^{300} \)).

   En tal caso, si bien podemos "hablar" de cantidades grandes en nuestra máquina, no podemos acceder a las 300 cifras que estarían antes del punto fraccionario.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Las leyes de los exponentes nos dan las siguientes reglas para cálculos con estos números:

\( (\mu\cdot  b^r)(\nu\cdot  b^s)=\mu\nu\cdot  b^{r+s} \)
\( (\mu\cdot  b^r)/(\nu\cdot  b^s)=\mu\nu\cdot  b^{r-s} \)
\( (\mu\cdot  b^r)^s=\mu^s\cdot  b^{rs} \)

   Como se ve, los cálculos de productos, cocientes y potencias resultan bastante cómodos en el formato exponencial.
   Se hace difícil, en cambio, establecer relaciones claras entre mantisas y exponentes, al efectuar sumas y restas.

   En cualquier caso, siempre habrá redondeos automáticos efectuados por la máquina, que nos obligarán a ir arrastrando errores.
   Este tipo de errores de imprecisión en los cálculos suelen denominarse errores numéricos. Es un nombre que se refiere al efecto de acumular errores con la computadora, tras una serie de varios cálculos, cada uno con su contribución al desastre.
   Se trata de una realidad de las computadoras, inevitable (al menos a nivel de hardware), y que conviene que reflexionemos sobre ello.

¿Por qué hay que reflexionar sobre esto? Bueno, porque el modo en que el lenguaje C representa los números reales sigue el formato de los números con punto flotante, y adolece de los problemas comunes a estos números.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Hemos visto también que el grado de exactitud en los cálculos con números en punto flotante depende de la cantidad fija de dígitos que se utilicen en su representación.

   La exactitud depende del hardware y del software.
   Cada sistema (combinación de hardware y software) tendrá sus propios números de punto flotante.
   Peor todavía, cada procesador matemático tendrá sus propios criterios sobre cómo hacer redondeos, u otras convenciones sobre los cálculos y el manejo de los errores numéricos.

   En consecuencia, si hacemos un programa que nos da determinados resultados en una máquina, puede que nos dé otros resultados en otra máquina distinta, dependiendo de cuánta precisión y cuáles convenciones tenga cada una.

   Los microprocesadores, corazón de las computadoras, suelen tener adosado un coprocesador matemático que, entre otras cosas, se encarga de realizar los cálculos con números en punto flotante.
   A fin de obligar a los fabricantes a mantener la mayor compatibilidad posible, es que se ha establecido un estándar sobre los números en punto flotante.
   Es el ISO/IEC/IEEE 60559:2011. También se le conoce como estándar IEEE 754. Ambos tienen el mismo contenido.

   Este estándar no se refiere a ningún lenguaje en particular, sino a especificaciones sobre los sistemas (hardware+software) de computación de cálculo matemático.
   A su vez, el estándar del lenguaje C define tipos de datos de punto flotante que procuran respetar al pie de la letra el estándar ISO/IEC/IEEE 60559:2011.
   Es importante pues, para nosotros, conocer los detalles que dan ambos estándares respecto a los números en punto flotante.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Nuestro plan para el futuro es como sigue:

\( \bullet \)   Estudiar el estándar IEEE 754 de formatos de punto flotante, y conocer sus mínimos requerimientos obligatorios y sugerencias.
\( \bullet \)   Estudiar cómo el estándar C99 del lenguaje C impone reglas sobre los tipos de datos floating point, qué relación tienen con el estándar IEEE 754, y qué exigencias adicionales presenta el lenguaje C.
\( \bullet \)   Entender nuestro particular compilador GCC, el cual puede que no respete ni el estándar de IEEE 754, ni el estándar de C99, y que incluso tenga características propias. Debemos analizar los detalles para estar prevenidos.

   De hecho, en la versión actual de GCC, su punto más problemático en relación al estándar C99 es justamente el de los tipos de punto flotante.
   Tendremos que tener la mente abierta para esperar problemas de incompatibilidad, entender sus motivos, y ver las soluciones o alternativas que hay a mano.
   Este ciclo de 3 etapas se va a repetir varias veces, cada vez que tengamos que estudiar un nuevo tópico sobre los números de punto flotante.

   También tendremos ocasión de estudiar los detalles matemáticos de los cálculos en punto flotante, y analizaremos los detalles finos de las operaciones aritméticas con esos números.
   El tema de los números en punto flotante se ve, pues, que es arduo de manejar con absoluta exactitud.
   No tienen la sencillez de los números enteros, y hay involucrados temas profundos de las ciencias de la computación].

   Pero todo a su debido tiempo. No se extrañen que vaya dejando algunos temas postergados.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

La bibliografía que consultaremos para este tema es la siguiente:

\( \bullet \)  IEEE Standard for Binary Floating-Point Arithmetic

   La versión de 1985 que contempla el caso de los formatos binarios, lo incluimos como adjunto a este post.

\( \bullet \)  What Every Computer Scientist Should Know About Floating-Point Arithmetic, David Goldberg (March 1991). ACM Computing Surveys 23 (1): 5–48.

   Utilizaremos de ese trabajo el apéndice D, el cual incluimos como archivo adjunto en este post.

http://en.wikipedia.org/wiki/IEEE_754-2008
http://en.wikipedia.org/wiki/Half_precision_floating-point_format
http://en.wikipedia.org/wiki/Single_precision_floating-point_format
http://en.wikipedia.org/wiki/Double_precision_floating-point_format
http://en.wikipedia.org/wiki/Quadruple_precision_floating-point_format
http://en.wikipedia.org/wiki/Decimal32_floating-point_format
http://en.wikipedia.org/wiki/Decimal64_floating-point_format
http://en.wikipedia.org/wiki/Decimal128_floating-point_format

   Sobre el estado actual del compilador GCC respecto C99, se puede consultar una tabla aquí:

\( \bullet \)  http://gcc.gnu.org/c99status.html


CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC



Organización

Comentarios y Consultas

15 Enero, 2013, 06:32 am
Respuesta #18

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
18. Números de punto flotante. Estándar IEEE 754

   El estándar internacional ISO/IEC/IEEE 60559:2011 tiene idéntico contenido al estándar IEEE 754.
   Lo podemos encontrar en la literatura de las dos formas, y nosotros usaremos la segunda nomenclatura sólo por brevedad.

   La bibliografía que hemos de utilizar está indicada en el post anterior.

   Cuando el estándar define un formato de números de punto flotante, no indica cómo tienen que representarse exactamente en hardware (el procesador), software (el sistema operativo, o programas ejecutables, o el lenguaje o el compilador), o una combinación de ambos.
   Al mismo tiempo, el estándar está basado en la experiencia de funcionamento de los procesadores y diversos sistemas en cuanto al manejo de los números de punto flotante.
   Cada fabricante tiene sus propios y válidos criterios, pero un estándar intenta uniformar estos criterios, a fin de que los resultados sean compatibles entre distintos sistemas de computación, sin importar el hardwara o el software utilizado. 

\( \bullet \)   Un formato IEEE 754 se refiere a un conjunto de números, y posibles reglas de representación, reglas de redondeo y otros cálculos entre ellos.

   Los formatos se dividen en básicos y no básicos, y el estándar da indicaciones para ambos, aún cuando los no básicos no los define con exactitud.
\( \bullet \)   Los formatos básicos de base 2 se denominan binary32, binary64, binary128, y les corresponden tamaños en memoria de 32, 64 y 128 bits respectivamente.
\( \bullet \)   Los formatos básicos de base diez se denominan decimal64, decimal128.

   No obstante, hablaremos en abstracto de lo que se supone que es un formato básico, y en lo que sigue procuramos explicar en detalle las especificaciones del estándar para formatos tanto básicos como formatos cualesquiera.

   En cualquier caso, un tal conjunto de números es un subconjunto de los números reales, que está determinado por los siguientes parámetros globales:

\( b \): base
\( p \): precisión
\( emin \): exponente mínimo
\( emax \): exponente máximo

   La base limita el alfabeto de dígitos que se pueden usar. Quiere decir que se usan sólo los números enteros de \( 0 \) a \( b-1 \) como dígitos.
   Si la base es \( b = 2 \) (binaria), estamos en el típico caso de dígitos binarios de las computadoras: 0 y 1.
   Si \( b = \textsf{diez} \) (decimal), estamos ante la forma humana de representación, con dígitos que van del 0 al 9.
   Aparentemente el estándar no da indicaciones para bases distintas que la binaria y la decimal. Podría pensarse que, en relación a la base binaria, sería buena idea decir algo sobre las bases octal y hexadecimal. Veremos luego que conviene "quedarse a vivir" en la base binaria.

   Recordemos la notación científica: \( \sigma\cdot m \cdot b^r \),  donde tenemos un signo \( \sigma \), una mantisa \( m \) y un exponente \( r \).

   La precisión \( p \) es la cantidad de dígitos que se admitirán en una mantisa. Este número es fijo por cada conjunto de números de punto flotante. También se dice que \( p \) es la cantidad de dígitos significativos.
   Los coeficientes \( e_{min} \) y \( e_{max} \) se entenderán cuando estudiemos otros detalles.

   Los elementos del conjunto que definen un formato dado se clasifican en: números finitos, infinitos, NaN (Not-a-Number: un-no-número).
   A su vez, los números finitos se dividen en números normalizados y números subnormales (o denormalizados).
                                                                                             
   Un número finito del conjunto de números de punto flotante indicado por \( (b,p,emin,emax) \), estará determinado por 3 números enteros \( (s,m,r) \), de la siguiente manera:

\( s \): signo
\( m \): mantisa (en realidad el estándar le llama significando)
\( q \): exponente

   Son tales que \( s=0 \) ó \( 1 \), \( m \) es un entero no negativo, y \( q \) es un entero. Representan el número:

\( \bullet \)         \( (-1)^s\cdot m\cdot b^q \)

   Obviamente, \( s=0 \) da signo positivo, y \( s=1 \) da signo negativo: \( (-1)^0=+1, (-1)^1=-1 \).
   Es incómodo que se le llame signo, a lo que en realidad es un exponente que te ayuda a obtener el signo. Pero tomémoslo como viene sin discutir demasiado...  :-\

   Procedamos ahora con sumo cuidado.  :o :o :o :-*
   Las mantisas tienen la forma \( d_0.d_1...d_{p-1} \), donde cada \( d_k \) es un dígito binario (0 ó 1).
   Aquellos valores cuyas mantisas tienen el dígito líder \( d_0=1 \), son los números normalizados.
   Aquellos que tienen \( d_0 = 0 \) son los subnormales.
   En cualquier caso, se denomina fracción, y se denota con \( f \), a la parte que está a la derecha del punto fraccionario. Así:

\( f = .d_1...d_{p-1}. \)

   Observemos que todo número binario escrito en notación científica se puede escribir de forma normalizada, es decir, con \( d_0 = 1 \).
   Eso hace que almacenar en memoria el dígito líder 1 sea redundante, ya que siempre está implícitamente en ese caso.

   El estándar exige que los formatos básicos binarios (¡ojo! esto no se exige para los no básicos) representen a los valores normalizados

\( \pm (1.d_1...d_{p-1}) 2^{e} \), con \( e_{min}\leq e\leq e_{max} \),

de manera que el primer dígito 1 sea considerado implícito. O sea que no se almacena ese bit en memoria real.
   Sólo se almacenan los bits de la fracción \( f \). Así, hablaremos del tamaño (o ancho, width) en bits de la fracción, la cual será en este caso 1 menos que la precisión \( p \).

   Ahora bien, los números de la forma \( \pm (0.d_1...d_{p-1}) 2^{e_{min}} \) serán los subnormales.
   Sin embargo, tienen que tener una representación distinta, porque una fracción \( f = .d_1...d_{p-1} \) con un exponente \( e_min \), representa como ya vimos al número normalizado \( 1.d_1...d_{p-1} 2^{e_{min}} \).
   Para los números subnormales se usarán, pues, las potencias con exponente \( e_{min}-1 \), conviniendo que una terna \( (s, m, e_{min}-1), \quad(s=\textsf{0 ó 1}) \), donde \( m = d_1...d_{p-1} \), codificará un número subnormal de la forma\(  \pm (0.d_1...d_{p-1}) 2 ^{e_{min}} \).

   Nótese que, con estas convenciones, todo número finito distinto de cero, ya sea normal o subnormal, tiene una única codificación posible.
   Esto asegura que no hay ambigüedad en el formato. Hay una excepción a esto, y la constituyen los ceros signados.

   Cuando los dígitos \( d_1,...,d_{p-1} \) son todos 0 y el exponente es \( e_{min}-1 \), se conviene que representa el valor matemático 0.
   Sin embargo, aquí hay ambigüedad, porque todavía hay un bit de signo.
   Si \( s = 0 \), se considera que el valor 0 es positivo, y se expresa \( +0 \), mientras que si \( s = 1 \), el valor 0 es negativo, y se expresa \( -0 \).
   La notación \( +0, -0, \) es sólo una abreviatura, que de paso nos recuerda que "internamente" coexisten dos representaciones distintas del mismo valor 0.
   Desde el punto de vista aritmético y lógico, no hay diferencia entre ellos: se consideran el mismo número 0, que no tiene signo alguno.

   Observemos también que si todos los dígitos son 0, pero \( e \geq e_{min} \), el número representado no es 0, sino \( \pm 2^e \) (el signo depende, claro, del bit de signo).

   Ahora hablemos un poco de cómo se codifica el exponente \( e \).
   Si \( e_{min} \leq e \leq e_{max} \), se utiliza una representación sesgada o descentrada (biased).
   Sea \( H = e_{max} - e_{min}+1 \). Los números de \( 1 \) hasta \( H \) codificarán exponentes desde \( e_{min} \) hasta \( e_{max} \), de la siguiente manera:
   El número \( \tilde e \) codifica el exponente \( e - H \).
   El número \( H \) en inglés se llama bias. Podríamos traducirlo como el "sesgo", el "corrimiento".
   De esta manera, tenemos la relación \( e = \tilde e + H \).
   Especial cuidado ha de tenerse al sumar y restar exponentes sesgados, ya que debe tenerse en cuenta que los números \( \tilde e \) representan un valor distinto a ellos mismos.
   No voy a apabullarles con ejemplos. En todo caso, podemos retomar esto después.

   En cambio, \( \tilde e = 0 \) se usará para guardar los ceros signados y los números subnormales.

   El exponente descentrado \( \tilde e = H+1 \) se usará para los valores infinitos y los NaN.

   Hablemos ahora de las convenciones del estándar acerca de los infinitos y los valores NaN.

\( \bullet \)   En todo formato debe h aber "al menos" un valor \( -\infty \) y uno \( +\infty \).
\( \bullet \)   No se exige que haya otrosposibles valores "infinitos".

   En los formatos básicos binarios se especifica además cómo tiene que ser la codificación en bits de estos valores infinitos (al menos, de los dos que se exigen).
   Ellos utilizan el exponente \( e = e_{max}+1 \), que es lo mismo que decir que usan el exponente descentrado \( \tilde e = H+1 \).
   Más precisamente, si tenemos la terna \( (s, m, e) \) donde \( m = 0...0 \), \( e = e_{max}+1 \) (o sea, \( \tilde e = H+1 \)), el valor representado es \( (-1)^s \infty \).

   En cuanto a los valores NaN, el estándar exige que todo formato contenga "al menos" un valor NaN silencioso y "al menos" un valor NaN señalizador.
   Los NaN se deben representar como una terna \( (s, m, e) \), donde \( m \neq 0...0 \), y \( e = e_{max+1} \) (o sea, \( \tilde e =H+1 \)).
   El estándar no obliga a que el signo \( s \) de un valor NaN tenga significado.
   Así, valores NaN con signos opuestos representarían el mismo valor NaN.
   Los valores NaN surgen en operaciones inválidas. Estudiaremos esto más adelante.
   Además, los valores NaN señalizadores son capaces de arrastrar alguna información sobre la circunstancia que los generó.

   Se aprecia que son posibles muchos valores NaN, uno por cada valor de \( m \) (distinto de 0).
   No quiere decir esto que todos estos posibles valores estén asignados a un NaN.
   Estas asignaciones dependen de la implementación.

   Dado que los valores infinitos y los valores NaN comparten el mismo exponente \( e = e_{max}+1 \), una implementación particular podría querer usar algunos de esos valores para representar nuevos valores de infinitos (por ejemplo, infinitos obtenidos en cálculos con números complejos, o quizá, más probablemente, distintos niveles de overflow).
   En ese caso, habría que especificar con claridad cuáles valores se usan para infinitos, y cuáles para NaNs. 

   Como vemos, aparecen dentro del conjunto de valores posibles unos entes extraños, que encima se clasifican, y reaccionan a quién sabe qué procesos... la cosa ya se complicó.  :-\

   Por ahora, nosotros sólo estamos haciendo el recuento de los valores posibles que puede tomar un conjunto de números de un formato floating point dado por el estándar.
   Les podemos seguir diciendo "números" aunque haya objetos allí que matemáticamente no son números: los infinitos y los NaN.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Además de los valores antes especificados, el estándar exige que en un formato dado existan, "al menos", 5 tipos de señales, asociadas a errores de diversa índole.
   Estas señales no se codifican como los valores \( (s, m, e) \) que hemos explicado, sino que se deben indicar como "banderines" (flags) que andan "por ahí".
   Se supone que el sistema utilizado (hardware/software) tiene "por ahí" unos bits de estado que indican si las señales de error están activas o no.
   Los 5 tipos de señales exigidas por el estándar son:

Operación inválida (invalid operation).
División por cero  (division by zero).
Desbordamiento     (overflow).
Rebase por defecto (underlow).
Inexacto           (inexact).

   Es importante conocer las nomenclaturas en inglés porque son la manera típica en que aparecen comunmente en la literatura.

Las situaciones que originan esas señales, y lo que el estándar exige que se haga en consecuencia, incluyendo los valores que deben tomar los resultados de las operaciones bajo esas circunstancias, son un tema que estudiaremos más adelante.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   El estándar también sugiere que se definan procedimientos para captura y manejo de errores, así como la existencia de ciertas funciones específicas para valores de punto flotante.
   No vamos a explicar esto en este momento. Sólo enfatizamos que se trata de sugerencias, y no reglas de los formatos del estándar.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Cuando utilizamos base 2, los formatos básicos del estándar aprovechan el bit extra que se gana en la forma normalizada.
   Ese bit se denomina oculto, y así la precisión se expresa como el número de bits que se usan en memoria más 1, el bit oculto.
   El estándar define cinco formatos básicos y dos no básicos, cada uno determinando su correspondiente conjunto de números de punto flotante.


binary16:   base = b = 2,  precisión = p = 10+1,  emin = -14,    emax = +15.
binary32:   base = b = 2,  precisión = p = 23+1,  emin = -126,   emax = +127.
binary64:   base = b = 2,  precisión = p = 52+1,  emin = -1022,  emax = +1023
binary128:  base = b = 2,  precisión = p = 112+1, emin = -16382, emax = +16383.
decimal32:  base = b = 10, precisión = p = 7,     emin = -95,    emax = +96.
decimal64:  base = b = 10, precisión = p = 16,    emin = -383,   emax = +384.
decimal128: base = b = 10, precisión = p = 34,    emin = -6143,  emax = +6144.


Los formatos binarios se conocen con los siguientes nombres:

binary16  Half Precision      (Mitad de Precisión Simple).
binary32  Single Precision    (Precisión Simple).
binary64  Double Precision    (Precisión Doble).
binary128 Quadruple Precision (Precisión Cuádruple Binaria).

   El tamaño (o ancho, width) \( w \) de un formato básico binario binaryXX es de XX bits, como su nombre indica.
   Se usa 1 bit para el signo, \( p \) bits para la fracción de la mantisa, y los bits restantes para el exponente.
   El estándar también impone reglas sobre las operaciones aritméticas que deben estar definidas para un determinado formato, funciones matemáticas que deben estar definidas, conversiones entre formatos de punto flotante distintos, conversiones entre formatos de punto flotante y tipos de datos enteros, reglas de redondeo de punto flotante a número entero, conversiones de binario a decimal y viceversa, reglas de comparación (o sea, definición de una relación de orden), aritmética infinita, aritmética con NaN, y reglas para el bit de signo.

(Todo esto será analizado en el momento oportuno. Hacerlo aquí sería muy tedioso.)

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   Para terminar esta exposición necesitamos hablar de los formatos extendidos.

El estándar define dos formatos denominados extendidos, de precisión simple y doble.
Sus requerimientos son los siguientes:


Extended Single precision: \( p \geq 32, e_{max}\geq +1023, e_{min} \leq -1022, w_e \geq 11, w\geq 43. \)
Extended Double precision: \( p \geq 64, e_{max}\geq +16383, e_{min} \leq -16382, w_e \geq 15, w\geq 79. \)


   Hemos indicado con \( w_e \) la cantidad de bits que se usa para representar el exponente.
   Con \( w \) indicamos el ancho total en bits del formato.
   El estándar NO EXIGE a los formatos extendidos que utilicen la convención del bit implícito, o sea, no presuponen la forma normalizada para los valores finitos.
Esto puede hacer que se pierda 1 bit de precisión respecto a lo que uno esperaría.
   Tampoco se especifica cuál ha de ser el modo en que se representan los exponentes, y así el sesgo (bias), no está especificado.
   Como se ve arriba, tampoco está especificado el tamaño exacto del formato.
   Tan sólo hay valores mínimos para la precisión \( p \), el exponente máximo \( e_{max} \), el ancho del exponente \( w_e \), y valores máximos para el exponente mínimo \( e_{min} \).
   El ancho total \( w \) del formato será la suma de esos valores más 1 bit de signo: \( w = 1+p+w_e \).

   ¿Qué sentido tiene que un estándar tenga tantas imprecisiones? ¿Qué función cumplen los formatos extendidos?
   La idea detrás de los formatos extendidos es ayudar en la exactitud de los cálculos con los formatos básicos.
   Recordemos por ejemplo lo que ocurre con las calculadoras de bolsillo. En ellas se muestran diez dígitos en el visor de resultados, mientras que internamente se realizan los cálculos con 2 o 3 dígitos más que no se muestran.
   Estos dígitos extra permiten disimular mejor la pérdida de exactitud que se tiene por los errores de redondeo, inevitables en un sistema que usa una cantidad fija de dígitos.

   Así, el Extended Single precision: ayudará a mantener mejor exactitud en los cálculos que involucren valores del formato Single Precision, y el Extended Double Precision hará lo propio con el Double Precision.
   Sin embargo, el estándar procura no obligar a ningún fabricante, programador o sistema, a caber en un formato específico y rígido, y así sólo establece lo mínimo que requiere un formato extendido.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

¿Son obligatorios los formatos básicos y extendidos?

   Los formatos binary16 y binary32 existen para propósitos de almacenamiento de datos numéricos, antes que para propósitos de cálculo.
   Luego, se consideran formatos básicos a los cinco restantes: binary32, binary64, binary128, decimal64, decimal128.
   El estándar sólo obliga a un sistema dado a implementar uno solo "al menos" de los formatos básicos. (¿Binarios, decimales, cualquiera? Debo revisar este detalle).
   No obliga a utilizar ninguno de los formatos extendidos.
   Sin embargo, sugiere fuertemente que se incorpore un formato extendido cuyo ancho sea estrictamente mayor que el más ancho de los formatos básicos presentes en el sistema.
   Por ejemplo, si en nuestro sistema se soporta el Double Precision como máximo formato básico, entonces se recomienda que haya al menos un Extended Double Precision presente.
   El estándar además dice que, una vez presente este formato extendido, no hace falta ningún otro en el sistema.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas

17 Enero, 2013, 12:31 am
Respuesta #19

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,292
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
19. Números de punto flotante. Estándar C99. Constantes

   Veamos lo que dice el estándar C99 del lenguaje C, acera de los números de punto flotante.

Ante todo, debemos tener paciencia...  :'(

   En primer lugar se define en abstracto aquello que ha de considerarse un modelo para los tipos de punto flotante.
   Esto da la definición matemática de los números de un tipo de punto flotante, según se entienden en el estándar C99.
   Dados un signo \( s \) (\( = +1 \) ó \( -1 \)), una base \( b \) (un entero \( > 1 \)), un exponente \( e \) (un entero cuyo valor está entre dos valores \( e_{min}, e_{max} \), \( e_{min}\leq e  \leq e_{max} \)), una precisión \( p \), unos dígitos \( f_1, f_2 ..., f_{p} \) en base \( b \) (es decir, enteros \( 0\leq f_k\leq b-1 \)), definen un modelo \( x \) de números de punto flotante, así:

\( x=s\cdot b^e\cdot \sum_{k=1}^{p} f_kb^{-k}. \)

   Esto define el término número de punto flotante.

   Por un lado, el estándar C99 recomienda tener en cuenta el estándar ISO/IEC/IEEE 60559 (brevemente IEEE 754), y la terminología que se usa sobre los números de punto flotante está inspirada en el IEEE 754.
Pero no parece que en ninguna parte el C99 obligue a ajustarse al IEEE 754.

Por un lado, esto nos ayuda a entender la terminología referida a los tipos de punto flotante, a la vez que nos previene de que muchas cuestiones pueden ser opcionales.
Y cuando para un estándar algo es opcional, quiere decir que en las implementaciones reales no hay acuerdo, y pueden haber problemas de portabilidad de los programas.

Para el estándar C99 hay tres tipos de datos considerados tipos de punto flotante reales. Ellos son float, double, long double.

(En programas antiguos se puede ver el uso de long float como sinónimo de double, pero actualmente no se soporta ese uso).

   Lo que se acostumbra es hacer coincidir los tipos de C con los formatos de IEEE 754, así:
el tipo float se corresponde con Single Precision (o binary16),
el tipo double se corresponde con Double Precision (o binary64),
el tipo long double se corresponde con Extended Double Precision.

Esa correspondencia usémosla sólo como un ejemplo que nos sirva de ejemplo en nuestra exposición, a fin de no tener definiciones tan genéricas dando vueltas en la cabeza.

   Pero en las implementaciones reales no hay razón para creer que float, double, long double, corresponden a un formato u otro, ni tan siquiera del IEEE 754 o no.
   El comité de estandarización para el C99 ha discutido mucho a este respecto, y el resultado de toda esa discusión no se ve en el resultado final que vemos nosotros: todo es opcional.
   Eso es porque hay multitud de fabricantes, implementaciones y demás desarrollos importantes que siguen cada cual su especificación propia, y el estándar pretende ser compatible con todas ellas.

   Es muy común que en una implementación concreta, coincidan double y long double.
   O bien que coincidan los tres tipos de punto flotante.
   Cuando decimos que coinciden, queremos decir que tienen el mismo conjunto de valores numéricos, la misma precisión, etc. Sin embargo, sintácticamente hablando, siguen siendo tipos de datos distintos.

   El estándar sin embargo nos dice algunas cosas:

   El conjunto de valores representado por float es un subconjunto de los valores representados por double.
   El conjunto de valores representado por double es un subconjunto de los valores representados por long double.

   También define la macro __STDC_IEC_559__, cuyo valor es 1 si el compilador de la implementación local "dice que"  ;) ;) soporta compatibilidad con la normativa ISO/IEC/IEEE 60559 (IEEE 754), y vale 0 en caso contrario.

   Aún en el caso de que __STDC_IEC_559__ esté definida como 1, eso no especifica de qué manera o en qué medida la implementación local es compatible con ISO/IEC/IEEE 60559 (IEEE 754).
   Y aún si fuera totalmente compatible, el susodicho ISO/IEC/IEEE 60559 (IEEE 754) está abierto a tantas opciones, que no se logra demasiada certidumbre.

   Lo que se debe hacer es estudiar la documentación del compilador que usamos (en nuestro caso GCC 4.7), y establecer qué dice ahí.

   Veremos que el estándar C99 ofrece algunos valores mínimos que hay que respetar, en la librería <float.h>.
   Pero antes vamos a ver cómo se escriben las constantes de punto flotante en un programa en C.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Otros tipos de punto flotante declarados en C

   En la librería <math.h> del estándar C están especificados, acorde a lo requerido por C99, los siguientes tipos de punto flotante:

float_t
double_t

   Estos tipos de datos están pensados de la siguiente manera:

   Supongamos que en nuestro sistema es posible acceder a tipos de punto flotante que tienen al menos la misma exactitud que float y/o double, pero que además permiten realizar cálculos más eficientes.
   En ese caso, el programador puede elegir tomar ventaja de esa eficiencia, y para ello no tendría que hacer muchos cambios en su programa fuente en C, sino hacer algo tan simple como escribir float_t en vez de float y/o double_t en vez de double.

   Qué es lo que representan exactamente estos tipos depende del hardware y/o de la implementación local del compilador, etc.
Sin embargo, el estándar C99 establece una pautas mínimas que han de cumplir,
y que dependen del valor asignado a la macro FLT_EVAL_METHOD, que está definida en la librería float.h:

Si el valor de FLT_EVAL_METHOD es 0,
   entonces float_t coincide con float, y double_t coincide con double.

Si el valor de FLT_EVAL_METHOD es 1,
   entonces float_t y double_t coinciden ambos con double.

Si el valor de FLT_EVAL_METHOD es 2,
   entonces float_t y double_t coinciden ambos con long double.

Para otros valores de FLT_EVAL_METHOD, es la implementación local la que debe especificar qué tipos son float_t y double_t.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Constantes de punto flotante en C.

   Desde el estándar C99 hay dos tipos de constantes de punto flotante: decimales y hexadecimales.
   El formato general de una constante decimal de punto flotante es éste:

n.de+r

Representa al número real:

\(   \alpha \) = (n+0.d)\( \cdot 10^{{\color{blue}\pm}{\color{red}\mathbb r} } \).

\( \bullet \)   En n va una secuencia finita de dígitos decimales (del 0 al 9), que denotan la parte entera.
\( \bullet \)   El . es el punto decimal (que separa la parte entera de la parte fraccionaria).
\( \bullet \)   En d va una secuencia finita de dígitos decimales, que denotan la parte fraccionaria.
\( \bullet \)   La letra e es el indicador de exponente, y puede usarse indistantemente e ó E.
\( \bullet \)   El signo + es el signo del exponente, y puede ser \( + \) ó \( - \).
\( \bullet \)   En r va una secuencia de dígitos del 0 al 9, que representa el valor absoluto del exponente.

   La parte entera n es opcional. Si no está, se interpreta que la parte entera es 0. Pero en este caso, tiene que estar presente algún dígito en la parte fraccionaria.
   La parte fraccionaria d es opcional. Si no está, se interpreta que la parte fraccionaria es .0. Pero en este caso, tiene que estar presente algún dígito en la parte entera.

   El punto decimal . y el indicador de exponente e ó E, son opcionales.
   Pero al menos tiene que estar presente alguno de los dos.
   El punto decimal es obligatorio cuando se indica una parte fraccionaria.

   Si el indicador de exponente e ó E no está,  quiere decir que el signo del exponente + es \( + \) y que el exponente r es 0.
   En tal caso, no pueden estar escritos explícitamente ni el signo + ni r.

   Cuando e ó E está presente, el signo + es opcional (si no está se toma por defecto como \( + \)), pero el exponente r tiene que estar presente.

   Las constantes que hemos descrito arriba se consideran por defecto de tipo double.
   Se puede agregar a la derecha un sufijo de estos:

f ó F: fuerza a la constante a ser de tipo float.
l ó L: fuerza a la constante a ser de tipo long double.


Ejemplos:

12.81e-25F, 1.281e-24f, .1281e-0023f, 000000.00001281e-19, 0.00000000000000000000000128, .000000000000000000000000128e+1L, 0.00000000000000000000000000128e3l

   Todas esas constantes representan al mismo número real, pero escrito en distintas formas, combinando partes entera, fraccionaria, y exponentes.
   Además, tienen tipos distintos: las primeras tres son float, las siguientes dos son double, y las últimas dos son long double.


Algunas consideraciones:

\( \bullet \)   Es importante notar que tanto las partes entera, fraccionaria, como el exponente, se indican con dígitos decimales. El compilador interpreta todo eso en base diez.

\( \bullet \)   Si la parte entera tuviera 0's a la izquierda, el número no se considera "octal" (como ocurría con las constantes enteras), sino que sigue siendo decimal, y esos 0's extra no alteran el valor matemático del numero, ni la representación interna del mismo, ni el tipo de datos al que pertenece la constante.

\( \bullet \)   Si la parte fraccionaria tuviera 0's a la derecha, no producen ningún efecto matemático, ni de representación interna, ni cambia el tipo de datos al que pertenece la constante.

   Agregando muchos 0's a la derecha se puede dar la falsa impresión de que estamos ante una constante que tiene tantos dígitos de precisión (a nivel de bits, internamente), como 0's se muestran. Pero esto no es así, y por lo tanto debe considerarse una mala práctica de programación.

\( \bullet \)   Notar que la presencia del punto decimal en cualquier caso impide que se confundan las constantes enteras con las constantes de punto flotante. A nivel sintáctico ambas están perfectamente diferenciadas.

\( \bullet \)   En cuanto a si conviene usar e ó E, la recomendación general es usar las minúsculas, porque supuestamente se lee mejor...  ::)

\( \bullet \)   En cuanto a los sufijos f, F, l, L, en cambio, se sugiere usar las mayúsculas. Por ejemplo, una l], en minúsculas, se confundiría fácilmente con un 1.

\( \bullet \)   Las constantes que pueden escribirse con el formato anterior son positivas ó cero (0.0), pero nunca negativas.

   Anteponer un signo - delante es posible, pero no se considera parte de la constante, sino como un operador que calcula el negativo de la constante en cuestión.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Constantes hexadecimales:

   El formato general de una constante hexadecimal de punto flotante es éste:

0xn.dp+r

Representa al número real:

\( \alpha \) = (n+0.d)\( {}_{16}\cdot 2^{{\color{blue}\pm}{\color{red} \matbb r}_{10} } \).

   El prefijo 0x ó 0X indica que la constante es hexadecimal;) ;)
   Los dígitos en la parte entera n y en la parte fraccionaria d son hexadecimales (0 a 9, y de A a F, y se admiten las minúsculas de a hasta f).
   La letra p ó P es el indicador de exponente binario.
   Indica que la potencia 2 será elevada al exponente que viene a la derecha: +r.

Importante:  :o :o :o :o :o el exponente r se sobreentiende expresado en base decimal.  :banghead: :banghead:

(Sí: un número en base 16, tiene adosado un exponente para una potencia de base 2, expresado en base 10... es para hacerle saltar las tuercas a cualquiera  :'( :'( )

   Las demás consideraciones sobre las posibles maneras de escribir una constante hexadecimal, son similares al caso decimal, en que puede faltar parte entera o parte fraccionaria, etc.
   Por ejemplo, es posible usar los sufijos f, F, l, L.

Sin embargo:

   En el caso hexadecimal siempre  :o tiene que estar presente el indicador exponencial binario p ó P.

   Esto logra evitar ambigüedades que surgirían al querer indicar constantes hexadecimales de tipo float, ya que la letra f se usa tanto para indica el "dígito hexadecimal quince" como "constante de tipo float".

Ejemplos:

\( 1.0p11: \qquad\qquad\qquad\quad = 2^{(11)_{10}} \)                (double)
\( 0x31FCp-17f: \qquad\qquad = (31FC)_{16}\cdot 2^{-(17)_{10}} \) (float)
\( 0xEF.CC1400A5p0L: \qquad = (EF.CC1400A5)_{16} \) (long double)

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

   ¿Por qué el C99 incorpora constantes de punto flotante hexadecimales?
   El motivo es que internamente las constantes se almacenan, generalmente, en base binaria.
   Es más fácil traducir de base 16 a base 2 y viceversa.
   Más aún, no se pierde exactitud en la conversión de una base a otra.

   Pero cuando de números con parte fraccionaria se trata, si bien "es posible"  ;) no perder precisión al convertir de base binaria a base decimal, en cambio de base decimal a base binaria lo más común es que se obtengan desarrollos binarios periódicos, con la consecuente pérdida de precisión por truncamiento.

   En algunas circunstancias, el programador necesita tener mayor control sobre la precisión de sus constantes, y entonces la notación hexadecimal es de mucha ayuda al respecto.
   También hay otros inconvenientes mucho más sutiles.
   Por ejemplo, el compilador puede elegir convertir de decimal a binario con determinadas reglas de redondeo, entre otras pautas durante la conversión, mientras que el programa ejecutable tendrá otros criterios (interpretando las constantes decimales de otra manera), e incluso puede haber incompatibilidad con criterios de conversión adoptados por funciones de librería (como printf()).
   A fin de estar seguros de una representación binaria uniforme de ciertas cantidades, la notación hexadecimal da una gran ayuda al programador.

   Debemos notar también que, aunque la notación hexadecimal de constantes de punto flotante permite una traducción directa al formato binario interno utilizado, esto no sirve para que "hagamos trampa".
   Por ejemplo, si en nuestro sistema el tipo float coincide con el Single Precision de IEEE 754, podríamos querer generar un valor NaN mediante: 0x1.FFFFFEp128f (obsérvese que el exponente binario es 128, coincidiendo con el exponente \( e = e_{max}+1 \), utilizado para los valores NaN).
Por el contrario, una constante así sólo producirá un error de desbordamiento, indicando que la constante es demasiado grande para caber en un float.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Normativa ISO/IEC/IEEE 60559 (ó IEEE 754)

   En cuanto a los temas que hemos desarrollado en este post, veamos qué dice el estándar C99 respecto el ISO/IEC/IEEE 60559.

   Cuando la macro __STDC_IEC_559__ tiene valor 1, el estándar C99 se ajusta al estándar ISO/IEC/IEEE 60559 de la siguiente manera:

\( \bullet \)   El tipo float coincide con el Single Precision de IEEE 754.
\( \bullet \)   El tipo double coincide con el Double Precision de IEEE 754.
\( \bullet \)   El tipo long double coincide con algún formato Extended de IEEE 754.
         Si esto no fuera posible, entonces coincide con algún formato extendido, que no pertenece al IEEE 754.
         Si esto tampoco se puede, entonces coincide con el Double Precision de IEEE 754.

   Todo formato extendido no perteneciente a IEEE 754 usado para long double, debe tener más precisión que el Double Precision de IEEE 754, y "al menos" el mismo rango de valores (del Double Precision).

Nota: Acorde a las reglas vistas al principio del post, se deduce también que el long double debe contener al conjunto de valores del double, de C.

   El estándar C99 no especifica valores NaN señalizadores (aunque está permitido implementarlos).

   Las constantes en la etapa del compilador tienen que tener el mismo valor que tendrían en la etapa de ejecución del programa.
   Esta norma se refiere a que el compilador puede elegir traducir constantes escritas a mano, convirtiendo de decimal a binario según criterios distintos a los que se usan durante un programa en ejecución (en donde el compilador no está actuando, sino nuestro programa compilado).
   Es una preocupación legítima acerca de la coherencia de nuestro sistema en torno al manejo de las constantes de punto flotante.

Hay más consideraciones, pero las explicaremos en el momento oportuno.

CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Organización

Comentarios y Consultas