1. Sumar un entero a un puntero
Si p apunta a un elemento de un arreglo, p + n apunta al elemento que está n posiciones más adelante, y p - n al que está n posiciones antes. La regla clave es que se avanzan elementos, no bytes: el compilador multiplica n por el tamaño del tipo apuntado.
#include <stdio.h>
struct cabecera {
char tipo[4];
unsigned int largo;
unsigned long marca;
};
int main(void)
{
char c[4];
int i[4];
double d[4];
struct cabecera s[4];
printf("char: %p -> %p (+%zu)\n", (void *) c, (void *) (c + 1), sizeof *c);
printf("int: %p -> %p (+%zu)\n", (void *) i, (void *) (i + 1), sizeof *i);
printf("double: %p -> %p (+%zu)\n", (void *) d, (void *) (d + 1), sizeof *d);
printf("cabecera: %p -> %p (+%zu)\n", (void *) s, (void *) (s + 1), sizeof *s);
printf("int + 3: %p, es decir &i[3] = %p\n", (void *) (i + 3), (void *) &i[3]);
return 0;
}
$ ./escala
char: 0x7ffed4346b24 -> 0x7ffed4346b25 (+1)
int: 0x7ffed4346ab0 -> 0x7ffed4346ab4 (+4)
double: 0x7ffed4346ac0 -> 0x7ffed4346ac8 (+8)
cabecera: 0x7ffed4346ae0 -> 0x7ffed4346af0 (+16)
int + 3: 0x7ffed4346abc, es decir &i[3] = 0x7ffed4346abc
El mismo + 1 avanzó 1, 4, 8 o 16 bytes según el tipo: siempre exactamente un elemento. Incluso con una estructura, que ocupa 16 bytes, s + 1 es el comienzo de la siguiente. Así, i + 3 es &i[3], sin que el programador tenga que pensar en bytes. Es la razón de ser del tipo del puntero que vimos en el tema 15: además de decir cómo leer el dato apuntado, dice cuánto mide un paso.
La aritmética solo tiene sentido dentro de un arreglo: p + n está definido mientras el resultado siga apuntando a un elemento del mismo arreglo, o a la posición justo después del último. Una variable suelta cuenta como un arreglo de un elemento. Las secciones 6 y 10 explican por qué esa restricción importa.
2. Recorrer un arreglo con un puntero
Con la aritmética, un arreglo se puede recorrer sin índice, con un puntero que avanza de elemento en elemento hasta un puntero de fin:
#include <stdio.h>
#include <stddef.h>
/* la misma suma, con índices y con un puntero que avanza */
long sumar_indices(const int *datos, size_t n)
{
long total = 0;
for (size_t i = 0; i < n; i++)
total += datos[i];
return total;
}
long sumar_puntero(const int *datos, size_t n)
{
long total = 0;
for (const int *p = datos; p < datos + n; p++)
total += *p;
return total;
}
int main(void)
{
int valores[] = {3, 1, 4, 1, 5, 9, 2, 6};
size_t n = sizeof valores / sizeof valores[0];
printf("con índices: %ld\n", sumar_indices(valores, n));
printf("con puntero: %ld\n", sumar_puntero(valores, n));
return 0;
}
$ ./recorrer
con índices: 31
con puntero: 31
$ gcc -std=c17 -O2 -S -fno-asynchronous-unwind-tables -o - recorrer.c
sumar_indices:
…
leaq (%rdi,%rsi,4), %rcx # fin = datos + n * 4
xorl %eax, %eax
.L3:
movslq (%rdi), %rdx # leer *p
addq $4, %rdi # p++ (4 bytes)
addq %rdx, %rax
cmpq %rcx, %rdi
jne .L3
ret
sumar_puntero:
…
leaq (%rdi,%rsi,4), %rcx
xorl %eax, %eax
…
.L9:
movslq (%rdi), %rdx
addq $4, %rdi
addq %rdx, %rax
cmpq %rcx, %rdi
jb .L9
ret
Las dos funciones dan el mismo resultado, y con -O2 el compilador generó para ambas el mismo bucle: un puntero que avanza de a 4 bytes hasta una dirección de fin calculada de antemano. Es decir, el compilador ya convierte los índices en aritmética de punteros, y escribir punteros «para que sea más rápido» no gana nada. Los comentarios del ensamblador son nuestros. Lo que sí cambia es la forma del código, y cada estilo tiene sus usos, como veremos en la sección 12.
El bucle con puntero tiene una forma que se repite en todo el código de sistemas: un puntero que recorre, p, y un puntero de fin, datos + n, que marca dónde parar. El fin apunta justo después del último elemento, y la condición es p < fin, o p != fin.
3. *p++ y sus variantes
Al recorrer con punteros es muy común combinar la desreferencia con el incremento en una sola expresión. Las cuatro combinaciones posibles hacen cosas distintas, y la diferencia está en la precedencia del tema 7: los operadores sufijo, como p++, se aplican antes que los prefijos, como * y ++p.
$ ./incremento
*p++ vale 10 p apunta a a[1] a = {10, 20, 30}
(*p)++ vale 10 p apunta a a[0] a = {11, 20, 30}
*++p vale 20 p apunta a a[1] a = {10, 20, 30}
++*p vale 11 p apunta a a[0] a = {11, 20, 30}
| Expresión | Se agrupa como | Hace | Vale |
|---|---|---|---|
*p++ | *(p++) | Lee el elemento actual y avanza el puntero. | El elemento actual. |
(*p)++ | — | Incrementa el elemento; el puntero no se mueve. | El valor anterior del elemento. |
*++p | *(++p) | Avanza el puntero y lee el nuevo elemento. | El elemento siguiente. |
++*p | ++(*p) | Incrementa el elemento y lo lee. | El valor nuevo del elemento. |
La que más se usa es *p++: «tomar el elemento actual y pasar al siguiente». Es el corazón de los bucles de copia de la biblioteca, como *d++ = *s++, que veremos en la sección 7. La trampa más común es escribir *p++ queriendo incrementar el valor apuntado: para eso hacen falta los paréntesis, (*p)++. En caso de duda, los paréntesis no cuestan nada.
4. Restar dos punteros
Si dos punteros apuntan a elementos del mismo arreglo, su resta da la distancia entre ellos en elementos, no en bytes. El resultado es de tipo ptrdiff_t, un entero con signo definido en stddef.h, de 8 bytes en Linux, que se imprime con %td:
#include <stdio.h>
#include <stddef.h>
/* busca valor en datos; devuelve un puntero al elemento o NULL */
static const double *buscar(const double *datos, size_t n, double valor)
{
const double *fin = datos + n; /* uno más allá del último */
for (const double *p = datos; p != fin; p++)
if (*p == valor)
return p;
return NULL;
}
int main(void)
{
double medidas[] = {1.5, 2.25, 8.0, 0.5, 3.75};
size_t n = sizeof medidas / sizeof medidas[0];
const double *encontrado = buscar(medidas, n, 0.5);
if (encontrado != NULL) {
ptrdiff_t indice = encontrado - medidas;
printf("0.5 está en el índice %td\n", indice);
printf("distancia en bytes: %td\n", (const char *) encontrado - (const char *) medidas);
}
printf("elementos entre el primero y el final: %td\n", (medidas + n) - medidas);
printf("¿el encontrado está antes del final? %d\n", encontrado < medidas + n);
return 0;
}
$ ./restar
0.5 está en el índice 3
distancia en bytes: 24
elementos entre el primero y el final: 5
¿el encontrado está antes del final? 1
buscar devuelve un puntero al elemento encontrado, el patrón del tema 16, y restándole el comienzo del arreglo se obtiene su índice: 3. La misma resta sobre punteros a char da la distancia en bytes, 24, que son 3 double de 8 bytes. La resta es la operación inversa de la suma: si q = p + n, entonces q - p vale n. Y restar el fin menos el comienzo da la cantidad de elementos, 5.
Solo se pueden restar punteros al mismo arreglo. Restar punteros a arreglos distintos es comportamiento indefinido, y sumar dos punteros no tiene ningún sentido: el compilador lo rechaza, porque una dirección más otra dirección no es nada.
5. Comparar punteros
Los punteros se comparan con los operadores relacionales del tema 7, con dos reglas distintas según el operador:
==y!=se pueden usar con cualquier par de punteros del mismo tipo, y conNULL: dicen si apuntan al mismo lugar.<,<=,>y>=solo tienen significado definido entre punteros al mismo arreglo, donde comparan posiciones:p < qsipestá antes. Entre punteros a objetos distintos, el resultado no está definido por el estándar.
Por eso los bucles con punteros usan como condición p < fin o p != fin, siempre con punteros al mismo arreglo. Las dos funcionan; p != fin es más general, porque solo necesita igualdad, y es la que se usa con estructuras que no son arreglos, como los iteradores de otros lenguajes. Y gcc advierte cuando compara directamente dos arreglos con <, como en la sección 10.
6. El puntero uno más allá del final
El puntero de fin, datos + n, apunta a una posición donde no hay ningún elemento: la que sigue al último. El estándar le da un estatus especial: se puede calcular y comparar, pero no desreferenciar. Es la única dirección fuera del arreglo que tiene un significado definido.
Este estatus especial existe justamente para que los bucles puedan terminar: el último p++ deja a p en el fin, se compara con fin, y el bucle sale sin leerlo. Las posiciones más allá del fin, o antes del comienzo, son otra cosa: el estándar dice que ni siquiera calcularlas está definido, aunque nunca se lean. La sección 10 muestra por qué eso no es un tecnicismo.
El mismo patrón, un comienzo y un fin que no se incluye, aparece en todo el código de sistemas: es el intervalo semiabierto [comienzo, fin). Tiene propiedades cómodas: la cantidad de elementos es fin - comienzo, un intervalo vacío tiene comienzo == fin, y dos intervalos consecutivos comparten el límite sin solaparse. AddressSanitizer lo usa en sus mensajes, como el [32, 44) del tema 1.
7. Recorrer cadenas
Una cadena de C es un arreglo de caracteres terminado en \0, de modo que se recorre sin necesidad de conocer su tamaño: se avanza hasta encontrar el terminador. Así están hechas strlen y las copias de cadenas:
#include <stdio.h>
#include <string.h>
/* versión propia de strlen: avanza hasta el '\0' y resta */
static size_t largo(const char *s)
{
const char *p = s;
while (*p != '\0')
p++;
return (size_t) (p - s);
}
/* copia src en dst (de tam bytes), siempre terminada en '\0'; devuelve dst */
static char *copiar(char *dst, const char *src, size_t tam)
{
char *d = dst;
if (tam == 0)
return dst;
while (tam > 1 && *src != '\0') {
*d++ = *src++;
tam--;
}
*d = '\0';
return dst;
}
int main(void)
{
const char *texto = "/proc/self/status";
char corto[8];
printf("largo = %zu, strlen = %zu\n", largo(texto), strlen(texto));
printf("copiado en 8 bytes: \"%s\"\n", copiar(corto, texto, sizeof corto));
return 0;
}
$ ./largo
largo = 17, strlen = 17
copiado en 8 bytes: "/proc/s"
largo avanza un puntero hasta el \0 y resta: la distancia en caracteres es el largo de la cadena, sin contar el terminador. copiar usa el modismo *d++ = *src++: copia el carácter actual y avanza los dos punteros, en una sola sentencia. A diferencia de strcpy, nunca escribe más de tam bytes y siempre termina el resultado con \0; por eso «/proc/self/status» se recortó a 7 caracteres más el terminador. Es, en esencia, lo que hace la función strlcpy, que glibc incorporó en su versión 2.38; las formas seguras de copiar cadenas son el tema del tema 22. Las cadenas y sus funciones son el tema 21.
8. Recorrer un buffer del sistema
Muchos datos que entrega el núcleo llegan como un bloque de bytes en un buffer, y el programa tiene que recorrerlo para encontrar lo que busca. El archivo /proc/self/status, generado por Linux, describe el propio proceso en líneas de la forma Clave: valor. Este programa lo lee con read y busca campos recorriendo el buffer con punteros:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
/* busca en buf (de n bytes) la línea que empieza con clave y la muestra */
static void mostrar_campo(const char *buf, size_t n, const char *clave)
{
const char *fin = buf + n;
size_t largo_clave = strlen(clave);
const char *linea = buf;
while (linea < fin) {
const char *salto = linea;
while (salto < fin && *salto != '\n') /* buscar el fin de la línea */
salto++;
if ((size_t) (salto - linea) > largo_clave &&
memcmp(linea, clave, largo_clave) == 0) {
printf("%.*s\n", (int) (salto - linea), linea);
return;
}
linea = salto + 1; /* saltar el '\n' */
}
printf("%s no encontrado\n", clave);
}
int main(void)
{
char buf[4096];
int fd = open("/proc/self/status", O_RDONLY);
if (fd < 0) {
perror("/proc/self/status");
return 1;
}
ssize_t n = read(fd, buf, sizeof buf);
close(fd);
if (n <= 0)
return 1;
printf("leídos %zd bytes de /proc/self/status\n", n);
mostrar_campo(buf, (size_t) n, "Name:");
mostrar_campo(buf, (size_t) n, "Pid:");
mostrar_campo(buf, (size_t) n, "VmRSS:");
mostrar_campo(buf, (size_t) n, "Threads:");
mostrar_campo(buf, (size_t) n, "NoExiste:");
return 0;
}
$ ./estado
leídos 1461 bytes de /proc/self/status
Name: estado
Pid: 518
VmRSS: 1152 kB
Threads: 1
NoExiste: no encontrado
El programa le preguntó al núcleo por sí mismo: se llama estado, su identificador de proceso es 518, ocupa 1152 KiB de memoria física y tiene un solo hilo. Lo interesante es cómo recorre el buffer. Lo que devolvió read no es una cadena: no termina en \0, y no se puede usar strlen ni strchr sobre él. Por eso todo el recorrido está acotado por fin = buf + n, calculado a partir de la cantidad de bytes leídos. Dos punteros delimitan cada línea, linea y salto, y su resta da el largo; printf la muestra con %.*s, la precisión variable del tema 10, que imprime exactamente esa cantidad de bytes sin necesitar un terminador. Y la comprobación salto < fin está siempre antes de leer *salto, gracias al cortocircuito de &&: así el bucle nunca lee fuera de lo que entregó el núcleo. Es la estructura de casi todo programa que interpreta datos binarios o de texto: protocolos de red, archivos de configuración, formatos de archivo.
9. Aritmética sobre bytes
Cuando hay que avanzar por bytes en lugar de por elementos, por ejemplo para saltar una cabecera de tamaño conocido o calcular una distancia en bytes, se convierte el puntero a char * o unsigned char *, cuyo tamaño de elemento es 1. Es lo que hizo restar.c para medir 24 bytes.
La aritmética sobre void *, en cambio, no está permitida por el estándar, porque void no tiene tamaño. gcc la acepta como extensión, tratando el tamaño como 1, pero advierte si se le pide:
$ gcc -std=c17 -Wall -Wextra -o va va.c && ./va
2
$ gcc -std=c17 -Wall -Wextra -Wpointer-arith -c va.c
va.c: In function ‘main’:
va.c:6:17: warning: pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]
6 | void *w = v + 4;
| ^
El núcleo Linux usa esta extensión por todas partes, pero en código portable se escribe (char *) v + 4. Y hay un cuidado más al avanzar por bytes y luego leer con otro tipo: la alineación. Muchos procesadores exigen, o prefieren, que un dato de 4 bytes esté en una dirección múltiplo de 4:
$ ./des
0x7ffd055cd681 0x04030201
$ gcc -std=c17 -Wall -Wextra -fsanitize=alignment -o des des.c && ./des
des.c:7:5: runtime error: load of misaligned address 0x7fff8716bea1 for type 'uint32_t', which requires 4 byte alignment
En x86-64 la lectura desalineada funciona, aunque puede ser más lenta, y leyó los bytes 1 a 4 como 0x04030201. En otros procesadores puede terminar el programa con SIGBUS, y para el estándar es comportamiento indefinido; -fsanitize=alignment lo detecta. La forma correcta de leer un entero de una posición cualquiera de un buffer es copiarlo con memcpy a una variable del tipo correcto, como en el tema 5. La alineación y su efecto en las estructuras son el tema 25.
10. Los límites que no se deben cruzar
Ahora podemos ver los errores de la aritmética de punteros. Este programa tiene tres, y solo uno de ellos produce un síntoma visible:
#include <stdio.h>
int main(void)
{
int datos[4] = {1, 2, 3, 4};
int otro[4] = {5, 6, 7, 8};
long suma = 0;
/* recorrer hacia atrás: el error clásico */
for (int *p = datos + 3; p >= datos; p--)
suma += *p;
printf("suma hacia atrás = %ld\n", suma);
/* leer antes del principio */
int *antes = datos - 1;
printf("datos[-1] = %d\n", *antes);
/* comparar punteros a arreglos distintos */
printf("datos < otro: %d\n", datos < otro);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o limites limites.c
limites.c: In function ‘main’:
limites.c:19:40: warning: comparison between two arrays [-Warray-compare]
…
$ ./limites
suma hacia atrás = 10
datos[-1] = 32764
datos < otro: 1
$ gcc -std=c17 -g -fsanitize=address -o limites limites.c && ./limites
==530==ERROR: AddressSanitizer: stack-buffer-underflow on address 0x7b52e00d001c …
READ of size 4 at 0x7b52e00d001c thread T0
#0 0x581e4bdf65b1 in main /home/ana/c/tema18/limites.c:16
- El bucle hacia atrás dio la suma correcta, 10, y nadie se quejó. Pero tiene comportamiento indefinido: en la última vuelta,
p--calculadatos - 1, una posición antes del comienzo, que el estándar no permite ni siquiera calcular. Aquí la dirección resultante simplemente no cumplióp >= datos, y el bucle terminó. Pero un compilador que optimiza puede razonar quep >= datoses siempre verdadero, porque un puntero válido nunca está antes del comienzo de su arreglo, y convertir el bucle en infinito. Es un error que funciona en las pruebas y no está garantizado. - La lectura de
datos[-1]sí leyó memoria ajena: 32764, lo que haya en la pila antes del arreglo. AddressSanitizer la detectó comostack-buffer-underflow, el desborde por debajo del comienzo. - La comparación de
datosconotrodio 1, pero no significa nada: dónde queda cada arreglo en la pila es decisión del compilador.gcclo advirtió.
Las reglas que se desprenden de la sección 6 son: un puntero recorre su arreglo, desde el primer elemento hasta el fin, y nunca sale de ese intervalo, ni para leer ni para calcular; las comparaciones de orden se hacen solo entre punteros al mismo arreglo; y toda lectura se hace después de comprobar que el puntero no llegó al fin.
11. Recorrer hacia atrás sin cruzarlos
¿Cómo se recorre un arreglo hacia atrás sin calcular datos - 1? La solución es usar el fin como punto de partida y decrementar antes de leer, de modo que el puntero nunca baje del comienzo:
/* p empieza en el fin y se decrementa antes de cada lectura */
for (int *p = datos + n; p > datos; ) {
p--;
suma += *p;
}
/* la versión compacta del mismo bucle */
int *p = datos + n;
while (p > datos)
suma += *--p;
El puntero toma los valores datos + n, que se puede calcular, y luego los de cada elemento hasta datos, y el bucle termina cuando p es igual a datos, sin haber bajado nunca de él. Es la misma idea que el bucle con size_t del tema 7, que contaba de n a 1 y usaba i - 1: en los dos casos, la variable de control se mueve dentro del intervalo permitido, y el ajuste se hace al acceder.
12. Índices o punteros
Como el compilador genera el mismo código, elegir entre índices y punteros es una cuestión de claridad. Algunas pautas:
| Situación | Conviene | Por qué |
|---|---|---|
| Recorrer un arreglo de tamaño conocido | Índices | Más legibles, y el índice es útil en sí, por ejemplo para mostrar la posición. |
Recorrer hasta un terminador, como el \0 | Punteros | No hay tamaño conocido; el puntero avanza hasta encontrarlo. |
| Interpretar un buffer con partes de largo variable | Punteros | Varios punteros delimitan las partes, y su resta da el largo, como en estado.c. |
| Devolver una posición dentro de los datos | Puntero | Como buscar, strchr o ultimo_componente: el llamador lo usa directamente. |
| Varios recorridos simultáneos sobre el mismo arreglo | Índices | Un número es más fácil de razonar que varias direcciones. |
Lo importante es la consistencia y los límites. Con índices, el límite es i < n; con punteros, p < fin o p != fin. En ambos casos, el tamaño tiene que acompañar a los datos desde que se crean, como insiste el tema 17.
13. La aritmética en gdb
gdb entiende la aritmética de punteros de C, con sus escalas:
$ gdb -q -batch -ex 'break 22' -ex run -ex 'p encontrado' -ex 'p *encontrado' -ex 'p encontrado - medidas' -ex 'p medidas + 2' -ex 'p *(medidas + 2)' ./restar
Breakpoint 1, main () at restar.c:22
22 ptrdiff_t indice = encontrado - medidas;
$1 = (const double *) 0x7fffffffd7b8
$2 = 0.5
$3 = 3
$4 = (double *) 0x7fffffffd7b0
$5 = 8
p encontrado - medidas da 3, en elementos, como en C, y p medidas + 2 da una dirección 16 bytes más allá del comienzo, porque el tipo es double. Combinado con x y con @ de los temas 15 y 17, permite seguir paso a paso un recorrido con punteros y ver en qué elemento está cada uno.
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Pensar que p + 1 avanza un byte | Avanza un elemento: 4, 8 o más bytes. | Para bytes, convertir a char *. |
*p++ queriendo incrementar el valor | Avanza el puntero, no el dato. | (*p)++. |
Leer *p antes de comprobar p < fin | Lectura fuera del buffer. | Comprobar primero: p < fin && *p != …. |
| Desreferenciar el puntero de fin | Lee el elemento que no existe. | El fin solo se compara. |
Recorrer hacia atrás con p >= datos; p-- | Calcula datos - 1: comportamiento indefinido. | Empezar en el fin y decrementar antes de leer. |
Restar o comparar con < punteros a arreglos distintos | Resultado sin significado. | Solo dentro del mismo arreglo; == sí vale siempre. |
Usar strlen sobre un buffer leído con read | No hay \0: se sale del buffer. | Recorrer acotado por la cantidad leída. |
Aritmética sobre void * | Extensión de gcc, no estándar. | (char *) v + n. |
| Leer un entero desde una posición cualquiera de un buffer | Acceso desalineado: lento o SIGBUS. | memcpy a una variable. |
Guardar una resta de punteros en un int | Puede no caber en buffers grandes. | ptrdiff_t, con %td. |
15. Resumen
p + navanza n elementos: el compilador multiplica por el tamaño del tipo apuntado.- Un arreglo se recorre con un puntero que va del comienzo al fin,
datos + n; con-O2, el compilador genera el mismo código que con índices. *p++lee el elemento actual y avanza;(*p)++incrementa el elemento;*++pavanza y lee;++*pincrementa y lee.- Restar dos punteros al mismo arreglo da la distancia en elementos, de tipo
ptrdiff_t, que se imprime con%td. ==y!=comparan cualquier par de punteros;<y>, solo punteros al mismo arreglo.- El puntero uno más allá del final se puede calcular y comparar, pero no leer; los intervalos
[comienzo, fin)son la forma estándar de delimitar datos. - Las cadenas se recorren hasta el
\0;*d++ = *s++copia y avanza. - Un buffer leído del sistema no tiene
\0: se recorre acotado por la cantidad de bytes leídos, comprobando el fin antes de cada lectura. - Para avanzar por bytes se usa
char *; la aritmética sobrevoid *es una extensión. Leer un dato desde una dirección desalineada es indefinido: se copia conmemcpy. - Calcular una posición antes del comienzo o más allá del fin es comportamiento indefinido aunque no se lea; para recorrer hacia atrás se parte del fin y se decrementa antes de leer.
- Índices y punteros son igual de eficientes; se elige lo más claro para cada caso.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. Si d es un double * que vale 0x1000, ¿cuánto vale d + 3?
0x1018. Sumar 3 a un puntero avanza 3 elementos, y cada double ocupa 8 bytes: la dirección aumenta 3 × 8 = 24 bytes, que en hexadecimal es 0x18.
2. ¿Por qué sumar_indices y sumar_puntero generan el mismo código con -O2?
Porque datos[i] es por definición *(datos + i), y el compilador transforma el bucle con índice en uno con un puntero que avanza y un puntero de fin calculado de antemano, que es exactamente lo que escribe la versión con puntero. Las dos expresan la misma operación.
3. Con p apuntando a a[0] y a = {10, 20, 30}, ¿qué hacen *p++ y (*p)++?
*p++ se agrupa como *(p++): vale 10, el elemento actual, y deja a p apuntando a a[1], sin cambiar el arreglo. (*p)++ incrementa el elemento apuntado: vale 10, el valor anterior, deja a[0] en 11, y p no se mueve.
4. En restar.c, ¿por qué encontrado - medidas da 3 pero la resta con char * da 24?
Porque restar dos punteros da la distancia en elementos del tipo apuntado. Con double *, la distancia es de 3 elementos; convertidos a char *, cuyo elemento es de un byte, la misma distancia se mide en bytes: 3 × 8 = 24.
5. ¿Qué se puede hacer con el puntero datos + n, y qué no?
Se puede calcular, comparar con otros punteros al mismo arreglo y restar, por ejemplo para usarlo como límite de un bucle o calcular la cantidad de elementos. No se puede desreferenciar, porque en esa posición no hay ningún elemento del arreglo.
6. ¿Por qué estado.c no usa strlen ni strchr sobre el buffer?
Porque read entrega bytes, no una cadena: el buffer no termina en \0, y esas funciones seguirían leyendo más allá de los datos válidos. El programa acota todo el recorrido con fin = buf + n, la cantidad de bytes que devolvió read.
7. En while (salto < fin && *salto != '\n'), ¿por qué importa el orden de las dos condiciones?
Porque && evalúa en cortocircuito: si salto ya llegó a fin, la primera condición es falsa y *salto no se lee. En el orden inverso, se leería el byte en la posición de fin, fuera de los datos, antes de comprobar el límite.
8. ¿Por qué no se puede hacer aritmética con un void * en C estándar?
Porque la aritmética de punteros avanza de a elementos del tipo apuntado, y void no tiene tamaño: no hay forma de saber cuánto mide un paso. gcc lo permite como extensión, con tamaño 1, y lo advierte con -Wpointer-arith. Lo portable es convertir a char *.
9. El bucle for (p = datos + 3; p >= datos; p--) dio el resultado correcto. ¿Por qué es un error?
Porque en la última vuelta p-- calcula datos - 1, una posición antes del comienzo del arreglo, y el estándar declara indefinido incluso calcularla. Funcionó por casualidad; un compilador puede suponer que p >= datos es siempre cierto para un puntero válido y producir un bucle infinito. Lo correcto es partir del fin y decrementar antes de leer.
10. ¿Qué detectó AddressSanitizer en limites.c, y qué no?
Detectó la lectura de datos[-1], un stack-buffer-underflow: un acceso real a memoria fuera del arreglo. No detectó el bucle hacia atrás, porque allí nunca se lee datos - 1, solo se calcula, ni la comparación entre arreglos distintos, que no accede a memoria.
11. ¿Por qué leer un uint32_t desde bytes + 1 es un problema, si en x86-64 funcionó?
Porque la dirección no es múltiplo de 4, y un uint32_t requiere alineación de 4 bytes. x86-64 tolera el acceso desalineado, pero otros procesadores lo rechazan con SIGBUS, y para el estándar es comportamiento indefinido. Lo correcto es copiar los 4 bytes con memcpy a una variable uint32_t.
Para practicar
- Escribe
const int *maximo(const int *comienzo, const int *fin), que devuelva un puntero al mayor elemento del intervalo[comienzo, fin), oNULLsi está vacío. Úsala para obtener el índice del máximo con una resta. - Reescribe
invertirdel tema 17 con dos punteros, uno que avanza desde el comienzo y otro que retrocede desde el fin, hasta que se crucen, sin calcular nunca una posición fuera del arreglo. - Escribe tu propia versión de
strchr,const char *buscar_caracter(const char *s, int c), y compárala con la de la biblioteca buscando'/','x'y'\0'en"/usr/bin". - Modifica
estado.cpara que muestre solo el valor de cada campo, sin la clave ni los espacios o tabuladores que la siguen, avanzando un puntero sobre ellos. - Escribe una función que cuente las líneas de un buffer leído con
readde un archivo cualquiera, acotada por la cantidad de bytes leídos, y compara el resultado conwc -l. - Compila
limites.ccon-O2y luego con-fsanitize=address,undefined, y anota qué detecta cada combinación. Corrige el programa con la técnica de la sección 11. - Lee los 4 primeros bytes de un ejecutable (por ejemplo
/bin/ls) en un buffer y muestra con un puntero cada byte en hexadecimal. ¿Qué texto forman los bytes 1 a 3? Es la «firma» del formato ELF del tema 1.