5.7 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()).
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'".