Estructura de memòria
Objectius
- Entendre el concepte d’adreça de memòria.
- Conèixer l’espai en memòria dels tipus de dades.
- Entendre la relació entre espai de memòria i capacitat de representació.
- Entendre el concepte de memòria estàtica.
- Introduir el concepte de punter en llenguatge C.
Introducció
Fins ara hem vist com escriure un programa i posteriorment compilar-lo i enllaçar-lo per a aconseguir un executable. Els detalls de què passa quan executem el programa queden fora de l’abast d’aquesta assignatura, però en aquesta unitat s’introduiran alguns conceptes bàsics (i simplificats) de quins efectes té en la memòria.
1. Memòria
El primer que cal saber és que un programa no s’executa des del disc, sinó que el primer pas és carregar-lo a la memòria de l’ordinador, la qual cosa coneixem com a RAM (Random Access Memory). Atès que la memòria és un recurs escàs en un ordinador, el sistema operatiu s’encarrega d’anar movent coses de memòria a disc constantment però, des del punt de vista del programador, podem assumir que sempre està carregat en memòria i veiem la memòria com una sèrie numerada de posicions on podem guardar informació. Cada posició està identificada unívocament amb un valor numèric (la seva adreça) i té capacitat per a un byte (8 bits).
Quan definim un objecte en el nostre programa, ja sigui una constant o una variable, a aquest se li assigna un rang d’adreces on posarà el seu valor. Podem pensar en la memòria com un aparcament públic on tenim moltes places numerades de la grandària d’una moto i sense cap columna pel mig. Si volem aparcar una moto (assignació), amb una plaça tindrem suficient (1 byte), i caldrà que recordem el número de la plaça quan vulguem recuperar la moto (accés). Si en comptes d’una moto volem aparcar un cotxe, amb una sola plaça no en tindrem prou, sinó que acabarem ocupant segurament quatre places (4 bytes) i, per a recuperar el cotxe, si recordem el número de la primera plaça ocupada en tindrem prou per a trobar-ho.
En la resta d’aquesta unitat veurem quanta memòria ocupa cada tipus de dades i la relació que això té amb el rang de valors que pot representar. També veurem que C té un conjunt més ampli de tipus de dades, que ens permetran ajustar millor la quantitat de memòria a les nostres necessitats.
1.1. Informació i espai
L’espai que ocupa una variable està estretament relacionat amb la quantitat d’informació que pot guardar. El bit és la unitat mínima d’informació i pot guardar solament 21 = 2 valors (0 i 1). Amb solament un bit podem representar per exemple el resultat de tirar un moneda a l’aire, assignant 1 a la cara i 0 a la creu (o a l’inrevés), però no en tenim prou per a poder representar el resultat de tirar un dau. Si afegim un segon bit, el nombre de valors que podrem representar serà de 22 = 4 valors. Això ens permetria, per exemple, guardar les estacions de l’any, codificant-les de la manera següent:
Codificació | Estació |
---|---|
00 | Primavera |
01 | Estiu |
10 | Tardor |
11 | Hivern |
Tenint en compte que un byte conté 8 bits, la seva capacitat de representació és de 28 = 256 valors diferents. Per tant, la capacitat de representació està estretament lligada a la quantitat de bits que utilitzem. Quan programem cal que ho tinguem en compte, ja que cada tipus de dades té una longitud en bytes determinada, i això ens fixa quants valors diferents podem emmagatzemar. L’assignació entre valor i la seva codificació binària ve determinada per una sèrie d’estàndards, que queden fora de l’abast d’aquesta assignatura.
En C trobarem a la nostra disposició diferents alternatives per a cada tipus de dades numèriques del llenguatge algorísmic segons l’espai que ocupen i, per tant, la seva capacitat de representació. A la taula següent se’n pot veure un resum:
Tipus | Tipus C | Mida en bytes | Rang de valors |
---|---|---|---|
Enter | char | 1 | -128 to 127 o 0 to 255 |
Enter | unsigned char | 1 | 0 a 255 |
Enter | signed char | 1 | -128 a 127 |
Enter | int | 2 o 4 | -32,768 a 32,767 o -2,147,483,648 a 2,147,483,647 |
Enter | unsigned int | 2 o 4 | 0 a 65,535 o 0 a 4,294,967,295 |
Enter | short | 2 | -32,768 a 32,767 |
Enter | unsigned short | 2 | 0 a 65,535 |
Enter | long | 4 | -2,147,483,648 a 2,147,483,647 |
Enter | unsigned long | 4 | 0 a 4,294,967,295 |
Real | float | 4 | 1.2E-38 a 3.4E+38 -> 6 decimals |
Real | double | 8 | 2.3E-308 a 1.7E+308 -> 15 decimals |
Real | long double | 10 | 3.4E-4932 a 1.1E+4932 -> 19 decimals |
Cal tenir en compte que en alguns casos hi ha dues opcions, això és perquè depèn del sistema on executem el nostre programa. Per a poder saber amb exactitud quant ocupa un tipus de dades en el sistema on s’executa el nostre programa, disposem de la instrucció sizeof de C. A continuació es mostra un exemple on es veu com utilitzar aquest comandament i com accedir a les constants que indiquen els valors màxims i mínims en el cas d’un real declarat en C com a float:
[Exemple 06_01]
#include <float.h>
int main() {
printf("Storage size for float : %d \n", sizeof(float));
printf("Minimum float positive value: %E\n", FLT_MIN );
printf("Maximum float positive value: %E\n", FLT_MAX );
printf("Precision value: %d\n", FLT_DIG );
return 0;
}
2. Memòria estàtica
Ara que ja sabem què ocupa una variable, veurem què ocupa un programa en memòria. Quan executem un programa, el sistema operatiu busca una zona en la memòria on aquest hi càpiga. Per tant, l’adreça inicial del nostre programa canviarà cada vegada que l’executem. El que no canvia és la seva estructura en memòria (fixeu-vos que les adreces van en ordre invers):
Podem veure que hi ha unes quantes posicions de memòria marcades com a args. En aquestes posicions és on es guarden els paràmetres que s’han passat al programa en la seva execució. Per exemple, quan compilem un programa amb GCC -o main main.c, al programa GCC li estem passant tres paràmetres: -o, main i main.c.
El bloc BSS és on es guarden totes les variables globals i estàtiques que no s’han inicialitzat quan s’han declarat. Totes les variables que arriben a aquest bloc s’inicialitzen posant tots els bits a 0 abans de començar a executar el programa.
El bloc DS o Data Segment és on es guarden totes les variables globals o estàtiques que s’han inicialitzat al moment de declarar-se i la resta de variables declarades dins del programa (no estàtiques ni globals).
Finalment, el bloc text o Code Segment conté part de les instruccions del nostre programa, que s’aniran executant una a una.
Per exemple, si tenim el següent programa:
[Exemple 06_02]
int a;
int b=2;
int main() {
static char c;
const float d=3.14;
return 0;
}
Les variables a i c quedarien guardades al bloc BBS, mentre que les variables b i d es guardarien al bloc DS.
La quantitat de memòria que ocupen els segments BBS i DS es fixa des del moment en què s’executa el programa fins que aquest finalitza i no es pot modificar la seva mida durant l’execució. Per aquest motiu, aquesta memòria es coneix com a memòria estàtica.
3. Relació entre objectes i memòria
Quan declarem un objecte, ja sigui variable o constant, el nom que li donem queda vinculat amb l’adreça de la posició de memòria on aquest comença. Per exemple, si tenim el codi:
[Exemple 06_03]
int main() {
int a=0;
return 0;
}
En alguna zona del bloc DS tindrem un espai de 4 bytes (o 2 segons el sistema) on es guardarà la informació d’aquesta variable:
Per a saber l’adreça on ha quedat guardada una variable, ho podem fer amb l’operador &. Per exemple, el codi següent declara dues variables a BSS i dues a DS i mostra l’adreça inicial i final, i la mida en bytes que ocupa cada variable.
[Exemple 06_04]
long b;
int c;
int main() {
int a=0;
float d=5.0;
printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'a', sizeof(a), (unsigned long)(&a), (unsigned long)(&a)+(sizeof(a)-1));
printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'b', sizeof(b), (unsigned long)(&b), (unsigned long)(&b)+sizeof(b)-1);
printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'c', sizeof(c), (unsigned long)(&c), (unsigned long)(&c)+sizeof(c)-1);
printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'd', sizeof(d), (unsigned long)(&d), (unsigned long)(&d)+sizeof(d)-1);
return 0;
}
En general, en aquesta assignatura no utilitzarem ni variables globals ni estàtiques, per tant, podem assumir que totes les nostres variables van al bloc DS i, per tant, ens podem fer una idea de com quedarien en memòria. Per exemple, si volem fer la representació gràfica de la memòria per al programa següent:
[Exemple 06_05]
long b;
int c;
int main() {
int a=5;
short b=7;
char c='a';
return 0;
}
Tindríem un resultat similar al següent (els valors binaris que hi ha no són els correctes):
4. El tipus punter en C
Per la seva estreta relació amb la memòria, en aquest apartat s’introdueix de manera molt breu un nou tipus de dades que existeix en C anomenat punter. Els punters són variables que en comptes de contenir un valor (com els enters, caràcters o reals), contenen una adreça de memòria.
Un punter és una variable el valor de la qual és una adreça de memòria. Atès que un punter conté una adreça de memòria, l’espai que ocupa en memòria serà sempre el mateix, la mida d’una adreça. Aquesta mida dependrà de si el nostre sistema és de 32 bits o 64 bits, però serà independent del tipus que li assignem al punter.
4.1. Declaració
Igual que qualsevol altra variable, un punter es declararà per a poder-lo utilitzar:
[Exemple 13_01]
int main() {
/* Variable definition */
type *p=NULL;
}
On type és qualsevol tipus de dades (enter, real, etc.). La constant NULL té un valor 0, i s’interpreta com que aquest punter no ha estat inicialitzat, o sigui, que no apunta a cap lloc.
4.2. Operacions
Encara que les adreces són iguals sigui quin sigui el tipus, se li assigna un tipus per a indicar què conté l’adreça que es guarda en aquest punter. Cal tenir molta cura amb els tipus dels punters, encara que els punters de tots els tipus ocupen el mateix, el tipus assignat a un punter té un gran efecte en el resultat que s’obté en operar amb ell.
Atès que un punter conté una adreça de memòria, que és un valor numèric, podem realitzar les mateixes operacions que amb qualsevol valor numèric:
4.2.1. Assignació
Com hem dit anteriorment, un punter conté una adreça de memòria. Si volem que un punter apunti a una determinada variable, o sigui, assignar al punter l’adreça d’una determinada variable, ho farem de la manera següent (fixeu-vos que els tipus han de coincidir entre variable i punter):
[Exemple 13_02]
int main() {
/* Variable definition */
int a=3;
float b=4.5;
int *pa=&a;
float *pb=&b;
}
En aquest exemple, a la variable pa se li assigna l’adreça de la variable a, i a la variable pb l’adreça de la variable b. A continuació, es mostra una representació simplificada de com quedaria en memòria:
Observem que quan assignem a un punter l’adreça d’una variable, estem assignant l’adreça del primer byte, per tant, aquesta operació no es veu afectada per la mida en memòria de la variable (donada pel seu tipus).
4.2.2. Contingut
L’adreça d’una variable és una informació que per si sola no ens aporta gran cosa, ja que cada vegada que executem el nostre programa aquest valor canviarà. El que és realment important és el fet que a partir d’aquesta adreça podem accedir al contingut que es guarda en aquesta posició de memòria. Seguint amb l’exemple anterior, a partir de l’adreça guardada a pb, podem recuperar el contingut b. Dit d’una altra manera, podem accedir al contingut de la variable apuntada per pb.
El codi següent mostra l’operador contingut, que ens permet recuperar el contingut a partir del punter.
[Exemple 13_03]
int main() {
/* Variable definition */
int a=5;
int *pa=NULL;
pa=&a;
*pa=25;
}
Observeu que quan recuperem el contingut, el tipus del punter és molt important, ja que el punter només conté l’adreça inicial i el tipus és el que ens permetrà saber quants bytes cal utilitzar i com interpretar-los. A continuació, es mostra una representació de la memòria en diferents situacions:
- L’estat inicial de les variables.
- El resultat d’assignar l’adreça d’a al punter pa.
- Canviar el contingut del punter pa, assignant-li un nou valor.
Cal tenir present que el contingut d’un punter a enter és un enter i, per tant, es pot assignar a una variable de tipus enter i se li pot aplicar qualsevol operació entera, com per exemple, sumar-li un altre enter o escriure’l per pantalla. El mateix ocorre amb qualsevol altre tipus, és a dir, el contingut d’un punter a real és un real i per tant el podem tractar com a tal.
Resum
En aquesta unitat hem vist:
- La representació en memòria d’un programa.
- Els tipus de dades en C.
- La representació de les dades en memòria i la relació amb les seves adreces.
- El concepte de memòria estàtica.
- El tipus punter en C.