Skip to main content

Pralab 01: Del C a la memòria (I)

Author

Jordi Mateo Fornés

Introducció

Durant el primer curs has treballat amb variables, arrays i cadenes de caràcters en C.

Exercici de Programació

El primer exercici d’aquest pralab consisteix a sumar dos vectors vector1 amb (1.5, 2.0, -3.5) i vector2 (2.5, -1.0, 4.0) de tres elements cadascun, i imprimir el vector resultant.

Pots veure la solució
#include <stdio.h>

#define DIMENSIO 3

int main() {
    float vector1[DIMENSIO] = {1.5, 2.0, -3.5};
    float vector2[DIMENSIO] = {2.5, -1.0, 4.0};
    float vector_suma[DIMENSIO];

    for (int i = 0; i < DIMENSIO; i++) {
        vector_suma[i] = vector1[i] + vector2[i];
    }

    printf("El vector resultant de la suma es:\n");
    printf("(");
    for (int i = 0; i < DIMENSIO; i++) {
        printf("%.2f", vector_suma[i]);
        if (i < DIMENSIO - 1) {
            printf(", "); 
        }
    }
    printf(")\n");

    return 0;
}

Però hi ha una pregunta que l’any passat no vau resoldre. Imagineu que el programa té un array v i que volem accedir a l’element v[i]. Com sap el programa on està v[i]?

En aquest pralab comencem a mirar la programació amb C des d’un altre punt de vista: el de la memòria. Aquest laboratori és la base sobre la qual construirem, més endavant, com un sistema operatiu organitza la memòria d’un procés. Aquesta sessió és més experimental que teòrica: anirem fent proves amb petits programes i mirant què passa a la memòria.


Part A — On estan les dades?

Comencem amb aquest programa:

#include <stdio.h>

int main(void)
{
    int x = 42;

    printf("x = %d\n", x);
    printf("&x = %p\n", (void *) &x);

    return 0;
}
x   = ?
&x  = ?
Exercici

Abans d’executar-ho, anota en el teu quadern les respostes a aquestes preguntes:

  • Què imprimirà x?
  • Quina mena de valor representa &x?
  • Esperes que &x tingui el mateix valor que x?
Exercici

Compila i executa el programa dues vegades seguides i compara els valors de &x que obtens.

  • Són iguals o diferents?
  • Si són diferents, què creus que ha passat?
Exercici

Afegeix ara aquesta línia i torna a executar:

printf("%zu\n", sizeof(x));

Compara el que imprimeix sizeof(x) amb el que imprimeix &x. Són el mateix tipus d’informació?

Pots veure el raonament
Solució
  • x és una variable de tipus enter i conté el valor 42.
  • &x és l’adreça de memòria on està emmagatzemat x. És un valor que indica la posició a la memòria del procés on es troba x.
  • No, &x no té el mateix valor que x.
  • Dues execucions poden donar adreces diferents per &x perquè el sistema operatiu assigna, a cada execució, una disposició diferent de la memòria del procés. Aquest mecanisme es diu ASLR (Address Space Layout Randomization, aleatorització de l’espai d’adreces) i el veurem amb detall quan parlem de com el sistema operatiu gestiona la memòria d’un procés. De moment, queda’t amb la idea: l’adreça de x no és fixa ni depèn només del teu codi, sinó també de decisions que pren el sistema operatiu en temps d’execució.
  • sizeof(x) retorna la mida en bytes de la variable x, que és diferent de l’adreça de memòria que retorna &x. Per exemple, en un sistema de 64 bits, sizeof(x) podria retornar 4 (si int ocupa 4 bytes), mentre que &x podria ser una adreça com 0x7ffee3b8c4a0.

flowchart LR
    x["x = 42"]
    a["&x = 0x7ffee3b8c4a0"] -->|"és l'adreça de"| x


Part B — Què és un punter?

Introduïm ara:

int x = 42;
int *p = &x;

flowchart LR
    P["p"] --> X["x = 42"]

printf("%d\n", *p);
Exercici

Què imprimirà aquest printf?

Pots veure el raonament
Solució

El printf("%d\n", *p); imprimirà 42, ja que *p accedeix al valor de la variable a la qual apunta p, que és x.

Ara prova això:

*p = 99;
printf("%d\n", x);
Exercici

Has modificat p o has modificat x?

Pots veure el raonament
Solució

Quan fas *p = 99;, estàs modificant el valor de x a través del punter p. Així que després d’aquesta línia, x tindrà el valor 99.

Amb això ja podem fixar dues definicions formals:

Definició

Un punter és una variable que emmagatzema una adreça de memòria.

Definició

L’operador * permet accedir a l’objecte que es troba a l’adreça emmagatzemada en el punter.

Reflexió

Nota de compilador. Prova de compilar printf("%d\n", p); (sense *) amb gcc -Wall -Wextra. Fixa’t en l’avís del compilador: el valor que passes a %d té tipus int *, però %d espera un int — hi ha una incompatibilitat de tipus entre l’argument i el format. Per imprimir un punter amb printf, fem servir %p i convertim el valor a void *, exactament com hem fet a la Part A amb (void *)&x.


Part C — Arrays

Partim de:

int v[5] = {10, 20, 30, 40, 50};

Comencem amb una cosa que probablement ja has fet servir:

printf("%zu\n", sizeof(v));
printf("%zu\n", sizeof(v[0]));
printf("%zu\n", sizeof(v) / sizeof(v[0]));
Exercici

Què imprimeix cada línia?

Pots veure el raonament
Solució
  • sizeof(v) retorna la mida total de l’array v en bytes. Com que v és un array de 5 enters i cada enter ocupa, per exemple, 4 bytes, sizeof(v) seria 20.
  • sizeof(v[0]) retorna la mida d’un sol element de l’array, que és un enter, així que seria 4.
  • sizeof(v) / sizeof(v[0]) calcula el nombre d’elements de l’array dividint la mida total de l’array per la mida d’un element, així que seria 20 / 4 = 5.

Ja saps utilitzar v[0], v[1], v[2]. Ara mirem-ho des del punt de vista de la memòria:

printf("%p\n", (void *)&v[0]);
printf("%p\n", (void *)&v[1]);
Exercici

Què observes en les dues adreces que s’imprimeixen?

Pots veure el raonament
Solució

Les adreces segueixen el mateix patró: cada element ocupa sizeof(int) bytes i els elements d’un array estan emmagatzemats de manera contigua.

Ara mesura-ho:

printf("%zu\n", sizeof(v[0]));
printf("%td\n", (char *)&v[1] - (char *)&v[0]);

Convertim temporalment les adreces a char *. Això ens permet expressar la diferència entre les dues adreces en unitats de bytes.

Ara compara els dos resultats.

Exercici

Quina relació observes entre el resultat de la resta amb char * i el valor de sizeof(v[0])?

Pots veure el raonament
Solució

Quan restem dos punters a elements del mateix array, el resultat depèn del tipus del punter. Sense el cast, &v[1] - &v[0] també és una resta vàlida, però el resultat s’expressa en nombre d’elements int, no en bytes. Com que apunten a elements consecutius, dona 1. La diferència en bytes és la que has obtingut amb (char *)&v[1] - (char *)&v[0], i coincideix amb sizeof(int) en aquest sistema (C no garanteix que sizeof(int) valgui el mateix a tot arreu, només que sigui com a mínim de 2 bytes).

Amplia-ho amb &v[2]:

printf("%p\n", (void *)&v[2]);
printf("%td\n", (char *)&v[2] - (char *)&v[1]);
Exercici

Es manté el mateix patró? Diries que els elements de v estan repartits de qualsevol manera per la memòria, o segueixen una estructura?

Pots veure el raonament
Solució

Els elements de l’array v estan emmagatzemats de manera contigua a la memòria. Podem visualitzar el que acabes de mesurar:

graph LR
    subgraph v["array v"]
        A["v[0]<br/><b>10</b><br/>0x1000"]
        B["v[1]<br/><b>20</b><br/>0x1004"]
        C["v[2]<br/><b>30</b><br/>0x1008"]
        D["v[3]<br/><b>40</b><br/>0x100C"]
        E["v[4]<br/><b>50</b><br/>0x1010"]

        A -->|"+sizeof(int) bytes"| B
        B -->|"+sizeof(int) bytes"| C
        C -->|"+sizeof(int) bytes"| D
        D -->|"+sizeof(int) bytes"| E
    end

Ara fes una mesura global:

printf("%zu\n", sizeof(v));
Exercici

Aquest resultat, coincideix amb el que esperaries a partir del que has mesurat element a element?

Pots veure el raonament
Solució

Sí, sizeof(v) hauria de ser igual a sizeof(v[0]) * 5, ja que v és un array de 5 elements de tipus int. Si sizeof(v[0]) és 4 bytes, aleshores sizeof(v) serà 20 bytes.

Ara revisem el que hem anat aprenent sobre v i &v:

int *pv = &v[0];

printf("%zu\n", sizeof(v));
printf("%zu\n", sizeof(pv));
Exercici
  • Raona si v i pv et permeten accedir a les mateixes dades (v[2] i pv[2] valen el mateix). sizeof donarà el mateix resultat per tots dos?
Pots veure el raonament
Solució

sizeof(v) continua valent 20: és la mida de tot l’array, els seus 5 elements sencers. En canvi, sizeof(pv) val 8 (en un sistema de 64 bits): és només la mida d’una adreça de memòria, independentment de quantes dades hi hagi al darrere. Encara que v i pv es comportin de manera semblant quan hi accedeixes amb [i], no són el mateix tipus de cosa: v és un array i per tant el compilador en coneix la mida sencera, mentre que pv és només un punter, el compilador només sap que apunta a un int, no quants n’hi ha darrere.

Fins ara hem utilitzat punters per guardar adreces, accedir als valors i mesurar distàncies entre adreces. Ara veurem què passa quan sumem un nombre a un punter.

printf("%p\n", (void *)&v[2]);
printf("%p\n", (void *)(pv + 2));
Exercici

Abans d’executar-ho: quan sumes 2 a pv, creus que avança 2 bytes o 2 elements de tipus int? Compara els resultats.

Pots veure el raonament
Solució

Sí, &v[2] i pv + 2 donen exactament la mateixa adreça. Igual que passava amb la resta de punters, la suma també té en compte automàticament la mida del tipus: pv + 2 no avança 2 bytes, sinó 2 elements int (és a dir, 2 * sizeof(int) bytes). Si el compilador avancés sempre en bytes, independentment del tipus al qual apunta pv, pv + 2 acabaria apuntant enmig d’un element en lloc de l’inici del tercer, i tota l’aritmètica d’arrays deixaria de funcionar.

Exercici de Programació

Un programa llegeix les dades d’un sensor i les guarda en un array. El programador afirma que el sensor ha registrat: 10, 20, 30, 40. Però quan executa el programa obté:

gcc -Wall -Wextra sensor.c -o sensor
# En compilar-lo amb -Wall -Wextra apareixen alguns avisos, però no tots els errors del programa en generen.

Aquest és el codi del programa sensor.c:

#include <stdio.h>

int main(void)
{
    int mesures[4] = {10, 20, 30, 40};

    int *sensor = &mesures[0];

    printf("Mesura actual: %d\n", sensor);
    printf("Adreça de la mesura: %d\n", *sensor);

    sensor = 99;

    printf("Nova mesura: %d\n", *sensor);
    printf("Mesura original: %d\n", mesures[0]);

    return 0;
}
  1. El programa té diversos errors. Troba’ls sense executar-lo.
Pots veure el raonament
Solució
  1. (avís del compilador) sensor és un punter i conté una adreça. Passar-lo directament a %d és un error de tipus que -Wall detecta (“format ‘%d’ expects argument of type ‘int’, but argument has type ’int ’“*). Per obtenir el valor al qual apunta hem d’utilitzar *sensor.
  2. (error de lectura, el compilador no l’avisa) *sensor és el valor de la mesura (10), no la seva adreça — és un int, i %d és tècnicament correcte per imprimir-lo. El problema és només que l’etiqueta del text (“Adreça de la mesura”) no es correspon amb el que realment s’imprimeix. Cap avís del compilador et pot dir això: cal llegir el codi amb cura. Per imprimir l’adreça de debò cal sensor amb %p.
  3. (avís del compilador) sensor = 99 assigna un enter literal a un punter, i el compilador ja avisa d’això amb -Wextra (“makes pointer from integer without a cast”). sensor passa a contenir l’adreça 99, que gairebé segur no és una adreça vàlida dins l’espai de memòria del procés. Desreferenciar-la després amb *sensor és comportament indefinit: normalment provocarà un segmentation fault. Per això et demanem que trobis els errors sense executar el programa.
  1. Volem modificar la mesura a través del punter sensor perquè passi de 10 a 99. Quina instrucció has d’escriure? Explica què passa amb mesures[0] i per què?
Pots veure el raonament
Solució

Quan assignem *sensor = 99, estem modificant el valor de la variable a la qual apunta sensor, que és mesures[0]. Per tant, després d’aquesta assignació, mesures[0] tindrà el valor 99. Això és perquè sensor és un punter que apunta a l’adreça de mesures[0], i desreferenciar-lo amb *sensor ens permet accedir i modificar el valor emmagatzemat en aquesta adreça.

  1. Corregeix el programa, intentant mantenir el mateix nombre de línies, per obtenir la sortida següent:
Mesura actual: 10
Adreça de la mesura: 0x...
Nova mesura: 99
Mesura original: 99
Pots veure la solució
Solució
#include <stdio.h>
int main(void)
{
    int mesures[4] = {10, 20, 30, 40};
    int *sensor = &mesures[0];

    printf("Mesura actual: %d\n", *sensor);
    printf("Adreça de la mesura: %p\n", (void *)sensor);

    *sensor = 99;

    printf("Nova mesura: %d\n", *sensor);
    printf("Mesura original: %d\n", mesures[0]);
    return 0;
}

Ja podem començar a respondre la pregunta inicial: com pot C localitzar v[i]?

Resultat d'Aprenentatge

Els elements d’un array estan emmagatzemats de manera contigua. Per accedir a v[i], C parteix de l’adreça del primer element i avança i elements. Com que cada element ocupa sizeof(int) bytes, això equival a avançar i * sizeof(int) bytes. En C, aquesta idea es representa mitjançant l’aritmètica de punters: &v[i] == &v[0] + i. Quan sumem i a un punter a int, C no avança i bytes, sinó i elements de tipus int. El compilador té en compte automàticament la mida de cada element.