Pralab 04: Memòria dinàmica (II): Punters a punters
Introducció
Al Pralab 03 vas veure que un bloc dinàmic viu al heap fins que algú en fa free, i que té un propietari. També vas veure que crea_vector havia de retornar el punter perquè arribés al cridant.
Avui ens fem una pregunta més general:
Pot una funció modificar un punter que pertany a qui l’ha cridada?
La resposta necessita un tipus que ja vas trobar sense adonar-te’n: &v quan v era un punter tenia tipus int **.
En acabar el pralab hauràs de ser capaç de:
- decidir si una funció necessita
T *oT **segons què ha de modificar; - explicar què és un punter a punter (
int **) i quantes desreferències calen per arribar a un valor; - explicar per què C passa els arguments per valor, també els punters;
- escriure una funció que modifiqui un punter del cridant i una que el destrueixi;
- crear, omplir i destruir un array dinàmic amb funcions, amb un contracte clar de propietat.
- Pralab 03 (
malloc,free, propietari, punter penjat,&vd’un punter). - Teoria: Crides de funció (cada crida té el seu stack frame).
gcc. Sistema de referència: Debian bookworm.- Arquitectura: x86-64 o arm64. Tot el pralab funciona igual a les dues.
- Compila sempre amb
gcc -Wall -Wextra -g -O0.
Les mides que veuràs (sizeof(int) és 4 i un punter en ocupa 8) són les del nostre sistema de referència de 64 bits: el llenguatge C no les fixa.
Part A — Punter a punter
Crea 01_ptrptr.c:
#include <stdio.h>
int main(void)
{
int x = 42;
int *p = &x;
int **pp = &p;
printf("%d\n", x);
printf("%d\n", *p);
printf("%d\n", **pp);
return 0;
}Sense executar-lo: què imprimeix cada línia? Quantes desreferències (*) necessites per arribar al 42 des de x, des de p i des de pp?
Pots veure el raonament
Les tres línies imprimeixen 42. Des de x no en calen cap; des de p en cal una (*p); des de pp en calen dues (**pp): la primera (*pp) ens dona p, la segona ens dona x.
Afegeix ara:
printf("&x = %p\n", (void *)&x);
printf(" p = %p\n", (void *)p);
printf("&p = %p\n", (void *)&p);
printf("pp = %p\n", (void *)pp);Quines d’aquestes quatre adreces creus que seran iguals? Dibuixa x, p i pp amb fletxes.
gcc -Wall -Wextra -g -O0 -o 01_ptrptr 01_ptrptr.c
./01_ptrptrPots veure el raonament
p conté el mateix valor d’adreça que l’expressió &x, i pp conté el mateix valor que &p. Fixa’t que p és una variable (que guarda una adreça) i &x és una expressió (que produeix una adreça): imprimeixen el mateix perquè p es va inicialitzar amb &x. Però pp no conté el mateix valor que &x: hi ha tres variables i, per tant, tres adreces diferents.
pp p x
+------+ +------+ +------+
| &p | --> | &x | --> | 42 |
+------+ +------+ +------+
Cada * és un pas per una fletxa.
Ara mesurem, com al Pralab 02:
printf("%zu %zu %zu\n", sizeof(x), sizeof(p), sizeof(pp));
printf("%td\n", (char *)(p + 1) - (char *)p);
printf("%td\n", (char *)(pp + 1) - (char *)pp);Prediu els tres resultats. Recorda: l’aritmètica de punters avança tants bytes com ocupa el tipus al qual apunta. A què apunta pp?
Pots veure el raonament
Imprimeix 4 8 8, després 4 i després 8. Al nostre sistema de referència de 64 bits, els punters ocupen 8 bytes independentment del tipus apuntat. p + 1 avança sizeof(int) (4 bytes aquí); pp + 1 avança sizeof(int *) (8 bytes aquí), perquè pp apunta a un punter. És la mateixa regla del Pralab 02, un nivell més amunt.
Els tipus, de fora cap a dins:
ppté tipusint ***ppté tipusint *(ésp)**ppté tipusint(ésx)
Tots els programes que has escrit ja usen un char **. Crea 02_argv.c:
#include <stdio.h>
int main(int argc, char **argv)
{
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
printf("%td\n", (char *)(argv + 1) - (char *)argv);
printf("%d\n", argv[argc] == NULL);
return 0;
}gcc -Wall -Wextra -g -O0 -o 02_argv 02_argv.c
./02_argv hola adeu- Quin és el tipus de
argv[1]? I d’argv? - Per què
argv + 1avança 8 bytes? - Què vol dir que
argv[argc] == NULL? Recorda la teoria: què passava en executar./progsense arguments i imprimirargv[1]?
Pots veure el raonament
argv[1] és un char * (un punter a la primera lletra de la cadena "hola") i argv és un char **: un punter al primer element d’un array de punters. argv + 1 avança sizeof(char *) (8 bytes aquí). L’array d’arguments acaba amb un NULL (argv[argc]), tal com les cadenes acaben amb '\0'. Per a ./02_argv hola adeu:
argv --> +--------+
| char * | --> "./02_argv\0"
+--------+
| char * | --> "hola\0"
+--------+
| char * | --> "adeu\0"
+--------+
| NULL |
+--------+
És un array de punters acabat amb una sentinella. A la teoria, ./prog sense arguments feia que argv[1] fos aquest NULL, i imprimir-lo amb %s era comportament indefinit.
Part B — Per què necessitem int **
B.1 — Modificar el que apunta un punter
Crea 03_set.c:
#include <stdio.h>
void posa42(int *p)
{
*p = 42;
}
int main(void)
{
int x = 0;
posa42(&x);
printf("x = %d\n", x);
return 0;
}Què imprimeix? Per què la funció ha pogut modificar x, que és una variable de main?
Pots veure el raonament
Imprimeix x = 42. La funció rep l’adreça de x i, com al Pralab 01 amb *p = 99, escriu al lloc on apunta el punter.
B.2 — Modificar el propi punter
Ara volem una funció que faci que un punter del cridant apunti a un bloc nou. Crea 04_modifica.c:
#include <stdio.h>
#include <stdlib.h>
void modifica(int *p)
{
printf("dins : &p = %p, p = %p\n", (void *)&p, (void *)p);
p = malloc(sizeof(int));
printf("dins : p despres de malloc = %p\n", (void *)p);
}
int main(void)
{
int *p = NULL;
printf("main : &p = %p, p = %p\n", (void *)&p, (void *)p);
modifica(p);
printf("main : p = %p\n", (void *)p);
return 0;
}Abans d’executar-ho:
- Després de
modifica(p), continuaràpdemainsentNULL? - Creus que els dos
&pseran iguals? - Aquesta funció té un segon problema. Quin?
gcc -Wall -Wextra -g -O0 -o 04_modifica 04_modifica.c
./04_modificaPots veure el raonament
p de main continua sent NULL ((nil) a glibc). Els dos &p són diferents: main i modifica tenen cadascun el seu stack frame i, per tant, la seva pròpia variable p. Són dues variables diferents: quan es fa la crida, el valor que conté la p de main (NULL) es copia a la nova variable p de modifica. Després, modifica canvia només la seva.
en cridar: després del malloc: en retornar:
main modifica main modifica main
p: NULL p: NULL p: NULL p: --> [bloc] p: NULL [bloc]
(dues variables, (només canvia la de (la p de modifica desapareix;
mateix valor) modifica) ningú coneix el bloc)
El segon problema és una fuga de memòria: el bloc reservat amb malloc només és apuntat per la variable local p de modifica. En retornar la funció, aquesta variable desapareix i ningú no coneix l’adreça del bloc: no es pot alliberar.
Si esborres els dos printf de dins de modifica, gcc -Wextra t’avisa: parameter 'p' set but not used.
Si passo un punter a una funció, la funció pot canviar el meu punter. Fals. C passa sempre els arguments per valor, també els punters: la funció té la seva pròpia variable p, on es copia el valor de la p del cridant. Pot canviar el que hi ha darrere de l’adreça (com posa42), però no la variable del cridant.
B.3 — La solució: passar l’adreça del punter
Per modificar una variable de tipus T des d’una funció, cal passar-ne l’adreça, de tipus T *. La pregunta per decidir el tipus del paràmetre és: què vull modificar?
| Què vull modificar? | Passo | Paràmetre |
|---|---|---|
el valor d’un int (x) |
&x |
int * |
el valor d’un punter (p) |
&p |
int ** |
A posa42, T era int, i el paràmetre era int *. Ara volem modificar un punter: T és int *, així que el paràmetre ha de ser int **.
Crea 05_out.c:
#include <stdio.h>
#include <stdlib.h>
int crea_int(int **pp)
{
if (pp == NULL) {
return -1;
}
*pp = malloc(sizeof(int));
if (*pp == NULL) {
return -1;
}
**pp = 42;
return 0;
}
void destrueix_int(int **pp)
{
if (pp == NULL) {
return;
}
free(*pp);
*pp = NULL;
}
int main(void)
{
int *p = NULL;
if (crea_int(&p) != 0) {
return 1;
}
printf("p = %p, *p = %d\n", (void *)p, *p);
destrueix_int(&p);
printf("despres: p = %p\n", (void *)p);
destrueix_int(&p);
return 0;
}- Dins de
crea_int, a què apuntapp? Què és*pp? I**pp? - Dibuixa
mainicrea_intambpipp. - Per què
destrueix_intrep unint **i no unint *? Què li passaria apdemainsidestrueix_intrebés nomésint *pi fesfree(p)? - Per què no passa res en cridar
destrueix_int(&p)per segona vegada?
Pots veure el raonament
ppapunta a la variablepdemain.*ppés aquesta mateixap, i**ppés el bloc al qual apunta.
main crea_int
+---------+ +---------+
| p | <------- | pp |
+---------+ +---------+
|
v
[ bloc al heap ]
- Amb
int *p,free(p)alliberaria el bloc peròpdemaincontinuaria contenint l’adreça: un punter penjat (Pralab 03). Ambint **, la funció allibera el bloc i posa elpdel cridant aNULL. - La segona crida fa
free(NULL), que és vàlid i no fa res. Per això després dedestrueix_intés segur tornar-la a cridar. La seqüència és:
1a crida: free(*pp); *pp = NULL; --> el p de main passa a NULL
2a crida: *pp ja és NULL --> free(NULL) (no fa res)
Per què crea_int retorna un int i no el punter? El valor de retorn es reserva per a un codi d’error (0 correcte, -1 error). El punter creat surt per un paràmetre. Aquest patró és el de les APIs reals de C: posix_memalign(void **memptr, size_t alignment, size_t size) i getline(char **lineptr, size_t *n, FILE *stream) retornen l’estat i deixen el resultat a través d’un punter a punter.
Part C — Array dinàmic + int **
Ara ho ajuntem tot. Volem tres funcions per gestionar un vector d’enters al heap. Crea 06_vector.c amb aquest main i escriu tu les tres funcions:
#include <stdio.h>
#include <stdlib.h>
/* Reserva n enters al heap i deixa l'adreça a *v.
Retorna 0 si va bé, -1 si hi ha error (*v queda sense canvis).
El cridant és el propietari del bloc. */
int crea_vector(int **v, int n);
/* Escriu 1, 2, ..., n a v. */
void omple_vector(int *v, int n);
/* Allibera el bloc i deixa *v a NULL. */
void destrueix_vector(int **v);
int main(void)
{
int *v = NULL;
int n = 5;
if (crea_vector(&v, n) != 0) {
return 1;
}
omple_vector(v, n);
for (int i = 0; i < n; i++) {
printf("v[%d] = %d\n", i, v[i]);
}
destrueix_vector(&v);
return 0;
}Abans d’implementar res, mira els prototips i completa la taula: per a cada funció, què ha de modificar i per què el paràmetre és T * o T **.
| Funció | Què ha de modificar? | Tipus del paràmetre |
|---|---|---|
crea_vector |
? | ? |
omple_vector |
? | ? |
destrueix_vector |
? | ? |
Pots veure el raonament
| Funció | Què ha de modificar? | Tipus del paràmetre |
|---|---|---|
crea_vector |
el punter v del cridant (perquè apunti al bloc nou) |
int ** |
omple_vector |
el contingut del bloc (els enters) | int * |
destrueix_vector |
el punter v del cridant (perquè quedi a NULL) |
int ** |
Regla: si la funció ha de canviar el que hi ha darrere de l’adreça → T *; si ha de canviar l’adreça mateixa (el punter del cridant) → T **.
Implementa crea_vector, omple_vector i destrueix_vector seguint els comentaris. Han de complir:
crea_vectorcomprova quevno siguiNULL, quen > 0i quemallocno falli;destrueix_vectores pot cridar dues vegades seguides sense problema.
Pots veure la solució
int crea_vector(int **v, int n)
{
if (v == NULL || n <= 0) {
return -1;
}
int *nou = malloc(n * sizeof(int));
if (nou == NULL) {
return -1;
}
*v = nou;
return 0;
}
void omple_vector(int *v, int n)
{
for (int i = 0; i < n; i++) {
v[i] = i + 1;
}
}
void destrueix_vector(int **v)
{
if (v == NULL) {
return;
}
free(*v);
*v = NULL;
}crea_vector es fa servir una variable temporal nou i només s’assigna a *v si el malloc ha funcionat: així, si falla, *v queda sense canvis, tal com diu el comentari.
gcc -Wall -Wextra -g -O0 -o 06_vector 06_vector.c
./06_vector- Després de
destrueix_vector(&v), què valvamain? - Qui és el propietari del vector? Qui l’ha d’alliberar?
- El programa guarda
vinen dues variables separades. Quin risc té?
Pots veure el raonament
vvalNULL.- El propietari és el cridant (
main): és qui fadestrueix_vector. La funciócrea_vectornomés reserva. vinviatgen per separat: si els passes malament o modifiques un i no l’altre, tens un vector amb una mida incorrecta i un accés fora de límits. Cada cop que cridem una funció ambvhem de recordar passar lanque li correspon. Això és el problema que resol unastruct, i és el tema de la propera sessió.
Opcional. Escriu int redimensiona_vector(int **v, int nou_n) que canviï la mida del vector amb realloc.
Pista: realloc(p, mida) pot moure el bloc a una altra adreça, i pot fallar retornant NULL sense alliberar el bloc original. Què ha de passar amb *v en cada cas?
Pots veure el raonament
int redimensiona_vector(int **v, int nou_n)
{
if (v == NULL || nou_n <= 0) {
return -1;
}
int *nou = realloc(*v, nou_n * sizeof(int));
if (nou == NULL) {
return -1;
}
*v = nou;
return 0;
}Aquí es veu per què cal int **: realloc pot moure el bloc, de manera que l’adreça antiga pot deixar de ser vàlida (seria un punter penjat). La funció ha d’actualitzar el v del cridant.
No escriguis *v = realloc(*v, ...) directament: si realloc falla, retorna NULL i sobreescriuries *v perdent l’únic punter al bloc original, que continua reservat: una fuga. Amb la variable temporal nou, si falla, *v continua apuntant al bloc vàlid. Els elements nous no estan inicialitzats.
Extensió opcional: fins on podem continuar? Si podem tenir un punter a un punter, també podem afegir-ne un altre nivell:
int x = 42;
int *p = &x;
int **pp = &p;
int ***ppp = &pp;
printf("%d\n", ***ppp);
printf("%zu %zu %zu %zu\n", sizeof(x), sizeof(p), sizeof(pp), sizeof(ppp));Quin valor imprimeix ***ppp? Quant ocupa cada variable?
Pots veure el raonament
***ppp és 42 i les mides són 4 8 8 8 als nostres sistemes: tots els punters ocupen el mateix, sigui a què apunti.
int → int * → int ** → int ***
Cada * afegeix un nivell d’indirecció; no cal aprendre cap regla nova. Però en aquest curs ens interessa sobretot entendre per què int ** és necessari, no arribar a més nivells. En aquest curs no necessitarem més de dos nivells. Quan la representació comença a acumular molts nivells d’indirecció, sovint val la pena preguntar-se si una struct faria el codi més clar.
Resum
punter
+
punter a punter
+
malloc
+
array
+
funció
+
lifetime
int **pp = &pés un punter que apunta a un punter. Cada*és un pas per una fletxa.- C passa sempre per valor: la funció té la seva pròpia variable, amb una còpia del valor, i no pot canviar la del cridant.
- Què vull modificar? El valor d’un
int→ passo&x(int *). El valor d’un punter → passo&p(int **). Per modificar una variable de tipusTdes d’una funció, cal passarT *. - Una funció que crea memòria ha de dir qui n’és el propietari, i una que la destrueix pot deixar el punter del cridant a
NULL.