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

0 Usuarios y 2 Visitantes están viendo este tema.

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

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,274
  • 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,274
  • 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,274
  • 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,274
  • 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,274
  • 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,274
  • 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,274
  • 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,274
  • 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,274
  • 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,274
  • 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