10 KiB
Punteros y cadenas
En C no hay tipo string
En Go, string es un tipo propio: por dentro guarda un puntero y una
longitud, y lo manejas como un valor.
En C, una cadena es solo una secuencia de bytes en memoria terminada en '\0'.
La longitud no se guarda en ningún sitio: para saberla hay que recorrer la
cadena hasta encontrar el '\0'.
"1.0.0" → '1' '.' '0' '.' '0' '\0' (6 bytes, no 5)
Los literales como "1.0.0" viven en memoria de solo lectura. Si
intentas escribir en ellos, el comportamiento es indefinido; en la práctica,
el programa se cierra.
'A' frente a "A"
| Literal | Qué es | Tipo | Ocupa |
|---|---|---|---|
'A' (comillas simples) |
Un carácter: el número 65 | int (cabe en un uint8_t) |
1 byte guardado en un uint8_t |
"A" (comillas dobles) |
Una cadena: los bytes 'A' y '\0' en memoria de solo lectura |
char *: un puntero a esos bytes |
2 bytes, más el puntero |
En Go es la misma diferencia que entre 'A' (un rune) y "A" (un
string).
Pasar "A" donde se espera un byte es pasar una dirección donde se
espera un número. gcc lo detecta: makes integer from pointer without a cast o differ in signedness.
const char *
Una función que "devuelve una cadena" en realidad devuelve la dirección de su primer byte:
const char *version = escpos_version();
char: en esa dirección hay caracteres.*:versiones un puntero; guarda una dirección, no un carácter.const: a través de este puntero no se pueden modificar los caracteres.
char es un entero de 1 byte. Si guardas un puntero en un char, pierdes
la dirección. Por eso el error dice makes pointer from integer.
Los dos significados de *
| Dónde aparece | Qué significa |
|---|---|
En una declaración: const char *p |
"p es un puntero" |
En una expresión: *p |
"el valor que hay en la dirección de p" (desreferenciar) |
Si p apunta a "1.0.0", *p es solo el '1'.
En Go: es lo mismo que var p *T frente a *p.
Regla práctica: ¿quiero la dirección o la cosa?
Un puntero es un papel con la dirección de una casa:
bufferes el papel, la dirección.*bufferes la casa, el struct que está en esa dirección.buffer->lenes "ve a la casa y cogelen". La flecha ya va a la casa, así que no lleva*. Equivale a(*buffer).len.&xes lo contrario de*: "dame la dirección dex".
Antes de escribir, pregúntate: ¿esta función o esta operación necesita la dirección o la cosa?
Aplicado a buffer.c:
| Código | ¿*? |
Por qué |
|---|---|---|
escpos_buffer *buffer = ... |
Sí | Declaración: dice que buffer es un puntero |
malloc(sizeof *buffer) |
Sí | Quiero el tamaño de la casa (el struct, 24 bytes). sizeof buffer sería el tamaño del papel (el puntero, 8 bytes) |
if (buffer == NULL) |
No | Compruebo si el papel tiene una dirección válida |
buffer->data = NULL |
No | -> ya va a la casa |
return buffer; |
No | Devuelvo el papel; la función devuelve escpos_buffer * |
free(buffer) |
No | free necesita la dirección del bloque |
En Go las reglas son las mismas, pero Go oculta casi todos los *:
p.X desreferencia solo, y los métodos con receptor puntero también. Por eso en
Go casi solo ves * en los tipos (*Buffer) y rara vez en expresiones
(*p = v). En C la diferencia entre papel y casa siempre se escribe.
Dónde va el *: siempre con el tipo
Los espacios alrededor del * dan igual. Estas tres líneas son idénticas:
escpos_buffer* escpos_buffer_new(void);
escpos_buffer * escpos_buffer_new(void);
escpos_buffer *escpos_buffer_new(void);
En todas, el tipo de retorno es escpos_buffer * ("puntero a
escpos_buffer"). El * no es de la función, es del tipo que devuelve.
En Go se lee igual: func New() *Buffer.
La costumbre en C es pegarlo al nombre por esta trampa:
int *a, b; /* a es int *, pero b es int, NO un puntero */
En una declaración múltiple, el * solo afecta al nombre que tiene al lado.
Pegarlo al nombre lo deja a la vista. Mejor aún: una variable por línea.
Nombres de parámetros en las declaraciones
En una declaración (el prototipo de la cabecera), el nombre del parámetro
es opcional: void escpos_buffer_free(escpos_buffer *); es válido. El
compilador solo necesita los tipos.
En la definición (en el .c) sí hace falta, porque el cuerpo lo usa.
Aun así, conviene ponerlo también en la cabecera: es documentación. Con
void copy(char *, const char *) no sabes cuál es el destino;
con void copy(char *dst, const char *src) sí.
En Go pasa lo mismo en los tipos función y en las interfaces:
func(int) error es válido sin nombres.
Paso por valor: un struct entero frente a un puntero
C copia siempre los argumentos, igual que Go. Si un parámetro es
escpos_buffer (sin *), la función recibiría una copia del struct
entero. Eso tiene dos problemas:
- Con un tipo opaco ni siquiera compila: el struct está incompleto fuera de
su
.cy no se sabe cuánto copiar. - Liberar una copia no sirve de nada: el bloque que reservó
malloces el original.
Por eso las funciones de un tipo opaco reciben siempre un puntero. Es lo
mismo que los métodos con receptor puntero de Go (func (b *Buffer) Free()).
Un puntero no sabe cuántos elementos hay
Un slice de Go lleva dentro tres cosas: puntero, len y cap. Un puntero de
C lleva solo la dirección del primer elemento. No hay forma de preguntarle
cuántos hay detrás.
Por eso las funciones de C que reciben un bloque de datos piden siempre dos parámetros: el puntero y la cantidad.
int escpos_buffer_append(escpos_buffer *buffer, const uint8_t *bytes, size_t count);
En Go sería un solo parámetro: func (b *Buffer) Append(bytes []byte).
Si el que llama pasa un count mayor que lo que hay de verdad, la función lee
fuera del bloque. El compilador no puede detectarlo. Es la fuente de los
desbordamientos de memoria más famosos.
Las cadenas son la excepción: su longitud se deduce porque acaban en '\0'.
Pero los datos binarios (un comando ESC/POS, una imagen) pueden contener
bytes 0x00 en medio, así que necesitan su cantidad aparte.
const en parámetros puntero: una promesa
const uint8_t *bytes
Se lee "puntero a uint8_t que no voy a modificar". Es un contrato con el
que llama: le garantizas que sus datos salen intactos. Si dentro de la función
intentas escribir bytes[0] = 0;, el compilador da error.
- Pasar un
uint8_t *normal donde se pideconst uint8_t *está permitido: solo añades una restricción. - Al revés da aviso: estarías quitando una promesa.
En Go no existe. Un slice que recibes lo puedes modificar, y el que llama no tiene ninguna garantía.
Arrays
uint8_t init[] = { 0x1B, 0x40 }; /* ESC @ */
- Un array de tamaño fijo. Si no pones el tamaño entre corchetes, el compilador lo cuenta a partir de los valores.
sizeof initda el tamaño en bytes del array completo (aquí 2). Solo funciona con el array en su ámbito, no con un puntero.- Al pasar un array a una función, se convierte en un puntero a su primer
elemento (decay). La función recibe la dirección, no una copia, y pierde
el tamaño. Por eso hay que pasar
sizeof initaparte.
En Go [2]byte{0x1B, 0x40} es un array con su longitud dentro del tipo.
Se pasa por copia, y para pasarlo sin copiar se usa un slice: init[:].
Aritmética de punteros: [] también lleva un * escondido
Con dibujos interactivos y preguntas: 02-pointers-visual.html.
Sumar un entero a un puntero da otra dirección: p + i avanza i
elementos, es decir, i * sizeof *p bytes. No va a la casa. Solo calcula
qué casa es.
p[i] es una abreviatura de *(p + i): calcula la dirección y va a ella.
Igual que ->, los corchetes llevan el * dentro.
La cadena completa con el buffer:
| Escribes | Equivale a | Tipo | Qué es |
|---|---|---|---|
buffer |
escpos_buffer * |
El papel (8 bytes) | |
*buffer |
struct escpos_buffer |
La casa (24 bytes) | |
buffer->data |
(*buffer).data |
uint8_t * |
Un papel guardado dentro de la casa, con la dirección del bloque de bytes |
buffer->data + i |
&buffer->data[i] |
uint8_t * |
La dirección de la casilla i |
buffer->data[i] |
*(buffer->data + i) |
uint8_t |
El byte de la casilla i |
Para saber si una expresión lleva *, cuenta: *, -> y [] desreferencian
una vez cada uno, y & quita una desreferencia.
Qué viaja cuando llamas a una función
En C, a una función le llega siempre una copia del valor que le pasas.
f(buffer): viaja una copia del papel, 8 bytes. La función llega a la misma casa, así que lo que cambie en ella lo ves tú.f(*buffer): viaja una copia de la casa, 24 bytes. Lo que cambie la función se queda en su copia.f(data), condataun array de 300 bytes: el array decae a la dirección de su primera casilla. Viaja un papel de 8 bytes, no los 300 bytes. Dentro de la función,sizeof *dataes 1, el tamaño de una casilla.
Restar direcciones
Las direcciones se escriben en hexadecimal, en base 16: cada posición vale 16
veces la de su derecha, y a–f valen 10–15.
| Hex | Decimal |
|---|---|
0x08 |
8 |
0x10 |
16 |
0x18 |
24 |
0x20 |
32 |
0x40 |
64 |
0x80 |
128 |
0x100 |
256 |
0x20 es 2 × 16 + 0 = 32. Para no hacer la cuenta a mano:
- En gdb:
p 0xd210 - 0xd1f0da32, yp/x 32da0x20. - En bash:
echo $((0xd210 - 0xd1f0)).
Si restas dos punteros del mismo tipo (p &b - &a), el resultado no son
bytes: son elementos, igual que en la suma. Dos escpos_buffer * separados
32 bytes dan 4, porque cada uno ocupa 8.
En Go pasa lo mismo, todo se copia: un *Buffer copia el puntero y un
slice copia la cabecera (puntero, len y cap). La diferencia es que en el slice
viaja la longitud, y en C tienes que pasarla tú.
printf y el formato
El primer argumento de printf es el formato. Nunca pases ahí una cadena
variable:
printf(s); /* mal */
printf("%s\n", s); /* bien */
Si s contuviera un %, printf iría a leer argumentos que no existen, lo
que es comportamiento indefinido y un agujero de seguridad clásico
(format string). %s significa "imprime los bytes desde esta dirección hasta
el '\0'".