Cbeej
Cbeej
1 Prefacio 1
1.1 Importante! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Cómo leer este libro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Plataforma y Compilador . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.4 Página Web Oficial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.5 Política de Email . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.6 Duplicación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.7 Nota para traductores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.8 Derechos de autor y distribución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.9 Dedicatoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Hello, World! 5
2.1 Qué esperar de C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2 Hello, World! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Detalles de la Compilación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.4 Construyendo con gcc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.5 Construyendo con clang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.6 Construyendo con IDEs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.7 Versiones de C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3 Variables y Declaraciones 11
3.1 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.1.1 Nombres de las variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.1.2 Tipos de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.1.3 Tipo booleano . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2 Operadores y Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2.1 Operadores aritméticos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2.2 Operador ternario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.3 Pre-y-Post Incremento-y-Decremento . . . . . . . . . . . . . . . . . . . . . . . 15
3.2.4 El operador coma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.2.5 Operadores condicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2.6 Operadores Booleanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2.7 El operador sizeof . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.3 Control de flujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.3.1 El estado if-else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.3.2 La declaración while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.3.3 La sentencia do-while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.3.4 La sentencia ‘for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3.5 Declaración switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4 Funciones 27
4.1 Transmisión por valor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.2 Prototipos de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.3 Listas de parámetros vacías . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
i
CONTENTS ii
6 Arrays 40
6.1 Ejemplo sencillo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
6.2 Obtener la longitud de una matriz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
6.3 Inicializadores de matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.4 ¡Fuera de los límites! (Out of Bounds!) . . . . . . . . . . . . . . . . . . . . . . . . . . 43
6.5 Matrices multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
6.6 Matrices y punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.6.1 Obtener un puntero a una matriz . . . . . . . . . . . . . . . . . . . . . . . . . . 45
6.6.2 Paso de matrices unidimensionales a funciones . . . . . . . . . . . . . . . . . . . 46
6.6.3 Modificación de matrices en funciones . . . . . . . . . . . . . . . . . . . . . . . . 47
6.6.4 Paso de matrices multidimensionales a funciones . . . . . . . . . . . . . . . . . 48
8 Estructuras (Structs) 55
8.1 Declaración de una estructura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
8.2 Inicializadores de estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
8.3 Paso de estructuras a funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
8.4 El operador Arrow / flecha (->) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
8.5 Copiar y devolver structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
8.6 Comparación de structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
13 Alcance 91
13.1 Alcance del bloque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
13.1.1 Dónde definir las variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
13.1.2 Ocultación de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
13.2 Alcance de fichero / Archivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
13.3 Ambito del bucle for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
13.4 Nota sobre el alcance de las funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
19 El preprocesador C 141
19.1 #include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
19.2 Macros sencillas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
19.3 Compilación condicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
19.3.1 Si está definido, #ifdef y #endif. . . . . . . . . . . . . . . . . . . . . . . . . 143
19.3.2 Si no está definido, #ifndef. . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
19.3.3 #else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
19.3.4 Else-If: #elifdef, #elifndef . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
19.3.5 Condicional general: #if, #elif . . . . . . . . . . . . . . . . . . . . . . . . . 145
19.3.6 Perder una macro: #undef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
19.4 Macros integradas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
19.4.1 Macros obligatorias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
19.4.2 Macros opcionales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
19.5 Macros con argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
19.5.1 Macros con un argumento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
19.5.2 Macros con más de un argumento . . . . . . . . . . . . . . . . . . . . . . . . . 150
19.5.3 Macros con argumentos variables . . . . . . . . . . . . . . . . . . . . . . . . . . 151
19.5.4 Stringificación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
19.5.5 Concatenación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
19.6 Macros multilínea . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
19.7 Ejemplo: Una macro Assert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
19.8 Directiva #error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
CONTENTS v
31 goto 248
31.1 Un ejemplo sencillo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
31.2 Etiqueta continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
31.3 Libertad bajo fianza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
31.4 Etiqueta break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
31.5 Limpieza multinivel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
31.6 Optimización de las llamadas de cola . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
31.7 Reinicio de llamadas al sistema interrumpidas . . . . . . . . . . . . . . . . . . . . . . . 254
31.8 goto y el Hilo conductor preferente (Thread Preemption) . . . . . . . . . . . . . . . . . 254
31.9 goto y el ámbito de las variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
31.10 goto y matrices de longitud variable (Variable-Length Arrays) . . . . . . . . . . . . . . 256
40 Atomics 316
40.1 Pruebas de compatibilidad atómica . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
40.2 Variables atómicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
40.3 Sincronización . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
40.4 Adquirir y Liberar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
40.5 Consistencia secuencial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
40.6 Asignaciones y operadores atómicos . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
40.7 Funciones de biblioteca que se sincronizan automáticamente . . . . . . . . . . . . . . . 323
40.8 Especificador de Tipo Atómico, Calificador . . . . . . . . . . . . . . . . . . . . . . . . 324
40.9 Variables atómicas sin bloqueo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
40.9.1 Manejadores de señales y atómicos sin bloqueo . . . . . . . . . . . . . . . . . . 326
40.10 Banderas atómicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
40.11 Estructuras y uniones atómicas(Atomic structs and unions) . . . . . . . . . . . . . . . 327
40.12 Punteros Atómicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
40.13 Orden de Memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329
40.13.1 Consistencia Secuencial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
40.13.2 Acquire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
40.13.3 Release . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
40.13.4 Consume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
40.13.5 Acquire/Release . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
40.13.6 Relaxed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
40.14 Fences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
40.15 References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Prefacio
E((ck?main((z?(stat(M,&t)?P+=a+'{'?[Link]
execv(M,k),a=G,i=P,y=G&255,
sprintf(Q,y/'@'-3?A(*L(V(%d+%d)+%d,0)
1
Chapter 1. Prefacio 2
1.1 Importante!
Esta guía asume que ya tienes algunos conocimientos de programación provenientes de otro lenguaje, como
Python2 , JavaScript3 , Java4 , Rust5 , Go6 , Swift7 , etc. ¡Los desarrolladores de (Objective-C8 lo tendrán bas-
tante fácil!)
Vamos a suponer que sabes qué son las variables, qué hacen los bucles, cómo funcionan las funciones, etc.
Si ese no es tu caso, lo mejor que puedo ofrecerte es un entretenimiento honesto para tu placer lector. Lo
único que puedo prometer, es que esta guía no terminará en un suspenso… ¿o SI ?
Los usuarios de Windows deberían consultar Visual Studio Community12 . O, si está buscando una experi-
encia más similar a Unix (¡recomendado!), instalar WSL13 y gcc .
Los usuarios de Mac querrán instalar XCode14 y, en particular, las herramientas de línea de comandos.
Hay muchos compiladores por ahí, y todos ellos funcionarán para este libro. Y un compilador de C++ com-
pilará la mayoría (¡pero no todo!) del código C. De ser posible, es mejor usar un compilador propiamente de
C.
2
[Link]
3
[Link]
4
[Link]
5
[Link]
6
[Link]
7
[Link]
8
[Link]
9
[Link]
10
[Link]
11
[Link]
12
[Link]
13
[Link]
14
[Link]
Chapter 1. Prefacio 3
1.6 Duplicación
Eres más que bienvenido a duplicar este sitio, ya sea pública o privadamente. Si reflejas públicamente el
sitio y quieres que lo enlace desde la página principal, escríbeme a beej@[Link] .
Una excepción específica a la parte de la licencia “Sin obras derivadas” es la siguiente: esta guía se puede
traducir libremente a cualquier idioma, siempre que la traducción sea precisa y la guía se reimprima en su
totalidad. Se aplican las mismas restricciones de licencia a la traducción que a la guía original. La traducción
también puede incluir el nombre y la información de contacto del traductor.
El código fuente C presentado en este documento se otorga al dominio público y está completamente libre
de cualquier restricción de licencia.
Se alienta libremente a los educadores a recomendar o proporcionar copias de esta guía a sus alumnos.
Póngase en contacto beej@[Link] para obtener más información.
1.9 Dedicatoria
Las cosas más difíciles de escribir esta guía son:
• Aprender el material con suficiente detalle para poder explicarlo.
• Descubrir la mejor manera de explicarlo de forma clara, un proceso iterativo aparentemente inter-
minable.
• Exponerme como una supuesta autoridad, cuando en realidad solo soy un ser humano normal que trata
de encontrarle sentido a todo, como todos los demás.
• Continuar cuando tantas otras cosas me llaman la atención.
Mucha gente me ha ayudado a través de este proceso, y quiero reconocer a aquellos que han hecho posible
este libro.
• Todos los usuarios de Internet que decidieron ayudar a compartir sus conocimientos de una forma u
otra. El intercambio gratuito de información instructiva es lo que hace que Internet sea el gran lugar
que es.
• Los voluntarios de cppreference.com16 que proporcionan el puente que lleva de la especificación al
mundo real.
• Las personas útiles y conocedoras de [Link].c17 y r/C_Programming18 que me ayudó a superar las
partes más difíciles del lenguaje.
• Todos los que enviaron correcciones y los pedidos de incorporación (pull-requests), desde instrucciones
confusas hasta errores tipográficos.
¡Gracias! ♥
16
[Link]
17
[Link]
18
[Link] com/r/C_Programming/
Chapter 2
Hello, World!
5
Chapter 2. Hello, World! 6
Es especialmente insidioso porque una vez que comprendes los punteros, de repente se vuelven fáciles. Pero
hasta ese momento, son como anguilas resbaladizas.
Todo lo demás en C es simplemente memorizar otra forma (¡o a veces la misma forma!) de hacer algo que
ya has hecho antes. Los punteros son la parte extraña. Y, se podría argumentar, que incluso los punteros, son
variaciones de un tema con el que probablemente estás familiarizado.
Así que.. prepárate! para una emocionante aventura, tan cerca del núcleo de la computadora como puedas
estar, sin llegar al lenguaje ensamblador, en el lenguaje de computadora más influyente de todos los tiempos6 .
¡Agárrate fuerte!
3 #include <stdio.h>
4
5 int main(void)
6 {
7 printf("Hello, World!\n"); // En realidad se hace el trabajo aquí
8 }
Vamos a ponernos nuestros guantes de goma resistentes y de manga larga, agarrar un bisturí y abrir esto para
ver cómo funciona. Así que lávate bien, porque allá vamos. Cortando muuuy suavemente…
Vamos a quitar lo fácil del camino: cualquier cosa entre los dígrafos /* y */ es un comentario y será com-
pletamente ignorado por el compilador. Lo mismo ocurre con cualquier cosa en una línea después de un //.
Esto te permite dejar mensajes para ti y para otros, de modo que cuando vuelvas y leas tu código en el futuro
lejano, sabrás qué demonios estabas tratando de hacer. Créeme, lo olvidarás; Sucede.
Ahora, ¿qué es esto #include? ¡GRANDE! Bueno, le dice al Preprocesador C que saque el contenido de
otro archivo y lo inserte en el código justo ahí.
Espera!… ¿qué es el Preprocesador de C? Buena pregunta. Hay dos etapas7 en la compilación: el preproce-
sador y el compilador. Cualquier cosa que comiense con el signo, almohadilla o “numeral” (#) es algo para
que el preprocesador opere, antes de que la compilación siquiera comiece. Comunmente las directivas del
preprocesador, son llamadas por #include y #define. Pero hablaremos de ello más adelante.
Antes de continuar, ¿por qué me molestaría en señalar que el signo de numeral se llama almohadilla? La
respuesta es simple: creo que la palabra almohadilla es tan extremadamente divertida[Traductor: me recurda
a las patitas de un gato] que tengo que difundir su nombre gratuitamente siempre que tenga la oportunidad.
Almohadilla. Almohadilla, Almohadilla, Almohadilla.
Entonces, de todas formas. Después de que el preprocesador de C haya terminado de preprocesar todo, los
resultados están listos para que el compilador los tome y produzca código ensamblador8 , código máquina9 ,
o lo que sea que esté a punto de hacer. El código máquina es el “lenguaje” que entiende la CPU, y lo puede
entender muy rápidamente. Esta es una de las razones por las que los programas en C tienden a ser rápidos.
Por ahora, no te preocupes por los detalles técnicos de la compilación; solo debes saber que tu código fuente
pasa por el preprocesador, la salida de eso pasa por el compilador, y luego eso produce un ejecutable para
6
Sé que alguien me discutirá esto, pero debe estar al menos entre los tres primeros, ¿verdad?
7
Bueno, técnicamente hay más de dos, pero bueno, finjamos que hay dos, ¿verdad?
8
[Link]
9
[Link]
Chapter 2. Hello, World! 7
que lo ejecutes.
¿Qué hay del resto de la línea? ¿Qué es <stdio.h>? Eso es lo que se conoce como un archivo de encabezado.
Es el “.h” al final lo que lo delata. De hecho, es el archivo de encabezado de “Entrada/Salida Estándar” (stdio
: STanDar Input/Output) que llegarás a conocer y amar. Nos da acceso a un montón de funcionalidades de
E/S 10 . Para nuestro programa de demostración, estamos mostrando la cadena “¡Hola, Mundo!”, por lo que en
particular necesitamos acceso a la función printf() para hacer esto. El archivo <stdio.h> nos proporciona
este acceso. Básicamente, si intentáramos usar printf() sin #include <stdio.h>, el compilador se nos
habría quejado.
¿Cómo supe que necesitaba #include <stdio.h>para printf()? Respuesta: está en la documentación.
Si estás en un sistema Unix, man 3 printf te dirá justo al principio de la página del manual qué archivos de
encabezado se requieren o consulta la sección de referencia en este libro. :-)
¡Santo cielo! Todo eso fue para cubrir la primera línea. Pero, seamos sinceros, ha sido completamente
diseccionada. ¡No quedará ningún misterio!
Así que toma un respiro… repasa el código de muestra. Solo quedan un par de líneas fáciles.
¡Bienvenido de nuevo de tu descanso! Sé que realmente no tomaste un descanso; solo te estaba haciendo una
broma.
La siguiente linea es main(). Esta es la definición de la función main(); todo lo que está entre las llaves ({
y }) es parte de la definición de la función.
(¿Cómo se llama a una función? La respuesta está en la línea printf(), pero llegaremos a ella en un minuto).
La función main, es muy especial, se destaca sobre las demás ya que es la función que se llamará automática-
mente cuando tu programa comienza a ejecutarse. Nada de tu código se llama antes de main(). En el caso
de nuestro ejemplo, esto funciona bien, ya que todo lo que queremos hacer es imprimir una línea y salir.
Otra cosa: una vez que el programa se ejecute, más allá del final de main() y por debajo de la llave de cierre,
el programa terminará y volverás a tu símbolo del sistema / Terminal / Consola.
Así que, sabemos que ese programa ha traído un fichero de cabecera, stdio.h, y ha declarado una función
main() que se ejecutará cuando se inicie el programa. ¿Cuáles son las bondades de main()?
Me alegra mucho que lo hayas preguntado. ¡De verdad! Solo tenemos una ventaja: una llamada a la función
printf(). Puedes darte cuenta de que esto es una llamada a una función y no una definición de función de
varias maneras, pero un indicador es la falta de llaves después de ella. Y terminas la llamada a la función
con un punto y coma para que el compilador sepa que es el final de la expresión. Verás que estarás poniendo
puntos y comas después de casi todo.
Estás pasando un argumento a la función printf(): una cadena que se imprimirá cuando la llames. Oh, sí,
¡estamos llamando a una función! ¡Somos geniales! Espera, espera, no te pongas arrogante. ¿Qué es ese
loco \n al final de la cadena? Bueno, la mayoría de los caracteres en la cadena se imprimirán tal como están
almacenados. Pero hay ciertos caracteres que no se pueden imprimir bien en pantalla y que están incrustados
como códigos de barra invertida de dos caracteres. Uno de los más populares es \n (se lee “barra invertida-N”
o simplemente “nueva línea”) que corresponde al carácter nueva línea. Este es el carácter que hace que la
impresión continúe al principio de la siguiente línea, en lugar de la actual. Es como presionar enter al final
de la línea.
Así que copia ese código en un archivo llamado hello.c y compílalo. En una plataforma similar a Unix
(por ejemplo, Linux, BSD, Mac o WSL), desde la línea de comandos lo compilarás con un comando como
este:
10
Técnicamente, contiene directivas de preprocesador y prototipos de funciones (más sobre eso adelante) para necesidades comunes
de entrada y salida.
Chapter 2. Hello, World! 8
./hello
Hello, World!
11
[Link]
Chapter 2. Hello, World! 9
El -o significa “salida a este archivo” (Output)12 . Y al final está hello.c, que es el nombre del archivo que
queremos compilar.
Si tu código fuente está dividido en varios archivos, puedes compilarlos todos juntos (casi como si fueran un
solo archivo, aunque las reglas son más complejas que eso) colocando todos los archivos .c en la línea de
comandos:
2.7 Versiones de C
C ha recorrido un largo camino a lo largo de los años, y ha tenido muchas versiones numeradas para describir
el dialecto del lenguaje estás utilizando.
Estos generalmente se refieren al año de la especificación.
Los más famosos son C89, C99, C11 y C2x. Nos centraremos en este último en el libro.
Pero aquí tienes una tabla más completa:
12
Si no le proporcionas un nombre de archivo de salida, por defecto se exportará a un archivo llamado [Link]—este nombre de archivo
tiene sus raíces en la historia profunda de Unix.
13
[Link]
Chapter 2. Hello, World! 10
Version Descripción
K&R C En 1978, la versión original. Nombrada en honor a Brian Kernighan y
Dennis Ritchie. Ritchie diseñó y codificó el lenguaje, y Kernighan
coescribió el libro sobre él. Hoy en día rara vez se ve código original de
K&R. Si lo ves, se verá extraño, como el inglés medio, luce extraño para los
lectores de inglés moderno.
C89, ANSI C, C90 En 1989, el American National Standards Institute (ANSI) produjo una
especificación del lenguaje C que marcó el tono para C que persiste hasta
hoy. Un año después, la responsabilidad pasó a la Organización Internacional
de Normalización (ISO), que produjo el estándar C90, idéntico al de ANSI.
C95 Una adición mencionada raramente a C89 que incluía soporte para
caracteres.
C99 La primera gran revisión con muchas adiciones al lenguaje. Lo que la
mayoría de la gente recuerda es la adición de los comentarios de estilo //.
Esta es la versión más popular de C en uso hasta la fecha de esta escritura.
C11 Esta actualización mayor incluye soporte para Unicode y multi-threading.
Ten en cuenta que si comienzas a usar estas características del lenguaje,
podrías estar sacrificando la portabilidad en lugares que aún están usando
C99. Sin embargo, honestamente, 1999 ya fue hace un tiempo.
C17, C18 Actualización de corrección de errores para C11. C17 parece ser el nombre
oficial, pero la publicación se retrasó hasta 2018. Según tengo entendido,
ambos términos son intercambiables, prefiriéndose C17.
C2x Lo que viene a continuación se espera que eventualmente se convierta en
C23.
Puedes forzar a GCC a usar uno de estos estándares con el argumento de línea de comandos -std=. Si quieres
que sea estricto con el estándar, añade -pedantic
Por ejemplo:
Para este libro, compilo programas para C2x con todas las advertencias activadas:
Variables y Declaraciones
“¿Para hacer un mundo se necesitan de todo tipo de personas, ¿no es así, Padre?”
“Así es, hijo mío, así es.”
—El capitán pirata Thomas Bartholomew Red al Padre, Piratas
Puede haber muchas cosas en un programa en C.
Sí.
Y por varias razones, será más fácil para todos nosotros si clasificamos algunos de los tipos de cosas que
puedes encontrar en un programa, para que podamos ser claros sobre lo que estamos hablando.
3.1 Variables
Se dice que “las variables contienen valores”. Pero otra manera de pensarlo es que una variable es un nombre
legible por humanos que se refiere a algún dato en la memoria.
Vamos a tomarnos un momento para echar un vistazo a los punteros. No te preocupes por esto.
Puedes pensar en la memoria como un gran array de bytes1 . Los datos se almacenan en este “array”2 . Si un
número es más grande que un solo byte, se almacena en múltiples bytes. Debido a que la memoria es como
un array, cada byte de memoria puede ser referido por su índice. Este índice en la memoria también se llama
una dirección, o una ubicación, o un puntero.
Cuando tienes una variable en C, el valor de esa variable está en la memoria en algún lugar, en alguna
dirección. Por supuesto. Después de todo, ¿dónde más estaría? Pero es un dolor referirse a un valor por su
dirección numérica, así que le damos un nombre en su lugar, y eso es lo que es una variable.
La razón por la que menciono todo esto es doble:
1. Va a hacer que sea más fácil entender las variables de puntero más adelante: ¡son variables que con-
tienen la dirección de otras variables!
2. También va a hacer que sea más fácil entender los punteros más adelante.
Así que una variable es un nombre para algunos datos que están almacenados en la memoria en alguna
dirección.
1
Un “byte” es típicamente un número binario de 8 bits. Piensa en ello como un entero que solo puede contener valores del 0 al 255,
inclusive. Técnicamente, C permite que los bytes sean de cualquier número de bits y si quieres referirte inequívocamente a un número
de 8 bits, deberías usar el término octeto. Pero los programadores asumirán que te refieres a 8 bits cuando dices “byte” a menos que
especifiques lo contrario.
2
Estoy simplificando mucho cómo funciona la memoria moderna aquí. Pero el modelo mental funciona, así que por favor perdóname.
11
Chapter 3. Variables y Declaraciones 12
C hace un esfuerzo por convertir automáticamente entre la mayoría de los tipos numéricos cuando se lo pides,
pero, aparte de eso, todas las conversiones son manuales, en particular entre cadenas y números.
Casi todos los tipos en C son variantes de estos tipos básicos.
Antes de poder usar una variable, debes declarar esa variable y decirle a C qué tipo de datos contiene. Una
vez declarada, el tipo de variable no puede cambiarse más tarde durante la ejecución. Lo que estableces es
lo que es, hasta que salga de alcance y sea reabsorbida en el universo.
Tomemos nuestro código anterior de “Hola, mundo” y agreguemos un par de variables a él:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i; // Almacena enteros con signo, por ejemplo, -3, -2, 0, 1, 10.
6 float f; // Almacena números de punto flotante con signo, por ejemplo, -3.1416.
7
¡Listo! Hemos declarado un par de variables. Aún no las hemos usado y ambas están sin inicializar. Una con-
tiene un número entero y la otra contiene un número de punto flotante (un número real, si tienes conocimientos
de matemáticas).
3
Estoy siendo un poco impreciso aquí. Técnicamente, 3.14159 es del tipo double, pero aún no hemos llegado allí y quiero que
asocies float con “Punto Flotante”, y C convertirá ese tipo felizmente en un float. En resumen, no te preocupes por ello hasta más
adelante.
4
Lee esto como “puntero a un char” o “char pointer”. “Char” por carácter. Aunque no puedo encontrar un estudio, parece anecdótica-
mente que la mayoría de las personas pronuncian esto como “char”, una minoría dice “car”, y algunos pocos dicen “care”. Hablaremos
más sobre los punteros más adelante.
Chapter 3. Variables y Declaraciones 13
Las variables no inicializadas tienen un valor indeterminado5 . Deben ser inicializadas o de lo contrario debes
asumir que contienen algún número absurdo.
Este es uno de los puntos donde C puede “atraparte”. En mi experiencia, la mayor parte del tiempo,
el valor indeterminado es cero… ¡pero puede variar de ejecución en ejecución! Nunca asumas que el
valor será cero, incluso si ves que lo es. Siempre inicializa explícitamente las variables a algún valor
antes de usarlasa .
a
Esto no es estrictamente 100% cierto. Cuando aprendamos sobre la duración de almacenamiento estática, descubrirás que
algunas variables se inicializan automáticamente a cero. Pero lo seguro es siempre inicializarlas.
1 int main(void)
2 {
3 int i;
4
7 printf("Hello, World!\n");
8 }
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i = 2;
6 float f = 3.14;
7 char *s = "Hello, world!"; // char * ("Puntero a caracter") es del tipo String
8
Y la salida será:
De esta manera, printf() podría ser similar a varios tipos de cadenas de formato o cadenas parametrizadas
en otros lenguajes con los que estás familiarizado.
5
Coloquialmente, decimos que tienen valores “aleatorios”, pero no son realmente—ni siquiera pseudo-realmente—números aleato-
rios.
Chapter 3. Variables y Declaraciones 14
Históricamente, C no tenía un tipo booleano, y algunos podrían argumentar que todavía no lo tiene.
En C, 0 significa “falso”, y cualquier valor no-cero significa “verdadero”.
Entonces 1 es verdadero. Y -37 es verdadero. Y 0 es falso.
Puedes simplemente declarar tipos booleanos como int:
int x = 1;
if (x) {
printf("x es verdadero!\n");
}
Si incluyes #include <stdbool.h>, también obtienes acceso a algunos nombres simbólicos que podrían
hacer que las cosas parezcan más familiares, específicamente un tipo bool y los valores true y false:
1 #include <stdio.h>
2 #include <stdbool.h>
3
4 int main(void) {
5 bool x = true;
6
7 if (x) {
8 printf("x es verdadero!\n");
9 }
10 }
Pero estos son idénticos a usar valores enteros para verdadero y falso. Son solo una fachada para que las
cosas luzcan bien.
Hay variantes abreviadas para todo lo anterior. Cada una de esas líneas podría escribirse de manera más
concisa como:
Chapter 3. Variables y Declaraciones 15
No hay operador de exponenciación en C. Tendrás que usar una de las variantes de la función pow() de
math.h.
¡Vamos a adentrarnos en algunas cosas más extrañas que es posible que no encuentres en tus otros lenguajes!
¡Qué lío! Te acostumbrarás a medida que lo leas. Para ayudar un poco, reescribiré la expresión anterior
usando declaraciones if:
// Esta expresión
if (x > 10)
y += 17;
else
y += 37;
Compara esos dos hasta que veas cada uno de los componentes del operador ternario.
Otro ejemplo, el cual imprime si un número almacenado en x es par o impar sería:
Muy comúnmente, estos se utilizan simplemente como versiones más cortas de:
i += 1; // Suma 1 a i
i -= 1; // Resta 1 a i
pero los astutos bribones son un poco más sutilmente diferentes que eso.
Echemos un vistazo a esta variante, pre-incremento y pre-decremento:
i = 10;
j = 5 + i++; // Calcula 5 + i, _luego_ incrementa i
i = 10;
j = 5 + ++i; // Incrementa i, _luego_ calcula 5 + i
Esta técnica se usa frecuentemente con el acceso y la manipulación de arreglos y punteros. Te da una manera
de usar el valor en una variable y también incrementar o decrementar ese valor antes o después de que se use.
Pero, con mucho, el lugar más común donde verás esto es en un bucle for:
Parece un poco tonto, ¿verdad? Porque podrías simplemente reemplazar la coma, con un punto y coma, ¿no
es así?
Chapter 3. Variables y Declaraciones 17
Pero eso es un poco diferente. El segundo caso son dos expresiones separadas, mientras que el primero es
una sola expresión.
Con el operador coma, el valor de la expresión coma es el valor de la expresión más a la derecha:
x = (1, 2, 3);
Pero incluso eso es bastante forzado. Un lugar común donde se usa el operador coma es en los bucles for
para hacer múltiples cosas en cada sección de la declaración:
A == B; // Verdadero si A es equivalente a B
A != B; // Verdadero si A no es equivalente a B
A < B; // Verdadero si A es menor que B
A > B; // Verdadero si A es más grande que B
A <= B; // Verdadero si A es menor o igual que B
A >= B; // Verdadero si A es mayor o igual que B
¡No mezcles la asignación (=) con la comparación (==)! Usa dos signos iguales para comparar y uno para
asignar.
Podemos usar las expresiones de comparación con declaraciones if:
if (a <= 10)
printf("Exito!\n");
! tiene mayor precedencia que los otros operadores booleanos, por lo que debemos usar paréntesis en ese
caso.
Por supuesto, eso es simplemente lo mismo que:
if (x >= 12)
printf("x no es menor que 12\n");
int a = 999;
6
El _t es abreviatura de type (Tipo).
Chapter 3. Variables y Declaraciones 19
Recuerda: es el tamaño en bytes del tipo de la expresión, no el tamaño de la expresión en sí. Es por eso que
el tamaño de 2+7 es el mismo que el tamaño de a—ambos son de tipo int. Revisaremos este número 4 en
el próximo bloque de código…
…Donde veremos que puedes obtener el sizeof de un tipo (nota que los paréntesis son requeridos alrededor
del nombre de un tipo, a diferencia de una expresión):
Es importante tener en cuenta que sizeof es una operación en tiempo de compilación 7 . El resultado de la
expresión se determina completamente en tiempo de compilación, no en tiempo de ejecución.
Más adelante haremos uso de esto.
Esto también a veces se escribe en una línea separada. (Los espacios en blanco son en gran medida irrele-
vantes en C—no es como en Python.)
if (x == 10)
printf("x es 10\n");
Pero ¿qué pasa si quieres que ocurran varias cosas debido a la condición? Puedes usar llaves para marcar un
bloque o declaración compuesta.
if (x == 10) {
printf("x es 10\n");
printf("Y también esto ocurre cuando x es 10\n");
}
if (x == 10) {
printf("x es 10\n");
}
Algunos desarrolladores sienten que el código es más fácil de leer y evita errores como este, donde visual-
mente parece que las cosas están dentro del bloque if, pero en realidad no lo están.
7
Excepto con arreglos de longitud variable—pero eso es una historia para otro momento.
Chapter 3. Variables y Declaraciones 20
if (x == 10)
printf("Esto sucede si x es 10\n");
printf("Esto sucede SIEMPRE\n"); // ¡Sorpresa! ¡Incondicional!
while, for y los demás constructos de bucles funcionan de la misma manera que los ejemplos anteriores.
Si deseas hacer múltiples cosas en un bucle o después de un if, envuélvelas en llaves.
En otras palabras, el if ejecutará lo que esté después de él. Y eso puede ser una sola declaración o un bloque
de declaraciones.
int i = 10;
if (i > 10) {
printf("Sí, i es mayor que 10.\n");
printf("Y esto también se imprimirá si i es mayor que 10.\n");
}
En el código de ejemplo, el mensaje se imprimirá si i es mayor que 10, de lo contrario, la ejecución continúa
en la siguiente línea. Observa las llaves después de la instrucción if; si la condición es verdadera, se ejecutará
la primera instrucción o expresión justo después del if, o bien, se ejecutará el conjunto de código dentro de las
llaves después del if. Este tipo de comportamiento de bloque de código es común en todas las instrucciones.
Por supuesto, dado que C es divertido de esta manera, también puedes hacer algo si la condición es else con
una cláusula else en tu if:
int i = 99;
if (i == 10)
printf("i es 10!\n");
else {
printf("i definitivamente no es 10.\n");
printf("Que, francamente, me irrita un poco.\n");
}
Y puedes incluso encadenar estos para probar una variedad de condiciones, como esto:
int i = 99;
if (i == 10)
printf("i es 10!\n");
else if (i == 20)
printf("i es 20!\n");
Chapter 3. Variables y Declaraciones 21
else if (i == 99) {
printf("i es 99! Mi favorito\n");
printf("No puedo decirte lo feliz que estoy.\n");
printf("En serio.\n");
}
else
printf("i es algún número loco que nunca he escuchado antes.\n");
Si vas por ese camino, asegúrate de revisar la declaración switch para una solución potencialmente mejor.
La única limitación es que switch solo funciona con comparaciones de igualdad con números constantes. La
cascada if-else anterior podría verificar desigualdades, rangos, variables o cualquier otra cosa que puedas
crear en una expresión condicional.
int i = 0;
printf("¡Todo hecho!\n");
Así se obtiene un bucle básico. C también tiene un bucle for que habría sido más limpio para ese ejemplo.
Un uso no poco común de while es para bucles infinitos donde se repite mientras es verdadero:
while (1) {
printf("1 es siempre cierto, así que esto se repite para siempre.\n");
}
i = 10;
i = 10;
do {
printf("do-while: i es %d\n", i);
i++;
} while (i < 10);
printf("¡Todo hecho!\n");
Observa que en ambos casos, la condición del bucle es falsa inmediatamente. Así que en el while, el bucle
falla, y el siguiente bloque de código nunca se ejecuta. Con el do-while, sin embargo, la condición se
comprueba después de que se ejecute el bloque de código, por lo que siempre se ejecuta al menos una vez.
En este caso, imprime el mensaje, incrementa i, falla la condición y continúa con la salida “¡Todo hecho!
La moraleja de la historia es la siguiente: si quieres que el bucle se ejecute al menos una vez, sin importar la
condición del bucle, usa do-while.
Todos estos ejemplos podrían haberse hecho mejor con un bucle for. Hagamos algo menos determinista:
¡repetir hasta que salga un cierto número aleatorio!
4 int main(void)
5 {
6 int r;
7
8 do {
9 r = rand() % 100; // Obtener un número aleatorio entre 0 y 99
10 printf("%d\n", r);
11 } while (r != 37); // Repetir hasta que aparezca 37
12 }
Nota al margen: ¿lo has hecho más de una vez? Si lo hiciste, ¿te diste cuenta de que volvió a aparecer
la misma secuencia de números? Y otra vez. ¿Y otra vez? Esto se debe a que rand() es un generador
de números pseudoaleatorios que debe ser sembrado con un número diferente para generar una secuencia
Chapter 3. Variables y Declaraciones 23
i = 0;
while (i < 10) {
printf("i es %d\n", i);
i++;
}
Así es, hacen exactamente lo mismo. Pero puedes ver cómo la sentencia for es un poco compacta y agradable
a la vista. (Los usuarios de JavaScript apreciarán plenamente sus orígenes en C en este punto).
Está dividida en tres partes, separadas por punto y coma. La primera es la inicialización, la segunda es la
condición del bucle, y la tercera es lo que debe ocurrir al final del bloque si la condición del bucle es verdadera.
Estas tres partes son opcionales.
for (inicializar cosas; bucle si esto es cierto; hacer esto después de cada bucle)
Tenga en cuenta que el bucle no se ejecutará ni una sola vez si la condición del bucle comienza siendo falsa.
Curiosidad del bucle ‘for
Puedes usar el operador coma para hacer múltiples cosas en cada cláusula del bucle for.
8
[Link]
Chapter 3. Variables y Declaraciones 24
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int goat_count = 2; // goat_count = contador de cabras
6
7 switch (goat_count) {
8 case 0:
9 printf("No tienes cabras :(\n");
10 break;
11
12 case 1:
13 printf("Solo tienes una cabra\n");
14 break;
15
16 case 2:
17 printf("Tienes un par de cabras\n");
18 break;
19
20 default:
21 printf("¡Tienes una gran cantidad de cabras!\n");
22 break;
23 }
24 }
En ese ejemplo, el switch saltará al case 2 y ejecutará desde allí. Cuando (si) llega a un break, salta fuera
del switch.
Además, puede que veas la etiqueta default en la parte inferior. Esto es lo que ocurre cuando ningún caso
coincide.
Cada case, incluyendo default, es opcional. Y pueden ocurrir en cualquier orden, pero es realmente típico
que default, si lo hay, aparezca último.
Así que todo actúa como una cascada if-else:
if (goat_count == 0)
printf("No tienes cabras\n");
else if (goat_count == 1)
Chapter 3. Variables y Declaraciones 25
switch (x) {
case 1:
printf("1\n");
// Falla el salto! Sigue ejecutando!
case 2:
printf("2\n");
break;
case 3:
printf("3\n");
break;
}
Si x == 1, el switch irá primero al caso 1, imprimirá el 1, pero luego simplemente continúa con la siguiente
línea de código… ¡que imprime 2!
Y entonces, por fin, llegamos a un break así que saltamos del switch.
si x == 2, entonces simplemente entramos dentro del case 2, imprimimos 2, y break como es normal.
Al no tener un break se falla la salida.
Consejo de experto: Siempre ponga un comentario en el código en el que tiene intención de fallar la salida,
como he hecho yo más arriba. Evitará que otros programadores se pregunten si realmente querías hacer eso.
De hecho, este es uno de los lugares comunes para introducir errores en los programas en C: olvidar poner
un break en tu case. Tienes que hacerlo si no quieres simplemente pasar al siguiente caso9 .
Antes dije que switch funciona con tipos enteros– mantenlo así. No utilices tipos de coma flotante o cadenas.
Una laguna legal aquí es que puedes usar tipos de caracteres porque estos son secretamente números enteros.
Por lo tanto, esto es perfectamente aceptable:
char c = 'b';
9
Esto se consideró tal peligro que los diseñadores del Lenguaje de Programación Go hicieron break por defecto; tienes que usar
explícitamente la sentencia fallthrough (fallar la salida) de Go si quieres pasar al siguiente caso
Chapter 3. Variables y Declaraciones 26
switch (c) {
case 'a':
printf("La letra es 'a'!\n");
break;
case 'b':
printf("La letra es 'b'!\n");
break;
case 'c':
printf("La letra es 'c'!\n");
break;
}
Finalmente, puedes usar enum en switch ya que también son tipos enteros. Pero más sobre esto en el capítulo
enum.
Chapter 4
Funciones
“Señor, no en un ambiente como éste. Por eso también he sido programado para más de treinta fun-
ciones secundarias que…”_>
—C3PO, antes de ser interrumpido bruscamente, informando de un número ya poco impresionante
de funciones adicionales, Star Wars script
Muy parecido a otros lenguajes a los que estás acostumbrado, C tiene el concepto de funciones.
Las funciones pueden aceptar una variedad de argumentos y devolver un valor. Sin embargo, hay algo
importante: los tipos de argumentos y valores de retorno están predeclarados,—¡porque así lo prefiere C!
Veamos una función. Esta es una función que toma un int como argumento, y devuelve un int.
1 #include <stdio.h>
2
8 int main(void)
9 {
10 int i = 10, j;
11
12 j = plus_one(i); // La "llamada"
13
27
Chapter 4. Funciones 28
15 }
Antes de que se me olvide, fíjate en que he definido la función antes de usarla. Si no lo hubiera hecho,
el compilador aún no la conocería al compilar main() y habría dado un error de llamada a función
desconocida. Hay una forma más adecuada de hacer el código anterior con prototipos de función,
pero hablaremos de eso más adelante.
Observa también que main() ¡es una función!
Devuelve un int.
¿Pero qué es eso de void? Es una palabra clave para indicar que la función no acepta argumentos.
También puede devolver void para indicar que no devuelve ningún valor:
1 #include <stdio.h>
2
5 void hello(void)
6 {
7 printf("Hello, world!\n");
8 }
9
10 int main(void)
11 {
12 hello(); // Imprime "Hello, world!"
13 }
1 #include <stdio.h>
2
3 void increment(int a)
4 {
5 a++;
6 }
7
8 int main(void)
9 {
10 int i = 10;
11
Chapter 4. Funciones 29
12 increment(i);
13
A primera vista, parece que i es 10, y lo pasamos a la función increment(). Allí el valor se incrementa,
así que cuando lo imprimimos, debe ser 11, ¿no?
“Acostúmbrate a la decepción.”
—El temible pirata Roberts, La princesa prometida
Pero no es 11… ¡imprime 10! ¿Cómo?
Se trata de que las expresiones que pasas a las funciones se copian en sus parámetros correspondientes. El
parámetro es una copia, no el original
Así que i es 10 en main(). Y se lo pasamos a increment(). El parámetro correspondiente se llama a en
esa función.
Y la copia ocurre, como si fuera una asignación. Más o menos, a = i. Así que en ese punto, a es 10. Y en
main(), i es también 10.
Entonces incrementamos a a 11. ¡Pero no estamos tocando i en absoluto! Sigue siendo 10.
Finalmente, la función está completa. Todas sus variables locales se descartan (¡adiós, a!) y volvemos a
main(), donde i sigue siendo 10.
1 #include <stdio.h>
2
5 int main(void)
6 {
7 int i;
8
12 i = foo();
13
Si no declaras tu función antes de usarla (ya sea con un prototipo o con su definición), estás realizando
algo llamado declaración implí[Link] estaba permitido en el primer estándar C (C89), y ese estándar tiene
reglas al respecto, pero ya no está permitido hoy en día. Y no hay ninguna razón legítima para confiar en
ello en código nuevo.
Puede que notes algo en el código de ejemplo que hemos estado utilizando… Es decir, ¡hemos estado usando
la vieja función printf() sin definirla ni declarar un prototipo! ¿Cómo nos libramos de esta ilegalidad?
En realidad, no lo hacemos. Hay un prototipo; está en ese fichero de cabecera stdio.h que incluimos con
#include, ¿recuerdas? ¡Así que seguimos siendo legales, oficial!
Aunque la especificación dice que el comportamiento en este caso es como si hubieras indicado void
(C11§[Link]¶14), el tipo void está ahí por una razón. Utilícelo.
Pero en el caso de un prototipo de función, hay una diferencia significativa entre usar void y no:
1
Nunca digas “nunca”.
Chapter 4. Funciones 31
void foo();
void foo(void); // ¡No es lo mismo!
Dejar void fuera del prototipo indica al compilador que no hay información adicional sobre los parámetros
de la función. De hecho, desactiva toda la comprobación de tipos.
Con un prototipo definitivamente use void cuando tenga una lista de parámetros vacía.
Chapter 5
1
Típicamente. Estoy seguro de que hay excepciones en los oscuros pasillos de la historia de la informática
2
Un byte es un número formado por no más de 8 dígitos binarios, o bits para abreviar. Esto significa que en dígitos decimales como
los que usaba la abuela, puede contener un número sin signo entre 0 y 255, ambos inclusive
32
Chapter 5. Punteros… ¡Poder con miedo! 33
Datos curiosos sobre la memoria: Cuando tienes un tipo de datos (como el típico int) que utiliza
más de un byte de memoria, los bytes que componen los datos son siempre adyacentes en memoria. A
veces están en el orden que esperas, y a veces noa . Aunque C no garantiza ningún orden de memoria
en particular (depende de la plataforma), en general es posible escribir código de forma independiente
de la plataforma sin tener que tener en cuenta estos molestos ordenamientos de bytes.
a
El orden en que vienen los bytes se denomina endianidad del número. Los sospechosos habituales son big-endian (con
el byte más significativo primero) y little-endian (con el byte más significativo al final), o, ahora poco común, mixed-endian
(con los bytes más significativos en otro lugar)
Así que, de todos modos, si podemos ponernos manos a la obra y poner un redoble de tambores y algo
de música premonitoria para la definición de puntero, un puntero es una variable que contiene una direc-
ción. Imagina la partitura clásica de 2001: Una Odisea del Espacio en este punto. Ba bum ba bum ba bum
¡BAAAAH!
Vale, quizás un poco exagerado, ¿no? No hay mucho misterio sobre los punteros. Son la dirección de los
datos. Al igual que una variable int puede contener el valor 12, una variable puntero puede contener la
dirección de los datos.
Esto significa que todas estas cosas son lo mismo, es decir, un número que representa un punto en la memoria:
• Índice en memoria (si piensas en la memoria como una gran matriz)
• Dirección
• Ubicación
Voy a usarlos indistintamente. Y sí, acabo de incluir localización porque nunca hay suficientes palabras que
signifiquen lo mismo.
Y una variable puntero contiene ese número de dirección. Al igual que una variable float puede contener
3.14159.
Imagina que tienes un montón de notas Post-it® numeradas en secuencia con su dirección. (La primera está
en el índice numerado 0, la siguiente en el índice 1, y así sucesivamente).
Además del número que representa su posición, también puedes escribir otro número de tu elección en cada
uno. Puede ser el número de perros que tienes. O el número de lunas alrededor de Marte…
…O, podría ser el índice de otra nota Post-it
Si has escrito el número de perros que tienes, eso es sólo una variable normal. Pero si has escrito ahí el índice
de otro Post-it, eso es un puntero. ¡Apunta a la otra nota!
Otra analogía podría ser con las direcciones de las casas. Puedes tener una casa con ciertas cualidades, patio,
tejado metálico, solar, etc. O puedes tener la dirección de esa casa. La dirección no es lo mismo que la casa
en sí. Una es una casa completa, y la otra son sólo unas líneas de texto. Pero la dirección de la casa es un
puntero a esa casa. No es la casa en sí, pero te dice dónde encontrarla.
Y podemos hacer lo mismo en el ordenador con los datos. Puedes tener una variable de datos que contenga
algún valor. Y ese valor está en la memoria en alguna dirección. Y puedes tener una variable puntero
diferente, que contenga la dirección de esa variable de datos.
No es la variable de datos en sí, pero, como con la dirección de una casa, nos dice dónde encontrarla.
Cuando tenemos eso, decimos que tenemos un “puntero a” esos datos. Y podemos seguir el puntero para
acceder a los datos en sí.
(Aunque todavía no parece especialmente útil, todo esto se vuelve indispensable cuando se utiliza con lla-
madas a funciones. Ten paciencia conmigo hasta que lleguemos allí).
Chapter 5. Punteros… ¡Poder con miedo! 34
Así que si tenemos un int, digamos, y queremos un puntero a él, lo que queremos es alguna forma de obtener
la dirección de ese int, ¿verdad? Después de todo, el puntero sólo contiene la dirección de los datos. ¿Qué
operador crees que usaríamos para encontrar la dirección del int?
Pues bien, por una sorpresa que debe resultarle chocante a usted, amable lector, utilizamos el operador
dirección (que resulta ser un ampersand: “&”) para encontrar la dirección de los datos. Ampersand.
Así que para un ejemplo rápido, introduciremos un nuevo especificador de formato para printf() para que
puedas imprimir un puntero. Ya sabes cómo %d imprime un entero decimal, ¿verdad? Pues bien, %p imprime
un puntero. Ahora, este puntero va a parecer un número basura (y podría imprimirse en hexadecimal3 en
lugar de decimal), pero es simplemente el índice en memoria en el que se almacenan los datos. (O el índice
en memoria en el que se almacena el primer byte de datos, si los datos son multibyte). En prácticamente
todas las circunstancias, incluyendo ésta, el valor real del número impreso no es importante para usted, y lo
muestro aquí sólo para la demostración del operador de dirección.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i = 10;
6
El código anterior contiene un cast donde coaccionamos el tipo de la expresión &i para que sea del
tipo void*. Esto es para evitar que el compilador arroje una advertencia aquí. Esto es todo lo que no
hemos cubierto todavía, así que por ahora ignora el (void*) en el código de arriba y finge que no
está ahí.
En mi computadora, se imprime esto:
El valor de i es 10
Y su dirección es 0x7ffddf7072a4
Si tienes curiosidad, ese número hexadecimal es [Link].068 en decimal (base 10 como la que usaba
la abuela). Ese es el índice en memoria donde se almacenan los datos de la variable i. Es la dirección de i.
Es la ubicación de i. Es un puntero a i.
Espera-¿Tienes 140 terabytes de RAM? ¡Sí! ¿No? Pero me causa gracia, por supuesto que no
(ca. 2024). Los ordenadores modernos usan una tecnología milagrosa llamada memoria virtuala que
hace que los procesos piensen que tienen todo el espacio de memoria de tu ordenador para ellos solos,
independientemente de cuánta RAM física lo respalde. Así que aunque la dirección era ese enorme
número, está siendo mapeada a alguna dirección de memoria física más baja por el sistema de memoria
virtual de mi CPU. Este ordenador en particular tiene 16 GB de RAM (de nuevo, ca. 2024, pero uso
Linux, así que es suficiente). ¿Terabytes de RAM? Soy profesor, no un multimillonario punto-com.
Nada de esto es algo de lo que que preocuparse, excepto la parte en la que no soy fenomenalmente
rico.
a
[Link]
Es un puntero porque te permite saber dónde está i en la memoria. Al igual que una dirección escrita en un
trozo de papel te dice dónde puedes encontrar una casa en particular, este número nos indica en qué parte de
la memoria podemos encontrar el valor de i. Apunta a i.
3
Es decir, base 16 con dígitos 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, y F.
Chapter 5. Punteros… ¡Poder con miedo! 35
Una vez más, no nos importa cuál es el número exacto de la dirección, por lo general. Sólo nos importa que
es un puntero a “i”.
SERVICIOS DE LIMPIEZA ROBOTIZADA DE VIVIENDAS. SU VIVIENDA SERÁ DRÁSTICAMENTE MEJORADA O SERÁ DESPEDID
Bienvenidos a otra entrega de la Guía de Beej. La última vez que nos vimos estuvimos hablando de cómo
hacer uso de los punteros. Pues bien, lo que vamos a hacer es almacenar un puntero en una variable para
poder utilizarlo más adelante. Puedes identificar el tipo de puntero porque hay un asterisco (*) antes del
nombre de la variable y después de su tipo:
1 int main(void)
2 {
3 int i; // El tipo de i es "int"
4 int *p; // El tipo de p es "puntero a un int", o "int-pointer".
5 }
Así que.. aquí tenemos una variable, que es de tipo puntero, y puede apuntar a otros ints. Es decir, puede
contener la dirección de otros ints. Sabemos que apunta a ints, ya que es de tipo int* (léase “int-pointer”).
Cuando haces una asignación a una variable puntero, el tipo de la parte derecha de la asignación tiene que
ser del mismo tipo que la variable puntero. Afortunadamente para nosotros, cuando tomas la dirección de
(address-of) de una variable, el tipo resultante es un puntero a ese tipo de variable, por lo que asignaciones
como la siguiente son perfectas:
int i;
int *p; // p es un puntero, pero no está inicializado y apunta a basura
A la izquierda de la asignación, tenemos una variable de tipo puntero a int (int*), y a la derecha, tenemos
una expresión de tipo puntero a int ya que i es un int (porque la dirección de int te da un puntero a int).
La dirección de una cosa puede almacenarse en un puntero a esa cosa.
¿Lo entiendes? Sé que todavía no tiene mucho sentido ya que no has visto un uso “real” para la variable pun-
tero, pero estamos dando pequeños pasos aquí para que nadie se pierda. Así que ahora, vamos a presentarte
el operador anti-dirección-de. Es algo así como lo que sería address-of en Bizarro World.
5.3 Desreferenciación
Una variable puntero puede considerarse como referida a otra variable apuntando a ella. Es raro que oigas a
alguien en la tierra de C hablar de “referir” o “referencias”, pero lo traigo a colación sólo para que el nombre
de este operador tenga un poco más de sentido.
Cuando tienes un puntero a una variable (más o menos “una referencia a una variable”), puedes usar la
variable original a través del puntero referenciando el puntero. (Puedes pensar en esto como “despointerizar”
el puntero, pero nadie dice nunca “despointerizar”).
Chapter 5. Punteros… ¡Poder con miedo! 36
Volviendo a nuestra analogía, esto es vagamente como mirar la dirección de una casa y luego ir a esa casa.
Ahora bien, ¿qué quiero decir con “acceder a la variable original”? Bueno, si tienes una variable llamada i,
y tienes un puntero a i llamado p, ¡puedes usar el puntero desreferenciado p exactamente como si fuera la
variable original i!
Casi tienes conocimientos suficientes para manejar un ejemplo. El último dato que necesitas saber es el
siguiente: ¿qué es el operador de desreferencia? En realidad se llama operador de dirección, porque estás
accediendo a valores indirectamente a través del puntero. Y es el asterisco, otra vez: *. No lo confundas con
el asterisco que usaste antes en la declaración del puntero. Son el mismo carácter, pero tienen significados
diferentes en contextos diferentes4 .
He aquí un ejemplo en toda regla:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 int *p; // esto NO es una desreferencia--esto es un tipo "int*"
7
10 i = 10; // i es ahora 10
11 *p = 20; // lo que p señala (es decir, i!) es ahora 20!!
12
Recuerda que p contiene la dirección de i, como puedes ver donde hicimos la asignación a p en la línea 8.
Lo que hace el operador de indirección es decirle al ordenador que utilice el objeto al que apunta el puntero
en lugar de utilizar el propio puntero. De esta manera, hemos convertido *p en una especie de alias para i.
Genial, pero ¿por qué? ¿Por qué hacer algo de esto?
Pero, y esta es la parte inteligente: habremos configurado el puntero de antemano para que apunte a una
variable… ¡y entonces la función puede desreferenciar su copia del puntero para volver a la variable original!
La función no puede ver la variable en sí, ¡pero sí puede desreferenciar un puntero a esa variable!
Esto es análogo a escribir la dirección de una casa en un papel y luego copiarla en otro papel. Ahora tienes
dos punteros a esa casa, y ambos son igualmente buenos para llevarte a la casa misma.
En el caso de una llamada a una función, una de las copias se almacena en una variable puntero fuera del
ámbito de llamada, y la otra se almacena en una variable puntero que es el parámetro de la función.
Ejemplo: Volvamos a nuestra vieja función increment(), pero esta vez hagámosla de modo que realmente
incremente el valor en el ámbito de la llamada.
1 #include <stdio.h>
2
9 int main(void)
10 {
11 int i = 10;
12 int *j = &i; // Nota: la dirección de [address-of (&)]; lo convierte en un puntero a i
13
17 increment(j); // j es un int*--a i
18
¡Ok! Hay un par de cosas que ver aquí… la menor de ellas es que la función increment() toma un int*
como argumento. Le pasamos un int* en la llamada cambiando la variable int i a un int* usando el oper-
ador address-of (&). (Recuerda, un puntero contiene una dirección, así que hacemos punteros a variables
pasándolas por el operador address-of).
La función increment() obtiene una copia del puntero. Tanto el puntero original j (en main()) como
la copia de ese puntero p (el parámetro en increment()) apuntan a la misma dirección, la que contiene
el valor i. (De nuevo, por analogía, como dos trozos de papel con la misma dirección escrita en ellos).
Si desreferenciamos cualquiera de las dos, podremos modificar la variable original i. La función puede
modificar una variable en otro ámbito. ¡Muévete!
El ejemplo anterior a menudo se escribe de forma más concisa en la llamada simplemente utilizando address-
of en la lista de argumentos:
Como regla general, si quieres que la función modifique la cosa que estás pasando para que veas el resultado,
tendrás que pasar un puntero a esa cosa.
Chapter 5. Punteros… ¡Poder con miedo! 38
int *p;
p = NULL;
int *p = NULL;
A pesar de ser llamado el error del millon de dolares por su creador5 , el puntero NULL es un buen sentinela6
e indicador general de que un puntero aún no ha sido inicializado.
(Por supuesto, al igual que otras variables, el puntero apunta a basura a menos que le asignes explícitamente
que apunte a una dirección o a NULL).
int a;
int b;
int a, b; // Es lo mismo
int a;
int *p;
5
[Link]
6
[Link] value
Chapter 5. Punteros… ¡Poder con miedo! 39
Esto puede ser particularmente insidioso si el programador escribe la siguiente línea de código (válida) que
es funcionalmente idéntica a la anterior.
Así que echa un vistazo a esto y determina qué variables son punteros y cuáles no:
int *p;
Usted puede ver el código en la naturaleza con ese último sizeof allí. Recuerda que sizeof se refiere al
tipo de expresión, no a las variables de la expresión.
7
Las variables de tipo puntero son a, d, f e i, porque son las que tienen * delante
Chapter 6
Arrays
Por suerte, C tiene matrices. Ya sé que se considera un lenguaje de bajo nivel 1 , pero al menos incorpora
el concepto de arrays. Y como muchos lenguajes se inspiraron en la sintaxis de C, probablemente ya estés
familiarizado con el uso de [ y ] para declarar y usar matrices.
Pero C apenas tiene arrays. Como veremos más adelante, los arrays, en el fondo, son sólo azúcar sintáctico
en C—en realidad son todo punteros. Pero por ahora, usémoslos como arrays. Phew.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 float f[4]; // Declara un array de 4 floats
7
13 // Imprímelos todos:
14
1
Hoy en día, por lo menos
40
Chapter 6. Arrays 41
Cuando declaras un array, tienes que darle un tamaño. Y el tamaño tiene que ser fijo 2 .
En el ejemplo anterior, hicimos un array de 4 floats. El valor entre corchetes de la declaración nos lo indica.
Más tarde, en las líneas siguientes, accedemos a los valores de la matriz, estableciéndolos u obteniéndolos,
de nuevo con corchetes.
Espero que le suenen de alguno de los idiomas que ya conoce.
Si es un array de chars, entonces sizeof del array es el número de elementos, ya que sizeof(char) está
definido como 1. Para cualquier otro tipo, tienes que dividir por el tamaño de cada elemento.
Pero este truco sólo funciona en el ámbito en el que se definió el array. Si pasas el array a una función, no
funciona. Incluso si lo haces “grande” en la firma de la función:
Esto se debe a que cuando “pasas” arrays a funciones, sólo estás pasando un puntero al primer elemento, y
eso es lo que mide sizeof. Más sobre esto en la sección, Pasar arrays unidimensionales a funciones. más
abajo.
Otra cosa que puedes hacer con sizeof y arrays es obtener el tamaño de un array de un número fijo de
elementos sin declarar el array. Es como obtener el tamaño de un int con sizeof(int).
Por ejemplo, para ver cuántos bytes se necesitarían para un array de 48 dobless, puedes hacer esto:
2
De nuevo, en realidad no, pero las matrices de longitud variable -de las que no soy muy fan- son una historia para otro momento
3
Dado que los arrays son sólo punteros al primer elemento del array bajo el capó, no hay información adicional que registre la
longitud
4
Porque cuando pasas un array a una función, en realidad sólo estás pasando un puntero al primer elemento de ese array, no el array
“entero”
Chapter 6. Arrays 42
sizeof(double [48]);
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 int a[5] = {22, 37, 3490, 18, 95}; // Inicializar con estos valores
7
Nunca debes tener más elementos en tu inicializador de los que caben en el array, o el compilador se pondrá
de mal humor:
Pero (¡dato curioso!) puedes tener menos elementos en tu inicializador de los que caben en el array. Los ele-
mentos restantes de la matriz se inicializarán automáticamente con cero. Esto es cierto en general para todos
los tipos de inicializadores de matrices: si tienes un inicializador, todo lo que no se establezca explícitamente
a un valor se establecerá a cero.
// Es lo mismo que:
Es un atajo común ver esto en un inicializador cuando quieres poner un array entero a cero:
Lo que significa, “Haz el primer elemento cero, y luego automáticamente haz el resto cero, también”.
También puedes establecer elementos específicos del array en el inicializador, especificando un índice para
el valor. Cuando haces esto, C seguirá inicializando los valores subsiguientes por ti hasta que el inicializador
se agote, llenando todo lo demás con 0.
Para hacer esto, pon el índice entre corchetes con un = después, y luego establece el valor.
Aquí hay un ejemplo donde construimos un array:
Chapter 6. Arrays 43
Como hemos puesto el índice 5 como inicio para 55, los datos resultantes en el array son:
0 11 22 0 0 55 66 77 0 0
#define COUNT 5
0 0 3 2 1
Por último, también puedes hacer que C calcule el tamaño del array a partir del inicializador, simplemente
dejando el tamaño desactivado:
// Es lo mismo que:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 int a[5] = {22, 37, 3490, 18, 95};
7
22
37
3490
18
95
Chapter 6. Arrays 44
32765
1847052032
1780534144
-56487472
21890
¡Caramba! ¿Qué es esto? Bueno, resulta que imprimir el final de un array resulta en lo que los desarrolladores
de C llaman comportamiento indefinido. Hablaremos más sobre esta bestia más adelante, pero por ahora
significa: “Has hecho algo malo, y cualquier cosa podría pasar durante la ejecución de tu programa”.
Y por cualquier cosa, me refiero típicamente a cosas como encontrar ceros, encontrar números basura, o
bloquearse. Pero en realidad la especificación de C dice que en estas circunstancias el compilador puede
emitir código que haga cualquier cosa5 .
Versión corta: no hagas nada que cause un comportamiento indefinido. Nunca 6 .
int a[10];
int b[2][7];
int c[4][5][6];
Se almacenan en memoria en row-major order7 .Esto significa que en una matriz 2D, el primer índice de la
lista indica la fila y el segundo la columna. También puedes utilizar inicializadores en matrices multidimen-
sionales anidándolos:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int row, col;
6
(0,0) = 0
(0,1) = 1
(0,2) = 2
(0,3) = 3
(0,4) = 4
(1,0) = 5
(1,1) = 6
(1,2) = 7
(1,3) = 8
(1,4) = 9
1 0 0
0 1 0
0 0 1
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a[5] = {11, 22, 33, 44, 55};
6 int *p;
7
Esto es tan común de hacer en C que el lenguaje nos permite una forma abreviada:
8
Esto es técnicamente incorrecto, ya que un puntero a un array y un puntero al primer elemento de un array tienen tipos diferentes.
Pero podemos quemar ese puente cuando lleguemos a él
Chapter 6. Arrays 46
3 // Es lo mismo que:
4
Hacer referencia al nombre del array de forma aislada es lo mismo que obtener un puntero al primer elemento
del array. Vamos a utilizar esto ampliamente en los próximos ejemplos.
Pero espera un segundo… ¿no es p un int*? ¿Y *p nos da 11, lo mismo que a[0]? Sí. Estás empezando a
ver cómo se relacionan las matrices y los punteros en C.
1 #include <stdio.h>
2
24 int main(void)
25 {
26 int x[5] = {11, 22, 33, 44, 55};
27
28 times2(x, 5);
29 times3(x, 5);
30 times4(x, 5);
31 }
Todos esos métodos de enumerar el array como parámetro en la función son idénticos.
Chapter 6. Arrays 47
En el uso por parte de los habituales de C, la primera es la más común, con diferencia.
Y, de hecho, en la última situación, el compilador ni siquiera le importa qué número le pasas (aparte de que
tiene que ser mayor que cero9 ). No impone nada en absoluto.
Ahora que lo he dicho, el tamaño del array en la declaración de la función realmente importa cuando pasas
arrays multidimensionales a funciones, pero volveremos a eso.
1 #include <stdio.h>
2
14 int main(void)
15 {
16 int x[5] = {1, 2, 3, 4, 5};
17
18 double_array(x, 5);
19
Aunque pasamos el array como parámetro a que es de tipo int*, ¡mira cómo accedemos a él usando la
notación array con a[i]! Vaya. Esto está totalmente permitido.
Más adelante, cuando hablemos de la equivalencia entre arrays y punteros, veremos que esto tiene mucho
más sentido. Por ahora, es suficiente saber que las funciones pueden hacer cambios a los arrays que son
9
C11 §[Link]¶1 requiere que sea mayor que cero. Pero puede que veas código por ahí con arrays declarados de longitud cero al final
de structs y GCC es particularmente indulgente al respecto a menos que compiles con -pedantic. Este array de longitud cero era un
mecanismo para hacer estructuras de longitud variable. Desafortunadamente, es técnicamente un comportamiento indefinido acceder a
un array de este tipo aunque básicamente funcionaba en todas partes. C99 codificó un reemplazo bien definido llamado flexible array
members, del que hablaremos más adelante
Chapter 6. Arrays 48
visibles en el llamador.
1 #include <stdio.h>
2
12 int main(void)
13 {
14 int x[2][3] = {
15 {1, 2, 3},
16 {4, 5, 6}
17 };
18
19 print_2D_array(x);
20 }
En realidad, el compilador sólo necesita la segunda dimensión para poder calcular la distancia de memoria que
debe saltarse en cada incremento de la primera dimensión. En general, necesita conocer todas las dimensiones
excepto la primera.
Además, recuerda que el compilador hace una comprobación mínima de los límites en tiempo de compilación
(si tienes suerte), y C no hace ninguna comprobación de los límites en tiempo de ejecución.¡Sin cinturones
de seguridad! No te estrelles accediendo a elementos del array fuera de los límites.
10
Esto también es equivalente: void print_2D_array(int (*a)[3]), pero eso es más de lo que quiero entrar ahora
Chapter 7
"Hello, world!\n"
"This is a test."
"When asked if this string had quotes in it, she replied, \"It does.\""
Fíjate en el tipo: puntero a un char. La variable de cadena s es en realidad un puntero al primer carácter de
esa cadena, concretamente la H.
Y podemos imprimirlo con el especificador de formato %s (de String «cadena»):
49
Chapter 7. Strings (“Cadenas” de caracteres) 50
Esto significa que puedes utilizar la notación de matrices para acceder a los caracteres de una ca-
[Link] exactamente eso para imprimir todos los caracteres de una cadena en la misma línea:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char s[] = "Hello, world!";
6
Tenga en cuenta que estamos utilizando el especificador de formato %c para imprimir un solo carácter.
Además, fíjate en esto. El programa seguirá funcionando bien si cambiamos la definición de s para que sea
de tipo char*:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char *s = "Hello, world!"; // char* aqui
6
Y aún podemos utilizar la notación de matrices para imprimirlo. Esto es sorprendente, pero sólo porque aún
no hemos hablado de la equivalencia matriz/puntero. Pero esto es otra pista de que los arrays y los punteros
son la misma cosa, en el fondo.
Pero estas dos inicializaciones son sutilmente diferentes. Un literal de cadena, similar a un literal de número
entero, tiene su memoria gestionada automáticamente por el compilador. Con un entero, es decir, un dato
de tamaño fijo, el compilador puede gestionarlo con bastante facilidad. Pero las cadenas son una bestia de
bytes variables que el compilador domestica lanzándolas a un trozo de memoria, y dándote un puntero a él.
Esta forma apunta al lugar donde se colocó esa cadena. Típicamente, ese lugar está en una tierra lejana del
resto de la memoria de tu programa – memoria de sólo lectura – por razones relacionadas con el rendimiento
y la seguridad.
Así que recuerda: si tienes un puntero a un literal de cadena, ¡no intentes cambiarlo! Y si usas una cadena
entre comillas dobles para inicializar un array, no es realmente un literal de cadena.
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void)
5 {
6 char *s = "Hello, world!";
7
1
Aunque es cierto que C no rastrea la longitud de las cadenas
2
Si estás usando el juego de caracteres básico o un juego de caracteres de 8 bits, estás acostumbrado a que un carácter sea un byte.
Sin embargo, esto no es cierto en todas las codificaciones de caracteres
Chapter 7. Strings (“Cadenas” de caracteres) 52
La función strlen() devuelve el tipo size_t, que es un tipo entero por lo que se puede utilizar para
matemáticas de enteros. Imprimimos size_t con %zu.
El programa anterior imprime:
Así que con esto en mente, vamos a escribir nuestra propia función strlen() que cuenta caracteres en
una cadena hasta que encuentra un NUL.
El procedimiento es buscar en la cadena un único carácter NUL, contando a medida que avanzamos4 :
return count;
3
Esto es diferente del puntero NULL, y lo abreviaré NUL cuando hable del carácter frente a NULL para el puntero
4
Más adelante aprenderemos una forma más ordenada de hacerlo con aritmética de punteros
Chapter 7. Strings (“Cadenas” de caracteres) 53
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char s[] = "Hello, world!";
6 char *t;
7
8 // Esto hace una copia del puntero, ¡no una copia de la cadena!
9 t = s;
10
11 // Modificamos t
12 t[0] = 'z';
13
Si quieres hacer una copia de una cadena, tienes que copiarla byte a byte—pero esto es más fácil con la
función strcpy()5 .
Antes de copiar la cadena, asegúrate de que tienes espacio para copiarla, es decir, la matriz de destino que va
a contener los caracteres debe ser al menos tan larga como la cadena que estás copiando.
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void)
5 {
6 char s[] = "Hello, world!";
7 char t[100]; // Cada char es un byte, así que hay espacio de sobra
8
12 // Modificamos t
13 t[0] = 'z';
14
5
Hay una función más segura llamada strncpy() que probablemente deberías usar en su lugar, pero llegaremos a eso más tarde
Chapter 7. Strings (“Cadenas” de caracteres) 54
18 // Pero t ha cambiado
19 printf("%s\n", t); // "zello, world!"
20 }
Observe que con strcpy(), el puntero de destino es el primer argumento, y el puntero de origen es el
segundo. Una mnemotécnica que uso para recordar esto es que es el orden en el que habrías puesto t y s si
una asignación = funcionara para cadenas, con el origen a la derecha y el destino a la izquierda.
Chapter 8
Estructuras (Structs)
En C, tenemos algo llamado struct, que es un tipo definible por el usuario, el cual, contiene múltiples piezas
de datos, potencialmente de diferentes tipos.
Es una forma conveniente de agrupar múltiples variables en una sola. Esto puede ser beneficioso para pasar
variables a funciones (así sólo tienes que pasar una en lugar de muchas), y útil para organizar datos y hacer
el código más legible.
Si vienes de otro lenguaje, puede que estés familiarizado con la idea de clases y objetos. Estos no existen
en C, de forma nativa1 . Puedes pensar en una struct como una clase con sólo miembros de datos, y sin
métodos.
struct car {
char *name;
float price;
int speed;
};
Esto se hace a menudo en el ámbito global, fuera de cualquier función, para que la «estructura» esté disponible
globalmente.
Cuando haces esto, estás creando un nuevo tipo. El nombre completo del tipo es struct car. (No sólo
car—eso no funcionará).
55
Chapter 8. Estructuras (Structs) 56
Como en muchos otros lenguajes que lo robaron de C, vamos a usar el operador punto (.) para acceder a los
campos individuales.
Allí en las primeras líneas, establecemos los valores en la struct car, y luego en la siguiente parte, imprim-
imos esos valores.
struct car {
char *name;
float price;
int speed;
};
El hecho de que los campos del inicializador tengan que estar en el mismo orden, es un poco raro. Si alguien
cambia el orden en struct car, ¡podría romper el resto del código!
Podemos ser más específicos con nuestros inicializadores:
Ahora, es independiente del orden, en la declaración struct. Lo que sin duda es un código más seguro.
De forma similar a los inicializadores de array, cualquier designador de campo que falte, se inicializa a cero
(en este caso, sería .price, que he omitido).
Recuerda que cuando pasas algo a una función, se hace una copia de esa cosa para que la función opere sobre
ella, ya sea una copia de un puntero, un int, una struct, o cualquier otra cosa.
Hay básicamente dos casos en los que querrías pasar un puntero a la struct:
1. Necesitas que la función sea capaz de hacer cambios a la struct que fue pasada, y que esos cambios
se muestren en la llamada.
2. La struct es algo grande y es más caro copiarla en la pila que copiar un puntero3 .
Por estas dos razones, es mucho más común pasar un puntero a una estructura es una función, aunque no
es ilegal pasar solamente la estructura.
Intentemos pasar un puntero, haciendo una función que nos permita establecer el campo .price de la
struct car:
1 #include <stdio.h>
2
3 struct car {
4 char *name;
5 float price;
6 int speed;
7 };
8
9 int main(void)
10 {
11 struct car saturn = {.speed=175, .name="Saturn SL/2"};
12
Usted debe ser capaz de llegar a la firma de la función para set_price() con sólo mirar los tipos de los
argumentos que tenemos.
saturn es un struct car, así que &saturn debe ser la dirección del struct car, es decir, un puntero a un
struct car, un struct car*.
Y 799.99 es un float.
Así que la declaración de la función debe tener este aspecto:
Eso no funcionará porque el operador punto sólo funciona en structs… no funciona en punteros a structs.
3
Un puntero es probablemente de 8 bytes en un sistema de 64 bits
Chapter 8. Estructuras (Structs) 58
Entonces podemos desreferenciar la variable c para des-apuntarla y llegar a la propia struct. Dereferenciar
una struct car* resulta en la struct car a la que apunta el puntero, sobre la que deberíamos poder usar
el operador punto:
Y funciona. Pero es un poco engorroso teclear todos esos paréntesis y el asterisco. C tiene un azúcar sintáctico
llamado, operador flecha (arrow) que ayuda con eso.
Así que.. cuando accedemos a campos, ¿cuándo usamos punto, y cuándo usamos flecha?
• Si tienes una struct, usa punto (.).
• Si tienes un puntero a una struct, usa arrow/flecha (->).
struct car a, b;
b = a; // Copiar la estructura
Devolver una estructura (en lugar de un puntero a una) desde una función, también hace una copia similar
a la variable receptora.
Esto no es una «copia profunda»4 . Todos los campos se copian tal cual, incluyendo los punteros a cosas.
Si primero borras la struct a cero con memset()6 , entonces podría funcionar, aunque podría haber elemen-
tos extraños que puede que no se compare como usted espera7 .
6
[Link]
7
[Link]
Chapter 9
Archivo de Entrada/Salida
(Input/Output)
Ya hemos visto algunos ejemplos de E/S con printf() para hacer E/S en la consola.
Pero llevaremos estos conceptos un poco más lejos en este capítulo.
Resulta que ya los hemos estado utilizando implícitamente. Por ejemplo, estas dos llamadas son iguales:
printf("Hello, world!\n");
fprintf(stdout, "Hello, world!\n"); // printf a un fichero
60
Chapter 9. Archivo de Entrada/Salida (Input/Output) 61
Por ejemplo, en un shell POSIX (como sh, ksh, bash, zsh, etc.) en un sistema tipo Unix, podríamos ejecutar
un programa y enviar sólo la salida no error (stdout) a un fichero, y toda la salida error (stderr) a otro
fichero.
Por este motivo, debe enviar los mensajes de error graves a stderr en lugar de a stdout.
Más adelante se explica cómo hacerlo.
Hello, world!
Y vamos a escribir un programa para abrir el archivo, leer un carácter fuera de él, y luego cerrar el archivo
cuando hayamos terminado. ¡Ese es el plan!
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE *fp; // Variable para representar el archivo abierto
6
Mira como, cuando abrimos el fichero con fopen(), nos devolvió el FILE* para que pudiéramos usarlo más
tarde.
(Lo estoy omitiendo por brevedad, pero fopen() devolverá NULL si algo va mal, como file-not-found
(archivo no encontrado), ¡así que deberías comprobar el error!)
Fíjate también en la «r» que pasamos—esto significa «abrir un flujo de texto para lectura». (Hay varias
cadenas que podemos pasar a fopen() con significado adicional, como escribir, o añadir, etc.).
1
Solíamos tener tres nuevas líneas diferentes en amplio efecto: Retorno de carro (CR, usado en los viejos Macs), Salto de línea (LF,
usado en sistemas Unix), y Retorno de carro/Salto de línea (CRLF, usado en sistemas Windows). Afortunadamente, la introducción de
OS X, al estar basado en Unix, redujo este número a dos
Chapter 9. Archivo de Entrada/Salida (Input/Output) 62
Después, usamos la función fgetc() para obtener un carácter del flujo. Te estarás preguntando, por qué he
hecho que c sea un int en lugar de un char… ¡espera un momento!
Por último, cerramos el flujo cuando hemos terminado con él. Todos los flujos se cierran automáticamente
cuando el programa se cierra, pero es de buena educación y buena limpieza cerrar explícitamente cualquier
archivo cuando se termina con ellos.
El FILE* mantiene un registro de nuestra posición en el fichero. Así, las siguientes llamadas a fgetc()
obtendrían el siguiente carácter del fichero, y luego el siguiente, hasta el final.
Pero eso parece complicado. Veamos si podemos hacerlo más fácil.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE *fp;
6 int c;
7
8 fp = fopen("[Link]", "r");
9
13 fclose(fp);
14 }
(Si la línea 10 es demasiado rara, basta con descomponerla empezando por los paréntesis más internos. Lo
primero que hacemos es asignar el resultado de fgetc() a c, y luego comparamos eso con EOF. Lo hemos
metido todo en una sola línea. Esto puede parecer difícil de leer, pero estúdialo—es C idiomático).
Y ejecutando esto, vemos:
Hello, world!
Pero aún así, estamos operando carácter por carácter, y muchos archivos de texto tienen más sentido a nivel
de línea. Vamos a cambiar a eso.
del que leer. Devuelve NULL al final del archivo o en caso de error. fgets() es incluso lo suficientemente
amable como para terminar con NUL la cadena cuando ha terminado2 .
Vamos a hacer un bucle similar al anterior, excepto que vamos a tener un fichero multilínea y lo vamos a leer
línea a línea.
Aquí hay un archivo [Link]:
Y aquí hay algo de código que lee ese archivo línea por línea e imprime un número de línea antes de cada
una:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE *fp;
6 char s[1024]; // Suficientemente grande para cualquier línea
7 // que encuentre este programa.
8
9 int linecount = 0;
10
11 fp = fopen("[Link]", "r");
12
16 fclose(fp);
17 }
Lo que da la salida:
Antes de empezar, deberías saber que usar funciones del estilo de scanf() puede ser peligroso con
entradas no confiables. Si no especifica anchos de campo con tu %s, podrías desbordar el buffer.
Peor aún, una conversión numérica inválida puede resultar en un comportamiento indefinido. Lo más
seguro es usar %s con un ancho de campo, luego usar funciones como strtol() o strtod() para
hacer las conversiones.
2
Si el buffer no es lo suficientemente grande como para leer una línea entera, se detendrá la lectura a mitad de línea, y la siguiente
llamada a fgets() continuará leyendo el resto de la línea
Chapter 9. Archivo de Entrada/Salida (Input/Output) 64
Dispongamos de un fichero con una serie de registros de datos. En este caso, ballenas, con nombre, longitud
en metros y peso en toneladas. [Link]:
Sí, podríamos leerlos con fgets() y luego analizar la cadena con sscanf() (y en eso es más resistente
contra archivos corruptos), pero en este caso, vamos a usar fscanf() y sacarlo directamente.
La función fscanf() se salta los espacios en blanco al leer, y devuelve EOF al final del fichero o en caso de
error.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE *fp;
6 char name[1024]; // Suficientemente grande para cualquier
7 //línea que encuentre este programa.
8 float length;
9 int mass;
10
11 fp = fopen("[Link]", "r");
12
16 fclose(fp);
17 }
Lo que da el resultado:
Para ello, tenemos que fopen() el archivo, en modo de escritura pasando «w» como segundo argumento.
Abrir un fichero existente en modo «w» truncará instantáneamente ese fichero a 0 bytes para una sobreescrit-
ura completa.
Vamos a montar un programa sencillo que da salida a un archivo [Link] usando una variedad de fun-
ciones de salida.
Chapter 9. Archivo de Entrada/Salida (Input/Output) 65
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE *fp;
6 int x = 32;
7
8 fp = fopen("[Link]", "w");
9
10 fputc('B', fp);
11 fputc('\n', fp); // Salto de linea
12 fprintf(fp, "x = %d\n", x);
13 fputs("Hello, world!\n", fp);
14
15 fclose(fp);
16 }
B
x = 32
Hello, world!
fp = stdout;
1 #include <stdio.h>
2
3
Normalmente el segundo programa leería todos los bytes a la vez, y entonces los imprimiría en un bucle. Eso sería más eficiente.
Pero vamos para el valor de demostración, aquí
Chapter 9. Archivo de Entrada/Salida (Input/Output) 66
3 int main(void)
4 {
5 FILE *fp;
6 unsigned char bytes[6] = {5, 37, 0, 88, 255, 12};
7
19 fclose(fp);
20 }
Esos dos argumentos centrales de fwrite() son bastante extraños. Pero básicamente lo que queremos decirle
a la función es: «Tenemos elementos que son así de grandes, y queremos escribir así muchos de ellos». Esto
hace que sea conveniente si usted tiene un registro de una longitud fija, y usted tiene un montón de ellos en
una matriz. Sólo tienes que decirle el tamaño de un registro y cuántos escribir.
En el ejemplo anterior, le decimos que cada registro es del tamaño de un char, y tenemos 6 de ellos.
Ejecutando el programa obtenemos un fichero [Link], pero al abrirlo en un editor de texto no aparece
nada amigable. Son datos binarios, no texto. Y datos binarios aleatorios que me acabo de inventar.
Si lo paso por un programa hex dump4 , podemos ver la salida como bytes:
05 25 00 58 ff 0c
Muchos sistemas Unix incluyen un programa llamado hexdump para hacer esto. Usted puede usarlo
con el modificador -C (“canonical”) para obtener una buena salida:
$ hexdump -C [Link]
00000000 05 25 00 58 ff 0c |.%.X..|
El 00000000 es el offset dentro del archivo en el que comienza esta línea de salida. Los
05 25 00 58 ff 0c son los valores en bytes (y esto sería más largo (hasta 16 bytes por línea) si hu-
biera más bytes en el fichero). Y a la derecha entre los símbolos de tubería (|) está el mejor intento de
hexdump de imprimir los caracteres que corresponden a esos bytes. Imprime un punto si el carácter
no se puede imprimir. En este caso, como sólo estamos imprimiendo datos binarios aleatorios, esta
parte de la salida es basura. Pero si imprimiéramos una cadena ASCII en el fichero, veríamos eso ahí.
Y esos valores en hexadecimal coinciden con los valores (en decimal) que escribimos.
Pero ahora vamos a intentar leerlos de nuevo con un programa diferente. Este abrirá el fichero para lectura
binaria (modo «rb») y leerá los bytes de uno en uno en un bucle.
La función fread() devuelve el número de bytes leídos, o 0 en caso de EOF. Así que podemos hacer un
bucle hasta que veamos eso, imprimiendo números a medida que avanzamos.
4
[Link]
Chapter 9. Archivo de Entrada/Salida (Input/Output) 67
1 #include <stdio.h>
2
3 int main(void)
4 {
5 FILE *fp;
6 unsigned char c;
7
13 fclose(fp);
14 }
5
37
0
88
255
12
Woo hoo!
34 12
5
[Link]
6
Y esta es la razón por la que usé bytes individuales en mis ejemplos fwrite() y fread(), arriba, astutamente
7
[Link]
Chapter 10
Bueno, no tanto crear nuevos tipos como obtener nuevos nombres para tipos existentes. Suena un poco inútil
en la superficie, pero realmente podemos utilizar esto para hacer nuestro código más limpio.
Puede tomar cualquier tipo existente y hacerlo. Usted puede incluso hacer un número de tipos con una lista
de comas:
Eso es muy útil, ¿verdad? ¿Que puedas escribir «mushroom» en lugar de «bagel»? Debes de estar muy
emocionado con esta función.
De acuerdo, Profesor Sarcasmo… llegaremos a algunas aplicaciones más comunes de esto en un momento.
10.1.1 Alcance
typedef sigue las reglas de ámbito habituales.
Por esta razón, es bastante común encontrar typedef en el ámbito del archivo («global») para que todas las
funciones puedan utilizar los nuevos tipos a voluntad.
69
Chapter 10. typedef: Creación de nuevos tipos 70
struct animal {
char *name;
int leg_count, speed;
};
Personalmente, no me gusta esta práctica. Me gusta la claridad que tiene el código cuando añades la palabra
struct al tipo; los programadores saben lo que obtienen. Pero es muy común, así que lo incluyo aquí.
Ahora quiero ejecutar exactamente el mismo ejemplo de una manera que se puede ver comúnmente. Vamos
a poner el struct animal en el typedef. Puedes mezclarlo todo así:
// Nombre Original
// |
// v
// |-----------|
typedef struct animal {
char *name;
int leg_count, speed;
} animal; // <-- Nuevo nombre
1
Hablaremos más de ellas más adelante
Chapter 10. typedef: Creación de nuevos tipos 71
typedef struct {
int x, y;
} point;
// y
Si más tarde quieres cambiar a otro tipo, como long double, sólo tienes que cambiar el typedef:
// voila!
// |---------|
typedef long double app_float;
app_float f1, f2, f3; // Ahora todos estos son long double
int a = 10;
intptr x = &a; // «intptr» es tipo «int*»
Realmente no me gusta esta práctica. Oculta el hecho de que x es un tipo puntero porque no se ve un * en la
declaración.
En mi opinión, es mejor mostrar explícitamente que estás declarando un tipo puntero para que otros desar-
rolladores puedan verlo claramente y no confundan x con un tipo no puntero.
Chapter 10. typedef: Creación de nuevos tipos 72
Pero en el último recuento, digamos, 832.007 personas tenían una opinión diferente.
typedef struct {
int x, y;
} my_point; // lower snake case
typedef struct {
int x, y;
} MyPoint; // CamelCase
typedef struct {
int x, y;
} Mypoint; // Leading uppercase
typedef struct {
int x, y;
} MY_POINT; // UPPER SNAKE CASE
¡Es hora de entrar más en materia con una serie de nuevos temas sobre punteros! Si no estás al día con los
punteros, echa un vistazo a la primera sección de la guía sobre el tema.
Ahora vamos a utilizar la aritmética de punteros para imprimir el siguiente elemento de la matriz, el que está
en el índice 1:
73
Chapter 11. Punteros II: Aritmética 74
¿Qué ha pasado ahí? C sabe que p es un puntero a un int. Así que sabe el tamaño de un int1 y sabe que
debe saltarse esa cantidad de bytes para llegar al siguiente int después del primero.
De hecho, el ejemplo anterior podría escribirse de estas dos formas equivalentes:
¡Y eso funciona igual que si utilizáramos la notación array! ¡Oooo! Cada vez más cerca de la equivalencia
entre array y puntero. Más sobre esto en este capítulo.
Pero, ¿qué está pasando realmente aquí? ¿Cómo funciona?
¿Recuerdas que la memoria es como un gran array, donde un byte se almacena en cada índice del array?
Y el índice del array en la memoria tiene algunos nombres:
• Índice en memoria
• Localización
• Dirección
• Puntero!
Así que un puntero es un índice en la memoria, en algún lugar.
Por poner un ejemplo al azar, digamos que un número 3490 se almacenó en la dirección («índice»)
23,237,489,202. Si tenemos un puntero int a ese 3490, el valor de ese puntero es 23,237,489,202… porque
el puntero es la dirección de memoria. Diferentes palabras para la misma cosa.
Y ahora digamos que tenemos otro número, 4096, almacenado justo después del 3490 en la dirección
23,237,489,210 (8 más alto que el 3490 porque cada int en este ejemplo tiene 8 bytes de longitud).
Si añadimos 1 a ese puntero, en realidad salta sizeof(int) bytes hasta el siguiente int. Sabe que debe
saltar tan lejos porque es un puntero int. Si fuera un puntero float, saltaría sizeof(float) bytes adelante
para llegar al siguiente float.
Así que puedes ver el siguiente int, añadiendo 1 al puntero, el siguiente añadiendo 2 al puntero, y así
sucesivamente.
int a[] = {11, 22, 33, 44, 55, 999}; // Añade 999 aquí como centinela
Y también tenemos p apuntando al elemento en el índice 0 de a, es decir 11, igual que antes.
Ahora empecemos a incrementar p para que apunte a los siguientes elementos del array. Haremos esto hasta
que p apunte al 999; es decir, lo haremos hasta que *p == 999:
La idea es que si tenemos un puntero al principio de la cadena, podemos encontrar un puntero al final de la
cadena buscando el carácter NUL.
Y si tenemos un puntero al principio de la cadena, y hemos calculado el puntero al final de la cadena, podemos
restar los dos punteros para obtener la longitud de la cadena.
1 #include <stdio.h>
2
2
O cadena, que en realidad es un array de chars. Curiosamente, también puedes tener un puntero que haga referencia a uno pasado
el final del array sin problema y seguir haciendo cálculos con él
Chapter 11. Punteros II: Aritmética 76
14 }
15
16 int main(void)
17 {
18 printf("%d\n", my_strlen("Hello, world!")); // Imprime "13"
19 }
Recuerda que sólo puedes utilizar la resta de punteros entre dos punteros que apunten a la misma matriz.
a[b] == *(a + b)
pero eso es un poco más difícil de entender. Sólo asegúrate de incluir paréntesis si las expresiones son
complicadas para que todas tus matemáticas ocurran en el orden correcto.
Esto significa que podemos decidir si vamos a usar la notación array o puntero para cualquier array o puntero
(asumiendo que apunta a un elemento de un array).
Usemos un array y un puntero con ambas notaciones, array y puntero:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a[] = {11, 22, 33, 44, 55};
6
Así que puedes ver que en general, si tienes una variable array, puedes usar puntero o noción de array para
acceder a los elementos. Lo mismo con una variable puntero.
La única gran diferencia es que puedes modificar un puntero para que apunte a una dirección diferente, pero
no puedes hacer eso con una variable array.
esto significa que puedes pasar un array o un puntero a esta función y que funcione.
Y también es la razón por la que estas dos firmas de función son equivalentes:
conoce el tipo—y tú los conviertes al tipo que necesitas. Las funciones incorporadas qsort()3 y
bsearch()4 utilizan esta técnica.
Esta función copia n bytes a partir de la dirección s2 en la memoria a partir de la dirección s1.
Pero, ¡mira! ¡s1 y s2 son void*s! ¿Por qué? ¿Qué significa esto? Veamos más ejemplos.
Por ejemplo, podríamos copiar una cadena con memcpy() (aunque strcpy() es más apropiado para cade-
nas):
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void)
5 {
6 char s[] = "Goats!";
7 char t[100];
8
1 #include <stdio.h>
2 #include <string.h>
3
4 int main(void)
5 {
6 int a[] = {11, 22, 33};
7 int b[3];
8
11 printf("%d\n", b[1]); // 22
12 }
Esto es un poco salvaje… ¿has visto lo que hemos hecho con memcpy()? Copiamos los datos de a a b, pero
tuvimos que especificar cuántos bytes copiar, y un int es más de un byte.
Bien, entonces… ¿cuántos bytes ocupa un int? Respuesta: depende del sistema. Pero podemos saber
cuántos bytes ocupa cualquier tipo con el operador sizeof.
Así que.. ahí está la respuesta: un int ocupa sizeof(int) bytes de memoria para almacenarse.
Y si tenemos 3 de ellos en nuestro array, como en el ejemplo, todo el espacio usado para los 3 ints debe ser
3 * sizeof(int).
(En el ejemplo de la cadena, habría sido técnicamente más exacto copiar 7 * sizeof(char) bytes. Pero los
chars son siempre de un byte, por definición, así que se convierte en 7 * 1).
3
[Link]
4
[Link]
Chapter 11. Punteros II: Aritmética 79
Incluso podríamos copiar un float o un struct con memcpy(). (Aunque esto es abusivo—deberíamos usar
= para eso):
// ...
¡Mira qué versátil es memcpy()! Si tienes un puntero a un origen y un puntero a un destino, y tienes el
número de bytes que quieres copiar, puedes copiar cualquier tipo de datos.
Imagina que no tuviéramos void*. Tendríamos que escribir funciones memcpy() especializadas para cada
tipo:
// etc... blech!
Es mucho mejor usar void* y tener una función que lo haga todo.
Ese es el poder de void*. Puedes escribir funciones que no se preocupan por el tipo y aún así son capaces
de hacer cosas con él.
Pero un gran poder conlleva una gran responsabilidad. Tal vez no tan grande en este caso, pero hay algunos
límites.
1. No se puede hacer aritmética de punteros en un void*.
2. No se puede desreferenciar un void*.
3. No puedes usar el operador flecha en un void*, ya que también es una dereferencia.
4. No puedes usar la notación array en un void*, ya que también es una dereferencia 5 .
Y si lo piensas, estas reglas tienen sentido. Todas esas operaciones se basan en conocer el tamaño del tipo
de dato apuntado, y con void* no sabemos el tamaño del dato apuntado, ¡puede ser cualquier cosa!
Pero espera… si no puedes desreferenciar un void* ¿de qué te puede servir?
Como con memcpy(), te ayuda a escribir funciones genéricas que pueden manejar múltiples tipos de datos.
¡Pero el secreto es que, en el fondo, conviertes el void* a otro tipo antes de usarlo!
Y la conversión es fácil: sólo tienes que asignar a una variable del tipo deseado 6 .
5
Porque recuerda que la notación de array es sólo una una desreferencia y algo de matemática de punteros, y no puedes desreferenciar
un void*
6
También puedes castear el void* a otro tipo, pero aún no hemos llegado a los castts
Chapter 11. Punteros II: Aritmética 80
Escribamos nuestro propio memcpy() para probarlo. Podemos copiar bytes (chars), y sabemos el número
de bytes porque se pasa.
Justo al principio, copiamos los void*s en char*s para poder usarlos como char*s. Así de fácil.
Luego un poco de diversión en un bucle while, donde decrementamos byte_count hasta que se convierte
en false (0). Recuerda que con el post-decremento, se calcula el valor de la expresión (para que while lo
use) y entonces se decrementa la variable.
Y algo de diversión en la copia, donde asignamos *d = *s para copiar el byte, pero lo hacemos con post-
incremento para que tanto d como s se muevan al siguiente byte después de hacer la asignación.
Por último, la mayoría de las funciones de memoria y cadena devuelven una copia de un puntero a la cadena
de destino por si el que llama quiere utilizarla.
Ahora que hemos hecho esto, sólo quiero señalar rápidamente que podemos utilizar esta técnica para iterar
sobre los bytes de cualquier objeto en C, floats, structs, ¡o cualquier cosa!
Vamos a ejecutar un ejemplo más del mundo real con la rutina incorporada qsort() que puede ordenar
cualquier cosa gracias a la magia de los void*s.
(En el siguiente ejemplo, puede ignorar la palabra const, que aún no hemos tratado).
1 #include <stdio.h>
2 #include <stdlib.h>
3
10 // Esta es una función de comparación llamada por qsort() para ayudarle a determinar
11 // qué ordenar exactamente. La usaremos para ordenar un array de struct
12 // animales por leg_count.
13 int compar(const void *elem1, const void *elem2)
14 {
Chapter 11. Punteros II: Aritmética 81
15 // Sabemos que estamos ordenando struct animals, así que hagamos ambos
16 // argumentos punteros a struct animals
17 const struct animal *animal1 = elem1;
18 const struct animal *animal2 = elem2;
19
30 return 0;
31 }
32
33 int main(void)
34 {
35 // Construyamos un array de 4 struct animals con diferentes
36 // características. Este array está desordenado por leg_count, pero
37 // lo ordenaremos en un segundo.
38 struct animal a[4] = {
39 {.name="Dog", .leg_count=4},
40 {.name="Monkey", .leg_count=2},
41 {.name="Antelope", .leg_count=4},
42 {.name="Snake", .leg_count=0}
43 };
44
53 // Imprímelos todos
54 for (int i = 0; i < 4; i++) {
55 printf("%d: %s\n", a[i].leg_count, a[i].name);
56 }
57 }
Mientras le des a qsort() una función que pueda comparar dos elementos que tengas en tu array a ordenar,
puede ordenar cualquier cosa. Y lo hace sin necesidad de tener los tipos de los elementos codificados en
cualquier lugar. qsort() simplemente reordena bloques de bytes basándose en los resultados de la función
compar() que le pasaste.
Chapter 12
Esta es una de las grandes áreas en las que C probablemente diverge de los lenguajes que ya conoces: gestión
manual de memoria.
Otros lenguajes usan el conteo de referencias, la recolección de basura u otros medios para determinar cuándo
asignar nueva memoria para algunos datos–y desasignarla cuando ninguna variable hace referencia a ella.
Y eso está bien. Está bien poder despreocuparse de ello, simplemente, eliminar todas las referencias a un
elemento y confiar en que en algún momento se liberará la memoria asociada a él.
Pero C no es así, del todo.
Por supuesto, en C, algunas variables se asignan y se liberan automáticamente, cuando entran y salen del
ámbito. Llamamos a estas variables automáticas. Son las típicas variables «locales» de ámbito de bloque.
No hay problema.
Pero, ¿y si quieres que algo persista más tiempo que un bloque concreto? Aquí es donde entra en juego la
gestión manual de la memoria.
Puedes decirle explícitamente a C que te asigne un número determinado de bytes que podrás utilizar a tu
antojo. Y estos bytes permanecerán asignados hasta que liberes explícitamente esa memoria1 .
Es importante que liberes la memoria que hayas utilizado. Si no lo haces, lo llamamos una fuga de memoria
y tu proceso continuará reservando esa memoria hasta que termine.
Si la asignaste manualmente, tienes que liberarla manualmente cuando termines de usarla.
¿Cómo lo hacemos? Vamos a aprender un par de nuevas funciones, y hacer uso del operador sizeof para
ayudarnos a saber cuántos bytes asignar.
En el lenguaje común de C, los desarrolladores dicen que las variables locales automáticas se asignan «en
la pila» y que la memoria asignada manualmente está «en el montón (heap)». La especificación no habla de
ninguna de estas cosas, pero todos los desarrolladores de C, sabrán de qué estás hablando si las mencionas.
Todas las funciones que vamos a aprender en este capítulo se encuentran en <stdlib.h>.
82
Chapter 12. Asignación manual de memoria 83
Como es un void*, puedes asignarlo al tipo de puntero que quieras… normalmente se corresponderá de
alguna manera con el número de bytes que estás asignando.
Entonces… ¿cuántos bytes debo asignar? Podemos usar sizeof para ayudarnos con eso. Si queremos
asignar espacio suficiente para un único int, podemos usar sizeof(int) y pasárselo a malloc().
Después de que hayamos terminado con alguna memoria asignada, podemos llamar a free() para indicar
que hemos terminado con esa memoria y que puede ser utilizada para otra cosa. Como argumento, pasas el
mismo puntero que obtuviste de malloc() (o una copia del mismo). Es un comportamiento indefinido usar
una región de memoria después de haberla liberado (free()).
Intentémoslo. Asignaremos suficiente memoria para un int, luego almacenaremos algo allí, y lo imprimire-
mos.
int *p = malloc(sizeof(int));
En ese ejemplo artificioso, realmente no hay ningún beneficio. Nosotros podríamos haber usado un int
automático y habría funcionado. Pero veremos cómo la capacidad de asignar memoria de esta manera tiene
sus ventajas, especialmente con estructuras de datos más complejas.
Otra cosa que verás comúnmente aprovecha el hecho de que sizeof puede darte el tamaño del tipo de
resultado de cualquier expresión constante. Así que podrías poner un nombre de variable ahí también, y usar
eso. Aquí hay un ejemplo de eso, igual que el anterior:
int *x;
x = malloc(sizeof(int) * 10);
if (x == NULL) {
printf("Error al asignar 10 ints\n");
// Haga algo aquí para manejarlo
}
Este es un patrón común que verás, donde hacemos la asignación y la condición en la misma línea:
Chapter 12. Asignación manual de memoria 84
int *x;
Y, de hecho, es una matriz de 3490 chars (también conocida como cadena), ya que cada char es 1 byte. En
otras palabras, sizeof(char) es 1.
Nota: no se ha hecho ninguna inicialización en la memoria recién asignada—está llena de basura. Límpiela
con memset() si quiere, o vea calloc(), más abajo.
Pero podemos simplemente multiplicar el tamaño de la cosa que queremos por el número de elementos que
queremos, y luego acceder a ellos usando la notación de puntero o de array.
Ejemplo
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 // Asignar espacio para 10 ints
7 int *p = malloc(sizeof(int) * 10);
8
17 // Liberar el espacio
18 free(p);
19 }
La clave está en la línea malloc(). Si sabemos que cada int necesita sizeof(int) bytes para contenerlo,
y sabemos que queremos 10 de ellos, podemos simplemente asignar exactamente esa cantidad de bytes con:
sizeof(int) * 10
Chapter 12. Asignación manual de memoria 85
Y este truco funciona para todos los tipos. Basta con pasarlo a sizeof y multiplicarlo por el tamaño del
array.
De nuevo, el resultado es el mismo para ambos excepto que malloc() no pone a cero la memoria por defecto.
num_floats *= 2;
Vamos a asignar un array de 20 floats, y luego cambiamos de idea y lo convertimos en un array de 40.
Vamos a asignar el valor de retorno de realloc() a otro puntero para asegurarnos de que no es NULL. Si
no lo es, podemos reasignarlo a nuestro puntero original. (Si simplemente asignáramos el valor de retorno
directamente al puntero original, perderíamos ese puntero si la función devolviera NULL y no tendríamos
forma de recuperarlo).
Chapter 12. Asignación manual de memoria 86
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 // Asignar espacio para 20 floats
7 float *p = malloc(sizeof *p * 20); // sizeof *p igual que sizeof(float)
8
33 // Liberar el espacio
34 free(p);
35 }
Fíjate en cómo tomamos el valor de retorno de realloc() y lo reasignamos a la misma variable puntero p
que pasamos. Esto es bastante común.
Además, si la línea 7 te parece rara, con ese sizeof *p ahí, recuerda que sizeof funciona con el tamaño
del tipo de la expresión. Y el tipo de *p es float, así que esa línea es equivalente a sizeof(float).
1 #include <stdio.h>
2 #include <stdlib.h>
3
36 if (new_buf == NULL) {
37 free(buf); // En caso de error, libera y paga su fianza.
38 return NULL;
39 }
40
54 }
55
56 // Ajustar
57 if (offset < bufsize - 1) { // Si nos falta para el final
58 char *new_buf = realloc(buf, offset + 1); // +1 por terminación NUL
59
69 return buf;
70 }
71
72 int main(void)
73 {
74 FILE *fp = fopen("[Link]", "r");
75
76 char *line;
77
83 fclose(fp);
84 }
Cuando la memoria crece de esta manera, es común (aunque no es una ley) doblar el espacio necesario en
cada paso para minimizar el número de realloc()s que ocurren.
Por último, tenga en cuenta que readline() devuelve un puntero a un buffer malloc(). Como tal, es
responsabilidad de quien lo llama liberar explícitamente esa memoria cuando termine de usarla.
char *p = malloc(3490);
char *p = realloc(NULL, 3490);
Esto podría ser conveniente si se tiene algún tipo de bucle de asignación y no se quiere poner en un caso
especial el primer malloc().
int *p = NULL;
int length = 0;
while (!done) {
// Asigna 10 ints más:
Chapter 12. Asignación manual de memoria 89
length += 10;
p = realloc(p, sizeof *p * length);
En ese ejemplo, no necesitábamos un malloc() inicial ya que p era NULL para empezar.
La otra restricción es que el número de bytes que asignes tiene que ser múltiplo de la alineación. Pero esto
puede estar cambiando. Véase C Informe de defectos 4602
Hagamos un ejemplo, asignando en un límite de 64 bytes:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int main(void)
6 {
7 // Asignar 256 bytes alineados en un límite de 64 bytes
8 char *p = aligned_alloc(64, 256); // 256 == 64 * 4
9
14 // Liberar el espacio
2
[Link]
Chapter 12. Asignación manual de memoria 90
15 free(p);
16 }
Quiero hacer un comentario sobre realloc() y aligned_alloc(). realloc() no tiene ninguna garantía
de alineación, así que si necesitas obtener espacio reasignado alineado, tendrás que hacerlo por las malas con
memcpy().
if (new_ptr == NULL)
return NULL;
if (ptr != NULL)
memcpy(new_ptr, ptr, copy_size);
free(ptr);
return new_ptr;
}
Tenga en cuenta que siempre copia datos, lo que lleva tiempo, mientras que realloc() real lo evitará si
puede. Así que esto es poco eficiente. Evita tener que reasignar datos alineados a medida.
Chapter 13
Alcance
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int a = 12; // Local al bloque exterior, pero visible en el bloque interior
6
7 if (a == 12) {
8 int b = 99; // Local al bloque interior, no visible en el bloque exterior
9
1
[Link] bucket
91
Chapter 13. Alcance 92
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i = 0;
6
11 int j = 5;
12
Históricamente, C exigía que todas las variables estuvieran definidas antes de cualquier código del bloque,
pero esto ya no es así en el estándar C99.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i = 10;
6
7 {
8 int i = 20;
9
Te habrás dado cuenta de que en ese ejemplo acabo de lanzar un bloque en la línea 7, ¡ni siquiera una sentencia
for o if para iniciarlo! Esto es perfectamente legal. A veces un desarrollador querrá agrupar un montón de
variables locales para un cálculo rápido y hará esto, pero es raro de ver.
1 #include <stdio.h>
2
6 void func1(void)
7 {
8 shared += 100; // Ahora shared tiene 110
9 }
10
11 void func2(void)
12 {
13 printf("%d\n", shared); // Imprime "110"
14 }
15
16 int main(void)
17 {
18 func1();
19 func2();
20 }
Ten en cuenta que si shared se declarara al final del fichero, no compilaría. Tiene que ser declarado antes
de que cualquier función lo use.
Hay otras formas de modificar elementos en el ámbito del fichero, concretamente con static y extern, pero
hablaremos de ellas más adelante.
En ese ejemplo, el tiempo de vida de i comienza en el momento en que se define, y continúa durante la
duración del bucle.
Si el cuerpo del bucle está encerrado en un bloque, las variables definidas en el bucle for son visibles desde
ese ámbito interno.
A menos, por supuesto, que ese ámbito interno las oculte. Este ejemplo loco imprime 999 cinco veces:
1 #include <stdio.h>
2
3 int main(void)
4 {
Chapter 13. Alcance 94
Estamos acostumbrados a los tipos char, int y float, pero ha llegado el momento de pasar al siguiente
nivel y ver qué más tenemos en el departamento de tipos.
¿Por qué? ¿Por qué decidiste que sólo querías contener números positivos?
Respuesta: puedes obtener números más grandes en una variable sin signo que en una con signo.
Pero, ¿por qué?
Puedes pensar que los números enteros están representados por un cierto número de bits1 . En mi ordenador,
un int se representa con 64 bits.
Y cada permutación de bits que son 1 o 0 representa un número. Podemos decidir cómo repartir estos
números.
Con los números con signo, utilizamos (aproximadamente) la mitad de las permutaciones para representar
números negativos, y la otra mitad para representar números positivos.
Con números sin signo, usamos todas las permutaciones para representar números positivos.
En mi ordenador con ints de 64 bits que utiliza el complemento a dos2 para representar números sin signo,
tengo los siguientes límites en el rango de enteros:
1
«Bit» es la abreviatura de dígito binario. El binario es otra forma de representar números. En lugar de los dígitos 0-9 a los que
estamos acostumbrados, son los dígitos 0-1
2
[Link]
95
Chapter 14. Tipos II: ¡Muchos más tipos! 96
Fíjate en que el mayor unsigned int positivo es aproximadamente el doble de grande que el mayor int
positivo. Así que puedes tener cierta flexibilidad.
char c = 'B';
char c = 'B';
En el fondo, char no es más que un pequeño int, es decir, un entero que utiliza un único byte de espacio,
limitando su rango a…
Aquí la especificación C se pone un poco rara. Nos asegura que un char es un único byte, es decir,
sizeof(char) == 1. Pero entonces en C11 §3.6¶3 se sale de su camino para decir:
Un byte está compuesto por una secuencia contigua de bits, cuyo número está definido por la imple-
mentación.
Espera… ¿qué? Algunos de ustedes pueden estar acostumbrados a la noción de que un byte es de 8 bits,
¿verdad? Quiero decir, eso es lo que es, ¿verdad? Y la respuesta es: «Casi seguro»3 . Pero C es un lenguaje
antiguo, y las máquinas de antes tenían, digamos, una opinión más relajada sobre cuántos bits había en un
byte. Y a lo largo de los años, C ha conservado esta flexibilidad.
Pero asumiendo que tus bytes en C son de 8 bits, como lo son en prácticamente todas las máquinas del mundo
que verás, el rango de un char es…
—Así que antes de que pueda decírtelo, resulta que chars puede ser con o sin signo dependiendo de tu
compilador. A menos que lo especifiques explícitamente.
En muchos casos, sólo tener chars está bien porque no te importa el signo de los datos. Pero si necesitas
chars con signo o sin signo, debes ser específico:
OK, ahora, finalmente, podemos averiguar el rango de números si asumimos que un char es de 8 bits y su
sistema utiliza la virtualmente universal representación de complemento a dos para con signo y sin signo4 .
3
El término industrial para una secuencia de exactamente, indiscutiblemente 8 bits es un octeto
4
En general, f usted tiene un 𝑛 bit número de complemento a dos, el rango con signo es −2𝑛−1 a 2𝑛−1 − 1. Y el rango sin signo
es de 0 a 2𝑛 − 1
Chapter 14. Tipos II: ¡Muchos más tipos! 97
Así que, asumiendo esas limitaciones, por fin podemos calcular nuestros rangos:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char a = 10, b = 20;
6
¿Qué ocurre con los caracteres constantes entre comillas simples, como 'B'? ¿Cómo puede eso tener un
valor numérico?
La especificación también es imprecisa en este caso, ya que C no está diseñado para ejecutarse en un único
tipo de sistema subyacente.
Pero asumamos por el momento que su juego de caracteres está basado en ASCII5 para al menos los primeros
128 caracteres. En ese caso, la constante de carácter se convertirá en un char cuyo valor es el mismo que el
valor ASCII del carácter.
Eso ha sido un trabalenguas. Pongamos un ejemplo:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char a = 10;
6 char b = 'B'; // Valor en ASCII 66
7
Esto depende de su entorno de ejecución y del juego de caracteres utilizado6 . Uno de los conjuntos de
caracteres más populares hoy en día es Unicode7 (que es un superconjunto de ASCII), por lo que para tus
0-9, A-Z, a-z y signos de puntuación básicos, casi seguro que obtendrás los valores ASCII.
• char
• int
y recientemente hemos aprendido sobre las variantes sin signo de los tipos enteros. Y aprendimos que char
era secretamente un pequeño int disfrazado. Así que sabemos que los ints pueden venir en múltiples
tamaños de bit.
Pero hay un par de tipos enteros más que deberíamos ver, y los valores mínimo minímo y máximo que pueden
contener.
Sí, he dicho «mínimo» dos veces. La especificación dice que estos tipos contendrán números de al menos
estos tamaños, así que tu implementación puede ser diferente. El fichero de cabecera <limits.h> define
macros que contienen los valores enteros mínimo y máximo; confíe en ello para estar seguro, y nunca codi-
fique o asuma estos valores. Estos tipos adicionales son short int, long int y long long int. Normal-
mente, cuando se utilizan estos tipos, los desarrolladores de C omiten la parte int (por ejemplo, long long),
y el compilador no tiene ningún problema.
// Y estos también:
short int x;
short x;
Veamos los tipos y tamaños de datos enteros en orden ascendente, agrupados por signatura.
No existe el tipo long long long. No puedes seguir añadiendo longs así. No seas tonto.
Los aficionados a los complementos a dos habrán notado algo raro en esos números. ¿Por qué, por
ejemplo, el signed char se detiene en -127 en lugar de -128? Recuerde: estos son sólo los míni-
mos requeridos por la especificación. Algunas representaciones numéricas (como signo y magnituda )
tienen un máximo de ±127.
a
[Link]
Ejecutemos la misma tabla en mi sistema de 64 bits y complemento a dos y veamos qué sale:
8
Depende de si el valor por defecto de char es signed char o unsigned char
Chapter 14. Tipos II: ¡Muchos más tipos! 99
Eso es un poco más sensato, pero podemos ver cómo mi sistema tiene límites mayores que los mínimos de
la especificación.
Entonces, ¿qué son las macros en <limits.h>?
Fíjate que hay una forma oculta de determinar si un sistema utiliza chars con signo o sin signo. Si
CHAR_MAX == UCHAR_MAX, debe ser sin signo.
Fíjate también en que no hay macro mínima para las variantes «sin signo»: son simplemente «0».
Parametro Definición
𝑠 signo (±1)
𝑏 base o radix de la representación del exponente
(un entero > 1)
𝑒 exponente (un número entero entre un mínimo
𝑒𝑚𝑖𝑛 y un máximo 𝑒𝑚𝑎𝑥 )
9
Mi char está con signo.
Chapter 14. Tipos II: ¡Muchos más tipos! 100
Tipo sizeof
float 4
double 8
long double 16
10
[Link]
Chapter 14. Tipos II: ¡Muchos más tipos! 101
Así que cada uno de los tipos (en mi sistema) utiliza esos bits adicionales para obtener más precisión.
¿Pero de cuánta precisión estamos hablando? ¿Cuántos números decimales pueden ser representados por
estos valores?
Bueno, C nos proporciona un montón de macros en <float.h> para ayudarnos a averiguarlo.
La cosa se complica un poco si estás usando un sistema de base-2 (binario) para almacenar los números (que
es prácticamente todo el mundo en el planeta, probablemente incluyéndote a ti), pero ten paciencia conmigo
mientras lo resolvemos.
En mi sistema, FLT_DIG es 6, así que puedo estar seguro de que si imprimo un float de 6 dígitos, obtendré
lo mismo de vuelta. (Podrían ser más dígitos—algunos números volverán correctamente con más dígitos.
Pero sin duda me devolverá 6).
Por ejemplo, imprimiendo floats siguiendo este patrón de dígitos crecientes, aparentemente llegamos a 8
dígitos antes de que algo vaya mal, pero después de eso volvemos a 7 dígitos correctos.
0.12345
0.123456
0.1234567
0.12345678
0.123456791 <-- Las cosas empiezan a ir mal
0.1234567910
Hagamos otra demostración. En este código tendremos dos floats que contendrán números que tienen
FLT_DIG dígitos decimales significativos11 . Luego los sumamos, para lo que deberían ser 12 dígitos deci-
males significativos. Pero eso es más de lo que podemos almacenar en un float y recuperar correctamente
como una cadena—así que vemos que cuando lo imprimimos, las cosas empiezan a ir mal después del 7º
dígito significativo.
1 #include <stdio.h>
2 #include <float.h>
3
4 int main(void)
5 {
6 // Ambos números tienen 6 dígitos significativos, por lo que pueden ser
7 // almacenados con precisión en un float:
8
11
Este programa se ejecuta como indican sus comentarios en un sistema con FLT_DIG de 6 que utiliza números en coma flotante
IEEE-754 base-2. De lo contrario podrías obtener una salida diferente
Chapter 14. Tipos II: ¡Muchos más tipos! 102
9 float f = 3.14159f;
10 float g = 0.00000265358f;
11
15 // Ahora súmalos
16 f += g; // 3.14159265358 es lo que f _debería_ ser
17
(El código anterior tiene una f después de las constantes numéricas—esto indica que la constante es de tipo
float, en oposición al valor por defecto de double. Más sobre esto más adelante).
Recuerde que FLT_DIG es el número seguro de dígitos que puede almacenar en un float y recuperar correc-
tamente.
A veces puedes sacar uno o dos más. Pero otras veces sólo recuperarás dígitos FLT_DIG. Lo más seguro: si
almacenas cualquier número de dígitos hasta e incluyendo FLT_DIG en un float, seguro que los recuperas
correctamente.
Así que ésa es la historia de FLT_DIG. Fin.
…¿O no?
¿Pero qué pasa con los números de coma flotante que no están en el hueco? ¿Cuántos lugares necesita para
imprimirlos con precisión?
Otra forma de formular esta pregunta es, para cualquier número en coma flotante, ¿cuántos dígitos decimales
tengo que conservar si quiero volver a convertir el número decimal en un número idéntico en coma flotante?
Es decir, ¿cuántos dígitos tengo que imprimir en base 10 para recuperar todos los dígitos en base 2 del número
original?
A veces pueden ser sólo unos pocos. Pero para estar seguro, querrás convertir a decimal con un cierto número
seguro de decimales. Ese número está codificado en las siguientes macros:
Macro Descripción
FLT_DECIMAL_DIG Número de dígitos decimales codificados en un float.
DBL_DECIMAL_DIG Número de dígitos decimales codificados en un double.
LDBL_DECIMAL_DIG Número de dígitos decimales codificados en un long double.
DECIMAL_DIG Igual que la codificación más amplia, LDBL_DECIMAL_DIG.
Chapter 14. Tipos II: ¡Muchos más tipos! 103
Veamos un ejemplo en el que DBL_DIG es 15 (por lo que es todo lo que podemos tener en una constante),
pero DBL_DECIMAL_DIG es 17 (por lo que tenemos que convertir a 17 números decimales para conservar
todos los bits del double original).
Asignemos el número de 15 dígitos significativos 0.123456789012345 a x, y asignemos el número de 1
dígito significativo 0.0000000000000006 a y.
Pero sumémoslos. Esto debería dar 0.1234567890123456, pero es más que DBL_DIG, así que podrían pasar
cosas raras… veamos:
Eso nos pasa por imprimir más que DBL_DIG, ¿no? Pero fíjate… ¡ese número de arriba es exactamente
representable tal cual!
Si asignamos 0.12345678901234559 (17 dígitos) a z y lo imprimimos, obtenemos:
Si hubiéramos truncado z a 15 dígitos, no habría sido el mismo número. Por eso, para conservar todos los
bits de un doble, necesitamos DBL_DECIMAL_DIG y no sólo el menor DBL_DIG.
Dicho esto, está claro que cuando estamos jugando con números decimales en general, no es seguro imprimir
más de FLT_DIG, DBL_DIG, o LDBL_DIG dígitos para ser sensato en relación con los números originales de
base 10 y cualquier matemática posterior.
Pero cuando convierta de float a una representación decimal y de vuelta a float, use definitivamente
FLT_DECIMAL_DIG para hacerlo, de forma que todos los bits se conserven exactamente.
int a = 012;
Chapter 14. Tipos II: ¡Muchos más tipos! 104
Esto es especialmente problemático para los programadores principiantes que tratan de rellenar los números
decimales a la izquierda con «0» para alinear las cosas bien y bonito, cambiando inadvertidamente la base
del número:
No existe un especificador de formato printf() para imprimir un número binario. Hay que hacerlo carácter
a carácter con operadores bit a bit.
int x = 1234;
long int x = 1234L;
long long int x = 1234LL
Tipo Sufijo
int Sin sufijo
long int L
long long int LL
unsigned int U
unsigned long int UL
unsigned long long int ULL
12
Realmente me sorprende que C no tenga esto en la especificación todavía. En el documento C99 Rationale, escriben: «Una
propuesta para añadir constantes binarias fue rechazada por falta de precedentes y utilidad insuficiente». Lo que parece una tontería a
la luz de algunas de las otras características que han incluido. Apuesto a que una de las próximas versiones lo tiene.
Chapter 14. Tipos II: ¡Muchos más tipos! 105
En la tabla mencioné que «sin sufijo» significa int… pero en realidad es más complejo que eso.
Entonces, ¿qué sucede cuando usted tiene un número sin sufijo como:
int x = 1234;
Lo que esto quiere decir es que, por ejemplo, si especificas un número como 123456789U, primero
C verá si puede ser unsigned int. Si no cabe ahí, probará con unsigned long int. Y luego
unsigned long long int. Usará el tipo más pequeño que pueda contener el número.
Puede forzar que sea de tipo float añadiendo una f (o F—no distingue mayúsculas de minúsculas). Puedes
forzar que sea del tipo long double añadiendo l (o L).
Tipo Sufijo
float F
double None
long double L
Por ejemplo:
float x = 3.14f;
double x = 3.14;
long double x = 3.14L;
Todo este tiempo, sin embargo, hemos estado haciendo esto, ¿verdad?
float x = 3.14;
¿No es la izquierda un float y la derecha un double? Sí. Pero C es bastante bueno con las conversiones
numéricas automáticas, así que es más común tener una constante de coma flotante fijada que no fijada. Más
adelante hablaremos de ello.
Sin embargo, al imprimirlo, cambiará el exponente para que haya sólo un dígito delante del punto decimal.
13
[Link] Scientific_notation
Chapter 14. Tipos II: ¡Muchos más tipos! 107
• El más se puede dejar fuera del exponente, ya que es por defecto, pero esto es poco común en la práctica
por lo que he visto.
1.2345e10 == 1.2345e+10
1.2345e10F
1.2345e10L
double x = 0xa.1p3;
En este capítulo, queremos hablar de la conversión de un tipo a otro. C tiene una variedad de formas de hacer
esto, y algunas pueden ser un poco diferentes a las que estás acostumbrado en otros lenguajes.
Antes de hablar de cómo hacer que las conversiones ocurran, hablemos de cómo funcionan cuando ocurren.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 char s[10];
6 float f = 3.14159;
7
1
Son lo mismo, salvo que snprintf() permite especificar un número máximo de bytes de salida, evitando que se sobrepase el final
de la cadena. Así que es más seguro
108
Chapter 15. Tipos III: Conversiones 109
Así que puedes usar %d o %u como estás acostumbrado para los enteros.
Función Descripción
atoi Cadena a int
atof Cadena a float
atol Cadena a long int
atoll Cadena a long long int
Aunque la especificación no lo menciona, la a al principio de la función significa ASCII2 , así que en realidad
atoi() es «ASCII a entero» (Ascii To Integer, pero decirlo hoy en día es un poco ASCII-céntrico.
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char *pi = "3.14159";
7 float f;
8
9 f = atof(pi);
10
11 printf("%f\n", f);
12 }
Pero, como he dicho, obtenemos un comportamiento indefinido de cosas raras como esta:
(Cuando ejecuto eso, obtengo 0 de vuelta, pero realmente no deberías contar con eso de ninguna manera.
Podrías obtener algo completamente diferente).
Para obtener mejores características de manejo de errores, echemos un vistazo a todas esas funciones strtol,
también en <stdlib.h>. No sólo eso, ¡también convierten a más tipos y más bases!
Función Descripción
strtol Cadena a long int
strtoll Cadena a long long int
strtoul Cadena a unsigned long int
strtoull Cadena a unsigned long long int
strtof Cadena a float
2
[Link]
Chapter 15. Tipos III: Conversiones 110
Función Descripción
strtod Cadena a double
strtold Cadena a long double
Todas estas funciones siguen un patrón de uso similar y constituyen la primera experiencia de mucha gente
con punteros a punteros. Pero no te preocupes, es más fácil de lo que parece.
Hagamos un ejemplo en el que convertimos una cadena a un unsigned long, descartando la información
de error (es decir, la información sobre caracteres erróneos en la cadena de entrada):
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char *s = "3490";
7
Fíjate en un par de cosas. Aunque no nos dignamos a capturar ninguna información sobre caracteres de error
en la cadena, strtoul() no nos dará un comportamiento indefinido; simplemente devolverá 0.
Además, especificamos que se trataba de un número decimal (base 10).
¿Significa esto que podemos convertir números de bases diferentes? Por supuesto. ¡Hagámoslo en binario!
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char *s = "101010"; // ¿Qué significa este número?
7
12 printf("%lu\n", x); // 42
13 }
Vale, eso es muy divertido, pero ¿qué es eso de «NULL»? ¿Para qué sirve?
Nos ayuda a averiguar si se ha producido un error al procesar la cadena. Es un puntero a un puntero a un
char, que suena espeluznante, pero no lo es una vez que te haces a la idea.
Hagamos un ejemplo en el que introducimos un número deliberadamente malo, y veremos cómo strtol()
nos permite saber dónde está el primer dígito inválido.
Chapter 15. Tipos III: Conversiones 111
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char *s = "34x90"; // ¡«x» no es un dígito válido en base 10!
7 char *badchar;
8
13 // Intenta convertir tanto como sea posible, así que llega hasta aquí:
14
15 printf("%lu\n", x); // 34
16
Así que tenemos a strtoul() modificando lo que badchar señala para mostrarnos dónde han ido mal las
cosas3 .
Pero, ¿y si no pasa nada? En ese caso, badchar apuntará al terminador NUL al final de la cadena. Así que
podemos comprobarlo:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char *s = "3490"; // ¡«x» no es un dígito válido en base 10!
7 char *badchar;
8
15 if (*badchar == '\0') {
16 printf("Éxito! %lu\n", x);
17 } else {
18 printf("Conversión parcial: %lu\n", x);
19 printf("Carácter no válido: %c\n", *badchar);
20 }
21 }
3
Tenemos que pasar un puntero a badchar a strtoul() o no será capaz de modificarlo de ninguna manera que podamos ver, de
forma análoga a por qué tienes que pasar un puntero a un int a una función si quieres que esa función sea capaz de cambiar el valor de
ese int
Chapter 15. Tipos III: Conversiones 112
Ahí lo tienes. Las funciones estilo atoi() son buenas en un apuro controlado, pero las funciones estilo
strtol() le dan mucho más control sobre el manejo de errores y la base de la entrada.
5 53
Así que… no. ¿Y 53? ¿Qué es eso? Es el punto de código UTF-8 (y ASCII) para el símbolo de carácter
'5'4 .
Entonces, ¿cómo convertimos el carácter '5' (que aparentemente tiene valor 53) en el valor 5?
Con un ingenioso truco, ¡así es cómo!
El estándar C garantiza que estos caracteres tendrán puntos de código que están en secuencia y en este orden:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
48 49 50 51 52 53 54 55 56 57
Ahí ves que '5' es 53, tal como nos salía. Y «0» es «48».
Así que podemos restar «0» de cualquier dígito para obtener su valor numérico:
char c = '6';
int x = 6;
4
Cada carácter tiene un valor asociado para cualquier esquema de codificación de caracteres
Chapter 15. Tipos III: Conversiones 113
Puede que pienses que es una forma rara de hacer esta conversión, y para los estándares de hoy en día,
ciertamente lo es. Pero en los viejos tiempos, cuando los ordenadores se hacían literalmente de madera, éste
era el método para hacer esta conversión. Y no estaba roto, así que C nunca lo arregló.
En ese caso, x e y son promovidos a int por C antes de que se realice la operación matemática.
Las promociones a enteros tienen lugar durante las conversiones aritméticas habituales, con funciones var-
iádicas8 , operadores unarios + y -, o al pasar valores a funciones sin prototipos9 .
5
En la práctica, lo que probablemente está ocurriendo en tu implementación es que los bits de orden alto simplemente se eliminan
del resultado, de modo que un número de 16 bits 0x1234 que se convierte a un número de 8 bits termina como 0x0034, o simplemente
0x34
6
De nuevo, en la práctica, lo que probablemente ocurrirá en tu sistema es que el patrón de bits para el original se truncará y luego
sólo se usará para representar el número con signo, complemento a dos. Por ejemplo, mi sistema toma un unsigned char de 192 y lo
convierte a signed char -64. En complemento a dos, el patrón de bits para ambos números es binario 11000000.
7
En realidad no—simplemente se descarta con regularidad
8
Funciones con un número variable de argumentos.
9
Esto se hace raramente porque el compilador se quejará y tener un prototipo es lo Correcto de hacer. Creo que esto todavía funciona
Chapter 15. Tipos III: Conversiones 114
15.4.3 void*
El tipo void* es interesante porque puede convertirse desde o hacia cualquier tipo de puntero.
int x = 10;
15.5.1 Casting
Puedes cambiar explícitamente el tipo de una expresión poniendo un nuevo tipo entre paréntesis delante
de ella. Algunos desarrolladores de C fruncen el ceño ante esta práctica a menos que sea absolutamente
necesario, pero es probable que te encuentres con algún código C que contenga estos paréntesis.
Hagamos un ejemplo en el que queremos convertir un int en un long para poder almacenarlo en un long.
Nota: este ejemplo es artificial y la conversión en este caso es completamente innecesaria porque la expresión
x + 12 se cambiaría automáticamente a long int para coincidir con el tipo más amplio de y.
int x = 10;
long int y = (long int)x + 12;
En ese ejemplo, aunque x era antes de tipo int, la expresión (long int)x es de tipo long int. Decimos:
«Castamos x a long int».
Más comúnmente, se puede ver una conversión para convertir un void* a un tipo de puntero específico para
que pueda ser dereferenciado.
Una llamada de retorno de la función incorporada qsort() puede mostrar este comportamiento ya que tiene
void*s pasados a ella:
Uno de los lugares en los que verás más comúnmente las conversiones es para evitar una advertencia al
imprimir valores de puntero con el raramente usado %p que se pone quisquilloso con cualquier cosa que no
sea un void*:
int x = 3490;
int *p = &x;
printf("%p\n", p);
warning: format ‘%p’ expects argument of type ‘void *’, but argument
2 has type ‘int *’
Otro lugar es con cambios explícitos de puntero, si no quieres usar un void* intermedio, pero estos también
son bastante infrecuentes:
long x = 3490;
long *p = &x;
unsigned char *c = (unsigned char *)p;
Un tercer lugar donde suele ser necesario es con las funciones de conversión de caracteres en <ctype.h>10
donde debe convertir los valores con signo dudoso a unsigned char para evitar comportamientos
indefinidos.
Una vez más, en la práctica rara vez se necesita el reparto. Si te encuentras casteando, puede que haya otra
forma de hacer lo mismo, o puede que estés casteando innecesariamente.
O puede que sea necesario. Personalmente, intento evitarlo, pero no tengo miedo de utilizarlo si es necesario.
10
[Link]
Chapter 16
Ahora que tenemos algunos tipos más en nuestro haber, resulta que podemos dar a estos tipos algunos atrib-
utos adicionales que controlan su comportamiento. Estos son los calificadores de tipo y los especificadores
de clase de almacenamiento.
16.1.1 const
Es el calificador de tipo más común. Significa que la variable es constante y que cualquier intento de modi-
ficarla, provocará el enfado del compilador.
const int x = 2;
117
Chapter 16. Tipos IV: Calificadores y especificadores 118
Genial, así que no podemos cambiar la cosa a la que apunta el puntero, pero podemos cambiar el propio
puntero. ¿Qué pasa si queremos lo contrario? ¿Queremos poder cambiar aquello a lo que apunta el puntero,
pero no el puntero en sí?
Basta con mover el const después del asterisco en la declaración:
int x = 10;
int *const p = &x;
Por último, si tienes varios niveles de indirección, debes const los niveles apropiados. Sólo porque un
puntero sea const, no significa que el puntero al que apunta también deba serlo. Puedes establecerlos ex-
plícitamente como en los siguientes ejemplos:
char **p;
p++; // OK!
(*p)++; // OK!
char **const p;
p++; // Error!
(*p)++; // OK!
(*p)++; // Error!
El compilador nos está avisando de que el valor de la derecha de la asignación es const, pero el de la
izquierda no. Y el compilador nos está avisando de que está descartando la «const-idad» de la expresión de
la derecha.
Es decir, podemos seguir intentando hacer lo siguiente, pero es incorrecto. El compilador avisará, y es un
comportamiento indefinido:
16.1.2 restrict
TLDR: nunca tienes que usar esto y puedes ignorarlo cada vez que lo veas. Si lo usas correctamente, es proba-
ble que obtengas alguna ganancia de rendimiento. Si lo usas incorrectamente, obtendrás un comportamiento
indefinido.
restrict es una sugerencia al compilador de que una determinada parte de la memoria sólo será accedida
por un puntero y nunca por otro. (Es decir, no habrá aliasing del objeto concreto al que apunta el puntero
restrict). Si un desarrollador declara que un puntero es restrict y luego accede al objeto al que apunta
de otra manera (por ejemplo, a través de otro puntero), el comportamiento es indefinido.
Básicamente le estás diciendo a C, «Hey—te garantizo que este único puntero es la única forma en la que
accedo a esta memoria, y si miento, puedes sacarme un comportamiento indefinido».
Y C usa esa información para realizar ciertas optimizaciones. Por ejemplo, si estás desreferenciando el
puntero restrict repetidamente en un bucle, C podría decidir almacenar en caché el resultado en un registro
Chapter 16. Tipos IV: Calificadores y especificadores 120
y sólo almacenar el resultado final una vez que el bucle haya terminado. Si cualquier otro puntero hiciera
referencia a esa misma memoria y accediera a ella en el bucle, los resultados no serían exactos.
(Nótese que restrict no tiene efecto si nunca se escribe en el objeto apuntado. Se trata de optimizaciones
en torno a las escrituras en memoria).
Escribamos una función para intercambiar dos variables, y usaremos la palabra clave restrict para asegurar
a C que nunca pasaremos punteros a la misma cosa. Y luego intentemos pasar punteros a la misma cosa.
5 t = *a;
6 *a = *b;
7 *b = t;
8 }
9
10 int main(void)
11 {
12 int x = 10, y = 20;
13
Si elimináramos las palabras clave restrict, ambas llamadas funcionarían de forma segura. Pero entonces
el compilador podría no ser capaz de optimizar.
restrict tiene ámbito de bloque, es decir, la restricción sólo dura el ámbito en el que se usa. Si está en una
lista de parámetros de una función, está en el ámbito de bloque de esa función.
Si el puntero restringido apunta a un array, sólo se aplica a los objetos individuales del array. Otros punteros
pueden leer y escribir desde el array siempre que no lean o escriban ninguno de los mismos elementos que
el restringido.
Si está fuera de cualquier función en el ámbito del fichero, la restricción cubre todo el programa.
Es probable que veas esto en funciones de biblioteca como printf():
De nuevo, esto sólo indica al compilador que dentro de la función printf() sólo habrá un puntero que haga
referencia a cualquier parte de la cadena format.
Una última nota: si por alguna razón estás usando la notación de array en el parámetro de tu función en lugar
de la notación de puntero, puedes usar restrict así:
16.1.3 volatile
Es poco probable que veas o necesites esto a menos que estés tratando con hardware directamente.
volatile indica al compilador que un valor puede cambiar a sus espaldas y que debe buscarlo cada vez.
Un ejemplo podría ser cuando el compilador está buscando en la memoria una dirección que se actualiza
continuamente entre bastidores, por ejemplo, algún tipo de temporizador de hardware.
Si el compilador decide optimizar eso y almacenar el valor en un registro durante un tiempo prolongado, el
valor en memoria se actualizará y no se reflejará en el registro.
Al declarar algo volátil, le estás diciendo al compilador: «Oye, la cosa a la que esto apunta puede cambiar
en cualquier momento por razones ajenas a este código de programa».
16.1.4 _Atomic
Esta es una característica opcional de C de la que hablaremos en el capítulo Atómica.
16.2.1 auto
Apenas se ve esta palabra clave, ya que auto es el valor por defecto para las variables de ámbito de bloque.
Está implícita.
Son los mismos:
{
int a; // auto es el valor por defecto...
auto int a; // Esto es redundante
}
La palabra clave auto indica que este objeto tiene duración de almacenamiento automática. Es decir, existe
en el ámbito en el que se define, y se desasigna automáticamente cuando se sale del ámbito.
Un inconveniente de las variables automáticas es que su valor es indeterminado hasta que se inicializan
explícitamente. Decimos que están llenas de datos «aleatorios» o «basura», aunque ninguna de las dos cosas
me hace feliz. En cualquier caso, no sabrás lo que contiene a menos que la inicialices.
Inicialice siempre todas las variables automáticas antes de utilizarlas.
16.2.2 static
Esta palabra clave tiene dos significados, dependiendo de si la variable es de ámbito de fichero o de ámbito
de bloque.
Empecemos con el ámbito de bloque.
Chapter 16. Tipos IV: Calificadores y especificadores 122
1 #include <stdio.h>
2
3 void counter(void)
4 {
5 static int count = 1; // Se inicializa una vez
6
9 count++;
10 }
11
12 int main(void)
13 {
14 counter(); // «Se ha llamado 1 vez(es)»
15 counter(); // «Se ha llamado 2 vez(es)»
16 counter(); // «Se ha llamado 3 vez(es)»
17 counter(); // «Se ha llamado 4 vez(es)»
18 }
Por último, ten en cuenta que si escribes programas multihilo, tienes que asegurarte de no dejar que varios
hilos pisoteen la misma variable.
16.2.3 extern
El especificador de clase de almacenamiento extern nos da una forma de referirnos a objetos en otros
ficheros fuente.
Chapter 16. Tipos IV: Calificadores y especificadores 123
1 // bar.c
2
3 int a = 37;
1 // foo.c
2
3 extern int a;
4
5 int main(void)
6 {
7 printf("%d\n", a); // 37, ¡desde bar.c!
8
9 a = 99;
10
También podríamos haber hecho el extern int a en el ámbito del bloque, y aún así se habría referido al a
en bar.c:
1 // foo.c
2
3 int main(void)
4 {
5 extern int a;
6
9 a = 99;
10
Ahora bien, si a en bar.c se hubiera marcado como static, esto no habría funcionado. Las variables
static en el ámbito de un fichero no son visibles fuera de ese fichero.
Una nota final sobre extern en funciones. Para las funciones, extern es el valor por defecto, por lo que es
redundante. Puedes declarar una función static si sólo quieres que sea visible en un único fichero fuente.
16.2.4 register
Se trata de una palabra clave que indica al compilador que esta variable se utiliza con frecuencia, y que debe
ser lo más rápida posible. El compilador no está obligado a aceptarla.
Ahora bien, los modernos optimizadores del compilador de C son bastante eficaces a la hora de averiguar
esto por sí mismos, por lo que es raro verlo hoy en día.
Pero si es necesario:
Chapter 16. Tipos IV: Calificadores y especificadores 124
1 #include <stdio.h>
2
3 int main(void)
4 {
5 register int a; // Haz que «a» sea tan rápido de usar como sea posible.
6
register int a;
int *p = &a; // ¡ERROR DEL COMPILADOR!
// No se puede tomar la dirección de un registro!
con:
El hecho de que no se pueda tomar la dirección de una variable de registro libera al compilador para realizar
optimizaciones en torno a esa suposición, si es que aún no las ha deducido. Además, añadir register a una
variable const evita que accidentalmente se pase su puntero a otra función que ignore su constancia 1 .
Un poco de historia: en el interior de la CPU hay pequeñas «variables» dedicadas llamadas registers / reg-
istros2 . Su acceso es superrápido comparado con la RAM, por lo que usarlas aumenta la velocidad. Pero
no están en la RAM, así que no tienen una dirección de memoria asociada (por eso no puedes tomar la
dirección-de u obtener un puntero a ellas).
Pero, como ya he dicho, los compiladores modernos son realmente buenos a la hora de producir código
óptimo, utilizando registros siempre que sea posible independientemente de si se ha especificado o no la
palabra clave register. No sólo eso, sino que la especificación les permite tratarlo como si hubieras escrito
«auto», si quieren. Así que no hay garantías.
1
[Link]
2
[Link]
Chapter 16. Tipos IV: Calificadores y especificadores 125
16.2.5 _Thread_local
Cuando utilizas varios subprocesos y tienes algunas variables en el ámbito global o static del bloque, esta
es una forma de asegurarte de que cada subproceso obtiene su propia copia de la variable. Esto te ayudará a
evitar condiciones de carrera y que los hilos se pisen unos a otros.
Si estás en ámbito de bloque, tienes que usar esto junto con extern o static.
Además, si incluyes <threads.h>, puedes usar el más agradable thread_local como alias del más feo
_Thread_local.
Proyectos multiarchivos
Hasta ahora hemos visto programas de juguete que, en su mayor parte, caben en un único archivo. Pero los
programas C complejos se componen de muchos archivos que se compilan y enlazan en un único ejecutable.
En este capítulo veremos algunos de los patrones y prácticas más comunes para crear proyectos más grandes.
1 // Archivo bar.c
2
1 // Archivo foo.c
2
3 #include <stdio.h>
126
Chapter 17. Proyectos multiarchivos 127
5 int main(void)
6 {
7 printf("%d\n", add(2, 3)); // 5!
8 }
Mira cómo desde main() llamamos a add()–¡pero add() está en un fichero fuente completamente diferente!
Está en bar.c, ¡mientras que la llamada a él está en foo.c!
Si construimos esto con:
(O puede recibir una advertencia. Que no debes ignorar. Nunca ignores las advertencias en C; atiéndelas
todas).
Si recuerdas la sección sobre prototipos, las declaraciones implícitas están prohibidas en el C moderno y no
hay ninguna razón legítima para introducirlas en código nuevo. Deberíamos arreglarlo.
Lo que significa declaración implícita es que estamos usando una función, en este caso add(), sin que
C sepa nada de ella de antemano. C quiere saber qué devuelve, qué tipos toma como argumentos, y cosas
por el estilo.
Ya vimos cómo arreglar esto antes con un prototipo de función. De hecho, si añadimos uno de esos a foo.c
antes de hacer la llamada, todo funciona bien:
1 // File foo.c
2
3 #include <stdio.h>
4
7 int main(void)
8 {
9 printf("%d\n", add(2, 3)); // 5!
10 }
1 // Archivo bar.h
2
Y ahora vamos a modificar foo.c para incluir ese fichero. Suponiendo que está en el mismo directorio, lo
incluimos entre comillas dobles (en lugar de corchetes angulares):
1 // Archivo foo.c
2
3 #include <stdio.h>
4
7 int main(void)
8 {
9 printf("%d\n", add(2, 3)); // 5!
10 }
Fíjate en que ya no tenemos el prototipo en foo.c, lo hemos incluido en bar.h. Ahora cualquier fichero
que quiera la funcionalidad add() puede simplemente #include «bar.h» para obtenerla, y no necesitas
preocuparte de escribir el prototipo de la función.
Como habrás adivinado, #include incluye literalmente el fichero nombrado ahí mismo en tu código fuente,
como si lo hubieras tecleado.
Y al construir y ejecutar:
./foo
5
Peor aún, ¡podríamos llegar a una situación loca en la que la cabecera a.h incluya la cabecera b.h, y b.h
incluya a.h! ¡Es un ciclo infinito #include!
Intentar construir algo así da error:
Lo que tenemos que hacer es que si un archivo se incluye una vez, los subsiguientes #includes para ese
archivo sean ignorados.
Chapter 17. Proyectos multiarchivos 129
Lo que vamos a hacer es tan común que deberías hacerlo automáticamente cada vez que crees un
archivo de cabecera.
Y la forma común de hacerlo es con una variable de preprocesador que establecemos la primera vez que
#incluimos el archivo. Y entonces para los subsiguientes #includes, primero comprobamos que la vari-
able no está definida.
Para ese nombre de variable, es supercomún tomar el nombre del fichero de cabecera, como bar.h, ponerlo
en mayúsculas, y sustituir el punto por un guión bajo: BAR_H.
Por lo tanto, compruebe en la parte superior del archivo si ya se ha incluido y, en caso afirmativo, coméntelo
todo.
(No pongas un guión bajo inicial (porque un guión bajo inicial seguido de una letra mayúscula está reservado)
o un doble guión bajo inicial (porque también está reservado.))
4 // Archivo bar.h
5
Esto hará que el fichero de cabecera se incluya sólo una vez, sin importar en cuántos sitios se intente hacer
#include.
Voila, hemos producido un ejecutable foo a partir de los dos ficheros objeto.
Pero usted está pensando, ¿por qué molestarse? ¿No podemos simplemente:
1
[Link]
Chapter 18
Entorno exterior
Cuando ejecutas un programa, en realidad eres tú quien le habla al shell, diciéndole: «Oye, por favor, ejecuta
esto». Y el intérprete de comandos dice: «Claro», y luego le dice al sistema operativo: «Oye, ¿podrías crear
un nuevo proceso y ejecutar esta cosa?». Y si todo va bien, el sistema operativo cumple y tu programa se
ejecuta.
Pero hay todo un mundo fuera de tu programa en el shell con el que se puede interactuar desde C. Veremos
algunos de ellos en este capítulo.
ls *.txt
./add 10 30 5
45
1
Históricamente, los programas de MS-DOS y Windows hacían esto de forma diferente a Unix. En Unix, el intérprete de comandos
expandía el comodín en todos los archivos coincidentes antes de que el programa lo viera, mientras que las variantes de Microsoft
pasaban la expresión del comodín al programa para que éste se ocupara de ella. En cualquier caso, hay argumentos que se pasan al
programa
131
Chapter 18. Entorno exterior 132
arg 0: ./foo
arg 1: i
arg 2: like
arg 3: turtles
Es un poco raro, porque el argumento zero es el nombre del ejecutable, en sí mismo. Pero es algo a lo que
hay que acostumbrarse. Los argumentos en sí siguen directamente.
Fuente:
1 #include <stdio.h>
2
¡Vaya! ¿Qué pasa con la firma de la función main()? ¿Qué son argc y argv 2 . (pronunciados arg-cee y
arg-vee)?
Empecemos por lo más fácil: argc. Es el número de argumentos, incluido el propio nombre del programa. Si
piensas en todos los argumentos como un array de cadenas, que es exactamente lo que son, entonces puedes
pensar en argc como la longitud de ese array, que es exactamente lo que es.
Y así lo que estamos haciendo en ese bucle es ir a través de todos los argvs e imprimirlos uno a la vez, por
lo que para una entrada dada:
arg 0: ./foo
arg 1: i
arg 2: like
arg 3: turtles
Con esto en mente, deberíamos estar listos para empezar con nuestro programa de sumadores.
Nuestro plan:
• Mirar todos los argumentos de la línea de comandos (pasado argv[0], el nombre del programa)
• Convertirlos en enteros
• Sumarlos a un total
• Imprimir el resultado
Manos a la obra.
2
Como son nombres de parámetros normales, no tienes que llamarlos argc y argv. Pero es tan idiomático usar esos nombres, que
si te pones creativo, otros programadores de C te mirarán con ojos sospechosos
Chapter 18. Entorno exterior 133
1 #include <stdio.h>
2 #include <stdlib.h>
3
11 total += value;
12 }
13
14 printf("%d\n", total);
15 }
Recorridos de muestra:
$ ./add
0
$ ./add 1
1
$ ./add 1 2
3
$ ./add 1 2 3
6
$ ./add 1 2 3 4
10
Por supuesto, podría vomitar si le pasas un no entero, pero endurecer contra eso se deja como un ejercicio
para el lector.
argv[argc] == NULL
¡siempre es cierto!
Esto puede parecer inútil, pero resulta ser útil en un par de lugares; vamos a echar un vistazo a uno de ellos
ahora mismo.
Ahora, ha sido conveniente pensar en argv como una matriz de cadenas, es decir, una matriz de char*s, así
que esto tenía sentido:
¡Sí, es un puntero a un puntero, de acuerdo! Si te resulta más fácil, piensa que es un puntero a una cadena.
Pero en realidad, es un puntero a un valor que apunta a un char.
Recuerda también que son equivalentes:
argv[i]
*(argv + i)
1 #include <stdio.h>
2 #include <stdlib.h>
3
15 total += value;
16 }
17
18 printf("%d\n", total);
19 }
Personalmente, utilizo la notación array para acceder a argv, pero he visto este estilo flotando por ahí, tam-
bién.
• La especificación es bastante liberal con lo que una implementación puede hacer con argv y de dónde
vienen esos valores. Pero en todos los sistemas (en los que he estado) funciona de la misma manera,
como hemos discutido en esta sección.
• Puedes modificar argc, argv, o cualquiera de las cadenas a las que apunta argv. (¡Sólo no hagas esas
cadenas más largas de lo que ya son!)
• En algunos sistemas tipo Unix, modificar la cadena argv[0] hace que la salida de ps cambie3 .
Normalmente, si tienes un programa llamado foo que has ejecutado con ./foo,podrías ver esto en la salida
de ps:
Pero si modificas argv[0] así, teniendo cuidado de que la nueva cadena «Hi!» tenga la misma longitud que
la anterior «. foo»:
y luego ejecutamos ps mientras el programa ./foo aún se está ejecutando, veremos esto en su lugar:
Estado Descripción
EXIT_SUCCESS o 0 El programa ha finalizado correctamente.
EXIT_FAILURE El programa ha finalizado con un error.
Vamos a escribir un programa corto que multiplique dos números desde la línea de comandos. Requeriremos
que especifiques exactamente dos valores. Si no lo hace, vamos a imprimir un mensaje de error, y salir con
un estado de error.
1 #include <stdio.h>
2 #include <stdlib.h>
3
Ahora, si intentamos ejecutarlo, obtendremos el efecto esperado hasta que especifiquemos exactamente el
número correcto de argumentos de la línea de comandos:
$ ./mult
usage: mult x y
$ ./mult 3 4 5
usage: mult x y
$ ./mult 3 4
12
Pero eso no muestra realmente el estado de salida que hemos devuelto, ¿verdad? Sin embargo, podemos hacer
que el shell lo imprima. Asumiendo que estás ejecutando Bash u otro shell POSIX, puedes usar echo $? para
verlo6 .
6
En Windows [Link], escribe echo %errorlevel%. En PowerShell, escribe $LastExitCode
Chapter 18. Entorno exterior 137
Intentémoslo:
$ ./mult
usage: mult x y
$ echo $?
1
$ ./mult 3 4 5
usage: mult x y
$ echo $?
1
$ ./mult 3 4
12
$ echo $?
0
En Linux, si intentas cualquier código fuera del rango 0-255, el código será bitwise AND con 0xff, sujetán-
dolo efectivamente a ese rango.
Puedes programar el shell para que más tarde use estos códigos de estado para tomar decisiones sobre qué
hacer a continuación.
HISTFILE=/home/beej/.bash_history
HISTFILESIZE=500
HISTSIZE=500
HOME=/home/beej
HOSTNAME=FBILAPTOP
HOSTTYPE=x86_64
Chapter 18. Entorno exterior 138
IFS=$' \t\n'
Fíjese en que están en forma de pares clave/valor. Por ejemplo, una clave es HOSTTYPE y su valor es x86_64.
Desde una perspectiva C, todos los valores son cadenas, incluso si son números7 .
Así que, ¡como quieras! Resumiendo, es posible obtener estos valores desde dentro de tu programa C.
Escribamos un programa que utilice la función estándar getenv() para buscar un valor que hayas establecido
en el shell.
La función getenv() devolverá un puntero a la cadena de valores, o bien NULL si la variable de entorno no
existe.
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(void)
5 {
6 char *val = getenv("FROTZ"); // Intenta obtener el valor
7
$ ./foo
No se encuentra la variable de entorno FROTZ
$ ./foo
Value: C is awesome!
De este modo, puede establecer datos en variables de entorno, y puede obtenerlos en su código C y modificar
su comportamiento en consecuencia.
Cada cadena tiene la forma «clave=valor», por lo que tendrás que dividirla y analizarla tú mismo si quieres
obtener las claves y los valores.
Aquí hay un ejemplo de un bucle e impresión de las variables de entorno de un par de maneras diferentes:
1 #include <stdio.h>
2
5 int main(void)
6 {
7 for (char **p = environ; *p != NULL; p++) {
8 printf("%s\n", *p);
9 }
10
SHELL=/bin/bash
COLORTERM=truecolor
TERM_PROGRAM_VERSION=1.53.2
LOGNAME=beej
HOME=/home/beej
... etc ...
Utilice getenv() si es posible porque es más portable. Pero si tienes que iterar sobre variables de entorno,
usar environ puede ser la mejor opción.
Otra forma no estándar de obtener las variables de entorno es como parámetro de main(). Funciona de
forma muy parecida, pero se evita tener que añadir la variable environ extern. Ni siquiera la especificación
POSIX soporta esto.9 que yo sepa, pero es común en la tierra de Unix.
9
[Link]
Chapter 18. Entorno exterior 140
1 #include <stdio.h>
2
Es como usar environ pero incluso menos portable. Es bueno tener objetivos.
Chapter 19
El preprocesador C
Antes de que el programa se compile, pasa por una fase llamada preprocesamiento. Es casi como si hubiera
un lenguaje sobre el lenguaje C que se ejecuta primero. Y genera el código C, que luego se compila.
¡Ya hemos visto esto hasta cierto punto con #include! Ese es el preprocesador C. Cuando ve esa direc-
tiva, incluye el fichero nombrado allí mismo, como si lo hubieras escrito allí. Y entonces el compilador lo
construye todo.
Pero resulta que es mucho más potente que simplemente poder incluir cosas. Puedes definir macros que son
sustituidas… ¡e incluso macros que toman argumentos!
19.1 #include
Empecemos por la que ya hemos visto muchas veces. Se trata, por supuesto, de una forma de incluir otras
fuentes en tu fuente. Muy comúnmente usado con archivos de cabecera.
Mientras que la especificación permite todo tipo de comportamientos con #include, vamos a tomar un
enfoque más pragmático y hablar de la forma en que funciona en todos los sistemas que he visto.
Podemos dividir los ficheros de cabecera en dos categorías: sistema y local. Las cosas que están integradas,
como stdio.h, stdlib.h, math.h, etc., se pueden incluir con corchetes angulares:
#include <stdio.h>
#include <stdlib.h>
Los corchetes angulares le dicen a C: «Oye, no busques este archivo de cabecera en el directorio actual, sino
en el directorio de inclusión de todo el sistema».
Lo que, por supuesto, implica que debe haber una forma de incluir archivos locales del directorio actual. Y
la hay: con comillas dobles:
#include "myheader.h"
O muy probablemente puede buscar en directorios relativos usando barras inclinadas y puntos, así:
#include "mydir/myheader.h"
#include "../[Link]"
¡No use una barra invertida (\) para sus separadores de ruta en su #include! Es un comportamiento in-
definido. Utilice sólo la barra oblicua (/), incluso en Windows.
141
Chapter 19. El preprocesador C 142
En resumen, usa corchetes angulares (< y >) para los includes del sistema, y usa comillas dobles (") para tus
includes personales.
1 #include <stdio.h>
2
6 int main(void)
7 {
8 printf("%s, %f\n", HELLO, PI);
9 }
En las líneas 3 y 4 definimos un par de macros. Dondequiera que aparezcan en el código (línea 8), serán
sustituidas por los valores definidos.
Desde la perspectiva del compilador de C, es exactamente como si hubiéramos escrito esto, en su lugar:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf("%s, %f\n", "Hello, world", 3.14159);
6 }
¿Ve cómo HELLO ha sido sustituido por «Hola, mundo» y PI por 3,14159? Desde la perspectiva del com-
pilador, es como si esos valores hubieran “aparecido” en el código.
Tenga en cuenta que las macros no tienen un tipo específico, per se. Realmente todo lo que ocurre es que
son reemplazadas al por mayor por lo que sea que estén #definidas. Si el código C resultante no es válido,
el compilador vomitará.
También puedes definir una macro sin valor:
#define EXTRA_HAPPY
en ese caso, la macro existe y está definida, pero está definida para no ser nada. Así que en cualquier lugar
que aparezca en el texto será reemplazada por nada. Veremos un uso para esto más adelante.
Es convencional escribir los nombres de las macros en ALL_CAPS aunque técnicamente no sea necesario.
En general, esto le da una manera de definir valores constantes que son efectivamente globales y se pueden
utilizar en cualquier lugar. Incluso en aquellos lugares donde una variable const no funcionaría, por ejemplo
en switch cases y longitudes de array fijas.
Dicho esto, se debate en la red si una variable const tipada es mejor que la macro #define en el caso
general.
Chapter 19. El preprocesador C 143
También puede usarse para reemplazar o modificar palabras clave, un concepto completamente ajeno a const,
aunque esta práctica debería usarse con moderación.
1 #include <stdio.h>
2
3 #define EXTRA_HAPPY
4
5 int main(void)
6 {
7
8 #ifdef EXTRA_HAPPY
9 printf("I'm extra happy!\n");
10 #endif
11
12 printf("OK!\n");
13 }
En ese ejemplo, definimos EXTRA_HAPPY (para que no sea nada, pero está definido), luego en la línea 8
comprobamos si está definido con una directiva #ifdef. Si está definida, el código subsiguiente se incluirá
hasta el #endif.
Por lo tanto, al estar definido, el código se incluirá para la compilación y la salida será:
//#define EXTRA_HAPPY
OK!
Es importante recordar que estas decisiones se toman en tiempo de compilación. El código se compila o
elimina dependiendo de la condición. Esto contrasta con una sentencia if estándar que se evalúa mientras
el programa se está ejecutando.
8 #ifdef EXTRA_HAPPY
9 printf("I'm extra happy!\n");
10 #endif
11
12 #ifndef EXTRA_HAPPY
13 printf("I'm just regular\n");
14 #endif
int x = 12;
Esto demuestra cómo una macro persiste a través de archivos y múltiples #includes. Si aún no está definida,
definámosla y compilemos todo el fichero de cabecera.
Pero la próxima vez que se incluya, vemos que MYHEADER_H está definida, así que no enviamos el fichero
de cabecera al compilador— se elimina efectivamente.
19.3.3 #else
Pero eso no es todo lo que podemos hacer. También podemos añadir un ‘#else.
Modifiquemos el ejemplo anterior:
8 #ifdef EXTRA_HAPPY
9 printf("I'm extra happy!\n");
10 #else
11 printf("I'm just regular\n");
12 #endif
#ifdef MODE_1
printf("This is mode 1\n");
#elifdef MODE_2
Chapter 19. El preprocesador C 145
Por otro lado, puede utilizar #elifndef para «else if not defined».
1 #include <stdio.h>
2
3 #define HAPPY_FACTOR 1
4
5 int main(void)
6 {
7
8 #if HAPPY_FACTOR == 0
9 printf("I'm not happy!\n");
10 #elif HAPPY_FACTOR == 1
11 printf("I'm just regular\n");
12 #else
13 printf("I'm extra happy!\n");
14 #endif
15
16 printf("OK!\n");
17 }
De nuevo, para las cláusulas #if no emparejadas, el compilador ni siquiera verá esas líneas. Para el código
anterior, después de que el preprocesador haya terminado con él, todo lo que el compilador ve es:
1 #include <stdio.h>
2
3 int main(void)
4 {
5
8 printf("OK!\n");
9 }
#if 0
printf(«Todo este código»); /* está efectivamente */
printf(«comentado»); // por el #if 0
#endif
¿Qué pasa si estás en un compilador pre-C23 y no tienes soporte para las directivas #elifdef o #elifndef?
¿Cómo podemos conseguir el mismo efecto con #if? Es decir, qué pasaría si quisiera esto
#ifdef FOO
x = 2;
#elifdef BAR // ERROR POTENCIAL: No soportado antes de C23
x = 3;
#endif
#ifdef FOO
#if defined FOO
#if defined(FOO) // Paréntesis opcional
Como estos:
#ifndef FOO
#if !defined FOO
#if !defined(FOO) // Paréntesis opcional
Observe que podemos utilizar el operador lógico estándar NOT (!) para «no definido».
¡Así que ahora estamos de vuelta en la tierra de #if y podemos usar #elif impunemente!
Este código roto:
#ifdef FOO
x = 2;
#elifdef BAR // ERROR POTENCIAL: No soportado antes de C23
x = 3;
#endif
1 #include <stdio.h>
2
3 int main(void)
4 {
5 #define GOATS
6
7 #ifdef GOATS
8 printf("Goats detected!\n"); // Imprime
9 #endif
10
13 #ifdef GOATS
14 printf("Goats detected, again!\n"); // no imprime
15 #endif
16 }
Macro Descripción
__DATE__ La fecha de compilación –como cuando está compilando este archivo– en formato
Mmm dd yyyy
__TIME__ La hora de compilación en formato hh:mm:ss.
__FILE__ Una cadena que contiene el nombre de este archivo
__LINE__ El número de línea del archivo en el que aparece esta macro
__func__ El nombre de la función en la que aparece, como una cadena2 .
__STDC__ Definido con 1 si se trata de un compilador C estándar
__STDC_HOSTED__ Será 1 si el compilador es una implementación hospedada3 , en caso contrario 0.
__STDC_VERSION__ Esta versión de C, una constante long int de la forma yyyymmL, por ejemplo
201710L.
Pongámoslos juntos.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf("Esta función: %s\n", __func__);
6 printf("Este archivo %s\n", __FILE__);
2
Esto no es realmente una macro—es técnicamente un identificador. Pero es el único identificador predefinido y se parece mucho a
una macro, así que lo incluyo aquí. Como un rebelde
3
Una implementación hospedada significa básicamente que estás ejecutando el estándar C completo, probablemente en un sistema
operativo de algún tipo. Lo cual es probable. Si se está ejecutando en un sistema embebido, probablemente se trate de una imple-
mentación standalone
Chapter 19. El preprocesador C 148
__FILE__, __func__ y __LINE__ son particularmente útiles para informar de condiciones de error en men-
sajes a los desarrolladores. La macro assert() de <assert.h> las utiliza para indicar en qué parte del
código ha fallado la aserción.
[Link] __STDC_VERSION__s
Por si te lo estás preguntando, aquí tienes los números de versión de las distintas versiones principales de la
especificación del lenguaje C:
Macro Descripción
__STDC_ISO_10646__ Si está definido, wchar_t contiene valores Unicode, si no, otra cosa
Un “1” indica que los valores en caracteres multibyte pueden no
__STDC_MB_MIGHT_NEQ_WC__
corresponderse con los valores en caracteres anchos.
__STDC_UTF_16__ Un 1 indica que el sistema utiliza la codificación UTF-16 en el tipo char16_t.
__STDC_UTF_32__ A 1 indicates that the system uses UTF-32 encoding in type char32_t
__STDC_ANALYZABLE__ Un 1 indica que el código es analizable4 .
__STDC_IEC_559__ 1 if IEEE-754 (aka IEC 60559) floating point is supported
1 si se admite la coma flotante compleja IEC 60559
__STDC_IEC_559_COMPLEX__
4
OK, sé que era una respuesta evasiva. Básicamente hay una extensión opcional que los compiladores pueden implementar en la
que se comprometen a limitar ciertos tipos de comportamiento indefinido para que el código C sea más susceptible de análisis estático.
Es poco probable que necesites usar esto
Chapter 19. El preprocesador C 149
Macro Descripción
__STDC_LIB_EXT1__ 1 si esta implementación admite una serie de funciones de
biblioteca estándar
alternativas “seguras”
(tienen sufijos _s en el
nombre)
__STDC_NO_ATOMICS__ 1 si esta implementación no soporta _Atomic o <stdatomic.h>.
__STDC_NO_COMPLEX__ 1 si esta implementación no soporta tipos complejos o <complex.h>.
__STDC_NO_THREADS__ 1 si esta implementación no es compatible con <threads.h>.
__STDC_NO_VLA__ 1 si esta implementación no admite matrices de longitud variable
1 #include <stdio.h>
2
5 int main(void)
6 {
7 printf("%d\n", SQR(12)); // 144
8 }
Lo que está diciendo es “dondequiera que veas SQR con algún valor, reemplázalo con ese valor multiplicado
por sí mismo”.
Así que la línea 7 se cambiará a:
¿Qué ha pasado?
Chapter 19. El preprocesador C 150
Pero en realidad seguimos teniendo el mismo problema que podría manifestarse si tenemos cerca un operador
de mayor precedencia que multiplicar (*).
Así que la forma segura y adecuada de armar la macro es envolver todo entre paréntesis adicionales, así:
Vamos a hacer unas macros que resuelven para 𝑥 usando la fórmula cuadrática. Por si acaso no la tienes en
la cabeza, dice que para ecuaciones de la forma:
𝑎𝑥2 + 𝑏𝑥 + 𝑐 = 0
puedes resolver 𝑥 con la fórmula cuadrática:
√
−𝑏 ± 𝑏2 − 4𝑎𝑐
𝑥=
2𝑎
Lo cual es una locura. También observe el más-o-menos (±) allí, lo que indica que en realidad hay dos
soluciones.
Así que vamos a hacer macros para ambos:
Así que eso nos da algunas matemáticas. Pero vamos a definir una más que podemos utilizar como argumen-
tos a printf() para imprimir ambas respuestas.
Chapter 19. El preprocesador C 151
Eso es sólo un par de valores separados por una coma - y podemos usar eso como un argumento “combinado”
de clases a printf() como esto:
1 #include <stdio.h>
2 #include <math.h> // Para sqrt()
3
8 int main(void)
9 {
10 printf("2*x^2 + 10*x + 5 = 0\n");
11 printf("x = %f or x = %f\n", QUAD(2, 10, 5));
12 }
2*x^2 + 10*x + 5 = 0
x = -0.563508 or x = -4.436492
Si introducimos cualquiera de estos valores, obtendremos aproximadamente cero (un poco desviado porque
los números no son exactos):
2 × −0.5635082 + 10 × −0.563508 + 5 ≈ 0.000003
1 #include <stdio.h>
2
7 int main(void)
8 {
9 printf("%d %f %s %d\n", X(5, 4, 3.14, "Hi!", 12));
10 }
para la salida:
19.5.4 Stringificación
Ya se ha mencionado, justo arriba, que puede convertir cualquier argumento en una cadena precediéndolo de
un # en el texto de sustitución.
Por ejemplo, podríamos imprimir cualquier cosa como una cadena con esta macro y printf():
#define STR(x) #x
printf("%s\n", STR(3.14159));
printf("%s\n", "3.14159");
Veamos si podemos usar esto con mayor efecto para que podamos pasar cualquier nombre de variable int a
una macro, y hacer que imprima su nombre y valor.
1 #include <stdio.h>
2
5 int main(void)
6 {
7 int a = 5;
8
19.5.5 Concatenación
También podemos concatenar dos argumentos con ##. ¡Qué divertido!
#define CAT(a, b) a ## b
Chapter 19. El preprocesador C 153
1 #include <stdio.h>
2
3 #define PRINT_NUMS_TO_PRODUCT(a, b) do { \
4 int product = (a) * (b); \
5 for (int i = 0; i < product; i++) { \
6 printf("%d\n", i); \
7 } \
8 } while(0)
9
10 int main(void)
11 {
12 PRINT_NUMS_TO_PRODUCT(2, 4); // Salida de números del 0 al 7
13 }
1 #include <stdio.h>
2
5 int main(void)
6 {
7 int i = 0;
8
9 if (i == 0)
10 FOO(i);
11 else
12 printf(":-(\n");
13
14 printf("%d\n", i);
15 }
¿Lo ve?
Veamos la expansión:
if (i == 0) {
(i)++;
}; // <-- ¡Problema con MAYÚSCULAS!
else
printf(":-(\n");
El ; pone fin a la sentencia if, así que el else queda flotando por ahí ilegalmente5 .
Así que envuelve esa macro multilínea con un do-while(0).
Quiero que ocurra algo como esto (asumiendo que ASSERT() está en la línea 220 de foo.c):
Podemos obtener el nombre del fichero de la macro __FILE__, y el número de línea de __LINE__. El
mensaje ya es una cadena, pero x < 20 no lo es, así que tendremos que encadenarla con #. Podemos hacer
una macro multilínea utilizando barras invertidas al final de la línea.
#define ASSERT(c, m) \
do { \
if (!(c)) { \
fprintf(stderr, __FILE__ ":%d: assertion %s failed: %s\n", \
__LINE__, #c, m); \
exit(1); \
} \
} while(0)
5
Quebrantando la ley… quebrantando la ley…
Chapter 19. El preprocesador C 155
(Parece un poco raro con __FILE__ así delante, pero recuerda que es una cadena literal, y las cadenas literales
una al lado de la otra se concatenan automáticamente. En cambio, __LINE__ es sólo un int).
¡Y funciona! Si ejecuto esto
int x = 30;
¡Muy bonito!
Lo único que falta es una forma de activarlo y desactivarlo, y podríamos hacerlo con compilación condicional.
Aquí está el ejemplo completo:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 #define ASSERT_ENABLED 1
5
6 #if ASSERT_ENABLED
7 #define ASSERT(c, m) \
8 do { \
9 if (!(c)) { \
10 fprintf(stderr, __FILE__ ":%d: assertion %s failed: %s\n", \
11 __LINE__, #c, m); \
12 exit(1); \
13 } \
14 } while(0)
15 #else
16 #define ASSERT(c, m) // Macro vacía si no está activada
17 #endif
18
19 int main(void)
20 {
21 int x = 30;
22
#ifndef __STDC_IEC_559__
#error I really need IEEE-754 floating point to compile. Sorry!
#endif
Algunos compiladores tienen una directiva complementaria no estándar #warning que mostrará una adver-
tencia pero no detendrá la compilación, pero esto no está en la especificación C11.
int a[] = {
#embed "[Link]"
};
Se trata de una forma muy eficaz de inicializar una matriz con datos binarios sin necesidad de convertirlos
primero en código: ¡el preprocesador lo hace por ti!
Un caso de uso más típico podría ser un archivo que contenga una pequeña imagen para mostrar y que no
quieras cargar en tiempo de ejecución.
He aquí otro ejemplo:
int a[] = {
#embed <[Link]>
};
Si utiliza corchetes angulares, el preprocesador busca en una serie de lugares definidos por la implementación
para localizar el archivo, igual que haría #include. Si utiliza comillas dobles y el recurso no se encuentra,
el compilador lo intentará como si hubiera utilizado paréntesis angulares en un último intento desesperado
por encontrar el archivo.
#embed funciona como #include en el sentido de que pega los valores antes de que el compilador los vea.
Esto significa que puedes usarlo en todo tipo de lugares:
return
#embed "[Link]"
;
o
Chapter 19. El preprocesador C 157
int x =
#embed "[Link]"
;
¿Son siempre bytes? ¿Significa que tendrán valores de 0 a 255, ambos inclusive? La respuesta es definitiva-
mente por defecto “sí”, excepto cuando es “no”.
Técnicamente, los elementos serán CHAR_BIT bits de ancho. Y es muy probable que sean 8 en tu sistema,
por lo que obtendrías ese rango de 0 a 255 en tus valores. (Siempre serán no negativos).
Además, es posible que una implementación permita que esto se anule de alguna manera, por ejemplo, en la
línea de comandos o con parámetros.
El tamaño del fichero en bits debe ser múltiplo del tamaño del elemento. Es decir, si cada elemento tiene 8
bits, el tamaño del fichero (en bits) debe ser múltiplo de 8. En el uso cotidiano, esta es una forma confusa
de decir que cada fichero debe tener un número entero de bytes… que por supuesto lo es. Honestamente,
ni siquiera estoy seguro de por qué me molesté con este párrafo. Lee la especificación si realmente tienes
curiosidad.
int a[] = {
#embed "/dev/random" limit(5)
};
Pero, ¿y si ya tienes definido limit en otro lugar? Afortunadamente puedes poner __ alrededor de la palabra
clave y funcionará de la misma manera:
int a[] = {
#embed "/dev/random" __limit__(5)
};
int x =
#embed "[Link]" if_empty(999)
;
lo conseguiremos:
Pero, ¿y si el archivo [Link] tiene cero bytes (es decir, no contiene datos y está vacío)? En ese caso, se
expandiría a:
En particular, si el limit se establece en 0, entonces el if_empty siempre será sustituido. Es decir, un límite
cero significa que el fichero está vacío.
Esto siempre emitirá x = 999 sin importar lo que haya en [Link]:
int x =
#embed "[Link]" limit(0) if_empty(999)
;
Aquí hay un ejemplo en el que incrustamos tres números aleatorios, pero les ponemos como prefijo 11, y
como sufijo ,99:
int x[] = {
#embed "/dev/urandom" limit(3) prefix(11,) suffix(,99)
};
Ejemplo de resultado:
No es obligatorio utilizar tanto prefix como suffix. Puedes usar ambos, uno, el otro, o ninguno.
Podemos hacer uso de la característica de que estos sólo se aplican a los archivos no vacíos para un efecto
limpio, como se muestra en el siguiente ejemplo descaradamente robado de la especificación.
Supongamos que tenemos un archivo [Link] que contiene algunos datos. Y queremos usar esto para
inicializar un array, y entonces queremos un sufijo en el array que sea un elemento cero.
No hay problema, ¿verdad?
int x[] = {
#embed "[Link]" suffix(,0)
};
y eso no es bueno.
Pero podemos arreglarlo así:
int x[] = {
#embed "[Link]" suffix(,)
0
};
Dado que el parámetro suffix se omite si el archivo está vacío, esto se convertiría simplemente en:
int random_nums[] = {
#if __has_embed("/dev/urandom")
#embed "/dev/urandom" limit(5)
#elif __has_embed("[Link]")
#embed "[Link]" limit(5)
#else
140,178,92,167,120
#endif
};
Tengo buenas razones para creer que __STDC_EMBED_NOT_FOUND__ es 0 y los otros no son cero (porque
está implícito en la propuesta y tiene sentido lógico), pero tengo problemas para encontrarlo en esta versión
del borrador de la especificación.
TODO
Chapter 19. El preprocesador C 160
Puede ser sensato intentar detectar si están disponibles antes de usarlos, y por suerte podemos usar
__has_embed para ayudarnos aquí.
Normalmente, __has_embed() nos dirá si el fichero está ahí o no. Pero, y aquí viene lo divertido, ¡también
devolverá false si algún parámetro adicional no está soportado!
Así que si le damos un fichero que sabemos que existe y un parámetro cuya existencia queremos comprobar,
nos dirá efectivamente si ese parámetro está soportado.
Pero, ¿qué fichero existe siempre? Resulta que podemos usar la macro __FILE__, que se expande al nombre
del fichero fuente que lo referencia. Ese fichero debe existir, o algo va muy mal en el departamento del huevo
y la gallina.
Probemos el parámetro frotz para ver si podemos usarlo:
6
[Link]
Chapter 19. El preprocesador C 161
Hay todo tipo de directivas #pragma documentadas en las cuatro esquinas del globo.
Todos los #pragmas no reconocidos son ignorados por el compilador.
Por ejemplo:
#line 300
#ifdef FOO
#
#else
printf("Something");
#endif
que es sólo cosmético; la línea con el solitario # puede ser eliminado sin ningún efecto nocivo.
O tal vez por coherencia cosmética, así:
#
#ifdef FOO
x = 2;
#endif
#
#if BAR == 17
x = 12;
#endif
#
Mis búsquedas de fundamentos no están dando muchos frutos. Así que voy a decir que esto es algo de
esoterismo de C.
Chapter 20
Resulta que hay mucho más que puedes hacer con structs de lo que hemos hablado, pero es sólo un gran
montón de cosas varias. Así que las incluiremos en este capítulo.
Si eres bueno con lo básico de structs, puedes completar tus conocimientos aquí.
Resulta que tenemos más potencia en estos inicializadores de la que habíamos compartido en un principio.
¡Interesante!
Por un lado, si tienes una subestructura anidada como la siguiente, puedes inicializar miembros de esa sube-
structura siguiendo los nombres de las variables línea abajo:
Veamos un ejemplo:
1 #include <stdio.h>
2
3 struct cabin_information {
4 int window_count;
5 int o2level;
6 };
7
8 struct spaceship {
9 char *manufacturer;
10 struct cabin_information ci;
11 };
12
13 int main(void)
14 {
15 struct spaceship s = {
164
Chapter 20. structs II: Más diversión con structs 165
16 .manufacturer="General Products",
17 .ci.window_count = 8, // <-- ¡INICIALIZADOR ANIDADO!
18 .ci.o2level = 21
19 };
20
Fíjate en las líneas 16-17. Ahí es donde estamos inicializando los miembros de la struct cabin_information
en la definición de s, nuestra struct spaceship.
Y aquí hay otra opción para ese mismo inicializador - esta vez vamos a hacer algo más estándar, pero
cualquiera de los enfoques funciona:
15 struct spaceship s = {
16 .manufacturer="General Products",
17 .ci={
18 .window_count = 8,
19 .o2level = 21
20 }
21 };
Como si la información anterior no fuera lo suficientemente espectacular, también podemos mezclar inicial-
izadores de matrices.
Vamos a cambiar esto para obtener una matriz de información de pasajeros allí, y podemos comprobar cómo
los inicializadores trabajan allí, también.
1 #include <stdio.h>
2
3 struct passenger {
4 char *name;
5 int covid_vaccinated; // Booleano
6 };
7
8 #define MAX_PASSENGERS 8
9
10 struct spaceship {
11 char *manufacturer;
12 struct passenger passenger[MAX_PASSENGERS];
13 };
14
15 int main(void)
16 {
17 struct spaceship s = {
18 .manufacturer="General Products",
19 .passenger = {
20 // Inicializar un campo cada vez
21 [0].name = "Gridley, Lewis",
22 [0].covid_vaccinated = 0,
23
24 // O todos a la vez
Chapter 20. structs II: Más diversión con structs 166
struct animal {
char *name;
int leg_count, speed;
};
Okaaaaay. ¿Así que tenemos una “estructura”, pero no tiene nombre, por lo que no tenemos manera de
utilizarla más tarde? Parece bastante inútil.
Es cierto que en ese ejemplo lo es. Pero todavía podemos hacer uso de ella de un par de maneras.
Una es rara, pero como la struct anónima representa un tipo, podemos simplemente poner algunos nombres
de variables después de ella y usarlos.
[Link] = "antelope";
c.leg_count = 4; // Por ejemplo
animal a, b, c;
[Link] = "antelope";
c.leg_count = 4; // Por ejemplo
Personalmente, no utilizo muchas structs anónimas. Creo que es más agradable ver el struct animal
completo antes del nombre de la variable en una declaración.
Pero eso es sólo mi opinión.
struct node {
int data;
struct node *next;
};
Es importante tener en cuenta que next es un puntero. Esto es lo que permite todo el asunto incluso construir.
A pesar de que el compilador no sabe cómo es el nodo struct completo, todos los punteros tienen el mismo
tamaño.
Aquí hay un programa de lista enlazada para probarlo:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 struct node {
5 int data;
6 struct node *next;
7 };
8
9 int main(void)
10 {
11 struct node *head;
12
19 head->next->next->data = 33;
20 head->next->next->next = NULL;
21
22 // Atraviésalo
23 for (struct node *cur = head; cur != NULL; cur = cur->next) {
24 printf("%d\n", cur->data);
25 }
26 }
11
22
33
struct len_string {
int length;
char data[8];
};
Pero eso tiene “8” codificados como la longitud máxima de una cadena, y eso no es mucho. ¿Qué pasa si
hacemos algo limpio y simplemente malloc() algún espacio extra al final después de la estructura, y luego
dejar que los datos se desborden en ese espacio?
Hagamos eso, y luego asignemos otros 40 bytes encima:
Como data es el último campo de la struct, si desbordamos ese campo, ¡se acaba el espacio que ya
habíamos asignado! Por esta razón, este truco sólo funciona si el array corto es el último campo de la struct.
De hecho, existía una solución común en el compilador para hacer esto, en la que se asignaba un array de
longitud cero al final:
struct len_string {
int length;
char data[0];
Chapter 20. structs II: Más diversión con structs 169
};
Y entonces cada byte extra que asignaste estaba listo para ser usado en esa cadena.
Como data es el último campo de la struct, si desbordamos ese campo, ¡se acaba el espacio que ya
habíamos asignado!
Pero, por supuesto, acceder a los datos más allá del final de la matriz es un comportamiento indefinido. En
estos tiempos modernos, ya no nos dignamos a recurrir a semejante salvajada.
Por suerte para nosotros, todavía podemos conseguir el mismo efecto con C99 y posteriores, pero ahora es
legal.
Cambiemos nuestra definición anterior para que el array no tenga tamaño1 :
struct len_string {
int length;
char data[];
};
De nuevo, esto sólo funciona si el miembro del array flexible es el último campo de la struct.
Y entonces podemos asignar todo el espacio que queramos para esas cadenas haciendo malloc()mayor
que la struct len_string de construcción, como hacemos en este ejemplo que hace una nueva
struct len_string a partir de una cadena C:
ls->length = len;
return ls;
}
Echemos un vistazo a este programa. Obtenemos dos números. Uno es la suma del tamaño de los tipos de
campo individuales. El otro es el tamaño de toda la estructura.
Es de esperar que sean iguales. El tamaño del total es el tamaño de la suma de sus partes, ¿verdad?
1 #include <stdio.h>
2
3 struct foo {
4 int a;
5 char b;
6 int c;
7 char d;
8 };
9
10 int main(void)
11 {
12 printf("%zu\n", sizeof(int) + sizeof(char) + sizeof(int) + sizeof(char));
13 printf("%zu\n", sizeof(struct foo));
14 }
10
16
No son iguales. El compilador ha añadido 6 bytes de relleno para mejorar el rendimiento. Puede que tu
compilador te dé un resultado diferente, pero a menos que lo fuerces, no puedes estar seguro de que no haya
relleno.
20.6 offsetof
En la sección anterior, vimos que el compilador podía inyectar bytes de relleno a voluntad dentro de una
estructura.
¿Y si necesitáramos saber dónde están? Podemos medirlo con offsetof, definido en <stddef.h>.
Modifiquemos el código anterior para imprimir los desplazamientos de los campos individuales en la struct:
1 #include <stdio.h>
2 #include <stddef.h>
3
4 struct foo {
5 int a;
6 char b;
7 int c;
8 char d;
9 };
10
11 int main(void)
12 {
13 printf("%zu\n", offsetof(struct foo, a));
14 printf("%zu\n", offsetof(struct foo, b));
15 printf("%zu\n", offsetof(struct foo, c));
16 printf("%zu\n", offsetof(struct foo, d));
Chapter 20. structs II: Más diversión con structs 171
17 }
0
4
8
12
indicando que estamos utilizando 4 bytes para cada uno de los campos. Es un poco raro, porque char es sólo
1 byte, ¿verdad? El compilador está poniendo 3 bytes de relleno después de cada char para que todos los
campos tengan 4 bytes. Presumiblemente esto se ejecutará más rápido en mi CPU.
struct parent {
int a, b;
};
struct child {
struct parent super; // DEBE ser el primero
int c, d;
};
Entonces podemos pasar un puntero a una struct hija a una función que espera o bien eso o ¡un puntero
a una struct padre!
Como struct padre super es el primer elemento de struct hijo, un puntero a cualquier struct hijo
es lo mismo que un puntero a ese campo super3 .
Pongamos un ejemplo. Haremos structs como arriba, pero luego pasaremos un puntero a una struct hija
a una función que necesita un puntero a una struct padre… y seguirá funcionando.
1 #include <stdio.h>
2
3 struct parent {
4 int a, b;
5 };
6
7 struct child {
8 struct parent super; // DEBE ser el primero
9 int c, d;
10 };
11
3
super no es una palabra clave, por cierto. Sólo estoy robando terminología de programación orientada a objetos
Chapter 20. structs II: Más diversión con structs 172
29 int main(void)
30 {
31 struct child c = {.super.a=1, .super.b=2, .c=3, .d=4};
32
33 print_child(&c);
34 print_parent(&c); // ¡También funciona aunque sea un struct hijo!
35 }
¿Ves lo que hemos hecho en la última línea de main()? Llamamos a print_parent() pero pasamos
una struct child* como argumento. Aunque print_parent() necesita que el argumento apunte a una
struct padre, nos estamos saliendo con la nuestra porque el primer campo de la struct hija es una
struct padre.
De nuevo, esto funciona porque un puntero a una struct tiene el mismo valor que un puntero al primer
campo de esa struct.
Todo depende de esta parte de la especificación:
§[Link]¶15 […] Un puntero a un objeto estructura, convenientemente convertido, apunta a su miem-
bro inicial […], y viceversa.
y
§§6.5¶7** Sólo se puede acceder al valor almacenado de un objeto mediante una expresión > expresión
lvalue que tenga uno de los siguientes tipos: > > * un tipo compatible con el tipo efectivo del objeto > * […]
y mi suposición de que “convenientemente convertido” significa “moldeado al tipo efectivo del miembro
inicial”.
1 #include <stdio.h>
2
3 struct foo {
Chapter 20. structs II: Más diversión con structs 173
4 unsigned int a;
5 unsigned int b;
6 unsigned int c;
7 unsigned int d;
8 };
9
10 int main(void)
11 {
12 printf("%zu\n", sizeof(struct foo));
13 }
Para mí, esto imprime 16. Lo cual tiene sentido, ya que unsigneds son 4 bytes en mi sistema.
Pero, ¿y si supiéramos que todos los valores que se van a almacenar en a y b se pueden almacenar en 5 bits,
y los valores en c, y d se pueden almacenar en 3 bits? Eso es sólo un total de 16 bits. ¿Por qué tener 128 bits
reservados para ellos si sólo vamos a usar 16?
Bueno, podemos decirle a C que por favor intente empaquetar estos valores. Podemos especificar el número
máximo de bits que pueden tener los valores (desde 1 hasta el tamaño del tipo que los contiene).
Esto se hace poniendo dos puntos después del nombre del campo, seguido del ancho del campo en bits.
3 struct foo {
4 unsigned int a:5;
5 unsigned int b:5;
6 unsigned int c:3;
7 unsigned int d:3;
8 };
Ahora, cuando le pregunto a C cuánto mide mi estructura foo, ¡me dice 4! Eran 16 bytes, pero ahora
son sólo 4. Ha “empaquetado” esos 4 valores en 4 bytes, lo que supone un ahorro de memoria cuatro veces
mayor.
La contrapartida es, por supuesto, que los campos de 5 bits sólo pueden contener valores del 0 al 31 y los
de 3 bits sólo pueden contener valores del 0 al 7. Pero la vida es así. Pero, al fin y al cabo, la vida es un
compromiso.
En ese ejemplo, como “a” no es adyacente a “c”, ambas están “empaquetadas” en sus propios “int”.
Así que tenemos un int para a, b, c y d. Como mis ints son de 4 bytes, hay un total de 16 bytes.
Una rápida reorganización nos permite ahorrar espacio, de 16 a 12 bytes (en mi sistema):
Chapter 20. structs II: Más diversión con structs 174
struct foo {
unsigned char a:2;
unsigned char dummy:5;
unsigned char b:1;
};
Y eso funciona–en nuestro código usamos a y b, pero nunca dummy. Sólo está ahí para consumir 5 bits y
asegurarse de que “a” y “b” están en las posiciones “requeridas” (por este ejemplo artificial) dentro del byte.
C nos permite una forma de limpiar esto: campos de bits sin nombre. Puedes omitir el nombre (dummy) en
este caso, y C está perfectamente satisfecho con el mismo efecto:
struct foo {
unsigned char a:2;
unsigned char :5; // <-- campo de bits sin nombre
unsigned char b:1;
};
4
Suponiendo charts de 8 bits, es decir CHAR_BIT == 8.
Chapter 20. structs II: Más diversión con structs 175
struct foo {
unsigned int a:1;
unsigned int b:2;
unsigned int c:3;
unsigned int d:4;
};
el compilador los empaqueta todos en un único unsigned int. ¿Pero qué pasa si necesitas a y b en un int,
y c y d en otro diferente?
Hay una solución para eso: poner un campo de bits sin nombre de ancho 0 donde quieras que el compilador
empiece de nuevo a empaquetar bits en un int diferente:
struct foo {
unsigned int a:1;
unsigned int b:2;
unsigned int :0; // <--Campo de bits sin nombre de ancho cero
unsigned int c:3;
unsigned int d:4;
};
Es análogo a un salto de página explícito en un procesador de textos. Le estás diciendo al compilador: “Deja
de empaquetar bits en este unsigned, y empieza a empaquetarlos en el siguiente”.
Añadiendo el campo de bits sin nombre de ancho cero en ese lugar, el compilador pone a y b en un
unsigned int, y c y d en otro unsigned int. Dos en total, para un tamaño de 8 bytes en mi sistema
(unsigned ints son 4 bytes cada uno).
union foo {
int a, b, c, d, e, f;
float g, h;
char i, j, k, l;
};
Eso son muchos campos. Si esto fuera una estructura, mi sistema me diría que se necesitan 36 bytes para
contenerlo todo.
Pero es una union, así que todos esos campos se solapan en el mismo espacio de memoria. El más grande
es int (o float), que ocupa 4 bytes en mi sistema. Y, de hecho, si pregunto por el sizeof de la unión foo,
¡me dice 4!
El inconveniente es que sólo se puede utilizar uno de esos campos a la vez. Pero…
Esto se llama type punning5 , y lo usarías si realmente supieras lo que estás haciendo, normalmente con algún
tipo de programación de bajo nivel.
Dado que los miembros de una unión comparten la misma memoria, escribir en un miembro afecta necesari-
amente a los demás. Y si se lee de uno lo que se ha escrito en otro, se obtienen efectos extraños.
1 #include <stdio.h>
2
3 union foo {
4 float b;
5 short a;
6 };
7
8 int main(void)
9 {
10 union foo x;
11
12 x.b = 3.14159;
13
3.141590
4048
porque bajo el capó, la representación del objeto para el float 3.14159 era la misma que la representación
del objeto para el short 4048. En mi sistema. Tus resultados pueden variar.
1 #include <stdio.h>
2
3 union foo {
4 int a, b, c, d, e, f;
5 float g, h;
6 char i, j, k, l;
7 };
8
9 int main(void)
10 {
11 union foo x;
5
[Link]
Chapter 20. structs II: Más diversión con structs 177
12
16 x.a = 12;
17 printf("%d\n", x.a); // 12
18 printf("%d\n", *foo_int_p); // 12, nuevamente
19
20 x.g = 3.141592;
21 printf("%f\n", x.g); // 3.141592
22 printf("%f\n", *foo_float_p); // 3.141592, nuevamente
23 }
Lo contrario también es cierto. Si tenemos un puntero a un tipo dentro de union, podemos convertirlo en un
puntero a union y acceder a sus miembros.
union foo x;
int *foo_int_p = (int *)&x; // Puntero a campo int
union foo *p = (union foo *)foo_int_p; // Volver al puntero de la unión
Todo esto sólo te permite saber que, bajo el capó, todos estos valores en un union comienzan en el mismo
lugar en la memoria, y eso es lo mismo que donde todo el ‘union es.
struct a {
int x; //
float y; // Secuencia inicial común
char *p;
};
struct b {
int x; //
float y; // Secuencia inicial común
double *p;
short z;
};
¿Lo ves? Es que empiezan con int seguido de float—esa es la secuencia inicial común. Los miembros en
la secuencia de las structs tienen que ser tipos compatibles. Y lo vemos con x y y, que son int y float
respectivamente.
Ahora vamos a construir una unión de estos:
Chapter 20. structs II: Más diversión con structs 178
union foo {
struct a sa;
struct b sb;
};
Lo que nos dice esta regla es que tenemos garantizado que los miembros de las secuencias iniciales comunes
son intercambiables en código. Es decir:
• [Link].x es lo mismo que [Link].x.
y
• [Link].y es lo mismo que [Link].y.
Porque los campos x e y están ambos en la secuencia inicial común.
Además, los nombres de los miembros de la secuencia inicial común no importan. todo lo que importa es
que los tipos son los mismos.
En conjunto, esto nos permite añadir de forma segura alguna información compartida entre structs en la
union. El mejor ejemplo de esto es probablemente el uso de un campo para determinar el tipo de struct
de todas las structs en la union que está actualmente “en uso”.
Es decir, si no se nos permitiera esto y pasáramos la union a alguna función, ¿cómo sabría esa función qué
miembro de la union es el que debería mirar?
Echa un vistazo a estas structs. Observa la secuencia inicial común:
1 #include <stdio.h>
2
3 struct common {
4 int type; // secuencia inicial común
5 };
6
7 struct antelope {
8 int type; // secuencia inicial común
9
10 int loudness;
11 };
12
13 struct octopus {
14 int type; // secuencia inicial común
15
16 int sea_creature;
17 float intelligence;
18 };
20 union animal {
21 struct common common;
22 struct antelope antelope;
23 struct octopus octopus;
24 };
26 #define ANTELOPE 1
27 #define OCTOPUS 2
Hasta ahora, aquí no ha pasado nada especial. Parece que el campo type es completamente inútil.
Pero ahora hagamos una función genérica que imprima un union animal. De alguna manera tiene que ser
capaz de decir si está mirando un struct antílope o un struct pulpo.
Gracias a la magia de las secuencias iniciales comunes, puede buscar el tipo de animal en cualquiera de estos
lugares para un animal de unión x en particular:
36 case OCTOPUS:
37 printf("Octopus : sea_creature=%d\n", x->octopus.sea_creature);
38 printf(" intelligence=%f\n", x->[Link]);
39 break;
40
41 default:
42 printf("Unknown animal type\n");
43 }
44
45 }
46
47 int main(void)
48 {
49 union animal a = {.[Link]=ANTELOPE, .[Link]=12};
50 union animal b = {.[Link]=OCTOPUS, .octopus.sea_creature=1,
51 .[Link]=12.8};
52
53 print_animal(&a);
54 print_animal(&b);
55 }
Mira cómo en la línea 29 sólo estamos pasando la union –no tenemos ni idea de qué tipo de animal struct
está en uso dentro de ella.
Pero no pasa nada. Porque en la línea 31 comprobamos el tipo para ver si es un antílope o un pulpo. Y
entonces podemos mirar en la struct apropiada para obtener los miembros.
Chapter 20. structs II: Más diversión con structs 180
Definitivamente es posible conseguir este mismo efecto usando sólo structs, pero puedes hacerlo de esta
manera si quieres los efectos de ahorro de memoria de una union.
struct {
int x, y;
} s;
Eso define una variable s que es de tipo struct anónimo (porque la struct no tiene etiqueta de nombre),
con los miembros x e y.
Así que cosas como esta son válidas:
s.x = 34;
s.y = 90;
Resulta que puedes soltar esas structs sin nombre en unions tal y como cabría esperar:
union foo {
struct { // sin nombre!
int x, y;
} a;
union foo f;
f.a.x = 1;
f.a.y = 2;
f.b.z = 3;
f.b.w = 4;
No hay problema.
1 #include <stdio.h>
2
3 struct foo {
4 int x, y;
5 };
6
12 int main(void)
13 {
14 struct foo a = f(); // Se realiza la copia
15
Dato curioso: si haces esto, puedes utilizar el operador . justo después de la llamada a la función:
(Por supuesto, ese ejemplo llama a la función dos veces, de forma ineficiente).
Y lo mismo vale para devolver punteros a structs y unions—sólo asegúrate de usar el operador de flecha
-> en ese caso.
Chapter 21
Caracteres y Strings II
Ya hemos hablado de cómo los tipos char son en realidad tipos de enteros pequeños… pero es lo mismo
para un carácter entre comillas simples.
Pero una cadena entre comillas dobles es del tipo const char *.
Resulta que hay algunos tipos más de cadenas y caracteres, y esto nos lleva a uno de los agujeros de conejo
más infames del lenguaje: todo el asunto multibyte/ancho/Unicode/localización.
Vamos a asomarnos a esa madriguera de conejo, pero sin entrar. …¡Todavía!
char *s = "Hello!";
char t = 'c';
Pero, ¿y si queremos introducir algún carácter especial que no podemos escribir con el teclado porque no
existe (por ejemplo, “€”), o incluso si queremos un carácter que sea una comilla simple? Está claro que no
podemos hacerlo:
char t = ''';
Para hacer estas cosas, utilizamos algo llamado secuencias de escape. Se trata del carácter barra invertida
(\) seguido de otro carácter. Los dos (o más) caracteres juntos tienen un significado especial.
Para nuestro ejemplo de carácter de comilla simple, podemos poner un escape (es decir, \) delante de la
comilla simple central para resolverlo:
char t = '\'';
Ahora C sabe que \' significa sólo una comilla normal que queremos imprimir, no el final de la secuencia
de caracteres.
Puedes decir “barra invertida” o “escape” en este contexto (“escape esa comilla”) y los desarrolladores de C
sabrán de qué estás hablando. Además, “escape” en este contexto es diferente de la tecla Esc o del código
ASCII ESC.
182
Chapter 21. Caracteres y Strings II 183
Codigo Descripción
\n Carácter de nueva línea—cuando se imprime, continúa la salida subsiguiente en la línea
siguiente
\' Comilla simple: se utiliza para una constante de carácter de comilla simple.
\" Comilla doble: se utiliza para una comilla doble en una cadena literal.
\\ Barra diagonal inversa—utilizada para un literal \ en una cadena o carácter
Estos son algunos ejemplos de los escapes y lo que muestran cuando se imprimen.
Código Description
\a Alerta. Esto hace que el terminal emita un sonido o un destello, ¡o ambos!
\b Retroceso. Desplaza el cursor un carácter hacia atrás. No borra el carácter.
\f Alimentar formulario. Esto se mueve a la siguiente “página”, pero eso no tiene mucho
significado moderno. En mi sistema, esto se comporta como \v.
\r Volver. Desplazarse al principio de la misma línea.
\t Tabulador horizontal. Se mueve al siguiente tabulador horizontal. En mi máquina, esto se
alinea en columnas que son múltiplos de 8, pero YMMV.
\v Tabulación vertical. Se mueve al siguiente tabulador vertical. En mi máquina, esto se
mueve a la misma columna en la línea siguiente.
\? Signo de interrogación literal. A veces es necesario para evitar los trígrafos, como se
muestra a continuación.
1 #include <stdio.h>
2 #include <threads.h>
3
4 int main(void)
5 {
6 for (int i = 10; i >= 0; i--) {
7 printf("\rT minutos %d segundo%s... \b", i, i != 1? "s": "");
8
1
me acabo de inventar esa cifra, pero probablemente no esté muy lejos
Chapter 21. Caracteres y Strings II 184
15 printf("\rLiftoff! \n");
16 }
En la línea 7 ocurren varias cosas. En primer lugar, empezamos con un \r para llegar al principio de la
línea actual, luego sobrescribimos lo que haya allí con la cuenta atrás actual. (Hay un operador ternario para
asegurarnos de que imprimimos 1 segundo en lugar de 1 segundos).
Además, hay un espacio después de ... Eso es para que sobrescribamos correctamente el último . cuando
i baje de 10 a 9 y tengamos una columna más estrecha. Pruébalo sin el espacio para ver a qué me refiero.
Y lo envolvemos con un \b para retroceder sobre ese espacio para que el cursor se sitúe en el final exacto de
la línea de una manera estéticamente agradable.
Observe que la línea 14 también tiene un montón de espacios al final para sobrescribir los caracteres que ya
estaban allí desde la cuenta atrás.
Finalmente, tenemos un extraño fflush(stdout) ahí, sea lo que sea lo que signifique. La respuesta corta
es que la mayoría de los terminales están line buffered por defecto, lo que significa que no muestran nada
hasta que se encuentra un carácter de nueva línea. Dado que no tenemos una nueva línea (sólo tenemos
\r), sin esta línea, el programa se quedaría ahí hasta ¡Liftoff! y entonces imprimiría todo en un instante.
fflush() anula este comportamiento y fuerza la salida a suceder ahora.
printf("Doesn't it?\n");
printf("Doesn't it??!\n");
Doesn't it|
Chapter 21. Caracteres y Strings II 185
printf("Doesn't it?\?!\n");
Código Description
\123 Incrusta el byte con valor octal 123, 3 dígitos exactamente.
\x4D Incrusta el byte con valor hexadecimal 4D, 2 dígitos.
\u2620 Incrusta el carácter Unicode en el punto de código con valor hexadecimal 2620, 4 dígitos.
\U0001243F Incrusta el carácter Unicode en el punto de código con valor hexadecimal 1243F, 8
dígitos.
He aquí un ejemplo de la notación octal, menos utilizada, para representar la letra “B” entre “A” y “C”.
Normalmente esto se usaría para algún tipo de carácter especial no imprimible, pero tenemos otras formas
de hacerlo, más abajo, y esto es sólo una demostración octal:
Tenga en cuenta que no hay cero a la izquierda en el número octal cuando se incluye de esta manera. Pero
tiene que tener tres caracteres, así que rellénalo con ceros a la izquierda si es necesario.
Pero mucho más común es utilizar constantes hexadecimales en estos días. Aquí tienes una demostración
que no deberías usar, pero que muestra cómo incrustar los bytes UTF-8 0xE2, 0x80 y 0xA2 en una cadena,
lo que corresponde al carácter Unicode “bullet” (-).
Produce la siguiente salida si estás en una consola UTF-8 (o probablemente basura si no lo estás):
Chapter 21. Caracteres y Strings II 186
• Bullet 1
• Bullet 2
• Bullet 3
Pero esa es una forma deficiente de hacer Unicode. Puedes usar los escapes \u (16 bits) o \U (32 bits) para
referirte a Unicode por el número de punto de código. La viñeta es 2022 (hexadecimal) en Unicode, así que
puedes hacer esto y obtener resultados más portables:
Asegúrese de rellenar “u” con suficientes ceros a la izquierda para llegar a cuatro caracteres, y “U” con
suficientes ceros a la izquierda para llegar a ocho.
Por ejemplo, esa viñeta podría hacerse con “U” y cuatro ceros a la izquierda:
C nos ofrece otra forma de tener valores enteros constantes por nombre: enum.
Por ejemplo:
enum {
ONE=1,
TWO=2
};
En algunos aspectos, puede ser mejor –o diferente– que usar un #define. Diferencias clave:
• Los enums sólo pueden ser tipos enteros.
• #define puede definir cualquier cosa.
• Los enums se muestran a menudo por su nombre de identificador simbólico en un depurador.
• Los números definidos con #define se muestran como números brutos que son más difíciles de cono-
cer mientras se depura.
Ya que son tipos enteros, pueden ser usados en cualquier lugar donde se puedan usar enteros, incluyendo en
dimensiones de arreglos y sentencias case.
Vamos a profundizar en esto.
enum {
SHEEP, // El Valor es 0
WHEAT, // El Valor es 1
WOOD, // El Valor es 2
BRICK, // El Valor es 3
ORE // El Valor es 4
187
Chapter 22. Tipos Enumerados: enum 188
};
enum {
X=2,
Y=18,
Z=-2
};
enum {
X=2,
Y=2,
Z=2
};
si se omiten los valores, la numeración continúa contando en sentido positivo a partir del último valor especi-
ficado. Por ejemplo:
enum {
A, // 0, valor inicial por defecto
B, // 1
C=4, // 4, ajustar manualmente
D, // 5
E, // 6
F=3 // 3, ajustar manualmente
G, // 4
H // 5
}
enum {
X=2,
Y=18,
Z=-2, // <-- Coma final
};
Se ha hecho más popular en los idiomas de las últimas décadas, así que puede que te alegre verlo.
22.1.3 Alcance
enums scope como era de esperar. Si está en el ámbito del fichero, todo el fichero puede verlo. Si está en un
bloque, es local a ese bloque.
Es muy común que los enums se definan en ficheros de cabecera para que puedan ser #include en el ámbito
del fichero.
Chapter 22. Tipos Enumerados: enum 189
22.1.4 Estilo
Como habrás notado, es común declarar los símbolos enum en mayúsculas (con guiones bajos).
Esto no es un requisito, pero es un modismo muy, muy común.
Hagamos un ejemplo donde declaramos una variable r de tipo enum resource que puede contener esos
valores:
enum resource {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
};
if (r == BRICK) {
printf("I'll trade you a brick for two sheep.\n");
}
También puede typedef estos, por supuesto, aunque yo personalmente no me gusta hacerlo.
typedef enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} RESOURCE;
RESOURCE r = BRICK;
Otro atajo que es legal pero raro es declarar variables cuando declaras el enum:
Chapter 22. Tipos Enumerados: enum 190
enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} r = BRICK, s = WOOD;
También puedes dar un nombre al enum para poder utilizarlo más tarde, que es probablemente lo que quieres
hacer en la mayoría de los casos:
En resumen, los enums son una gran manera de escribir código limpio, tipado, de alcance y agradable.
Chapter 23
Aquí es donde cubrimos algunos usos intermedios y avanzados de los punteros. Si no conoces bien los
punteros, repasa los capítulos anteriores sobre punteros y aritmética de punteros antes de empezar con esto.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int x = 3490; // Tipo: int
6 int *p = &x; // Tipo: puntero a un int
7
Bastante sencillo, ¿verdad? Tenemos dos tipos representados: int e int*, y configuramos p para que apunte
a x. Entonces podemos desreferenciar p en la línea 8 e imprimir el valor 3490.
1
Hay un poco de diablo en los detalles con los valores que se almacenan sólo en los registros, pero podemos ignorar con seguridad
que para nuestros propósitos aquí. Además, la especificación C no se pronuncia sobre estos “registros” más allá de la palabra clave
register, cuya descripción no menciona los registros
191
Chapter 23. Punteros III: Punteros a punteros y más 192
Pero, como hemos dicho, podemos tener un puntero a cualquier variable… ¿eso significa que podemos tener
un puntero a p?
En otras palabras, ¿de qué tipo es esta expresión?
Si x es un int, entonces &x es un puntero a un int que hemos almacenado en p que es de tipo int*. ¿En-
tiendes? (¡Repite este párrafo hasta que lo hagas!)
Y por tanto &p es un puntero a un int*, alias “puntero a un puntero a un int”. También conocido como
“int-pointer-pointer”.
¿Lo ha entendido? (¡Repite el párrafo anterior hasta que lo entiendas!)
Escribimos este tipo con dos asteriscos: int **. Veámoslo en acción.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int x = 3490; // Tipo: int
6 int *p = &x; // Tipo: puntero a un int
7 int **q = &p; // Tipo: puntero a puntero a int
8
Vamos a inventar algunas direcciones ficticias para los valores anteriores como ejemplos y ver lo que estas
tres variables podrían parecer en la memoria. Los valores de dirección, a continuación son sólo inventados
por mí para fines de ejemplo:
De hecho, vamos a probarlo de verdad en mi ordenador2 e imprimir los valores de los punteros con %p y
volveré a hacer la misma tabla con las referencias reales (impresas en hexadecimal).
Puedes ver que esas direcciones son las mismas excepto el último byte, así que céntrate en ellas.
2
Es muy probable que obtengas números diferentes en el tuyo
Chapter 23. Punteros III: Punteros a punteros y más 193
En mi sistema, los ints son de 4 bytes, por lo que vemos que la dirección aumenta en 4 de x a p3 y luego
aumenta en 8 de p a q. En mi sistema, todos los punteros son de 8 bytes.
¿Importa si es un int* o un int**? ¿Es uno más bytes que el otro? No. Recuerda que todos los punteros
son direcciones, es decir, índices de memoria. Y en mi máquina puedes representar un índice con 8 bytes…
no importa lo que esté almacenado en ese índice.
Fíjate en lo que hicimos en la línea 9 del ejemplo anterior: hicimos doble dereferencia q para volver a nuestro
3490.
En términos de tipo, cada vez que &, se añade otro nivel de puntero al tipo.
Tenga en cuenta que puede utilizar varias *s seguidas para realizar una desreferencia rápida, como vimos en
el código de ejemplo con **q, más arriba. Cada uno elimina un nivel de indirección.
3
No hay absolutamente nada en la especificación que diga que esto funcionará siempre así, pero resulta que funciona así en mi
sistema
4
Incluso si E es NULL, resulta, extrañamente
Chapter 23. Punteros III: Punteros a punteros y más 194
int *const p;
int ***const p;
p++; // No autorizado
1 int main(void)
2 {
3 int x = 3490;
4 int *const p = &x;
5 int **q = &p;
6 }
¿Qué es lo que ocurre? El compilador nos está diciendo aquí que teníamos una variable que era const, y
estamos asignando su valor a otra variable que no es const. La “constancia” se descarta, que probablemente
no es lo que queríamos hacer.
El tipo de p es int *const p, y &p es del tipo int *const *. E intentamos asignarlo a q.
¡Pero q es int **! ¡Un tipo con diferente constness en el primer *! Así que recibimos un aviso de que el
const en int *const * de p está siendo ignorado y desechado.
int x = 3490;
int *const p = &x;
int *const *q = &p;
Podríamos hacer q aún más constante. Tal como está, arriba, estamos diciendo, “q no es en sí const, pero
la cosa a la que apunta es const”. Pero podríamos hacer que ambos sean const:
int x = 3490;
int *const p = &x;
int *const *const q = &p; // ¡Más const!
return dest;
}
(También hay algunos buenos ejemplos de post-incremento y post-decremento para que los estudies).
Es importante tener en cuenta que la versión anterior es probablemente menos eficiente que la que viene con
su sistema.
Pero puedes pasarle punteros a cualquier cosa, y copiará esos objetos. Puede ser int*, struct animal*, o
cualquier cosa.
Hagamos otro ejemplo que imprima los bytes de representación del objeto de una struct para que podamos
ver si hay algún relleno ahí y qué valores tiene6 .
1 #include <stdio.h>
2
5
[Link] guide/bgclr/html/split/[Link]#man-memcpy
6
Tu compilador de C no está obligado a añadir bytes de relleno, y los valores de cualquier byte de relleno que se añada son indeter-
minados
Chapter 23. Punteros III: Punteros a punteros y más 196
3 struct foo {
4 char a;
5 int b;
6 };
7
8 int main(void)
9 {
10 struct foo x = {0x12, 0x12345678};
11 unsigned char *p = (unsigned char *)&x;
12
Lo que tenemos ahí es una estructura foo que está construida de tal manera que debería animar al compi-
lador a inyectar bytes de relleno (aunque no tiene por qué). Y entonces obtenemos un unsigned char * en
el primer byte de la variable x de la struct foo.
A partir de ahí, todo lo que necesitamos saber es el sizeof x y podemos hacer un bucle a través de esa
cantidad de bytes, imprimiendo los valores (en hexadecimal para mayor facilidad).
Ejecutar esto da un montón de números como salida. He anotado a continuación para identificar donde se
almacenan los valores:
12 | x.a == 0x12
AB |
BF | padding bytes with "random" value
26 |
78 |
56 | x.b == 0x12345678
34 |
12 |
En todos los sistemas, sizeof(char) es 1, y vemos que el primer byte en la parte superior de la salida
contiene el valor 0x12 que almacenamos allí.
Luego tenemos algunos bytes de relleno–para mí, estos variaron de una ejecución a otra.
Finalmente, en mi sistema, sizeof(int) es 4, y podemos ver esos 4 bytes al final. Observa que son los
mismos bytes que hay en el valor hexadecimal 0x12345678, pero extrañamente en orden inverso7 .
Esto es un pequeño vistazo a los bytes de una entidad más compleja en memoria.
• '\0'
• (void *)0
Personalmente, siempre utilizo NULL cuando me refiero a NULL, pero puede que veas otras variantes de vez
en cuando. Aunque '\0' (un byte con todos los bits a cero) también se compara igual, es raro compararlo
con un puntero; deberías comparar NULL contra el puntero. (Por supuesto, muchas veces en el procesamiento
de cadenas, estás comparando la cosa a la que apunta el puntero con '\0, y eso es correcto).
A 0 se le llama la constante de puntero nulo, y, cuando se compara o se asigna a otro puntero, se convierte
en un puntero nulo del mismo tipo.
Además, si te apetece que te firmen, puedes usar intptr_t con el mismo efecto.
int a = 1;
int *p = &a;
p es un puntero a un int, y apunta a un tipo compatible–a saber int–así que estamos bien.
int a = 1;
float *p = (float *)&a;
Aquí hay un programa de demostración que hace algo de aliasing. Toma una variable v de tipo int32_t
y la aliasea a un puntero a una struct words. Esa struct tiene dos int16_ts dentro. Estos tipos son
incompatibles, por lo que estamos violando las reglas estrictas de aliasing. El compilador asumirá que estos
dos punteros nunca apuntan al mismo objeto… pero nosotros estamos haciendo que lo hagan. Lo cual es
malo por nuestra parte.
Veamos si podemos romper algo.
1 #include <stdio.h>
2 #include <stdint.h>
3
4 struct words {
5 int16_t v[2];
6 };
7
19 int main(void)
20 {
21 int32_t v = 0x12345678;
22
25 fun(&v, pw);
26 }
¿Ves cómo paso los dos punteros incompatibles a fun()? Uno de los tipos es int32_t* y el otro es
struct words*.
9
Estoy imprimiendo los valores de 16 bits invertidos porque estoy en una máquina little-endian y así es más fácil de leer
Chapter 23. Punteros III: Punteros a punteros y más 199
12345679, 1234-5679
1234567a, 1234-567a
1234567b, 1234-567b
1234567c, 1234-567c
1234567d, 1234-567d
12345679, 1234-5678
1234567a, 1234-5679
1234567b, 1234-567a
1234567c, 1234-567b
1234567d, 1234-567c
¡Están separados por uno! ¡Pero apuntan al mismo recuerdo! ¿Cómo es posible? Respuesta: es un compor-
tamiento indefinido poner alias a la memoria de esa manera. Todo es posible, excepto que no en el buen
sentido.
Si tu código viola las estrictas reglas de aliasing, que funcione o no depende de cómo alguien decida com-
pilarlo. Y eso es un fastidio, ya que está fuera de tu control. A menos que seas una especie de deidad
omnipotente.
Poco probable, lo siento.
GCC puede ser forzado a no usar las reglas de aliasing estricto con -fno-strict-aliasing. Compilar el
programa de demostración, arriba, con -O3 y esta bandera hace que la salida sea la esperada.
Por último, type punning es usar punteros de diferentes tipos para ver los mismos datos. Antes del aliasing
estricto, este tipo de cosas era bastante común:
int a = 0x12345678;
short b = *((short *)&a); // Viola el aliasing estricto
Si desea realizar puntuaciones (relativamente) seguras, consulte la sección sobre Uniones y puntuaciones.
int cats[100];
ptrdiff_t d = g - f; // la diferencia es de 40
10
Suponiendo que apunten al mismo objeto array.
Chapter 23. Punteros III: Punteros a punteros y más 200
Fíjate también en que no tienes que dar nombres a los parámetros. Pero puedes hacerlo si quieres; simple-
mente se ignoran.
Ahora que sabemos cómo declarar una variable, ¿cómo sabemos qué asignarle? ¿Cómo obtenemos la direc-
ción de una función?
Resulta que hay un atajo, igual que para obtener un puntero a un array: puedes referirte al nombre de la
función sin paréntesis. (Puedes poner un & delante si quieres, pero es innecesario y no es idiomático).
Una vez que tienes un puntero a una función, puedes llamarla simplemente añadiendo paréntesis y una lista
de argumentos.
Hagamos un ejemplo simple donde efectivamente hago un alias para una función estableciendo un puntero
a ella. Luego la llamaremos.
Este código imprime 3490:
1 #include <stdio.h>
2
3 void print_int(int n)
4 {
Chapter 23. Punteros III: Punteros a punteros y más 201
5 printf("%d\n", n);
6 }
7
8 int main(void)
9 {
10 // Asigna p a print_int:
11
Observa cómo el tipo de p representa el valor de retorno y los tipos de parámetros de print_int. Tiene que
ser así, o C se quejará de incompatibilidad de tipos de punteros.
Otro ejemplo muestra cómo podemos pasar un puntero a una función como argumento de otra función.
Escribiremos una función que toma un par de argumentos enteros, más un puntero a una función que opera
sobre esos dos argumentos. Luego imprime el resultado.
1 #include <stdio.h>
2
17 printf("%d\n", result);
18 }
19
20 int main(void)
21 {
22 print_math(add, 5, 7); // 12
23 print_math(mult, 5, 7); // 35
24 }
Tómate un momento para asimilarlo. La idea aquí es que vamos a pasar un puntero a una función a
print_math(), y va a llamar a esa función para hacer algo de matemáticas.
De esta forma podemos cambiar el comportamiento de print_math() pasándole otra función. Puedes ver
que lo hacemos en las líneas 22-23 cuando pasamos punteros a las funciones add y mult, respectivamente.
Ahora, en la línea 13, creo que todos estamos de acuerdo en que la firma de la función print_math() es un
espectáculo para la vista. Y, si puedes creerlo, ésta es en realidad bastante sencilla comparada con algunas
cosas que puedes construir11 .
11
El Lenguaje de Programación Go inspiró su sintaxis de declaración de tipos en lo contrario de lo que hace C.
Chapter 23. Punteros III: Punteros a punteros y más 202
Pero vamos a digerirlo. Resulta que sólo hay tres parámetros, pero son un poco difíciles de ver:
// op x y
// |-----------------| |---| |---|
void print_math(int (*op)(int, int), int x, int y)
El primero, op, es un puntero a una función que toma dos int como argumentos y devuelve un int. Esto
coincide con las firmas de add() y mult().
El segundo y el tercero, x e y, son parámetros int estándar.
Deja que tus ojos recorran la firma lenta y deliberadamente mientras identificas las partes que funcionan.
Una cosa que siempre me llama la atención es la secuencia (*op)(, los paréntesis y el asterisco. Eso te
delata que es un puntero a una función.
Por último, vuelve al capítulo Pointers II para ver un puntero a función ejemplo usando la función incorporada
qsort().
Chapter 24
Estas operaciones numéricas permiten manipular bits individuales de las variables, lo que encaja con el hecho
de que C sea un lenguaje de bajo nivel1 .
Si no estás familiarizado con las operaciones bit a bit, Wikipedia tiene un buen artículo sobre operaciones
bit a bit2 .
203
Chapter 24. Operaciones bit a bit 204
Los nuevos bits se rellenan con ceros, con una posible excepción indicada en el comportamiento definido
por la implementación, a continuación.
Funciones variádicas
Variadic_ es una palabra elegante para referirse a las funciones que toman un número arbitrario de argumen-
tos.
Por ejemplo, una función normal toma un número determinado de argumentos:
Sólo se puede llamar con exactamente dos argumentos que corresponden a los parámetros x e y.
add(2, 3);
add(5, 12);
printf("Hello, world!\n");
printf("The number is %d\n", 2);
printf("The number is %d and pi is %f\n", 2, 3.14159);
printf(); // ERROR
Esto nos lleva a una de las limitaciones de las funciones variádicas en C: deben tener al menos un argumento.
Pero aparte de eso, son bastante flexibles, incluso permiten que los argumentos tengan diferentes tipos como
hace printf().
205
Chapter 25. Funciones variádicas 206
#include <stdio.h>
int main(void)
{
func(2, 3, 4, 5, 6);
}
Así que, genial, podemos obtener ese primer argumento que está en la variable a, pero ¿qué pasa con el resto
de argumentos? ¿Cómo se llega a ellos?
Aquí empieza la diversión.
1 #include <stdio.h>
2 #include <stdarg.h>
3
7 va_list va;
8
14 total += n;
15 }
16
19 return total;
20 }
21
22 int main(void)
23 {
24 printf("%d\n", add(4, 6, 2, -4, 17)); // 6 + 2 - 4 + 17 = 21
25 printf("%d\n", add(2, 22, 44)); // 22 + 44 = 66
26 }
(Tenga en cuenta que cuando se llama a printf(), utiliza el número de %ds (o lo que sea) en la cadena de
formato para saber cuántos argumentos más hay).
Si la sintaxis de va_arg() te parece extraña (debido a ese nombre de tipo suelto flotando por ahí), no eres
el único. Esto se implementa con macros de preprocesador para conseguir toda la magia apropiada.
Y cuando hayas terminado, llama a va_end() para terminar. Debes (según la especificación) llamar a esto
en una variable va_list en particular antes de decidir llamar a va_start() o va_copy() de nuevo. Sé que
aún no hemos hablado de va_copy().
• va_start() para inicializar tu variable va_list
• Repetidamente va_arg() para obtener los valores
• va_end() para desinicializar la variable va_list
También mencioné va_copy() ahí arriba; hace una copia de tu variable va_list exactamente en el mismo
estado. Es decir, si no has empezado con va_arg() con la variable fuente, la nueva tampoco se iniciará. Si
has consumido 5 variables con va_arg() hasta ahora, la copia también lo reflejará.
va_copy()‘ puede ser útil si necesita recorrer los argumentos pero también necesita recordar su posición
actual.
1 #include <stdio.h>
2 #include <stdarg.h>
3
16 return rv;
17 }
18
19 int main(void)
20 {
21 int x = 10;
22 float y = 3.2;
23
¿Ves lo que hemos hecho? En las líneas 12-14 iniciamos una nueva variable va_list, y luego la pasamos
directamente a vprintf(). Y sabe lo que tiene que hacer con ella, porque tiene toda la inteligencia de
printf() incorporada.
Sin embargo, aún tenemos que llamar a va_end() cuando hayamos terminado, ¡así que no lo olvides!
Chapter 26
Configuración regional e
internacionalización
La localización es el proceso de preparar tu aplicación para que funcione bien en distintas localizaciones (o
países).
Como sabrás, no todo el mundo utiliza el mismo carácter para los decimales o para los separadores de miles…
o para la moneda.
Estas localizaciones tienen nombres, y puedes seleccionar una para usarla. Por ejemplo, una configuración
regional de [Link]. podría escribir un número como:
100,000.00
Mientras que en Brasil, lo mismo podría escribirse con las comas y los puntos decimales intercambiados:
100.000,00
Así es más fácil escribir el código para que se adapte fácilmente a otras nacionalidades.
Bueno, más o menos. Resulta que C sólo tiene una configuración regional incorporada, y es limitada. La
especificación realmente deja mucha ambigüedad aquí; es difícil ser completamente portable.
Pero haremos lo que podamos.
Usted querrá llamar a eso para que el programa se inicialice con su configuración regional actual.
Entrando en más detalles, hay una cosa más que puedes hacer y seguir siendo portable:
210
Chapter 26. Configuración regional e internacionalización 211
pero se ejecuta por defecto cada vez que se inicia el programa, por lo que no es necesario que lo hagas tú
mismo.
En la segunda cadena, puedes especificar cualquier configuración regional soportada por tu sistema. Esto
depende completamente del sistema, así que variará. En mi sistema, puedo especificar esto:
Y funcionará. Pero sólo es portable a sistemas que tengan exactamente el mismo nombre para la misma
localización, y no puedes garantizarlo.
Al pasar una cadena vacía ("") como segundo argumento, le estás diciendo a C: “Oye, averigua cuál es la
configuración regional actual en este sistema para que yo no tenga que decírtelo”.
Esta función devuelve un puntero a una struct lconv estáticamente asignada que contiene toda la informa-
ción que estás buscando.
Estos son los campos de struct lconv y sus significados.
Primero, algunas convenciones. Un _p_ significa “positivo”, y _n_ significa “negativo”, y int_ significa
“internacional”. Aunque muchos de ellos son del tipo char o char*, la mayoría (o las cadenas a las que
apuntan) se tratan en realidad como enteros2 .
Antes de continuar, debes saber que CHAR_MAX (de <limits.h>) es el valor máximo que puede contener un
char. Y que muchos de los siguientes valores char lo usan para indicar que el valor no está disponible en
la localización dada.
Campo Descripción
char *mon_decimal_point Carácter puntero decimal para dinero, por ejemplo ".".
char *mon_thousands_sep Carácter separador de miles para dinero, por ejemplo ",".
char *mon_grouping Descripción de la agrupación por dinero (véase más abajo).
char *positive_sign Signo positivo para el dinero, por ejemplo "+" o "".
char *negative_sign Signo negativo para el dinero, por ejemplo "-".
char *currency_symbol Símbolo de moneda, por ejemplo "$".
char frac_digits Al imprimir importes monetarios, cuántos dígitos imprimir después del
punto decimal, por ejemplo 2.
char p_cs_precedes 1 si el símbolo_moneda viene antes del valor de una cantidad
monetaria no negativa, 0 si viene después.
char n_cs_precedes 1 si el símbolo_moneda viene antes del valor para una cantidad
monetaria negativa, 0 si viene después.
1
“Este planeta tiene -o más bien tenía- un problema: la mayoría de las personas que viven en él son infelices durante casi todo el
tiempo. Se sugirieron muchas soluciones para este problema, pero la mayoría de ellas tenían que ver con el movimiento de pequeños
trozos de papel verde, lo cual era extraño porque, en general, no eran los pequeños trozos de papel verde los que eran infelices.” —La
Guía del Autoestopista Galáctico, Douglas Adams
2
Recuerda que char es sólo un entero del tamaño de un byte
Chapter 26. Configuración regional e internacionalización 212
Campo Descripción
char p_sep_by_space Determina la separación del símbolo de moneda del valor para
importes no negativos (véase más abajo).
char n_sep_by_space Determina la separación del símbolo de moneda del valor para los
importes negativos (véase más abajo).
char p_sign_posn Determina la posición de positive_sign para valores no negativos.
char n_sign_posn Determina la posición de positive_sign para valores negativos.
char *int_curr_symbol Símbolo de moneda internacional, por ejemplo "USD".
char int_frac_digits Valor internacional para frac_digits.
char int_p_cs_precedes Valor internacional para p_cs_precedes.
char int_n_cs_precedes Valor internacional para n_cs_precedes.
char int_p_sep_by_space Valor internacional para p_sep_by_space.
char int_n_sep_by_space Valor internacional para n_sep_by_space.
char int_p_sign_posn Valor internacional para p_sign_posn.
char int_n_sign_posn Valor internacional para n_sign_posn.
2 1 0
--- --- ---
$100,000,000.00
Se trata de grupos de tres. El grupo 0 (justo a la izquierda del decimal) tiene 3 dígitos. El grupo 1 (el siguiente
a la izquierda) tiene 3 dígitos, y el último también tiene 3.
Así que podríamos describir estos grupos, de la derecha (el decimal) a la izquierda con un montón de valores
enteros que representan los tamaños de los grupos:
3 3 3
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
pero eso es una locura. Por suerte, podemos especificar 0 para indicar que se repite el tamaño de grupo
anterior:
3 0
Lo que significa repetir cada 3. Eso manejaría $100, $1,000, $10,000, $10,000,000, $100,000,000,000, y así
sucesivamente.
Usted puede ir legítimamente loco con estos para indicar algunas agrupaciones extrañas.
Por ejemplo:
Chapter 26. Configuración regional e internacionalización 213
4 3 2 1 0
indicaría:
$1,0,0,0,0,00,000,0000.00
Otro valor que puede aparecer es CHAR_MAX. Indica que no se debe agrupar más, y puede aparecer en
cualquier parte de la matriz, incluido el primer valor.
3 2 CHAR_MAX
indicaría:
100000000,00,000.00
por ejemplo.
Y el simple hecho de tener CHAR_MAX en la primera posición del array te indicaría que no iba a haber ningún
tipo de agrupación.
Valor Descripción
0 No hay espacio entre el símbolo de la moneda y el valor.
1 Separe el símbolo de moneda (y el signo, si existe) del valor con un espacio.
2 Separe el símbolo de signo del símbolo de moneda (si es adyacente) con un
espacio; de lo contrario, separe el símbolo de signo del valor con un espacio.
Valor Descripción
0 Pon paréntesis alrededor del valor y del símbolo monetario.
1 Coloque el signo delante del símbolo monetario y del valor.
2 Poner el signo después del símbolo monetario y del valor.
3 Poner el signo directamente delante del símbolo de moneda
4 Coloque el signo directamente detrás del símbolo de moneda.
mon_decimal_point = "."
mon_thousands_sep = ","
mon_grouping = 3 3 0
positive_sign = ""
negative_sign = "-"
Chapter 26. Configuración regional e internacionalización 214
currency_symbol = "$"
frac_digits = 2
p_cs_precedes = 1
n_cs_precedes = 1
p_sep_by_space = 0
n_sep_by_space = 0
p_sign_posn = 1
n_sign_posn = 1
int_curr_symbol = "USD "
int_frac_digits = 2
int_p_cs_precedes = 1
int_n_cs_precedes = 1
int_p_sep_by_space = 1
int_n_sep_by_space = 1
int_p_sign_posn = 1
int_n_sign_posn = 1
Macro Descripción
LC_ALL Establece todo lo siguiente a la configuración regional dada.
LC_COLLATE Controla el comportamiento de las funciones strcoll() y strxfrm().
LC_CTYPE Controla el comportamiento de las funciones de tratamiento de caracteres3 ..
LC_MONETARY Controla los valores devueltos por localeconv().
LC_NUMERIC Controla el punto decimal para la familia de funciones printf().
LC_TIME Controla el formato de hora de las funciones de impresión de fecha y hora
strftime() y wcsftime().
3
Excepto isdigit() e isxdigit().
Chapter 27
Antes de empezar, ten en cuenta que esta es un área activa del desarrollo del lenguaje C, ya que trabaja para
superar algunos, erm, dolores de crecimiento. Cuando salga C2x, es probable que haya actualizaciones.
La mayoría de la gente está básicamente interesada en la engañosamente simple pregunta: “¿Cómo uso tal
y tal juego de caracteres en C?”. Ya llegaremos a eso. Pero como veremos, puede que ya funcione en tu
sistema. O puede que tengas que recurrir a una biblioteca de terceros.
Vamos a hablar de muchas cosas en este capítulo—algunas son agnósticas a la plataforma, y otras son especí-
ficas de C.
Hagamos primero un resumen de lo que vamos a ver:
• Antecedentes de Unicode
• Antecedentes de codificación de caracteres
• Conjuntos de caracteres de origen y ejecución
• Uso de Unicode y UTF-8
• Usando otros tipos de caracteres como wchar_t, char16_t, y char32_t
¡Vamos a sumergirnos!
215
Chapter 27. Unicode, caracteres anchos y todo eso 216
Definamos vagamente punto de código como un valor numérico que representa un carácter. (Los puntos de
código también pueden representar caracteres de control no imprimibles, pero suponga que me refiero a algo
como la letra “B” o el carácter “π”).
Cada punto de código representa un carácter único. Y cada carácter tiene asociado un punto de código
numérico único.
Por ejemplo, en Unicode, el valor numérico 66 representa “B”, y 960 representa “π”. Otros mapeados de
caracteres que no son Unicode utilizan valores diferentes, pero olvidémonos de ellos y concentrémonos en
Unicode, ¡el futuro!
Así que eso es una cosa: hay un número que representa a cada carácter. En Unicode, estos números van de
0 a más de 1 millón.
¿Entendido?
Porque estamos a punto de voltear la mesa un poco.
27.3 Codificación
Si recuerdas, un byte de 8 bits puede contener valores de 0 a 255, ambos inclusive. Eso está muy bien para
“B” que es 66—que cabe en un byte. Pero “π” es 960, ¡y eso no cabe en un byte! Necesitamos otro byte.
¿Cómo almacenamos todo eso en la memoria? ¿O qué pasa con los números más grandes, como 195.024?
Necesitaremos varios bytes.
La gran pregunta: ¿cómo se representan estos números en la memoria? Esto es lo que llamamos la codifi-
cación de los caracteres.
Así que tenemos dos cosas: una es el punto de código, que nos indica efectivamente el número de serie de
un carácter concreto. Y tenemos la codificación, que nos dice cómo vamos a representar ese número en la
memoria.
Hay muchas codificaciones. 1 . Pero vamos a ver algunas codificaciones realmente comunes que se usan con
Unicode.
Codificación Descripción
UTF-8 Codificación orientada a bytes que utiliza un número variable de bytes por
carácter. Esta es la que se debe utilizar.
UTF-16 Una codificación de 16 bits por carácter2 .
UTF-32 Una codificación de 32 bits por carácter.
Con UTF-16 y UTF-32, el orden de bytes importa, por lo que puede ver UTF-16BE para big-endian y UTF-
16LE para little-endian. Lo mismo ocurre con UTF-32. Técnicamente, si no se especifica, se debe asumir
big-endian. Pero como Windows usa UTF-16 extensivamente y es little-endian, a veces se asume3 .
Veamos algunos ejemplos. Voy a escribir los valores en hexadecimal porque son exactamente dos dígitos
por byte de 8 bits, y así es más fácil ver cómo se ordenan las cosas en la memoria.
1
Por ejemplo, podríamos almacenar el punto de código en un entero big-endian de 32 bits. ¡Sencillo! Acabamos de inventar una
codificación. En realidad no; eso es lo que es la codificación UTF-32BE. Oh, bueno… ¡volvamos a la rutina!
2
Ish. Técnicamente, es de anchura variable—hay una manera de representar puntos de código superiores a 216 juntando dos carac-
teres UTF 16.
3
Hay un carácter especial llamado Byte Order Mark (BOM), punto de código 0xFEFF, que puede preceder opcionalmente al flujo
de datos e indicar el endianess. Sin embargo, no es obligatorio
Chapter 27. Unicode, caracteres anchos y todo eso 217
Busca ahí los patrones. Tenga en cuenta que UTF-16BE y UTF-32BE son simplemente el punto de código
representado directamente como valores de 16 y 32 bits4 .
Little-endian es lo mismo, excepto que los bytes están en orden little-endian.
Luego tenemos UTF-8 al final. En primer lugar, te darás cuenta de que los puntos de código de un solo byte
se representan como un solo byte. Eso está bien. También puedes observar que los distintos puntos de código
ocupan un número diferente de bytes. Se trata de una codificación de ancho variable.
Así que tan pronto como superamos un cierto valor, UTF-8 empieza a utilizar bytes adicionales para almace-
nar los valores. Y tampoco parecen estar correlacionados con el valor del punto de código.
Los detalles de la codificación UTF-85 quedan fuera del alcance de esta guía, pero basta con saber que tiene
un número variable de bytes por punto de código, y que esos valores de bytes no coinciden con el punto de
código excepto los 128 primeros puntos de código. Si realmente quieres aprender más, Computerphile tiene
un gran video de UTF-8 con Tom Scott6 .
Esto último es lo bueno de Unicode y UTF-8 desde una perspectiva norteamericana: ¡es compatible con
la codificación ASCII de 7 bits! Así que si estás acostumbrado a ASCII, UTF-8 es lo mismo. Todos los
documentos codificados en ASCII también están codificados en UTF-8. (Pero no al revés, obviamente).
Probablemente sea este último punto más que ningún otro el que está impulsando a UTF-8 a conquistar el
mundo.
A B C D E F G H I J K L M
N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m
n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9
! " # % & ' ( ) * + , - . / :
; < = > ? [ \ ] ^ _ { | } ~
4
De nuevo, esto sólo es cierto en UTF-16 para caracteres que caben en dos bytes
5
[Link]
6
[Link]
Chapter 27. Unicode, caracteres anchos y todo eso 218
Esos son los caracteres que puedes utilizar en tu código fuente y seguir siendo 100% portable.
El conjunto de caracteres de ejecución tendrá además caracteres para alerta (campana/flash), retroceso, re-
torno de carro y nueva línea.
Pero la mayoría de la gente no llega a ese extremo y utiliza libremente sus conjuntos de caracteres extendidos
en el código fuente y el ejecutable, especialmente ahora que Unicode y UTF-8 son cada vez más comunes.
Quiero decir, ¡el juego de caracteres básico ni siquiera permite @, $, o !
En particular, es un engorro (aunque posible con secuencias de escape) introducir caracteres Unicode uti-
lizando sólo el juego de caracteres básico.
27.5 Unicode en C
Antes de entrar en la codificación en C, hablemos de Unicode desde el punto de vista de los puntos de código.
Hay una manera en C para especificar caracteres Unicode y estos serán traducidos por el compilador en el
conjunto de caracteres de ejecución7 .
Entonces, ¿cómo lo hacemos?
¿Qué tal el símbolo del euro, punto de código 0x20AC? (Lo he escrito en hexadecimal porque ambas formas
de representarlo en C requieren hexadecimal). ¿Cómo podemos ponerlo en nuestro código C?
Utiliza el escape \u para ponerlo en una cadena, por ejemplo "\u20AC" (las mayúsculas y minúsculas del
hexadecimal no importan). Debe poner exactamente cuatro dígitos hexadecimales después de la “u”, rel-
lenando con ceros a la izquierda si es necesario.
He aquí un ejemplo:
char *s = "\u20AC1.23";
Así pues, \u funciona para los puntos de código Unicode de 16 bits, pero ¿qué pasa con los que tienen más
de 16 bits? Para eso, necesitamos mayúsculas: \U.
Por ejemplo:
char *s = "\U0001D4D1";
Es lo mismo que \u, sólo que con 32 bits en lugar de 16. Son equivalentes:
\u03C0
\U000003C0
char *s = "€1.23";
Y probablemente pueda, dado un compilador moderno. El juego de caracteres fuente será traducido por el
compilador al juego de caracteres de ejecución. Pero los compiladores son libres de vomitar si encuentran
cualquier carácter que no esté incluido en su juego de caracteres extendido, y el símbolo € ciertamente no
está en el juego de caracteres básico.
Advertencia de la especificación: no se puede utilizar \u o \U para codificar ningún punto de código por
debajo de 0xA0 excepto 0x24 ($), 0x40 (@), y 0x60 (“ “‘)—sí, esos son precisamente el trío de signos de
puntuación comunes que faltan en el juego de caracteres básico. Al parecer, esta restricción se relajará en la
próxima versión de la especificación.
Por último, también puede utilizar estos en identificadores en su código, con algunas restricciones. Pero no
quiero entrar en eso aquí. En este capítulo nos centramos en el manejo de cadenas.
Y eso es todo sobre Unicode en C (excepto la codificación).
Pero eso es demasiado fácil. ¡Hagamos las cosas mucho más difíciles! ¡Sí!
Lo que queremos decir con esto es que un carácter concreto que no esté en el juego de caracteres básico
podría estar compuesto por varios bytes. Hasta MB_LEN_MAX de ellos (de <limits.h>). Claro, sólo parece
un carácter en la pantalla, pero podrían ser múltiples bytes.
También puedes meter valores Unicode, como vimos antes:
char *s = "\u20AC1.23";
printf("%zu\n", strlen(s)); // 7!
¡¿La longitud de la cadena de "€1.23" es 7?! ¡Sí! Bueno, en mi sistema, ¡sí! Recuerde que strlen()
devuelve el número de bytes de la cadena, no el número de caracteres. (Cuando lleguemos a “caracteres
anchos”, más adelante, veremos una forma de obtener el número de caracteres de la cadena).
Tenga en cuenta que aunque C permite constantes individuales multibyte char (en oposición a char*), el
comportamiento de éstas varía según la implementación y su compilador podría advertirle de ello.
GCC, por ejemplo, advierte de constantes de caracteres multibyte para las dos líneas siguientes (y, en mi
sistema, imprime la codificación UTF-8):
printf("%x\n", '€');
printf("%x\n", '\u20ac');
Los caracteres anchos pueden representarse mediante varios tipos, pero el más destacado es wchar_t. Es el
principal. Es como char, pero ancho.
Te estarás preguntando si no puedes saber si es Unicode o no, ¿cómo te permite eso mucha flexibilidad a la
hora de escribir código? wchar_t abre algunas de esas puertas, ya que hay un rico conjunto de funciones
que puedes usar para tratar con cadenas wchar_t (como obtener la longitud, etc.) sin preocuparte de la
codificación.
Ahora bien, ¿estos caracteres se almacenan como puntos de código Unicode o no? Depende de la imple-
mentación. Pero puedes comprobar si lo están con la macro . __STDC_ISO_10646__. Si está definida, la
respuesta es: “¡Es Unicode!”.
Más detalladamente, el valor de esa macro es un número entero de la forma yyyymm que le permite saber en
qué estándar Unicode puede confiar—el que estuviera en vigor en esa fecha.
Pero, ¿cómo se utilizan?
Así que si queremos convertir una cadena multibyte en una cadena de caracteres anchos, podemos llamar a
mbstowcs(). Y al revés: wcstombs().
Chapter 27. Unicode, caracteres anchos y todo eso 222
Hagamos una demostración rápida en la que convertiremos una cadena multibyte en una cadena de caracteres
anchos, y compararemos las longitudes de cadena de ambas utilizando sus respectivas funciones.
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <wchar.h>
4 #include <string.h>
5 #include <locale.h>
6
7 int main(void)
8 {
9 // Salir de la configuración regional C a una que probablemente tenga el símbolo del euro
10 setlocale(LC_ALL, "");
11
12 // Cadena multibyte original con el símbolo del euro (punto 20ac de Unicode)
13 char *mb_string = "The cost is \u20ac1.23"; // €1.23
14 size_t mb_len = strlen(mb_string);
15
setlocale(LC_ALL, "");
printf("%zu", len_in_chars); // 7
Muchas de estas funciones utilizan un wint_t para contener caracteres individuales, ya sean pasados o
devueltos.
Está relacionado con wchar_t por naturaleza. Un wint_t es un entero que puede representar todos los
valores del juego de caracteres extendido, y también un carácter especial de fin de fichero, WEOF.
Lo utilizan varias funciones de caracteres anchos orientadas a un solo carácter.
Función de
comparación Descripción
wcscmp() Compara cadenas lexicográficamente.
wcsncmp() Compara cadenas lexicográficamente, con límite de longitud.
wcscoll() Compara cadenas en orden de diccionario por configuración regional.
Chapter 27. Unicode, caracteres anchos y todo eso 225
Función de
comparación Descripción
wmemcmp() Compara la memoria lexicográficamente.
wcsxfrm() Transforma cadenas en versiones tales que wcscmp() se comporta como
wcscoll()9 .
Length/Misc
Function Description
iswalnum() True if the character is alphanumeric.
iswalpha() True if the character is alphabetic.
iswblank() True if the character is blank (space-ish, but not a newline).
iswcntrl() True if the character is a control character.
iswdigit() True if the character is a digit.
iswgraph() True if the character is printable (except space).
iswlower() True if the character is lowercase.
iswprint() True if the character is printable (including space).
9
wcscoll() es lo mismo que wcsxfrm() seguido de wcscmp().
Chapter 27. Unicode, caracteres anchos y todo eso 226
Length/Misc
Function Description
iswpunct() True if the character is punctuation.
iswspace() True if the character is whitespace.
iswupper() True if the character is uppercase.
iswxdigit() True if the character is a hex digit.
towlower() Convert character to lowercase.
towupper() Convert character to uppercase.
mbstate_t mbs;
Esta es una lista de las funciones de conversión reiniciables: tenga en cuenta la convención de nomenclatura
de poner una “r” después del tipo “from”:
• mbrtowc()‘—carácter multibyte a carácter ancho
• wcrtomb()–carácter ancho a multibyte
• mbsrtowcs()—cadena multibyte a cadena de caracteres anchos
• wcsrtombs()—cadena de caracteres anchos a cadena multibyte
Son muy similares a sus equivalentes no reiniciables, salvo que requieren que pases un puntero a tu propia
variable mbstate_t. Y también modifican el puntero de la cadena fuente (para ayudarte si se encuentran
bytes inválidos), por lo que puede ser útil guardar una copia del original.
Aquí está el ejemplo de antes en el capítulo reelaborado para pasar nuestro propio mbstate_t.
Chapter 27. Unicode, caracteres anchos y todo eso 227
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <stddef.h>
4 #include <wchar.h>
5 #include <string.h>
6 #include <locale.h>
7
8 int main(void)
9 {
10 // Salir de la configuración regional C a una que probablemente tenga el símbolo del euro
11 setlocale(LC_ALL, "");
12
13 // Cadena multibyte original con el símbolo del euro (punto 20ac de Unicode)
14 char *mb_string = "The cost is \u20ac1.23"; // €1.23
15 size_t mb_len = strlen(mb_string);
16
36 if (invalid == NULL) {
37 printf("No invalid characters found\n");
38
Para las funciones de conversión que gestionan su propio estado, puedes restablecer su estado interno al
inicial pasando NULL para sus argumentos char*, por ejemplo:
Para la E/S, cada flujo ancho gestiona su propio mbstate_t y lo utiliza para las conversiones de entrada y
salida sobre la marcha.
Y algunas de las funciones de E/S orientadas a bytes como printf() y scanf() mantienen su propio estado
interno mientras hacen su trabajo.
Finalmente, estas funciones de conversión reiniciables tienen su propio estado interno si pasas NULL por el
parámetro mbstate_t. Esto hace que se comporten más como sus homólogas no reiniciables.
27.11.1 UTF-8
To refresh before this section, read the UTF-8 quick note, above.
Aside from that, what are C’s UTF-8 capabilities?
Well, not much, unfortunately.
You can tell C that you specifically want a string literal to be UTF-8 encoded, and it’ll do it for you. You can
prefix a string with u8:
char *s = u8"€123";
Sure! If the extended source character set supports it. (gcc does.)
What if it doesn’t? You can specify a Unicode code point with your friendly neighborhood \u and \U, as
noted above.
But that’s about it. There’s no portable way in the standard library to take arbirary input and turn it into
UTF-8 unless your locale is UTF-8. Or to parse UTF-8 unless your locale is UTF-8.
So if you want to do it, either be in a UTF-8 locale and:
setlocale(LC_ALL, "");
or figure out a UTF-8 locale name on your local machine and set it explicitly like so:
they lose their wide character nature. But the spec refers them as “wide character” types all over the place,
so there we are.
These are here to make things a little more Unicode-friendly, potentially.
To use, include <uchar.h>. (That’s “u”, not “w”.)
This header file doesn’t exist on OS X—bummer. If you just want the types, you can:
#include <stdint.h>
If you’re curious, and I know you are, the values, if UTF-16 or UTF-32, are stored in the native endianess.
That is, you should be able to compare them straight up to Unicode code point values:
#if __STDC_UTF_16__
pi == 0x3C0; // Always true
#else
pi == 0x3C0; // Probably not true
#endif
10
Ish—things get funky with multi-char16_t UTF-16 encodings.
Chapter 27. Unicode, caracteres anchos y todo eso 230
11
[Link]
12
[Link]
Chapter 28
Salir de un programa
Resulta que hay un montón de maneras de hacer esto, e incluso maneras de configurar “ganchos” para que
una función se ejecute cuando un programa salga.
En este capítulo nos sumergiremos en ellas y las comprobaremos.
Ya hemos cubierto el significado del código de estado de salida en la sección Exit Status, así que vuelve allí
y repásalo si es necesario.
Todas las funciones de esta sección están en <stdlib.h>.
Esto se debe a que main() sólo (y no puedo enfatizar lo suficiente este caso especial sólo se aplica a main()
y a ninguna otra función en ninguna parte) tiene un return 0 implícito si te caes del final.
Puedes return explícitamente desde main() cuando quieras, y algunos programadores creen que es más
Correcto tener siempre un return al final de main(). Pero si lo dejas, C pondrá uno por ti.
Así que… aquí están las reglas de return para main():
• Puede devolver un estado de salida desde main() con una sentencia return. main() es la única
función con este comportamiento especial. Usar return en cualquier otra función sólo devuelve desde
esa función a quien la llamó.
• Si no se usa return explícitamente y se sale al final de main(), es como si se hubiera devuelto 0 o
EXIT_SUCCESS.
231
Chapter 28. Salir de un programa 232
28.1.2 exit()
Éste también ha aparecido unas cuantas veces. Si llama a exit() desde cualquier parte de su programa, éste
saldrá en ese punto.
El argumento que pasas a exit() es el estado de salida.
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void on_exit_1(void)
5 {
6 printf("¡Controlador de salida 1 llamado!\n");
7 }
8
9 void on_exit_2(void)
10 {
11 printf("¡Controlador de salida 2 llamado!\n");
12 }
13
14 int main(void)
15 {
16 atexit(on_exit_1);
17 atexit(on_exit_2);
18
Y la salida es:
A punto de salir...
¡Controlador de salida 2 llamado!
¡Controlador de salida 1 llamado!
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 void on_quick_exit_1(void)
5 {
6 printf("Llamada al gestor de salida rápida 1\n");
7 }
8
9 void on_quick_exit_2(void)
10 {
11 printf("Llamada al gestor de salida rápida 2\n");
12 }
13
14 void on_exit(void)
15 {
16 printf("Salida normal... ¡No me llamarán!\n");
17 }
18
19 int main(void)
20 {
21 at_quick_exit(on_quick_exit_1);
22 at_quick_exit(on_quick_exit_2);
23
28 quick_exit(0);
29 }
Funciona igual que exit()/atexit(), excepto por el hecho de que el vaciado y limpieza de ficheros puede
no realizarse.
#define PI 3.14159
versus:
goats -= 100;
1
[Link]
Chapter 29
Manejo de señales
Antes de empezar, voy a aconsejarte que ignores todo este capítulo y utilices las (muy probablemente) supe-
riores funciones de manejo de señales de tu sistema operativo. Los Unix tienen la función .
Una vez aclarado esto, ¿qué son las señales?
Signal Descipción
SIGABRT Terminación anormal—lo que ocurre cuando se llama a abort().
SIGFPE Excepción de coma flotante.
SIGILL Instrucción ilegal.
SIGINT Interrupción: normalmente el resultado de pulsar “CTRL-C”.
SIGSEGV “Violación de segmentación”: acceso inválido a memoria.
SIGTERM Terminación solicitada.
Puede configurar su programa para ignorar, manejar o permitir la acción por defecto para cada uno de ellos
utilizando la función signal().
235
Chapter 29. Manejo de señales 236
1 #include <stdio.h>
2 #include <signal.h>
3
4 int main(void)
5 {
6 char s[1024];
7
12 // Esperar una línea de entrada para que el programa no salga sin más
13 fgets(s, sizeof s, stdin);
14 }
Mira la línea 8: le decimos al programa que ignore “SIGINT”, la señal de interrupción que se activa cuando
se pulsa “CTRL-C”. No importa cuánto la pulses, la señal permanece ignorada. Si comentas la línea 8, verás
que puedes pulsar CTRL-C impunemente y salir del programa en el acto.
sig func
|-----| |---------------|
void (*signal(int sig, void (*func)(int)))(int);
Básicamente, vamos a pasar en el número de señal que estamos interesados en la captura, y vamos a pasar
un puntero a una función de la forma:
Ahora… ¿qué pasa con el resto del prototipo? Básicamente es todo el tipo de retorno. Verás, signal()
devolverá lo que hayas pasado como func en caso de éxito… así que eso significa que devuelve un puntero
a una función que devuelve void y toma un int como argumento.
returned
function indicates we're and
returns returning a that function
void pointer to function takes an int
|--| | |---|
void (*signal(int sig, void (*func)(int)))(int);
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <signal.h>
4
5 int count = 0;
6
25 int main(void)
26 {
27 signal(SIGINT, sigint_handler);
28
Una de las cosas que notarás es que en la línea 14 reiniciamos el manejador de señales. Esto es porque C tiene
la opción de resetear el manejador de señales a su SIG_DFL antes de ejecutar tu manejador personalizado. En
otras palabras. Así que lo reseteamos a la primera para volver a manejarlo en la siguiente.
Chapter 29. Manejo de señales 238
Estamos ignorando el valor de retorno de signal() en este caso. Si lo hubiéramos puesto antes en un
manejador diferente, devolvería un puntero a ese manejador, que podríamos obtener así:
void (*old_handler)(int);
Dicho esto, no estoy seguro de que haya un caso de uso común para esto. Pero si necesitas el antiguo
manejador por alguna razón, puedes conseguirlo de esa manera.
Nota rápida sobre la línea 16—es sólo para decirle al compilador que no advierta que no estamos usando esta
variable. Es como decir, “Sé que no la estoy usando; no tienes que advertirme”.
Y por último verás que he marcado comportamiento indefinido en un par de sitios. Más sobre esto en la
siguiente sección.
Cambiemos nuestro manejador SIGINT para que no haga nada excepto incrementar un valor de tipo
volatile sig_atomic_t. Así contará el número de CTRL-Cs que han sido pulsados.
1 #include <stdio.h>
2 #include <signal.h>
3
15 int main(void)
16 {
17 signal(SIGINT, sigint_handler);
18
¿Otra vez comportamiento indefinido? Yo creo que sí, porque tenemos que leer el valor para incrementarlo
y almacenarlo.
Si sólo queremos posponer la salida una pulsación de CTRL-C, podemos hacerlo sin demasiados problemas.
Pero cualquier otro aplazamiento requeriría un encadenamiento de funciones ridículo.
Lo que haremos es manejarlo una vez, y el manejador restablecerá la señal a su comportamiento por defecto
(es decir, a la salida):
1 #include <stdio.h>
2 #include <signal.h>
3
10 int main(void)
11 {
12 signal(SIGINT, sigint_handler);
13
16 while(1);
17 }
Chapter 29. Manejo de señales 240
Más adelante, cuando veamos las variables atómicas sin bloqueo, veremos una forma de arreglar la versión
count (suponiendo que las variables atómicas sin bloqueo estén disponibles en tu sistema en particular).
Esta es la razón por la que al principio, sugería comprobar el sistema de señales integrado en tu sistema
operativo como una alternativa probablemente superior.
C permite declarar un array cuyo tamaño se determina en tiempo de ejecución. Esto te da los beneficios
del dimensionamiento dinámico en tiempo de ejecución que obtienes con malloc(), pero sin tener que
preocuparte de free() la memoria después.
A mucha gente no le gustan los VLAs. Por ejemplo, han sido prohibidos en el kernel de Linux. Profundizare-
mos más en ese razonamiento más tarde.
Se trata de una característica opcional del lenguaje. La macro __STDC_NO_VLA__ se pone a 1 si los VLAs
no están presentes. (Eran obligatorios en C99, y luego pasaron a ser opcionales en C11).
#if __STDC_NO_VLA__ == 1
#error Sorry, need VLAs for this program!
#endif
Pero como ni GCC ni Clang se molestan en definir esta macro, puede que le saques poco provecho.
Vamos a sumergirnos primero con un ejemplo, y luego buscaremos el diablo en los detalles.
30.1 Lo Básico
Un array normal se declara con un tamaño constante, así:
int v[10];
Pero con VLAs, podemos utilizar un tamaño determinado en tiempo de ejecución para establecer la matriz,
así:
int n = 10;
int v[n];
Ahora, eso parece lo mismo, y en muchos sentidos lo es, pero esto le da la flexibilidad para calcular el tamaño
que necesita, y luego obtener una matriz de exactamente ese tamaño.
Vamos a pedir al usuario que introduzca el tamaño de la matriz, y luego almacenar el índice 10 veces, en
cada uno de los elementos de la matriz:
241
Chapter 30. Matrices de longitud variable (VLA) 242
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int n;
6 char buf[32];
7
12 int v[n];
13
(En la línea 7, tengo un fflush() que debería forzar la salida de la línea aunque no tenga una nueva línea
al final).
La línea 10 es donde declaramos el VLA—una vez que la ejecución pasa esa línea, el tamaño del array se
establece a lo que sea n en ese momento. La longitud del array no se puede cambiar más tarde.
También puedes poner una expresión entre paréntesis:
Algunas restricciones:
• No puedes declarar una VLA en el ámbito de un fichero, y no puedes hacer una static en el ámbito
de un bloque1 .
• No puedes usar una lista inicializadora para inicializar el array.
Además, introducir un valor negativo para el tamaño del array invoca un comportamiento indefinido— al
menos en este universo.
1
Esto se debe a que las VLAs se asignan típicamente en la pila, mientras que las variables static están en el montón. Y la idea con
las VLAs es que serán automáticamente desasignadas cuando el marco de la pila sea vaciado al final de la función
Chapter 30. Matrices de longitud variable (VLA) 243
Hay una implicación sutil y correcta en la línea anterior: la aritmética de punteros funciona como cabría
esperar para una matriz normal. Así que adelante, úsala a tu antojo:
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int n = 5;
6 int v[n];
7
8 int *p = v;
9
10 *(p+2) = 12;
11 printf("%d\n", v[2]); // 12
12
13 p[3] = 34;
14 printf("%d\n", v[3]); // 34
15 }
Al igual que con las matrices normales, puede utilizar paréntesis con sizeof() para obtener el tamaño de
un posible VLA sin tener que declararlo:
int x = 12;
int w = 10;
int h = 20;
int x[h][w];
int y[5][w];
int z[10][w][20];
De nuevo, puedes navegar por ellas como lo harías por un array normal.
1 #include <stdio.h>
2
8 total += v[i];
9
10 return total;
11 }
12
13 int main(void)
14 {
15 int x[5]; // Standard array
16
17 int a = 5;
18 int y[a]; // VLA
19
Pero hay algo más. También puedes hacer saber a C que el array tiene un tamaño VLA específico pasándolo
primero y luego dando esa dimensión en la lista de parámetros:
Por cierto, hay un par de formas de listar un prototipo para la función anterior; una de ellas implica un * si
no se quiere nombrar específicamente el valor en el VLA. Sólo indica que el tipo es un VLA en lugar de un
puntero normal.
Prototipos VLA:
De nuevo, eso de * sólo funciona con el prototipo–en la función en sí, tendrás que poner el tamaño explícito.
Ahora… ¡vamos a lo multidimensional! Aquí empieza la diversión.
1 #include <stdio.h>
2
12 int main(void)
13 {
14 int rows = 4;
15 int cols = 7;
16
17 int matrix[rows][cols];
18
1 #include <stdio.h>
2
12 int main(void)
13 {
14 int rec_count = 3;
15 int records[rec_count][5];
16
22 print_records(rec_count, records);
23 }
Chapter 30. Matrices de longitud variable (VLA) 246
\\ ...
int w = 3, h = 5;
int matrix[h][w];
foo(matrix); // OK!
Del mismo modo, si tiene una función VLA, puede pasarle una matriz normal:
\\ ...
int matrix[3][5];
Pero cuidado: si las dimensiones no coinciden, es probable que se produzcan comportamientos indefinidos.
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int w = 10;
6
16 // Imprimirlos
17 for (int i = 0; i < w; i++)
18 printf("%d\n", x[i]);
19
22 w = 20;
23
goto
1 #include <stdio.h>
2
3 int main(void)
4 {
5 printf("One\n");
6 printf("Two\n");
7
8 goto skip_3;
9
10 printf("Three\n");
11
12 skip_3:
13
14 printf("Five!\n");
15 }
La salida es :
1
[Link]
2
Me gustaría señalar que usar goto en todos estos casos es evitable. Puedes usar variables y bucles en su lugar. Es sólo que algunas
personas piensan que goto produce el mejor código en esas circunstancias
248
Chapter 31. goto 249
One
Two
Five!
goto envía la ejecución saltando a la etiqueta especificada, saltándose todo lo que hay entre medias.
infinite_loop:
print("Hello, world!\n");
goto infinite_loop;
Las etiquetas se omiten durante la ejecución. Lo siguiente imprimirá los tres números en orden como si las
etiquetas no estuvieran allí:
printf("Zero\n");
label_1:
label_2:
printf("One\n");
label_3:
printf("Two\n");
label_4:
printf("Three\n");
Como habrá notado, es una convención común justificar las etiquetas hasta el final a la izquierda. Esto
aumenta la legibilidad porque un lector puede escanear rápidamente para encontrar el destino.
Las etiquetas tienen alcance de función. Es decir, no importa a cuántos niveles de profundidad en los bloques
aparezcan, puedes “ir a ellas” desde cualquier parte de la función.
También significa que sólo se puede “ir a” las etiquetas que están en la misma función que la propia “ir
a”. Las etiquetas de otras funciones están fuera del alcance de goto. Y significa que puedes usar el mismo
nombre de etiqueta en dos funciones, pero no en la misma función.
Como vemos, ese continue, como todos los continues, va a la siguiente iteración del bucle más cercano.
¿Y si queremos continuar en el siguiente bucle exterior, el bucle con i?
Bueno, podemos break para volver al bucle exterior, ¿no?
Chapter 31. goto 250
Eso nos da dos niveles de bucle anidado. Pero si anidamos otro bucle, nos quedamos sin opciones. ¿Qué
pasa con esto, donde no tenemos ninguna declaración que nos llevará a la siguiente iteración de i?
}
}
}
Tenemos un ; al final—eso es porque no se puede tener una etiqueta apuntando al final plano de una sentencia
compuesta (o antes de una declaración de variable).
// Pseudocode
for(...) {
for (...) {
while (...) {
do {
if (some_error_condition)
Chapter 31. goto 251
goto bail;
} while(...);
}
}
}
bail:
// Limpieza aquí
Sin goto, tendrías que comprobar una bandera de condición de error en todos los bucles para llegar hasta el
final.
printf("Done!\n");
break_i:
printf("Done!\n");
if (init_system_1() == -1)
goto shutdown;
if (init_system_2() == -1)
goto shutdown_1;
if (init_system_3() == -1)
goto shutdown_2;
if (init_system_4() == -1)
goto shutdown_3;
shutdown_system4();
shutdown_3:
shutdown_system3();
shutdown_2:
shutdown_system2();
shutdown_1:
shutdown_system1();
shutdown:
print("All subsystems shut down.\n");
Ten en cuenta que estamos apagando en el orden inverso al que inicializamos los subsistemas. Así que si el
subsistema 4 no arranca, apagará el 3, el 2 y el 1 en ese orden.
1 #include <stdio.h>
2 #include <complex.h>
3
3
[Link]
Chapter 31. goto 253
6 if (n == 0)
7 return a;
8
12 int main(void)
13 {
14 for (int i = 0; i < 8; i++)
15 printf("%d! == %ld\n", i, factorial(i, 1));
16 }
1 #include <stdio.h>
2
7 if (n == 0)
8 return a;
9
23 int main(void)
24 {
25 for (int i = 0; i < 8; i++)
26 printf("%d! == %d\n", i, factorial(i, 1));
27 }
Utilicé variables temporales ahí arriba para establecer los siguientes valores de los parámetros antes de saltar
al inicio de la función. ¿Ves cómo corresponden a los argumentos recursivos que estaban en la llamada
recursiva?
Ahora bien, ¿por qué usar variables temporales? Podría haber hecho esto en su lugar:
Chapter 31. goto 254
a *= n;
n -= 1;
goto tco;
y eso funciona muy bien. Pero si descuidadamente invierto esas dos líneas de código:
n -= 1; // MALAS NOTICIAS
a *= n;
—ahora estamos en problemas. Modificamos n antes de usarlo para modificar a. Eso es malo porque no es
así como funciona cuando llamas recursivamente. Usar las variables temporales evita este problema incluso
si no estás atento a ello. Y el compilador probablemente las optimiza, de todos modos.
retry:
byte_count = read(0, buf, sizeof(buf) - 1); // Llamada al sistema Unix read()
Muchos Unix-likes tienen una bandera SA_RESTART que puede pasar a sigaction() para solicitar al SO
que reinicie automáticamente cualquier syscall lenta en lugar de fallar con EINTR.
De nuevo, esto es específico de Unix y está fuera del estándar C.
Dicho esto, es posible usar una técnica similar cada vez que cualquier función deba ser reiniciada.
retry:
pthread_mutex_lock(L1);
if (pthread_mutex_trylock(L2) != 0) {
pthread_mutex_unlock(L1);
goto retry;
Chapter 31. goto 255
save_the_day();
pthread_mutex_unlock(L2);
pthread_mutex_unlock(L1);
Allí el hilo adquiere felizmente el mutex L1, pero entonces falla potencialmente en conseguir el segundo
recurso custodiado por el mutex L2 (si algún otro hilo no cooperativo lo tiene, digamos). Si nuestro hilo no
puede conseguir el bloqueo L2, desbloquea L1 y usa goto para reintentarlo limpiamente.
Esperamos que nuestra heroica hebra consiga finalmente adquirir ambos mutexes y salvar el día, todo ello
evitando el malvado punto muerto.
goto label;
{
int x = 12345;
label:
printf("%d\n", x);
}
goto label;
{
int x;
label:
x = 12345;
printf("%d\n", x);
}
{
int x = 10;
label:
printf("%d\n", x);
}
goto label;
int x = 10;
goto label;
{
int v[x];
label:
printf("Hi!\n");
}
Me aparece un error:
int x = 10;
goto label;
{
label: ;
int v[x];
Chapter 31. goto 257
printf("Hi!\n");
}
Porque de ese modo el VLA se asigna correctamente antes de su inevitable desasignación una vez que queda
fuera del ámbito de aplicación.
Chapter 32
(int []){1,2,3,4}
Ahora, esa línea de código no hace nada por sí misma. Crea un array sin nombre de 4 ints, y luego los tira
sin usarlos.
Podríamos usar un puntero para almacenar una referencia al array…
printf("%d\n", p[1]); // 2
Pero eso parece una forma un poco prolija de tener una matriz. Quiero decir, podríamos haber hecho esto1 :
1
Que no es exactamente lo mismo, ya que es una matriz, no un puntero a un int
258
Chapter 32. Tipos Parte V: Literales compuestos y selecciones genéricas 259
printf("%d\n", p[1]); // 2
return total;
}
Si quisiéramos llamarla, normalmente tendríamos que hacer algo como esto, declarando un array y almace-
nando valores en él para pasárselos a la función:
Pero los objetos sin nombre nos dan una forma de saltarnos la variable pasándola directamente (nombres de
parámetros listados arriba). Compruébalo: vamos a sustituir la variable “a” por una matriz sin nombre que
pasaremos como primer argumento:
// p[] count
// |-----------------| |
int s = sum((int []){1, 2, 3, 4}, 4);
¡Muy hábil!
1 #include <stdio.h>
2
3 struct coord {
4 int x, y;
5 };
6
10 }
11
12 int main(void)
13 {
14 struct coord t = {.x=10, .y=20};
15
¿Suficientemente sencillo?
Vamos a modificarlo para utilizar un objeto sin nombre en lugar de la variable t que estamos pasando a
print_coord().
¡Todavía funciona!
1 #include <stdio.h>
2
3 struct coord {
4 int x, y;
5 };
6
12 int main(void)
13 {
14 // Nota el &
15 // |
16 print_coord(&(struct coord){.x=10, .y=20}); // Imprime "10, 20"
17 }
Además, esto puede ser una buena manera de pasar incluso punteros a objetos simples:
Chapter 32. Tipos Parte V: Literales compuestos y selecciones genéricas 261
Así de fácil.
int *p;
{
p = &(int){10};
}
Del mismo modo, no se puede devolver un puntero a un objeto sin nombre desde una función. El objeto se
desasigna cuando sale del ámbito:
1 #include <stdio.h>
2
3 int *get3490(void)
4 {
5 // No hagas esto
6 return &(int){3490};
7 }
8
9 int main(void)
10 {
11 printf("%d\n", *get3490()); // INVALID: (int){3490} cayó fuera de ámbito
12 }
Piense en su alcance como en el de una variable local normal. Tampoco puedes devolver un puntero a una
variable local.
int x = 3490;
Esto último no tiene nombre, pero es una tontería. También podría hacer el simple en la línea anterior.
Chapter 32. Tipos Parte V: Literales compuestos y selecciones genéricas 262
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int i;
6 float f;
7 char c;
8
9 char *s = _Generic(i,
10 int: "that variable is an int",
11 float: "that variable is a float",
12 default: "that variable is some type"
13 );
14
15 printf("%s\n", s);
16 }
En este caso, i es un int, por lo que coincide con ese caso. Entonces la cadena es sustituida por la expresión.
Así que la línea se convierte en esto cuando el compilador lo ve:
Si el compilador no puede encontrar una coincidencia de tipo en _Generic, busca el caso opcional default
y lo utiliza.
Si no puede encontrar una coincidencia de tipo y no hay “default”, obtendrá un error de compilación. error
de compilación. La primera expresión debe coincidir con uno de los tipos o con default.
Como es inconveniente escribir _Generic una y otra vez, se usa a menudo para hacer el cuerpo de una macro
que pueda ser fácilmente reutilizada repetidamente.
2
Una variable utilizada aquí es una expresión.
Chapter 32. Tipos Parte V: Literales compuestos y selecciones genéricas 263
Hagamos una macro TYPESTR(x) que toma un argumento y devuelve una cadena con el tipo del argumento.
Así, TYPESTR(1) devolverá la cadena "int", por ejemplo.
Allá vamos:
#include <stdio.h>
int main(void)
{
int i;
long l;
float f;
double d;
char c;
Estas salidas:
i is type int
l is type long
f is type float
d is type double
c is type something else
Lo cual no debería sorprender, porque, como dijimos, ese código en main() es reemplazado por lo siguiente
cuando se compila:
int i = 10;
char *s = "Foo!";
Chapter 32. Tipos Parte V: Literales compuestos y selecciones genéricas 264
PRINT_VAL(i);
PRINT_VAL(s);
se obtiene la salida:
i = 10
s = Foo!
1 #include <stdio.h>
2 #include <string.h>
3
20 int main(void)
21 {
22 int i = 10;
23 float f = 3.14159;
24 char *s = "Hello, world!";
25
26 PRINT_VAL(i);
27 PRINT_VAL(f);
28 PRINT_VAL(s);
29 }
para la salida:
i = 10
f = 3.141590
s = Hello, world!
Podríamos haberlo metido todo en una gran macro, pero lo dividí en dos para evitar el sangrado de los ojos.
Chapter 33
Matrices Parte II
En este capítulo vamos a repasar algunas cosas extra relacionadas con los arrays.
• Calificadores de tipo con parámetros de arrays
• La palabra clave static con parámetros de arrays
• Inicializadores parciales de arrays multidimensionales
No son muy comunes, pero los veremos ya que son parte de la nueva especificación.
Y puede que también recuerdes que puedes añadir calificadores de tipo a una variable puntero de esta forma:
int *const p;
int *volatile p;
int *const volatile p;
// etc.
Pero, ¿cómo podemos hacer eso cuando estamos utilizando la notación de matriz en su lista de parámetros?
Resulta que va entre paréntesis. Y puedes poner el recuento opcional después. Las dos líneas siguientes son
equivalentes:
Si tiene una matriz multidimensional, debe colocar los calificadores de tipo en el primer conjunto de
corchetes.
265
Chapter 33. Matrices Parte II 266
Esto es algo que nunca he visto en la naturaleza. Es siempre seguido de una dimensión:
Lo que esto significa, en el ejemplo anterior, es que el compilador va a asumir que cualquier array que pases
a la función tendrá al menos 4 elementos.
Cualquier otra cosa es un comportamiento indefinido.
int main(void)
{
int a[] = {11, 22, 33, 44};
int b[] = {11, 22, 33, 44, 55};
int c[] = {11, 22};
int a[3][2];
#include <stdio.h>
int main(void)
{
int a[3][2] = {
{1, 2},
{3, 4},
{5, 6}
};
1 2
3 4
5 6
Dejemos fuera algunos de los elementos inicializadores y veamos cómo se ponen a cero:
int a[3][2] = {
{1, 2},
{3}, // ¡Deja el 4!
{5, 6}
};
que produce:
1 2
3 0
5 6
int a[3][2] = {
{1, 2},
// {3, 4}, // Solo corta todo esto
{5, 6}
};
1 2
5 6
0 0
Pero si te paras a pensarlo, sólo proporcionamos inicializadores suficientes para dos filas, por lo que se
utilizaron para las dos primeras filas. Y los elementos restantes se inicializaron a cero.
Chapter 33. Matrices Parte II 268
Hasta aquí todo bien. Generalmente, si omitimos partes del inicializador, el compilador pone los elementos
correspondientes a “0”.
Pero pongámonos locos.
int a[3][2] = { 1, 2, 3, 4, 5, 6 };
1 2
3 4
5 6
int a[3][2] = { 1, 2, 3 };
1 2
3 0
0 0
Pero mi recomendación es que si tienes un array 2D, uses un inicializador 2D. Hace el código más legible.
(Excepto para inicializar todo el array con 0, en cuyo caso es idiomático usar {0} sin importar la dimensión
del array).
Chapter 34
Ya hemos visto goto, que salta en el ámbito de la función. Pero longjmp() te permite saltar a un punto
anterior en la ejecución, a una función que llamó a ésta.
Hay muchas limitaciones y advertencias, pero puede ser una función útil para saltar desde lo profundo de la
pila de llamadas a un estado anterior.
En mi experiencia, esta funcionalidad se utiliza muy raramente.
1 #include <stdio.h>
2 #include <setjmp.h>
3
4 jmp_buf env;
5
6 void depth2(void)
7 {
8 printf("Entering depth 2\n");
9 longjmp(env, 3490); // Libertad bajo fianza
10 printf("Leaving depth 2\n"); // Esto no sucederá
11 }
12
13 void depth1(void)
14 {
269
Chapter 34. Saltos largos con setjmp, longjmp 270
20 int main(void)
21 {
22 switch (setjmp(env)) {
23 case 0:
24 printf("Calling into functions, setjmp() returned 0\n");
25 depth1();
26 printf("Returned from functions\n"); // Esto no sucederá
27 break;
28
29 case 3490:
30 printf("Bailed back to main, setjmp() returned 3490\n");
31 break;
32 }
33 }
Si intentas cotejar esa salida con el código, está claro que están pasando cosas realmente funky.
Una de las cosas más notables es que setjmp() devuelve twice. ¿Qué demonios? ¡¿Qué es esta brujería?!
Así que esto es lo que pasa: si setjmp() devuelve 0, significa que has establecido con éxito el “marcador”
en ese punto.
Si devuelve un valor distinto de cero, significa que has vuelto al “marcador” establecido anteriormente. (Y
el valor devuelto es el que pasas a longjmp().)
De esta forma puedes diferenciar entre establecer el marcador y volver a él más tarde.
Así que cuando el código de arriba llama a setjmp() la primera vez, setjmp() almacena el estado en la
variable env y devuelve 0. Más tarde, cuando llamamos a longjmp() con ese mismo env, se restaura el
estado y setjmp() devuelve el valor que se le pasó a longjmp().
Pero una variedad de factores confunden esto, haciendo un número significativo de trampas de compor-
tamiento indefinido.
Técnicamente, sólo tienen que ser volátiles si cambian entre el momento en que se llama a setjmp() y
se llama a longjmp()2 .
Por ejemplo, si ejecutamos este código
int x = 20;
if (setjmp(env) == 0) {
x = 30;
}
if (setjmp(env) == 0) {
x = 30;
}
Ahora el valor será el correcto 30 después de que un longjmp() nos devuelve a este punto.
Eso es demasiado complejo para que lo permita la especificación debido a las maquinaciones que deben
ocurrir al desenrollar la pila y todo eso. No podemos longjmp() volver a una expresión compleja que sólo
se ha ejecutado parcialmente.
Así que hay límites en la complejidad de esa expresión.
• Puede ser toda la expresión controladora de la condicional.
if (setjmp(env)) {...}
• Puede formar parte de una expresión relacional o de igualdad, siempre que el otro operando sea una
constante entera. Y el entero es la expresión controladora del condicional.
if (setjmp(env) == 0) {...}
• El operando de una operación lógica NOT (!), siendo toda la expresión controladora.
if (!setjmp(env)) {...}
setjmp(env);
(void)setjmp(env);
3
Es decir, permanecer asignada hasta que el programa termine sin que haya forma de liberarla
Chapter 35
Tipos incompletos
int main(void)
{
struct foo *x;
union bar *y;
enum baz *z;
}
Nunca hemos dado un tamaño para “a”. Y tenemos punteros a structs foo, bar, y baz que nunca parecen
estar declarados en ninguna parte.
Y las únicas advertencias que recibo son que x, y, y z no se usan.
Estos son ejemplos de tipos incompletos.
Un tipo incompleto es un tipo cuyo tamaño (es decir, el tamaño que obtendrías de sizeof) no se conoce.
Otra forma de verlo es un tipo que no has terminado de declarar.
Puedes tener un puntero a un tipo incompleto, pero no puedes desreferenciarlo o usar aritmética de punteros
en él. Y no se puede sizeof.
¿Qué puedes hacer con él?
274
Chapter 35. Tipos incompletos 275
struct node {
int val;
struct node *next; // El nodo struct está incompleto, ¡pero no pasa nada!
};
Aunque el nodo struct está incompleto en la línea 3, aún podemos declarar un puntero a uno1 .
Podemos hacer lo mismo si tenemos dos structs diferentes que se refieren la una a la otra:
struct a {
struct b *x; // Se refiere a una `estructura b`
};
struct b {
struct a *x; // Se refiere a una `estructura a`.
};
Nunca seríamos capaces de hacer ese par de estructuras sin las reglas relajadas para tipos incompletos.
Culpable más probable: probablemente olvidó #incluir el fichero de cabecera que declara el tipo.
Si es un array no externo sin tamaño seguido de un inicializador, está incompleto hasta la llave de cierre
del inicializador.
1 // File: bar.h
2
3 #ifndef BAR_H
4 #define BAR_H
5
8 #endif
1 // File: bar.c
2
A continuación, puede incluir el encabezado de tantos lugares como desee, y cada uno de esos lugares se
refieren a la misma subyacente my_array.
1 // File: foo.c
2
3 #include <stdio.h>
4 #include "bar.h" // incluye el tipo incompleto para mi_array
5
6 int main(void)
7 {
8 my_array[0] = 10;
9
10 printf("%d\n", my_array[0]);
11 }
Cuando compile varios archivos, recuerde especificar todos los archivos .c al compilador, pero no los
archivos .h, p. ej:
struct foo {
int x, y, z;
}; // ¡Ahora la estructura foo está completa!
Ten en cuenta que aunque void es un tipo incompleto, no hay forma de completarlo. No es que a nadie se le
ocurra hacer esa cosa rara. Pero explica por qué se puede hacer esto:
y no ninguno de estos:
Números complejos
#ifdef __STDC_NO_COMPLEX__
#error Complex numbers not supported!
#endif
Además, hay una macro que indica la adhesión a la norma ISO 60559 (IEEE 754) para matemáticas en coma
flotante con números complejos, así como la presencia del tipo _Imaginary.
#if __STDC_IEC_559_COMPLEX__ != 1
#error Need IEC 60559 complex support!
#endif
1
[Link]
278
Chapter 36. Números complejos 279
_Complex
complex
Ambos significan lo mismo, por lo que es mejor utilizar el más bonito complex.
También dispone de algunos tipos para números imaginarios si su aplicación cumple la norma IEC 60559:
_Imaginary
imaginary
Ambos significan lo mismo, así que puedes usar el más bonito imaginary.
También se obtienen valores para el propio número imaginario 𝑖:
I
_Complex_I
_Imaginary_I
La macro I se establece en _Imaginario_I (si está disponible), o _I_Complejo. Así que sólo tiene que
utilizar I para el número imaginario.
Un inciso: he dicho que si un compilador tiene __STDC_IEC_559_COMPLEX__ a 1, debe soportar tipos
_Imaginary para ser compatible. Esa es mi lectura de la especificación. Sin embargo, no conozco ningún
compilador que soporte _Imaginary aunque tenga __STDC_IEC_559_COMPLEX__. Así que voy a escribir
algo de código con ese tipo que no tengo forma de probar. Lo siento.
Bien, ahora que sabemos que existe un tipo complejo, ¿cómo podemos usarlo?
Eso está muy bien para las declaraciones, pero ¿cómo las inicializamos o asignamos?
Resulta que podemos utilizar una notación bastante natural. Ejemplo
Tampoco hay problema en utilizar otros números de coma flotante para construirlo:
double a = 5;
double b = 2;
double complex x = a + b*I;
También hay un conjunto de macros para ayudar a construir estos. El código anterior podría escribirse uti-
lizando la macro CMPLX(), así:
Pero la macro CMPLX() tratará siempre correctamente los ceros negativos en la parte imaginaria, mientras
que la otra forma podría convertirlos en ceros positivos. Yo pienso2 . Esto parece implicar que si existe la
posibilidad de que la parte imaginaria sea cero, debería usar la macro… ¡pero que alguien me corrija si me
equivoco!
La macro CMPLX() funciona con tipos doble. Hay otras dos macros para float y long double: CMPLXF()
y CMPLXL(). (Estos sufijos “f” y “l” aparecen en prácticamente todas las funciones relacionadas con los
números complejos).
Ahora intentemos lo contrario: si tenemos un número complejo, ¿cómo lo descomponemos en sus partes real
e imaginaria?
Aquí tenemos un par de funciones que extraerán las partes real e imaginaria del número: creal() y cimag():
para la salida:
x = 5.000000 + 2.000000i
y = 10.000000 + 3.000000i
Tenga en cuenta que la i que tengo en la cadena de formato printf() es una i literal que se imprime—no
es parte del especificador de formato. Ambos valores devueltos por creal() y cimag() son double.
Y como siempre, hay variantes float y long double de estas funciones: crealf(), cimagf(), creall(),
and cimagl().
2
Esto ha sido más difícil de investigar, y aceptaré cualquier información adicional que alguien pueda darme. que I podría definirse
como _Complex_I o _Imaginary_I, si este último existe. _Imaginary_I manejará ceros con signo, pero _Complex_I puede que no.
Esto tiene implicaciones con los cortes de rama y otras cosas de números complejos. Tal vez. ¿Te das cuenta de que me estoy saliendo
de mi elemento? En cualquier caso, las macros CMPLX() se comportan como si I estuviera definido como _Imaginary_I, con ceros
con signo, aunque _Imaginary_I no exista en el sistema
Chapter 36. Números complejos 281
1 #include <stdio.h>
2 #include <complex.h>
3
4 int main(void)
5 {
6 double complex x = 1 + 2*I;
7 double complex y = 3 + 4*I;
8 double complex z;
9
10 z = x + y;
11 printf("x + y = %f + %fi\n", creal(z), cimag(z));
12
13 z = x - y;
14 printf("x - y = %f + %fi\n", creal(z), cimag(z));
15
16 z = x * y;
17 printf("x * y = %f + %fi\n", creal(z), cimag(z));
18
19 z = x / y;
20 printf("x / y = %f + %fi\n", creal(z), cimag(z));
21 }
x + y = 4.000000 + 6.000000i
x - y = -2.000000 + -2.000000i
x * y = -5.000000 + 10.000000i
x / y = 0.440000 + 0.080000i
1 #include <stdio.h>
2 #include <complex.h>
3
4 int main(void)
5 {
6 double complex x = 1 + 2*I;
7 double complex y = 3 + 4*I;
8
con la salida:
x == y = 0
x != y = 1
Chapter 36. Números complejos 282
Son iguales si ambos componentes prueban igual. Tenga en cuenta que, al igual que ocurre con todas las
operaciones en coma flotante, podrían ser iguales si se aproximan lo suficiente debido a un error de redondeo3 .
Function Description
ccos() Coseno
csin() Seno
ctan() Tangente
cacos() Arco coseno
casin() Arco seno
catan() Jugar a Settlers of Catan
ccosh() Coseno hiperbólico
csinh() Hyperbolic sine
ctanh() Tangente hiperbólica
cacosh() Arco coseno hiperbólico
casinh() Arco seno hiperbólico
catanh() Arco hiperbólico tangente
Función Descripción
cexp() Base-𝑒 exponente
clog() Logaritmo natural (base-𝑒)
Función Descripción
cabs() Valor absoluto
cpow() Potencia
csqrt() Raíz cuadrada
3
La simplicidad de esta afirmación no hace justicia a la increíble cantidad de trabajo que supone simplemente entender cómo funciona
realmente la coma flotante. [Link]
Chapter 36. Números complejos 283
Función Descripción
creal() Devolver parte real
cimag() Devolver parte imaginaria
CMPLX() Construir un número complejo
carg() Argumento/ángulo de fase
conj() Conjugar4
cproj() Proyección sobre la esfera de Riemann
4
Este es el único que no comienza con una “c” extra, extrañamente.
Chapter 37
C tiene todos esos tipos de enteros pequeños, grandes y más grandes como int y long y todo eso. Y puedes
mirar en la sección sobre límites para ver cuál es el int más grande con INT_MAX y así sucesivamente.
¿Qué tamaño tienen esos tipos? Es decir, ¿cuántos bytes ocupan? Podríamos usar sizeof para obtener esa
respuesta.
Pero, ¿y si quisiera ir por otro camino? ¿Y si necesitara un tipo que tuviera exactamente 32 bits (4 bytes) o
al menos 16 bits o algo así?
¿Cómo podemos declarar un tipo que tenga un tamaño determinado? La cabecera <stdint.h> nos da una
manera.
1
Algunas arquitecturas tienen datos de distinto tamaño con los que la CPU y la RAM pueden operar a mayor velocidad que con otros.
En esos casos, si necesitas el número de 8 bits más rápido, puede que te de un tipo de 16 o 32 bits en su lugar porque simplemente es
más rápido. Así que con esto, no sabrás lo grande que es el tipo, pero será al menos tan grande como tú digas.
284
Chapter 37. Tipos enteros de anchura fija 285
int_least8_t uint_least8_t
int_least16_t uint_least16_t
int_least32_t uint_least32_t
int_least64_t uint_least64_t
int_fast8_t uint_fast8_t
int_fast16_t uint_fast16_t
int_fast32_t uint_fast32_t
int_fast64_t uint_fast64_t
int8_t uint8_t
int16_t uint16_t
int32_t uint32_t
int64_t uint64_t
Pueden definirse otras variantes con anchuras diferentes, pero son opcionales.
intmax_t
uintmax_t
INT8_C(x) UINT8_C(x)
INT16_C(x) UINT16_C(x)
2
Es decir, que el sistema tenga enteros de 8, 16, 32 o 64 bits sin relleno que utilicen la representación del complemento a dos, en
cuyo caso la variante intN_t para ese número concreto de bits debe estar definida
Chapter 37. Tipos enteros de anchura fija 286
INT32_C(x) UINT32_C(x)
INT64_C(x) UINT64_C(x)
INTMAX_C(x) UINTMAX_C(x)
uint16_t x = UINT16_C(12);
intmax_t y = INTMAX_C(3490);
Tenga en cuenta que MIN para todos los tipos sin signo es 0, por lo que, como tal, no hay macro para ello.
Busque allí los patrones. Puedes ver que hay variantes para los tipos fijo, mínimo, rápido y máximo.
Y también tienes una “d” minúscula y una “i” minúscula. Corresponden a los especificadores de formato
printf() %d y %i.
int_least16_t x = 3490;
Para ello, podemos aprovechar un hecho sobre C que puede que hayas olvidado: las cadenas literales adya-
centes se concatenan automáticamente en una sola cadena. Por ejemplo
1 #include <stdio.h>
2 #include <stdint.h>
3 #include <inttypes.h>
4
5 int main(void)
6 {
7 int_least16_t x = 3490;
8
Recuerde: cuando quiera imprimir un tipo entero de tamaño fijo con printf() o scanf(), tome la especi-
ficación de formato correspondiente de <inttypes.h>.
Chapter 38
289
Chapter 38. Funciones de fecha y hora 290
struct tm {
int tm_sec; // segundos después del minuto -- [0, 60]
int tm_min; // minutos después de la hora -- [0, 59]
int tm_hour; // horas desde medianoche -- [0, 23]
int tm_mday; // día del mes -- [1, 31]
int tm_mon; // meses desde enero -- [0, 11]
int tm_year; // años desde 1900
int tm_wday; // días desde el domingo -- [0, 6]
int tm_yday; // días desde el 1 de enero -- [0, 365]
int tm_isdst; // indicador del horario de verano
};
Tenga en cuenta que todo tiene base cero excepto el día del mes.
Es importante saber que puedes poner los valores que quieras en estos tipos. Hay funciones que ayudan a
obtener la hora ahora, pero los tipos contienen una hora, no la hora.
Así que la pregunta es: “¿Cómo se inicializan los datos de estos tipos, y cómo se convierten entre ellos?”
3
Hay que admitir que hay más de dos.
4
[Link]
Chapter 38. Funciones de fecha y hora 291
now = time(NULL);
printf("%s", ctime(&now));
Devuelve una cadena con una forma muy específica que incluye una nueva línea al final:
Así que eso es un poco inflexible. Si quieres más control, deberías convertir ese time_t en un struct tm.
Una vez que tienes tu time_t en una struct tm, se abren todo tipo de puertas. Puede imprimir la hora de
varias maneras, averiguar qué día de la semana es una fecha, y así sucesivamente. O convertirlo de nuevo en
un time_t.
Pronto hablaremos de ello.
struct tm algún_tiempo = {
.tm_year=82, // años desde 1900
.tm_mon=3, // meses desde enero -- [0, 11]
.tm_mday=12, // día del mes -- [1, 31]
.tm_hour=12, // horas desde medianoche -- [0, 23]
.tm_min=0, // minutos después de la hora -- [0, 59]
.tm_sec=4, // segundos después del minuto -- [0, 60]
.tm_isdst=-1, // indicador del horario de verano
Chapter 38. Funciones de fecha y hora 292
};
time_t some_time_epoch;
some_time_epoch = mktime(&some_time);
printf("%s", ctime(&some_time_epoch));
printf("Is DST: %d\n", some_time.tm_isdst);
Salida:
Cuando cargas manualmente una struct tm como esa, debería estar en hora local. mktime() convertirá esa
hora local en una hora de calendario time_t.
Extrañamente, sin embargo, el estándar no nos da una manera de cargar una struct tm con una hora UTC
y convertirla en un time_t. Si quieres hacer eso con Unix-likes, prueba la no-estándar . timegm(). En
Windows, _mkgmtime().
Pero, ¿y si te dijera, querido lector, que hay una forma de tener mucho más control sobre la impresión de la
fecha?
Claro, podríamos pescar campos individuales de la struct tm, pero hay una gran función llamada
strftime() que hará mucho del trabajo duro por ti. Es como printf(), ¡pero para fechas!
Veamos algunos ejemplos. En cada uno de ellos, pasamos un buffer de destino, un número máximo de
caracteres a escribir, y luego una cadena de formato (al estilo de—pero no igual que—printf()) que le dice
a strftime() qué componentes de una struct tm imprimir y cómo.
Puede añadir otros caracteres constantes para incluir en la salida de la cadena de formato, así, al igual que
con printf().
Obtenemos un struct tm en este caso de localtime(), pero cualquier fuente funciona bien.
1 #include <stdio.h>
2 #include <time.h>
3
4 int main(void)
5 {
Chapter 38. Funciones de fecha y hora 293
6 char s[128];
7 time_t now = time(NULL);
8
Hay toneladas de especificadores de formato de impresión de fecha para strftime(), así que asegúrese de
comprobarlos en la fl[ página de referencia strftime() |[Link]
strftime]].
struct timespec {
time_t tv_sec; // Segundos
long tv_nsec; // Nanosegundos (milmillonésimas de segundo)
};
He aquí un ejemplo en el que obtenemos la hora y la imprimimos como valor entero y también como valor
flotante:
Chapter 38. Funciones de fecha y hora 294
timespec_get(&ts, TIME_UTC);
Ejemplo de salida:
1614581530 s, 806325800 ns
1614581530.806326 seconds since epoch
struct timespec también hace su aparición en un número de funciones de threading que necesitan ser
capaces de especificar el tiempo con esa resolución.
1 #include <stdio.h>
2 #include <time.h>
3
4 int main(void)
5 {
6 struct tm time_a = {
7 .tm_year=82, // años desde 1900
8 .tm_mon=3, // meses desde enero -- [0, 11]
9 .tm_mday=12, // día del mes -- [1, 31]
10 .tm_hour=4, // horas desde medianoche -- [0, 23]
11 .tm_min=00, // minutos después de la hora -- [0, 59]
12 .tm_sec=04, // segundos después del minuto -- [0, 60]
13 .tm_isdst=-1, // indicador del horario de verano
14 };
15
16 struct tm time_b = {
17 .tm_year=120, // años desde 1900
18 .tm_mon=10, // meses desde enero -- [0, 11]
19 .tm_mday=15, // día del mes -- [1, 31]
20 .tm_hour=16, // horas desde medianoche -- [0, 23]
21 .tm_min=27, // minutos después de la hora -- [0, 59]
22 .tm_sec=00, // segundos después del minuto -- [0, 60]
5
Lo harás en POSIX, donde time_t es definitivamente un entero. Desafortunadamente el mundo entero no es POSIX, así que ahí
estamos
Chapter 38. Funciones de fecha y hora 295
Output:
Y ya está. Recuerda usar difftime() para tomar la diferencia de tiempo. Aunque puedes simplemente
restar en un sistema POSIX, es mejor ser portable.
Chapter 39
Multihilo (Multithreading)
C11 introdujo, formalmente, el multithreading en el lenguaje C. Es muy similar a POSIX threads1 , si alguna
vez los has usado.
Y si no, no te preocupes. Hablaremos de ello.
Sin embargo, ten en cuenta, que no pretendo que esto sea un tutorial completo de multihilo clásico2 ; tendrás
que coger un libro diferente y muy grueso específicamente para eso. Lo siento.
El roscado es una característica opcional. Si un compilador C11+ define __STDC_NO_THREADS__, los hilos
no estarán presentes en la librería. Por qué decidieron usar un sentido negativo en esa macro es algo que se
me escapa, pero ahí estamos.
Puedes comprobarlo así:
#ifdef __STDC_NO_THREADS__
#error I need threads to build this program!
#endif
Además, es posible que tenga que especificar ciertas opciones del enlazador durante la compilación. En el
caso de los Unix, prueba añadir -lpthreads al final de la línea de órdenes para enlazar la librería pthreads3 :
Si obtiene errores del enlazador en su sistema, podría deberse a que no se incluyó la biblioteca apropiada.
39.1 Background
Los hilos son una forma de hacer que todos esos brillantes núcleos de CPU por los que has pagado trabajen
para ti en el mismo programa.
Normalmente, un programa en C se ejecuta en un único núcleo de la CPU. Pero si sabes cómo dividir el
trabajo, puedes dar partes de él a un número de hilos y hacer que lo hagan simultáneamente.
Aunque la especificación no lo dice, en tu sistema es muy probable que C (o el SO a sus órdenes) intente
equilibrar los hilos entre todos los núcleos de la CPU.
1
[Link]
2
Yo soy más un fan de compartir-nada, y mis habilidades con las construcciones de multihilo clásico están oxidadas, por decir algo
3
Sí, pthreads con “p”. Es la abreviatura de POSIX threads, una librería de la que C11 tomó prestado libremente para su imple-
mentación de hilos
296
Chapter 39. Multihilo (Multithreading) 297
Y si tienes más hilos que núcleos, no pasa nada. Simplemente no te darás cuenta de todas esas ganancias si
todos ellos están tratando de competir por el tiempo de CPU.
4. Mientras tanto, el hilo principal puede seguir haciendo lo que tenga que hacer.
5. Cuando el hilo principal lo decida, puede esperar a que el hilo hijo termine llamando a la función .
thrd_join(). Generalmente debe thrd_join() el hilo para limpiar después de él o de lo contrario
perderá memoria5 .
thrd_create() toma un puntero a la función a ejecutar, y es de tipo thrd_start_t, que es
int (*)(void *). Esto significa en griego “un puntero a una función que toma un void* como
argumento, y devuelve un int”.
¡Vamos a crear un hilo! Lo lanzaremos desde el hilo principal con thrd_create() para ejecutar una fun-
ción, hacer algunas otras cosas, y luego esperar a que se complete con thrd_join(). He llamado a la
función principal del hilo run(), pero puedes llamarla como quieras siempre que los tipos coincidan con
thrd_start_t.
1 #include <stdio.h>
2 #include <threads.h>
3
20 int main(void)
21 {
22 thrd_t t; // t contendrá el ID del hilo
23 int arg = 3490;
24
25 printf("Launching a thread\n");
26
5
A menos que thrd_detach(). Más sobre esto más adelante
Chapter 39. Multihilo (Multithreading) 299
40
41 thrd_join(t, &res);
42
¿Ves cómo hicimos el thrd_create() allí para llamar a la función run()? Luego hicimos otras cosas en
main() y luego paramos y esperamos a que el hilo se complete con thrd_join().
Launching a thread
Doing other things while the thread runs
Waiting for thread to complete...
THREAD: Running thread with arg 3490
Thread exited with return value 12
La arg que pases a la función tiene que tener un tiempo de vida lo suficientemente largo como para que el
hilo pueda recogerla antes de que desaparezca. Además, no debe ser sobrescrita por el hilo principal antes
de que el nuevo hilo pueda utilizarla.
Veamos un ejemplo que lanza 5 hilos. Una cosa a tener en cuenta aquí es cómo usamos un array de thrd_ts
para mantener un registro de todos los IDs de los hilos.
1 #include <stdio.h>
2 #include <threads.h>
3
10 return i;
11 }
12
13 #define THREAD_COUNT 5
14
15 int main(void)
16 {
17 thrd_t t[THREAD_COUNT];
18
19 int i;
20
21 printf("Launching threads...\n");
22 for (i = 0; i < THREAD_COUNT; i++)
23
Cuando ejecuto los hilos, cuento i de 0 a 4. Y le paso un puntero a thrd_create(). Este puntero termina
en la rutina run() donde hacemos una copia del mismo.
¿Es sencillo? Aquí está el resultado:
Launching threads...
THREAD 2: running!
THREAD 3: running!
THREAD 4: running!
THREAD 2: running!
Doing other things while the thread runs...
Waiting for thread to complete...
Thread 2 complete!
Thread 2 complete!
THREAD 5: running!
Thread 3 complete!
Thread 4 complete!
Thread 5 complete!
All threads complete!
¿Qué…? ¿Dónde está el “HILO 0”? ¿Y por qué tenemos un THREAD 5 cuando claramente i nunca es más
de 4 cuando llamamos a thrd_create()? ¿Y dos “THREAD 2”? ¡Qué locura!
Esto es entrar en la divertida tierra de condiciones de carrera. El hilo principal está modificando i antes de
que el hilo tenga la oportunidad de copiarlo. De hecho, i llega hasta 5 y termina el bucle antes de que el
último hilo tenga la oportunidad de copiarlo.
Tenemos que tener una variable por hilo a la que podamos referirnos para poder pasarla como arg.
Podríamos tener un gran array de ellas. O podríamos malloc() espacio (y liberarlo en algún lugar - tal vez
en el propio hilo.)
Vamos a intentarlo:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <threads.h>
4
13 return i;
14 }
15
16 #define THREAD_COUNT 5
17
18 int main(void)
19 {
20 thrd_t t[THREAD_COUNT];
21
22 int i;
23
24 printf("Launching threads...\n");
25 for (i = 0; i < THREAD_COUNT; i++) {
26
35 // ...
Observa que en las líneas 27-30 hacemos malloc() para un int y copiamos el valor de i en él. Cada nuevo
hilo obtiene su propia variable recién malloc() y le pasamos un puntero a la función run().
Una vez que run() hace su propia copia de la arg en la línea 7, free()s la malloc() int. Y ahora que
tiene su propia copia, puede hacer con ella lo que le plazca.
Y una ejecución muestra el resultado:
Launching threads...
THREAD 0: running!
THREAD 1: running!
THREAD 2: running!
THREAD 3: running!
Doing other things while the thread runs...
Waiting for thread to complete...
Thread 0 complete!
Thread 1 complete!
Thread 2 complete!
Thread 3 complete!
THREAD 4: running!
Thread 4 complete!
All threads complete!
completaron. De hecho, si ejecuto esto de nuevo, es probable que obtenga resultados diferentes. No podemos
garantizar un orden de ejecución de hilos.
Esto elimina la capacidad de la hebra padre de obtener el valor de retorno de la hebra hija, pero si no te
importa eso y sólo quieres que las hebras se limpien bien por sí solas, este es el camino a seguir.
Básicamente vamos a hacer esto:
donde la llamada a thrd_detach() es la hebra padre diciendo, “Hey, no voy a esperar a que esta hebra hija
termine con thrd_join(). Así que sigue adelante y límpialo por tu cuenta cuando se complete”.
1 #include <stdio.h>
2 #include <threads.h>
3
11 return 0;
12 }
13
14 #define THREAD_COUNT 10
15
16 int main(void)
17 {
18 thrd_t t;
19
Tenga en cuenta que en este código, ponemos el hilo principal a dormir durante 1 segundo con
thrd_sleep()—más sobre esto más adelante.
También en la función run(), tengo una línea comentada que imprime el ID del hilo como un
unsigned long. Esto es no-portable, porque la especificación no dice qué tipo es un thrd_t bajo el
capó—podría ser una struct por lo que sabemos. Pero esa línea funciona en mi sistema.
Chapter 39. Multihilo (Multithreading) 303
Algo interesante que vi cuando ejecuté el código anterior e imprimí los IDs de los hilos fue que ¡algunos
hilos tenían IDs duplicados! Esto parece que debería ser imposible, pero a C se le permite reutilizar los IDs
de los hilos después de que el hilo correspondiente haya salido. Así que lo que estaba viendo era que algunos
hilos completaban su ejecución antes de que otros hilos fueran lanzados.
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <threads.h>
4
9 free(arg);
10
15 // Acabamos de asignar x desde foo, así que más vale que sean iguales aquí.
16 // (En todas mis pruebas, lo eran, ¡pero ni siquiera esto está garantizado!)
17
26 if (x != foo) {
27 printf("Thread %d: Craziness! x != foo! %d != %d\n", n, x, foo);
28 }
29
6
Aunque no creo que tengan que serlo. Es sólo que los hilos no parecen ser reprogramados hasta que alguna llamada al sistema
como podría ocurrir con un printf()… que es por lo que tengo el printf() ahí
Chapter 39. Multihilo (Multithreading) 304
31
32 return 0;
33 }
34
35 #define THREAD_COUNT 5
36
37 int main(void)
38 {
39 thrd_t t[THREAD_COUNT];
40
En el hilo 1, entre los dos printf()s, el valor de foo de alguna manera cambió de 10 a 11, ¡aunque clara-
mente no hay incremento entre los printf()s!
¡Fue otro hilo el que entró ahí (probablemente el hilo 0, por lo que parece) e incrementó el valor de foo a
espaldas del hilo 1!
Resolvamos este problema de dos maneras diferentes. (Si quieres que todos los hilos compartan la variable
y no se pisen unos a otros, tendrás que seguir leyendo la sección mutex).
Básicamente, vamos a ponerla delante de nuestra variable static de ámbito de bloque, ¡y todo funcionará!
Le dice a C que cada hilo debe tener su propia versión de esta variable, para que ninguno de ellos pise a los
demás.
El <threads.h> define thread_local como un alias de _Thread_local para que tu código no tenga que
verse tan feo.
Tomemos el ejemplo anterior y convirtamos foo en una variable thread_local para no compartir esos
datos.
Chapter 39. Multihilo (Multithreading) 305
9 free(arg);
10
Y corriendo llegamos:
Generalmente, thread_local es probablemente tu mejor opción, pero si te gusta la idea del destructor,
entonces puedes hacer uso de eso.
El uso es un poco raro en el sentido de que necesitamos una variable de tipo tss_t para representar el valor de
cada hilo. Luego la inicializamos con tss_create(). Finalmente nos deshacemos de él con tss_delete().
Nótese que llamar a tss_delete() no ejecuta todos los destructores–es thrd_exit() (o volver de la fun-
ción run) la que lo hace. tss_delete() sólo libera la memoria asignada por tss_create().
En el medio, los hilos pueden llamar tss_set() y tss_get() para establecer y obtener el valor.
En el siguiente código, establecemos la variable TSS antes de crear los hilos, y luego limpiamos después de
los hilos.
En la función run(), los hilos malloc() un poco de espacio para una cadena y almacenan ese puntero en la
variable TSS.
Cuando el hilo sale, la función destructora (free() en este caso) es llamada para todos los hilos.
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <threads.h>
Chapter 39. Multihilo (Multithreading) 306
5 tss_t str;
6
7 void some_function(void)
8 {
9 // Recuperar el valor por hilo de esta cadena
10 char *tss_string = tss_get(str);
11
12 // E imprimirlo
13 printf("TSS string: %s\n", tss_string);
14 }
15
34 #define THREAD_COUNT 15
35
36 int main(void)
37 {
38 thrd_t t[THREAD_COUNT];
39
53 // Todos los hilos están hechos, así que hemos terminado con esto
54 tss_delete(str);
55 }
Una vez más, esta es una forma un poco dolorosa de hacer las cosas en comparación con thread_local, así
Chapter 39. Multihilo (Multithreading) 307
que a menos que realmente necesites esa funcionalidad destructor, yo usaría eso en su lugar.
39.7 Mutexes
Si sólo quieres permitir que un único hilo entre en una sección crítica de código a la vez, puedes proteger esa
sección con un mutex7 .
Por ejemplo, si tuviéramos una variable static y quisiéramos poder obtenerla y establecerla en dos opera-
ciones sin que otro hilo saltara en medio y la corrompiera, podríamos usar un mutex para eso.
Puedes adquirir un mutex o liberarlo. Si intentas adquirir el mutex y tienes éxito, puedes continuar la ejecu-
ción. Si lo intentas y fallas (porque alguien más lo tiene), te bloquearás8 hasta que el mutex sea liberado.
Si varios hilos están bloqueados esperando a que se libere un mutex, uno de ellos será elegido para ejecutarse
(al azar, desde nuestra perspectiva), y los demás seguirán durmiendo.
El plan de juego es que primero inicializaremos una variable mutex para que esté lista para usar con
mtx_init().
Entonces los hilos subsiguientes pueden llamar a mtx_lock() y mtx_unlock() para obtener y liberar el
mutex.
Cuando hayamos terminado completamente con el mutex, podemos destruirlo con la función mtx_destroy(),
el opuesto lógico de mtx_init().
Primero, veamos algo de código que no usa un mutex, e intenta imprimir un número de serie compartido
(static) y luego incrementarlo. Debido a que no estamos usando un mutex sobre la obtención del valor
(para imprimirlo) y el ajuste (para incrementarlo), los hilos podrían interponerse en el camino de los demás
en esa sección crítica.
1 #include <stdio.h>
2 #include <threads.h>
3
12 serial++;
13
14 return 0;
15 }
16
17 #define THREAD_COUNT 10
18
19 int main(void)
20 {
21 thrd_t t[THREAD_COUNT];
22
7
Abreviatura de “exclusión mutua”, alias un “bloqueo” en una sección de código que sólo un hilo puede ejecutar
8
Es decir, tu proceso entrará en reposo
Chapter 39. Multihilo (Multithreading) 308
25 }
26
Thread running! 0
Thread running! 0
Thread running! 0
Thread running! 3
Thread running! 4
Thread running! 5
Thread running! 6
Thread running! 7
Thread running! 8
Thread running! 9
Claramente múltiples hilos están entrando y ejecutando el printf() antes de que nadie pueda actualizar la
variable serial.
Lo que queremos hacer es envolver la obtención de la variable y su establecimiento en un único tramo de
código protegido por mutex.
Añadiremos una nueva variable para representar el mutex de tipo mtx_t en el ámbito del fichero, la ini-
cializaremos, y entonces los hilos podrán bloquearla y desbloquearla en la función run().
1 #include <stdio.h>
2 #include <threads.h>
3
19 serial++;
20
25
26 return 0;
27 }
28
29 #define THREAD_COUNT 10
30
31 int main(void)
32 {
33 thrd_t t[THREAD_COUNT];
34
Thread running! 0
Thread running! 1
Thread running! 2
Thread running! 3
Thread running! 4
Thread running! 5
Thread running! 6
Thread running! 7
Thread running! 8
Thread running! 9
Si necesitas múltiples mutexes, no hay problema: simplemente ten múltiples variables mutex.
Y recuerda siempre la Regla Número Uno de los Mutexes Múltiples: Desbloquea los mutex en el orden
opuesto al que los bloqueaste.
Chapter 39. Multihilo (Multithreading) 310
Tipo Descripción
mtx_plain Mutex normal y corriente
mtx_timed Mutex que admite tiempos de espera
mtx_plain|mtx_recursive Mutex recursivo
mtx_timed|mtx_recursive Mutex recursivo que admite tiempos de espera
“Recursivo” significa que el poseedor de un bloqueo puede llamar a mtx_lock() varias veces sobre el mismo
bloqueo. (Tienen que desbloquearlo un número igual de veces antes de que alguien más pueda tomar el
mutex). Esto puede facilitar la codificación de vez en cuando, especialmente si llamas a una función que
necesita bloquear el mutex cuando ya tienes el mutex.
Y el tiempo de espera da a un hilo la oportunidad de intentar obtener el bloqueo durante un tiempo, pero
luego abandonarlo si no puede conseguirlo en ese plazo.
Para un mutex con tiempo de espera, asegúrate de crearlo con mtx_timed:
mtx_init(&serial_mtx, mtx_timed);
Y luego, cuando lo esperas, tienes que especificar una hora en UTC en la que se desbloqueará9
La función timespec_get() de <time.h> puede ser de ayuda aquí. Te dará la hora actual en UTC en una
struct timespec que es justo lo que necesitamos. De hecho, parece existir sólo para este propósito.
Tiene dos campos: tv_sec tiene el tiempo actual en segundos desde la época, y tv_nsec tiene los nanose-
gundos (milmillonésimas de segundo) como parte “fraccionaria”.
Así que puedes cargarlo con el tiempo actual, y luego añadirlo para obtener un tiempo de espera específico.
Entonces llame a mtx_timedlock() en lugar de a mtx_lock(). Si devuelve el valor thrd_timedout, se
ha agotado el tiempo de espera.
if (result == thrd_timedout) {
printf("Mutex lock timed out!\n");
}
Aparte de eso, los bloqueos temporizados son iguales que los bloqueos normales.
Una variable de condición proporciona una manera para que los hilos vayan a dormir hasta que ocurra algún
evento en otro hilo.
En otras palabras, podemos tener un número de hilos que están listos para empezar, pero tienen que esperar
hasta que algún evento se cumpla antes de continuar. Básicamente se les está diciendo “¡esperad!” hasta que
se les notifique.
Y esto trabaja mano a mano con mutexes ya que lo que vamos a esperar generalmente depende del valor de
algunos datos, y esos datos generalmente necesitan ser protegidos por un mutex.
Es importante tener en cuenta que la variable de condición en sí no es el titular de ningún dato en particular
desde nuestra perspectiva. Es simplemente la variable mediante la cual C realiza un seguimiento del estado
de espera/no espera de un hilo o grupo de hilos en particular.
Escribamos un programa artificial que lea grupos de 5 números del hilo principal de uno en uno. Entonces,
cuando se hayan introducido 5 números, el subproceso hijo se despierta, suma esos 5 números e imprime el
resultado.
Los números se almacenarán en una matriz global compartida, al igual que el índice de la matriz del número
que se va a introducir.
Como se trata de valores compartidos, al menos tenemos que esconderlos detrás de un mutex tanto para el
hilo principal como para el secundario. (El principal escribirá datos en ellos y el hijo los leerá).
Pero eso no es suficiente. El subproceso hijo necesita bloquearse (“dormir”) hasta que se hayan leído 5
números en el array. Y entonces la hebra padre tiene que despertar a la hebra hija para que pueda hacer su
trabajo.
Y cuando se despierte, necesita mantener ese mutex. Y lo hará. Cuando un hilo espera en una variable de
condición, también adquiere un mutex cuando se despierta.
Todo esto tiene lugar alrededor de una variable adicional de tipo cnd_t que es la variable de condición.
Creamos esta variable con la función cnd_init() y la destruimos cuando acabemos con ella con la
cnd_destroy().
Pero, ¿cómo funciona todo esto? Veamos el esquema de lo que hará el hilo hijo:
1. Bloquea el mutex con mtx_lock().
2. Si no hemos introducido todos los números, esperar en la variable condición con cnd_wait().
3. Hacer el trabajo que haya que hacer
4. Desbloquear el mutex con mtx_unlock().
Mientras tanto el hilo principal estará haciendo lo siguiente
1. Bloquear el mutex con mtx_lock().
2. Almacenar el número leído recientemente en el array
3. Si el array está lleno, indica al hijo que se despierte con cnd_signal().
4. Desbloquea el mutex con mtx_unlock().
Si no lo has ojeado demasiado (no pasa nada, no me ofendo), puede que notes algo raro: ¿cómo puede el
hilo principal mantener el bloqueo mutex y enviar una señal al hijo, si el hijo tiene que mantener el bloqueo
mutex para esperar la señal? No pueden mantener ambos el bloqueo.
Y de hecho no lo hacen. Hay algo de magia entre bastidores con las variables de condición: cuando
cnd_wait(), libera el mutex que especifiques y el hilo se va a dormir. Y cuando alguien indica a ese hilo
que se despierte, vuelve a adquirir el bloqueo como si nada hubiera pasado.
Es un poco diferente en el lado cnd_signal() de las cosas. Esto no hace nada con el mutex. La hebra
señalizadora aún debe liberar manualmente el mutex antes de que las hebras en espera puedan despertarse.
Una cosa más sobre cnd_wait(). Probablemente llame a cnd_wait() si alguna condición10 aún no se
10
¡Y por eso se llaman variables de condición!
Chapter 39. Multihilo (Multithreading) 312
cumple (por ejemplo, en este caso, si aún no se han introducido todos los números). Este es el problema:
esta condición debería estar en un bucle while, no en una sentencia if. ¿Por qué?
Por un misterioso fenómeno llamado spurious wakeup. A veces, en algunas implementaciones, un hilo puede
ser despertado de una suspensión cnd_wait() aparentemente sin razón [X-Files music]. No digo que sean
aliens… pero son aliens. Vale, en realidad es más probable que otro hilo se haya despertado y haya llegado al
trabajo primero]. Y así tenemos que comprobar que la condición que necesitamos todavía se cumple cuando
nos despertamos. Y si no es así, ¡a dormir!
Así que ¡manos a la obra! Empezando por el hilo principal:
• El hilo principal creará el mutex y la variable condición, y lanzará el hilo hijo.
• Luego, en un bucle infinito, obtendrá números de la consola.
• También adquirirá el mutex para almacenar los números introducidos en un array global.
• Cuando el array tenga 5 números, la hebra principal indicará a la hebra hija que es hora de despertar y
hacer su trabajo.
• Entonces el hilo principal desbloqueará el mutex y volverá a leer el siguiente número de la consola.
Mientras tanto, el subproceso hijo ha estado haciendo sus propias travesuras:
• El hilo hijo toma el mutex
• Mientras no se cumpla la condición (es decir, mientras el array compartido no tenga todavía 5 números),
la hebra hija duerme esperando en la variable de condición. Cuando espera, implícitamente desbloquea
el mutex.
• Una vez que el hilo principal indica al hilo hijo que se despierte, éste se despierta para hacer el trabajo
y recupera el bloqueo mutex.
• El subproceso hijo suma los números y restablece la variable que es el índice en la matriz.
• Entonces libera el mutex y se ejecuta de nuevo en un bucle infinito.
Y aquí está el código. Estudialo un poco para que puedas ver donde se manejan todas las piezas anteriores:
1 #include <stdio.h>
2 #include <threads.h>
3
4 #define VALUE_COUNT_MAX 5
5
16 for (;;) {
17 mtx_lock(&value_mtx); // <-- GRAB THE MUTEX
18
22 }
23
24 printf("Thread: is awake!\n");
25
26 int t = 0;
27
28 // Add everything up
29 for (int i = 0; i < VALUE_COUNT_MAX; i++)
30 t += value[i];
31
40 return 0;
41 }
42
43 int main(void)
44 {
45 thrd_t t;
46
54 mtx_init(&value_mtx, mtx_plain);
55 cnd_init(&value_cnd);
56
57 for (;;) {
58 int n;
59
60 scanf("%d", &n);
61
64 value[value_count++] = n;
65
66 if (value_count == VALUE_COUNT_MAX) {
67 printf("Main: signaling thread\n");
68 cnd_signal(&value_cnd); // <-- SIGNAL CONDITION
69 }
70
77 mtx_destroy(&value_mtx);
78 cnd_destroy(&value_cnd);
79 }
Y aquí hay algunos ejemplos de salida (los números individuales en las líneas son mis entradas):
Thread: is waiting
1
1
1
1
1
Main: signaling thread
Thread: is awake!
Thread: total is 5
Thread: is waiting
2
8
5
9
0
Main: signaling thread
Thread: is awake!
Thread: total is 24
Thread: is waiting
Es un uso común de las variables de condición en situaciones productor-consumidor como ésta. Si no tu-
viéramos una forma de poner el hilo hijo a dormir mientras espera a que se cumpla alguna condición, se
vería forzado a sondear, lo cual es un gran desperdicio de CPU.
if (result == thrd_timedout) {
printf("Condition variable timed out!\n");
}
void run_once_function(void)
{
printf("I'll only run once!\n");
}
call_once(&of, run_once_function);
// ...
In this example, no matter how many threads get to the run() function, the run_once_function() will
only be called a single time.
11
Survival of the fittest! Right? I admit it’s actually nothing like that.
Chapter 40
Atomics
“¿Lo intentaron y fracasaron, todos ellos?”> “Oh, no.” Sacudió la cabeza. “Lo intentaron y murieron.”
—Paul Atreides y la Reverenda Madre Gaius Helen Mohiam, Dune
Este es uno de los aspectos más desafiantes del multithreading con C. Pero intentaremos tomárnoslo con
calma.
Básicamente, hablaré de los usos más sencillos de las variables atómicas, qué son, cómo funcionan, etc. Y
mencionaré algunos de los caminos más increíblemente complejos que están a tu disposición.
Pero no voy a ir por esos caminos. No sólo apenas estoy cualificado para escribir sobre ellos, sino que me
imagino que si sabes que los necesitas, ya sabes más que yo.
Pero hay algunas cosas raras incluso en lo básico. Así que abróchense los cinturones, porque Kansas se va.
Si esas pruebas pasan, entonces puedes incluir con seguridad <stdatomic.h>, la cabecera en la que se basa
el resto de este capítulo. Pero si no hay soporte atómico, puede que esa cabecera ni siquiera exista.
En algunos sistemas, puede que necesites añadir -latomic al final de tu línea de comandos de compilación
para usar cualquier función del fichero de cabecera.
316
Chapter 40. Atomics 317
Si tienes una variable atómica compartida y escribes en ella desde una hebra, esa escritura será todo o nada
en otra hebra.
Es decir, el otro proceso verá la escritura completa de, digamos, un valor de 32 bits. No la mitad. No hay
forma de que un subproceso interrumpa a otro que está en medio de una escritura atómica multibyte.
Es casi como si hubiera un pequeño bloqueo en torno a la obtención y el establecimiento de esa variable. (¡Y
podría haberlo! Ver Variables atómicas libres de bloqueo, más abajo).
Y en esa nota, usted puede conseguir lejos con nunca usando atomics si usted utiliza mutexes para trabar sus
secciones críticas. Es sólo que hay una clase de estructuras de datos libres de bloqueo que siempre permiten
a otros hilos progresar en lugar de ser bloqueados por un mutex… pero son difíciles de crear correctamente
desde cero, y son una de las cosas que están más allá del alcance de la guía, lamentablemente.
Eso es sólo una parte de la historia. Pero es la parte con la que empezaremos.
Antes de continuar, ¿cómo se declara que una variable es atómica?
Primero, incluye <stdatomic.h>.
Esto nos da tipos como atomic_int.
Y entonces podemos simplemente declarar variables para que sean de ese tipo.
Pero hagamos una demostración donde tenemos dos hilos. El primero se ejecuta durante un tiempo y luego
establece una variable a un valor específico, luego sale. El otro se ejecuta hasta que ve que el valor se
establece, y luego se sale.
1 #include <stdio.h>
2 #include <threads.h>
3 #include <stdatomic.h>
4
17 printf("Thread 1: Exiting\n");
18 return 0;
19 }
20
30 }
31
32 int main(void)
33 {
34 x = 0;
35
41 thrd_join(t1, NULL);
42 thrd_join(t2, NULL);
43
El segundo hilo gira en su lugar, mirando la bandera y esperando a que se establezca en el valor 3490. Y el
primero lo hace.
Y obtengo esta salida:
¡Mira, ma! ¡Estamos accediendo a una variable desde diferentes hilos y sin usar un mutex! Y eso funcionará
siempre gracias a la naturaleza atómica de las variables atómicas.
Te estarás preguntando qué pasa si en vez de eso es un int normal no atómico. Bueno, en mi sistema sigue
funcionando… a menos que haga una compilación optimizada, en cuyo caso se cuelga en el hilo 2 esperando
a que se establezca el 34902 .
Pero esto es sólo el principio de la historia. La siguiente parte va a requerir más poder mental y tiene que ver
con algo llamado sincronización.
40.3 Sincronización
La siguiente parte de nuestra historia trata sobre cuándo ciertas escrituras de memoria en un hilo se hacen
visibles para las de otro hilo.
Podrías pensar que es inmediatamente, ¿verdad? Pero no es así. Varias cosas pueden ir mal. Raramente mal.
El compilador puede haber reordenado los accesos a memoria de modo que cuando crees que estableces un
valor relativo a otro puede no ser cierto. E incluso si el compilador no lo hizo, tu CPU podría haberlo hecho
2
La razón de esto es que cuando está optimizado, mi compilador ha puesto el valor de x en un registro para hacer que el bucle while
sea rápido. Pero el registro no tiene forma de saber que la variable fue actualizada en otro hilo, así que nunca ve el 3490. Esto no está
realmente relacionado con la parte todo o nada de la atomicidad, sino que está más relacionado con los aspectos de sincronización de
la siguiente sección
Chapter 40. Atomics 319
sobre la marcha. O puede que haya algo más en esta arquitectura que haga que las escrituras en una CPU se
retrasen antes de ser visibles en otra.
La buena noticia es que podemos condensar todos estos problemas potenciales en uno: los accesos no sin-
cronizados a la memoria pueden aparecer fuera de orden dependiendo del hilo que esté haciendo la obser-
vación, como si las propias líneas de código hubieran sido reordenadas.
A modo de ejemplo, ¿qué ocurre primero en el siguiente código, la escritura en x o la escritura en y?
1 int x, y; // global
2
3 // ...
4
5 x = 2;
6 y = 3;
7
Respuesta: no lo sabemos. El compilador o la CPU podrían invertir silenciosamente las líneas 5 y 6 y no nos
daríamos cuenta. El código se ejecutaría con un único hilo como si se ejecutara en el orden del código.
En un escenario multihilo, podríamos tener algo como este pseudocódigo:
1 int x = 0, y = 0;
2
3 thread1() {
4 x = 2;
5 y = 3;
6 }
7
8 thread2() {
9 while (y != 3) {} // spin
10 printf("x is now %d\n", x); // 2? ...or 0?
11 }
x is now 2
Pero algo astuto podría reordenar las líneas 4 y 5 haciendo que veamos el valor de 0 para x cuando lo
imprimamos.
En otras palabras, todo está perdido a menos que podamos decir de alguna manera: “A partir de este punto,
espero que todas las escrituras anteriores en otro hilo sean visibles en este hilo”.
Dos hilos sincronizan cuando coinciden en el estado de la memoria compartida. Como hemos visto, no
siempre están de acuerdo con el código. Entonces, ¿cómo se ponen de acuerdo?
El uso de variables atómicas puede forzar el acuerdo3 . Si un hilo escribe en una variable atómica, está
diciendo “cualquiera que lea esta variable atómica en el futuro también verá todos los cambios que hice en
la memoria (atómica o no) hasta la variable atómica inclusive”.
3
Hasta que diga lo contrario, estoy hablando en general de operaciones secuencialmente consistentes. Más sobre lo que eso significa
pronto
Chapter 40. Atomics 320
1 int x = 0;
2 atomic int y = 0; // Make y atomic
3
4 thread1() {
5 x = 2;
6 y = 3; // Sincronizar al escribir
7 }
8
9 thread2() {
10 while (y != 3) {} // Sincronizar en lectura
11 printf("x is now %d\n", x); // 2, period.
12 }
Como los hilos se sincronizan a través de y, todas las escrituras en el hilo 1 que ocurrieron antes de la escritura
en y son visibles en el hilo 2 después de la lectura de y (en el bucle while).
Es importante tener en cuenta un par de cosas aquí:
1. Nada duerme. La sincronización no es una operación de bloqueo. Ambos hilos están funcionando a
toda máquina hasta que salen. Incluso el que está atascado en el bucle no está bloqueando la ejecución
de ningún otro.
2. La sincronización ocurre cuando un hilo lee una variable atómica que otro hilo escribió. Así que
cuando el hilo 2 lee y, todas las escrituras de memoria anteriores en el hilo 1 (es decir, la configuración
de x) serán visibles en el hilo 2.
3. Observa que x no es atómica. Eso está bien porque no estamos sincronizando sobre x, y la sin-
cronización sobre y cuando la escribimos en el hilo 1 significa que todas las escrituras previas -
incluyendo x - en el hilo 1 serán visibles para otros hilos… si esos otros hilos leen y para sincronizarse.
Forzar esta sincronización es ineficiente y puede ser mucho más lento que usar una variable normal. Esta es
la razón por la que no usamos atomics a menos que sea necesario para una aplicación en particular.
Esto es lo básico. Profundicemos un poco más.
Cuando un hilo adquiere una variable atómica, puede ver los valores establecidos en otro hilo que liberó esa
misma variable.
En otras palabras:
Cuando un hilo lee una variable atómica, puede ver los valores establecidos en otro hilo que escribió en esa
misma variable.
La sincronización se produce a través del par acquire/release.
Más detalles:
Con lectura/carga/adquisición de una variable atómica particular:
• Todas las escrituras (atómicas o no atómicas) en otro hilo que ocurrieron antes de que ese otro hilo
escribiera/almacenara/liberara esta variable atómica son ahora visibles en este hilo.
• El nuevo valor de la variable atómica establecida por el otro hilo también es visible en este hilo.
• Ninguna lectura o escritura de cualquier variable/memoria en el hilo actual puede ser reordenada para
ocurrir antes de esta adquisición.
• La adquisición actúa como una barrera unidireccional cuando se trata de reordenar código; las lecturas
y escrituras en el hilo actual pueden moverse de antes de la adquisición a después de ella. Pero, más
importante para la sincronización, nada puede moverse hacia arriba desde después de la adquisición a
antes de ella.
Con escritura/almacenamiento/liberación de una variable atómica particular:
• Todas las escrituras (atómicas o no atómicas) en el subproceso actual que se produjeron antes de esta
liberación se vuelven visibles para otros subprocesos que han leído/cargado/adquirido la misma vari-
able atómica.
• El valor escrito en esta variable atómica por este hilo también es visible para otros hilos.
• Ninguna lectura o escritura de cualquier variable/memoria en el hilo actual puede ser reordenada para
que ocurra después de esta liberación.
• La liberación actúa como una barrera unidireccional cuando se trata de reordenar código: las lecturas
y escrituras en el hilo actual pueden moverse de después de la liberación a antes de ella. Pero, lo que
es más importante para la sincronización, nada puede moverse hacia abajo desde antes de la liberación
a después de ella.
De nuevo, el resultado es la sincronización de la memoria de un subproceso a otro. El segundo hilo puede
estar seguro de que las variables y la memoria se escriben en el orden previsto por el programador.
int x, y, z = 0;
atomic_int a = 0;
thread1() {
x = 10;
y = 20;
a = 999; // Liberación
z = 30;
}
thread2()
{
while (a != 999) { } // Adquirir
Chapter 40. Atomics 322
En el ejemplo anterior, thread2 puede estar seguro de los valores de x y y después de adquirir a porque
fueron establecidos antes de que thread1 liberara el atómico a.
Pero thread2 no puede estar seguro del valor de z porque ocurrió después de la liberación. Quizás la
asignación a z se movió antes que la asignación a a.
Una nota importante: liberar una variable atómica no tiene efecto sobre las adquisiciones de diferentes vari-
ables atómicas. Cada variable está aislada de las demás.
atomic_int x = 0;
thread1() {
x = x + 3; // NOT atomic!
}
Dado que hay una lectura de x a la derecha de la asignación y una escritura efectiva a la izquierda, se trata
de dos operaciones. Otro hilo podría colarse en medio y hacerte infeliz.
Pero puedes usar la abreviatura += para obtener una operación atómica:
atomic_int x = 0;
thread1() {
x += 3; // ATOMIC!
}
En ese caso, x se incrementará atómicamente en 3–ningún otro hilo puede saltar en medio.
En particular, los siguientes operadores son operaciones atómicas de lectura-modificación-escritura con con-
sistencia secuencial, así que úsalos con alegre abandono. (En el ejemplo, a es atómico).
call_once()—Sincroniza con todas las llamadas posteriores a call_once() para una bandera en particular.
De esta forma, las llamadas posteriores pueden estar seguras de que si otro hilo establece la bandera, la verán.
thrd_create()—Sincroniza con el inicio del nuevo hilo, que puede estar seguro de que verá todas las
escrituras en memoria compartida del hilo padre desde antes de la llamada a thrd_create().
thrd_join()Cuando un hilo muere, se sincroniza con esta función. La hebra que ha llamado a
thrd_join() puede estar segura de que puede ver todas las escrituras compartidas de la hebra fallecida.
Utilízalos cuando quieras. Son consistentes con los alias atómicos que se encuentran en C++, si eso ayuda.
Pero, ¿y si quieres más?
Puedes hacerlo con un calificador de tipo o un especificador de tipo.
En primer lugar, el especificador. Es la palabra clave _Atomic con un tipo en paréntesis después5 —apto
para usarse con typedef:
atomic_double f;
Restricciones en el especificador: el tipo que está haciendo atómico no puede ser de tipo array o función, ni
puede ser atómico o calificado de otra manera.
Siguiente, ¡calificador! Es la palabra clave _Atomic sin un tipo entre paréntesis.
Así que hacen cosas similares6 :
Lo que ocurre es que puedes incluir otros calificadores de tipo con este último:
Restricciones en el calificador: el tipo que estás haciendo atómico no puede ser de tipo array o función.
Value Meaning
0 Never lock-free.
1 Sometimes lock-free.
2 Always lock-free.
Espera… ¿cómo puede algo estar a veces libre de bloqueos? Esto sólo significa que la respuesta no se
conoce en tiempo de compilación, pero podría conocerse en tiempo de ejecución. Tal vez la respuesta varía
dependiendo de si se está ejecutando este código en Intel o AMD Genuine, o algo así7
Pero siempre se puede probar en tiempo de ejecución con la atomic_is_lock_free(). Esta función de-
vuelve verdadero o falso si el tipo en particular es atómico en este momento.
¿Por qué nos importa?
Lock-free es más rápido, así que tal vez hay un problema de velocidad que usted codificaría de otra manera.
O quizás necesites usar una variable atómica en un manejador de señales.
atomic_flag f = ATOMIC_FLAG_INIT;
#include <stdio.h>
#include <stdbool.h>
#include <stdatomic.h>
atomic_flag f = ATOMIC_FLAG_INIT;
int main(void)
{
bool r = atomic_flag_test_and_set(&f);
printf("Value was: %d\n", r); // 0
r = atomic_flag_test_and_set(&f);
printf("Value was: %d\n", r); // 1
atomic_flag_clear(&f);
r = atomic_flag_test_and_set(&f);
printf("Value was: %d\n", r); // 0
}
1 #include <stdio.h>
2 #include <stdatomic.h>
3
4 int main(void)
5 {
6 struct point {
7 float x, y;
8 };
9
9
[Link]
Chapter 40. Atomics 328
10 _Atomic(struct point) p;
11
Aquí está el truco: no puedes acceder a los campos de una struct o union atómica… ¿entonces qué sen-
tido tiene? Bueno, puedes copiar atómicamente toda la struct en una variable no atómica y luego usarla.
También puedes copiar atómicamente a la inversa.
1 #include <stdio.h>
2 #include <stdatomic.h>
3
4 int main(void)
5 {
6 struct point {
7 float x, y;
8 };
9
10 _Atomic(struct point) p;
11 struct point t;
12
17 t = p; // Atomic copy
18
También puede declarar una estructura en la que los campos individuales sean atómicos. La imple-
mentación define si los tipos atómicos están permitidos en los campos de bits.
_Atomic int x;
_Atomic int *p; // p es un puntero a un int atómico
p = &x; // OK!
En segundo lugar, los punteros atómicos a valores no atómicos (es decir, el valor del puntero en sí es atómico,
pero la cosa a la que apunta no lo es):
int x;
int * _Atomic p; // p es un puntero atómico a un int
p = &x; // OK!
Chapter 40. Atomics 329
Por último, punteros atómicos a valores atómicos (es decir, el puntero y la cosa a la que apunta son ambos
atómicos):
_Atomic int x;
_Atomic int * _Atomic p; // p es un puntero atómico a un int atómico
p = &x; // OK!
memory_order Description
memory_order_seq_cst Sequential Consistency
memory_order_acq_rel Acquire/Release
memory_order_release Release
memory_order_acquire Acquire
memory_order_consume Consume
memory_order_relaxed Relaxed
Puede especificar otras con determinadas funciones de biblioteca. Por ejemplo, puedes añadir un valor a una
variable atómica así:
atomic_int x = 0;
atomic_int x = 0;
atomic_int x = 0;
atomic_fetch_add_explicit(&x, 5, memory_order_seq_cst);
Pero, ¿y si no quisiéramos coherencia secuencial? Y quisieras adquirir / liberar en su lugar por cualquier
razón? Sólo dilo:
atomic_int x = 0;
atomic_fetch_add_explicit(&x, 5, memory_order_acq_rel);
A continuación haremos un desglose de los diferentes órdenes de memoria. No te metas con nada que no
sea consistencia secuencial a menos que sepas lo que estás haciendo. Es realmente fácil cometer errores que
causarán fallos raros y difíciles de reproducir.
Chapter 40. Atomics 330
40.13.2 Acquire
Esto es lo que ocurre en una operación de carga/lectura de una variable atómica.
• Si otro hilo liberó esta variable atómica, todas las escrituras que ese hilo hizo son ahora visibles en este
hilo.
• Los accesos a memoria en este thread que ocurran después de esta carga no pueden ser reordenados
antes.
40.13.3 Release
Esto es lo que ocurre al almacenar/escribir una variable atómica.
• Si otro hilo adquiere más tarde esta variable atómica, toda la memoria en este hilo antes de su escritura
atómica se vuelven visibles para ese otra hebra.
• Los accesos a memoria en este hilo que ocurran antes de la liberación no pueden reordenarse después.
40.13.4 Consume
Esta es una extraña, similar a una versión menos estricta de adquirir. Afecta a los accesos a memoria que son
data dependent de la variable atómica.
Ser “dependiente de datos” significa vagamente que la variable atómica se utiliza en un cálculo.
Es decir, si un hilo consume una variable atómica entonces todas las operaciones en ese hilo que utilicen esa
variable atómica podrán ver las escrituras de memoria en el hilo que la libera.
Compárese con adquirir donde las escrituras en memoria en el subproceso que libera serán visibles para todas
las operaciones en el subproceso actual, no sólo las que dependen de los datos. las dependientes de datos.
También como en acquire, hay una restricción sobre qué operaciones pueden ser reordenadas antes de con-
sumir. Con acquire, no se podía reordenar nada antes. Con consume, no puedes reordenar nada que dependa
del valor atómico cargado antes de él.
40.13.5 Acquire/Release
Esto sólo se aplica a las operaciones de lectura-modificación-escritura. Es una adquisición y liberación en
uno.
• Una adquisición ocurre para la lectura.
• Una liberación ocurre para la escritura.
40.13.6 Relaxed
Sin reglas; ¡es la anarquía! Perros y gatos viviendo juntos… ¡histeria colectiva!
En realidad, hay una regla. Las lecturas y escrituras atómicas siguen siendo “todo o nada”. Pero las opera-
ciones pueden reordenarse caprichosamente y hay cero sincronización entre hilos.
Chapter 40. Atomics 331
Hay algunos casos de uso para este orden de memoria, que puedes encontrar con un poco de búsqueda, por
ejemplo, contadores simples.
Y puedes usar una valla para forzar la sincronización después de un montón de escrituras relajadas.
40.14 Fences
¿Sabes que las liberaciones y adquisiciones de variables atómicas se producen al leerlas y escribirlas?
Bueno, es posible hacer una liberación o adquisición sin una variable atómica, también.
Esto se llama un fence. Así que si quieres que todas las escrituras en un hilo sean visibles en otro lugar, puedes
poner una valla de liberación en un hilo y una valla de adquisición en otro, igual que con el funcionamiento
de las variables atómicas.
Como una operación consume no tiene sentido en un vallado10 , memory_order_consume se trata como una
adquisición.
Usted puede poner una cerca con cualquier orden especificado:
atomic_thread_fence(memory_order_release);
También hay una versión ligera de una valla para usar con manejadores de señales, llamada atomic_signal_fence().
Funciona de la misma manera que atomic_thread_fence(), excepto que:
• Sólo se ocupa de la visibilidad de valores dentro del mismo hilo; no hay sincronización con otros hilos.
• No se emiten instrucciones hardware fence.
Si quieres estar seguro de que los efectos secundarios de las operaciones no atómicas (y de las operaciones
atómicas relajadas) son visibles en el manejador de señales, puedes usar esta valla.
La idea es que el manejador de señales se está ejecutando en este hilo, no en otro, por lo que esta es una
forma más ligera de asegurarse de que los cambios fuera del manejador de señales son visibles dentro de él
(es decir, que no han sido reordenados).
40.15 References
Si quieres aprender más sobre este tema, aquí tienes algunas de las cosas que me ayudaron a superarlo:
• Herb Sutter’s atomic<> Weapons talk:
• Part 111
• part 212
• Jeff Preshing’s materials13 , in particular:
• An Introduction to Lock-Free Programming14
• Acquire and Release Semantics15
• The Happens-Before Relation16
10
Porque consume se refiere a las operaciones que dependen del valor de la variable atómica adquirida, y no hay variable atómica en
un vallado
11
[Link]
12
[Link]
13
[Link]
14
[Link]
15
[Link]
16
[Link]
Chapter 40. Atomics 332
17
[Link]
18
[Link]
19
[Link]
20
[Link]
21
[Link]
22
[Link]
23
[Link]
Chapter 41
Especificadores de Función,
Especificadores/Operadores de
Alineación
En mi experiencia, estos no se utilizan mucho, pero los cubriremos aquí en aras de la exhaustividad.
Esto pretende animar al compilador a hacer esta llamada a la función lo más rápido posible. Y, histórica-
mente, una forma de hacerlo era inlining, lo que significa que el cuerpo de la función se incrustaba en su
totalidad donde se realizaba la llamada. Esto evitaría toda la sobrecarga de establecer la llamada a la función
y desmontarla a expensas de un mayor tamaño del código, ya que la función se copiaba por todas partes en
lugar de reutilizarse.
Las cosas rápidas y sucias que hay que recordar son:
1. Probablemente no necesites usar inline por velocidad. Los compiladores modernos saben qué es lo
mejor.
2. Si lo usas por velocidad, úsalo con ámbito de archivo, es decir, static inline. Esto evita las desor-
denadas reglas de vinculación externa y funciones inline.
Deja de leer esta sección ahora.
Glotón para el castigo, ¿eh?
Vamos a tratar de dejar el static off.
333
Chapter 41. Especificadores de Función, Especificadores/Operadores de Alineación 334
1 #include <stdio.h>
2
8 int main(void)
9 {
10 printf("%d\n", add(1, 2));
11 }
gcc da un error de enlazador en add()1 . La especificación requiere que si tienes una función en línea no
externa también debes proporcionar una versión con enlace externo.
Así que tendrías que tener una versión externa en algún otro lugar para que esto funcione. Si el compilador
tiene tanto una función inline en el fichero actual como una versión externa de la misma función en otro
lugar, puede elegir a cuál llamar. Así que recomiendo encarecidamente que sean la misma.
Otra cosa que puedes hacer es declarar la función como extern inline. Esto intentará inline en el mismo
archivo (por velocidad), pero también creará una versión con enlace externo.
La palabra clave incorporada es _Noreturn, pero si no rompe su código existente, todo el mundo recomen-
daría incluir <stdnoreturn.h> y usar la más fácil de leer noreturn en su lugar.
Es un comportamiento indefinido si una función especificada como noreturn realmente retorna. Es com-
putacionalmente deshonesto.
Aquí hay un ejemplo de uso correcto de noreturn:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <stdnoreturn.h>
4
1
¡A menos que compiles con las optimizaciones activadas (probablemente)! Pero creo que cuando hace esto, no se está comportando
según la especificación
2
[Link]
3
[Link]
Chapter 41. Especificadores de Función, Especificadores/Operadores de Alineación 335
10 }
11
12 int main(void)
13 {
14 foo();
15 }
Si el compilador detecta que una función noreturn podría retornar, podría advertirte, de forma útil.
Sustituyendo la función foo() por esto:
me da una advertencia:
char alignas(int) c;
También puede pasar un valor constante o una expresión para la alineación. Esto tiene que ser algo sopor-
tado por el sistema, pero la especificación no llega a dictar qué valores se pueden poner ahí. Las potencias
pequeñas de 2 (1, 2, 4, 8 y 16) suelen ser apuestas seguras.
Si quiere alinear al máximo alineamiento usado por su sistema, incluya <stddef.h> y use el tipo
max_align_t, así:
char alignas(max_align_t) c;
4
[Link]
Chapter 41. Especificadores de Función, Especificadores/Operadores de Alineación 336
Usted podría potencialmente sobre-alinear especificando una alineación mayor que la de max_align_t, pero
si tales cosas están o no permitidas depende del sistema.
1 #include <stdalign.h>
2 #include <stdio.h> // for printf()
3 #include <stddef.h> // for max_align_t
4
5 struct t {
6 int a;
7 char b;
8 float c;
9 };
10
11 int main(void)
12 {
13 printf("char : %zu\n", alignof(char));
14 printf("short : %zu\n", alignof(short));
15 printf("int : %zu\n", alignof(int));
16 printf("long : %zu\n", alignof(long));
17 printf("long long : %zu\n", alignof(long long));
18 printf("double : %zu\n", alignof(double));
19 printf("long double: %zu\n", alignof(long double));
20 printf("struct t : %zu\n", alignof(struct t));
21 printf("max_align_t: %zu\n", alignof(max_align_t));
22 }
Salida en mi sistema:
char : 1
short : 2
int : 4
long : 8
long long : 8
double : 8
long double: 16
struct t : 16
max_align_t: 16
(Advertencia: ninguno de mis compiladores soporta esta función todavía, así que el código está en gran parte
sin probar).
alignof es genial si conoces el tipo de tus datos. ¿Pero qué pasa si desconoce el tipo y sólo tiene un puntero
a los datos?
¿Cómo podría ocurrir eso?
Bueno, con nuestro buen amigo el void*, por supuesto. No podemos pasarlo a alignof, pero ¿y si necesi-
tamos saber la alineación de lo que apunta?
Podríamos querer saber esto si estamos a punto de usar la memoria para algo que tiene necesidades signi-
ficativas de alineación. Por ejemplo, los tipos atómicos y flotantes a menudo se comportan mal si están mal
alineados.
Así que con esta función podemos comprobar la alineación de algunos datos siempre que tengamos un puntero
a esos datos, incluso si es un void*.
Hagamos una prueba rápida para ver si un puntero void está bien alineado para usarlo como tipo atómico, y,
si es así, hagamos que una variable lo use como ese tipo:
...
Sospecho que rara vez (hasta el punto de nunca, probablemente) necesitará utilizar esta función a menos que
esté haciendo algunas cosas de bajo nivel.
Y ahí lo tienen. ¡Alineación!
Index
338
INDEX 339